feat: complete phase2 baseline cdr quality rbac

This commit is contained in:
hectorzhao
2026-06-24 18:40:21 +08:00
parent 7057fd3c42
commit a86de6545f
63 changed files with 7853 additions and 3678 deletions
+209
View File
@@ -0,0 +1,209 @@
import { Alert, Badge, Button } from './ui.jsx';
const selectedBlue = '#2563EB';
export function toneForStatus(status) {
if (['启用', '在线', '正常', '生效', '已计费', '完成', '已完成', '开启', '可试听', '成功'].includes(status)) return 'success';
if (['观察', '抖动', '待质检', '处理中', '失败不计费'].includes(status)) return 'warning';
if (['停用', '禁用', '暂停', '告警', '关闭', '失败'].includes(status)) return 'danger';
return 'neutral';
}
export function Icon({ type }) {
const common = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' };
const paths = {
plus: <path d="M12 5v14M5 12h14" />,
search: <><circle cx="11" cy="11" r="7" /><path d="m20 20-3.4-3.4" /></>,
export: <><path d="M12 3v12" /><path d="m7 10 5 5 5-5" /><path d="M5 21h14" /></>,
reload: <><path d="M21 12a9 9 0 0 1-15 6.7" /><path d="M3 12a9 9 0 0 1 15-6.7" /><path d="M18 3v5h-5" /><path d="M6 21v-5h5" /></>,
arrow: <><path d="M5 12h14" /><path d="m13 6 6 6-6 6" /></>,
phoneOff: <><path d="M10.7 13.3a10.8 10.8 0 0 0 3.9 2.9" /><path d="M17 13.5l3 3v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.5 6.2 2 2 0 0 1 4.5 4h3l3 3-2 2a13 13 0 0 0 1.2 2.3" /><path d="M22 2 2 22" /></>,
collapse: <><path d="M15 18l-6-6 6-6" /><path d="M20 4v16" /></>,
expand: <><path d="M9 18l6-6-6-6" /><path d="M4 4v16" /></>,
logout: <><path d="M10 17l5-5-5-5" /><path d="M15 12H3" /><path d="M21 19V5a2 2 0 0 0-2-2h-5" /></>,
};
return <svg {...common}>{paths[type]}</svg>;
}
export function PageTitle({ title, desc, actions }) {
return (
<div className="page-title">
<div>
<h1>{title}</h1>
<p>{desc}</p>
</div>
<div className="page-actions">{actions}</div>
</div>
);
}
export function Toolbar({ children }) {
return <div className="toolbar">{children}</div>;
}
export function Panel({ title, aside, children, className = '' }) {
return (
<section className={`prototype-panel ${className}`}>
<div className="panel-head">
<h2>{title}</h2>
{aside}
</div>
{children}
</section>
);
}
export function ApiNotice({ loading, error, onRetry }) {
if (loading) {
return <Alert title="正在读取真实 API">正在从 LisgloSIPS API 拉取页面数据</Alert>;
}
if (error) {
return (
<Alert title="API 数据不可用" tone="warning">
{error}
{onRetry ? <Button size="sm" variant="outline" onClick={onRetry}>重试</Button> : null}
</Alert>
);
}
return null;
}
export function EmptyState({ title = '暂无数据', children = '当前筛选条件下没有可展示的数据。' }) {
return (
<div className="empty-state">
<strong>{title}</strong>
<span>{children}</span>
</div>
);
}
export function Modal({ title, aside, children, onClose, size = 'lg' }) {
return (
<div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
<section className={`modal-dialog modal-${size}`} role="dialog" aria-modal="true" aria-label={title} onMouseDown={(event) => event.stopPropagation()}>
<div className="modal-head">
<div>
<h2>{title}</h2>
{aside}
</div>
<Button variant="ghost" size="sm" onClick={onClose}>关闭</Button>
</div>
<div className="modal-body">{children}</div>
</section>
</div>
);
}
export function ConfirmDialog({
title,
children,
confirmLabel = '确认',
confirmVariant = 'primary',
busy = false,
onCancel,
onConfirm,
}) {
return (
<Modal title={title} onClose={busy ? () => {} : onCancel} size="sm">
<div className="confirm-copy">
{typeof children === 'string' ? <p>{children}</p> : children}
</div>
<div className="modal-actions">
<Button type="button" variant="outline" disabled={busy} onClick={onCancel}>取消</Button>
<Button type="button" variant={confirmVariant} disabled={busy} onClick={onConfirm}>
{busy ? '处理中' : confirmLabel}
</Button>
</div>
</Modal>
);
}
export function Drawer({ title, aside, children, onClose }) {
return (
<div className="drawer-backdrop" role="presentation" onMouseDown={onClose}>
<aside className="drawer-panel" role="dialog" aria-modal="true" aria-label={title} onMouseDown={(event) => event.stopPropagation()}>
<div className="drawer-head">
<div>
<h2>{title}</h2>
{aside}
</div>
<Button variant="ghost" size="sm" onClick={onClose}>关闭</Button>
</div>
<div className="drawer-body">{children}</div>
</aside>
</div>
);
}
export function StatusBadge({ children }) {
return <Badge tone={toneForStatus(children)}>{children}</Badge>;
}
export function SimpleTable({ columns, rows, onRowClick, selectedKey }) {
return (
<div className="table-shell">
<table className="proto-table">
{columns.some((column) => column.width) ? (
<colgroup>
{columns.map((column) => <col key={column.key} style={column.width ? { width: column.width } : undefined} />)}
</colgroup>
) : null}
<thead>
<tr>
{columns.map((column) => <th key={column.key} className={column.className}>{column.label}</th>)}
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={columns.length}>
<EmptyState />
</td>
</tr>
) : rows.map((row, index) => (
<tr
key={row.id || row.callId || row.name || index}
className={selectedKey && selectedKey === (row.id || row.callId) ? 'is-selected' : ''}
onClick={onRowClick ? () => onRowClick(row) : undefined}
>
{columns.map((column) => (
<td key={column.key} className={column.className}>
{column.status ? <StatusBadge>{row[column.key]}</StatusBadge> : column.render ? column.render(row) : row[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export function MiniBarChart({ data, color = selectedBlue }) {
const max = Math.max(1, ...data);
return (
<div className="mini-chart" aria-label="趋势图">
{data.map((value, index) => (
<span key={index} style={{ height: `${Math.max(14, (value / max) * 100)}%`, background: color }} />
))}
</div>
);
}
export function LineChart({ data }) {
const points = data.map((value, index) => `${(index / Math.max(1, data.length - 1)) * 100},${100 - value}`).join(' ');
return (
<svg className="line-chart" viewBox="0 0 100 100" preserveAspectRatio="none" aria-label="接通率趋势">
<polyline points={points} fill="none" stroke={selectedBlue} strokeWidth="3" vectorEffect="non-scaling-stroke" />
</svg>
);
}
export function KeyValue({ label, value }) {
return (
<div className="kv">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}