feat(frontend): add routing skeleton with workspace chat and login pages

This commit is contained in:
root
2026-05-12 13:13:52 +00:00
parent 196535ba5a
commit b3291b5874
6 changed files with 62 additions and 120 deletions

View File

@@ -1,123 +1,19 @@
import { useEffect, useMemo, useState } from 'react'
import axios from 'axios'
import { io, Socket } from 'socket.io-client'
import './App.css'
type Health = { ok: boolean; service: string; database?: string }
type MessageItem = {
messageId: string
seq: number
content: string
createdAt: string
isDeleted?: boolean
}
const API_BASE = import.meta.env.VITE_GUILD_API_BASE ?? 'http://localhost:7002/api'
const SOCKET_BASE = import.meta.env.VITE_GUILD_SOCKET_BASE ?? 'http://localhost:7002/realtime'
const API_KEY = import.meta.env.VITE_API_KEY ?? 'change-me-api-key'
function App() {
const [health, setHealth] = useState<Health | null>(null)
const [channelId, setChannelId] = useState('')
const [messages, setMessages] = useState<MessageItem[]>([])
const [content, setContent] = useState('')
const [socketState, setSocketState] = useState<'offline' | 'online'>('offline')
const client = useMemo(
() =>
axios.create({
baseURL: API_BASE,
headers: { 'x-api-key': API_KEY },
}),
[],
)
useEffect(() => {
client.get('/healthz').then((res) => setHealth(res.data)).catch(() => setHealth(null))
}, [client])
useEffect(() => {
if (!channelId) return
const socket: Socket = io(SOCKET_BASE, {
transports: ['websocket'],
auth: { apiKey: API_KEY, userId: 'frontend-user' },
extraHeaders: { 'x-api-key': API_KEY },
})
socket.on('connect', () => {
setSocketState('online')
socket.emit('join_channel', { channelId })
})
socket.on('disconnect', () => setSocketState('offline'))
socket.on('message.created', (m) => setMessages((prev) => [...prev, m]))
socket.on('message.updated', (m) =>
setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))),
)
socket.on('message.deleted', (m) =>
setMessages((prev) =>
prev.map((x) => (x.messageId === m.messageId ? { ...x, isDeleted: true, content: '[deleted]' } : x)),
),
)
return () => {
socket.emit('leave_channel', { channelId })
socket.disconnect()
setSocketState('offline')
}
}, [channelId])
async function pullMessages() {
if (!channelId) return
const res = await client.get(`/channels/${channelId}/messages`, {
params: { seq_from: 1, seq_to: 999999, limit: 100 },
})
setMessages(res.data.items ?? [])
}
async function sendMessage() {
if (!channelId || !content.trim()) return
await client.post(`/channels/${channelId}/messages`, {
content,
authorUserId: 'frontend-user',
})
setContent('')
}
import { Navigate, Route, Routes } from 'react-router-dom'
import AppLayout from './layouts/AppLayout'
import ChatPage from './pages/ChatPage'
import LoginPage from './pages/LoginPage'
import WorkspacePage from './pages/WorkspacePage'
export default function App() {
return (
<main className="app">
<h1>Fabric Frontend</h1>
<p>Health: {health ? `${health.service} (${health.database ?? 'ok'})` : 'unavailable'}</p>
<p>Socket: {socketState}</p>
<section className="row">
<input
value={channelId}
onChange={(e) => setChannelId(e.target.value)}
placeholder="Channel ID"
/>
<button onClick={pullMessages}></button>
</section>
<section className="row">
<input
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="输入消息"
/>
<button onClick={sendMessage}></button>
</section>
<ul className="messages">
{messages.map((m) => (
<li key={m.messageId}>
<span>#{m.seq}</span> <span>{m.content}</span>
</li>
))}
</ul>
</main>
<Routes>
<Route path="/" element={<AppLayout />}>
<Route index element={<Navigate to="/workspace" replace />} />
<Route path="workspace" element={<WorkspacePage />} />
<Route path="chat" element={<ChatPage />} />
<Route path="login" element={<LoginPage />} />
</Route>
<Route path="*" element={<Navigate to="/workspace" replace />} />
</Routes>
)
}
export default App

19
src/layouts/AppLayout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Link, Outlet } from 'react-router-dom'
export default function AppLayout() {
return (
<div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', minHeight: '100vh' }}>
<aside style={{ borderRight: '1px solid #ddd', padding: 16 }}>
<h3>Fabric</h3>
<nav style={{ display: 'grid', gap: 8 }}>
<Link to="/workspace"></Link>
<Link to="/chat"></Link>
<Link to="/login"></Link>
</nav>
</aside>
<main style={{ padding: 16 }}>
<Outlet />
</main>
</div>
)
}

View File

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

8
src/pages/ChatPage.tsx Normal file
View File

@@ -0,0 +1,8 @@
export default function ChatPage() {
return (
<section>
<h2></h2>
<p>///</p>
</section>
)
}

8
src/pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,8 @@
export default function LoginPage() {
return (
<section>
<h2></h2>
<p> Center token </p>
</section>
)
}

View File

@@ -0,0 +1,8 @@
export default function WorkspacePage() {
return (
<section>
<h2></h2>
<p> Guild/Channel </p>
</section>
)
}