93 lines
5.2 KiB
TypeScript
93 lines
5.2 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
export type Route = "home" | "docs" | "console";
|
|
const companyLogo = new URL("../../web/public/brand-logo.svg", import.meta.url).href;
|
|
|
|
export function navigate(route: Route) {
|
|
window.location.hash = route === "home" ? "/" : `/${route}`;
|
|
}
|
|
|
|
export function currentRoute(): Route {
|
|
const path = window.location.hash.replace(/^#/, "");
|
|
if (path.startsWith("/docs")) return "docs";
|
|
if (path.startsWith("/console")) return "console";
|
|
return "home";
|
|
}
|
|
|
|
export function Brand({ compact = false }: { compact?: boolean }) {
|
|
return (
|
|
<button className={`brand${compact ? " compact" : ""}`} onClick={() => navigate("home")} aria-label="返回开放平台首页">
|
|
<img src={companyLogo} alt="羚牛智能" />
|
|
<span className="brand-divider" aria-hidden="true" />
|
|
<span className="brand-product">开放平台</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export type IconName =
|
|
| "home" | "apps" | "users" | "car" | "shield" | "book" | "key"
|
|
| "terminal" | "activity" | "settings" | "plus" | "search" | "close"
|
|
| "check" | "clock" | "copy" | "logout" | "arrow" | "menu";
|
|
|
|
const paths: Record<IconName, ReactNode> = {
|
|
home: <><path d="m3 10 9-7 9 7" /><path d="M5 9v11h14V9M9 20v-6h6v6" /></>,
|
|
apps: <><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></>,
|
|
users: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8" /></>,
|
|
car: <><path d="m5 17-1 2v2M19 17l1 2v2" /><path d="M3 17h18v-5l-2-5H5l-2 5v5Z" /><circle cx="7" cy="14" r="1" /><circle cx="17" cy="14" r="1" /></>,
|
|
shield: <><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" /><path d="m9 12 2 2 4-4" /></>,
|
|
book: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" /><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z" /></>,
|
|
key: <><circle cx="8" cy="15" r="5" /><path d="m12 11 9-9M18 5l3 3M15 8l3 3" /></>,
|
|
terminal: <><path d="m4 17 6-5-6-5M12 19h8" /></>,
|
|
activity: <path d="M3 12h4l3-8 4 16 3-8h4" />,
|
|
settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.09A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.1.37.3.72.6 1 .3.28.68.42 1.1.4H21v4h-.09A1.7 1.7 0 0 0 19.4 15Z" /></>,
|
|
plus: <path d="M12 5v14M5 12h14" />,
|
|
search: <><circle cx="11" cy="11" r="7" /><path d="m20 20-4-4" /></>,
|
|
close: <path d="m6 6 12 12M18 6 6 18" />,
|
|
check: <path d="m5 12 4 4L19 6" />,
|
|
clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
|
|
copy: <><rect x="8" y="8" width="12" height="12" rx="2" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></>,
|
|
logout: <><path d="M10 17l5-5-5-5M15 12H3" /><path d="M14 3h7v18h-7" /></>,
|
|
arrow: <path d="m9 18 6-6-6-6" />,
|
|
menu: <path d="M4 7h16M4 12h16M4 17h16" />
|
|
};
|
|
|
|
export function Icon({ name, size = 20 }: { name: IconName; size?: number }) {
|
|
return <svg className="icon" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{paths[name]}</svg>;
|
|
}
|
|
|
|
export function Status({ value, label }: { value: string; label?: string }) {
|
|
const normalized = value === "enabled" || value === "success" || value === "available" ? "enabled" : value === "disabled" || value === "failed" ? "disabled" : value;
|
|
const text = label || (value === "enabled" ? "启用" : value === "disabled" ? "已停用" : value === "success" ? "成功" : value);
|
|
return <span className={`status ${normalized}`}><i />{text}</span>;
|
|
}
|
|
|
|
export function PageHeader({ title, description, action }: { title: string; description?: string; action?: ReactNode }) {
|
|
return <header className="workspace-header"><div><h1>{title}</h1>{description && <p>{description}</p>}</div>{action}</header>;
|
|
}
|
|
|
|
export function EmptyState({ title, body, action }: { title: string; body: string; action?: ReactNode }) {
|
|
return <div className="empty-state"><span><Icon name="apps" size={25} /></span><h2>{title}</h2><p>{body}</p>{action}</div>;
|
|
}
|
|
|
|
export function dateInput(value?: string) {
|
|
return value ? value.slice(0, 10) : "";
|
|
}
|
|
|
|
export function rfcDate(value: string) {
|
|
return `${value}T00:00:00+08:00`;
|
|
}
|
|
|
|
export function formatDate(value?: string) {
|
|
if (!value) return "长期有效";
|
|
return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(value));
|
|
}
|
|
|
|
export function formatTime(value?: string) {
|
|
if (!value) return "从未登录";
|
|
return new Intl.DateTimeFormat("zh-CN", { dateStyle: "short", timeStyle: "short" }).format(new Date(value));
|
|
}
|
|
|
|
export function errorMessage(reason: unknown) {
|
|
return reason instanceof Error ? reason.message : "操作失败,请稍后重试";
|
|
}
|