feat(tenant): multi-tenant domain routing endpoint + shell_theme dedup + dim_test path

This commit is contained in:
yumoqing 2026-08-04 12:11:25 +08:00
parent 447078a1b4
commit a6ba382241
4 changed files with 1121 additions and 2 deletions

View File

@ -10,6 +10,7 @@ paths="""/ any
/i18n_getmsgs any
/tenant any
/tenant/index.ui any
/get_domain_info.dspy any
/uapi logined
/uapi/uapi logined
/uapi/uapi/delete_uapi.dspy logined
@ -54,6 +55,7 @@ paths="""/ any
/llmage/show_same_catelog_llm.ui logined
/llmage/model_estimate.dspy logined
/llmage/model_plaza.ui logined
/llmage/dim_test.ui logined
/llmage/plaza_model_cards.ui logined
/llmage/api/get_plaza_models.dspy logined
/llmage/api/plaza_categories.dspy logined

View File

@ -0,0 +1,13 @@
# 多租户域名路由:解析当前请求域名对应的租户/分销商
env = request._run_ns
host = request.headers.get('X-Forwarded-Host') or request.host
host = host.split(':')[0]
try:
rec = await env.get_domain_by_host(request, host)
except Exception as e:
return json.dumps({'success': False, 'domain': host, 'error': str(e)}, ensure_ascii=False)
if rec:
result = {'success': True, 'domain': host, 'resellerid': rec.resellerid, 'orgid': rec.orgid}
else:
result = {'success': False, 'domain': host, 'error': 'Domain not found'}
return json.dumps(result, ensure_ascii=False)

View File

@ -1 +0,0 @@
../../dashboard_for_sage/wwwroot/shell_theme.css

843
wwwroot/shell_theme.css Normal file
View File

