data&device ready
This commit is contained in:
265
scripts/app.js
Normal file
265
scripts/app.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* TalkingQ Platform - App Core
|
||||
* Router, sidebar, topbar clock, toast notifications
|
||||
*/
|
||||
|
||||
// ─── Toast ──────────────────────────────────────────────────────────────────
|
||||
const Toast = (() => {
|
||||
let container = null;
|
||||
function getContainer() {
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function show(message, type = 'success', duration = 3000) {
|
||||
const icons = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' };
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.innerHTML = `
|
||||
<i data-lucide="${icons[type] || 'info'}" style="width:16px;height:16px;flex-shrink:0"></i>
|
||||
<span style="font-size:13.5px">${message}</span>
|
||||
`;
|
||||
getContainer().appendChild(toast);
|
||||
lucide.createIcons({ elements: [toast] });
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
toast.style.transition = 'all 0.3s';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return { show,
|
||||
success: (m, duration) => show(m, 'success', duration),
|
||||
error: (m, duration) => show(m, 'error', duration),
|
||||
warning: (m, duration) => show(m, 'warning', duration),
|
||||
info: (m, duration) => show(m, 'info', duration),
|
||||
};
|
||||
})();
|
||||
|
||||
// ─── Router ──────────────────────────────────────────────────────────────────
|
||||
const pageNames = {
|
||||
'login': '登录',
|
||||
'dashboard': '数据仪表盘',
|
||||
'agent-dev': '对话应用配置',
|
||||
'agent-manage': '应用管理',
|
||||
'device': '设备管理',
|
||||
'ota': 'OTA 升级',
|
||||
'session': '会话详情',
|
||||
'badcase': 'Bad Case 管理',
|
||||
'rbac': '权限管理 (RBAC)',
|
||||
'privacy': '隐私保护 (PII)',
|
||||
'settings': '系统设置',
|
||||
'knowledge-base': '知识库管理',
|
||||
'kb-create': '创建知识库',
|
||||
'kb-detail': '知识库详情',
|
||||
};
|
||||
|
||||
const pageRenderers = {};
|
||||
|
||||
function registerPage(id, renderer) {
|
||||
pageRenderers[id] = renderer;
|
||||
}
|
||||
|
||||
function navigateTo(pageId) {
|
||||
// Auth check - login page doesn't need auth
|
||||
if (pageId !== 'login' && !checkAuth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const topbar = document.querySelector('.topbar');
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
const container = document.getElementById('pageContainer');
|
||||
|
||||
// Handle login page - full screen, hide sidebar and topbar
|
||||
if (pageId === 'login') {
|
||||
document.body.classList.add('login-mode');
|
||||
|
||||
if (pageRenderers[pageId]) {
|
||||
container.innerHTML = `<div class="page active" id="page-${pageId}"></div>`;
|
||||
pageRenderers[pageId](document.getElementById(`page-${pageId}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore normal layout for other pages
|
||||
document.body.classList.remove('login-mode');
|
||||
|
||||
// Update nav
|
||||
document.querySelectorAll('.nav-item').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.page === pageId);
|
||||
});
|
||||
|
||||
// Update breadcrumb
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
if (breadcrumb) breadcrumb.textContent = pageNames[pageId] || pageId;
|
||||
|
||||
// Render page
|
||||
if (!container) return;
|
||||
|
||||
if (pageRenderers[pageId]) {
|
||||
container.innerHTML = `<div class="page active" id="page-${pageId}"></div>`;
|
||||
pageRenderers[pageId](document.getElementById(`page-${pageId}`));
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="page active">
|
||||
<div class="empty-state">
|
||||
<i data-lucide="construction" style="width:48px;height:48px;color:#d9d9d9"></i>
|
||||
<p style="font-size:16px;font-weight:600;margin-bottom:8px">功能开发中</p>
|
||||
<p>该页面正在建设中,敬请期待</p>
|
||||
</div>
|
||||
</div>`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Topbar Clock ────────────────────────────────────────────────────────────
|
||||
function updateClock() {
|
||||
const el = document.getElementById('topbarTime');
|
||||
if (!el) return;
|
||||
const now = new Date();
|
||||
el.textContent = now.toLocaleString('zh-CN', {
|
||||
month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Sidebar Toggle ──────────────────────────────────────────────────────────
|
||||
function initSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggleBtn = document.getElementById('sidebarToggle');
|
||||
const menuBtn = document.getElementById('menuBtn');
|
||||
|
||||
toggleBtn?.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
});
|
||||
|
||||
menuBtn?.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
} else {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Nav Click ───────────────────────────────────────────────────────────────
|
||||
function initNav() {
|
||||
document.querySelectorAll('.nav-item[data-page]').forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
navigateTo(el.dataset.page);
|
||||
if (window.innerWidth <= 768) {
|
||||
document.getElementById('sidebar')?.classList.remove('mobile-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Modal Helper ────────────────────────────────────────────────────────────
|
||||
function createModal({ title, content, onConfirm, confirmText = '确认', cancelText = '取消', size = '', showCancel = true }) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
const sizeClass = size ? ` ${String(size).startsWith('modal-') ? size : `modal-${size}`}` : '';
|
||||
overlay.innerHTML = `
|
||||
<div class="modal${sizeClass}">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">${title}</span>
|
||||
<button class="modal-close"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">${content}</div>
|
||||
<div class="modal-footer">
|
||||
${showCancel ? `<button class="btn btn-outline cancel-btn">${cancelText}</button>` : ''}
|
||||
${onConfirm ? `<button class="btn btn-primary confirm-btn">${confirmText}</button>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
lucide.createIcons({ elements: [overlay] });
|
||||
|
||||
const close = () => {
|
||||
overlay.classList.remove('open');
|
||||
setTimeout(() => overlay.remove(), 200);
|
||||
};
|
||||
|
||||
overlay.querySelector('.modal-close').addEventListener('click', close);
|
||||
overlay.querySelector('.cancel-btn')?.addEventListener('click', close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
|
||||
|
||||
if (onConfirm) {
|
||||
overlay.querySelector('.confirm-btn').addEventListener('click', async () => {
|
||||
const confirmBtn = overlay.querySelector('.confirm-btn');
|
||||
confirmBtn.disabled = true;
|
||||
try {
|
||||
const result = await onConfirm(overlay.querySelector('.modal-body'));
|
||||
if (result === false) return;
|
||||
close();
|
||||
} finally {
|
||||
if (document.body.contains(overlay)) confirmBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => overlay.classList.add('open'));
|
||||
return { overlay, close };
|
||||
}
|
||||
|
||||
// ─── Pagination Helper ───────────────────────────────────────────────────────
|
||||
function renderPagination(container, { total, page, pageSize, onChange }) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
container.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;justify-content:flex-end;margin-top:16px">
|
||||
<span style="font-size:13px;color:var(--text-muted)">共 ${total} 条</span>
|
||||
<button class="btn btn-outline btn-sm" ${page <= 1 ? 'disabled' : ''} data-page="${page-1}">上一页</button>
|
||||
<span style="font-size:13px;padding:0 4px">${page} / ${totalPages}</span>
|
||||
<button class="btn btn-outline btn-sm" ${page >= totalPages ? 'disabled' : ''} data-page="${page+1}">下一页</button>
|
||||
</div>`;
|
||||
container.querySelectorAll('[data-page]').forEach(btn => {
|
||||
btn.addEventListener('click', () => !btn.disabled && onChange(parseInt(btn.dataset.page)));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Init ────────────────────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSidebar();
|
||||
initNav();
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
lucide.createIcons();
|
||||
|
||||
// Check auth and navigate
|
||||
const loggedIn = sessionStorage.getItem('talkingq_logged_in');
|
||||
const targetPage = loggedIn === 'true' ? 'dashboard' : 'login';
|
||||
|
||||
// Use a small delay to ensure DOM is fully ready
|
||||
setTimeout(() => {
|
||||
navigateTo(targetPage);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Auth check helper
|
||||
function checkAuth() {
|
||||
return sessionStorage.getItem('talkingq_logged_in') === 'true';
|
||||
}
|
||||
|
||||
// Logout helper
|
||||
function doLogout() {
|
||||
sessionStorage.removeItem('talkingq_user');
|
||||
sessionStorage.removeItem('talkingq_logged_in');
|
||||
Toast.info('已退出登录');
|
||||
setTimeout(() => {
|
||||
navigateTo('login');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
window.Toast = Toast;
|
||||
window.navigateTo = navigateTo;
|
||||
window.registerPage = registerPage;
|
||||
window.createModal = createModal;
|
||||
window.renderPagination = renderPagination;
|
||||
window.checkAuth = checkAuth;
|
||||
Reference in New Issue
Block a user