first commit

This commit is contained in:
JiXieShi
2024-11-14 22:55:43 +08:00
commit 421cfb8cfa
98 changed files with 12617 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
/* 全局样式 */
.login-body {
background-color: #f2f2f2;
padding-top: 100px;
}
.login-box {
background-color: #fff;
padding: 30px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-box h2 {
text-align: center;
margin-bottom: 30px;
}
.forget-pwd {
float: left;
}
.register {
float: right;
}
.captcha-img {
width: 100%;
height: 38px;
cursor: pointer;
}
/* 主界面样式 */
.layui-layout-admin .layui-body {
padding: 15px;
background-color: #f2f2f2;
}
.layui-card {
margin-bottom: 15px;
}
.layui-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 文件上传相关样式 */
.upload-progress {
margin: 10px 0;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: bold;
}
.file-meta {
color: #999;
font-size: 12px;
}
.file-actions {
margin-left: 20px;
}
.upload-form {
padding: 20px;
}
.selected-file {
margin-left: 10px;
color: #666;
}
.layui-progress {
margin: 10px 0;
}
/* 上传按钮组样式 */
.layui-card-header .layui-btn-group {
float: right;
}
/* 文件列表表格样式 */
.layui-table {
margin-top: 15px;
}
.layui-table th {
font-weight: bold;
background-color: #f2f2f2;
}
/* 上传进度条样式 */
.upload-progress-container {
margin: 10px 0;
padding: 10px;
background-color: #f8f8f8;
border-radius: 4px;
}
.upload-progress-info {
margin-top: 5px;
font-size: 12px;
color: #666;
}
/* 主界面布局样式 */
.layui-layout-admin .layui-logo {
display: flex;
align-items: center;
padding: 0 20px;
}
.layui-layout-admin .layui-logo img {
height: 30px;
margin-right: 10px;
}
.layui-layout-admin .layui-logo span {
color: #fff;
font-size: 18px;
font-weight: 600;
}
.layui-nav .layui-icon {
margin-right: 10px;
}
/* 底部样式 */
.layui-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
color: #666;
font-size: 12px;
}
.footer-left {
font-size: 12px;
}
.footer-right a {
color: #666;
text-decoration: none;
}
.footer-right a:hover {
color: #009688;
}
.captcha-img {
height: 38px;
cursor: pointer;
}
.layui-form-item .captcha-wrapper {
display: flex;
align-items: center;
}
.layui-form-item .captcha-input {
margin-right: 10px;
}
/* 主页布局样式 */
.layui-layout-admin .layui-logo {
display: flex;
align-items: center;
padding: 0 20px;
color: #fff;
font-size: 18px;
}
.layui-layout-admin .layui-logo img {
height: 30px;
margin-right: 10px;
}
.content-container {
padding: 15px;
height: calc(100% - 50px);
}
.layadmin-iframe {
width: 100%;
height: 100%;
background-color: #fff;
}
.layui-breadcrumb {
padding: 10px 15px;
background-color: #fff;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
}
/* 导航菜单样式 */
.layui-nav .layui-icon {
margin-right: 10px;
font-size: 16px;
}
.layui-nav-item a {
font-size: 14px;
}
.layui-nav-child dd {
padding-left: 20px;
}
/* 底部样式 */
.layui-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
color: #666;
font-size: 12px;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.layui-side {
display: none;
}
.layui-body {
left: 0;
}
.layui-footer {
left: 0;
}
}
/* 统计卡片样式 */
.big-font {
font-size: 24px;
font-weight: bold;
color: #009688;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+41
View File
@@ -0,0 +1,41 @@
layui.use(['layer'], function(){
var layer = layui.layer;
var $ = layui.$;
// 加载统计数据
function loadStats() {
fetch('/api/dashboard/stats', {
credentials: 'include'
})
.then(response => {
if (response.status === 401) {
window.location.href = '/login';
throw new Error('认证失败');
}
return response.json();
})
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
// 更新统计数据
$('#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);
})
.catch(error => {
layer.msg('加载统计数据失败:' + error.message);
});
}
// 初始加载
loadStats();
// 定时刷新(每30秒)
setInterval(loadStats, 30000);
});
+198
View File
@@ -0,0 +1,198 @@
layui.use(['upload', 'table', 'layer', 'form'], function(){
var upload = layui.upload;
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var $ = layui.$;
// 获取设备型号
var deviceModel = decodeURIComponent(location.search.match(/model=([^&]+)/)[1]);
// 初始化表格
table.render({
elem: '#file-table',
url: '/api/uploads/device/' + deviceModel,
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
toolbar: '#tableToolbar',
defaultToolbar: ['filter', 'exports', 'print'],
cols: [[
{type: 'checkbox'},
{field: 'file_name', title: '文件名', width: 200},
{field: 'version', title: '版本', width: 100},
{field: 'file_type', title: '类型', width: 100, templet: function(d){
if(d.is_update) return '<span class="layui-badge layui-bg-blue">更新</span>';
return '<span class="layui-badge layui-bg-gray">普通</span>';
}},
{field: 'file_size', title: '大小', width: 120, templet: function(d){
return formatFileSize(d.file_size);
}},
{field: 'downloads', title: '下载次数', width: 100},
{field: 'description', title: '描述'},
{field: 'created_at', title: '上传时间', width: 160, templet: function(d){
return new Date(d.created_at).toLocaleString();
}},
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
}
});
// 上传普通文件
upload.render({
elem: '#uploadFile',
url: '/api/uploads',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
data: {
device_model: deviceModel,
is_update: false
},
accept: 'file',
before: function(obj){
layer.load();
},
done: function(res){
layer.closeAll('loading');
if(res.error){
layer.msg(res.error);
return;
}
layer.msg('上传成功');
table.reload('file-table');
},
error: function(){
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 上传更新文件
upload.render({
elem: '#uploadUpdate',
url: '/api/uploads',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
data: {
device_model: deviceModel,
is_update: true
},
accept: 'file',
before: function(obj){
// 弹出版本信息输入框
layer.prompt({
formType: 0,
title: '请输入版本号',
area: ['300px', '150px']
}, function(value, index, elem){
this.field.version = value;
layer.close(index);
layer.load();
});
},
done: function(res){
layer.closeAll('loading');
if(res.error){
layer.msg(res.error);
return;
}
layer.msg('上传成功');
table.reload('file-table');
},
error: function(){
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 表格工具栏事件
table.on('toolbar(file-table)', function(obj){
var checkStatus = table.checkStatus(obj.config.id);
switch(obj.event){
case 'refresh':
table.reload('file-table');
break;
case 'batchDel':
var data = checkStatus.data;
if(data.length === 0){
layer.msg('请选择要删除的文件');
return;
}
layer.confirm('确定删除选中的文件吗?', function(index){
var ids = data.map(item => item.id);
Promise.all(ids.map(id =>
fetch('/api/uploads/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}).then(response => response.json())
))
.then(() => {
layer.msg('批量删除成功');
table.reload('file-table');
})
.catch(error => {
layer.msg('批量删除失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 行工具栏事件
table.on('tool(file-table)', function(obj){
var data = obj.data;
switch(obj.event){
case 'download':
window.location.href = '/api/uploads/' + data.id;
break;
case 'del':
layer.confirm('确定删除该文件吗?', function(index){
fetch('/api/uploads/' + data.id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(result => {
if(result.error){
layer.msg(result.error);
return;
}
layer.msg('删除成功');
obj.del();
})
.catch(error => {
layer.msg('删除失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 格式化文件大小
function formatFileSize(size) {
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var index = 0;
while(size >= 1024 && index < units.length - 1) {
size /= 1024;
index++;
}
return size.toFixed(2) + ' ' + units[index];
}
});
+202
View File
@@ -0,0 +1,202 @@
layui.use(['table', 'form', 'layer'], function(){
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
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,
toolbar: '#tableToolbar',
defaultToolbar: ['filter', 'exports', 'print'],
cols: [[
{type: 'checkbox'},
{field: 'uid', title: '设备UID', width: 180},
{field: 'device_model', title: '设备型号', width: 120},
{field: 'license_code', title: '授权码', width: 180},
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
var types = {
'time': '时间授权',
'count': '次数授权',
'permanent': '永久授权'
};
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: 'last_active_at', title: '最后活跃', width: 160, templet: function(d){
return d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-';
}},
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 250}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
}
});
// 表格工具栏事件
table.on('toolbar(device-table)', function(obj){
switch(obj.event){
case 'refresh':
table.reload('device-table');
break;
}
});
// 行工具栏事件
table.on('tool(device-table)', function(obj){
var data = obj.data;
switch(obj.event){
case 'view':
layer.open({
type: 1,
title: '设备详情',
area: ['600px', '500px'],
content: laytpl($('#deviceDetailTpl').html()).render(data)
});
break;
case 'bind':
layer.prompt({
formType: 0,
value: '',
title: '请输入授权码',
area: ['300px', '150px']
}, function(value, index, elem){
// 绑定授权码
fetch('/api/devices/' + data.uid + '/license', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
license_code: value
})
})
.then(response => response.json())
.then(result => {
if(result.error) {
layer.msg(result.error);
return;
}
layer.msg('绑定成功');
table.reload('device-table');
layer.close(index);
})
.catch(error => {
layer.msg('绑定失败:' + error.message);
});
});
break;
case 'logs':
layer.open({
type: 2,
title: '设备日志',
area: ['800px', '600px'],
content: '/admin/device-logs?uid=' + data.uid
});
break;
case 'revoke':
layer.confirm('确定要撤销该设备的授权吗?', function(index){
fetch('/api/devices/' + data.uid + '/license', {
method: 'DELETE',
credentials: 'include'
})
.then(response => response.json())
.then(result => {
if(result.error) {
layer.msg(result.error);
return;
}
layer.msg('撤销成功');
table.reload('device-table');
})
.catch(error => {
layer.msg('撤销失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 搜索表单提交
form.on('submit(search)', function(data){
table.reload('device-table', {
where: data.field,
page: {
curr: 1
}
});
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);
}
});
});
+223
View File
@@ -0,0 +1,223 @@
layui.use(['table', 'form', 'layer'], function(){
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 初始化表格
table.render({
elem: '#device-table',
url: '/api/devices/models',
headers: undefined,
toolbar: '#tableToolbar',
defaultToolbar: ['filter', 'exports', 'print'],
cols: [[
{type: 'checkbox'},
{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: '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
}) : '';
}},
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
}
});
// 表格工具栏事件
table.on('toolbar(device-table)', function(obj){
var checkStatus = table.checkStatus(obj.config.id);
switch(obj.event){
case 'refresh':
table.reload('device-table');
break;
case 'batchDel':
var data = checkStatus.data;
if(data.length === 0){
layer.msg('请选择要删除的设备型号');
return;
}
layer.confirm('确定删除选中的设备型号吗?', function(index){
var ids = data.map(item => item.id);
fetch('/api/devices/models/batch', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({ids: ids})
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('批量删除成功');
table.reload('device-table');
})
.catch(error => {
layer.msg('批量删除失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 行工具栏事件
table.on('tool(device-table)', function(obj){
var data = obj.data;
switch(obj.event){
case 'edit':
layer.open({
type: 1,
title: '编辑设备型号',
area: ['500px', '400px'],
content: $('#deviceFormTpl').html(),
success: function(){
form.val('deviceForm', data);
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, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('删除成功');
obj.del();
})
.catch(error => {
layer.msg('删除失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 搜索表单提交
form.on('submit(search)', function(data){
table.reload('device-table', {
where: data.field
});
return false;
});
// 添加设备型号按钮点击事件
$('#add-device').on('click', function(){
layer.open({
type: 1,
title: '添加设备型号',
area: ['500px', '400px'],
content: $('#deviceFormTpl').html(),
success: function(){
// 初始化设备类型选择
form.val('deviceForm', {
'device_type': 'software' // 设置默认值
});
form.render();
}
});
});
// 设备型号表单提交
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'
};
// 如果是编辑模式,添加 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';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(submitData)
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.closeAll('page');
layer.msg(data.field.id ? '更新成功' : '添加成功');
table.reload('device-table');
})
.catch(error => {
layer.msg((data.field.id ? '更新' : '添加') + '失败:' + error.message);
});
return false;
});
});
+340
View File
@@ -0,0 +1,340 @@
layui.use(['table', 'form', 'layer'], function(){
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 自定义验证规则
form.verify({
min1: function(value) {
if (value < 1) {
return '必须大于0';
}
}
});
// 初始化表格
table.render({
elem: '#license-table',
url: '/api/licenses',
headers: undefined,
toolbar: '#tableToolbar',
defaultToolbar: ['filter', 'exports', 'print'],
cols: [[
{type: 'checkbox'},
{field: 'code', title: '授权码', width: 320, templet: function(d){
return '<div class="layui-table-cell laytable-cell-1-code">' +
d.code +
'<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="copy" style="margin-left:5px;">复制</a>' +
'</div>';
}},
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
var types = {
'time': '时间授权',
'count': '次数授权',
'permanent': '永久授权'
};
return types[d.license_type.toLowerCase()] || d.license_type;
}},
{field: 'duration', title: '有效期', width: 150, templet: function(d){
if(d.license_type.toLowerCase() === 'time') {
let minutes = d.duration;
if (minutes >= 525600) {
return Math.floor(minutes / 525600) + '年';
} else if (minutes >= 43200) {
return Math.floor(minutes / 43200) + '月';
} else if (minutes >= 1440) {
return Math.floor(minutes / 1440) + '天';
} else if (minutes >= 60) {
return Math.floor(minutes / 60) + '小时';
} else {
return minutes + '分钟';
}
}
return '-';
}},
{field: 'max_uses', title: '使用次数', width: 100, templet: function(d){
if(d.license_type.toLowerCase() === 'count') {
return d.max_uses || 0;
}
return '-';
}},
{field: 'status', title: '状态', width: 100, templet: function(d){
var status = d.status.toLowerCase();
if(status === 'unused') return '<span class="layui-badge layui-bg-green">未使用</span>';
if(status === 'used') return '<span class="layui-badge layui-bg-gray">已使用</span>';
if(status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
if(status === 'revoked') return '<span class="layui-badge layui-bg-red">已撤销</span>';
return '<span class="layui-badge layui-bg-black">未知</span>';
}},
{field: 'used_by', title: '使用设备', width: 180},
{field: 'used_at', title: '使用时间', width: 160, templet: function(d){
return d.used_at ? new Date(d.used_at).toLocaleString() : '-';
}},
{field: 'batch_no', title: '批次号', width: 160},
{field: 'remark', title: '备注'},
{field: 'bind_count', title: '可绑定次数', width: 100, templet: function(d){
if(d.bind_count === -1) return '<span class="layui-badge layui-bg-blue">无限制</span>';
if(d.bind_count === 0) return '<span class="layui-badge layui-bg-gray">已用完</span>';
return d.bind_count;
}},
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180, templet: function(d){
var btns = [
'<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>',
'<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>'
];
// 只有未过期且未撤销的授权码才能撤销
if(d.status.toLowerCase() !== 'expired' && d.status.toLowerCase() !== 'revoked') {
btns.push('<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>');
}
return btns.join('');
}}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return {
"code": res.code,
"msg": res.msg,
"count": res.count,
"data": res.data
};
}
});
// 表格工具栏事件
table.on('toolbar(license-table)', function(obj){
var checkStatus = table.checkStatus(obj.config.id);
switch(obj.event){
case 'refresh':
table.reload('license-table');
break;
case 'copySelected':
var data = checkStatus.data;
if(data.length === 0){
layer.msg('请选择要复制的授权码');
return;
}
// 提取授权码并按格式组织
var codes = data.map(item => item.code);
var copyText = '';
// 弹出选择框
layer.confirm('请选择复制格式', {
btn: ['换行分隔', '逗号分隔']
}, function(index){
// 换行分隔
copyText = codes.join('\n');
copyToClipboard(copyText);
layer.close(index);
}, function(index){
// 逗号分隔
copyText = codes.join(',');
copyToClipboard(copyText);
layer.close(index);
});
break;
case 'batchDel':
var data = checkStatus.data;
if(data.length === 0){
layer.msg('请选择要删除的授权码');
return;
}
layer.confirm('确定撤销选中的授权码吗?', function(index){
var codes = data.map(item => item.code);
fetch('/api/licenses/batch/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({codes: codes})
})
.then(response => response.json())
.then(result => {
if(result.error) {
layer.msg(result.error);
return;
}
layer.msg('批量撤销成功');
table.reload('license-table');
})
.catch(error => {
layer.msg('批量撤销失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 添加复制到剪贴板的函数
function copyToClipboard(text) {
// 创建临时文本区域
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
// 选择文本
textarea.select();
textarea.setSelectionRange(0, 99999); // 兼容移动设备
try {
// 执行复制
var successful = document.execCommand('copy');
if (successful) {
layer.msg('复制成功');
} else {
layer.msg('复制失败,请手动复制');
}
} catch (err) {
layer.msg('复制失败:' + err.message);
}
// 移除临时文本区域
document.body.removeChild(textarea);
}
// 行工具栏事件
table.on('tool(license-table)', function(obj){
var data = obj.data;
switch(obj.event){
case 'view':
layer.alert(JSON.stringify(data, null, 2), {
title: '授权码详情'
});
break;
case 'logs':
layer.open({
type: 2,
title: '使用日志',
area: ['800px', '600px'],
content: '/admin/license-logs?id=' + data.id
});
break;
case 'revoke':
layer.confirm('确定撤销该授权码吗?', function(index){
fetch('/api/licenses/' + data.code + '/revoke', {
method: 'POST',
credentials: 'include'
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('撤销成功');
table.reload('license-table');
})
.catch(error => {
layer.msg('撤销失败:' + error.message);
});
layer.close(index);
});
break;
case 'copy':
copyToClipboard(data.code);
break;
}
});
// 搜索表单提交
form.on('submit(search)', function(data){
// 转换所有字段为小写
Object.keys(data.field).forEach(key => {
if(data.field[key]) {
data.field[key] = data.field[key].toLowerCase();
}
});
table.reload('license-table', {
where: data.field,
page: {
curr: 1
}
});
return false;
});
// 创建授权码按钮点击事件
$('#create-license').on('click', function(){
layer.open({
type: 1,
title: '生成授权码',
area: ['500px', '400px'],
content: $('#createLicenseTpl').html(),
success: function(){
form.render();
}
});
});
// 监听授权类型切换
form.on('select(licenseType)', function(data){
if(data.value === 'time'){
$('#durationItem').show();
$('#maxUsesItem').hide();
} else if(data.value === 'count'){
$('#durationItem').hide();
$('#maxUsesItem').show();
} else {
$('#durationItem').hide();
$('#maxUsesItem').hide();
}
});
// 创建授权码表单提交
form.on('submit(licenseSubmit)', function(data){
var field = data.field;
// 构造请求数据,确保数值类型正确
const submitData = {
license_type: field.license_type.toLowerCase(),
count: parseInt(field.count),
remark: field.remark
};
// 根据授权类型处理参数
if(field.license_type.toLowerCase() === 'time'){
submitData.duration = parseInt(field.duration) * parseInt(field.duration_unit);
delete field.max_uses;
} else if(field.license_type.toLowerCase() === 'count'){
submitData.max_uses = parseInt(field.max_uses);
delete field.duration;
} else {
delete field.duration;
delete field.max_uses;
}
fetch('/api/licenses', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(submitData)
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.closeAll('page');
layer.msg('授权码生成成功');
table.reload('license-table');
})
.catch(error => {
layer.msg('生成失败:' + error.message);
});
return false;
});
});
+127
View File
@@ -0,0 +1,127 @@
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 检查用户是否已登录
function checkIfLoggedIn() {
const token = localStorage.getItem('token');
if (token) {
window.location.href = '/';
}
}
// 页面加载时检查登录状态
checkIfLoggedIn();
// 加载验证码
function loadCaptcha() {
fetch('/api/captcha')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.error) {
layer.msg(data.error);
return;
}
// 确保图片数据正确加载
if (data.imageBase64) {
$('#captchaImg').attr('src', 'data:image/png;base64,' + data.imageBase64);
$('input[name=captchaId]').val(data.captchaId);
} else {
throw new Error('验证码图片数据无效');
}
})
.catch(error => {
layer.msg('获取验证码失败:' + error.message);
// 设置一个默认的错误图片
$('#captchaImg').attr('src', '/static/images/captcha-error.png');
});
}
// 页面加载时获取验证码
loadCaptcha();
// 点击验证码图片刷新
$('#captchaImg').on('click', function() {
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;
}
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();
})
.then(result => {
if (result.error) {
layer.msg(result.error);
loadCaptcha(); // 刷新验证码
return;
}
// 确保 token 被正确设置
document.cookie = `token=${result.token}; path=/; secure; samesite=strict`;
localStorage.setItem('token', result.token);
window.location.href = '/';
})
.catch(error => {
layer.msg('登录失败:' + error.message);
loadCaptcha(); // 刷新验证码
});
return false;
});
// 注册账号点击事件
$('.register').on('click', function(){
layer.open({
type: 2,
title: '注册账号',
area: ['500px', '400px'],
content: '/register.html'
});
});
// 忘记密码点击事件
$('.forget-pwd').on('click', function(){
layer.open({
type: 2,
title: '重置密码',
area: ['500px', '300px'],
content: '/reset-password.html'
});
});
});
+190
View File
@@ -0,0 +1,190 @@
layui.use(["element", "layer"], function () {
var element = layui.element;
var layer = layui.layer;
var $ = layui.$;
// 检查认证状态
function checkAuth() {
try {
// 从 cookie 中获取 token
const token = localStorage.getItem('token');
// console.log(token);
if (!token) {
window.location.href = "/login";
return false;
}
return true;
} catch (error) {
console.error("认证检查失败:", error);
window.location.href = "/login";
return false;
}
}
// 添加通用的 fetch 封装,自动处理认证
window.authFetch = function (url, options = {}) {
return 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;
});
};
// 在页面加载时检查认证
$(document).ready(function () {
if (!checkAuth()) return;
// 加载用户信息
authFetch("/api/users/profile")
.then((user) => {
$("#current-user").text(user.username);
// 根据用户角色显示/隐藏菜单
if (user.role !== "admin") {
$(".admin-only").hide();
}
})
.catch((error) => {
layer.msg("加载用户信息失败:" + error.message);
});
// 加载站点配置
loadSiteConfig();
// 默认加载 dashboard
loadPage("/admin/dashboard", "控制台");
});
// 左侧菜单点击事件
$(".layui-nav-item a").on("click", function () {
var url = $(this).data("url");
if (url) {
var title = $(this).text().trim();
loadPage(url, title);
}
});
// 加载页面内容
function loadPage(url, title) {
// 更新面包屑
updateBreadcrumb(title);
// 加载页面内容
$("#content-frame").attr("src", url);
// 更新选中状态
$(".layui-nav-item").removeClass("layui-this");
$(`a[data-url="${url}"]`).parent().addClass("layui-this");
}
// 更新面包屑导航
function updateBreadcrumb(title) {
var html =
'<a href="javascript:;">首页</a> <span lay-separator="">/</span> ' +
title;
$(".layui-breadcrumb").html(html);
element.render("breadcrumb");
}
// 修改密码
$(".change-password").on("click", function () {
layer.open({
type: 2,
title: "修改密码",
area: ["500px", "300px"],
content: "/admin/change-password",
});
});
// 退出登录
$(".logout").on("click", function () {
layer.confirm("确定要退出登录吗?", function (index) {
// 清除 cookie
document.cookie = "token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
window.location.href = '/login';
layer.close(index);
});
});
// 加载站点配置
function loadSiteConfig() {
authFetch("/api/site/settings")
.then((data) => {
if (data.error) {
layer.msg(data.error);
return;
}
// 更新页面元素
document.title = data.title;
$("#site-title").text(data.title);
$("#site-description").attr("content", data.description);
$("#site-favicon").attr("href", data.favicon);
$("#site-logo").attr("src", data.logo);
$("#site-name").text(data.title);
$("#site-copyright").text(data.copyright);
$("#site-icp").text(data.icp);
})
.catch((error) => {
layer.msg("加载配置失败:" + error.message);
});
}
// 监听子页面消息
window.addEventListener('message', function(event) {
if (event.data.type === 'updateBreadcrumb') {
updateBreadcrumb(event.data.title);
}
});
});
+209
View File
@@ -0,0 +1,209 @@
layui.use(['element', 'layer'], function(){
var element = layui.element;
var layer = layui.layer;
var $ = layui.$;
// 初始化所有图表
var cpuChart = echarts.init(document.getElementById('cpu-chart'));
var memoryChart = echarts.init(document.getElementById('memory-chart'));
var diskChart = echarts.init(document.getElementById('disk-chart'));
var networkChart = echarts.init(document.getElementById('network-chart'));
// 更新系统状态
function updateSystemStatus() {
fetch('/api/monitor/status', {
credentials: 'include'
})
.then(response => {
if (response.status === 401) {
window.location.href = '/login';
throw new Error('认证失败');
}
return response.json();
})
.then(data => {
if (data.error) {
layer.msg(data.error);
return;
}
// 更新基础信息
$('#uptime').text(formatDuration(Math.floor(data.system.uptime / 1e9))); // 转换纳秒为秒
$('#active-users').text(data.system.active_users);
$('#total-devices').text(data.system.total_devices);
$('#load-avg').text(data.cpu.load_avg.map(v => v.toFixed(2)).join(' '));
// 更新系统信息
$('#hostname').text(data.host.hostname);
$('#os').text(data.host.os);
$('#platform').text(data.host.platform);
$('#kernel').text(data.host.kernel_version);
$('#cpu-model').text(data.cpu.model_name);
$('#cpu-cores').text(data.cpu.core_count);
$('#boot-time').text(new Date(data.host.boot_time).toLocaleString());
// 更新进程列表
var processHtml = '';
data.process.list.forEach(function(proc) {
processHtml += `
<tr>
<td>${proc.pid}</td>
<td>${proc.name}</td>
<td>${proc.cpu.toFixed(1)}%</td>
<td>${proc.memory.toFixed(1)}%</td>
<td>${formatDuration(Math.floor((Date.now() - proc.created) / 1000))}</td>
</tr>
`;
});
$('#process-list').html(processHtml);
$('#total-processes').text(data.process.total);
// 更新图表
updateCPUChart(data.cpu);
updateMemoryChart(data.memory);
updateDiskChart(data.disk);
updateNetworkChart(data.network);
})
.catch(error => {
layer.msg('获取系统状态失败:' + error.message);
});
}
// 更新CPU图表
function updateCPUChart(cpu) {
var option = {
title: { text: 'CPU使用率' },
tooltip: { formatter: '{b}: {c}%' },
series: [{
type: 'gauge',
min: 0,
max: 100,
detail: { formatter: '{value}%' },
data: [{ value: cpu.usage.toFixed(1), name: 'CPU' }]
}]
};
cpuChart.setOption(option);
}
// 更新内存图表
function updateMemoryChart(memory) {
var used = (memory.used / 1024 / 1024 / 1024).toFixed(1);
var free = (memory.free / 1024 / 1024 / 1024).toFixed(1);
var option = {
title: { text: '内存使用情况' },
tooltip: { formatter: '{b}: {c}GB ({d}%)' },
series: [{
type: 'pie',
radius: ['50%', '70%'],
data: [
{ value: used, name: '已用内存' },
{ value: free, name: '空闲内存' }
]
}]
};
memoryChart.setOption(option);
}
// 更新磁盘图表
function updateDiskChart(disk) {
var option = {
title: { text: '磁盘使用情况' },
tooltip: {
formatter: function(params) {
var data = params.data;
return `${params.name}<br/>
总空间: ${formatSize(data.total)}<br/>
已用空间: ${formatSize(data.used)}<br/>
剩余空间: ${formatSize(data.free)}<br/>
使用率: ${data.usage_rate.toFixed(1)}%`;
}
},
series: [{
type: 'pie',
radius: '60%',
data: disk.partitions.map(p => ({
name: p.mountpoint,
value: p.usage_rate,
total: p.total,
used: p.used,
free: p.free,
usage_rate: p.usage_rate
}))
}]
};
diskChart.setOption(option);
}
// 更新网络图表
function updateNetworkChart(network) {
var option = {
title: { text: '网络流量' },
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params.map(p =>
`${p.seriesName}: ${formatSize(p.value)}/s`
).join('<br/>');
}
},
legend: { data: ['发送', '接收'] },
xAxis: {
type: 'category',
data: network.interfaces.map(i => i.name)
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value) {
return formatSize(value) + '/s';
}
}
},
series: [{
name: '发送',
type: 'bar',
data: network.interfaces.map(i => i.bytes_sent)
}, {
name: '接收',
type: 'bar',
data: network.interfaces.map(i => i.bytes_recv)
}]
};
networkChart.setOption(option);
}
// 格式化文件大小
function formatSize(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];
}
// 格式化时间
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分钟';
}
// 初始加载
updateSystemStatus();
// 定时更新
setInterval(updateSystemStatus, 5000);
// 窗口大小改变时重绘图表
window.onresize = function(){
cpuChart.resize();
memoryChart.resize();
diskChart.resize();
networkChart.resize();
};
});
+142
View File
@@ -0,0 +1,142 @@
layui.use(['form', 'upload', 'layer'], function(){
var form = layui.form;
var upload = layui.upload;
var layer = layui.layer;
var $ = layui.$;
// 加载当前配置
fetch('/api/site/settings', {
credentials: 'include'
})
.then(response => {
if (response.status === 401) {
window.location.href = '/login';
throw new Error('认证失败');
}
return response.json();
})
.then(data => {
if (data.error) {
layer.msg(data.error);
return;
}
console.log(data);
// 填充表单数据
form.val('siteSettingsForm', {
'title': data.title,
'description': data.description,
'baseUrl': data.base_url,
'icp': data.icp,
'copyright': data.copyright,
'logo': data.logo,
'favicon': data.favicon
});
// 显示当前图片
if (data.logo) {
$('#currentLogo').attr('src', data.logo).show();
}
if (data.favicon) {
$('#currentFavicon').attr('src', data.favicon).show();
}
})
.catch(error => {
layer.msg('加载配置失败:' + error.message);
});
// 上传Logo
upload.render({
elem: '#uploadLogo',
url: '/api/uploads/site',
accept: 'images',
acceptMime: 'image/*',
field: 'file',
before: function() {
layer.load();
},
done: function(res) {
layer.closeAll('loading');
if (res.error) {
layer.msg(res.error);
return;
}
$('input[name=logo]').val(res.url);
$('#currentLogo').attr('src', res.url).show();
layer.msg('Logo上传成功');
},
error: function() {
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 上传Favicon
upload.render({
elem: '#uploadFavicon',
url: '/api/uploads/site',
accept: 'images',
acceptMime: 'image/*',
field: 'file',
before: function() {
layer.load();
},
done: function(res) {
layer.closeAll('loading');
if (res.error) {
layer.msg(res.error);
return;
}
$('input[name=favicon]').val(res.url);
$('#currentFavicon').attr('src', res.url).show();
layer.msg('Favicon上传成功');
},
error: function() {
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 表单提交
form.on('submit(siteSubmit)', function(data){
// 转换字段名以匹配后端
const submitData = {
title: data.field.title,
description: data.field.description,
base_url: data.field.baseUrl,
icp: data.field.icp,
copyright: data.field.copyright,
logo: data.field.logo,
favicon: data.field.favicon
};
fetch('/api/site/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(submitData)
})
.then(response => {
if (response.status === 401) {
window.location.href = '/login';
throw new Error('认证失败');
}
return response.json();
})
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('保存成功');
// 刷新父页面
parent.window.location.reload();
})
.catch(error => {
layer.msg('保存失败:' + error.message);
});
return false;
});
});
+214
View File
@@ -0,0 +1,214 @@
layui.use(["table", "form", "layer"], function () {
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 初始化表格
table.render({
elem: "#token-table",
url: "/api/tokens",
headers: [
{
Authorization: "Bearer " + localStorage.getItem("token"),
},
],
toolbar: "#tableToolbar",
defaultToolbar: ["filter", "exports", "print"],
cols: [
[
{ type: "checkbox" },
{ field: "token", title: "访问令牌", width: 320 },
{ field: "deviceUID", title: "设备UID", width: 180 },
{ field: "type", 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 === "revoked")
return '<span class="layui-badge layui-bg-gray">已撤销</span>';
return '<span class="layui-badge layui-bg-orange">已过期</span>';
},
},
{ field: "expireTime", title: "过期时间", width: 160 },
{ field: "lastUsed", title: "最后使用", width: 160 },
{ field: "usageCount", title: "使用次数", width: 100 },
{ field: "ipList", title: "IP限制", width: 200 },
{ fixed: "right", title: "操作", toolbar: "#tableRowBar", width: 180 },
],
],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
},
});
// 表格工具栏事件
table.on("toolbar(token-table)", function (obj) {
var checkStatus = table.checkStatus(obj.config.id);
switch (obj.event) {
case "refresh":
table.reload("token-table");
break;
case "batchRevoke":
var data = checkStatus.data;
if (data.length === 0) {
layer.msg("请选择要撤销的令牌");
return;
}
layer.confirm("确定撤销选中的令牌吗?", function (index) {
var tokens = data.map((item) => item.token);
// 执行批量撤销
Promise.all(
tokens.map((token) =>
fetch("/api/tokens/" + token, {
method: "DELETE",
headers: {
Authorization: "Bearer " + localStorage.getItem("token"),
},
}).then((response) => response.json())
)
)
.then(() => {
layer.msg("批量撤销成功");
table.reload("token-table");
})
.catch((error) => {
layer.msg("批量撤销失败:" + error.message);
});
layer.close(index);
});
break;
}
});
// 行工具栏事件
table.on("tool(token-table)", function (obj) {
var data = obj.data;
switch (obj.event) {
case "view":
layer.alert(JSON.stringify(data, null, 2), {
title: "令牌详情",
});
break;
case "logs":
layer.open({
type: 2,
title: "令牌使用日志",
area: ["800px", "600px"],
content: "/admin/token-logs.html?id=" + data.id,
});
break;
case "revoke":
layer.confirm("确定撤销该令牌吗?", function (index) {
fetch("/api/tokens/" + data.token, {
method: "DELETE",
headers: {
Authorization: "Bearer " + localStorage.getItem("token"),
},
})
.then((response) => response.json())
.then((result) => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg("撤销成功");
obj.update({
status: "revoked",
});
})
.catch((error) => {
layer.msg("撤销失败:" + error.message);
});
layer.close(index);
});
break;
}
});
// 搜索表单提交
form.on("submit(search)", function (data) {
table.reload("token-table", {
where: data.field,
});
return false;
});
// 创建令牌按钮点击事件
$("#create-token").on("click", function () {
// 加载设备列表
fetch("/api/devices", {
headers: {
Authorization: "Bearer " + localStorage.getItem("token"),
},
})
.then((response) => response.json())
.then((result) => {
var devices = result.data;
var options = devices
.map(
(device) =>
`<option value="${device.uid}">${device.uid} (${device.deviceModel})</option>`
)
.join("");
layer.open({
type: 1,
title: "创建访问令牌",
area: ["500px", "400px"],
content: $("#createTokenTpl").html(),
success: function () {
$("select[name=device_uid]").append(options);
form.render("select");
},
});
})
.catch((error) => {
layer.msg("加载设备列表失败:" + error.message);
});
});
// 创建令牌表单提交
form.on("submit(tokenSubmit)", function (data) {
var ipList = data.field.ip_list.split(/[,\s]+/).filter((ip) => ip);
fetch("/api/tokens", {
method: "POST",
headers: {
Authorization: "Bearer " + localStorage.getItem("token"),
"Content-Type": "application/json",
},
body: JSON.stringify({
device_uid: data.field.device_uid,
token_type: data.field.token_type,
expire_days: parseInt(data.field.expire_days),
ip_list: ipList,
}),
})
.then((response) => response.json())
.then((result) => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.closeAll("page");
layer.msg("创建成功");
table.reload("token-table");
})
.catch((error) => {
layer.msg("创建失败:" + error.message);
});
return false;
});
});
+200
View File
@@ -0,0 +1,200 @@
layui.use(['upload', 'element', 'layer'], function(){
var upload = layui.upload;
var element = layui.element;
var layer = layui.layer;
var $ = layui.$;
// 普通文件上传
upload.render({
elem: '#fileUpload',
url: '/api/uploads',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
data: {
device_uid: function() {
return $('#deviceUID').val();
},
description: function() {
return $('#description').val();
}
},
accept: 'file',
before: function(obj) {
layer.load();
},
done: function(res) {
layer.closeAll('loading');
if (res.error) {
layer.msg(res.error);
return;
}
layer.msg('上传成功');
// 刷新文件列表
loadFileList();
},
error: function() {
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 分片上传
upload.render({
elem: '#chunkUpload',
url: '/api/uploads/chunk',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
data: {
deviceUID: function() {
return $('#deviceUID').val();
}
},
accept: 'file',
size: 1024 * 1024 * 2, // 每片2MB
chunked: true,
chunkSize: 1024 * 1024 * 2,
before: function(obj) {
layer.load();
},
progress: function(n) {
var percent = n + '%';
element.progress('uploadProgress', percent);
},
done: function(res) {
layer.closeAll('loading');
if (res.error) {
layer.msg(res.error);
return;
}
// 如果所有分片都上传完成,开始合并
if (res.completed) {
mergeChunks(res.fileHash);
} else {
layer.msg('分片上传成功');
}
},
error: function() {
layer.closeAll('loading');
layer.msg('上传失败');
}
});
// 合并分片
function mergeChunks(fileHash) {
fetch('/api/uploads/merge', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'fileHash=' + fileHash
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('文件合并成功');
// 刷新文件列表
loadFileList();
})
.catch(error => {
layer.msg('文件合并失败:' + error.message);
});
}
// 加载文件列表
function loadFileList() {
var deviceUID = $('#deviceUID').val();
if (!deviceUID) return;
fetch('/api/uploads/device/' + deviceUID, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
renderFileList(result);
})
.catch(error => {
layer.msg('加载文件列表失败:' + error.message);
});
}
// 渲染文件列表
function renderFileList(files) {
var html = '';
files.forEach(file => {
html += `
<tr>
<td>${file.fileName}</td>
<td>${formatFileSize(file.fileSize)}</td>
<td>${file.fileType}</td>
<td>${formatTime(file.createdAt)}</td>
<td>
<a href="javascript:;" class="layui-btn layui-btn-xs" onclick="downloadFile(${file.id})">下载</a>
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" onclick="deleteFile(${file.id})">删除</a>
</td>
</tr>
`;
});
$('#fileList').html(html);
}
// 格式化文件大小
function formatFileSize(size) {
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var index = 0;
while (size >= 1024 && index < units.length - 1) {
size /= 1024;
index++;
}
return size.toFixed(2) + ' ' + units[index];
}
// 格式化时间
function formatTime(time) {
return new Date(time).toLocaleString();
}
// 下载文件
window.downloadFile = function(id) {
window.location.href = '/api/uploads/' + id;
};
// 删除文件
window.deleteFile = function(id) {
layer.confirm('确定删除该文件吗?', function(index) {
fetch('/api/uploads/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('删除成功');
loadFileList();
})
.catch(error => {
layer.msg('删除失败:' + error.message);
});
layer.close(index);
});
};
// 初始加载文件列表
loadFileList();
});
+146
View File
@@ -0,0 +1,146 @@
layui.use(['table', 'form', 'layer'], function(){
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 初始化表格
table.render({
elem: '#user-table',
url: '/api/users',
headers: undefined,
toolbar: '#tableToolbar',
defaultToolbar: ['filter', 'exports', 'print'],
cols: [[
{type: 'checkbox'},
{field: 'username', title: '用户名', width: 150},
{field: 'email', title: '邮箱', width: 200},
{field: 'role', title: '角色', width: 100, templet: '#roleTpl'},
{field: 'lastLogin', title: '最后登录', width: 160},
{field: 'createdAt', title: '创建时间', width: 160},
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 150}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
}
});
// 表格工具栏事件
table.on('toolbar(user-table)', function(obj){
switch(obj.event){
case 'refresh':
table.reload('user-table');
break;
}
});
// 行工具栏事件
table.on('tool(user-table)', function(obj){
var data = obj.data;
switch(obj.event){
case 'edit':
layer.open({
type: 1,
title: '编辑用户',
area: ['500px', '400px'],
content: $('#userFormTpl').html(),
success: function(){
// 填充表单数据
form.val('userForm', {
'id': data.id,
'username': data.username,
'email': data.email,
'role': data.role
});
// 编辑时密码可选
$('input[name=password]').removeAttr('required').removeAttr('lay-verify');
form.render();
}
});
break;
case 'del':
layer.confirm('确定删除该用户吗?', function(index){
fetch('/api/users/' + data.id, {
method: 'DELETE',
credentials: 'include'
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('删除成功');
obj.del();
})
.catch(error => {
layer.msg('删除失败:' + error.message);
});
layer.close(index);
});
break;
}
});
// 搜索表单提交
form.on('submit(search)', function(data){
table.reload('user-table', {
where: data.field
});
return false;
});
// 添加用户按钮点击事件
$('#add-user').on('click', function(){
layer.open({
type: 1,
title: '添加用户',
area: ['500px', '400px'],
content: $('#userFormTpl').html(),
success: function(){
form.render();
}
});
});
// 用户表单提交
form.on('submit(userSubmit)', function(data){
var url = data.field.id ? '/api/users/' + data.field.id : '/api/users';
var method = data.field.id ? 'PUT' : 'POST';
// 如果是编辑且没有输入密码,则删除密码字段
if (data.field.id && !data.field.password) {
delete data.field.password;
}
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(data.field)
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.closeAll('page');
layer.msg(data.field.id ? '更新成功' : '添加成功');
table.reload('user-table');
})
.catch(error => {
layer.msg((data.field.id ? '更新' : '添加') + '失败:' + error.message);
});
return false;
});
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+45
View File
File diff suppressed because one or more lines are too long
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>修改密码</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" lay-filter="changePasswordForm">
<div class="layui-form-item">
<label class="layui-form-label">原密码</label>
<div class="layui-input-block">
<input type="password" name="old_password" required lay-verify="required"
placeholder="请输入原密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<div class="layui-input-block">
<input type="password" name="newPassword" required lay-verify="required|password"
placeholder="请输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" name="confirmPassword" required lay-verify="required|confirmPassword"
placeholder="请再次输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="passwordSubmit">修改</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
// 自定义验证规则
form.verify({
password: [
/^[\S]{6,12}$/,
'密码必须6到12位,且不能出现空格'
],
confirmPassword: function(value) {
var password = $('input[name=newPassword]').val();
if (value !== password) {
return '两次输入的密码不一致';
}
}
});
// 表单提交
form.on('submit(passwordSubmit)', function(data){
fetch('/api/users/change-password', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify({
old_password: data.field.oldPassword,
new_password: data.field.newPassword
})
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('密码修改成功,请重新登录');
setTimeout(function(){
localStorage.removeItem('token');
top.location.href = '/login.html';
}, 1500);
})
.catch(error => {
layer.msg('修改失败:' + error.message);
});
return false;
});
});
</script>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>控制台</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</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>
</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>
</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>
</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>
</div>
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">激活设备</div>
<div class="layui-card-body big-font" id="active-devices">0</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="expired-devices">0</div>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/dashboard.js"></script>
</body>
</html>
+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>设备文件管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>设备文件管理</span>
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm" id="uploadFile">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="uploadUpdate">
<i class="layui-icon">&#xe67c;</i>上传更新
</button>
</div>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">设备型号</label>
<div class="layui-input-inline">
<select name="deviceModel" lay-search>
<option value="">全部</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">文件类型</label>
<div class="layui-input-inline">
<select name="fileType">
<option value="">全部</option>
<option value="update">更新文件</option>
<option value="normal">普通文件</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
</div>
</div>
</form>
<!-- 文件列表 -->
<table class="layui-table">
<thead>
<tr>
<th>文件名</th>
<th>设备型号</th>
<th>版本</th>
<th>类型</th>
<th>大小</th>
<th>下载次数</th>
<th>上传时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="fileList"></tbody>
</table>
</div>
</div>
</div>
<!-- 上传表单模板 -->
<script type="text/html" id="uploadFormTpl">
<form class="layui-form" style="padding: 20px;">
<div class="layui-form-item">
<label class="layui-form-label">设备型号</label>
<div class="layui-input-block">
<select name="deviceModel" lay-verify="required" lay-search>
<option value="">请选择设备型号</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文件版本</label>
<div class="layui-input-block">
<input type="text" name="version" required lay-verify="required"
placeholder="请输入文件版本" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文件描述</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入文件描述"
class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item" id="updateOptions" style="display:none;">
<label class="layui-form-label">更新选项</label>
<div class="layui-input-block">
<input type="checkbox" name="forceUpdate" title="强制更新">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">选择文件</label>
<div class="layui-input-block">
<button type="button" class="layui-btn" id="selectFile">
<i class="layui-icon">&#xe67c;</i>选择文件
</button>
<div class="layui-inline layui-word-aux" id="selectedFile"></div>
</div>
</div>
</form>
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/upload.js"></script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>设备授权管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>设备授权管理</span>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">设备UID</label>
<div class="layui-input-inline">
<input type="text" name="uid" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">设备型号</label>
<div class="layui-input-inline">
<select name="device_model" lay-search>
<option value="">全部</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">授权状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">全部</option>
<option value="active">正常</option>
<option value="expired">已过期</option>
<option value="inactive">未激活</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
<button type="button" class="layui-btn layui-btn-primary" id="export-devices">
<i class="layui-icon">&#xe67d;</i> 导出
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="device-table" lay-filter="device-table"></table>
</div>
</div>
</div>
<!-- 表格工具栏模板 -->
<script type="text/html" id="tableToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="refresh">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
</div>
</script>
<!-- 行工具栏模板 -->
<script type="text/html" id="tableRowBar">
<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'){ }}
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
{{# } }}
</script>
<!-- 设备详情模板 -->
<script type="text/html" id="deviceDetailTpl">
<div style="padding: 20px;">
<table class="layui-table" lay-skin="line">
<colgroup>
<col width="150">
<col>
</colgroup>
<tbody>
<tr>
<td>设备UID</td>
<td>{{ d.uid }}</td>
</tr>
<tr>
<td>设备型号</td>
<td>{{ d.device_model }}</td>
</tr>
<tr>
<td>授权码</td>
<td>{{ d.license_code || '-' }}</td>
</tr>
<tr>
<td>授权类型</td>
<td>{{ d.license_type || '-' }}</td>
</tr>
<tr>
<td>过期时间</td>
<td>{{ d.expire_time ? new Date(d.expire_time).toLocaleString() : '-' }}</td>
</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>
</tr>
<tr>
<td>注册时间</td>
<td>{{ new Date(d.register_time).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/device-license.js"></script>
</body>
</html>
+213
View File
@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>设备管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css" />
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>设备型号管理</span>
<button
class="layui-btn layui-btn-sm layui-btn-normal"
id="add-device"
>
<i class="layui-icon">&#xe654;</i> 添加设备型号
</button>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">设备型号</label>
<div class="layui-input-inline">
<input
type="text"
name="deviceModel"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">设备类型</label>
<div class="layui-input-inline">
<select name="deviceType">
<option value="">全部</option>
<option value="软件">软件</option>
<option value="网站">网站</option>
<option value="嵌入式设备">嵌入式设备</option>
<option value="单片机设备">单片机设备</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">所属公司</label>
<div class="layui-input-inline">
<input
type="text"
name="company"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
<button
type="button"
class="layui-btn layui-btn-primary"
id="export-devices"
>
<i class="layui-icon">&#xe67d;</i> 导出
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="device-table" lay-filter="device-table"></table>
</div>
</div>
</div>
<!-- 表格工具栏模板 -->
<script type="text/html" id="tableToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="refresh">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
<button
class="layui-btn layui-btn-sm layui-btn-danger"
lay-event="batchDel"
>
<i class="layui-icon">&#xe640;</i> 批量删除
</button>
</div>
</script>
<!-- 行工具栏模板 -->
<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">
<input type="hidden" name="id" />
<div class="layui-form-item">
<label class="layui-form-label">设备型号</label>
<div class="layui-input-block">
<input
type="text"
name="model_name"
required
lay-verify="required"
placeholder="请输入设备型号"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">设备类型</label>
<div class="layui-input-block">
<select name="device_type" required lay-verify="required">
<option value="software">软件</option>
<option value="website">网站</option>
<option value="embedded">嵌入式设备</option>
<option value="mcu">单片机设备</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">所属公司</label>
<div class="layui-input-block">
<input
type="text"
name="company"
placeholder="请输入所属公司"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注说明</label>
<div class="layui-input-block">
<textarea
name="remark"
placeholder="请输入备注说明"
class="layui-textarea"
></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="deviceSubmit">
提交
</button>
<button type="reset" class="layui-btn layui-btn-primary">
重置
</button>
</div>
</div>
</form>
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/devices.js"></script>
</body>
</html>
+172
View File
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>授权码操作日志</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>授权码操作日志</span>
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm" id="refresh-logs">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="export-logs">
<i class="layui-icon">&#xe67d;</i> 导出
</button>
</div>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">操作类型</label>
<div class="layui-input-inline">
<select name="action">
<option value="">全部</option>
<option value="create">创建</option>
<option value="use">使用</option>
<option value="verify">验证</option>
<option value="revoke">撤销</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">全部</option>
<option value="success">成功</option>
<option value="failed">失败</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">设备UID</label>
<div class="layui-input-inline">
<input type="text" name="device_uid" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="log-table" lay-filter="log-table"></table>
</div>
</div>
</div>
<!-- 操作类型模板 -->
<script type="text/html" id="actionTpl">
{{# var types = {
'create': '<span class="layui-badge layui-bg-blue">创建</span>',
'use': '<span class="layui-badge layui-bg-green">使用</span>',
'verify': '<span class="layui-badge layui-bg-orange">验证</span>',
'revoke': '<span class="layui-badge layui-bg-red">撤销</span>'
}; }}
{{# return types[d.action] || d.action; }}
</script>
<!-- 状态模板 -->
<script type="text/html" id="statusTpl">
{{# if(d.status === 'success'){ }}
<span class="layui-badge layui-bg-green">成功</span>
{{# } else { }}
<span class="layui-badge layui-bg-red">失败</span>
{{# } }}
</script>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['table', 'layer', 'form'], function(){
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var $ = layui.$;
// 获取授权码ID
var licenseId = location.search.match(/id=(\d+)/)[1];
// 初始化表格
var tableIns = table.render({
elem: '#log-table',
url: '/api/licenses/' + licenseId + '/logs',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
cols: [[
{field: 'action', title: '操作类型', width: 100, templet: '#actionTpl', sort: true},
{field: 'device_uid', title: '设备UID', width: 180},
{field: 'ip', title: 'IP地址', width: 150},
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
{field: 'message', title: '详细信息'},
{field: 'created_at', title: '操作时间', width: 180, sort: true, templet: function(d){
return new Date(d.created_at).toLocaleString();
}}
]],
page: true,
parseData: function(res) {
if (res.code === 401) {
window.location.href = '/login';
return;
}
return res;
}
});
// 搜索表单提交
form.on('submit(search)', function(data){
tableIns.reload({
where: data.field,
page: {
curr: 1
}
});
return false;
});
// 刷新按钮点击事件
$('#refresh-logs').on('click', function(){
tableIns.reload();
});
// 导出按钮点击事件
$('#export-logs').on('click', function(){
var loadIndex = layer.load(2);
fetch('/api/licenses/' + licenseId + '/logs?export=1', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.blob())
.then(blob => {
layer.close(loadIndex);
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'license_logs_' + licenseId + '_' +
new Date().toISOString().slice(0,19).replace(/[-:]/g, '') + '.csv';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(error => {
layer.close(loadIndex);
layer.msg('导出失败:' + error.message);
});
});
});
</script>
</body>
</html>
+263
View File
@@ -0,0 +1,263 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>授权码管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css" />
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>授权码管理</span>
<button
class="layui-btn layui-btn-sm layui-btn-normal"
id="create-license"
>
<i class="layui-icon">&#xe654;</i> 生成授权码
</button>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">授权码</label>
<div class="layui-input-inline">
<input
type="text"
name="code"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">授权类型</label>
<div class="layui-input-inline">
<select name="license_type">
<option value="">全部</option>
<option value="time">时间授权</option>
<option value="count">次数授权</option>
<option value="permanent">永久授权</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">全部</option>
<option value="unused">未使用</option>
<option value="used">已使用</option>
<option value="expired">已过期</option>
<option value="revoked">已撤销</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">批次号</label>
<div class="layui-input-inline">
<input
type="text"
name="batch_no"
autocomplete="off"
class="layui-input"
/>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
<button
type="button"
class="layui-btn layui-btn-primary"
id="export-licenses"
>
<i class="layui-icon">&#xe67d;</i> 导出
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="license-table" lay-filter="license-table"></table>
</div>
</div>
</div>
<!-- Layui 表格工具栏模板 -->
<script type="text/html" id="tableToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="refresh">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
<button
class="layui-btn layui-btn-sm layui-btn-normal"
lay-event="copySelected"
>
<i class="layui-icon">&#xe64c;</i> 复制选中
</button>
<button
class="layui-btn layui-btn-sm layui-btn-danger"
lay-event="batchDel"
>
<i class="layui-icon">&#xe640;</i> 批量删除
</button>
</div>
</script>
<!-- 行工具栏模板 -->
<script id="tableRowBar" type="text/html">
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
{{# if(d.status === 'unused'){ }}
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke"
>撤销</a
>
{{# } }}
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/licenses.js"></script>
<!-- 在页面底部添加创建授权码表单模板 -->
<script type="text/html" id="createLicenseTpl">
<form class="layui-form" style="padding: 20px;" lay-filter="licenseForm">
<div class="layui-form-item">
<label class="layui-form-label">授权类型</label>
<div class="layui-input-block">
<select
name="license_type"
lay-verify="required"
lay-filter="licenseType"
>
<option value="time">时间授权</option>
<option value="count">次数授权</option>
<option value="permanent">永久授权</option>
</select>
</div>
</div>
<div class="layui-form-item" id="durationItem">
<label class="layui-form-label">有效期</label>
<div class="layui-input-inline" style="width: 150px;">
<input
type="number"
name="duration"
class="layui-input"
placeholder="请输入数值"
/>
</div>
<div class="layui-input-inline" style="width: 100px;">
<select name="duration_unit">
<option value="1">分钟</option>
<option value="60">小时</option>
<option value="1440"></option>
<option value="43200"></option>
<option value="525600"></option>
</select>
</div>
</div>
<div class="layui-form-item" id="maxUsesItem" style="display:none;">
<label class="layui-form-label">使用次数</label>
<div class="layui-input-block">
<input
type="number"
name="max_uses"
class="layui-input"
placeholder="请输入最大使用次数"
/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">生成数量</label>
<div class="layui-input-block">
<input
type="number"
name="count"
required
lay-verify="required|number|min1"
value="1"
class="layui-input"
/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea
name="remark"
placeholder="请输入备注信息"
class="layui-textarea"
></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="licenseSubmit">
生成
</button>
<button type="reset" class="layui-btn layui-btn-primary">
重置
</button>
</div>
</div>
</form>
</script>
</body>
</html>
+325
View File
@@ -0,0 +1,325 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>系统监控</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</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="uptime">
0天0小时0分钟
</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="active-users">
0
</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-devices">
0
</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="load-avg">
0.00
</div>
</div>
</div>
</div>
<!-- 资源监控 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">CPU使用率</div>
<div class="layui-card-body">
<div id="cpu-chart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">内存使用情况</div>
<div class="layui-card-body">
<div id="memory-chart" style="height: 300px;"></div>
</div>
</div>
</div>
</div>
<!-- 磁盘和网络 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">磁盘使用情况</div>
<div class="layui-card-body">
<div id="disk-chart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">网络流量</div>
<div class="layui-card-body">
<div id="network-chart" style="height: 300px;"></div>
</div>
</div>
</div>
</div>
<!-- 进程和系统信息 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>进程列表(Top 10</span>
<span class="layui-badge layui-bg-blue" id="total-processes">0</span>
</div>
<div class="layui-card-body">
<table class="layui-table" lay-skin="line">
<thead>
<tr>
<th>PID</th>
<th>名称</th>
<th>CPU使用率</th>
<th>内存使用率</th>
<th>运行时间</th>
</tr>
</thead>
<tbody id="process-list"></tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">系统信息</div>
<div class="layui-card-body">
<table class="layui-table" lay-skin="nob">
<tbody>
<tr>
<td>主机名</td>
<td id="hostname"></td>
</tr>
<tr>
<td>操作系统</td>
<td id="os"></td>
</tr>
<tr>
<td>平台</td>
<td id="platform"></td>
</tr>
<tr>
<td>内核版本</td>
<td id="kernel"></td>
</tr>
<tr>
<td>CPU型号</td>
<td id="cpu-model"></td>
</tr>
<tr>
<td>CPU核心数</td>
<td id="cpu-cores"></td>
</tr>
<tr>
<td>启动时间</td>
<td id="boot-time"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script src="/static/lib/echarts.min.js"></script>
<script src="/static/js/monitor.js"></script>
<script type="text/html" id="processListTpl">
{{# layui.each(d, function(index, item){ }}
<tr>
<td>{{item.pid}}</td>
<td>{{item.name}}</td>
<td>{{item.cpu.toFixed(1)}}%</td>
<td>{{item.memory.toFixed(1)}}%</td>
<td>{{formatDuration(Math.floor((Date.now() - item.created) / 1000))}}</td>
</tr>
{{# }); }}
</script>
</body>
</html>
+185
View File
@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>站点设置</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">站点设置</div>
<div class="layui-card-body">
<form class="layui-form" lay-filter="siteSettingsForm">
<div class="layui-form-item">
<label class="layui-form-label">站点标题</label>
<div class="layui-input-block">
<input type="text" name="title" required lay-verify="required"
placeholder="请输入站点标题" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">站点描述</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入站点描述"
class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">基础URL</label>
<div class="layui-input-block">
<input type="text" name="baseUrl" required lay-verify="required"
placeholder="请输入基础URL" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">ICP备案号</label>
<div class="layui-input-block">
<input type="text" name="icp" placeholder="请输入ICP备案号"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">版权信息</label>
<div class="layui-input-block">
<input type="text" name="copyright" required lay-verify="required"
placeholder="请输入版权信息" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Logo</label>
<div class="layui-input-inline">
<input type="text" name="logo" placeholder="Logo路径"
autocomplete="off" class="layui-input">
</div>
<div class="layui-input-inline" style="width: auto;">
<button type="button" class="layui-btn" id="uploadLogo">
<i class="layui-icon">&#xe67c;</i>上传Logo
</button>
</div>
<div class="layui-input-inline" style="width: auto;">
<img id="currentLogo" src="" style="display:none;max-height:38px;margin-left:10px;">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Favicon</label>
<div class="layui-input-inline">
<input type="text" name="favicon" placeholder="Favicon路径"
autocomplete="off" class="layui-input">
</div>
<div class="layui-input-inline" style="width: auto;">
<button type="button" class="layui-btn" id="uploadFavicon">
<i class="layui-icon">&#xe67c;</i>上传Favicon
</button>
</div>
<div class="layui-input-inline" style="width: auto;">
<img id="currentFavicon" src="" style="display:none;max-height:38px;margin-left:10px;">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="siteSubmit">保存设置</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['form', 'upload', 'layer'], function(){
var form = layui.form;
var upload = layui.upload;
var layer = layui.layer;
var $ = layui.$;
// 加载当前配置
fetch('/api/site/settings', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
layer.msg(data.error);
return;
}
form.val('siteForm', data);
})
.catch(error => {
layer.msg('加载配置失败:' + error.message);
});
// 上传Logo
upload.render({
elem: '#uploadLogo',
url: '/api/uploads/site',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
accept: 'images',
done: function(res){
if (res.error) {
layer.msg(res.error);
return;
}
$('input[name=logo]').val(res.url);
}
});
// 上传Favicon
upload.render({
elem: '#uploadFavicon',
url: '/api/uploads/site',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
accept: 'images',
done: function(res){
if (res.error) {
layer.msg(res.error);
return;
}
$('input[name=favicon]').val(res.url);
}
});
// 表单提交
form.on('submit(siteSubmit)', function(data){
fetch('/api/site/settings', {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify(data.field)
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('保存成功');
})
.catch(error => {
layer.msg('保存失败:' + error.message);
});
return false;
});
});
</script>
</body>
</html>
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>令牌使用日志</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">令牌使用日志</div>
<div class="layui-card-body">
<table id="log-table" lay-filter="log-table"></table>
</div>
</div>
</div>
<!-- 状态模板 -->
<script type="text/html" id="statusTpl">
{{# if(d.status === 'success'){ }}
<span class="layui-badge layui-bg-green">成功</span>
{{# } else { }}
<span class="layui-badge layui-bg-red">失败</span>
{{# } }}
</script>
<!-- 操作类型模板 -->
<script type="text/html" id="actionTpl">
{{# var types = {
'create': '创建',
'use': '使用',
'revoke': '撤销'
}; }}
{{# var type = types[d.action] || d.action; }}
<span>{{type}}</span>
</script>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['table', 'layer'], function(){
var table = layui.table;
var layer = layui.layer;
var $ = layui.$;
// 获取令牌ID
var tokenId = location.search.match(/id=(\d+)/)[1];
// 初始化表格
table.render({
elem: '#log-table',
url: '/api/tokens/' + tokenId + '/logs',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
cols: [[
{field: 'action', title: '操作类型', width: 100, templet: '#actionTpl'},
{field: 'ip', title: 'IP地址', width: 150},
{field: 'userAgent', title: 'User-Agent', width: 300},
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
{field: 'message', title: '详细信息'},
{field: 'createdAt', title: '时间', width: 160}
]],
page: true
});
});
</script>
</body>
</html>
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>访问令牌管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>访问令牌管理</span>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="create-token">
<i class="layui-icon">&#xe654;</i> 创建令牌
</button>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">设备UID</label>
<div class="layui-input-inline">
<input type="text" name="device_uid" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">令牌类型</label>
<div class="layui-input-inline">
<select name="token_type">
<option value="">全部</option>
<option value="api">API</option>
<option value="device">设备</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">全部</option>
<option value="active">有效</option>
<option value="revoked">已撤销</option>
<option value="expired">已过期</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="token-table" lay-filter="token-table"></table>
</div>
</div>
</div>
<!-- 表格工具栏模板 -->
<script type="text/html" id="tableToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="refresh">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="batchRevoke">
<i class="layui-icon">&#xe640;</i> 批量撤销
</button>
</div>
</script>
<!-- 行工具栏模板 -->
<script type="text/html" id="tableRowBar">
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
[[# if(d.status === 'active'){ ]]
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
[[# } ]]
</script>
<!-- 创建令牌表单模板 -->
<script type="text/html" id="createTokenTpl">
<form class="layui-form" style="padding: 20px;" lay-filter="tokenForm">
<div class="layui-form-item">
<label class="layui-form-label">设备UID</label>
<div class="layui-input-block">
<select name="device_uid" lay-verify="required" lay-search>
<option value="">请选择设备</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">令牌类型</label>
<div class="layui-input-block">
<select name="token_type" lay-verify="required">
<option value="api">API</option>
<option value="device">设备</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">有效期</label>
<div class="layui-input-block">
<input type="number" name="expire_days" required lay-verify="required|number"
placeholder="请输入有效期天数" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">IP限制</label>
<div class="layui-input-block">
<textarea name="ip_list" placeholder="请输入允许访问的IP地址,多个IP用逗号分隔"
class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="tokenSubmit">创建</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/tokens.js"></script>
</body>
</html>
+126
View File
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>编辑用户</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" lay-filter="userForm">
<input type="hidden" name="id">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" required lay-verify="required"
placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block">
<input type="text" name="email" required lay-verify="required|email"
placeholder="请输入邮箱" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">角色</label>
<div class="layui-input-block">
<select name="role" required lay-verify="required">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="userSubmit">保存</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 获取用户ID
var userId = location.search.match(/id=(\d+)/);
if (userId) {
userId = userId[1];
// 加载用户数据
fetch('/api/users/' + userId, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
layer.msg(data.error);
return;
}
// 填充表单
form.val('userForm', {
'id': data.id,
'username': data.username,
'email': data.email,
'role': data.role
});
})
.catch(error => {
layer.msg('加载用户数据失败:' + error.message);
});
}
// 表单提交
form.on('submit(userSubmit)', function(data){
var field = data.field;
var url = userId ? '/api/users/' + userId : '/api/users';
var method = userId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify(field)
})
.then(response => response.json())
.then(result => {
if (result.error) {
layer.msg(result.error);
return;
}
layer.msg('保存成功');
// 如果是在弹窗中,则关闭弹窗并刷新父页面的表格
var index = parent.layer.getFrameIndex(window.name);
if (index) {
parent.layui.table.reload('user-table');
parent.layer.close(index);
}
})
.catch(error => {
layer.msg('保存失败:' + error.message);
});
return false;
});
});
</script>
</body>
</html>
+124
View File
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>用户管理</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
<span>用户管理</span>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="add-user">
<i class="layui-icon">&#xe654;</i> 添加用户
</button>
</div>
<div class="layui-card-body">
<!-- 搜索表单 -->
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">用户名</label>
<div class="layui-input-inline">
<input type="text" name="username" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">角色</label>
<div class="layui-input-inline">
<select name="role">
<option value="">全部</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">
<i class="layui-icon">&#xe615;</i> 搜索
</button>
</div>
</div>
</form>
<!-- 数据表格 -->
<table id="user-table" lay-filter="user-table"></table>
</div>
</div>
</div>
<!-- 表格工具栏模板 -->
<script type="text/html" id="tableToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="refresh">
<i class="layui-icon">&#xe669;</i> 刷新
</button>
</div>
</script>
<!-- 角色模板 -->
<script type="text/html" id="roleTpl">
{{# if(d.role === 'admin'){ }}
<span class="layui-badge layui-bg-blue">管理员</span>
{{# } else { }}
<span class="layui-badge layui-bg-gray">普通用户</span>
{{# } }}
</script>
<!-- 行工具栏模板 -->
<script type="text/html" id="tableRowBar">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
{{# if(d.role !== 'admin'){ }}
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
{{# } }}
</script>
<!-- 用户表单模板 -->
<script type="text/html" id="userFormTpl">
<form class="layui-form" style="padding: 20px;" lay-filter="userForm">
<input type="hidden" name="id">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" required lay-verify="required"
placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="password" required lay-verify="required"
placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block">
<input type="text" name="email" required lay-verify="required|email"
placeholder="请输入邮箱" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">角色</label>
<div class="layui-input-block">
<select name="role" required lay-verify="required">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="userSubmit">提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</script>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/users.js"></script>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>授权验证管理平台</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<!-- 头部 -->
<div class="layui-header">
<div class="layui-logo">
<img src="/static/images/logo.png" alt="logo" id="site-logo">
<span id="site-name">授权验证管理平台</span>
</div>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;">
<span id="current-user"></span>
</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;" class="change-password">修改密码</a></dd>
<dd><a href="javascript:;" class="logout">退出登录</a></dd>
</dl>
</li>
</ul>
</div>
<!-- 左侧导航 -->
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<ul class="layui-nav layui-nav-tree">
<li class="layui-nav-item layui-this">
<a href="javascript:;" data-url="/admin/dashboard">
<i class="layui-icon">&#xe68e;</i> 控制台
</a>
</li>
<li class="layui-nav-item">
<a href="javascript:;">
<i class="layui-icon">&#xe665;</i> 设备管理
</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;" data-url="/admin/devices">设备型号管理</a></dd>
<dd><a href="javascript:;" data-url="/admin/device-files">设备文件管理</a></dd>
<dd><a href="javascript:;" data-url="/admin/device-license">设备授权管理</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">
<i class="layui-icon">&#xe672;</i> 授权管理
</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;" data-url="/admin/licenses">授权码管理</a></dd>
<dd><a href="javascript:;" data-url="/admin/license-logs">授权日志</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">
<i class="layui-icon">&#xe674;</i> 令牌管理
</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;" data-url="/admin/tokens">访问令牌</a></dd>
<dd><a href="javascript:;" data-url="/admin/token-logs">令牌日志</a></dd>
</dl>
</li>
<li class="layui-nav-item admin-only">
<a href="javascript:;" data-url="/admin/users">
<i class="layui-icon">&#xe770;</i> 用户管理
</a>
</li>
<li class="layui-nav-item">
<a href="javascript:;" data-url="/admin/monitor">
<i class="layui-icon">&#xe665;</i> 系统监控
</a>
</li>
<li class="layui-nav-item admin-only">
<a href="javascript:;" data-url="/admin/site-settings">
<i class="layui-icon">&#xe716;</i> 站点设置
</a>
</li>
</ul>
</div>
</div>
<!-- 内容主体 -->
<div class="layui-body">
<div class="layui-breadcrumb" lay-separator="/">
<a href="javascript:;">首页</a>
<a><cite>控制台</cite></a>
</div>
<div class="content-container">
<iframe id="content-frame" frameborder="0" class="layadmin-iframe"></iframe>
</div>
</div>
<!-- 底部 -->
<div class="layui-footer">
<div class="footer-left">
<span id="site-copyright"></span>
</div>
<div class="footer-right">
<span id="site-icp"></span>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>登录 - 授权验证管理平台</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/style.css">
</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>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script src="/static/js/login.js"></script>
</body>
</html>