更改项目名为NebulaShell
This commit is contained in:
8
store/@{NebulaShell}/webui/SIGNATURE
Normal file
8
store/@{NebulaShell}/webui/SIGNATURE
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"signature": "EHeyw9j4zTQyLiTHSqEHQQL1wrzJOjm4jRKHIWuIiRUY6YORSLip1aDVP+aGpCf+KYGROE/pMt3SDUI7h+5VWAh9x/AYf0UrCOq38dNJ4+5TeHxOwUZvic2Ua26LBWRp0GfdRq/t06/dtXkIwD+0albetQNJoPkORBTCuxPVZqGVU6WkKWuYJ9xuQDhpn266qy6ZQfVe88BcNPbO//AIR8+t5gpd+hRmhbhxV58Omm+R0jtlx3ABEOH4g2HGkX961UvUdFSaoVMw7KR4lv9GQU1rMraP/zyHTLAQQlt/SxJAi3db51KWzFuH8rDsGKnB7LbJvnV32ojUNQs0SIO8935UY6RuHnKr8KHuAxFNX/1GA4MdloHhrK0Fm6Tx5FDXamthUFqJzYvjMtsGGN24p7/DQwaHqonB9AJ5szRf/vBYmsGs1WTCX/e89IN/uiVUPuqEiRxiJBRMLwpr2mz0r6e3keozWdPuxZ58WVH3Gd3gXvLngs+Gx3FyCd7RLtn24gkq/w16bCuA3XBE+9+n6QvAUBfvjiODCb9fjdPL/YNoJRMKqE1iAhMI+I5Cmu0ISOdTL4aYZEjZP3YwjauMKlpXMhclOwIv2I2btNQIKPOJj4SormqPweK0QXAVbOr+u/S0Z4L2vISGwJBetQl8fpSpdL2NLVmZM9xAoa1AZTY=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964952.7199903,
|
||||
"plugin_hash": "1aee0b23a28d31b62a8863d1feff8a53e0a1221572cba160642ac18d10a8f52f",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
29
store/@{NebulaShell}/webui/config.json
Normal file
29
store/@{NebulaShell}/webui/config.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": {
|
||||
"type": "string",
|
||||
"name": "网站标题",
|
||||
"description": "侧边栏和页面标题显示的文字",
|
||||
"default": "NebulaShell",
|
||||
"order": 1
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"name": "端口号",
|
||||
"description": "WebUI 监听端口",
|
||||
"default": 8080,
|
||||
"min": 1024,
|
||||
"max": 65535,
|
||||
"order": 2
|
||||
},
|
||||
"theme": {
|
||||
"type": "select",
|
||||
"name": "主题",
|
||||
"description": "界面主题风格",
|
||||
"default": "dark",
|
||||
"options": [
|
||||
{ "label": "深色", "value": "dark" },
|
||||
{ "label": "浅色", "value": "light" }
|
||||
],
|
||||
"order": 3
|
||||
}
|
||||
}
|
||||
49
store/@{NebulaShell}/webui/config/database.sql
Normal file
49
store/@{NebulaShell}/webui/config/database.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- NebulaShell WebUI 数据库初始化脚本
|
||||
-- 此脚本创建基础表结构,其他插件可以添加自己的表
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS nebulashell
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE nebulashell;
|
||||
|
||||
-- 用户表 (示例)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 插件配置表
|
||||
CREATE TABLE IF NOT EXISTS plugin_configs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
plugin_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL,
|
||||
config_value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_plugin_config (plugin_name, config_key),
|
||||
INDEX idx_plugin_name (plugin_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 系统日志表
|
||||
CREATE TABLE IF NOT EXISTS system_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
level VARCHAR(20) NOT NULL DEFAULT 'INFO',
|
||||
message TEXT NOT NULL,
|
||||
source VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_level (level),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 插入默认配置
|
||||
INSERT IGNORE INTO plugin_configs (plugin_name, config_key, config_value) VALUES
|
||||
('webui', 'theme', 'dark'),
|
||||
('webui', 'title', 'NebulaShell'),
|
||||
('webui', 'version', '1.0.0');
|
||||
0
store/@{NebulaShell}/webui/core/__init__.py
Normal file
0
store/@{NebulaShell}/webui/core/__init__.py
Normal file
181
store/@{NebulaShell}/webui/core/server.py
Normal file
181
store/@{NebulaShell}/webui/core/server.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""WebUI 服务器 - 容器模式"""
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
from oss.plugin.types import Response
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class WebUIServer:
|
||||
"""WebUI 服务器"""
|
||||
|
||||
def __init__(self, router, config: dict):
|
||||
self.router = router
|
||||
self.config = config
|
||||
self.frontend_dir = Path(__file__).parent.parent / "frontend"
|
||||
|
||||
# 页面注册表
|
||||
self.pages = {} # path -> content_provider
|
||||
self.nav_items = [] # 导航项列表
|
||||
|
||||
def start(self):
|
||||
"""注册默认路由"""
|
||||
# 静态资源
|
||||
self.router.get("/static/css/main.css", self._handle_css)
|
||||
self.router.get("/static/js/main.js", self._handle_js)
|
||||
self.router.get("/health", self._handle_health)
|
||||
|
||||
def register_page(self, path: str, content_provider, nav_item: dict = None):
|
||||
"""供其他插件注册页面"""
|
||||
self.pages[path] = content_provider
|
||||
if nav_item:
|
||||
nav_item['url'] = path
|
||||
self.nav_items.append(nav_item)
|
||||
|
||||
# 注册路由
|
||||
self.router.get(path, lambda req: self._render_page(path, req))
|
||||
|
||||
def _render_page(self, path: str, request):
|
||||
"""渲染页面布局 + 内容"""
|
||||
provider = self.pages.get(path)
|
||||
content = provider() if provider else ""
|
||||
|
||||
# 排序导航项(首页在前)
|
||||
sorted_nav = sorted(self.nav_items, key=lambda x: 0 if x.get('url') == '/' else 1)
|
||||
|
||||
# 构建导航项 HTML
|
||||
nav_html = ""
|
||||
icon_map = {
|
||||
'🏠': 'ri-home-4-line',
|
||||
'📊': 'ri-dashboard-line',
|
||||
'📋': 'ri-file-list-3-line',
|
||||
'🧩': 'ri-puzzle-line',
|
||||
'⚙️': 'ri-settings-3-line',
|
||||
'🔌': 'ri-plug-line',
|
||||
'📦': 'ri-box-3-line',
|
||||
'🌐': 'ri-global-line',
|
||||
}
|
||||
for item in sorted_nav:
|
||||
url = item.get('url', '#')
|
||||
is_active = 'active' if url == path else ''
|
||||
icon = item.get('icon', 'ri-dashboard-line')
|
||||
text = item.get('text', '')
|
||||
ri_icon = icon_map.get(icon, icon)
|
||||
title = text
|
||||
nav_html += f'''
|
||||
<a href="{url}" class="nav-item {is_active}" title="{title}">
|
||||
<i class="{ri_icon}"></i>
|
||||
</a>
|
||||
'''
|
||||
|
||||
page_title = self.config.get("title", "NebulaShell")
|
||||
|
||||
# 读取 HTML 模板
|
||||
template_file = self.frontend_dir / "views" / "layout.html"
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
html_template = f.read()
|
||||
|
||||
html = html_template.replace('{{ pageTitle }}', page_title)
|
||||
html = html.replace('{{ navItems }}', nav_html)
|
||||
html = html.replace('{{ content }}', content)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
headers={"Content-Type": "text/html; charset=utf-8"},
|
||||
body=html
|
||||
)
|
||||
def _default_home_content(self) -> str:
|
||||
"""默认首页内容"""
|
||||
return """
|
||||
<div class="home-content">
|
||||
<div class="welcome-banner">
|
||||
<h2>👋 欢迎使用 NebulaShell</h2>
|
||||
<p>一切皆为插件的轻量级框架</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _execute_php(self, php_file: str, variables: dict = None) -> str:
|
||||
"""执行 PHP 文件"""
|
||||
variables = variables or {}
|
||||
|
||||
# 构建 PHP 变量注入
|
||||
php_vars = ""
|
||||
for key, value in variables.items():
|
||||
if isinstance(value, dict):
|
||||
php_vars += f"${key} = {self._php_array(value)};\n"
|
||||
elif isinstance(value, list):
|
||||
php_vars += f"${key} = {self._php_array_list(value)};\n"
|
||||
elif isinstance(value, str):
|
||||
php_vars += f"${key} = '{value.replace(chr(39), chr(92) + chr(39))}';\n"
|
||||
else:
|
||||
php_vars += f"${key} = {str(value).lower() if isinstance(value, bool) else value};\n"
|
||||
|
||||
with open(php_file, 'r', encoding='utf-8') as f:
|
||||
php_content = f.read()
|
||||
|
||||
# 临时文件必须和 views 在同一目录,这样 __DIR__ 才能正确解析
|
||||
views_dir = str(Path(php_file).parent)
|
||||
tmp_file = os.path.join(views_dir, '.temp_render.php')
|
||||
|
||||
try:
|
||||
with open(tmp_file, 'w', encoding='utf-8') as f:
|
||||
f.write(f"<?php\n{php_vars}\n?>\n{php_content}")
|
||||
|
||||
result = subprocess.run(
|
||||
["php", "-f", tmp_file],
|
||||
capture_output=True, text=True, timeout=10, cwd=views_dir,
|
||||
encoding='utf-8', errors='replace'
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"[webui] PHP 执行错误: {result.stderr}")
|
||||
return f"<div class='error'>PHP Error: {result.stderr}</div>"
|
||||
|
||||
return result.stdout
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(tmp_file):
|
||||
os.unlink(tmp_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _php_array(self, py_dict: dict) -> str:
|
||||
"""Python Dict -> PHP Array"""
|
||||
items = []
|
||||
for key, value in py_dict.items():
|
||||
if isinstance(value, str):
|
||||
items.append(f"'{key}' => '{value.replace(chr(39), chr(92) + chr(39))}'")
|
||||
elif isinstance(value, dict):
|
||||
items.append(f"'{key}' => {self._php_array(value)}")
|
||||
else:
|
||||
items.append(f"'{key}' => {value}")
|
||||
return "[" + ", ".join(items) + "]"
|
||||
|
||||
def _php_array_list(self, py_list: list) -> str:
|
||||
"""Python List -> PHP Array"""
|
||||
items = []
|
||||
for item in py_list:
|
||||
if isinstance(item, dict):
|
||||
items.append(self._php_array(item))
|
||||
elif isinstance(item, str):
|
||||
items.append(f"'{item.replace(chr(39), chr(92) + chr(39))}'")
|
||||
else:
|
||||
items.append(str(item))
|
||||
return "[" + ", ".join(items) + "]"
|
||||
|
||||
def _handle_css(self, request):
|
||||
css_file = self.frontend_dir / "assets" / "css" / "main.css"
|
||||
with open(css_file, 'r', encoding='utf-8') as f:
|
||||
css = f.read()
|
||||
return Response(status=200, headers={"Content-Type": "text/css; charset=utf-8"}, body=css)
|
||||
|
||||
def _handle_js(self, request):
|
||||
js_file = self.frontend_dir / "assets" / "js" / "main.js"
|
||||
with open(js_file, 'r', encoding='utf-8') as f:
|
||||
js = f.read()
|
||||
return Response(status=200, headers={"Content-Type": "application/javascript; charset=utf-8"}, body=js)
|
||||
|
||||
def _handle_health(self, request):
|
||||
import json
|
||||
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps({"status": "ok"}))
|
||||
145
store/@{NebulaShell}/webui/frontend/assets/css/main.css
Normal file
145
store/@{NebulaShell}/webui/frontend/assets/css/main.css
Normal file
@@ -0,0 +1,145 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Dock 侧边栏 */
|
||||
.sidebar {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
background: #1a1a2e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(74, 144, 217, 0.2);
|
||||
color: #4a90d9;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
background: #4a90d9;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* Tooltip on hover */
|
||||
.nav-item:hover::after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
left: 52px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-item:hover:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.settings-btn i {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
font-size: 15px;
|
||||
}
|
||||
36
store/@{NebulaShell}/webui/frontend/assets/js/main.js
Normal file
36
store/@{NebulaShell}/webui/frontend/assets/js/main.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* NebulaShell WebUI 主脚本
|
||||
* 提供基础框架功能
|
||||
*/
|
||||
|
||||
window.WebUI = {
|
||||
/**
|
||||
* 打开设置面板
|
||||
* 其他插件可以扩展此功能
|
||||
*/
|
||||
openSettings: function() {
|
||||
console.log('[WebUI] 打开设置面板');
|
||||
// 设置面板逻辑 - 其他插件可以扩展
|
||||
alert('设置功能需要其他插件支持');
|
||||
},
|
||||
|
||||
/**
|
||||
* 注册导航项
|
||||
* 其他插件可以调用此方法添加导航
|
||||
*/
|
||||
registerNavItem: function(item) {
|
||||
console.log('[WebUI] 注册导航项:', item);
|
||||
// 实际实现需要与后端通信
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载内容到主内容区
|
||||
* 其他插件可以调用此方法加载内容
|
||||
*/
|
||||
loadContent: function(url) {
|
||||
console.log('[WebUI] 加载内容:', url);
|
||||
// 实际实现需要 AJAX 请求
|
||||
}
|
||||
};
|
||||
|
||||
console.log('NebulaShell WebUI 框架已加载');
|
||||
110
store/@{NebulaShell}/webui/frontend/views/index.html
Normal file
110
store/@{NebulaShell}/webui/frontend/views/index.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebulaShell - 首页</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.1.0/remixicon.min.css">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
<style>
|
||||
.home-content {
|
||||
padding: 40px;
|
||||
}
|
||||
.welcome-banner {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.welcome-banner h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.welcome-banner p {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.feature-card {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.feature-card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.feature-card p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="nav-item active" title="首页">
|
||||
<i class="ri-home-4-line"></i>
|
||||
</a>
|
||||
<a href="/dashboard" class="nav-item" title="仪表盘">
|
||||
<i class="ri-dashboard-line"></i>
|
||||
</a>
|
||||
<a href="/plugins" class="nav-item" title="插件管理">
|
||||
<i class="ri-puzzle-line"></i>
|
||||
</a>
|
||||
<a href="/settings" class="nav-item" title="设置">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="settings-btn" title="设置">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<div class="content-body">
|
||||
<div class="home-content">
|
||||
<div class="welcome-banner">
|
||||
<h2>👋 欢迎使用 NebulaShell</h2>
|
||||
<p>一切皆为插件的轻量级框架</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<h3><i class="ri-plug-line"></i> 插件化架构</h3>
|
||||
<p>所有功能皆可通过插件扩展,灵活定制您的系统</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3><i class="ri-shield-check-line"></i> 安全隔离</h3>
|
||||
<p>进程级沙箱保护,确保插件运行安全</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3><i class="ri-global-line"></i> 多语言支持</h3>
|
||||
<p>内置国际化框架,支持全球多种语言</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3><i class="ri-box-3-line"></i> 轻松部署</h3>
|
||||
<p>Docker 容器化部署,一键启动服务</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
store/@{NebulaShell}/webui/frontend/views/layout.html
Normal file
33
store/@{NebulaShell}/webui/frontend/views/layout.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ pageTitle }}</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.1.0/remixicon.min.css">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
{{ navItems }}
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="settings-btn" title="设置">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<div class="content-body">
|
||||
{{ content }}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
133
store/@{NebulaShell}/webui/main.py
Normal file
133
store/@{NebulaShell}/webui/main.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""WebUI - Web 控制台 (容器模式)"""
|
||||
from pathlib import Path
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.types import Plugin, Response, register_plugin_type
|
||||
from oss.config import get_config
|
||||
from .core.server import WebUIServer
|
||||
|
||||
|
||||
class WebUIPlugin(Plugin):
|
||||
"""WebUI 插件 - 提供页面容器"""
|
||||
|
||||
def __init__(self):
|
||||
self.http_api = None
|
||||
self.server = None
|
||||
self.config = {}
|
||||
|
||||
def meta(self):
|
||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
||||
config = get_config()
|
||||
return Manifest(
|
||||
metadata=Metadata(
|
||||
name="webui",
|
||||
version="2.1.0",
|
||||
author="NebulaShell",
|
||||
description="Web 控制台容器 - 供其他插件注册页面"
|
||||
),
|
||||
config=PluginConfig(
|
||||
enabled=True,
|
||||
args={
|
||||
"port": config.get("HTTP_API_PORT", 8080),
|
||||
"theme": "dark",
|
||||
"title": "NebulaShell"
|
||||
}
|
||||
),
|
||||
dependencies=["http-api"]
|
||||
)
|
||||
|
||||
def set_http_api(self, http_api):
|
||||
"""注入 http-api"""
|
||||
self.http_api = http_api
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
"""初始化 WebUI 服务器"""
|
||||
if not self.http_api:
|
||||
Log.error("webui", "错误: 未找到 http-api 依赖")
|
||||
return
|
||||
|
||||
config = {}
|
||||
if deps:
|
||||
config = deps.get("config", {})
|
||||
|
||||
self.config = {
|
||||
"port": config.get("port", get_config().get("HTTP_API_PORT", 8080)),
|
||||
"theme": config.get("theme", "dark"),
|
||||
"title": config.get("title", "NebulaShell")
|
||||
}
|
||||
|
||||
# 使用 http-api 的路由器
|
||||
self.server = WebUIServer(
|
||||
self.http_api.router,
|
||||
self.config
|
||||
)
|
||||
Log.info("webui", "容器初始化完成")
|
||||
|
||||
def start(self):
|
||||
"""启动服务器(注册默认路由)"""
|
||||
if self.server:
|
||||
# 检测仪表盘是否已安装,自动设为首页
|
||||
self._setup_home_page()
|
||||
|
||||
self.server.start()
|
||||
Log.info("webui", f"WebUI 容器已启动: http://localhost:{self.config['port']}")
|
||||
|
||||
def _setup_home_page(self):
|
||||
"""设置首页:如果仪表盘已安装则跳转到仪表盘,否则显示默认首页"""
|
||||
# 通过文件系统检查 dashboard 是否存在
|
||||
dashboard_exists = False
|
||||
store_dirs = [
|
||||
Path("store/@{NebulaShell}/dashboard"),
|
||||
]
|
||||
for d in store_dirs:
|
||||
if d.exists() and (d / "main.py").exists():
|
||||
dashboard_exists = True
|
||||
break
|
||||
|
||||
if dashboard_exists:
|
||||
# 仪表盘已安装,注册首页重定向到仪表盘
|
||||
self.server.router.get("/", self._handle_home_redirect)
|
||||
Log.info("webui", "检测到仪表盘,首页自动跳转到 /dashboard")
|
||||
else:
|
||||
# 默认首页
|
||||
self.server.register_page(
|
||||
path="/",
|
||||
content_provider=self.server._default_home_content,
|
||||
nav_item={'icon': 'ri-home-4-line', 'text': '首页'}
|
||||
)
|
||||
|
||||
def _handle_home_redirect(self, request):
|
||||
"""处理首页重定向到仪表盘"""
|
||||
return Response(
|
||||
status=302,
|
||||
headers={"Location": "/dashboard", "Content-Type": "text/html"},
|
||||
body=""
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
Log.error("webui", "WebUI 容器已停止")
|
||||
|
||||
# --- 公开 API 供其他插件调用 ---
|
||||
|
||||
def register_page(self, path: str, content_provider, nav_item: dict = None):
|
||||
"""
|
||||
其他插件调用此方法注册页面。
|
||||
:param path: 路由路径 (e.g., '/dashboard')
|
||||
:param content_provider: 无参函数,返回 HTML 字符串
|
||||
:param nav_item: 导航项 {'icon': '📊', 'text': '仪表盘'}
|
||||
"""
|
||||
if self.server:
|
||||
self.server.register_page(path, content_provider, nav_item)
|
||||
else:
|
||||
Log.warn("webui", f"警告: 试图注册页面 {path},但服务器未初始化")
|
||||
|
||||
def add_nav_item(self, item: dict):
|
||||
"""仅添加导航项(如果页面由其他方式处理)"""
|
||||
if self.server:
|
||||
self.server.nav_items.append(item)
|
||||
|
||||
|
||||
register_plugin_type("WebUIPlugin", WebUIPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return WebUIPlugin()
|
||||
32
store/@{NebulaShell}/webui/manifest.json
Normal file
32
store/@{NebulaShell}/webui/manifest.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "webui",
|
||||
"version": "2.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "Web 控制台 - 多语言支持/插件管理/安全配置/系统监控",
|
||||
"type": "webui"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"port": 8080,
|
||||
"theme": "dark",
|
||||
"title": "NebulaShell",
|
||||
"language": "zh-CN",
|
||||
"supported_languages": ["zh-CN", "en-US", "zh-TW", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES"],
|
||||
"session_timeout": 3600,
|
||||
"enable_2fa": false,
|
||||
"show_plugins": true,
|
||||
"show_security": true,
|
||||
"show_deployments": true
|
||||
}
|
||||
},
|
||||
"dependencies": ["http-api", "i18n"],
|
||||
"permissions": ["*"],
|
||||
"frontend": "php",
|
||||
"database": {
|
||||
"type": "mysql",
|
||||
"name": "nebulashell",
|
||||
"init_script": "config/database.sql"
|
||||
}
|
||||
}
|
||||
0
store/@{NebulaShell}/webui/static/__init__.py
Normal file
0
store/@{NebulaShell}/webui/static/__init__.py
Normal file
112
store/@{NebulaShell}/webui/static/assets.py
Normal file
112
store/@{NebulaShell}/webui/static/assets.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""静态资源"""
|
||||
|
||||
|
||||
class StaticAssets:
|
||||
"""静态资源管理器"""
|
||||
|
||||
@staticmethod
|
||||
def get_css() -> str:
|
||||
return """* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #1a1a2e;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-left: 3px solid #4a90d9;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
padding: 20px 30px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}"""
|
||||
|
||||
@staticmethod
|
||||
def get_js() -> str:
|
||||
return """console.log('NebulaShell WebUI loaded');"""
|
||||
0
store/@{NebulaShell}/webui/templates/__init__.py
Normal file
0
store/@{NebulaShell}/webui/templates/__init__.py
Normal file
49
store/@{NebulaShell}/webui/templates/layout.py
Normal file
49
store/@{NebulaShell}/webui/templates/layout.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""页面布局模板"""
|
||||
|
||||
|
||||
class LayoutTemplate:
|
||||
"""布局模板"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def render(self) -> str:
|
||||
"""渲染页面"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{self.config.get('title', 'NebulaShell')}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🚀 {self.config.get('title', 'NebulaShell')}</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="nav-item active">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-text">首页</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="settings-btn">⚙️ 设置</button>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<header class="content-header">
|
||||
<h2>欢迎使用 NebulaShell</h2>
|
||||
</header>
|
||||
<div class="content-body">
|
||||
<div class="empty-state">
|
||||
<p>暂无内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>"""
|
||||
Reference in New Issue
Block a user