feat: add stage 0 foundation
This commit is contained in:
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Ticket Platform</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3784
frontend/package-lock.json
generated
Normal file
3784
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
frontend/package.json
Normal file
34
frontend/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "ai-ticket-platform-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"typecheck": "tsc -b",
|
||||
"lint": "eslint src --ext ts,tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.16.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
5
frontend/src/app/App.tsx
Normal file
5
frontend/src/app/App.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview'
|
||||
|
||||
export function App() {
|
||||
return <DesignSystemPreview />
|
||||
}
|
||||
20
frontend/src/app/layout/AppShell.tsx
Normal file
20
frontend/src/app/layout/AppShell.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
frontend/src/app/layout/Sidebar.tsx
Normal file
9
frontend/src/app/layout/Sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
frontend/src/app/layout/Topbar.tsx
Normal file
7
frontend/src/app/layout/Topbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
frontend/src/app/router.tsx
Normal file
5
frontend/src/app/router.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { App } from './App'
|
||||
|
||||
export function Router() {
|
||||
return <App />
|
||||
}
|
||||
19
frontend/src/components/app/AppPageHeader.tsx
Normal file
19
frontend/src/components/app/AppPageHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
13
frontend/src/components/app/AppPriorityBadge.tsx
Normal file
13
frontend/src/components/app/AppPriorityBadge.tsx
Normal 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>
|
||||
}
|
||||
15
frontend/src/components/app/AppStatusBadge.tsx
Normal file
15
frontend/src/components/app/AppStatusBadge.tsx
Normal 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>
|
||||
}
|
||||
29
frontend/src/components/ui/badge.tsx
Normal file
29
frontend/src/components/ui/badge.tsx
Normal 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} />
|
||||
}
|
||||
37
frontend/src/components/ui/button.tsx
Normal file
37
frontend/src/components/ui/button.tsx
Normal 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'
|
||||
15
frontend/src/components/ui/card.tsx
Normal file
15
frontend/src/components/ui/card.tsx
Normal 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} />
|
||||
}
|
||||
48
frontend/src/features/design-system/DesignSystemPreview.tsx
Normal file
48
frontend/src/features/design-system/DesignSystemPreview.tsx
Normal 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
6
frontend/src/lib/cn.ts
Normal 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
11
frontend/src/main.tsx
Normal 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>
|
||||
)
|
||||
30
frontend/src/styles/globals.css
Normal file
30
frontend/src/styles/globals.css
Normal 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;
|
||||
}
|
||||
}
|
||||
37
frontend/tailwind.config.ts
Normal file
37
frontend/tailwind.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
surface: 'hsl(var(--surface))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
success: 'hsl(var(--success))',
|
||||
warning: 'hsl(var(--warning))',
|
||||
danger: 'hsl(var(--danger))',
|
||||
info: 'hsl(var(--info))',
|
||||
ai: 'hsl(var(--ai-accent))'
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')]
|
||||
}
|
||||
|
||||
export default config
|
||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
12
frontend/tsconfig.node.json
Normal file
12
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"lib": ["DOM", "ES2020"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user