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

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

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

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
web/static/js/devices.js Normal file
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
web/static/js/licenses.js Normal file
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
web/static/js/login.js Normal file
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
web/static/js/main.js Normal file
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
web/static/js/monitor.js Normal file
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();
};
});

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
web/static/js/tokens.js Normal file
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
web/static/js/upload.js Normal file
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
web/static/js/users.js Normal file
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;
});
});