fix: harden real backend workflows and channel connections

This commit is contained in:
hectorzhao
2026-07-06 17:54:53 +08:00
parent 8cca361441
commit b5132d7f4e
47 changed files with 2530 additions and 314 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ export function ClientApplicationsPage() {
<dd>{formatPrice(application.customerUnitPrice)}</dd>
</div>
<div>
<dt>CMPP接状态</dt>
<dt>CMPP接状态</dt>
<dd><Tag tone={statusToneMap[linkStatus]}>{statusLabelMap[linkStatus]}</Tag></dd>
</div>
</dl>
+8 -2
View File
@@ -8,8 +8,8 @@ import {
Upload,
UserCheck,
} from 'lucide-react';
import { Button, Input, Select, Textarea } from '@/components/ui';
import { clientApi, type EnterpriseCertification, type FileObject } from '@/api/adminApi';
import { Button, FileActions, Input, Select, Textarea } from '@/components/ui';
import { clientApi, type EnterpriseCertification, type FileObject, type FileRef } from '@/api/adminApi';
type AuthStep = 'overview' | 'profile' | 'method' | 'recharge' | 'face' | 'faceScan' | 'pending' | 'success' | 'failed';
type AuthMethod = 'face' | 'recharge';
@@ -52,10 +52,15 @@ const emptyCertificationForm: CertificationForm = {
};
function UploadPanel({ file, uploading, onFile }: { file: FileObject | null; uploading: boolean; onFile: (file: File | undefined) => void }) {
const fileRef: FileRef | null = file
? { contentType: file.contentType, fileName: file.fileName, fileObjectId: file.id }
: null;
return (
<label className="enterprise-upload">
<Upload size={38} />
<strong>{uploading ? '上传中...' : file?.fileName ?? '点击上传'}</strong>
<FileActions file={fileRef} />
<input
accept="image/png,image/jpeg,image/webp,application/pdf"
disabled={uploading}
@@ -203,6 +208,7 @@ export function ClientEnterpriseAuthPage() {
materials: {
licenseFileObjectId: licenseFile?.id,
licenseFileName: licenseFile?.fileName,
licenseFileContentType: licenseFile?.contentType,
province: form.province,
city: form.city,
address: form.address,
+31 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { Check, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
import { Check, Download, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
import { Button, DateTimeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignature, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi';
@@ -29,6 +29,7 @@ export function ClientSendPage() {
const [recipients, setRecipients] = useState<Recipient[]>([{ id: '1', phone: '' }]);
const [importContent, setImportContent] = useState('');
const [importFileName, setImportFileName] = useState('');
const [importFileUrl, setImportFileUrl] = useState('');
const [importPreview, setImportPreview] = useState<ImportPreviewResponse | null>(null);
const [importLoading, setImportLoading] = useState(false);
const [submittedRecord, setSubmittedRecord] = useState<SmsBatchTask | null>(null);
@@ -44,6 +45,12 @@ export function ClientSendPage() {
.catch((reason: Error) => setError(reason.message || '短信发送基础数据加载失败'));
}, []);
useEffect(() => () => {
if (importFileUrl) {
URL.revokeObjectURL(importFileUrl);
}
}, [importFileUrl]);
const selectedSignature = useMemo(
() => signatures.find((item) => item.id === signatureId),
[signatures, signatureId],
@@ -133,10 +140,22 @@ export function ClientSendPage() {
});
setImportContent(content);
setImportFileName(file.name);
setImportFileUrl((current) => {
if (current) {
URL.revokeObjectURL(current);
}
return URL.createObjectURL(file);
});
setImportPreview(preview);
} catch (reason) {
setImportContent('');
setImportFileName('');
setImportFileUrl((current) => {
if (current) {
URL.revokeObjectURL(current);
}
return '';
});
setImportPreview(null);
setError(reason instanceof Error ? reason.message : '导入预览失败');
} finally {
@@ -294,7 +313,17 @@ export function ClientSendPage() {
<Button disabled={importLoading || !templateId} onClick={() => document.getElementById('sms-import-file')?.click()} variant="ghost">
{importLoading ? '解析中...' : '选择文件'}
</Button>
{importFileName ? <span>{importFileName}</span> : null}
{importFileName ? (
<span>
{importFileName}
{importFileUrl ? (
<a className="file-action-link" download={importFileName} href={importFileUrl}>
<Download size={14} />
</a>
) : null}
</span>
) : null}
{importPreview ? (
<div className="detail-grid">
<div><span></span><strong>{importPreview.totalRows}</strong></div>
+13 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { FilePenLine, Plus, Search, Trash2, Upload } from 'lucide-react';
import { Button, Input, Modal, Select, Tag } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignature } from '@/api/adminApi';
import { Button, FileActions, Input, Modal, Select, Tag } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignature, type FileRef } from '@/api/adminApi';
const statusTone: Record<string, 'success' | 'info' | 'danger' | 'warning'> = {
approved: 'success',
@@ -18,6 +18,14 @@ const statusLabel: Record<string, string> = {
disabled: '已禁用',
};
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 };
}
export function ClientSignaturesPage() {
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
@@ -124,6 +132,9 @@ export function ClientSignaturesPage() {
<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>