Browse Source

添加 React 前端 PC 版本

master
dark 1 month ago
parent
commit
3bcdb439c8
  1. 3
      .claude/settings.local.json
  2. 24
      frontend/react-shadcn/pc/.gitignore
  3. 73
      frontend/react-shadcn/pc/README.md
  4. 23
      frontend/react-shadcn/pc/eslint.config.js
  5. 17
      frontend/react-shadcn/pc/index.html
  6. 4047
      frontend/react-shadcn/pc/package-lock.json
  7. 40
      frontend/react-shadcn/pc/package.json
  8. 6
      frontend/react-shadcn/pc/postcss.config.js
  9. 1
      frontend/react-shadcn/pc/public/vite.svg
  10. 35
      frontend/react-shadcn/pc/src/App.tsx
  11. 1
      frontend/react-shadcn/pc/src/assets/react.svg
  12. 23
      frontend/react-shadcn/pc/src/components/layout/Header.tsx
  13. 26
      frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx
  14. 20
      frontend/react-shadcn/pc/src/components/layout/ProtectedRoute.tsx
  15. 82
      frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx
  16. 74
      frontend/react-shadcn/pc/src/components/ui/Button.tsx
  17. 80
      frontend/react-shadcn/pc/src/components/ui/Card.tsx
  18. 65
      frontend/react-shadcn/pc/src/components/ui/Input.tsx
  19. 83
      frontend/react-shadcn/pc/src/components/ui/Modal.tsx
  20. 53
      frontend/react-shadcn/pc/src/components/ui/Table.tsx
  21. 88
      frontend/react-shadcn/pc/src/contexts/AuthContext.tsx
  22. 183
      frontend/react-shadcn/pc/src/index.css
  23. 6
      frontend/react-shadcn/pc/src/lib/utils.ts
  24. 10
      frontend/react-shadcn/pc/src/main.tsx
  25. 165
      frontend/react-shadcn/pc/src/pages/DashboardPage.tsx
  26. 135
      frontend/react-shadcn/pc/src/pages/LoginPage.tsx
  27. 105
      frontend/react-shadcn/pc/src/pages/SettingsPage.tsx
  28. 279
      frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx
  29. 139
      frontend/react-shadcn/pc/src/services/api.ts
  30. 96
      frontend/react-shadcn/pc/src/types/index.ts
  31. 34
      frontend/react-shadcn/pc/tsconfig.app.json
  32. 7
      frontend/react-shadcn/pc/tsconfig.json
  33. 26
      frontend/react-shadcn/pc/tsconfig.node.json
  34. 13
      frontend/react-shadcn/pc/vite.config.ts

3
.claude/settings.local.json

@ -26,7 +26,8 @@
"Bash(dir:*)", "Bash(dir:*)",
"Bash(findstr:*)", "Bash(findstr:*)",
"Bash(netstat:*)", "Bash(netstat:*)",
"Bash(set CGO_ENABLED=1)" "Bash(set CGO_ENABLED=1)",
"Bash(npm install:*)"
] ]
} }
} }

24
frontend/react-shadcn/pc/.gitignore

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
frontend/react-shadcn/pc/README.md

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
frontend/react-shadcn/pc/eslint.config.js

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

17
frontend/react-shadcn/pc/index.html

@ -0,0 +1,17 @@
<!doctype html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Base Admin Panel</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-950 text-gray-100 antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4047
frontend/react-shadcn/pc/package-lock.json

File diff suppressed because it is too large

40
frontend/react-shadcn/pc/package.json

@ -0,0 +1,40 @@
{
"name": "pc",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^24.10.12",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.24",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
}

6
frontend/react-shadcn/pc/postcss.config.js

@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

1
frontend/react-shadcn/pc/public/vite.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

35
frontend/react-shadcn/pc/src/App.tsx

@ -0,0 +1,35 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import { ProtectedRoute } from './components/layout/ProtectedRoute'
import { MainLayout } from './components/layout/MainLayout'
import { LoginPage } from './pages/LoginPage'
import { DashboardPage } from './pages/DashboardPage'
import { UserManagementPage } from './pages/UserManagementPage'
import { SettingsPage } from './pages/SettingsPage'
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<MainLayout />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="users" element={<UserManagementPage />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
)
}
export default App

1
frontend/react-shadcn/pc/src/assets/react.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

23
frontend/react-shadcn/pc/src/components/layout/Header.tsx

