feat: 新增系统状态页面功能

This commit is contained in:
augushong
2026-01-22 23:28:13 +08:00
parent ee40374732
commit 3a4194d3e9
7 changed files with 606 additions and 1 deletions

View File

@@ -159,7 +159,7 @@ Scheme 机制是 CURD 生成的前置依赖,开发流程如下:
1. **设计表结构**
- 方式 A手动创建/修改数据库表结构。
- 方式 B手动编写/修改 `app/admin/scheme/` 下的 Scheme 类。
- 方式 B手动编写/修改 `app/admin/scheme/` 下的 Scheme 类。(推荐)
2. **同步结构(确保一致性)**
- 若采用方式 A执行 `php think scheme:make -t {table}` 将变更同步到 Scheme。

View File

@@ -0,0 +1,9 @@
<?php
namespace app\admin\controller;
use base\admin\controller\StatusBase;
class Status extends StatusBase
{
}

View File

View File

@@ -0,0 +1,222 @@
<?php
namespace base\admin\controller;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use app\common\command\admin\Version;
use app\common\controller\AdminController;
use think\facade\Cache;
use think\facade\Config;
use think\facade\Db;
/**
* Class Status.
* @ControllerAnnotation(title="系统状态")
*/
class StatusBase extends AdminController
{
/**
* @NodeAnotation(title="列表")
*/
public function index()
{
$systemInfo = $this->getSystemInfo();
$serverInfo = $this->getServerInfo();
$databaseInfo = $this->getDatabaseInfo();
$cacheInfo = $this->getCacheInfo();
$storageInfo = $this->getStorageInfo();
$phpConfigInfo = $this->getPhpConfigInfo();
$performanceInfo = $this->getPerformanceInfo();
$this->assign('systemInfo', $systemInfo);
$this->assign('serverInfo', $serverInfo);
$this->assign('databaseInfo', $databaseInfo);
$this->assign('cacheInfo', $cacheInfo);
$this->assign('storageInfo', $storageInfo);
$this->assign('phpConfigInfo', $phpConfigInfo);
$this->assign('performanceInfo', $performanceInfo);
return $this->fetch();
}
protected function getSystemInfo()
{
return [
'framework' => 'Ulthon Admin',
'version' => Version::VERSION,
'product_version' => Version::PRODUCT_VERSION ?: Version::VERSION,
'layui_version' => Version::LAYUI_VERSION,
'thinkphp_version' => app()->version(),
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'timezone' => date_default_timezone_get(),
];
}
protected function getServerInfo()
{
$serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
$os = PHP_OS;
$sapi = php_sapi_name();
$ip = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1';
$port = $_SERVER['SERVER_PORT'] ?? '80';
return [
'server_software' => $serverSoftware,
'os' => $os,
'sapi' => $sapi,
'ip' => $ip,
'port' => $port,
];
}
protected function getDatabaseInfo()
{
$connectionName = Config::get('database.default', 'mysql');
$dbConfig = Config::get('database.connections.' . $connectionName);
$info = [
'type' => $dbConfig['type'] ?? 'Unknown',
'hostname' => $dbConfig['hostname'] ?? 'Unknown',
'database' => $dbConfig['database'] ?? 'Unknown',
'prefix' => $dbConfig['prefix'] ?? '',
'charset' => $dbConfig['charset'] ?? 'Unknown',
'status' => 'unknown',
'version' => 'Unknown',
'table_count' => 0,
];
try {
$connection = Db::connect($connectionName);
$version = $connection->query('SELECT VERSION() as version')[0]['version'] ?? 'Unknown';
$prefix = $dbConfig['prefix'] ?? '';
$tableCount = 0;
if ($prefix) {
$tables = $connection->query("SHOW TABLES LIKE '{$prefix}%'");
$tableCount = count($tables);
} else {
$tables = $connection->query('SHOW TABLES');
$tableCount = count($tables);
}
$info['status'] = 'connected';
$info['version'] = $version;
$info['table_count'] = $tableCount;
} catch (\Exception $e) {
$info['status'] = 'error';
$info['error'] = $e->getMessage();
}
return $info;
}
protected function getCacheInfo()
{
$defaultDriver = Config::get('cache.default');
$cacheConfig = Config::get('cache.stores.' . $defaultDriver);
$info = [
'default_driver' => $defaultDriver,
'type' => $cacheConfig['type'] ?? 'Unknown',
'path' => $cacheConfig['path'] ?? '',
'prefix' => $cacheConfig['prefix'] ?? '',
'expire' => $cacheConfig['expire'] ?? 0,
'status' => 'unknown',
];
try {
$testKey = 'system_status_test_' . time();
Cache::set($testKey, 'test', 10);
$testValue = Cache::get($testKey);
Cache::delete($testKey);
if ($testValue === 'test') {
$info['status'] = 'available';
} else {
$info['status'] = 'error';
}
} catch (\Exception $e) {
$info['status'] = 'error';
$info['error'] = $e->getMessage();
}
return $info;
}
protected function getStorageInfo()
{
$rootPath = app()->getRootPath();
$freeBytes = disk_free_space($rootPath);
$totalBytes = disk_total_space($rootPath);
$usedBytes = $totalBytes - $freeBytes;
$usagePercent = ($usedBytes / $totalBytes) * 100;
return [
'root_path' => $rootPath,
'total' => $this->formatBytes($totalBytes),
'free' => $this->formatBytes($freeBytes),
'used' => $this->formatBytes($usedBytes),
'usage_percent' => round($usagePercent, 2),
];
}
protected function getPhpConfigInfo()
{
return [
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time') . 's',
'max_input_time' => ini_get('max_input_time') . 's',
'safe_mode' => ini_get('safe_mode') ? 'On' : 'Off',
'allow_url_fopen' => ini_get('allow_url_fopen') ? 'On' : 'Off',
'file_uploads' => ini_get('file_uploads') ? 'On' : 'Off',
'disabled_functions' => ini_get('disable_functions') ?: 'None',
];
}
protected function getPerformanceInfo()
{
$memoryUsage = memory_get_usage(true);
$memoryLimit = $this->convertToBytes(ini_get('memory_limit'));
$memoryPercent = $memoryLimit > 0 ? ($memoryUsage / $memoryLimit) * 100 : 0;
return [
'memory_usage' => $this->formatBytes($memoryUsage),
'memory_limit' => $this->formatBytes($memoryLimit),
'memory_percent' => round($memoryPercent, 2),
'peak_memory' => $this->formatBytes(memory_get_peak_usage(true)),
'load_average' => function_exists('sys_getloadavg') ? implode(', ', sys_getloadavg()) : 'N/A',
];
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
protected function convertToBytes($value)
{
$value = trim($value);
$unit = strtolower($value[strlen($value) - 1]);
$value = (float) $value;
switch ($unit) {
case 'g':
$value *= 1024;
case 'm':
$value *= 1024;
case 'k':
$value *= 1024;
}
return $value;
}
}

View File

@@ -143,6 +143,17 @@ $ul_system_menu = array(
"sort" => 0,
"status" => 1,
),
array(
"id" => 255,
"pid" => 228,
"title" => "系统状态",
"icon" => "fa fa-info-circle",
"href" => "status/index",
"params" => "",
"target" => "_self",
"sort" => 0,
"status" => 1,
),
array(
"id" => 254,
"pid" => 249,

View File

@@ -0,0 +1,323 @@
<link rel="stylesheet" href="__STATIC__/admin/css/welcome.css?v={:time()}" media="all">
<div class="layuimini-container">
<div class="layuimini-main">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-info-circle icon"></i>系统概览</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="120">
<col width="150">
<col width="120">
<col>
</colgroup>
<tbody>
<tr>
<td>框架名称</td>
<td>{$systemInfo.framework}</td>
<td>框架版本</td>
<td>{$systemInfo.version}</td>
</tr>
<tr>
<td>Layui版本</td>
<td>{$systemInfo.layui_version}</td>
<td>ThinkPHP版本</td>
<td>{$systemInfo.thinkphp_version}</td>
</tr>
<tr>
<td>PHP版本</td>
<td>{$systemInfo.php_version}</td>
<td>服务器时间</td>
<td>{$systemInfo.server_time}</td>
</tr>
<tr>
<td>时区</td>
<td colspan="3">{$systemInfo.timezone}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-server icon icon-blue"></i>服务器环境</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="120">
<col>
</colgroup>
<tbody>
<tr>
<td>服务器软件</td>
<td data-toggle="copy-text" data-clipboard-text="{$serverInfo.server_software}">
{$serverInfo.server_software} <i class="fa fa-copy"></i>
</td>
</tr>
<tr>
<td>操作系统</td>
<td data-toggle="copy-text" data-clipboard-text="{$serverInfo.os}">
{$serverInfo.os} <i class="fa fa-copy"></i>
</td>
</tr>
<tr>
<td>运行模式</td>
<td>{$serverInfo.sapi}</td>
</tr>
<tr>
<td>服务器IP</td>
<td>{$serverInfo.ip}:{$serverInfo.port}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-database icon icon-green"></i>数据库状态</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="100">
<col>
</colgroup>
<tbody>
<tr>
<td>数据库类型</td>
<td>{$databaseInfo.type}</td>
</tr>
<tr>
<td>连接状态</td>
<td>
{if $databaseInfo.status == 'connected'}
<span class="layui-badge layui-bg-green">正常</span>
{elseif $databaseInfo.status == 'error'}
<span class="layui-badge layui-bg-red">异常</span>
{else/}
<span class="layui-badge layui-bg-gray">未知</span>
{/if}
</td>
</tr>
<tr>
<td>数据库版本</td>
<td>{$databaseInfo.version}</td>
</tr>
<tr>
<td>数据库名称</td>
<td>{$databaseInfo.database}</td>
</tr>
<tr>
<td>表前缀</td>
<td>{$databaseInfo.prefix}</td>
</tr>
<tr>
<td>数据表数量</td>
<td>{$databaseInfo.table_count}</td>
</tr>
{if $databaseInfo.status == 'error'}
<tr>
<td>错误信息</td>
<td class="layui-text-red">{$databaseInfo.error}</td>
</tr>
{/if}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-hdd-o icon icon-orange"></i>存储状态</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="100">
<col>
</colgroup>
<tbody>
<tr>
<td>根目录</td>
<td data-toggle="copy-text" data-clipboard-text="{$storageInfo.root_path}">
{$storageInfo.root_path} <i class="fa fa-copy"></i>
</td>
</tr>
<tr>
<td>总容量</td>
<td>{$storageInfo.total}</td>
</tr>
<tr>
<td>已用空间</td>
<td>{$storageInfo.used}</td>
</tr>
<tr>
<td>可用空间</td>
<td>{$storageInfo.free}</td>
</tr>
<tr>
<td>使用率</td>
<td>
<div class="layui-progress" lay-showpercent="true" lay-filter="storage">
<div class="layui-progress-bar" lay-percent="{$storageInfo.usage_percent}%"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-bolt icon icon-yellow"></i>缓存配置</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="100">
<col>
</colgroup>
<tbody>
<tr>
<td>默认驱动</td>
<td>{$cacheInfo.default_driver}</td>
</tr>
<tr>
<td>驱动类型</td>
<td>{$cacheInfo.type}</td>
</tr>
<tr>
<td>缓存路径</td>
<td>{$cacheInfo.path}</td>
</tr>
<tr>
<td>缓存前缀</td>
<td>{$cacheInfo.prefix}</td>
</tr>
<tr>
<td>有效期</td>
<td>{$cacheInfo.expire == 0 ? '永久' : $cacheInfo.expire . '秒'}</td>
</tr>
<tr>
<td>状态</td>
<td>
{if $cacheInfo.status == 'available'}
<span class="layui-badge layui-bg-green">可用</span>
{elseif $cacheInfo.status == 'error'}
<span class="layui-badge layui-bg-red">异常</span>
{else/}
<span class="layui-badge layui-bg-gray">未知</span>
{/if}
</td>
</tr>
{if $cacheInfo.status == 'error'}
<tr>
<td>错误信息</td>
<td class="layui-text-red">{$cacheInfo.error}</td>
</tr>
{/if}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-code icon icon-purple"></i>PHP配置</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="140">
<col>
</colgroup>
<tbody>
<tr>
<td>上传文件限制</td>
<td>{$phpConfigInfo.upload_max_filesize}</td>
</tr>
<tr>
<td>POST最大限制</td>
<td>{$phpConfigInfo.post_max_size}</td>
</tr>
<tr>
<td>内存限制</td>
<td>{$phpConfigInfo.memory_limit}</td>
</tr>
<tr>
<td>最大执行时间</td>
<td>{$phpConfigInfo.max_execution_time}</td>
</tr>
<tr>
<td>最大输入时间</td>
<td>{$phpConfigInfo.max_input_time}</td>
</tr>
<tr>
<td>文件上传</td>
<td>{$phpConfigInfo.file_uploads}</td>
</tr>
<tr>
<td>Allow URL Fopen</td>
<td>{$phpConfigInfo.allow_url_fopen}</td>
</tr>
<tr>
<td>禁用函数</td>
<td class="layui-text-gray" title="{$phpConfigInfo.disabled_functions}">
{php}echo mb_strlen($phpConfigInfo['disabled_functions']) > 50 ? mb_substr($phpConfigInfo['disabled_functions'], 0, 50) . '...' : $phpConfigInfo['disabled_functions'];{/php}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-tachometer icon icon-cyan"></i>性能指标</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="120">
<col>
</colgroup>
<tbody>
<tr>
<td>内存使用</td>
<td>{$performanceInfo.memory_usage}</td>
</tr>
<tr>
<td>内存限制</td>
<td>{$performanceInfo.memory_limit}</td>
</tr>
<tr>
<td>内存使用率</td>
<td>
<div class="layui-progress" lay-showpercent="true" lay-filter="memory">
<div class="layui-progress-bar" lay-percent="{$performanceInfo.memory_percent}%"></div>
</div>
</td>
</tr>
<tr>
<td>峰值内存</td>
<td>{$performanceInfo.peak_memory}</td>
</tr>
<tr>
<td>系统负载</td>
<td>{$performanceInfo.load_average}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,40 @@
$(function () {
layui.use(['element', 'layer'], function () {
var element = layui.element;
var layer = layui.layer;
var storageUsage = parseFloat($('div[lay-filter="storage"] .layui-progress-bar').attr('lay-percent'));
var memoryUsage = parseFloat($('div[lay-filter="memory"] .layui-progress-bar').attr('lay-percent'));
if (storageUsage > 80) {
$('div[lay-filter="storage"] .layui-progress-bar').addClass('layui-bg-red');
} else if (storageUsage > 60) {
$('div[lay-filter="storage"] .layui-progress-bar').addClass('layui-bg-orange');
}
if (memoryUsage > 80) {
$('div[lay-filter="memory"] .layui-progress-bar').addClass('layui-bg-red');
} else if (memoryUsage > 60) {
$('div[lay-filter="memory"] .layui-progress-bar').addClass('layui-bg-orange');
}
if (typeof ClipboardJS !== 'undefined') {
var clipboard = new ClipboardJS('[data-toggle="copy-text"]', {
text: function (trigger) {
return $(trigger).attr('data-clipboard-text');
}
});
clipboard.on('success', function (e) {
layer.msg('复制成功', {icon: 1, time: 1500});
e.clearSelection();
});
clipboard.on('error', function (e) {
layer.msg('复制失败,请手动复制', {icon: 2});
});
}
});
ua.listen();
});