新增简易的8080面板😊

This commit is contained in:
Falck
2026-04-17 23:15:15 +08:00
parent c38d2f66d1
commit 9d19d09821
465 changed files with 9235 additions and 35285 deletions

View 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": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775964952.7199903,
"plugin_hash": "1aee0b23a28d31b62a8863d1feff8a53e0a1221572cba160642ac18d10a8f52f",
"author": "FutureOSS"
}

View File

@@ -0,0 +1,29 @@
{
"title": {
"type": "string",
"name": "网站标题",
"description": "侧边栏和页面标题显示的文字",
"default": "FutureOSS",
"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
}
}

View File

@@ -0,0 +1,49 @@
-- FutureOSS WebUI 数据库初始化脚本
-- 此脚本创建基础表结构,其他插件可以添加自己的表
CREATE DATABASE IF NOT EXISTS futureoss
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE futureoss;
-- 用户表 (示例)
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', 'FutureOSS'),
('webui', 'version', '1.0.0');

View File

@@ -0,0 +1,155 @@
"""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)
variables = {
"pageTitle": self.config.get("title", "FutureOSS"),
"currentPage": path,
"navItems": sorted_nav,
"content": content
}
php_file = self.frontend_dir / "views" / "layout.php"
html = self._execute_php(str(php_file), variables)
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>👋 欢迎使用 FutureOSS</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
)
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"}))

View 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;
}

View File

@@ -0,0 +1,36 @@
/**
* FutureOSS 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('FutureOSS WebUI 框架已加载');

View File

@@ -0,0 +1,26 @@
<?php
/**
* FutureOSS WebUI 配置文件
*/
return [
// 数据库配置
'database' => [
'host' => 'localhost',
'port' => 3306,
'username' => 'root',
'password' => '',
'dbname' => 'futureoss',
'charset' => 'utf8mb4'
],
// 应用配置
'app' => [
'title' => 'FutureOSS',
'theme' => 'dark',
'version' => '1.0.0'
],
// 其他插件可以添加配置
'plugins' => []
];

View File

@@ -0,0 +1,115 @@
<?php
/**
* 数据库连接类
* 提供 MySQL 数据库连接和基础查询功能
*/
class Database {
private static $instance = null;
private $connection;
private $config;
private function __construct() {
$configFile = __DIR__ . '/../config/config.php';
if (!file_exists($configFile)) {
throw new Exception('配置文件不存在: ' . $configFile);
}
$this->config = include $configFile;
$dbConfig = $this->config['database'];
try {
$dsn = "mysql:host={$dbConfig['host']};port={$dbConfig['port']};dbname={$dbConfig['dbname']};charset={$dbConfig['charset']}";
$this->connection = new PDO($dsn, $dbConfig['username'], $dbConfig['password']);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->connection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// 数据库连接失败时记录日志但不阻止页面加载
error_log('[FutureOSS WebUI] 数据库连接失败: ' . $e->getMessage());
$this->connection = null;
}
}
/**
* 获取单例实例
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 获取数据库连接
*/
public function getConnection() {
return $this->connection;
}
/**
* 检查数据库是否可用
*/
public function isConnected() {
return $this->connection !== null;
}
/**
* 执行查询
*/
public function query($sql, $params = []) {
if (!$this->isConnected()) {
return false;
}
try {
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
error_log('[FutureOSS WebUI] 数据库查询错误: ' . $e->getMessage());
return false;
}
}
/**
* 获取所有结果
*/
public function fetchAll($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt ? $stmt->fetchAll() : [];
}
/**
* 获取单条结果
*/
public function fetchOne($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt ? $stmt->fetch() : null;
}
/**
* 插入数据并返回 ID
*/
public function insert($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt ? $this->connection->lastInsertId() : false;
}
/**
* 防止 SQL 注入
*/
public function escape($value) {
if (!$this->isConnected()) {
return addslashes($value);
}
return $this->connection->quote($value);
}
// 防止克隆
private function __clone() {}
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* 仪表盘页面视图
*/
$pageTitle = 'FutureOSS - 仪表盘';
$currentPage = 'dashboard';
// 内容区直接包含仪表盘内容
if (isset($dashboardContent)) {
$content = $dashboardContent;
} else {
$content = '<div class="empty-state"><p>仪表盘内容加载中...</p></div>';
}
// 复用 layout
include __DIR__ . '/layout.php';

View File

@@ -0,0 +1,17 @@
<?php
/**
* 首页视图
* 这是 webui 插件的默认首页
* 其他插件可以替换或扩展此页面
*/
$pageTitle = $config['title'] ?? 'FutureOSS';
$currentPage = 'home';
// 默认导航项(其他插件可以添加更多)
$navItems = [];
// 内容区(其他插件可以注入内容)
$content = '<div class="empty-state"><p>暂无内容</p></div>';
include __DIR__ . '/layout.php';

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($pageTitle ?? 'FutureOSS') ?></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">
<?php if (!empty($navItems)): ?>
<?php foreach ($navItems as $item): ?>
<?php
$url = $item['url'] ?? '#';
$isActive = ($currentPage ?? '') === $url;
$icon = $item['icon'] ?? 'ri-dashboard-line';
// 如果图标是 emoji转换为 remixicon 类名
$iconMap = [
'🏠' => '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',
];
$riIcon = $iconMap[$icon] ?? $icon;
?>
<a href="<?= htmlspecialchars($url) ?>"
class="nav-item <?= $isActive ? 'active' : '' ?>"
title="<?= htmlspecialchars($item['text'] ?? '') ?>">
<i class="<?= htmlspecialchars($riIcon) ?>"></i>
</a>
<?php endforeach; ?>
<?php endif; ?>
</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">
<?php if (isset($content)): ?>
<?= $content ?>
<?php else: ?>
<div class="empty-state">
<p>暂无内容</p>
</div>
<?php endif; ?>
</div>
</main>
</div>
<script src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,132 @@
"""WebUI - Web 控制台 (容器模式)"""
from pathlib import Path
from oss.logger.logger import Log
from oss.plugin.types import Plugin, Response, register_plugin_type
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
return Manifest(
metadata=Metadata(
name="webui",
version="2.1.0",
author="FutureOSS",
description="Web 控制台容器 - 供其他插件注册页面"
),
config=PluginConfig(
enabled=True,
args={
"port": 8080,
"theme": "dark",
"title": "FutureOSS"
}
),
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", 8080),
"theme": config.get("theme", "dark"),
"title": config.get("title", "FutureOSS")
}
# 使用 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/@{FutureOSS}/dashboard"),
Path("./data/pkg/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()

View File

@@ -0,0 +1,25 @@
{
"metadata": {
"name": "webui",
"version": "2.0.0",
"author": "FutureOSS",
"description": "Web 控制台 - 使用 PHP 前端和 MySQL 数据库",
"type": "webui"
},
"config": {
"enabled": true,
"args": {
"port": 8080,
"theme": "dark",
"title": "FutureOSS"
}
},
"dependencies": ["http-api"],
"permissions": ["*"],
"frontend": "php",
"database": {
"type": "mysql",
"name": "futureoss",
"init_script": "config/database.sql"
}
}

View 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('FutureOSS WebUI loaded');"""

View 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', 'FutureOSS')}</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', 'FutureOSS')}</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>欢迎使用 FutureOSS</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>"""