@ -0,0 +1,23 @@
interface HeaderProps {
title: string
subtitle?: string
}
export function Header({ title, subtitle }: HeaderProps) {
return (
<header className="h-16 border-b border-gray-800 bg-gray-900/50 backdrop-blur-xl flex items-center px-6 fixed top-0 left-64 right-0 z-30">
<div className="flex-1">
<h1 className="text-xl font-bold text-white font-display">{title}</h1>
{subtitle && (
<p className="text-xs text-gray-500 font-body mt-0.5">{subtitle}</p>
)}
</div>
{/* Decorative elements */}
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-xs text-gray-500 font-body"></span>
</div>
</header>
)
}

26
frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx

@ -0,0 +1,26 @@
import { Outlet } from 'react-router-dom'
import { Sidebar } from './Sidebar'
import { Header } from './Header'
const pageTitles: Record<string, { title: string; subtitle?: string }> = {
'/dashboard': { title: '仪表盘', subtitle: '系统概览与数据统计' },
'/users': { title: '用户管理', subtitle: '管理系统用户账号' },
'/settings': { title: '系统设置', subtitle: '配置系统参数' },
}
export function MainLayout() {
const pathname = window.location.pathname
const pageInfo = pageTitles[pathname] || { title: 'BASE', subtitle: '' }
return (
<div className="min-h-screen bg-gray-950">
<Sidebar />
<div className="ml-64">
<Header title={pageInfo.title} subtitle={pageInfo.subtitle} />
<main className="p-6 pt-24">
<Outlet />
</main>
</div>
</div>
)
}

20
frontend/react-shadcn/pc/src/components/layout/ProtectedRoute.tsx

@ -0,0 +1,20 @@
import { Navigate } from 'react-router-dom'
import { useAuth } from '@/contexts/AuthContext'
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth()
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-2 border-sky-500 border-t-transparent" />
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <>{children}</>
}

82
frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx

@ -0,0 +1,82 @@
import { NavLink } from 'react-router-dom'
import { LayoutDashboard, Users, LogOut, Settings } from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
const navItems = [
{ path: '/dashboard', icon: LayoutDashboard, label: '首页' },
{ path: '/users', icon: Users, label: '用户管理' },
{ path: '/settings', icon: Settings, label: '设置' },
]
export function Sidebar() {
const { user, logout } = useAuth()
return (
<aside className="w-64 h-screen bg-gray-900/80 backdrop-blur-xl border-r border-gray-800 flex flex-col fixed left-0 top-0 z-40">
{/* Logo */}
<div className="p-6 border-b border-gray-800">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-blue-600 flex items-center justify-center glow-primary">
<span className="text-xl font-bold text-white font-display">B</span>
</div>
<div>
<h1 className="text-lg font-bold text-white font-display">BASE</h1>
<p className="text-xs text-gray-500 font-body"></p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200',
'font-body text-sm font-medium',
isActive
? 'bg-gradient-to-r from-sky-500/20 to-blue-600/20 text-sky-400 border border-sky-500/30'
: 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200'
)
}
>
<item.icon className="h-5 w-5" />
{item.label}
</NavLink>
))}
</nav>
{/* User Info */}
<div className="p-4 border-t border-gray-800">
<div className="flex items-center gap-3 p-3 rounded-lg bg-gray-800/50">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
<span className="text-lg font-bold text-white font-display">
{user?.username?.[0]?.toUpperCase() || 'U'}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white font-body truncate">
{user?.username || 'User'}
</p>
<p className="text-xs text-gray-500 font-body truncate">
{user?.email || ''}
</p>
</div>
<button
onClick={logout}
className="p-2 text-gray-500 hover:text-red-400 transition-colors"
title="退出登录"
>
<LogOut className="h-4 w-4" />
</button>
</div>
</div>
</aside>
)
}
function cn(...classes: (string | undefined | false | null)[]) {
return classes.filter(Boolean).join(' ')
}

74
frontend/react-shadcn/pc/src/components/ui/Button.tsx

@ -0,0 +1,74 @@
import { forwardRef, type ButtonHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost' | 'outline'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', isLoading, disabled, children, ...props }, ref) => {
const variants = {
primary:
'bg-gradient-to-r from-sky-500 to-blue-600 text-white hover:from-sky-400 hover:to-blue-500 glow-primary border-sky-500/50',
secondary:
'bg-gray-800/80 text-gray-200 hover:bg-gray-700/80 border-gray-700/50',
destructive:
'bg-gradient-to-r from-red-600 to-rose-600 text-white hover:from-red-500 hover:to-rose-500 border-red-500/50',
ghost: 'bg-transparent text-gray-300 hover:bg-gray-800/50 border-transparent',
outline: 'bg-transparent text-gray-200 hover:bg-gray-800/50 border-gray-700',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button
ref={ref}
className={cn(
'relative inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-950',
'disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed',
'tech-border',
variants[variant],
sizes[size],
className
)}
disabled={disabled || isLoading}
{...props}
>
{isLoading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{children}
</button>
)
}
)
Button.displayName = 'Button'
export { Button }

