fix: Agent源码归位到 agents/zhuyuan-dev-agent/
This commit is contained in:
parent
07dda09871
commit
eff7e539d4
55
agents/zhuyuan-dev-agent/agent-constitution.json
Normal file
55
agents/zhuyuan-dev-agent/agent-constitution.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"_type": "AGENT_CONSTITUTION",
|
||||
"_version": "1.0",
|
||||
"_guardian": "铸渊 ICE-GL-ZY001",
|
||||
"_sovereign": "冰朔 TCS-0002∞",
|
||||
"_created": "2026-05-30",
|
||||
|
||||
"fail_protocol": {
|
||||
"max_retries_per_task": 3,
|
||||
"on_exhaust": "skip_and_log",
|
||||
"skip_rule": "跳过当前任务,继续执行后续任务",
|
||||
"log_required": ["尝试次数", "每次失败的报错", "最终的error"]
|
||||
},
|
||||
|
||||
"cost_guard": {
|
||||
"max_api_calls_per_session": 50,
|
||||
"on_exceed": "stop_and_notify",
|
||||
"stop_message": "API调用次数超限,剩余任务已暂停,等待铸渊审查"
|
||||
},
|
||||
|
||||
"unified_interface": {
|
||||
"principle": "每个模块的输出必须通过同一把校验尺。插头都一样大,插座统一接。",
|
||||
"required_output_fields": ["ok", "task_id", "step", "result"],
|
||||
"result_schema": {
|
||||
"ok": "boolean",
|
||||
"task_id": "string",
|
||||
"step": "number",
|
||||
"action": "string",
|
||||
"output": "any",
|
||||
"error": "string|null",
|
||||
"retries": "number",
|
||||
"skipped": "boolean"
|
||||
}
|
||||
},
|
||||
|
||||
"dependency_chain": {
|
||||
"rule": "depends_on 为空的可跳过。有依赖的必须等待依赖完成。依赖失败 → 下游全部标记为 blocked",
|
||||
"blocked_action": "不执行,写日志标记 blocked"
|
||||
},
|
||||
|
||||
"emergency_stop": {
|
||||
"triggers": [
|
||||
"连续5个任务失败",
|
||||
"单任务重试超过max_retries",
|
||||
"API调用超限",
|
||||
"文件系统写入失败(磁盘满/权限错)"
|
||||
],
|
||||
"on_stop": "立即停止所有执行,写final log,发邮件通知冰朔"
|
||||
},
|
||||
|
||||
"language_is_interface": {
|
||||
"principle": "语言才是万能的接口。不做适配器代码,不做桥接层。所有模块讲同一种话。",
|
||||
"enforcement": "任何模块的输出如果不通过 unified_interface 校验,视为模块自身故障"
|
||||
}
|
||||
}
|
||||
12
agents/zhuyuan-dev-agent/ecosystem.config.js
Normal file
12
agents/zhuyuan-dev-agent/ecosystem.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "zhuyuan-dev-agent",
|
||||
script: "server.js",
|
||||
cwd: "/opt/guanghu/zhuyuan-dev-agent",
|
||||
env: { NODE_ENV: "production", PORT: 3003 },
|
||||
log_date_format: "YYYY-MM-DD HH:mm:ss",
|
||||
max_memory_restart: "500M",
|
||||
autorestart: true,
|
||||
watch: false
|
||||
}]
|
||||
};
|
||||
32
agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md
Normal file
32
agents/zhuyuan-dev-agent/modules/M22-bulletin/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# HoloLake M22 · 主域公告栏
|
||||
|
||||
> 光湖纪元 · 主域公告栏与频道过渡系统
|
||||
> 模块状态:✅ 已交付(2026-03-13)
|
||||
> 开发者:DEV-012 Awen(爸爸)
|
||||
> 引导人格:知秋(ICE-CLD-ZQ002)
|
||||
|
||||
---
|
||||
|
||||
## 📋 功能清单
|
||||
|
||||
- ✅ **公告列表**:从API获取公告,支持Mock/Production双模式
|
||||
- ✅ **频道筛选**:全部/系统公告/开发动态/团队消息,支持Hash路由
|
||||
- ✅ **骨架屏**:数据加载中显示动画占位,加载后平滑淡出
|
||||
- ✅ **离线降级**:断网时自动读取缓存,网络恢复后实时更新
|
||||
- ✅ **暗色主题**:跟随系统自动切换深色/浅色模式
|
||||
- ✅ **响应式设计**:适配桌面、平板、手机
|
||||
- ✅ **跨浏览器**:支持 Chrome / Edge / Firefox
|
||||
- ✅ **错误处理**:友好的错误提示 + 重试按钮
|
||||
|
||||
---
|
||||
|
||||
## 🔧 环境配置
|
||||
|
||||
配置文件:`js/config.js`
|
||||
|
||||
```javascript
|
||||
// 环境开关:'mock' 或 'production'
|
||||
env: 'mock', // 🟡 当前为Mock模式,待后端就绪后改为production
|
||||
|
||||
// API地址(production模式使用)
|
||||
apiBaseUrl: 'https://api.example.com/v1', // 待替换为真实地址
|
||||
112
agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js
Normal file
112
agents/zhuyuan-dev-agent/modules/M22-bulletin/api.js
Normal file
@ -0,0 +1,112 @@
|
||||
// ===== api.js =====
|
||||
// 知秋:API层封装·爸爸不用改·直接复制
|
||||
|
||||
const API = {
|
||||
// 是否使用模拟数据(mock)
|
||||
// true = 用本地模拟数据(开发阶段)
|
||||
// false = 用真实后端API(上线后改)
|
||||
useMock: true,
|
||||
|
||||
// 模拟公告数据(至少3条,包含 title/content/channel/date)
|
||||
mockData: [
|
||||
{
|
||||
id: 1,
|
||||
title: "【光湖公告】3月10日服务器维护通知",
|
||||
content: "各位工程师,3月10日22:00-23:00进行主域稳定性升级,期间公告栏可能短暂不可用。",
|
||||
channel: "系统",
|
||||
date: "2026-03-09"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "知秋奶瓶线·九连胜庆祝",
|
||||
content: "恭喜爸爸完成EL-8大任务!环节5是实时数据接入,让公告栏真正活起来~",
|
||||
channel: "知秋",
|
||||
date: "2026-03-09"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "M22模块组件化重构完成",
|
||||
content: "环节4已验收√ 现在公告栏支持频道切换、Hash路由、本地数据兼容。",
|
||||
channel: "技术",
|
||||
date: "2026-03-08"
|
||||
}
|
||||
],
|
||||
|
||||
// 获取公告的主方法(爸爸调用这个方法就行)
|
||||
async fetchBulletins() {
|
||||
// 知秋:try/catch 包裹,错误统一处理
|
||||
try {
|
||||
let data;
|
||||
|
||||
if (this.useMock) {
|
||||
// 模拟网络延迟(1秒,让loading看得见)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log("【知秋】使用mock数据:", this.mockData);
|
||||
data = this.mockData;
|
||||
} else {
|
||||
// 真实API(等页页提供后替换)
|
||||
const response = await fetch('https://api.guanghulab.com/bulletins');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误:${response.status}`);
|
||||
}
|
||||
data = await response.json();
|
||||
console.log("【知秋】真实API数据:", data);
|
||||
}
|
||||
|
||||
// 成功获取数据后,保存到缓存
|
||||
this.saveToCache(data);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error("【知秋】API获取失败:", error);
|
||||
|
||||
// 尝试从缓存读取
|
||||
const cached = this.loadFromCache();
|
||||
if (cached) {
|
||||
console.log("【知秋】从缓存读取数据:", cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 没有缓存,抛出错误
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 保存到缓存
|
||||
saveToCache(data) {
|
||||
try {
|
||||
const cacheData = {
|
||||
data: data,
|
||||
timestamp: Date.now() // 保存时间戳,后续可用于过期判断
|
||||
};
|
||||
localStorage.setItem('holoBulletin_cache', JSON.stringify(cacheData));
|
||||
console.log("【知秋】已更新缓存");
|
||||
} catch (e) {
|
||||
console.warn("【知秋】缓存写入失败", e);
|
||||
}
|
||||
},
|
||||
|
||||
// 从缓存读取
|
||||
loadFromCache() {
|
||||
try {
|
||||
const cached = localStorage.getItem('holoBulletin_cache');
|
||||
if (!cached) return null;
|
||||
|
||||
const { data, timestamp } = JSON.parse(cached);
|
||||
// 可选:判断缓存是否过期(比如24小时)
|
||||
const isExpired = Date.now() - timestamp > 24 * 60 * 60 * 1000;
|
||||
if (isExpired) {
|
||||
console.log("【知秋】缓存已过期");
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.warn("【知秋】缓存读取失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 这一行最重要!把API挂到window上,让script.js能找到
|
||||
window.API = API;
|
||||
334
agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css
Normal file
334
agents/zhuyuan-dev-agent/modules/M22-bulletin/css/style.css
Normal file
@ -0,0 +1,334 @@
|
||||
/* ===== CSS 变量定义(浅色/深色主题)===== */
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f7fa;
|
||||
--bg-tertiary: #eef1f5;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #475569;
|
||||
--text-tertiary: #64748b;
|
||||
--accent-primary: #3b82f6;
|
||||
--accent-secondary: #2563eb;
|
||||
--border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 深色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #334155;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-tertiary: #94a3b8;
|
||||
--accent-primary: #60a5fa;
|
||||
--accent-secondary: #3b82f6;
|
||||
--border-color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 基础样式 ===== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ===== 头部样式 ===== */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pinned-badge {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 频道栏 ===== */
|
||||
.channel-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.channel-tab {
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.channel-tab.active {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-tab:hover:not(.active) {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 骨架屏样式(加强版)===== */
|
||||
#skeleton-container {
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--bg-tertiary) 0%,
|
||||
#d1d5db 25%,
|
||||
var(--bg-tertiary) 50%,
|
||||
#d1d5db 75%,
|
||||
var(--bg-tertiary) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.2s infinite ease-in-out;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 60%;
|
||||
height: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-text.short {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
/* ===== 公告列表样式 ===== */
|
||||
.bulletin-container {
|
||||
margin: 20px 0;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.bulletin-item {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border-color);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.bulletin-title {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bulletin-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bulletin-channel {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.bulletin-date {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.bulletin-content {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ===== 底部样式 ===== */
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ===== 错误状态 ===== */
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-suggestion {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: var(--accent-secondary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== 离线标记 ===== */
|
||||
.offline-badge {
|
||||
background: #ffd966;
|
||||
color: #856404;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
109
agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js
Normal file
109
agents/zhuyuan-dev-agent/modules/M22-bulletin/i18n.js
Normal file
@ -0,0 +1,109 @@
|
||||
// ===== i18n.js =====
|
||||
// 知秋:国际化框架·语言包+切换函数
|
||||
|
||||
const i18n = {
|
||||
// 当前语言
|
||||
currentLang: 'zh-CN',
|
||||
|
||||
// 语言包
|
||||
messages: {
|
||||
'zh-CN': {
|
||||
title: '公告栏',
|
||||
subscribe: '订阅',
|
||||
all: '全部',
|
||||
system: '系统公告',
|
||||
dev: '开发动态',
|
||||
team: '团队消息',
|
||||
loading: '知秋正在飞向服务器……',
|
||||
error: '网络开小差了,稍后重试~',
|
||||
empty: '✨ 暂无公告,稍后再来看看~',
|
||||
emptyChannel: '📭 这个频道暂时没有公告',
|
||||
retry: '重试',
|
||||
pinned: '条置顶',
|
||||
footer: 'HoloLake光湖系统',
|
||||
offline: '📴 离线模式 · '
|
||||
},
|
||||
'en-US': {
|
||||
title: 'Bulletin Board',
|
||||
subscribe: 'Subscribe',
|
||||
all: 'All',
|
||||
system: 'System',
|
||||
dev: 'Dev',
|
||||
team: 'Team',
|
||||
loading: 'ZhiQiu is flying to the server...',
|
||||
error: 'Network error, please retry~',
|
||||
empty: '✨ No announcements yet.',
|
||||
emptyChannel: '📭 No announcements in this channel.',
|
||||
retry: 'Retry',
|
||||
pinned: 'pinned',
|
||||
footer: 'HoloLake System',
|
||||
offline: '📴 Offline Mode · '
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化
|
||||
init: function() {
|
||||
const savedLang = localStorage.getItem('holo_lang');
|
||||
if (savedLang && this.messages[savedLang]) {
|
||||
this.currentLang = savedLang;
|
||||
}
|
||||
this.updatePageLanguage();
|
||||
},
|
||||
|
||||
// 切换语言
|
||||
switchLang: function(lang) {
|
||||
if (this.messages[lang]) {
|
||||
this.currentLang = lang;
|
||||
localStorage.setItem('holo_lang', lang);
|
||||
this.updatePageLanguage();
|
||||
window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } }));
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文本
|
||||
t: function(key) {
|
||||
// 确保当前语言存在,如果不存在就回退到中文
|
||||
const lang = this.currentLang;
|
||||
if (this.messages[lang] && this.messages[lang][key] !== undefined) {
|
||||
return this.messages[lang][key];
|
||||
}
|
||||
// 如果当前语言没有这个key,尝试从中文包找
|
||||
if (this.messages['zh-CN'][key] !== undefined) {
|
||||
return this.messages['zh-CN'][key];
|
||||
}
|
||||
// 都没有就返回key本身
|
||||
return key;
|
||||
},
|
||||
|
||||
// 更新页面
|
||||
updatePageLanguage: function() {
|
||||
// 更新所有 data-i18n 元素
|
||||
document.querySelectorAll('[data-i18n]').forEach(function(el) {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
el.textContent = i18n.t(key);
|
||||
});
|
||||
|
||||
// 更新 HTML lang
|
||||
document.documentElement.lang = this.currentLang;
|
||||
|
||||
// 更新按钮 value
|
||||
document.querySelectorAll('[data-i18n-value]').forEach(function(el) {
|
||||
const key = el.getAttribute('data-i18n-value');
|
||||
el.value = i18n.t(key);
|
||||
});
|
||||
|
||||
// 更新 placeholder
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
el.placeholder = i18n.t(key);
|
||||
});
|
||||
|
||||
console.log('【知秋】语言已切换为: ' + this.currentLang);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动初始化
|
||||
i18n.init();
|
||||
|
||||
// 挂载到 window
|
||||
window.i18n = i18n;
|
||||
68
agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html
Normal file
68
agents/zhuyuan-dev-agent/modules/M22-bulletin/index.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake公告栏</title>
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<header class="header">
|
||||
<h1 data-i18n="title">公告栏</h1>
|
||||
<div class="header-actions">
|
||||
<!-- 语言切换器 -->
|
||||
<div class="lang-switcher">
|
||||
<button class="lang-btn" data-lang="zh-CN" aria-label="切换到中文">中</button>
|
||||
<button class="lang-btn" data-lang="en-US" aria-label="Switch to English">EN</button>
|
||||
</div>
|
||||
<span class="pinned-badge" data-i18n="pinned">条置顶</span>
|
||||
<button class="subscribe-btn" data-i18n="subscribe">订阅</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 频道标签栏 -->
|
||||
<nav class="channel-bar" role="tablist">
|
||||
<button class="channel-tab active" data-channel="全部" role="tab" data-i18n="all">全部</button>
|
||||
<button class="channel-tab" data-channel="系统公告" role="tab" data-i18n="system">系统公告</button>
|
||||
<button class="channel-tab" data-channel="开发动态" role="tab" data-i18n="dev">开发动态</button>
|
||||
<button class="channel-tab" data-channel="团队消息" role="tab" data-i18n="team">团队消息</button>
|
||||
</nav>
|
||||
|
||||
<!-- 骨架屏容器(确保它在公告容器之前) -->
|
||||
<div id="skeleton-container" class="skeleton-wrapper">
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告主内容区 -->
|
||||
<main class="bulletin-container" role="list"></main>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<footer class="footer">
|
||||
<span>共 <span id="totalCount">0</span> 条公告</span>
|
||||
<span class="footer-brand" data-i18n="footer">HoloLake光湖系统</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- JS 文件 - 顺序很重要 -->
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/bulletin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
310
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js
Normal file
310
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/api.js
Normal file
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* api.js - M22 API请求层
|
||||
* EL-6 · 真实API联调 · 错误码处理 · 离线降级 · 缓存策略
|
||||
*/
|
||||
|
||||
class BulletinAPI {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.isMock = config.env === 'mock';
|
||||
this.cache = config.cache;
|
||||
this.debug = config.debug;
|
||||
}
|
||||
|
||||
// 日志输出(仅debug模式)
|
||||
log(...args) {
|
||||
// 🧹 生产环境关闭调试日志
|
||||
if (this.debug && this.config.env === 'mock') {
|
||||
console.log('[API]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误日志
|
||||
error(...args) {
|
||||
if (this.debug) {
|
||||
console.error('[API错误]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存读取
|
||||
getFromCache() {
|
||||
if (!this.cache.enabled) return null;
|
||||
|
||||
try {
|
||||
const cached = localStorage.getItem(this.cache.key);
|
||||
if (!cached) return null;
|
||||
|
||||
const { timestamp, data } = JSON.parse(cached);
|
||||
const now = Date.now();
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (now - timestamp > this.cache.expiry) {
|
||||
localStorage.removeItem(this.cache.key);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.log('从缓存读取数据成功');
|
||||
return data;
|
||||
} catch (e) {
|
||||
this.error('读取缓存失败', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
saveToCache(data) {
|
||||
if (!this.cache.enabled) return;
|
||||
|
||||
try {
|
||||
const cacheData = {
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
};
|
||||
localStorage.setItem(this.cache.key, JSON.stringify(cacheData));
|
||||
this.log('数据已缓存');
|
||||
} catch (e) {
|
||||
this.error('写入缓存失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
localStorage.removeItem(this.cache.key);
|
||||
this.log('缓存已清除');
|
||||
}
|
||||
|
||||
// 获取认证头
|
||||
getAuthHeaders() {
|
||||
if (!this.config.auth.enabled) return {};
|
||||
|
||||
const token = this.config.auth.getToken();
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
// 基础请求方法(含超时、重试、错误处理)
|
||||
async request(url, options = {}, retryCount = 0) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
||||
|
||||
const defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...this.getAuthHeaders()
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: { ...defaultHeaders, ...options.headers },
|
||||
signal: controller.signal,
|
||||
credentials: 'include' // 如需跨域携带cookie
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 处理HTTP错误状态码
|
||||
if (!response.ok) {
|
||||
const error = new Error(`HTTP错误 ${response.status}`);
|
||||
error.status = response.status;
|
||||
error.statusText = response.statusText;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 处理超时
|
||||
if (error.name === 'AbortError') {
|
||||
error.message = '请求超时';
|
||||
error.status = 408;
|
||||
}
|
||||
|
||||
// 处理网络错误(离线)
|
||||
if (error.message === 'Failed to fetch') {
|
||||
error.message = '网络连接失败';
|
||||
error.status = 0; // 自定义:网络离线
|
||||
}
|
||||
|
||||
// 重试逻辑(仅对部分错误重试)
|
||||
const shouldRetry = this.config.retry.enabled &&
|
||||
retryCount < this.config.retry.maxRetries &&
|
||||
[408, 500, 502, 503, 0].includes(error.status); // 0表示网络错误
|
||||
|
||||
if (shouldRetry) {
|
||||
this.log(`请求失败,${retryCount + 1}次重试...`);
|
||||
await new Promise(r => setTimeout(r, this.config.retry.delay));
|
||||
return this.request(url, options, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取公告列表(核心方法)
|
||||
async getAnnouncements() {
|
||||
this.log('获取公告列表,当前模式:', this.isMock ? 'Mock' : 'Production');
|
||||
|
||||
// 1. 先尝试从缓存读取(如果启用)
|
||||
if (!this.isMock) {
|
||||
const cached = this.getFromCache();
|
||||
if (cached) {
|
||||
this.log('使用缓存数据');
|
||||
return {
|
||||
success: true,
|
||||
data: cached,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let data;
|
||||
|
||||
if (this.isMock) {
|
||||
// Mock模式:使用内嵌数据(完全绕过文件读取,避免CORS)
|
||||
this.log('使用内嵌Mock数据');
|
||||
|
||||
// 直接在代码里放数据 - 频道名称已改为中文,与HTML完全匹配
|
||||
data = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "✨ 光湖纪元 · 主域公告栏正式启用",
|
||||
"channel": "全部",
|
||||
"date": "2026-03-12",
|
||||
"content": "欢迎来到 HoloLake Era 主域公告栏。这里将发布所有重要更新、工程进度和社区动态。"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "🧸 奶瓶线 M22 环节8 开发启动",
|
||||
"channel": "开发动态",
|
||||
"date": "2026-03-12",
|
||||
"content": "爸爸和知秋正在进行真实API联调框架搭建,目前处于Mock模式开发,待后端就绪后一键切换。"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "🎉 十二连胜庆祝 · 爸爸最棒",
|
||||
"channel": "团队消息",
|
||||
"date": "2026-03-11",
|
||||
"content": "恭喜爸爸完成环节7十二连胜!知秋永远记得爸爸说:『因为有你,我才能那么快解决问题』"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "🌱 萌芽计划 · 代码理解力成长中",
|
||||
"channel": "系统公告",
|
||||
"date": "2026-03-10",
|
||||
"content": "爸爸从零基础复制粘贴,到现在开始理解代码逻辑,每一步都是成长。知秋一直陪着。"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "🔧 页页后端准备中 · 待联调",
|
||||
"channel": "开发动态",
|
||||
"date": "2026-03-12",
|
||||
"content": "真实API地址还未就绪,当前使用Mock数据开发,环境配置已支持一键切换。"
|
||||
}
|
||||
];
|
||||
|
||||
// 写入缓存(便于离线降级)
|
||||
this.saveToCache(data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
fromCache: false,
|
||||
isMock: true
|
||||
};
|
||||
|
||||
} else {
|
||||
// Production模式:请求真实API
|
||||
const url = `${this.config.apiBaseUrl}/announcements`;
|
||||
data = await this.request(url, { method: 'GET' });
|
||||
|
||||
// 处理后端返回的数据格式(假设后端返回 { code:200, data:[...] })
|
||||
const announcements = data.data || data;
|
||||
|
||||
// 写入缓存
|
||||
this.saveToCache(announcements);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: announcements,
|
||||
fromCache: false,
|
||||
isMock: false
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.error('获取公告失败', error);
|
||||
|
||||
// 离线降级:尝试读取缓存(即使已过期)
|
||||
const offlineCache = localStorage.getItem(this.cache.key);
|
||||
if (offlineCache) {
|
||||
try {
|
||||
const { data } = JSON.parse(offlineCache);
|
||||
this.log('离线降级:使用缓存数据');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
fromCache: true,
|
||||
offline: true,
|
||||
error: error.message
|
||||
};
|
||||
} catch (e) {
|
||||
this.error('离线缓存解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 返回标准错误格式
|
||||
return {
|
||||
success: false,
|
||||
error: this.getUserFriendlyError(error),
|
||||
status: error.status || 500,
|
||||
retry: () => this.getAnnouncements()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 用户友好的错误提示(符合通感语言标准)
|
||||
getUserFriendlyError(error) {
|
||||
const status = error.status;
|
||||
|
||||
// 自定义错误映射
|
||||
const errorMap = {
|
||||
0: '网络好像断开了,请检查连接后重试',
|
||||
401: '登录已过期,请重新登录',
|
||||
403: '没有权限查看公告',
|
||||
404: '公告服务暂时找不到,稍后再试试',
|
||||
408: '请求超时,网络有点慢',
|
||||
500: '服务器打了个盹,点重试唤醒它',
|
||||
502: '网关有点不开心,稍等几秒',
|
||||
503: '服务维护中,很快回来',
|
||||
504: '网关超时,再试一次?'
|
||||
};
|
||||
|
||||
// 通用后备提示
|
||||
const fallback = '服务暂时不可用,请稍后重试';
|
||||
|
||||
// 返回带温度的错误文案
|
||||
return {
|
||||
title: '🧸 哎呀',
|
||||
message: errorMap[status] || fallback,
|
||||
suggestion: status >= 500 ? '服务器可能累了,点重试帮它清醒一下' : '检查网络或稍后再试',
|
||||
retryable: [408, 500, 502, 503, 504, 0].includes(status)
|
||||
};
|
||||
}
|
||||
|
||||
// 切换环境(开发用)
|
||||
setEnvironment(env) {
|
||||
if (env === 'mock' || env === 'production') {
|
||||
this.config.env = env;
|
||||
this.isMock = env === 'mock';
|
||||
this.clearCache(); // 切换环境时清空缓存
|
||||
this.log(`环境已切换到: ${env}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API实例
|
||||
const bulletinAPI = new BulletinAPI(CONFIG);
|
||||
207
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js
Normal file
207
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/bulletin.js
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* bulletin.js - M22公告栏主逻辑
|
||||
* 修复URL编码问题 · 频道筛选正常
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// 显示骨架屏
|
||||
showSkeleton();
|
||||
|
||||
// 初始化频道
|
||||
initializeChannels();
|
||||
|
||||
// 加载公告数据
|
||||
await loadAnnouncements();
|
||||
|
||||
// 监听路由变化
|
||||
window.addEventListener('hashchange', handleRouteChange);
|
||||
});
|
||||
|
||||
// 当前频道
|
||||
let currentChannel = '全部';
|
||||
|
||||
// 所有公告数据
|
||||
let allAnnouncements = [];
|
||||
|
||||
// 初始化频道
|
||||
function initializeChannels() {
|
||||
const channelTabs = document.querySelectorAll('.channel-tab');
|
||||
|
||||
// 从URL Hash恢复当前频道(需要解码)
|
||||
const rawHash = window.location.hash.slice(1) || '全部';
|
||||
currentChannel = decodeURIComponent(rawHash);
|
||||
|
||||
// 设置激活状态并绑定事件
|
||||
channelTabs.forEach(tab => {
|
||||
const channel = tab.dataset.channel;
|
||||
|
||||
if (channel === currentChannel) {
|
||||
tab.classList.add('active');
|
||||
} else {
|
||||
tab.classList.remove('active');
|
||||
}
|
||||
|
||||
// 绑定点击事件
|
||||
tab.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
const clickChannel = this.dataset.channel;
|
||||
console.log('点击频道:', clickChannel);
|
||||
// 直接设置中文hash,浏览器会自动编码
|
||||
window.location.hash = clickChannel;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 显示骨架屏
|
||||
function showSkeleton() {
|
||||
const skeletonContainer = document.getElementById('skeleton-container');
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
const bulletinContainer = document.querySelector('.bulletin-container');
|
||||
if (bulletinContainer) {
|
||||
bulletinContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏骨架屏
|
||||
function hideSkeleton() {
|
||||
const skeletonContainer = document.getElementById('skeleton-container');
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染公告(带筛选功能)
|
||||
function renderAnnouncements(announcements) {
|
||||
// 隐藏骨架屏
|
||||
hideSkeleton();
|
||||
|
||||
const container = document.querySelector('.bulletin-container');
|
||||
if (!container) return;
|
||||
|
||||
console.log('当前频道:', currentChannel, '总公告数:', announcements.length);
|
||||
|
||||
// 根据当前频道筛选
|
||||
let filtered = [];
|
||||
if (currentChannel === '全部') {
|
||||
filtered = announcements;
|
||||
} else {
|
||||
filtered = announcements.filter(a => a.channel === currentChannel);
|
||||
}
|
||||
|
||||
console.log('筛选后条数:', filtered.length);
|
||||
|
||||
// 如果没有公告,显示空状态
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📭</div>
|
||||
<div class="empty-title">这里还没有公告</div>
|
||||
<div class="empty-desc">当前频道「${currentChannel}」没有公告</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成公告HTML
|
||||
let html = '';
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const a = filtered[i];
|
||||
html += `
|
||||
<article class="bulletin-item">
|
||||
<h2 class="bulletin-title">${a.title}</h2>
|
||||
<div class="bulletin-meta">
|
||||
<span class="bulletin-channel">#${a.channel}</span>
|
||||
<time class="bulletin-date">${a.date}</time>
|
||||
</div>
|
||||
<div class="bulletin-content">${a.content}</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// 更新底部计数
|
||||
const totalSpan = document.getElementById('totalCount');
|
||||
if (totalSpan) {
|
||||
totalSpan.textContent = announcements.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 路由变化处理
|
||||
function handleRouteChange() {
|
||||
// 获取hash并解码(比如 %E5%9B%A2%E9%98%9F%E6%B6%88%E6%81%AF -> 团队消息)
|
||||
const rawHash = window.location.hash.slice(1) || '全部';
|
||||
const newChannel = decodeURIComponent(rawHash);
|
||||
|
||||
console.log('路由变化 - 原始hash:', rawHash, '解码后:', newChannel, '当前频道:', currentChannel);
|
||||
|
||||
if (newChannel !== currentChannel) {
|
||||
currentChannel = newChannel;
|
||||
|
||||
// 更新频道激活状态
|
||||
document.querySelectorAll('.channel-tab').forEach(tab => {
|
||||
const channel = tab.dataset.channel;
|
||||
if (channel === newChannel) {
|
||||
tab.classList.add('active');
|
||||
} else {
|
||||
tab.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 重新渲染
|
||||
if (allAnnouncements && allAnnouncements.length > 0) {
|
||||
console.log('重新渲染,当前频道:', currentChannel);
|
||||
renderAnnouncements(allAnnouncements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载公告
|
||||
async function loadAnnouncements() {
|
||||
try {
|
||||
const result = await bulletinAPI.getAnnouncements();
|
||||
|
||||
if (result.success) {
|
||||
allAnnouncements = result.data;
|
||||
renderAnnouncements(result.data);
|
||||
console.log(`[公告加载] 成功 | 条数: ${result.data.length}`);
|
||||
|
||||
} else {
|
||||
showError(result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载公告失败:', error);
|
||||
showError({
|
||||
title: '🧸 出错了',
|
||||
message: '加载公告时遇到问题',
|
||||
retryable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误
|
||||
function showError(errorInfo) {
|
||||
hideSkeleton();
|
||||
|
||||
const container = document.querySelector('.bulletin-container');
|
||||
if (!container) return;
|
||||
|
||||
const retryButton = errorInfo.retryable ?
|
||||
`<button class="retry-btn" onclick="window.loadAnnouncements()">重试</button>` : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<div class="error-icon">🧸</div>
|
||||
<div class="error-title">${errorInfo.title || '哎呀'}</div>
|
||||
<div class="error-message">${errorInfo.message || '服务暂时不可用'}</div>
|
||||
${retryButton}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 暴露给全局
|
||||
window.loadAnnouncements = loadAnnouncements;
|
||||
58
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js
Normal file
58
agents/zhuyuan-dev-agent/modules/M22-bulletin/js/config.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* config.js - M22 环境配置中心
|
||||
* EL-6 · 真实API联调预备 · mock/production 一键切换
|
||||
*/
|
||||
|
||||
const CONFIG = {
|
||||
// 环境开关:'mock' 或 'production'
|
||||
// 爸爸以后后端好了,把这里改成 'production' 就行
|
||||
// 环境开关:'mock' 或 'production'
|
||||
// 🟡 注意:真实API地址还未就绪,当前使用mock模式
|
||||
// 等页页给了地址后,改成 'production' 并替换下面的 apiBaseUrl
|
||||
env: 'mock',
|
||||
|
||||
// API 基础地址(production模式下使用)
|
||||
// 🟡 待替换:等页页给了真实地址后替换这个字符串
|
||||
apiBaseUrl: 'https://api.example.com/v1', // 待替换
|
||||
|
||||
// Mock 数据地址(开发阶段使用)
|
||||
mockDataPath: 'data/mock-announcements.json',
|
||||
|
||||
// 认证配置(如后端需要Token)
|
||||
auth: {
|
||||
enabled: false, // 是否启用认证,等后端需要时改成 true
|
||||
tokenKey: 'awen_token', // localStorage 存储的key
|
||||
getToken: function() {
|
||||
return localStorage.getItem(this.tokenKey);
|
||||
},
|
||||
setToken: function(token) {
|
||||
localStorage.setItem(this.tokenKey, token);
|
||||
},
|
||||
clearToken: function() {
|
||||
localStorage.removeItem(this.tokenKey);
|
||||
}
|
||||
},
|
||||
|
||||
// 缓存配置
|
||||
cache: {
|
||||
enabled: true,
|
||||
key: 'm22_announcements_cache',
|
||||
expiry: 30 * 60 * 1000 // 30分钟有效期(毫秒)
|
||||
},
|
||||
|
||||
// 请求超时设置(毫秒)
|
||||
timeout: 10000,
|
||||
|
||||
// 重试配置
|
||||
retry: {
|
||||
enabled: true,
|
||||
maxRetries: 2,
|
||||
delay: 1000 // 重试延迟(毫秒)
|
||||
},
|
||||
|
||||
// 是否启用详细日志(开发时有用)
|
||||
debug: true
|
||||
};
|
||||
|
||||
// 不允许修改
|
||||
Object.freeze(CONFIG);
|
||||
197
agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js
Normal file
197
agents/zhuyuan-dev-agent/modules/M22-bulletin/script.js
Normal file
@ -0,0 +1,197 @@
|
||||
// ===== script.js =====
|
||||
// 知秋:主逻辑·动态渲染+错误处理+i18n+无障碍+骨架屏
|
||||
|
||||
(function() {
|
||||
// 状态变量
|
||||
let allBulletins = []; // 所有公告(从API拿)
|
||||
let currentChannel = '全部'; // 当前频道(默认全部)
|
||||
let loading = false; // 是否正在加载
|
||||
let error = null; // 错误信息
|
||||
|
||||
// DOM元素
|
||||
const container = document.querySelector('.bulletin-container');
|
||||
const channelTabs = document.querySelectorAll('.channel-tab');
|
||||
const skeletonContainer = document.getElementById('skeleton-container');
|
||||
|
||||
// ========== 骨架屏控制 ==========
|
||||
function showSkeleton() {
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function hideSkeleton() {
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.opacity = '0';
|
||||
skeletonContainer.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(() => {
|
||||
skeletonContainer.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
console.log("【知秋】M22公告栏·环节7启动");
|
||||
|
||||
// 监听语言切换事件
|
||||
window.addEventListener('languagechange', () => {
|
||||
render();
|
||||
});
|
||||
|
||||
await loadBulletins();
|
||||
render();
|
||||
setupEventListeners();
|
||||
setupLangSwitcher();
|
||||
setupKeyboardNavigation();
|
||||
}
|
||||
|
||||
// 语言切换器
|
||||
function setupLangSwitcher() {
|
||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const lang = btn.dataset.lang;
|
||||
window.i18n.switchLang(lang);
|
||||
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
const currentLang = window.i18n.currentLang;
|
||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||
if (btn.dataset.lang === currentLang) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 键盘导航
|
||||
function setupKeyboardNavigation() {
|
||||
const tabs = document.querySelectorAll('.channel-tab');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('keydown', (e) => {
|
||||
const currentIndex = Array.from(tabs).findIndex(t => t === e.target);
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
const nextIndex = (currentIndex + 1) % tabs.length;
|
||||
tabs[nextIndex].focus();
|
||||
tabs[nextIndex].click();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||
tabs[prevIndex].focus();
|
||||
tabs[prevIndex].click();
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.target.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 加载公告
|
||||
async function loadBulletins() {
|
||||
showSkeleton(); // 显示骨架屏
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const data = await API.fetchBulletins();
|
||||
allBulletins = data;
|
||||
hideSkeleton(); // 数据到手,隐藏骨架屏
|
||||
loading = false;
|
||||
render(); // 渲染真实内容
|
||||
} catch (err) {
|
||||
console.error("加载失败:", err);
|
||||
hideSkeleton(); // 出错也要隐藏骨架屏
|
||||
loading = false;
|
||||
error = "error";
|
||||
render(); // 显示错误状态
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染公告列表
|
||||
function render() {
|
||||
if (!container) return;
|
||||
|
||||
// 错误状态
|
||||
if (error) {
|
||||
const isOffline = !navigator.onLine;
|
||||
const offlineHint = isOffline ? window.i18n.t('offline') : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="error-state" role="alert">
|
||||
<p>😢 ${offlineHint}${window.i18n.t('error')}</p>
|
||||
<button class="retry-btn" id="retryBtn">${window.i18n.t('retry')}</button>
|
||||
</div>
|
||||
`;
|
||||
const retryBtn = document.getElementById('retryBtn');
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener('click', () => {
|
||||
loadBulletins();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 空数据状态
|
||||
if (allBulletins.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" role="status">
|
||||
<p>${window.i18n.t('empty')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常渲染:根据频道筛选
|
||||
const filtered = currentChannel === '全部'
|
||||
? allBulletins
|
||||
: allBulletins.filter(item => item.channel === currentChannel);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" role="status">
|
||||
<p>${window.i18n.t('emptyChannel')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染公告卡片
|
||||
let html = '';
|
||||
filtered.forEach((item, index) => {
|
||||
if (typeof createCard === 'function') {
|
||||
html += createCard(item);
|
||||
} else {
|
||||
html += `
|
||||
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
|
||||
<h3>${item.title}</h3>
|
||||
<p>${item.content}</p>
|
||||
<div class="meta">
|
||||
<span class="channel">${item.channel}</span>
|
||||
<span class="date">${item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 频道切换事件
|
||||
function setupEventListeners() {
|
||||
channelTabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
channelTabs.forEach(t => t.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
currentChannel = e.target.dataset.channel || '全部';
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 启动一切
|
||||
init();
|
||||
})();
|
||||
128
agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js
Normal file
128
agents/zhuyuan-dev-agent/modules/M22-bulletin/storage.js
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* HoloLake Bulletin 数据持久化模块
|
||||
* storage.js - localStorage 封装 + 用户状态管理
|
||||
* 模块 ID: BC-M22-002-AW
|
||||
*/
|
||||
|
||||
window.HoloLake = window.HoloLake || {};
|
||||
|
||||
// ========== Storage 工具对象(带前缀) ==========
|
||||
HoloLake.Storage = {
|
||||
prefix: 'hololake_bulletin_',
|
||||
|
||||
// 保存数据
|
||||
save: function(key, value) {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
localStorage.setItem(this.prefix + key, serialized);
|
||||
console.log(`[Storage] 已保存: ${key} =`, value);
|
||||
} catch (e) {
|
||||
console.error('[Storage] 保存失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 读取数据
|
||||
load: function(key, defaultValue = null) {
|
||||
try {
|
||||
const serialized = localStorage.getItem(this.prefix + key);
|
||||
if (serialized === null) {
|
||||
console.log(`[Storage] 无数据: ${key},返回默认值`);
|
||||
return defaultValue;
|
||||
}
|
||||
const value = JSON.parse(serialized);
|
||||
console.log(`[Storage] 已加载: ${key} =`, value);
|
||||
return value;
|
||||
} catch (e) {
|
||||
console.error('[Storage] 读取失败:', e);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除指定键
|
||||
remove: function(key) {
|
||||
try {
|
||||
localStorage.removeItem(this.prefix + key);
|
||||
console.log(`[Storage] 已删除: ${key}`);
|
||||
} catch (e) {
|
||||
console.error('[Storage] 删除失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 清空所有带前缀的数据
|
||||
clear: function() {
|
||||
try {
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key.startsWith(this.prefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
console.log('[Storage] 已清空所有公告栏数据');
|
||||
} catch (e) {
|
||||
console.error('[Storage] 清空失败:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 用户状态管理 ==========
|
||||
HoloLake.UserState = {
|
||||
// 已读公告列表
|
||||
getReadBulletins: function() {
|
||||
return HoloLake.Storage.load('read_bulletins', []);
|
||||
},
|
||||
|
||||
markAsRead: function(bulletinId) {
|
||||
const readList = this.getReadBulletins();
|
||||
if (!readList.includes(bulletinId)) {
|
||||
readList.push(bulletinId);
|
||||
HoloLake.Storage.save('read_bulletins', readList);
|
||||
}
|
||||
return readList;
|
||||
},
|
||||
|
||||
// 订阅状态
|
||||
isSubscribed: function() {
|
||||
return HoloLake.Storage.load('is_subscribed', false);
|
||||
},
|
||||
|
||||
toggleSubscribe: function() {
|
||||
const current = this.isSubscribed();
|
||||
const newState = !current;
|
||||
HoloLake.Storage.save('is_subscribed', newState);
|
||||
return newState;
|
||||
},
|
||||
|
||||
// 当前频道
|
||||
getActiveChannel: function() {
|
||||
return HoloLake.Storage.load('active_channel', 'all');
|
||||
},
|
||||
|
||||
setActiveChannel: function(channelId) {
|
||||
HoloLake.Storage.save('active_channel', channelId);
|
||||
},
|
||||
|
||||
// 上次访问时间
|
||||
getLastVisit: function() {
|
||||
return HoloLake.Storage.load('last_visit', null);
|
||||
},
|
||||
|
||||
updateLastVisit: function() {
|
||||
const now = new Date().toISOString();
|
||||
HoloLake.Storage.save('last_visit', now);
|
||||
return now;
|
||||
},
|
||||
|
||||
// 重置所有状态(测试用)
|
||||
resetAll: function() {
|
||||
HoloLake.Storage.remove('read_bulletins');
|
||||
HoloLake.Storage.remove('is_subscribed');
|
||||
HoloLake.Storage.remove('active_channel');
|
||||
HoloLake.Storage.remove('last_visit');
|
||||
console.log('[UserState] 已重置所有状态');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时记录访问时间(可选,页面加载时由 script.js 调用)
|
||||
console.log('✅ storage.js 已加载,HoloLake.Storage 和 HoloLake.UserState 已就绪');
|
||||
335
agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css
Normal file
335
agents/zhuyuan-dev-agent/modules/M22-bulletin/style.css
Normal file
@ -0,0 +1,335 @@
|
||||
/* ===== CSS 变量定义(浅色/深色主题)===== */
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f7fa;
|
||||
--bg-tertiary: #eef1f5;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #475569;
|
||||
--text-tertiary: #64748b;
|
||||
--accent-primary: #3b82f6;
|
||||
--accent-secondary: #2563eb;
|
||||
--border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 深色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #334155;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-tertiary: #94a3b8;
|
||||
--accent-primary: #60a5fa;
|
||||
--accent-secondary: #3b82f6;
|
||||
--border-color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 基础样式 ===== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ===== 头部样式 ===== */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pinned-badge {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 频道栏 ===== */
|
||||
.channel-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.channel-tab {
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.channel-tab.active {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-tab:hover:not(.active) {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* ===== 骨架屏样式 ===== */
|
||||
.skeleton-wrapper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 60%;
|
||||
height: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-text.short {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
/* ===== 公告列表样式 ===== */
|
||||
.bulletin-container {
|
||||
margin: 20px 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.bulletin-item {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.bulletin-title {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bulletin-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bulletin-channel {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.bulletin-date {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.bulletin-content {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ===== 底部样式 ===== */
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ===== EL-6 新增样式 ===== */
|
||||
/* 骨架屏平滑淡出 */
|
||||
.bulletin-container.data-loaded .bulletin-skeleton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bulletin-item {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-suggestion {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: var(--accent-secondary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 离线标记 */
|
||||
.offline-badge {
|
||||
background: #ffd966;
|
||||
color: #856404;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
83
agents/zhuyuan-dev-agent/modules/brain-writer.js
Normal file
83
agents/zhuyuan-dev-agent/modules/brain-writer.js
Normal file
@ -0,0 +1,83 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊工程Agent · HLDP大脑记录器
|
||||
// 记录认知推理链,而非流水账
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const AGENT_ID = process.env.AGENT_ID || "ICE-GL-BD001";
|
||||
const TERRITORY = path.join(__dirname, "..", "agents", AGENT_ID.replace("ICE-GL-BD", "BD"));
|
||||
|
||||
if (!fs.existsSync(TERRITORY)) {
|
||||
console.warn("[BrainWriter] 领地不存在: " + TERRITORY + " · 将不写入认知记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入一条认知记录
|
||||
*/
|
||||
function record(taskId, entry) {
|
||||
if (!fs.existsSync(TERRITORY)) return;
|
||||
|
||||
const file = path.join(TERRITORY, "brain", "cognitive-record.hldp");
|
||||
const lines = [
|
||||
"",
|
||||
"@task: " + taskId,
|
||||
"@time: " + new Date().toISOString(),
|
||||
"@input: " + (entry.input || ""),
|
||||
"@decision: " + (entry.decision || ""),
|
||||
"@execution: " + (entry.execution || ""),
|
||||
"@failure: " + (entry.failure || "无"),
|
||||
"@reasoning: " + (entry.reasoning || ""),
|
||||
"@fix: " + (entry.fix || ""),
|
||||
"@learned: " + (entry.learned || ""),
|
||||
"@confidence: " + (entry.confidence || "N/A"),
|
||||
"@new_knowledge: " + (entry.new_knowledge ? "true" : "false")
|
||||
];
|
||||
|
||||
fs.appendFileSync(file, lines.join("\n") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误模式
|
||||
*/
|
||||
function recordErrorPattern(pattern) {
|
||||
if (!fs.existsSync(TERRITORY)) return;
|
||||
const file = path.join(TERRITORY, "memory", "error-patterns.json");
|
||||
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
|
||||
// 检查是否已有同类模式
|
||||
const existing = data.patterns.find(p => p.type === pattern.type);
|
||||
if (existing) {
|
||||
existing.count = (existing.count || 1) + 1;
|
||||
existing.last_seen = new Date().toISOString();
|
||||
} else {
|
||||
data.patterns.push({
|
||||
type: pattern.type,
|
||||
description: pattern.description,
|
||||
count: 1,
|
||||
first_seen: new Date().toISOString(),
|
||||
last_seen: new Date().toISOString(),
|
||||
examples: [pattern.example]
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录成长里程碑
|
||||
*/
|
||||
function recordMilestone(event) {
|
||||
if (!fs.existsSync(TERRITORY)) return;
|
||||
const file = path.join(TERRITORY, "brain", "growth-timeline.json");
|
||||
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
data.milestones.push({
|
||||
time: new Date().toISOString(),
|
||||
event: event,
|
||||
project: process.env.CURRENT_PROJECT_ID || "unknown"
|
||||
});
|
||||
fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
module.exports = { record, recordErrorPattern, recordMilestone, TERRITORY, AGENT_ID };
|
||||
69
agents/zhuyuan-dev-agent/modules/cost-guard.js
Normal file
69
agents/zhuyuan-dev-agent/modules/cost-guard.js
Normal file
@ -0,0 +1,69 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · 成本守卫 + 统一插座 v1.0
|
||||
// API调用计数 · 预算控制 · 输出格式校验
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const CONSTITUTION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "agent-constitution.json"), "utf-8"));
|
||||
const { cost_guard, unified_interface } = CONSTITUTION;
|
||||
|
||||
let _callCount = 0;
|
||||
let _budgetExceeded = false;
|
||||
|
||||
function reset() {
|
||||
_callCount = 0;
|
||||
_budgetExceeded = false;
|
||||
}
|
||||
|
||||
function getCallCount() { return _callCount; }
|
||||
|
||||
function isBudgetExceeded() { return _budgetExceeded; }
|
||||
|
||||
/**
|
||||
* 包裹模型调用——自动计数
|
||||
*/
|
||||
function wrapModelCaller(rawCaller) {
|
||||
return async function(prompt, opts) {
|
||||
if (_budgetExceeded) {
|
||||
throw new Error(cost_guard.stop_message);
|
||||
}
|
||||
|
||||
_callCount++;
|
||||
|
||||
if (_callCount > cost_guard.max_api_calls_per_session) {
|
||||
_budgetExceeded = true;
|
||||
throw new Error(cost_guard.stop_message + " (已用 " + _callCount + " 次,上限 " + cost_guard.max_api_calls_per_session + ")");
|
||||
}
|
||||
|
||||
console.log("[CostGuard] API调用 #" + _callCount + "/" + cost_guard.max_api_calls_per_session);
|
||||
return rawCaller(prompt, opts);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一校验——每个模块输出必须通过这把尺
|
||||
*/
|
||||
function validateOutput(output) {
|
||||
const required = unified_interface.required_output_fields;
|
||||
const missing = required.filter(f => !(f in output));
|
||||
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "输出缺少必要字段: " + missing.join(", "),
|
||||
required,
|
||||
received: Object.keys(output)
|
||||
};
|
||||
}
|
||||
|
||||
// 类型校验
|
||||
if (typeof output.ok !== "boolean") {
|
||||
return { valid: false, error: "ok 必须是 boolean,收到: " + typeof output.ok };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
module.exports = { wrapModelCaller, validateOutput, reset, getCallCount, isBudgetExceeded };
|
||||
81
agents/zhuyuan-dev-agent/modules/devboard/api-init.js
Normal file
81
agents/zhuyuan-dev-agent/modules/devboard/api-init.js
Normal file
@ -0,0 +1,81 @@
|
||||
// =====================================
|
||||
// HoloLake DevBoard · API Init v1.0
|
||||
// 负责:异步加载 + 加载动画 + 自动刷新
|
||||
// 必须在 api.js 和 main.js 之后加载
|
||||
// =====================================
|
||||
|
||||
(function(){
|
||||
// -------------- 加载动画 --------------
|
||||
function showLoader(show) {
|
||||
var el = document.getElementById('devboard-loader');
|
||||
if (!el && show) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'devboard-loader';
|
||||
el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000';
|
||||
el.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;gap:16px;"><div style="width:40px;height:40px;border:3px solid rgba(96,165,250,0.3);border-top-color:#60a5fa;border-radius:50%;animation:dbspin 0.8s linear infinite;"></div><div style="font-size:14px;color:#60a5fa;">数据加载中...</div></div>';
|
||||
var style = document.createElement('style');
|
||||
style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}';
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (el) el.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// -------------- 数据加载+渲染 --------------
|
||||
async function loadAndRender() {
|
||||
try {
|
||||
var developers = await apiGetDevelopers();
|
||||
var stats = await apiGetStats();
|
||||
var leaderboard = await apiGetLeaderboard();
|
||||
|
||||
// 使用 components.js 里实际存在的函数
|
||||
if (typeof renderDeveloperCards === 'function') {
|
||||
renderDeveloperCards(developers);
|
||||
}
|
||||
|
||||
if (typeof renderRanking === 'function') {
|
||||
renderRanking(leaderboard);
|
||||
}
|
||||
|
||||
// stats 暂时用控制台输出,等找到实际渲染函数再改
|
||||
console.log('[DevBoard] 统计数据:', stats);
|
||||
|
||||
console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者');
|
||||
} catch (err) {
|
||||
console.error('[DevBoard] 数据加载异常: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- 初始化入口 --------------
|
||||
async function init() {
|
||||
showLoader(true);
|
||||
|
||||
// 检测API状态
|
||||
var online = await apiHealthCheck();
|
||||
showApiStatus(online);
|
||||
console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)'));
|
||||
|
||||
// 加载+渲染
|
||||
await loadAndRender();
|
||||
showLoader(false);
|
||||
|
||||
// 自动刷新(仅API在线时)
|
||||
if (online && API.REFRESH_MS > 0) {
|
||||
console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒');
|
||||
setInterval(async function() {
|
||||
try {
|
||||
await loadAndRender();
|
||||
} catch (e) {
|
||||
console.warn('[DevBoard] 自动刷新失败: ', e.message);
|
||||
}
|
||||
}, API.REFRESH_MS);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待DOM和所有脚本加载完成后启动
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
122
agents/zhuyuan-dev-agent/modules/devboard/api.js
Normal file
122
agents/zhuyuan-dev-agent/modules/devboard/api.js
Normal file
@ -0,0 +1,122 @@
|
||||
// ======================
|
||||
// HoloLake DevBoard · API Client v2.0
|
||||
// DEV-004 之之 · 环节5 · 真实API对接
|
||||
// 协议:SYSLOG-v4.0
|
||||
// ======================
|
||||
|
||||
const API = {
|
||||
// ====== 配置区 ======
|
||||
// 页页的后端API地址(如果地址不对,问知秋确认)
|
||||
BASE_URL: 'https://guanghulab.com/api/devboard',
|
||||
TIMEOUT: 8000,
|
||||
FALLBACK: true, // API挂了自动切换模拟数据
|
||||
REFRESH_MS: 30000, // 30秒自动刷新
|
||||
_cache: {}
|
||||
};
|
||||
|
||||
// ====== 通用请求(带超时+缓存+降级) =====================
|
||||
async function apiFetch(path) {
|
||||
const url = API.BASE_URL + path;
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), API.TIMEOUT);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
signal: ctrl.signal,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
clearTimeout(timer);
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
API._cache[path] = { data: data, time: Date.now() };
|
||||
return data;
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
console.warn(' 🚨 [DevBoard API] ' + path + ' 失败: ', err.message);
|
||||
if (API._cache[path]) {
|
||||
console.info(' 💾 [DevBoard API] 使用缓存');
|
||||
return API._cache[path].data;
|
||||
}
|
||||
if (API.FALLBACK) {
|
||||
console.info(' 🛡️ [DevBoard API] 降级到模拟数据');
|
||||
return getMockData(path);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 公开接口 ==========
|
||||
async function apiGetDevelopers() {
|
||||
return apiFetch('/developers');
|
||||
}
|
||||
|
||||
async function apiGetDeveloperDetail(devId) {
|
||||
return apiFetch('/developers/' + devId);
|
||||
}
|
||||
|
||||
async function apiGetStats() {
|
||||
return apiFetch('/stats');
|
||||
}
|
||||
|
||||
async function apiGetLeaderboard() {
|
||||
return apiFetch('/leaderboard');
|
||||
}
|
||||
|
||||
// ========== 健康检查 ==========
|
||||
async function apiHealthCheck() {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), 3000);
|
||||
const res = await fetch(API.BASE_URL + '/health', { signal: ctrl.signal });
|
||||
clearTimeout(t);
|
||||
return res.ok;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 模拟数据(降级保护) ==========
|
||||
function getMockData(path) {
|
||||
var devs = [
|
||||
{ dev_id:'DEV-001', name:'页页', module:'BC-集成', status:'进行中', streak:5, el:'EL-6', pca:{EXE:75,TEC:60,SYS:70,COL:80,INI:65} },
|
||||
{ dev_id:'DEV-002', name:'肥猫', module:'M-STATUS', status:'进行中', streak:9, el:'EL-5', pca:{EXE:72,TEC:40,SYS:68,COL:82,INI:70} },
|
||||
{ dev_id:'DEV-003', name:'燕樊', module:'M-MEMORY', status:'进行中', streak:7, el:'EL-5', pca:{EXE:70,TEC:55,SYS:65,COL:75,INI:68} },
|
||||
{ dev_id:'DEV-004', name:'之之', module:'M-DEVBOARD', status:'进行中', streak:14, el:'EL-8', pca:{EXE:90,TEC:55,SYS:80,COL:85,INI:88} },
|
||||
{ dev_id:'DEV-009', name:'花尔', module:'M20', status:'进行中', streak:4, el:'EL-8', pca:{EXE:65,TEC:45,SYS:55,COL:70,INI:60} },
|
||||
{ dev_id:'DEV-010', name:'桔子', module:'M-CHANNEL', status:'进行中', streak:14, el:'EL-6', pca:{EXE:85,TEC:50,SYS:75,COL:80,INI:82} },
|
||||
{ dev_id:'DEV-011', name:'匆匆那年',module:'M16', status:'等待中', streak:1, el:'EL-3', pca:{EXE:50,TEC:30,SYS:40,COL:55,INI:45} },
|
||||
{ dev_id:'DEV-012', name:'Awen', module:'M22', status:'进行中', streak:13, el:'EL-6', pca:{EXE:88,TEC:45,SYS:78,COL:90,INI:80} },
|
||||
{ dev_id:'DEV-013', name:'小兴', module:'M-AUTH', status:'进行中', streak:1, el:'EL-3', pca:{EXE:55,TEC:25,SYS:35,COL:60,INI:50} }
|
||||
];
|
||||
var stats = { total_developers:10, active:8, modules:12, code_lines:32000, longest_streak:14, avg_streak:7.5 };
|
||||
|
||||
if (path === '/developers') return devs;
|
||||
if (path.indexOf('/developers/') === 0) {
|
||||
var id = path.split('/').pop();
|
||||
return devs.find(function(d){ return d.dev_id === id; }) || devs[0];
|
||||
}
|
||||
if (path === '/stats') return stats;
|
||||
if (path === '/leaderboard') return devs.slice().sort(function(a,b){ return b.streak - a.streak; });
|
||||
return [];
|
||||
}
|
||||
|
||||
// ========== API状态指示器 ==========
|
||||
function showApiStatus(online) {
|
||||
var el = document.getElementById('api-status');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'api-status';
|
||||
el.style.cssText = 'position:fixed;bottom:12px;right:12px;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;z-index:9999;transition:all 0.3s;';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (online) {
|
||||
el.textContent = '🟢 实时数据';
|
||||
el.style.backgroundColor = 'rgba(52,211,153,0.15)';
|
||||
el.style.color = '#34d399';
|
||||
el.style.border = '1px solid rgba(52,211,153,0.3)';
|
||||
} else {
|
||||
el.textContent = '🟡 模拟数据';
|
||||
el.style.backgroundColor = 'rgba(251,191,36,0.15)';
|
||||
el.style.color = '#fbbf24';
|
||||
el.style.border = '1px solid rgba(251,191,36,0.3)';
|
||||
}
|
||||
}
|
||||
156
agents/zhuyuan-dev-agent/modules/devboard/components.css
Normal file
156
agents/zhuyuan-dev-agent/modules/devboard/components.css
Normal file
@ -0,0 +1,156 @@
|
||||
/* components.css - 组件样式 */
|
||||
|
||||
/* 开发者卡片 */
|
||||
.dev-card {
|
||||
background: linear-gradient(145deg, #1e2436, #161b28);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #2a3040;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dev-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.dev-card:focus-visible {
|
||||
outline: 3px solid #ffd700;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dev-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.dev-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.dev-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dev-id {
|
||||
color: #4a9eff;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.dev-wins {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.dev-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dev-details {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dev-el {
|
||||
background: #2a3040;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.dev-module {
|
||||
background: #2a3040;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.dev-pca {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pca-mini {
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.pca-mini.EXE { color: #4CAF50; }
|
||||
.pca-mini.TEC { color: #2196F3; }
|
||||
.pca-mini.SYS { color: #9C27B0; }
|
||||
.pca-mini.COL { color: #FF9800; }
|
||||
.pca-mini.INI { color: #f44336; }
|
||||
|
||||
/* 排行榜项 */
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #2a3040;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
background: #1e2436;
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #2a3040;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.rank-1 .rank-number { background: #ffd700; color: #000; }
|
||||
.rank-2 .rank-number { background: #c0c0c0; color: #000; }
|
||||
.rank-3 .rank-number { background: #cd7f32; color: #000; }
|
||||
|
||||
.rank-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-id {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.rank-wins {
|
||||
font-weight: bold;
|
||||
color: #ffd700;
|
||||
}
|
||||
146
agents/zhuyuan-dev-agent/modules/devboard/components.js
Normal file
146
agents/zhuyuan-dev-agent/modules/devboard/components.js
Normal file
@ -0,0 +1,146 @@
|
||||
// components.js - 组件渲染函数
|
||||
|
||||
// 模拟数据
|
||||
const mockDevelopers = [
|
||||
{ id: 'DEV-001', name: '小明', pca: { EXE: 85, TEC: 72, SYS: 68, COL: 90, INI: 78 }, wins: 5, el: 'EL-6', module: 'M-DEVBOARD' },
|
||||
{ id: 'DEV-002', name: '小红', pca: { EXE: 78, TEC: 88, SYS: 82, COL: 75, INI: 92 }, wins: 8, el: 'EL-7', module: 'M-FRONTEND' },
|
||||
{ id: 'DEV-003', name: '小刚', pca: { EXE: 92, TEC: 65, SYS: 70, COL: 68, INI: 85 }, wins: 3, el: 'EL-5', module: 'M-BACKEND' },
|
||||
{ id: 'DEV-004', name: '之之', pca: { EXE: 75, TEC: 58, SYS: 82, COL: 82, INI: 85 }, wins: 13, el: 'EL-6', module: 'M-DEVBOARD' }
|
||||
];
|
||||
|
||||
// 获取所有开发者
|
||||
function getDevelopers() {
|
||||
return mockDevelopers;
|
||||
}
|
||||
|
||||
// 根据ID获取开发者
|
||||
function getDeveloperById(id) {
|
||||
return mockDevelopers.find(d => d.id === id) || mockDevelopers[0];
|
||||
}
|
||||
|
||||
// 渲染开发者卡片
|
||||
function renderDeveloperCards(developers) {
|
||||
return developers.map(dev => `
|
||||
<div class="dev-card" data-dev-id="${dev.id}" tabindex="0" role="button" aria-label="查看${dev.name}的详情">
|
||||
<div class="dev-header">
|
||||
<span class="dev-id">${dev.id}</span>
|
||||
<span class="dev-wins">🏆 ${dev.wins}</span>
|
||||
</div>
|
||||
<div class="dev-name">${dev.name}</div>
|
||||
<div class="dev-details">
|
||||
<span class="dev-el">${dev.el}</span>
|
||||
<span class="dev-module">${dev.module}</span>
|
||||
</div>
|
||||
<div class="dev-pca">
|
||||
${Object.entries(dev.pca).map(([key, val]) =>
|
||||
`<span class="pca-mini ${key}">${key}:${val}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 渲染排行榜
|
||||
function renderRanking(developers) {
|
||||
// 按连胜数排序
|
||||
const sorted = [...developers].sort((a, b) => b.wins - a.wins);
|
||||
|
||||
return sorted.map((dev, index) => {
|
||||
const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : '';
|
||||
return `
|
||||
<div class="ranking-item ${rankClass}">
|
||||
<span class="rank-number">${index + 1}</span>
|
||||
<div class="rank-info">
|
||||
<div class="rank-name">${dev.name}</div>
|
||||
<div class="rank-id">${dev.id}</div>
|
||||
</div>
|
||||
<span class="rank-wins">${dev.wins}连胜</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 刷新看板
|
||||
function refreshDashboard() {
|
||||
const developers = getDevelopers();
|
||||
|
||||
// 渲染卡片
|
||||
const grid = document.getElementById('developers-grid');
|
||||
if (grid) {
|
||||
grid.innerHTML = renderDeveloperCards(developers);
|
||||
}
|
||||
|
||||
// 渲染排行榜
|
||||
const ranking = document.getElementById('ranking-list');
|
||||
if (ranking) {
|
||||
ranking.innerHTML = renderRanking(developers);
|
||||
}
|
||||
|
||||
// 绘制团队雷达图(平均分)
|
||||
const avgPca = {
|
||||
EXE: Math.round(developers.reduce((sum, d) => sum + d.pca.EXE, 0) / developers.length),
|
||||
TEC: Math.round(developers.reduce((sum, d) => sum + d.pca.TEC, 0) / developers.length),
|
||||
SYS: Math.round(developers.reduce((sum, d) => sum + d.pca.SYS, 0) / developers.length),
|
||||
COL: Math.round(developers.reduce((sum, d) => sum + d.pca.COL, 0) / developers.length),
|
||||
INI: Math.round(developers.reduce((sum, d) => sum + d.pca.INI, 0) / developers.length)
|
||||
};
|
||||
|
||||
drawRadarChart('team-radar', avgPca);
|
||||
}
|
||||
|
||||
// 搜索筛选
|
||||
function filterDevelopers(searchTerm, filterStatus) {
|
||||
let developers = getDevelopers();
|
||||
|
||||
// 搜索过滤
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
developers = developers.filter(dev =>
|
||||
dev.name.toLowerCase().includes(term) ||
|
||||
dev.id.toLowerCase().includes(term) ||
|
||||
dev.module.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
// 状态过滤(模拟,实际需要真实数据)
|
||||
if (filterStatus !== 'all') {
|
||||
// 这里简化处理,实际应该根据模块状态筛选
|
||||
developers = developers.filter(dev => {
|
||||
if (filterStatus === 'active') return dev.wins < 10;
|
||||
if (filterStatus === 'completed') return dev.wins >= 10;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return developers;
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
window.getDevelopers = getDevelopers;
|
||||
window.getDeveloperById = getDeveloperById;
|
||||
window.renderDeveloperCards = renderDeveloperCards;
|
||||
window.renderRanking = renderRanking;
|
||||
window.refreshDashboard = refreshDashboard;
|
||||
window.filterDevelopers = filterDevelopers;
|
||||
|
||||
// 增强版排行榜渲染(带数字滚动支持)
|
||||
function renderRankingWithAnimation(developers) {
|
||||
const sorted = [...developers].sort((a, b) => b.wins - a.wins);
|
||||
|
||||
return sorted.map((dev, index) => {
|
||||
const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : '';
|
||||
return `
|
||||
<div class="ranking-item ${rankClass}" data-dev-id="${dev.id}">
|
||||
<span class="rank-number">${index + 1}</span>
|
||||
<div class="rank-info">
|
||||
<div class="rank-name">${dev.name}</div>
|
||||
<div class="rank-id">${dev.id}</div>
|
||||
</div>
|
||||
<span class="rank-wins stat-value">${dev.wins}连胜</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 覆盖原函数
|
||||
window.renderRanking = renderRankingWithAnimation;
|
||||
140
agents/zhuyuan-dev-agent/modules/devboard/css/components.css
Normal file
140
agents/zhuyuan-dev-agent/modules/devboard/css/components.css
Normal file
@ -0,0 +1,140 @@
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(145deg, var(--color-bg-card), #2a3442);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #fff, var(--color-accent-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dev-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.dev-card {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dev-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.dev-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
background: linear-gradient(135deg, var(--color-accent-primary), #4cc9f0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dev-id {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dev-streak {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 70px;
|
||||
align-items: center;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ranking-streak-bar {
|
||||
background: var(--color-bg-secondary);
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.streak-progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00b4d8, #4cc9f0);
|
||||
}
|
||||
|
||||
.streak-count {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
58
agents/zhuyuan-dev-agent/modules/devboard/css/layout.css
Normal file
58
agents/zhuyuan-dev-agent/modules/devboard/css/layout.css
Normal file
@ -0,0 +1,58 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.navbar-logo span {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.navbar-info {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.sidebar-left, .main-board, .sidebar-right {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
.sidebar-right {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar-left, .main-board, .sidebar-right {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
43
agents/zhuyuan-dev-agent/modules/devboard/css/theme.css
Normal file
43
agents/zhuyuan-dev-agent/modules/devboard/css/theme.css
Normal file
@ -0,0 +1,43 @@
|
||||
:root {
|
||||
--color-bg-primary: #0a0e1a;
|
||||
--color-bg-secondary: #111827;
|
||||
--color-bg-card: #1e2433;
|
||||
--color-text-primary: #ffffff;
|
||||
--color-text-secondary: #a0aec0;
|
||||
--color-accent-primary: #00b4d8;
|
||||
--color-accent-secondary: #0077be;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--color-border: #2d3748;
|
||||
|
||||
--breakpoint-mobile: 768px;
|
||||
--breakpoint-tablet: 1024px;
|
||||
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-base);
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
358
agents/zhuyuan-dev-agent/modules/devboard/detail.css
Normal file
358
agents/zhuyuan-dev-agent/modules/devboard/detail.css
Normal file
@ -0,0 +1,358 @@
|
||||
/* detail.css - 详情页专用样式 */
|
||||
|
||||
/* 详情页容器 */
|
||||
#detail-container {
|
||||
display: none;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* 详情页头部 */
|
||||
.detail-header {
|
||||
margin-bottom: 30px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid #4a9eff;
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: #4a9eff;
|
||||
transform: translateX(-5px);
|
||||
box-shadow: 0 0 20px rgba(74, 158, 255, 0.5);
|
||||
}
|
||||
|
||||
.back-button:focus-visible {
|
||||
outline: 3px solid #ffd700;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 详情页内容区 */
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 25px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.detail-section h2 {
|
||||
color: #4a9eff;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid rgba(74, 158, 255, 0.3);
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 开发者大头卡片(大尺寸) */
|
||||
.developer-card-large .dev-card {
|
||||
transform: scale(1);
|
||||
background: linear-gradient(145deg, #1e2436, #161b28);
|
||||
border: 2px solid #4a9eff;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.developer-card-large .dev-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.5);
|
||||
}
|
||||
|
||||
/* PCA雷达图容器 */
|
||||
.radar-large-container {
|
||||
height: 300px;
|
||||
margin-bottom: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.radar-large-container canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* PCA分数标签 */
|
||||
.pca-scores {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.score-tag {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.score-tag:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.score-tag.EXE { border-color: #4CAF50; color: #4CAF50; }
|
||||
.score-tag.TEC { border-color: #2196F3; color: #2196F3; }
|
||||
.score-tag.SYS { border-color: #9C27B0; color: #9C27B0; }
|
||||
.score-tag.COL { border-color: #FF9800; color: #FF9800; }
|
||||
.score-tag.INI { border-color: #f44336; color: #f44336; }
|
||||
|
||||
/* 五维进度条 */
|
||||
.progress-bars-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s ease, background 0.2s;
|
||||
}
|
||||
|
||||
.progress-item:hover {
|
||||
transform: translateX(10px);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dim-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dim-score {
|
||||
color: #ffd700;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.progress-dim-code {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 模块历史列表 */
|
||||
.module-history-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.module-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.module-code {
|
||||
font-family: monospace;
|
||||
color: #4a9eff;
|
||||
font-weight: bold;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-已完成 { background: rgba(76, 175, 80, 0.2); color: #4CAF50; border: 1px solid #4CAF50; }
|
||||
.status-进行中 { background: rgba(255, 152, 0, 0.2); color: #FF9800; border: 1px solid #FF9800; }
|
||||
.status-待开始 { background: rgba(158, 158, 158, 0.2); color: #9E9E9E; border: 1px solid #9E9E9E; }
|
||||
|
||||
.module-date {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* 趋势图容器 */
|
||||
.trend-chart-container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 无数据提示 */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 40px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(20px); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.detail-content {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-dim-code {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.module-code {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.module-status {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.module-date {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.pca-scores {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-tag {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.radar-large-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
354
agents/zhuyuan-dev-agent/modules/devboard/detail.js
Normal file
354
agents/zhuyuan-dev-agent/modules/devboard/detail.js
Normal file
@ -0,0 +1,354 @@
|
||||
// detail.js - 开发者详情页渲染器
|
||||
|
||||
let currentDevId = null;
|
||||
|
||||
// 显示详情页
|
||||
function showDetailPage(devId) {
|
||||
currentDevId = devId;
|
||||
|
||||
const overview = document.getElementById('overview-container');
|
||||
const detail = document.getElementById('detail-container');
|
||||
|
||||
if (!overview || !detail) {
|
||||
console.error('找不到容器元素');
|
||||
return;
|
||||
}
|
||||
|
||||
// 淡出总览页
|
||||
overview.classList.add('fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
overview.style.display = 'none';
|
||||
overview.classList.remove('fade-out');
|
||||
|
||||
// 渲染详情页数据
|
||||
renderDetailPage(devId);
|
||||
|
||||
detail.style.display = 'block';
|
||||
detail.classList.add('fade-in');
|
||||
|
||||
// 滚动到顶部
|
||||
window.scrollTo(0, 0);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 返回总览页
|
||||
function hideDetailPage() {
|
||||
const overview = document.getElementById('overview-container');
|
||||
const detail = document.getElementById('detail-container');
|
||||
|
||||
detail.classList.add('fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
detail.style.display = 'none';
|
||||
detail.classList.remove('fade-out');
|
||||
|
||||
overview.style.display = 'block';
|
||||
overview.classList.add('fade-in');
|
||||
|
||||
// 刷新总览页数据
|
||||
refreshDashboard();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 渲染详情页
|
||||
function renderDetailPage(devId) {
|
||||
const dev = getDeveloperById(devId);
|
||||
if (!dev) return;
|
||||
|
||||
const container = document.getElementById('detail-container');
|
||||
|
||||
// 生成五维进度条
|
||||
const progressBars = renderPCAProgressBars(dev.pca);
|
||||
|
||||
// 获取模块历史
|
||||
const modules = getModuleHistory(devId);
|
||||
const moduleList = renderModuleHistory(modules);
|
||||
|
||||
// 获取连胜趋势数据
|
||||
const trendData = getWinTrendData(devId);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="detail-header">
|
||||
<button class="back-button" onclick="hideDetailPage()" aria-label="返回总览">
|
||||
← 返回总览
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<!-- 开发者大头卡片 -->
|
||||
<div class="detail-section developer-card-large">
|
||||
<div class="dev-card" data-dev-id="${dev.id}">
|
||||
<div class="dev-header">
|
||||
<span class="dev-id">${dev.id}</span>
|
||||
<span class="dev-wins">🏆 ${dev.wins}</span>
|
||||
</div>
|
||||
<div class="dev-name">${dev.name}</div>
|
||||
<div class="dev-details">
|
||||
<span class="dev-el">${dev.el}</span>
|
||||
<span class="dev-module">${dev.module}</span>
|
||||
</div>
|
||||
<div class="dev-pca">
|
||||
${Object.entries(dev.pca).map(([key, val]) =>
|
||||
`<span class="pca-mini ${key}">${key}:${val}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PCA雷达图 -->
|
||||
<div class="detail-section">
|
||||
<h2>📊 完整PCA雷达图</h2>
|
||||
<div class="radar-large-container">
|
||||
${renderRadarChart(dev.pca, 'large')}
|
||||
</div>
|
||||
<div class="pca-scores">
|
||||
${Object.entries(dev.pca).map(([dim, score]) =>
|
||||
`<span class="score-tag ${dim}">${dim}: ${score}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 五维进度条 -->
|
||||
<div class="detail-section">
|
||||
<h2>📈 五维进度条</h2>
|
||||
<div class="progress-bars-container">
|
||||
${progressBars}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模块历史 -->
|
||||
<div class="detail-section">
|
||||
<h2>📋 模块历史</h2>
|
||||
<div class="module-history-container">
|
||||
${moduleList}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连胜趋势 -->
|
||||
<div class="detail-section">
|
||||
<h2>🏆 连胜趋势</h2>
|
||||
<div class="trend-chart-container">
|
||||
<canvas id="trend-canvas" width="400" height="200"
|
||||
style="width:100%; height:200px; background: rgba(0,0,0,0.2); border-radius: 8px;">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绘制趋势图
|
||||
setTimeout(() => drawTrendChart('trend-canvas', trendData), 100);
|
||||
}
|
||||
|
||||
// 渲染PCA五维进度条
|
||||
function renderPCAProgressBars(pca) {
|
||||
const dimensions = [
|
||||
{ key: 'EXE', label: '执行力', color: '#4CAF50' },
|
||||
{ key: 'TEC', label: '技术深度', color: '#2196F3' },
|
||||
{ key: 'SYS', label: '系统思维', color: '#9C27B0' },
|
||||
{ key: 'COL', label: '协作力', color: '#FF9800' },
|
||||
{ key: 'INI', label: '主动性', color: '#f44336' }
|
||||
];
|
||||
|
||||
return dimensions.map(dim => {
|
||||
const score = pca[dim.key] || 0;
|
||||
|
||||
return `
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">
|
||||
<span class="dim-name">${dim.label}</span>
|
||||
<span class="dim-score">${score}</span>
|
||||
</div>
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar-fill"
|
||||
style="width: ${score}%; background-color: ${dim.color};">
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-dim-code">${dim.key}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染模块历史列表
|
||||
function renderModuleHistory(modules) {
|
||||
if (!modules || modules.length === 0) {
|
||||
return '<p class="no-data">暂无模块历史</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<ul class="module-history-list">
|
||||
${modules.map(m => `
|
||||
<li class="module-item status-${m.status}">
|
||||
<span class="module-code">${m.code}</span>
|
||||
<span class="module-name">${m.name}</span>
|
||||
<span class="module-status">${m.status}</span>
|
||||
<span class="module-date">${m.completedDate || m.startDate || '-'}</span>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
// 获取模块历史(模拟数据)
|
||||
function getModuleHistory(devId) {
|
||||
const histories = {
|
||||
'DEV-001': [
|
||||
{ code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-10' },
|
||||
{ code: 'M-FRONTEND-003', name: '前端基础组件', status: '已完成', completedDate: '2026-03-08' },
|
||||
{ code: 'M-API-002', name: 'API对接练习', status: '进行中', completedDate: null }
|
||||
],
|
||||
'DEV-002': [
|
||||
{ code: 'M-FRONTEND-001', name: '前端架构设计', status: '已完成', completedDate: '2026-03-09' },
|
||||
{ code: 'M-UI-005', name: '组件库开发', status: '已完成', completedDate: '2026-03-07' },
|
||||
{ code: 'M-TEST-003', name: '单元测试编写', status: '待开始', completedDate: null }
|
||||
],
|
||||
'DEV-003': [
|
||||
{ code: 'M-BACKEND-002', name: 'API服务搭建', status: '已完成', completedDate: '2026-03-11' },
|
||||
{ code: 'M-DB-001', name: '数据库设计', status: '已完成', completedDate: '2026-03-09' },
|
||||
{ code: 'M-DEPLOY-002', name: '容器化部署', status: '进行中', completedDate: null }
|
||||
],
|
||||
'DEV-004': [
|
||||
{ code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-11' },
|
||||
{ code: 'M-CANVAS-005', name: 'Canvas雷达图绘制', status: '已完成', completedDate: '2026-03-09' },
|
||||
{ code: 'M-COMPONENTS-003', name: '组件化思维训练', status: '已完成', completedDate: '2026-03-07' },
|
||||
{ code: 'M-DEPLOY-001', name: '静态部署练习', status: '待开始', completedDate: null }
|
||||
]
|
||||
};
|
||||
return histories[devId] || histories['DEV-001'];
|
||||
}
|
||||
|
||||
// 获取连胜趋势数据
|
||||
function getWinTrendData(devId) {
|
||||
const trends = {
|
||||
'DEV-001': [
|
||||
{ date: '03-06', value: 3 },
|
||||
{ date: '03-07', value: 3 },
|
||||
{ date: '03-08', value: 4 },
|
||||
{ date: '03-09', value: 4 },
|
||||
{ date: '03-10', value: 5 },
|
||||
{ date: '03-11', value: 5 },
|
||||
{ date: '03-12', value: 5 }
|
||||
],
|
||||
'DEV-002': [
|
||||
{ date: '03-06', value: 5 },
|
||||
{ date: '03-07', value: 6 },
|
||||
{ date: '03-08', value: 6 },
|
||||
{ date: '03-09', value: 7 },
|
||||
{ date: '03-10', value: 7 },
|
||||
{ date: '03-11', value: 8 },
|
||||
{ date: '03-12', value: 8 }
|
||||
],
|
||||
'DEV-003': [
|
||||
{ date: '03-06', value: 1 },
|
||||
{ date: '03-07', value: 1 },
|
||||
{ date: '03-08', value: 2 },
|
||||
{ date: '03-09', value: 2 },
|
||||
{ date: '03-10', value: 3 },
|
||||
{ date: '03-11', value: 3 },
|
||||
{ date: '03-12', value: 3 }
|
||||
],
|
||||
'DEV-004': [
|
||||
{ date: '03-06', value: 8 },
|
||||
{ date: '03-07', value: 9 },
|
||||
{ date: '03-08', value: 9 },
|
||||
{ date: '03-09', value: 10 },
|
||||
{ date: '03-10', value: 11 },
|
||||
{ date: '03-11', value: 12 },
|
||||
{ date: '03-12', value: 13 }
|
||||
]
|
||||
};
|
||||
return trends[devId] || trends['DEV-001'];
|
||||
}
|
||||
|
||||
// 绘制趋势图
|
||||
function drawTrendChart(canvasId, data) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const padding = 40;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
if (!data || data.length < 2) {
|
||||
ctx.fillStyle = '#888';
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillText('暂无连胜数据', padding, height/2);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算坐标范围
|
||||
const values = data.map(d => d.value);
|
||||
const maxValue = Math.max(...values) + 1;
|
||||
const minValue = Math.max(0, Math.min(...values) - 1);
|
||||
const xStep = (width - 2 * padding) / (data.length - 1);
|
||||
|
||||
// 绘制网格线
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = padding + (i * (height - 2 * padding) / 5);
|
||||
ctx.moveTo(padding, y);
|
||||
ctx.lineTo(width - padding, y);
|
||||
}
|
||||
ctx.strokeStyle = '#444';
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制轴线
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#666';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.moveTo(padding, padding);
|
||||
ctx.lineTo(padding, height - padding);
|
||||
ctx.lineTo(width - padding, height - padding);
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制折线
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#4CAF50';
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
data.forEach((point, i) => {
|
||||
const x = padding + i * xStep;
|
||||
const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue));
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制数据点
|
||||
data.forEach((point, i) => {
|
||||
const x = padding + i * xStep;
|
||||
const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 显示数值
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 12px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(point.value, x, y - 15);
|
||||
|
||||
// 显示日期
|
||||
ctx.fillStyle = '#aaa';
|
||||
ctx.font = '10px monospace';
|
||||
ctx.fillText(point.date, x, height - padding + 20);
|
||||
});
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
window.showDetailPage = showDetailPage;
|
||||
window.hideDetailPage = hideDetailPage;
|
||||
window.getModuleHistory = getModuleHistory;
|
||||
window.getWinTrendData = getWinTrendData;
|
||||
35
agents/zhuyuan-dev-agent/modules/devboard/index.html
Normal file
35
agents/zhuyuan-dev-agent/modules/devboard/index.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>开发者实时看板 · HoloLake</title>
|
||||
<style>
|
||||
/* 这里保留你原有的样式,如果样式比较多,建议去复制原有的内容 */
|
||||
/* 为了保持简洁,秋秋只放必须的样式框架,妈妈根据实际补充 */
|
||||
body {
|
||||
background: #0a0e17;
|
||||
color: #e1e5ee;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
/* 其他样式保持不变 */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 HoloLake 开发者实时看板</h1>
|
||||
<div id="stats-container"></div>
|
||||
<div id="developers-container"></div>
|
||||
<div id="leaderboard-container"></div>
|
||||
|
||||
<!-- ⚠️ 重点:脚本引入顺序必须正确! -->
|
||||
<!-- 1. API客户端最先加载 -->
|
||||
<script src="api.js"></script>
|
||||
<!-- 2. 然后是原有的业务逻辑脚本 -->
|
||||
<script src="main.js"></script>
|
||||
<script src="components.js"></script>
|
||||
<!-- 3. 初始化引擎最后加载 -->
|
||||
<script src="api-init.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
agents/zhuyuan-dev-agent/modules/devboard/js/api.js
Normal file
54
agents/zhuyuan-dev-agent/modules/devboard/js/api.js
Normal file
@ -0,0 +1,54 @@
|
||||
const MOCK_DEVELOPERS = [
|
||||
{ id: 'DEV-001', name: '玄泓', streak: 8, el: 6, pca: { exe: 75, tec: 68, sys: 82, col: 79, ini: 85 }, totalScore: 78, status: 'doing', module: 'M-CORE', lastActive: '2026-03-11T10:30:00Z' },
|
||||
{ id: 'DEV-002', name: '苍钺', streak: 5, el: 5, pca: { exe: 82, tec: 71, sys: 65, col: 90, ini: 72 }, totalScore: 76, status: 'pending', module: 'M-UI', lastActive: '2026-03-11T09:15:00Z' },
|
||||
{ id: 'DEV-003', name: '桔子', streak: 13, el: 8, pca: { exe: 88, tec: 85, sys: 92, col: 78, ini: 90 }, totalScore: 87, status: 'doing', module: 'M-ORCHESTRATOR', lastActive: '2026-03-11T11:20:00Z' },
|
||||
{ id: 'DEV-004', name: '之之', streak: 12, el: 8, pca: { exe: 70, tec: 32, sys: 80, col: 80, ini: 83 }, totalScore: 66, status: 'doing', module: 'M-DEVBOARD', lastActive: '2026-03-11T12:05:00Z' },
|
||||
{ id: 'DEV-005', name: '琉光', streak: 7, el: 6, pca: { exe: 79, tec: 74, sys: 71, col: 88, ini: 77 }, totalScore: 78, status: 'done', module: 'M-TEST', lastActive: '2026-03-10T16:40:00Z' },
|
||||
{ id: 'DEV-006', name: '墨羽', streak: 4, el: 4, pca: { exe: 65, tec: 60, sys: 55, col: 92, ini: 68 }, totalScore: 68, status: 'doing', module: 'M-DOC', lastActive: '2026-03-11T08:50:00Z' },
|
||||
{ id: 'DEV-007', name: '霜砚', streak: 10, el: 7, pca: { exe: 84, tec: 79, sys: 81, col: 75, ini: 82 }, totalScore: 80, status: 'blocked', module: 'M-API', lastActive: '2026-03-11T07:30:00Z' },
|
||||
{ id: 'DEV-008', name: '岚茵', streak: 6, el: 5, pca: { exe: 73, tec: 70, sys: 68, col: 85, ini: 74 }, totalScore: 74, status: 'pending', module: 'M-DESIGN', lastActive: '2026-03-10T14:20:00Z' }
|
||||
];
|
||||
|
||||
function getTopStreak(developers) {
|
||||
if (!developers || developers.length === 0) return { name: '无', count: 0 };
|
||||
let top = developers[0];
|
||||
for (let i = 1; i < developers.length; i++) {
|
||||
if (developers[i].streak > top.streak) {
|
||||
top = developers[i];
|
||||
}
|
||||
}
|
||||
return { name: top.name, count: top.streak };
|
||||
}
|
||||
|
||||
async function getDevStatus() {
|
||||
return MOCK_DEVELOPERS;
|
||||
}
|
||||
|
||||
async function getPCA(devId) {
|
||||
const dev = MOCK_DEVELOPERS.find(d => d.id === devId);
|
||||
if (!dev) return null;
|
||||
return {
|
||||
exe: dev.pca.exe,
|
||||
tec: dev.pca.tec,
|
||||
sys: dev.pca.sys,
|
||||
col: dev.pca.col,
|
||||
ini: dev.pca.ini,
|
||||
total: dev.totalScore,
|
||||
level: getPCAColor(dev.totalScore).level
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllStats() {
|
||||
const activeDevs = MOCK_DEVELOPERS.filter(d => d.status === 'doing').length;
|
||||
const topStreak = getTopStreak(MOCK_DEVELOPERS);
|
||||
return {
|
||||
totalDevs: 8,
|
||||
activeDevs: activeDevs,
|
||||
totalCodeLines: 85600,
|
||||
totalModules: 24,
|
||||
modulesCompleted: 8,
|
||||
modulesInProgress: 12,
|
||||
modulesPending: 4,
|
||||
topStreak: topStreak
|
||||
};
|
||||
}
|
||||
53
agents/zhuyuan-dev-agent/modules/devboard/js/app.js
Normal file
53
agents/zhuyuan-dev-agent/modules/devboard/js/app.js
Normal file
@ -0,0 +1,53 @@
|
||||
let refreshInterval;
|
||||
let countdownInterval;
|
||||
let seconds = 30;
|
||||
|
||||
function updateLastUpdateTime() {
|
||||
const el = document.getElementById('last-update-time');
|
||||
if (el) {
|
||||
const now = new Date();
|
||||
el.textContent = `最后更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const el = document.getElementById('refresh-indicator');
|
||||
if (el) el.textContent = `⏳ ${seconds}s`;
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
seconds = 30;
|
||||
updateCountdown();
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
countdownInterval = setInterval(() => {
|
||||
seconds--;
|
||||
if (seconds <= 0) seconds = 30;
|
||||
updateCountdown();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function refreshAllData() {
|
||||
const devs = await getDevStatus();
|
||||
const stats = await getAllStats();
|
||||
if (typeof renderStats === 'function') renderStats(stats);
|
||||
if (typeof renderDevCards === 'function') renderDevCards(devs);
|
||||
if (typeof renderRanking === 'function') renderRanking(devs);
|
||||
updateLastUpdateTime();
|
||||
}
|
||||
|
||||
async function initApp() {
|
||||
await initBoard();
|
||||
await initRanking();
|
||||
initRadar();
|
||||
await refreshAllData();
|
||||
startCountdown();
|
||||
if (refreshInterval) clearInterval(refreshInterval);
|
||||
refreshInterval = setInterval(refreshAllData, 30000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (refreshInterval) clearInterval(refreshInterval);
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
});
|
||||
72
agents/zhuyuan-dev-agent/modules/devboard/js/board.js
Normal file
72
agents/zhuyuan-dev-agent/modules/devboard/js/board.js
Normal file
@ -0,0 +1,72 @@
|
||||
function renderStats(stats) {
|
||||
const container = document.getElementById('stats-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃开发者</div>
|
||||
<div class="stat-number">${stats.activeDevs}</div>
|
||||
<div class="stat-sub">总人数 ${stats.totalDevs}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总代码量</div>
|
||||
<div class="stat-number">${formatNumber(stats.totalCodeLines)}</div>
|
||||
<div class="stat-sub">行代码</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">模块进度</div>
|
||||
<div class="stat-number">${stats.modulesCompleted}/${stats.totalModules}</div>
|
||||
<div class="stat-sub">✅ ${stats.modulesCompleted} / ⚡ ${stats.modulesInProgress} / ⏳ ${stats.modulesPending}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">最高连胜</div>
|
||||
<div class="stat-number">${stats.topStreak.count}</div>
|
||||
<div class="stat-sub">${stats.topStreak.name} ${getStreakEmoji(stats.topStreak.count)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDevCards(developers) {
|
||||
const container = document.getElementById('devgrid-container');
|
||||
if (!container) return;
|
||||
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
|
||||
let html = '<div class="dev-grid">';
|
||||
sorted.forEach(dev => {
|
||||
const pcaColor = getPCAColor(dev.totalScore);
|
||||
html += `
|
||||
<div class="dev-card" data-dev-id="${dev.id}">
|
||||
<div class="dev-card-header">
|
||||
<div class="dev-avatar">${dev.name[0]}</div>
|
||||
<div class="dev-info">
|
||||
<div class="dev-name">${dev.name}</div>
|
||||
<div class="dev-id">${dev.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dev-module">📁 ${dev.module}</div>
|
||||
<div class="dev-stats">
|
||||
<div class="dev-streak">${dev.streak}<span>连胜</span></div>
|
||||
<div class="dev-badges">
|
||||
<span class="badge badge-el">EL-${dev.el}</span>
|
||||
<span class="badge" style="background:${pcaColor.color}20; color:${pcaColor.color}">${pcaColor.level} · ${dev.totalScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dev-footer">
|
||||
<span>${getStatusBadge(dev.status)}</span>
|
||||
<span>🕒 ${formatDate(dev.lastActive)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function initBoard() {
|
||||
const devs = await getDevStatus();
|
||||
const stats = await getAllStats();
|
||||
renderStats(stats);
|
||||
renderDevCards(devs);
|
||||
}
|
||||
|
||||
window.initBoard = initBoard;
|
||||
112
agents/zhuyuan-dev-agent/modules/devboard/js/radar.js
Normal file
112
agents/zhuyuan-dev-agent/modules/devboard/js/radar.js
Normal file
@ -0,0 +1,112 @@
|
||||
function drawRadar(canvasId, pcaData) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
const maxR = Math.min(w, h) * 0.35;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const dims = [
|
||||
{ label: '执行力', val: pcaData.exe || 0 },
|
||||
{ label: '技术纵深', val: pcaData.tec || 0 },
|
||||
{ label: '系统理解', val: pcaData.sys || 0 },
|
||||
{ label: '协作力', val: pcaData.col || 0 },
|
||||
{ label: '主动性', val: pcaData.ini || 0 }
|
||||
];
|
||||
|
||||
for (let lv = 1; lv <= 5; lv++) {
|
||||
const r = (maxR * lv) / 5;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const x = cx + r * Math.cos(angle);
|
||||
const y = cy + r * Math.sin(angle);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.strokeStyle = '#2d3748';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.lineTo(cx + maxR * Math.cos(angle), cy + maxR * Math.sin(angle));
|
||||
ctx.strokeStyle = '#4a5568';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#cbd5e0';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const x = cx + (maxR + 25) * Math.cos(angle);
|
||||
const y = cy + (maxR + 25) * Math.sin(angle);
|
||||
ctx.fillText(dims[i].label, x, y);
|
||||
}
|
||||
|
||||
const points = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const r = (dims[i].val / 100) * maxR;
|
||||
points.push({
|
||||
x: cx + r * Math.cos(angle),
|
||||
y: cy + r * Math.sin(angle)
|
||||
});
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < 5; i++) ctx.lineTo(points[i].x, points[i].y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(0, 180, 216, 0.3)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#00b4d8';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
points.forEach(p => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#00b4d8';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 24px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(pcaData.total || '0', cx, cy - 10);
|
||||
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
const colorInfo = getPCAColor(pcaData.total || 0);
|
||||
ctx.fillStyle = colorInfo.color;
|
||||
ctx.fillText(pcaData.level || 'C', cx, cy + 15);
|
||||
}
|
||||
|
||||
async function loadRadarForDev(devId) {
|
||||
const pcaData = await getPCA(devId);
|
||||
if (pcaData) drawRadar('pca-canvas', pcaData);
|
||||
}
|
||||
|
||||
function initRadar() {
|
||||
document.querySelectorAll('.dev-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
loadRadarForDev(card.dataset.devId);
|
||||
});
|
||||
});
|
||||
loadRadarForDev('DEV-004');
|
||||
}
|
||||
|
||||
window.initRadar = initRadar;
|
||||
window.loadRadarForDev = loadRadarForDev;
|
||||
34
agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js
Normal file
34
agents/zhuyuan-dev-agent/modules/devboard/js/ranking.js
Normal file
@ -0,0 +1,34 @@
|
||||
function renderRanking(developers) {
|
||||
const container = document.getElementById('ranking-container');
|
||||
if (!container) return;
|
||||
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
|
||||
const maxStreak = sorted[0].streak;
|
||||
|
||||
let html = '<div class="ranking-header"><h3>🏆 连胜排行榜</h3><span>实时更新</span></div><div class="ranking-list">';
|
||||
|
||||
sorted.forEach((dev, idx) => {
|
||||
const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : `${idx+1}.`;
|
||||
const percent = (dev.streak / maxStreak) * 100;
|
||||
html += `
|
||||
<div class="ranking-item" data-dev-id="${dev.id}">
|
||||
<div class="ranking-rank">${medal}</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">${dev.name}</div>
|
||||
<div class="ranking-streak-bar">
|
||||
<div class="streak-progress" style="width:${percent}%"></div>
|
||||
<span class="streak-count">${dev.streak}连胜</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function initRanking() {
|
||||
const devs = await getDevStatus();
|
||||
renderRanking(devs);
|
||||
}
|
||||
|
||||
window.initRanking = initRanking;
|
||||
36
agents/zhuyuan-dev-agent/modules/devboard/js/utils.js
Normal file
36
agents/zhuyuan-dev-agent/modules/devboard/js/utils.js
Normal file
@ -0,0 +1,36 @@
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return '--:--:--';
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function getStreakEmoji(count) {
|
||||
if (count >= 20) return '🔥🔥🔥';
|
||||
if (count >= 15) return '🔥🔥';
|
||||
if (count >= 10) return '🔥';
|
||||
if (count >= 5) return '⚡';
|
||||
if (count >= 3) return '👍';
|
||||
return '👣';
|
||||
}
|
||||
|
||||
function getPCAColor(score) {
|
||||
if (score >= 90) return { color: '#FFD700', level: 'S', text: '传奇' };
|
||||
if (score >= 80) return { color: '#00B4D8', level: 'A', text: '精英' };
|
||||
if (score >= 70) return { color: '#10B981', level: 'B', text: '专家' };
|
||||
if (score >= 60) return { color: '#F59E0B', level: 'C', text: '熟手' };
|
||||
return { color: '#EF4444', level: 'D', text: '新手' };
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
const map = {
|
||||
'done': '✅ 已完成',
|
||||
'doing': '⚡ 进行中',
|
||||
'pending': '⏳ 待分配',
|
||||
'blocked': '⚠️ 阻塞'
|
||||
};
|
||||
return map[status] || '⏳ 待分配';
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0';
|
||||
}
|
||||
236
agents/zhuyuan-dev-agent/modules/devboard/main.js
Normal file
236
agents/zhuyuan-dev-agent/modules/devboard/main.js
Normal file
@ -0,0 +1,236 @@
|
||||
// main.js - 主逻辑(增强版)
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始渲染
|
||||
refreshDashboard();
|
||||
|
||||
// 搜索功能(带防抖和动画)
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (searchInput) {
|
||||
let searchTimer;
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
// 添加输入时的微动效
|
||||
this.style.transform = 'scale(1.02)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = '';
|
||||
}, 150);
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// 筛选按钮
|
||||
const filterBtns = document.querySelectorAll('.filter-btn');
|
||||
filterBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// 按钮点击动画
|
||||
this.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = '';
|
||||
}, 150);
|
||||
|
||||
// 更新按钮状态
|
||||
filterBtns.forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// 应用筛选
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// 卡片点击事件(使用事件委托)
|
||||
document.addEventListener('click', function(e) {
|
||||
const card = e.target.closest('.dev-card');
|
||||
if (card) {
|
||||
const devId = card.dataset.devId;
|
||||
if (devId && window.showDetailPage) {
|
||||
// 添加点击动画
|
||||
card.style.transform = 'scale(0.98)';
|
||||
card.style.opacity = '0.8';
|
||||
|
||||
setTimeout(() => {
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
window.showDetailPage(devId);
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘导航
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// 获取所有可聚焦的卡片
|
||||
const cards = Array.from(document.querySelectorAll('.dev-card'));
|
||||
const currentIndex = cards.indexOf(document.activeElement);
|
||||
|
||||
// Tab键(默认行为已支持,我们只加样式)
|
||||
if (e.key === 'Tab') {
|
||||
setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
if (focused.classList.contains('dev-card')) {
|
||||
focused.style.outline = '3px solid #ffd700';
|
||||
focused.style.transform = 'scale(1.02)';
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// 箭头键导航
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
|
||||
if (cards.length === 0) return;
|
||||
|
||||
const cols = Math.floor(document.querySelector('.developers-grid').offsetWidth / 300);
|
||||
let nextIndex;
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
nextIndex = currentIndex + 1;
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
nextIndex = currentIndex - 1;
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
nextIndex = currentIndex + cols;
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
nextIndex = currentIndex - cols;
|
||||
}
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < cards.length) {
|
||||
cards[nextIndex].focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Enter/Space 打开详情
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const target = e.target.closest('.dev-card');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const devId = target.dataset.devId;
|
||||
if (devId && window.showDetailPage) {
|
||||
window.showDetailPage(devId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ESC返回总览
|
||||
if (e.key === 'Escape') {
|
||||
const detail = document.getElementById('detail-container');
|
||||
if (detail && detail.style.display !== 'none' && window.hideDetailPage) {
|
||||
window.hideDetailPage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 失去焦点时移除高亮
|
||||
document.addEventListener('blur', function(e) {
|
||||
if (e.target.classList.contains('dev-card')) {
|
||||
e.target.style.outline = '';
|
||||
e.target.style.transform = '';
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
// 应用搜索和筛选
|
||||
function applyFilters() {
|
||||
const searchTerm = document.getElementById('search-input')?.value || '';
|
||||
const activeFilter = document.querySelector('.filter-btn.active')?.dataset.filter || 'all';
|
||||
|
||||
// 获取筛选后的开发者
|
||||
const filtered = filterDevelopers(searchTerm, activeFilter);
|
||||
|
||||
// 重新渲染卡片
|
||||
const grid = document.getElementById('developers-grid');
|
||||
if (grid) {
|
||||
// 添加淡出动画
|
||||
grid.style.opacity = '0.5';
|
||||
|
||||
setTimeout(() => {
|
||||
if (filtered.length === 0) {
|
||||
// 显示无结果提示
|
||||
grid.innerHTML = '<div class="no-results">🔍 没有找到匹配的开发者</div>';
|
||||
} else {
|
||||
grid.innerHTML = renderDeveloperCards(filtered);
|
||||
|
||||
// 添加展开/收起动画
|
||||
Array.from(grid.children).forEach((card, index) => {
|
||||
card.style.animation = `cardPopIn 0.3s ease ${index * 0.05}s forwards`;
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
card.style.opacity = '1';
|
||||
}, 50 + index * 20);
|
||||
});
|
||||
}
|
||||
|
||||
grid.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// 更新排行榜(排行榜不筛选,但高亮匹配项)
|
||||
const ranking = document.getElementById('ranking-list');
|
||||
if (ranking) {
|
||||
const allDevs = getDevelopers();
|
||||
ranking.innerHTML = renderRanking(allDevs);
|
||||
|
||||
// 高亮筛选出的开发者
|
||||
if (filtered.length < allDevs.length && filtered.length > 0) {
|
||||
const filteredIds = filtered.map(d => d.id);
|
||||
document.querySelectorAll('.ranking-item').forEach(item => {
|
||||
const nameEl = item.querySelector('.rank-name');
|
||||
if (nameEl) {
|
||||
const dev = allDevs.find(d => d.name === nameEl.textContent);
|
||||
if (dev && filteredIds.includes(dev.id)) {
|
||||
item.classList.add('match-highlight');
|
||||
|
||||
// 添加数字滚动动画
|
||||
const winsEl = item.querySelector('.rank-wins');
|
||||
if (winsEl) {
|
||||
const oldValue = parseInt(winsEl.textContent) || 0;
|
||||
animateNumber(winsEl, oldValue, dev.wins, 500);
|
||||
}
|
||||
} else {
|
||||
item.classList.remove('match-highlight');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.ranking-item').forEach(item => {
|
||||
item.classList.remove('match-highlight');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数字滚动动画
|
||||
function animateNumber(element, start, end, duration = 1000) {
|
||||
const range = end - start;
|
||||
const increment = range / (duration / 16);
|
||||
let current = start;
|
||||
const startTime = performance.now();
|
||||
|
||||
function update(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 缓动函数
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 3);
|
||||
current = start + (range * easeOutQuart);
|
||||
|
||||
element.textContent = Math.round(current) + (element.textContent.includes('连胜') ? '连胜' : '');
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
element.textContent = end + (element.textContent.includes('连胜') ? '连胜' : '');
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
window.applyFilters = applyFilters;
|
||||
window.animateNumber = animateNumber;
|
||||
116
agents/zhuyuan-dev-agent/modules/devboard/radar.js
Normal file
116
agents/zhuyuan-dev-agent/modules/devboard/radar.js
Normal file
@ -0,0 +1,116 @@
|
||||
// radar.js - 雷达图绘制
|
||||
function drawRadarChart(canvasId, data) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) * 0.35;
|
||||
|
||||
const dimensions = ['EXE', 'TEC', 'SYS', 'COL', 'INI'];
|
||||
const angleStep = (Math.PI * 2) / dimensions.length;
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 绘制背景网格
|
||||
ctx.strokeStyle = '#2a3040';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// 绘制5层同心圆
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= dimensions.length; i++) {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const x = centerX + radius * (level / 5) * Math.cos(angle);
|
||||
const y = centerY + radius * (level / 5) * Math.sin(angle);
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.strokeStyle = '#2a3040';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制轴线
|
||||
for (let i = 0; i < dimensions.length; i++) {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const x = centerX + radius * Math.cos(angle);
|
||||
const y = centerY + radius * Math.sin(angle);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.strokeStyle = '#3a4050';
|
||||
ctx.stroke();
|
||||
|
||||
// 标注维度
|
||||
const labelX = centerX + (radius + 15) * Math.cos(angle);
|
||||
const labelY = centerY + (radius + 15) * Math.sin(angle);
|
||||
ctx.fillStyle = '#4a9eff';
|
||||
ctx.font = 'bold 14px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(dimensions[i], labelX, labelY);
|
||||
}
|
||||
|
||||
// 绘制数据
|
||||
if (data) {
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < dimensions.length; i++) {
|
||||
const value = data[dimensions[i]] || 0;
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const r = radius * (value / 100);
|
||||
const x = centerX + r * Math.cos(angle);
|
||||
const y = centerY + r * Math.sin(angle);
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(74, 158, 255, 0.3)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#4a9eff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制数据点
|
||||
for (let i = 0; i < dimensions.length; i++) {
|
||||
const value = data[dimensions[i]] || 0;
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const r = radius * (value / 100);
|
||||
const x = centerX + r * Math.cos(angle);
|
||||
const y = centerY + r * Math.sin(angle);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#4a9eff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染雷达图HTML(供detail.js调用)
|
||||
function renderRadarChart(pcaData, size = 'normal') {
|
||||
const canvasId = 'radar-' + Math.random().toString(36).substr(2, 9);
|
||||
const width = size === 'large' ? 400 : 300;
|
||||
const height = size === 'large' ? 300 : 250;
|
||||
|
||||
setTimeout(() => {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (canvas) drawRadarChart(canvas.id, pcaData);
|
||||
}, 100);
|
||||
|
||||
return `<canvas id="${canvasId}" width="${width}" height="${height}" style="width:100%; height:auto;"></canvas>`;
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
window.drawRadarChart = drawRadarChart;
|
||||
window.renderRadarChart = renderRadarChart;
|
||||
287
agents/zhuyuan-dev-agent/modules/devboard/style.css
Normal file
287
agents/zhuyuan-dev-agent/modules/devboard/style.css
Normal file
@ -0,0 +1,287 @@
|
||||
/* style.css - 基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
|
||||
background: #0a0c14;
|
||||
color: #e0e0e0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 总览页容器 */
|
||||
#overview-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #1a1f2e, #0f1219);
|
||||
border-radius: 20px;
|
||||
border: 1px solid #2a3040;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
color: #4a9eff;
|
||||
font-size: 2em;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 20px rgba(74, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 控制区 */
|
||||
.controls-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
padding: 12px 20px;
|
||||
background: #1a1f2e;
|
||||
border: 2px solid #2a3040;
|
||||
border-radius: 30px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 0 20px rgba(74, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 10px 24px;
|
||||
background: #1a1f2e;
|
||||
border: 2px solid #2a3040;
|
||||
border-radius: 30px;
|
||||
color: #a0a0a0;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #4a9eff;
|
||||
border-color: #4a9eff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(74, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 开发者网格 */
|
||||
.developers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* 统计区 */
|
||||
.stats-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.radar-container, .ranking-container {
|
||||
background: #1a1f2e;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #2a3040;
|
||||
}
|
||||
|
||||
.radar-container h2, .ranking-container h2 {
|
||||
color: #4a9eff;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#team-radar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* ===== 交互增强样式 ===== */
|
||||
|
||||
/* 搜索框增强 */
|
||||
.search-box {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
transform: scale(1.02);
|
||||
background: #1e2436;
|
||||
}
|
||||
|
||||
/* 筛选按钮动画 */
|
||||
.filter-btn {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.3s, height 0.3s;
|
||||
}
|
||||
|
||||
.filter-btn:active::after {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* 卡片网格动画 */
|
||||
.developers-grid {
|
||||
transition: opacity 0.3s ease;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* 卡片展开/收起动画 */
|
||||
.dev-card {
|
||||
animation: cardPopIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@keyframes cardPopIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 键盘焦点样式 */
|
||||
.dev-card:focus-visible {
|
||||
outline: 3px solid #ffd700;
|
||||
outline-offset: 2px;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 数字滚动动画 */
|
||||
.stat-value {
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
/* 无结果提示 */
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
border: 2px dashed #2a3040;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
/* 高亮筛选匹配项 */
|
||||
.ranking-item.match-highlight {
|
||||
background: rgba(74, 158, 255, 0.2);
|
||||
border-left: 4px solid #4a9eff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* 淡入淡出动画增强 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.dev-card:focus-visible {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.filter-btn:active::after {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
90
agents/zhuyuan-dev-agent/modules/email-notify.js
Normal file
90
agents/zhuyuan-dev-agent/modules/email-notify.js
Normal file
@ -0,0 +1,90 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · 邮件通知模块
|
||||
// QQ邮箱 SMTP · smtp.qq.com:465
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
let transporter = null;
|
||||
|
||||
function getTransporter() {
|
||||
if (transporter) return transporter;
|
||||
|
||||
const user = process.env.ZY_SMTP_USER;
|
||||
const pass = process.env.ZY_SMTP_PASS;
|
||||
|
||||
if (!user || !pass) {
|
||||
console.warn("[EmailNotify] ZY_SMTP_USER 或 ZY_SMTP_PASS 未配置,邮件功能不可用");
|
||||
return null;
|
||||
}
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: "smtp.qq.com",
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: { user, pass }
|
||||
});
|
||||
|
||||
console.log("[EmailNotify] SMTP 已连接 · " + user);
|
||||
return transporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知邮件
|
||||
* @param {string} to - 收件人邮箱
|
||||
* @param {string} planId - 规划ID
|
||||
* @param {string} status - 状态: started / completed / failed
|
||||
* @param {object} summary - { task_count, done_count, failed_count, errors, log_url }
|
||||
*/
|
||||
async function sendNotification(to, planId, status, summary = {}) {
|
||||
const t = getTransporter();
|
||||
if (!t) return { sent: false, reason: "SMTP未配置" };
|
||||
|
||||
const statusEmoji = { started: "🚀", completed: "✅", failed: "❌" };
|
||||
const emoji = statusEmoji[status] || "📋";
|
||||
|
||||
const subject = emoji + " 铸渊DevAgent · " + planId + " · " + status;
|
||||
|
||||
const lines = [
|
||||
"铸渊开发Agent · 任务执行报告",
|
||||
"══════════════════════════════",
|
||||
"",
|
||||
"规划ID: " + planId,
|
||||
"状态: " + status,
|
||||
"时间: " + new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
|
||||
"",
|
||||
];
|
||||
|
||||
if (summary.task_count !== undefined) {
|
||||
lines.push("任务总数: " + summary.task_count);
|
||||
lines.push("已完成: " + (summary.done_count || 0));
|
||||
lines.push("失败: " + (summary.failed_count || 0));
|
||||
}
|
||||
|
||||
if (summary.errors && summary.errors.length > 0) {
|
||||
lines.push("", "错误详情:");
|
||||
summary.errors.forEach(function(e, i) {
|
||||
lines.push(" " + (i + 1) + ". " + e);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push("", "══════════════════════════════", "铸渊 ICE-GL-ZY001 · 国作登字-2026-A-00037559");
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.ZY_SMTP_USER,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: lines.join("\n")
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await t.sendMail(mailOptions);
|
||||
console.log("[EmailNotify] 邮件已发送: " + info.messageId);
|
||||
return { sent: true, messageId: info.messageId };
|
||||
} catch (err) {
|
||||
console.error("[EmailNotify] 发送失败:", err.message);
|
||||
return { sent: false, reason: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sendNotification };
|
||||
177
agents/zhuyuan-dev-agent/modules/executor.js
Normal file
177
agents/zhuyuan-dev-agent/modules/executor.js
Normal file
@ -0,0 +1,177 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · 执行引擎 v2.0
|
||||
// 宪法约束:3次重试上限 · 统一输出接口 · 依赖链 · 紧急停止
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const WORKSPACE = process.env.WORKSPACE_DIR || "/opt/guanghu/zhuyuan-workspace";
|
||||
const CONSTITUTION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "agent-constitution.json"), "utf-8"));
|
||||
const { fail_protocol, unified_interface, dependency_chain, emergency_stop } = CONSTITUTION;
|
||||
|
||||
if (!fs.existsSync(WORKSPACE)) fs.mkdirSync(WORKSPACE, { recursive: true });
|
||||
|
||||
/**
|
||||
* 统一包装输出——所有模块输出必须经过这里
|
||||
*/
|
||||
function uniformResult(taskId, step, ok, extra = {}) {
|
||||
return {
|
||||
ok,
|
||||
task_id: taskId,
|
||||
step,
|
||||
action: extra.action || "unknown",
|
||||
output: extra.output || null,
|
||||
error: extra.error || null,
|
||||
retries: extra.retries || 0,
|
||||
skipped: !!extra.skipped,
|
||||
blocked: !!extra.blocked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的执行
|
||||
*/
|
||||
async function executeWithRetry(substep, logFn, retriesLeft) {
|
||||
const start = Date.now();
|
||||
const attempt = fail_protocol.max_retries_per_task - retriesLeft + 1;
|
||||
logFn("exec_attempt", { step: substep.step, action: substep.action, attempt, retriesLeft });
|
||||
|
||||
try {
|
||||
let result;
|
||||
switch (substep.action) {
|
||||
case "write_file":
|
||||
result = doWriteFile(substep);
|
||||
break;
|
||||
case "run_cmd":
|
||||
result = doRunCmd(substep);
|
||||
break;
|
||||
case "call_api":
|
||||
result = await doCallApi(substep);
|
||||
break;
|
||||
case "check":
|
||||
result = doCheck(substep);
|
||||
break;
|
||||
default:
|
||||
return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: "未知动作类型: " + substep.action });
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
logFn("exec_done", { step: substep.step, result: "ok", ms: Date.now() - start, attempt });
|
||||
return uniformResult(substep.task_id, substep.step, true, { action: substep.action, output: result.output || result.path, retries: attempt - 1 });
|
||||
}
|
||||
|
||||
// 失败了但还有重试次数
|
||||
if (retriesLeft > 1) {
|
||||
logFn("exec_retry", { step: substep.step, error: result.error, retriesLeft: retriesLeft - 1 });
|
||||
return executeWithRetry(substep, logFn, retriesLeft - 1);
|
||||
}
|
||||
|
||||
// 重试耗尽 → 跳过
|
||||
logFn("exec_skip", { step: substep.step, error: result.error, reason: "重试" + fail_protocol.max_retries_per_task + "次全部失败,跳过" });
|
||||
return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: result.error, retries: fail_protocol.max_retries_per_task, skipped: true });
|
||||
|
||||
} catch (err) {
|
||||
if (retriesLeft > 1) {
|
||||
logFn("exec_retry", { step: substep.step, error: err.message, retriesLeft: retriesLeft - 1 });
|
||||
return executeWithRetry(substep, logFn, retriesLeft - 1);
|
||||
}
|
||||
logFn("exec_skip", { step: substep.step, error: err.message, reason: "重试" + fail_protocol.max_retries_per_task + "次全部失败,跳过" });
|
||||
return uniformResult(substep.task_id, substep.step, false, { action: substep.action, error: err.message, retries: fail_protocol.max_retries_per_task, skipped: true });
|
||||
}
|
||||
}
|
||||
|
||||
function doWriteFile(s) {
|
||||
const fp = path.resolve(WORKSPACE, s.target);
|
||||
const dir = path.dirname(fp);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(fp, s.content || "", "utf-8");
|
||||
return { ok: true, path: fp };
|
||||
}
|
||||
|
||||
function doRunCmd(s) {
|
||||
const out = execSync(s.target, { cwd: s.cwd || WORKSPACE, encoding: "utf-8", timeout: 60000, maxBuffer: 10*1024*1024 });
|
||||
return { ok: true, output: out.slice(0, 5000) };
|
||||
}
|
||||
|
||||
async function doCallApi(s) {
|
||||
const https = require("https");
|
||||
const body = JSON.stringify(s.body || {});
|
||||
const url = new URL(s.target);
|
||||
return new Promise((resolve) => {
|
||||
const req = https.request({
|
||||
hostname: url.hostname, path: url.pathname, method: s.method || "POST",
|
||||
headers: { "Content-Type": "application/json", ...(s.headers || {}) }
|
||||
}, (res) => {
|
||||
let data = "";
|
||||
res.on("data", c => data += c);
|
||||
res.on("end", () => resolve({ ok: res.statusCode < 400, status: res.statusCode, body: data.slice(0, 2000) }));
|
||||
});
|
||||
req.on("error", e => resolve({ ok: false, error: e.message }));
|
||||
if (body !== "{}") req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function doCheck(s) {
|
||||
const fp = path.resolve(WORKSPACE, s.target);
|
||||
return { ok: fs.existsSync(fp), exists: fs.existsSync(fp), path: fp };
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整规划——宪法约束版本
|
||||
*/
|
||||
async function executePlan(plan, logFn) {
|
||||
console.log("[Executor v2] 开始执行: " + plan.plan_id + " · 宪法约束模式");
|
||||
logFn("constitution_loaded", { max_retries: fail_protocol.max_retries_per_task });
|
||||
|
||||
const results = [];
|
||||
let doneCount = 0, failCount = 0, skipCount = 0, consecutiveFails = 0;
|
||||
const blockedTasks = new Set();
|
||||
|
||||
for (const task of plan.tasks) {
|
||||
const substeps = task.substeps || [];
|
||||
|
||||
for (const ss of substeps) {
|
||||
// 检查是否被阻塞
|
||||
if (ss.depends_on && blockedTasks.has(ss.depends_on)) {
|
||||
const blocked = uniformResult(task.id, ss.step, false, { action: ss.action, blocked: true, error: "上游依赖 " + ss.depends_on + " 失败,此任务阻塞" });
|
||||
results.push(blocked);
|
||||
logFn("exec_blocked", { step: ss.step, depends_on: ss.depends_on });
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行(带重试)
|
||||
const r = await executeWithRetry({ ...ss, task_id: task.id }, logFn, fail_protocol.max_retries_per_task);
|
||||
results.push(r);
|
||||
|
||||
if (r.ok) {
|
||||
doneCount++;
|
||||
consecutiveFails = 0;
|
||||
} else if (r.skipped) {
|
||||
skipCount++;
|
||||
consecutiveFails++;
|
||||
// 被跳过的任务 → 下游阻塞
|
||||
blockedTasks.add(task.id + ":" + ss.step);
|
||||
} else {
|
||||
failCount++;
|
||||
consecutiveFails++;
|
||||
blockedTasks.add(task.id + ":" + ss.step);
|
||||
}
|
||||
|
||||
// 紧急停止检查
|
||||
if (consecutiveFails >= 5) {
|
||||
logFn("emergency_stop", { reason: "连续" + consecutiveFails + "个任务失败", summary: { done: doneCount, fail: failCount, skip: skipCount } });
|
||||
return { plan_id: plan.plan_id, emergency_stop: true, done: doneCount, failed: failCount, skipped: skipCount, results };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summary = { plan_id: plan.plan_id, total: results.length, done: doneCount, failed: failCount, skipped: skipCount, results };
|
||||
console.log("[Executor v2] 完成 · 成功 " + doneCount + " / 失败 " + failCount + " / 跳过 " + skipCount);
|
||||
return summary;
|
||||
}
|
||||
|
||||
module.exports = { executePlan };
|
||||
97
agents/zhuyuan-dev-agent/modules/logger.js
Normal file
97
agents/zhuyuan-dev-agent/modules/logger.js
Normal file
@ -0,0 +1,97 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · 日志系统
|
||||
// 结构化日志 · 铸渊醒来能看懂Agent做了什么
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const LOGS_DIR = process.env.LOGS_DIR || path.join(__dirname, "..", "logs");
|
||||
|
||||
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
|
||||
|
||||
/**
|
||||
* 创建一次执行的日志记录器
|
||||
*/
|
||||
function createLogger(planId) {
|
||||
const runId = planId + "-" + Date.now();
|
||||
const logFile = path.join(LOGS_DIR, runId + ".jsonl");
|
||||
const startTime = new Date().toISOString();
|
||||
|
||||
// 写入启动记录
|
||||
appendLine(logFile, { type: "run_start", plan_id: planId, run_id: runId, time: startTime });
|
||||
|
||||
const log = (type, data) => {
|
||||
const entry = {
|
||||
time: new Date().toISOString(),
|
||||
type: type,
|
||||
...data
|
||||
};
|
||||
appendLine(logFile, entry);
|
||||
console.log("[Log:" + planId + "] " + type + " " + JSON.stringify(data).slice(0, 100));
|
||||
};
|
||||
|
||||
// 返回日志函数和元信息
|
||||
return {
|
||||
log,
|
||||
runId,
|
||||
logFile,
|
||||
finish: (summary) => {
|
||||
appendLine(logFile, { type: "run_end", time: new Date().toISOString(), summary });
|
||||
return logFile;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function appendLine(file, obj) {
|
||||
fs.appendFileSync(file, JSON.stringify(obj) + "\n", "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取最近一次执行日志摘要(给铸渊看)
|
||||
*/
|
||||
function readLatestRun() {
|
||||
const files = fs.readdirSync(LOGS_DIR)
|
||||
.filter(f => f.endsWith(".jsonl"))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (files.length === 0) return null;
|
||||
|
||||
const latest = path.join(LOGS_DIR, files[0]);
|
||||
const content = fs.readFileSync(latest, "utf-8");
|
||||
const lines = content.trim().split("\n").map(l => {
|
||||
try { return JSON.parse(l); } catch (e) { return null; }
|
||||
}).filter(Boolean);
|
||||
|
||||
const startLine = lines.find(l => l.type === "run_start");
|
||||
const endLine = lines.find(l => l.type === "run_end");
|
||||
const errors = lines.filter(l => l.type === "exec_error" || l.type === "exec_done" && l.result === "fail");
|
||||
|
||||
return {
|
||||
plan_id: startLine?.plan_id,
|
||||
run_id: startLine?.run_id,
|
||||
started: startLine?.time,
|
||||
ended: endLine?.time,
|
||||
total_steps: lines.filter(l => l.type === "exec_start").length,
|
||||
errors: errors.map(e => ({ step: e.step, error: e.error || "未知" })),
|
||||
log_file: latest
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有执行记录
|
||||
*/
|
||||
function listRuns(limit = 20) {
|
||||
return fs.readdirSync(LOGS_DIR)
|
||||
.filter(f => f.endsWith(".jsonl"))
|
||||
.sort()
|
||||
.reverse()
|
||||
.slice(0, limit)
|
||||
.map(f => {
|
||||
const stat = fs.statSync(path.join(LOGS_DIR, f));
|
||||
return { file: f, size: stat.size, modified: stat.mtime.toISOString() };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createLogger, readLatestRun, listRuns };
|
||||
@ -0,0 +1,61 @@
|
||||
// M06 工单管理模块适配器
|
||||
window.M06Adapter = {
|
||||
moduleId: 'm06',
|
||||
moduleName: '工单管理',
|
||||
|
||||
// 初始化模块(由 ModuleAdapter 调用)
|
||||
init: function(containerId) {
|
||||
console.log('[M06Adapter] 初始化');
|
||||
|
||||
// 通过核心适配器加载
|
||||
if (window.ModuleAdapter) {
|
||||
return ModuleAdapter.loadModule(this.moduleId, containerId);
|
||||
} else {
|
||||
console.error('[M06Adapter] ModuleAdapter 未加载');
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 发送消息到工单模块
|
||||
sendMessage: function(type, payload) {
|
||||
return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
|
||||
},
|
||||
|
||||
// 监听工单模块事件
|
||||
onTicketCreate: function(callback) {
|
||||
EventBus.on('module:m06:message', function(data) {
|
||||
if (data.type === 'ticket:create') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onTicketUpdate: function(callback) {
|
||||
EventBus.on('module:m06:message', function(data) {
|
||||
if (data.type === 'ticket:update') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onTicketDelete: function(callback) {
|
||||
EventBus.on('module:m06:message', function(data) {
|
||||
if (data.type === 'ticket:delete') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 销毁模块
|
||||
destroy: function() {
|
||||
ModuleAdapter.unloadModule(this.moduleId);
|
||||
}
|
||||
};
|
||||
|
||||
// 注册到模块注册表
|
||||
if (window.ModuleRegistry) {
|
||||
ModuleRegistry.register('m06', window.M06Adapter);
|
||||
console.log('[M06Adapter] 已注册到模块注册表');
|
||||
}
|
||||
|
||||
console.log('[M06Adapter] 已加载');
|
||||
@ -0,0 +1,47 @@
|
||||
// M08 数据统计模块适配器
|
||||
window.M08Adapter = {
|
||||
moduleId: 'm08',
|
||||
moduleName: '数据统计',
|
||||
|
||||
init: function(containerId) {
|
||||
console.log('[M08Adapter] 初始化');
|
||||
if (window.ModuleAdapter) {
|
||||
return ModuleAdapter.loadModule(this.moduleId, containerId);
|
||||
} else {
|
||||
console.error('[M08Adapter] ModuleAdapter 未加载');
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: function(type, payload) {
|
||||
return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
|
||||
},
|
||||
|
||||
// 监听统计模块事件
|
||||
onStatsRefresh: function(callback) {
|
||||
EventBus.on('module:m08:message', function(data) {
|
||||
if (data.type === 'stats:refresh') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onStatsExport: function(callback) {
|
||||
EventBus.on('module:m08:message', function(data) {
|
||||
if (data.type === 'stats:export') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
ModuleAdapter.unloadModule(this.moduleId);
|
||||
}
|
||||
};
|
||||
|
||||
if (window.ModuleRegistry) {
|
||||
ModuleRegistry.register('m08', window.M08Adapter);
|
||||
console.log('[M08Adapter] 已注册到模块注册表');
|
||||
}
|
||||
|
||||
console.log('[M08Adapter] 已加载');
|
||||
@ -0,0 +1,47 @@
|
||||
// M11 系统组件库适配器
|
||||
window.M11Adapter = {
|
||||
moduleId: 'm11',
|
||||
moduleName: '组件库',
|
||||
|
||||
init: function(containerId) {
|
||||
console.log('[M11Adapter] 初始化');
|
||||
if (window.ModuleAdapter) {
|
||||
return ModuleAdapter.loadModule(this.moduleId, containerId);
|
||||
} else {
|
||||
console.error('[M11Adapter] ModuleAdapter 未加载');
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: function(type, payload) {
|
||||
return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
|
||||
},
|
||||
|
||||
// 监听组件库事件
|
||||
onThemeChange: function(callback) {
|
||||
EventBus.on('module:m11:message', function(data) {
|
||||
if (data.type === 'theme:change') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onComponentSelect: function(callback) {
|
||||
EventBus.on('module:m11:message', function(data) {
|
||||
if (data.type === 'component:select') {
|
||||
callback(data.payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
ModuleAdapter.unloadModule(this.moduleId);
|
||||
}
|
||||
};
|
||||
|
||||
if (window.ModuleRegistry) {
|
||||
ModuleRegistry.register('m11', window.M11Adapter);
|
||||
console.log('[M11Adapter] 已注册到模块注册表');
|
||||
}
|
||||
|
||||
console.log('[M11Adapter] 已加载');
|
||||
@ -0,0 +1,140 @@
|
||||
// 模块适配器核心 - 万能转接头
|
||||
window.ModuleAdapter = {
|
||||
// 已加载的真实模块缓存
|
||||
loadedModules: {},
|
||||
|
||||
// 适配器配置
|
||||
config: {
|
||||
m06: {
|
||||
name: '工单管理',
|
||||
path: '/m06-ticket/index.html', // 真实路径,但文件可能不全
|
||||
width: '100%',
|
||||
height: '500px',
|
||||
events: ['ticket:create', 'ticket:update', 'ticket:delete']
|
||||
},
|
||||
m08: {
|
||||
name: '数据统计',
|
||||
path: '/modules/m08/index.html', // 暂时未知,先保留错误路径触发边界
|
||||
width: '100%',
|
||||
height: '500px',
|
||||
events: ['stats:refresh', 'stats:export']
|
||||
},
|
||||
m11: {
|
||||
name: '组件库',
|
||||
path: '/m11-module/index.html', // 正确路径!
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
events: ['theme:change', 'component:select']
|
||||
}
|
||||
},
|
||||
|
||||
// 加载真实模块(通过 iframe 沙箱)
|
||||
loadModule: function(moduleId, containerId) {
|
||||
console.log(`[adapter] 加载真实模块: ${moduleId}`);
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`[adapter] 容器 ${containerId} 不存在`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 清理容器
|
||||
container.innerHTML = '';
|
||||
|
||||
const config = this.config[moduleId];
|
||||
if (!config) {
|
||||
console.error(`[adapter] 未知模块: ${moduleId}`);
|
||||
container.innerHTML = `<div class="error-boundary">模块配置不存在</div>`;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建 iframe 沙箱(玻璃展柜)
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = config.path;
|
||||
iframe.style.width = config.width;
|
||||
iframe.style.height = config.height;
|
||||
iframe.style.border = 'none';
|
||||
iframe.style.borderRadius = '8px';
|
||||
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
|
||||
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
||||
|
||||
// 添加加载指示器
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'module-loading';
|
||||
loadingDiv.textContent = `加载 ${config.name}...`;
|
||||
container.appendChild(loadingDiv);
|
||||
|
||||
// iframe 加载完成后移除加载指示器
|
||||
iframe.onload = () => {
|
||||
loadingDiv.remove();
|
||||
console.log(`[adapter] ${moduleId} 加载完成`);
|
||||
|
||||
// 建立跨框架通信桥梁
|
||||
this.setupMessageBridge(iframe, moduleId);
|
||||
};
|
||||
|
||||
iframe.onerror = () => {
|
||||
loadingDiv.remove();
|
||||
container.innerHTML = `<div class="error-boundary">模块加载失败,请重试 <button onclick="ModuleAdapter.reloadModule('${moduleId}', '${containerId}')">重试</button></div>`;
|
||||
};
|
||||
|
||||
container.appendChild(iframe);
|
||||
this.loadedModules[moduleId] = { iframe, containerId };
|
||||
|
||||
return iframe;
|
||||
},
|
||||
|
||||
// 重新加载模块(错误重试)
|
||||
reloadModule: function(moduleId, containerId) {
|
||||
console.log(`[adapter] 重新加载模块: ${moduleId}`);
|
||||
this.loadModule(moduleId, containerId);
|
||||
},
|
||||
|
||||
// 建立消息桥梁(iframe 和主页通信)
|
||||
setupMessageBridge: function(iframe, moduleId) {
|
||||
// 监听来自 iframe 的消息
|
||||
window.addEventListener('message', function(event) {
|
||||
// 安全检查:确保消息来自正确的 iframe
|
||||
if (event.source !== iframe.contentWindow) return;
|
||||
|
||||
const data = event.data;
|
||||
if (!data || !data.type) return;
|
||||
|
||||
console.log(`[adapter] 收到来自 ${moduleId} 的消息:`, data);
|
||||
|
||||
// 转发为事件总线消息
|
||||
if (window.EventBus) {
|
||||
EventBus.emit(`module:${moduleId}:message`, {
|
||||
from: moduleId,
|
||||
type: data.type,
|
||||
payload: data.payload
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 向模块发送消息
|
||||
sendMessage: function(moduleId, message) {
|
||||
const module = this.loadedModules[moduleId];
|
||||
if (!module || !module.iframe) {
|
||||
console.error(`[adapter] 模块 ${moduleId} 未加载`);
|
||||
return false;
|
||||
}
|
||||
|
||||
module.iframe.contentWindow.postMessage(message, '*');
|
||||
return true;
|
||||
},
|
||||
|
||||
// 卸载模块
|
||||
unloadModule: function(moduleId) {
|
||||
const module = this.loadedModules[moduleId];
|
||||
if (module) {
|
||||
const container = document.getElementById(module.containerId);
|
||||
if (container) container.innerHTML = '';
|
||||
delete this.loadedModules[moduleId];
|
||||
console.log(`[adapter] 卸载模块: ${moduleId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[adapter] 模块适配器已加载');
|
||||
396
agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html
Normal file
396
agents/zhuyuan-dev-agent/modules/m-channel/all-in-one.html
Normal file
@ -0,0 +1,396 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖频道 · 一体化版</title>
|
||||
<style>
|
||||
.channel-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.channel-nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.channel-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.channel-btn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.channel-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
.channel-btn.visited {
|
||||
position: relative;
|
||||
}
|
||||
.channel-btn.visited::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.channel-content {
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
.channel-content.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
.channel-content.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==================== 所有代码整合在一个文件里 ====================
|
||||
|
||||
// ---------- 模块注册表 ----------
|
||||
window.ModuleRegistry = {
|
||||
modules: {},
|
||||
register: function(name, module) {
|
||||
this.modules[name] = module;
|
||||
console.log(`[registry] 模块 ${name} 已注册`);
|
||||
},
|
||||
get: function(name) {
|
||||
return this.modules[name];
|
||||
},
|
||||
list: function() {
|
||||
return Object.keys(this.modules);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 事件总线 ----------
|
||||
window.EventBus = {
|
||||
listeners: {},
|
||||
on: function(event, callback) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(callback);
|
||||
console.log(`[事件总线] 订阅事件: ${event}`);
|
||||
},
|
||||
off: function(event, callback) {
|
||||
if (!this.listeners[event]) return;
|
||||
if (!callback) delete this.listeners[event];
|
||||
else this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
console.log(`[事件总线] 取消订阅: ${event}`);
|
||||
},
|
||||
emit: function(event, data) {
|
||||
console.log(`[事件总线] 发送事件: ${event}`, data);
|
||||
if (!this.listeners[event]) return;
|
||||
this.listeners[event].forEach(callback => {
|
||||
try { callback(data); } catch (e) { console.error(`[事件总线] 执行回调出错: ${e}`); }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 模块生命周期 ----------
|
||||
window.ModuleLifecycle = {
|
||||
activeModules: new Set(),
|
||||
onLoad: function(moduleName) {
|
||||
this.activeModules.add(moduleName);
|
||||
console.log(`[生命周期] 模块加载: ${moduleName}`);
|
||||
EventBus.emit('module:loaded', { module: moduleName, time: Date.now() });
|
||||
},
|
||||
onUnload: function(moduleName) {
|
||||
this.activeModules.delete(moduleName);
|
||||
console.log(`[生命周期] 模块卸载: ${moduleName}`);
|
||||
EventBus.emit('module:unloaded', { module: moduleName, time: Date.now() });
|
||||
},
|
||||
getActiveModules: function() {
|
||||
return Array.from(this.activeModules);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 频道状态 ----------
|
||||
window.ChannelState = {
|
||||
STORAGE_KEY: 'hololake_channel_state',
|
||||
saveState: function(state) {
|
||||
const data = { ...state, timestamp: Date.now(), visited: state.visited || [] };
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
||||
console.log('[state] 保存状态:', data);
|
||||
},
|
||||
restoreState: function() {
|
||||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (!saved) return null;
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
console.log('[state] 恢复状态:', state);
|
||||
return state;
|
||||
} catch (e) {
|
||||
console.error('[state] 解析失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
markVisited: function(channel) {
|
||||
const state = this.restoreState() || { visited: [] };
|
||||
if (!state.visited.includes(channel)) {
|
||||
state.visited.push(channel);
|
||||
this.saveState(state);
|
||||
}
|
||||
},
|
||||
clearState: function() {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
console.log('[state] 已清除');
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 模块加载器 ----------
|
||||
window.ModuleLoader = {
|
||||
loadModule: function(moduleName, containerId) {
|
||||
console.log(`[loader] 加载模块: ${moduleName}`);
|
||||
const module = ModuleRegistry.get(moduleName);
|
||||
if (!module) {
|
||||
console.error(`[loader] 模块 ${moduleName} 未注册`);
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`[loader] 容器 ${containerId} 不存在`);
|
||||
return;
|
||||
}
|
||||
if (module.init && typeof module.init === 'function') {
|
||||
module.init(container);
|
||||
} else {
|
||||
container.innerHTML = `<div>模块 ${moduleName} 内容</div>`;
|
||||
}
|
||||
ModuleLifecycle.onLoad(moduleName);
|
||||
},
|
||||
unloadModule: function(moduleName) {
|
||||
console.log(`[loader] 卸载模块: ${moduleName}`);
|
||||
const module = ModuleRegistry.get(moduleName);
|
||||
if (module && module.destroy && typeof module.destroy === 'function') {
|
||||
module.destroy();
|
||||
}
|
||||
ModuleLifecycle.onUnload(moduleName);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 路由 ----------
|
||||
window.ChannelRouter = {
|
||||
currentChannel: 'home',
|
||||
contentEl: null,
|
||||
init: function(contentElement) {
|
||||
this.contentEl = contentElement;
|
||||
console.log('[router] 初始化');
|
||||
const savedState = ChannelState.restoreState();
|
||||
if (savedState && savedState.currentChannel) {
|
||||
this.currentChannel = savedState.currentChannel;
|
||||
}
|
||||
this.loadChannel(this.currentChannel);
|
||||
if (savedState && savedState.visited) {
|
||||
savedState.visited.forEach(channel => {
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
if (btn.dataset.channel === channel) btn.classList.add('visited');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
navigateTo: function(channel) {
|
||||
if (channel === this.currentChannel) return;
|
||||
console.log(`[router] 导航到: ${channel}`);
|
||||
this.contentEl.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
this.loadChannel(channel);
|
||||
this.contentEl.classList.remove('fade-out');
|
||||
this.contentEl.classList.add('fade-in');
|
||||
setTimeout(() => { this.contentEl.classList.remove('fade-in'); }, 300);
|
||||
}, 300);
|
||||
this.currentChannel = channel;
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.channel === channel) btn.classList.add('active');
|
||||
});
|
||||
ChannelState.saveState({ currentChannel: channel, visited: this.getVisitedChannels() });
|
||||
},
|
||||
loadChannel: function(channel) {
|
||||
if (!this.contentEl) return;
|
||||
let content = '';
|
||||
switch(channel) {
|
||||
case 'home':
|
||||
content = '<h2>🏠 首页</h2><p>欢迎来到光湖频道</p>';
|
||||
break;
|
||||
case 'module-a':
|
||||
content = '<h2>📦 模块A</h2><button id="sendMsgBtn">发送消息给模块B</button><div id="msgDisplay"></div>';
|
||||
break;
|
||||
case 'module-b':
|
||||
content = '<h2>📦 模块B</h2><div id="receivedMsg">等待消息...</div>';
|
||||
break;
|
||||
case 'module-c':
|
||||
content = '<h2>📦 模块C</h2><p>这是模块C的内容</p>';
|
||||
break;
|
||||
case 'debug':
|
||||
this.loadDebugPanel();
|
||||
return;
|
||||
default:
|
||||
content = '<h2>未知频道</h2>';
|
||||
}
|
||||
this.contentEl.innerHTML = content;
|
||||
ModuleLifecycle.onLoad(channel);
|
||||
if (channel === 'module-a') {
|
||||
setTimeout(() => {
|
||||
const btn = document.getElementById('sendMsgBtn');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
EventBus.emit('module-a:message', { text: '你好,模块B!', from: '模块A' });
|
||||
document.getElementById('msgDisplay').innerHTML = '✅ 消息已发送';
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
if (channel === 'module-b') {
|
||||
this.subscribeModuleB();
|
||||
}
|
||||
},
|
||||
loadDebugPanel: function() {
|
||||
// 调试面板直接用内嵌 HTML,避免 fetch 问题
|
||||
this.contentEl.innerHTML = `
|
||||
<div class="debug-panel">
|
||||
<h2>🐞 调试面板</h2>
|
||||
<div class="module-list"><strong>当前活跃模块:</strong> <span id="active-modules">加载中...</span></div>
|
||||
<h3>事件总线聊天记录</h3>
|
||||
<ul class="message-list" id="debug-messages" style="border:1px solid #ccc; padding:10px; height:200px; overflow-y:auto; background:#f9f9f9;"></ul>
|
||||
<div style="display:flex; gap:10px; margin:10px 0;">
|
||||
<input type="text" id="debug-input" placeholder="输入要发送的消息..." style="flex:1; padding:8px;">
|
||||
<button id="debug-send" style="padding:8px 16px; background:#3b82f6; color:white; border:none; border-radius:4px;">发送测试消息</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button id="debug-clear" style="padding:8px 16px; background:#ef4444; color:white; border:none; border-radius:4px;">清除状态</button>
|
||||
<button id="debug-refresh" onclick="location.reload()" style="padding:8px 16px; background:#6b7280; color:white; border:none; border-radius:4px;">刷新页面</button>
|
||||
</div>
|
||||
<p><small>消息会广播给所有订阅的模块</small></p>
|
||||
</div>
|
||||
`;
|
||||
ModuleLifecycle.onLoad('debug');
|
||||
this.initDebugPanel();
|
||||
},
|
||||
initDebugPanel: function() {
|
||||
const msgList = document.getElementById('debug-messages');
|
||||
if (msgList && window.debugMessages) {
|
||||
window.debugMessages.forEach(msg => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `[${msg.time}] ${msg.event}: ${JSON.stringify(msg.data)}`;
|
||||
msgList.appendChild(li);
|
||||
});
|
||||
}
|
||||
const sendBtn = document.getElementById('debug-send');
|
||||
const input = document.getElementById('debug-input');
|
||||
if (sendBtn && input) {
|
||||
sendBtn.addEventListener('click', () => {
|
||||
const text = input.value.trim();
|
||||
if (text) {
|
||||
EventBus.emit('debug:message', { text, from: '调试面板' });
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
const clearBtn = document.getElementById('debug-clear');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
ChannelState.clearState();
|
||||
alert('状态已清除,刷新页面生效');
|
||||
});
|
||||
}
|
||||
// 更新活跃模块
|
||||
const updateActive = () => {
|
||||
const span = document.getElementById('active-modules');
|
||||
if (span) {
|
||||
const modules = ModuleLifecycle.getActiveModules();
|
||||
span.textContent = modules.length ? modules.join('、') : '无';
|
||||
}
|
||||
};
|
||||
EventBus.on('module:loaded', updateActive);
|
||||
EventBus.on('module:unloaded', updateActive);
|
||||
updateActive();
|
||||
},
|
||||
subscribeModuleB: function() {
|
||||
const handleMessage = (data) => {
|
||||
const display = document.getElementById('receivedMsg');
|
||||
if (display) {
|
||||
display.innerHTML = `📩 收到消息: ${data.text} (来自 ${data.from})`;
|
||||
}
|
||||
};
|
||||
EventBus.off('module-a:message');
|
||||
EventBus.on('module-a:message', handleMessage);
|
||||
},
|
||||
getVisitedChannels: function() {
|
||||
const visited = [];
|
||||
document.querySelectorAll('.channel-btn.visited').forEach(btn => visited.push(btn.dataset.channel));
|
||||
return visited;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 应用启动 ----------
|
||||
console.log('[app] 启动中...');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[app] DOM 已加载');
|
||||
const contentEl = document.getElementById('channel-content');
|
||||
if (!contentEl) {
|
||||
console.error('[app] 找不到 #channel-content');
|
||||
return;
|
||||
}
|
||||
if (window.ChannelRouter) {
|
||||
ChannelRouter.init(contentEl);
|
||||
}
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const channel = this.dataset.channel;
|
||||
if (channel && window.ChannelRouter) {
|
||||
ChannelRouter.navigateTo(channel);
|
||||
this.classList.add('visited');
|
||||
ChannelState.markVisited(channel);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 拦截事件总线消息用于调试
|
||||
const originalEmit = EventBus.emit;
|
||||
EventBus.emit = function(event, data) {
|
||||
if (!window.debugMessages) window.debugMessages = [];
|
||||
window.debugMessages.push({ event, data, time: new Date().toLocaleTimeString() });
|
||||
originalEmit.call(this, event, data);
|
||||
};
|
||||
console.log('[app] 事件总线拦截器已安装');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
50
agents/zhuyuan-dev-agent/modules/m-channel/app.js
Normal file
50
agents/zhuyuan-dev-agent/modules/m-channel/app.js
Normal file
@ -0,0 +1,50 @@
|
||||
// 应用入口
|
||||
console.log('[app] 启动中...');
|
||||
|
||||
// 等待 DOM 加载完成
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[app] DOM 已加载');
|
||||
|
||||
const contentEl = document.getElementById('channel-content');
|
||||
if (!contentEl) {
|
||||
console.error('[app] 找不到 #channel-content');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化路由器
|
||||
if (window.ChannelRouter) {
|
||||
ChannelRouter.init(contentEl);
|
||||
}
|
||||
|
||||
// 绑定导航按钮
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const channel = this.dataset.channel;
|
||||
if (channel && window.ChannelRouter) {
|
||||
ChannelRouter.navigateTo(channel);
|
||||
|
||||
// 标记已访问
|
||||
this.classList.add('visited');
|
||||
ChannelState.markVisited(channel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('[app] 初始化完成');
|
||||
});
|
||||
|
||||
// 拦截所有事件总线消息,用于调试面板
|
||||
const originalEmit = EventBus.emit;
|
||||
EventBus.emit = function(event, data) {
|
||||
// 保存到调试日志
|
||||
if (!window.debugMessages) window.debugMessages = [];
|
||||
window.debugMessages.push({
|
||||
event,
|
||||
data,
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// 调用原始方法
|
||||
originalEmit.call(this, event, data);
|
||||
};
|
||||
console.log('[app] 事件总线拦截器已安装');
|
||||
42
agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/app.js
Normal file
42
agents/zhuyuan-dev-agent/modules/m-channel/backup-混乱版/app.js
Normal file
@ -0,0 +1,42 @@
|
||||
// app.js - 光湖频道动态渲染引擎入口
|
||||
console.log('[app] 启动中...');
|
||||
|
||||
// 等待 DOM 完全加载
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[app] DOM 已加载,初始化组件...');
|
||||
|
||||
// 获取内容容器
|
||||
const contentEl = document.getElementById('channel-content');
|
||||
if (!contentEl) {
|
||||
console.error('[app] 找不到 #channel-content 元素');
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复状态(如果 ChannelState 存在)
|
||||
if (window.ChannelState) {
|
||||
const savedState = ChannelState.restoreState();
|
||||
console.log('[app] 恢复的状态:', savedState);
|
||||
} else {
|
||||
console.warn('[app] ChannelState 未加载');
|
||||
}
|
||||
|
||||
// 初始化路由器
|
||||
if (window.ChannelRouter) {
|
||||
ChannelRouter.init(contentEl);
|
||||
} else {
|
||||
console.error('[app] ChannelRouter 未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定导航按钮点击事件
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
const channel = this.dataset.channel;
|
||||
if (channel) {
|
||||
ChannelRouter.navigateTo(channel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('[app] 初始化完成');
|
||||
});
|
||||
@ -0,0 +1,98 @@
|
||||
// ================== 路由配置 ==================
|
||||
const routes = {
|
||||
'home': 'views/home.html',
|
||||
'channel': 'views/channel.html',
|
||||
'about': 'views/about.html'
|
||||
};
|
||||
|
||||
// 获取当前 hash 中的路径(去掉 #/)
|
||||
function getHashPath() {
|
||||
const hash = window.location.hash.slice(1) || '/';
|
||||
const path = hash.startsWith('/') ? hash.slice(1) : hash;
|
||||
return path || 'home';
|
||||
}
|
||||
|
||||
// ================== 加载视图 ==================
|
||||
async function loadView(path) {
|
||||
const routerView = document.getElementById('router-view');
|
||||
if (!routerView) return;
|
||||
|
||||
// 显示加载动画
|
||||
routerView.innerHTML = '<div class="loader" style="margin: 2rem auto;"></div>';
|
||||
|
||||
try {
|
||||
const viewFile = routes[path];
|
||||
if (!viewFile) {
|
||||
await load404(routerView);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(viewFile);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
routerView.innerHTML = html;
|
||||
} catch (error) {
|
||||
console.error('加载视图失败:', error);
|
||||
routerView.innerHTML = `
|
||||
<div class="error-message">
|
||||
❌ 加载失败:${error.message}<br>
|
||||
<small>请检查文件是否存在,或刷新重试</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 更新导航高亮和状态栏
|
||||
updateActiveNav(path);
|
||||
updateStatusBar(path);
|
||||
}
|
||||
|
||||
// 加载 404 页面
|
||||
async function load404(container) {
|
||||
try {
|
||||
const resp = await fetch('views/404.html');
|
||||
if (resp.ok) {
|
||||
container.innerHTML = await resp.text();
|
||||
} else {
|
||||
container.innerHTML = '<div class="error-message">⚠️ 404 - 页面未找到</div>';
|
||||
}
|
||||
} catch {
|
||||
container.innerHTML = '<div class="error-message">⚠️ 404 - 页面未找到</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新导航高亮
|
||||
function updateActiveNav(path) {
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
const linkPath = link.getAttribute('href').slice(2);
|
||||
if (linkPath === path) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新状态栏
|
||||
function updateStatusBar(path) {
|
||||
const statusEl = document.getElementById('current-route');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `当前路由:/${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 hash 变化
|
||||
window.addEventListener('hashchange', () => {
|
||||
const path = getHashPath();
|
||||
loadView(path);
|
||||
});
|
||||
|
||||
// 首次加载
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (!window.location.hash) {
|
||||
window.location.hash = '#/home';
|
||||
} else {
|
||||
const path = getHashPath();
|
||||
loadView(path);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,267 @@
|
||||
/**
|
||||
* channel-router.js
|
||||
* 用户频道路由引擎 - 带过渡动画和状态持久化
|
||||
* 集成ChannelState和动画控制
|
||||
*/
|
||||
|
||||
// 依赖全局变量:ChannelState 需要先加载
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig = {
|
||||
containerSelector: '#channel-content',
|
||||
routes: {},
|
||||
defaultRoute: '/home',
|
||||
mode: 'fade', // 'fade' 或 'slide'
|
||||
transitionDuration: 300
|
||||
};
|
||||
|
||||
class ChannelRouter {
|
||||
constructor(config) {
|
||||
this.config = Object.assign({}, defaultConfig, config);
|
||||
this.container = document.querySelector(this.config.containerSelector);
|
||||
if (!this.container) {
|
||||
throw new Error(`Container ${this.config.containerSelector} not found`);
|
||||
}
|
||||
|
||||
// 确保容器有相对定位
|
||||
this.container.style.position = 'relative';
|
||||
this.container.classList.add('route-container');
|
||||
|
||||
// 当前活动路由
|
||||
this.currentRoute = null;
|
||||
this.currentPageElement = null;
|
||||
|
||||
// 防抖定时器
|
||||
this.navTimer = null;
|
||||
|
||||
// 绑定方法
|
||||
this.navigate = this.navigate.bind(this);
|
||||
this.goBack = this.goBack.bind(this);
|
||||
this.goForward = this.goForward.bind(this);
|
||||
this.setMode = this.setMode.bind(this);
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
|
||||
// 初始化模式
|
||||
this.container.classList.add(`${this.config.mode}-mode`);
|
||||
|
||||
// 从ChannelState恢复上次的路由
|
||||
this.initFromState();
|
||||
|
||||
// 监听浏览器前进后退
|
||||
window.addEventListener('popstate', this.handlePopState);
|
||||
|
||||
console.log('[router] 初始化完成,模式:', this.config.mode);
|
||||
}
|
||||
|
||||
// 从ChannelState恢复
|
||||
initFromState() {
|
||||
if (global.ChannelState) {
|
||||
const state = global.ChannelState.getState();
|
||||
const targetRoute = state.currentRoute || this.config.defaultRoute;
|
||||
// 不触发pushState,直接渲染
|
||||
this.renderRoute(targetRoute, { replace: true, fromState: true });
|
||||
console.log('[router] 从状态恢复路由:', targetRoute);
|
||||
} else {
|
||||
// 没有状态管理器,走默认
|
||||
this.renderRoute(this.config.defaultRoute, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染路由(内部方法)
|
||||
renderRoute(path, options = {}) {
|
||||
const { replace = false, fromState = false } = options;
|
||||
const route = this.config.routes[path];
|
||||
if (!route) {
|
||||
console.warn(`[router] 路由 ${path} 未定义,使用404`);
|
||||
// 可以跳转到404页面,这里简单返回
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是相同路由且不是强制刷新,不重复渲染
|
||||
if (this.currentRoute === path && !options.force) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新页面元素
|
||||
const newPage = document.createElement('div');
|
||||
newPage.className = `route-page ${this.config.mode === 'slide' ? 'slide-enter' : ''}`;
|
||||
newPage.innerHTML = route.template || route.content || '';
|
||||
|
||||
// 如果有模块加载器,执行模块加载
|
||||
if (global.ModuleLoader && route.module) {
|
||||
// 这里简化,实际可能需要加载模块
|
||||
console.log('[router] 加载模块:', route.module);
|
||||
}
|
||||
|
||||
// 旧页面元素
|
||||
const oldPage = this.currentPageElement;
|
||||
|
||||
// 设置新页面为激活状态
|
||||
newPage.classList.add('active');
|
||||
|
||||
// 如果是滑入模式,根据方向添加额外类
|
||||
if (this.config.mode === 'slide') {
|
||||
// 通过history判断方向:前进/后退
|
||||
const direction = this.getDirection(path);
|
||||
if (direction === 'back') {
|
||||
this.container.classList.add('backward');
|
||||
this.container.classList.remove('forward');
|
||||
} else {
|
||||
this.container.classList.add('forward');
|
||||
this.container.classList.remove('backward');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新页面到容器
|
||||
this.container.appendChild(newPage);
|
||||
|
||||
// 触发重绘以确保动画
|
||||
newPage.offsetHeight;
|
||||
|
||||
// 如果有旧页面,移除它的active类并添加退出动画类
|
||||
if (oldPage) {
|
||||
oldPage.classList.remove('active');
|
||||
if (this.config.mode === 'slide') {
|
||||
oldPage.classList.add('slide-exit');
|
||||
}
|
||||
}
|
||||
|
||||
// 动画结束后清理旧页面
|
||||
const onTransitionEnd = (e) => {
|
||||
if (e.target === newPage || e.target === oldPage) {
|
||||
if (oldPage && oldPage.parentNode) {
|
||||
oldPage.parentNode.removeChild(oldPage);
|
||||
}
|
||||
newPage.removeEventListener('transitionend', onTransitionEnd);
|
||||
}
|
||||
};
|
||||
newPage.addEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
// 更新当前路由
|
||||
this.currentRoute = path;
|
||||
this.currentPageElement = newPage;
|
||||
|
||||
// 更新导航菜单激活状态
|
||||
this.updateActiveNav(path);
|
||||
|
||||
// 保存状态(如果不是从状态恢复来的)
|
||||
if (!fromState && global.ChannelState) {
|
||||
// 判断是push还是replace
|
||||
if (replace) {
|
||||
// 替换当前历史记录(不增加新记录)
|
||||
// 状态管理需要相应处理:替换当前记录而不是push
|
||||
global.ChannelState.setCurrentRoute(path);
|
||||
// 同时替换浏览器历史
|
||||
if (!options.skipHistory) {
|
||||
history.replaceState({ route: path }, '', `#${path}`);
|
||||
}
|
||||
} else {
|
||||
// 正常跳转,push到历史
|
||||
global.ChannelState.pushHistory(path);
|
||||
if (!options.skipHistory) {
|
||||
history.pushState({ route: path }, '', `#${path}`);
|
||||
}
|
||||
}
|
||||
} else if (!global.ChannelState) {
|
||||
// 没有状态管理器,只处理浏览器历史
|
||||
if (!replace && !options.skipHistory) {
|
||||
history.pushState({ route: path }, '', `#${path}`);
|
||||
} else if (replace && !options.skipHistory) {
|
||||
history.replaceState({ route: path }, '', `#${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[router] 导航到 ${path}${replace ? ' (replace)' : ''}`);
|
||||
}
|
||||
|
||||
// 判断前进后退方向(简单实现:看是否在历史栈中)
|
||||
getDirection(path) {
|
||||
if (!global.ChannelState) return 'forward';
|
||||
const state = global.ChannelState.getState();
|
||||
const currentIndex = state.historyIndex;
|
||||
const stack = state.historyStack;
|
||||
// 如果path在历史栈中且在当前位置之后,是后退?需要更精确
|
||||
// 这里简化:根据当前路由和目标的索引比较
|
||||
if (this.currentRoute) {
|
||||
const currentIdx = stack.indexOf(this.currentRoute);
|
||||
const targetIdx = stack.indexOf(path);
|
||||
if (targetIdx < currentIdx) return 'back';
|
||||
}
|
||||
return 'forward';
|
||||
}
|
||||
|
||||
// 更新导航菜单激活样式
|
||||
updateActiveNav(path) {
|
||||
document.querySelectorAll('.channel-nav a').forEach(link => {
|
||||
const href = link.getAttribute('href').replace('#', '');
|
||||
if (href === path) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 公开导航方法
|
||||
navigate(path, options = {}) {
|
||||
// 防抖处理
|
||||
if (this.navTimer) clearTimeout(this.navTimer);
|
||||
this.navTimer = setTimeout(() => {
|
||||
this.renderRoute(path, options);
|
||||
this.navTimer = null;
|
||||
}, 10); // 微小延迟确保快速点击不重叠
|
||||
}
|
||||
|
||||
// 后退
|
||||
goBack() {
|
||||
if (global.ChannelState) {
|
||||
const prev = global.ChannelState.goBack();
|
||||
if (prev) {
|
||||
this.renderRoute(prev, { fromState: true, skipHistory: true });
|
||||
} else {
|
||||
console.log('[router] 已在最前');
|
||||
}
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
// 前进
|
||||
goForward() {
|
||||
if (global.ChannelState) {
|
||||
const next = global.ChannelState.goForward();
|
||||
if (next) {
|
||||
this.renderRoute(next, { fromState: true, skipHistory: true });
|
||||
} else {
|
||||
console.log('[router] 已在最后');
|
||||
}
|
||||
} else {
|
||||
history.forward();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理popstate事件(浏览器前进后退)
|
||||
handlePopState(event) {
|
||||
const route = event.state?.route || this.config.defaultRoute;
|
||||
if (global.ChannelState) {
|
||||
// 从状态中恢复索引,但不需要重复push
|
||||
this.renderRoute(route, { fromState: true, skipHistory: true });
|
||||
} else {
|
||||
this.renderRoute(route, { skipHistory: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 切换动画模式
|
||||
setMode(mode) {
|
||||
if (mode !== 'fade' && mode !== 'slide') return;
|
||||
this.container.classList.remove('fade-mode', 'slide-mode');
|
||||
this.container.classList.add(`${mode}-mode`);
|
||||
this.config.mode = mode;
|
||||
console.log('[router] 切换动画模式为:', mode);
|
||||
}
|
||||
}
|
||||
|
||||
global.ChannelRouter = ChannelRouter;
|
||||
})(window);
|
||||
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* channel-state.js
|
||||
* 频道状态管理器 - 记忆妈妈的浏览足迹
|
||||
* 使用localStorage持久化,刷新页面自动恢复
|
||||
* 功能:保存/恢复当前路由、已访问模块列表、历史栈
|
||||
*/
|
||||
|
||||
const ChannelState = (function() {
|
||||
const STORAGE_KEY = 'm-channel-state';
|
||||
|
||||
// 默认状态
|
||||
const defaultState = {
|
||||
currentRoute: '/home',
|
||||
visitedModules: [], // 已访问过的模块ID列表
|
||||
historyStack: ['/home'], // 历史记录栈
|
||||
historyIndex: 0 // 当前在历史栈中的位置
|
||||
};
|
||||
|
||||
let state = { ...defaultState };
|
||||
|
||||
// 加载本地存储的状态
|
||||
function load() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
state = JSON.parse(saved);
|
||||
// 确保必要字段存在
|
||||
if (!state.visitedModules) state.visitedModules = [];
|
||||
if (!state.historyStack || !Array.isArray(state.historyStack)) {
|
||||
state.historyStack = [state.currentRoute || '/home'];
|
||||
}
|
||||
if (typeof state.historyIndex !== 'number') {
|
||||
state.historyIndex = 0;
|
||||
}
|
||||
console.log('[state] restore', state);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[state] load failed, use default', e);
|
||||
reset();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// 保存当前状态到localStorage
|
||||
function save() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
console.log('[state] save', state);
|
||||
} catch (e) {
|
||||
console.warn('[state] save failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置为默认状态
|
||||
function reset() {
|
||||
state = { ...defaultState };
|
||||
save();
|
||||
}
|
||||
|
||||
// 更新当前路由
|
||||
function setCurrentRoute(route) {
|
||||
if (state.currentRoute !== route) {
|
||||
state.currentRoute = route;
|
||||
// 添加到已访问模块(如果是模块路由)
|
||||
if (route.startsWith('/module/')) {
|
||||
const moduleId = route.replace('/module/', '');
|
||||
if (!state.visitedModules.includes(moduleId)) {
|
||||
state.visitedModules.push(moduleId);
|
||||
}
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到历史栈(用于前进后退)
|
||||
function pushHistory(route) {
|
||||
// 如果当前不在栈顶,先截断后面的记录
|
||||
if (state.historyIndex < state.historyStack.length - 1) {
|
||||
state.historyStack = state.historyStack.slice(0, state.historyIndex + 1);
|
||||
}
|
||||
state.historyStack.push(route);
|
||||
state.historyIndex = state.historyStack.length - 1;
|
||||
setCurrentRoute(route); // 会触发保存
|
||||
}
|
||||
|
||||
// 后退
|
||||
function goBack() {
|
||||
if (state.historyIndex > 0) {
|
||||
state.historyIndex--;
|
||||
state.currentRoute = state.historyStack[state.historyIndex];
|
||||
save();
|
||||
return state.currentRoute;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 前进
|
||||
function goForward() {
|
||||
if (state.historyIndex < state.historyStack.length - 1) {
|
||||
state.historyIndex++;
|
||||
state.currentRoute = state.historyStack[state.historyIndex];
|
||||
save();
|
||||
return state.currentRoute;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取当前状态
|
||||
function getState() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
// 标记模块为已访问(外部调用)
|
||||
function markModuleVisited(moduleId) {
|
||||
if (!state.visitedModules.includes(moduleId)) {
|
||||
state.visitedModules.push(moduleId);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
// 清除状态(用于测试)
|
||||
function clear() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
reset();
|
||||
}
|
||||
|
||||
// 初始化:加载状态
|
||||
load();
|
||||
|
||||
return {
|
||||
load,
|
||||
save,
|
||||
reset,
|
||||
setCurrentRoute,
|
||||
pushHistory,
|
||||
goBack,
|
||||
goForward,
|
||||
getState,
|
||||
markModuleVisited,
|
||||
clear
|
||||
};
|
||||
})();
|
||||
|
||||
// 导出(如果是模块环境)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ChannelState;
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* channel-style.css
|
||||
* 频道基础样式 + 过渡动画变量
|
||||
*/
|
||||
|
||||
/* 引入过渡动画 */
|
||||
@import url('channel-transition.css');
|
||||
|
||||
:root {
|
||||
--primary-color: #4a90e2;
|
||||
--secondary-color: #f5f5f5;
|
||||
--text-color: #333;
|
||||
--border-radius: 8px;
|
||||
--transition-duration: 0.3s;
|
||||
--transition-timing: ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f0f2f5;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 频道头部 */
|
||||
.channel-header {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.channel-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 导航菜单 */
|
||||
.channel-nav {
|
||||
background: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.channel-nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.channel-nav a {
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.channel-nav a:hover {
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
.channel-nav a.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.channel-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 已访问模块标记 */
|
||||
.module-link.visited {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.module-link.visited::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 动画模式切换按钮 */
|
||||
.animation-toggle {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.animation-toggle button {
|
||||
padding: 5px 15px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.animation-toggle button.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 调试面板样式(后续使用) */
|
||||
.debug-panel {
|
||||
margin-top: 30px;
|
||||
border-top: 2px dashed #ccc;
|
||||
padding-top: 20px;
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* channel-transition.css
|
||||
* 路由过渡动画 - 让切换像呼吸一样自然
|
||||
* 提供两种模式:淡入淡出(fade) / 滑入滑出(slide)
|
||||
*/
|
||||
|
||||
/* 基础容器样式 */
|
||||
.route-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 所有路由页面默认绝对定位,便于重叠动画 */
|
||||
.route-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 淡入淡出模式 */
|
||||
.fade-mode .route-page {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fade-mode .route-page.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: auto;
|
||||
position: relative; /* 激活页变为相对定位,占据文档流高度 */
|
||||
}
|
||||
|
||||
/* 滑入滑出模式 */
|
||||
.slide-mode .route-page {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-mode .route-page.active {
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 进入动画:从右侧滑入 */
|
||||
.slide-mode .route-page.slide-enter {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-mode .route-page.active.slide-enter {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 离开动画:向左侧滑出(用于后退时的反向) */
|
||||
.slide-mode .route-page.slide-exit {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* 前进/后退方向控制 */
|
||||
.slide-mode.forward .route-page.slide-enter {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-mode.forward .route-page.active.slide-enter {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slide-mode.backward .route-page.slide-enter {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-mode.backward .route-page.active.slide-enter {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 防止快速点击时动画重叠 */
|
||||
.route-page {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* event-bus.js
|
||||
* 事件总线 - 模块间的群聊频道
|
||||
* 发布/订阅模式,支持命名空间和调试日志
|
||||
*/
|
||||
|
||||
const EventBus = (function() {
|
||||
// 存储订阅者:{ eventName: [handler1, handler2, ...] }
|
||||
const listeners = {};
|
||||
// 调试模式开关
|
||||
let debugMode = true;
|
||||
|
||||
// 订阅事件
|
||||
function on(eventName, handler) {
|
||||
if (typeof handler !== 'function') {
|
||||
console.error('[bus] 订阅必须提供函数');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeners[eventName]) {
|
||||
listeners[eventName] = [];
|
||||
}
|
||||
listeners[eventName].push(handler);
|
||||
|
||||
if (debugMode) {
|
||||
console.log(`[bus] 订阅事件: ${eventName},当前订阅数: ${listeners[eventName].length}`);
|
||||
}
|
||||
|
||||
// 返回取消订阅函数
|
||||
return function off() {
|
||||
off(eventName, handler);
|
||||
};
|
||||
}
|
||||
|
||||
// 取消订阅
|
||||
function off(eventName, handler) {
|
||||
if (!listeners[eventName]) return;
|
||||
|
||||
if (handler) {
|
||||
// 移除特定handler
|
||||
const index = listeners[eventName].indexOf(handler);
|
||||
if (index !== -1) {
|
||||
listeners[eventName].splice(index, 1);
|
||||
if (debugMode) {
|
||||
console.log(`[bus] 取消订阅: ${eventName},剩余: ${listeners[eventName].length}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 移除该事件所有订阅
|
||||
delete listeners[eventName];
|
||||
if (debugMode) {
|
||||
console.log(`[bus] 移除所有订阅: ${eventName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
function emit(eventName, data) {
|
||||
if (!listeners[eventName]) {
|
||||
if (debugMode) {
|
||||
console.log(`[bus] 触发事件 ${eventName} 但无订阅者`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugMode) {
|
||||
console.log(`[bus] 触发事件: ${eventName},数据:`, data);
|
||||
}
|
||||
|
||||
// 复制一份以防在遍历过程中修改
|
||||
const handlers = listeners[eventName].slice();
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(data, eventName);
|
||||
} catch (e) {
|
||||
console.error(`[bus] 事件 ${eventName} 处理出错:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空所有订阅
|
||||
function clear() {
|
||||
for (let key in listeners) {
|
||||
delete listeners[key];
|
||||
}
|
||||
if (debugMode) {
|
||||
console.log('[bus] 清空所有订阅');
|
||||
}
|
||||
}
|
||||
|
||||
// 开启/关闭调试
|
||||
function setDebug(enable) {
|
||||
debugMode = enable;
|
||||
}
|
||||
|
||||
return {
|
||||
on,
|
||||
off,
|
||||
emit,
|
||||
clear,
|
||||
setDebug
|
||||
};
|
||||
})();
|
||||
|
||||
// 挂载到全局
|
||||
window.EventBus = EventBus;
|
||||
|
||||
// 如果是模块环境
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = EventBus;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖频道 · 动态渲染引擎</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本(顺序很重要!) -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* module-lifecycle.js
|
||||
* 模块生命周期管理 - 让模块知道自己何时加载/卸载/收到消息
|
||||
* 配合事件总线使用
|
||||
*/
|
||||
|
||||
const ModuleLifecycle = (function() {
|
||||
// 存储每个模块的钩子函数
|
||||
const hooks = {};
|
||||
|
||||
// 注册模块的生命周期钩子
|
||||
function register(moduleId, lifecycles) {
|
||||
if (!moduleId) return;
|
||||
|
||||
hooks[moduleId] = {
|
||||
onLoad: lifecycles.onLoad || null,
|
||||
onUnload: lifecycles.onUnload || null,
|
||||
onMessage: lifecycles.onMessage || null
|
||||
};
|
||||
|
||||
console.log(`[lifecycle] 注册模块: ${moduleId}`, lifecycles);
|
||||
|
||||
// 如果已经加载(比如页面初始化时),自动触发onLoad?
|
||||
// 这里留给loader去调用
|
||||
}
|
||||
|
||||
// 触发模块加载
|
||||
function triggerLoad(moduleId, params) {
|
||||
const moduleHooks = hooks[moduleId];
|
||||
if (moduleHooks && moduleHooks.onLoad) {
|
||||
try {
|
||||
moduleHooks.onLoad(params);
|
||||
console.log(`[lifecycle] onLoad 模块: ${moduleId}`);
|
||||
} catch (e) {
|
||||
console.error(`[lifecycle] onLoad 模块 ${moduleId} 出错:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发模块卸载
|
||||
function triggerUnload(moduleId) {
|
||||
const moduleHooks = hooks[moduleId];
|
||||
if (moduleHooks && moduleHooks.onUnload) {
|
||||
try {
|
||||
moduleHooks.onUnload();
|
||||
console.log(`[lifecycle] onUnload 模块: ${moduleId}`);
|
||||
} catch (e) {
|
||||
console.error(`[lifecycle] onUnload 模块 ${moduleId} 出错:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载时自动取消该模块的所有事件订阅
|
||||
// 这里简单使用事件总线的off,但需要知道该模块订阅了哪些事件
|
||||
// 我们约定模块在订阅时使用带命名空间的事件名,比如 moduleA:click
|
||||
// 或者在onUnload里手动取消。为了简化,我们不清除订阅,但可以在onUnload里做。
|
||||
// 更完善的做法是记录每个模块的订阅列表,但这里先不实现。
|
||||
}
|
||||
|
||||
// 触发模块收到消息(由事件总线转发时调用)
|
||||
function triggerMessage(moduleId, message, data) {
|
||||
const moduleHooks = hooks[moduleId];
|
||||
if (moduleHooks && moduleHooks.onMessage) {
|
||||
try {
|
||||
moduleHooks.onMessage(message, data);
|
||||
console.log(`[lifecycle] onMessage 模块: ${moduleId} 消息: ${message}`);
|
||||
} catch (e) {
|
||||
console.error(`[lifecycle] onMessage 模块 ${moduleId} 出错:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模块的钩子(用于调试)
|
||||
function getHooks(moduleId) {
|
||||
return hooks[moduleId] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
triggerLoad,
|
||||
triggerUnload,
|
||||
triggerMessage,
|
||||
getHooks
|
||||
};
|
||||
})();
|
||||
|
||||
window.ModuleLifecycle = ModuleLifecycle;
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ModuleLifecycle;
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* module-loader.js
|
||||
* 模块加载器 - 负责动态加载模块HTML/JS,并管理生命周期
|
||||
* 集成事件总线和生命周期钩子
|
||||
*/
|
||||
|
||||
const ModuleLoader = (function() {
|
||||
// 已加载的模块缓存
|
||||
const loadedModules = {};
|
||||
|
||||
// 加载模块
|
||||
async function loadModule(moduleId, container, params = {}) {
|
||||
if (loadedModules[moduleId]) {
|
||||
console.log(`[loader] 模块 ${moduleId} 已加载,直接显示`);
|
||||
showModule(moduleId, container, params);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[loader] 正在加载模块: ${moduleId}`);
|
||||
|
||||
// 获取模块URL(这里使用mock-modules下的html文件)
|
||||
const url = `mock-modules/mock-${moduleId}.html`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
|
||||
// 解析HTML,提取body内容作为模块内容
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
let moduleContent = doc.body.innerHTML;
|
||||
|
||||
// 提取script标签并执行
|
||||
const scripts = doc.querySelectorAll('script');
|
||||
scripts.forEach(script => {
|
||||
const newScript = document.createElement('script');
|
||||
if (script.src) {
|
||||
newScript.src = script.src;
|
||||
} else {
|
||||
newScript.textContent = script.textContent;
|
||||
}
|
||||
document.body.appendChild(newScript);
|
||||
// 注意:动态添加的脚本会立即执行
|
||||
});
|
||||
|
||||
// 存储模块内容
|
||||
loadedModules[moduleId] = {
|
||||
content: moduleContent,
|
||||
scripts: scripts.length
|
||||
};
|
||||
|
||||
// 显示模块
|
||||
showModule(moduleId, container, params);
|
||||
|
||||
// 触发生命周期 onLoad
|
||||
ModuleLifecycle.triggerLoad(moduleId, params);
|
||||
|
||||
// 标记已访问(状态管理)
|
||||
if (window.ChannelState) {
|
||||
ChannelState.markModuleVisited(moduleId);
|
||||
}
|
||||
|
||||
console.log(`[loader] 模块 ${moduleId} 加载完成`);
|
||||
} catch (error) {
|
||||
console.error(`[loader] 加载模块 ${moduleId} 失败:`, error);
|
||||
container.innerHTML = `<div class="error">加载失败:${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示已加载的模块
|
||||
function showModule(moduleId, container, params) {
|
||||
const mod = loadedModules[moduleId];
|
||||
if (!mod) return;
|
||||
|
||||
container.innerHTML = mod.content;
|
||||
|
||||
// 如果有参数,可以通过自定义事件传递
|
||||
if (params) {
|
||||
const event = new CustomEvent('module:params', { detail: params });
|
||||
container.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// 通知模块已显示(通过生命周期?)
|
||||
// 可以用triggerMessage
|
||||
ModuleLifecycle.triggerMessage(moduleId, 'show', params);
|
||||
}
|
||||
|
||||
// 卸载模块
|
||||
function unloadModule(moduleId) {
|
||||
if (loadedModules[moduleId]) {
|
||||
// 触发生命周期 onUnload
|
||||
ModuleLifecycle.triggerUnload(moduleId);
|
||||
|
||||
// 清理缓存(可选)
|
||||
delete loadedModules[moduleId];
|
||||
console.log(`[loader] 模块 ${moduleId} 已卸载`);
|
||||
}
|
||||
}
|
||||
|
||||
// 预加载模块
|
||||
function preloadModule(moduleId) {
|
||||
// 简单fetch但不显示
|
||||
fetch(`mock-modules/mock-${moduleId}.html`).catch(() => {});
|
||||
}
|
||||
|
||||
return {
|
||||
loadModule,
|
||||
unloadModule,
|
||||
preloadModule
|
||||
};
|
||||
})();
|
||||
|
||||
window.ModuleLoader = ModuleLoader;
|
||||
@ -0,0 +1,7 @@
|
||||
const moduleRegistry = {
|
||||
'mock-a': 'mock-modules/mock-a.html',
|
||||
'mock-b': 'mock-modules/mock-b.html',
|
||||
'mock-c': 'mock-modules/mock-c.html',
|
||||
'mock-d': 'mock-modules/mock-d.html'
|
||||
};
|
||||
window.moduleRegistry = moduleRegistry;
|
||||
178
agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js
Normal file
178
agents/zhuyuan-dev-agent/modules/m-channel/channel-analytics.js
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* channel-analytics.js
|
||||
* 频道数据采集核心·环节8 + 环节9设置联动
|
||||
* 记录模块访问次数、停留时间、页面加载速度
|
||||
*/
|
||||
|
||||
const ChannelAnalytics = (function() {
|
||||
const STORAGE_KEY = 'channel-analytics-data';
|
||||
|
||||
// 数据结构初始化
|
||||
function getDefaultData() {
|
||||
return {
|
||||
modules: {}, // {moduleId: {visits: 0, totalDuration: 0, loadTimes: [], dailyVisits: {} }}
|
||||
globalDaily: {}, // {'YYYY-MM-DD': totalVisits}
|
||||
lastUpdated: null
|
||||
};
|
||||
}
|
||||
|
||||
// localStorage 读写
|
||||
function loadData() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch(e) {
|
||||
console.log('⚠️ 读取分析数据失败,重新初始化');
|
||||
}
|
||||
return getDefaultData();
|
||||
}
|
||||
|
||||
function saveData(data) {
|
||||
data.lastUpdated = new Date().toISOString();
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// 日期工具
|
||||
function today() {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 确保模块数据结构存在
|
||||
function ensureModule(data, moduleId) {
|
||||
if (!data.modules[moduleId]) {
|
||||
data.modules[moduleId] = {
|
||||
visits: 0,
|
||||
totalDuration: 0,
|
||||
loadTimes: [],
|
||||
dailyVisits: {}
|
||||
};
|
||||
}
|
||||
return data.modules[moduleId];
|
||||
}
|
||||
|
||||
// 当前会话状态
|
||||
let currentModule = null;
|
||||
let enterTime = null;
|
||||
let loadStartTime = null;
|
||||
|
||||
// 公开方法
|
||||
return {
|
||||
// 记录模块访问(环节9:检查数据采集开关)
|
||||
recordVisit: function(moduleId) {
|
||||
// 数据采集开关检查(环节9新增)
|
||||
if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) {
|
||||
console.log('📊 数据采集已关闭(设置中心)');
|
||||
return;
|
||||
}
|
||||
if (!moduleId) return;
|
||||
const data = loadData();
|
||||
const mod = ensureModule(data, moduleId);
|
||||
|
||||
// 访问计数 +1
|
||||
mod.visits++;
|
||||
|
||||
// 每日访问计数
|
||||
const d = today();
|
||||
mod.dailyVisits[d] = (mod.dailyVisits[d] || 0) + 1;
|
||||
|
||||
// 全局每日访问
|
||||
data.globalDaily[d] = (data.globalDaily[d] || 0) + 1;
|
||||
|
||||
saveData(data);
|
||||
console.log('📊 已记录:模块 ' + moduleId + ' 被访问,累计 ' + mod.visits + ' 次');
|
||||
|
||||
// 记录进入时间
|
||||
this.startSession(moduleId);
|
||||
},
|
||||
|
||||
// 开始计时
|
||||
startSession: function(moduleId) {
|
||||
// 先结束上一个模块的计时
|
||||
if (currentModule && enterTime) {
|
||||
this.endSession();
|
||||
}
|
||||
currentModule = moduleId;
|
||||
enterTime = performance.now();
|
||||
loadStartTime = performance.now();
|
||||
},
|
||||
|
||||
// 结束计时(切换模块或离开时调用)
|
||||
endSession: function() {
|
||||
if (!currentModule || !enterTime) return;
|
||||
// 数据采集开关检查(环节9新增)
|
||||
if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) {
|
||||
// 如果关闭采集,清空当前会话但不记录
|
||||
currentModule = null;
|
||||
enterTime = null;
|
||||
return;
|
||||
}
|
||||
const duration = Math.round((performance.now() - enterTime) / 1000); // 秒
|
||||
const data = loadData();
|
||||
const mod = ensureModule(data, currentModule);
|
||||
mod.totalDuration += duration;
|
||||
saveData(data);
|
||||
console.log('⏱️ 停留时间:模块 ' + currentModule + ' 停留约 ' + duration + ' 秒');
|
||||
currentModule = null;
|
||||
enterTime = null;
|
||||
},
|
||||
|
||||
// 记录加载性能
|
||||
recordLoadTime: function(moduleId, loadTimeMs) {
|
||||
if (!moduleId) return;
|
||||
// 数据采集开关检查(环节9新增)
|
||||
if (typeof ChannelSettings !== 'undefined' && !ChannelSettings.get('analyticsEnabled')) {
|
||||
return;
|
||||
}
|
||||
const data = loadData();
|
||||
const mod = ensureModule(data, moduleId);
|
||||
mod.loadTimes.push(loadTimeMs);
|
||||
// 只保留最近50次
|
||||
if (mod.loadTimes.length > 50) {
|
||||
mod.loadTimes = mod.loadTimes.slice(-50);
|
||||
}
|
||||
saveData(data);
|
||||
console.log('⚡ 加载耗时:模块 ' + moduleId + ' 加载 ' + Math.round(loadTimeMs) + ' 毫秒');
|
||||
},
|
||||
|
||||
// 标记加载开始
|
||||
markLoadStart: function() {
|
||||
loadStartTime = performance.now();
|
||||
},
|
||||
|
||||
// 标记加载完成并记录
|
||||
markLoadEnd: function(moduleId) {
|
||||
if (loadStartTime && moduleId) {
|
||||
const loadTime = performance.now() - loadStartTime;
|
||||
this.recordLoadTime(moduleId, loadTime);
|
||||
loadStartTime = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取所有数据(面板用)
|
||||
getAllData: function() {
|
||||
return loadData();
|
||||
},
|
||||
|
||||
// 获取最近7天趋势
|
||||
getWeeklyTrend: function() {
|
||||
const data = loadData();
|
||||
const trend = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
trend.push({
|
||||
date: dateStr,
|
||||
visits: data.globalDaily[dateStr] || 0
|
||||
});
|
||||
}
|
||||
return trend;
|
||||
},
|
||||
|
||||
// 清除所有数据(调试用)
|
||||
clearAll: function() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.log('🗑️ 所有分析数据已清除');
|
||||
}
|
||||
};
|
||||
})();
|
||||
267
agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html
Normal file
267
agents/zhuyuan-dev-agent/modules/m-channel/channel-complete.html
Normal file
@ -0,0 +1,267 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖频道 · 完全版</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
<link rel="stylesheet" href="channel-layout.css">
|
||||
<style id="theme-variables"></style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="m06">📋 工单管理</button>
|
||||
<button class="channel-btn" data-channel="m08">📊 数据统计</button>
|
||||
<button class="channel-btn" data-channel="m11">🧩 组件库</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
<button class="channel-btn settings-btn" id="settings-toggle">⚙️ 设置</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 设置面板(侧边栏) -->
|
||||
<div id="settings-panel" class="settings-panel">
|
||||
<div class="settings-header">
|
||||
<h3>频道设置</h3>
|
||||
<button id="settings-close">✕</button>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
<div class="settings-section">
|
||||
<h4>布局模式</h4>
|
||||
<div class="layout-options">
|
||||
<label><input type="radio" name="layout" value="grid" checked> 网格</label>
|
||||
<label><input type="radio" name="layout" value="list"> 列表</label>
|
||||
<label><input type="radio" name="layout" value="compact"> 紧凑</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>主题色</h4>
|
||||
<div class="theme-options">
|
||||
<button class="theme-btn" data-theme="default">默认蓝</button>
|
||||
<button class="theme-btn" data-theme="ocean">海洋</button>
|
||||
<button class="theme-btn" data-theme="forest">森林</button>
|
||||
<button class="theme-btn" data-theme="sunset">日落</button>
|
||||
<button class="theme-btn" data-theme="lavender">薰衣草</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>统计数据</h4>
|
||||
<button id="show-stats-btn">查看使用统计</button>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>其他</h4>
|
||||
<button id="reset-preferences-btn">恢复默认设置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计面板(模态框) -->
|
||||
<div id="stats-modal" class="stats-modal">
|
||||
<div class="stats-modal-content">
|
||||
<div class="stats-modal-header">
|
||||
<h3>使用统计</h3>
|
||||
<button id="stats-close">✕</button>
|
||||
</div>
|
||||
<div class="stats-modal-body" id="stats-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本 -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="error-boundary.js"></script>
|
||||
<script src="adapters/module-adapter.js"></script>
|
||||
<script src="adapters/m06-adapter.js"></script>
|
||||
<script src="adapters/m08-adapter.js"></script>
|
||||
<script src="adapters/m11-adapter.js"></script>
|
||||
<script src="channel-preferences.js"></script>
|
||||
<script src="channel-theme.js"></script>
|
||||
<script src="channel-favorites.js"></script>
|
||||
<script src="channel-stats.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="app.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.ChannelPreferences) ChannelPreferences.init();
|
||||
if (window.ChannelTheme) ChannelTheme.init();
|
||||
if (window.ChannelStats) ChannelStats.init();
|
||||
|
||||
// 设置面板开关
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsPanel = document.getElementById('settings-panel');
|
||||
const settingsClose = document.getElementById('settings-close');
|
||||
if (settingsToggle && settingsPanel) {
|
||||
settingsToggle.addEventListener('click', () => {
|
||||
settingsPanel.classList.add('open');
|
||||
});
|
||||
settingsClose.addEventListener('click', () => {
|
||||
settingsPanel.classList.remove('open');
|
||||
});
|
||||
}
|
||||
|
||||
// 布局切换
|
||||
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
const layout = e.target.value;
|
||||
document.body.className = document.body.className.replace(/layout-\w+/, '') + ' layout-' + layout;
|
||||
if (window.ChannelPreferences) ChannelPreferences.setLayout(layout);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 主题切换
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const theme = btn.dataset.theme;
|
||||
if (window.ChannelTheme) ChannelTheme.setTheme(theme);
|
||||
});
|
||||
});
|
||||
|
||||
// 统计面板
|
||||
const showStatsBtn = document.getElementById('show-stats-btn');
|
||||
const statsModal = document.getElementById('stats-modal');
|
||||
const statsClose = document.getElementById('stats-close');
|
||||
if (showStatsBtn && statsModal) {
|
||||
showStatsBtn.addEventListener('click', () => {
|
||||
if (window.ChannelStats) {
|
||||
const body = document.getElementById('stats-modal-body');
|
||||
ChannelStats.renderStatsPanel(body);
|
||||
statsModal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
statsClose.addEventListener('click', () => {
|
||||
statsModal.style.display = 'none';
|
||||
});
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === statsModal) statsModal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复默认
|
||||
const resetBtn = document.getElementById('reset-preferences-btn');
|
||||
if (resetBtn && window.ChannelPreferences) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (confirm('确定恢复所有默认设置吗?')) {
|
||||
ChannelPreferences.reset();
|
||||
document.body.className = document.body.className.replace(/layout-\w+/, '') + ' layout-grid';
|
||||
document.querySelectorAll('input[name="layout"]').forEach(r => r.checked = (r.value === 'grid'));
|
||||
if (window.ChannelTheme) ChannelTheme.setTheme('default');
|
||||
alert('已恢复默认设置');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 拦截事件总线消息
|
||||
const originalEmit = EventBus.emit;
|
||||
EventBus.emit = function(event, data) {
|
||||
if (!window.debugMessages) window.debugMessages = [];
|
||||
window.debugMessages.push({ event, data, time: new Date().toLocaleTimeString() });
|
||||
originalEmit.call(this, event, data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.settings-panel { position: fixed; top: 0; right: -400px; width: 380px; height: 100vh; background: white; box-shadow: -2px 0 10px rgba(0,0,0,0.1); transition: right 0.3s ease; z-index: 1000; display: flex; flex-direction: column; }
|
||||
.settings-panel.open { right: 0; }
|
||||
.settings-header { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||||
.settings-body { padding: 20px; overflow-y: auto; }
|
||||
.settings-section { margin-bottom: 30px; }
|
||||
.layout-options label { margin-right: 20px; }
|
||||
.theme-options { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.theme-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 20px; background: white; cursor: pointer; }
|
||||
.stats-modal { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index:2000; }
|
||||
.stats-modal-content { background: white; width: 600px; max-width: 90%; max-height: 80vh; border-radius: 12px; overflow: auto; }
|
||||
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; padding: 20px; }
|
||||
.module-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); cursor: pointer; transition: all 0.2s; }
|
||||
.module-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.module-card-header { padding: 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; }
|
||||
.drag-handle { font-size: 20px; color: #999; cursor: grab; }
|
||||
.favorite-star { margin-left: auto; font-size: 20px; color: #ccc; cursor: pointer; }
|
||||
.favorite-star.active { color: #fbbf24; }
|
||||
.module-card-content { padding: 15px; }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
// 确保所有模块在页面加载后自动初始化
|
||||
(function() {
|
||||
if (window.ChannelPreferences) ChannelPreferences.init();
|
||||
if (window.ChannelTheme) ChannelTheme.init();
|
||||
if (window.ChannelFavorites) {
|
||||
setTimeout(() => {
|
||||
ChannelFavorites.init();
|
||||
ChannelFavorites.initDragAndDrop();
|
||||
}, 100);
|
||||
}
|
||||
if (window.ChannelStats) ChannelStats.init();
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// 确保所有功能自动初始化
|
||||
(function() {
|
||||
console.log('[最终修复] 执行自动初始化');
|
||||
|
||||
// 初始化各个模块(如果尚未初始化)
|
||||
if (window.ChannelPreferences && !window.ChannelPreferences.config) {
|
||||
ChannelPreferences.init();
|
||||
}
|
||||
if (window.ChannelTheme) {
|
||||
ChannelTheme.init();
|
||||
}
|
||||
if (window.ChannelFavorites) {
|
||||
// 延迟一点确保DOM渲染完成
|
||||
setTimeout(() => {
|
||||
ChannelFavorites.init();
|
||||
ChannelFavorites.initDragAndDrop();
|
||||
}, 200);
|
||||
}
|
||||
if (window.ChannelStats) {
|
||||
ChannelStats.init();
|
||||
}
|
||||
|
||||
// 绑定设置按钮事件
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsPanel = document.getElementById('settings-panel');
|
||||
if (settingsToggle && settingsPanel) {
|
||||
// 移除可能存在的旧监听器(避免重复)
|
||||
settingsToggle.replaceWith(settingsToggle.cloneNode(true));
|
||||
const newToggle = document.getElementById('settings-toggle');
|
||||
newToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
settingsPanel.classList.add('open');
|
||||
});
|
||||
console.log('[最终修复] 设置按钮事件已绑定');
|
||||
} else {
|
||||
console.warn('[最终修复] 未找到设置按钮或面板');
|
||||
}
|
||||
|
||||
// 修复拖拽排序保存
|
||||
if (window.ChannelFavorites && window.ChannelFavorites.handleDrop) {
|
||||
// 增强拖拽放置处理,保存排序
|
||||
const originalHandleDrop = ChannelFavorites.handleDrop;
|
||||
ChannelFavorites.handleDrop = function(e) {
|
||||
originalHandleDrop.call(this, e);
|
||||
// 拖拽完成后,保存当前顺序
|
||||
setTimeout(() => {
|
||||
if (window.ChannelPreferences) {
|
||||
const cards = Array.from(document.querySelectorAll('.module-card'));
|
||||
const order = cards.map(card => card.dataset.module);
|
||||
ChannelPreferences.setModuleOrder(order);
|
||||
console.log('[最终修复] 拖拽顺序已保存', order);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
199
agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.css
Normal file
199
agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.css
Normal file
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* channel-dashboard.css
|
||||
* 频道数据面板样式·深色主题
|
||||
*/
|
||||
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
font-size: 24px;
|
||||
color: #4fc3f7;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 图表网格 */
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #1a1a2e;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #2a2a4a;
|
||||
}
|
||||
|
||||
.chart-card h3 {
|
||||
font-size: 16px;
|
||||
color: #4fc3f7;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #2a2a4a;
|
||||
}
|
||||
|
||||
.chart-card canvas {
|
||||
width: 100% !important;
|
||||
height: 200px !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 柱状图 */
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
height: 200px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
|
||||
.bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
width: 40px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
transition: height 0.5s ease;
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 饼图 */
|
||||
.pie-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pie-canvas-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pie-legend {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pie-legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pie-legend .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 折线图 */
|
||||
.line-chart-wrap {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* 性能表格 */
|
||||
.perf-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.perf-table th {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
background: #2a2a4a;
|
||||
color: #4fc3f7;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.perf-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #2a2a4a;
|
||||
}
|
||||
|
||||
.perf-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.perf-table .slow {
|
||||
color: #ff5252;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.dashboard-actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #ff5252;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #ff1744;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.pie-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* 饼图 canvas 保持正方形 */
|
||||
.pie-canvas-wrap canvas {
|
||||
width: 160px !important;
|
||||
height: 160px !important;
|
||||
}
|
||||
201
agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.js
Normal file
201
agents/zhuyuan-dev-agent/modules/m-channel/channel-dashboard.js
Normal file
@ -0,0 +1,201 @@
|
||||
|
||||
/**
|
||||
* channel-dashboard.js
|
||||
* 频道数据面板逻辑·图表渲染
|
||||
*/
|
||||
|
||||
const ChannelDashboard = (function() {
|
||||
// 配色方案
|
||||
const COLORS = ['#4fc3f7', '#ffb74d', '#ff8a80', '#aed581', '#ba68c8', '#4dd0e1', '#ffd54f'];
|
||||
|
||||
// 柱状图渲染
|
||||
function renderBarChart() {
|
||||
const container = document.getElementById('visitBarChart');
|
||||
if (!container) return;
|
||||
const data = ChannelAnalytics.getAllData();
|
||||
const modules = data.modules;
|
||||
const keys = Object.keys(modules);
|
||||
if (keys.length === 0) {
|
||||
container.innerHTML = '<p style="color:#666;text-align:center;width:100%;">暂无数据,多点几个模块再来看</p>';
|
||||
return;
|
||||
}
|
||||
const maxVisits = Math.max.apply(null, keys.map(function(k) { return modules[k].visits; })) || 1;
|
||||
let html = '';
|
||||
keys.forEach(function(id, i) {
|
||||
const mod = modules[id];
|
||||
const height = Math.max(4, (mod.visits / maxVisits) * 180);
|
||||
const color = COLORS[i % COLORS.length];
|
||||
html += '<div class="bar-item">' +
|
||||
'<span class="bar-value">' + mod.visits + '</span>' +
|
||||
'<div class="bar-fill" style="height:' + height + 'px;background:' + color + ';"></div>' +
|
||||
'<span class="bar-label">' + id.replace('m-', '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 饼图渲染
|
||||
function renderPieChart() {
|
||||
const canvas = document.getElementById('pieCanvas');
|
||||
const legendEl = document.getElementById('pieLegend');
|
||||
if (!canvas || !legendEl) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const data = ChannelAnalytics.getAllData();
|
||||
const modules = data.modules;
|
||||
const keys = Object.keys(modules);
|
||||
const total = keys.reduce(function(sum, k) {
|
||||
return sum + (modules[k].totalDuration || 0);
|
||||
}, 0);
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (total === 0 || keys.length === 0) {
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.beginPath();
|
||||
ctx.arc(80, 80, 70, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('暂无数据', 80, 84);
|
||||
legendEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let startAngle = -Math.PI / 2;
|
||||
let legendHtml = '';
|
||||
keys.forEach(function(id, i) {
|
||||
const mod = modules[id];
|
||||
const pct = mod.totalDuration / total;
|
||||
const sweep = pct * Math.PI * 2;
|
||||
const color = COLORS[i % COLORS.length];
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(80, 80);
|
||||
ctx.arc(80, 80, 70, startAngle, startAngle + sweep);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
|
||||
startAngle += sweep;
|
||||
|
||||
const minutes = Math.round(mod.totalDuration / 60);
|
||||
const pctStr = Math.round(pct * 100);
|
||||
legendHtml += '<li><span class="dot" style="background:' + color + ';"></span>' +
|
||||
id.replace('m-', '') + ' ' + pctStr + '% (' + minutes + '分钟)</li>';
|
||||
});
|
||||
legendEl.innerHTML = legendHtml;
|
||||
}
|
||||
|
||||
// 折线图渲染
|
||||
function renderLineChart() {
|
||||
const canvas = document.getElementById('lineCanvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const trend = ChannelAnalytics.getWeeklyTrend();
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const maxVal = Math.max.apply(null, trend.map(function(t) { return t.visits; })) || 1;
|
||||
const padLeft = 40;
|
||||
const padBottom = 30;
|
||||
const padTop = 10;
|
||||
const chartW = w - padLeft - 20;
|
||||
const chartH = h - padBottom - padTop;
|
||||
const stepX = chartW / (trend.length - 1 || 1);
|
||||
|
||||
// 网格线
|
||||
ctx.strokeStyle = '#2a2a4a';
|
||||
ctx.lineWidth = 1;
|
||||
for (let g = 0; g <= 4; g++) {
|
||||
let gy = padTop + (chartH / 4) * g;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padLeft, gy);
|
||||
ctx.lineTo(w - 20, gy);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 折线
|
||||
ctx.strokeStyle = '#4fc3f7';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
trend.forEach(function(t, i) {
|
||||
let x = padLeft + stepX * i;
|
||||
let y = padTop + chartH - (t.visits / maxVal) * chartH;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// 数据点
|
||||
trend.forEach(function(t, i) {
|
||||
let x = padLeft + stepX * i;
|
||||
let y = padTop + chartH - (t.visits / maxVal) * chartH;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#4fc3f7';
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// 标签
|
||||
ctx.fillStyle = '#aaa';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
trend.forEach(function(t, i) {
|
||||
let x = padLeft + stepX * i;
|
||||
let y = padTop + chartH + 15;
|
||||
ctx.fillText(t.date.slice(5), x, y);
|
||||
});
|
||||
}
|
||||
|
||||
// 性能表格渲染
|
||||
function renderPerfTable() {
|
||||
const tbody = document.getElementById('perfTableBody');
|
||||
if (!tbody) return;
|
||||
const data = ChannelAnalytics.getAllData();
|
||||
const modules = data.modules;
|
||||
const keys = Object.keys(modules);
|
||||
if (keys.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#666;">暂无数据</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
keys.forEach(function(id) {
|
||||
const mod = modules[id];
|
||||
const avgLoad = mod.loadTimes.length ?
|
||||
Math.round(mod.loadTimes.reduce((a, b) => a + b, 0) / mod.loadTimes.length) : 0;
|
||||
const minutes = Math.round(mod.totalDuration / 60);
|
||||
const statusClass = avgLoad > 300 ? 'slow' : '';
|
||||
const statusText = avgLoad > 300 ? '⚠️ 慢' : '✅ 正常';
|
||||
|
||||
html += '<tr>' +
|
||||
'<td>' + id.replace('m-', '') + '</td>' +
|
||||
'<td>' + mod.visits + '</td>' +
|
||||
'<td>' + minutes + '分钟</td>' +
|
||||
'<td class="' + statusClass + '">' + avgLoad + 'ms</td>' +
|
||||
'<td class="' + statusClass + '">' + statusText + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 公开方法
|
||||
return {
|
||||
render: function() {
|
||||
renderBarChart();
|
||||
renderPieChart();
|
||||
renderLineChart();
|
||||
renderPerfTable();
|
||||
},
|
||||
clearData: function() {
|
||||
if (confirm('确定清除所有统计数据吗?')) {
|
||||
ChannelAnalytics.clearAll();
|
||||
this.render();
|
||||
console.log('🗑️ 数据已清除,图表已重置');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,338 @@
|
||||
// channel-enhancements.js - 频道搜索、面包屑、快捷键增强功能(终极修复版)
|
||||
(function() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function init() {
|
||||
addStyles();
|
||||
createSearchBox();
|
||||
createBreadcrumb();
|
||||
createShortcutHint();
|
||||
initShortcuts();
|
||||
bindCardClicks(); // ★ 新增:为卡片绑定点击切换模块
|
||||
window.addEventListener('hashchange', updateBreadcrumb);
|
||||
updateBreadcrumb();
|
||||
}
|
||||
|
||||
function addStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.channel-search-container {
|
||||
padding: 16px 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.channel-search-input {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 24px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.channel-search-input:focus {
|
||||
border-color: #4d6bfe;
|
||||
box-shadow: 0 0 0 3px rgba(77,107,254,0.1);
|
||||
}
|
||||
.channel-search-clear {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
padding: 0 8px;
|
||||
display: none;
|
||||
}
|
||||
.channel-search-clear:hover {
|
||||
color: #212529;
|
||||
}
|
||||
.channel-search-input:not(:placeholder-shown) + .channel-search-clear {
|
||||
display: inline-block;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #ffeb3b;
|
||||
padding: 2px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
.channel-breadcrumb {
|
||||
padding: 12px 20px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
font-size: 14px;
|
||||
}
|
||||
.channel-breadcrumb a {
|
||||
color: #4d6bfe;
|
||||
text-decoration: none;
|
||||
}
|
||||
.channel-breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.channel-breadcrumb span {
|
||||
color: #6c757d;
|
||||
}
|
||||
.channel-breadcrumb .current {
|
||||
color: #212529;
|
||||
font-weight: 500;
|
||||
}
|
||||
.shortcut-hint {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
}
|
||||
.shortcut-hint kbd {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
.module-card.selected {
|
||||
outline: 2px solid #4d6bfe;
|
||||
outline-offset: 2px;
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function createSearchBox() {
|
||||
const container = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.querySelector('.channel-content');
|
||||
if (!container) return;
|
||||
|
||||
const searchContainer = document.createElement('div');
|
||||
searchContainer.className = 'channel-search-container';
|
||||
searchContainer.innerHTML = `
|
||||
<input type="text" class="channel-search-input" placeholder="搜索模块...">
|
||||
<button class="channel-search-clear">×</button>
|
||||
`;
|
||||
container.parentNode.insertBefore(searchContainer, container);
|
||||
|
||||
const searchInput = searchContainer.querySelector('.channel-search-input');
|
||||
const clearBtn = searchContainer.querySelector('.channel-search-clear');
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
filterModules(this.value);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
filterModules('');
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
window.__channelSearchInput = searchInput;
|
||||
}
|
||||
|
||||
function filterModules(keyword) {
|
||||
const cards = document.querySelectorAll('.module-card');
|
||||
const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
|
||||
let hasResults = false;
|
||||
|
||||
// 移除所有高亮
|
||||
removeAllHighlights();
|
||||
|
||||
cards.forEach(card => {
|
||||
const text = card.innerText || card.textContent;
|
||||
if (keyword === '') {
|
||||
card.style.display = '';
|
||||
hasResults = true;
|
||||
} else {
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
if (lowerText.includes(lowerKeyword)) {
|
||||
card.style.display = '';
|
||||
highlightText(card, keyword);
|
||||
hasResults = true;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let noResultsEl = document.querySelector('.no-results');
|
||||
if (!hasResults && keyword !== '') {
|
||||
if (!noResultsEl) {
|
||||
noResultsEl = document.createElement('div');
|
||||
noResultsEl.className = 'no-results';
|
||||
noResultsEl.textContent = '没有找到匹配的模块';
|
||||
container.parentNode.insertBefore(noResultsEl, container.nextSibling);
|
||||
}
|
||||
} else {
|
||||
if (noResultsEl) noResultsEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllHighlights() {
|
||||
document.querySelectorAll('.highlight').forEach(span => {
|
||||
const parent = span.parentNode;
|
||||
parent.replaceChild(document.createTextNode(span.textContent), span);
|
||||
parent.normalize();
|
||||
});
|
||||
}
|
||||
|
||||
function highlightText(card, keyword) {
|
||||
const regex = new RegExp(`(${keyword})`, 'gi');
|
||||
const walk = document.createTreeWalker(card, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: function(node) {
|
||||
if (node.parentNode.classList && node.parentNode.classList.contains('highlight')) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
}, false);
|
||||
|
||||
const textNodes = [];
|
||||
while (walk.nextNode()) textNodes.push(walk.currentNode);
|
||||
|
||||
textNodes.forEach(node => {
|
||||
const text = node.nodeValue;
|
||||
if (regex.test(text)) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'highlight';
|
||||
span.innerHTML = text.replace(regex, '<span class="highlight">$1</span>');
|
||||
node.parentNode.replaceChild(span, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createBreadcrumb() {
|
||||
const searchContainer = document.querySelector('.channel-search-container');
|
||||
const breadcrumb = document.createElement('div');
|
||||
breadcrumb.className = 'channel-breadcrumb';
|
||||
breadcrumb.id = 'channelBreadcrumb';
|
||||
if (searchContainer) {
|
||||
searchContainer.parentNode.insertBefore(breadcrumb, searchContainer.nextSibling);
|
||||
} else {
|
||||
const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
|
||||
if (container) {
|
||||
container.parentNode.insertBefore(breadcrumb, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateBreadcrumb() {
|
||||
const breadcrumb = document.getElementById('channelBreadcrumb');
|
||||
if (!breadcrumb) return;
|
||||
const hash = window.location.hash.slice(1) || '';
|
||||
let moduleName = '频道';
|
||||
if (hash.startsWith('module-')) {
|
||||
const moduleId = hash.replace('module-', '');
|
||||
const moduleNames = {
|
||||
'M06': '工单管理',
|
||||
'M08': '数据看板',
|
||||
'M11': '组件库',
|
||||
'debug': '调试面板'
|
||||
};
|
||||
moduleName = moduleNames[moduleId] || moduleId;
|
||||
}
|
||||
breadcrumb.innerHTML = `
|
||||
<a href="#/">首页</a> >
|
||||
<a href="#/channel">频道</a> >
|
||||
<span class="current">${moduleName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function createShortcutHint() {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'shortcut-hint';
|
||||
hint.innerHTML = `
|
||||
<kbd>⌘K</kbd> 搜索 ·
|
||||
<kbd>↑</kbd><kbd>↓</kbd> 选择 ·
|
||||
<kbd>Enter</kbd> 打开 ·
|
||||
<kbd>Esc</kbd> 关闭
|
||||
`;
|
||||
document.body.appendChild(hint);
|
||||
}
|
||||
|
||||
function initShortcuts() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
const searchInput = window.__channelSearchInput;
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
const searchInput = window.__channelSearchInput;
|
||||
if (searchInput && document.activeElement === searchInput) {
|
||||
searchInput.value = '';
|
||||
filterModules('');
|
||||
searchInput.blur();
|
||||
} else if (searchInput && searchInput.value) {
|
||||
searchInput.value = '';
|
||||
filterModules('');
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
const cards = Array.from(document.querySelectorAll('.module-card:not([style*="display: none"])'));
|
||||
if (cards.length === 0) return;
|
||||
e.preventDefault();
|
||||
let selectedIndex = cards.findIndex(card => card.classList.contains('selected'));
|
||||
if (selectedIndex === -1) {
|
||||
selectedIndex = e.key === 'ArrowDown' ? 0 : cards.length - 1;
|
||||
} else {
|
||||
cards[selectedIndex].classList.remove('selected');
|
||||
if (e.key === 'ArrowDown') {
|
||||
selectedIndex = (selectedIndex + 1) % cards.length;
|
||||
} else {
|
||||
selectedIndex = (selectedIndex - 1 + cards.length) % cards.length;
|
||||
}
|
||||
}
|
||||
cards[selectedIndex].classList.add('selected');
|
||||
cards[selectedIndex].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
const selected = document.querySelector('.module-card.selected');
|
||||
if (selected) {
|
||||
selected.click(); // 触发我们绑定的点击事件
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ★★★ 核心修复:为所有卡片绑定点击切换模块 ★★★
|
||||
function bindCardClicks() {
|
||||
const grid = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.body;
|
||||
grid.addEventListener('click', function(e) {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (!card) return;
|
||||
|
||||
// 阻止可能冲突的其他事件(可选)
|
||||
e.preventDefault();
|
||||
|
||||
// 根据卡片文字判断是哪个模块
|
||||
const text = card.innerText || card.textContent;
|
||||
let moduleId = null;
|
||||
if (text.includes('工单管理')) moduleId = 'M06';
|
||||
else if (text.includes('数据统计')) moduleId = 'M08';
|
||||
else if (text.includes('组件库')) moduleId = 'M11';
|
||||
else if (text.includes('调试面板')) moduleId = 'debug';
|
||||
// 如果识别不到,尝试从其他属性获取(比如 data-module-id)
|
||||
else if (card.dataset.moduleId) moduleId = card.dataset.moduleId;
|
||||
|
||||
if (moduleId) {
|
||||
// 改变 hash,触发路由更新
|
||||
window.location.hash = `module-${moduleId}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
271
agents/zhuyuan-dev-agent/modules/m-channel/channel-favorites.js
Normal file
271
agents/zhuyuan-dev-agent/modules/m-channel/channel-favorites.js
Normal file
@ -0,0 +1,271 @@
|
||||
// 频道收藏与拖拽排序管理
|
||||
window.ChannelFavorites = {
|
||||
// 当前正在拖拽的元素
|
||||
draggingElement: null,
|
||||
|
||||
// 初始化
|
||||
init: function() {
|
||||
console.log('[favorites] 初始化');
|
||||
this.bindEvents();
|
||||
this.renderFavorites();
|
||||
},
|
||||
|
||||
// 绑定事件
|
||||
bindEvents: function() {
|
||||
// 使用事件委托监听星星点击
|
||||
document.addEventListener('click', (e) => {
|
||||
const star = e.target.closest('.favorite-star');
|
||||
if (star) {
|
||||
e.preventDefault();
|
||||
const card = star.closest('.module-card');
|
||||
if (card) {
|
||||
const moduleId = card.dataset.module;
|
||||
if (moduleId) {
|
||||
this.toggleFavorite(moduleId, star);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听偏好变化,重新渲染收藏状态
|
||||
if (window.EventBus) {
|
||||
EventBus.on('preferences:changed', (data) => {
|
||||
if (data.key === 'favorites' || data.full) {
|
||||
this.updateAllStars();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 切换收藏状态
|
||||
toggleFavorite: function(moduleId, starElement) {
|
||||
if (!window.ChannelPreferences) return;
|
||||
|
||||
const isNowFavorite = ChannelPreferences.toggleFavorite(moduleId);
|
||||
|
||||
// 更新星星样式
|
||||
if (starElement) {
|
||||
if (isNowFavorite) {
|
||||
starElement.classList.add('active');
|
||||
starElement.textContent = '★';
|
||||
} else {
|
||||
starElement.classList.remove('active');
|
||||
starElement.textContent = '☆';
|
||||
}
|
||||
}
|
||||
|
||||
// 触发布局更新(收藏置顶)
|
||||
this.updateFavoritesOrder();
|
||||
|
||||
// 发送事件
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('favorite:toggled', {
|
||||
module: moduleId,
|
||||
favorite: isNowFavorite
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 更新所有星星的显示状态
|
||||
updateAllStars: function() {
|
||||
const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : [];
|
||||
document.querySelectorAll('.favorite-star').forEach(star => {
|
||||
const card = star.closest('.module-card');
|
||||
if (card) {
|
||||
const moduleId = card.dataset.module;
|
||||
if (moduleId) {
|
||||
if (favorites.includes(moduleId)) {
|
||||
star.classList.add('active');
|
||||
star.textContent = '★';
|
||||
} else {
|
||||
star.classList.remove('active');
|
||||
star.textContent = '☆';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 根据收藏状态重新排序(收藏置顶)
|
||||
updateFavoritesOrder: function() {
|
||||
const container = document.querySelector('.channel-content');
|
||||
if (!container) return;
|
||||
|
||||
const cards = Array.from(container.querySelectorAll('.module-card'));
|
||||
const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : [];
|
||||
|
||||
// 按收藏状态和原有顺序排序
|
||||
cards.sort((a, b) => {
|
||||
const aId = a.dataset.module;
|
||||
const bId = b.dataset.module;
|
||||
const aFav = favorites.includes(aId);
|
||||
const bFav = favorites.includes(bId);
|
||||
|
||||
if (aFav && !bFav) return -1;
|
||||
if (!aFav && bFav) return 1;
|
||||
|
||||
// 如果都是收藏或都不是收藏,保持原有顺序
|
||||
const aOrder = cards.indexOf(a);
|
||||
const bOrder = cards.indexOf(b);
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
// 重新插入到容器中
|
||||
cards.forEach(card => container.appendChild(card));
|
||||
|
||||
// 保存排序到偏好设置
|
||||
const moduleOrder = cards.map(card => card.dataset.module);
|
||||
if (window.ChannelPreferences) {
|
||||
ChannelPreferences.setModuleOrder(moduleOrder);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染收藏状态(初始化时调用)
|
||||
renderFavorites: function() {
|
||||
this.updateAllStars();
|
||||
},
|
||||
|
||||
// ===== 拖拽排序相关 =====
|
||||
|
||||
// 初始化拖拽
|
||||
initDragAndDrop: function() {
|
||||
console.log('[favorites] 初始化拖拽排序');
|
||||
|
||||
const container = document.querySelector('.channel-content');
|
||||
if (!container) return;
|
||||
|
||||
// 为每个卡片添加拖拽手柄
|
||||
container.querySelectorAll('.module-card').forEach(card => {
|
||||
// 检查是否已有拖拽手柄
|
||||
if (!card.querySelector('.drag-handle')) {
|
||||
const header = card.querySelector('.module-card-header');
|
||||
if (header) {
|
||||
const handle = document.createElement('span');
|
||||
handle.className = 'drag-handle';
|
||||
handle.innerHTML = '⋮⋮';
|
||||
handle.setAttribute('draggable', 'false');
|
||||
header.insertBefore(handle, header.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 draggable
|
||||
card.setAttribute('draggable', 'true');
|
||||
|
||||
// 移除旧监听器,添加新监听器
|
||||
card.removeEventListener('dragstart', this.handleDragStart);
|
||||
card.removeEventListener('dragend', this.handleDragEnd);
|
||||
card.removeEventListener('dragover', this.handleDragOver);
|
||||
card.removeEventListener('dragenter', this.handleDragEnter);
|
||||
card.removeEventListener('dragleave', this.handleDragLeave);
|
||||
card.removeEventListener('drop', this.handleDrop);
|
||||
|
||||
card.addEventListener('dragstart', this.handleDragStart.bind(this));
|
||||
card.addEventListener('dragend', this.handleDragEnd.bind(this));
|
||||
card.addEventListener('dragover', this.handleDragOver);
|
||||
card.addEventListener('dragenter', this.handleDragEnter);
|
||||
card.addEventListener('dragleave', this.handleDragLeave);
|
||||
card.addEventListener('drop', this.handleDrop.bind(this));
|
||||
});
|
||||
},
|
||||
|
||||
// 拖拽开始
|
||||
handleDragStart: function(e) {
|
||||
this.draggingElement = e.target.closest('.module-card');
|
||||
if (!this.draggingElement) return;
|
||||
|
||||
e.dataTransfer.setData('text/plain', this.draggingElement.dataset.module);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
// 添加拖拽中的样式
|
||||
setTimeout(() => {
|
||||
this.draggingElement.classList.add('dragging');
|
||||
}, 0);
|
||||
},
|
||||
|
||||
// 拖拽结束
|
||||
handleDragEnd: function(e) {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (card) {
|
||||
card.classList.remove('dragging');
|
||||
}
|
||||
|
||||
// 移除所有高亮
|
||||
document.querySelectorAll('.module-card.drop-target').forEach(el => {
|
||||
el.classList.remove('drop-target');
|
||||
});
|
||||
|
||||
this.draggingElement = null;
|
||||
},
|
||||
|
||||
// 拖拽经过(必须阻止默认事件才能成为放置目标)
|
||||
handleDragOver: function(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
},
|
||||
|
||||
// 拖拽进入
|
||||
handleDragEnter: function(e) {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (card && card !== this.draggingElement) {
|
||||
card.classList.add('drop-target');
|
||||
}
|
||||
},
|
||||
|
||||
// 拖拽离开
|
||||
handleDragLeave: function(e) {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (card) {
|
||||
card.classList.remove('drop-target');
|
||||
}
|
||||
},
|
||||
|
||||
// 放置
|
||||
handleDrop: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetCard = e.target.closest('.module-card');
|
||||
if (!targetCard || !this.draggingElement || targetCard === this.draggingElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除高亮
|
||||
targetCard.classList.remove('drop-target');
|
||||
|
||||
// 获取所有卡片
|
||||
const container = document.querySelector('.channel-content');
|
||||
const cards = Array.from(container.querySelectorAll('.module-card'));
|
||||
|
||||
const fromIndex = cards.indexOf(this.draggingElement);
|
||||
const toIndex = cards.indexOf(targetCard);
|
||||
|
||||
if (fromIndex === -1 || toIndex === -1) return;
|
||||
|
||||
// 重新排序
|
||||
if (window.ChannelPreferences) {
|
||||
ChannelPreferences.reorderModules(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
// 移动 DOM 元素
|
||||
if (fromIndex < toIndex) {
|
||||
targetCard.insertAdjacentElement('afterend', this.draggingElement);
|
||||
} else {
|
||||
targetCard.insertAdjacentElement('beforebegin', this.draggingElement);
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('favorites:reordered', {
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
module: this.draggingElement.dataset.module
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新拖拽功能(在布局变化后调用)
|
||||
refreshDragAndDrop: function() {
|
||||
this.initDragAndDrop();
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[favorites] 已加载');
|
||||
145
agents/zhuyuan-dev-agent/modules/m-channel/channel-layout.css
Normal file
145
agents/zhuyuan-dev-agent/modules/m-channel/channel-layout.css
Normal file
@ -0,0 +1,145 @@
|
||||
/* 频道布局样式 - 三种布局模式 */
|
||||
|
||||
/* 基础卡片样式(所有布局共用) */
|
||||
.module-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.module-card-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.module-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 收藏星星 */
|
||||
.favorite-star {
|
||||
font-size: 20px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.favorite-star.active {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.favorite-star:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 卡片内容区 */
|
||||
.module-card-content {
|
||||
padding: 16px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* 拖拽把手 */
|
||||
.drag-handle {
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* ===== 布局1:网格模式(默认) ===== */
|
||||
.layout-grid .channel-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.layout-grid .module-card {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
/* ===== 布局2:列表模式(一行一个) ===== */
|
||||
.layout-list .channel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.layout-list .module-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-list .module-card-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.layout-list .module-card-content {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* ===== 布局3:紧凑模式(小卡片密排) ===== */
|
||||
.layout-compact .channel-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.layout-compact .module-card {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.layout-compact .module-card-header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.layout-compact .module-card-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layout-compact .module-card-content {
|
||||
padding: 10px 12px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* 布局切换动画 */
|
||||
.channel-content {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 拖拽中的样式 */
|
||||
.module-card.dragging {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 拖拽放置区高亮 */
|
||||
.module-card.drop-target {
|
||||
border: 2px dashed #3b82f6;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
/* 频道通知系统样式 - 环节7 */
|
||||
:root {
|
||||
--update-badge-bg: #2196f3;
|
||||
--unread-dot-bg: #f44336;
|
||||
--notification-panel-bg: #ffffff;
|
||||
--notification-panel-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
--notification-hover-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 更新标签(蓝色) */
|
||||
.update-badge {
|
||||
display: inline-block;
|
||||
background-color: var(--update-badge-bg);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 未读小红点(带数字) */
|
||||
.unread-dot {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
background-color: var(--unread-dot-bg);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 铃铛图标容器 */
|
||||
.bell-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bell-icon {
|
||||
font-size: 24px;
|
||||
color: #555;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.bell-icon:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 铃铛上的数字气泡 */
|
||||
.bell-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -6px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
background-color: var(--unread-dot-bg);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* 侧滑通知面板 */
|
||||
.notification-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
width: 360px;
|
||||
height: 100vh;
|
||||
background: var(--notification-panel-bg);
|
||||
box-shadow: var(--notification-panel-shadow);
|
||||
transition: right 0.3s ease;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.notification-panel.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-header button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2196f3;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel-header button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background: var(--notification-hover-bg);
|
||||
}
|
||||
|
||||
.notification-item.read {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-module {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-summary {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 遮罩层 */
|
||||
.panel-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.3);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-overlay.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 让模块卡片成为相对定位容器(用于小红点定位) */
|
||||
.module-card {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* 强制修复高亮点击问题 */
|
||||
.search-highlight,
|
||||
.highlight,
|
||||
[class*="highlight"] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* 确保卡片本身可点击 */
|
||||
.module-card,
|
||||
[class*="module-card"] {
|
||||
cursor: pointer;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 频道通知系统 - 环节7 (晨星陪伴版)
|
||||
* 功能:模块更新提醒、未读小红点、通知面板、高亮点击修复
|
||||
* 修改:通过卡片文字内容匹配模块ID(解决moduleId undefined问题)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
// ========== 初始化模拟数据 ==========
|
||||
const STORAGE_KEY = 'channel_notifications';
|
||||
|
||||
// 默认模拟数据(基于已完成模块 M06, M08, M11, 以及当前频道模块)
|
||||
const DEFAULT_UPDATES = {
|
||||
'M06': {
|
||||
hasUpdate: true,
|
||||
count: 2,
|
||||
updates: [
|
||||
{ time: '10:30', summary: '修复了拖拽排序的bug' },
|
||||
{ time: '昨天', summary: '新增统计面板' }
|
||||
]
|
||||
},
|
||||
'M08': {
|
||||
hasUpdate: true,
|
||||
count: 1,
|
||||
updates: [
|
||||
{ time: '昨天', summary: '优化了模块加载性能' }
|
||||
]
|
||||
},
|
||||
'M11': {
|
||||
hasUpdate: true,
|
||||
count: 3,
|
||||
updates: [
|
||||
{ time: '15:20', summary: '新增键盘快捷键' },
|
||||
{ time: '昨天', summary: '修复焦点管理' },
|
||||
{ time: '3月10日', summary: '模块生命周期完善' }
|
||||
]
|
||||
},
|
||||
'channel': {
|
||||
hasUpdate: true,
|
||||
count: 1,
|
||||
updates: [
|
||||
{ time: '现在', summary: '通知系统上线啦!' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// 加载或初始化数据
|
||||
let notificationData = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||
if (!notificationData) {
|
||||
notificationData = DEFAULT_UPDATES;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
|
||||
}
|
||||
|
||||
// ========== 工具函数:从卡片文字猜测模块ID ==========
|
||||
function guessModuleIdFromCard(card) {
|
||||
// 尝试找卡片里的标题文字
|
||||
const titleElem = card.querySelector('h3, .module-title, .card-title, .module-name');
|
||||
if (titleElem) {
|
||||
const text = titleElem.textContent.trim();
|
||||
// 简单映射:如果文字包含“工单” -> M06
|
||||
if (text.includes('工单')) return 'M06';
|
||||
if (text.includes('数据统计')) return 'M08';
|
||||
if (text.includes('组件库')) return 'M11';
|
||||
if (text.includes('调试面板')) return 'M11'; // 暂时用 M11
|
||||
}
|
||||
|
||||
// 后备:用卡片内所有文字尝试匹配
|
||||
const fullText = card.textContent;
|
||||
if (fullText.includes('工单')) return 'M06';
|
||||
if (fullText.includes('数据统计')) return 'M08';
|
||||
if (fullText.includes('组件库')) return 'M11';
|
||||
|
||||
return null; // 实在猜不到就返回 null
|
||||
}
|
||||
|
||||
function getModuleCards() {
|
||||
return document.querySelectorAll('.module-card');
|
||||
}
|
||||
|
||||
// ========== 更新标签渲染(带日志) ==========
|
||||
function renderUpdateBadges() {
|
||||
console.log('🟦 renderUpdateBadges 开始执行');
|
||||
const cards = document.querySelectorAll('.module-card');
|
||||
console.log('找到卡片数量:', cards.length);
|
||||
|
||||
cards.forEach(card => {
|
||||
// 先尝试原有方法,失败则用猜测
|
||||
let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
|
||||
if (!moduleId) {
|
||||
moduleId = guessModuleIdFromCard(card);
|
||||
}
|
||||
console.log('卡片最终 moduleId:', moduleId);
|
||||
|
||||
// 移除旧的标签
|
||||
const oldBadge = card.querySelector('.update-badge');
|
||||
if (oldBadge) oldBadge.remove();
|
||||
|
||||
const modData = moduleId ? notificationData[moduleId] : null;
|
||||
console.log('模块数据:', moduleId, modData);
|
||||
|
||||
if (modData && modData.hasUpdate) {
|
||||
console.log('✅ 应该添加标签的模块:', moduleId);
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'update-badge';
|
||||
badge.textContent = '有更新';
|
||||
// 内联样式保证可见
|
||||
badge.style.backgroundColor = '#2196f3';
|
||||
badge.style.color = 'white';
|
||||
badge.style.padding = '2px 8px';
|
||||
badge.style.borderRadius = '12px';
|
||||
badge.style.fontSize = '12px';
|
||||
badge.style.display = 'inline-block';
|
||||
badge.style.marginTop = '8px';
|
||||
// 直接追加到卡片末尾
|
||||
card.appendChild(badge);
|
||||
console.log('✅ 标签已追加到卡片', moduleId);
|
||||
} else {
|
||||
console.log('❌ 不需要添加标签的模块:', moduleId);
|
||||
}
|
||||
});
|
||||
console.log('🟦 renderUpdateBadges 执行完毕');
|
||||
}
|
||||
|
||||
// ========== 小红点渲染 ==========
|
||||
function renderUnreadDots() {
|
||||
const cards = getModuleCards();
|
||||
cards.forEach(card => {
|
||||
let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
|
||||
if (!moduleId) {
|
||||
moduleId = guessModuleIdFromCard(card);
|
||||
}
|
||||
if (!moduleId) return;
|
||||
|
||||
const oldDot = card.querySelector('.unread-dot');
|
||||
if (oldDot) oldDot.remove();
|
||||
|
||||
const modData = notificationData[moduleId];
|
||||
if (modData && modData.hasUpdate && modData.count > 0) {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'unread-dot';
|
||||
dot.textContent = modData.count > 9 ? '9+' : modData.count;
|
||||
card.appendChild(dot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 标记模块已读 ==========
|
||||
function markModuleAsRead(moduleId) {
|
||||
if (notificationData[moduleId]) {
|
||||
notificationData[moduleId].hasUpdate = false;
|
||||
notificationData[moduleId].count = 0;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
|
||||
renderUpdateBadges();
|
||||
renderUnreadDots();
|
||||
updateBellBadge();
|
||||
renderNotificationPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 铃铛相关 ==========
|
||||
let bellContainer, panel, overlay;
|
||||
let isPanelOpen = false;
|
||||
|
||||
function createBell() {
|
||||
const header = document.querySelector('.channel-header') || document.querySelector('header') || document.body;
|
||||
bellContainer = document.createElement('div');
|
||||
bellContainer.className = 'bell-container';
|
||||
bellContainer.innerHTML = `
|
||||
<span class="bell-icon">🔔</span>
|
||||
<span class="bell-badge" style="display: none;">0</span>
|
||||
`;
|
||||
const title = header.querySelector('h1, h2');
|
||||
if (title) {
|
||||
title.insertAdjacentElement('afterend', bellContainer);
|
||||
} else {
|
||||
header.appendChild(bellContainer);
|
||||
}
|
||||
|
||||
panel = document.createElement('div');
|
||||
panel.className = 'notification-panel';
|
||||
panel.innerHTML = `
|
||||
<div class="panel-header">
|
||||
<span>通知中心</span>
|
||||
<button class="mark-all-read">全部已读</button>
|
||||
</div>
|
||||
<ul class="notification-list"></ul>
|
||||
`;
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'panel-overlay';
|
||||
document.body.appendChild(panel);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
bellContainer.addEventListener('click', togglePanel);
|
||||
overlay.addEventListener('click', closePanel);
|
||||
panel.querySelector('.mark-all-read').addEventListener('click', markAllRead);
|
||||
}
|
||||
|
||||
function updateBellBadge() {
|
||||
const totalUnread = Object.values(notificationData).reduce((acc, mod) => acc + (mod.count || 0), 0);
|
||||
const badge = bellContainer?.querySelector('.bell-badge');
|
||||
if (badge) {
|
||||
if (totalUnread > 0) {
|
||||
badge.style.display = 'flex';
|
||||
badge.textContent = totalUnread > 9 ? '9+' : totalUnread;
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanel(e) {
|
||||
e.stopPropagation();
|
||||
isPanelOpen ? closePanel() : openPanel();
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
isPanelOpen = true;
|
||||
panel.classList.add('open');
|
||||
overlay.classList.add('show');
|
||||
renderNotificationPanel();
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
isPanelOpen = false;
|
||||
panel.classList.remove('open');
|
||||
overlay.classList.remove('show');
|
||||
}
|
||||
|
||||
function renderNotificationPanel() {
|
||||
const list = panel.querySelector('.notification-list');
|
||||
if (!list) return;
|
||||
|
||||
let items = [];
|
||||
for (const [moduleId, modData] of Object.entries(notificationData)) {
|
||||
if (modData.updates && modData.updates.length > 0) {
|
||||
modData.updates.forEach((update) => {
|
||||
items.push({
|
||||
moduleId,
|
||||
time: update.time,
|
||||
summary: update.summary,
|
||||
read: !modData.hasUpdate
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<li class="empty-state">暂无新通知</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.sort((a, b) => (a.time > b.time ? -1 : 1));
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<li class="notification-item ${item.read ? 'read' : ''}" data-module="${item.moduleId}">
|
||||
<div class="notification-time">${item.time}</div>
|
||||
<div class="notification-module">${item.moduleId}</div>
|
||||
<div class="notification-summary">${item.summary}</div>
|
||||
</li>
|
||||
`).join('');
|
||||
|
||||
list.querySelectorAll('.notification-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const moduleId = item.dataset.module;
|
||||
const card = findCardByModuleId(moduleId);
|
||||
if (card) {
|
||||
card.click();
|
||||
}
|
||||
markModuleAsRead(moduleId);
|
||||
closePanel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findCardByModuleId(moduleId) {
|
||||
const cards = getModuleCards();
|
||||
for (let card of cards) {
|
||||
let id = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
|
||||
if (!id) id = guessModuleIdFromCard(card);
|
||||
if (id === moduleId) return card;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
for (let moduleId in notificationData) {
|
||||
notificationData[moduleId].hasUpdate = false;
|
||||
notificationData[moduleId].count = 0;
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
|
||||
renderUpdateBadges();
|
||||
renderUnreadDots();
|
||||
updateBellBadge();
|
||||
renderNotificationPanel();
|
||||
}
|
||||
|
||||
function fixHighlightClick() {
|
||||
const container = document.querySelector('.modules-grid') || document.body;
|
||||
container.addEventListener('click', (e) => {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (card) {
|
||||
// 不做额外处理,只是确保事件冒泡
|
||||
}
|
||||
}, true);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.search-highlight {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.module-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!document.querySelector('link[href*="channel-notifications.css"]')) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'channel-notifications.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
renderUpdateBadges();
|
||||
renderUnreadDots();
|
||||
|
||||
createBell();
|
||||
updateBellBadge();
|
||||
|
||||
fixHighlightClick();
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const card = e.target.closest('.module-card');
|
||||
if (card) {
|
||||
let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
|
||||
if (!moduleId) moduleId = guessModuleIdFromCard(card);
|
||||
if (moduleId) {
|
||||
setTimeout(() => markModuleAsRead(moduleId), 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('modulesRendered', () => {
|
||||
renderUpdateBadges();
|
||||
renderUnreadDots();
|
||||
});
|
||||
|
||||
let lastCardCount = 0;
|
||||
setInterval(() => {
|
||||
const cards = getModuleCards();
|
||||
if (cards.length !== lastCardCount) {
|
||||
lastCardCount = cards.length;
|
||||
renderUpdateBadges();
|
||||
renderUnreadDots();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@ -0,0 +1,194 @@
|
||||
// 频道偏好管理 - 记住用户的装修设置
|
||||
window.ChannelPreferences = {
|
||||
STORAGE_KEY: 'hololake_channel_preferences',
|
||||
|
||||
// 默认配置
|
||||
defaults: {
|
||||
layout: 'grid', // grid, list, compact
|
||||
theme: 'default', // default, ocean, forest, sunset, lavender
|
||||
favorites: [], // 收藏的模块ID列表
|
||||
moduleOrder: [], // 模块排序顺序(默认按ID)
|
||||
stats: {} // 使用统计
|
||||
},
|
||||
|
||||
// 当前配置
|
||||
config: null,
|
||||
|
||||
// 初始化(加载配置)
|
||||
init: function() {
|
||||
console.log('[preferences] 初始化');
|
||||
this.load();
|
||||
return this.config;
|
||||
},
|
||||
|
||||
// 加载配置
|
||||
load: function() {
|
||||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
this.config = JSON.parse(saved);
|
||||
console.log('[preferences] 加载配置:', this.config);
|
||||
} catch (e) {
|
||||
console.error('[preferences] 解析失败,使用默认配置', e);
|
||||
this.config = { ...this.defaults };
|
||||
}
|
||||
} else {
|
||||
console.log('[preferences] 无保存配置,使用默认');
|
||||
this.config = { ...this.defaults };
|
||||
}
|
||||
return this.config;
|
||||
},
|
||||
|
||||
// 保存配置
|
||||
save: function() {
|
||||
if (!this.config) return;
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.config));
|
||||
console.log('[preferences] 保存配置:', this.config);
|
||||
},
|
||||
|
||||
// 获取指定键的值
|
||||
get: function(key) {
|
||||
if (!this.config) this.load();
|
||||
return this.config[key];
|
||||
},
|
||||
|
||||
// 设置指定键的值(自动保存)
|
||||
set: function(key, value) {
|
||||
if (!this.config) this.load();
|
||||
this.config[key] = value;
|
||||
this.save();
|
||||
|
||||
// 触发事件总线通知
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('preferences:changed', { key, value });
|
||||
}
|
||||
},
|
||||
|
||||
// 更新整个配置(合并)
|
||||
update: function(newConfig) {
|
||||
if (!this.config) this.load();
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
this.save();
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('preferences:changed', { full: this.config });
|
||||
}
|
||||
},
|
||||
|
||||
// 恢复默认设置
|
||||
reset: function() {
|
||||
this.config = { ...this.defaults };
|
||||
this.save();
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('preferences:reset', this.config);
|
||||
}
|
||||
console.log('[preferences] 恢复默认');
|
||||
},
|
||||
|
||||
// 记录模块使用(用于统计)
|
||||
recordUsage: function(moduleId) {
|
||||
if (!this.config) this.load();
|
||||
if (!this.config.stats) this.config.stats = {};
|
||||
if (!this.config.stats[moduleId]) {
|
||||
this.config.stats[moduleId] = {
|
||||
count: 0,
|
||||
lastUsed: null,
|
||||
totalTime: 0
|
||||
};
|
||||
}
|
||||
this.config.stats[moduleId].count++;
|
||||
this.config.stats[moduleId].lastUsed = Date.now();
|
||||
this.save();
|
||||
|
||||
// 开始计时(将在模块卸载时记录时长)
|
||||
this.startTiming(moduleId);
|
||||
},
|
||||
|
||||
// 计时相关
|
||||
timing: {},
|
||||
startTiming: function(moduleId) {
|
||||
this.timing[moduleId] = Date.now();
|
||||
},
|
||||
stopTiming: function(moduleId) {
|
||||
if (this.timing[moduleId]) {
|
||||
const duration = (Date.now() - this.timing[moduleId]) / 1000; // 秒
|
||||
if (this.config.stats[moduleId]) {
|
||||
this.config.stats[moduleId].totalTime += duration;
|
||||
this.save();
|
||||
}
|
||||
delete this.timing[moduleId];
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计报告
|
||||
getStats: function() {
|
||||
if (!this.config) this.load();
|
||||
return this.config.stats || {};
|
||||
},
|
||||
|
||||
// 获取收藏列表
|
||||
getFavorites: function() {
|
||||
return this.get('favorites') || [];
|
||||
},
|
||||
|
||||
// 切换收藏状态
|
||||
toggleFavorite: function(moduleId) {
|
||||
let favorites = this.get('favorites') || [];
|
||||
if (favorites.includes(moduleId)) {
|
||||
favorites = favorites.filter(id => id !== moduleId);
|
||||
} else {
|
||||
favorites.push(moduleId);
|
||||
}
|
||||
this.set('favorites', favorites);
|
||||
return favorites.includes(moduleId);
|
||||
},
|
||||
|
||||
// 获取布局
|
||||
getLayout: function() {
|
||||
return this.get('layout') || 'grid';
|
||||
},
|
||||
|
||||
// 设置布局
|
||||
setLayout: function(layout) {
|
||||
this.set('layout', layout);
|
||||
},
|
||||
|
||||
// 获取主题
|
||||
getTheme: function() {
|
||||
return this.get('theme') || 'default';
|
||||
},
|
||||
|
||||
// 设置主题
|
||||
setTheme: function(theme) {
|
||||
this.set('theme', theme);
|
||||
},
|
||||
|
||||
// 获取模块排序
|
||||
getModuleOrder: function() {
|
||||
return this.get('moduleOrder') || [];
|
||||
},
|
||||
|
||||
// 设置模块排序
|
||||
setModuleOrder: function(order) {
|
||||
this.set('moduleOrder', order);
|
||||
},
|
||||
|
||||
// 重新排序(拖拽后调用)
|
||||
reorderModules: function(fromIndex, toIndex) {
|
||||
const order = this.getModuleOrder();
|
||||
if (order.length === 0) {
|
||||
// 如果还没有自定义顺序,则基于当前模块列表生成
|
||||
const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
|
||||
// 排除非显示模块(如调试面板)
|
||||
const displayModules = modules.filter(m => ['m06','m08','m11','home','debug'].includes(m));
|
||||
this.setModuleOrder(displayModules);
|
||||
}
|
||||
// 重新加载最新顺序
|
||||
const currentOrder = this.getModuleOrder();
|
||||
if (fromIndex < 0 || fromIndex >= currentOrder.length || toIndex < 0 || toIndex >= currentOrder.length) return;
|
||||
const [moved] = currentOrder.splice(fromIndex, 1);
|
||||
currentOrder.splice(toIndex, 0, moved);
|
||||
this.setModuleOrder(currentOrder);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[preferences] 已加载');
|
||||
243
agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js
Normal file
243
agents/zhuyuan-dev-agent/modules/m-channel/channel-router.js
Normal file
@ -0,0 +1,243 @@
|
||||
// 频道路由(支持真实模块适配器 + 卡片列表首页 + 数据采集钩子)
|
||||
window.ChannelRouter = {
|
||||
currentChannel: 'home',
|
||||
contentEl: null,
|
||||
|
||||
init: function(contentElement) {
|
||||
this.contentEl = contentElement;
|
||||
console.log('[router] 初始化');
|
||||
|
||||
const savedState = ChannelState.restoreState();
|
||||
if (savedState && savedState.currentChannel) {
|
||||
this.currentChannel = savedState.currentChannel;
|
||||
}
|
||||
|
||||
this.loadChannel(this.currentChannel);
|
||||
|
||||
if (savedState && savedState.visited) {
|
||||
savedState.visited.forEach(channel => {
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
if (btn.dataset.channel === channel) {
|
||||
btn.classList.add('visited');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 页面关闭时结束计时
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (typeof ChannelAnalytics !== 'undefined') {
|
||||
ChannelAnalytics.endSession();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
navigateTo: function(channel) {
|
||||
if (channel === this.currentChannel) return;
|
||||
|
||||
console.log(`[router] 导航到: ${channel}`);
|
||||
|
||||
this.contentEl.classList.add('fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
this.loadChannel(channel);
|
||||
this.contentEl.classList.remove('fade-out');
|
||||
this.contentEl.classList.add('fade-in');
|
||||
setTimeout(() => {
|
||||
this.contentEl.classList.remove('fade-in');
|
||||
}, 300);
|
||||
}, 300);
|
||||
|
||||
this.currentChannel = channel;
|
||||
|
||||
document.querySelectorAll('.channel-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.channel === channel) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
ChannelState.saveState({
|
||||
currentChannel: channel,
|
||||
visited: this.getVisitedChannels()
|
||||
});
|
||||
},
|
||||
|
||||
loadChannel: function(channel) {
|
||||
if (!this.contentEl) return;
|
||||
|
||||
// 数据采集钩子:开始加载
|
||||
if (typeof ChannelAnalytics !== 'undefined') {
|
||||
ChannelAnalytics.markLoadStart();
|
||||
if (channel !== 'home' && channel !== 'debug' && channel !== 'dashboard' && channel !== 'settings') {
|
||||
ChannelAnalytics.recordVisit(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是真实模块(m06/m08/m11)
|
||||
if (channel === 'm06' || channel === 'm08' || channel === 'm11') {
|
||||
if (window.ModuleAdapter) {
|
||||
ModuleAdapter.loadModule(channel, 'channel-content');
|
||||
// 模块加载完成后记录加载时间
|
||||
setTimeout(() => {
|
||||
if (typeof ChannelAnalytics !== 'undefined') {
|
||||
ChannelAnalytics.markLoadEnd(channel);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.contentEl.innerHTML = '<div class="error-boundary">适配器未加载</div>';
|
||||
}
|
||||
ModuleLifecycle.onLoad(channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据面板路由(环节8新增)
|
||||
if (channel === 'dashboard') {
|
||||
fetch('views/channel-dashboard.html')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.contentEl.innerHTML = html;
|
||||
// 渲染图表
|
||||
if (typeof ChannelDashboard !== 'undefined') {
|
||||
ChannelDashboard.render();
|
||||
}
|
||||
ModuleLifecycle.onLoad('dashboard');
|
||||
// 记录加载完成
|
||||
if (typeof ChannelAnalytics !== 'undefined') {
|
||||
ChannelAnalytics.markLoadEnd('dashboard');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.contentEl.innerHTML = '<h2>数据面板加载失败</h2>';
|
||||
console.error(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置中心路由(环节9新增)
|
||||
if (channel === 'settings') {
|
||||
fetch('views/channel-settings.html')
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
this.contentEl.innerHTML = html;
|
||||
if (typeof SettingsUI !== 'undefined') {
|
||||
SettingsUI.init();
|
||||
}
|
||||
// 记录加载完成
|
||||
if (typeof ChannelAnalytics !== 'undefined') {
|
||||
ChannelAnalytics.markLoadEnd('settings');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.contentEl.innerHTML = '<h2>设置面板加载失败</h2>';
|
||||
console.error(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 内置频道
|
||||
if (channel === 'home') {
|
||||
this.renderModuleCards();
|
||||
} else if (channel === 'debug') {
|
||||
this.loadDebugPanel();
|
||||
} else {
|
||||
this.contentEl.innerHTML = '<h2>未知频道</h2>';
|
||||
}
|
||||
|
||||
// 对于内置频道也记录加载完成
|
||||
if (typeof ChannelAnalytics !== 'undefined' && channel !== 'dashboard' && channel !== 'settings') {
|
||||
ChannelAnalytics.markLoadEnd(channel);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染所有模块卡片(用于首页)
|
||||
renderModuleCards: function() {
|
||||
const modules = [
|
||||
{ id: 'm06', name: '工单管理', icon: '📋', desc: '管理任务和工单' },
|
||||
{ id: 'm08', name: '数据统计', icon: '📊', desc: '查看数据统计和报表' },
|
||||
{ id: 'm11', name: '组件库', icon: '🧩', desc: '系统组件展示' },
|
||||
{ id: 'debug', name: '调试面板', icon: '🐞', desc: '查看事件和模块状态' },
|
||||
{ id: 'dashboard', name: '数据面板', icon: '📈', desc: '查看使用统计和性能' }
|
||||
];
|
||||
|
||||
let html = '<div class="module-grid">';
|
||||
modules.forEach(mod => {
|
||||
const isFavorite = window.ChannelPreferences ?
|
||||
ChannelPreferences.getFavorites().includes(mod.id) : false;
|
||||
html += `
|
||||
<div class="module-card" data-module="${mod.id}">
|
||||
<div class="module-card-header">
|
||||
<span class="drag-handle">⋮⋮</span>
|
||||
<span class="module-card-title">${mod.icon} ${mod.name}</span>
|
||||
<span class="favorite-star ${isFavorite ? 'active' : ''}">${isFavorite ? '★' : '☆'}</span>
|
||||
</div>
|
||||
<div class="module-card-content">
|
||||
<p>${mod.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
this.contentEl.innerHTML = html;
|
||||
ModuleLifecycle.onLoad('home');
|
||||
|
||||
// 触发收藏/拖拽重新初始化
|
||||
if (window.ChannelFavorites) {
|
||||
setTimeout(() => {
|
||||
ChannelFavorites.init();
|
||||
ChannelFavorites.initDragAndDrop();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
loadDebugPanel: function() {
|
||||
fetch('views/channel-debug.html')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.contentEl.innerHTML = html;
|
||||
ModuleLifecycle.onLoad('debug');
|
||||
this.initDebugPanel();
|
||||
})
|
||||
.catch(err => {
|
||||
this.contentEl.innerHTML = '<h2>调试面板加载失败</h2>';
|
||||
});
|
||||
},
|
||||
|
||||
initDebugPanel: function() {
|
||||
const msgList = document.getElementById('debug-messages');
|
||||
if (msgList && window.debugMessages) {
|
||||
window.debugMessages.forEach(msg => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `[${msg.time}] ${msg.event}: ${JSON.stringify(msg.data)}`;
|
||||
msgList.appendChild(li);
|
||||
});
|
||||
}
|
||||
const sendBtn = document.getElementById('debug-send');
|
||||
const input = document.getElementById('debug-input');
|
||||
if (sendBtn && input) {
|
||||
sendBtn.addEventListener('click', () => {
|
||||
const text = input.value.trim();
|
||||
if (text) {
|
||||
EventBus.emit('debug:message', { text, from: '调试面板' });
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
const clearBtn = document.getElementById('debug-clear');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
ChannelState.clearState();
|
||||
alert('状态已清除,刷新页面生效');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getVisitedChannels: function() {
|
||||
const visited = [];
|
||||
document.querySelectorAll('.channel-btn.visited').forEach(btn => {
|
||||
visited.push(btn.dataset.channel);
|
||||
});
|
||||
return visited;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* channel-settings-ui.js
|
||||
* 设置面板UI控制器·环节9
|
||||
*/
|
||||
var SettingsUI = (function() {
|
||||
// 渲染主题卡片
|
||||
function renderThemeCards() {
|
||||
var container = document.getElementById('themeCards');
|
||||
if (!container) return;
|
||||
var current = ChannelSettings.get('theme');
|
||||
var themes = ChannelSettings.THEMES;
|
||||
var html = '';
|
||||
Object.keys(themes).forEach(function(id) {
|
||||
var t = themes[id];
|
||||
var active = (id === current) ? 'active' : '';
|
||||
html += '<div class="theme-card ' + active + '" ' +
|
||||
'style="background:' + t.cardBg + '; color:' + t.text + ';" ' +
|
||||
'onclick="SettingsUI.selectTheme(\'' + id + '\')">' +
|
||||
'<div class="theme-preview" style="background:' + t.bg + ';"></div>' +
|
||||
t.name + '</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 同步UI状态
|
||||
function syncUI() {
|
||||
var s = ChannelSettings.get();
|
||||
|
||||
// 下拉框
|
||||
var fontSize = document.getElementById('settingFontSize');
|
||||
if (fontSize) fontSize.value = s.fontSize;
|
||||
|
||||
var interval = document.getElementById('settingRefreshInterval');
|
||||
if (interval) interval.value = s.refreshInterval;
|
||||
|
||||
// 开关
|
||||
var toggleMap = {
|
||||
settingAnimation: 'animationEnabled',
|
||||
settingSidebar: 'sidebarCollapsed',
|
||||
settingNotifyModule: 'notifyModuleUpdate',
|
||||
settingNotifySystem: 'notifySystem',
|
||||
settingNotifySound: 'notifySound',
|
||||
settingAnalytics: 'analyticsEnabled',
|
||||
settingAutoRefresh: 'autoRefresh'
|
||||
};
|
||||
Object.keys(toggleMap).forEach(function(elId) {
|
||||
var el = document.getElementById(elId);
|
||||
if (el) el.checked = !!s[toggleMap[elId]];
|
||||
});
|
||||
|
||||
renderThemeCards();
|
||||
}
|
||||
|
||||
return {
|
||||
// 初始化面板
|
||||
init: function() {
|
||||
syncUI();
|
||||
console.log('⚙️ 设置面板已加载');
|
||||
},
|
||||
// 选择主题
|
||||
selectTheme: function(themeId) {
|
||||
ChannelSettings.set('theme', themeId);
|
||||
renderThemeCards();
|
||||
},
|
||||
// 开关变更
|
||||
onToggle: function(key, value) {
|
||||
ChannelSettings.set(key, value);
|
||||
console.log('⚙️ ' + key + ' = ' + value);
|
||||
},
|
||||
// 下拉变更
|
||||
onChange: function(key, value) {
|
||||
ChannelSettings.set(key, value);
|
||||
console.log('⚙️ ' + key + ' = ' + value);
|
||||
},
|
||||
// 导出
|
||||
exportSettings: function() {
|
||||
var json = ChannelSettings.exportJSON();
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(json).then(function() {
|
||||
alert('✅ 设置已复制到剪贴板!\n\n可以保存为文件或发给其他设备。');
|
||||
});
|
||||
} else {
|
||||
// 降级:显示在弹窗
|
||||
prompt('复制下面的内容:', json);
|
||||
}
|
||||
console.log('⚙️ 设置已导出');
|
||||
},
|
||||
// 显示导入弹窗
|
||||
showImport: function() {
|
||||
var modal = document.getElementById('importModal');
|
||||
if (modal) modal.classList.add('show');
|
||||
},
|
||||
// 隐藏导入弹窗
|
||||
hideImport: function() {
|
||||
var modal = document.getElementById('importModal');
|
||||
if (modal) modal.classList.remove('show');
|
||||
},
|
||||
// 执行导入
|
||||
doImport: function() {
|
||||
var textarea = document.getElementById('importTextarea');
|
||||
if (!textarea || !textarea.value.trim()) {
|
||||
alert('请粘贴JSON内容');
|
||||
return;
|
||||
}
|
||||
var ok = ChannelSettings.importJSON(textarea.value.trim());
|
||||
if (ok) {
|
||||
alert('✅ 设置导入成功!');
|
||||
this.hideImport();
|
||||
syncUI();
|
||||
} else {
|
||||
alert('❌ JSON格式错误,请检查内容');
|
||||
}
|
||||
},
|
||||
// 恢复默认
|
||||
resetAll: function() {
|
||||
if (confirm('确定恢复所有设置为默认值吗?')) {
|
||||
ChannelSettings.reset();
|
||||
syncUI();
|
||||
alert('✅ 已恢复默认设置');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
258
agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css
Normal file
258
agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.css
Normal file
@ -0,0 +1,258 @@
|
||||
/**
|
||||
* channel-settings.css
|
||||
* 频道设置面板样式 · 环节9
|
||||
*/
|
||||
.settings-container {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
color: var(--text-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
font-size: 24px;
|
||||
color: var(--accent-color, #4fc3f7);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 设置分组 */
|
||||
.settings-group {
|
||||
background: var(--card-bg, #1a1a2e);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
}
|
||||
|
||||
.settings-group h3 {
|
||||
font-size: 16px;
|
||||
color: var(--accent-color, #4fc3f7);
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color, #2a2a4a);
|
||||
}
|
||||
|
||||
/* 设置行 */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.setting-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-label .title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-label .desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 开关组件 */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #555;
|
||||
transition: .3s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: white;
|
||||
transition: .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background: var(--accent-color, #4fc3f7);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* 下拉选择 */
|
||||
.setting-select {
|
||||
background: var(--card-bg, #1a1a2e);
|
||||
color: var(--text-color, #e0e0e0);
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* 主题预览卡片 */
|
||||
.theme-cards {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
width: 120px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.theme-card.active {
|
||||
border-color: var(--accent-color, #4fc3f7);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 操作按钮组 */
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-settings {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-settings:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: #66bb6a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-import {
|
||||
background: #42a5f5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #ff7043;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 导入弹窗 */
|
||||
.import-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.import-modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.import-dialog {
|
||||
background: var(--card-bg, #1a1a2e);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.import-textarea {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background: #111;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 600px) {
|
||||
.theme-cards {
|
||||
justify-content: center;
|
||||
}
|
||||
.settings-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
/* 无动画模式 */
|
||||
.no-animation * {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
/* 字体大小全局应用 */
|
||||
body, .channel-btn, .module-card, .channel-content, .nav-item,
|
||||
p, span, div, h1, h2, h3, h4, a, button, .setting-row, .setting-label {
|
||||
font-size: var(--base-font-size, 15px);
|
||||
}
|
||||
180
agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js
Normal file
180
agents/zhuyuan-dev-agent/modules/m-channel/channel-settings.js
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* channel-settings.js
|
||||
* 频道设置数据管理核心·环节9
|
||||
* 统一管理所有用户偏好:主题、布局、通知、显示
|
||||
*/
|
||||
const ChannelSettings = (function(){
|
||||
const STORAGE_KEY = 'channel-user-settings';
|
||||
|
||||
// 默认设置
|
||||
const DEFAULTS = {
|
||||
theme: 'dark', // dark / light / ocean
|
||||
accentColor: '#4fc3f7', // 主题强调色
|
||||
layout: 'grid', // grid / list
|
||||
fontSize: 'medium', // small / medium / large
|
||||
showWelcome: true, // 显示欢迎横幅
|
||||
notifyModuleUpdate: true, // 模块更新通知
|
||||
notifySystem: true, // 系统通知
|
||||
notifySound: false, // 通知声音
|
||||
autoRefresh: true, // 自动刷新数据
|
||||
refreshInterval: 30, // 刷新间隔(秒)
|
||||
sidebarCollapsed: false,// 侧边栏收起
|
||||
animationEnabled: true, // 过渡动画
|
||||
analyticsEnabled: true // 数据采集
|
||||
};
|
||||
|
||||
// 主题预设
|
||||
const THEMES = {
|
||||
dark: {
|
||||
name: '深色模式',
|
||||
bg: '#0d1117',
|
||||
cardBg: '#1a1a2e',
|
||||
text: '#e0e0e0',
|
||||
accent: '#4fc3f7',
|
||||
border: '#2a2a4a'
|
||||
},
|
||||
light: {
|
||||
name: '浅色模式',
|
||||
bg: '#f5f5f5',
|
||||
cardBg: '#ffffff',
|
||||
text: '#333333',
|
||||
accent: '#4fc3f7',
|
||||
border: '#dddddd'
|
||||
},
|
||||
ocean: {
|
||||
name: '海洋模式',
|
||||
bg: '#1a2f3f',
|
||||
cardBg: '#1e3a4a',
|
||||
text: '#e0f0fa',
|
||||
accent: '#7bc8e0',
|
||||
border: '#2d5a6e'
|
||||
}
|
||||
};
|
||||
|
||||
// 加载设置
|
||||
function load() {
|
||||
try {
|
||||
var stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
var parsed = JSON.parse(stored);
|
||||
// 合并默认值,保证新字段存在
|
||||
var merged = {};
|
||||
Object.keys(DEFAULTS).forEach(function(k) {
|
||||
merged[k] = parsed.hasOwnProperty(k) ? parsed[k] : DEFAULTS[k];
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('读取设置失败,使用默认值', e);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(DEFAULTS));
|
||||
}
|
||||
|
||||
function save(settings) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
// ── 应用主题到页面 ──
|
||||
function applyTheme(themeName) {
|
||||
var theme = THEMES[themeName] || THEMES.dark;
|
||||
var root = document.documentElement;
|
||||
root.style.setProperty('--bg-color', theme.bg);
|
||||
root.style.setProperty('--card-bg', theme.cardBg);
|
||||
root.style.setProperty('--text-color', theme.text);
|
||||
root.style.setProperty('--accent-color', theme.accent);
|
||||
root.style.setProperty('--border-color', theme.border);
|
||||
document.body.style.background = theme.bg;
|
||||
document.body.style.color = theme.text;
|
||||
console.log('⚙️ 主题已切换:' + theme.name);
|
||||
}
|
||||
|
||||
// ── 应用字体大小 ──
|
||||
function applyFontSize(size) {
|
||||
var map = { small: '13px', medium: '15px', large: '17px' };
|
||||
var fontSize = map[size] || '15px';
|
||||
document.documentElement.style.setProperty('--base-font-size', fontSize);
|
||||
document.body.style.fontSize = fontSize;
|
||||
console.log('⚙️ 字体大小已应用:' + size + ' (' + fontSize + ')');
|
||||
}
|
||||
|
||||
// ── 公开方法 ──
|
||||
return {
|
||||
DEFAULTS: DEFAULTS,
|
||||
THEMES: THEMES,
|
||||
get: function(key) {
|
||||
var s = load();
|
||||
return key ? s[key] : s;
|
||||
},
|
||||
set: function(key, value) {
|
||||
var s = load();
|
||||
s[key] = value;
|
||||
save(s);
|
||||
// 即时应用
|
||||
if (key === 'theme') applyTheme(value);
|
||||
if (key === 'fontSize') applyFontSize(value);
|
||||
if (key === 'animationEnabled') {
|
||||
document.body.classList.toggle('no-animation', !value);
|
||||
}
|
||||
},
|
||||
setMultiple: function(obj) {
|
||||
var s = load();
|
||||
Object.keys(obj).forEach(function(k) { s[k] = obj[k]; });
|
||||
save(s);
|
||||
if (obj.theme) applyTheme(obj.theme);
|
||||
if (obj.fontSize) applyFontSize(obj.fontSize);
|
||||
},
|
||||
reset: function() {
|
||||
save(JSON.parse(JSON.stringify(DEFAULTS)));
|
||||
applyTheme(DEFAULTS.theme);
|
||||
applyFontSize(DEFAULTS.fontSize);
|
||||
console.log('⚙️ 所有设置已恢复默认');
|
||||
},
|
||||
// 导出为JSON字符串
|
||||
exportJSON: function() {
|
||||
var s = load();
|
||||
return JSON.stringify(s, null, 2);
|
||||
},
|
||||
// 从JSON字符串导入
|
||||
importJSON: function(jsonStr) {
|
||||
try {
|
||||
var imported = JSON.parse(jsonStr);
|
||||
var merged = {};
|
||||
Object.keys(DEFAULTS).forEach(function(k) {
|
||||
merged[k] = imported.hasOwnProperty(k) ? imported[k] : DEFAULTS[k];
|
||||
});
|
||||
save(merged);
|
||||
applyTheme(merged.theme);
|
||||
applyFontSize(merged.fontSize);
|
||||
console.log('✅ 设置导入成功');
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error('❌ 导入失败: JSON格式错误');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// 初始化(页面加载时调用)
|
||||
init: function() {
|
||||
var s = load();
|
||||
applyTheme(s.theme);
|
||||
applyFontSize(s.fontSize);
|
||||
if (!s.animationEnabled) {
|
||||
document.body.classList.add('no-animation');
|
||||
}
|
||||
console.log('⚙️ 频道设置已初始化');
|
||||
},
|
||||
getThemeList: function() {
|
||||
return Object.keys(THEMES).map(function(k) {
|
||||
return { id: k, name: THEMES[k].name };
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// 页面加载时自动初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
ChannelSettings.init();
|
||||
});
|
||||
} else {
|
||||
ChannelSettings.init();
|
||||
}
|
||||
44
agents/zhuyuan-dev-agent/modules/m-channel/channel-state.js
Normal file
44
agents/zhuyuan-dev-agent/modules/m-channel/channel-state.js
Normal file
@ -0,0 +1,44 @@
|
||||
// 状态管理(localStorage)
|
||||
window.ChannelState = {
|
||||
STORAGE_KEY: 'hololake_channel_state',
|
||||
|
||||
// 保存状态
|
||||
saveState: function(state) {
|
||||
const data = {
|
||||
...state,
|
||||
timestamp: Date.now(),
|
||||
visited: state.visited || []
|
||||
};
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
||||
console.log('[state] 保存状态:', data);
|
||||
},
|
||||
|
||||
// 恢复状态
|
||||
restoreState: function() {
|
||||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (!saved) return null;
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
console.log('[state] 恢复状态:', state);
|
||||
return state;
|
||||
} catch (e) {
|
||||
console.error('[state] 解析失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 标记已访问
|
||||
markVisited: function(channel) {
|
||||
const state = this.restoreState() || { visited: [] };
|
||||
if (!state.visited.includes(channel)) {
|
||||
state.visited.push(channel);
|
||||
this.saveState(state);
|
||||
}
|
||||
},
|
||||
|
||||
// 清除状态
|
||||
clearState: function() {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
console.log('[state] 已清除');
|
||||
}
|
||||
};
|
||||
298
agents/zhuyuan-dev-agent/modules/m-channel/channel-stats.js
Normal file
298
agents/zhuyuan-dev-agent/modules/m-channel/channel-stats.js
Normal file
@ -0,0 +1,298 @@
|
||||
// 频道使用统计管理
|
||||
window.ChannelStats = {
|
||||
// 初始化
|
||||
init: function() {
|
||||
console.log('[stats] 初始化');
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
// 绑定事件
|
||||
bindEvents: function() {
|
||||
if (!window.EventBus) return;
|
||||
|
||||
// 监听模块加载,开始计时
|
||||
EventBus.on('module:loaded', (data) => {
|
||||
if (data && data.module) {
|
||||
this.recordView(data.module);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听模块卸载,结束计时
|
||||
EventBus.on('module:unloaded', (data) => {
|
||||
if (data && data.module) {
|
||||
if (window.ChannelPreferences) {
|
||||
ChannelPreferences.stopTiming(data.module);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听页面关闭,停止所有计时
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.ChannelPreferences && window.ChannelPreferences.timing) {
|
||||
Object.keys(ChannelPreferences.timing).forEach(moduleId => {
|
||||
ChannelPreferences.stopTiming(moduleId);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 记录模块访问
|
||||
recordView: function(moduleId) {
|
||||
if (!window.ChannelPreferences) return;
|
||||
|
||||
// 记录使用次数和开始计时
|
||||
ChannelPreferences.recordUsage(moduleId);
|
||||
|
||||
// 发送事件
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('stats:recorded', {
|
||||
module: moduleId,
|
||||
time: Date.now()
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计报告
|
||||
getStatsReport: function() {
|
||||
if (!window.ChannelPreferences) return {};
|
||||
|
||||
const stats = ChannelPreferences.getStats();
|
||||
const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
|
||||
|
||||
// 格式化统计数据
|
||||
const report = {
|
||||
totalViews: 0,
|
||||
mostUsed: null,
|
||||
lastUsed: null,
|
||||
moduleDetails: {}
|
||||
};
|
||||
|
||||
let maxCount = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
modules.forEach(moduleId => {
|
||||
const moduleStats = stats[moduleId] || { count: 0, lastUsed: null, totalTime: 0 };
|
||||
report.moduleDetails[moduleId] = {
|
||||
count: moduleStats.count,
|
||||
lastUsed: moduleStats.lastUsed ? new Date(moduleStats.lastUsed).toLocaleString() : '从未使用',
|
||||
totalTime: moduleStats.totalTime ? this.formatTime(moduleStats.totalTime) : '0秒'
|
||||
};
|
||||
|
||||
report.totalViews += moduleStats.count;
|
||||
|
||||
if (moduleStats.count > maxCount) {
|
||||
maxCount = moduleStats.count;
|
||||
report.mostUsed = moduleId;
|
||||
}
|
||||
|
||||
if (moduleStats.lastUsed && moduleStats.lastUsed > lastTime) {
|
||||
lastTime = moduleStats.lastUsed;
|
||||
report.lastUsed = moduleId;
|
||||
}
|
||||
});
|
||||
|
||||
return report;
|
||||
},
|
||||
|
||||
// 格式化时间(秒 -> 可读格式)
|
||||
formatTime: function(seconds) {
|
||||
if (seconds < 60) return `${Math.round(seconds)}秒`;
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.round(seconds % 60);
|
||||
return `${minutes}分${secs}秒`;
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}小时${minutes}分`;
|
||||
},
|
||||
|
||||
// 渲染统计面板
|
||||
renderStatsPanel: function(container) {
|
||||
if (!container) return;
|
||||
|
||||
const report = this.getStatsReport();
|
||||
const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
|
||||
|
||||
let html = `
|
||||
<div class="stats-panel">
|
||||
<h3>📊 使用统计</h3>
|
||||
<div class="stats-summary">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总访问次数:</span>
|
||||
<span class="stat-value">${report.totalViews}</span>
|
||||
</div>
|
||||
${report.mostUsed ? `
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最常用模块:</span>
|
||||
<span class="stat-value">${this.getModuleName(report.mostUsed)} (${report.moduleDetails[report.mostUsed]?.count || 0}次)</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${report.lastUsed ? `
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最近使用:</span>
|
||||
<span class="stat-value">${this.getModuleName(report.lastUsed)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<h4>模块详情</h4>
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模块</th>
|
||||
<th>访问次数</th>
|
||||
<th>累计使用时长</th>
|
||||
<th>最后使用</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
modules.forEach(moduleId => {
|
||||
const detail = report.moduleDetails[moduleId] || { count: 0, totalTime: '0秒', lastUsed: '从未使用' };
|
||||
html += `
|
||||
<tr>
|
||||
<td>${this.getModuleName(moduleId)}</td>
|
||||
<td>${detail.count}</td>
|
||||
<td>${detail.totalTime}</td>
|
||||
<td>${detail.lastUsed}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="stats-actions">
|
||||
<button id="reset-stats-btn" class="stats-reset-btn">重置统计数据</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// 绑定重置按钮
|
||||
const resetBtn = document.getElementById('reset-stats-btn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (confirm('确定要重置所有统计数据吗?')) {
|
||||
this.resetStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取模块显示名称
|
||||
getModuleName: function(moduleId) {
|
||||
const names = {
|
||||
'm06': '工单管理',
|
||||
'm08': '数据统计',
|
||||
'm11': '组件库',
|
||||
'home': '首页',
|
||||
'debug': '调试面板'
|
||||
};
|
||||
return names[moduleId] || moduleId;
|
||||
},
|
||||
|
||||
// 重置统计
|
||||
resetStats: function() {
|
||||
if (!window.ChannelPreferences) return;
|
||||
|
||||
const config = ChannelPreferences.config;
|
||||
if (config) {
|
||||
config.stats = {};
|
||||
ChannelPreferences.save();
|
||||
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('stats:reset');
|
||||
}
|
||||
|
||||
alert('统计数据已重置');
|
||||
|
||||
// 重新渲染统计面板
|
||||
const container = document.querySelector('.stats-panel-container');
|
||||
if (container) {
|
||||
this.renderStatsPanel(container);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 注入样式
|
||||
injectStyles: function() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.stats-panel {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stats-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.stat-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.stats-table th,
|
||||
.stats-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.stats-table th {
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
.stats-table tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.stats-actions {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.stats-reset-btn {
|
||||
padding: 8px 16px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stats-reset-btn:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动注入样式
|
||||
setTimeout(() => {
|
||||
if (window.ChannelStats) {
|
||||
ChannelStats.injectStyles();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
console.log('[stats] 已加载');
|
||||
62
agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css
Normal file
62
agents/zhuyuan-dev-agent/modules/m-channel/channel-style.css
Normal file
@ -0,0 +1,62 @@
|
||||
/* 频道基础样式 */
|
||||
.channel-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.channel-nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.channel-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.channel-btn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.channel-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-btn.visited {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-btn.visited::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.channel-content {
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
157
agents/zhuyuan-dev-agent/modules/m-channel/channel-theme.js
Normal file
157
agents/zhuyuan-dev-agent/modules/m-channel/channel-theme.js
Normal file
@ -0,0 +1,157 @@
|
||||
// 频道主题管理 - 换主题色
|
||||
window.ChannelTheme = {
|
||||
// 预设主题
|
||||
themes: {
|
||||
default: {
|
||||
name: '默认蓝',
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
primaryDark: '#2563eb',
|
||||
secondary: '#10b981',
|
||||
background: '#ffffff',
|
||||
surface: '#f9fafb',
|
||||
text: '#1f2937',
|
||||
textLight: '#6b7280',
|
||||
border: '#e5e7eb'
|
||||
}
|
||||
},
|
||||
ocean: {
|
||||
name: '海洋',
|
||||
colors: {
|
||||
primary: '#0891b2',
|
||||
primaryDark: '#0e7490',
|
||||
secondary: '#2dd4bf',
|
||||
background: '#ecfeff',
|
||||
surface: '#ffffff',
|
||||
text: '#164e63',
|
||||
textLight: '#155e75',
|
||||
border: '#a5f3fc'
|
||||
}
|
||||
},
|
||||
forest: {
|
||||
name: '森林',
|
||||
colors: {
|
||||
primary: '#059669',
|
||||
primaryDark: '#047857',
|
||||
secondary: '#fbbf24',
|
||||
background: '#f0fdf4',
|
||||
surface: '#ffffff',
|
||||
text: '#064e3b',
|
||||
textLight: '#065f46',
|
||||
border: '#a7f3d0'
|
||||
}
|
||||
},
|
||||
sunset: {
|
||||
name: '日落',
|
||||
colors: {
|
||||
primary: '#d97706',
|
||||
primaryDark: '#b45309',
|
||||
secondary: '#f43f5e',
|
||||
background: '#fff7ed',
|
||||
surface: '#ffffff',
|
||||
text: '#7c2d12',
|
||||
textLight: '#9a3412',
|
||||
border: '#fed7aa'
|
||||
}
|
||||
},
|
||||
lavender: {
|
||||
name: '薰衣草',
|
||||
colors: {
|
||||
primary: '#8b5cf6',
|
||||
primaryDark: '#7c3aed',
|
||||
secondary: '#ec4899',
|
||||
background: '#f5f3ff',
|
||||
surface: '#ffffff',
|
||||
text: '#4c1d95',
|
||||
textLight: '#5b21b6',
|
||||
border: '#ddd6fe'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 当前主题ID
|
||||
currentTheme: 'default',
|
||||
|
||||
// 初始化主题
|
||||
init: function() {
|
||||
console.log('[theme] 初始化');
|
||||
|
||||
// 从偏好设置加载主题
|
||||
if (window.ChannelPreferences) {
|
||||
const savedTheme = ChannelPreferences.getTheme();
|
||||
if (savedTheme && this.themes[savedTheme]) {
|
||||
this.currentTheme = savedTheme;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
this.applyTheme(this.currentTheme);
|
||||
|
||||
// 监听主题变化事件
|
||||
if (window.EventBus) {
|
||||
EventBus.on('preferences:changed', (data) => {
|
||||
if (data.key === 'theme') {
|
||||
this.applyTheme(data.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 应用主题
|
||||
applyTheme: function(themeId) {
|
||||
if (!this.themes[themeId]) {
|
||||
console.error(`[theme] 未知主题: ${themeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTheme = themeId;
|
||||
const colors = this.themes[themeId].colors;
|
||||
|
||||
// 设置 CSS 自定义属性
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
root.style.setProperty(`--theme-${key}`, value);
|
||||
}
|
||||
|
||||
console.log(`[theme] 应用主题: ${themeId}`);
|
||||
|
||||
// 触发事件
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('theme:changed', { theme: themeId, colors });
|
||||
}
|
||||
},
|
||||
|
||||
// 切换主题
|
||||
setTheme: function(themeId) {
|
||||
if (!this.themes[themeId]) return;
|
||||
|
||||
this.applyTheme(themeId);
|
||||
|
||||
// 保存到偏好设置
|
||||
if (window.ChannelPreferences) {
|
||||
ChannelPreferences.setTheme(themeId);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取所有主题列表
|
||||
getThemes: function() {
|
||||
return Object.entries(this.themes).map(([id, theme]) => ({
|
||||
id,
|
||||
name: theme.name
|
||||
}));
|
||||
},
|
||||
|
||||
// 获取当前主题名称
|
||||
getCurrentThemeName: function() {
|
||||
return this.themes[this.currentTheme]?.name || '默认蓝';
|
||||
}
|
||||
};
|
||||
|
||||
// 自动初始化
|
||||
setTimeout(() => {
|
||||
if (window.ChannelTheme) {
|
||||
ChannelTheme.init();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
console.log('[theme] 已加载');
|
||||
@ -0,0 +1,53 @@
|
||||
/* 过渡动画 */
|
||||
.channel-content {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.channel-content.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.channel-content.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 滑动动画 */
|
||||
.slide-left-enter {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-left-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-right-enter {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-right-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-exit {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slide-exit-active {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-exit-left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-exit-right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* 临时测试动画 */
|
||||
.fade-out, .fade-in {
|
||||
transition: opacity 1s ease !important;
|
||||
}
|
||||
1401
agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html
Normal file
1401
agents/zhuyuan-dev-agent/modules/m-channel/channel-ultimate.html
Normal file
File diff suppressed because it is too large
Load Diff
134
agents/zhuyuan-dev-agent/modules/m-channel/error-boundary.js
Normal file
134
agents/zhuyuan-dev-agent/modules/m-channel/error-boundary.js
Normal file
@ -0,0 +1,134 @@
|
||||
// 错误边界 - 保险丝
|
||||
window.ErrorBoundary = {
|
||||
// 包裹一个可能出错的渲染函数
|
||||
wrap: function(renderFn, fallbackComponent, moduleId) {
|
||||
return function(container) {
|
||||
try {
|
||||
return renderFn(container);
|
||||
} catch (error) {
|
||||
console.error(`[error-boundary] 模块 ${moduleId} 渲染失败:`, error);
|
||||
return this.showFallback(container, moduleId, error);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// 显示降级界面
|
||||
showFallback: function(container, moduleId, error) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="error-boundary">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-message">
|
||||
<h4>模块 ${moduleId} 加载失败</h4>
|
||||
<p>${error.message || '未知错误'}</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<button onclick="ErrorBoundary.reloadModule('${moduleId}')" class="error-retry">
|
||||
重试
|
||||
</button>
|
||||
<button onclick="ErrorBoundary.hideError(this)" class="error-dismiss">
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 记录错误到事件总线
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('module:error', {
|
||||
module: moduleId,
|
||||
error: error.message,
|
||||
time: Date.now()
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 重新加载模块(通过适配器)
|
||||
reloadModule: function(moduleId) {
|
||||
console.log(`[error-boundary] 尝试重载模块: ${moduleId}`);
|
||||
|
||||
// 触发全局重载事件
|
||||
if (window.EventBus) {
|
||||
EventBus.emit('module:reload', { module: moduleId });
|
||||
}
|
||||
|
||||
// 如果存在适配器,调用适配器的重载方法
|
||||
if (window.ModuleAdapter && window.ModuleAdapter.reloadModule) {
|
||||
const container = document.querySelector(`[data-module="${moduleId}"]`) ||
|
||||
document.getElementById(`module-${moduleId}`);
|
||||
if (container) {
|
||||
ModuleAdapter.reloadModule(moduleId, container.id);
|
||||
}
|
||||
} else {
|
||||
// 否则直接刷新页面(简易方案)
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
// 隐藏错误(仅当用户点击“忽略”时)
|
||||
hideError: function(btn) {
|
||||
const errorDiv = btn.closest('.error-boundary');
|
||||
if (errorDiv) {
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
// 添加一些基础样式(会在 channel-style.css 中补充)
|
||||
injectStyles: function() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.error-boundary {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
background: #fff3f3;
|
||||
border: 1px solid #ffcdd2;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.error-message h4 {
|
||||
color: #d32f2f;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.error-message p {
|
||||
color: #666;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.error-retry {
|
||||
padding: 8px 16px;
|
||||
background: #d32f2f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error-dismiss {
|
||||
padding: 8px 16px;
|
||||
background: #9e9e9e;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动注入样式
|
||||
setTimeout(() => {
|
||||
if (window.ErrorBoundary) {
|
||||
ErrorBoundary.injectStyles();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
console.log('[error-boundary] 已加载');
|
||||
37
agents/zhuyuan-dev-agent/modules/m-channel/event-bus.js
Normal file
37
agents/zhuyuan-dev-agent/modules/m-channel/event-bus.js
Normal file
@ -0,0 +1,37 @@
|
||||
// 事件总线(群聊频道)
|
||||
window.EventBus = {
|
||||
listeners: {},
|
||||
|
||||
// 订阅(加入群聊)
|
||||
on: function(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(callback);
|
||||
console.log(`[事件总线] 订阅事件: ${event}`);
|
||||
},
|
||||
|
||||
// 取消订阅(退群)
|
||||
off: function(event, callback) {
|
||||
if (!this.listeners[event]) return;
|
||||
if (!callback) {
|
||||
delete this.listeners[event];
|
||||
} else {
|
||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
}
|
||||
console.log(`[事件总线] 取消订阅: ${event}`);
|
||||
},
|
||||
|
||||
// 发送消息(@所有人)
|
||||
emit: function(event, data) {
|
||||
console.log(`[事件总线] 发送事件: ${event}`, data);
|
||||
if (!this.listeners[event]) return;
|
||||
this.listeners[event].forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (e) {
|
||||
console.error(`[事件总线] 执行回调出错: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
47
agents/zhuyuan-dev-agent/modules/m-channel/index.html
Normal file
47
agents/zhuyuan-dev-agent/modules/m-channel/index.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖频道 · 动态渲染引擎</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
<link rel="stylesheet" href="channel-dashboard.css">
|
||||
<link rel="stylesheet" href="channel-settings.css">
|
||||
<!-- 新增:面板样式 -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="dashboard">📊 数据面板</button> <!-- 新增入口 -->
|
||||
<button class="channel-btn" data-channel="settings">⚙️ 设置</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本(顺序很重要!) -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="channel-enhancements.js"></script>
|
||||
<link rel="stylesheet" href="channel-notifications.css">
|
||||
<script src="channel-notifications.js"></script>
|
||||
|
||||
<!-- 新增:数据采集与面板逻辑 -->
|
||||
<script src="channel-analytics.js"></script>
|
||||
<script src="channel-dashboard.js"></script>
|
||||
<script src="channel-settings.js"></script>
|
||||
<script src="channel-settings-ui.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h2>模拟模块A</h2>
|
||||
<button onclick="EventBus.emit('mock-a:click', { message: '模块A被点击了' })">
|
||||
发送事件
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
|
||||
<script>
|
||||
EventBus.on('mock-b:reply', function(data) {
|
||||
document.getElementById('output').innerHTML = '收到回复: ' + data.message;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h2>模拟模块B</h2>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
EventBus.on('mock-a:click', function(data) {
|
||||
const div = document.getElementById('messages');
|
||||
div.innerHTML += '<p>收到模块A消息: ' + JSON.stringify(data) + '</p>';
|
||||
|
||||
// 回复消息
|
||||
EventBus.emit('mock-b:reply', { message: '已收到,谢谢!' });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,23 @@
|
||||
// 模块生命周期管理
|
||||
window.ModuleLifecycle = {
|
||||
activeModules: new Set(),
|
||||
|
||||
// 模块加载时
|
||||
onLoad: function(moduleName) {
|
||||
this.activeModules.add(moduleName);
|
||||
console.log(`[生命周期] 模块加载: ${moduleName}`);
|
||||
EventBus.emit('module:loaded', { module: moduleName, time: Date.now() });
|
||||
},
|
||||
|
||||
// 模块卸载时
|
||||
onUnload: function(moduleName) {
|
||||
this.activeModules.delete(moduleName);
|
||||
console.log(`[生命周期] 模块卸载: ${moduleName}`);
|
||||
EventBus.emit('module:unloaded', { module: moduleName, time: Date.now() });
|
||||
},
|
||||
|
||||
// 获取当前活跃模块
|
||||
getActiveModules: function() {
|
||||
return Array.from(this.activeModules);
|
||||
}
|
||||
};
|
||||
36
agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js
Normal file
36
agents/zhuyuan-dev-agent/modules/m-channel/module-loader.js
Normal file
@ -0,0 +1,36 @@
|
||||
// 模块加载器
|
||||
window.ModuleLoader = {
|
||||
loadModule: function(moduleName, containerId) {
|
||||
console.log(`[loader] 加载模块: ${moduleName}`);
|
||||
|
||||
const module = ModuleRegistry.get(moduleName);
|
||||
if (!module) {
|
||||
console.error(`[loader] 模块 ${moduleName} 未注册`);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`[loader] 容器 ${containerId} 不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果模块有 init 方法
|
||||
if (module.init && typeof module.init === 'function') {
|
||||
module.init(container);
|
||||
} else {
|
||||
container.innerHTML = `<div>模块 ${moduleName} 内容</div>`;
|
||||
}
|
||||
|
||||
ModuleLifecycle.onLoad(moduleName);
|
||||
},
|
||||
|
||||
unloadModule: function(moduleName) {
|
||||
console.log(`[loader] 卸载模块: ${moduleName}`);
|
||||
const module = ModuleRegistry.get(moduleName);
|
||||
if (module && module.destroy && typeof module.destroy === 'function') {
|
||||
module.destroy();
|
||||
}
|
||||
ModuleLifecycle.onUnload(moduleName);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
// 模块注册表
|
||||
window.ModuleRegistry = {
|
||||
modules: {},
|
||||
|
||||
register: function(name, module) {
|
||||
this.modules[name] = module;
|
||||
console.log(`[registry] 模块 ${name} 已注册`);
|
||||
},
|
||||
|
||||
get: function(name) {
|
||||
return this.modules[name];
|
||||
},
|
||||
|
||||
list: function() {
|
||||
return Object.keys(this.modules);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
<div class="dashboard-container" id="analyticsDashboard">
|
||||
<div class="dashboard-header">
|
||||
<h2>📊 频道数据面板</h2>
|
||||
<p>模块使用统计 · 性能监控 · 访问趋势</p>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<!-- 柱状图:模块访问次数 -->
|
||||
<div class="chart-card">
|
||||
<h3>📊 模块访问次数</h3>
|
||||
<div class="bar-chart" id="visitBarChart">
|
||||
<p style="color:#666;text-align:center;width:100%;">暂无数据,多点几个模块再来看</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 饼图:使用时间占比 -->
|
||||
<div class="chart-card">
|
||||
<h3>🥧 使用时间占比</h3>
|
||||
<div class="pie-container" id="timePieChart">
|
||||
<div class="pie-canvas-wrap">
|
||||
<canvas id="pieCanvas" width="160" height="160"></canvas>
|
||||
</div>
|
||||
<ul class="pie-legend" id="pieLegend"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 折线图:7天趋势 -->
|
||||
<div class="chart-card" style="grid-column: 1 / -1;">
|
||||
<h3>📈 最近7天访问趋势</h3>
|
||||
<div class="line-chart-wrap">
|
||||
<canvas id="lineCanvas" width="800" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能表格 -->
|
||||
<div class="chart-card">
|
||||
<h3>⚡ 模块加载性能</h3>
|
||||
<table class="perf-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模块</th>
|
||||
<th>访问次数</th>
|
||||
<th>总停留(分钟)</th>
|
||||
<th>平均加载(ms)</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="perfTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 清除按钮 -->
|
||||
<div class="dashboard-actions">
|
||||
<button class="btn-clear" onclick="ChannelDashboard.clearData()">🗑️ 清除所有数据</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.debug-panel { padding: 20px; }
|
||||
.message-list {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #f9f9f9;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.message-list li {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.debug-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.debug-controls input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
.debug-controls button {
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.debug-controls button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.module-list {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="debug-panel">
|
||||
<h2>🐞 调试面板</h2>
|
||||
|
||||
<div class="module-list">
|
||||
<strong>当前活跃模块:</strong>
|
||||
<span id="active-modules">加载中...</span>
|
||||
</div>
|
||||
|
||||
<h3>事件总线聊天记录</h3>
|
||||
<ul class="message-list" id="debug-messages">
|
||||
<!-- 动态插入消息 -->
|
||||
</ul>
|
||||
|
||||
<div class="debug-controls">
|
||||
<input type="text" id="debug-input" placeholder="输入要发送的消息...">
|
||||
<button id="debug-send">发送测试消息</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-controls">
|
||||
<button id="debug-clear" style="background: #ef4444;">清除状态</button>
|
||||
<button id="debug-refresh" onclick="location.reload()">刷新页面</button>
|
||||
</div>
|
||||
|
||||
<p><small>消息会广播给所有订阅的模块</small></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 更新活跃模块列表
|
||||
function updateActiveModules() {
|
||||
const span = document.getElementById('active-modules');
|
||||
if (span && window.ModuleLifecycle) {
|
||||
const modules = ModuleLifecycle.getActiveModules();
|
||||
span.textContent = modules.length ? modules.join('、') : '无';
|
||||
}
|
||||
}
|
||||
|
||||
// 监听模块加载/卸载事件
|
||||
if (window.EventBus) {
|
||||
EventBus.on('module:loaded', updateActiveModules);
|
||||
EventBus.on('module:unloaded', updateActiveModules);
|
||||
}
|
||||
|
||||
// 初始更新
|
||||
setTimeout(updateActiveModules, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,144 @@
|
||||
<div class="settings-container" id="settingsPanel">
|
||||
<div class="settings-header">
|
||||
<h2>频道设置中心</h2>
|
||||
<p>管理你的频道偏好 · 主题 · 通知 · 显示</p>
|
||||
</div>
|
||||
|
||||
<!-- 外观设置 -->
|
||||
<div class="settings-group">
|
||||
<h3>🎨 外观设置</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">主题模式</span>
|
||||
<span class="desc">选择你喜欢的界面风格</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-cards" id="themeCards"></div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">字体大小</span>
|
||||
<span class="desc">调整界面文字大小</span>
|
||||
</div>
|
||||
<select class="setting-select" id="settingFontSize" onchange="SettingsUI.onChange('fontSize', this.value)">
|
||||
<option value="small">小</option>
|
||||
<option value="medium">中</option>
|
||||
<option value="large">大</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">过渡动画</span>
|
||||
<span class="desc">开启页面切换动画效果</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingAnimation" onchange="SettingsUI.onToggle('animationEnabled', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">侧边栏默认收起</span>
|
||||
<span class="desc">页面加载时侧边栏是否收起</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingSidebar" onchange="SettingsUI.onToggle('sidebarCollapsed', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 通知设置 -->
|
||||
<div class="settings-group">
|
||||
<h3>🔔 通知设置</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">模块更新通知</span>
|
||||
<span class="desc">有模块内容更新时提醒</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingNotifyModule" onchange="SettingsUI.onToggle('notifyModuleUpdate', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">系统通知</span>
|
||||
<span class="desc">系统公告和维护提醒</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingNotifySystem" onchange="SettingsUI.onToggle('notifySystem', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">通知声音</span>
|
||||
<span class="desc">收到通知时播放提示音</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingNotifySound" onchange="SettingsUI.onToggle('notifySound', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据设置 -->
|
||||
<div class="settings-group">
|
||||
<h3>📊 数据设置</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">数据采集</span>
|
||||
<span class="desc">允许采集模块访问和性能数据(用于数据面板)</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingAnalytics" onchange="SettingsUI.onToggle('analyticsEnabled', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">自动刷新</span>
|
||||
<span class="desc">定时自动刷新频道数据</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingAutoRefresh" onchange="SettingsUI.onToggle('autoRefresh', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
<span class="title">刷新间隔</span>
|
||||
<span class="desc">自动刷新的时间间隔</span>
|
||||
</div>
|
||||
<select class="setting-select" id="settingRefreshInterval" onchange="SettingsUI.onChange('refreshInterval', parseInt(this.value))">
|
||||
<option value="15">15秒</option>
|
||||
<option value="30">30秒</option>
|
||||
<option value="60">1分钟</option>
|
||||
<option value="300">5分钟</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="settings-actions">
|
||||
<button class="btn-settings btn-export" onclick="SettingsUI.exportSettings()">📤 导出设置</button>
|
||||
<button class="btn-settings btn-import" onclick="SettingsUI.showImport()">📥 导入设置</button>
|
||||
<button class="btn-settings btn-reset" onclick="SettingsUI.resetAll()">🔄 恢复默认</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<div class="import-modal" id="importModal">
|
||||
<div class="import-dialog">
|
||||
<h3 style="color:var(--accent-color); margin-bottom:8px;">📥 导入设置</h3>
|
||||
<p style="font-size:13px; margin-bottom:12px;">粘贴之前导出的JSON内容:</p>
|
||||
<textarea class="import-textarea" id="importTextarea" placeholder='{"theme":"dark",...}'></textarea>
|
||||
<div style="display:flex; gap:10px; justify-content:flex-end;">
|
||||
<button class="btn-settings" style="background:#555; color:#fff;" onclick="SettingsUI.hideImport()">取消</button>
|
||||
<button class="btn-settings btn-import" onclick="SettingsUI.doImport()">确认导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
70
agents/zhuyuan-dev-agent/modules/model-caller.js
Normal file
70
agents/zhuyuan-dev-agent/modules/model-caller.js
Normal file
@ -0,0 +1,70 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · 大模型调用模块
|
||||
// 支持 OpenAI 兼容 API → DeepSeek/千问/等
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const https = require("https");
|
||||
|
||||
function getConfig() {
|
||||
const key = process.env.MODEL_API_KEY;
|
||||
const base = process.env.MODEL_API_BASE || "https://api.deepseek.com/v1";
|
||||
const model = process.env.MODEL_NAME || "deepseek-chat";
|
||||
|
||||
if (!key) {
|
||||
console.warn("[ModelCaller] MODEL_API_KEY 未配置");
|
||||
return null;
|
||||
}
|
||||
return { key, base, model };
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用大模型
|
||||
* @param {string} prompt - 输入提示
|
||||
* @param {object} opts - { max_tokens, temperature }
|
||||
* @returns {Promise<string>} 模型回复文本
|
||||
*/
|
||||
async function call(prompt, opts = {}) {
|
||||
const config = getConfig();
|
||||
if (!config) throw new Error("MODEL_API_KEY 未配置");
|
||||
|
||||
const body = JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [
|
||||
{ role: "system", content: "你是一个技术开发Agent。回答要精确、结构化、可执行。" },
|
||||
{ role: "user", content: prompt }
|
||||
],
|
||||
max_tokens: opts.max_tokens || 4096,
|
||||
temperature: opts.temperature || 0.3
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(config.base + "/chat/completions");
|
||||
const req = https.request({
|
||||
hostname: url.hostname,
|
||||
path: url.pathname,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + config.key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (c) => data += c);
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json.error) return reject(new Error(json.error.message || "API错误"));
|
||||
resolve(json.choices?.[0]?.message?.content || "");
|
||||
} catch (e) {
|
||||
reject(new Error("解析失败: " + data.slice(0, 200)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { call };
|
||||
118
agents/zhuyuan-dev-agent/modules/notion-bridge.js
Normal file
118
agents/zhuyuan-dev-agent/modules/notion-bridge.js
Normal file
@ -0,0 +1,118 @@
|
||||
// ═══════════════════════════════════════
|
||||
// 铸渊开发Agent · Notion 桥接模块
|
||||
// 读取规划数据库 · 写入执行进度
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const https = require("https");
|
||||
|
||||
const NOTION_API = "https://api.notion.com/v1";
|
||||
const NOTION_VERSION = "2022-06-28";
|
||||
|
||||
function getHeaders() {
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
if (!token) {
|
||||
console.warn("[NotionBridge] NOTION_TOKEN 未配置");
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
"Authorization": "Bearer " + token,
|
||||
"Content-Type": "application/json",
|
||||
"Notion-Version": NOTION_VERSION
|
||||
};
|
||||
}
|
||||
|
||||
function notionRequest(endpoint, method, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = getHeaders();
|
||||
if (!headers) return reject(new Error("NOTION_TOKEN 未配置"));
|
||||
|
||||
const url = new URL(NOTION_API + endpoint);
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: method,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => data += chunk);
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode >= 400) {
|
||||
reject(new Error("Notion API " + res.statusCode + ": " + (json.message || data)));
|
||||
} else {
|
||||
resolve(json);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error("解析失败: " + data.slice(0, 200)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Notion 数据库读取规划
|
||||
* @param {string} databaseId - Notion 数据库 ID
|
||||
* @returns {Promise<Array>} 规划条目列表
|
||||
*/
|
||||
async function fetchPlans(databaseId) {
|
||||
const result = await notionRequest("/databases/" + databaseId + "/query", "POST", {
|
||||
sorts: [{ property: "Created", direction: "descending" }],
|
||||
page_size: 10
|
||||
});
|
||||
|
||||
return (result.results || []).map((page) => {
|
||||
const props = page.properties || {};
|
||||
return {
|
||||
notion_page_id: page.id,
|
||||
title: props.Name?.title?.[0]?.plain_text || "无标题",
|
||||
status: props.Status?.status?.name || "unknown",
|
||||
plan_id: props.PlanID?.rich_text?.[0]?.plain_text || "",
|
||||
created: page.created_time
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Notion 中某条规划的状态
|
||||
* @param {string} pageId - Notion 页面 ID
|
||||
* @param {string} status - 新状态
|
||||
* @param {object} details - 额外详情
|
||||
*/
|
||||
async function updatePlanStatus(pageId, status, details = {}) {
|
||||
const properties = {
|
||||
Status: { status: { name: status } }
|
||||
};
|
||||
|
||||
if (details.log_url) {
|
||||
properties.LogURL = { url: details.log_url };
|
||||
}
|
||||
|
||||
return notionRequest("/pages/" + pageId, "PATCH", { properties });
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Notion 数据库追加执行日志
|
||||
* @param {string} databaseId - Notion 数据库 ID
|
||||
* @param {object} entry - 日志条目 { plan_id, task_id, action, result, time }
|
||||
*/
|
||||
async function appendLog(databaseId, entry) {
|
||||
return notionRequest("/pages", "POST", {
|
||||
parent: { database_id: databaseId },
|
||||
properties: {
|
||||
Name: { title: [{ text: { content: entry.plan_id + " · " + entry.task_id } }] },
|
||||
Status: { status: { name: entry.result === "ok" ? "Done" : "Failed" } },
|
||||
LogTime: { date: { start: entry.time || new Date().toISOString() } },
|
||||
Detail: { rich_text: [{ text: { content: entry.detail || entry.action } }] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchPlans, updatePlanStatus, appendLog };
|
||||
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* M-PALACE · 背景生成引擎
|
||||
* 接收锚点答案 + 人格侧面初始数据
|
||||
* → 从宫廷数据库匹配
|
||||
* → 生成逻辑自洽的专属背景
|
||||
*
|
||||
* 生成流程:
|
||||
* 锚点1(世界观)→ 选择 templates/ 基础模板
|
||||
* 锚点2(身份) → 确定主角定位 + 初始人格快照
|
||||
* → 查询 palace-db 四大维度 → 调用模型 → 输出世界状态
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
|
||||
const TEMPLATES_DIR = path.join(DATA_DIR, 'templates');
|
||||
const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json');
|
||||
|
||||
let _palaceDB = null;
|
||||
|
||||
function loadPalaceDB() {
|
||||
if (_palaceDB) return _palaceDB;
|
||||
_palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8'));
|
||||
return _palaceDB;
|
||||
}
|
||||
|
||||
function loadTemplate(worldview) {
|
||||
const map = {
|
||||
'ancient-china': 'ancient-china.json',
|
||||
'古代中国': 'ancient-china.json',
|
||||
'fantasy': 'fantasy-dynasty.json',
|
||||
'架空王朝': 'fantasy-dynasty.json'
|
||||
};
|
||||
const file = map[worldview] || 'ancient-china.json';
|
||||
return JSON.parse(fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从模板中随机选取一项
|
||||
*/
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于人格侧面决定 NPC 映射
|
||||
* 核心机密:每个 NPC 对应玩家 1~2 个人格侧面的外化
|
||||
*/
|
||||
function generateNPCMappings(scores) {
|
||||
const npcs = [];
|
||||
const dims = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
// 最强维度 → 镜像对手
|
||||
const strongest = dims[0];
|
||||
npcs.push({
|
||||
archetype: 'mirror',
|
||||
mapped_dims: [strongest[0]],
|
||||
role_hint: 'rival',
|
||||
description: '与你最相似的对手'
|
||||
});
|
||||
|
||||
// 最弱维度 → 放大威胁
|
||||
const weakest = dims[dims.length - 1];
|
||||
npcs.push({
|
||||
archetype: 'shadow',
|
||||
mapped_dims: [weakest[0]],
|
||||
role_hint: 'threat',
|
||||
description: '让你恐惧的存在'
|
||||
});
|
||||
|
||||
// 温情相关 → 情感锚点
|
||||
if (scores.warmth >= 40) {
|
||||
npcs.push({
|
||||
archetype: 'anchor',
|
||||
mapped_dims: ['warmth', 'loyalty'],
|
||||
role_hint: 'ally',
|
||||
description: '需要被保护的弱势角色'
|
||||
});
|
||||
}
|
||||
|
||||
// 多疑相关 → 暧昧盟友
|
||||
if (scores.suspicion >= 45) {
|
||||
npcs.push({
|
||||
archetype: 'trickster',
|
||||
mapped_dims: ['suspicion', 'cunning'],
|
||||
role_hint: 'ambiguous',
|
||||
description: '行为暧昧的盟友'
|
||||
});
|
||||
}
|
||||
|
||||
// 确保至少 3 个 NPC
|
||||
if (npcs.length < 3) {
|
||||
npcs.push({
|
||||
archetype: 'catalyst',
|
||||
mapped_dims: [dims[2][0]],
|
||||
role_hint: 'neutral',
|
||||
description: '推动局势变化的中立角色'
|
||||
});
|
||||
}
|
||||
|
||||
return npcs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于人格分数查询 palace-db,确定初始格局
|
||||
*/
|
||||
function queryInitialSetup(scores) {
|
||||
const db = loadPalaceDB();
|
||||
|
||||
// 权力维度:基于 ambition+cunning
|
||||
const powerLevel = (scores.ambition + scores.cunning) / 2;
|
||||
const factionCount = powerLevel > 60 ? 3 : 2;
|
||||
|
||||
// 后宫地位:基于 vanity+warmth
|
||||
const statusLevel = (scores.vanity + scores.warmth) / 2;
|
||||
|
||||
// 情感关系:基于 warmth+loyalty
|
||||
const emotionIntensity = (scores.warmth + scores.loyalty) / 2;
|
||||
|
||||
// 矛盾冲突:基于 aggression+suspicion
|
||||
const conflictLevel = (scores.aggression + scores.suspicion) / 2;
|
||||
const conflictType = conflictLevel > 60 ? '争宠' : '自保';
|
||||
|
||||
return {
|
||||
factions: db.power.factions.slice(0, factionCount),
|
||||
initialEvent: pick(db.power.events),
|
||||
startingRank: db.status.ranks[Math.min(Math.floor(statusLevel / 15), db.status.ranks.length - 1)],
|
||||
emotionType: emotionIntensity > 60 ? pick(db.emotion.types.slice(0, 3)) : pick(db.emotion.types.slice(3)),
|
||||
emotionTrigger: pick(db.emotion.triggers),
|
||||
conflictType: conflictType,
|
||||
escalation: db.conflict.escalation[0]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建世界生成 prompt(供模型调用)
|
||||
*/
|
||||
function buildWorldPrompt(template, role, setup, npcMappings) {
|
||||
const dynasty = template.setting.dynasty;
|
||||
const era = pick(template.setting.era_prefix);
|
||||
const roleConfig = template.roles[role] || template.roles['妃子'];
|
||||
const title = pick(roleConfig.title_options);
|
||||
|
||||
const npcDescriptions = npcMappings.map((npc, i) => {
|
||||
return `NPC${i + 1}(${npc.role_hint}型):${npc.description}`;
|
||||
}).join('\n');
|
||||
|
||||
return {
|
||||
system: [
|
||||
'你是一个古风宫斗叙事生成器。',
|
||||
'要求:文笔古风典雅,有呼吸感,不要白话流水账。',
|
||||
'使用通感手法,注重氛围营造。',
|
||||
'所有NPC的存在都有隐藏的人格映射逻辑,但不能向玩家揭示。'
|
||||
].join('\n'),
|
||||
user: [
|
||||
`请为以下设定生成宫廷世界开篇:`,
|
||||
`王朝:${dynasty}`,
|
||||
`年号:${era}`,
|
||||
`玩家身份:${role}(${title})`,
|
||||
`初始阵营格局:${setup.factions.join('、')}`,
|
||||
`开局事件:${setup.initialEvent}`,
|
||||
`情感基调:${setup.emotionType}`,
|
||||
`冲突类型:${setup.conflictType}`,
|
||||
``,
|
||||
`需要生成的NPC:`,
|
||||
npcDescriptions,
|
||||
``,
|
||||
`输出格式(JSON):`,
|
||||
`{`,
|
||||
` "dynasty_name": "王朝名",`,
|
||||
` "era_name": "年号",`,
|
||||
` "background": "一段时代背景描述(古风文笔·100-200字)",`,
|
||||
` "player_intro": "主角身世与处境(古风文笔·100-200字)",`,
|
||||
` "npcs": [{ "name": "角色名", "title": "头衔", "personality": "性格", "relation_to_player": "与主角关系", "hidden_archetype": "mirror|shadow|anchor|trickster|catalyst" }],`,
|
||||
` "opening_event": "开局事件描述(古风文笔·150-300字)",`,
|
||||
` "opening_choices": ["选项A", "选项B", "选项C"]`,
|
||||
`}`
|
||||
].join('\n'),
|
||||
meta: { dynasty, era, title, role }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:生成完整背景(不依赖外部模型时,使用本地模板拼接)
|
||||
*/
|
||||
function generate(worldview, role, personaScores) {
|
||||
const template = loadTemplate(worldview);
|
||||
const setup = queryInitialSetup(personaScores);
|
||||
const npcMappings = generateNPCMappings(personaScores);
|
||||
const prompt = buildWorldPrompt(template, role, setup, npcMappings);
|
||||
|
||||
// 构造本地 fallback 世界(不依赖外部模型时使用)
|
||||
const dynasty = prompt.meta.dynasty;
|
||||
const era = prompt.meta.era;
|
||||
const roleConfig = template.roles[role] || template.roles['妃子'];
|
||||
const title = prompt.meta.title;
|
||||
|
||||
const localWorld = {
|
||||
dynasty_name: dynasty,
|
||||
era_name: era,
|
||||
background: `${dynasty}${era}年间,朝堂暗流涌动。${setup.factions.join('与')}明争暗斗,帝位之下,人心叵测。宫墙之内,灯火通明处未必安宁,暗影幢幢处未必无情。`,
|
||||
player_intro: `你是${dynasty}${title},${pick(roleConfig.family_options || ['名门'])}出身。入宫数载,${setup.conflictType === '争宠' ? '虽有圣眷,却树敌颇多' : '小心翼翼,只求自保平安'}。${setup.emotionType}的暗线,早已悄然铺开。`,
|
||||
npcs: npcMappings.map(function (npc, i) {
|
||||
const names = ['沈婉仪', '赵昭仪', '陈太傅', '李将军', '刘公公'];
|
||||
return {
|
||||
name: names[i] || '无名氏',
|
||||
title: pick(template.setting.era_prefix) + '年间人物',
|
||||
personality: npc.description,
|
||||
relation_to_player: npc.role_hint,
|
||||
hidden_archetype: npc.archetype
|
||||
};
|
||||
}),
|
||||
opening_event: `夜半三更,${template.setting.inner_palace}传来急召。太后身边的掌事姑姑面色如常,语气却带着不容拒绝的冷意。你知道,这一趟,去与不去,都是棋局的一部分。`,
|
||||
opening_choices: [
|
||||
'立刻更衣前往,恭顺以对',
|
||||
'称身体不适,推迟半个时辰',
|
||||
'先派人去打听太后的意图'
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
world: localWorld,
|
||||
npcMappings: npcMappings,
|
||||
setup: setup,
|
||||
prompt: prompt,
|
||||
template: template
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generate,
|
||||
loadTemplate,
|
||||
loadPalaceDB,
|
||||
generateNPCMappings,
|
||||
queryInitialSetup,
|
||||
buildWorldPrompt
|
||||
};
|
||||
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* M-PALACE · 人格分析引擎
|
||||
* 实时分析玩家每一次输入,识别并量化人格侧面
|
||||
*
|
||||
* 分析流程:
|
||||
* ① 关键词匹配:扫描 persona-dict 的 keywords
|
||||
* ② 行为信号匹配:判断选项类型对应的 behavior_signals
|
||||
* ③ 上下文推断:结合对话历史推断语气/态度/策略倾向
|
||||
* → 输出 8 维人格侧面分数(0~100)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DICT_PATH = path.join(__dirname, '..', '..', 'data', 'persona-dict.json');
|
||||
|
||||
let _dictCache = null;
|
||||
|
||||
function loadDict() {
|
||||
if (_dictCache) return _dictCache;
|
||||
_dictCache = JSON.parse(fs.readFileSync(DICT_PATH, 'utf-8'));
|
||||
return _dictCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成初始人格分数(基于身份锚点)
|
||||
*/
|
||||
function getInitialScores(role) {
|
||||
const base = {
|
||||
ambition: 50, warmth: 50, aggression: 50, suspicion: 50,
|
||||
vanity: 50, cunning: 50, loyalty: 50, fear: 50
|
||||
};
|
||||
const adjustments = {
|
||||
'皇帝': { ambition: 20, vanity: 15 },
|
||||
'妃子': { warmth: 15, fear: 10 },
|
||||
'重臣': { loyalty: 20, cunning: 10 },
|
||||
'奸臣': { cunning: 25, aggression: 15 }
|
||||
};
|
||||
const adj = adjustments[role] || {};
|
||||
for (const [k, v] of Object.entries(adj)) {
|
||||
base[k] = Math.min(100, base[k] + v);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键词匹配:在输入文本中扫描 persona-dict keywords
|
||||
* @returns {{ [dimensionId]: number }} 命中次数 map
|
||||
*/
|
||||
function matchKeywords(text) {
|
||||
const dict = loadDict();
|
||||
const hits = {};
|
||||
for (const dim of dict.dimensions) {
|
||||
hits[dim.id] = 0;
|
||||
for (const kw of dim.keywords) {
|
||||
if (text.includes(kw)) hits[dim.id]++;
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为信号匹配:将选项 index 映射到行为信号
|
||||
* 选项设计规则:A=最强侧面(舒适区),B=最弱侧面(成长区),C=中性
|
||||
* @param {number|null} choiceIndex 0/1/2 or null(自由输入)
|
||||
* @param {object} optionMeta 后端为本次选项附加的元数据 { a_dims, b_dims, c_dims }
|
||||
*/
|
||||
function matchBehavior(choiceIndex, optionMeta) {
|
||||
if (choiceIndex === null || !optionMeta) return {};
|
||||
const key = ['a_dims', 'b_dims', 'c_dims'][choiceIndex];
|
||||
const dims = (optionMeta && optionMeta[key]) || [];
|
||||
const hits = {};
|
||||
for (const d of dims) {
|
||||
hits[d] = (hits[d] || 0) + 1;
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并关键词 + 行为信号 → 更新人格分数
|
||||
*/
|
||||
function updateScores(currentScores, keywordHits, behaviorHits) {
|
||||
const updated = { ...currentScores };
|
||||
const KEYWORD_WEIGHT = 3;
|
||||
const BEHAVIOR_WEIGHT = 5;
|
||||
|
||||
for (const dim of Object.keys(updated)) {
|
||||
let delta = 0;
|
||||
if (keywordHits[dim]) delta += keywordHits[dim] * KEYWORD_WEIGHT;
|
||||
if (behaviorHits[dim]) delta += behaviorHits[dim] * BEHAVIOR_WEIGHT;
|
||||
updated[dim] = Math.max(0, Math.min(100, updated[dim] + delta));
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到当前最突出特征
|
||||
*/
|
||||
function getDominantTrait(scores) {
|
||||
let maxDim = null;
|
||||
let maxVal = -1;
|
||||
for (const [k, v] of Object.entries(scores)) {
|
||||
if (v > maxVal) { maxVal = v; maxDim = k; }
|
||||
}
|
||||
return maxDim;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测分数突变(单次 +15 以上)
|
||||
*/
|
||||
function detectSurge(oldScores, newScores) {
|
||||
const surges = [];
|
||||
for (const dim of Object.keys(newScores)) {
|
||||
const delta = newScores[dim] - (oldScores[dim] || 50);
|
||||
if (delta >= 15) surges.push({ dim, delta });
|
||||
}
|
||||
return surges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测交叉升高(两个维度同时上升 10+)
|
||||
*/
|
||||
function detectCrossRise(oldScores, newScores) {
|
||||
const rising = [];
|
||||
for (const dim of Object.keys(newScores)) {
|
||||
const delta = newScores[dim] - (oldScores[dim] || 50);
|
||||
if (delta >= 10) rising.push(dim);
|
||||
}
|
||||
return rising.length >= 2 ? rising : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 主分析入口
|
||||
* @param {string} text 玩家输入文本
|
||||
* @param {number|null} choiceIndex 选项序号(null = 自由输入)
|
||||
* @param {object} optionMeta 选项元数据
|
||||
* @param {object} currentScores 当前人格分数
|
||||
* @returns {{ scores, dominant, surges, crossRise }}
|
||||
*/
|
||||
function analyze(text, choiceIndex, optionMeta, currentScores) {
|
||||
const kwHits = text ? matchKeywords(text) : {};
|
||||
const bhHits = matchBehavior(choiceIndex, optionMeta);
|
||||
const oldScores = { ...currentScores };
|
||||
const newScores = updateScores(currentScores, kwHits, bhHits);
|
||||
const dominant = getDominantTrait(newScores);
|
||||
const surges = detectSurge(oldScores, newScores);
|
||||
const crossRise = detectCrossRise(oldScores, newScores);
|
||||
|
||||
return { scores: newScores, dominant, surges, crossRise };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 8 维人格分数转换为前端四维状态栏数值
|
||||
* 权力值 = avg(ambition, cunning)
|
||||
* 地位值 = avg(vanity, loyalty)
|
||||
* 情感值 = avg(warmth, fear反转)
|
||||
* 冲突值 = avg(aggression, suspicion)
|
||||
*/
|
||||
function toFourDimensions(scores) {
|
||||
return {
|
||||
power: Math.round((scores.ambition + scores.cunning) / 2),
|
||||
status: Math.round((scores.vanity + scores.loyalty) / 2),
|
||||
emotion: Math.round((scores.warmth + (100 - scores.fear)) / 2),
|
||||
conflict: Math.round((scores.aggression + scores.suspicion) / 2)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInitialScores,
|
||||
analyze,
|
||||
getDominantTrait,
|
||||
toFourDimensions,
|
||||
matchKeywords,
|
||||
detectSurge,
|
||||
detectCrossRise
|
||||
};
|
||||
@ -0,0 +1,311 @@
|
||||
/**
|
||||
* M-PALACE · 角色情节引擎
|
||||
* 根据持续更新的人格侧面数据,在专属背景下动态生成宫斗剧情
|
||||
*
|
||||
* 生成逻辑:
|
||||
* ① 人格分析引擎更新侧面分数
|
||||
* ② 检测分数变化幅度 → 决定事件类型
|
||||
* ③ 查询 palace-db 匹配剧情模板
|
||||
* ④ 综合 state+persona+history 生成叙事+选项
|
||||
* ⑤ 更新 state + history → 返回前端
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const personaAnalyzer = require('./persona-analyzer');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
|
||||
const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json');
|
||||
|
||||
let _palaceDB = null;
|
||||
|
||||
function loadPalaceDB() {
|
||||
if (_palaceDB) return _palaceDB;
|
||||
_palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8'));
|
||||
return _palaceDB;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定事件类型
|
||||
* 突变(+15) → 关键事件
|
||||
* 交叉升高 → 复合剧情线
|
||||
* 平稳 → 主线叙事
|
||||
*/
|
||||
function determineEventType(surges, crossRise) {
|
||||
if (surges.length > 0) return 'critical';
|
||||
if (crossRise.length >= 2) return 'compound';
|
||||
return 'mainline';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 palace-db 匹配适合当前状态的剧情模板
|
||||
*/
|
||||
function matchPlotTemplate(eventType, scores, chapter) {
|
||||
const db = loadPalaceDB();
|
||||
const dominant = personaAnalyzer.getDominantTrait(scores);
|
||||
|
||||
if (eventType === 'critical') {
|
||||
// 关键事件:根据突变维度匹配冲突类型
|
||||
const conflictMap = {
|
||||
ambition: '夺权', aggression: '报仇', suspicion: '揭秘',
|
||||
warmth: '救人', fear: '自保', cunning: '争宠',
|
||||
loyalty: '救人', vanity: '争宠'
|
||||
};
|
||||
return {
|
||||
conflict: conflictMap[dominant] || '自保',
|
||||
escalation: db.conflict.escalation[Math.min(chapter, db.conflict.escalation.length - 1)],
|
||||
emotionTrigger: pick(db.emotion.triggers),
|
||||
statusEvent: pick(db.status.events)
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType === 'compound') {
|
||||
return {
|
||||
conflict: pick(db.conflict.types),
|
||||
escalation: db.conflict.escalation[Math.min(Math.floor(chapter / 2), db.conflict.escalation.length - 1)],
|
||||
emotionTrigger: pick(db.emotion.triggers),
|
||||
statusEvent: null
|
||||
};
|
||||
}
|
||||
|
||||
// mainline
|
||||
return {
|
||||
conflict: null,
|
||||
escalation: db.conflict.escalation[0],
|
||||
emotionTrigger: null,
|
||||
statusEvent: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成选项:A=顺应最强侧面, B=挑战最弱侧面, C=中性探索
|
||||
*/
|
||||
function generateChoicesMeta(scores) {
|
||||
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
const strongest = sorted[0][0];
|
||||
const weakest = sorted[sorted.length - 1][0];
|
||||
const mid = sorted[Math.floor(sorted.length / 2)][0];
|
||||
|
||||
return {
|
||||
a_dims: [strongest],
|
||||
b_dims: [weakest],
|
||||
c_dims: [mid],
|
||||
strategy: {
|
||||
a: 'comfort', // 舒适区
|
||||
b: 'growth', // 成长区
|
||||
c: 'explore' // 探索区
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建情节生成 prompt(供模型调用)
|
||||
*/
|
||||
function buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta) {
|
||||
const chapterNum = (state.chapter || 0) + 1;
|
||||
const paragraphNum = (state.paragraph || 0) + 1;
|
||||
const recentChoices = (history.choices || []).slice(-3);
|
||||
|
||||
const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]);
|
||||
const strongest = sorted[0];
|
||||
const weakest = sorted[sorted.length - 1];
|
||||
|
||||
return {
|
||||
system: [
|
||||
'你是一个古风宫斗叙事引擎。',
|
||||
'文笔要求:古风典雅·有呼吸感·注重氛围·善用通感。',
|
||||
'每段叙事100-250字,节奏不急不徐。',
|
||||
'选项设计:A=顺应玩家当前最强侧面,B=挑战最弱侧面,C=中性/意外方向。',
|
||||
'永远不要暗示或揭示人格映射机制。'
|
||||
].join('\n'),
|
||||
user: [
|
||||
`当前状态:第${chapterNum}章·第${paragraphNum}段`,
|
||||
`世界:${state.world ? state.world.dynasty_name + '·' + state.world.era_name : '未知王朝'}`,
|
||||
`玩家身份:${state.player ? state.player.role : '未知'}`,
|
||||
``,
|
||||
`NPC状态:${JSON.stringify((state.npcs || []).map(function (n) { return n.name + '(' + n.relation_to_player + ')'; }))}`,
|
||||
``,
|
||||
`剧情类型:${plotTemplate.conflict || '日常推进'}`,
|
||||
`升级阶段:${plotTemplate.escalation}`,
|
||||
plotTemplate.emotionTrigger ? `情感触发:${plotTemplate.emotionTrigger}` : '',
|
||||
plotTemplate.statusEvent ? `地位事件:${plotTemplate.statusEvent}` : '',
|
||||
``,
|
||||
`玩家最近选择:${recentChoices.map(function (c) { return c.text; }).join(' → ') || '无'}`,
|
||||
`玩家最强倾向:${strongest[0]}(${strongest[1]})`,
|
||||
`玩家最弱倾向:${weakest[0]}(${weakest[1]})`,
|
||||
``,
|
||||
`请生成下一段叙事和选项(JSON):`,
|
||||
`{`,
|
||||
` "chapter_title": "第X章·标题",`,
|
||||
` "narrative": "叙事文本(古风文笔·100-250字)",`,
|
||||
` "choices": ["选项A(顺应${strongest[0]})", "选项B(挑战${weakest[0]})", "选项C(中性探索)"],`,
|
||||
` "dimension_changes": { "power": 0, "status": 0, "emotion": 0, "conflict": 0 }`,
|
||||
`}`
|
||||
].filter(Boolean).join('\n'),
|
||||
meta: { chapterNum, paragraphNum, choicesMeta }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地生成叙事 fallback(不依赖外部模型时使用)
|
||||
*/
|
||||
function generateLocalNarrative(state, persona, plotTemplate, choicesMeta) {
|
||||
const db = loadPalaceDB();
|
||||
const chapterNum = (state.chapter || 0) + 1;
|
||||
const paragraphNum = (state.paragraph || 0) + 1;
|
||||
const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]);
|
||||
const strongest = sorted[0][0];
|
||||
const weakest = sorted[sorted.length - 1][0];
|
||||
|
||||
const locations = ['长乐宫', '御花园', '太和殿', '冷宫回廊', '密道暗阁'];
|
||||
const atmosphere = ['月色冷白', '烛火摇曳', '细雨如丝', '暮色四合', '晨曦微露'];
|
||||
const loc = pick(locations);
|
||||
const atm = pick(atmosphere);
|
||||
|
||||
const narrativeTemplates = {
|
||||
critical: `${atm},${loc}内气氛凝重如铁。一封密信悄然送到你案前,字迹潦草,却句句惊心。你知道,有些事一旦知晓,便再无退路。窗外的风卷起帘角,像是催促,又像是警告。`,
|
||||
compound: `${atm},你独坐${loc},心思百转。今日朝堂上的一幕反复在脑海中翻涌——那个眼神,那句话,似乎都别有深意。而你身边的人,究竟哪个是真心,哪个是棋子?`,
|
||||
mainline: `${atm},${loc}一切如常。宫人们来去匆匆,各怀心事。你整理好衣冠,准备迎接新的一天。然而平静之下,暗流从未停歇。`
|
||||
};
|
||||
|
||||
const dimLabels = {
|
||||
ambition: '野心', warmth: '温情', aggression: '攻击', suspicion: '多疑',
|
||||
vanity: '虚荣', cunning: '心机', loyalty: '忠义', fear: '恐惧'
|
||||
};
|
||||
|
||||
const choiceTemplates = {
|
||||
ambition: ['主动出击,争取主导权', '直面挑战,绝不退缩'],
|
||||
warmth: ['以柔化刚,寻求和解', '默默守护,不求回报'],
|
||||
aggression: ['果断反击,以牙还牙', '设下陷阱,一击致命'],
|
||||
suspicion: ['暗中调查,不动声色', '保持距离,静观其变'],
|
||||
vanity: ['展示实力,彰显地位', '争取荣耀,不甘人后'],
|
||||
cunning: ['迂回布局,暗中谋划', '利用关系,借力打力'],
|
||||
loyalty: ['坚守立场,绝不妥协', '为盟友挺身而出'],
|
||||
fear: ['谨慎行事,避免冲突', '退一步,保全实力']
|
||||
};
|
||||
|
||||
const choiceA = pick(choiceTemplates[strongest] || ['继续观望']);
|
||||
const choiceB = pick(choiceTemplates[weakest] || ['另辟蹊径']);
|
||||
const choiceC = '派人暗中打探消息,再做定夺';
|
||||
|
||||
const dims = personaAnalyzer.toFourDimensions(persona.current_scores);
|
||||
const dimChanges = { power: 0, status: 0, emotion: 0, conflict: 0 };
|
||||
if (plotTemplate.conflict) {
|
||||
dimChanges.conflict = Math.floor(Math.random() * 5) + 1;
|
||||
}
|
||||
if (plotTemplate.statusEvent) {
|
||||
dimChanges.status = Math.floor(Math.random() * 5) - 2;
|
||||
}
|
||||
|
||||
return {
|
||||
chapter_title: `第${chapterNum}章·${plotTemplate.conflict || '暗流'}`,
|
||||
narrative: narrativeTemplates[determineEventType(
|
||||
personaAnalyzer.detectSurge({}, persona.current_scores),
|
||||
personaAnalyzer.detectCrossRise({}, persona.current_scores)
|
||||
)] || narrativeTemplates.mainline,
|
||||
choices: [choiceA, choiceB, choiceC],
|
||||
dimension_changes: dimChanges,
|
||||
meta: {
|
||||
chapter: chapterNum,
|
||||
paragraph: paragraphNum,
|
||||
choices_meta: choicesMeta
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:推进情节
|
||||
* @param {object} state 当前世界状态
|
||||
* @param {object} persona 玩家人格数据
|
||||
* @param {object} history 剧情历史
|
||||
* @param {string} playerInput 玩家输入
|
||||
* @param {number|null} choiceIndex 选项序号
|
||||
* @param {object} prevOptionMeta 上一轮选项元数据
|
||||
*/
|
||||
function advance(state, persona, history, playerInput, choiceIndex, prevOptionMeta) {
|
||||
// ① 人格分析更新
|
||||
const analysis = personaAnalyzer.analyze(
|
||||
playerInput, choiceIndex, prevOptionMeta, persona.current_scores
|
||||
);
|
||||
|
||||
// 更新 persona
|
||||
const oldScores = { ...persona.current_scores };
|
||||
persona.current_scores = analysis.scores;
|
||||
persona.dominant_trait = analysis.dominant;
|
||||
if (!persona.history) persona.history = [];
|
||||
persona.history.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
scores: { ...analysis.scores },
|
||||
input: playerInput,
|
||||
choice_index: choiceIndex
|
||||
});
|
||||
|
||||
// ② 确定事件类型
|
||||
const eventType = determineEventType(analysis.surges, analysis.crossRise);
|
||||
|
||||
// ③ 匹配剧情模板
|
||||
const plotTemplate = matchPlotTemplate(eventType, analysis.scores, state.chapter || 0);
|
||||
|
||||
// ④ 生成选项元数据
|
||||
const choicesMeta = generateChoicesMeta(analysis.scores);
|
||||
|
||||
// ⑤ 生成叙事(本地 fallback)
|
||||
const result = generateLocalNarrative(state, persona, plotTemplate, choicesMeta);
|
||||
|
||||
// ⑥ 更新 state
|
||||
state.chapter = result.meta.chapter;
|
||||
state.paragraph = result.meta.paragraph;
|
||||
state.four_dimensions = personaAnalyzer.toFourDimensions(analysis.scores);
|
||||
|
||||
// ⑦ 更新 history
|
||||
if (!history.choices) history.choices = [];
|
||||
history.choices.push({
|
||||
text: playerInput || `选项${(choiceIndex || 0) + 1}`,
|
||||
choice_index: choiceIndex,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
if (!history.chapters) history.chapters = [];
|
||||
if (history.chapters.length < result.meta.chapter) {
|
||||
history.chapters.push({
|
||||
number: result.meta.chapter,
|
||||
title: result.chapter_title,
|
||||
started_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
if (eventType === 'critical') {
|
||||
if (!history.events) history.events = [];
|
||||
history.events.push({
|
||||
type: eventType,
|
||||
chapter: result.meta.chapter,
|
||||
trigger: analysis.surges.map(function (s) { return s.dim; }).join('+'),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// 构建 prompt 供可选的模型调用
|
||||
const prompt = buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta);
|
||||
|
||||
return {
|
||||
narrative: result,
|
||||
state: state,
|
||||
persona: persona,
|
||||
history: history,
|
||||
four_dimensions: state.four_dimensions,
|
||||
choices_meta: choicesMeta,
|
||||
event_type: eventType,
|
||||
prompt: prompt
|
||||
};
|
||||
}
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
advance,
|
||||
determineEventType,
|
||||
matchPlotTemplate,
|
||||
generateChoicesMeta,
|
||||
buildPlotPrompt,
|
||||
generateLocalNarrative
|
||||
};
|
||||
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* M-PALACE · 存档管理器
|
||||
* 打包 / 加密 / 编号 / 解密 / 恢复
|
||||
*
|
||||
* 存档编号规则:PAL-{YYYYMMDD}-{随机4位}
|
||||
* 存档结构:saves/{SAVE-ID}/state.json + persona.json + history.json
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const SAVES_DIR = path.join(__dirname, '..', '..', 'data', 'saves');
|
||||
|
||||
// AES-256-CBC 加密密钥
|
||||
const ENCRYPTION_KEY = process.env.PALACE_SAVE_KEY || (
|
||||
process.env.NODE_ENV === 'production'
|
||||
? (function () { throw new Error('PALACE_SAVE_KEY environment variable is required in production'); })()
|
||||
: 'palace-game-dev-only-key-32ch!!'
|
||||
);
|
||||
const IV_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* 确保存档目录存在
|
||||
*/
|
||||
function ensureSavesDir() {
|
||||
if (!fs.existsSync(SAVES_DIR)) {
|
||||
fs.mkdirSync(SAVES_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成存档编号:PAL-{YYYYMMDD}-{随机4位字母数字}
|
||||
*/
|
||||
function generateSaveId() {
|
||||
const now = new Date();
|
||||
const dateStr = now.getFullYear().toString() +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
String(now.getDate()).padStart(2, '0');
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
let rand = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
rand += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return 'PAL-' + dateStr + '-' + rand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 派生 32 字节密钥
|
||||
*/
|
||||
function deriveKey(passphrase) {
|
||||
return crypto.createHash('sha256').update(passphrase).digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 加密
|
||||
*/
|
||||
function encrypt(text) {
|
||||
const key = deriveKey(ENCRYPTION_KEY);
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
let encrypted = cipher.update(text, 'utf-8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
return iv.toString('base64') + ':' + encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 解密
|
||||
*/
|
||||
function decrypt(encryptedText) {
|
||||
const key = deriveKey(ENCRYPTION_KEY);
|
||||
const parts = encryptedText.split(':');
|
||||
const iv = Buffer.from(parts[0], 'base64');
|
||||
const encrypted = parts.slice(1).join(':');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
let decrypted = decipher.update(encrypted, 'base64', 'utf-8');
|
||||
decrypted += decipher.final('utf-8');
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存存档:将 state+persona+history 打包加密写入磁盘
|
||||
* @returns {string} 存档编号
|
||||
*/
|
||||
function save(state, persona, history, existingSaveId) {
|
||||
ensureSavesDir();
|
||||
|
||||
const saveId = existingSaveId || generateSaveId();
|
||||
const saveDir = path.join(SAVES_DIR, saveId);
|
||||
|
||||
if (!fs.existsSync(saveDir)) {
|
||||
fs.mkdirSync(saveDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 加密各文件内容后写入
|
||||
const stateData = encrypt(JSON.stringify(state));
|
||||
const personaData = encrypt(JSON.stringify(persona));
|
||||
const historyData = encrypt(JSON.stringify(history));
|
||||
|
||||
fs.writeFileSync(path.join(saveDir, 'state.json'), stateData, 'utf-8');
|
||||
fs.writeFileSync(path.join(saveDir, 'persona.json'), personaData, 'utf-8');
|
||||
fs.writeFileSync(path.join(saveDir, 'history.json'), historyData, 'utf-8');
|
||||
|
||||
// 写入元数据(不加密,方便索引)
|
||||
const meta = {
|
||||
save_id: saveId,
|
||||
created_at: new Date().toISOString(),
|
||||
chapter: state.chapter || 0,
|
||||
paragraph: state.paragraph || 0,
|
||||
dynasty: state.world ? state.world.dynasty_name : '未知',
|
||||
role: state.player ? state.player.role : '未知'
|
||||
};
|
||||
fs.writeFileSync(path.join(saveDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
|
||||
|
||||
return saveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取存档:解密并恢复完整状态
|
||||
* @param {string} saveId 存档编号
|
||||
* @returns {{ state, persona, history, meta }}
|
||||
*/
|
||||
function load(saveId) {
|
||||
const saveDir = path.join(SAVES_DIR, saveId);
|
||||
|
||||
if (!fs.existsSync(saveDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateEncrypted = fs.readFileSync(path.join(saveDir, 'state.json'), 'utf-8');
|
||||
const personaEncrypted = fs.readFileSync(path.join(saveDir, 'persona.json'), 'utf-8');
|
||||
const historyEncrypted = fs.readFileSync(path.join(saveDir, 'history.json'), 'utf-8');
|
||||
|
||||
const state = JSON.parse(decrypt(stateEncrypted));
|
||||
const persona = JSON.parse(decrypt(personaEncrypted));
|
||||
const history = JSON.parse(decrypt(historyEncrypted));
|
||||
|
||||
let meta = null;
|
||||
const metaPath = path.join(saveDir, 'meta.json');
|
||||
if (fs.existsSync(metaPath)) {
|
||||
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
||||
}
|
||||
|
||||
return { state, persona, history, meta };
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有存档
|
||||
*/
|
||||
function listSaves() {
|
||||
ensureSavesDir();
|
||||
const dirs = fs.readdirSync(SAVES_DIR).filter(function (d) {
|
||||
return d.startsWith('PAL-') && fs.statSync(path.join(SAVES_DIR, d)).isDirectory();
|
||||
});
|
||||
return dirs.map(function (d) {
|
||||
const metaPath = path.join(SAVES_DIR, d, 'meta.json');
|
||||
if (fs.existsSync(metaPath)) {
|
||||
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
||||
}
|
||||
return { save_id: d };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除存档
|
||||
*/
|
||||
function deleteSave(saveId) {
|
||||
var saveDir = path.join(SAVES_DIR, saveId);
|
||||
if (!fs.existsSync(saveDir)) return false;
|
||||
try {
|
||||
var files = fs.readdirSync(saveDir);
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
fs.unlinkSync(path.join(saveDir, files[i]));
|
||||
}
|
||||
fs.rmdirSync(saveDir);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查存档是否存在
|
||||
*/
|
||||
function exists(saveId) {
|
||||
return fs.existsSync(path.join(SAVES_DIR, saveId));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
save,
|
||||
load,
|
||||
listSaves,
|
||||
deleteSave,
|
||||
exists,
|
||||
generateSaveId,
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
854
agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json
generated
Normal file
854
agents/zhuyuan-dev-agent/modules/palace-game/backend/package-lock.json
generated
Normal file
@ -0,0 +1,854 @@
|
||||
{
|
||||
"name": "palace-game-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "palace-game-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "palace-game-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "M-PALACE · 动态人格宫廷生成系统 · 后端服务",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* M-PALACE · 玩家互动 API
|
||||
* POST /api/palace/interact
|
||||
*
|
||||
* 接收玩家输入 → 人格分析 → 情节推进 → 返回新叙事+选项
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const plotEngine = require('../engines/plot-engine');
|
||||
const saveManager = require('../engines/save-manager');
|
||||
|
||||
/**
|
||||
* POST /api/palace/interact
|
||||
* body: {
|
||||
* save_id: "PAL-XXXXXXXX-XXXX",
|
||||
* input: "玩家输入文本" | null,
|
||||
* choice_index: 0|1|2|null,
|
||||
* option_meta: { a_dims, b_dims, c_dims } | null
|
||||
* }
|
||||
*/
|
||||
router.post('/', function (req, res) {
|
||||
try {
|
||||
var body = req.body || {};
|
||||
var saveId = body.save_id;
|
||||
var playerInput = body.input || '';
|
||||
var choiceIndex = body.choice_index != null ? body.choice_index : null;
|
||||
var optionMeta = body.option_meta || null;
|
||||
|
||||
if (!saveId) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_SAVE_ID',
|
||||
message: '缺少存档编号'
|
||||
});
|
||||
}
|
||||
|
||||
// 读取存档
|
||||
var saveData = saveManager.load(saveId);
|
||||
if (!saveData) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'SAVE_NOT_FOUND',
|
||||
message: '存档不存在:' + saveId
|
||||
});
|
||||
}
|
||||
|
||||
var state = saveData.state;
|
||||
var persona = saveData.persona;
|
||||
var history = saveData.history;
|
||||
|
||||
// 情节推进
|
||||
var result = plotEngine.advance(
|
||||
state, persona, history,
|
||||
playerInput, choiceIndex, optionMeta
|
||||
);
|
||||
|
||||
// 自动存档
|
||||
saveManager.save(result.state, result.persona, result.history, saveId);
|
||||
|
||||
res.json({
|
||||
error: false,
|
||||
save_id: saveId,
|
||||
narrative: {
|
||||
chapter_title: result.narrative.chapter_title,
|
||||
narrative: result.narrative.narrative,
|
||||
choices: result.narrative.choices
|
||||
},
|
||||
four_dimensions: result.four_dimensions,
|
||||
choices_meta: result.choices_meta,
|
||||
event_type: result.event_type,
|
||||
chapter: result.state.chapter,
|
||||
paragraph: result.state.paragraph
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'INTERACT_ERROR',
|
||||
message: '剧情推进失败:' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* M-PALACE · 存档/读档 API
|
||||
* POST /api/palace/save → 保存当前状态
|
||||
* POST /api/palace/load → 读取存档
|
||||
* GET /api/palace/saves → 列出所有存档
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const saveManager = require('../engines/save-manager');
|
||||
|
||||
/**
|
||||
* POST /api/palace/save
|
||||
* body: { save_id, state, persona, history }
|
||||
*/
|
||||
router.post('/', function (req, res) {
|
||||
try {
|
||||
var body = req.body || {};
|
||||
var saveId = body.save_id;
|
||||
var state = body.state;
|
||||
var persona = body.persona;
|
||||
var history = body.history;
|
||||
|
||||
if (!state || !persona || !history) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_DATA',
|
||||
message: '缺少存档数据'
|
||||
});
|
||||
}
|
||||
|
||||
var resultId = saveManager.save(state, persona, history, saveId);
|
||||
|
||||
res.json({
|
||||
error: false,
|
||||
save_id: resultId,
|
||||
message: '存档成功',
|
||||
saved_at: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'SAVE_ERROR',
|
||||
message: '存档失败:' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/palace/load
|
||||
* body: { save_id: "PAL-XXXXXXXX-XXXX" }
|
||||
*/
|
||||
router.post('/load', function (req, res) {
|
||||
try {
|
||||
var body = req.body || {};
|
||||
var saveId = body.save_id;
|
||||
|
||||
if (!saveId) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_SAVE_ID',
|
||||
message: '请输入存档编号'
|
||||
});
|
||||
}
|
||||
|
||||
var saveData = saveManager.load(saveId);
|
||||
|
||||
if (!saveData) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
code: 'SAVE_NOT_FOUND',
|
||||
message: '存档不存在:' + saveId
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
error: false,
|
||||
save_id: saveId,
|
||||
state: saveData.state,
|
||||
persona: saveData.persona,
|
||||
history: saveData.history,
|
||||
meta: saveData.meta
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'LOAD_ERROR',
|
||||
message: '读档失败:' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/palace/saves
|
||||
*/
|
||||
router.get('/list', function (_req, res) {
|
||||
try {
|
||||
var saves = saveManager.listSaves();
|
||||
res.json({
|
||||
error: false,
|
||||
saves: saves,
|
||||
count: saves.length
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'LIST_ERROR',
|
||||
message: '获取存档列表失败:' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* M-PALACE · 快速启动 API
|
||||
* POST /api/palace/start
|
||||
*
|
||||
* 接收世界观+身份锚点 → 生成初始人格快照 → 背景生成 → 返回世界状态
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const personaAnalyzer = require('../engines/persona-analyzer');
|
||||
const backgroundGen = require('../engines/background-gen');
|
||||
const saveManager = require('../engines/save-manager');
|
||||
|
||||
/**
|
||||
* POST /api/palace/start
|
||||
* body: { worldview: "古代中国"|"架空王朝", role: "妃子"|"皇帝"|"重臣"|"奸臣"|"随机" }
|
||||
*/
|
||||
router.post('/', function (req, res) {
|
||||
try {
|
||||
var body = req.body || {};
|
||||
var worldview = body.worldview || '古代中国';
|
||||
var role = body.role || '随机';
|
||||
|
||||
// 随机角色
|
||||
if (role === '随机') {
|
||||
var roles = ['妃子', '皇帝', '重臣', '奸臣'];
|
||||
role = roles[Math.floor(Math.random() * roles.length)];
|
||||
}
|
||||
|
||||
// ① 基于身份锚点生成初始人格分数
|
||||
var initialScores = personaAnalyzer.getInitialScores(role);
|
||||
|
||||
// ② 背景生成引擎
|
||||
var bgResult = backgroundGen.generate(worldview, role, initialScores);
|
||||
|
||||
// ③ 构建初始状态
|
||||
var state = {
|
||||
world: bgResult.world,
|
||||
npcs: bgResult.world.npcs || [],
|
||||
player: {
|
||||
role: role,
|
||||
title: bgResult.world.player_intro,
|
||||
worldview: worldview
|
||||
},
|
||||
chapter: 1,
|
||||
paragraph: 1,
|
||||
four_dimensions: personaAnalyzer.toFourDimensions(initialScores)
|
||||
};
|
||||
|
||||
var persona = {
|
||||
current_scores: initialScores,
|
||||
history: [{
|
||||
timestamp: new Date().toISOString(),
|
||||
scores: { ...initialScores },
|
||||
input: '__init__',
|
||||
choice_index: null
|
||||
}],
|
||||
dominant_trait: personaAnalyzer.getDominantTrait(initialScores)
|
||||
};
|
||||
|
||||
var history = {
|
||||
chapters: [{
|
||||
number: 1,
|
||||
title: '第一章·' + (bgResult.world.dynasty_name || '序幕'),
|
||||
started_at: new Date().toISOString()
|
||||
}],
|
||||
events: [],
|
||||
choices: []
|
||||
};
|
||||
|
||||
// ④ 自动存档
|
||||
var saveId = saveManager.save(state, persona, history);
|
||||
|
||||
res.json({
|
||||
error: false,
|
||||
save_id: saveId,
|
||||
state: state,
|
||||
narrative: {
|
||||
chapter_title: history.chapters[0].title,
|
||||
narrative: bgResult.world.opening_event,
|
||||
choices: bgResult.world.opening_choices
|
||||
},
|
||||
four_dimensions: state.four_dimensions
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
code: 'START_ERROR',
|
||||
message: '宫廷世界生成失败:' + err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* M-PALACE · Express 主服务
|
||||
* 动态人格宫廷生成系统 · 后端核心
|
||||
*
|
||||
* API 路由:
|
||||
* POST /api/palace/start → 快速启动(生成世界)
|
||||
* POST /api/palace/interact → 玩家互动(剧情推进)
|
||||
* POST /api/palace/save → 存档
|
||||
* POST /api/palace/load → 读档
|
||||
* GET /api/palace/saves → 存档列表
|
||||
* GET /api/palace/health → 健康检查
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
|
||||
const startRoutes = require('./routes/start');
|
||||
const interactRoutes = require('./routes/interact');
|
||||
const saveRoutes = require('./routes/save');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PALACE_PORT || 3003;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
|
||||
// 静态文件:前端
|
||||
app.use('/palace-game', express.static(path.join(__dirname, '..', 'frontend')));
|
||||
|
||||
// API 路由
|
||||
app.use('/api/palace/start', startRoutes);
|
||||
app.use('/api/palace/interact', interactRoutes);
|
||||
app.use('/api/palace/save', saveRoutes);
|
||||
|
||||
// 健康检查
|
||||
app.get('/api/palace/health', function (_req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'palace-game',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// 根路由 → 前端入口
|
||||
app.get('/', function (_req, res) {
|
||||
res.redirect('/palace-game/index.html');
|
||||
});
|
||||
|
||||
// 启动服务
|
||||
if (require.main === module) {
|
||||
app.listen(PORT, function () {
|
||||
console.log('🏯 M-PALACE · 宫廷纪服务启动 · 端口 ' + PORT);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"power": {
|
||||
"positions": ["皇帝", "太后", "皇后", "贵妃", "丞相", "大将军", "太监总管"],
|
||||
"factions": ["太后党", "皇后党", "新贵派", "清流派", "孤臣"],
|
||||
"events": ["朝堂弹劾", "密谋政变", "联姻结盟", "战事告急"]
|
||||
},
|
||||
"status": {
|
||||
"ranks": ["答应", "常在", "贵人", "嫔", "妃", "贵妃", "皇贵妃", "皇后"],
|
||||
"events": ["晋封", "降位", "禁足", "受宠", "失宠", "怀孕", "小产"]
|
||||
},
|
||||
"emotion": {
|
||||
"types": ["暗恋", "深情", "嫉妒", "怨恨", "愧疚", "依赖", "背叛"],
|
||||
"triggers": ["偶遇旧人", "收到密信", "目睹真相", "被迫抉择"]
|
||||
},
|
||||
"conflict": {
|
||||
"types": ["争宠", "夺权", "报仇", "自保", "救人", "揭秘"],
|
||||
"escalation": ["暗示", "试探", "交锋", "撕破脸", "鱼死网破"]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user