This commit is contained in:
JiXieShi
2024-11-16 23:59:15 +08:00
parent f722153536
commit 87859c7bb8
42 changed files with 2018 additions and 485 deletions

View File

@@ -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();
};
});

View File

@@ -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);
}
});
});

View File

@@ -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())

View File

@@ -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)

View File

@@ -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;
});

View File

@@ -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'));
// 更新选中状态

View File

@@ -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)

View File

@@ -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: [[