更新后台管理消息列表;
This commit is contained in:
2
backstage/.env.sample
Normal file
2
backstage/.env.sample
Normal file
@@ -0,0 +1,2 @@
|
||||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://mp.yizhezhe.com/api
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 主内容区 */}
|
||||
|
||||
@@ -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 工具
|
||||
|
||||
// 模拟延迟
|
||||
@@ -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>
|
||||
|
||||
499
backstage/src/pages/LoadArchive.jsx
Normal file
499
backstage/src/pages/LoadArchive.jsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user