Enhance user authentication and authentication

Fix the modification of personal information
Fix the formatted page template
This commit is contained in:
2025-10-26 03:05:27 +08:00
parent 3e170ad526
commit c93ee377fe
22 changed files with 2728 additions and 2420 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -38,59 +38,54 @@
</div>
</div>
</section>
<script>
// 仪表盘统计脚本(采用箭头函数与中文注释)
layui.use(['layer', 'util'], function(){
const layer = layui.layer;
const util = layui.util;
const $ = layui.$;
// 仪表盘统计脚本(采用箭头函数与中文注释)
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 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) {
$('.system-info-item').each(function() {
const label = $(this).find('.system-info-label').text();
if (label === '运行时长') {
$(this).find('.system-info-value').text(data.uptime);
}
});
}
// 工具函数:加载 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);
}
}).fail(() => {
console.log('获取系统信息失败');
});
};
};
// 立即刷新一次系统信息
refreshSystemInfo();
});
// 函数:刷新基本信息和运行状态
// 说明:请求后台获取最新的系统信息并更新页面显示
const refreshSystemInfo = () => {
$.get('/admin/api/system/info', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
// 更新运行时长
if (data.uptime) {
$('.system-info-item').each(function () {
const label = $(this).find('.system-info-label').text();
if (label === '运行时长') {
$(this).find('.system-info-value').text(data.uptime);
}
});
}
}
}).fail(() => {
console.log('获取系统信息失败');
});
};
// 立即刷新一次系统信息
refreshSystemInfo();
});
</script>
{{ end }}

View File