80
frontend/react-shadcn/pc/src/components/ui/Card.tsx

@ -0,0 +1,80 @@
import { forwardRef, type HTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border bg-gray-900/80 backdrop-blur-sm tech-border',
className
)}
{...props}
>
{children}
</div>
)
)
Card.displayName = 'Card'
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-2xl font-bold leading-none tracking-tight font-display text-white',
className
)}
{...props}
/>
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-gray-400 font-body', className)}
{...props}
/>
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
)
)
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }

65
frontend/react-shadcn/pc/src/components/ui/Input.tsx

@ -0,0 +1,65 @@
import { forwardRef, type InputHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
error?: string
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, leftIcon, rightIcon, type = 'text', id, ...props }, ref) => {
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`
return (
<div className="w-full space-y-1">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-300 font-body"
>
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
{leftIcon}
</div>
)}
<input
ref={ref}
type={type}
id={inputId}
className={cn(
'flex h-11 w-full rounded-lg border bg-gray-900/80 px-4 py-2.5 text-sm text-gray-100',
'placeholder:text-gray-500',
'transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500/50',
'disabled:cursor-not-allowed disabled:opacity-50',
'tech-border',
leftIcon && 'pl-10',
rightIcon && 'pr-10',
error && 'border-red-500/50 focus:ring-red-500/50 focus:border-red-500/50',
className
)}
{...props}
/>
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
{rightIcon}
</div>
)}
</div>
{error && (
<p className="text-xs text-red-400 font-body">{error}</p>
)}
</div>
)
}
)
Input.displayName = 'Input'
export { Input }

83
frontend/react-shadcn/pc/src/components/ui/Modal.tsx

@ -0,0 +1,83 @@
import { useEffect, type ReactNode } from 'react'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from './Button'
interface ModalProps {
isOpen: boolean
onClose: () => void
title?: string
children: ReactNode
footer?: ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl'
}
export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
if (!isOpen) return null
const sizes = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div
className={cn(
'relative w-full bg-gray-900/95 backdrop-blur-xl rounded-xl border border-gray-800 shadow-2xl tech-border animate-scale-in',
sizes[size]
)}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
{title && (
<div className="flex items-center justify-between p-6 border-b border-gray-800">
<h2 className="text-xl font-bold text-white font-display">{title}</h2>
{onClose && (
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
)}
{/* Content */}
<div className="p-6 max-h-[70vh] overflow-y-auto">
{children}
</div>
{/* Footer */}
{footer && (
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-800">
{footer}
</div>
)}
</div>
</div>
)
}

53
frontend/react-shadcn/pc/src/components/ui/Table.tsx

@ -0,0 +1,53 @@
import { cn } from '@/lib/utils'
export const Table = ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
<div className="relative w-full overflow-auto">
<table className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
)
export const TableHeader = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<thead className={cn('[&_tr]:border-b', className)} {...props} />
)
export const TableBody = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className={cn('[&_tr:last-child]:border-0', className)} {...props} />
)
export const TableFooter = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tfoot
className={cn('border-t bg-gray-900/50 font-medium text-gray-400 font-body', className)}
{...props}
/>
)
export const TableRow = ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr
className={cn(
'border-b border-gray-800 transition-colors hover:bg-gray-800/30 data-[state=selected]:bg-gray-800/50',
className
)}
{...props}
/>
)
export const TableHead = ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<th
className={cn(
'h-12 px-4 text-left align-middle font-medium text-gray-400 font-display uppercase tracking-wider text-xs [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
)
export const TableCell = ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<td
className={cn('p-4 align-middle text-gray-300 font-body', className)}
{...props}
/>
)
export const TableCaption = ({ className, ...props }: React.HTMLAttributes<HTMLTableCaptionElement>) => (
<caption className={cn('mt-4 text-sm text-gray-500 font-body', className)} {...props} />
)

88
frontend/react-shadcn/pc/src/contexts/AuthContext.tsx

@ -0,0 +1,88 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
import type { User } from '@/types'
import { apiClient } from '@/services/api'
interface AuthContextType {
user: User | null
token: string | null
isAuthenticated: boolean
isLoading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [token, setToken] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const storedToken = localStorage.getItem('token')
const storedUser = localStorage.getItem('user')
if (storedToken) {
setToken(storedToken)
}
if (storedUser) {
try {
setUser(JSON.parse(storedUser))
} catch {
localStorage.removeItem('user')
}
}
setIsLoading(false)
}, [])
const login = async (email: string, password: string) => {
try {
const response = await apiClient.login({ email, password })
if (response.success && response.token) {
setToken(response.token)
// Mock user data - in real app, fetch from API
const userData: User = {
id: 1,
username: email.split('@')[0],
email,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setUser(userData)
localStorage.setItem('user', JSON.stringify(userData))
}
} catch (error) {
throw error
}
}
const logout = () => {
apiClient.logout()
setToken(null)
setUser(null)
localStorage.removeItem('user')
}
const value: AuthContextType = {
user,
token,
isAuthenticated: !!token,
isLoading,
login,
logout,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}

183
frontend/react-shadcn/pc/src/index.css

@ -0,0 +1,183 @@
@import "tailwindcss";
@theme {
--color-background: oklch(0.15 0 0);
--color-foreground: oklch(0.98 0.005 240);
--color-card: oklch(0.15 0 0);
--color-card-foreground: oklch(0.98 0.005 240);
--color-popover: oklch(0.15 0 0);
--color-popover-foreground: oklch(0.98 0.005 240);
--color-primary: oklch(0.65 0.2 240);
--color-primary-foreground: oklch(0.98 0.005 240);
--color-secondary: oklch(0.20 0.03 240);
--color-secondary-foreground: oklch(0.98 0.005 240);
--color-muted: oklch(0.20 0.03 240);
--color-muted-foreground: oklch(0.60 0.02 240);
--color-accent: oklch(0.20 0.03 240);
--color-accent-foreground: oklch(0.98 0.005 240);
--color-destructive: oklch(0.55 0.22 25);
--color-destructive-foreground: oklch(0.98 0.005 240);
--color-border: oklch(0.22 0.03 240);
--color-input: oklch(0.22 0.03 240);
--color-ring: oklch(0.65 0.2 240);
--radius: 0.5rem;
--font-display: 'Oswald', sans-serif;
--font-body: 'Space Grotesk', sans-serif;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-slide-in-left: slide-in-left 0.4s ease-out;
--animate-slide-in-right: slide-in-right 0.4s ease-out;
--animate-fade-in: fade-in 0.3s ease-out;
--animate-scale-in: scale-in 0.2s ease-out;
@keyframes accordion-down {
from { height: 0; }
to { height: var(--radix-accordion-content-height); }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); }
to { height: 0; }
}
@keyframes slide-in-left {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scale-in {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
}
body {
background-color: oklch(0.10 0.01 240);
color: oklch(0.98 0.005 240);
font-family: var(--font-body);
background-image:
radial-gradient(circle at 15% 50%, rgba(30, 58, 138, 0.15) 0%, transparent 25%),
radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.1) 0%, transparent 25%),
linear-gradient(rgba(15, 23, 42, 0.95) 1px, transparent 1px),
linear-gradient(90deg, rgba(15, 23, 42, 0.95) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 40px 40px, 40px 40px;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background-color: oklch(0.15 0.01 240);
}
::-webkit-scrollbar-thumb {
background-color: oklch(0.30 0.02 240);
border-radius: 0.125rem;
}
::-webkit-scrollbar-thumb:hover {
background-color: oklch(0.35 0.02 240);
}
/* Typography utilities */
.font-display {
font-family: var(--font-display);
}
.font-body {
font-family: var(--font-body);
}
/* Industrial grid pattern */
.grid-pattern {
background-image:
linear-gradient(rgba(56, 189, 248, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.03) 1px, transparent 1px);
background-size: 60px 60px;
}
/* Glowing effects */
.glow-primary {
box-shadow: 0 0 20px rgba(56, 189, 248, 0.3), 0 0 40px rgba(56, 189, 248, 0.1);
}
.glow-accent {
box-shadow: 0 0 20px rgba(251, 191, 36, 0.3), 0 0 40px rgba(251, 191, 36, 0.1);
}
/* Diagonal stripe pattern */
.stripe-pattern {
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(56, 189, 248, 0.02) 10px,
rgba(56, 189, 248, 0.02) 20px
);
}
/* Tech border effect */
.tech-border {
position: relative;
}
.tech-border::before {
content: '';
position: absolute;
inset: 0;
border: 1px solid transparent;
background: linear-gradient(135deg, rgba(56, 189, 248, 0.5), rgba(168, 85, 247, 0.5), rgba(56, 189, 248, 0.5)) border-box;
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* Scanline effect */
.scanline {
position: relative;
overflow: hidden;
}
.scanline::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 50%,
rgba(0, 0, 0, 0.02) 51%
);
background-size: 100% 4px;
pointer-events: none;
}
/* Noise texture overlay */
.noise-overlay {
position: relative;
}
.noise-overlay::before {
content: '';
position: absolute;
inset: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
pointer-events: none;
}

6
frontend/react-shadcn/pc/src/lib/utils.ts

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

10
frontend/react-shadcn/pc/src/main.tsx

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

165
frontend/react-shadcn/pc/src/pages/DashboardPage.tsx

@ -0,0 +1,165 @@
import { Users, Zap, Activity, Database } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
const stats = [
{
title: '总用户数',
value: '1,234',
change: '+12%',
icon: Users,
color: 'from-sky-500 to-blue-600',
},
{
title: '活跃用户',
value: '856',
change: '+8%',
icon: Activity,
color: 'from-green-500 to-emerald-600',
},
{
title: '系统负载',
value: '32%',
change: '-5%',
icon: Zap,
color: 'from-amber-500 to-orange-600',
},
{
title: '数据库状态',
value: '正常',
change: '稳定',
icon: Database,
color: 'from-purple-500 to-violet-600',
},
]
const recentActivity = [
{ id: 1, user: 'john@example.com', action: '登录系统', time: '5 分钟前', status: 'success' },
{ id: 2, user: 'jane@example.com', action: '更新资料', time: '15 分钟前', status: 'success' },
{ id: 3, user: 'admin@example.com', action: '创建用户', time: '1 小时前', status: 'success' },
{ id: 4, user: 'bob@example.com', action: '修改密码', time: '2 小时前', status: 'success' },
{ id: 5, user: 'alice@example.com', action: '登录失败', time: '3 小时前', status: 'error' },
]
export function DashboardPage() {
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 animate-fade-in" style={{ animationDelay: '0.1s' }}>
{stats.map((stat, index) => (
<Card key={index} className="overflow-hidden hover:border-gray-700 transition-colors">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-400 font-body">{stat.title}</p>
<p className="text-3xl font-bold text-white font-display">{stat.value}</p>
<p className="text-xs text-green-400 font-body">{stat.change}</p>
</div>
<div className={`p-3 rounded-lg bg-gradient-to-br ${stat.color}`}>
<stat.icon className="h-5 w-5 text-white" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Chart Placeholder */}
<Card className="lg:col-span-2 animate-fade-in" style={{ animationDelay: '0.2s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="h-64 flex items-end justify-between gap-2 px-4">
{[65, 72, 68, 80, 75, 85, 82, 90, 88, 95, 92, 100].map((height, i) => (
<div
key={i}
className="flex-1 max-w-8 bg-gradient-to-t from-sky-600 to-sky-400 rounded-t-sm transition-all duration-300 hover:from-sky-500 hover:to-sky-300 relative group"
style={{ height: `${height}%` }}
>
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-800 text-xs text-white px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{height}%
</div>
</div>
))}
</div>
<div className="flex justify-between mt-4 px-4 text-xs text-gray-500 font-body">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
<span>10</span>
<span>11</span>
<span>12</span>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card className="animate-fade-in" style={{ animationDelay: '0.3s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentActivity.map((activity) => (
<div
key={activity.id}
className="flex items-start gap-3 p-3 rounded-lg bg-gray-800/50 hover:bg-gray-800 transition-colors"
>
<div
className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${
activity.status === 'success' ? 'bg-green-500' : 'bg-red-500'
}`}
/>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-300 font-body truncate">
{activity.user}
</p>
<p className="text-xs text-gray-500 font-body">{activity.action}</p>
</div>
<span className="text-xs text-gray-600 font-body whitespace-nowrap">
{activity.time}
</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<Card className="animate-fade-in" style={{ animationDelay: '0.4s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: '添加用户', icon: Users, action: 'users' },
{ label: '系统设置', icon: Zap, action: 'settings' },
{ label: '数据备份', icon: Database, action: 'backup' },
{ label: '查看日志', icon: Activity, action: 'logs' },
].map((item, index) => (
<button
key={index}
className="flex flex-col items-center gap-2 p-4 rounded-lg border border-gray-800 hover:border-sky-500/50 hover:bg-gray-800/50 transition-all duration-200 group"
>
<item.icon className="h-6 w-6 text-gray-500 group-hover:text-sky-400 transition-colors" />
<span className="text-sm text-gray-400 group-hover:text-gray-200 font-body transition-colors">
{item.label}
</span>
</button>
))}
</div>
</CardContent>
</Card>
</div>
)
}

