Files
openclaw-monitor/code/frontend/public/index.html
flying-hero 57fa27c616 feat: 添加记忆功能 - 日记查看器和日历组件
- 后端 API: 获取日记日期列表和详情
- 前端组件:记忆弹窗、日历组件
- 点击记忆按钮查看龙虾工作日记
- 日历高亮显示有日记的日期
2026-04-01 22:36:06 +08:00

510 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🦞 龙虾舰队监控中心</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a365d 0%, #2c5282 100%);
min-height: 100vh;
padding: 20px;
color: #fff;
}
.container { max-width: 1400px; margin: 0 auto; }
.header {
text-align: center;
padding: 30px 0;
border-bottom: 2px solid rgba(255,255,255,0.1);
margin-bottom: 30px;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.8; font-size: 1.1em; }
.overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: rgba(255,255,255,0.1);
border-radius: 12px;
padding: 25px;
text-align: center;
backdrop-filter: blur(10px);
}
.stat-value { font-size: 2.5em; font-weight: bold; margin-bottom: 5px; }
.stat-label { opacity: 0.8; font-size: 0.95em; }
.lobsters {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
}
.lobster-card {
background: rgba(255,255,255,0.95);
border-radius: 16px;
padding: 25px;
color: #1a365d;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.lobster-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.lobster-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.lobster-name { font-size: 1.5em; font-weight: bold; }
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-healthy { background: #48bb78; }
.status-error { background: #f56565; }
.lobster-info { margin-bottom: 15px; }
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.info-label { opacity: 0.7; font-size: 0.9em; }
.info-value { font-weight: 500; }
.lobster-actions {
display: flex;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 2px solid rgba(0,0,0,0.1);
}
.action-btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.btn-memory { background: #9f7aea; color: white; }
.btn-memory:hover { background: #805ad5; }
.refresh-time { margin-top: 10px; font-size: 0.9em; opacity: 0.6; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🦞 龙虾舰队监控中心</h1>
<p>实时监控龙虾舰队运行状态</p>
<div class="refresh-time">最后更新:<span id="lastUpdate">-</span></div>
</div>
<div class="overview">
<div class="stat-card">
<div class="stat-value" id="totalLobsters">6</div>
<div class="stat-label">龙虾总数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="healthyCount" style="color: #48bb78;">-</div>
<div class="stat-label">健康运行</div>
</div>
<div class="stat-card">
<div class="stat-value" id="errorCount" style="color: #f56565;">-</div>
<div class="stat-label">异常</div>
</div>
</div>
<div class="lobsters" id="lobsterGrid">
<!-- 龙虾卡片将通过 JS 动态生成 -->
</div>
</div>
<script>
// 龙虾配置
const lobsters = [
{ id: 1, name: '飞行侠', emoji: '🦸', port: 18789, specialty: '主力/通用' },
{ id: 2, name: '道童', emoji: '☯️', port: 18889, specialty: '道德经注解' },
{ id: 3, name: '墨子', emoji: '🔧', port: 18689, specialty: '代码专家' },
{ id: 4, name: '织网者', emoji: '🕸️', port: 18589, specialty: '网站制作' },
{ id: 5, name: '费曼', emoji: '⚛️', port: 18989, specialty: '物理研究' },
{ id: 6, name: '守望者', emoji: '👁️', port: 18080, specialty: '舰队监控' },
];
// 当前打开的记忆弹窗
let currentMemoryModal = null;
// 打开记忆弹窗
function openMemory(lobster) {
if (currentMemoryModal) {
currentMemoryModal.remove();
}
const modal = document.createElement('div');
modal.className = 'memory-modal-container';
document.body.appendChild(modal);
// 加载记忆组件
loadMemoryComponent(lobster, modal);
}
async function loadMemoryComponent(lobster, container) {
try {
// 获取有日记的日期
const datesResp = await fetch(`http://localhost:8000/api/lobsters/${lobster.id}/memory/dates/`);
const datesData = await datesResp.json();
const dates = datesData.dates || [];
// 默认显示最新日记
let selectedDate = dates.length > 0 ? dates[0] : '';
let content = '这一天还没有日记';
if (selectedDate) {
const contentResp = await fetch(`http://localhost:8000/api/lobsters/${lobster.id}/memory/${selectedDate}/`);
const contentData = await contentResp.json();
content = contentData.content || '日记内容为空';
}
renderMemoryModal(lobster, dates, selectedDate, content, container);
} catch (error) {
container.innerHTML = `<div class="memory-error">加载失败:${error.message}</div>`;
}
}
function renderMemoryModal(lobster, dates, selectedDate, content, container) {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
container.innerHTML = `
<div class="memory-overlay" onclick="closeMemory()">
<div class="memory-dialog" onclick="event.stopPropagation()">
<div class="memory-header">
<h2>📔 ${lobster.emoji} ${lobster.name} - 工作日记</h2>
<button class="memory-close" onclick="closeMemory()">×</button>
</div>
<div class="memory-body">
<div class="memory-content-area">
${content ? `
<div class="memory-date-title">📅 ${selectedDate}</div>
<pre>${escapeHtml(content)}</pre>
` : '<div class="memory-empty">这一天还没有日记</div>'}
</div>
<div class="memory-calendar">
<div class="calendar-nav">
<button onclick="changeMonth(-1)">◀</button>
<span>${year}${month + 1}月</span>
<button onclick="changeMonth(1)">▶</button>
</div>
<div class="calendar-grid">
${['日', '一', '二', '三', '四', '五', '六'].map(d => `<div class="cal-weekday">${d}</div>`).join('')}
${renderCalendarDays(year, month, dates, selectedDate, lobster.id)}
</div>
<div class="calendar-legend">
<span class="legend-item"><span class="legend-dot has-memory"></span>有日记</span>
<span class="legend-item"><span class="legend-dot no-memory"></span>无日记</span>
</div>
</div>
</div>
</div>
</div>
<style>
.memory-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.memory-dialog {
background: white;
border-radius: 16px;
width: 90%;
max-width: 1000px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.memory-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e2e8f0;
}
.memory-header h2 {
color: #1a365d;
margin: 0;
}
.memory-close {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #718096;
}
.memory-body {
display: grid;
grid-template-columns: 1fr 350px;
gap: 20px;
padding: 20px;
overflow: hidden;
}
.memory-content-area {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
overflow-y: auto;
max-height: 600px;
}
.memory-date-title {
color: #553c9a;
margin-bottom: 15px;
font-size: 1.2em;
}
.memory-content-area pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #2d3748;
line-height: 1.6;
}
.memory-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #a0aec0;
}
.memory-calendar {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
background: white;
}
.calendar-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.calendar-nav button {
background: #4299e1;
color: white;
border: none;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.cal-weekday {
text-align: center;
font-size: 0.8em;
color: #718096;
padding: 8px 0;
}
.cal-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
font-size: 0.9em;
cursor: pointer;
}
.cal-day:hover:not(.empty) {
background: #e2e8f0;
}
.cal-day.has-memory {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: bold;
}
.cal-day.selected {
border: 2px solid #ed8936;
}
.calendar-legend {
margin-top: 15px;
display: flex;
gap: 15px;
font-size: 0.85em;
}
.legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.legend-dot.has-memory {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.legend-dot.no-memory {
background: #e2e8f0;
}
.memory-error {
padding: 20px;
color: #f56565;
}
@media (max-width: 768px) {
.memory-body {
grid-template-columns: 1fr;
}
.memory-calendar {
order: -1;
}
}
</style>
`;
currentMemoryModal = container;
}
function renderCalendarDays(year, month, dates, selectedDate, lobsterId) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startWeekday = firstDay.getDay();
let html = '';
// 空白
for (let i = 0; i < startWeekday; i++) {
html += '<div class="cal-day empty"></div>';
}
// 日期
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const hasMemory = dates.includes(dateStr);
const isSelected = selectedDate === dateStr;
html += `<div class="cal-day ${hasMemory ? 'has-memory' : ''} ${isSelected ? 'selected' : ''}"
onclick="selectDate('${dateStr}', ${lobsterId})">${day}</div>`;
}
return html;
}
function selectDate(date, lobsterId) {
// 重新加载选中日期的日记
const lobster = lobsters.find(l => l.id === lobsterId);
if (lobster && currentMemoryModal) {
currentMemoryModal.innerHTML = '';
loadMemoryComponent(lobster, currentMemoryModal);
}
}
function changeMonth(delta) {
// 简化:刷新页面重新计算月份
location.reload();
}
function closeMemory() {
if (currentMemoryModal) {
currentMemoryModal.remove();
currentMemoryModal = null;
}
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// 渲染龙虾卡片
function renderLobsterCard(lobster) {
const status = 'healthy';
const statusClass = status === 'healthy' ? 'status-healthy' : 'status-error';
const statusText = status === 'healthy' ? '健康' : '异常';
return `
<div class="lobster-card">
<div class="lobster-header">
<div class="lobster-name">
${lobster.emoji} ${lobster.name}
</div>
<span class="status-indicator ${statusClass}" title="${statusText}"></span>
</div>
<div class="lobster-info">
<div class="info-row">
<span class="info-label">专长</span>
<span class="info-value">${lobster.specialty}</span>
</div>
<div class="info-row">
<span class="info-label">端口</span>
<span class="info-value">${lobster.port}</span>
</div>
<div class="info-row">
<span class="info-label">状态</span>
<span class="info-value" style="color: ${status === 'healthy' ? '#48bb78' : '#f56565'}">${statusText}</span>
</div>
</div>
<div class="lobster-actions">
<button class="action-btn btn-memory" onclick="openMemory(lobsters.find(l => l.id === ${lobster.id}))">🧠 记忆</button>
</div>
</div>
`;
}
// 更新监控数据
function updateDashboard() {
const grid = document.getElementById('lobsterGrid');
let healthy = 0, error = 0;
const results = lobsters.map(l => {
healthy++;
return { lobster: l, status: 'healthy' };
});
grid.innerHTML = results.map(({ lobster }) =>
renderLobsterCard(lobster)
).join('');
document.getElementById('totalLobsters').textContent = lobsters.length;
document.getElementById('healthyCount').textContent = healthy;
document.getElementById('errorCount').textContent = error;
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN');
}
// 初始化
updateDashboard();
setInterval(updateDashboard, 5000); // 每 5 秒刷新
</script>
</body>
</html>