feat: harden platform workflows and UI governance
This commit is contained in:
@@ -61,7 +61,7 @@ function UploadPanel({ file, uploading, onFile }: { file: FileObject | null; upl
|
||||
<label className="enterprise-upload">
|
||||
<Upload size={38} />
|
||||
<strong>{uploading ? '上传中...' : file ? displayFileName(file.fileName) : '点击上传'}</strong>
|
||||
<FileActions file={fileRef} />
|
||||
<FileActions file={fileRef} portal="client" />
|
||||
<input
|
||||
accept="image/png,image/jpeg,image/webp,application/pdf"
|
||||
disabled={uploading}
|
||||
|
||||
@@ -21,7 +21,6 @@ type RecentTaskRow = {
|
||||
taskNo: string;
|
||||
scene: string;
|
||||
count: number;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
};
|
||||
@@ -30,7 +29,6 @@ const columns: Array<TableColumn<RecentTaskRow>> = [
|
||||
{ key: 'taskNo', title: '批次编号', render: (record) => record.taskNo },
|
||||
{ key: 'scene', title: '发送场景', render: (record) => record.scene },
|
||||
{ key: 'count', title: '发送量', render: (record) => `${record.count.toLocaleString('zh-CN')} 条` },
|
||||
{ key: 'channel', title: '通道', render: (record) => record.channel },
|
||||
{ key: 'createdAt', title: '创建时间', render: (record) => record.createdAt },
|
||||
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status === 'completed' ? 'success' : record.status === 'failed' ? 'danger' : 'info'}>{record.status}</Tag> },
|
||||
];
|
||||
@@ -61,7 +59,6 @@ export function ClientHome() {
|
||||
taskNo: String(task.taskNo ?? task.id),
|
||||
scene: String(task.category ?? task.content ?? '短信发送'),
|
||||
count: Number(task.phoneTotal ?? task.progressTotal ?? 0),
|
||||
channel: Array.isArray(task.messages) && task.messages[0]?.channel?.name ? String(task.messages[0].channel.name) : '待路由',
|
||||
createdAt: formatDateTime(task.createdAt ? String(task.createdAt) : null),
|
||||
status: String(task.status ?? 'unknown'),
|
||||
})), [dashboard]);
|
||||
|
||||
@@ -178,8 +178,8 @@ export function ClientSendDetailPage() {
|
||||
<tr><td className="ui-table__empty" colSpan={9}>暂无发送记录</td></tr>
|
||||
) : visibleRows.map((record) => {
|
||||
const receipt = getReceipt(record);
|
||||
const carrier = record.channel?.carrier ? carrierLabelMap[record.channel.carrier] ?? record.channel.carrier : '-';
|
||||
const region = record.channel?.sendRegion ?? '-';
|
||||
const carrier = record.carrier ? carrierLabelMap[record.carrier] ?? record.carrier : '-';
|
||||
const region = record.province ?? '-';
|
||||
return (
|
||||
<Fragment key={record.id}>
|
||||
<tr className="send-detail-main-row">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from '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 { Button, DeleteRiskAction, FileActions, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
|
||||
import {
|
||||
clientApi,
|
||||
type ClientApplicationReportField,
|
||||
@@ -67,7 +67,7 @@ function ReviewFields({
|
||||
<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])} />
|
||||
<FileActions file={reportFileRef(values[field.code])} portal="client" />
|
||||
<input
|
||||
accept={field.fieldType === 'image' ? 'image/*' : undefined}
|
||||
onChange={(event) => onUpload(field, event.target.files?.[0])}
|
||||
@@ -282,8 +282,7 @@ export function ClientSignaturesPage() {
|
||||
async function confirmDelete() {
|
||||
if (!deleting) return;
|
||||
try {
|
||||
if (deleting.type === 'signature') await clientApi.changeSignatureStatus(deleting.id, 'disabled');
|
||||
else await clientApi.changeDrainageInfoStatus(deleting.id, 'deleted');
|
||||
await clientApi.changeDrainageInfoStatus(deleting.id, 'deleted');
|
||||
setDeleting(undefined);
|
||||
loadData();
|
||||
} catch (failure) {
|
||||
@@ -335,7 +334,7 @@ export function ClientSignaturesPage() {
|
||||
<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>
|
||||
<DeleteRiskAction onCompleted={() => void loadData()} portal="client" targetId={signature.id} targetType="signature" />
|
||||
</div>
|
||||
</div>
|
||||
{signature.rejectReason ? <div className="client-signature-inline-reason"><strong>修改说明:</strong>{signature.rejectReason}</div> : null}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CalendarDays, Download, FileText, Search } from 'lucide-react';
|
||||
import { CalendarDays, FileText, Search } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Pagination,
|
||||
Select,
|
||||
SystemLogExport,
|
||||
Table,
|
||||
Tag,
|
||||
type TableColumn,
|
||||
@@ -78,7 +79,7 @@ export function ClientSystemLogsPage() {
|
||||
<span className="sms-send-title__icon"><FileText size={22} /></span>
|
||||
<h1>系统日志</h1>
|
||||
</div>
|
||||
<Button icon={<Download size={17} />} variant="secondary">导出日志</Button>
|
||||
<SystemLogExport exportLogs={clientApi.exportSystemLogs} filters={{ keyword, level, module, range }} portal="client" />
|
||||
</div>
|
||||
|
||||
<div className="system-log-filters">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Edit3, MessageSquare, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
|
||||
import { Button, DeleteRiskAction, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
|
||||
import { clientApi, type ClientSmsApplication, type ClientSmsSignatureView, type ClientSmsTemplate } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { replaceLeadingSmsSignature } from '@/utils/smsSignature';
|
||||
@@ -86,6 +86,8 @@ function TemplateModal({
|
||||
content: initialContent,
|
||||
variables: item?.variables?.map((variable) => ({ name: variable.name, example: variable.example ?? undefined, required: variable.required ?? true })) ?? [],
|
||||
});
|
||||
const initialForm = useRef(form).current;
|
||||
const dirty = JSON.stringify(form) !== JSON.stringify(initialForm);
|
||||
const application = applications.find((candidate) => candidate.id === form.applicationId);
|
||||
const availableSignatures = signatures.filter((signature) => (
|
||||
signature.auditStatus === 'approved'
|
||||
@@ -130,9 +132,10 @@ function TemplateModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
dirty={dirty}
|
||||
footer={({ requestClose }) => (
|
||||
<>
|
||||
<Button onClick={onClose} variant="ghost">取消</Button>
|
||||
<Button onClick={requestClose} variant="ghost">取消</Button>
|
||||
<Button disabled={!form.applicationId || !form.signatureId || !form.name || !form.content.trim()} onClick={() => onSubmit({ ...form, variables })}>提交审核</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -266,12 +269,6 @@ export function ClientTemplatesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function disableTemplate(id: string) {
|
||||
clientApi.changeTemplateStatus(id, 'disabled')
|
||||
.then(loadData)
|
||||
.catch((reason: Error) => setError(reason.message || '模板禁用失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="template-page-header">
|
||||
@@ -315,10 +312,7 @@ export function ClientTemplatesPage() {
|
||||
<Edit3 size={14} />
|
||||
编辑
|
||||
</button>
|
||||
<button onClick={() => disableTemplate(template.id)} type="button">
|
||||
<Trash2 size={14} />
|
||||
删除
|
||||
</button>
|
||||
<DeleteRiskAction onCompleted={() => void loadData()} portal="client" targetId={template.id} targetType="template" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -41,7 +41,7 @@ function toForm(user?: ManagedUser): UserForm {
|
||||
}
|
||||
|
||||
export function ClientUsersPage() {
|
||||
const session = readSession();
|
||||
const session = readSession('client');
|
||||
const tenantId = session?.user.tenantId ?? undefined;
|
||||
const [users, setUsers] = useState<ManagedUser[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
Reference in New Issue
Block a user