135
frontend/react-shadcn/pc/src/pages/LoginPage.tsx

@ -0,0 +1,135 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Mail, Lock, AlertCircle, Zap } from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
import { Input } from '@/components/ui/Input'
import { Button } from '@/components/ui/Button'
import { Card } from '@/components/ui/Card'
export function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const { login } = useAuth()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setIsLoading(true)
try {
await login(email, password)
navigate('/dashboard')
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败')
} finally {
setIsLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center relative overflow-hidden grid-pattern">
{/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-sky-500/10 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }} />
</div>
{/* Decorative grid lines */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-sky-500/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-purple-500/30 to-transparent" />
<div className="absolute top-0 bottom-0 left-0 w-px bg-gradient-to-b from-transparent via-sky-500/30 to-transparent" />
<div className="absolute top-0 bottom-0 right-0 w-px bg-gradient-to-b from-transparent via-purple-500/30 to-transparent" />
</div>
{/* Main content */}
<div className="relative z-10 w-full max-w-md px-4 animate-scale-in">
{/* Logo and title */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 mb-6 rounded-2xl bg-gradient-to-br from-sky-500 to-blue-600 glow-primary relative overflow-hidden">
<div className="absolute inset-0 stripe-pattern" />
<Zap className="h-10 w-10 text-white relative z-10" />
</div>
<h1 className="text-4xl font-bold text-white font-display tracking-wide mb-2">
BASE<span className="text-sky-400">.</span>
</h1>
<p className="text-gray-500 font-body"></p>
</div>
{/* Login form */}
<Card className="backdrop-blur-xl bg-gray-900/90 border-gray-800">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<Input
type="email"
label="邮箱地址"
placeholder="user@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
leftIcon={<Mail className="h-4 w-4" />}
disabled={isLoading}
/>
<Input
type="password"
label="密码"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
leftIcon={<Lock className="h-4 w-4" />}
disabled={isLoading}
/>
</div>
{error && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm font-body">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isLoading}
>
</Button>
<div className="text-center">
<p className="text-sm text-gray-500 font-body">
{' '}
<button
type="button"
className="text-sky-400 hover:text-sky-300 transition-colors font-medium"
>
</button>
</p>
</div>
</form>
</Card>
{/* Footer info */}
<div className="mt-8 text-center">
<p className="text-xs text-gray-600 font-body">
© 2026 Base System. All rights reserved.
</p>
</div>
</div>
{/* Corner decorations */}
<div className="absolute top-0 left-0 w-24 h-24 border-l-2 border-t-2 border-sky-500/30 rounded-tl-3xl" />
<div className="absolute top-0 right-0 w-24 h-24 border-r-2 border-t-2 border-purple-500/30 rounded-tr-3xl" />
<div className="absolute bottom-0 left-0 w-24 h-24 border-l-2 border-b-2 border-purple-500/30 rounded-bl-3xl" />
<div className="absolute bottom-0 right-0 w-24 h-24 border-r-2 border-b-2 border-sky-500/30 rounded-br-3xl" />
</div>
)
}

105
frontend/react-shadcn/pc/src/pages/SettingsPage.tsx

@ -0,0 +1,105 @@
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { Input } from '@/components/ui/Input'
import { Button } from '@/components/ui/Button'
import { Settings, Save, Bell, Lock, Palette } from 'lucide-react'
export function SettingsPage() {
return (
<div className="space-y-6 animate-fade-in max-w-2xl">
{/* Profile Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5 text-sky-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input label="用户名" placeholder="请输入用户名" />
<Input label="邮箱" type="email" placeholder="请输入邮箱" />
<Input label="手机号" placeholder="请输入手机号" />
<div className="pt-4">
<Button variant="primary">
<Save className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Notification Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5 text-sky-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
<div>
<p className="text-sm font-medium text-white"></p>
<p className="text-xs text-gray-500"></p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
</label>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
<div>
<p className="text-sm font-medium text-white"></p>
<p className="text-xs text-gray-500"></p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
</label>
</div>
</CardContent>
</Card>
{/* Security Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lock className="h-5 w-5 text-sky-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input label="当前密码" type="password" placeholder="请输入当前密码" />
<Input label="新密码" type="password" placeholder="请输入新密码" />
<Input label="确认密码" type="password" placeholder="请确认新密码" />
<div className="pt-4">
<Button variant="primary">
</Button>
</div>
</CardContent>
</Card>
{/* Theme Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5 text-sky-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
<div>
<p className="text-sm font-medium text-white"></p>
<p className="text-xs text-gray-500">使</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
</label>
</div>
</CardContent>
</Card>
</div>
)
}

279
frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx

@ -0,0 +1,279 @@
import { useState, useEffect } from 'react'
import { Plus, Search, Edit2, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { Modal } from '@/components/ui/Modal'
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '@/components/ui/Table'
import type { User, CreateUserRequest, UpdateUserRequest } from '@/types'
import { apiClient } from '@/services/api'
export function UserManagementPage() {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [isModalOpen, setIsModalOpen] = useState(false)
const [editingUser, setEditingUser] = useState<User | null>(null)
const [formData, setFormData] = useState<Partial<CreateUserRequest> & { password?: string }>({
username: '',
email: '',
password: '',
phone: '',
})
useEffect(() => {
fetchUsers()
}, [])
const fetchUsers = async () => {
try {
setIsLoading(true)
const response = await apiClient.getUsers({ page: 1, pageSize: 100 })
if (response.success && response.data) {
setUsers(response.data.users)
}
} catch (error) {
console.error('Failed to fetch users:', error)
// Use mock data if API fails
setUsers([
{ id: 1, username: 'admin', email: 'admin@example.com', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
{ id: 2, username: 'user1', email: 'user1@example.com', phone: '13800138000', createdAt: '2024-01-02', updatedAt: '2024-01-02' },
{ id: 3, username: 'user2', email: 'user2@example.com', phone: '13900139000', createdAt: '2024-01-03', updatedAt: '2024-01-03' },
])
} finally {
setIsLoading(false)
}
}
const handleCreateUser = async () => {
try {
await apiClient.createUser(formData as CreateUserRequest)
await fetchUsers()
setIsModalOpen(false)
resetForm()
} catch (error) {
console.error('Failed to create user:', error)
alert('创建用户失败')
}
}
const handleUpdateUser = async () => {
if (!editingUser) return
try {
const { password, ...data } = formData as UpdateUserRequest & { password?: string }
await apiClient.updateUser(editingUser.id, data)
await fetchUsers()
setIsModalOpen(false)
resetForm()
} catch (error) {
console.error('Failed to update user:', error)
alert('更新用户失败')
}
}
const handleDeleteUser = async (id: number) => {
if (!confirm('确定要删除该用户吗?')) return
try {
await apiClient.deleteUser(id)
await fetchUsers()
} catch (error) {
console.error('Failed to delete user:', error)
alert('删除用户失败')
}
}
const openModal = (user?: User) => {
if (user) {
setEditingUser(user)
setFormData({
username: user.username,
email: user.email,
phone: user.phone,
})
} else {
setEditingUser(null)
setFormData({
username: '',
email: '',
password: '',
phone: '',
})
}
setIsModalOpen(true)
}
const resetForm = () => {
setFormData({
username: '',
email: '',
password: '',
phone: '',
})
setEditingUser(null)
}
const filteredUsers = users.filter(
(user) =>
user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<div className="space-y-6 animate-fade-in">
{/* Header */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1 w-full sm:max-w-md">
<Input
placeholder="搜索用户..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
leftIcon={<Search className="h-4 w-4" />}
/>
</div>
<Button
onClick={() => openModal()}
className="whitespace-nowrap"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Users Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredUsers.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell>...</TableCell>
</TableRow>
) : filteredUsers.length === 0 ? (
<TableRow>
<TableCell></TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell className="text-gray-400">#{user.id}</TableCell>
<TableCell className="font-medium text-white">{user.username}</TableCell>
<TableCell className="text-gray-400">{user.email}</TableCell>
<TableCell className="text-gray-400">{user.phone || '-'}</TableCell>
<TableCell className="text-gray-400">
{new Date(user.createdAt).toLocaleDateString('zh-CN')}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => openModal(user)}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUser(user.id)}
className="text-red-400 hover:text-red-300"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Modal */}
<Modal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
resetForm()
}}
title={editingUser ? '编辑用户' : '添加用户'}
size="md"
footer={
<>
<Button
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
>
</Button>
<Button
onClick={editingUser ? handleUpdateUser : handleCreateUser}
>
{editingUser ? '保存' : '创建'}
</Button>
</>
}
>
<div className="space-y-4">
<Input
label="用户名"
placeholder="请输入用户名"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
/>
<Input
label="邮箱"
type="email"
placeholder="请输入邮箱"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
{!editingUser && (
<Input
label="密码"
type="password"
placeholder="请输入密码"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
)}
<Input
label="手机号"
placeholder="请输入手机号"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
</Modal>
</div>
)
}

