diff --git a/extend/base/admin/view/system/nginx_log_stat/_common.js b/extend/base/admin/view/system/nginx_log_stat/_common.js
new file mode 100644
index 0000000..bdf1790
--- /dev/null
+++ b/extend/base/admin/view/system/nginx_log_stat/_common.js
@@ -0,0 +1,3 @@
+// NginxLogStat dashboard _common.js
+// 仪表盘页面不使用 ua.table.render(数据由 AJAX 预取后客户端渲染),故 init 对象为空。
+// 本文件仅用于满足 STRICT_VIEW_JS=true(AdminControllerBase::fetchJS 要求 _common 模板存在)。
diff --git a/extend/base/admin/view/system/nginx_log_stat/index.html b/extend/base/admin/view/system/nginx_log_stat/index.html
new file mode 100644
index 0000000..8e2b463
--- /dev/null
+++ b/extend/base/admin/view/system/nginx_log_stat/index.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extend/base/admin/view/system/nginx_log_stat/index.js b/extend/base/admin/view/system/nginx_log_stat/index.js
new file mode 100644
index 0000000..d1d5baf
--- /dev/null
+++ b/extend/base/admin/view/system/nginx_log_stat/index.js
@@ -0,0 +1,398 @@
+/**
+ * NginxLogStat dashboard
+ *
+ * 依赖:echarts(_require.html 全局加载)、layui(table / laydate / util)、ua(admin-core.js)
+ *
+ * 后端 URL 不硬编码:用 ua.url('system.nginx_log_stat/index') 由 CONFIG.ADMIN 拼接。
+ * AJAX 触发方式:URL 带 _ajax=1(不依赖 Accept header,命令行测试友好)。
+ */
+layui.use(['table', 'laydate', 'util'], function () {
+
+ var table = layui.table;
+ var laydate = layui.laydate;
+ var util = layui.util;
+
+ // ── 配置(URL 不硬编码:由 ua.url 拼接 /{ADMIN}/system.nginx_log_stat/index)──
+ var STAT_INDEX_URL = ua.url('system.nginx_log_stat/index');
+
+ // ── DOM 引用 ──
+ var $dashboard = $('#nginxStatDashboard');
+ var $periodGroup = $('#periodGroup');
+ var $metricPv = $('#metricPv');
+ var $metricUv = $('#metricUv');
+ var $metricBytes = $('#metricBytes');
+ var $metricAvgTime = $('#metricAvgTime');
+ var $uvBadge = $('#uvEstimateBadge');
+ var $statusPie = $('#statusPieChart');
+ var $statusPieEmpty = $('#statusPieEmpty');
+ var $trend = $('#trendChart');
+ var $trendEmpty = $('#trendEmpty');
+
+ // ── echarts 实例(惰性初始化,容器存在时才创建)──
+ var statusPieChart = null;
+ var trendChart = null;
+
+ // ── 状态:当前选中的 period ──
+ var currentPeriod = $dashboard.data('period') || 'day';
+
+ // ================================================================
+ // 工具函数
+ // ================================================================
+
+ /**
+ * 格式化字节数为人类可读(B / KB / MB / GB)
+ */
+ function formatBytes(bytes) {
+ bytes = Number(bytes) || 0;
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
+ }
+
+ /**
+ * 格式化响应时间(秒 → ms / s)
+ */
+ function formatTime(seconds) {
+ seconds = Number(seconds) || 0;
+ if (seconds < 1) return Math.round(seconds * 1000) + ' ms';
+ return seconds.toFixed(2) + ' s';
+ }
+
+ /**
+ * 数字千分位
+ */
+ function formatNumber(num) {
+ num = Number(num) || 0;
+ return util.number ? util.number(num) : num.toLocaleString();
+ }
+
+ /**
+ * 截断长文本(表格 label 列)
+ */
+ function truncate(text, maxLen) {
+ text = String(text || '');
+ maxLen = maxLen || 50;
+ if (text.length <= maxLen) return text;
+ return text.substring(0, maxLen) + '...';
+ }
+
+ // ================================================================
+ // period 按钮切换
+ // ================================================================
+
+ /**
+ * 设置 period 按钮高亮(active 用 layui-btn 实色,其余 layui-btn-primary)
+ */
+ function setActivePeriod(period) {
+ currentPeriod = period;
+ $periodGroup.find('button[data-period]').each(function () {
+ var $btn = $(this);
+ if ($btn.data('period') === period) {
+ $btn.removeClass('layui-btn-primary');
+ } else {
+ $btn.addClass('layui-btn-primary');
+ }
+ });
+ }
+
+ $periodGroup.on('click', 'button[data-period]', function () {
+ var period = $(this).data('period');
+ setActivePeriod(period);
+ loadData({ period: period });
+ });
+
+ // ================================================================
+ // 自定义日期区间
+ // ================================================================
+
+ laydate.render({
+ elem: '#dateStart',
+ type: 'date',
+ format: 'yyyy-MM-dd',
+ });
+ laydate.render({
+ elem: '#dateEnd',
+ type: 'date',
+ format: 'yyyy-MM-dd',
+ });
+
+ $('#customSearchBtn').on('click', function () {
+ var startVal = $('#dateStart').val();
+ var endVal = $('#dateEnd').val();
+ if (!startVal || !endVal) {
+ ua.msg.error('请选择开始和结束日期');
+ return;
+ }
+ // 选了自定义区间后取消 period 按钮高亮
+ setActivePeriod('');
+ loadData({ period: 'custom', start: startVal, end: endVal });
+ });
+
+ // ================================================================
+ // AJAX 加载数据
+ // ================================================================
+
+ /**
+ * 请求后端统计数据并重绘全部区块
+ *
+ * @param {object} extra 额外参数(period / start / end)
+ */
+ function loadData(extra) {
+ extra = extra || {};
+
+ var params = $.extend({ _ajax: 1, period: currentPeriod }, extra);
+
+ ua.request.get({
+ url: STAT_INDEX_URL + '?_ajax=1',
+ data: params,
+ statusName: 'code',
+ statusCode: 0,
+ }, function (res) {
+ // ok:code=0
+ var data = (res && res.data) ? res.data : null;
+ if (!data) {
+ renderAll(buildEmptyData());
+ return;
+ }
+ renderAll(data);
+ }, function (res) {
+ // no:code!=0 或业务错误
+ var msg = (res && res.msg) ? res.msg : '加载统计数据失败';
+ ua.msg.error(msg);
+ renderAll(buildEmptyData());
+ }, function () {
+ // ex:网络异常等
+ renderAll(buildEmptyData());
+ });
+ }
+
+ function buildEmptyData() {
+ return {
+ summary: {
+ pv: 0, uv: 0, total_bytes: 0,
+ status_2xx: 0, status_3xx: 0, status_4xx: 0, status_5xx: 0, status_other: 0,
+ avg_request_time: 0,
+ },
+ trend: [],
+ top_urls: [],
+ top_referers: [],
+ top_uas: [],
+ uv_is_estimate: false,
+ };
+ }
+
+ // ================================================================
+ // 渲染:汇总入口
+ // ================================================================
+
+ function renderAll(data) {
+ renderSummary(data.summary || {}, data.uv_is_estimate);
+ renderStatusPie(data.summary || {});
+ renderTrend(data.trend || []);
+ renderTopTables({
+ top_urls: data.top_urls || [],
+ top_referers: data.top_referers || [],
+ top_uas: data.top_uas || [],
+ });
+ }
+
+ // ================================================================
+ // 渲染:指标卡片
+ // ================================================================
+
+ function renderSummary(summary, uvIsEstimate) {
+ $metricPv.text(formatNumber(summary.pv));
+ $metricUv.text(formatNumber(summary.uv));
+ $metricBytes.text(formatBytes(summary.total_bytes));
+ $metricAvgTime.text(formatTime(summary.avg_request_time));
+
+ // UV 近似角标
+ if (uvIsEstimate) {
+ $uvBadge.show();
+ } else {
+ $uvBadge.hide();
+ }
+ }
+
+ // ================================================================
+ // 渲染:状态码饼图
+ // ================================================================
+
+ function renderStatusPie(summary) {
+ var segments = [
+ { name: '2xx', value: Number(summary.status_2xx) || 0, color: '#5FB878' },
+ { name: '3xx', value: Number(summary.status_3xx) || 0, color: '#1E9FFF' },
+ { name: '4xx', value: Number(summary.status_4xx) || 0, color: '#FFB800' },
+ { name: '5xx', value: Number(summary.status_5xx) || 0, color: '#FF5722' },
+ { name: '其他', value: Number(summary.status_other) || 0, color: '#999' },
+ ];
+
+ var total = 0;
+ for (var i = 0; i < segments.length; i++) {
+ total += segments[i].value;
+ }
+
+ // 空数据 → 隐藏 chart、显示占位
+ if (total === 0) {
+ $statusPie.hide();
+ $statusPieEmpty.show();
+ return;
+ }
+ $statusPie.show();
+ $statusPieEmpty.hide();
+
+ if (!statusPieChart) {
+ statusPieChart = echarts.init($statusPie[0]);
+ }
+
+ statusPieChart.setOption({
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+ legend: { bottom: 0, left: 'center', data: segments.map(function (s) { return s.name; }) },
+ series: [{
+ name: '状态码',
+ type: 'pie',
+ radius: ['40%', '65%'],
+ center: ['50%', '45%'],
+ avoidLabelOverlap: true,
+ label: { show: true, formatter: '{b}\n{d}%' },
+ labelLine: { show: true },
+ data: segments.map(function (s) {
+ return { name: s.name, value: s.value, itemStyle: { color: s.color } };
+ }),
+ }],
+ });
+ }
+
+ // ================================================================
+ // 渲染:趋势折线图(PV/UV 双 Y 轴)
+ // ================================================================
+
+ function renderTrend(trend) {
+ var hasData = trend && trend.length > 0;
+ var pvSum = 0;
+ if (hasData) {
+ for (var i = 0; i < trend.length; i++) {
+ pvSum += Number(trend[i].pv) || 0;
+ }
+ }
+
+ // 全部点 PV 都为 0 → 视为空数据
+ if (!hasData || pvSum === 0) {
+ $trend.hide();
+ $trendEmpty.show();
+ return;
+ }
+ $trend.show();
+ $trendEmpty.hide();
+
+ if (!trendChart) {
+ trendChart = echarts.init($trend[0]);
+ }
+
+ var labels = trend.map(function (d) { return d.label; });
+ var pvData = trend.map(function (d) { return Number(d.pv) || 0; });
+ var uvData = trend.map(function (d) { return Number(d.uv) || 0; });
+
+ trendChart.setOption({
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['PV', 'UV'], bottom: 0 },
+ grid: { left: '3%', right: '4%', bottom: '12%', top: '8%', containLabel: true },
+ xAxis: { type: 'category', boundaryGap: false, data: labels },
+ yAxis: [
+ { type: 'value', name: 'PV', position: 'left' },
+ { type: 'value', name: 'UV', position: 'right' },
+ ],
+ series: [
+ {
+ name: 'PV',
+ type: 'line',
+ smooth: true,
+ symbol: 'circle',
+ symbolSize: 6,
+ itemStyle: { color: '#1E9FFF' },
+ areaStyle: { opacity: 0.1 },
+ data: pvData,
+ },
+ {
+ name: 'UV',
+ type: 'line',
+ smooth: true,
+ yAxisIndex: 1,
+ symbol: 'circle',
+ symbolSize: 6,
+ itemStyle: { color: '#5FB878' },
+ areaStyle: { opacity: 0.1 },
+ data: uvData,
+ },
+ ],
+ }, true);
+ }
+
+ // ================================================================
+ // 渲染:Top 表格(客户端数据,无分页)
+ // ================================================================
+
+ /**
+ * 通用 Top 表格渲染(客户端 data 模式)
+ *
+ * @param {string} elem 表格选择器
+ * @param {string} filter lay-filter
+ * @param {array} rows 数据行 [{label,pv,uv}]
+ * @param {number} maxLabelLen label 截断长度
+ */
+ function renderTopTable(elem, filter, rows, maxLabelLen) {
+ // layui table 客户端渲染:传 data 数组(非 url)
+ table.render({
+ elem: elem,
+ filter: filter,
+ data: rows || [],
+ page: false,
+ limit: 1000,
+ even: true,
+ text: { none: '暂无数据' },
+ cols: [[
+ { type: 'numbers', title: '#', width: 45 },
+ {
+ field: 'label', title: '名称', minWidth: 120,
+ templet: function (d) {
+ var text = truncate(d.label, maxLabelLen || 50);
+ // 鼠标悬停显示完整内容
+ if (d.label && String(d.label).length > (maxLabelLen || 50)) {
+ return '' + text + '';
+ }
+ return text;
+ },
+ },
+ { field: 'pv', title: 'PV', width: 80, sort: true },
+ { field: 'uv', title: 'UV', width: 80, sort: true },
+ ]],
+ });
+ }
+
+ function renderTopTables(data) {
+ renderTopTable('#topUrlTable', 'topUrlTable', data.top_urls, 50);
+ renderTopTable('#topRefererTable', 'topRefererTable', data.top_referers, 40);
+ renderTopTable('#topUaTable', 'topUaTable', data.top_uas, 30);
+ }
+
+ // ================================================================
+ // 窗口 resize → echarts 自适应
+ // ================================================================
+
+ $(window).on('resize', function () {
+ if (statusPieChart) statusPieChart.resize();
+ if (trendChart) trendChart.resize();
+ });
+
+ // ================================================================
+ // 初始化
+ // ================================================================
+
+ // 初始高亮当前 period 按钮(由控制器 assign 的 period 决定)
+ setActivePeriod(currentPeriod);
+
+ // 首次加载数据
+ loadData({ period: currentPeriod });
+
+});