@@ -1,73 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ .Title }} - {{ .SystemName }}</title>
<!-- 站点图标 -->
<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>
</head>
<body>
<div class="layui-layout layui-layout-admin" id="app">
<div class="layui-header">
<!-- 头部区域可配合layui 已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<!-- 移动端显示 -->
<li class="layui-nav-item layui-show-xs-inline-block" lay-header-event="menuLeft">
<i class="layui-icon layui-icon-spread-left"></i>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<!-- 刷新页面按钮 -->
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="refresh-btn" style="background-color: unset" title="刷新页面">
<i class="layui-icon layui-icon-refresh-3" style="font-size: 20px"></i>
</a>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ .Title }} - {{ .SystemName }}</title>
<!-- 站点图标 -->
<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>
</head>
<body>
<div class="layui-layout layui-layout-admin" id="app">
<div class="layui-header">
<!-- 头部区域可配合layui 已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<!-- 移动端显示 -->
<li class="layui-nav-item layui-show-xs-inline-block" lay-header-event="menuLeft">
<i class="layui-icon layui-icon-spread-left"></i>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<!-- 刷新页面按钮 -->
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="refresh-btn" style="background-color: unset" title="刷新页面">
<i class="layui-icon layui-icon-refresh-3" style="font-size: 20px"></i>
</a>
</li>
<li class="layui-nav-item">
<i id="change-theme" class="layui-icon layui-icon-theme" style="font-size: 20px"></i>
</li>
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="logout-btn" style="background-color: unset" title="退出登录">
<i class="layui-icon layui-icon-logout" style="font-size: 20px"></i>
</a>
</li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域 -->
<div class="layui-logo layui-bg-black logo-enhanced">{{ .SystemName }}</div>
<ul class="layui-nav layui-nav-tree" lay-shrink="all" lay-unselect lay-filter="nav-side" id="ws-nav-side">
<li class="layui-nav-item">
<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="settings" href="javascript:;">系统设置</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<i id="change-theme" class="layui-icon layui-icon-theme" style="font-size: 20px"></i>
</li>
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="logout-btn" style="background-color: unset" title="退出登录">
<i class="layui-icon layui-icon-logout" style="font-size: 20px"></i>
</a>
<a href="javascript:;">应用管理</a>
<dl class="layui-nav-child">
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
<dd><a data-path="apis" href="javascript:;">接口列表</a></dd>
<dd><a data-path="variables" href="javascript:;">变量列表</a></dd>
</dl>
</li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域 -->
<div class="layui-logo layui-bg-black logo-enhanced">{{ .SystemName }}</div>
<ul class="layui-nav layui-nav-tree" lay-shrink="all" lay-unselect lay-filter="nav-side" id="ws-nav-side">
<li class="layui-nav-item">
<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="settings" href="javascript:;">系统设置</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">应用管理</a>
<dl class="layui-nav-child">
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
<dd><a data-path="apis" href="javascript:;">接口列表</a></dd>
<dd><a data-path="variables" href="javascript:;">变量列表</a></dd>
</dl>
</li>
</ul>
</div>
</div>
<div class="layui-body">
<!-- 内容主体区域 -->
<wc-include id="router-view" allow-scripts></wc-include>
</div>
<div class="layui-footer">{{ .FooterText }}</div>
</div>
<script type="module" src="./static/js/admin.js"></script>
</body>
</html>
<div class="layui-body">
<!-- 内容主体区域 -->
<wc-include id="router-view" allow-scripts></wc-include>
</div>
<div class="layui-footer">{{ .FooterText }}</div>
</div>
<script type="module" src="./static/js/admin.js"></script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
{{/* 管理员登录页面模板使用layui构建的登录界面 */}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
@@ -21,6 +22,7 @@
align-items: center;
justify-content: center;
}
.demo-login-container {
width: 400px;
margin: 21px auto 0;
@@ -29,42 +31,48 @@
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
text-align: center;
color: #fff;
}
.login-header h1 {
margin: 0;
font-size: 24px;
font-weight: 300;
}
.login-header p {
margin: 8px 0 0;
opacity: 0.8;
font-size: 14px;
}
.login-form {
padding: 30px 20px;
}
/* 调整表单项间距 */
.login-form .layui-form-item {
margin-bottom: 20px;
}
/* 最后一个表单项不需要底部边距 */
.login-form .layui-form-item:last-child {
margin-bottom: 0;
}
.demo-login-other .layui-icon {
position: relative;
display: inline-block;
margin: 0 2px;
top: 2px;
font-size: 26px;
}
position: relative;
display: inline-block;
margin: 0 2px;
top: 2px;
font-size: 26px;
}
.login-footer {
text-align: center;
padding: 20px;
@@ -72,7 +80,7 @@
font-size: 12px;
border-top: 1px solid #f0f0f0;
}
/* 响应式设计 - 移动端适配 */
@media (max-width: 768px) {
.demo-login-container {
@@ -80,22 +88,25 @@
margin: 10px auto;
border-radius: 4px;
}
.login-header {
padding: 25px 15px;
}
.login-header h1 {
font-size: 20px;
}
.login-form {
padding: 25px 15px;
}
/* 移动端表单项间距调整 */
.login-form .layui-form-item {
margin-bottom: 18px;
}
}
@media (max-width: 480px) {
.demo-login-container {
width: 95%;
@@ -105,16 +116,19 @@
display: flex;
flex-direction: column;
}
.login-header {
padding: 20px 15px;
}
.login-header h1 {
font-size: 18px;
}
.login-form {
padding: 20px 15px;
}
/* 小屏幕表单项间距调整 */
.login-form .layui-form-item {
margin-bottom: 16px;
@@ -126,6 +140,7 @@
}
</style>
</head>
<body>
<form class="layui-form">
<div class="demo-login-container">
@@ -133,26 +148,31 @@
<h1>{{ .SystemName }}</h1>
<p>管理员登录</p>
</div>
<div class="login-form">
<!-- CSRF令牌隐藏字段 -->
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}" id="csrf-token">
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名" lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名"
lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码" lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码"
lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<div class="layui-row">
<div class="layui-col-xs7">
@@ -160,22 +180,26 @@
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-vercode"></i>
</div>
<input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码" lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
<input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码"
lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-xs5">
<div style="margin-left: 5px; text-align: right;">
<img id="captcha-img" src="/admin/captcha" onclick="this.src='/admin/captcha?t='+ new Date().getTime();" style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;" title="点击刷新验证码">
<img id="captcha-img" src="/admin/captcha"
onclick="this.src='/admin/captcha?t='+ new Date().getTime();"
style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;"
title="点击刷新验证码">
</div>
</div>
</div>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">立即登录</button>
</div>
</div>
<div class="login-footer">
<p>{{ .FooterText }}</p>
</div>
@@ -185,58 +209,63 @@
<!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
<script src="//unpkg.com/layui@2.12.1/dist/layui.js"></script>
<script>
layui.use(function(){
layui.use(function () {
var form = layui.form;
var layer = layui.layer;
// 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否
form.on('submit(demo-login)', function(data){
form.on('submit(demo-login)', function (data) {
var loadIndex = layer.load(1, {
shade: [0.1, '#fff']
});
// 获取CSRF令牌
var csrfToken = document.getElementById('csrf-token').value;
// 发送登录请求
fetch('/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data.field)
})
.then(response => response.json())
.then(result => {
layer.close(loadIndex);
// 根据统一接口code === 0 表示成功
const isOk = result && result.code === 0;
if (isOk) {
layer.msg('登录成功', {
icon: 1,
time: 1500
}, function(){
const redirect = (result.data && result.data.redirect) || '/admin';
window.location.href = redirect;
});
} else {
const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码';
layer.msg(msg, {icon: 2});
// 登录失败时刷新验证码
.then(response => response.json())
.then(result => {
layer.close(loadIndex);
// 根据统一接口code === 0 表示成功
const isOk = result && result.code === 0;
if (isOk) {
layer.msg('登录成功', {
icon: 1,
time: 1500
}, function () {
const redirect = (result.data && result.data.redirect) || '/admin';
window.location.href = redirect;
});
} else {
const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码';
layer.msg(msg, { icon: 2 });
// 登录失败时刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
}
})
.catch(error => {
layer.close(loadIndex);
console.error('登录错误:', error);
layer.msg('网络错误,请稍后重试', { icon: 2 });
// 网络错误时也刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
}
})
.catch(error => {
layer.close(loadIndex);
console.error('登录错误:', error);
layer.msg('网络错误,请稍后重试', {icon: 2});
// 网络错误时也刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
});
});
return false; // 阻止表单跳转
});
});
</script>
</body>
</html>

View File

@@ -1,7 +1,6 @@
{{ define "settings.html" }}
<section>
<h2>系统设置</h2>
<!-- 基本信息设置 -->
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">基本信息设置</div>
@@ -34,7 +33,7 @@
</form>
</div>
</div>
<!-- 系统配置设置 -->
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">系统配置</div>
@@ -61,7 +60,8 @@
<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" class="layui-input" style="width: 120px;" />
<input type="number" name="session_timeout" placeholder="3600" min="300" max="86400" class="layui-input"
style="width: 120px;" />
<span class="layui-form-mid">300-86400秒</span>
</div>
</div>
@@ -70,7 +70,7 @@
</form>
</div>
</div>
<!-- 页脚与备案信息 -->
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">页脚与备案</div>
@@ -103,13 +103,14 @@
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="psb-record-link">备案链接</label>
<div class="layui-input-block">
<input type="url" name="psb_record_link" placeholder="http://www.beian.gov.cn/portal/registerSystemInfo" class="layui-input" />
<input type="url" name="psb_record_link" placeholder="http://www.beian.gov.cn/portal/registerSystemInfo"
class="layui-input" />
</div>
</div>
</form>
</div>
</div>
<!-- 操作按钮 -->
<div class="layui-form-item" style="margin-top: 24px;">
<div class="layui-input-block">
@@ -129,159 +130,159 @@
}
}
waitForLayui(function() {
layui.use(['jquery', 'form', 'layer', 'util'], function() {
waitForLayui(function () {
layui.use(['jquery', 'form', 'layer', 'util'], function () {
const { $, form, layer, util } = layui;
// 缓存上次加载的设置值,用于“重置”恢复
let originalSettings = {};
// 缓存上次加载的设置值,用于“重置”恢复
let originalSettings = {};
/**
* 加载后台所有设置并回填到三个表单
* - 从 /admin/api/settings 获取 name:value 映射
* - 处理开关型字段maintenance_mode
* - 渲染 layui 组件
*/
const loadSettings = async () => {
try {
const res = await fetch('/admin/api/settings', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (data.code !== 0) {
layer.msg(data.msg || '加载设置失败', { icon: 2 });
return;
}
originalSettings = data.data || {};
fillForms(originalSettings);
} catch (err) {
console.error('获取设置失败:', err);
layer.msg('网络错误,无法加载设置', { icon: 2 });
}
};
/**
* 将 settings 数据回填到各表单控件
* - 文本/文本域/下拉:直接赋值
* - 开关:根据 "1"/"0" 置为选中/未选中
*/
const fillForms = (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');
// 页脚与备案
$('[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 || '');
// 渲染 layui 组件
form.render();
};
/**
* 收集某个表单下所有可用控件的值
* - 统一将 checkbox 转为 "1"/"0"
* - 其他控件转为字符串,避免后端类型不一致
*/
const collectForm = (selector) => {
const obj = {};
const $form = $(selector);
$form.find('input, textarea, select').each(function() {
const $el = $(this);
const name = $el.attr('name');
if (!name) return; // 无 name 不纳入
const type = ($el.attr('type') || '').toLowerCase();
let value = '';
if (type === 'checkbox') {
value = $el.prop('checked') ? '1' : '0';
} else {
value = ($el.val() ?? '').toString();
}
obj[name] = value;
});
return obj;
};
/**
* 汇总三个表单的字段为一个扁平对象
*/
const collectAllSettings = () => {
return {
...collectForm('#basicForm'),
...collectForm('#systemForm'),
...collectForm('#footerForm'),
};
};
/**
* 处理“保存所有设置”点击
* - 二次确认后提交
* - 显示加载中,防重复提交
* - 成功后提示并刷新缓存的 originalSettings
*/
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');
/**
* 加载后台所有设置并回填到三个表单
* - 从 /admin/api/settings 获取 name:value 映射
* - 处理开关型字段maintenance_mode
* - 渲染 layui 组件
*/
const loadSettings = async () => {
try {
const res = await fetch('/admin/api/settings', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
});
};
const data = await res.json();
if (data.code !== 0) {
layer.msg(data.msg || '加载设置失败', { icon: 2 });
return;
}
originalSettings = data.data || {};
fillForms(originalSettings);
} catch (err) {
console.error('获取设置失败:', err);
layer.msg('网络错误,无法加载设置', { icon: 2 });
}
};
/**
* 处理“重置”点击
* - 恢复为上次加载的 originalSettings
*/
const handleReset = () => {
fillForms(originalSettings);
layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 });
};
/**
* 将 settings 数据回填到各表单控件
* - 文本/文本域/下拉:直接赋值
* - 开关:根据 "1"/"0" 置为选中/未选中
*/
const fillForms = (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 || '');
// 事件绑定
$('#saveAllBtn').off('click').on('click', handleSaveAll);
$('#resetBtn').off('click').on('click', handleReset);
// 系统配置
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');
// 初始化:加载设置
loadSettings();
// 页脚与备案
$('[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 || '');
// 渲染 layui 组件
form.render();
};
/**
* 收集某个表单下所有可用控件的值
* - 统一将 checkbox 转为 "1"/"0"
* - 其他控件转为字符串,避免后端类型不一致
*/
const collectForm = (selector) => {
const obj = {};
const $form = $(selector);
$form.find('input, textarea, select').each(function () {
const $el = $(this);
const name = $el.attr('name');
if (!name) return; // 无 name 不纳入
const type = ($el.attr('type') || '').toLowerCase();
let value = '';
if (type === 'checkbox') {
value = $el.prop('checked') ? '1' : '0';
} else {
value = ($el.val() ?? '').toString();
}
obj[name] = value;
});
return obj;
};
/**
* 汇总三个表单的字段为一个扁平对象
*/
const collectAllSettings = () => {
return {
...collectForm('#basicForm'),
...collectForm('#systemForm'),
...collectForm('#footerForm'),
};
};
/**
* 处理“保存所有设置”点击
* - 二次确认后提交
* - 显示加载中,防重复提交
* - 成功后提示并刷新缓存的 originalSettings
*/
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');
});
});
};
/**
* 处理“重置”点击
* - 恢复为上次加载的 originalSettings
*/
const handleReset = () => {
fillForms(originalSettings);
layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 });
};
// 事件绑定
$('#saveAllBtn').off('click').on('click', handleSaveAll);
$('#resetBtn').off('click').on('click', handleReset);
// 初始化:加载设置
loadSettings();
});
});
</script>

