This commit is contained in:
2025-04-18 01:20:11 +08:00
commit 03e2d4f092
15 changed files with 4824 additions and 0 deletions

162
.gitignore vendored Normal file
View File

@@ -0,0 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
# CGNICO 任务报告系统
这是一个用于跟踪和报告工具使用情况的系统专为群晖DSM的Webstation环境优化。
## 系统功能
- 记录工具使用数据(用户名、工具名称、任务名称、节省时间、花费时间)
- 提供数据可视化界面,展示工具使用统计和趋势
- 支持数据筛选、排序和导出
- 提供API接口方便其他应用程序提交数据
- 支持定期生成报告
## 技术栈
- PHP 7.4+
- MariaDB 10
- HTML/CSS/JavaScript
- Chart.js (用于数据可视化)
## 文件结构
- `index.php` - 主页和API处理
- `query_task_data.php` - 数据查询和可视化界面
- `api.php` - 独立的API接口
- `client_example.php` - API使用示例
- `insert_test_data.php` - 插入测试数据的工具
- `check_data.php` - 检查数据库记录的工具
- `generate_report.php` - 定期报告生成工具
- `static/` - 静态资源文件夹
## 部署指南
### 系统要求
- 群晖DSM系统
- Webstation已安装使用Apache HTTP Server 2.4
- PHP 7.4+
- MariaDB 10
### 部署步骤
1. 将所有文件复制到群晖DSM的Web目录`/volume1/web/task-reporter`
2. 设置正确的文件权限:
```bash
chmod -R 755 /volume1/web/task-reporter
chown -R http:http /volume1/web/task-reporter
```
3. 在MariaDB中创建数据库和用户
```sql
CREATE DATABASE task_reporter;
CREATE USER 'task_reporter'@'%' IDENTIFIED BY 'Pass12349ers!';
GRANT ALL PRIVILEGES ON task_reporter.* TO 'task_reporter'@'%';
FLUSH PRIVILEGES;
```
4. 在Webstation中配置网站
- 虚拟主机名称toolstrack.cgnico.com
- 端口9000
- 文档根目录:/volume1/web/task-reporter
- 启用PHP
- Web服务器Apache HTTP Server 2.4
### 访问系统
- 本地访问:[http://192.168.2.4:9000](http://192.168.2.4:9000)
- 域名访问:[https://toolstrack.cgnico.com](https://toolstrack.cgnico.com)
## API使用
系统提供了简单的API接口用于提交任务数据
```bash
POST [https://toolstrack.cgnico.com/api.php](https://toolstrack.cgnico.com/api.php)
```
请求体示例JSON格式
```json
{
"username": "用户名",
"tool_name": "工具名称",
"task_name": "任务名称",
"time_saved": "1.5",
"time_cost": "0.2",
"timestamp": "2025-04-13 12:30:00" // 可选
}
```
成功响应示例:
```json
{
"success": true,
"message": "2025-04-13 12:30:00 工具名称-任务名称 reported successfully!",
"id": 123
}
```
详细的API使用示例请参考 `client_example.php` 文件。
## 定期报告功能
系统支持定期生成工具使用报告:
### 功能特点
- 自动生成全面的工具使用报告
- 支持多种时间范围(周报、月报、季报、年报)
- 包含总体统计、工具排名、用户排名和每日趋势
- 自动保存报告历史记录
- 支持在线查看和下载报告
### 使用方法
1. **手动生成报告**
访问 [https://toolstrack.cgnico.com/generate_report.php](https://toolstrack.cgnico.com/generate_report.php) 页面,选择报告时间范围,点击"生成报告"按钮。
2. **设置定期执行**
在群晖DSM的控制面板中找到"任务计划",添加新的计划任务:
```bash
# 每周日凌晨生成周报
0 0 * * 7 php /volume1/web/task-reporter/generate_report.php
# 或每月1日生成月报
0 0 1 * * php /volume1/web/task-reporter/generate_report.php
```
3. **查看历史报告**
在生成报告页面下方可以看到历史报告列表,点击"查看"或"下载"按钮访问历史报告。
所有生成的报告都会保存在 `reports` 目录下,以日期命名,方便随时查看历史报告。也可以在主页上通过"查看报告"按钮直接访问最新报告。
## 联系方式
如有问题,请联系:[jeffreytsai1004@gmail.com](mailto:jeffreytsai1004@gmail.com)

231
TaskReporter.py Normal file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
TaskReporter - Task Reporting Tool
This script is designed to record and track user task execution. It sends task information
(including username, tool name, task name, time saved, time cost) to a specified server for statistics.
Usage:
#---------------------------------- usage 1: decorator ----------------------------------
from TaskReporter import task_reporter_decorator
debug_mode = True
@task_reporter_decorator(tool_name='MetaBox', task_name='CheckUV', debug=debug_mode, time_saved=5)
def your_function():
# your code
#---------------------------------- usage 2: function ----------------------------------
from TaskReporter import task_reporter
debug_mode = True
def your_function():
start_time = time.time()
# your function code
task_reporter(tool_name='MetaBox', task_name='CheckUV', debug=debug_mode, time_saved=5, time_cost=round(time.time()-start_time, 3))
"""
import os, sys
from functools import wraps
import time, datetime, json
# add lib path to import requests
py_version = sys.version_info.major
minor_version = sys.version_info.minor
version = "py{}{}".format(py_version, minor_version) if py_version in [2,3] and minor_version<=7 else "py38+"
lib_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "reporter_lib", version)
if lib_path not in sys.path:
sys.path.append(lib_path)
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
class TaskReporter:
def __init__(self):
self.session = requests.Session()
self.session.mount('http://', HTTPAdapter(max_retries=3))
self.session.mount('https://', HTTPAdapter(max_retries=3))
self.server_status = {'available': True, 'last_check': 0}
self.SERVER_URL = 'https://toolstrack.cgnico.com'
self.cache_file = os.path.join(os.environ.get('APPDATA'), 'task_cache.json')
def report_task(self, data, is_cached_task=False):
# first check and send cached data
if not is_cached_task:
self._try_send_cached_tasks()
task_has_cached = False
current_time = time.time()
if not self.server_status['available'] and not is_cached_task:
if current_time - self.server_status['last_check'] < 10:
self._cache_task(data)
return False
try:
response = self.session.post(
'{}/api.php'.format(self.SERVER_URL),
json=data,
timeout=1
)
self.server_status['available'] = True
print(response.json())
return response.json()
except (requests.exceptions.RequestException, TimeoutError) as e:
self.server_status['available'] = False
self.server_status['last_check'] = current_time
if not task_has_cached and not is_cached_task: self._cache_task(data)
task_has_cached = True
print("Failed to connect to server, task has been cached\n{}".format(str(e)))
# print("Failed to connect to server: {}".format(str(e)))
return False
def _cache_task(self, data):
try:
data['timestamp'] = (datetime.datetime.utcnow() + datetime.timedelta(hours=8)).isoformat()
cache = self._read_cache()
cache.append(data)
with open(self.cache_file, 'w', encoding='utf-8') as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
# print("Task cached: {}".format(self.cache_file))
except Exception as e:
print("Cache task failed:{}".format(str(e)))
def _read_cache(self):
try:
if os.path.exists(self.cache_file):
with open(self.cache_file, 'r', encoding='utf-8') as f:
return json.load(f)
return []
except Exception as e:
print("Read cache failed: {}".format(str(e)))
return []
def _try_send_cached_tasks(self):
if not os.path.exists(self.cache_file):
return
cache = self._read_cache()
if not cache:
return
# print("Find {} cached tasks, try to send again...".format(len(cache)))
if not self.server_status['available']:
return
successful_indices = []
for i, task in enumerate(cache):
try:
response = self.report_task(task, is_cached_task=True)
if response != False:
successful_indices.append(i)
print("Cached task sent successfully: {}".format(task.get('task_name')))
else:
# print("Cached task sent failed")
break
except Exception as e:
print("Send cached task failed: {}".format(str(e)))
break
if successful_indices:
new_cache = [task for i, task in enumerate(cache) if i not in successful_indices]
if new_cache:
with open(self.cache_file, 'w', encoding='utf-8') as f:
json.dump(new_cache, f, ensure_ascii=False, indent=2)
print("{} tasks left to send".format(len(new_cache)))
else:
os.remove(self.cache_file)
print("All cached tasks sent")
reporter = TaskReporter()
def task_reporter_decorator(tool_name, task_name, debug=False, time_saved=0):
"""
task reporter decorator
:param tool_name: name of the tool, eg "MetaBox"
:param task_name: name of the task, eg "CheckUV"
:param debug: set to True when develop, and set it to False when release the tool to artists
:param time_saved: time saved for this task, usually should communicate with artists to get a reasonable value
"""
# reporter = TaskReporter()
def run_and_report(func):
@wraps(func)
def execute_task(*args, **kwargs):
# record start time
start_time = time.time()
# execute original function
try:
result = func(*args, **kwargs)
return result
except Exception as e:
raise e
finally:
# report task - 无论函数执行成功与否都发送报告
success = result != False
data= {
'username': os.environ.get('USERNAME'),
'tool_name': tool_name + "(Debug)" if debug else tool_name,
'task_name': task_name,
'time_saved': str(time_saved if success else 0), # 如果失败节省时间为0
'time_cost': str(round(time.time() - start_time, 3)), # 确保是字符串格式
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 使用文档中指定的时间格式
'status': "success" if success else "failed" # 添加状态字段
}
reporter.report_task(data)
return execute_task
return run_and_report
def task_reporter(tool_name, task_name, debug=False, time_saved=0, time_cost=0):
'''
task reporter function
:param tool_name: name of the tool, eg "MetaBox"
:param task_name: name of the task, eg "CheckUV"
:param debug: set to True when develop, and set it to False when release the tool to artists
:param time_saved: time saved for this task, usually should communicate with artists to get a reasonable value
:param time_cost: time cost for this task, you can get it from
time_cost = round(time.time() - start_time, 3)
'''
# reporter = TaskReporter()
data = {
'username': os.environ.get('USERNAME'),
'tool_name': tool_name + "(Debug)" if debug else tool_name,
'task_name': task_name,
'time_saved': str(time_saved), # 确保是字符串格式
'time_cost': str(round(time_cost, 3)), # 确保是字符串格式
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 使用文档中指定的时间格式
}
reporter.report_task(data)
if __name__ == '__main__':
#---------------------------------- usage 1: decorator ----------------------------------
debug_mode = True
@task_reporter_decorator(tool_name='MetaBox', task_name='CheckUV', debug=debug_mode, time_saved=5)
def check_uv_dec():
print("Check UV, and report by decorator")
#---------------------------------- usage 2: function ----------------------------------
def check_uv_func():
debug_mode = True
start_time = time.time()
# function code
print("Check UV, and report by function")
task_reporter(tool_name='MetaBox', task_name='CheckUV', debug=debug_mode, time_saved=5, time_cost=round(time.time()-start_time, 3))
check_uv_dec()
check_uv_func()

140
api.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 允许跨域请求
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json; charset=UTF-8");
// 处理OPTIONS请求预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// 数据库连接信息
$db_host = '192.168.2.4';
$db_port = 3307;
$db_name = 'task_reporter';
$db_user = 'task_reporter';
$db_pass = 'Pass12349ers!';
// 创建数据库连接
try {
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
} catch (PDOException $e) {
echo json_encode(["error" => "数据库连接失败: " . $e->getMessage()]);
exit;
}
// 记录请求到日志文件
function log_request($message) {
$log_file = __DIR__ . '/api_log.txt';
$timestamp = date('Y-m-d H:i:s');
$log_message = "[$timestamp] $message\n";
file_put_contents($log_file, $log_message, FILE_APPEND);
}
// 处理添加任务的POST请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
log_request("收到POST请求");
$raw_data = file_get_contents('php://input');
log_request("原始数据: $raw_data");
$data = json_decode($raw_data, true);
if (!$data) {
throw new Exception("无效的JSON数据: " . json_last_error_msg());
}
log_request("解析后的数据: " . print_r($data, true));
// 验证必要字段
$required_fields = ['username', 'tool_name', 'task_name', 'time_saved', 'time_cost'];
foreach ($required_fields as $field) {
if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) {
throw new Exception("缺少必要字段: $field");
}
}
// 如果没有提供时间戳,使用当前时间
if (!isset($data['timestamp']) || empty($data['timestamp'])) {
$data['timestamp'] = date('Y-m-d H:i:s');
log_request("使用当前时间作为时间戳: {$data['timestamp']}");
}
// 确保时间戳格式正确
if (strpos($data['timestamp'], 'T') !== false) {
$parts = explode(".", $data['timestamp']);
$data['timestamp'] = str_replace("T", " ", $parts[0]);
log_request("转换ISO时间戳格式: {$data['timestamp']}");
}
// 准备SQL语句
$stmt = $pdo->prepare("INSERT INTO task (username, tool_name, task_name, time_saved, time_cost, timestamp)
VALUES (:username, :tool_name, :task_name, :time_saved, :time_cost, :timestamp)");
// 绑定参数
$stmt->bindParam(':username', $data['username']);
$stmt->bindParam(':tool_name', $data['tool_name']);
$stmt->bindParam(':task_name', $data['task_name']);
$stmt->bindParam(':time_saved', $data['time_saved']);
$stmt->bindParam(':time_cost', $data['time_cost']);
$stmt->bindParam(':timestamp', $data['timestamp']);
log_request("准备执行SQL插入");
// 执行SQL
$result = $stmt->execute();
if ($result) {
$last_id = $pdo->lastInsertId();
log_request("数据成功插入ID: $last_id");
echo json_encode([
"success" => true,
"message" => "{$data['timestamp']} {$data['tool_name']}-{$data['task_name']} reported successfully!",
"id" => $last_id
]);
} else {
log_request("数据插入失败");
throw new Exception("数据插入失败");
}
} catch (Exception $e) {
log_request("错误: " . $e->getMessage());
http_response_code(400);
echo json_encode([
"success" => false,
"error" => $e->getMessage()
]);
}
exit;
}
// 处理简单的GET请求用于测试API是否正常工作
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo json_encode([
"status" => "API正常工作",
"message" => "使用POST请求提交任务数据",
"example" => [
"username" => "用户名",
"tool_name" => "工具名称",
"task_name" => "任务名称",
"time_saved" => "节省时间(小时)",
"time_cost" => "花费时间(小时)",
"timestamp" => "可选格式YYYY-MM-DD HH:MM:SS"
],
"contact" => "jeffreytsai1004@gmail.com"
]);
exit;
}
// 如果不是GET或POST请求返回错误
http_response_code(405);
echo json_encode(["error" => "不支持的请求方法"]);

67
check_data.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 数据库连接信息
$db_host = '192.168.2.4';
$db_port = 3307;
$db_name = 'task_reporter';
$db_user = 'task_reporter';
$db_pass = 'Pass12349ers!';
// 创建数据库连接
try {
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
echo "数据库连接成功!<br>";
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
// 查询最近的记录
try {
$stmt = $pdo->query("SELECT * FROM task ORDER BY id DESC LIMIT 20");
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "<h2>最近20条记录</h2>";
if (count($records) > 0) {
echo "<table border='1' cellpadding='5' cellspacing='0'>";
echo "<tr><th>ID</th><th>用户名</th><th>工具名称</th><th>任务名称</th><th>节省时间</th><th>花费时间</th><th>时间戳</th></tr>";
foreach ($records as $record) {
echo "<tr>";
echo "<td>{$record['id']}</td>";
echo "<td>{$record['username']}</td>";
echo "<td>{$record['tool_name']}</td>";
echo "<td>{$record['task_name']}</td>";
echo "<td>{$record['time_saved']}</td>";
echo "<td>{$record['time_cost']}</td>";
echo "<td>{$record['timestamp']}</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "没有找到记录";
}
} catch (PDOException $e) {
echo "查询失败: " . $e->getMessage();
}
// 检查API请求日志
echo "<h2>API请求日志检查</h2>";
// 检查api.php是否正确处理请求
echo "<p>请确认以下几点:</p>";
echo "<ol>";
echo "<li>api.php文件是否正确接收和处理请求</li>";
echo "<li>数据库连接信息是否正确</li>";
echo "<li>TaskReporter.py是否正确发送数据到api.php</li>";
echo "</ol>";
// 显示刷新页面的链接
echo "<p><a href='query_task_data.php'>刷新数据查询页面</a></p>";
?>

93
clean_old_reports.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/**
* 清理旧报告文件脚本
*
* 此脚本用于自动清理过期的报告文件,保持报告目录整洁
* 可以通过群晖的计划任务定期执行
*/
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 配置信息
$config = [
// 保留最近的报告数量
'keep_recent' => 30,
// 报告目录
'reports_dir' => __DIR__ . '/reports/',
// 日志文件
'log_file' => __DIR__ . '/logs/clean_reports.log',
];
// 记录日志
function log_message($message) {
global $config;
// 确保日志目录存在
$log_dir = dirname($config['log_file']);
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
$date = date('Y-m-d H:i:s');
$log_entry = "[$date] $message\n";
// 写入日志文件
file_put_contents($config['log_file'], $log_entry, FILE_APPEND);
// 如果是CLI模式也输出到控制台
if (php_sapi_name() === 'cli') {
echo $log_entry;
}
}
// 主程序
try {
log_message('===== 开始执行报告清理脚本 =====');
// 检查报告目录是否存在
if (!is_dir($config['reports_dir'])) {
throw new Exception("报告目录不存在: {$config['reports_dir']}");
}
// 获取所有报告文件
$reports = glob($config['reports_dir'] . 'report_*.html');
if (empty($reports)) {
log_message('没有找到需要清理的报告文件');
exit;
}
log_message('找到 ' . count($reports) . ' 个报告文件');
// 按修改时间排序(最新的在前)
usort($reports, function($a, $b) {
return filemtime($b) - filemtime($a);
});
// 保留最近的N个报告
$reports_to_keep = array_slice($reports, 0, $config['keep_recent']);
$reports_to_delete = array_slice($reports, $config['keep_recent']);
log_message('将保留最近的 ' . count($reports_to_keep) . ' 个报告');
log_message('将删除 ' . count($reports_to_delete) . ' 个旧报告');
// 删除旧报告
foreach ($reports_to_delete as $report) {
$filename = basename($report);
if (unlink($report)) {
log_message("已删除: $filename");
} else {
log_message("删除失败: $filename");
}
}
log_message('===== 报告清理完成 =====');
} catch (Exception $e) {
log_message('错误: ' . $e->getMessage());
}
?>

79
client_example.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
/**
* 任务报告系统客户端示例
*
* 此示例演示如何使用PHP向任务报告系统API提交数据
*/
// 设置API地址
$api_url = 'https://toolstrack.cgnico.com/api.php';
// 本地测试可以使用:
// $api_url = 'http://192.168.2.4:9000/api.php';
// 准备要提交的数据
$data = [
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'ExampleTask',
'time_saved' => '1.5', // 节省时间(小时)
'time_cost' => '0.2', // 花费时间(小时)
// 'timestamp' => date('Y-m-d H:i:s') // 可选,如果不提供将使用服务器时间
];
// 将数据转换为JSON
$json_data = json_encode($data);
// 设置cURL选项
$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($json_data)
]);
// 执行请求
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 输出结果
echo "HTTP响应代码: $http_code\n";
echo "响应内容:\n";
echo $response;
/**
* Python客户端示例
*
* 以下是使用Python向API提交数据的示例代码
*
* ```python
* import requests
* import json
* from datetime import datetime
*
* # 设置API地址
* api_url = 'https://toolstrack.cgnico.com/api.php'
* # 本地测试可以使用:
* # api_url = 'http://192.168.2.4:9000/api.php'
*
* # 准备要提交的数据
* data = {
* 'username': 'JeffreyTsai',
* 'tool_name': 'MetaBox',
* 'task_name': 'ExampleTask',
* 'time_saved': '1.5', # 节省时间(小时)
* 'time_cost': '0.2', # 花费时间(小时)
* # 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 可选
* }
*
* # 发送POST请求
* response = requests.post(api_url, json=data)
*
* # 输出结果
* print(f"HTTP响应代码: {response.status_code}")
* print("响应内容:")
* print(response.json())
* ```
*/

9
data.csv Normal file
View File

@@ -0,0 +1,9 @@
index,username,tool_name,task_name,timestamp
1,JeffreyTsai,MetaBox,StudioLibrary,2025-01-20 17:53:54
2,JeffreyTsai,MetaBox,UVTools,2025-01-20 17:53:54
3,JeffreyTsai,MetaBox,UVTools,2025-01-20 17:53:54
4,JeffreyTsai,MetaBox,ahoge,2025-01-21 10:23:37
5,JeffreyTsai,MetaBox,gscurvetool,2025-01-21 10:23:37
6,JeffreyTsai,MetaBox,DebugTool,2025-01-21 10:29:27
7,JeffreyTsai,MetaBox,CheckNormal,2025-01-21 10:42:27
8,JeffreyTsai,MetaBox,CheckNormal,2025-01-21 10:42:27
1 index username tool_name task_name timestamp
2 1 JeffreyTsai MetaBox StudioLibrary 2025-01-20 17:53:54
3 2 JeffreyTsai MetaBox UVTools 2025-01-20 17:53:54
4 3 JeffreyTsai MetaBox UVTools 2025-01-20 17:53:54
5 4 JeffreyTsai MetaBox ahoge 2025-01-21 10:23:37
6 5 JeffreyTsai MetaBox gscurvetool 2025-01-21 10:23:37
7 6 JeffreyTsai MetaBox DebugTool 2025-01-21 10:29:27
8 7 JeffreyTsai MetaBox CheckNormal 2025-01-21 10:42:27
9 8 JeffreyTsai MetaBox CheckNormal 2025-01-21 10:42:27

642
generate_report.php Normal file
View File

@@ -0,0 +1,642 @@
<?php
/**
* 定期报告生成脚本
*
* 此脚本生成任务报告并保存到服务器
* 可以通过群晖的计划任务定期执行
*/
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 启用网页输出模式
header('Content-Type: text/html; charset=utf-8');
// 配置信息
$config = [
// 数据库配置
'db_host' => '192.168.2.4',
'db_port' => 3307,
'db_name' => 'task_reporter',
'db_user' => 'task_reporter',
'db_pass' => 'Pass12349ers!',
// 报告生成选项
'author' => 'CGNICO Task Reporter', // 报告作者
// 报告配置
'report_days' => 7, // 报告包含的天数例如7表示过去一周
'report_title' => 'CGNICO工具使用报告', // 报告标题
];
// 根据报告天数设置标题
if (isset($_GET['days'])) {
$config['report_days'] = intval($_GET['days']);
// 根据天数调整报告标题
if ($config['report_days'] == 7) {
$config['report_title'] = 'CGNICO工具使用周报';
} elseif ($config['report_days'] == 30) {
$config['report_title'] = 'CGNICO工具使用月报';
} elseif ($config['report_days'] == 90) {
$config['report_title'] = 'CGNICO工具使用季报';
} elseif ($config['report_days'] == 365) {
$config['report_title'] = 'CGNICO工具使用年报';
}
}
// 创建数据库连接
try {
$pdo = new PDO(
"mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_name']}",
$config['db_user'],
$config['db_pass']
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
// 生成报告
function generate_report($pdo, $days) {
$report = [];
// 获取日期范围
$end_date = date('Y-m-d');
$start_date = date('Y-m-d', strtotime("-{$days} days"));
// 1. 总体统计
$stmt = $pdo->prepare("
SELECT
COUNT(*) as total_tasks,
SUM(time_saved) as total_time_saved,
COUNT(DISTINCT username) as total_users,
COUNT(DISTINCT tool_name) as total_tools
FROM task
WHERE timestamp BETWEEN :start_date AND :end_date
AND tool_name NOT LIKE '%(Debug)%'
");
$stmt->bindParam(':start_date', $start_date);
$stmt->bindParam(':end_date', $end_date);
$stmt->execute();
$report['summary'] = $stmt->fetch(PDO::FETCH_ASSOC);
// 2. 工具使用统计
$stmt = $pdo->prepare("
SELECT
tool_name,
COUNT(*) as usage_count,
SUM(time_saved) as time_saved
FROM task
WHERE timestamp BETWEEN :start_date AND :end_date
AND tool_name NOT LIKE '%(Debug)%'
GROUP BY tool_name
ORDER BY usage_count DESC
LIMIT 10
");
$stmt->bindParam(':start_date', $start_date);
$stmt->bindParam(':end_date', $end_date);
$stmt->execute();
$report['tools'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 3. 用户使用统计
$stmt = $pdo->prepare("
SELECT
username,
COUNT(*) as usage_count,
SUM(time_saved) as time_saved
FROM task
WHERE timestamp BETWEEN :start_date AND :end_date
AND tool_name NOT LIKE '%(Debug)%'
GROUP BY username
ORDER BY usage_count DESC
LIMIT 10
");
$stmt->bindParam(':start_date', $start_date);
$stmt->bindParam(':end_date', $end_date);
$stmt->execute();
$report['users'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 4. 每日使用趋势
$stmt = $pdo->prepare("
SELECT
DATE(timestamp) as date,
COUNT(*) as usage_count,
SUM(time_saved) as time_saved
FROM task
WHERE timestamp BETWEEN :start_date AND :end_date
AND tool_name NOT LIKE '%(Debug)%'
GROUP BY DATE(timestamp)
ORDER BY date
");
$stmt->bindParam(':start_date', $start_date);
$stmt->bindParam(':end_date', $end_date);
$stmt->execute();
$report['daily'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'report' => $report,
'start_date' => $start_date,
'end_date' => $end_date
];
}
// 生成HTML报告
function generate_html_report($report_data) {
$report = $report_data['report'];
$start_date = $report_data['start_date'];
$end_date = $report_data['end_date'];
$html = '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CGNICO工具使用报告</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3 {
color: #0066cc;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.summary {
background-color: #f0f7ff;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.highlight {
color: #0066cc;
font-weight: bold;
}
</style>
</head>
<body>
<h1>CGNICO工具使用报告</h1>
<p>报告时间范围: ' . $start_date . ' 至 ' . $end_date . '</p>
<div class="summary">
<h2>总体统计</h2>
<p>在过去的 ' . round((strtotime($end_date) - strtotime($start_date)) / 86400) . ' 天中:</p>
<ul>
<li>总任务数: <span class="highlight">' . number_format($report['summary']['total_tasks']) . '</span></li>
<li>总节省时间: <span class="highlight">' . number_format($report['summary']['total_time_saved'], 2) . ' 小时</span></li>
<li>活跃用户数: <span class="highlight">' . $report['summary']['total_users'] . '</span></li>
<li>使用的工具数: <span class="highlight">' . $report['summary']['total_tools'] . '</span></li>
</ul>
</div>
<h2>工具使用排名</h2>
<table>
<tr>
<th>排名</th>
<th>工具名称</th>
<th>使用次数</th>
<th>节省时间 (小时)</th>
</tr>';
$rank = 1;
foreach ($report['tools'] as $tool) {
$html .= '
<tr>
<td>' . $rank++ . '</td>
<td>' . htmlspecialchars($tool['tool_name']) . '</td>
<td>' . number_format($tool['usage_count']) . '</td>
<td>' . number_format($tool['time_saved'], 2) . '</td>
</tr>';
}
$html .= '
</table>
<h2>用户使用排名</h2>
<table>
<tr>
<th>排名</th>
<th>用户名</th>
<th>使用次数</th>
<th>节省时间 (小时)</th>
</tr>';
$rank = 1;
foreach ($report['users'] as $user) {
$html .= '
<tr>
<td>' . $rank++ . '</td>
<td>' . htmlspecialchars($user['username']) . '</td>
<td>' . number_format($user['usage_count']) . '</td>
<td>' . number_format($user['time_saved'], 2) . '</td>
</tr>';
}
$html .= '
</table>
<h2>每日使用趋势</h2>
<table>
<tr>
<th>日期</th>
<th>使用次数</th>
<th>节省时间 (小时)</th>
</tr>';
foreach ($report['daily'] as $day) {
$html .= '
<tr>
<td>' . $day['date'] . '</td>
<td>' . number_format($day['usage_count']) . '</td>
<td>' . number_format($day['time_saved'], 2) . '</td>
</tr>';
}
$html .= '
</table>
<div style="margin-top: 30px; color: #666; font-size: 12px; text-align: center;">
<p>此报告由CGNICO Task Reporter自动生成于 ' . date('Y-m-d H:i:s') . '</p>
</div>
</body>
</html>';
return $html;
}
// 记录日志
function log_message($message) {
$log_dir = __DIR__ . '/logs';
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
$log_file = $log_dir . '/report_' . date('Y-m-d') . '.log';
$time = date('Y-m-d H:i:s');
file_put_contents($log_file, "[$time] $message\n", FILE_APPEND);
}
// 主程序
try {
// 创建报告目录(如果不存在)
$report_dir = __DIR__ . '/reports/';
if (!is_dir($report_dir)) {
mkdir($report_dir, 0755, true);
}
// 检查是否需要生成新报告
$generate_new_report = false;
$report_file = $report_dir . 'report_' . date('Y-m-d') . '.html';
if (isset($_GET['generate']) && $_GET['generate'] == '1') {
$generate_new_report = true;
} elseif (!file_exists($report_file)) {
// 如果今天的报告不存在,自动生成
$generate_new_report = true;
}
if ($generate_new_report) {
log_message('===== 开始执行报告生成脚本 =====');
// 生成报告数据
log_message('开始生成报告数据...');
$days = $config['report_days'];
$report_data = generate_report($pdo, $days);
log_message('报告数据生成完成');
// 生成HTML报告
log_message('生成HTML报告...');
$html_report = generate_html_report($report_data);
log_message('HTML报告生成完成');
// 保存报告到文件
file_put_contents($report_file, $html_report);
log_message('报告已保存到: ' . $report_file);
}
// 开始HTML输出
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CGNICO工具使用报告生成器</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1000px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #ff8c00;
text-align: center;
margin-bottom: 30px;
}
h3 {
color: #555;
margin-top: 0;
}
.success-message {
background-color: #dff0d8;
border: 1px solid #d6e9c6;
color: #3c763d;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 10px 15px;
text-decoration: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
border: none;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #337ab7;
color: white;
}
.btn-primary:hover {
background-color: #286090;
}
.btn-success {
background-color: #4CAF50;
color: white;
}
.btn-success:hover {
background-color: #3d8b40;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.panel {
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: inline-block;
margin-bottom: 5px;
font-weight: bold;
}
select {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
max-width: 300px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f0f0f0;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f1f1f1;
}
.table-actions {
text-align: center;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #eee;
color: #777;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>CGNICO工具使用报告生成器</h1>
<?php if ($generate_new_report): ?>
<div class="success-message">
<p><strong>报告已生成成功!</strong></p>
<p>报告时间范围: 过去<?php echo $config['report_days']; ?>天 (<?php echo date('Y-m-d', strtotime("-{$config['report_days']} days")); ?> 至 <?php echo date('Y-m-d'); ?>)</p>
</div>
<?php endif; ?>
<div class="action-buttons">
<a href="reports/report_<?php echo date('Y-m-d'); ?>.html" target="_blank" class="btn btn-primary">查看最新报告</a>
<a href="reports/report_<?php echo date('Y-m-d'); ?>.html" download class="btn btn-success">下载最新报告</a>
<a href="index.php" class="btn btn-secondary">返回首页</a>
</div>
<div class="panel">
<h3>生成新报告</h3>
<form method="get" action="">
<div class="form-group">
<label for="days">报告时间范围:</label>
<select name="days" id="days">
<option value="7" <?php echo ($config['report_days'] == 7) ? 'selected' : ''; ?>>过去7天 (一周)</option>
<option value="30" <?php echo ($config['report_days'] == 30) ? 'selected' : ''; ?>>过去30天 (一个月)</option>
<option value="90" <?php echo ($config['report_days'] == 90) ? 'selected' : ''; ?>>过去90天 (一季度)</option>
<option value="365" <?php echo ($config['report_days'] == 365) ? 'selected' : ''; ?>>过去365天 (一年)</option>
</select>
</div>
<input type="hidden" name="generate" value="1">
<button type="submit" class="btn btn-primary">生成报告</button>
</form>
</div>
<?php
// 历史报告列表
$reports_dir = __DIR__ . '/reports/';
if (is_dir($reports_dir)) {
$reports = glob($reports_dir . 'report_*.html');
if (!empty($reports)) {
echo "<div class=\"panel\">\n";
echo "<h3>历史报告</h3>\n";
echo "<table>\n";
echo "<tr>\n";
echo "<th>报告日期</th>\n";
echo "<th>报告类型</th>\n";
echo "<th>操作</th>\n";
echo "</tr>\n";
rsort($reports); // 按日期降序排序
foreach (array_slice($reports, 0, 10) as $report) { // 只显示最近10个
$filename = basename($report);
$date = substr($filename, 7, 10); // 从 report_YYYY-MM-DD.html 提取日期
// 尝试确定报告类型
$report_type = "常规报告";
$file_content = file_get_contents($report);
if (strpos($file_content, "工具使用周报") !== false) {
$report_type = "周报";
} elseif (strpos($file_content, "工具使用月报") !== false) {
$report_type = "月报";
} elseif (strpos($file_content, "工具使用季报") !== false) {
$report_type = "季报";
} elseif (strpos($file_content, "工具使用年报") !== false) {
$report_type = "年报";
}
echo "<tr>\n";
echo "<td>{$date}</td>\n";
echo "<td>{$report_type}</td>\n";
echo "<td class=\"table-actions\">\n";
echo "<a href='reports/{$filename}' target='_blank' class='btn btn-primary' style='padding: 5px 10px; margin-right: 5px;'>查看</a>\n";
echo "<a href='reports/{$filename}' download class='btn btn-success' style='padding: 5px 10px;'>下载</a>\n";
echo "</td>\n";
echo "</tr>\n";
}
echo "</table>\n";
echo "</div>\n";
}
}
?>
<div class="footer">
<p>CGNICO Task Report System &copy; <?php echo date('Y'); ?> | 联系方式: <a href="mailto:jeffreytsai1004@gmail.com">jeffreytsai1004@gmail.com</a></p>
</div>
</div>
</body>
</html>
<?php
} catch (Exception $e) {
log_message('错误: ' . $e->getMessage());
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误 - CGNICO工具使用报告生成器</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #d9534f;
}
.error-message {
background-color: #f2dede;
border: 1px solid #ebccd1;
color: #a94442;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 10px 15px;
background-color: #6c757d;
color: white;
text-decoration: none;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>发生错误</h1>
<div class="error-message">
<p><strong>错误:</strong> <?php echo $e->getMessage(); ?></p>
</div>
<a href="index.php" class="btn">返回首页</a>
</div>
</body>
</html>
<?php
}
?>

585
index.php Normal file
View File

@@ -0,0 +1,585 @@
<?php
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 数据库连接信息
$db_host = '192.168.2.4';
$db_port = 3307;
$db_name = 'task_reporter';
$db_user = 'task_reporter';
$db_pass = 'Pass12349ers!';
// 创建数据库连接
try {
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
// 创建任务表(如果不存在)
try {
$sql = "CREATE TABLE IF NOT EXISTS task (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL,
tool_name VARCHAR(100) NOT NULL,
task_name VARCHAR(100) NOT NULL,
time_saved VARCHAR(100) NOT NULL,
time_cost VARCHAR(100) NOT NULL,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)";
$pdo->exec($sql);
} catch (PDOException $e) {
die("创建表失败: " . $e->getMessage());
}
// 处理添加任务的POST请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['action'] === 'add_task') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
throw new Exception("无效的JSON数据");
}
$stmt = $pdo->prepare("INSERT INTO task (username, tool_name, task_name, time_saved, time_cost, timestamp)
VALUES (:username, :tool_name, :task_name, :time_saved, :time_cost, :timestamp)");
$stmt->bindParam(':username', $data['username']);
$stmt->bindParam(':tool_name', $data['tool_name']);
$stmt->bindParam(':task_name', $data['task_name']);
$stmt->bindParam(':time_saved', $data['time_saved']);
$stmt->bindParam(':time_cost', $data['time_cost']);
$stmt->bindParam(':timestamp', $data['timestamp']);
$stmt->execute();
$time = explode(".", $data['timestamp'])[0];
$time = str_replace("T", " ", $time);
header('Content-Type: application/json');
echo json_encode([
"message" => "$time {$data['tool_name']}-{$data['task_name']} reported successfully!"
]);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 处理查询任务数据的GET请求
if (isset($_GET['action']) && $_GET['action'] === 'query_task_data') {
try {
// 获取筛选参数
$start_date = isset($_GET['start_date']) ? $_GET['start_date'] : null;
$end_date = isset($_GET['end_date']) ? $_GET['end_date'] : null;
$task_name = isset($_GET['task_name']) ? $_GET['task_name'] : null;
$tool_name = isset($_GET['tool_name']) ? $_GET['tool_name'] : null;
$time_saved = isset($_GET['time_saved']) ? $_GET['time_saved'] : null;
$time_cost = isset($_GET['time_cost']) ? $_GET['time_cost'] : null;
$show_debug = isset($_GET['show_debug']) && $_GET['show_debug'] === 'true';
// 获取排序参数
$sort_by = isset($_GET['sort_by']) ? $_GET['sort_by'] : 'timestamp';
$sort_direction = isset($_GET['sort_direction']) ? $_GET['sort_direction'] : 'desc';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 50;
// 构建查询
$query = "SELECT * FROM task WHERE 1=1";
$params = [];
// 应用日期筛选
if ($start_date) {
$query .= " AND timestamp >= :start_date";
$params[':start_date'] = $start_date . " 00:00:00";
}
if ($end_date) {
$query .= " AND timestamp <= :end_date";
$params[':end_date'] = $end_date . " 23:59:59";
}
// 应用任务名称筛选
if ($task_name) {
$query .= " AND task_name LIKE :task_name";
$params[':task_name'] = "%$task_name%";
}
// 应用工具名称筛选
if ($tool_name) {
$query .= " AND tool_name LIKE :tool_name";
$params[':tool_name'] = "%$tool_name%";
}
// 排除调试工具(如果不显示调试)
if (!$show_debug) {
$query .= " AND tool_name NOT LIKE '%(Debug)%'";
}
// 应用排序
$valid_sort_fields = ['timestamp', 'time_saved', 'time_cost'];
if (!in_array($sort_by, $valid_sort_fields)) {
$sort_by = 'timestamp';
}
$query .= " ORDER BY $sort_by " . ($sort_direction === 'asc' ? 'ASC' : 'DESC');
// 应用分页
$offset = ($page - 1) * $per_page;
$query .= " LIMIT :offset, :limit";
$params[':offset'] = $offset;
$params[':limit'] = $per_page;
// 执行查询
$stmt = $pdo->prepare($query);
foreach ($params as $key => $value) {
if ($key === ':offset' || $key === ':limit') {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue($key, $value);
}
}
$stmt->execute();
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 获取总记录数
$count_query = "SELECT COUNT(*) FROM task WHERE 1=1";
$count_params = [];
if ($start_date) {
$count_query .= " AND timestamp >= :start_date";
$count_params[':start_date'] = $start_date . " 00:00:00";
}
if ($end_date) {
$count_query .= " AND timestamp <= :end_date";
$count_params[':end_date'] = $end_date . " 23:59:59";
}
if ($task_name) {
$count_query .= " AND task_name LIKE :task_name";
$count_params[':task_name'] = "%$task_name%";
}
if ($tool_name) {
$count_query .= " AND tool_name LIKE :tool_name";
$count_params[':tool_name'] = "%$tool_name%";
}
if (!$show_debug) {
$count_query .= " AND tool_name NOT LIKE '%(Debug)%'";
}
$count_stmt = $pdo->prepare($count_query);
foreach ($count_params as $key => $value) {
$count_stmt->bindValue($key, $value);
}
$count_stmt->execute();
$total_count = $count_stmt->fetchColumn();
// 获取所有工具名称和任务名称(用于筛选)
$tools_stmt = $pdo->query("SELECT DISTINCT tool_name FROM task ORDER BY tool_name");
$tools = $tools_stmt->fetchAll(PDO::FETCH_COLUMN);
$tasks_stmt = $pdo->query("SELECT DISTINCT task_name FROM task ORDER BY task_name");
$task_names = $tasks_stmt->fetchAll(PDO::FETCH_COLUMN);
// 返回数据
$result = [
'tasks' => $tasks,
'total' => $total_count,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil($total_count / $per_page),
'tools' => $tools,
'task_names' => $task_names
];
header('Content-Type: application/json');
echo json_encode($result);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 处理获取工具统计数据的GET请求
if (isset($_GET['action']) && $_GET['action'] === 'get_tool_statistics') {
try {
// 获取日期范围
$start_date = isset($_GET['start_date']) ? $_GET['start_date'] : null;
$end_date = isset($_GET['end_date']) ? $_GET['end_date'] : null;
// 构建查询
$query = "SELECT tool_name, COUNT(*) as request_count, SUM(time_saved) as time_saved
FROM task
WHERE tool_name NOT LIKE '%(Debug)%'";
$params = [];
if ($start_date) {
$query .= " AND timestamp >= :start_date";
$params[':start_date'] = $start_date . " 00:00:00";
}
if ($end_date) {
$query .= " AND timestamp <= :end_date";
$params[':end_date'] = $end_date . " 23:59:59";
}
$query .= " GROUP BY tool_name ORDER BY request_count DESC";
// 执行查询
$stmt = $pdo->prepare($query);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$tools_stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 计算总请求数和总节省时间
$total_requests = 0;
$total_time_saved = 0;
foreach ($tools_stats as $stat) {
$total_requests += $stat['request_count'];
$total_time_saved += $stat['time_saved'];
}
// 返回数据
$result = [
'tools' => $tools_stats,
'total_requests' => $total_requests,
'total_time_saved' => $total_time_saved
];
header('Content-Type: application/json');
echo json_encode($result);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 处理导出CSV的GET请求
if (isset($_GET['action']) && $_GET['action'] === 'export_csv') {
try {
// 获取筛选参数
$start_date = isset($_GET['start_date']) ? $_GET['start_date'] : null;
$end_date = isset($_GET['end_date']) ? $_GET['end_date'] : null;
$task_name = isset($_GET['task_name']) ? $_GET['task_name'] : null;
$tool_name = isset($_GET['tool_name']) ? $_GET['tool_name'] : null;
$show_debug = isset($_GET['show_debug']) && $_GET['show_debug'] === 'true';
// 构建查询
$query = "SELECT id, username, tool_name, task_name, time_saved, time_cost, timestamp
FROM task WHERE 1=1";
$params = [];
if ($start_date) {
$query .= " AND timestamp >= :start_date";
$params[':start_date'] = $start_date . " 00:00:00";
}
if ($end_date) {
$query .= " AND timestamp <= :end_date";
$params[':end_date'] = $end_date . " 23:59:59";
}
if ($task_name) {
$query .= " AND task_name LIKE :task_name";
$params[':task_name'] = "%$task_name%";
}
if ($tool_name) {
$query .= " AND tool_name LIKE :tool_name";
$params[':tool_name'] = "%$tool_name%";
}
if (!$show_debug) {
$query .= " AND tool_name NOT LIKE '%(Debug)%'";
}
$query .= " ORDER BY timestamp DESC";
// 执行查询
$stmt = $pdo->prepare($query);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 设置CSV文件头
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=task_data.csv');
// 创建CSV输出流
$output = fopen('php://output', 'w');
// 添加CSV头行
fputcsv($output, ['ID', 'Username', 'Tool Name', 'Task Name', 'Time Saved', 'Time Cost', 'Timestamp']);
// 添加数据行
foreach ($tasks as $task) {
fputcsv($output, [
$task['id'],
$task['username'],
$task['tool_name'],
$task['task_name'],
$task['time_saved'],
$task['time_cost'],
$task['timestamp']
]);
}
fclose($output);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 处理获取特定工具任务的GET请求
if (isset($_GET['action']) && $_GET['action'] === 'get_tool_tasks') {
try {
$tool_name = isset($_GET['tool_name']) ? $_GET['tool_name'] : null;
if (!$tool_name) {
throw new Exception("Tool name is required");
}
// 查询该工具的所有任务
$stmt = $pdo->prepare("SELECT task_name, COUNT(*) as total_requests, SUM(time_saved) as time_saved
FROM task
WHERE tool_name = :tool_name AND tool_name NOT LIKE '%(Debug)%'
GROUP BY task_name
ORDER BY total_requests DESC");
$stmt->bindParam(':tool_name', $tool_name);
$stmt->execute();
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/json');
echo json_encode(['tasks' => $tasks]);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 处理获取趋势数据的GET请求
if (isset($_GET['action']) && $_GET['action'] === 'get_trend_data') {
try {
// 获取查询参数中的日期范围
$start_date = isset($_GET['start_date']) ? $_GET['start_date'] : null;
$end_date = isset($_GET['end_date']) ? $_GET['end_date'] : null;
// 如果没有指定日期范围默认显示最近30天
if (!$start_date || !$end_date) {
$end_date = date('Y-m-d');
$start_date = date('Y-m-d', strtotime('-30 days'));
}
// 将字符串日期转换为日期对象
$start_datetime = date_create($start_date);
$end_datetime = date_create($end_date);
// 设置结束日期为当天的最后一秒
$end_datetime->setTime(23, 59, 59);
// 使用PDO查询
$stmt = $pdo->prepare("SELECT DATE(timestamp) as date, COUNT(*) as total_requests, SUM(time_saved) as time_saved
FROM task
WHERE timestamp BETWEEN :start_date AND :end_date
GROUP BY DATE(timestamp)
ORDER BY date");
$stmt->bindValue(':start_date', $start_datetime->format('Y-m-d 00:00:00'));
$stmt->bindValue(':end_date', $end_datetime->format('Y-m-d H:i:s'));
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 填充所有日期(包括没有数据的日期)
$dates = [];
$requests = [];
$time_saved = [];
// 创建日期范围内的所有日期
$date_range = [];
$current = clone $start_datetime;
while ($current <= $end_datetime) {
$date_range[$current->format('Y-m-d')] = [
'total_requests' => 0,
'time_saved' => 0
];
$current->modify('+1 day');
}
// 填充实际数据
foreach ($results as $row) {
$date_range[$row['date']] = [
'total_requests' => (int)$row['total_requests'],
'time_saved' => (float)$row['time_saved']
];
}
// 转换为数组格式
foreach ($date_range as $date => $stats) {
$dates[] = $date;
$requests[] = $stats['total_requests'];
$time_saved[] = $stats['time_saved'];
}
$response_data = [
'dates' => $dates,
'requests' => $requests,
'timeSaved' => $time_saved
];
header('Content-Type: application/json');
echo json_encode($response_data);
exit;
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
exit;
}
}
// 如果没有特定操作,显示主页
?>
<!DOCTYPE html>
<html>
<head>
<title>Task Report System</title>
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-image: url('static/background.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
z-index: 1;
}
.content {
position: relative;
z-index: 2;
text-align: center;
}
h1 {
color: rgb(255, 166, 0);
font-size: 4em;
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(114, 77, 10, 0.5);
animation: fadeIn 1.5s ease-out;
}
.view-btn {
background: transparent;
border: 2px solid white;
color: white;
padding: 15px 40px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 30px;
text-decoration: none;
animation: slideUp 1s ease-out 0.5s both;
}
.view-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
h1 {
font-size: 2.5em;
padding: 0 20px;
}
.view-btn {
padding: 12px 30px;
font-size: 1em;
}
}
</style>
</head>
<body>
<div class="content">
<h1>CGNICO Task Report System</h1>
<button class="view-btn" onclick="window.location.href='query_task_data.php'" style="margin-right: 15px;">
查看任务数据
</button>
<button class="view-btn" onclick="window.location.href='generate_report.php'" style="margin-left: 15px;">
查看报告
</button>
</div>
</body>
</html>

236
insert_test_data.php Normal file
View File

@@ -0,0 +1,236 @@
<?php
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 数据库连接信息
$db_host = '192.168.2.4';
$db_port = 3307;
$db_name = 'task_reporter';
$db_user = 'task_reporter';
$db_pass = 'Pass12349ers!';
// 创建数据库连接
try {
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
echo "数据库连接成功!<br>";
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
// 确保表存在
try {
$sql = "CREATE TABLE IF NOT EXISTS task (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL,
tool_name VARCHAR(100) NOT NULL,
task_name VARCHAR(100) NOT NULL,
time_saved VARCHAR(100) NOT NULL,
time_cost VARCHAR(100) NOT NULL,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)";
$pdo->exec($sql);
echo "表结构检查完成!<br>";
} catch (PDOException $e) {
die("创建表失败: " . $e->getMessage());
}
// 清空现有数据(可选)
try {
$pdo->exec("TRUNCATE TABLE task");
echo "表已清空,准备插入测试数据...<br>";
} catch (PDOException $e) {
echo "清空表失败: " . $e->getMessage() . "<br>";
}
// 准备测试数据
$test_data = [
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'CheckUV',
'time_saved' => '5.2',
'time_cost' => '0.3',
'timestamp' => '2025-04-01 09:30:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'FixNormals',
'time_saved' => '3.5',
'time_cost' => '0.2',
'timestamp' => '2025-04-01 10:15:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'RigTools',
'task_name' => 'AutoRig',
'time_saved' => '12.0',
'time_cost' => '1.5',
'timestamp' => '2025-04-02 14:20:00'
],
[
'username' => 'LiuYang',
'tool_name' => 'TextureManager',
'task_name' => 'BatchExport',
'time_saved' => '8.5',
'time_cost' => '0.8',
'timestamp' => '2025-04-03 11:45:00'
],
[
'username' => 'LiuYang',
'tool_name' => 'TextureManager',
'task_name' => 'ResizeTextures',
'time_saved' => '4.2',
'time_cost' => '0.5',
'timestamp' => '2025-04-03 15:30:00'
],
[
'username' => 'ZhangWei',
'tool_name' => 'AnimTools',
'task_name' => 'CycleCheck',
'time_saved' => '6.8',
'time_cost' => '0.4',
'timestamp' => '2025-04-04 09:15:00'
],
[
'username' => 'ZhangWei',
'tool_name' => 'AnimTools',
'task_name' => 'KeyCleaner',
'time_saved' => '5.5',
'time_cost' => '0.3',
'timestamp' => '2025-04-04 14:45:00'
],
[
'username' => 'WangFang',
'tool_name' => 'LightingSetup',
'task_name' => 'HDRIManager',
'time_saved' => '7.2',
'time_cost' => '0.6',
'timestamp' => '2025-04-05 10:30:00'
],
[
'username' => 'WangFang',
'tool_name' => 'LightingSetup',
'task_name' => 'ShadowFix',
'time_saved' => '3.8',
'time_cost' => '0.4',
'timestamp' => '2025-04-05 16:20:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'CheckUV',
'time_saved' => '5.0',
'time_cost' => '0.3',
'timestamp' => '2025-04-06 11:10:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox(Debug)',
'task_name' => 'TestFeature',
'time_saved' => '0.5',
'time_cost' => '1.2',
'timestamp' => '2025-04-07 09:45:00'
],
[
'username' => 'LiuYang',
'tool_name' => 'TextureManager',
'task_name' => 'BatchExport',
'time_saved' => '8.0',
'time_cost' => '0.7',
'timestamp' => '2025-04-08 13:30:00'
],
[
'username' => 'ZhangWei',
'tool_name' => 'AnimTools',
'task_name' => 'CycleCheck',
'time_saved' => '6.5',
'time_cost' => '0.4',
'timestamp' => '2025-04-09 10:20:00'
],
[
'username' => 'WangFang',
'tool_name' => 'LightingSetup',
'task_name' => 'HDRIManager',
'time_saved' => '7.0',
'time_cost' => '0.6',
'timestamp' => '2025-04-10 15:45:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'RigTools',
'task_name' => 'AutoRig',
'time_saved' => '11.5',
'time_cost' => '1.4',
'timestamp' => '2025-04-11 09:30:00'
],
[
'username' => 'LiuYang',
'tool_name' => 'TextureManager',
'task_name' => 'ResizeTextures',
'time_saved' => '4.0',
'time_cost' => '0.5',
'timestamp' => '2025-04-12 14:15:00'
],
[
'username' => 'ZhangWei',
'tool_name' => 'AnimTools',
'task_name' => 'KeyCleaner',
'time_saved' => '5.2',
'time_cost' => '0.3',
'timestamp' => '2025-04-12 16:40:00'
],
[
'username' => 'WangFang',
'tool_name' => 'LightingSetup',
'task_name' => 'ShadowFix',
'time_saved' => '3.5',
'time_cost' => '0.4',
'timestamp' => '2025-04-13 10:15:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'FixNormals',
'time_saved' => '3.2',
'time_cost' => '0.2',
'timestamp' => '2025-04-13 11:30:00'
],
[
'username' => 'JeffreyTsai',
'tool_name' => 'MetaBox',
'task_name' => 'CheckUV',
'time_saved' => '5.1',
'time_cost' => '0.3',
'timestamp' => '2025-04-13 13:45:00'
]
];
// 插入测试数据
$success_count = 0;
$stmt = $pdo->prepare("INSERT INTO task (username, tool_name, task_name, time_saved, time_cost, timestamp)
VALUES (:username, :tool_name, :task_name, :time_saved, :time_cost, :timestamp)");
foreach ($test_data as $data) {
try {
$stmt->bindParam(':username', $data['username']);
$stmt->bindParam(':tool_name', $data['tool_name']);
$stmt->bindParam(':task_name', $data['task_name']);
$stmt->bindParam(':time_saved', $data['time_saved']);
$stmt->bindParam(':time_cost', $data['time_cost']);
$stmt->bindParam(':timestamp', $data['timestamp']);
$stmt->execute();
$success_count++;
} catch (PDOException $e) {
echo "插入数据失败: " . $e->getMessage() . "<br>";
}
}
echo "成功插入 {$success_count} 条测试数据!<br>";
echo "<br>所有操作已完成!<br>";
echo "<a href='index.php'>返回首页</a> | <a href='query_task_data.php'>查看数据</a>";
?>

943
query_task_data.php Normal file
View File

@@ -0,0 +1,943 @@
<?php
// 设置错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 数据库连接信息
$db_host = '192.168.2.4';
$db_port = 3307;
$db_name = 'task_reporter';
$db_user = 'task_reporter';
$db_pass = 'Pass12349ers!';
// 创建数据库连接
try {
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
// 获取所有工具名称和任务名称(用于筛选)
$tools_stmt = $pdo->query("SELECT DISTINCT tool_name FROM task ORDER BY tool_name");
$tools = $tools_stmt->fetchAll(PDO::FETCH_COLUMN);
$tasks_stmt = $pdo->query("SELECT DISTINCT task_name FROM task ORDER BY task_name");
$task_names = $tasks_stmt->fetchAll(PDO::FETCH_COLUMN);
?>
<!DOCTYPE html>
<html>
<head>
<title>Task Report Data</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h1 {
color: #ff8c00;
text-align: center;
margin-bottom: 30px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.filters {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background-color: #f9f9f9;
border-radius: 5px;
}
.filter-group {
display: flex;
flex-direction: column;
}
.filter-group label {
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.filter-group select,
.filter-group input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.buttons {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
button {
padding: 10px 20px;
background-color: #ff8c00;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
button:hover {
background-color: #e67e00;
}
.export-btn {
background-color: #4CAF50;
}
.export-btn:hover {
background-color: #45a049;
}
.back-btn {
background-color: #555;
}
.back-btn:hover {
background-color: #444;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: bold;
cursor: pointer;
}
th:hover {
background-color: #e6e6e6;
}
tr:hover {
background-color: #f9f9f9;
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.pagination button {
margin: 0 5px;
padding: 8px 12px;
background-color: #f2f2f2;
color: #333;
border: 1px solid #ddd;
}
.pagination button.active {
background-color: #ff8c00;
color: white;
border-color: #ff8c00;
}
.pagination button:hover:not(.active) {
background-color: #ddd;
}
.stats-container {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 5px;
flex: 1;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.stats-header h2 {
margin: 0;
color: #555;
}
/* 标签页样式 */
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
background-color: #f2f2f2;
border: 1px solid #ddd;
border-bottom: none;
margin-right: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.tab.active {
background-color: #fff;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
font-weight: bold;
color: #ff8c00;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.chart-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.chart {
flex: 1;
min-width: 300px;
height: 300px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 15px;
position: relative;
}
.tool-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.tool-card {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 15px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.tool-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.tool-card h3 {
margin-top: 0;
color: #ff8c00;
}
.tool-card p {
margin: 5px 0;
color: #666;
}
.tool-card .count {
font-weight: bold;
color: #333;
}
.tool-card .time-saved {
color: #4CAF50;
font-weight: bold;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: #fff;
margin: 10% auto;
padding: 20px;
width: 80%;
max-width: 800px;
border-radius: 5px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #555;
}
#loading {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
z-index: 9999;
justify-content: center;
align-items: center;
}
.spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #ff8c00;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="loading">
<div class="spinner"></div>
</div>
<div class="container">
<h1>Task Report Data</h1>
<div class="filters">
<div class="filter-group">
<label for="start-date">开始日期:</label>
<input type="date" id="start-date">
</div>
<div class="filter-group">
<label for="end-date">结束日期:</label>
<input type="date" id="end-date">
</div>
<div class="filter-group">
<label for="tool-name">工具名称:</label>
<select id="tool-name">
<option value="">全部</option>
<?php foreach ($tools as $tool): ?>
<option value="<?php echo htmlspecialchars($tool); ?>"><?php echo htmlspecialchars($tool); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="task-name">任务名称:</label>
<select id="task-name">
<option value="">全部</option>
<?php foreach ($task_names as $task_name): ?>
<option value="<?php echo htmlspecialchars($task_name); ?>"><?php echo htmlspecialchars($task_name); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="show-debug">显示调试工具:</label>
<select id="show-debug">
<option value="false">否</option>
<option value="true">是</option>
</select>
</div>
<div class="filter-group">
<label for="sort-by">排序字段:</label>
<select id="sort-by">
<option value="timestamp">时间</option>
<option value="time_saved">节省时间</option>
<option value="time_cost">花费时间</option>
</select>
</div>
<div class="filter-group">
<label for="sort-direction">排序方向:</label>
<select id="sort-direction">
<option value="desc">降序</option>
<option value="asc">升序</option>
</select>
</div>
</div>
<div class="buttons">
<button id="filter-btn">应用筛选</button>
<button id="export-btn" class="export-btn">导出CSV</button>
<button class="report-btn" onclick="window.location.href='generate_report.php'">查看报告</button>
<button class="back-btn" onclick="window.location.href='index.php'">返回首页</button>
</div>
<div class="stats-container">
<div class="stats-header">
<h2>数据与统计</h2>
</div>
<div class="tabs">
<div class="tab active" data-tab="data">请求数据</div>
<div class="tab" data-tab="trends">趋势分析</div>
<div class="tab" data-tab="tools">工具统计</div>
</div>
<div id="data-tab" class="tab-content active">
<table id="task-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>工具名称</th>
<th>任务名称</th>
<th>节省时间</th>
<th>花费时间</th>
<th>时间戳</th>
</tr>
</thead>
<tbody id="task-data">
<!-- 任务数据将通过JavaScript动态加载 -->
</tbody>
</table>
<div class="pagination" id="pagination">
<!-- 分页按钮将通过JavaScript动态生成 -->
</div>
</div>
<div id="trends-tab" class="tab-content">
<div class="chart-container">
<div class="chart" id="requests-chart">
<canvas id="requests-canvas"></canvas>
</div>
<div class="chart" id="time-saved-chart">
<canvas id="time-saved-canvas"></canvas>
</div>
</div>
</div>
<div id="tools-tab" class="tab-content">
<div class="tool-stats" id="tool-stats">
<!-- 工具统计数据将通过JavaScript动态加载 -->
</div>
</div>
</div>
</div>
<div id="tool-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2 id="modal-title">工具详情</h2>
<div id="modal-content">
<!-- 工具详情将通过JavaScript动态加载 -->
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 全局变量
let currentPage = 1;
let totalPages = 1;
let perPage = 50;
let requestsChart = null;
let timeSavedChart = null;
// DOM元素
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const toolNameSelect = document.getElementById('tool-name');
const taskNameSelect = document.getElementById('task-name');
const showDebugSelect = document.getElementById('show-debug');
const sortBySelect = document.getElementById('sort-by');
const sortDirectionSelect = document.getElementById('sort-direction');
const filterBtn = document.getElementById('filter-btn');
const exportBtn = document.getElementById('export-btn');
const taskDataContainer = document.getElementById('task-data');
const paginationContainer = document.getElementById('pagination');
const toolStatsContainer = document.getElementById('tool-stats');
const modal = document.getElementById('tool-modal');
const modalTitle = document.getElementById('modal-title');
const modalContent = document.getElementById('modal-content');
const closeModal = document.getElementsByClassName('close')[0];
const loading = document.getElementById('loading');
// 设置默认日期范围最近30天
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
startDateInput.value = formatDate(thirtyDaysAgo);
endDateInput.value = formatDate(today);
// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
loadTaskData();
loadToolStatistics();
loadTrendData();
// 事件监听器
filterBtn.addEventListener('click', function() {
currentPage = 1;
loadTaskData();
loadToolStatistics();
loadTrendData();
});
exportBtn.addEventListener('click', exportCsv);
closeModal.addEventListener('click', function() {
modal.style.display = 'none';
});
window.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 标签页切换
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
// 移除所有标签页的active类
document.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
});
// 移除所有内容区的active类
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 添加当前标签页的active类
this.classList.add('active');
// 显示对应的内容区
const tabId = this.getAttribute('data-tab');
document.getElementById(tabId + '-tab').classList.add('active');
// 如果切换到趋势分析标签页,重新渲染图表
if (tabId === 'trends') {
setTimeout(() => {
if (requestsChart) requestsChart.resize();
if (timeSavedChart) timeSavedChart.resize();
}, 10);
}
});
});
});
// 加载任务数据
function loadTaskData() {
showLoading();
const params = new URLSearchParams({
action: 'query_task_data',
start_date: startDateInput.value,
end_date: endDateInput.value,
tool_name: toolNameSelect.value,
task_name: taskNameSelect.value,
show_debug: showDebugSelect.value,
sort_by: sortBySelect.value,
sort_direction: sortDirectionSelect.value,
page: currentPage,
per_page: perPage
});
fetch(`index.php?${params.toString()}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
return;
}
renderTaskData(data.tasks);
renderPagination(data.page, data.total_pages);
totalPages = data.total_pages;
hideLoading();
})
.catch(error => {
console.error('Error:', error);
alert('加载数据时出错');
hideLoading();
});
}
// 渲染任务数据
function renderTaskData(tasks) {
taskDataContainer.innerHTML = '';
if (tasks.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="7" style="text-align: center;">没有找到匹配的数据</td>';
taskDataContainer.appendChild(row);
return;
}
tasks.forEach(task => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${task.id}</td>
<td>${task.username}</td>
<td>${task.tool_name}</td>
<td>${task.task_name}</td>
<td>${task.time_saved}</td>
<td>${task.time_cost}</td>
<td>${task.timestamp}</td>
`;
taskDataContainer.appendChild(row);
});
}
// 渲染分页
function renderPagination(currentPage, totalPages) {
paginationContainer.innerHTML = '';
if (totalPages <= 1) {
return;
}
// 添加"上一页"按钮
if (currentPage > 1) {
const prevBtn = document.createElement('button');
prevBtn.textContent = '上一页';
prevBtn.addEventListener('click', () => {
goToPage(currentPage - 1);
});
paginationContainer.appendChild(prevBtn);
}
// 添加页码按钮
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, startPage + 4);
if (endPage - startPage < 4) {
startPage = Math.max(1, endPage - 4);
}
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = i === currentPage ? 'active' : '';
pageBtn.addEventListener('click', () => {
goToPage(i);
});
paginationContainer.appendChild(pageBtn);
}
// 添加"下一页"按钮
if (currentPage < totalPages) {
const nextBtn = document.createElement('button');
nextBtn.textContent = '下一页';
nextBtn.addEventListener('click', () => {
goToPage(currentPage + 1);
});
paginationContainer.appendChild(nextBtn);
}
}
// 跳转到指定页
function goToPage(page) {
currentPage = page;
loadTaskData();
window.scrollTo(0, 0);
}
// 加载工具统计数据
function loadToolStatistics() {
const params = new URLSearchParams({
action: 'get_tool_statistics',
start_date: startDateInput.value,
end_date: endDateInput.value
});
fetch(`index.php?${params.toString()}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
return;
}
renderToolStatistics(data.tools, data.total_requests, data.total_time_saved);
})
.catch(error => {
console.error('Error:', error);
});
}
// 渲染工具统计数据
function renderToolStatistics(tools, totalRequests, totalTimeSaved) {
toolStatsContainer.innerHTML = '';
if (tools.length === 0) {
toolStatsContainer.innerHTML = '<p>没有找到匹配的数据</p>';
return;
}
tools.forEach(tool => {
const card = document.createElement('div');
card.className = 'tool-card';
card.innerHTML = `
<h3>${tool.tool_name}</h3>
<p>请求次数: <span class="count">${tool.request_count}</span></p>
<p>节省时间: <span class="time-saved">${parseFloat(tool.time_saved).toFixed(2)} 小时</span></p>
`;
card.addEventListener('click', () => {
showToolDetails(tool.tool_name);
});
toolStatsContainer.appendChild(card);
});
}
// 显示工具详情
function showToolDetails(toolName) {
showLoading();
const params = new URLSearchParams({
action: 'get_tool_tasks',
tool_name: toolName
});
fetch(`index.php?${params.toString()}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
return;
}
modalTitle.textContent = `${toolName} 详情`;
if (data.tasks.length === 0) {
modalContent.innerHTML = '<p>没有找到匹配的数据</p>';
} else {
let html = '<table style="width:100%"><thead><tr><th>任务名称</th><th>请求次数</th><th>节省时间</th></tr></thead><tbody>';
data.tasks.forEach(task => {
html += `
<tr>
<td>${task.task_name}</td>
<td>${task.total_requests}</td>
<td>${parseFloat(task.time_saved).toFixed(2)} 小时</td>
</tr>
`;
});
html += '</tbody></table>';
modalContent.innerHTML = html;
}
modal.style.display = 'block';
hideLoading();
})
.catch(error => {
console.error('Error:', error);
hideLoading();
});
}
// 加载趋势数据
function loadTrendData() {
const params = new URLSearchParams({
action: 'get_trend_data',
start_date: startDateInput.value,
end_date: endDateInput.value
});
fetch(`index.php?${params.toString()}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
return;
}
renderTrendCharts(data.dates, data.requests, data.timeSaved);
})
.catch(error => {
console.error('Error:', error);
});
}
// 渲染趋势图表
function renderTrendCharts(dates, requests, timeSaved) {
// 请求次数趋势图
const requestsCtx = document.getElementById('requests-canvas').getContext('2d');
if (requestsChart) {
requestsChart.destroy();
}
requestsChart = new Chart(requestsCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '每日请求次数',
data: requests,
backgroundColor: 'rgba(255, 140, 0, 0.2)',
borderColor: 'rgba(255, 140, 0, 1)',
borderWidth: 2,
pointBackgroundColor: 'rgba(255, 140, 0, 1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '每日请求次数趋势',
font: {
size: 16
}
},
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
// 节省时间趋势图
const timeSavedCtx = document.getElementById('time-saved-canvas').getContext('2d');
if (timeSavedChart) {
timeSavedChart.destroy();
}
timeSavedChart = new Chart(timeSavedCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '每日节省时间(小时)',
data: timeSaved,
backgroundColor: 'rgba(76, 175, 80, 0.2)',
borderColor: 'rgba(76, 175, 80, 1)',
borderWidth: 2,
pointBackgroundColor: 'rgba(76, 175, 80, 1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '每日节省时间趋势',
font: {
size: 16
}
},
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 导出CSV
function exportCsv() {
const params = new URLSearchParams({
action: 'export_csv',
start_date: startDateInput.value,
end_date: endDateInput.value,
tool_name: toolNameSelect.value,
task_name: taskNameSelect.value,
show_debug: showDebugSelect.value
});
window.location.href = `index.php?${params.toString()}`;
}
// 格式化日期为YYYY-MM-DD
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 显示加载中
function showLoading() {
loading.style.display = 'flex';
}
// 隐藏加载中
function hideLoading() {
loading.style.display = 'none';
}
</script>
</body>
</html>

BIN
static/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

1387
templates/data_table.html Normal file

File diff suppressed because it is too large Load Diff

110
templates/index.html Normal file
View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<title>Task Report System</title>
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-image: url('static/background.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
z-index: 1;
}
.content {
position: relative;
z-index: 2;
text-align: center;
}
h1 {
color: rgb(255, 166, 0);
font-size: 4em;
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(114, 77, 10, 0.5);
animation: fadeIn 1.5s ease-out;
}
.view-btn {
background: transparent;
border: 2px solid white;
color: white;
padding: 15px 40px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 30px;
text-decoration: none;
animation: slideUp 1s ease-out 0.5s both;
}
.view-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
h1 {
font-size: 2.5em;
padding: 0 20px;
}
.view-btn {
padding: 12px 30px;
font-size: 1em;
}
}
</style>
</head>
<body>
<div class="content">
<h1>CGNICO Task Report System</h1>
<button class="view-btn" onclick="window.location.href='/query_task_data'">
View Task Data
</button>
</div>
</body>
</html>