up
This commit is contained in:
@@ -2,10 +2,17 @@ layui.use(['layer'], function(){
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化图表
|
||||
var registerTrendChart = echarts.init(document.getElementById('register-trend'));
|
||||
var deviceTypesChart = echarts.init(document.getElementById('device-types'));
|
||||
|
||||
// 加载统计数据
|
||||
function loadStats() {
|
||||
fetch('/api/dashboard/stats', {
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
@@ -21,21 +28,157 @@ layui.use(['layer'], function(){
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
$('#total-devices').text(result.data.total_devices);
|
||||
$('#total-licenses').text(result.data.total_licenses);
|
||||
$('#today-new').text(result.data.today_new);
|
||||
$('#online-devices').text(result.data.online_devices);
|
||||
$('#active-devices').text(result.data.active_devices);
|
||||
$('#expired-devices').text(result.data.expired_devices);
|
||||
updateStats(result.data);
|
||||
// 更新图表
|
||||
updateCharts(result.data);
|
||||
// 更新系统状态
|
||||
updateSystemStatus(result.data);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载统计数据失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
function updateStats(data) {
|
||||
$('#total-devices').text(data.total_devices);
|
||||
$('#total-licenses').text(data.total_licenses);
|
||||
$('#online-devices').text(data.online_devices);
|
||||
$('#expired-devices').text(data.expired_devices);
|
||||
$('#today-new').text(data.today_new);
|
||||
$('#unused-licenses').text(data.unused_licenses);
|
||||
|
||||
// 计算比率
|
||||
var activeRate = data.total_devices > 0 ?
|
||||
((data.online_devices / data.total_devices) * 100).toFixed(1) : 0;
|
||||
var expiredRate = data.total_devices > 0 ?
|
||||
((data.expired_devices / data.total_devices) * 100).toFixed(1) : 0;
|
||||
|
||||
$('#active-rate').text(activeRate + '%');
|
||||
$('#expired-rate').text(expiredRate + '%');
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateCharts(data) {
|
||||
// 设备注册趋势图
|
||||
var trendOption = {
|
||||
title: {
|
||||
text: '最近7天设备注册趋势'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.trend_dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: '新增设备',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: data.trend_counts,
|
||||
itemStyle: {
|
||||
color: '#009688'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 150, 136, 0.3)'
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(0, 150, 136, 0.1)'
|
||||
}])
|
||||
}
|
||||
}]
|
||||
};
|
||||
registerTrendChart.setOption(trendOption);
|
||||
|
||||
// 设备类型分布图
|
||||
var typesOption = {
|
||||
title: {
|
||||
text: '设备类型分布'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: data.device_types.map(item => ({
|
||||
name: item.type,
|
||||
value: item.count
|
||||
}))
|
||||
}]
|
||||
};
|
||||
deviceTypesChart.setOption(typesOption);
|
||||
}
|
||||
|
||||
// 更新系统状态
|
||||
function updateSystemStatus(data) {
|
||||
$('#cpu-usage').text(data.cpu_usage + '%');
|
||||
$('#memory-usage').text(data.memory_usage + '%');
|
||||
$('#disk-usage').text(data.disk_usage + '%');
|
||||
$('#uptime').text(formatDuration(data.uptime));
|
||||
$('#load-avg').text(data.load_avg.join(' '));
|
||||
$('#network-traffic').text(formatBytes(data.network_traffic) + '/s');
|
||||
$('#online-users').text(data.online_users);
|
||||
$('#last-update').text(new Date().toLocaleString());
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatDuration(seconds) {
|
||||
var days = Math.floor(seconds / 86400);
|
||||
var hours = Math.floor((seconds % 86400) / 3600);
|
||||
var minutes = Math.floor((seconds % 3600) / 60);
|
||||
var parts = [];
|
||||
if (days > 0) parts.push(days + '天');
|
||||
if (hours > 0) parts.push(hours + '小时');
|
||||
if (minutes > 0) parts.push(minutes + '分钟');
|
||||
return parts.join(' ') || '0分钟';
|
||||
}
|
||||
|
||||
// 格式化字节大小
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024;
|
||||
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadStats();
|
||||
|
||||
// 定时刷新(每30秒)
|
||||
setInterval(loadStats, 30000);
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
window.onresize = function() {
|
||||
registerTrendChart.resize();
|
||||
deviceTypesChart.resize();
|
||||
};
|
||||
});
|
@@ -1,36 +1,24 @@
|
||||
layui.use(['table', 'form', 'layer'], function(){
|
||||
layui.use(['table', 'form', 'layer', 'laytpl'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var laytpl = layui.laytpl;
|
||||
var $ = layui.$;
|
||||
|
||||
// 加载设备型号列表
|
||||
fetch('/api/devices/models', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.data) {
|
||||
var options = '<option value="">全部</option>';
|
||||
result.data.forEach(function(model) {
|
||||
options += '<option value="' + model.model_name + '">' + model.model_name + '</option>';
|
||||
});
|
||||
$('select[name=device_model]').html(options);
|
||||
form.render('select');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#device-table',
|
||||
url: '/api/devices/registered', // 已注册设备列表接口
|
||||
headers: undefined,
|
||||
url: '/api/devices/registered',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'uid', title: '设备UID', width: 180},
|
||||
{field: 'device_model', title: '设备型号', width: 120},
|
||||
{field: 'device_type', title: '设备类型', width: 120},
|
||||
{field: 'license_code', title: '授权码', width: 180},
|
||||
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
|
||||
var types = {
|
||||
@@ -40,15 +28,17 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
};
|
||||
return types[d.license_type] || '-';
|
||||
}},
|
||||
{field: 'expire_time', title: '过期时间', width: 160, templet: function(d){
|
||||
return d.expire_time ? new Date(d.expire_time).toLocaleString() : '-';
|
||||
}},
|
||||
{field: 'start_count', title: '启动次数', width: 100},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">正常</span>';
|
||||
if(d.status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">未激活</span>';
|
||||
}},
|
||||
{field: 'start_count', title: '启动次数', width: 100, templet: function(d){
|
||||
if(d.license_type === 'count') {
|
||||
return d.start_count + ' / ' + d.max_uses;
|
||||
}
|
||||
return d.start_count || 0;
|
||||
}},
|
||||
{field: 'last_active_at', title: '最后活跃', width: 160, templet: function(d){
|
||||
return d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-';
|
||||
}},
|
||||
@@ -79,11 +69,14 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
|
||||
switch(obj.event){
|
||||
case 'view':
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '设备详情',
|
||||
area: ['600px', '500px'],
|
||||
content: laytpl($('#deviceDetailTpl').html()).render(data)
|
||||
// 使用laytpl渲染设备详情
|
||||
laytpl(document.getElementById('deviceDetailTpl').innerHTML).render(data, function(html){
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '设备详情',
|
||||
area: ['600px', '600px'],
|
||||
content: html
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'bind':
|
||||
@@ -161,42 +154,4 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 导出设备列表
|
||||
$('#export-devices').on('click', function(){
|
||||
var checkStatus = table.checkStatus('device-table');
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要导出的设备');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建CSV内容
|
||||
var csv = '设备UID,设备型号,授权码,授权类型,过期时间,启动次数,状态,最后活跃\n';
|
||||
data.forEach(function(item){
|
||||
csv += [
|
||||
item.uid,
|
||||
item.device_model,
|
||||
item.license_code || '',
|
||||
item.license_type || '',
|
||||
item.expire_time ? new Date(item.expire_time).toLocaleString() : '',
|
||||
item.start_count,
|
||||
item.status,
|
||||
item.last_active_at ? new Date(item.last_active_at).toLocaleString() : ''
|
||||
].join(',') + '\n';
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
var link = document.createElement("a");
|
||||
if (link.download !== undefined) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "devices.csv");
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
});
|
||||
});
|
@@ -8,39 +8,22 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
table.render({
|
||||
elem: '#device-table',
|
||||
url: '/api/devices/models',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'ID', hide: true},
|
||||
{field: 'model_name', title: '设备型号', width: 180},
|
||||
{field: 'device_type', title: '设备类型', width: 120, templet: function(d){
|
||||
var types = {
|
||||
'software': '软件',
|
||||
'website': '网站',
|
||||
'embedded': '嵌入式设备',
|
||||
'mcu': '单片机设备'
|
||||
};
|
||||
return types[d.device_type] || d.device_type;
|
||||
}},
|
||||
{field: 'device_type', title: '设备类型', width: 120},
|
||||
{field: 'company', title: '所属公司', width: 150},
|
||||
{field: 'remark', title: '备注说明'},
|
||||
{field: 'device_count', title: '设备数量', width: 100},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">已激活</span>';
|
||||
if(d.status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">未激活</span>';
|
||||
}},
|
||||
{field: 'CreatedAt', title: '创建时间', width: 180, templet: function(d){
|
||||
return d.CreatedAt ? new Date(d.CreatedAt).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}) : '';
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">启用</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">禁用</span>';
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
|
||||
]],
|
||||
@@ -69,7 +52,7 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
return;
|
||||
}
|
||||
layer.confirm('确定删除选中的设备型号吗?', function(index){
|
||||
var ids = data.map(item => item.id);
|
||||
var ids = data.map(item => item.ID);
|
||||
fetch('/api/devices/models/batch', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -108,22 +91,21 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
form.val('deviceForm', data);
|
||||
form.val('deviceForm', {
|
||||
id: data.ID,
|
||||
model_name: data.model_name,
|
||||
device_type: data.device_type,
|
||||
company: data.company,
|
||||
status: data.status || 'active',
|
||||
remark: data.remark
|
||||
});
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'files':
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '设备文件管理',
|
||||
area: ['900px', '600px'],
|
||||
content: '/admin/device-files?model=' + encodeURIComponent(data.deviceModel)
|
||||
});
|
||||
break;
|
||||
case 'del':
|
||||
layer.confirm('确定删除该设备型号吗?', function(index){
|
||||
fetch('/api/devices/models/' + data.id, {
|
||||
fetch('/api/devices/models/' + data.ID, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
@@ -150,7 +132,10 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
table.reload('device-table', {
|
||||
where: data.field
|
||||
where: data.field,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
@@ -163,10 +148,6 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
// 初始化设备类型选择
|
||||
form.val('deviceForm', {
|
||||
'device_type': 'software' // 设置默认值
|
||||
});
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
@@ -174,34 +155,27 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
|
||||
// 设备型号表单提交
|
||||
form.on('submit(deviceSubmit)', function(data){
|
||||
// 如果是编辑模式,确保 id 是数字类型
|
||||
if(data.field.id) {
|
||||
data.field.id = parseInt(data.field.id);
|
||||
}
|
||||
|
||||
// 构造提交数据,使用下划线命名
|
||||
// 构造请求数据
|
||||
const submitData = {
|
||||
model_name: data.field.model_name,
|
||||
device_type: data.field.device_type,
|
||||
company: data.field.company,
|
||||
remark: data.field.remark,
|
||||
status: 'active'
|
||||
status: data.field.status,
|
||||
remark: data.field.remark
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加 ID
|
||||
if(data.field.id) {
|
||||
submitData.id = data.field.id;
|
||||
}
|
||||
|
||||
var url = data.field.id ? '/api/devices/models/' + data.field.id : '/api/devices/models';
|
||||
var method = data.field.id ? 'PUT' : 'POST';
|
||||
// 确定请求URL和方法
|
||||
const url = data.field.id ?
|
||||
`/api/devices/models/${data.field.id}` :
|
||||
'/api/devices/models';
|
||||
const method = data.field.id ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
@@ -17,7 +17,9 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
table.render({
|
||||
elem: '#license-table',
|
||||
url: '/api/licenses',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
@@ -215,7 +217,7 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
type: 2,
|
||||
title: '使用日志',
|
||||
area: ['800px', '600px'],
|
||||
content: '/admin/license-logs?id=' + data.id
|
||||
content: '/admin/license-logs?id=' + data.id + '&token=' + localStorage.getItem('token')
|
||||
});
|
||||
break;
|
||||
case 'revoke':
|
||||
@@ -316,7 +318,8 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
fetch('/api/licenses', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
|
@@ -3,18 +3,8 @@ layui.use(['form', 'layer'], function(){
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 检查用户是否已登录
|
||||
function checkIfLoggedIn() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
checkIfLoggedIn();
|
||||
|
||||
// 加载验证码
|
||||
var captchaId = '';
|
||||
function loadCaptcha() {
|
||||
fetch('/api/captcha')
|
||||
.then(response => {
|
||||
@@ -32,6 +22,7 @@ layui.use(['form', 'layer'], function(){
|
||||
if (data.imageBase64) {
|
||||
$('#captchaImg').attr('src', 'data:image/png;base64,' + data.imageBase64);
|
||||
$('input[name=captchaId]').val(data.captchaId);
|
||||
captchaId = data.captchaId;
|
||||
} else {
|
||||
throw new Error('验证码图片数据无效');
|
||||
}
|
||||
@@ -42,65 +33,41 @@ layui.use(['form', 'layer'], function(){
|
||||
$('#captchaImg').attr('src', '/static/images/captcha-error.png');
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时获取验证码
|
||||
loadCaptcha();
|
||||
|
||||
// 点击验证码图片刷新
|
||||
$('#captchaImg').on('click', function() {
|
||||
loadCaptcha();
|
||||
});
|
||||
// 点击验证码刷新
|
||||
$('#captchaImg').on('click', loadCaptcha);
|
||||
|
||||
// 登录表单提交
|
||||
// 表单提交
|
||||
form.on('submit(login)', function(data){
|
||||
var field = data.field;
|
||||
|
||||
// 添加验证码验证
|
||||
if (!field.captcha) {
|
||||
layer.msg('请输入验证码');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!field.captchaId) {
|
||||
layer.msg('验证码已失效,请刷新');
|
||||
loadCaptcha();
|
||||
return false;
|
||||
}
|
||||
data.field.captchaId = captchaId;
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: field.username,
|
||||
password: field.password,
|
||||
captcha: field.captcha,
|
||||
captchaId: field.captchaId
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
body: JSON.stringify(data.field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
if(result.error) {
|
||||
layer.msg(result.error);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
loadCaptcha();
|
||||
return;
|
||||
}
|
||||
// 确保 token 被正确设置
|
||||
document.cookie = `token=${result.token}; path=/; secure; samesite=strict`;
|
||||
localStorage.setItem('token', result.token);
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', result.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(result.data.user));
|
||||
|
||||
// 跳转到首页
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('登录失败:' + error.message);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
loadCaptcha();
|
||||
});
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@@ -24,28 +24,42 @@ layui.use(["element", "layer"], function () {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加通用的 fetch 封装,自动处理认证
|
||||
// 添加请求拦截器
|
||||
function addAuthHeader(url) {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
window.authFetch = function (url, options = {}) {
|
||||
return fetch(url, {
|
||||
return {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
// 封装fetch请求
|
||||
async function request(url, options = {}) {
|
||||
const headers = addAuthHeader(url);
|
||||
if (!headers) return;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
credentials: 'include', // 自动携带 cookie
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
throw new Error(`请求失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("请求处理失败:", error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 在页面加载时检查认证
|
||||
|
||||
@@ -99,7 +113,7 @@ layui.use(["element", "layer"], function () {
|
||||
|
||||
// 加载页面内容
|
||||
|
||||
$("#content-frame").attr("src", url);
|
||||
$("#content-frame").attr("src", url+"?token="+localStorage.getItem('token'));
|
||||
|
||||
// 更新选中状态
|
||||
|
||||
|
@@ -6,7 +6,10 @@ layui.use(['form', 'upload', 'layer'], function(){
|
||||
|
||||
// 加载当前配置
|
||||
fetch('/api/site/settings', {
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
@@ -112,7 +115,8 @@ layui.use(['form', 'upload', 'layer'], function(){
|
||||
fetch('/api/site/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
|
@@ -8,7 +8,9 @@ layui.use(['table', 'form', 'layer'], function(){
|
||||
table.render({
|
||||
elem: '#user-table',
|
||||
url: '/api/users',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
|
@@ -4,51 +4,137 @@
|
||||
<meta charset="utf-8">
|
||||
<title>控制台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stat-title {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stat-number {
|
||||
color: #009688;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-icon {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
color: #009688;
|
||||
}
|
||||
.stat-trend {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.trend-up {
|
||||
color: #5FB878;
|
||||
}
|
||||
.trend-down {
|
||||
color: #FF5722;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">设备总数</div>
|
||||
<div class="layui-card-body big-font" id="total-devices">0</div>
|
||||
<div class="stat-card">
|
||||
<i class="layui-icon layui-icon-component stat-icon"></i>
|
||||
<div class="stat-title">设备总数</div>
|
||||
<div class="stat-number" id="total-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
今日新增: <span id="today-new">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">授权码总数</div>
|
||||
<div class="layui-card-body big-font" id="total-licenses">0</div>
|
||||
<div class="stat-card">
|
||||
<i class="layui-icon layui-icon-vercode stat-icon"></i>
|
||||
<div class="stat-title">授权码总数</div>
|
||||
<div class="stat-number" id="total-licenses">0</div>
|
||||
<div class="stat-trend">
|
||||
未使用: <span id="unused-licenses">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">今日新增</div>
|
||||
<div class="layui-card-body big-font" id="today-new">0</div>
|
||||
<div class="stat-card">
|
||||
<i class="layui-icon layui-icon-circle-dot stat-icon"></i>
|
||||
<div class="stat-title">在线设备</div>
|
||||
<div class="stat-number" id="online-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
活跃率: <span id="active-rate">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">在线设备</div>
|
||||
<div class="layui-card-body big-font" id="online-devices">0</div>
|
||||
<div class="stat-card">
|
||||
<i class="layui-icon layui-icon-chart stat-icon"></i>
|
||||
<div class="stat-title">过期设备</div>
|
||||
<div class="stat-number" id="expired-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
占比: <span id="expired-rate">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="layui-col-md8">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">激活设备</div>
|
||||
<div class="layui-card-body big-font" id="active-devices">0</div>
|
||||
<div class="layui-card-header">设备注册趋势</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="register-trend" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-col-md4">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">过期设备</div>
|
||||
<div class="layui-card-body big-font" id="expired-devices">0</div>
|
||||
<div class="layui-card-header">设备类型分布</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="device-types" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">系统状态</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-row layui-col-space10">
|
||||
<div class="layui-col-md3">
|
||||
<div>CPU使用率:<span id="cpu-usage">0%</span></div>
|
||||
<div>内存使用率:<span id="memory-usage">0%</span></div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div>系统运行时间:<span id="uptime">0天</span></div>
|
||||
<div>磁盘使用率:<span id="disk-usage">0%</span></div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div>系统负载:<span id="load-avg">0</span></div>
|
||||
<div>网络流量:<span id="network-traffic">0 KB/s</span></div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div>当前在线用户:<span id="online-users">0</span></div>
|
||||
<div>最后更新时间:<span id="last-update">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/lib/echarts.min.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -72,7 +72,7 @@
|
||||
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="bind">绑定授权</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
|
||||
{{# if(d.status !== 'expired'){ }}
|
||||
{{# if(d.status !== 'expired' && d.license_code){ }}
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
@@ -94,13 +94,33 @@
|
||||
<td>设备型号</td>
|
||||
<td>{{ d.device_model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>设备类型</td>
|
||||
<td>{{ d.device_type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>所属公司</td>
|
||||
<td>{{ d.company || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权码</td>
|
||||
<td>{{ d.license_code || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权类型</td>
|
||||
<td>{{ d.license_type || '-' }}</td>
|
||||
<td>{{ {
|
||||
'time': '时间授权',
|
||||
'count': '次数授权',
|
||||
'permanent': '永久授权'
|
||||
}[d.license_type] || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权状态</td>
|
||||
<td>{{ {
|
||||
'active': '<span class="layui-badge layui-bg-green">正常</span>',
|
||||
'expired': '<span class="layui-badge layui-bg-orange">已过期</span>',
|
||||
'inactive': '<span class="layui-badge layui-bg-gray">未激活</span>'
|
||||
}[d.status] || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>过期时间</td>
|
||||
@@ -108,16 +128,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启动次数</td>
|
||||
<td>{{ d.start_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最后活跃</td>
|
||||
<td>{{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }}</td>
|
||||
<td>{{ d.start_count || 0 }}{{ d.max_uses ? ' / ' + d.max_uses : '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册时间</td>
|
||||
<td>{{ new Date(d.register_time).toLocaleString() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最后活跃</td>
|
||||
<td>{{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@@ -119,16 +119,12 @@
|
||||
<script type="text/html" id="tableRowBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="files"
|
||||
>文件</a
|
||||
>
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del"
|
||||
>删除</a
|
||||
>
|
||||
</script>
|
||||
|
||||
<!-- 设备表单模板 -->
|
||||
<!-- 设备型号表单模板 -->
|
||||
|
||||
<script type="text/html" id="deviceFormTpl">
|
||||
<form class="layui-form" style="padding: 20px;" lay-filter="deviceForm">
|
||||
@@ -180,6 +176,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="status" value="active" title="启用" checked>
|
||||
<input type="radio" name="status" value="disabled" title="禁用">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注说明</label>
|
||||
|
||||
|
@@ -4,44 +4,85 @@
|
||||
<meta charset="utf-8">
|
||||
<title>登录 - 授权验证管理平台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.login-container {
|
||||
width: 360px;
|
||||
padding: 30px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.1);
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.captcha-img {
|
||||
height: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.layui-input {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.layui-btn {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<div class="layui-container">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<div class="login-box">
|
||||
<h2>授权验证管理平台</h2>
|
||||
<form class="layui-form" action="">
|
||||
<div class="layui-form-item">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input type="password" name="password" required lay-verify="required"
|
||||
placeholder="密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<input type="text" name="captcha" required lay-verify="required"
|
||||
placeholder="请输入验证码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: 120px;">
|
||||
<img src="" id="captchaImg" style="height:38px;cursor:pointer;" alt="点击刷新">
|
||||
<input type="hidden" name="captchaId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<a href="javascript:;" class="forget-pwd">忘记密码?</a>
|
||||
<a href="javascript:;" class="register">注册账号</a>
|
||||
</div>
|
||||
</form>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2 class="login-title">授权验证管理平台</h2>
|
||||
<form class="layui-form login-form">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="请输入用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<input type="password" name="password" required lay-verify="required"
|
||||
placeholder="请输入密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block captcha-container" style="margin-left: 0;">
|
||||
<input type="text" name="captcha" required lay-verify="required"
|
||||
placeholder="请输入验证码" autocomplete="off" class="layui-input captcha-input">
|
||||
<img id="captchaImg" class="captcha-img" alt="验证码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="login">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
|
Reference in New Issue
Block a user