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