feat: wire admin workflows to real APIs
This commit is contained in:
@@ -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')}>企业认证 ></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>59分57秒</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>2026年07月02日 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>2天23小时59分</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user