- 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
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import api from '@/services/api'
|
|
import type { Issue, PaginatedResponse } from '@/types'
|
|
|
|
export default function IssuesPage() {
|
|
const [issues, setIssues] = useState<Issue[]>([])
|
|
const [total, setTotal] = useState(0)
|
|
const [page, setPage] = useState(1)
|
|
const [totalPages, setTotalPages] = useState(1)
|
|
const [statusFilter, setStatusFilter] = useState('')
|
|
const [priorityFilter, setPriorityFilter] = useState('')
|
|
const navigate = useNavigate()
|
|
|
|
const fetchIssues = () => {
|
|
const params = new URLSearchParams({ page: String(page), page_size: '20' })
|
|
if (statusFilter) params.set('issue_status', statusFilter)
|
|
api.get<PaginatedResponse<Issue>>(`/issues?${params}`).then(({ data }) => {
|
|
setIssues(data.items)
|
|
setTotal(data.total)
|
|
setTotalPages(data.total_pages)
|
|
})
|
|
}
|
|
|
|
useEffect(() => { fetchIssues() }, [page, statusFilter, priorityFilter])
|
|
|
|
const statusColors: Record<string, string> = {
|
|
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
|
|
closed: '#6b7280', blocked: '#ef4444',
|
|
}
|
|
|
|
return (
|
|
<div className="issues-page">
|
|
<div className="page-header">
|
|
<h2>📋 Issues ({total})</h2>
|
|
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ 新建 Issue</button>
|
|
</div>
|
|
|
|
<div className="filters">
|
|
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}>
|
|
<option value="">全部状态</option>
|
|
<option value="open">Open</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="resolved">Resolved</option>
|
|
<option value="closed">Closed</option>
|
|
<option value="blocked">Blocked</option>
|
|
</select>
|
|
</div>
|
|
|
|
<table className="issues-table">
|
|
<thead>
|
|
<tr><th>#</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th><th>标签</th><th>创建时间</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{issues.map((i) => (
|
|
<tr key={i.id} onClick={() => navigate(`/issues/${i.id}`)} className="clickable">
|
|
<td>{i.id}</td>
|
|
<td className="issue-title">{i.title}</td>
|
|
<td><span className="badge" style={{ backgroundColor: statusColors[i.status] || '#ccc' }}>{i.status}</span></td>
|
|
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
|
<td>{i.issue_type}</td>
|
|
<td>{i.tags || '-'}</td>
|
|
<td>{new Date(i.created_at).toLocaleDateString()}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="pagination">
|
|
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>上一页</button>
|
|
<span>{page} / {totalPages}</span>
|
|
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}>下一页</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|