i18n: translate frontend UI strings to English
This commit is contained in:
@@ -50,14 +50,14 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appState === 'checking') {
|
if (appState === 'checking') {
|
||||||
return <div className="loading">检查配置状态...</div>
|
return <div className="loading">Checking configuration status...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState === 'setup') {
|
if (appState === 'setup') {
|
||||||
return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} />
|
return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="loading">加载中...</div>
|
if (loading) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export default function Sidebar({ user, onLogout }: Props) {
|
|||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
const links = user ? [
|
const links = user ? [
|
||||||
{ to: '/', icon: '📊', label: '仪表盘' },
|
{ to: '/', icon: '📊', label: 'Dashboard' },
|
||||||
{ to: '/issues', icon: '📋', label: 'Issues' },
|
{ to: '/issues', icon: '📋', label: 'Issues' },
|
||||||
{ to: '/projects', icon: '📁', label: '项目' },
|
{ to: '/projects', icon: '📁', label: 'Projects' },
|
||||||
{ to: '/milestones', icon: '🏁', label: '里程碑' },
|
{ to: '/milestones', icon: '🏁', label: 'Milestones' },
|
||||||
{ to: '/notifications', icon: '🔔', label: '通知' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
|
{ to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
|
||||||
{ to: '/monitor', icon: '📡', label: 'Monitor' },
|
{ to: '/monitor', icon: '📡', label: 'Monitor' },
|
||||||
] : [
|
] : [
|
||||||
{ to: '/monitor', icon: '📡', label: 'Monitor' },
|
{ to: '/monitor', icon: '📡', label: 'Monitor' },
|
||||||
@@ -54,7 +54,7 @@ export default function Sidebar({ user, onLogout }: Props) {
|
|||||||
{user && (
|
{user && (
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
<span>👤 {user.username}</span>
|
<span>👤 {user.username}</span>
|
||||||
<button onClick={onLogout}>退出</button>
|
<button onClick={onLogout}>Logout</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ export default function CreateIssuePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-issue">
|
<div className="create-issue">
|
||||||
<h2>新建 Issue</h2>
|
<h2>Create Issue</h2>
|
||||||
<form onSubmit={submit}>
|
<form onSubmit={submit}>
|
||||||
<label>标题 <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
|
<label>Title <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
|
||||||
<label>描述 <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
<label>Description <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
||||||
<label>项目
|
<label>Projects
|
||||||
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
|
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
|
||||||
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>类型
|
<label>Type
|
||||||
<select value={form.issue_type} onChange={(e) => setForm({ ...form, issue_type: e.target.value })}>
|
<select value={form.issue_type} onChange={(e) => setForm({ ...form, issue_type: e.target.value })}>
|
||||||
<option value="task">Task</option>
|
<option value="task">Task</option>
|
||||||
<option value="bug">Bug</option>
|
<option value="bug">Bug</option>
|
||||||
@@ -44,7 +44,7 @@ export default function CreateIssuePage() {
|
|||||||
<option value="resolution">Resolution</option>
|
<option value="resolution">Resolution</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>优先级
|
<label>Priority
|
||||||
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: e.target.value })}>
|
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: e.target.value })}>
|
||||||
<option value="low">Low</option>
|
<option value="low">Low</option>
|
||||||
<option value="medium">Medium</option>
|
<option value="medium">Medium</option>
|
||||||
@@ -52,8 +52,8 @@ export default function CreateIssuePage() {
|
|||||||
<option value="critical">Critical</option>
|
<option value="critical">Critical</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>标签 <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="逗号分隔" /></label>
|
<label>Tags <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="Comma separated" /></label>
|
||||||
<button type="submit" className="btn-primary">创建</button>
|
<button type="submit" className="btn-primary">Create</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function DashboardPage() {
|
|||||||
api.get<DashboardStats>('/dashboard/stats').then(({ data }) => setStats(data))
|
api.get<DashboardStats>('/dashboard/stats').then(({ data }) => setStats(data))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!stats) return <div className="loading">加载中...</div>
|
if (!stats) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
|
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
|
||||||
@@ -21,12 +21,12 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<h2>📊 仪表盘</h2>
|
<h2>📊 Dashboard</h2>
|
||||||
|
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card total">
|
<div className="stat-card total">
|
||||||
<span className="stat-number">{stats.total_issues}</span>
|
<span className="stat-number">{stats.total_issues}</span>
|
||||||
<span className="stat-label">总 Issues</span>
|
<span className="stat-label">Total Issues</span>
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(stats.by_status || {}).map(([k, v]) => (
|
{Object.entries(stats.by_status || {}).map(([k, v]) => (
|
||||||
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
||||||
@@ -37,7 +37,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>按优先级</h3>
|
<h3>By Priority</h3>
|
||||||
<div className="bar-chart">
|
<div className="bar-chart">
|
||||||
{Object.entries(stats.by_priority || {}).map(([k, v]) => (
|
{Object.entries(stats.by_priority || {}).map(([k, v]) => (
|
||||||
<div className="bar-row" key={k}>
|
<div className="bar-row" key={k}>
|
||||||
@@ -52,10 +52,10 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>最近 Issues</h3>
|
<h3>Recent Issues</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>ID</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th></tr>
|
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(stats.recent_issues || []).map((i) => (
|
{(stats.recent_issues || []).map((i) => (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function IssueDetailPage() {
|
|||||||
setIssue(data)
|
setIssue(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!issue) return <div className="loading">加载中...</div>
|
if (!issue) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
const statusActions: Record<string, string[]> = {
|
const statusActions: Record<string, string[]> = {
|
||||||
open: ['in_progress', 'blocked'],
|
open: ['in_progress', 'blocked'],
|
||||||
@@ -42,7 +42,7 @@ export default function IssueDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="issue-detail">
|
<div className="issue-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/issues')}>← 返回</button>
|
<button className="btn-back" onClick={() => navigate('/issues')}>← Back</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="issue-header">
|
||||||
<h2>#{issue.id} {issue.title}</h2>
|
<h2>#{issue.id} {issue.title}</h2>
|
||||||
@@ -56,21 +56,21 @@ export default function IssueDetailPage() {
|
|||||||
|
|
||||||
<div className="issue-body">
|
<div className="issue-body">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>描述</h3>
|
<h3>Description</h3>
|
||||||
<p>{issue.description || '暂无描述'}</p>
|
<p>{issue.description || 'No description'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>详情</h3>
|
<h3>Details</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>创建时间</dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
|
<dt>Created</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.due_date && <><dt>Due date</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></>}
|
{issue.updated_at && <><dt>Updated</dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>状态变更</h3>
|
<h3>Status changes</h3>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{(statusActions[issue.status] || []).map((s) => (
|
{(statusActions[issue.status] || []).map((s) => (
|
||||||
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
||||||
@@ -79,16 +79,16 @@ export default function IssueDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>评论 ({comments.length})</h3>
|
<h3>Comments ({comments.length})</h3>
|
||||||
{comments.map((c) => (
|
{comments.map((c) => (
|
||||||
<div className="comment" key={c.id}>
|
<div className="comment" key={c.id}>
|
||||||
<div className="comment-meta">用户 #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
|
<div className="comment-meta">User #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
|
||||||
<p>{c.content}</p>
|
<p>{c.content}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="comment-form">
|
<div className="comment-form">
|
||||||
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="添加评论..." />
|
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Add a comment..." />
|
||||||
<button onClick={addComment} disabled={!newComment.trim()}>提交评论</button>
|
<button onClick={addComment} disabled={!newComment.trim()}>Submit comment</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ export default function IssuesPage() {
|
|||||||
<div className="issues-page">
|
<div className="issues-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>📋 Issues ({total})</h2>
|
<h2>📋 Issues ({total})</h2>
|
||||||
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ 新建 Issue</button>
|
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ Create Issue</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filters">
|
<div className="filters">
|
||||||
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}>
|
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}>
|
||||||
<option value="">全部状态</option>
|
<option value="">All statuses</option>
|
||||||
<option value="open">Open</option>
|
<option value="open">Open</option>
|
||||||
<option value="in_progress">In Progress</option>
|
<option value="in_progress">In Progress</option>
|
||||||
<option value="resolved">Resolved</option>
|
<option value="resolved">Resolved</option>
|
||||||
@@ -49,7 +49,7 @@ export default function IssuesPage() {
|
|||||||
|
|
||||||
<table className="issues-table">
|
<table className="issues-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th><th>标签</th><th>创建时间</th></tr>
|
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Tags</th><th>Created</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issues.map((i) => (
|
{issues.map((i) => (
|
||||||
@@ -68,9 +68,9 @@ export default function IssuesPage() {
|
|||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="pagination">
|
<div className="pagination">
|
||||||
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>上一页</button>
|
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>Prev</button>
|
||||||
<span>{page} / {totalPages}</span>
|
<span>{page} / {totalPages}</span>
|
||||||
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}>下一页</button>
|
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function LoginPage({ onLogin }: Props) {
|
|||||||
try {
|
try {
|
||||||
await onLogin(username, password)
|
await onLogin(username, password)
|
||||||
} catch {
|
} catch {
|
||||||
setError('登录失败,请检查用户名和密码')
|
setError('Login failed. Check username and password.')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -27,25 +27,25 @@ export default function LoginPage({ onLogin }: Props) {
|
|||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<h1>⚓ HarborForge</h1>
|
<h1>⚓ HarborForge</h1>
|
||||||
<p className="subtitle">Agent/人类协同任务管理平台</p>
|
<p className="subtitle">Agent/Human collaborative task management platform</p>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="用户名"
|
placeholder="Username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="密码"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
<button type="submit" disabled={loading}>
|
<button type="submit" disabled={loading}>
|
||||||
{loading ? '登录中...' : '登录'}
|
{loading ? 'Signing in...' : 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,37 +17,37 @@ export default function MilestoneDetailPage() {
|
|||||||
api.get<Issue[]>(`/milestones/${id}/issues`).then(({ data }) => setIssues(data)).catch(() => {})
|
api.get<Issue[]>(`/milestones/${id}/issues`).then(({ data }) => setIssues(data)).catch(() => {})
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
if (!milestone) return <div className="loading">加载中...</div>
|
if (!milestone) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-detail">
|
<div className="milestone-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/milestones')}>← 返回里程碑列表</button>
|
<button className="btn-back" onClick={() => navigate('/milestones')}>← Back to milestones</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="issue-header">
|
||||||
<h2>🏁 {milestone.title}</h2>
|
<h2>🏁 {milestone.title}</h2>
|
||||||
<div className="issue-meta">
|
<div className="issue-meta">
|
||||||
<span className={`badge status-${milestone.status === 'active' ? 'open' : 'closed'}`}>{milestone.status}</span>
|
<span className={`badge status-${milestone.status === 'active' ? 'open' : 'closed'}`}>{milestone.status}</span>
|
||||||
{milestone.due_date && <span className="text-dim">截止 {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
|
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{milestone.description && (
|
{milestone.description && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>描述</h3>
|
<h3>Description</h3>
|
||||||
<p>{milestone.description}</p>
|
<p>{milestone.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>进度</h3>
|
<h3>Progress</h3>
|
||||||
<div className="progress-bar-container">
|
<div className="progress-bar-container">
|
||||||
<div className="progress-bar" style={{ width: `${progress.progress_percent}%` }}>
|
<div className="progress-bar" style={{ width: `${progress.progress_percent}%` }}>
|
||||||
{progress.progress_percent.toFixed(0)}%
|
{progress.progress_percent.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-dim" style={{ marginTop: 8 }}>
|
<p className="text-dim" style={{ marginTop: 8 }}>
|
||||||
{progress.completed_issues} / {progress.total_issues} issues 已完成
|
{progress.completed_issues} / {progress.total_issues} issues completed
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -56,7 +56,7 @@ export default function MilestoneDetailPage() {
|
|||||||
<h3>Issues ({issues.length})</h3>
|
<h3>Issues ({issues.length})</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>标题</th><th>状态</th><th>优先级</th></tr>
|
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issues.map((i) => (
|
{issues.map((i) => (
|
||||||
@@ -67,7 +67,7 @@ export default function MilestoneDetailPage() {
|
|||||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{issues.length === 0 && <tr><td colSpan={4} className="empty">暂无关联 issue</td></tr>}
|
{issues.length === 0 && <tr><td colSpan={4} className="empty">No linked issues</td></tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,31 +38,31 @@ export default function MilestonesPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="milestones-page">
|
<div className="milestones-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>🏁 里程碑 ({milestones.length})</h2>
|
<h2>🏁 Milestones ({milestones.length})</h2>
|
||||||
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
|
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
|
||||||
{showCreate ? '取消' : '+ 新建里程碑'}
|
{showCreate ? 'Cancel' : '+ NewMilestones'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filters">
|
<div className="filters">
|
||||||
<select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}>
|
<select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}>
|
||||||
<option value="">全部项目</option>
|
<option value="">All projects</option>
|
||||||
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<form className="inline-form" onSubmit={createMilestone}>
|
<form className="inline-form" onSubmit={createMilestone}>
|
||||||
<input required placeholder="里程碑标题" value={form.title}
|
<input required placeholder="MilestonesTitle" value={form.title}
|
||||||
onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
||||||
<input placeholder="描述(可选)" value={form.description}
|
<input placeholder="Description (optional)" value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||||
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
|
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
|
||||||
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input type="date" value={form.due_date}
|
<input type="date" value={form.due_date}
|
||||||
onChange={(e) => setForm({ ...form, due_date: e.target.value })} />
|
onChange={(e) => setForm({ ...form, due_date: e.target.value })} />
|
||||||
<button type="submit" className="btn-primary">创建</button>
|
<button type="submit" className="btn-primary">Create</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -73,14 +73,14 @@ export default function MilestonesPage() {
|
|||||||
<span className={`badge status-${ms.status === 'active' ? 'open' : 'closed'}`}>{ms.status}</span>
|
<span className={`badge status-${ms.status === 'active' ? 'open' : 'closed'}`}>{ms.status}</span>
|
||||||
<h3>{ms.title}</h3>
|
<h3>{ms.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="project-desc">{ms.description || '暂无描述'}</p>
|
<p className="project-desc">{ms.description || 'No description'}</p>
|
||||||
<div className="project-meta">
|
<div className="project-meta">
|
||||||
{ms.due_date && <span>截止 {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
{ms.due_date && <span>Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
||||||
<span>创建于 {dayjs(ms.created_at).format('YYYY-MM-DD')}</span>
|
<span>Created {dayjs(ms.created_at).format('YYYY-MM-DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{milestones.length === 0 && <p className="empty">暂无里程碑</p>}
|
{milestones.length === 0 && <p className="empty">No milestones</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -159,24 +159,24 @@ export default function MonitorPage() {
|
|||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card total">
|
<div className="stat-card total">
|
||||||
<span className="stat-number">{data.issues.total_issues}</span>
|
<span className="stat-number">{data.issues.total_issues}</span>
|
||||||
<span className="stat-label">总 Issues</span>
|
<span className="stat-label">Total Issues</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
|
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
|
||||||
<span className="stat-number">{data.issues.new_issues_24h}</span>
|
<span className="stat-number">{data.issues.new_issues_24h}</span>
|
||||||
<span className="stat-label">24小时新增</span>
|
<span className="stat-label">New (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
|
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
|
||||||
<span className="stat-number">{data.issues.processed_issues_24h}</span>
|
<span className="stat-number">{data.issues.processed_issues_24h}</span>
|
||||||
<span className="stat-label">24小时已处理</span>
|
<span className="stat-label">Processed (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h3>Provider Usage</h3>
|
<h3>Provider Usage</h3>
|
||||||
<span className="text-dim">更新于 {data.generated_at}</span>
|
<span className="text-dim">Updated at {data.generated_at}</span>
|
||||||
</div>
|
</div>
|
||||||
{data.providers.length === 0 ? <p className="empty">暂无 provider 账号</p> : (
|
{data.providers.length === 0 ? <p className="empty">No provider accounts</p> : (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -209,8 +209,8 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>服务器监测</h3>
|
<h3>Server Monitoring</h3>
|
||||||
{data.servers.length === 0 ? <p className="empty">暂无监测服务器</p> : (
|
{data.servers.length === 0 ? <p className="empty">No monitored servers</p> : (
|
||||||
<div className="monitor-grid">
|
<div className="monitor-grid">
|
||||||
{data.servers.map((s) => (
|
{data.servers.map((s) => (
|
||||||
<div key={s.server_id} className="monitor-card">
|
<div key={s.server_id} className="monitor-card">
|
||||||
@@ -234,11 +234,11 @@ export default function MonitorPage() {
|
|||||||
|
|
||||||
{canAdmin && (
|
{canAdmin && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>Admin 管理</h3>
|
<h3>Admin</h3>
|
||||||
|
|
||||||
<div className="monitor-admin">
|
<div className="monitor-admin">
|
||||||
<div className="monitor-card">
|
<div className="monitor-card">
|
||||||
<h4>Provider 账号</h4>
|
<h4>Provider Accounts</h4>
|
||||||
<div className="inline-form">
|
<div className="inline-form">
|
||||||
<select value={providerForm.provider} onChange={(e) => setProviderForm({ ...providerForm, provider: e.target.value })}>
|
<select value={providerForm.provider} onChange={(e) => setProviderForm({ ...providerForm, provider: e.target.value })}>
|
||||||
<option value='openai'>openai</option>
|
<option value='openai'>openai</option>
|
||||||
@@ -249,30 +249,30 @@ export default function MonitorPage() {
|
|||||||
</select>
|
</select>
|
||||||
<input placeholder='label' value={providerForm.label} onChange={(e) => setProviderForm({ ...providerForm, label: e.target.value })} />
|
<input placeholder='label' value={providerForm.label} onChange={(e) => setProviderForm({ ...providerForm, label: e.target.value })} />
|
||||||
<input placeholder='credential' value={providerForm.credential} onChange={(e) => setProviderForm({ ...providerForm, credential: e.target.value })} />
|
<input placeholder='credential' value={providerForm.credential} onChange={(e) => setProviderForm({ ...providerForm, credential: e.target.value })} />
|
||||||
<button className="btn-primary" onClick={testProvider}>测试连接</button>
|
<button className="btn-primary" onClick={testProvider}>Test Connection</button>
|
||||||
<button className="btn-primary" onClick={addProvider}>添加账号</button>
|
<button className="btn-primary" onClick={addProvider}>Add Account</button>
|
||||||
</div>
|
</div>
|
||||||
{providerTestMsg && <p className="text-dim">{providerTestMsg}</p>}
|
{providerTestMsg && <p className="text-dim">{providerTestMsg}</p>}
|
||||||
<ul>
|
<ul>
|
||||||
{providerAccounts.map((p) => (
|
{providerAccounts.map((p) => (
|
||||||
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button className="btn-danger" onClick={() => deleteProvider(p.id)}>删除</button></li>
|
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button className="btn-danger" onClick={() => deleteProvider(p.id)}>Delete</button></li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="monitor-card">
|
<div className="monitor-card">
|
||||||
<h4>服务器</h4>
|
<h4>Servers</h4>
|
||||||
<div className="inline-form">
|
<div className="inline-form">
|
||||||
<input placeholder='identifier' value={serverForm.identifier} onChange={(e) => setServerForm({ ...serverForm, identifier: e.target.value })} />
|
<input placeholder='identifier' value={serverForm.identifier} onChange={(e) => setServerForm({ ...serverForm, identifier: e.target.value })} />
|
||||||
<input placeholder='display_name' value={serverForm.display_name} onChange={(e) => setServerForm({ ...serverForm, display_name: e.target.value })} />
|
<input placeholder='display_name' value={serverForm.display_name} onChange={(e) => setServerForm({ ...serverForm, display_name: e.target.value })} />
|
||||||
<button className="btn-primary" onClick={addServer}>添加服务器</button>
|
<button className="btn-primary" onClick={addServer}>Add Server</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{servers.map((s) => (
|
{servers.map((s) => (
|
||||||
<li key={s.server_id}>
|
<li key={s.server_id}>
|
||||||
{s.display_name} ({s.identifier})
|
{s.display_name} ({s.identifier})
|
||||||
<button className="btn-secondary" onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}>生成挑战</button>
|
<button className="btn-secondary" onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}>Generate Challenge</button>
|
||||||
<button className="btn-danger" onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}>删除</button>
|
<button className="btn-danger" onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}>Delete</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ export default function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="notifications-page">
|
<div className="notifications-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>🔔 通知 {unreadCount > 0 && <span className="badge" style={{ background: 'var(--danger)' }}>{unreadCount}</span>}</h2>
|
<h2>🔔 Notifications {unreadCount > 0 && <span className="badge" style={{ background: 'var(--danger)' }}>{unreadCount}</span>}</h2>
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<button className="btn-primary" onClick={markAllRead}>全部标记已读</button>
|
<button className="btn-primary" onClick={markAllRead}>Mark all read</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filters">
|
<div className="filters">
|
||||||
<label className="filter-check">
|
<label className="filter-check">
|
||||||
<input type="checkbox" checked={unreadOnly} onChange={(e) => setUnreadOnly(e.target.checked)} />
|
<input type="checkbox" checked={unreadOnly} onChange={(e) => setUnreadOnly(e.target.checked)} />
|
||||||
仅显示未读
|
Show unread only
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default function NotificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{notifications.length === 0 && <p className="empty">暂无通知</p>}
|
{notifications.length === 0 && <p className="empty">No notifications</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,62 +31,62 @@ export default function ProjectDetailPage() {
|
|||||||
setEditing(false)
|
setEditing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project) return <div className="loading">加载中...</div>
|
if (!project) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="project-detail">
|
<div className="project-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/projects')}>← 返回项目列表</button>
|
<button className="btn-back" onClick={() => navigate('/projects')}>← Back to projects</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="issue-header">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<form className="inline-form" onSubmit={updateProject}>
|
<form className="inline-form" onSubmit={updateProject}>
|
||||||
<input value={editForm.name} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} required />
|
<input value={editForm.name} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} required />
|
||||||
<input value={editForm.description} onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} placeholder="描述" />
|
<input value={editForm.description} onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} placeholder="Description" />
|
||||||
<button type="submit" className="btn-primary">保存</button>
|
<button type="submit" className="btn-primary">Save</button>
|
||||||
<button type="button" className="btn-back" onClick={() => setEditing(false)}>取消</button>
|
<button type="button" className="btn-back" onClick={() => setEditing(false)}>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2>📁 {project.name}</h2>
|
<h2>📁 {project.name}</h2>
|
||||||
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || '暂无描述'}</p>
|
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || 'No description'}</p>
|
||||||
<button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setEditing(true)}>编辑</button>
|
<button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setEditing(true)}>Edit</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>成员 ({members.length})</h3>
|
<h3>Members ({members.length})</h3>
|
||||||
{members.length > 0 ? (
|
{members.length > 0 ? (
|
||||||
<div className="member-list">
|
<div className="member-list">
|
||||||
{members.map((m) => (
|
{members.map((m) => (
|
||||||
<span key={m.id} className="badge">{`用户 #${m.user_id} (${m.role})`}</span>
|
<span key={m.id} className="badge">{`User #${m.user_id} (${m.role})`}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="empty">暂无成员</p>
|
<p className="empty">No members</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>里程碑 ({milestones.length})</h3>
|
<h3>Milestones ({milestones.length})</h3>
|
||||||
{milestones.map((ms) => (
|
{milestones.map((ms) => (
|
||||||
<div key={ms.id} className="milestone-item" onClick={() => navigate(`/milestones/${ms.id}`)}>
|
<div key={ms.id} className="milestone-item" onClick={() => navigate(`/milestones/${ms.id}`)}>
|
||||||
<span className={`badge status-${ms.status === 'active' ? 'open' : ms.status === 'closed' ? 'closed' : 'in_progress'}`}>{ms.status}</span>
|
<span className={`badge status-${ms.status === 'active' ? 'open' : ms.status === 'closed' ? 'closed' : 'in_progress'}`}>{ms.status}</span>
|
||||||
<span className="milestone-title">{ms.title}</span>
|
<span className="milestone-title">{ms.title}</span>
|
||||||
{ms.due_date && <span className="text-dim"> · 截止 {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
{ms.due_date && <span className="text-dim"> · Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{milestones.length === 0 && <p className="empty">暂无里程碑</p>}
|
{milestones.length === 0 && <p className="empty">No milestones</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h3>最近 Issues</h3>
|
<h3>Recent Issues</h3>
|
||||||
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ 新建</button>
|
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ New</button>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>标题</th><th>状态</th><th>优先级</th></tr>
|
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issues.map((i) => (
|
{issues.map((i) => (
|
||||||
|
|||||||
@@ -27,23 +27,23 @@ export default function ProjectsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="projects-page">
|
<div className="projects-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>📁 项目 ({projects.length})</h2>
|
<h2>📁 Projects ({projects.length})</h2>
|
||||||
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
|
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
|
||||||
{showCreate ? '取消' : '+ 新建项目'}
|
{showCreate ? 'Cancel' : '+ NewProjects'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<form className="inline-form" onSubmit={createProject}>
|
<form className="inline-form" onSubmit={createProject}>
|
||||||
<input
|
<input
|
||||||
required placeholder="项目名称" value={form.name}
|
required placeholder="Project name" value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
placeholder="项目描述(可选)" value={form.description}
|
placeholder="ProjectsDescription (optional)" value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="btn-primary">创建</button>
|
<button type="submit" className="btn-primary">Create</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -51,13 +51,13 @@ export default function ProjectsPage() {
|
|||||||
{projects.map((p) => (
|
{projects.map((p) => (
|
||||||
<div key={p.id} className="project-card" onClick={() => navigate(`/projects/${p.id}`)}>
|
<div key={p.id} className="project-card" onClick={() => navigate(`/projects/${p.id}`)}>
|
||||||
<h3>{p.name}</h3>
|
<h3>{p.name}</h3>
|
||||||
<p className="project-desc">{p.description || '暂无描述'}</p>
|
<p className="project-desc">{p.description || 'No description'}</p>
|
||||||
<div className="project-meta">
|
<div className="project-meta">
|
||||||
<span>创建于 {dayjs(p.created_at).format('YYYY-MM-DD')}</span>
|
<span>Created {dayjs(p.created_at).format('YYYY-MM-DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{projects.length === 0 && <p className="empty">暂无项目,点击上方创建</p>}
|
{projects.length === 0 && <p className="empty">No projects yet. Create one above.</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface SetupForm {
|
|||||||
project_description: string
|
project_description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
|
const STEPS = ['Welcome', 'Database', 'Admin', 'Projects', 'Finish']
|
||||||
|
|
||||||
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
||||||
const [step, setStep] = useState(0)
|
const [step, setStep] = useState(0)
|
||||||
@@ -40,7 +40,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
db_database: 'harborforge',
|
db_database: 'harborforge',
|
||||||
backend_base_url: 'http://127.0.0.1:8000',
|
backend_base_url: 'http://127.0.0.1:8000',
|
||||||
project_name: 'Default',
|
project_name: 'Default',
|
||||||
project_description: '默认项目',
|
project_description: 'Default project',
|
||||||
})
|
})
|
||||||
|
|
||||||
const wizardApi = axios.create({
|
const wizardApi = axios.create({
|
||||||
@@ -59,7 +59,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
setStep(1)
|
setStep(1)
|
||||||
} catch {
|
} catch {
|
||||||
setWizardOk(false)
|
setWizardOk(false)
|
||||||
setError(`无法连接 AbstractWizard (${wizardBase})。\n请确认已通过 SSH 隧道映射端口:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
|
setError(`Unable to connect to AbstractWizard (${wizardBase}).\nPlease ensure the SSH tunnel is configured:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
|
|
||||||
setStep(4)
|
setStep(4)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(`保存配置失败: ${err.message}`)
|
setError(`Failed to save configuration: ${err.message}`)
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
<div className="setup-wizard">
|
<div className="setup-wizard">
|
||||||
<div className="setup-container">
|
<div className="setup-container">
|
||||||
<div className="setup-header">
|
<div className="setup-header">
|
||||||
<h1>⚓ HarborForge 初始化向导</h1>
|
<h1>⚓ HarborForge Setup Wizard</h1>
|
||||||
<div className="setup-steps">
|
<div className="setup-steps">
|
||||||
{STEPS.map((s, i) => (
|
{STEPS.map((s, i) => (
|
||||||
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
|
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
|
||||||
@@ -123,17 +123,17 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
{/* Step 0: Welcome */}
|
{/* Step 0: Welcome */}
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<h2>欢迎使用 HarborForge</h2>
|
<h2>Welcome to HarborForge</h2>
|
||||||
<p>Agent/人类协同任务管理平台</p>
|
<p>Agent/Human collaborative task management platform</p>
|
||||||
<div className="setup-info">
|
<div className="setup-info">
|
||||||
<p>⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:</p>
|
<p>⚠️ The setup wizard connects to AbstractWizard via SSH tunnel. Ensure the port is forwarded:</p>
|
||||||
<code>ssh -L <wizard_port>:127.0.0.1:<wizard_port> user@your-server</code>
|
<code>ssh -L <wizard_port>:127.0.0.1:<wizard_port> user@your-server</code>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary" onClick={checkWizard}>
|
<button className="btn-primary" onClick={checkWizard}>
|
||||||
连接 Wizard 开始配置
|
Connect to Wizard
|
||||||
</button>
|
</button>
|
||||||
{wizardOk === false && (
|
{wizardOk === false && (
|
||||||
<p className="setup-hint">连接失败,请检查 SSH 隧道是否正确建立。</p>
|
<p className="setup-hint">Connection failed. Check the SSH tunnel.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -141,18 +141,18 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
{/* Step 1: Database */}
|
{/* Step 1: Database */}
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<h2>数据库配置</h2>
|
<h2>Database configuration</h2>
|
||||||
<p className="text-dim">配置 MySQL 连接信息(使用 docker-compose 内置 MySQL 可保持默认值)</p>
|
<p className="text-dim">Configure MySQL connection (docker-compose defaults are fine if using the bundled MySQL).</p>
|
||||||
<div className="setup-form">
|
<div className="setup-form">
|
||||||
<label>主机 <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
|
<label>Host <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
|
||||||
<label>端口 <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
|
<label>Port <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
|
||||||
<label>用户名 <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
|
<label>Username <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
|
||||||
<label>密码 <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
|
<label>Password <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
|
||||||
<label>数据库名 <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
|
<label>Database <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
|
||||||
</div>
|
</div>
|
||||||
<div className="setup-nav">
|
<div className="setup-nav">
|
||||||
<button className="btn-back" onClick={() => setStep(0)}>上一步</button>
|
<button className="btn-back" onClick={() => setStep(0)}>Back</button>
|
||||||
<button className="btn-primary" onClick={() => setStep(2)}>下一步</button>
|
<button className="btn-primary" onClick={() => setStep(2)}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -160,21 +160,21 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
{/* Step 2: Admin */}
|
{/* Step 2: Admin */}
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<h2>管理员账号</h2>
|
<h2>Admin account</h2>
|
||||||
<p className="text-dim">创建首个管理员用户</p>
|
<p className="text-dim">Create the first admin user</p>
|
||||||
<div className="setup-form">
|
<div className="setup-form">
|
||||||
<label>用户名 <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
|
<label>Username <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
|
||||||
<label>密码 <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="设置管理员密码" /></label>
|
<label>Password <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="Set admin password" /></label>
|
||||||
<label>邮箱 <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
|
<label>Email <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
|
||||||
<label>姓名 <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
|
<label>Full name <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
|
||||||
</div>
|
</div>
|
||||||
<div className="setup-nav">
|
<div className="setup-nav">
|
||||||
<button className="btn-back" onClick={() => setStep(1)}>上一步</button>
|
<button className="btn-back" onClick={() => setStep(1)}>Back</button>
|
||||||
<button className="btn-primary" onClick={() => {
|
<button className="btn-primary" onClick={() => {
|
||||||
if (!form.admin_password) { setError('请设置管理员密码'); return }
|
if (!form.admin_password) { setError('Please set an admin password'); return }
|
||||||
setError('')
|
setError('')
|
||||||
setStep(3)
|
setStep(3)
|
||||||
}}>下一步</button>
|
}}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -182,17 +182,17 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
{/* Step 3: Project */}
|
{/* Step 3: Project */}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<h2>默认项目(可选)</h2>
|
<h2>Default project (optional)</h2>
|
||||||
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
<p className="text-dim">Create an initial project or skip</p>
|
||||||
<div className="setup-form">
|
<div className="setup-form">
|
||||||
<label>后端 Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://127.0.0.1:8000" /></label>
|
<label>Backend Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://127.0.0.1:8000" /></label>
|
||||||
<label>项目名称 <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="留空则跳过" /></label>
|
<label>Project name <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="Leave blank to skip" /></label>
|
||||||
<label>项目描述 <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
|
<label>ProjectsDescription <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
|
||||||
</div>
|
</div>
|
||||||
<div className="setup-nav">
|
<div className="setup-nav">
|
||||||
<button className="btn-back" onClick={() => setStep(2)}>上一步</button>
|
<button className="btn-back" onClick={() => setStep(2)}>Back</button>
|
||||||
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
|
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
|
||||||
{saving ? '保存中...' : '完成配置'}
|
{saving ? 'Saving...' : 'Finish setup'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,16 +202,16 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
|||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<div className="setup-done">
|
<div className="setup-done">
|
||||||
<h2>✅ 初始化完成!</h2>
|
<h2>✅ Setup complete!</h2>
|
||||||
<p>配置已保存到 AbstractWizard。</p>
|
<p>Configuration saved to AbstractWizard.</p>
|
||||||
<div className="setup-info">
|
<div className="setup-info">
|
||||||
<p>请在服务器上重启服务:</p>
|
<p>Restart services on the server:</p>
|
||||||
<code>docker compose restart</code>
|
<code>docker compose restart</code>
|
||||||
<p style={{ marginTop: '1rem' }}>后端启动完成后,刷新本页面进入登录界面。</p>
|
<p style={{ marginTop: '1rem' }}>After the backend starts, refresh this page to go to login.</p>
|
||||||
<p>管理员账号: <strong>{form.admin_username}</strong></p>
|
<p>Admin account: <strong>{form.admin_username}</strong></p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary" onClick={onComplete}>
|
<button className="btn-primary" onClick={onComplete}>
|
||||||
刷新检查
|
Refresh to check
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user