Compare commits
1 Commits
196535ba5a
...
b3291b5874
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3291b5874 |
134
src/App.tsx
134
src/App.tsx
@@ -1,123 +1,19 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import AppLayout from './layouts/AppLayout'
|
||||||
import { io, Socket } from 'socket.io-client'
|
import ChatPage from './pages/ChatPage'
|
||||||
import './App.css'
|
import LoginPage from './pages/LoginPage'
|
||||||
|
import WorkspacePage from './pages/WorkspacePage'
|
||||||
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('')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<main className="app">
|
<Routes>
|
||||||
<h1>Fabric Frontend</h1>
|
<Route path="/" element={<AppLayout />}>
|
||||||
<p>Health: {health ? `${health.service} (${health.database ?? 'ok'})` : 'unavailable'}</p>
|
<Route index element={<Navigate to="/workspace" replace />} />
|
||||||
<p>Socket: {socketState}</p>
|
<Route path="workspace" element={<WorkspacePage />} />
|
||||||
|
<Route path="chat" element={<ChatPage />} />
|
||||||
<section className="row">
|
<Route path="login" element={<LoginPage />} />
|
||||||
<input
|
</Route>
|
||||||
value={channelId}
|
<Route path="*" element={<Navigate to="/workspace" replace />} />
|
||||||
onChange={(e) => setChannelId(e.target.value)}
|
</Routes>
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|||||||
19
src/layouts/AppLayout.tsx
Normal file
19
src/layouts/AppLayout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
8
src/pages/ChatPage.tsx
Normal file
8
src/pages/ChatPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function ChatPage() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>聊天</h2>
|
||||||
|
<p>下一步接入消息拉取/发送/编辑/删除与实时订阅。</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/pages/LoginPage.tsx
Normal file
8
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>登录</h2>
|
||||||
|
<p>下一步接入 Center 登录与 token 管理。</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/pages/WorkspacePage.tsx
Normal file
8
src/pages/WorkspacePage.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function WorkspacePage() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>工作台</h2>
|
||||||
|
<p>这里会展示 Guild/Channel 概览与会话入口。</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user