更新后台管理消息列表;

This commit is contained in:
karlcarlo
2025-12-15 17:44:35 +08:00
parent 41d2d893d3
commit a7b3454f61
22 changed files with 1620 additions and 356 deletions

2
backstage/.env.sample Normal file
View File

@@ -0,0 +1,2 @@
# API 基础地址
VITE_API_BASE_URL=http://mp.yizhezhe.com/api

View File

@@ -4,6 +4,7 @@ import Dashboard from './pages/Dashboard'
import Messages from './pages/Messages'
import Settings from './pages/Settings'
import Login from './pages/Login'
import LoadArchive from './pages/LoadArchive'
function App() {
// 简单的认证检查(实际项目中应该使用更完善的认证方案)
@@ -17,6 +18,7 @@ function App() {
<Route index element={<Dashboard />} />
<Route path="messages" element={<Messages />} />
<Route path="settings" element={<Settings />} />
<Route path="load-archive" element={<LoadArchive />} />
</Route>
</Routes>
</Router>

View File

@@ -44,19 +44,6 @@ const Layout = () => {
))}
</nav>
<div className="border-t border-base-300 p-4">
<div className="flex items-center">
<div className="avatar placeholder">
<div className="bg-neutral text-neutral-content w-8 rounded-full">
<span className="text-xs">AD</span>
</div>
</div>
<div className="ml-3">
<p className="text-sm font-medium">管理员</p>
<p className="text-xs text-base-content/70">admin@example.com</p>
</div>
</div>
</div>
</div>
{/* 主内容区 */}

View File

