feat: wire admin workflows to real APIs

This commit is contained in:
hectorzhao
2026-07-02 13:50:09 +08:00
parent f8c9b78c21
commit ab421cf8a7
42 changed files with 2596 additions and 250 deletions
+60 -26
View File
@@ -1,10 +1,8 @@
import { useState } from 'react';
import {
AlertCircle,
Building2,
Check,
ChevronRight,
CreditCard,
Landmark,
ShieldCheck,
Upload,
@@ -12,8 +10,9 @@ import {
} from 'lucide-react';
import { Button, Input, Select, Textarea } from '@/components/ui';
type AuthStep = 'overview' | 'profile' | 'method' | 'recharge' | 'face' | 'faceScan' | 'success' | 'failed';
type AuthStep = 'overview' | 'profile' | 'method' | 'recharge' | 'face' | 'faceScan' | 'pending' | 'success' | 'failed';
type AuthMethod = 'face' | 'recharge';
type CertificationStatus = 'uncertified' | 'pending' | 'approved' | 'rejected';
const companyInfo = {
name: '上海闪联九玖信息通信技术有限公司',
@@ -56,14 +55,21 @@ function EnterpriseStepper({ current }: { current: number }) {
);
}
function AuthHeader({ onCertified }: { onCertified: () => void }) {
function AuthHeader({ status }: { status: CertificationStatus }) {
const statusText: Record<CertificationStatus, string> = {
uncertified: '未认证',
pending: '审核中',
approved: '已通过',
rejected: '未通过',
};
return (
<div className="system-page-toolbar">
<div className="sms-send-title">
<span className="sms-send-title__icon"><ShieldCheck size={22} /></span>
<h1></h1>
</div>
<Button onClick={onCertified} variant="secondary"></Button>
<span className={`enterprise-review-status enterprise-review-status--${status}`}>{statusText[status]}</span>
</div>
);
}
@@ -71,26 +77,39 @@ function AuthHeader({ onCertified }: { onCertified: () => void }) {
export function ClientEnterpriseAuthPage() {
const [step, setStep] = useState<AuthStep>('overview');
const [method, setMethod] = useState<AuthMethod>('face');
const [status, setStatus] = useState<CertificationStatus>('uncertified');
const currentStep = step === 'profile' ? 1 : step === 'method' ? 2 : step === 'recharge' || step === 'face' || step === 'faceScan' ? 3 : step === 'success' || step === 'failed' ? 4 : 1;
const currentStep = step === 'profile' ? 1 : step === 'method' ? 2 : step === 'recharge' || step === 'face' || step === 'faceScan' ? 3 : step === 'pending' || step === 'success' || step === 'failed' ? 4 : 1;
if (step === 'overview') {
const overviewCopy = status === 'approved'
? '企业认证已审核通过,可正常使用发送、签名报备等能力。'
: status === 'pending'
? '企业认证资料已提交,平台运营将在 1 个工作日内完成审核。'
: status === 'rejected'
? '企业认证未通过,请根据驳回原因修改资料后重新提交。'
: '您还未进行企业认证';
return (
<section className="page-stack enterprise-page">
<AuthHeader onCertified={() => setStep('success')} />
<AuthHeader status={status} />
<div className="surface enterprise-status-card">
<strong></strong>
<button type="button" onClick={() => setStep('profile')}> &gt;</button>
<div className={`surface enterprise-status-card enterprise-status-card--${status}`}>
<strong>{overviewCopy}</strong>
{status === 'approved' ? null : (
<button type="button" onClick={() => setStep(status === 'pending' ? 'pending' : 'profile')}>
{status === 'pending' ? '查看审核进度 >' : '企业认证 >'}
</button>
)}
</div>
<div className="surface enterprise-info-card">
<dl>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd>{status === 'approved' ? companyInfo.name : '待认证'}</dd></div>
<div><dt></dt><dd>{status === 'approved' ? companyInfo.certifiedAt : '待审核完成'}</dd></div>
<div><dt></dt><dd>{status === 'approved' ? companyInfo.code : '待认证'}</dd></div>
<div><dt></dt><dd>{status === 'approved' ? companyInfo.address : '待认证'}</dd></div>
<div><dt></dt><dd>{status === 'approved' ? companyInfo.legalPerson : '待认证'}</dd></div>
</dl>
</div>
</section>
@@ -202,7 +221,7 @@ export function ClientEnterpriseAuthPage() {
</div>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('success')}></Button>
<Button onClick={() => { setStatus('pending'); setStep('pending'); }}></Button>
<Button onClick={() => setStep('method')} variant="secondary"></Button>
</div>
</div>
@@ -235,20 +254,36 @@ export function ClientEnterpriseAuthPage() {
<h2></h2>
<p>使<em>5957</em></p>
<div className="enterprise-qr"></div>
<span></span>
<span></span>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('face')} variant="secondary"></Button>
<Button onClick={() => setStep('success')} className="enterprise-success-test"></Button>
<Button onClick={() => setStep('failed')} variant="danger"></Button>
<Button onClick={() => { setStatus('pending'); setStep('pending'); }}></Button>
</div>
</div>
</div>
) : null}
{step === 'pending' ? (
<div className="enterprise-result enterprise-result--pending">
<span><ShieldCheck size={70} /></span>
<h2></h2>
<p></p>
<dl>
<div><dt></dt><dd>{companyInfo.name}</dd></div>
<div><dt></dt><dd>20260702 09:58:00</dd></div>
<div><dt></dt><dd>1 </dd></div>
<div><dt></dt><dd></dd></div>
</dl>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
</div>
</div>
) : null}
{step === 'success' ? (
<div className="enterprise-result enterprise-result--success">
<span><Check size={70} /></span>
<h2></h2>
<h2></h2>
<dl>
<div><dt></dt><dd>{companyInfo.name}</dd></div>
<div><dt></dt><dd>{companyInfo.code}</dd></div>
@@ -256,19 +291,18 @@ export function ClientEnterpriseAuthPage() {
<div><dt></dt><dd>{companyInfo.certifiedAt}</dd></div>
<div><dt></dt><dd>{companyInfo.address}</dd></div>
</dl>
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
</div>
) : null}
{step === 'failed' ? (
<div className="enterprise-result enterprise-result--failed">
<span>!</span>
<h2></h2>
<p></p>
<strong>22359</strong>
<button type="button" onClick={() => setStep('method')}> <ChevronRight size={18} /></button>
<h2></h2>
<p></p>
<button type="button" onClick={() => setStep('profile')}> <ChevronRight size={18} /></button>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('method')} variant="secondary"></Button>
<Button onClick={() => setStep('profile')}></Button>
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
</div>
</div>
+1 -1
View File
@@ -201,7 +201,7 @@ export function ClientHome() {
<Chart height={300} option={sendTrendOption} />
</div>
<div className="surface chart-card">
<h2></h2>
<h2></h2>
<p className="muted"></p>
<Chart height={300} option={channelShareOption} />
</div>
+4 -4
View File
@@ -154,7 +154,7 @@ function SignatureForm({ signature }: { signature?: SignatureItem }) {
export function ClientSignaturesPage() {
const [signatures, setSignatures] = useState(initialSignatures);
const [keyword, setKeyword] = useState('');
const [expandedId, setExpandedId] = useState('sig-1');
const [expandedId, setExpandedId] = useState('');
const [signatureModal, setSignatureModal] = useState<{ mode: 'add' | 'edit'; signature?: SignatureItem } | null>(null);
const [editingDrainage, setEditingDrainage] = useState<{ signature: SignatureItem; drainage?: DrainageInfo } | null>(null);
@@ -228,7 +228,7 @@ export function ClientSignaturesPage() {
</div>
<div className="signature-actions">
<Button icon={<Edit3 size={16} />} onClick={() => setSignatureModal({ mode: 'edit', signature })} size="sm" variant="ghost"></Button>
<Button icon={<Trash2 size={16} />} onClick={() => deleteSignature(signature.id)} size="sm" variant="ghost"></Button>
<Button icon={<Trash2 size={16} />} onClick={() => deleteSignature(signature.id)} size="sm" variant="danger"></Button>
</div>
</div>
@@ -254,8 +254,8 @@ export function ClientSignaturesPage() {
<Tag tone={statusToneMap[item.telecom]}>{statusLabelMap[item.telecom]}</Tag>
<span className="muted">{item.submittedAt}</span>
<span className="drainage-row-actions">
<button onClick={() => setEditingDrainage({ signature, drainage: item })} type="button"></button>
<button onClick={() => deleteDrainage(signature.id, item.id)} type="button"></button>
<Button onClick={() => setEditingDrainage({ signature, drainage: item })} size="sm" variant="ghost"></Button>
<Button onClick={() => deleteDrainage(signature.id, item.id)} size="sm" variant="danger"></Button>
</span>
</div>
))}
+18 -4
View File
@@ -3,6 +3,7 @@ import { CalendarDays, Download, FileText, Search } from 'lucide-react';
import {
Button,
Input,
Pagination,
Select,
Table,
Tag,
@@ -53,6 +54,8 @@ export function ClientSystemLogsPage() {
const [level, setLevel] = useState('all');
const [module, setModule] = useState('all');
const [range, setRange] = useState('today');
const [page, setPage] = useState(1);
const pageSize = 5;
const moduleOptions = useMemo(() => {
const modules = Array.from(new Set(logsSeed.map((item) => item.module)));
@@ -66,6 +69,9 @@ export function ClientSystemLogsPage() {
const matchesModule = module === 'all' || item.module === module;
return matchesKeyword && matchesLevel && matchesModule;
});
const totalPages = Math.max(1, Math.ceil(filteredLogs.length / pageSize));
const currentPage = Math.min(page, totalPages);
const pagedLogs = filteredLogs.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const columns = useMemo<Array<TableColumn<SystemLog>>>(() => [
{ key: 'time', title: '时间', width: '190px', render: (record) => <span className="muted">{record.time}</span> },
@@ -89,13 +95,13 @@ export function ClientSystemLogsPage() {
<div className="system-log-filters">
<Input
onChange={(event) => setKeyword(event.target.value)}
onChange={(event) => { setKeyword(event.target.value); setPage(1); }}
placeholder="搜索操作人、操作或详情"
prefix={<Search size={16} />}
value={keyword}
/>
<Select
onChange={(event) => setLevel(event.target.value)}
onChange={(event) => { setLevel(event.target.value); setPage(1); }}
options={[
{ label: '全部级别', value: 'all' },
{ label: '信息', value: 'info' },
@@ -105,7 +111,7 @@ export function ClientSystemLogsPage() {
]}
value={level}
/>
<Select onChange={(event) => setModule(event.target.value)} options={moduleOptions} value={module} />
<Select onChange={(event) => { setModule(event.target.value); setPage(1); }} options={moduleOptions} value={module} />
</div>
<div className="system-log-range">
@@ -128,7 +134,15 @@ export function ClientSystemLogsPage() {
</div>
<div className="surface system-table-card">
<Table columns={columns} data={filteredLogs} emptyText="暂无系统日志" rowKey="id" />
<Table columns={columns} data={pagedLogs} emptyText="暂无系统日志" rowKey="id" />
<Pagination
nextDisabled={currentPage >= totalPages}
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
page={currentPage}
previousDisabled={currentPage <= 1}
total={filteredLogs.length}
/>
</div>
</section>
);
+8 -5
View File
@@ -11,7 +11,7 @@ import {
type TableColumn,
} from '@/components/ui';
type UserRole = 'admin' | 'user';
type UserRole = 'enterprise_admin' | 'user';
type UserStatus = 'active' | 'disabled';
type ClientUser = {
@@ -25,12 +25,12 @@ type ClientUser = {
};
const roleLabelMap: Record<UserRole, string> = {
admin: '管理员',
enterprise_admin: '企业管理员',
user: '普通用户',
};
const usersSeed: ClientUser[] = [
{ id: 'USER001', name: '张三', email: 'zhangsan@example.com', phone: '13800138000', role: 'admin', status: 'active', lastLoginAt: '2026-03-17 09:15:00' },
{ id: 'USER001', name: '张三', email: 'zhangsan@example.com', phone: '13800138000', role: 'enterprise_admin', status: 'active', lastLoginAt: '2026-03-17 09:15:00' },
{ id: 'USER002', name: '李四', email: 'lisi@example.com', phone: '13800138001', role: 'user', status: 'active', lastLoginAt: '2026-03-16 16:45:00' },
{ id: 'USER003', name: '王五', email: 'wangwu@example.com', phone: '13800138002', role: 'user', status: 'active', lastLoginAt: '2026-03-15 11:30:00' },
{ id: 'USER004', name: '赵六', email: 'zhaoliu@example.com', phone: '13800138003', role: 'user', status: 'disabled', lastLoginAt: '2026-02-20 14:00:00' },
@@ -51,6 +51,8 @@ export function ClientUsersPage() {
const [keyword, setKeyword] = useState('');
const [editingUser, setEditingUser] = useState<ClientUser | null>(null);
const [draft, setDraft] = useState<ClientUser>(emptyUser);
const enterpriseAdmin = usersSeed.find((item) => item.role === 'enterprise_admin');
const canSelectEnterpriseAdmin = !enterpriseAdmin || editingUser?.id === enterpriseAdmin.id;
const filteredUsers = usersSeed.filter((item) => {
const target = `${item.name} ${item.email} ${item.phone}`;
@@ -71,7 +73,7 @@ export function ClientUsersPage() {
key: 'role',
title: '角色',
width: '150px',
render: (record) => <Tag tone={record.role === 'admin' ? 'info' : 'success'}>{roleLabelMap[record.role]}</Tag>,
render: (record) => <Tag tone={record.role === 'enterprise_admin' ? 'info' : 'success'}>{roleLabelMap[record.role]}</Tag>,
},
{
key: 'status',
@@ -134,10 +136,11 @@ export function ClientUsersPage() {
<Input label="邮箱 *" onChange={(event) => setDraft({ ...draft, email: event.target.value })} placeholder="请输入邮箱" value={draft.email} />
<Input label="手机号 *" onChange={(event) => setDraft({ ...draft, phone: event.target.value })} placeholder="请输入手机号" value={draft.phone} />
<Select
hint={canSelectEnterpriseAdmin ? '企业管理员拥有企业空间最高权限。' : `当前企业管理员为 ${enterpriseAdmin?.name},每个企业仅允许 1 位企业管理员。`}
label="角色 *"
onChange={(event) => setDraft({ ...draft, role: event.target.value as UserRole })}
options={[
{ label: '管理员', value: 'admin' },
...(canSelectEnterpriseAdmin ? [{ label: '企业管理员', value: 'enterprise_admin' }] : []),
{ label: '普通用户', value: 'user' },
]}
value={draft.role}