mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
更新底层架构
This commit is contained in:
@@ -1,160 +1,181 @@
|
||||
{{ define "dashboard.html" }}
|
||||
<section>
|
||||
<h2>系统信息</h2>
|
||||
<div class="layui-row layui-col-space15" style="margin-top:12px">
|
||||
<!-- 系统信息面板 -->
|
||||
<div class="layui-col-md8">
|
||||
<div class="layui-panel">
|
||||
<div style="padding: 20px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统信息</h3>
|
||||
<table class="layui-table" lay-skin="nob">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 120px; font-weight: bold;">程序版本</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">v{{ .Version }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">存储方案</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span class="layui-badge layui-bg-cyan" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .DBType }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">开发模式</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
{{ if .Mode }}
|
||||
<span class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">开启</span>
|
||||
{{ else }}
|
||||
<span class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">关闭</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">运行时长</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="uptime-display" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .Uptime }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用统计面板 -->
|
||||
<div class="layui-col-md4">
|
||||
<div class="layui-panel">
|
||||
<div style="padding: 20px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">应用统计</h3>
|
||||
<table class="layui-table" lay-skin="nob">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 120px; font-weight: bold;">全部应用</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="total-apps" class="layui-badge" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">启用应用</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="enabled-apps" class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">禁用应用</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="disabled-apps" class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">变量数量</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="total-variables" class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
// 仪表盘统计脚本(采用箭头函数与中文注释)
|
||||
layui.use(['layer', 'util'], function () {
|
||||
const layer = layui.layer;
|
||||
const util = layui.util;
|
||||
const $ = layui.$;
|
||||
|
||||
// 全局引用:ECharts CDN 地址
|
||||
const echartsCdn = 'https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js';
|
||||
|
||||
// 工具函数:加载 ECharts 库(若已加载则直接回调)
|
||||
// 功能:通过全局的 loadScript 方法按需加载图表库,避免重复加载
|
||||
const ensureECharts = (cb) => {
|
||||
if (window.echarts) { cb && cb(); return; }
|
||||
if (typeof loadScript === 'function') {
|
||||
loadScript(echartsCdn, () => cb && cb());
|
||||
} else {
|
||||
// 兜底:直接插入 <script>
|
||||
const s = document.createElement('script');
|
||||
s.src = echartsCdn;
|
||||
s.onload = () => cb && cb();
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
};
|
||||
|
||||
// 函数:刷新基本信息和运行状态
|
||||
// 说明:请求后台获取最新的系统信息并更新页面显示
|
||||
const refreshSystemInfo = () => {
|
||||
$.get('/admin/api/system/info', (res) => {
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const data = res.data;
|
||||
// 更新运行时长,保持徽章样式
|
||||
if (data.uptime) {
|
||||
const uptimeElement = $('#uptime-display');
|
||||
uptimeElement.text(data.uptime);
|
||||
// 确保徽章样式保持一致
|
||||
if (!uptimeElement.hasClass('layui-badge')) {
|
||||
uptimeElement.addClass('layui-badge layui-bg-gray');
|
||||
uptimeElement.css({
|
||||
'font-size': '14px',
|
||||
'padding': '2px 8px',
|
||||
'line-height': '1.2'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}).fail(() => {
|
||||
console.log('获取系统信息失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 函数:刷新应用统计数据
|
||||
// 说明:请求后台获取应用统计信息并更新页面显示
|
||||
const refreshAppStats = () => {
|
||||
$.get('/admin/api/dashboard/stats', (res) => {
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const data = res.data;
|
||||
$('#total-apps').text(data.total_apps || 0);
|
||||
$('#enabled-apps').text(data.enabled_apps || 0);
|
||||
$('#disabled-apps').text(data.disabled_apps || 0);
|
||||
$('#total-variables').text(data.total_variables || 0);
|
||||
}
|
||||
}).fail(() => {
|
||||
// 显示默认值
|
||||
$('#total-apps').text('0');
|
||||
$('#enabled-apps').text('0');
|
||||
$('#disabled-apps').text('0');
|
||||
$('#total-variables').text('0');
|
||||
});
|
||||
};
|
||||
|
||||
// 立即刷新一次系统信息和应用统计
|
||||
refreshSystemInfo();
|
||||
refreshAppStats();
|
||||
});
|
||||
</script>
|
||||
{{ define "dashboard.html" }}
|
||||
<section>
|
||||
<h2>系统信息</h2>
|
||||
<div class="layui-row layui-col-space15" style="margin-top:12px">
|
||||
<!-- 系统信息面板 -->
|
||||
<div class="layui-col-md8">
|
||||
<div class="layui-panel">
|
||||
<div style="padding: 20px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统信息</h3>
|
||||
<table class="layui-table" lay-skin="nob">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 120px; font-weight: bold;">程序版本</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">v{{ .Version }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">存储方案</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span class="layui-badge layui-bg-cyan" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .DBType }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">开发模式</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
{{ if .Mode }}
|
||||
<span class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">开启</span>
|
||||
{{ else }}
|
||||
<span class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">关闭</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">运行时长</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="uptime-display" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .Uptime }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统统计面板 (预留) -->
|
||||
<div class="layui-col-md4">
|
||||
<div class="layui-panel">
|
||||
<div style="padding: 20px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统统计</h3>
|
||||
<table class="layui-table" lay-skin="nob">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 120px; font-weight: bold;">应用总数</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="total-apps" class="layui-badge" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">启用应用</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="enabled-apps" class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">禁用应用</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="disabled-apps" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold;">变量总数</td>
|
||||
<td style="height: 20px; vertical-align: middle;">
|
||||
<span id="total-variables" class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 20px;">最近登录日志</h2>
|
||||
<div class="layui-panel" style="margin-top:12px">
|
||||
<div style="padding: 20px;">
|
||||
<table id="loginLogsTable" lay-filter="loginLogsTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// 仪表盘脚本
|
||||
layui.use(['jquery', 'table', 'util'], () => {
|
||||
const $ = layui.$;
|
||||
const table = layui.table;
|
||||
const util = layui.util;
|
||||
|
||||
// 刷新基本信息和运行状态
|
||||
const refreshSystemInfo = () => {
|
||||
$.get('/admin/api/system/info', (res) => {
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const data = res.data;
|
||||
if (data.uptime) {
|
||||
const uptimeElement = $('#uptime-display');
|
||||
uptimeElement.text(data.uptime);
|
||||
if (!uptimeElement.hasClass('layui-badge')) {
|
||||
uptimeElement.addClass('layui-badge layui-bg-gray');
|
||||
uptimeElement.css({
|
||||
'font-size': '14px',
|
||||
'padding': '2px 8px',
|
||||
'line-height': '1.2'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}).fail(() => {
|
||||
console.log('获取系统信息失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 刷新系统统计数据
|
||||
const refreshAppStats = () => {
|
||||
$.get('/admin/api/dashboard/stats', (res) => {
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const data = res.data;
|
||||
$('#total-apps').text(data.total_apps || 0);
|
||||
$('#enabled-apps').text(data.enabled_apps || 0);
|
||||
$('#disabled-apps').text(data.disabled_apps || 0);
|
||||
$('#total-variables').text(data.total_variables || 0);
|
||||
}
|
||||
}).fail(() => {
|
||||
$('#total-apps').text('0');
|
||||
$('#enabled-apps').text('0');
|
||||
$('#disabled-apps').text('0');
|
||||
$('#total-variables').text('0');
|
||||
});
|
||||
};
|
||||
|
||||
// 立即刷新一次
|
||||
refreshSystemInfo();
|
||||
refreshAppStats();
|
||||
|
||||
// 渲染登录日志表格
|
||||
table.render({
|
||||
elem: '#loginLogsTable',
|
||||
url: '/admin/api/dashboard/login-logs',
|
||||
page: true,
|
||||
limit: 10,
|
||||
limits: [10, 20, 30, 50],
|
||||
cols: [[
|
||||
{field: 'created_at', title: '登录时间', width: 180, templet: (d) => {
|
||||
return util.toDateString(d.created_at);
|
||||
}},
|
||||
{field: 'username', title: '用户名', width: 150},
|
||||
{field: 'ip', title: '登录IP', width: 150},
|
||||
{field: 'status', title: '状态', width: 100, align: 'center', templet: (d) => {
|
||||
return d.status === 1 ?
|
||||
'<span class="layui-badge layui-bg-green">成功</span>' :
|
||||
'<span class="layui-badge layui-bg-red">失败</span>';
|
||||
}},
|
||||
{field: 'message', title: '详情', minWidth: 150},
|
||||
{field: 'user_agent', title: 'User Agent', minWidth: 200, templet: (d) => {
|
||||
return '<div title="'+d.user_agent+'" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">'+d.user_agent+'</div>';
|
||||
}}
|
||||
]],
|
||||
response: {
|
||||
statusCode: 0
|
||||
},
|
||||
parseData: (res) => {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.msg,
|
||||
"count": res.data ? res.data.total : 0,
|
||||
"data": res.data ? res.data.list : []
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/static/css/admin.css" />
|
||||
<script type="module" src="./static/lib/include.js"></script>
|
||||
<script type="module" src="/static/lib/include.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -48,7 +48,7 @@
|
||||
<a class="" href="javascript:;">系统管理</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a data-path="dashboard" href="javascript:;">仪表盘</a></dd>
|
||||
<dd><a data-path="user" href="javascript:;">个人资料</a></dd>
|
||||
<dd><a data-path="profile" href="javascript:;">个人资料</a></dd>
|
||||
<dd><a data-path="settings" href="javascript:;">系统设置</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
@@ -61,6 +61,13 @@
|
||||
<dd><a data-path="functions" href="javascript:;">公共函数</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">日志审计</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a data-path="login_logs" href="javascript:;">登录日志</a></dd>
|
||||
<dd><a data-path="operation_logs" href="javascript:;">操作日志</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +77,7 @@
|
||||
</div>
|
||||
<div class="layui-footer">{{ .FooterText }}</div>
|
||||
</div>
|
||||
<script type="module" src="./static/js/admin.js"></script>
|
||||
<script type="module" src="/static/js/admin.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,8 +9,7 @@
|
||||
<!-- 站点图标 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- 请勿在项目正式环境中引用该 layui.css 地址 -->
|
||||
<link href="//unpkg.com/layui@2.12.1/dist/css/layui.css" rel="stylesheet">
|
||||
<link href="/static/lib/layui/css/layui.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -206,8 +205,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
|
||||
<script src="//unpkg.com/layui@2.12.1/dist/layui.js"></script>
|
||||
<script src="/static/lib/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(function () {
|
||||
var form = layui.form;
|
||||
@@ -215,8 +213,10 @@
|
||||
|
||||
// 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否
|
||||
form.on('submit(demo-login)', function (data) {
|
||||
var loadIndex = layer.load(1, {
|
||||
shade: [0.1, '#fff']
|
||||
var loadIndex = layer.msg('登录中...', {
|
||||
icon: 16,
|
||||
shade: 0.01,
|
||||
time: 0
|
||||
});
|
||||
|
||||
// 获取CSRF令牌
|
||||
@@ -231,7 +231,15 @@
|
||||
},
|
||||
body: JSON.stringify(data.field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Non-JSON response:', text);
|
||||
throw new Error('服务器响应格式错误');
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
layer.close(loadIndex);
|
||||
|
||||
@@ -256,7 +264,8 @@
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('登录错误:', error);
|
||||
layer.msg('网络错误,请稍后重试', { icon: 2 });
|
||||
var msg = error.message || '网络错误,请稍后重试';
|
||||
layer.msg(msg, { icon: 2 });
|
||||
|
||||
// 网络错误时也刷新验证码
|
||||
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
|
||||
|
||||
171
web/template/admin/login_logs.html
Normal file
171
web/template/admin/login_logs.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{{ define "login_logs.html" }}
|
||||
<section>
|
||||
<h2>登录日志</h2>
|
||||
<div class="layui-panel" style="margin-top:12px">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form layui-form-pane" id="loginLogFilterForm" lay-filter="loginLogFilterForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">日期范围</label>
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<input type="text" class="layui-input" id="loginTimeRange" placeholder=" - " autocomplete="off">
|
||||
<input type="hidden" name="login_start_time" id="login_start_time">
|
||||
<input type="hidden" name="login_end_time" id="login_end_time">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="1">成功</option>
|
||||
<option value="0">失败</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="username" placeholder="请输入用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">登录IP</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="ip" placeholder="请输入IP地址" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnSearchLoginLogs">搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnResetLoginLogs">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-panel" style="margin-top:12px">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志列表</h3>
|
||||
<div style="padding: 20px;">
|
||||
<script type="text/html" id="loginLogsToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="clearLogs">
|
||||
<i class="layui-icon layui-icon-delete"></i> 清空日志
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
<table id="loginLogsTable" lay-filter="loginLogsTableFilter"></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'laydate', 'util', 'jquery'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var laydate = layui.laydate;
|
||||
var util = layui.util;
|
||||
var $ = layui.jquery;
|
||||
|
||||
// 日期范围选择器
|
||||
laydate.render({
|
||||
elem: '#loginTimeRange',
|
||||
range: true,
|
||||
type: 'datetime',
|
||||
format: 'yyyy-MM-dd HH:mm:ss',
|
||||
done: function(value, date, endDate){
|
||||
if(value) {
|
||||
const dates = value.split(' - ');
|
||||
$('#login_start_time').val(dates[0]);
|
||||
$('#login_end_time').val(dates[1]);
|
||||
} else {
|
||||
$('#login_start_time').val('');
|
||||
$('#login_end_time').val('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染表格
|
||||
var loginLogsTable = table.render({
|
||||
elem: '#loginLogsTable',
|
||||
url: '/admin/api/login_logs',
|
||||
toolbar: '#loginLogsToolbar',
|
||||
page: true,
|
||||
limit: 20,
|
||||
limits: [10, 20, 50, 100],
|
||||
cols: [[
|
||||
{field: 'username', title: '用户名', width: 150},
|
||||
{field: 'ip', title: '登录IP', width: 150},
|
||||
{field: 'status', title: '状态', width: 100, align: 'center', templet: function(d){
|
||||
return d.status === 1 ?
|
||||
'<span class="layui-badge layui-bg-green">成功</span>' :
|
||||
'<span class="layui-badge layui-bg-red">失败</span>';
|
||||
}},
|
||||
{field: 'message', title: '详情', minWidth: 150},
|
||||
{field: 'user_agent', title: 'User Agent', minWidth: 200, templet: function(d){
|
||||
return '<div title="'+d.user_agent+'" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">'+d.user_agent+'</div>';
|
||||
}},
|
||||
{field: 'created_at', title: '登录时间', width: 180, templet: function(d){
|
||||
return util.toDateString(d.created_at);
|
||||
}}
|
||||
]],
|
||||
response: {
|
||||
statusCode: 0
|
||||
},
|
||||
parseData: function(res){
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.msg,
|
||||
"count": res.data ? res.data.total : 0,
|
||||
"data": res.data ? res.data.list : []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索按钮
|
||||
$('#btnSearchLoginLogs').on('click', function(){
|
||||
const formData = form.val('loginLogFilterForm');
|
||||
loginLogsTable.reload({
|
||||
where: {
|
||||
status: formData.status,
|
||||
username: formData.username,
|
||||
ip: formData.ip,
|
||||
start_time: $('#login_start_time').val(),
|
||||
end_time: $('#login_end_time').val()
|
||||
},
|
||||
page: {curr: 1}
|
||||
});
|
||||
});
|
||||
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(loginLogsTableFilter)', function(obj){
|
||||
switch(obj.event){
|
||||
case 'clearLogs':
|
||||
layer.confirm('确定要清空所有登录日志吗?此操作不可恢复!', {icon: 3, title:'警告'}, function(index){
|
||||
$.post('/admin/api/login_logs/clear', function(res){
|
||||
if(res.code === 0){
|
||||
layer.msg('登录日志已清空', {icon: 1});
|
||||
loginLogsTable.reload({page: {curr: 1}});
|
||||
} else {
|
||||
layer.msg(res.msg || '清空失败', {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
// 重置按钮
|
||||
$('#btnResetLoginLogs').on('click', function(){
|
||||
$('#loginLogFilterForm')[0].reset();
|
||||
$('#login_start_time').val('');
|
||||
$('#login_end_time').val('');
|
||||
$('#loginTimeRange').val('');
|
||||
form.render('select');
|
||||
$('#btnSearchLoginLogs').click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
165
web/template/admin/operation_logs.html
Normal file
165
web/template/admin/operation_logs.html
Normal file
@@ -0,0 +1,165 @@
|
||||
{{ define "operation_logs.html" }}
|
||||
<section>
|
||||
<h2>日志操作</h2>
|
||||
<div class="layui-panel" style="margin-top:12px">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form layui-form-pane" id="operationLogFilterForm" lay-filter="operationLogFilterForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">日期范围</label>
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<input type="text" class="layui-input" id="operationTimeRange" placeholder=" - " autocomplete="off">
|
||||
<input type="hidden" name="operation_start_time" id="operation_start_time">
|
||||
<input type="hidden" name="operation_end_time" id="operation_end_time">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">操作方式</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="operation_type" placeholder="请输入操作方式" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">操作账号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="operator" placeholder="请输入操作账号" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">交易ID</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="transaction_id" placeholder="请输入交易ID" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnSearchOperationLogs">搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnResetOperationLogs">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-panel" style="margin-top:12px">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志列表</h3>
|
||||
<div style="padding: 20px;">
|
||||
<script type="text/html" id="operationLogsToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="clearLogs">
|
||||
<i class="layui-icon layui-icon-delete"></i> 清空日志
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
<table id="operationLogsTable" lay-filter="operationLogsTableFilter"></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'laydate', 'util', 'jquery'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var laydate = layui.laydate;
|
||||
var util = layui.util;
|
||||
var $ = layui.jquery;
|
||||
|
||||
// 日期范围选择器
|
||||
laydate.render({
|
||||
elem: '#operationTimeRange',
|
||||
range: true,
|
||||
type: 'datetime',
|
||||
format: 'yyyy-MM-dd HH:mm:ss',
|
||||
done: function(value, date, endDate){
|
||||
if(value) {
|
||||
const dates = value.split(' - ');
|
||||
$('#operation_start_time').val(dates[0]);
|
||||
$('#operation_end_time').val(dates[1]);
|
||||
} else {
|
||||
$('#operation_start_time').val('');
|
||||
$('#operation_end_time').val('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染表格
|
||||
var operationLogsTable = table.render({
|
||||
elem: '#operationLogsTable',
|
||||
url: '/admin/api/logs',
|
||||
toolbar: '#operationLogsToolbar',
|
||||
page: true,
|
||||
limit: 20,
|
||||
limits: [10, 20, 50, 100, 200, 500, 1000],
|
||||
cols: [[
|
||||
{field: 'app_name', title: '应用名称', minWidth: 150},
|
||||
{field: 'product_name', title: '商品名称', minWidth: 150},
|
||||
{field: 'transaction_id', title: '交易ID', width: 280},
|
||||
{field: 'operator', title: '操作账号', minWidth: 150},
|
||||
{field: 'operation_type', title: '操作方式', minWidth: 150},
|
||||
{field: 'details', title: '日志内容', minWidth: 200},
|
||||
{field: 'created_at', title: '创建时间', width: 180, templet: function(d){
|
||||
return util.toDateString(d.created_at);
|
||||
}}
|
||||
]],
|
||||
response: {
|
||||
statusName: 'code',
|
||||
statusCode: 0,
|
||||
msgName: 'msg',
|
||||
countName: 'count', // 解析数据长度的字段名称
|
||||
dataName: 'data' // 解析数据列表的字段名称
|
||||
},
|
||||
parseData: function(res) { // 将原始数据格式解析成 table 组件所规定的数据格式
|
||||
return {
|
||||
"code": res.code, // 解析接口状态
|
||||
"msg": res.msg, // 解析提示文本
|
||||
"count": res.data ? res.data.total : 0, // 解析数据长度
|
||||
"data": res.data ? res.data.list : [] // 解析数据列表
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索按钮
|
||||
$('#btnSearchOperationLogs').on('click', function(){
|
||||
const formData = form.val('operationLogFilterForm');
|
||||
operationLogsTable.reload({
|
||||
where: {
|
||||
operation_type: formData.operation_type,
|
||||
operator: formData.operator,
|
||||
transaction_id: formData.transaction_id,
|
||||
start_time: $('#operation_start_time').val(),
|
||||
end_time: $('#operation_end_time').val()
|
||||
},
|
||||
page: {curr: 1}
|
||||
});
|
||||
});
|
||||
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(operationLogsTableFilter)', function(obj){
|
||||
switch(obj.event){
|
||||
case 'clearLogs':
|
||||
layer.confirm('确定要清空所有日志吗?此操作不可恢复!', {icon: 3, title:'警告'}, function(index){
|
||||
$.post('/admin/api/logs/clear', function(res){
|
||||
if(res.code === 0){
|
||||
layer.msg('日志已清空', {icon: 1});
|
||||
operationLogsTable.reload({page: {curr: 1}});
|
||||
} else {
|
||||
layer.msg(res.msg || '清空失败', {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
// 重置按钮
|
||||
$('#btnResetOperationLogs').on('click', function(){
|
||||
$('#operationLogFilterForm')[0].reset();
|
||||
$('#operation_start_time').val('');
|
||||
$('#operation_end_time').val('');
|
||||
$('#operationTimeRange').val('');
|
||||
$('#btnSearchOperationLogs').click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
237
web/template/admin/profile.html
Normal file
237
web/template/admin/profile.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{{ define "profile.html" }}
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">个人资料</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" id="accountForm" lay-filter="accountForm" onsubmit="return false">
|
||||
<!-- 按照要求纵向排序:用户名、旧密码、新密码、确认新密码 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" placeholder="请输入用户名(不修改可留空或保持不变)" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">旧密码</label>
|
||||
<div class="layui-input-block">
|
||||
<!-- 不修改密码时可留空 -->
|
||||
<input type="password" name="old_password" placeholder="不修改可留空" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<div class="layui-input-block">
|
||||
<!-- 不修改密码时可留空 -->
|
||||
<input type="password" name="new_password" placeholder="不修改可留空(至少6位)" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认密码</label>
|
||||
<div class="layui-input-block">
|
||||
<!-- 不修改密码时可留空 -->
|
||||
<input type="password" name="confirm_password" placeholder="不修改可留空" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="submitAccount">保存更改</button>
|
||||
<!-- 将原先 type="reset" 改为自定义按钮,避免浏览器重置成初始空值 -->
|
||||
<button type="button" id="btnReset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
|
||||
(() => {
|
||||
// 等待layui加载完成
|
||||
function waitForLayui(callback) {
|
||||
if (typeof layui !== 'undefined') {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(() => waitForLayui(callback), 100);
|
||||
}
|
||||
}
|
||||
|
||||
waitForLayui(() => {
|
||||
layui.use(['form', 'layer'], () => {
|
||||
const form = layui.form
|
||||
const layer = layui.layer
|
||||
|
||||
// 记录初始用户名,用于判断是否需要更新
|
||||
let initialUsername = ''
|
||||
// 缓存最近一次加载到表单中的资料,用于“重置”恢复
|
||||
let lastProfile = null
|
||||
|
||||
// 加载个人资料:填充用户名
|
||||
// 返回:无;副作用:设置 initialUsername、lastProfile 与表单值
|
||||
const loadProfile = async () => {
|
||||
try {
|
||||
const res = await fetch('/admin/api/profile/info', {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '加载失败')
|
||||
const payload = data.data || {}
|
||||
initialUsername = payload.username || ''
|
||||
|
||||
const display = { ...payload }
|
||||
lastProfile = display
|
||||
form.val('accountForm', display)
|
||||
} catch (e) {
|
||||
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
|
||||
}
|
||||
}
|
||||
|
||||
// 校验密码表单:当任一密码字段填写时,要求三个字段均填写且有效
|
||||
// 返回:{ ok: boolean, msg?: string }
|
||||
const validatePassword = (fields) => {
|
||||
const oldPwd = (fields.old_password || '').trim()
|
||||
const newPwd = (fields.new_password || '').trim()
|
||||
const confirmPwd = (fields.confirm_password || '').trim()
|
||||
const anyFilled = !!(oldPwd || newPwd || confirmPwd)
|
||||
if (!anyFilled) return { ok: true }
|
||||
if (!oldPwd || !newPwd || !confirmPwd) return { ok: false, msg: '请完整填写旧密码/新密码/确认新密码' }
|
||||
if (newPwd.length < 6) return { ok: false, msg: '新密码长度不能少于6位' }
|
||||
if (newPwd !== confirmPwd) return { ok: false, msg: '两次输入的新密码不一致' }
|
||||
if (oldPwd === newPwd) return { ok: false, msg: '新密码不能与旧密码相同' }
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// 更新用户名:传输 username 与 old_password(当仅修改用户名时必须提供当前密码;同时修改密码时沿用同一 old_password)
|
||||
// 返回:Promise<void>
|
||||
const updateUsername = async (username, oldPassword) => {
|
||||
const payload = { username }
|
||||
if (oldPassword) payload.old_password = oldPassword
|
||||
const res = await fetch('/admin/api/profile/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '保存资料失败')
|
||||
}
|
||||
|
||||
// 更新密码:仅传输旧/新/确认三个字段
|
||||
// 返回:Promise<any> 后端响应数据,用于可能的重定向处理
|
||||
const updatePassword = async (fields) => {
|
||||
const payload = {
|
||||
old_password: fields.old_password,
|
||||
new_password: fields.new_password,
|
||||
confirm_password: fields.confirm_password
|
||||
}
|
||||
const res = await fetch('/admin/api/profile/password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
|
||||
return data
|
||||
}
|
||||
|
||||
// 提交综合更新:
|
||||
// 规则:
|
||||
// - 用户名:仅当与 initialUsername 不同且非空时更新
|
||||
// - 密码:当任一密码字段填写时,要求完整校验并更新;若均未填则不更新
|
||||
// - 若两者均无改动,则提示“未修改任何内容”
|
||||
form.on('submit(submitAccount)', async (obj) => {
|
||||
const fields = obj.field
|
||||
const desiredUsername = (fields.username || '').trim()
|
||||
const needUpdateUsername = desiredUsername && desiredUsername !== initialUsername
|
||||
|
||||
// 判定密码相关输入:
|
||||
// - wantChangePassword:输入了新密码或确认密码,视为尝试修改密码(将要求三个字段都填写)
|
||||
// - onlyOldProvided:仅输入了旧密码,用于支持“仅修改用户名需要当前密码”的场景
|
||||
const hasOld = !!(fields.old_password && fields.old_password.trim())
|
||||
const hasNewOrConfirm = !!((fields.new_password && fields.new_password.trim()) || (fields.confirm_password && fields.confirm_password.trim()))
|
||||
const wantChangePassword = hasNewOrConfirm
|
||||
const onlyOldProvided = hasOld && !hasNewOrConfirm
|
||||
|
||||
if (!needUpdateUsername && !wantChangePassword) {
|
||||
layer.msg('未修改任何内容', { icon: 0 })
|
||||
return false
|
||||
}
|
||||
|
||||
// 修改密码场景:需进行严格校验(旧/新/确认均必填)
|
||||
if (wantChangePassword) {
|
||||
const pwdCheck = validatePassword(fields)
|
||||
if (!pwdCheck.ok) {
|
||||
layer.msg(pwdCheck.msg, { icon: 2 })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 仅修改用户名:要求输入当前密码
|
||||
if (needUpdateUsername && !wantChangePassword && !hasOld) {
|
||||
layer.msg('修改用户名需要输入当前密码', { icon: 2 })
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 始终先更新用户名,再更新密码(避免改密后跳转导致无法继续)
|
||||
if (needUpdateUsername) {
|
||||
await updateUsername(desiredUsername, hasOld ? fields.old_password : '')
|
||||
initialUsername = desiredUsername
|
||||
}
|
||||
|
||||
if (wantChangePassword) {
|
||||
await updatePassword(fields)
|
||||
layer.msg('密码修改成功', { icon: 1 })
|
||||
// 清空密码框
|
||||
form.val('accountForm', {
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
} else {
|
||||
// 未修改密码,仅修改资料
|
||||
await loadProfile()
|
||||
layer.msg('保存成功', { icon: 1 })
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg(e.message || '保存失败', { icon: 2 })
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// 绑定“重置”按钮:将表单恢复为最近一次加载到表单中的资料
|
||||
// 逻辑:
|
||||
// - 如有 lastProfile,直接回填;
|
||||
// - 回填时同时清空三个密码字段;
|
||||
// - 如暂无缓存(极小概率),则重新请求资料
|
||||
const bindReset = () => {
|
||||
const btn = document.getElementById('btnReset')
|
||||
if (!btn) return
|
||||
btn.addEventListener('click', () => {
|
||||
if (lastProfile) {
|
||||
form.val('accountForm', { ...lastProfile, old_password: '', new_password: '', confirm_password: '' })
|
||||
layer.msg('已恢复为当前资料', { icon: 1 })
|
||||
} else {
|
||||
loadProfile()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
bindReset()
|
||||
loadProfile()
|
||||
})
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -1,6 +1,156 @@
|
||||
{{ define "settings.html" }}
|
||||
<section>
|
||||
<h2>系统设置</h2>
|
||||
<!-- 系统配置设置 -->
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">安全配置</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="systemForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
|
||||
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="encryption-key">加密密钥</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" name="encryption_key" placeholder="请输入数据加密密钥" class="layui-input" readonly>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="generateEncBtn">生成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-secret">JWT密钥</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" name="jwt_secret" placeholder="请输入JWT签名密钥" class="layui-input" readonly>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="generateJwtBtn">生成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-refresh">JWT刷新</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="number" name="jwt_refresh" placeholder="6" min="1" lay-affix="number" class="layui-input"
|
||||
style="width: 120px;" />
|
||||
<span class="layui-form-mid">小时(至少1小时)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-expire">JWT有效期</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="number" name="jwt_expire" placeholder="24" min="1" lay-affix="number" class="layui-input"
|
||||
style="width: 120px;" />
|
||||
<span class="layui-form-mid">小时(至少1小时)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_system">保存安全配置</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="system">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cookie 设置 -->
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">Cookie 设置</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="cookieForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-secure">Secure</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
|
||||
<input type="checkbox" name="cookie_secure" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-samesite">Same</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="cookie_same_site">
|
||||
<option value="Strict">Strict</option>
|
||||
<option value="Lax">Lax</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-domain">Domain</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="cookie_domain" placeholder="留空则默认为当前域名" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-max-age">MaxAge</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="number" name="cookie_max_age" placeholder="86400" min="0" lay-affix="number" class="layui-input"
|
||||
style="width: 120px;" />
|
||||
<span class="layui-form-mid">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_cookie">保存Cookie设置</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="cookie">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志清理设置 -->
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志清理设置</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="logCleanupForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="login-log-cleanup">登录日志</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="layui-form-mid">保留</span>
|
||||
<input type="number" name="login_log_cleanup_days" placeholder="30" min="0" lay-affix="number" class="layui-input" style="width: 80px;" />
|
||||
<span class="layui-form-mid">天,且保留最近</span>
|
||||
<input type="number" name="login_log_cleanup_limit" placeholder="10000" min="0" lay-affix="number" class="layui-input" style="width: 100px;" />
|
||||
<span class="layui-form-mid">条(0为不限制)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="operation-log-cleanup">操作日志</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="layui-form-mid">保留</span>
|
||||
<input type="number" name="operation_log_cleanup_days" placeholder="30" min="0" lay-affix="number" class="layui-input" style="width: 80px;" />
|
||||
<span class="layui-form-mid">天,且保留最近</span>
|
||||
<input type="number" name="operation_log_cleanup_limit" placeholder="10000" min="0" lay-affix="number" class="layui-input" style="width: 100px;" />
|
||||
<span class="layui-form-mid">条(0为不限制)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_cleanup">保存清理策略</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="cleanup">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息设置 -->
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">基本信息设置</h3>
|
||||
@@ -30,43 +180,12 @@
|
||||
<input type="text" name="site_logo" placeholder="/assets/logo.svg" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统配置设置 -->
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">系统配置</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="systemForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
|
||||
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
|
||||
</div>
|
||||
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_basic">保存基本信息</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="basic">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="default-user-role">默认角色</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="default_user_role">
|
||||
<option value="0">管理员</option>
|
||||
<option value="1">普通用户</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="session-timeout">会话超时</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="number" name="session_timeout" placeholder="3600" min="300" max="86400" lay-affix="number" class="layui-input"
|
||||
style="width: 120px;" />
|
||||
<span class="layui-form-mid">秒(300-86400秒)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,17 +226,15 @@
|
||||
class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_footer">保存页脚备案</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="footer">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="layui-form-item" style="margin-top: 24px;">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn" id="saveAllBtn" lay-submit lay-filter="saveAll">保存所有设置</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="resetBtn">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
@@ -164,29 +281,53 @@
|
||||
|
||||
/**
|
||||
* 将 settings 数据回填到各表单控件
|
||||
* - 文本/文本域/下拉:直接赋值
|
||||
* - 开关:根据 "1"/"0" 置为选中/未选中
|
||||
* - 拆分为独立的填充函数,便于局部重置
|
||||
*/
|
||||
const fillForms = (settings = {}) => {
|
||||
// 基本信息
|
||||
const fillSystem = (settings) => {
|
||||
const maintenanceChecked = (settings.maintenance_mode || '0') === '1';
|
||||
$('[name="maintenance_mode"]').prop('checked', maintenanceChecked);
|
||||
$('[name="jwt_secret"]').val(settings.jwt_secret || '');
|
||||
$('[name="encryption_key"]').val(settings.encryption_key || '');
|
||||
$('[name="jwt_refresh"]').val(settings.jwt_refresh || '6');
|
||||
$('[name="jwt_expire"]').val(settings.jwt_expire || '24');
|
||||
};
|
||||
|
||||
const fillCookie = (settings) => {
|
||||
const cookieSecureChecked = (settings.cookie_secure || 'true') === 'true' || settings.cookie_secure === '1';
|
||||
$('[name="cookie_secure"]').prop('checked', cookieSecureChecked);
|
||||
$('[name="cookie_same_site"]').val(settings.cookie_same_site || 'Lax');
|
||||
$('[name="cookie_domain"]').val(settings.cookie_domain || '');
|
||||
$('[name="cookie_max_age"]').val(settings.cookie_max_age || '86400');
|
||||
};
|
||||
|
||||
const fillCleanup = (settings) => {
|
||||
$('[name="login_log_cleanup_days"]').val(settings.login_log_cleanup_days || '30');
|
||||
$('[name="login_log_cleanup_limit"]').val(settings.login_log_cleanup_limit || '10000');
|
||||
$('[name="operation_log_cleanup_days"]').val(settings.operation_log_cleanup_days || '30');
|
||||
$('[name="operation_log_cleanup_limit"]').val(settings.operation_log_cleanup_limit || '10000');
|
||||
};
|
||||
|
||||
const fillBasic = (settings) => {
|
||||
$('[name="site_title"]').val(settings.site_title || '');
|
||||
$('[name="site_keywords"]').val(settings.site_keywords || '');
|
||||
$('[name="site_description"]').val(settings.site_description || '');
|
||||
$('[name="site_logo"]').val(settings.site_logo || '');
|
||||
};
|
||||
|
||||
// 系统配置
|
||||
const maintenanceChecked = (settings.maintenance_mode || '0') === '1';
|
||||
$('[name="maintenance_mode"]').prop('checked', maintenanceChecked);
|
||||
$('[name="default_user_role"]').val(settings.default_user_role || '1');
|
||||
$('[name="session_timeout"]').val(settings.session_timeout || '3600');
|
||||
|
||||
// 页脚与备案
|
||||
const fillFooter = (settings) => {
|
||||
$('[name="footer_text"]').val(settings.footer_text || '');
|
||||
$('[name="icp_record"]').val(settings.icp_record || '');
|
||||
$('[name="icp_record_link"]').val(settings.icp_record_link || '');
|
||||
$('[name="psb_record"]').val(settings.psb_record || '');
|
||||
$('[name="psb_record_link"]').val(settings.psb_record_link || '');
|
||||
};
|
||||
|
||||
const fillForms = (settings = {}) => {
|
||||
fillBasic(settings);
|
||||
fillSystem(settings);
|
||||
fillCookie(settings);
|
||||
fillCleanup(settings);
|
||||
fillFooter(settings);
|
||||
// 渲染 layui 组件
|
||||
form.render();
|
||||
};
|
||||
@@ -222,68 +363,146 @@
|
||||
return {
|
||||
...collectForm('#basicForm'),
|
||||
...collectForm('#systemForm'),
|
||||
...collectForm('#cookieForm'),
|
||||
...collectForm('#footerForm'),
|
||||
...collectForm('#logCleanupForm'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理“保存所有设置”点击
|
||||
* - 二次确认后提交
|
||||
* - 显示加载中,防重复提交
|
||||
* - 成功后提示并刷新缓存的 originalSettings
|
||||
* 提交设置到后端
|
||||
* @param {Object} payload - 要保存的设置对象
|
||||
* @param {HTMLElement} btnElem - 触发保存的按钮元素(用于禁用/恢复)
|
||||
* @param {String} successMsg - 成功提示信息
|
||||
*/
|
||||
const handleSaveAll = () => {
|
||||
const payload = collectAllSettings();
|
||||
layer.confirm('确认保存所有设置?', { icon: 3, title: '提示' }, (idx) => {
|
||||
layer.close(idx);
|
||||
const btn = $('#saveAllBtn');
|
||||
btn.prop('disabled', true).addClass('layui-btn-disabled');
|
||||
const loadIdx = layer.load(2, { content: '正在保存...' });
|
||||
|
||||
fetch('/admin/api/settings/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.then(res => {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg || '保存成功', { icon: 1, time: 1000 });
|
||||
originalSettings = { ...payload };
|
||||
} else {
|
||||
layer.msg(res.msg || '保存失败', { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('保存设置失败:', err);
|
||||
layer.msg('网络错误,保存失败', { icon: 2 });
|
||||
})
|
||||
.finally(() => {
|
||||
layer.close(loadIdx);
|
||||
btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
});
|
||||
const submitSettings = (payload, btnElem, successMsg = '保存成功') => {
|
||||
const $btn = $(btnElem);
|
||||
$btn.prop('disabled', true).addClass('layui-btn-disabled');
|
||||
const loadIdx = layer.msg('正在保存...', {
|
||||
icon: 16,
|
||||
time: 0,
|
||||
shade: 0.1
|
||||
});
|
||||
|
||||
fetch('/admin/api/settings/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.then(res => {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg || successMsg, { icon: 1, time: 1000 });
|
||||
// 更新本地缓存,合并新保存的设置
|
||||
originalSettings = { ...originalSettings, ...payload };
|
||||
} else {
|
||||
layer.msg(res.msg || '保存失败', { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('保存设置失败:', err);
|
||||
var msg = '网络错误,保存失败';
|
||||
if (err.response && err.response.data && err.response.data.msg) {
|
||||
msg = err.response.data.msg;
|
||||
} else if (err.message) {
|
||||
msg = err.message;
|
||||
}
|
||||
layer.msg(msg, { icon: 2 });
|
||||
})
|
||||
.finally(() => {
|
||||
layer.close(loadIdx);
|
||||
$btn.prop('disabled', false).removeClass('layui-btn-disabled');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理“重置”点击
|
||||
* - 恢复为上次加载的 originalSettings
|
||||
* 绑定各个分块的保存按钮
|
||||
*/
|
||||
const handleReset = () => {
|
||||
fillForms(originalSettings);
|
||||
layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 });
|
||||
form.on('submit(save_system)', function(data){
|
||||
submitSettings(collectForm('#systemForm'), data.elem, '安全配置已保存');
|
||||
return false;
|
||||
});
|
||||
form.on('submit(save_cookie)', function(data){
|
||||
submitSettings(collectForm('#cookieForm'), data.elem, 'Cookie设置已保存');
|
||||
return false;
|
||||
});
|
||||
form.on('submit(save_cleanup)', function(data){
|
||||
submitSettings(collectForm('#logCleanupForm'), data.elem, '清理策略已保存');
|
||||
return false;
|
||||
});
|
||||
form.on('submit(save_basic)', function(data){
|
||||
submitSettings(collectForm('#basicForm'), data.elem, '基本信息已保存');
|
||||
return false;
|
||||
});
|
||||
form.on('submit(save_footer)', function(data){
|
||||
submitSettings(collectForm('#footerForm'), data.elem, '页脚备案已保存');
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理各个分块的重置按钮
|
||||
*/
|
||||
$(document).on('click', '.reset-btn', function() {
|
||||
const type = $(this).data('type');
|
||||
switch (type) {
|
||||
case 'system':
|
||||
fillSystem(originalSettings);
|
||||
break;
|
||||
case 'cookie':
|
||||
fillCookie(originalSettings);
|
||||
break;
|
||||
case 'cleanup':
|
||||
fillCleanup(originalSettings);
|
||||
break;
|
||||
case 'basic':
|
||||
fillBasic(originalSettings);
|
||||
break;
|
||||
case 'footer':
|
||||
fillFooter(originalSettings);
|
||||
break;
|
||||
}
|
||||
form.render();
|
||||
layer.msg('已恢复该部分默认值', { icon: 1, time: 800 });
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成安全密钥
|
||||
*/
|
||||
const generateKey = async (type) => {
|
||||
try {
|
||||
const loadIdx = layer.load(2);
|
||||
const res = await fetch(`/admin/api/settings/generate_key?type=${type}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const data = await res.json();
|
||||
layer.close(loadIdx);
|
||||
|
||||
if (data.code === 0) {
|
||||
if (type === 'jwt') {
|
||||
$('[name="jwt_secret"]').val(data.data.key);
|
||||
} else if (type === 'encryption') {
|
||||
$('[name="encryption_key"]').val(data.data.key);
|
||||
}
|
||||
layer.msg('生成成功', { icon: 1 });
|
||||
} else {
|
||||
layer.msg(data.msg || '生成失败', { icon: 2 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('生成密钥失败:', err);
|
||||
layer.closeAll('loading');
|
||||
layer.msg('网络错误', { icon: 2 });
|
||||
}
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
$('#saveAllBtn').off('click').on('click', handleSaveAll);
|
||||
$('#resetBtn').off('click').on('click', handleReset);
|
||||
$('#generateJwtBtn').on('click', () => generateKey('jwt'));
|
||||
$('#generateEncBtn').on('click', () => generateKey('encryption'));
|
||||
|
||||
// 初始化:加载设置
|
||||
loadSettings();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
{{ define "user.html" }}
|
||||
<section>
|
||||
<h2>账户管理</h2>
|
||||
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">修改密码</li>
|
||||
<li>修改用户名</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<!-- 修改密码模块 -->
|
||||
<div class="layui-tab-item layui-show">
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改密码</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-old-password">当前密码</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-input-wrap">
|
||||
<input type="password" name="old_password" placeholder="请输入当前密码" autocomplete="off"
|
||||
class="layui-input" lay-verify="required" lay-affix="eye" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-new-password">新的密码</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-input-wrap">
|
||||
<input type="password" name="new_password" placeholder="请输入新密码(至少6位)" autocomplete="off"
|
||||
class="layui-input" lay-verify="required" lay-affix="eye" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认密码</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-input-wrap">
|
||||
<input type="password" name="confirm_password" placeholder="请再次输入新密码" autocomplete="off"
|
||||
class="layui-input" lay-verify="required" lay-affix="eye" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="submitPassword">
|
||||
<i class="layui-icon layui-icon-ok"></i> 修改密码
|
||||
</button>
|
||||
<button type="button" id="resetPasswordBtn" class="layui-btn layui-btn-primary">
|
||||
<i class="layui-icon layui-icon-refresh"></i> 重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改用户名模块 -->
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-panel" style="margin-top: 16px;">
|
||||
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改用户名</h3>
|
||||
<div style="padding: 20px;">
|
||||
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">当前用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="current_username" disabled readonly class="layui-input readonly-field" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-username">新用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input"
|
||||
lay-verify="required" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-old-password">当前密码</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-input-wrap">
|
||||
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off"
|
||||
class="layui-input" lay-verify="required" lay-affix="eye" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="submitUsername">
|
||||
<i class="layui-icon layui-icon-ok"></i> 修改用户名
|
||||
</button>
|
||||
<button type="button" id="resetUsernameBtn" class="layui-btn layui-btn-primary">
|
||||
<i class="layui-icon layui-icon-refresh"></i> 重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
|
||||
(() => {
|
||||
// 工具方法:将数值角色转为中文标签
|
||||
const roleToText = (role) => {
|
||||
const r = typeof role === 'string' ? parseInt(role, 10) : role
|
||||
return r === 0 ? '管理员' : '普通成员'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 如果未加载 layui,则按需加载
|
||||
const ensureLayui = () => new Promise((resolve) => {
|
||||
if (window.layui) return resolve(window.layui)
|
||||
const css = document.createElement('link')
|
||||
css.rel = 'stylesheet'
|
||||
css.href = 'https://unpkg.com/layui@2.10.1/dist/css/layui.css'
|
||||
document.head.appendChild(css)
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://unpkg.com/layui@2.10.1/dist/layui.js'
|
||||
script.onload = () => resolve(window.layui)
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
// 在确保 Layui 可用后再执行页面逻辑
|
||||
ensureLayui().then(() => {
|
||||
layui.use(['form', 'layer', 'element'], () => {
|
||||
const form = layui.form
|
||||
const layer = layui.layer
|
||||
const element = layui.element
|
||||
|
||||
// 全局变量
|
||||
let currentUsername = null
|
||||
|
||||
// 获取当前用户名
|
||||
const getCurrentUsername = async () => {
|
||||
try {
|
||||
const res = await fetch('/admin/api/user/profile')
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '获取用户信息失败')
|
||||
|
||||
currentUsername = data.data.username
|
||||
// 填充用户名修改表单的当前用户名
|
||||
form.val('usernameForm', { current_username: currentUsername })
|
||||
|
||||
} catch (e) {
|
||||
layer.msg(e.message || '获取用户信息失败', { icon: 2 })
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码模块
|
||||
const PasswordModule = {
|
||||
validate: (fields) => {
|
||||
const { old_password, new_password, confirm_password } = fields
|
||||
|
||||
if (!old_password || !new_password || !confirm_password) {
|
||||
return { ok: false, msg: '请填写完整的密码信息' }
|
||||
}
|
||||
|
||||
if (new_password.length < 6) {
|
||||
return { ok: false, msg: '新密码长度不能少于6位' }
|
||||
}
|
||||
|
||||
if (new_password !== confirm_password) {
|
||||
return { ok: false, msg: '两次输入的新密码不一致' }
|
||||
}
|
||||
|
||||
if (old_password === new_password) {
|
||||
return { ok: false, msg: '新密码不能与当前密码相同' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
},
|
||||
|
||||
submit: async (fields) => {
|
||||
const validation = PasswordModule.validate(fields)
|
||||
if (!validation.ok) {
|
||||
layer.msg(validation.msg, { icon: 2 })
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/api/user/password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
old_password: fields.old_password,
|
||||
new_password: fields.new_password,
|
||||
confirm_password: fields.confirm_password
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
|
||||
|
||||
// 检查是否需要跳转
|
||||
if (data.data?.redirect) {
|
||||
layer.msg('密码修改成功,即将跳转到登录页', { icon: 1, time: 1500 }, () => {
|
||||
window.location.href = data.data.redirect
|
||||
})
|
||||
} else {
|
||||
// 密码修改成功,不跳转,重置表单
|
||||
layer.msg('密码修改成功', { icon: 1 })
|
||||
document.getElementById('passwordForm').reset()
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
layer.msg(e.message || '修改密码失败', { icon: 2 })
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
document.getElementById('passwordForm').reset()
|
||||
layer.msg('表单已重置', { icon: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// 修改用户名模块
|
||||
const UsernameModule = {
|
||||
validate: (fields) => {
|
||||
const { new_username, password } = fields
|
||||
|
||||
if (!new_username || !password) {
|
||||
return { ok: false, msg: '请填写新用户名和当前密码' }
|
||||
}
|
||||
|
||||
if (new_username === currentUsername) {
|
||||
return { ok: false, msg: '新用户名不能与当前用户名相同' }
|
||||
}
|
||||
|
||||
if (new_username.length < 3) {
|
||||
return { ok: false, msg: '用户名长度不能少于3位' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
},
|
||||
|
||||
submit: async (fields) => {
|
||||
const validation = UsernameModule.validate(fields)
|
||||
if (!validation.ok) {
|
||||
layer.msg(validation.msg, { icon: 2 })
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/api/user/profile/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: fields.new_username,
|
||||
old_password: fields.password
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const ok = (data.success === true) || (data.code === 0)
|
||||
if (!ok) throw new Error(data.message || data.msg || '修改用户名失败')
|
||||
|
||||
layer.msg('用户名修改成功', { icon: 1 })
|
||||
|
||||
// 重新获取当前用户名
|
||||
await getCurrentUsername()
|
||||
|
||||
// 清空表单(不显示重置提示)
|
||||
form.val('usernameForm', {
|
||||
new_username: '',
|
||||
password: '',
|
||||
current_username: currentUsername || ''
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
layer.msg(e.message || '修改用户名失败', { icon: 2 })
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
form.val('usernameForm', {
|
||||
new_username: '',
|
||||
password: '',
|
||||
current_username: currentUsername || ''
|
||||
})
|
||||
layer.msg('表单已重置', { icon: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定表单提交事件
|
||||
form.on('submit(submitPassword)', (obj) => {
|
||||
return PasswordModule.submit(obj.field)
|
||||
})
|
||||
|
||||
form.on('submit(submitUsername)', (obj) => {
|
||||
return UsernameModule.submit(obj.field)
|
||||
})
|
||||
|
||||
// 绑定重置按钮
|
||||
document.getElementById('resetPasswordBtn')?.addEventListener('click', PasswordModule.reset)
|
||||
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
|
||||
|
||||
// 初始化加载
|
||||
getCurrentUsername()
|
||||
})
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</section>
|
||||
{{ end }}
|
||||
374
web/template/default/index.html
Normal file
374
web/template/default/index.html
Normal file
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<title>{{ .Title }}</title>
|
||||
<!-- 站 点 协 议 -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="content-language" content="zh-cn">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
|
||||
{{ if .Keywords }}<meta name="keywords" content="{{ .Keywords }}">{{ end }}
|
||||
|
||||
<!-- 站 点 图 标 -->
|
||||
<link href='/favicon.ico' rel='icon' type='image/x-icon'>
|
||||
<link href="/favicon.ico" rel="shortcut icon">
|
||||
<link href="/favicon.ico" rel="bookmark">
|
||||
<!-- 样 式 文 件 -->
|
||||
<link rel="stylesheet" href="/static/lib/layui/css/layui.css"/>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
.layui-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.body-background {
|
||||
width: 420px;
|
||||
min-height: 350px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
padding: 0 0 0 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.logo-title h1 {
|
||||
color: #2550dd;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 30px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.box-form {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(240, 248, 255, 0.9));
|
||||
border: 2px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 15px;
|
||||
padding: 30px 25px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-form::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
.box-form .layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 24px;
|
||||
color: #ff4757;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
|
||||
margin: 15px 0;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #3742fa;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 15px 0;
|
||||
text-shadow: 0 1px 2px rgba(55, 66, 250, 0.2);
|
||||
}
|
||||
|
||||
.body_box {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body_footer {
|
||||
padding-top: 15px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.body_beian {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.body_beian a {
|
||||
color: rgba(0, 212, 255, 0.8);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.body_beian a:hover {
|
||||
color: #00d4ff;
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
|
||||
margin: 20px 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 代 码 结 构 -->
|
||||
<div class="layui-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="body-background body_box">
|
||||
<div class="layui-form box-form body_box">
|
||||
<div class="layui-form-item logo-title">
|
||||
<h1><strong>{{ .SystemName }}</strong></h1>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="layui-form-item">
|
||||
<div class="warning-text">{{ .WarningText }}</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="info-text">{{ .InfoText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body_footer">{{ .FooterText }}</div>
|
||||
{{ if .ICPRecord }}
|
||||
<div class="body_beian"><a href="{{ .ICPRecordLink }}" target="_blank">{{ .ICPRecord }}</a></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资 源 引 入 -->
|
||||
<script src="/static/lib/jquery/jquery.min.js" type="text/javascript"></script>
|
||||
<script>
|
||||
// 设置版权年份 (保留此逻辑以防 FooterText 中使用了 id="currentYear")
|
||||
if(document.getElementById('currentYear')) {
|
||||
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
||||
}
|
||||
|
||||
// 获取canvas元素和绘图上下文
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 设置canvas尺寸为全屏
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// 粒子类
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// 重置粒子位置和属性
|
||||
reset() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.vx = (Math.random() - 0.5) * 2;
|
||||
this.vy = (Math.random() - 0.5) * 2;
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.opacity = Math.random() * 0.8 + 0.2;
|
||||
this.color = this.getRandomColor();
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
getRandomColor() {
|
||||
const colors = [
|
||||
'#00FF00', '#0080FF', '#FF0080', '#FFFF00',
|
||||
'#FF8000', '#8000FF', '#00FFFF', '#FF4000'
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
|
||||
// 更新粒子位置
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
// 边界检测,粒子超出边界时重置
|
||||
if (this.x < 0 || this.x > canvas.width ||
|
||||
this.y < 0 || this.y > canvas.height) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// 随机改变透明度
|
||||
this.opacity += (Math.random() - 0.5) * 0.02;
|
||||
this.opacity = Math.max(0.1, Math.min(1, this.opacity));
|
||||
}
|
||||
|
||||
// 绘制粒子
|
||||
draw() {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.opacity;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建粒子数组
|
||||
const particles = [];
|
||||
const particleCount = 150;
|
||||
|
||||
// 初始化粒子
|
||||
const initParticles = () => {
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
};
|
||||
|
||||
// 绘制连线
|
||||
const drawConnections = () => {
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 如果距离小于100像素,绘制连线
|
||||
if (distance < 100) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = (100 - distance) / 100 * 0.3;
|
||||
ctx.strokeStyle = '#00FF00';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 动画循环
|
||||
const animate = () => {
|
||||
// 清除画布,使用半透明黑色创建拖尾效果
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 更新和绘制所有粒子
|
||||
particles.forEach(particle => {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
});
|
||||
|
||||
// 绘制粒子间的连线
|
||||
drawConnections();
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// 鼠标交互效果
|
||||
const addMouseInteraction = () => {
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
|
||||
// 鼠标附近的粒子会被吸引
|
||||
particles.forEach(particle => {
|
||||
const dx = mouseX - particle.x;
|
||||
const dy = mouseY - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 150) {
|
||||
particle.vx += dx * 0.0001;
|
||||
particle.vy += dy * 0.0001;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 点击时添加新粒子
|
||||
canvas.addEventListener('click', (e) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const newParticle = new Particle();
|
||||
newParticle.x = e.clientX + (Math.random() - 0.5) * 20;
|
||||
newParticle.y = e.clientY + (Math.random() - 0.5) * 20;
|
||||
particles.push(newParticle);
|
||||
}
|
||||
|
||||
// 限制粒子数量
|
||||
if (particles.length > particleCount + 50) {
|
||||
particles.splice(0, 5);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 启动粒子系统
|
||||
initParticles();
|
||||
addMouseInteraction();
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,385 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
|
||||
<head>
|
||||
<title>{{.SystemName}} - 生活就像愤怒的小鸟,失败后总有几只猪在笑。</title>
|
||||
<!-- 站 点 协 议 -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="content-language" content="zh-cn">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<!-- 站 点 图 标 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="bookmark" href="/favicon.ico" />
|
||||
<!-- 样 式 文 件 -->
|
||||
<link rel="stylesheet" href="//lib.baomitu.com/layui/2.8.17/css/layui.css" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
.layui-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.body-background {
|
||||
width: 420px;
|
||||
min-height: 350px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
padding: 0 0 0 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.logo-title h1 {
|
||||
color: #2550dd;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
to {
|
||||
text-shadow: 0 0 30px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.box-form {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(240, 248, 255, 0.9));
|
||||
border: 2px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 15px;
|
||||
padding: 30px 25px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-form::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.box-form .layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 24px;
|
||||
color: #ff4757;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
|
||||
margin: 15px 0;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #3742fa;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 15px 0;
|
||||
text-shadow: 0 1px 2px rgba(55, 66, 250, 0.2);
|
||||
}
|
||||
|
||||
.body_box {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body_footer {
|
||||
padding-top: 15px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.body_beian {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.body_beian a {
|
||||
color: rgba(0, 212, 255, 0.8);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.body_beian a:hover {
|
||||
color: #00d4ff;
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
|
||||
margin: 20px 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 代 码 结 构 -->
|
||||
<div class="layui-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="body-background body_box">
|
||||
<div class="layui-form box-form body_box">
|
||||
<div class="layui-form-item logo-title">
|
||||
<h1><strong>系统提醒</strong></h1>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="layui-form-item">
|
||||
<div class="warning-text">🚫 未授权,拒绝访问</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="info-text">💬 如有问题,请联系网站管理员</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body_footer">{{.FooterText}}</div>
|
||||
{{if or .ICPRecord .PSBRecord}}<div class="body_beian">{{if .ICPRecord}}<a href="{{.ICPRecordLink}}"
|
||||
target="_blank">{{.ICPRecord}}</a>{{end}}{{if and .ICPRecord .PSBRecord}} {{end}}{{if .PSBRecord}}<a
|
||||
href="{{.PSBRecordLink}}" target="_blank">{{.PSBRecord}}</a>{{end}}</div>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 资 源 引 入 -->
|
||||
<script src="//lib.baomitu.com/jquery/3.6.4/jquery.min.js" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
// 获取canvas元素和绘图上下文
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 设置canvas尺寸为全屏
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// 粒子类
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// 重置粒子位置和属性
|
||||
reset() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.vx = (Math.random() - 0.5) * 2;
|
||||
this.vy = (Math.random() - 0.5) * 2;
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.opacity = Math.random() * 0.8 + 0.2;
|
||||
this.color = this.getRandomColor();
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
getRandomColor() {
|
||||
const colors = [
|
||||
'#00FF00', '#0080FF', '#FF0080', '#FFFF00',
|
||||
'#FF8000', '#8000FF', '#00FFFF', '#FF4000'
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
|
||||
// 更新粒子位置
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
// 边界检测,粒子超出边界时重置
|
||||
if (this.x < 0 || this.x > canvas.width ||
|
||||
this.y < 0 || this.y > canvas.height) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// 随机改变透明度
|
||||
this.opacity += (Math.random() - 0.5) * 0.02;
|
||||
this.opacity = Math.max(0.1, Math.min(1, this.opacity));
|
||||
}
|
||||
|
||||
// 绘制粒子
|
||||
draw() {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.opacity;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建粒子数组
|
||||
const particles = [];
|
||||
const particleCount = 150;
|
||||
|
||||
// 初始化粒子
|
||||
const initParticles = () => {
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
};
|
||||
|
||||
// 绘制连线
|
||||
const drawConnections = () => {
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 如果距离小于100像素,绘制连线
|
||||
if (distance < 100) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = (100 - distance) / 100 * 0.3;
|
||||
ctx.strokeStyle = '#00FF00';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 动画循环
|
||||
const animate = () => {
|
||||
// 清除画布,使用半透明黑色创建拖尾效果
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 更新和绘制所有粒子
|
||||
particles.forEach(particle => {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
});
|
||||
|
||||
// 绘制粒子间的连线
|
||||
drawConnections();
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// 鼠标交互效果
|
||||
const addMouseInteraction = () => {
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
|
||||
// 鼠标附近的粒子会被吸引
|
||||
particles.forEach(particle => {
|
||||
const dx = mouseX - particle.x;
|
||||
const dy = mouseY - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 150) {
|
||||
particle.vx += dx * 0.0001;
|
||||
particle.vy += dy * 0.0001;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 点击时添加新粒子
|
||||
canvas.addEventListener('click', (e) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const newParticle = new Particle();
|
||||
newParticle.x = e.clientX + (Math.random() - 0.5) * 20;
|
||||
newParticle.y = e.clientY + (Math.random() - 0.5) * 20;
|
||||
particles.push(newParticle);
|
||||
}
|
||||
|
||||
// 限制粒子数量
|
||||
if (particles.length > particleCount + 50) {
|
||||
particles.splice(0, 5);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 启动粒子系统
|
||||
initParticles();
|
||||
addMouseInteraction();
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
209
web/template/install/install.html
Normal file
209
web/template/install/install.html
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ .title }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="/static/lib/layui/css/layui.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.install-box {
|
||||
width: 600px;
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.install-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.install-header h2 {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.install-header p {
|
||||
color: #999;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="install-box">
|
||||
<div class="install-header">
|
||||
<h2>系统初始化</h2>
|
||||
<p>欢迎使用,请完成以下初始化设置</p>
|
||||
</div>
|
||||
|
||||
<form class="layui-form" lay-filter="install-form">
|
||||
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
|
||||
<legend style="font-size: 14px;">1. 数据库配置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库类型</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="db_type" value="sqlite" title="SQLite (默认)" lay-filter="db_type" checked>
|
||||
<input type="radio" name="db_type" value="mysql" title="MySQL" lay-filter="db_type">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mysql-config" style="display: none;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">主机地址</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="db_host" value="127.0.0.1" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">端口号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="db_port" value="3306" lay-affix="number" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="db_name" value="networkauth" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="db_user" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="db_pass" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend style="font-size: 14px;">2. 站点信息</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">站点标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="site_title" lay-verify="required" value="NetworkAuth" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend style="font-size: 14px;">3. 管理员设置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">管理员账号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="admin_username" lay-verify="required" placeholder="设置管理员账号" value="admin" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">管理员密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="admin_password" lay-verify="required|pass" placeholder="设置管理员密码(至少6位)" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="confirm_password" lay-verify="required|confirmPass" placeholder="请再次输入管理员密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item" style="margin-top: 40px; text-align: center;">
|
||||
<button class="layui-btn layui-btn-normal layui-btn-lg" style="width: 200px;" lay-submit lay-filter="install-submit">立即初始化</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="/static/lib/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer', 'jquery'], () => {
|
||||
const form = layui.form;
|
||||
const layer = layui.layer;
|
||||
const $ = layui.jquery;
|
||||
|
||||
// 监听数据库类型切换
|
||||
form.on('radio(db_type)', (data) => {
|
||||
if (data.value === 'mysql') {
|
||||
$('#mysql-config').slideDown();
|
||||
$('#mysql-config input').attr('lay-verify', 'required');
|
||||
} else {
|
||||
$('#mysql-config').slideUp();
|
||||
$('#mysql-config input').removeAttr('lay-verify');
|
||||
}
|
||||
});
|
||||
|
||||
// 自定义验证规则
|
||||
form.verify({
|
||||
pass: [
|
||||
/^[\S]{6,20}$/,
|
||||
'密码必须6到20位,且不能出现空格'
|
||||
],
|
||||
confirmPass: (value) => {
|
||||
const pass = $('input[name="admin_password"]').val();
|
||||
if(value !== pass){
|
||||
return '两次输入的密码不一致';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听提交
|
||||
form.on('submit(install-submit)', (data) => {
|
||||
const loading = layer.load(2, {shade: [0.1, '#fff']});
|
||||
|
||||
// 处理 db_port 转换为整数
|
||||
const payload = { ...data.field };
|
||||
if (payload.db_port) {
|
||||
payload.db_port = parseInt(payload.db_port, 10) || 3306;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/install',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload),
|
||||
success: (res) => {
|
||||
layer.close(loading);
|
||||
if (res.code === 0) {
|
||||
layer.msg('系统初始化成功!正在跳转登录...', {
|
||||
icon: 1,
|
||||
time: 2000
|
||||
}, () => {
|
||||
window.location.href = '/admin/login';
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '初始化失败', {icon: 2});
|
||||
}
|
||||
},
|
||||
error: (xhr) => {
|
||||
layer.close(loading);
|
||||
const res = xhr.responseJSON;
|
||||
layer.msg(res ? res.msg : '请求失败,请重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
|
||||
return false; // 阻止表单跳转
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user