feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user