feat: initial frontend - React + TypeScript + Vite
- Login page with JWT auth - Dashboard with stats and charts - Issues list with pagination, filtering - Issue detail with comments, status transitions - Create issue form - Dark theme UI - Docker (nginx) with API proxy to backend - Sidebar navigation
This commit is contained in:
75
src/pages/DashboardPage.tsx
Normal file
75
src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import api from '@/services/api'
|
||||
import type { DashboardStats } from '@/types'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.get<DashboardStats>('/dashboard/stats').then(({ data }) => setStats(data))
|
||||
}, [])
|
||||
|
||||
if (!stats) return <div className="loading">加载中...</div>
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
|
||||
closed: '#6b7280', blocked: '#ef4444',
|
||||
}
|
||||
const priorityColors: Record<string, string> = {
|
||||
low: '#6b7280', medium: '#3b82f6', high: '#f59e0b', critical: '#ef4444',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<h2>📊 仪表盘</h2>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card total">
|
||||
<span className="stat-number">{stats.total_issues}</span>
|
||||
<span className="stat-label">总 Issues</span>
|
||||
</div>
|
||||
{Object.entries(stats.by_status).map(([k, v]) => (
|
||||
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
||||
<span className="stat-number">{v}</span>
|
||||
<span className="stat-label">{k}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3>按优先级</h3>
|
||||
<div className="bar-chart">
|
||||
{Object.entries(stats.by_priority).map(([k, v]) => (
|
||||
<div className="bar-row" key={k}>
|
||||
<span className="bar-label">{k}</span>
|
||||
<div className="bar" style={{
|
||||
width: `${Math.max((v / stats.total_issues) * 100, 5)}%`,
|
||||
backgroundColor: priorityColors[k] || '#ccc',
|
||||
}}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3>最近 Issues</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>ID</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.recent_issues.map((i) => (
|
||||
<tr key={i.id}>
|
||||
<td>#{i.id}</td>
|
||||
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
||||
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
|
||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
||||
<td>{i.issue_type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user