feat: add stage 0 foundation

This commit is contained in:
hailin
2026-06-30 15:55:46 +08:00
parent 6d373da09f
commit 604f456e03
68 changed files with 4884 additions and 0 deletions

5
frontend/src/app/App.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview'
export function App() {
return <DesignSystemPreview />
}

View File

@@ -0,0 +1,20 @@
import type * as React from 'react'
import { Sidebar } from './Sidebar'
import { Topbar } from './Topbar'
type AppShellProps = {
children: React.ReactNode
}
export function AppShell({ children }: AppShellProps) {
return (
<div className="min-h-screen bg-background text-foreground">
<Sidebar />
<div className="min-h-screen lg:pl-64">
<Topbar />
<main>{children}</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
export function Sidebar() {
return (
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r border-border bg-surface lg:block">
<div className="border-b border-border px-4 py-3">
<p className="text-sm font-semibold text-foreground">AI Ticket Platform</p>
</div>
</aside>
)
}

View File

@@ -0,0 +1,7 @@
export function Topbar() {
return (
<header className="flex h-14 items-center border-b border-border bg-surface px-6">
<p className="text-sm text-muted-foreground">工单工作台</p>
</header>
)
}

View File

@@ -0,0 +1,5 @@
import { App } from './App'
export function Router() {
return <App />
}

View File

@@ -0,0 +1,19 @@
import type * as React from 'react'
type AppPageHeaderProps = {
title: string
description?: string
actions?: React.ReactNode
}
export function AppPageHeader({ title, description, actions }: AppPageHeaderProps) {
return (
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-xl font-semibold tracking-normal text-foreground">{title}</h1>
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
</div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</div>
)
}

View File

@@ -0,0 +1,13 @@
import { Badge } from '@/components/ui/badge'
const PRIORITY_LABELS: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
}
export function AppPriorityBadge({ priority }: { priority: string }) {
const variant = priority === 'urgent' ? 'danger' : priority === 'high' ? 'warning' : 'default'
return <Badge variant={variant}>{PRIORITY_LABELS[priority] ?? priority}</Badge>
}

View File

@@ -0,0 +1,15 @@
import { Badge } from '@/components/ui/badge'
const STATUS_LABELS: Record<string, string> = {
pending_info: '待补充',
pending: '待处理',
in_progress: '处理中',
pending_confirm: '待确认',
resolved: '已解决',
closed: '已关闭'
}
export function AppStatusBadge({ status }: { status: string }) {
const variant = status === 'resolved' || status === 'closed' ? 'success' : 'info'
return <Badge variant={variant}>{STATUS_LABELS[status] ?? status}</Badge>
}

View File

@@ -0,0 +1,29 @@
import type * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium',
{
variants: {
variant: {
default: 'border-border bg-muted text-foreground',
success: 'border-transparent bg-success/10 text-success',
warning: 'border-transparent bg-warning/15 text-warning',
danger: 'border-transparent bg-danger/10 text-danger',
info: 'border-transparent bg-info/10 text-info',
ai: 'border-transparent bg-ai/10 text-ai'
}
},
defaultVariants: {
variant: 'default'
}
}
)
type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>
export function Badge({ className, variant, ...props }: BadgeProps) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} />
}

View File

@@ -0,0 +1,37 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/cn'
const buttonVariants = cva(
'inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:opacity-90',
secondary: 'border border-border bg-surface text-foreground hover:bg-muted',
ghost: 'text-foreground hover:bg-muted',
danger: 'bg-danger text-white hover:opacity-90'
}
},
defaultVariants: {
variant: 'default'
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return <Comp className={cn(buttonVariants({ variant }), className)} ref={ref} {...props} />
}
)
Button.displayName = 'Button'

View File

@@ -0,0 +1,15 @@
import * as React from 'react'
import { cn } from '@/lib/cn'
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('rounded-lg border border-border bg-surface', className)} {...props} />
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('border-b border-border px-4 py-3', className)} {...props} />
}
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('p-4', className)} {...props} />
}

View File

@@ -0,0 +1,48 @@
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
export function DesignSystemPreview() {
return (
<main className="min-h-screen bg-background p-6 text-foreground">
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<AppPageHeader
title="设计系统预览"
description="业务页面必须基于这些 token 和组件开发。"
actions={<Button>创建工单</Button>}
/>
<Card>
<CardHeader>
<h2 className="text-sm font-medium">Buttons</h2>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<h2 className="text-sm font-medium">Badges</h2>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="ai">AI</Badge>
<AppStatusBadge status="in_progress" />
<AppPriorityBadge priority="urgent" />
</CardContent>
</Card>
</div>
</main>
)
}

6
frontend/src/lib/cn.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

11
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './app/App'
import './styles/globals.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 210 20% 98%;
--foreground: 222 25% 12%;
--surface: 0 0% 100%;
--muted: 210 18% 94%;
--muted-foreground: 215 12% 42%;
--border: 214 18% 86%;
--primary: 213 52% 32%;
--primary-foreground: 0 0% 100%;
--success: 151 55% 36%;
--warning: 38 86% 48%;
--danger: 0 70% 50%;
--info: 204 76% 44%;
--ai-accent: 186 52% 36%;
--radius: 0.5rem;
}
body {
margin: 0;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
}