@ -0,0 +1,843 @@
/* Sage Modern UI Theme System
Dark/Light theme switching via CSS custom properties
Applied to <html> element data-theme attribute
*/
/* ===== Dark Theme (default) ===== */
:root,
[data-theme="dark"] {
--sage-bg-primary: #0B1120;
--sage-bg-secondary: #111827;
--sage-bg-card: #1E293B;
--sage-bg-hover: #334155;
--sage-bg-input: #0F172A;
--sage-bg-toolbar: #111827;
--sage-border-primary: #334155;
--sage-border-weak: #1E293B;
--sage-border-focus: #3B82F6;
--sage-text-primary: #F1F5F9;
--sage-text-secondary: #94A3B8;
--sage-text-muted: #64748B;
--sage-text-inverse: #0F172A;
--sage-brand: #3B82F6;
--sage-brand-hover: #2563EB;
--sage-success: #22C55E;
--sage-warning: #F59E0B;
--sage-danger: #EF4444;
--sage-info: #06B6D4;
--sage-sidebar-width: 240px;
--sage-sidebar-collapsed-width: 64px;
--sage-topbar-height: 56px;
--sage-radius: 12px;
--sage-radius-sm: 8px;
--sage-shadow-card: 0 1px 3px rgba(0,0,0,0.3);
--sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.4);
}
/* ===== Light Theme ===== */
[data-theme="light"] {
--sage-bg-primary: #F8FAFC;
--sage-bg-secondary: #FFFFFF;
--sage-bg-card: #FFFFFF;
--sage-bg-hover: #F1F5F9;
--sage-bg-input: #F8FAFC;
--sage-bg-toolbar: #FFFFFF;
--sage-border-primary: #E2E8F0;
--sage-border-weak: #F1F5F9;
--sage-border-focus: #3B82F6;
--sage-text-primary: #0F172A;
--sage-text-secondary: #475569;
--sage-text-muted: #94A3B8;
--sage-text-inverse: #FFFFFF;
--sage-brand: #3B82F6;
--sage-brand-hover: #2563EB;
--sage-success: #16A34A;
--sage-warning: #D97706;
--sage-danger: #DC2626;
--sage-info: #0891B2;
--sage-shadow-card: 0 1px 3px rgba(0,0,0,0.08);
--sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.1);
}
/* ===== Global Overrides ===== */
body {
margin: 0;
padding: 0;
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: background-color 0.2s ease, color 0.2s ease;
}
/* Ensure theme variables override bricks.css hardcoded values */
[data-theme="dark"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
[data-theme="light"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
/* ===== Shell Layout ===== */
.sage-shell {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
transition: background-color 0.2s ease, color 0.2s ease;
}
.sage-topbar {
height: var(--sage-topbar-height);
background-color: var(--sage-bg-secondary);
border-bottom: 1px solid var(--sage-border-primary);
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
flex-shrink: 0;
z-index: 100;
}
.sage-body {
display: flex;
flex: 1;
overflow: hidden;
}
.sage-sidebar {
width: var(--sage-sidebar-width);
background-color: var(--sage-bg-secondary);
border-right: 1px solid var(--sage-border-primary);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
flex-shrink: 0;
transition: width 0.3s ease, transform 0.3s ease;
position: relative;
z-index: 50;
}
.sage-sidebar.collapsed {
width: var(--sage-sidebar-collapsed-width);
}
.sage-sidebar.collapsed .sidebar-text {
opacity: 0;
width: 0;
overflow: hidden;
}
/* Menu collapsed state - hide text labels, center icons */
.menu-collapsed .filler {
display: none !important;
}
.menu-collapsed .menuitem {
justify-content: center;
padding: 8px 0;
}
.menu-collapsed .menuitem:hover {
background-color: var(--sage-bg-hover);
}
/* Mobile: sidebar as overlay */
@media (max-width: 768px) {
.sage-sidebar {
position: fixed;
left: 0;
top: var(--sage-topbar-height);
height: calc(100vh - var(--sage-topbar-height));
transform: translateX(0);
box-shadow: var(--sage-shadow-lg);
}
.sage-sidebar.collapsed {
transform: translateX(-100%);
width: var(--sage-sidebar-width); /* Keep full width when hidden on mobile */
}
.sage-main {
width: 100%;
}
}
.sage-main {
flex: 1;
overflow: hidden;
overflow-x: hidden;
padding: 24px;
background-color: var(--sage-bg-primary);
min-width: 0; /* Prevent flex item from overflowing */
min-height: 0; /* Allow flex children to shrink below content size */
transition: margin-left 0.3s ease;
}
/* Responsive padding for mobile */
@media (max-width: 768px) {
.sage-main {
padding: 16px;
}
}
@media (max-width: 480px) {
.sage-main {
padding: 12px;
}
}
/* ===== Stat Cards ===== */
.stat-card {
background-color: var(--sage-bg-card);
border-radius: var(--sage-radius);
padding: 20px;
border: 1px solid var(--sage-border-primary);
box-shadow: var(--sage-shadow-card);
transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--sage-shadow-lg);
}
.stat-card .stat-icon {
width: 40px;
height: 40px;
border-radius: var(--sage-radius-sm);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.stat-card .stat-value {
font-size: 28px;
font-weight: 700;
color: var(--sage-text-primary);
line-height: 1.2;
}
.stat-card .stat-label {
font-size: 14px;
color: var(--sage-text-secondary);
margin-top: 4px;
}
.stat-card .stat-trend {
font-size: 13px;
margin-top: 8px;
}
.stat-card .stat-trend.up {
color: var(--sage-success);
}
.stat-card .stat-trend.down {
color: var(--sage-danger);
}
/* ===== Quick Links ===== */
.quick-link {
background-color: var(--sage-bg-card);
border-radius: var(--sage-radius);
padding: 20px;
border: 1px solid var(--sage-border-primary);
box-shadow: var(--sage-shadow-card);
transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease;
cursor: pointer;
text-align: center;
}
.quick-link:hover {
transform: translateY(-2px);
box-shadow: var(--sage-shadow-lg);
border-color: var(--sage-brand);
}
/* ===== Section Headers ===== */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--sage-text-primary);
}
/* ===== Buttons ===== */
.sage-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: var(--sage-radius-sm);
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.15s ease;
}
.sage-btn-primary {
background-color: var(--sage-brand);
color: #FFFFFF;
}
.sage-btn-primary:hover {
background-color: var(--sage-brand-hover);
}
.sage-btn-ghost {
background-color: transparent;
color: var(--sage-text-secondary);
border: 1px solid var(--sage-border-primary);
}
.sage-btn-ghost:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Theme Toggle Button ===== */
.theme-toggle {
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--sage-border-primary);
background-color: transparent;
color: var(--sage-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease, color 0.15s ease;
}
.theme-toggle:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Sidebar Toggle ===== */
.sidebar-toggle {
width: 36px;
height: 36px;
border-radius: var(--sage-radius-sm);
border: 1px solid var(--sage-border-primary);
background-color: transparent;
color: var(--sage-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
}
.sidebar-toggle:hover {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
/* ===== Breadcrumb ===== */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--sage-text-secondary);
}
.breadcrumb a {
color: var(--sage-text-secondary);
text-decoration: none;
}
.breadcrumb a:hover {
color: var(--sage-brand);
}
.breadcrumb .separator {
color: var(--sage-text-muted);
}
.breadcrumb .current {
color: var(--sage-text-primary);
font-weight: 500;
}
/* ===== DataViewer Overrides for theme ===== */
.tabular-row {
cursor: pointer;
}
[data-theme="dark"] .dataviewer-toolbar,
[data-theme="dark"] .tabular,
[data-theme="dark"] .data-row,
[data-theme="dark"] body {
background-color: var(--sage-bg-primary);
color: var(--sage-text-primary);
}
[data-theme="dark"] .tabular-header-row {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .tabular-row {
color: var(--sage-text-primary);
}
[data-theme="dark"] .tabular-row:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .tabular-row:nth-child(even) {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .tabular-row-selected {
background-color: var(--sage-bg-hover) !important;
}
[data-theme="dark"] .tabular-row-selected,
[data-theme="dark"] .tabular-row-selected .tabular-cell {
color: var(--sage-brand) !important;
}
/* Light theme selected row */
[data-theme="light"] .tabular-row-selected {
background-color: #E0F2FE !important;
}
[data-theme="light"] .tabular-row-selected,
[data-theme="light"] .tabular-row-selected .tabular-cell {
color: #0369A1 !important;
}
[data-theme="dark"] .tabular-cell {
border-color: var(--sage-border-weak);
color: var(--sage-text-primary);
}
[data-theme="dark"] .popup,
[data-theme="dark"] .modal,
[data-theme="dark"] .message {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .titlebar {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .inputbox {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .htoolbar,
[data-theme="dark"] .vtoolbar {
background-color: var(--sage-bg-toolbar);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .accordion-item {
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .accordion-item:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .accordion-item:nth-child(even) {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .accordion-item-header {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .accordion-item-info {
background-color: var(--sage-bg-card);
}
[data-theme="dark"] .accordion-item-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .accordion-item-info-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .card {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .subcard {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .curpos {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .selected {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .tabpanel {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .tabpanel-content {
background-color: var(--sage-bg-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .toolbar-button {
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .toolbar-button-active {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .auto-textarea {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="dark"] .droparea {
border-color: var(--sage-border-primary);
color: var(--sage-text-secondary);
}
[data-theme="dark"] .droparea:hover {
border-color: var(--sage-brand);
color: var(--sage-brand);
background: var(--sage-bg-hover);
}
[data-theme="dark"] .thinking-content {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] .resp-error {
background-color: rgba(239, 68, 68, 0.15);
color: var(--sage-danger);
}
[data-theme="dark"] .resp-content {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="dark"] .llm_title {
background-color: var(--sage-bg-hover);
}
[data-theme="dark"] .progress-container {
background-color: var(--sage-bg-secondary);
}
[data-theme="dark"] pre {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-primary);
}
[data-theme="dark"] .llm_msg {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .user_msg {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .dataviewer-toolbar,
[data-theme="light"] .tabular,
[data-theme="light"] .data-row,
[data-theme="light"] body {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular-header-row {
background-color: var(--sage-bg-hover);
color: var(--sage-text-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .tabular-row {
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular-row:nth-child(odd) {
background-color: #f9fafb;
}
[data-theme="light"] .tabular-row:nth-child(even) {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .tabular-row-selected {
color: var(--sage-brand-hover);
}
[data-theme="light"] .tabular-cell {
border-color: var(--sage-border-weak);
color: var(--sage-text-primary);
}
[data-theme="light"] .popup,
[data-theme="light"] .modal,
[data-theme="light"] .message {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .titlebar {
background-color: var(--sage-bg-secondary);
}
[data-theme="light"] .inputbox {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .htoolbar,
[data-theme="light"] .vtoolbar {
background-color: var(--sage-bg-toolbar);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .accordion-item {
border-color: var(--sage-border-primary);
}
[data-theme="light"] .accordion-item:nth-child(odd) {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .accordion-item:nth-child(even) {
background-color: #f9fafb;
}
[data-theme="light"] .accordion-item-header {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .accordion-item-info {
background-color: var(--sage-bg-card);
}
[data-theme="light"] .accordion-item-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .accordion-item-info-selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .card {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .subcard {
background-color: #f9fafb;
}
[data-theme="light"] .curpos {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .selected {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .tabpanel {
background-color: var(--sage-bg-card);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .tabpanel-content {
background-color: var(--sage-bg-secondary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .toolbar-button {
border-color: var(--sage-border-primary);
}
[data-theme="light"] .toolbar-button-active {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .auto-textarea {
background-color: var(--sage-bg-input);
color: var(--sage-text-primary);
border-color: var(--sage-border-primary);
}
[data-theme="light"] .droparea {
border-color: var(--sage-border-primary);
color: var(--sage-text-secondary);
}
[data-theme="light"] .droparea:hover {
border-color: var(--sage-brand);
color: var(--sage-brand);
background: var(--sage-bg-hover);
}
[data-theme="light"] .thinking-content {
background-color: var(--sage-bg-secondary);
}
[data-theme="light"] .resp-error {
background-color: rgba(239, 68, 68, 0.1);
color: var(--sage-danger);
}
[data-theme="light"] .resp-content {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .llm_title {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] .progress-container {
background-color: var(--sage-bg-hover);
}
[data-theme="light"] pre {
background-color: var(--sage-bg-secondary);
color: var(--sage-text-primary);
}
[data-theme="light"] .llm_msg {
background-color: var(--sage-bg-card);
color: var(--sage-text-primary);
}
[data-theme="light"] .user_msg {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .tabular { background-color: #f5f5f5; color: #1E293B; }
[data-theme="light"] .tabular-header-row { background-color: #E2E8F0; color: #0F172A; font-weight: 600; }
[data-theme="light"] .tabular-header-row .tabular-cell { color: #0F172A; }
[data-theme="light"] .tabular-row { color: #1E293B; }
[data-theme="light"] .tabular-row:nth-child(odd) { background-color: #FFFFFF; }
[data-theme="light"] .tabular-row:nth-child(even) { background-color: #F8FAFC; }
[data-theme="light"] .tabular-cell { border-color: #E2E8F0; color: #1E293B; }
[data-theme="light"] .card { color: #1E293B; }
[data-theme="light"] .inputbox { color: #1E293B; background-color: #FFFFFF; }
[data-theme="light"] .popup, [data-theme="light"] .modal { color: #1E293B; }
[data-theme="light"] .accordion-item-info { color: #475569; }
[data-theme="light"] .toolbar-button { color: #334155; }
[data-theme="light"] .message { color: #1E293B; }
/* ===== Menu Overrides ===== */
[data-theme="dark"] .menu-item {
color: var(--sage-text-secondary);
}
[data-theme="dark"] .menu-item:hover,
[data-theme="dark"] .menu-item.active {
background-color: var(--sage-bg-hover);
color: var(--sage-text-primary);
}
[data-theme="light"] .menu-item {
color: var(--sage-text-secondary);
}
[data-theme="light"] .menu-item:hover,
[data-theme="light"] .menu-item.active {
background-color: var(--sage-bg-hover);
color: var(--sage-brand);
}
/* ===== Utility Classes ===== */
.sage-brand-title {
color: var(--sage-text-primary);
font-weight: bold;
}
.sage-text-secondary {
color: var(--sage-text-secondary);
}
/* ===== Responsive - Topbar ===== */
@media (max-width: 768px) {
.sage-topbar {
padding: 0 8px;
gap: 6px;
}
.sage-brand-title {
font-size: 16px;
}
}
@media (max-width: 480px) {
.sage-brand-title {
display: none;
}
}
/* ===== Responsive - Stat Cards ===== */
@media (max-width: 768px) {
.stat-card {
padding: 14px;
}
.stat-card .stat-value {
font-size: 22px;
}
.stat-card .stat-label {
font-size: 12px;
}
.stat-card .stat-icon {
width: 32px;
height: 32px;
margin-bottom: 8px;
}
.quick-link {
padding: 14px;
}
.section-title {
font-size: 16px;
}
}

View File

@ -1 +0,0 @@
../../dashboard_for_sage/wwwroot/shell_theme.js

263
wwwroot/shell_theme.js Normal file
View File

@ -0,0 +1,263 @@
/* Sage Modern UI Shell Theme & Layout Controller
Handles: theme switching (dark/light), sidebar collapse, localStorage persistence
*/
(function() {
'use strict';
var THEME_KEY = 'sage_ui_theme';
var SIDEBAR_KEY = 'sage_sidebar_collapsed';
// Initialize theme on page load
function initTheme() {
var saved = null;
try { saved = localStorage.getItem(THEME_KEY); } catch(e) {}
var theme = saved || 'dark';
document.documentElement.setAttribute('data-theme', theme);
updateThemeIcon(theme);
}
// Toggle between dark and light
function toggleTheme() {
var current = document.documentElement.getAttribute('data-theme') || 'dark';
var next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem(THEME_KEY, next); } catch(e) {}
updateThemeIcon(next);
}
// Update the theme toggle icon based on current theme
function updateThemeIcon(theme) {
var btn = document.getElementById('theme_toggle_btn');
if (!btn) return;
if (theme === 'light') {
// Show moon icon (switch to dark)
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
} else {
// Show sun icon (switch to light)
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
}
}
// Initialize sidebar state
function initSidebar() {
var collapsed = false;
try { collapsed = localStorage.getItem(SIDEBAR_KEY) === 'true'; } catch(e) {}
// Auto-collapse on mobile
if (isMobile()) {
collapsed = true;
}
var sidebar = document.getElementById('sage_sidebar');
if (sidebar && collapsed) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
}
}
// Handle window resize - auto-collapse on mobile
function handleResize() {
var sidebar = document.getElementById('sage_sidebar');
if (!sidebar) return;
if (isMobile() && !sidebar.classList.contains('collapsed')) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(e) {}
updateSidebarIcon(true);
}
}
// Check if we're on mobile viewport
function isMobile() {
return window.innerWidth <= 768;
}
// Close sidebar when clicking outside on mobile
function setupMobileOverlay() {
document.addEventListener('click', function(e) {
if (!isMobile()) return;
var sidebar = document.getElementById('sage_sidebar');
var toggleBtn = document.getElementById('sidebar_toggle_btn');
if (!sidebar || sidebar.classList.contains('collapsed')) return;
// If click is outside sidebar and toggle button, close sidebar
if (!sidebar.contains(e.target) && (!toggleBtn || !toggleBtn.contains(e.target))) {
sidebar.classList.add('collapsed');
sidebar.style.width = '64px';
try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(ex) {}
updateSidebarIcon(true);
}
});
}
function updateSidebarIcon(isCollapsed) {
var btn = document.getElementById('sidebar_toggle_btn');
if (!btn) return;
if (isCollapsed) {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
} else {
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>';
}
}
// Toggle sidebar collapse
function toggleSidebar() {
var sidebar = document.getElementById('sage_sidebar');
if (!sidebar) return;
sidebar.classList.toggle('collapsed');
var isCollapsed = sidebar.classList.contains('collapsed');
try { localStorage.setItem(SIDEBAR_KEY, isCollapsed); } catch(e) {}
updateSidebarIcon(isCollapsed);
// Override inline width style (bricks sets inline style which beats CSS class)
sidebar.style.width = isCollapsed ? '64px' : '240px';
// Toggle Menu widget collapse state
if (typeof bricks !== 'undefined') {
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.toggle_collapse) {
menu.toggle_collapse();
}
}
}
// Initialize SPA Router
function initRouter() {
if (typeof bricks === 'undefined' || !bricks.Router) {
console.log('[Shell] Router not available');
return;
}
bricks.Router.init({
targets: [
{ id: 'sage_main_content', param: 'page' }
]
});
}
// Called by user_logined event — skip dashboard if Router has a saved route
window.sageOnLogin = async function(dashboardUrl) {
var target = bricks.getWidgetById('sage_main_content', bricks.app);
if (!target) return;
var hasRoute = false;
if (bricks.Router && bricks.Router._enabled) {
hasRoute = bricks.Router.current('sage_main_content')
|| new URLSearchParams(window.location.search).has('page');
}
if (hasRoute) {
console.log('[Shell] Router has route, skip dashboard load');
return;
}
// No route — load dashboard via Router
if (dashboardUrl && bricks.Router && bricks.Router._enabled) {
bricks.Router.navigate('sage_main_content', dashboardUrl);
} else if (dashboardUrl) {
var desc = { widgettype: 'urlwidget', options: { url: dashboardUrl } };
var w = await bricks.widgetBuild(desc, bricks.app);
if (w) {
target.clear_widgets();
target.add_widget(w);
}
}
};
// Reload global menu after login/logout
window.sageReloadMenu = async function() {
if (typeof bricks === 'undefined') return;
var sidebar = bricks.getWidgetById('sage_sidebar', bricks.app);
if (!sidebar) {
console.log('[Shell] sage_sidebar not found');
return;
}
// Check if sidebar is currently collapsed before clearing
var isCollapsed = sidebar.el && sidebar.el.classList.contains('collapsed');
// Clear existing children
sidebar.subwidgets.forEach(function(w) { w.destroy && w.destroy(); });
sidebar.subwidgets = [];
sidebar.el.innerHTML = '';
// Rebuild menu urlwidget
var menuUrl = bricks.app.baseUrl + '/global_menu.ui?_webbricks_=1';
var desc = {
"widgettype": "urlwidget",
"options": { "url": menuUrl }
};
try {
var w = await bricks.widgetBuild(desc, sidebar);
if (w) {
sidebar.addSubWidget(w);
// Re-apply sidebar width and collapsed state to newly built menu
sidebar.el.style.width = isCollapsed ? '64px' : '240px';
updateSidebarIcon(isCollapsed);
if (isCollapsed) {
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.collapse) {
menu.collapse();
}
}
console.log('[Shell] Menu reloaded');
}
} catch(e) {
console.log('[Shell] Menu reload error:', e);
}
};
// Run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initTheme();
initSidebar();
initRouter();
setupMobileOverlay();
window.addEventListener('resize', handleResize);
});
} else {
initTheme();
initSidebar();
initRouter();
setupMobileOverlay();
window.addEventListener('resize', handleResize);
}
// Bricks widgets render asynchronously after DOMContentLoaded.
// Retry updateThemeIcon until the button element exists.
(function retryThemeIcon() {
var btn = document.getElementById('theme_toggle_btn');
if (btn) {
var theme = document.documentElement.getAttribute('data-theme') || 'dark';
updateThemeIcon(theme);
} else {
setTimeout(retryThemeIcon, 200);
}
})();
// Retry sidebar icon init until the button element exists
(function retrySidebarIcon() {
var sidebar = document.getElementById('sage_sidebar');
var btn = document.getElementById('sidebar_toggle_btn');
if (btn && sidebar) {
var isCollapsed = sidebar.classList.contains('collapsed');
updateSidebarIcon(isCollapsed);
// Sync inline width with collapsed state (bricks may have overridden it)
sidebar.style.width = isCollapsed ? '64px' : '240px';
// Apply collapsed state to Menu if sidebar was already collapsed
if (isCollapsed && typeof bricks !== 'undefined') {
var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
if (menu && menu.collapse) {
menu.collapse();
} else {
setTimeout(retrySidebarIcon, 200);
return;
}
}
} else {
setTimeout(retrySidebarIcon, 200);
}
})();
// Expose global functions for bricks bind access
window.sageToggleTheme = toggleTheme;
window.sageToggleSidebar = toggleSidebar;
window.sageInitTheme = initTheme;
window.sageInitSidebar = initSidebar;
})();