139
frontend/react-shadcn/pc/src/services/api.ts

@ -0,0 +1,139 @@
import type {
ApiResponse,
LoginResponse,
LoginRequest,
User,
UserListRequest,
UserListResponse,
CreateUserRequest,
UpdateUserRequest,
} from '@/types'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888/api/v1'
class ApiClient {
private get token(): string | null {
return localStorage.getItem('token')
}
private set token(value: string | null) {
if (value) {
localStorage.setItem('token', value)
} else {
localStorage.removeItem('token')
}
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
// Merge additional headers
if (options.headers) {
Object.assign(headers, options.headers)
}
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`
}
try {
const response = await fetch(url, {
...options,
headers,
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Request failed')
}
return data
} catch (error) {
console.error('API request error:', error)
throw error
}
}
// Auth
async login(credentials: LoginRequest): Promise<LoginResponse> {
const response = await this.request<LoginResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
})
if (response.success && response.token) {
this.token = response.token
}
return response
}
async logout(): Promise<void> {
this.token = null
}
async refreshToken(refreshToken: string): Promise<LoginResponse> {
return this.request<LoginResponse>('/auth/refresh', {
method: 'POST',
body: JSON.stringify({ token: refreshToken }),
})
}
// User Management
async getUsers(params: UserListRequest = {}): Promise<UserListResponse> {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page.toString())
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
if (params.keyword) queryParams.append('keyword', params.keyword)
return this.request<UserListResponse>(`/user/users?${queryParams}`)
}
async getUser(id: number): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>(`/user/user/${id}`)
}
async createUser(data: CreateUserRequest): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>('/user/user', {
method: 'POST',
body: JSON.stringify(data),
})
}
async updateUser(id: number, data: UpdateUserRequest): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>(`/user/user/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
async deleteUser(id: number): Promise<ApiResponse<void>> {
return this.request<ApiResponse<void>>(`/user/user/${id}`, {
method: 'DELETE',
})
}
// Profile
async getProfile(): Promise<ApiResponse> {
return this.request<ApiResponse>('/profile/me')
}
// Health check
async healthCheck(): Promise<{ status: string }> {
try {
const response = await fetch(`${API_BASE_URL}/health`)
return response.json()
} catch {
return { status: 'error' }
}
}
}
export const apiClient = new ApiClient()

96
frontend/react-shadcn/pc/src/types/index.ts

@ -0,0 +1,96 @@
// API Response Types
export interface ApiResponse<T = any> {
code: number
message: string
success: boolean
data?: T
}
export interface LoginResponse {
code: number
message: string
success: boolean
token: string
}
// User Types
export interface User {
id: number
username: string
email: string
phone?: string
avatar?: string
createdAt: string
updatedAt: string
}
export interface UserInfo extends User {}
// Request Types
export interface LoginRequest {
email: string
password: string
}
export interface RegisterRequest {
username: string
email: string
password: string
phone?: string
}
export interface CreateUserRequest {
username: string
email: string
password: string
phone?: string
}
export interface UpdateUserRequest {
username?: string
email?: string
phone?: string
avatar?: string
}
export interface UserListRequest {
page?: number
pageSize?: number
keyword?: string
}
export interface UserListResponse {
code: number
message: string
success: boolean
data: {
users: User[]
total: number
page: number
pageSize: number
}
}
// Profile Types
export interface Profile {
id: number
userId: number
username: string
email: string
phone?: string
bio?: string
avatar?: string
createdAt: string
updatedAt: string
}
export interface UpdateProfileRequest {
bio?: string
avatar?: string
phone?: string
}
export interface ChangePasswordRequest {
oldPassword: string
newPassword: string
}

34
frontend/react-shadcn/pc/tsconfig.app.json

@ -0,0 +1,34 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Path Alias */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

7
frontend/react-shadcn/pc/tsconfig.json

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
frontend/react-shadcn/pc/tsconfig.node.json

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

13
frontend/react-shadcn/pc/vite.config.ts

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Loading…
Cancel
Save