@@ -1,111 +1,231 @@
import { useEffect , useMemo , useState } from 'react' ;
import { Edit3 , FilePenLine , Globe2 , Plus , Search , Trash2 , Upload } from 'lucide-react' ;
import { ChevronDown , ChevronRight , Edit3 , FileCheck2 , Globe2 , Plus , RotateCcw , Search , Trash2 , Upload } from 'lucide-react' ;
import { Button , FileActions , Input , Modal , Pagination , Select , Tag , Textarea } from '@/components/ui' ;
import { clientApi , type ApplicationReportField , type ClientSmsApplication , type ClientSmsSignature , type FileRef } from '@/api/adminApi' ;
import {
clientApi ,
type ClientApplicationReportField ,
type ClientSignatureWorkspace ,
type ClientSmsApplication ,
type ClientSmsSignatureView ,
type FileRef ,
} from '@/api/adminApi' ;
const EMPTY_WORKSPACE : ClientSignatureWorkspace = {
items : [ ] ,
summary : { total : 0 , pending : 0 , approved : 0 , rejected : 0 , draft : 0 } ,
} ;
const statusTone : Record < string , 'success' | 'info' | 'danger' | 'warning' > = {
approved : 'success' ,
pending : 'info' ,
rejected : 'danger' ,
draft : 'warning' ,
approved : 'success' , pending : 'info' , rejected : 'danger' , draft : 'warning' ,
} ;
const statusLabel : Record < string , string > = {
approved : '已通过 ' ,
pending : '审核中' ,
rejected : '已驳回' ,
draft : '草稿' ,
disabled : '已禁用' ,
approved : '审核通过' , pending : '资料审核中' , rejected : '需修改' , draft : '待提交 ',
} ;
function materialToFileRef ( material : Record < string , unknown > ) : FileRef | null {
const fileObjectId = String ( material . fileObjectId ? ? '' ) ;
if ( ! fileObjectId ) return null ;
const fileName = String ( material . title ? ? material . fileName ? ? '签名材料' ) ;
const contentType = typeof material . contentType === 'string' ? material.contentType : undefined ;
return { contentType , fileName , fileObjectId } ;
}
type ClientDrainageInfo = {
id : string ;
siteName : string ;
url : string ;
remark : string ;
reportValues : Record < string , unknown > ;
auditStatus : string ;
rejectReason? : string | null ;
submittedAt? : string ;
} ;
function drainageItems ( signature : ClientSmsSignature ) : ClientDrainageInfo [ ] {
const payload = signature . drainageInfo && typeof signature . drainageInfo === 'object' ? signature . drainageInfo : { } ;
const links = Array . isArray ( payload . links ) ? payload . links as Array < Record < string , unknown > > : [ ] ;
return links . map ( ( item ) = > ( {
id : String ( item . id ? ? '' ) ,
siteName : String ( item . siteName ? ? '' ) ,
url : String ( item . url ? ? '' ) ,
remark : String ( item . remark ? ? '' ) ,
reportValues : item.reportValues && typeof item . reportValues === 'object' ? item . reportValues as Record < string , unknown > : { } ,
auditStatus : String ( item . auditStatus ? ? 'pending' ) ,
rejectReason : item.rejectReason ? String ( item . rejectReason ) : null ,
submittedAt : item.submittedAt ? String ( item . submittedAt ) : undefined ,
} ) ) ;
}
type ClientDrainageInfo = ClientSmsSignatureView [ 'drainageInfo' ] [ 'links' ] [ number ] ;
function reportFileRef ( value : unknown ) : FileRef | null {
if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) return null ;
const item = value as Record < string , unknown > ;
const fileObjectId = String ( item . fileObjectId ? ? '' ) ;
const fileName = String ( item . fileName ? ? '' ) ;
return fileObjectId && fileName ? { fileObjectId , fileName , contentType : item.contentType ? String ( item . contentType ) : undefined } : null ;
return fileObjectId && fileName
? { fileObjectId , fileName , contentType : item.contentType ? String ( item . contentType ) : undefined }
: null ;
}
function ClientDrainageModal ( { item , onClose , onSaved , signature } : { item? : ClientDrainageInfo ; onClose : ( ) = > void ; onSaved : ( ) = > void ; signature : ClientSmsSignature } ) {
const [ fields , setFields ] = useState < ApplicationReportField [ ] > ( [ ] ) ;
const [ siteName , setSiteName ] = useState ( item ? . siteName ? ? '' ) ;
const [ url , setUrl ] = useState ( item ? . url ? ? '' ) ;
const [ remark , setRemark ] = useState ( item ? . remark ? ? '' ) ;
const [ values , setValues ] = useState < Record < string , unknown > > ( item ? . reportValues ? ? { } ) ;
const [ saving , setSaving ] = useState ( false ) ;
function formatDate ( value? : string | null ) {
if ( ! value ) return '-' ;
const date = new Date ( value ) ;
return Number . isNaN ( date . getTime ( ) ) ? '-' : date . toLocaleString ( 'zh-CN' , { hour12 : false } ) ;
}
function ReviewFields ( {
fields ,
values ,
uploadingCode ,
onChange ,
onUpload ,
} : {
fields : ClientApplicationReportField [ ] ;
values : Record < string , unknown > ;
uploadingCode : string ;
onChange : ( code : string , value : unknown ) = > void ;
onUpload : ( field : ClientApplicationReportField , file? : File ) = > void ;
} ) {
if ( ! fields . length ) return < p className = "client-signature-empty-hint" > 当 前 应 用 无 需 补 充 其 他 审 核 资 料 。 < / p > ;
return < div className = "signature-form-grid" >
{ fields . map ( ( field ) = > field . fieldType === 'string'
? < Input
key = { field . id }
label = { ` ${ field . required ? '* ' : '' } ${ field . name } ` }
onChange = { ( event ) = > onChange ( field . code , event . target . value ) }
value = { String ( values [ field . code ] ? ? '' ) }
/ >
: < label className = "signature-upload" key = { field . id } >
< Upload size = { 26 } / >
< strong > { reportFileRef ( values [ field . code ] ) ? . fileName ? ? ` ${ field . required ? '* ' : '' } 上传 ${ field . name } ` } < / strong >
< small > { uploadingCode === field . code ? '上传中...' : field . fieldType === 'image' ? '请选择图片文件' : '请选择文件' } < / small >
< FileActions file = { reportFileRef ( values [ field . code ] ) } / >
< input
accept = { field . fieldType === 'image' ? 'image/*' : undefined }
onChange = { ( event ) = > onUpload ( field , event . target . files ? . [ 0 ] ) }
style = { { display : 'none' } }
type = "file"
/ >
< / label > ) }
< / div > ;
}
function SignatureModal ( {
applications ,
signature ,
onClose ,
onSaved ,
} : {
applications : ClientSmsApplication [ ] ;
signature? : ClientSmsSignatureView ;
onClose : ( ) = > void ;
onSaved : ( ) = > void ;
} ) {
const [ applicationId , setApplicationId ] = useState ( signature ? . applicationId ? ? '' ) ;
const [ name , setName ] = useState ( signature ? . name ? ? '' ) ;
const [ purpose , setPurpose ] = useState ( signature ? . purpose ? ? '' ) ;
const [ fields , setFields ] = useState < ClientApplicationReportField [ ] > ( [ ] ) ;
const [ values , setValues ] = useState < Record < string , unknown > > ( signature ? . reportValues ? ? { } ) ;
const [ uploadingCode , setUploadingCode ] = useState ( '' ) ;
const [ saving , setSaving ] = useState ( false ) ;
const [ error , setError ] = useState ( '' ) ;
useEffect ( ( ) = > {
const request = signature . applicationId
? clientApi . listApplicationReportFields ( signature . applicationId , 'drainag e' )
: clientApi . listCommonApplicationReportFields ( 'drainag e' ) ;
request . then ( setFields ) . catch ( ( failure : Error ) = > setError ( failure . message || '引流报备字段 加载失败' ) ) ;
} , [ signature . applicationId ] ) ;
const request = applicationId
? clientApi . listApplicationReportFields ( applicationId , 'signatur e' )
: clientApi . listCommonApplicationReportFields ( 'signatur e' ) ;
request . then ( setFields ) . catch ( ( failure : Error ) = > setError ( failure . message || '审核资料 加载失败' ) ) ;
} , [ applicationId ] ) ;
async function upload ( field : ApplicationReportField , file? : File ) {
async function upload ( field : ClientApplicationReportField , file? : File ) {
if ( ! file ) return ;
setUploadingCode ( field . code ) ;
setError ( '' ) ;
try {
const uploaded = await clientApi . uploadFileObject ( file , { purpose : 'drainag e_report_material' , prefix : ` drainage-materials/ ${ signature . id } ` } ) ;
const uploaded = await clientApi . uploadFileObject ( file , { purpose : 'signatur e_report_material' , prefix : 'signature-materials' } ) ;
setValues ( ( current ) = > ( { . . . current , [ field . code ] : { fileObjectId : uploaded.id , fileName : uploaded.fileName , contentType : uploaded.contentType } } ) ) ;
} catch ( failure ) { setError ( failure instanceof Error ? failure . message : '文件上传失败' ) ; } finally { setUploadingCode ( '' ) ; }
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '资料上传失败' ) ;
} finally {
setUploadingCode ( '' ) ;
}
}
async function save() {
setSaving ( true ) ;
setError ( '' ) ;
try {
const body = { siteName , url , remark , reportValues : values } ;
if ( item ) await clientApi . updateDrainageInfo ( item . id , body ) ;
else await clientApi . createDrainageInfo ( signature . id , body ) ;
const body = { applicationId : applicationId || undefined , name : name.trim ( ) , purpose : purpose.trim ( ) , drainageInfo : { signatureReportValues : values } } ;
if ( signature ) await clientApi . updateSignature ( signature . id , body ) ;
else {
const created = await clientApi . createSignature ( body ) ;
await clientApi . submitSignature ( created . id ) ;
}
onSaved ( ) ;
} catch ( failure ) { setError ( failure instanceof Error ? failure . message : '引流信息提交审核失败' ) ; } finally { setSaving ( false ) ; }
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '签名资料提交失败' ) ;
} finally {
setSaving ( false ) ;
}
}
const missingRequired = fields . some ( ( field ) = > field . required && ! values [ field . code ] ) ;
return < Modal footer = { < > < Button onClick = { onClose } variant = "ghost" > 取 消 < / Button > < Button disabled = { ! siteName . trim ( ) || ! url . trim ( ) || missingRequired || saving || Boolean ( uploadingCode ) } onClick = { ( ) = > void save ( ) } > { saving ? '提交中...' : '提交审核' } < / Button > < / > } onClose = { onClose } open size = "xl" title = { item ? '修改引流信息' : '新增引流信息' } >
< div className = "signature-form drainage-edit-form" >
< Input label = "引流信息" onChange = { ( event ) = > setSiteName ( event . target . value ) } required value = { siteName } / >
< Input label = "引流地址" onChange = { ( event ) = > setUrl ( event . target . value ) } placeholder = "https://" required value = { url } / >
< Textarea label = "备注" onChange = { ( event ) = > setRemark ( event . target . value ) } rows = { 3 } value = { remark } / >
< section className = "surface" style = { { padding : 16 } } > < h3 > 引 流 信 息 报 备 资 料 ( 通 用 + 通 道 ) < / h3 > < div className = "signature-form-grid" style = { { marginTop : 12 } } >
{ fields . map ( ( field ) = > field . fieldType === 'string' ? < Input key = { field . id } label = { ` ${ field . required ? '* ' : '' } ${ field . name } ` } onChange = { ( event ) = > setValues ( ( current ) = > ( { . . . current , [ field . code ] : event . target . value } ) ) } value = { String ( values [ field . code ] ? ? '' ) } / > : < label className = "signature-upload" key = { field . id } > < Upload size = { 28 } / > < strong > { reportFileRef ( values [ field . code ] ) ? . fileName ? ? ` ${ field . required ? '* ' : '' } 上传 ${ field . name } ` } < / strong > < small > { uploadingCode === field . code ? '上传中...' : field . fieldType === 'image' ? '请选择图片文件' : '请选择文件' } < / small > < FileActions file = { reportFileRef ( values [ field . code ] ) } / > < input accept = { field . fieldType === 'image' ? 'image/*' : undefined } onChange = { ( event ) = > void upload ( field , event . target . files ? . [ 0 ] ) } style = { { display : 'none' } } type = "file" / > < / label > ) }
< / div > < / section >
return < Modal
footer = { < > < Button onClick = { onClose } variant = "ghost" > 取 消 < / Button > < Button disabled = { ! name . trim ( ) || missingRequired || saving || Boolean ( uploadingCode ) } onClick = { ( ) = > void save ( ) } > { saving ? '提交中...' : '提交审核' } < / Button > < / > }
onClose = { onClose }
open
size = "xl"
title = { signature ? '修改签名资料' : '新增签名' }
>
< div className = "signature-form" >
{ signature ? . rejectReason ? < div className = "client-signature-reason" > < strong > 修 改 说 明 < / strong > < span > { signature . rejectReason } < / span > < / div > : null }
< Select
label = "所属应用"
onChange = { ( event ) = > { setApplicationId ( event . target . value ) ; setValues ( { } ) ; } }
options = { [ { label : '不绑定应用' , value : '' } , . . . applications . map ( ( item ) = > ( { label : item.name , value : item.id } ) ) ] }
value = { applicationId }
/ >
< Input label = "短信签名" onChange = { ( event ) = > setName ( event . target . value ) } placeholder = "例如:某某科技(无需填写【】)" required value = { name } / >
< Input label = "使用场景" onChange = { ( event ) = > setPurpose ( event . target . value ) } placeholder = "例如:验证码、订单通知" value = { purpose ? ? '' } / >
< section className = "client-signature-form-section" >
< div > < h3 > 审 核 资 料 < / h3 > < p > 请 按 要 求 填 写 或 上 传 , 资 料 仅 用 于 签 名 审 核 。 < / p > < / div >
< ReviewFields fields = { fields } onChange = { ( code , value ) = > setValues ( ( current ) = > ( { . . . current , [ code ] : value } ) ) } onUpload = { ( field , file ) = > void upload ( field , file ) } uploadingCode = { uploadingCode } values = { values } / >
< / section >
{ error ? < p className = "form-error" > { error } < / p > : null }
< / div >
< / Modal > ;
}
function DrainageModal ( { item , signature , onClose , onSaved } : { item? : ClientDrainageInfo ; signature : ClientSmsSignatureView ; onClose : ( ) = > void ; onSaved : ( ) = > void } ) {
const [ fields , setFields ] = useState < ClientApplicationReportField [ ] > ( [ ] ) ;
const [ siteName , setSiteName ] = useState ( item ? . siteName ? ? '' ) ;
const [ url , setUrl ] = useState ( item ? . url ? ? '' ) ;
const [ remark , setRemark ] = useState ( item ? . remark ? ? '' ) ;
const [ values , setValues ] = useState < Record < string , unknown > > ( item ? . reportValues ? ? { } ) ;
const [ uploadingCode , setUploadingCode ] = useState ( '' ) ;
const [ saving , setSaving ] = useState ( false ) ;
const [ error , setError ] = useState ( '' ) ;
useEffect ( ( ) = > {
const request = signature . applicationId
? clientApi . listApplicationReportFields ( signature . applicationId , 'drainage' )
: clientApi . listCommonApplicationReportFields ( 'drainage' ) ;
request . then ( setFields ) . catch ( ( failure : Error ) = > setError ( failure . message || '审核资料加载失败' ) ) ;
} , [ signature . applicationId ] ) ;
async function upload ( field : ClientApplicationReportField , file? : File ) {
if ( ! file ) return ;
setUploadingCode ( field . code ) ;
try {
const uploaded = await clientApi . uploadFileObject ( file , { purpose : 'drainage_report_material' , prefix : ` drainage-materials/ ${ signature . id } ` } ) ;
setValues ( ( current ) = > ( { . . . current , [ field . code ] : { fileObjectId : uploaded.id , fileName : uploaded.fileName , contentType : uploaded.contentType } } ) ) ;
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '资料上传失败' ) ;
} finally {
setUploadingCode ( '' ) ;
}
}
async function save() {
setSaving ( true ) ;
setError ( '' ) ;
try {
const body = { siteName : siteName.trim ( ) , url : url.trim ( ) , remark : remark?.trim ( ) , reportValues : values } ;
if ( item ) await clientApi . updateDrainageInfo ( item . id , body ) ;
else await clientApi . createDrainageInfo ( signature . id , body ) ;
onSaved ( ) ;
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '引流信息提交失败' ) ;
} finally {
setSaving ( false ) ;
}
}
const missingRequired = fields . some ( ( field ) = > field . required && ! values [ field . code ] ) ;
return < Modal
footer = { < > < Button onClick = { onClose } variant = "ghost" > 取 消 < / Button > < Button disabled = { ! siteName . trim ( ) || ! url . trim ( ) || missingRequired || saving || Boolean ( uploadingCode ) } onClick = { ( ) = > void save ( ) } > { saving ? '提交中...' : '提交审核' } < / Button > < / > }
onClose = { onClose }
open
size = "xl"
title = { item ? '修改引流信息' : '新增引流信息' }
>
< div className = "signature-form" >
{ item ? . rejectReason ? < div className = "client-signature-reason" > < strong > 修 改 说 明 < / strong > < span > { item . rejectReason } < / span > < / div > : null }
< Input label = "名称" onChange = { ( event ) = > setSiteName ( event . target . value ) } placeholder = "例如:品牌官网" required value = { siteName } / >
< Input label = "访问地址" onChange = { ( event ) = > setUrl ( event . target . value ) } placeholder = "https://" required value = { url } / >
< Textarea label = "说明" onChange = { ( event ) = > setRemark ( event . target . value ) } rows = { 3 } value = { remark ? ? '' } / >
< section className = "client-signature-form-section" >
< div > < h3 > 审 核 资 料 < / h3 > < p > 请 补 充 此 链 接 对 应 的 主 体 或 页 面 证 明 。 < / p > < / div >
< ReviewFields fields = { fields } onChange = { ( code , value ) = > setValues ( ( current ) = > ( { . . . current , [ code ] : value } ) ) } onUpload = { ( field , file ) = > void upload ( field , file ) } uploadingCode = { uploadingCode } values = { values } / >
< / section >
{ error ? < p className = "form-error" > { error } < / p > : null }
< / div >
< / Modal > ;
@@ -113,193 +233,131 @@ function ClientDrainageModal({ item, onClose, onSaved, signature }: { item?: Cli
export function ClientSignaturesPage() {
const [ applications , setApplications ] = useState < ClientSmsApplication [ ] > ( [ ] ) ;
const [ signatures , setSignatures ] = useState < ClientSmsSignature [ ] > ( [ ] ) ;
const [ workspace , setWorkspace ] = useState < ClientSignatureWorkspace > ( EMPTY_WORKSPACE ) ;
const [ keyword , setKeyword ] = useState ( '' ) ;
const [ error , setError ] = useState ( '' ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ modalOpen , setModalOpen ] = useState ( false ) ;
const [ applicationId , setApplicationId ] = useState ( '' ) ;
const [ name , setName ] = useState ( '' ) ;
const [ purpose , setPurpose ] = useState ( '' ) ;
const [ signatureFields , setSignatureFields ] = useState < ApplicationReportField [ ] > ( [ ] ) ;
const [ signatureValues , setSignatureValues ] = useState < Record < string , unknown > > ( { } ) ;
const [ signatureUploadingCode , setSignatureUploadingCode ] = useState ( '' ) ;
const [ applicationFilter , setApplicationFilter ] = useState ( '' ) ;
const [ statusFilter , setStatusFilter ] = useState ( '' ) ;
const [ expandedIds , setExpandedIds ] = useState < Set < string > > ( new Set ( ) ) ;
const [ signatureModal , setSignatureModal ] = useState < ClientSmsSignatureView | 'new' > ( ) ;
const [ drainageModal , setDrainageModal ] = useState < { signature : ClientSmsSignatureView ; item? : ClientDrainageInfo } > ( ) ;
const [ deleting , setDeleting ] = useState < { type : 'signature' | 'drainage' ; id : string ; name : string } > ( ) ;
const [ page , setPage ] = useState ( 1 ) ;
const [ drainageModal , setDrainageModal ] = useState < { signature : ClientSmsSignature ; item? : ClientDrainageInfo } > ( ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState ( '' ) ;
function loadData() {
setLoading ( true ) ;
Promise . all ( [ clientApi . listApplications ( ) , clientApi . listSignatures ( ) ] )
. then ( ( [ applicationItems , signatureItems ] ) = > {
Promise . all ( [ clientApi . listApplications ( ) , clientApi . getSignatureWorkspace ( ) ] )
. then ( ( [ applicationItems , signatureWorkspace ] ) = > {
setApplications ( applicationItems . filter ( ( item ) = > item . status === 'active' ) ) ;
setSignatures ( signatureItems . filter ( ( item ) = > item . auditStatus !== 'disabled' && item . auditStatus !== 'deleted' ) ) ;
setWorkspace ( signatureWorkspace ) ;
setError ( '' ) ;
} )
. catch ( ( reason : Error ) = > setError ( reason . message || '签名数据 加载失败' ) )
. catch ( ( failure : Error ) = > setError ( failure . message || '签名与引流信息 加载失败' ) )
. finally ( ( ) = > setLoading ( false ) ) ;
}
useEffect ( ( ) = > {
loadData ( ) ;
} , [ ] ) ;
useEffect ( loadData , [ ] ) ;
useEffect ( ( ) = > {
if ( ! modalOpen ) return ;
const request = applicationId
? clientApi . listApplicationReportFields ( applicationId , 'signature' )
: clientApi . listCommonApplicationReportFields ( 'signature' ) ;
request . then ( setSignatureFields ) . catch ( ( reason : Error ) = > setError ( reason . message || '签名报备字段加载失败' ) ) ;
} , [ applicationId , modalOpen ] ) ;
const filteredItems = useMemo ( ( ) = > workspace . items . filter ( ( item ) = > {
const matchesKeyword = ! keyword . trim ( ) || [ item . name , item . purpose , item . application ? . name ] . join ( ' ' ) . toLowerCase ( ) . includes ( keyword . trim ( ) . toLowerCase ( ) ) ;
return matchesKeyword && ( ! applicationFilter || item . applicationId === applicationFilter ) && ( ! statusFilter || item . auditStatus === statusFilter ) ;
} ) , [ applicationFilter , keyword , statusFilter , workspace . items ] ) ;
const filteredSignatures = useMemo ( ( ) = > signatures . filter ( ( item ) = > (
! keyword || [ item . name , item . purpose , item . applicationId ] . join ( ' ' ) . includes ( keyword )
) ) , [ keyword , signatures ] ) ;
const pageSize = 10 ;
const totalPages = Math . max ( 1 , Math . ceil ( filteredSignatures . length / pageSize ) ) ;
const totalPages = Math . max ( 1 , Math . ceil ( filteredItems . length / pageSize ) ) ;
const currentPage = Math . min ( page , totalPages ) ;
const visibleSignatures = filteredSignatures . slice ( ( currentPage - 1 ) * pageSize , currentPage * pageSize ) ;
const visibleItems = filteredItems . slice ( ( currentPage - 1 ) * pageSize , currentPage * pageSize ) ;
useEffect ( ( ) = > {
setPage ( 1 ) ;
} , [ filteredSignatures . length , keyword ] ) ;
useEffect ( ( ) = > setPage ( 1 ) , [ applicationFilter , keyword , statusFilter ] ) ;
async function createSignature() {
function toggleExpanded ( id : string ) {
setExpandedIds ( ( current ) = > {
const next = new Set ( current ) ;
if ( next . has ( id ) ) next . delete ( id ) ; else next . add ( id ) ;
return next ;
} ) ;
}
async function confirmDelete() {
if ( ! deleting ) return ;
try {
const signature = await clientApi . createSignature ( { applicationId : applicationId || undefined , name , purpose , drainageInfo : { signatureReportValues : signatureValues } } ) ;
await clientApi . submitSignature ( signature . id ) ;
setModalOpen ( false ) ;
setApplicationId ( '' ) ;
setName ( '' ) ;
setPurpose ( '' ) ;
setSignatureFields ( [ ] ) ;
setSignatureValues ( { } ) ;
if ( deleting . type === 'signature' ) await clientApi . changeSignatureStatus ( deleting . id , 'disabled' ) ;
else await clientApi . changeDrainageInfoStatus ( deleting . id , 'deleted' ) ;
setDeleting ( undefined ) ;
loadData ( ) ;
} catch ( reason ) {
setError ( reason instanceof Error ? reason . message : '签名提交 失败' ) ;
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '删除 失败' ) ;
}
}
async function uploadSignatureField ( field : ApplicationReportField , file? : File ) {
if ( ! file ) return ;
setSignatureUploadingCode ( field . code ) ;
try {
const uploaded = await clientApi . uploadFileObject ( file , { purpose : 'signature_report_material' , prefix : 'signature-materials' } ) ;
setSignatureValues ( ( current ) = > ( { . . . current , [ field . code ] : { fileObjectId : uploaded.id , fileName : uploaded.fileName , contentType : uploaded.contentType } } ) ) ;
} catch ( reason ) {
setError ( reason instanceof Error ? reason . message : '签名报备资料上传失败' ) ;
} finally {
setSignatureUploadingCode ( '' ) ;
}
}
function disableSignature ( id : string ) {
clientApi . changeSignatureStatus ( id , 'disabled' )
. then ( loadData )
. catch ( ( reason : Error ) = > setError ( reason . message || '签名禁用失败' ) ) ;
}
function deleteDrainage ( id : string ) {
clientApi . changeDrainageInfoStatus ( id , 'deleted' ) . then ( loadData ) . catch ( ( reason : Error ) = > setError ( reason . message || '引流信息删除失败' ) ) ;
}
return (
< section className = "page-stack" >
< div className = "signature-page-header" >
< div className = "sms-send-title" >
< span className = "sms-send-title__icon" >
< FilePenLine size = { 22 } / >
< / span >
< h1 > 签 名 与 报 备 材 料 < / h1 >
< / div >
< Button icon = { < Plus size = { 17 } / > } onClick = { ( ) = > setModalOpen ( true ) } > 添 加 签 名 < / Button >
const resetFilters = ( ) = > { setKeyword ( '' ) ; setApplicationFilter ( '' ) ; setStatusFilter ( '' ) ; } ;
return < section className = "page-stack client-signature-page" >
< header className = "client-signature-heading" >
< div className = "client-signature-title" >
< span className = "client-signature-title__icon" > < FileCheck2 size = { 23 } / > < / span >
< div > < h1 > 签 名 与 引 流 信 息 < / h1 > < p > 集 中 管 理 短 信 签 名 及 短 信 中 使 用 的 网 站 、 应 用 页 面 等 引 流 信 息 。 < / p > < / div >
< / div >
< Button icon = { < Plus size = { 17 } / > } onClick = { ( ) = > setSignatureModal ( 'new' ) } > 新 增 签 名 < / Button >
< / header >
< div className = "signature-search-row " >
< Input
onChange = { ( event ) = > setKeyword ( event . target . value ) }
placeholder = "搜索签名名称、用途或应用"
prefix = { < Search size = { 17 } / > }
value = { keyword }
/ >
< / div >
{ loading ? < p className = "muted" > 正 在 加 载 签 名 . . . < / p > : null }
{ error ? < p className = "form-error" > { error } < / p > : null }
< div className = "signature-list" >
{ visibleSignatures . map ( ( signature ) = > (
< article className = "signature-card signature-card--green" key = { signature . id } >
< div className = "signature-summary" >
< div >
< span > 签 名 名 称 < / span >
< strong > { signature . name } < / strong >
< / div >
< div >
< span > 用 途 < / span >
< strong > { signature . purpose ? ? '-' } < / strong >
< / div >
< div >
< span > 审 核 状 态 < / span >
< Tag tone = { statusTone [ signature . auditStatus ] ? ? 'info' } > { statusLabel [ signature . auditStatus ] ? ? signature . auditStatus } < / Tag >
< / div >
< div >
< span > 材 料 < / span >
< strong > { signature . materials ? . length ? ? 0 } 份 < / strong >
{ signature . materials ? . map ( ( material ) = > (
< FileActions file = { materialToFileRef ( material ) } key = { String ( material . id ? ? material . fileObjectId ) } / >
) ) }
< / div >
< div className = "signature-actions" >
< Button icon = { < Trash2 size = { 16 } / > } onClick = { ( ) = > disableSignature ( signature . id ) } size = "sm" variant = "danger" > 删 除 < / Button >
< / div >
< / div >
< div className = "drainage-panel" >
< div className = "section-heading" > < div > < h2 > < Globe2 size = { 17 } / > 引 流 信 息 < / h2 > < p className = "muted" > 新 建 或 修 改 后 由 运 营 审 核 , 通 过 后 自 动 进 入 通 道 报 备 。 < / p > < / div > < Button disabled = { signature . auditStatus !== 'approved' } icon = { < Plus size = { 15 } / > } onClick = { ( ) = > setDrainageModal ( { signature } ) } size = "sm" variant = "ghost" > 新 增 引 流 信 息 < / Button > < / div >
{ drainageItems ( signature ) . length ? drainageItems ( signature ) . map ( ( item ) = > < div className = "surface" key = { item . id } style = { { display : 'grid' , gap : 12 , gridTemplateColumns : '1fr 1.5fr 120px auto' , marginTop : 10 , padding : 12 } } > < strong > { item . siteName } < / strong > < span className = "drainage-table__url" > { item . url } < / span > < Tag tone = { statusTone [ item . auditStatus ] ? ? 'info' } > { statusLabel [ item . auditStatus ] ? ? item . auditStatus } < / Tag > < div className = "table-actions" > < Button icon = { < Edit3 size = { 14 } / > } onClick = { ( ) = > setDrainageModal ( { signature , item } ) } size = "sm" variant = "ghost" > 修 改 < / Button > < Button icon = { < Trash2 size = { 14 } / > } onClick = { ( ) = > deleteDrainage ( item . id ) } size = "sm" variant = "danger" > 删 除 < / Button > < / div > { item . rejectReason ? < p className = "form-error" style = { { gridColumn : '1 / -1' } } > 驳 回 原 因 : { item . rejectReason } < / p > : null } < / div > ) : < p className = "muted" > 暂 无 引 流 信 息 。 < / p > }
< / div >
< / article >
) ) }
< / div >
< Pagination
nextDisabled = { currentPage >= totalPages }
onNext = { ( ) = > setPage ( ( value ) = > Math . min ( totalPages , value + 1 ) ) }
onPrevious = { ( ) = > setPage ( ( value ) = > Math . max ( 1 , value - 1 ) ) }
page = { currentPage }
totalPages = { totalPages }
onPageChange = { setPage }
previousDisabled = { currentPage <= 1 }
total = { filteredSignatures . length }
/ >
{ ! loading && ! error && filteredSignatures . length === 0 ? < p className = "muted" > 暂 无 签 名 记 录 。 < / p > : null }
< Modal
footer = { (
< >
< Button onClick = { ( ) = > setModalOpen ( false ) } variant = "ghost" > 取 消 < / Button >
< Button disabled = { ! name || signatureFields . some ( ( field ) = > field . required && ! signatureValues [ field . code ] ) || Boolean ( signatureUploadingCode ) } onClick = { createSignature } > 提 交 审 核 < / Button >
< / >
) }
onClose = { ( ) = > setModalOpen ( false ) }
open = { modalOpen }
size = "xl"
title = "添加签名"
>
< div className = "signature-form" >
< Select
label = "短信应用"
onChange = { ( event ) = > setApplicationId ( event . target . value ) }
options = { [ { label : '不绑定应用' , value : '' } , . . . applications . map ( ( item ) = > ( { label : item.name , value : item.id } ) ) ] }
value = { applicationId }
/ >
< Input label = "短信签名" onChange = { ( event ) = > setName ( event . target . value ) } placeholder = "请输入短信签名,如【某某科技】" value = { name } / >
< Input label = "用途" onChange = { ( event ) = > setPurpose ( event . target . value ) } placeholder = "请输入签名用途" value = { purpose } / >
< section className = "surface" style = { { padding : 16 } } > < h3 > 签 名 报 备 资 料 < / h3 > < div className = "signature-form-grid" style = { { marginTop : 12 } } >
{ signatureFields . map ( ( field ) = > field . fieldType === 'string'
? < Input key = { field . id } label = { ` ${ field . required ? '* ' : '' } ${ field . name } ` } onChange = { ( event ) = > setSignatureValues ( ( current ) = > ( { . . . current , [ field . code ] : event . target . value } ) ) } value = { String ( signatureValues [ field . code ] ? ? '' ) } / >
: < label className = "signature-upload" key = { field . id } > < Upload size = { 28 } / > < strong > { reportFileRef ( signatureValues [ field . code ] ) ? . fileName ? ? ` ${ field . required ? '* ' : '' } 上传 ${ field . name } ` } < / strong > < small > { signatureUploadingCode === field . code ? '上传中...' : field . fieldType === 'image' ? '请选择图片文件' : '请选择文件' } < / small > < FileActions file = { reportFileRef ( signatureValues [ field . code ] ) } / > < input accept = { field . fieldType === 'image' ? 'image/*' : undefined } onChange = { ( event ) = > void uploadSignatureField ( field , event . target . files ? . [ 0 ] ) } style = { { display : 'none' } } type = "file" / > < / label > ) }
< / div > < / section >
< / div >
< / Modal >
{ drainageModal ? < ClientDrainageModal item = { drainageModal . item } onClose = { ( ) = > setDrainageModal ( undefined ) } onSaved = { ( ) = > { setDrainageModal ( undefined ) ; loadData ( ) ; } } signature = { drainageModal . signature } / > : null }
< section className = "client- signature-overview" aria-label = "签名审核概览 ">
< div > < span > 全 部 签 名 < / span > < strong > { workspace . summary . total } < / strong > < / div >
< div > < span > 资 料 审 核 中 < / span > < strong > { workspace . summary . pending } < / strong > < / div >
< div > < span > 审 核 通 过 < / span > < strong > { workspace . summary . approved } < / strong > < / div >
< div > < span > 需 修 改 < / span > < strong > { workspace . summary . rejected } < / strong > < / div >
< / section >
) ;
< div className = "client-signature-toolbar" >
< Input onChange = { ( event ) = > setKeyword ( event . target . value ) } placeholder = "搜索签名、用途或应用" prefix = { < Search size = { 17 } / > } value = { keyword } / >
< Select onChange = { ( event ) = > setApplicationFilter ( event . target . value ) } options = { [ { label : '全部应用' , value : '' } , . . . applications . map ( ( item ) = > ( { label : item.name , value : item.id } ) ) ] } value = { applicationFilter } / >
< Select onChange = { ( event ) = > setStatusFilter ( event . target . value ) } options = { [ { label : '全部状态' , value : '' } , { label : '资料审核中' , value : 'pending' } , { label : '审核通过' , value : 'approved' } , { label : '需修改' , value : 'rejected' } , { label : '待提交' , value : 'draft' } ] } value = { statusFilter } / >
< Button icon = { < RotateCcw size = { 15 } / > } onClick = { resetFilters } variant = "ghost" > 重 置 < / Button >
< / div >
{ error ? < p className = "form-error" > { error } < / p > : null }
< section className = "client-signature-list-shell" >
< div className = "client-signature-list-head" > < span / > < span > 签 名 名 称 < / span > < span > 所 属 应 用 < / span > < span > 使 用 场 景 < / span > < span > 审 核 状 态 < / span > < span > 已 交 资 料 < / span > < span > 更 新 时 间 < / span > < span > 操 作 < / span > < / div >
{ loading ? < p className = "client-signature-list-empty" > 正 在 加 载 . . . < / p > : null }
{ ! loading && ! visibleItems . length ? < p className = "client-signature-list-empty" > 没 有 符 合 条 件 的 签 名 记 录 。 < / p > : null }
{ visibleItems . map ( ( signature ) = > {
const expanded = expandedIds . has ( signature . id ) ;
const links = signature . drainageInfo . links ;
const editable = signature . auditStatus !== 'pending' ;
return < article className = "client-signature-row-wrap" key = { signature . id } >
< div className = "client-signature-list-row" >
< button aria-label = { expanded ? '收起引流信息' : '展开引流信息' } className = "client-signature-expand" onClick = { ( ) = > toggleExpanded ( signature . id ) } type = "button" > { expanded ? < ChevronDown size = { 18 } / > : < ChevronRight size = { 18 } / > } < / button >
< strong > { signature . name } < / strong >
< span > { signature . application ? . name ? ? '未绑定' } < / span >
< span > { signature . purpose || '-' } < / span >
< Tag tone = { statusTone [ signature . auditStatus ] ? ? 'info' } > { statusLabel [ signature . auditStatus ] ? ? signature . auditStatus } < / Tag >
< span > { signature . submittedMaterialCount } 份 < / span >
< span > { formatDate ( signature . updatedAt ) } < / span >
< div className = "table-actions" >
< Button disabled = { ! editable } icon = { < Edit3 size = { 14 } / > } onClick = { ( ) = > setSignatureModal ( signature ) } size = "sm" variant = "ghost" > 修 改 < / Button >
< Button icon = { < Trash2 size = { 14 } / > } onClick = { ( ) = > setDeleting ( { type : 'signature' , id : signature.id , name : signature.name } ) } size = "sm" variant = "danger" > 删 除 < / Button >
< / div >
< / div >
{ signature . rejectReason ? < div className = "client-signature-inline-reason" > < strong > 修 改 说 明 : < / strong > { signature . rejectReason } < / div > : null }
{ expanded ? < div className = "client-drainage-panel" >
< div className = "client-drainage-panel__head" > < div > < h3 > < Globe2 size = { 17 } / > 关 联 引 流 信 息 < / h3 > < p > 管 理 短 信 内 容 中 可 能 使 用 的 网 站 或 页 面 地 址 。 < / p > < / div > < Button disabled = { signature . auditStatus !== 'approved' } icon = { < Plus size = { 15 } / > } onClick = { ( ) = > setDrainageModal ( { signature } ) } size = "sm" variant = "ghost" > 新 增 引 流 信 息 < / Button > < / div >
{ links . length ? < div className = "client-drainage-table" >
< div className = "client-drainage-table__head" > < span > 名 称 < / span > < span > 访 问 地 址 < / span > < span > 审 核 状 态 < / span > < span > 更 新 时 间 < / span > < span > 操 作 < / span > < / div >
{ links . map ( ( item ) = > < div className = "client-drainage-table__row" key = { item . id } >
< strong > { item . siteName } < / strong > < a href = { item . url } rel = "noreferrer" target = "_blank" > { item . url } < / a > < Tag tone = { statusTone [ item . auditStatus ] ? ? 'info' } > { statusLabel [ item . auditStatus ] ? ? item . auditStatus } < / Tag > < span > { formatDate ( item . updatedAt ) } < / span >
< div className = "table-actions" > < Button disabled = { item . auditStatus === 'pending' } icon = { < Edit3 size = { 14 } / > } onClick = { ( ) = > setDrainageModal ( { signature , item } ) } size = "sm" variant = "ghost" > 修 改 < / Button > < Button icon = { < Trash2 size = { 14 } / > } onClick = { ( ) = > setDeleting ( { type : 'drainage' , id : item.id , name : item.siteName } ) } size = "sm" variant = "danger" > 删 除 < / Button > < / div >
{ item . rejectReason ? < p className = "client-drainage-reason" > < strong > 修 改 说 明 : < / strong > { item . rejectReason } < / p > : null }
< / div > ) }
< / div > : < p className = "client-signature-empty-hint" > 暂 无 引 流 信 息 。 签 名 审 核 通 过 后 可 在 这 里 新 增 。 < / p > }
< / div > : null }
< / article > ;
} ) }
< / section >
< Pagination nextDisabled = { currentPage >= totalPages } onNext = { ( ) = > setPage ( ( value ) = > Math . min ( totalPages , value + 1 ) ) } onPageChange = { setPage } onPrevious = { ( ) = > setPage ( ( value ) = > Math . max ( 1 , value - 1 ) ) } page = { currentPage } previousDisabled = { currentPage <= 1 } total = { filteredItems . length } totalPages = { totalPages } / >
{ signatureModal ? < SignatureModal applications = { applications } onClose = { ( ) = > setSignatureModal ( undefined ) } onSaved = { ( ) = > { setSignatureModal ( undefined ) ; loadData ( ) ; } } signature = { signatureModal === 'new' ? undefined : signatureModal } / > : null }
{ drainageModal ? < DrainageModal item = { drainageModal . item } onClose = { ( ) = > setDrainageModal ( undefined ) } onSaved = { ( ) = > { setDrainageModal ( undefined ) ; loadData ( ) ; } } signature = { drainageModal . signature } / > : null }
{ deleting ? < Modal footer = { < > < Button onClick = { ( ) = > setDeleting ( undefined ) } variant = "ghost" > 取 消 < / Button > < Button onClick = { ( ) = > void confirmDelete ( ) } variant = "danger" > 确 认 删 除 < / Button > < / > } onClose = { ( ) = > setDeleting ( undefined ) } open title = "确认删除" > < p > 确 定 删 除 “ { deleting . name } ” 吗 ? 删 除 后 将 不 再 用 于 短 信 发 送 。 < / p > < / Modal > : null }
< / section > ;
}