后端: - SQLite 数据层 (rooms/users/chat_logs/settings/error_logs) - 抖音 WebSocket 抓取器 (proto 解析, 心跳, watchdog) - __ac_signature 算法 (纯 Go 移植) - a_bogus/sign.js 签名 (goja JS 引擎) - Echo HTTP API 服务器 (端口 5000) - SSE 事件广播 - 自动保活 + 授权守护 - PAYS 云授权对接 前端: - 顶栏: 品牌 + 日志/调试/客户名单/授权/文档/房间控制/主题切换 - 添加直播间 (链接解析 + 关键词) - Dashboard (卡片/列表视图, 公开/匿名房筛选, 分页) - 详情页 (多 tab 消息流, 右侧用户面板, 颜色标记, 精准客户, 已看标记) - 精准客户名单 (跨房间汇总) - 订阅授权页面 (登录/注册/购买) - 使用帮助 + 系统调试 - 白昼/黑夜主题切换 (CSS 变量 + iconfont) - 底部版权栏
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import React, { useState, useMemo } from 'react'
|
||
import RoomCard from './RoomCard'
|
||
import AddRoomBar from './AddRoomBar'
|
||
import type { RoomData } from '../types'
|
||
|
||
interface DashboardProps {
|
||
rooms: RoomData[]
|
||
viewMode: 'card' | 'list'
|
||
cardCols: number
|
||
listPerPage: number
|
||
onAddRoom: (url: string) => void
|
||
onToggleRoom: (id: string, action?: string) => void
|
||
onRemoveRoom: (id: string) => void
|
||
onSetActive: (id: string) => void
|
||
}
|
||
|
||
export default function Dashboard({ rooms, viewMode, cardCols, listPerPage, onAddRoom, onToggleRoom, onRemoveRoom, onSetActive }: DashboardProps) {
|
||
const [page, setPage] = useState(0)
|
||
|
||
const paginatedRooms = useMemo(() => {
|
||
if (viewMode === 'list') {
|
||
const start = page * listPerPage
|
||
return rooms.slice(start, start + listPerPage)
|
||
}
|
||
return rooms
|
||
}, [rooms, page, listPerPage, viewMode])
|
||
|
||
const totalPages = viewMode === 'list' ? Math.ceil(rooms.length / listPerPage) : 0
|
||
|
||
return (
|
||
<div className="dash-zone">
|
||
<AddRoomBar onAdd={onAddRoom} />
|
||
{viewMode === 'card' ? (
|
||
<div className="room-grid" style={{ gridTemplateColumns: `repeat(${cardCols}, 1fr)` }}>
|
||
{paginatedRooms.map(room => (
|
||
<RoomCard
|
||
key={room.id}
|
||
room={room}
|
||
onToggle={(a) => onToggleRoom(room.id, a)}
|
||
onRemove={() => onRemoveRoom(room.id)}
|
||
onDetail={() => onSetActive(room.id)}
|
||
/>
|
||
))}
|
||
{paginatedRooms.length === 0 && (
|
||
<div className="empty-state">
|
||
<p>暂无房间</p>
|
||
<p>在上方输入框添加直播间URL开始监控</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="room-list">
|
||
<table className="list-table">
|
||
<thead>
|
||
<tr>
|
||
<th>状态</th>
|
||
<th>房间</th>
|
||
<th>主播</th>
|
||
<th>类型</th>
|
||
<th>观众</th>
|
||
<th>消息</th>
|
||
<th>精准</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{paginatedRooms.map(room => (
|
||
<tr key={room.id} className={`list-row ${room.status}`} onClick={() => onSetActive(room.id)}>
|
||
<td><span className={`status-dot ${room.status}`} /></td>
|
||
<td className="list-title">{room.title}</td>
|
||
<td>{room.anchor_id}</td>
|
||
<td><span className={`kind-badge ${room.kind}`}>{room.kind === 'public' ? '公开' : '匿名'}</span></td>
|
||
<td>{room.viewers}</td>
|
||
<td>{room.total_msg}</td>
|
||
<td>{room.lead_count}</td>
|
||
<td className="list-actions">
|
||
<button className="list-btn start" onClick={e => { e.stopPropagation(); onToggleRoom(room.id, 'start') }}>▶</button>
|
||
<button className="list-btn stop" onClick={e => { e.stopPropagation(); onToggleRoom(room.id, 'stop') }}>⏹</button>
|
||
<button className="list-btn del" onClick={e => { e.stopPropagation(); onRemoveRoom(room.id) }}>✕</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{paginatedRooms.length === 0 && (
|
||
<div className="empty-state">
|
||
<p>暂无房间</p>
|
||
<p>在上方输入框添加直播间URL开始监控</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{totalPages > 1 && (
|
||
<div className="pagination">
|
||
<button className="page-btn" disabled={page === 0} onClick={() => setPage(p => p - 1)}>‹</button>
|
||
<span className="page-info">{page + 1} / {totalPages}</span>
|
||
<button className="page-btn" disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)}>›</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|