tag(功能):加入按照房间名称加入的功能

This commit is contained in:
OrangeRoll
2026-08-07 14:12:36 +08:00
parent 9c0a4f59f6
commit d00848ee36
5 changed files with 126 additions and 6 deletions
@@ -206,6 +206,24 @@ object RoomRepository {
}
}
fun findByTitle(title: String): RoomRecord? = Database.useConnection { connection ->
connection.prepareStatement(
"SELECT id, title, status, created_by FROM rooms WHERE LOWER(title) = LOWER(?)",
).use { statement ->
statement.setString(1, title)
statement.executeQuery().use { result ->
if (result.next()) {
RoomRecord(
result.getObject("id", UUID::class.java),
result.getString("title"),
result.getString("status"),
result.getObject("created_by", UUID::class.java),
)
} else null
}
}
}
fun member(roomId: UUID, userId: UUID): RoomMember? = Database.useConnection { connection ->
connection.prepareStatement(
"SELECT room_id, user_id, role FROM room_members WHERE room_id = ? AND user_id = ?",
@@ -105,6 +105,7 @@ fun Application.configureRouting() {
val user = call.requireCurrentUser()
val title = call.receive<CreateRoomRequest>().title.trim()
if (title.length !in 1..128) throw ApiException("房间标题必须为 1-128 个字符", 400)
if (RoomRepository.findByTitle(title) != null) throw ApiException("该房间标题已存在", 409)
call.respond(HttpStatusCode.Created, RoomResponse(RoomRepository.create(user.id, title), RoomRole.host))
}
@@ -121,6 +122,15 @@ fun Application.configureRouting() {
call.respond(RoomMemberResponse(RoomRepository.joinAsViewer(call.roomId(), user.id)))
}
post("/api/rooms/join-by-title") {
val user = call.requireCurrentUser()
val title = call.receive<FindRoomByTitleRequest>().title.trim()
if (title.isEmpty()) throw ApiException("请输入房间标题", 400)
val room = RoomRepository.findByTitle(title) ?: throw ApiException("未找到该房间", 404)
val membership = RoomRepository.joinAsViewer(room.id, user.id)
call.respond(RoomResponse(room, membership.role))
}
post("/api/rooms/{roomId}/members/{userId}/role") {
val requester = call.requireCurrentUser()
val roomId = call.roomId()
@@ -214,6 +224,7 @@ enum class RoomRole { host, publisher, viewer }
@Serializable data class RegisterRequest(val inviteCode: String, val username: String, val password: String, val displayName: String)
@Serializable data class CreateInvitationRequest(val type: String, val expiresInHours: Int = 168)
@Serializable data class CreateRoomRequest(val title: String)
@Serializable data class FindRoomByTitleRequest(val title: String)
@Serializable data class UpdateMemberRoleRequest(val role: String)
@Serializable data class AuthResponse(val token: String, val user: UserResponse)
@Serializable data class UserResponse(val id: String, val username: String, val displayName: String, val isAdmin: Boolean) {
@@ -0,0 +1 @@
CREATE UNIQUE INDEX uq_rooms_title_case_insensitive ON rooms (LOWER(title));
+31 -1
View File
@@ -89,6 +89,36 @@ h2 {
box-shadow: 0 8px 30px rgb(30 33 80 / 7%);
}
.room-panel {
grid-template-columns: minmax(180px, 1.4fr) auto auto minmax(180px, 1fr) auto;
}
.advanced-field {
font-size: 0.76rem;
}
.id-join {
align-self: end;
}
.room-history {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin: 14px 0 0;
color: #555871;
font-size: 0.86rem;
}
.room-history button {
min-height: 32px;
padding: 0 10px;
background: #ecebff;
color: #4536c7;
font-size: 0.82rem;
}
label {
display: grid;
gap: 7px;
@@ -234,7 +264,7 @@ button:disabled {
padding: 32px 0;
}
.join-panel, .screens {
.join-panel, .room-panel, .screens {
grid-template-columns: 1fr;
}
+65 -5
View File
@@ -23,6 +23,9 @@ type TokenResponse = {
}
type ScreenFeed = { track: Track; label: string }
type RoomHistoryItem = { id: string; title: string; visitedAt: string }
const ROOM_HISTORY_KEY = 'screen-share-room-history'
function ScreenTrack({ track, label }: ScreenFeed) {
const mediaContainerRef = useRef<HTMLDivElement>(null)
@@ -76,6 +79,14 @@ function App() {
const [inviteCode, setInviteCode] = useState('')
const [roomId, setRoomId] = useState('')
const [roomTitle, setRoomTitle] = useState('')
const [roomHistory, setRoomHistory] = useState<RoomHistoryItem[]>(() => {
try {
const stored = localStorage.getItem(ROOM_HISTORY_KEY)
return stored ? JSON.parse(stored) as RoomHistoryItem[] : []
} catch {
return []
}
})
const [connectionState, setConnectionState] = useState<'idle' | 'connecting' | 'connected'>('idle')
const [roomRole, setRoomRole] = useState<Role | null>(null)
const [isSharing, setIsSharing] = useState(false)
@@ -107,6 +118,17 @@ function App() {
setRemoteCount(room.remoteParticipants.size)
}
function rememberRoom(room: Pick<RoomHistoryItem, 'id' | 'title'>) {
setRoomHistory((history) => {
const next = [
{ ...room, visitedAt: new Date().toISOString() },
...history.filter((item) => item.id !== room.id),
].slice(0, 12)
localStorage.setItem(ROOM_HISTORY_KEY, JSON.stringify(next))
return next
})
}
async function submitAuth() {
setError('')
try {
@@ -146,17 +168,39 @@ function App() {
}
setError('')
try {
const room = await apiRequest<{ id: string }>('/api/rooms', {
const room = await apiRequest<{ id: string; title: string }>('/api/rooms', {
method: 'POST',
body: JSON.stringify({ title: roomTitle.trim() }),
}, session.token)
setRoomId(room.id)
setRoomTitle(room.title)
rememberRoom(room)
await connectRoom(room.id)
} catch (cause) {
setError(cause instanceof Error ? cause.message : '创建房间失败')
}
}
async function joinRoomByTitle() {
if (!session || !roomTitle.trim()) {
setError('请输入房间标题。')
return
}
setError('')
try {
const room = await apiRequest<{ id: string; title: string }>('/api/rooms/join-by-title', {
method: 'POST',
body: JSON.stringify({ title: roomTitle.trim() }),
}, session.token)
setRoomId(room.id)
setRoomTitle(room.title)
rememberRoom(room)
await connectRoom(room.id)
} catch (cause) {
setError(cause instanceof Error ? cause.message : '加入房间失败')
}
}
async function connectRoom(targetRoomId = roomId.trim()) {
if (!session) return
if (!targetRoomId) {
@@ -298,15 +342,31 @@ function App() {
</section>
)}
<section className="join-panel">
<label><input value={roomTitle} onChange={(event) => setRoomTitle(event.target.value)} disabled={connected} /></label>
<section className="join-panel room-panel">
<label><input list="room-title-history" value={roomTitle} onChange={(event) => setRoomTitle(event.target.value)} disabled={connected} placeholder="输入标题以创建或加入" />
<datalist id="room-title-history">{roomHistory.map((item) => <option key={item.id} value={item.title} />)}</datalist>
</label>
<button type="button" className="primary" onClick={() => void createRoom()} disabled={connected}></button>
<label> ID<input value={roomId} onChange={(event) => setRoomId(event.target.value)} disabled={connected} placeholder="创建后或从他人处获得" /></label>
{!connected
? <button type="button" className="secondary" onClick={() => void connectRoom()} disabled={connectionState === 'connecting'}>{connectionState === 'connecting' ? '正在连接…' : '加入房间'}</button>
? <button type="button" className="secondary" onClick={() => void joinRoomByTitle()} disabled={connectionState === 'connecting'}>{connectionState === 'connecting' ? '正在连接…' : '按标题加入'}</button>
: <button type="button" className="secondary" onClick={leaveRoom}></button>}
<label className="advanced-field"> ID<input value={roomId} onChange={(event) => setRoomId(event.target.value)} disabled={connected} placeholder="UUID" /></label>
{!connected && <button type="button" className="secondary id-join" onClick={() => void connectRoom()} disabled={connectionState === 'connecting'}>使 ID </button>}
</section>
{roomHistory.length > 0 && (
<section className="room-history">
<strong></strong>
{roomHistory.map((item) => (
<button key={item.id} type="button" onClick={() => {
setRoomTitle(item.title)
setRoomId(item.id)
void connectRoom(item.id)
}} disabled={connected}>{item.title}</button>
))}
</section>
)}
{error && <p className="error" role="alert">{error}</p>}
<section className="status-bar" aria-live="polite">