727 lines
21 KiB
PHP
727 lines
21 KiB
PHP
{include file="public/header" /}
|
|
<script src="__STATIC__/js/jquery.min.js"></script>
|
|
<style>
|
|
.dashboard-container {
|
|
padding: 24px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
/* background-color: #f5f7fa; */
|
|
/* min-height: calc(100vh - 60px); */
|
|
}
|
|
.welcome-header {
|
|
background: linear-gradient(135deg, #3881fd 0%, #2c5fd9 100%);
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
color: white;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 4px 20px rgba(56, 129, 253, 0.15);
|
|
}
|
|
.welcome-header h1 {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
.welcome-header p {
|
|
font-size: 15px;
|
|
opacity: 0.9;
|
|
margin: 0;
|
|
}
|
|
.stats-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
gap: 24px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
background: #3881fd;
|
|
}
|
|
.stat-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.stat-card .stat-value {
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
margin: 12px 0;
|
|
display: flex;
|
|
align-items: baseline;
|
|
}
|
|
.stat-card .stat-title {
|
|
color: #64748b;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.stat-card .stat-icon {
|
|
position: absolute;
|
|
right: 20px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 48px;
|
|
opacity: 0.1;
|
|
}
|
|
.quick-actions {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
}
|
|
.quick-actions h2 {
|
|
color: #1e293b;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.quick-actions h2::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 4px;
|
|
height: 18px;
|
|
background: #3881fd;
|
|
margin-right: 8px;
|
|
border-radius: 2px;
|
|
}
|
|
.action-buttons {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
.action-button {
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
color: #1e293b;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-decoration: none;
|
|
}
|
|
.action-button:hover {
|
|
background: #3881fd;
|
|
color: white;
|
|
border-color: #3881fd;
|
|
transform: translateY(-2px);
|
|
}
|
|
.action-button i {
|
|
margin-right: 8px;
|
|
}
|
|
.recent-activity {
|
|
margin-top: 24px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
}
|
|
.activity-list {
|
|
margin-top: 16px;
|
|
}
|
|
.activity-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
}
|
|
.activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.activity-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
background: #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 12px;
|
|
}
|
|
.activity-content {
|
|
flex: 1;
|
|
}
|
|
.activity-title {
|
|
font-weight: 500;
|
|
color: #9b9b9b;
|
|
}
|
|
.activity-time {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
.charts-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 24px;
|
|
margin-top: 24px;
|
|
}
|
|
.chart-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
}
|
|
.chart-card h2 {
|
|
color: #1e293b;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.chart-card h2::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 4px;
|
|
height: 18px;
|
|
background: #3881fd;
|
|
margin-right: 8px;
|
|
border-radius: 2px;
|
|
}
|
|
.chart-container {
|
|
height: 300px;
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
|
|
<div class="dashboard-container">
|
|
<div class="welcome-header">
|
|
<h1>欢迎使用{$config['admin_name']}</h1>
|
|
<p>今天是 <span id="current-time"></span>,祝您工作愉快</p>
|
|
</div>
|
|
|
|
<div class="stats-container">
|
|
<div class="stat-card">
|
|
<div class="stat-title">用户总数</div>
|
|
<div class="stat-value">{$todayStats.total_users|number_format}</div>
|
|
<div class="stat-icon">👥</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title">今日访问</div>
|
|
<div class="stat-value">{$todayStats.daily_visits|number_format}</div>
|
|
<div class="stat-icon">📊</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title">文章总数</div>
|
|
<div class="stat-value">{$todayStats.total_articles|number_format}</div>
|
|
<div class="stat-icon">📝</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title">资源总数</div>
|
|
<div class="stat-value">{$todayStats.total_resources|number_format}</div>
|
|
<div class="stat-icon">📦</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="quick-actions">
|
|
<h2>快捷操作</h2>
|
|
<div class="action-buttons">
|
|
<a href="{:url('user/index')}" class="action-button">
|
|
<i class="fas fa-users"></i>用户管理
|
|
</a>
|
|
<a href="{:url('content/publish')}" class="action-button">
|
|
<i class="fas fa-edit"></i>内容发布
|
|
</a>
|
|
<a href="{:url('statistics/index')}" class="action-button">
|
|
<i class="fas fa-chart-bar"></i>数据统计
|
|
</a>
|
|
<a href="{:url('system/settings')}" class="action-button">
|
|
<i class="fas fa-cog"></i>系统设置
|
|
</a>
|
|
<a href="{:url('system/clear_cache')}" class="action-button">
|
|
<i class="fas fa-broom"></i>清除缓存
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="recent-activity">
|
|
<h2>最近动态</h2>
|
|
<div class="activity-list">
|
|
{volist name="recentActivities" id="activity"}
|
|
<div class="activity-item">
|
|
<div class="activity-icon">{$activity.icon|default='📌'}</div>
|
|
<div class="activity-content">
|
|
<div class="activity-title">{$activity.content}</div>
|
|
</div>
|
|
</div>
|
|
{/volist}
|
|
</div>
|
|
</div>
|
|
<div class="charts-container">
|
|
<div class="chart-card">
|
|
<h2>访问趋势</h2>
|
|
<div id="visitTrend" class="chart-container"></div>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h2>用户增长</h2>
|
|
<div id="userGrowth" class="chart-container"></div>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h2>资源统计</h2>
|
|
<div id="resourceStats" class="chart-container"></div>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h2>文章统计</h2>
|
|
<div id="articleStats" class="chart-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<script src="__JS__/echarts.min.js"></script>
|
|
<script>
|
|
function updateTime() {
|
|
var now = new Date();
|
|
var year = now.getFullYear();
|
|
var month = now.getMonth() + 1;
|
|
var date = now.getDate();
|
|
var hours = now.getHours();
|
|
var minutes = now.getMinutes();
|
|
var seconds = now.getSeconds();
|
|
|
|
var padZero = function(num) {
|
|
return num < 10 ? '0' + num : num;
|
|
};
|
|
|
|
var timeString = year + '年' +
|
|
padZero(month) + '月' +
|
|
padZero(date) + '日 ' +
|
|
padZero(hours) + ':' +
|
|
padZero(minutes) + ':' +
|
|
padZero(seconds);
|
|
|
|
document.getElementById('current-time').innerHTML = timeString;
|
|
}
|
|
|
|
// 获取用户统计数据
|
|
function getUserCounts() {
|
|
fetch('{:url("users/counts")}')
|
|
.then(response => response.json())
|
|
.then(res => {
|
|
// console.log('用户统计接口返回数据:', res);
|
|
if (res.code === 0 && res.data) {
|
|
// 更新用户总数
|
|
document.querySelector('.stat-card:nth-child(1) .stat-value').textContent = res.data.total.toLocaleString();
|
|
|
|
// 更新用户增长图表
|
|
if (window.userChart) {
|
|
window.userChart.setOption({
|
|
xAxis: {
|
|
data: res.data.dates
|
|
},
|
|
series: [{
|
|
name: '新增用户',
|
|
data: res.data.counts
|
|
}, {
|
|
name: '总用户数',
|
|
data: res.data.totalCounts
|
|
}]
|
|
});
|
|
}
|
|
} else {
|
|
console.warn('用户统计接口返回异常:', res);
|
|
document.querySelector('.stat-card:nth-child(1) .stat-value').textContent = '0';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取用户统计失败:', error);
|
|
document.querySelector('.stat-card:nth-child(1) .stat-value').textContent = '0';
|
|
});
|
|
}
|
|
|
|
// 获取文章统计数据
|
|
function getArticleCounts() {
|
|
fetch('{:url("articles/counts")}')
|
|
.then(response => response.json())
|
|
.then(res => {
|
|
// console.log('文章统计接口返回数据:', res);
|
|
if (res.code === 0 && res.data) {
|
|
// 更新文章总数
|
|
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = res.data.total.toLocaleString();
|
|
|
|
// 更新文章统计图表
|
|
if (window.articleChart) {
|
|
window.articleChart.setOption({
|
|
xAxis: {
|
|
data: res.data.dates
|
|
},
|
|
series: [{
|
|
name: '新增文章',
|
|
data: res.data.counts
|
|
}, {
|
|
name: '总文章数',
|
|
data: res.data.totalCounts
|
|
}]
|
|
});
|
|
}
|
|
} else {
|
|
console.warn('文章统计接口返回异常:', res);
|
|
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = '0';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取文章统计失败:', error);
|
|
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = '0';
|
|
});
|
|
}
|
|
|
|
// 获取资源统计数据
|
|
function getResourcesCounts() {
|
|
fetch('{:url("resources/counts")}')
|
|
.then(response => response.json())
|
|
.then(res => {
|
|
// console.log('资源统计接口返回数据:', res);
|
|
if (res.code === 0 && res.data) {
|
|
// 更新资源总数
|
|
document.querySelector('.stat-card:nth-child(4) .stat-value').textContent = res.data.total.toLocaleString();
|
|
|
|
// 更新资源统计图表
|
|
if (window.resourceChart) {
|
|
window.resourceChart.setOption({
|
|
xAxis: {
|
|
data: res.data.dates
|
|
},
|
|
series: [{
|
|
name: '新增资源',
|
|
data: res.data.counts
|
|
}, {
|
|
name: '总资源数',
|
|
data: res.data.totalCounts
|
|
}]
|
|
});
|
|
}
|
|
} else {
|
|
console.warn('资源统计接口返回异常:', res);
|
|
document.querySelector('.stat-card:nth-child(4) .stat-value').textContent = '0';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取资源统计失败:', error);
|
|
document.querySelector('.stat-card:nth-child(4) .stat-value').textContent = '0';
|
|
});
|
|
}
|
|
|
|
updateTime();
|
|
setInterval(updateTime, 1000);
|
|
|
|
// 页面加载完成后获取统计数据
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
getUserCounts();
|
|
getArticleCounts();
|
|
getResourcesCounts();
|
|
});
|
|
|
|
// 访问趋势图表
|
|
function initVisitTrend() {
|
|
var chart = echarts.init(document.getElementById('visitTrend'));
|
|
var option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'shadow'
|
|
}
|
|
},
|
|
legend: {
|
|
data: ['访问量', '独立访客']
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: {$chartData.visitTrend.dates|json_encode|raw},
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: '#e2e8f0'
|
|
}
|
|
}
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: '#e2e8f0'
|
|
}
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
color: '#f1f5f9'
|
|
}
|
|
}
|
|
},
|
|
series: [{
|
|
name: '访问量',
|
|
data: {$chartData.visitTrend.visits|json_encode|raw},
|
|
type: 'line',
|
|
smooth: true,
|
|
areaStyle: {
|
|
opacity: 0.1
|
|
},
|
|
itemStyle: {
|
|
color: '#3881fd'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
}, {
|
|
name: '独立访客',
|
|
data: {$chartData.visitTrend.uvs|json_encode|raw},
|
|
type: 'line',
|
|
smooth: true,
|
|
itemStyle: {
|
|
color: '#10b981'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
}]
|
|
};
|
|
chart.setOption(option);
|
|
}
|
|
|
|
// 用户增长图表
|
|
function initUserGrowth() {
|
|
var chart = echarts.init(document.getElementById('userGrowth'));
|
|
var option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'cross',
|
|
label: {
|
|
backgroundColor: '#6a7985'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
data: ['新增用户', '总用户数']
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: {$chartData.userGrowth.dates|json_encode|raw}
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{
|
|
name: '新增用户',
|
|
type: 'bar',
|
|
data: {$chartData.userGrowth.newUsers|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#3881fd'
|
|
}
|
|
},
|
|
{
|
|
name: '总用户数',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: {$chartData.userGrowth.totalUsers|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#10b981'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
}
|
|
]
|
|
};
|
|
chart.setOption(option);
|
|
}
|
|
|
|
// 资源统计图表
|
|
function initResourceStats() {
|
|
var chart = echarts.init(document.getElementById('resourceStats'));
|
|
var option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'cross',
|
|
label: {
|
|
backgroundColor: '#6a7985'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
data: ['新增资源', '总资源数', '下载量']
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: {$chartData.resourceStats.dates|json_encode|raw}
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{
|
|
name: '新增资源',
|
|
type: 'bar',
|
|
data: {$chartData.resourceStats.newResources|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#3881fd'
|
|
}
|
|
},
|
|
{
|
|
name: '总资源数',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: {$chartData.resourceStats.totalResources|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#10b981'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
},
|
|
{
|
|
name: '下载量',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: {$chartData.resourceStats.downloads|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#f59e0b'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
}
|
|
]
|
|
};
|
|
chart.setOption(option);
|
|
}
|
|
|
|
// 文章统计图表
|
|
function initArticleStats() {
|
|
var chart = echarts.init(document.getElementById('articleStats'));
|
|
var option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'cross',
|
|
label: {
|
|
backgroundColor: '#6a7985'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
data: ['新增文章', '总文章数', '浏览量']
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: {$chartData.articleStats.dates|json_encode|raw}
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{
|
|
name: '新增文章',
|
|
type: 'bar',
|
|
data: {$chartData.articleStats.newArticles|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#3881fd'
|
|
}
|
|
},
|
|
{
|
|
name: '总文章数',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: {$chartData.articleStats.totalArticles|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#10b981'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
},
|
|
{
|
|
name: '浏览量',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: {$chartData.articleStats.views|json_encode|raw},
|
|
itemStyle: {
|
|
color: '#f59e0b'
|
|
},
|
|
lineStyle: {
|
|
width: 3
|
|
}
|
|
}
|
|
]
|
|
};
|
|
chart.setOption(option);
|
|
}
|
|
|
|
// 初始化所有图表
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 确保ECharts已加载
|
|
if (typeof echarts === 'undefined') {
|
|
console.error('ECharts未加载');
|
|
return;
|
|
}
|
|
|
|
// 初始化图表
|
|
try {
|
|
initVisitTrend();
|
|
initUserGrowth();
|
|
initResourceStats();
|
|
initArticleStats();
|
|
|
|
// 监听窗口大小变化,重绘图表
|
|
window.addEventListener('resize', function() {
|
|
var charts = document.querySelectorAll('.chart-container');
|
|
charts.forEach(function(chart) {
|
|
var instance = echarts.getInstanceByDom(chart);
|
|
if (instance) {
|
|
instance.resize();
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('初始化图表失败:', error);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{include file="public/tail" /} |