359 lines
12 KiB
HTML
359 lines
12 KiB
HTML
|
|
{% extends "base.html" %}
|
|||
|
|
|
|||
|
|
{% block title %}统计分析 - 软件授权管理系统{% endblock %}
|
|||
|
|
|
|||
|
|
{% block page_title %}统计分析{% endblock %}
|
|||
|
|
|
|||
|
|
{% block content %}
|
|||
|
|
<!-- 统计卡片 -->
|
|||
|
|
<div class="row mb-4">
|
|||
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|||
|
|
<div class="card card-stats">
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col">
|
|||
|
|
<h5 class="card-title text-uppercase text-muted mb-0">产品总数</h5>
|
|||
|
|
<span class="h2 font-weight-bold mb-0" id="total-products">-</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="col-auto">
|
|||
|
|
<div class="icon icon-shape bg-white text-primary rounded-circle shadow">
|
|||
|
|
<i class="fas fa-box"></i>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|||
|
|
<div class="card card-stats">
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col">
|
|||
|
|
<h5 class="card-title text-uppercase text-muted mb-0">卡密总数</h5>
|
|||
|
|
<span class="h2 font-weight-bold mb-0" id="total-licenses">-</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="col-auto">
|
|||
|
|
<div class="icon icon-shape bg-white text-warning rounded-circle shadow">
|
|||
|
|
<i class="fas fa-key"></i>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|||
|
|
<div class="card card-stats">
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col">
|
|||
|
|
<h5 class="card-title text-uppercase text-muted mb-0">设备总数</h5>
|
|||
|
|
<span class="h2 font-weight-bold mb-0" id="total-devices">-</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="col-auto">
|
|||
|
|
<div class="icon icon-shape bg-white text-success rounded-circle shadow">
|
|||
|
|
<i class="fas fa-desktop"></i>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|||
|
|
<div class="card card-stats">
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col">
|
|||
|
|
<h5 class="card-title text-uppercase text-muted mb-0">工单总数</h5>
|
|||
|
|
<span class="h2 font-weight-bold mb-0" id="total-tickets">-</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="col-auto">
|
|||
|
|
<div class="icon icon-shape bg-white text-info rounded-circle shadow">
|
|||
|
|
<i class="fas fa-ticket-alt"></i>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 图表区域 -->
|
|||
|
|
<div class="row mb-4">
|
|||
|
|
<div class="col-lg-8">
|
|||
|
|
<div class="card shadow">
|
|||
|
|
<div class="card-header bg-white">
|
|||
|
|
<h6 class="m-0 font-weight-bold">激活趋势(最近30天)</h6>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<canvas id="activationChart" height="300"></canvas>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-lg-4">
|
|||
|
|
<div class="card shadow">
|
|||
|
|
<div class="card-header bg-white">
|
|||
|
|
<h6 class="m-0 font-weight-bold">产品分布</h6>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<canvas id="productDistributionChart" height="300"></canvas>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 详细统计 -->
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col-lg-6">
|
|||
|
|
<div class="card shadow">
|
|||
|
|
<div class="card-header bg-white">
|
|||
|
|
<h6 class="m-0 font-weight-bold">最近激活的卡密</h6>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="table-responsive">
|
|||
|
|
<table class="table table-sm">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>卡密</th>
|
|||
|
|
<th>产品</th>
|
|||
|
|
<th>激活时间</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="recent-activations">
|
|||
|
|
<tr>
|
|||
|
|
<td colspan="3" class="text-center text-muted">加载中...</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-lg-6">
|
|||
|
|
<div class="card shadow">
|
|||
|
|
<div class="card-header bg-white">
|
|||
|
|
<h6 class="m-0 font-weight-bold">热门产品</h6>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="table-responsive">
|
|||
|
|
<table class="table table-sm">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>产品名称</th>
|
|||
|
|
<th>卡密数</th>
|
|||
|
|
<th>设备数</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="popular-products">
|
|||
|
|
<tr>
|
|||
|
|
<td colspan="3" class="text-center text-muted">加载中...</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{% endblock %}
|
|||
|
|
|
|||
|
|
{% block extra_js %}
|
|||
|
|
<script>
|
|||
|
|
let activationChart, productDistributionChart;
|
|||
|
|
|
|||
|
|
// 页面加载完成后初始化
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
loadStatistics();
|
|||
|
|
initCharts();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载统计数据
|
|||
|
|
function loadStatistics() {
|
|||
|
|
// 加载总览统计
|
|||
|
|
apiRequest('/api/v1/statistics/overview')
|
|||
|
|
.then(data => {
|
|||
|
|
if (data.success) {
|
|||
|
|
const stats = data.data;
|
|||
|
|
document.getElementById('total-products').textContent = stats.products.total;
|
|||
|
|
document.getElementById('total-licenses').textContent = stats.licenses.total;
|
|||
|
|
document.getElementById('total-devices').textContent = stats.devices.total;
|
|||
|
|
document.getElementById('total-tickets').textContent = stats.tickets.total;
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('Failed to load statistics:', error);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载激活趋势
|
|||
|
|
apiRequest('/api/v1/statistics/activations?days=30')
|
|||
|
|
.then(data => {
|
|||
|
|
if (data.success && activationChart) {
|
|||
|
|
const labels = data.data.activations.map(item => {
|
|||
|
|
const date = new Date(item.date);
|
|||
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|||
|
|
});
|
|||
|
|
const counts = data.data.activations.map(item => item.count);
|
|||
|
|
|
|||
|
|
activationChart.data.labels = labels;
|
|||
|
|
activationChart.data.datasets[0].data = counts;
|
|||
|
|
activationChart.update();
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('Failed to load activation trend:', error);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载产品分布
|
|||
|
|
apiRequest('/api/v1/statistics/products')
|
|||
|
|
.then(data => {
|
|||
|
|
if (data.success && productDistributionChart) {
|
|||
|
|
const products = data.data.products.slice(0, 5); // 只显示前5个产品
|
|||
|
|
const labels = products.map(p => p.product_name);
|
|||
|
|
const counts = products.map(p => p.license_count);
|
|||
|
|
|
|||
|
|
productDistributionChart.data.labels = labels;
|
|||
|
|
productDistributionChart.data.datasets[0].data = counts;
|
|||
|
|
productDistributionChart.update();
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('Failed to load product distribution:', error);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载最近激活
|
|||
|
|
loadRecentActivations();
|
|||
|
|
|
|||
|
|
// 加载热门产品
|
|||
|
|
loadPopularProducts();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化图表
|
|||
|
|
function initCharts() {
|
|||
|
|
// 激活趋势图表
|
|||
|
|
const activationCtx = document.getElementById('activationChart').getContext('2d');
|
|||
|
|
activationChart = new Chart(activationCtx, {
|
|||
|
|
type: 'line',
|
|||
|
|
data: {
|
|||
|
|
labels: [],
|
|||
|
|
datasets: [{
|
|||
|
|
label: '激活数',
|
|||
|
|
data: [],
|
|||
|
|
borderColor: 'rgb(75, 192, 192)',
|
|||
|
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
|||
|
|
tension: 0.1
|
|||
|
|
}]
|
|||
|
|
},
|
|||
|
|
options: {
|
|||
|
|
responsive: true,
|
|||
|
|
maintainAspectRatio: false,
|
|||
|
|
plugins: {
|
|||
|
|
legend: {
|
|||
|
|
display: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
scales: {
|
|||
|
|
y: {
|
|||
|
|
beginAtZero: true,
|
|||
|
|
ticks: {
|
|||
|
|
stepSize: 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 产品分布图表
|
|||
|
|
const productDistributionCtx = document.getElementById('productDistributionChart').getContext('2d');
|
|||
|
|
productDistributionChart = new Chart(productDistributionCtx, {
|
|||
|
|
type: 'doughnut',
|
|||
|
|
data: {
|
|||
|
|
labels: [],
|
|||
|
|
datasets: [{
|
|||
|
|
data: [],
|
|||
|
|
backgroundColor: [
|
|||
|
|
'rgba(255, 99, 132, 0.8)',
|
|||
|
|
'rgba(54, 162, 235, 0.8)',
|
|||
|
|
'rgba(255, 206, 86, 0.8)',
|
|||
|
|
'rgba(75, 192, 192, 0.8)',
|
|||
|
|
'rgba(153, 102, 255, 0.8)'
|
|||
|
|
]
|
|||
|
|
}]
|
|||
|
|
},
|
|||
|
|
options: {
|
|||
|
|
responsive: true,
|
|||
|
|
maintainAspectRatio: false,
|
|||
|
|
plugins: {
|
|||
|
|
legend: {
|
|||
|
|
position: 'bottom'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载最近激活
|
|||
|
|
function loadRecentActivations() {
|
|||
|
|
apiRequest('/api/v1/licenses?status=1&per_page=5&sort=activate_time&order=desc')
|
|||
|
|
.then(data => {
|
|||
|
|
if (data.success) {
|
|||
|
|
const tbody = document.getElementById('recent-activations');
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
|
|||
|
|
if (data.data.licenses.length === 0) {
|
|||
|
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">暂无数据</td></tr>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
data.data.licenses.forEach(license => {
|
|||
|
|
const row = `
|
|||
|
|
<tr>
|
|||
|
|
<td><code>${license.license_key.substring(0, 8)}...</code></td>
|
|||
|
|
<td>${license.product_name || '-'}</td>
|
|||
|
|
<td>${formatDate(license.activate_time)}</td>
|
|||
|
|
</tr>
|
|||
|
|
`;
|
|||
|
|
tbody.innerHTML += row;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('Failed to load recent activations:', error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载热门产品
|
|||
|
|
function loadPopularProducts() {
|
|||
|
|
apiRequest('/api/v1/statistics/products')
|
|||
|
|
.then(data => {
|
|||
|
|
if (data.success) {
|
|||
|
|
const tbody = document.getElementById('popular-products');
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
|
|||
|
|
if (data.data.products.length === 0) {
|
|||
|
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">暂无数据</td></tr>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示前5个产品
|
|||
|
|
data.data.products.slice(0, 5).forEach(product => {
|
|||
|
|
const row = `
|
|||
|
|
<tr>
|
|||
|
|
<td>${product.product_name}</td>
|
|||
|
|
<td>${product.license_count}</td>
|
|||
|
|
<td>${product.device_count}</td>
|
|||
|
|
</tr>
|
|||
|
|
`;
|
|||
|
|
tbody.innerHTML += row;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('Failed to load popular products:', error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
{% endblock %}
|