加入流量查看,访客与视频数量图片数量查看
This commit is contained in:
@@ -57,5 +57,23 @@ try {
|
|||||||
respond(false, '保存失败: ' . $e->getMessage());
|
respond(false, '保存失败: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath));
|
$bytes = @filesize($targetPath) ?: 0;
|
||||||
|
try {
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
$daily[$today]['bandwidth_out'] += (int)$bytes;
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath));
|
||||||
|
|||||||
@@ -62,5 +62,23 @@ try {
|
|||||||
respond(false, '保存失败: ' . $e->getMessage());
|
respond(false, '保存失败: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath));
|
$bytes = @filesize($targetPath) ?: 0;
|
||||||
|
try {
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
$daily[$today]['bandwidth_out'] += (int)$bytes;
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath));
|
||||||
|
|||||||
@@ -77,6 +77,25 @@ if ($skipDownload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bytes = @filesize($targetPath) ?: 0;
|
||||||
|
try {
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
$daily[$today]['bandwidth_out'] += (int)$bytes;
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
// 恢复清理逻辑:仅保留最近两天的视频目录,删除更早的目录
|
// 恢复清理逻辑:仅保留最近两天的视频目录,删除更早的目录
|
||||||
try {
|
try {
|
||||||
$dirs = @scandir($videosRoot);
|
$dirs = @scandir($videosRoot);
|
||||||
@@ -93,7 +112,7 @@ try {
|
|||||||
// 按日期升序排序
|
// 按日期升序排序
|
||||||
sort($dateDirs);
|
sort($dateDirs);
|
||||||
// 保留最近两天(末尾两项),其它全部删除
|
// 保留最近两天(末尾两项),其它全部删除
|
||||||
if (count($dateDirs) > 30) {
|
if (count($dateDirs) > 2) {
|
||||||
$toDelete = array_slice($dateDirs, 0, count($dateDirs) - 2);
|
$toDelete = array_slice($dateDirs, 0, count($dateDirs) - 2);
|
||||||
foreach ($toDelete as $del) {
|
foreach ($toDelete as $del) {
|
||||||
rrmdir($videosRoot . DIRECTORY_SEPARATOR . $del);
|
rrmdir($videosRoot . DIRECTORY_SEPARATOR . $del);
|
||||||
|
|||||||
99
api/stats.php
Normal file
99
api/stats.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$eventsRoot = $statsRoot . '/events';
|
||||||
|
if (!is_dir($eventsRoot)) { @mkdir($eventsRoot, 0777, true); }
|
||||||
|
$uvRoot = $statsRoot . '/uv';
|
||||||
|
if (!is_dir($uvRoot)) { @mkdir($uvRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
$keys = array_keys($daily);
|
||||||
|
sort($keys);
|
||||||
|
$last7 = array_slice($keys, max(0, count($keys) - 7));
|
||||||
|
$out = [];
|
||||||
|
foreach ($last7 as $k) { $out[$k] = $daily[$k]; }
|
||||||
|
$todayData = $daily[$today] ?? ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
echo json_encode(['success' => true, 'today' => $todayData, 'daily' => $out], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
$event = isset($data['event']) ? trim((string)$data['event']) : '';
|
||||||
|
$page = isset($data['page']) ? trim((string)$data['page']) : '';
|
||||||
|
$url = isset($data['url']) ? trim((string)$data['url']) : '';
|
||||||
|
$title = isset($data['title']) ? trim((string)$data['title']) : '';
|
||||||
|
$allowed = ['visit','video_play','picture_show','bandwidth'];
|
||||||
|
if (!in_array($event, $allowed, true)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'invalid event']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
if ($event === 'visit') { $daily[$today]['visits']++; }
|
||||||
|
if ($event === 'video_play') { $daily[$today]['video_plays']++; }
|
||||||
|
if ($event === 'picture_show') { $daily[$today]['picture_shows']++; }
|
||||||
|
|
||||||
|
if ($event === 'visit') {
|
||||||
|
$uvPath = $uvRoot . '/' . $today . '.json';
|
||||||
|
$ips = [];
|
||||||
|
if (file_exists($uvPath)) {
|
||||||
|
$rawIps = @file_get_contents($uvPath);
|
||||||
|
$decodedIps = json_decode($rawIps, true);
|
||||||
|
if (is_array($decodedIps)) { $ips = $decodedIps; }
|
||||||
|
}
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
if ($ip !== '') {
|
||||||
|
if (!in_array($ip, $ips, true)) { $ips[] = $ip; }
|
||||||
|
}
|
||||||
|
@file_put_contents($uvPath, json_encode($ips, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
||||||
|
$daily[$today]['uv'] = count($ips);
|
||||||
|
}
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
|
||||||
|
$eventsPath = $eventsRoot . '/' . $today . '.json';
|
||||||
|
$events = [];
|
||||||
|
if (file_exists($eventsPath)) {
|
||||||
|
$rawEvents = @file_get_contents($eventsPath);
|
||||||
|
$decoded = json_decode($rawEvents, true);
|
||||||
|
if (is_array($decoded)) { $events = $decoded; }
|
||||||
|
}
|
||||||
|
$events[] = [
|
||||||
|
'type' => $event,
|
||||||
|
'page' => $page,
|
||||||
|
'url' => $url,
|
||||||
|
'title' => $title,
|
||||||
|
'direction' => $direction ?? '',
|
||||||
|
'bytes' => $bytes ?? 0,
|
||||||
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||||
|
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
'ts' => (new DateTime('now'))->format(DateTime::ATOM),
|
||||||
|
];
|
||||||
|
@file_put_contents($eventsPath, json_encode($events, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'today' => $daily[$today]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$direction = isset($data['direction']) ? (string)$data['direction'] : '';
|
||||||
|
$bytes = isset($data['bytes']) ? intval($data['bytes']) : 0;
|
||||||
|
if ($event === 'bandwidth' && $bytes > 0) {
|
||||||
|
if ($direction === 'in') { $daily[$today]['bandwidth_in'] += $bytes; }
|
||||||
|
else if ($direction === 'out') { $daily[$today]['bandwidth_out'] += $bytes; }
|
||||||
|
}
|
||||||
@@ -71,6 +71,25 @@ if (!move_uploaded_file($file['tmp_name'], $target)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bytes = (int)($file['size'] ?? 0);
|
||||||
|
try {
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
$daily[$today]['bandwidth_in'] += (int)$bytes;
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
$meta = [
|
$meta = [
|
||||||
'original_name' => $origName,
|
'original_name' => $origName,
|
||||||
'saved_name' => basename($target),
|
'saved_name' => basename($target),
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ if (!move_uploaded_file($file['tmp_name'], $target)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bytes = (int)($file['size'] ?? 0);
|
||||||
|
try {
|
||||||
|
$statsRoot = __DIR__ . '/../stats';
|
||||||
|
if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); }
|
||||||
|
$dailyPath = $statsRoot . '/daily.json';
|
||||||
|
$today = (new DateTime('now'))->format('Y-m-d');
|
||||||
|
$daily = [];
|
||||||
|
if (file_exists($dailyPath)) {
|
||||||
|
$rawDaily = @file_get_contents($dailyPath);
|
||||||
|
$decoded = json_decode($rawDaily, true);
|
||||||
|
if (is_array($decoded)) { $daily = $decoded; }
|
||||||
|
}
|
||||||
|
if (!isset($daily[$today]) || !is_array($daily[$today])) {
|
||||||
|
$daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0];
|
||||||
|
}
|
||||||
|
$daily[$today]['bandwidth_in'] += (int)$bytes;
|
||||||
|
@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
// 可选:记录一个简单的元信息文件
|
// 可选:记录一个简单的元信息文件
|
||||||
$meta = [
|
$meta = [
|
||||||
'original_name' => $origName,
|
'original_name' => $origName,
|
||||||
|
|||||||
@@ -14,6 +14,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const videoWrap = document.getElementById('videoWrap');
|
const videoWrap = document.getElementById('videoWrap');
|
||||||
const videoPanel = document.getElementById('video');
|
const videoPanel = document.getElementById('video');
|
||||||
const includeLocalToggle = document.getElementById('includeLocalToggle');
|
const includeLocalToggle = document.getElementById('includeLocalToggle');
|
||||||
|
const statFlowEl = document.getElementById('statFlow');
|
||||||
|
const statVisitEl = document.getElementById('statVisit');
|
||||||
|
const statVideoEl = document.getElementById('statVideo');
|
||||||
|
const statPicEl = document.getElementById('statPic');
|
||||||
|
const formatBytes = (n) => {
|
||||||
|
let val = Number(n) || 0;
|
||||||
|
const units = ['B','KB','MB','GB','TB','PB','EB','ZB','YB'];
|
||||||
|
let i = 0;
|
||||||
|
while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
|
||||||
|
const digits = val >= 100 ? 0 : (val >= 10 ? 1 : 2);
|
||||||
|
return val.toFixed(digits) + ' ' + units[i];
|
||||||
|
};
|
||||||
|
const formatCount = (n) => {
|
||||||
|
if (n >= 100000000) return (n / 100000000).toFixed(n < 1000000000 ? 2 : 1) + ' 亿';
|
||||||
|
if (n >= 10000) return (n / 10000).toFixed(n < 100000 ? 2 : 1) + ' 万';
|
||||||
|
return new Intl.NumberFormat('zh-CN').format(n);
|
||||||
|
};
|
||||||
|
const updateStatsUI = (t) => {
|
||||||
|
if (!t) return;
|
||||||
|
const bw = (t.bandwidth_in ?? 0) + (t.bandwidth_out ?? 0);
|
||||||
|
if (statFlowEl) statFlowEl.textContent = formatBytes(bw);
|
||||||
|
if (statVisitEl) statVisitEl.textContent = formatCount(t.visits ?? 0);
|
||||||
|
if (statVideoEl) statVideoEl.textContent = formatCount(t.video_plays ?? 0);
|
||||||
|
if (statPicEl) statPicEl.textContent = formatCount(t.picture_shows ?? 0);
|
||||||
|
};
|
||||||
|
const track = async (payload) => {
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/stats.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
if (j && j.success && j.today) updateStatsUI(j.today);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
(async () => { try { const r = await fetch('api/stats.php'); const j = await r.json(); if (j && j.success && j.today) updateStatsUI(j.today); } catch {} })();
|
||||||
|
track({ event: 'visit', page: location.pathname });
|
||||||
// 底部工具栏与标题已移除,无需获取相关元素
|
// 底部工具栏与标题已移除,无需获取相关元素
|
||||||
|
|
||||||
if (!player || !videoWrap) return; // 仅在视频区存在时执行
|
if (!player || !videoWrap) return; // 仅在视频区存在时执行
|
||||||
@@ -332,6 +370,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pictureImg.src = url;
|
pictureImg.src = url;
|
||||||
picLastSwap = Date.now();
|
picLastSwap = Date.now();
|
||||||
if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0');
|
if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0');
|
||||||
|
track({ event: 'picture_show', page: location.pathname, url });
|
||||||
if (pictureWrap.matches(':hover')) showPic();
|
if (pictureWrap.matches(':hover')) showPic();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -341,6 +380,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pictureImg.src = url;
|
pictureImg.src = url;
|
||||||
picLastSwap = Date.now();
|
picLastSwap = Date.now();
|
||||||
if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0');
|
if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0');
|
||||||
|
track({ event: 'picture_show', page: location.pathname, url });
|
||||||
if (pictureWrap.matches(':hover')) showPic();
|
if (pictureWrap.matches(':hover')) showPic();
|
||||||
}
|
}
|
||||||
}, picIntervalMs);
|
}, picIntervalMs);
|
||||||
|
|||||||
@@ -178,6 +178,22 @@ body {
|
|||||||
}
|
}
|
||||||
.menu__controls input[type="checkbox"] { vertical-align: middle; margin-right: 6px; }
|
.menu__controls input[type="checkbox"] { vertical-align: middle; margin-right: 6px; }
|
||||||
|
|
||||||
|
.menu__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.menu__stats .stat {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--red);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.menu__stats .stat__label { opacity: .8; margin-right: 4px; }
|
||||||
|
.menu__stats .stat__value { font-weight: 600; }
|
||||||
|
|
||||||
video#player {
|
video#player {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|||||||
@@ -169,6 +169,12 @@
|
|||||||
<input type="checkbox" id="includeLocalToggle" /> 包含本地
|
<input type="checkbox" id="includeLocalToggle" /> 包含本地
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="menu__stats" id="menuStats" aria-label="统计">
|
||||||
|
<span class="stat"><span class="stat__label">流量</span> <span class="stat__value" id="statFlow">0</span></span>
|
||||||
|
<span class="stat"><span class="stat__label">访问</span> <span class="stat__value" id="statVisit">0</span></span>
|
||||||
|
<span class="stat"><span class="stat__label">视频</span> <span class="stat__value" id="statVideo">0</span></span>
|
||||||
|
<span class="stat"><span class="stat__label">图片</span> <span class="stat__value" id="statPic">0</span></span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
|||||||
Reference in New Issue
Block a user