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:
97
src/pages/IssueDetailPage.tsx
Normal file
97
src/pages/IssueDetailPage.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import api from '@/services/api'
|
||||
import type { Issue, Comment } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export default function IssueDetailPage() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [issue, setIssue] = useState<Issue | null>(null)
|
||||
const [comments, setComments] = useState<Comment[]>([])
|
||||
const [newComment, setNewComment] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Issue>(`/issues/${id}`).then(({ data }) => setIssue(data))
|
||||
api.get<Comment[]>(`/issues/${id}/comments`).then(({ data }) => setComments(data))
|
||||
}, [id])
|
||||
|
||||
const addComment = async () => {
|
||||
if (!newComment.trim() || !issue) return
|
||||
await api.post('/comments', { content: newComment, issue_id: issue.id, author_id: 1 })
|
||||
setNewComment('')
|
||||
const { data } = await api.get<Comment[]>(`/issues/${id}/comments`)
|
||||
setComments(data)
|
||||
}
|
||||
|
||||
const transition = async (newStatus: string) => {
|
||||
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
|
||||
const { data } = await api.get<Issue>(`/issues/${id}`)
|
||||
setIssue(data)
|
||||
}
|
||||
|
||||
if (!issue) return <div className="loading">加载中...</div>
|
||||
|
||||
const statusActions: Record<string, string[]> = {
|
||||
open: ['in_progress', 'blocked'],
|
||||
in_progress: ['resolved', 'blocked'],
|
||||
blocked: ['open', 'in_progress'],
|
||||
resolved: ['closed', 'open'],
|
||||
closed: ['open'],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="issue-detail">
|
||||
<button className="btn-back" onClick={() => navigate('/issues')}>← 返回</button>
|
||||
|
||||
<div className="issue-header">
|
||||
<h2>#{issue.id} {issue.title}</h2>
|
||||
<div className="issue-meta">
|
||||
<span className={`badge status-${issue.status}`}>{issue.status}</span>
|
||||
<span className={`badge priority-${issue.priority}`}>{issue.priority}</span>
|
||||
<span className="badge">{issue.issue_type}</span>
|
||||
{issue.tags && <span className="tags">{issue.tags}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="issue-body">
|
||||
<div className="section">
|
||||
<h3>描述</h3>
|
||||
<p>{issue.description || '暂无描述'}</p>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3>详情</h3>
|
||||
<dl>
|
||||
<dt>创建时间</dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
|
||||
{issue.due_date && <><dt>截止日期</dt><dd>{dayjs(issue.due_date).format('YYYY-MM-DD')}</dd></>}
|
||||
{issue.updated_at && <><dt>更新时间</dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3>状态变更</h3>
|
||||
<div className="actions">
|
||||
{(statusActions[issue.status] || []).map((s) => (
|
||||
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3>评论 ({comments.length})</h3>
|
||||
{comments.map((c) => (
|
||||
<div className="comment" key={c.id}>
|
||||
<div className="comment-meta">用户 #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
|
||||
<p>{c.content}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="comment-form">
|
||||
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="添加评论..." />
|
||||
<button onClick={addComment} disabled={!newComment.trim()}>提交评论</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user