@@ -1,35 +0,0 @@
import { useState } from 'react'
const DocumentationViewer = () => {
const [library, setLibrary] = useState('react')
const [mode, setMode] = useState('code')
const [topic, setTopic] = useState('')
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(false)
const [docs, setDocs] = useState(null)
const [error, setError] = useState('')
const libraries = [
{ id: 'react', name: 'React', description: '用于构建用户界面的 JavaScript 库' },
{ id: 'tailwindcss', name: 'Tailwind CSS', description: '实用优先的 CSS 框架' },
{ id: 'daisyui', name: 'DaisyUI', description: 'Tailwind CSS 组件库' },
{ id: 'vite', name: 'Vite', description: '下一代前端工具' },
{ id: 'axios', name: 'Axios', description: '基于 Promise 的 HTTP 客户端' },
]
const modes = [
{ id: 'code', name: '代码文档', description: 'API 参考和代码示例' },
{ id: 'info', name: '概念指南', description: '概念性指南和架构信息' },
]
const handleFetchDocs = async () => {
setLoading(true)
setError('')
setDocs(null)
try {
// 这里应该调用 Context7 MCP 工具
// 由于我们无法直接在前端调用 MCP 工具,这里模拟 API 调用
// 实际项目中应该通过后端代理调用 MCP 工具
// 模拟延迟

View File

@@ -1,4 +1,7 @@
import { useNavigate } from 'react-router-dom'
const Dashboard = () => {
const navigate = useNavigate()
const stats = [
{ title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' },
{ title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' },
@@ -75,19 +78,31 @@ const Dashboard = () => {
<div className="card-body">
<h2 className="card-title">快速操作</h2>
<div className="grid grid-cols-2 gap-4">
<button className="btn btn-primary">
<button
className="btn btn-primary"
onClick={() => navigate('/load-archive')}
>
<span className="mr-2">📥</span>
加载存档
</button>
<button className="btn btn-outline">
<span className="mr-2">📊</span>
查看统计
</button>
<button className="btn btn-outline">
<button
className="btn btn-outline"
onClick={() => navigate('/messages')}
>
<span className="mr-2">🔍</span>
搜索消息
</button>
<button className="btn btn-outline">
<button
className="btn btn-outline"
onClick={() => navigate('/messages')}
>
<span className="mr-2">📋</span>
消息列表
</button>
<button
className="btn btn-outline"
onClick={() => navigate('/settings')}
>
<span className="mr-2">⚙️</span>
系统设置
</button>

View File

@@ -0,0 +1,499 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { messageApi } from '../services/api'
const LoadArchive = () => {
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [progress, setProgress] = useState({
status: 'idle', // idle, loading, success, error
currentStep: 0,
steps: [
{
id: 'config',
name: '初始化配置',
status: 'pending', // pending, running, success, error
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'fetch_data',
name: '拉取会话数据',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'save_db',
name: '保存到数据库',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'complete',
name: '完成',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
}
],
config: {
corpid: '',
secret: '',
libPath: '',
rsaKeyPath: '',
dataDir: ''
},
result: null,
error: null,
startTime: null,
endTime: null,
totalDuration: null
})
// 更新步骤状态
const updateStep = (stepId, updates) => {
setProgress(prev => ({
...prev,
steps: prev.steps.map(step =>
step.id === stepId ? { ...step, ...updates } : step
)
}))
}
// 开始加载会话存档
const startLoadArchive = async () => {
const startTime = new Date()
setLoading(true)
setProgress(prev => ({
...prev,
status: 'loading',
startTime,
error: null,
result: null
}))
try {
// 步骤1: 初始化配置
updateStep('config', {
status: 'running',
startTime: new Date(),
details: '正在读取配置信息...'
})
// 获取配置信息
const configResponse = await messageApi.getConfig()
if (!configResponse.success) {
throw new Error(configResponse.message || '获取配置信息失败')
}
const config = configResponse.data
setProgress(prev => ({
...prev,
config
}))
updateStep('config', {
status: 'success',
endTime: new Date(),
duration: 500,
details: `配置加载完成\n企业ID: ${config.corpid}\n动态库路径: ${config.libPath}\n数据目录: ${config.dataDir}`
})
// 步骤3: 拉取会话数据
updateStep('fetch_data', {
status: 'running',
startTime: new Date(),
details: '正在从企业微信拉取会话数据...'
})
// 调用加载API
const loadResponse = await messageApi.loadMessages(0, 1000)
if (!loadResponse.success) {
throw new Error(loadResponse.message || '加载会话数据失败')
}
const result = loadResponse.data
updateStep('fetch_data', {
status: 'success',
endTime: new Date(),
duration: 2000, // 模拟耗时
details: `✓ 数据拉取成功\n共获取 ${result.total_fetched || 0} 条聊天记录`
})
// 步骤5: 保存到数据库
updateStep('save_db', {
status: 'running',
startTime: new Date(),
details: '开始存储消息到数据库...'
})
// 模拟数据库保存过程
await new Promise(resolve => setTimeout(resolve, 1200))
updateStep('save_db', {
status: 'success',
endTime: new Date(),
duration: 1200,
details: `数据库存储完成:\n总计消息: ${result.total_fetched || 0}\n成功存储: ${result.saved_count || 0}\n跳过重复: ${result.duplicates_skipped || 0}\n存储失败: ${result.failed_to_save || 0}`
})
// 步骤6: 完成
updateStep('complete', {
status: 'running',
startTime: new Date(),
details: '正在完成加载流程...'
})
const endTime = new Date()
const totalDuration = endTime - startTime
updateStep('complete', {
status: 'success',
endTime,
duration: 500,
details: `测试完成\n总耗时: ${(totalDuration / 1000).toFixed(2)}秒`
})
setProgress(prev => ({
...prev,
status: 'success',
endTime,
totalDuration,
result
}))
} catch (error) {
console.error('加载会话存档失败:', error)
const currentStep = progress.steps.find(step => step.status === 'running')
if (currentStep) {
updateStep(currentStep.id, {
status: 'error',
endTime: new Date(),
details: `错误: ${error.message}`
})
}
const endTime = new Date()
const totalDuration = endTime - startTime
setProgress(prev => ({
...prev,
status: 'error',
endTime,
totalDuration,
error: error.message
}))
} finally {
setLoading(false)
}
}
// 重置状态
const resetProgress = () => {
setProgress({
status: 'idle',
currentStep: 0,
steps: [
{
id: 'config',
name: '初始化配置',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'fetch_data',
name: '拉取会话数据',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'save_db',
name: '保存到数据库',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
},
{
id: 'complete',
name: '完成',
status: 'pending',
details: null,
startTime: null,
endTime: null,
duration: null
}
],
config: {
corpid: '',
secret: '',
libPath: '',
rsaKeyPath: '',
dataDir: ''
},
result: null,
error: null,
startTime: null,
endTime: null,
totalDuration: null
})
}
// 获取状态颜色
const getStatusColor = (status) => {
switch (status) {
case 'pending': return 'bg-gray-100 text-gray-800'
case 'running': return 'bg-blue-100 text-blue-800'
case 'success': return 'bg-green-100 text-green-800'
case 'error': return 'bg-red-100 text-red-800'
default: return 'bg-gray-100 text-gray-800'
}
}
// 获取状态图标
const getStatusIcon = (status) => {
switch (status) {
case 'pending': return '⏳'
case 'running': return '🔄'
case 'success': return '✅'
case 'error': return '❌'
default: return '⏳'
}
}
// 格式化时间
const formatTime = (time) => {
if (!time) return '--:--:--'
return new Date(time).toLocaleTimeString('zh-CN')
}
// 格式化时长
const formatDuration = (duration) => {
if (!duration) return '--'
return `${(duration / 1000).toFixed(2)}秒`
}
return (
<div className="space-y-6 p-6">
{/* 页面标题和操作 */}
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div>
<h1 className="text-3xl font-bold">加载会话存档</h1>
<p className="text-base-content/70 mt-2">从企业微信加载会话存档数据到数据库</p>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={startLoadArchive}
disabled={loading || progress.status === 'loading'}
className="btn btn-primary"
>
{loading || progress.status === 'loading' ? (
<>
<span className="loading loading-spinner loading-sm"></span>
加载中...
</>
) : (
<>
<span className="mr-2">🚀</span>
开始加载
</>
)}
</button>
<button
onClick={resetProgress}
disabled={loading || progress.status === 'loading'}
className="btn btn-outline"
>
<span className="mr-2">🔄</span>
重置
</button>
<button
onClick={() => navigate('/messages')}
className="btn btn-outline"
>
<span className="mr-2">←</span>
返回消息管理
</button>
</div>
</div>
{/* 整体状态 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<div className="flex items-center justify-between">
<div>
<h3 className="card-title">加载状态</h3>
<p className="text-sm text-base-content/70">
{progress.status === 'idle' && '等待开始加载...'}
{progress.status === 'loading' && '正在加载会话存档...'}
{progress.status === 'success' && '加载完成!'}
{progress.status === 'error' && '加载失败'}
</p>
</div>
<div className="text-right">
<div className="text-sm text-base-content/70">开始时间</div>
<div className="font-bold">{formatTime(progress.startTime)}</div>
</div>
<div className="text-right">
<div className="text-sm text-base-content/70">结束时间</div>
<div className="font-bold">{formatTime(progress.endTime)}</div>
</div>
<div className="text-right">
<div className="text-sm text-base-content/70">总耗时</div>
<div className="font-bold">{formatDuration(progress.totalDuration)}</div>
</div>
</div>
{progress.error && (
<div className="alert alert-error mt-4">
<span>错误: {progress.error}</span>
</div>
)}
</div>
</div>
{/* 步骤流程 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title mb-4">加载步骤流程</h3>
<div className="space-y-4">
{progress.steps.map((step, index) => (
<div key={step.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${getStatusColor(step.status)}`}>
<span className="font-bold">{index + 1}</span>
</div>
<div>
<h4 className="font-bold text-lg">{step.name}</h4>
<div className="flex items-center space-x-2 mt-1">
<span className={`text-xs px-2 py-1 rounded-full ${getStatusColor(step.status)}`}>
{getStatusIcon(step.status)} {step.status === 'pending' ? '等待中' :
step.status === 'running' ? '进行中' :
step.status === 'success' ? '成功' : '失败'}
</span>
{step.startTime && (
<span className="text-xs text-gray-500">
开始: {formatTime(step.startTime)}
</span>
)}
{step.endTime && (
<span className="text-xs text-gray-500">
结束: {formatTime(step.endTime)}
</span>
)}
{step.duration && (
<span className="text-xs text-gray-500">
耗时: {formatDuration(step.duration)}
</span>
)}
</div>
</div>
</div>
</div>
{step.details && (
<div className="mt-3 ml-11">
<div className="bg-gray-50 rounded p-3">
<pre className="text-sm whitespace-pre-wrap font-mono">{step.details}</pre>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* 结果详情 */}
{progress.result && (
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title mb-4">加载结果详情</h3>
<div className="overflow-x-auto">
<table className="table table-zebra">
<thead>
<tr>
<th>指标</th>
<th>数值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>拉取消息总数</td>
<td className="font-bold">{progress.result.total_fetched || 0}</td>
<td>从企业微信拉取的消息总数</td>
</tr>
<tr>
<td>加密消息数量</td>
<td className="font-bold">{progress.result.encrypted_count || 0}</td>
<td>需要解密的加密消息数量</td>
</tr>
<tr>
<td>成功解密</td>
<td className="font-bold text-green-600">{progress.result.decrypted_success || 0}</td>
<td>成功解密的加密消息数量</td>
</tr>
<tr>
<td>解密失败</td>
<td className="font-bold text-red-600">{progress.result.decrypted_failed || 0}</td>
<td>解密失败的消息数量</td>
</tr>
<tr>
<td>成功存储</td>
<td className="font-bold text-green-600">{progress.result.saved_count || 0}</td>
<td>成功保存到数据库的消息数量</td>
</tr>
<tr>
<td>跳过重复</td>
<td className="font-bold text-yellow-600">{progress.result.duplicates_skipped || 0}</td>
<td>数据库中已存在,跳过存储的消息数量</td>
</tr>
<tr>
<td>存储失败</td>
<td className="font-bold text-red-600">{progress.result.failed_to_save || 0}</td>
<td>存储到数据库失败的消息数量</td>
</tr>
<tr>
<td>最大seq值</td>
<td className="font-bold">{progress.result.max_seq || 0}</td>
<td>本次拉取的最大seq值,用于下次拉取</td>
</tr>
<tr>
<td>建议下次seq</td>
<td className="font-bold">{progress.result.next_seq || (progress.result.max_seq || 0) + 1}</td>
<td>下次拉取建议使用的seq值</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
)
}
export default LoadArchive

View File

@@ -1,32 +1,306 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { messageApi } from '../services/api'
const Messages = () => {
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([
{ id: 1, from: '张三', content: '你好,我想了解一下产品详情', time: '2025-12-12 10:30', type: 'text', room: '单聊' },
{ id: 2, from: '李四', content: '这是会议纪要文件', time: '2025-12-12 09:15', type: 'file', room: '群聊-项目组' },
{ id: 3, from: '王五', content: '产品图片已发送', time: '2025-12-11 16:45', type: 'image', room: '单聊' },
{ id: 4, from: '赵六', content: '关于下周会议安排', time: '2025-12-11 14:20', type: 'text', room: '群聊-管理组' },
{ id: 5, from: '钱七', content: '语音消息', time: '2025-12-10 11:10', type: 'voice', room: '单聊' },
])
const [messages, setMessages] = useState([])
const [pagination, setPagination] = useState({
page: 1,
pageSize: 20,
total: 0,
totalPages: 0
})
const [filters, setFilters] = useState({
msgtype: '',
roomType: '',
search: ''
})
const [stats, setStats] = useState({
today: { text: 0, image: 0, file: 0, voice: 0 },
loadStatus: { lastLoadTime: '', successRate: 0 }
})
const handleLoadArchive = async () => {
setLoading(true)
// 模拟 API 调用
setTimeout(() => {
setLoading(false)
alert('会话存档加载成功!')
}, 1500)
// 抽屉状态
const [selectedMessage, setSelectedMessage] = useState(null)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
// 数据格式转换函数
const formatMessage = (backendMsg) => {
// 处理发送者显示名称
let fromDisplay = '未知用户'
if (backendMsg.from_user) {
// 如果是企业微信用户ID,可以尝试映射或截断显示
if (backendMsg.from_user.startsWith('wmx')) {
fromDisplay = `用户${backendMsg.from_user.slice(-6)}`
} else {
fromDisplay = backendMsg.from_user
}
}
// 处理消息内容
let content = '无内容'
if (backendMsg.text && backendMsg.text.content) {
content = backendMsg.text.content
} else if (backendMsg.msgtype === 'image') {
content = '[图片消息]'
} else if (backendMsg.msgtype === 'file') {
content = '[文件消息]'
} else if (backendMsg.msgtype === 'voice') {
content = '[语音消息]'
}
// 处理时间显示
let timeDisplay = '未知时间'
if (backendMsg.msgtime) {
try {
const date = new Date(backendMsg.msgtime)
timeDisplay = date.toLocaleString('zh-CN')
} catch (e) {
timeDisplay = backendMsg.msgtime
}
}
// 处理聊天类型
let roomDisplay = '单聊'
if (backendMsg.roomid) {
roomDisplay = `群聊-${backendMsg.roomid.slice(-8)}`
}
return {
id: backendMsg.msgid || backendMsg._id,
from: fromDisplay,
content: content,
time: timeDisplay,
type: backendMsg.msgtype || 'text',
room: roomDisplay,
rawData: backendMsg // 保留原始数据供详细查看使用
}
}
const handleSearch = (e) => {
// 加载消息列表 - 使用React 19的use hook
const loadMessages = async (page = 1, filterParams = {}) => {
setLoading(true)
try {
const currentFilters = { ...filters, ...filterParams }
const params = {
page,
page_size: pagination.pageSize
}
// 如果有消息类型过滤
if (currentFilters.msgtype) {
params.msgtype = currentFilters.msgtype
}
// 如果有群聊过滤
if (currentFilters.roomType === 'group') {
params.roomid = { $exists: true, $ne: null }
} else if (currentFilters.roomType === 'single') {
params.roomid = null
}
// 如果有搜索关键词
if (currentFilters.search) {
params.search = currentFilters.search
}
const response = await messageApi.getMessages(params)
if (response.success) {
const formattedMessages = response.data.messages.map(formatMessage)
setMessages(formattedMessages)
setPagination({
page: response.data.page,
pageSize: response.data.page_size,
total: response.data.total,
totalPages: response.data.total_pages
})
// 更新统计信息
updateStats(formattedMessages)
}
} catch (error) {
console.error('加载消息失败:', error)
alert('加载消息失败,请检查网络连接或后端服务')
} finally {
setLoading(false)
}
}
// 更新统计信息
const updateStats = (messages) => {
// 今日消息统计(简化版,实际应该从后端获取)
const today = new Date().toISOString().split('T')[0]
const todayMessages = messages.filter(msg =>
msg.time.includes(today.replace(/-/g, '/'))
)
const todayStats = {
text: todayMessages.filter(m => m.type === 'text').length,
image: todayMessages.filter(m => m.type === 'image').length,
file: todayMessages.filter(m => m.type === 'file').length,
voice: todayMessages.filter(m => m.type === 'voice').length
}
setStats(prev => ({
...prev,
today: todayStats,
loadStatus: {
lastLoadTime: new Date().toLocaleString('zh-CN'),
successRate: messages.length > 0 ? 95 : 0
}
}))
}
// 获取统计信息用于显示(如果需要)
const getStatsDisplay = () => {
return {
today: stats.today,
loadStatus: stats.loadStatus
}
}
// 跳转到加载会话存档页面
const handleLoadArchive = () => {
navigate('/load-archive')
}
// 搜索处理
const handleSearch = async (e) => {
e.preventDefault()
// 搜索逻辑
alert('搜索功能开发中...')
console.log('执行搜索操作')
await loadMessages(1)
}
// 过滤条件变化处理
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }))
}
// 分页处理
const handlePageChange = (page) => {
if (page >= 1 && page <= pagination.totalPages) {
console.log(`切换到第 ${page} 页`)
loadMessages(page)
}
}
// 查看消息详情
const handleViewMessage = (message) => {
setSelectedMessage(message)
setIsDrawerOpen(true)
}
// 关闭抽屉
const closeDrawer = () => {
setIsDrawerOpen(false)
setSelectedMessage(null)
}
// 组件加载时初始化数据
useEffect(() => {
console.log(`Messages组件加载,初始化数据`)
loadMessages(1)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 渲染消息内容
const renderMessageContent = () => {
if (!selectedMessage) return null
const { rawData } = selectedMessage
switch (selectedMessage.type) {
case 'text':
return (
<div className="mt-4">
<div className="font-semibold mb-2">消息内容:</div>
<div className="bg-base-200 rounded-lg p-4 whitespace-pre-wrap break-words">
{rawData.text?.content || selectedMessage.content}
</div>
</div>
)
case 'image':
return (
<div className="mt-4">
<div className="font-semibold mb-2">图片消息:</div>
<div className="bg-base-200 rounded-lg p-4">
{rawData.image?.md5sum ? (
<div className="space-y-2">
<div className="text-sm">图片MD5: {rawData.image.md5sum}</div>
<div className="text-sm">图片大小: {rawData.image.filesize || '未知'} bytes</div>
<div className="text-sm text-base-content/70">
(图片文件需要从企业微信服务器下载)
</div>
</div>
) : (
<div className="text-base-content/70">无图片详细信息</div>
)}
</div>
</div>
)
case 'file':
return (
<div className="mt-4">
<div className="font-semibold mb-2">文件消息:</div>
<div className="bg-base-200 rounded-lg p-4">
{rawData.file ? (
<div className="space-y-2">
<div className="text-sm">文件名: {rawData.file.filename || '未知'}</div>
<div className="text-sm">文件大小: {rawData.file.filesize || '未知'} bytes</div>
<div className="text-sm">文件扩展名: {rawData.file.fileext || '未知'}</div>
<div className="text-sm text-base-content/70">
(文件需要从企业微信服务器下载)
</div>
</div>
) : (
<div className="text-base-content/70">无文件详细信息</div>
)}
</div>
</div>
)
case 'voice':
return (
<div className="mt-4">
<div className="font-semibold mb-2">语音消息:</div>
<div className="bg-base-200 rounded-lg p-4">
{rawData.voice ? (
<div className="space-y-2">
<div className="text-sm">语音时长: {rawData.voice.play_length || '未知'} 秒</div>
<div className="text-sm">文件大小: {rawData.voice.filesize || '未知'} bytes</div>
<div className="text-sm text-base-content/70">
(语音文件需要从企业微信服务器下载)
</div>
</div>
) : (
<div className="text-base-content/70">无语音详细信息</div>
)}
</div>
</div>
)
default:
return (
<div className="mt-4">
<div className="font-semibold mb-2">消息内容:</div>
<div className="bg-base-200 rounded-lg p-4">
{selectedMessage.content}
</div>
</div>
)
}
}
return (
<div className="space-y-6">
<div className="drawer drawer-end">
<input
id="message-drawer"
type="checkbox"
className="drawer-toggle"
checked={isDrawerOpen}
onChange={() => {}} // 只读,通过状态控制
/>
<div className="drawer-content space-y-6">
{/* 页面标题和操作 */}
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div>
@@ -51,14 +325,14 @@ const Messages = () => {
</>
)}
</button>
<button className="btn btn-outline">
{/* <button className="btn btn-outline">
<span className="mr-2">📊</span>
查看统计
</button>
<button className="btn btn-outline">
<span className="mr-2">📤</span>
导出数据
</button>
</button> */}
</div>
</div>
@@ -71,24 +345,53 @@ const Messages = () => {
type="text"
placeholder="搜索消息内容、发送者..."
className="input input-bordered w-full"
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
/>
</div>
<div className="flex gap-2">
<select className="select select-bordered">
<select
className="select select-bordered"
value={filters.msgtype}
onChange={(e) => handleFilterChange('msgtype', e.target.value)}
>
<option value="">所有消息类型</option>
<option value="text">文本消息</option>
<option value="image">图片消息</option>
<option value="file">文件消息</option>
<option value="voice">语音消息</option>
</select>
<select className="select select-bordered">
<select
className="select select-bordered"
value={filters.roomType}
onChange={(e) => handleFilterChange('roomType', e.target.value)}
>
<option value="">所有聊天类型</option>
<option value="single">单聊</option>
<option value="group">群聊</option>
</select>
<button type="submit" className="btn btn-primary">
<span className="mr-2">🔍</span>
搜索
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? (
<>
<span className="loading loading-spinner loading-sm mr-2"></span>
搜索中...
</>
) : (
<>
<span className="mr-2">🔍</span>
搜索
</>
)}
</button>
<button
type="button"
className="btn btn-outline"
onClick={() => {
setFilters({ msgtype: '', roomType: '', search: '' })
loadMessages(1)
}}
>
重置
</button>
</div>
</form>
@@ -153,14 +456,13 @@ const Messages = () => {
<td>{message.time}</td>
<td>
<div className="flex gap-2">
<button className="btn btn-ghost btn-xs">
<button
className="btn btn-ghost btn-xs"
onClick={() => handleViewMessage(message)}
>
<span className="mr-1">👁️</span>
查看
</button>
<button className="btn btn-ghost btn-xs">
<span className="mr-1">📋</span>
复制
</button>
</div>
</td>
</tr>
@@ -172,90 +474,164 @@ const Messages = () => {
{/* 分页 */}
<div className="flex items-center justify-between pt-4">
<div className="text-sm text-base-content/70">
显示 1-5 条,共 1234 条消息
</div>
<div className="join">
<button className="join-item btn btn-sm">«</button>
<button className="join-item btn btn-sm btn-active">1</button>
<button className="join-item btn btn-sm">2</button>
<button className="join-item btn btn-sm">3</button>
<button className="join-item btn btn-sm">4</button>
<button className="join-item btn btn-sm">»</button>
{messages.length > 0 ? (
`显示 ${(pagination.page - 1) * pagination.pageSize + 1}-${Math.min(pagination.page * pagination.pageSize, pagination.total)} 条,共 ${pagination.total} 条消息`
) : (
'暂无消息数据'
)}
</div>
{pagination.totalPages > 1 && (
<div className="join">
<button
className="join-item btn btn-sm"
onClick={() => handlePageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
>
«
</button>
{Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
let pageNum = i + 1
if (pagination.totalPages > 5) {
if (pagination.page <= 3) {
pageNum = i + 1
} else if (pagination.page >= pagination.totalPages - 2) {
pageNum = pagination.totalPages - 4 + i
} else {
pageNum = pagination.page - 2 + i
}
}
return (
<button
key={pageNum}
className={`join-item btn btn-sm ${pagination.page === pageNum ? 'btn-active' : ''}`}
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</button>
)
})}
<button
className="join-item btn btn-sm"
onClick={() => handlePageChange(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
>
»
</button>
</div>
)}
</div>
</div>
</div>
{/* 统计信息 */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">今日消息统计</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span>文本消息</span>
<span className="font-bold">45 条</span>
</div>
{/* 抽屉侧边栏 */}
<div className="drawer-side">
<label
htmlFor="message-drawer"
className="drawer-overlay"
onClick={closeDrawer}
></label>
<div className="bg-base-100 w-full max-w-md h-full overflow-y-auto">
{selectedMessage && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">消息详情</h2>
<button
onClick={closeDrawer}
className="btn btn-ghost btn-sm"
>
✕
</button>
</div>
<div className="flex justify-between">
<span>图片消息</span>
<span className="font-bold">12 条</span>
</div>
<div className="flex justify-between">
<span>文件消息</span>
<span className="font-bold">8 条</span>
</div>
<div className="flex justify-between">
<span>语音消息</span>
<span className="font-bold">3 条</span>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">加载状态</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between mb-1">
<span>上次加载时间</span>
<span className="font-bold">2025-12-12 10:30</span>
{/* 消息基本信息 */}
<div className="space-y-4">
<div>
<div className="font-semibold mb-1">发送者:</div>
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-neutral text-neutral-content w-10 rounded-full">
<span className="text-sm">{selectedMessage.from.charAt(0)}</span>
</div>
</div>
<div>
<div className="font-bold">{selectedMessage.from}</div>
<div className="text-sm text-base-content/70">
ID: {selectedMessage.rawData.from_user || '未知'}
</div>
</div>
</div>
</div>
<progress className="progress progress-primary w-full" value="100" max="100"></progress>
</div>
<div>
<div className="flex justify-between mb-1">
<span>加载成功率</span>
<span className="font-bold">98.5%</span>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="font-semibold mb-1">消息类型:</div>
<span className={`badge ${selectedMessage.type === 'text' ? 'badge-info' : selectedMessage.type === 'image' ? 'badge-success' : selectedMessage.type === 'file' ? 'badge-warning' : 'badge-error'}`}>
{selectedMessage.type === 'text' ? '文本' : selectedMessage.type === 'image' ? '图片' : selectedMessage.type === 'file' ? '文件' : '语音'}
</span>
</div>
<div>
<div className="font-semibold mb-1">聊天类型:</div>
<span className={`badge ${selectedMessage.room.includes('群聊') ? 'badge-primary' : 'badge-secondary'}`}>
{selectedMessage.room}
</span>
</div>
</div>
<div>
<div className="font-semibold mb-1">发送时间:</div>
<div className="text-lg">{selectedMessage.time}</div>
<div className="text-sm text-base-content/70">
原始时间戳: {selectedMessage.rawData.msgtime || '未知'}
</div>
</div>
{selectedMessage.rawData.tolist && selectedMessage.rawData.tolist.length > 0 && (
<div>
<div className="font-semibold mb-1">接收者:</div>
<div className="bg-base-200 rounded-lg p-3">
{selectedMessage.rawData.tolist.map((to, index) => (
<div key={index} className="text-sm">
{to}
</div>
))}
</div>
</div>
)}
{/* 消息内容 */}
{renderMessageContent()}
{/* 原始数据 */}
<div className="mt-6">
<div className="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div className="collapse-title font-semibold">
查看原始数据
</div>
<div className="collapse-content">
<pre className="text-xs bg-base-300 p-3 rounded-lg overflow-x-auto">
{JSON.stringify(selectedMessage.rawData, null, 2)}
</pre>
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex gap-2 mt-6 pt-4 border-t border-base-300">
<button className="btn btn-primary flex-1">
<span className="mr-2">📋</span>
复制内容
</button>
<button className="btn btn-outline flex-1">
<span className="mr-2">📤</span>
导出
</button>
</div>
<progress className="progress progress-success w-full" value="98.5" max="100"></progress>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">快速操作</h3>
<div className="space-y-2">
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">🔄</span>
刷新消息列表
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">🧹</span>
清理旧数据
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">📋</span>
批量操作
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">⚙️</span>
消息设置
</button>
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
// API 基础配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
// 创建 axios 实例
const apiClient = axios.create({
@@ -88,6 +88,10 @@ export const messageApi = {
// 获取单个消息详情
getMessageById: (msgid) =>
apiClient.get(`/messages/${msgid}`),
// 获取配置信息
getConfig: () =>
apiClient.get('/messages/config'),
}
// 认证相关 API