first commit
This commit is contained in:
41
web/static/js/dashboard.js
Normal file
41
web/static/js/dashboard.js
Normal 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
web/static/js/device-files.js
Normal file
198
web/static/js/device-files.js
Normal 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
web/static/js/device-license.js
Normal file
202
web/static/js/device-license.js
Normal 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
web/static/js/devices.js
Normal file
223
web/static/js/devices.js
Normal 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
web/static/js/licenses.js
Normal file
340
web/static/js/licenses.js
Normal 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
web/static/js/login.js
Normal file
127
web/static/js/login.js
Normal 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
web/static/js/main.js
Normal file
190
web/static/js/main.js
Normal 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
web/static/js/monitor.js
Normal file
209
web/static/js/monitor.js
Normal 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
web/static/js/site-settings.js
Normal file
142
web/static/js/site-settings.js
Normal 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
web/static/js/tokens.js
Normal file
214
web/static/js/tokens.js
Normal 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
web/static/js/upload.js
Normal file
200
web/static/js/upload.js
Normal 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
web/static/js/users.js
Normal file
146
web/static/js/users.js
Normal 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;
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user