View File

@@ -1,472 +1,357 @@
{{ define "user.html" }}
<style>
/* 基础模块样式 */
.user-module {
margin-bottom: 20px;
}
.user-module .layui-card-header {
font-weight: 600;
transition: background-color 0.3s ease, color 0.3s ease;
}
.module-tabs {
margin-bottom: 20px;
}
.module-tabs .layui-tab-title li {
font-weight: 500;
}
.readonly-field {
cursor: not-allowed !important;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 浅色模式样式 */
:root {
--user-card-header-bg: #f8f9fa;
--user-card-header-color: #333;
--user-readonly-bg: #f5f5f5;
--user-readonly-color: #666;
--user-card-bg: #ffffff;
--user-card-border: #e6e6e6;
--user-input-bg: #ffffff;
--user-input-border: #d9d9d9;
--user-input-color: #333;
}
/* 深色模式样式 */
@media (prefers-color-scheme: dark) {
:root {
--user-card-header-bg: #2f2f2f;
--user-card-header-color: #e6e6e6;
--user-readonly-bg: #3a3a3a;
--user-readonly-color: #999;
--user-card-bg: #1f1f1f;
--user-card-border: #404040;
--user-input-bg: #2a2a2a;
--user-input-border: #404040;
--user-input-color: #e6e6e6;
}
}
/* 手动深色模式类 */
.dark {
--user-card-header-bg: #2f2f2f;
--user-card-header-color: #e6e6e6;
--user-readonly-bg: #3a3a3a;
--user-readonly-color: #999;
--user-card-bg: #1f1f1f;
--user-card-border: #404040;
--user-input-bg: #2a2a2a;
--user-input-border: #404040;
--user-input-color: #e6e6e6;
}
/* 应用CSS变量到元素 */
.user-module .layui-card-header {
background-color: var(--user-card-header-bg) !important;
color: var(--user-card-header-color) !important;
}
.readonly-field {
background-color: var(--user-readonly-bg) !important;
color: var(--user-readonly-color) !important;
}
.user-module .layui-card {
background-color: var(--user-card-bg);
border-color: var(--user-card-border);
}
.user-module .layui-input {
background-color: var(--user-input-bg);
border-color: var(--user-input-border);
color: var(--user-input-color);
}
/* 确保表单元素在深色模式下的可读性 */
.user-module .layui-form-label {
color: var(--user-card-header-color);
}
/* 按钮在深色模式下的样式调整 */
.user-module .layui-btn-primary {
background-color: var(--user-input-bg);
border-color: var(--user-input-border);
color: var(--user-input-color);
}
.user-module .layui-btn-primary:hover {
background-color: var(--user-readonly-bg);
}
/* 标签页在深色模式下的样式 */
.module-tabs .layui-tab-title {
border-bottom-color: var(--user-card-border);
}
.module-tabs .layui-tab-title li {
color: var(--user-input-color);
}
.module-tabs .layui-tab-title .layui-this {
color: var(--lay-color-primary, #1e9fff);
}
/* 图标颜色适配 */
.user-module .layui-icon {
color: var(--user-card-header-color);
}
</style>
<div class="layui-tab layui-tab-brief module-tabs" lay-filter="userTabs">
<ul class="layui-tab-title">
<li class="layui-this">个人资料</li>
<li>修改密码</li>
<li>修改用户名</li>
</ul>
<div class="layui-tab-content">
<!-- 个人资料模块 -->
<div class="layui-tab-item layui-show">
<div class="layui-card user-module">
<div class="layui-card-header">
<i class="layui-icon layui-icon-user"></i> 个人资料
</div>
<div class="layui-card-body">
<form class="layui-form" id="profileForm" lay-filter="profileForm">
<div class="layui-form-item">
<label class="layui-form-label">UUID</label>
<div class="layui-input-block">
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field" style="font-family: monospace; font-size: 12px;" />
<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>
<li>修改用户名</li>d
</ul>
<div class="layui-tab-content">
<!-- 个人资料模块 -->
<div class="layui-tab-item layui-show">
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">个人资料</div>
<div class="layui-card-body">
<form class="layui-form" id="profileForm" lay-filter="profileForm">
<div class="layui-form-item">
<label class="layui-form-label">UUID</label>
<div class="layui-input-block">
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field"
style="font-family: monospace; font-size: 12px;" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户组</label>
<div class="layui-input-block">
<input type="text" name="role" disabled readonly class="layui-input readonly-field" />
<div class="layui-form-item">
<label class="layui-form-label">用户组</label>
<div class="layui-input-block">
<input type="text" name="role" disabled readonly class="layui-input readonly-field" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" disabled readonly class="layui-input readonly-field" />
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" disabled readonly class="layui-input readonly-field" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">创建时间</label>
<div class="layui-input-block">
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
<div class="layui-form-item">
<label class="layui-form-label">创建时间</label>
<div class="layui-input-block">
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
</div>
</div>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
<!-- 修改密码模块 -->
<div class="layui-tab-item">
<div class="layui-card user-module">
<div class="layui-card-header">
<i class="layui-icon layui-icon-password"></i> 修改密码
</div>
<div class="layui-card-body">
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
<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" lay-verify="required" />
<!-- 修改密码模块 -->
<div class="layui-tab-item">
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">修改密码</div>
<div class="layui-card-body">
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
<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" lay-verify="required" />
</div>
</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" lay-verify="required" />
<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" lay-verify="required" />
</div>
</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" lay-verify="required" />
<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" lay-verify="required" />
</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 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>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
<!-- 修改用户名模块 -->
<div class="layui-tab-item">
<div class="layui-card user-module">
<div class="layui-card-header">
<i class="layui-icon layui-icon-edit"></i> 修改用户名
</div>
<div class="layui-card-body">
<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 class="layui-tab-item">
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">修改用户名</div>
<div class="layui-card-body">
<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>
<div class="layui-form-item">
<label class="layui-form-label">新用户名</label>
<div class="layui-input-block">
<input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input" lay-verify="required" />
<div class="layui-form-item">
<label class="layui-form-label">新用户名</label>
<div class="layui-input-block">
<input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input"
lay-verify="required" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">当前密码</label>
<div class="layui-input-block">
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off" class="layui-input" lay-verify="required" />
<div class="layui-form-item">
<label class="layui-form-label">当前密码</label>
<div class="layui-input-block">
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off"
class="layui-input" lay-verify="required" />
</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 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>
</div>
</form>
</form>
</div>
</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 userProfile = null
// 加载个人资料
const loadProfile = 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 || '加载失败')
userProfile = data.data || {}
// 填充个人资料表单
const profileData = {
...userProfile,
role: roleToText(userProfile.role),
created_at: formatTime(userProfile.created_at)
}
form.val('profileForm', profileData)
// 填充用户名修改表单的当前用户名
form.val('usernameForm', { current_username: userProfile.username })
} catch (e) {
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
}
<script>
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
(() => {
// 工具方法:将数值角色转为中文标签
const roleToText = (role) => {
const r = typeof role === 'string' ? parseInt(role, 10) : role
return r === 0 ? '管理员' : '普通成员'
}
// 修改密码模块
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 }
},
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
submit: async (fields) => {
const validation = PasswordModule.validate(fields)
if (!validation.ok) {
layer.msg(validation.msg, { icon: 2 })
return false
}
// 如果未加载 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)
})
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()
// 在确保 Layui 可用后再执行页面逻辑
ensureLayui().then(() => {
layui.use(['form', 'layer', 'element'], () => {
const form = layui.form
const layer = layui.layer
const element = layui.element
// 全局变量
let userProfile = null
// 加载个人资料
const loadProfile = 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 || '加载失败')
userProfile = data.data || {}
// 填充个人资料表单
const profileData = {
...userProfile,
role: roleToText(userProfile.role),
created_at: formatTime(userProfile.created_at)
}
form.val('profileForm', profileData)
// 填充用户名修改表单的当前用户名
form.val('usernameForm', { current_username: userProfile.username })
} catch (e) {
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
}
} 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 === userProfile?.username) {
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 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 === userProfile?.username) {
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 loadProfile()
// 清空表单(不显示重置提示)
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
})
} catch (e) {
layer.msg(e.message || '修改用户名失败', { icon: 2 })
}
return false
},
reset: () => {
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
})
})
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 loadProfile()
// 清空表单
UsernameModule.reset()
} catch (e) {
layer.msg(e.message || '修改用户名失败', { icon: 2 })
layer.msg('表单已重置', { icon: 1 })
}
}
return false
},
reset: () => {
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
// 绑定表单提交事件
form.on('submit(submitPassword)', (obj) => {
return PasswordModule.submit(obj.field)
})
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)
// 初始化加载
loadProfile()
})
})
form.on('submit(submitUsername)', (obj) => {
return UsernameModule.submit(obj.field)
})
// 绑定重置按钮
document.getElementById('resetPasswordBtn')?.addEventListener('click', PasswordModule.reset)
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
// 初始化加载
loadProfile()
})
})
})()
</script>
})()
</script>
</section>
{{ end }}

View File

@@ -3,7 +3,8 @@
<h2>变量管理</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddVariable"><i class="layui-icon layui-icon-add-1"></i> 新增变量</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i>
批量删除</button>
</div>
<div class="layui-card" style="margin-top:12px">
@@ -62,7 +63,8 @@
<div class="layui-form-item">
<label class="layui-form-label">变量别名</label>
<div class="layui-input-block">
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)" autocomplete="off" class="layui-input" />
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)"
autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
@@ -90,47 +92,47 @@
}
}
waitForLayui(function() {
layui.use(['table', 'form', 'layer', 'element'], function() {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
waitForLayui(function () {
layui.use(['table', 'form', 'layer', 'element'], function () {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
// 自定义验证规则
form.verify({
alias: function(value) {
if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母';
// 自定义验证规则
form.verify({
alias: function (value) {
if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母';
}
}
}
});
});
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 加载应用列表到下拉框
function loadApps() {
$.ajax({
url: '/admin/variable/apps',
type: 'GET',
success: function(res) {
success: function (res) {
if (res.code === 0 && res.data) {
let options = '<option value="">请选择应用</option>';
res.data.forEach(function(app) {
res.data.forEach(function (app) {
options += '<option value="' + app.uuid + '">' + app.name + '</option>';
});
$('select[name="app_uuid"]').html(options);
form.render('select');
}
},
error: function() {
layer.msg('加载应用列表失败', {icon: 2});
error: function () {
layer.msg('加载应用列表失败', { icon: 2 });
}
});
}
@@ -143,7 +145,7 @@
elem: '#variablesTable',
id: 'variablesTable',
url: '/admin/variable/list',
parseData: function(res) {
parseData: function (res) {
return {
code: res.code,
msg: res.msg || '',
@@ -160,20 +162,20 @@
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function(res, curr, count) {
done: function (res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{type: 'checkbox', width: 50},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'app_name', title: '应用名称', minWidth: 120},
{field: 'number', title: '变量编号', width: 180},
{field: 'alias', title: '变量别名', minWidth: 150},
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'app_name', title: '应用名称', minWidth: 120 },
{ field: 'number', title: '变量编号', width: 180 },
{ field: 'alias', title: '变量别名', minWidth: 150 },
{
field: 'data',
title: '变量数据',
field: 'data',
title: '变量数据',
minWidth: 200,
templet: function(d) {
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.data && d.data.length > 50) {
return '<span title="' + d.data + '">' + d.data.substring(0, 50) + '...</span>';
@@ -182,10 +184,10 @@
}
},
{
field: 'remark',
title: '备注',
field: 'remark',
title: '备注',
minWidth: 150,
templet: function(d) {
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.remark && d.remark.length > 30) {
return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>';
@@ -194,32 +196,32 @@
}
},
{
field: 'created_at',
title: '创建时间',
field: 'created_at',
title: '创建时间',
width: 180,
templet: function(d) {
templet: function (d) {
return formatDateTime(d.created_at);
}
},
{title: '操作', width: 180, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right'}
{ title: '操作', width: 180, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right' }
]]
});
// 监听应用选择变化
form.on('select(appSelect)', function(data) {
variablesTable.reload({
where: {
app_uuid: data.value,
search: $('input[name="search"]').val()
},
page: {
curr: 1
}
});
});
form.on('select(appSelect)', function (data) {
variablesTable.reload({
where: {
app_uuid: data.value,
search: $('input[name="search"]').val()
},
page: {
curr: 1
}
});
});
// 搜索功能
$('#btnSearchVariables').on('click', function() {
$('#btnSearchVariables').on('click', function () {
variablesTable.reload({
where: {
app_uuid: $('select[name="app_uuid"]').val(),
@@ -232,7 +234,7 @@
});
// 重置搜索
$('#btnResetVariables').on('click', function() {
$('#btnResetVariables').on('click', function () {
$('#variableFilterForm')[0].reset();
form.render();
variablesTable.reload({
@@ -244,72 +246,72 @@
});
// 新增变量
$('#btnAddVariable').on('click', function() {
$('#btnAddVariable').on('click', function () {
console.log('新增变量按钮被点击');
$('#variableForm')[0].reset();
$('input[name="id"]').val('');
// 重新加载应用列表到表单中
loadApps();
layer.open({
type: 1,
title: '新增变量',
content: $('#variableFormLayer'),
area: ['500px', '460px'],
btn: ['创建', '取消'],
yes: function(index, layero) {
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function() {
$('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
console.log('新增变量 - 收集到的表单数据:', formData);
// 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2});
layer.msg('应用UUID不能为空', { icon: 2 });
return;
}
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2});
layer.msg('请输入变量别名', { icon: 2 });
return;
}
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2});
layer.msg('请输入变量数据', { icon: 2 });
return;
}
console.log('新增变量 - 发送的JSON数据:', JSON.stringify(formData));
$.ajax({
url: '/admin/variable/create',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function(res) {
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.msg(res.msg, { icon: 1 });
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', {icon: 2});
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2});
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function(index) {
btn2: function (index) {
layer.close(index);
},
success: function() {
success: function () {
form.render();
},
shadeClose: false
@@ -317,32 +319,32 @@
});
// 批量删除
$('#btnBatchDeleteVariables').on('click', function() {
$('#btnBatchDeleteVariables').on('click', function () {
const checkStatus = table.checkStatus('variablesTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要删除的变量', {icon: 2});
layer.msg('请选择要删除的变量', { icon: 2 });
return;
}
layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', {icon: 3, title: '提示'}, function(index) {
layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', { icon: 3, title: '提示' }, function (index) {
const ids = data.map(item => item.id);
$.ajax({
url: '/admin/variable/batch_delete',
type: 'POST',
data: JSON.stringify({ids: ids}),
data: JSON.stringify({ ids: ids }),
contentType: 'application/json',
success: function(res) {
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.msg(res.msg, { icon: 1 });
variablesTable.reload();
} else {
layer.msg(res.msg || '批量删除失败', {icon: 2});
layer.msg(res.msg || '批量删除失败', { icon: 2 });
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '批量删除失败', {icon: 2});
error: function (xhr) {
layer.msg(xhr.responseText || '批量删除失败', { icon: 2 });
}
});
layer.close(index);
@@ -350,107 +352,107 @@
});
// 表格工具栏事件
table.on('tool(variablesTableFilter)', function(obj) {
table.on('tool(variablesTableFilter)', function (obj) {
const data = obj.data;
if (obj.event === 'edit') {
// 编辑
console.log('编辑按钮被点击', data);
$('#variableForm')[0].reset();
$('input[name="uuid"]').val(data.uuid);
// 重新加载应用列表,然后设置选中值
loadApps();
setTimeout(function() {
setTimeout(function () {
$('select[name="app_uuid"]').val(data.app_uuid);
form.render('select');
}, 100);
$('input[name="alias"]').val(data.alias);
$('textarea[name="data"]').val(data.data);
$('textarea[name="remark"]').val(data.remark);
layer.open({
type: 1,
title: '编辑变量',
content: $('#variableFormLayer'),
area: ['500px', '460px'],
btn: ['保存', '取消'],
yes: function(index, layero) {
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function() {
$('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
console.log('编辑变量 - 收集到的表单数据:', formData);
// 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2});
layer.msg('应用UUID不能为空', { icon: 2 });
return;
}
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2});
layer.msg('请输入变量别名', { icon: 2 });
return;
}
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2});
layer.msg('请输入变量数据', { icon: 2 });
return;
}
console.log('编辑变量 - 发送的JSON数据:', JSON.stringify(formData));
$.ajax({
url: '/admin/variable/update',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function(res) {
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.msg(res.msg, { icon: 1 });
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', {icon: 2});
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2});
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function(index) {
btn2: function (index) {
layer.close(index);
},
success: function() {
success: function () {
form.render();
},
shadeClose: false
});
} else if (obj.event === 'del') {
// 删除
layer.confirm('确定删除该变量吗?', {icon: 3, title: '提示'}, function(index) {
layer.confirm('确定删除该变量吗?', { icon: 3, title: '提示' }, function (index) {
$.ajax({
url: '/admin/variable/delete',
type: 'POST',
data: JSON.stringify({id: data.id}),
data: JSON.stringify({ id: data.id }),
contentType: 'application/json',
success: function(res) {
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.msg(res.msg, { icon: 1 });
variablesTable.reload();
} else {
layer.msg(res.msg || '删除失败', {icon: 2});
layer.msg(res.msg || '删除失败', { icon: 2 });
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '删除失败', {icon: 2});
error: function (xhr) {
layer.msg(xhr.responseText || '删除失败', { icon: 2 });
}
});
layer.close(index);

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>{{.SystemName}} - 生活就像愤怒的小鸟,失败后总有几只猪在笑。</title>
<!-- 站 点 协 议 -->
@@ -17,9 +18,10 @@
<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"/>
<link rel="stylesheet" href="//lib.baomitu.com/layui/2.8.17/css/layui.css" />
<style>
html, body {
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
@@ -69,6 +71,7 @@
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);
}
@@ -79,7 +82,7 @@
border: 2px solid rgba(0, 212, 255, 0.3);
border-radius: 15px;
padding: 30px 25px;
box-shadow:
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);
@@ -100,8 +103,13 @@
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.box-form .layui-form-item {
@@ -119,8 +127,15 @@
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.info-text {
@@ -141,11 +156,11 @@
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;
@@ -174,193 +189,197 @@
}
</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) {
<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();
}
// 随机改变透明度
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();
// 重置粒子位置和属性
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 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;
// 鼠标附近的粒子会被吸引
// 创建粒子数组
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 => {
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;
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);
}
});
});
// 点击时添加新粒子
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>
};
// 启动粒子系统
initParticles();
addMouseInteraction();
animate();
</script>
</body>
</html>