Remove the card password related

New application-related
This commit is contained in:
2025-10-24 01:48:54 +08:00
parent 11bff937bd
commit f03d2d0b12
22 changed files with 657 additions and 3623 deletions

View File

@@ -33,8 +33,13 @@
<div class="layui-card-body">
<table id="appsTable" lay-filter="appsTableFilter"></table>
<script type="text/html" id="tpl-apps-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
<div style="white-space: nowrap;">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="more">
更多 <i class="layui-icon layui-icon-down"></i>
</a>
</div>
</script>
<script type="text/html" id="tpl-apps-status">
{{`{{# if(d.status === 1) { }}`}}
@@ -57,37 +62,29 @@
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">版本</label>
<label class="layui-form-label">应用版本</label>
<div class="layui-input-block">
<input type="text" name="version" placeholder="请输入版本默认1.0.0" autocomplete="off" class="layui-input" />
<input type="text" name="version" placeholder="请输入应用版本默认1.0.0" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-form-item" pane>
<label class="layui-form-label">应用状态</label>
<div class="layui-input-block">
<select name="status">
<option value="1" selected>启用</option>
<option value="0">禁用</option>
</select>
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" checked>
</div>
</div>
<div class="layui-form-item">
<div class="layui-form-item" pane>
<label class="layui-form-label">强制更新</label>
<div class="layui-input-block">
<select name="force_update">
<option value="0" selected>不开启</option>
<option value="1">开启</option>
</select>
<input type="checkbox" name="force_update" lay-skin="switch" lay-text="开启|关闭">
</div>
</div>
<div class="layui-form-item">
<div class="layui-form-item" pane>
<label class="layui-form-label">更新方式</label>
<div class="layui-input-block">
<select name="download_type" lay-filter="downloadTypeChange">
<option value="0" selected>不启用更新</option>
<option value="1">自动更新</option>
<option value="2">手动下载</option>
</select>
<input type="radio" name="download_type" value="0" title="不启用" checked lay-filter="downloadTypeChange">
<input type="radio" name="download_type" value="1" title="自动更新" lay-filter="downloadTypeChange">
<input type="radio" name="download_type" value="2" title="手动下载" lay-filter="downloadTypeChange">
</div>
</div>
<div class="layui-form-item" id="downloadUrlItem">
@@ -106,10 +103,11 @@
</div>
<script>
layui.use(['table', 'form', 'layer', 'element'], function() {
layui.use(['table', 'form', 'layer', 'element', 'dropdown'], function() {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const dropdown = layui.dropdown;
const $ = layui.$;
// 格式化时间函数
@@ -149,10 +147,10 @@
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'name', title: '应用名称', minWidth: 180 },
{ field: 'uuid', title: 'UUID', minWidth: 320 },
{ field: 'version', title: '版本', width: 100 },
{ field: 'version', title: '应用版本', width: 100 },
{
field: 'status',
title: '状态',
title: '应用状态',
width: 100,
templet: (d) => {
if (d.status === 1) return '<span style="color: #5FB878;">启用</span>';
@@ -171,7 +169,7 @@
width: 180,
templet: (d) => formatDateTime(d.created_at)
},
{ fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 120 }
{ fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 180 }
]]
});
@@ -216,7 +214,7 @@
});
// 监听更新方式切换(保留事件监听器以备将来扩展)
form.on('select(downloadTypeChange)', function(data) {
form.on('radio(downloadTypeChange)', function(data) {
// 下载地址字段现在始终显示,无需切换显示状态
});
@@ -228,9 +226,9 @@
// 转换字段类型为正确的数据类型
const formData = {
...data.field,
status: parseInt(data.field.status) || 0,
status: data.field.status === 'on' ? 1 : 0, // switch开关处理
download_type: parseInt(data.field.download_type) || 0,
force_update: parseInt(data.field.force_update) || 0
force_update: data.field.force_update === 'on' ? 1 : 0 // switch开关处理
};
// 如果是编辑模式确保id也是整数
@@ -274,10 +272,13 @@
$('input[name="id"]').val(data.id);
$('input[name="name"]').val(data.name);
$('input[name="version"]').val(data.version);
$('select[name="status"]').val(data.status);
$('select[name="download_type"]').val(data.download_type || 0);
// 设置应用状态开关
$('input[name="status"]').prop('checked', data.status === 1);
// 设置更新方式单选按钮
$('input[name="download_type"][value="' + (data.download_type || 0) + '"]').prop('checked', true);
$('input[name="download_url"]').val(data.download_url || '');
$('select[name="force_update"]').val(data.force_update || 0);
// 设置强制更新开关
$('input[name="force_update"]').prop('checked', data.force_update === 1);
layer.open({
type: 1,
@@ -311,6 +312,248 @@
});
layer.close(index);
});
} else if (obj.event === 'more') {
// 更多操作下拉菜单
dropdown.render({
elem: this, // 使用 this 而不是查找元素
show: true, // 外部事件触发即显示
data: [
{
title: '程序公告',
id: 'announcement'
},
{
title: '多开配置',
id: 'multi_instance'
},
{
title: '重置密钥',
id: 'reset_secret'
}
],
click: function(menudata, othis) {
if (menudata.id === 'announcement') {
// 程序公告
// 先获取当前公告内容
$.ajax({
url: '/admin/api/apps/get_announcement?uuid=' + obj.data.uuid,
type: 'GET',
success: function(res) {
var currentAnnouncement = '';
if (res.code === 0 && res.data && res.data.announcement) {
currentAnnouncement = res.data.announcement;
}
// 显示编辑弹窗
layer.open({
type: 1,
title: '编辑程序公告 - ' + obj.data.name,
area: ['600px', '400px'],
content: '<div style="padding: 20px;">' +
'<textarea id="announcementEditor" class="layui-textarea" placeholder="请输入程序公告内容..." style="height: 250px;">' +
currentAnnouncement +
'</textarea>' +
'</div>',
btn: ['保存', '取消'],
yes: function(index, layero) {
var announcementContent = $('#announcementEditor').val();
// 发送更新请求
$.ajax({
url: '/admin/api/apps/update_announcement',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
uuid: obj.data.uuid,
announcement: announcementContent
}),
success: function(res) {
if (res.code === 0) {
layer.msg('程序公告更新成功!', {
icon: 1,
time: 2000
});
layer.close(index);
} else {
layer.msg(res.msg || '更新程序公告失败', {icon: 2});
}
},
error: function() {
layer.msg('网络错误,请稍后重试', {icon: 2});
}
});
},
btn2: function(index) {
layer.close(index);
}
});
},
error: function() {
layer.msg('获取程序公告失败,请稍后重试', {icon: 2});
}
});
} else if (menudata.id === 'multi_instance') {
// 多开配置
$.ajax({
url: '/admin/api/apps/get_multi_config?uuid=' + obj.data.uuid,
type: 'GET',
success: function(config) {
layer.open({
type: 1,
title: '多开配置 - ' + obj.data.name,
area: ['550px', '450px'],
content: '<div style="padding: 20px;">' +
'<form class="layui-form layui-form-pane" lay-filter="multiConfigForm">' +
'<div class="layui-form-item" pane>' +
'<label class="layui-form-label">登录方式</label>' +
'<div class="layui-input-block">' +
'<input type="radio" name="login_type" value="0" title="顶号登录" ' + (config.login_type === 0 ? 'checked' : '') + '>' +
'<input type="radio" name="login_type" value="1" title="非顶号登录" ' + (config.login_type === 1 ? 'checked' : '') + '>' +
'</div>' +
'</div>' +
'<div class="layui-form-item" pane>' +
'<label class="layui-form-label">多开范围</label>' +
'<div class="layui-input-block">' +
'<input type="radio" name="multi_open_scope" value="0" title="单电脑" ' + (config.multi_open_scope === 0 ? 'checked' : '') + '>' +
'<input type="radio" name="multi_open_scope" value="1" title="单IP" ' + (config.multi_open_scope === 1 ? 'checked' : '') + '>' +
'<input type="radio" name="multi_open_scope" value="2" title="全部电脑" ' + (config.multi_open_scope === 2 ? 'checked' : '') + '>' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<div class="layui-inline">' +
'<label class="layui-form-label">清理间隔</label>' +
'<div class="layui-input-inline">' +
'<input type="number" name="clean_interval" class="layui-input" value="' + config.clean_interval + '" placeholder="请输入" lay-verify="required|number" min="1">' +
'</div>' +
'<div class="layui-form-mid layui-text-em">小时</div>' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<div class="layui-inline">' +
'<label class="layui-form-label">校验间隔</label>' +
'<div class="layui-input-inline">' +
'<input type="number" name="check_interval" class="layui-input" value="' + config.check_interval + '" placeholder="请输入" lay-verify="required|number" min="1">' +
'</div>' +
'<div class="layui-form-mid layui-text-em">分钟</div>' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">多开数量</label>' +
'<div class="layui-input-block">' +
'<input type="number" name="multi_open_count" class="layui-input" value="' + config.multi_open_count + '" placeholder="请输入允许的多开数量" lay-verify="required|number" min="1">' +
'</div>' +
'</div>' +
'</form>' +
'</div>',
btn: ['保存', '取消'],
yes: function(index, layero) {
var formData = {
uuid: obj.data.uuid,
login_type: parseInt($('input[name="login_type"]:checked').val()),
multi_open_scope: parseInt($('input[name="multi_open_scope"]:checked').val()),
clean_interval: parseInt($('input[name="clean_interval"]').val()),
check_interval: parseInt($('input[name="check_interval"]').val()),
multi_open_count: parseInt($('input[name="multi_open_count"]').val())
};
// 验证数据
if (isNaN(formData.login_type) || formData.login_type < 0 || formData.login_type > 1) {
layer.msg('请选择登录方式', {icon: 2});
return;
}
if (isNaN(formData.multi_open_scope) || formData.multi_open_scope < 0 || formData.multi_open_scope > 2) {
layer.msg('请选择多开范围', {icon: 2});
return;
}
if (isNaN(formData.clean_interval) || formData.clean_interval < 1) {
layer.msg('清理间隔必须大于0', {icon: 2});
return;
}
if (isNaN(formData.check_interval) || formData.check_interval < 1) {
layer.msg('校验间隔必须大于0', {icon: 2});
return;
}
if (isNaN(formData.multi_open_count) || formData.multi_open_count < 1) {
layer.msg('多开数量必须大于0', {icon: 2});
return;
}
// 发送更新请求
$.ajax({
url: '/admin/api/apps/update_multi_config',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(res) {
if (res.message) {
layer.msg('多开配置更新成功', {icon: 1});
layer.close(index);
table.reload('appsTable');
} else {
layer.msg(res.msg || '更新多开配置失败', {icon: 2});
}
},
error: function() {
layer.msg('网络错误,请稍后重试', {icon: 2});
}
});
},
btn2: function(index) {
layer.close(index);
},
success: function() {
// 重新渲染表单
form.render();
}
});
},
error: function() {
layer.msg('获取多开配置失败,请稍后重试', {icon: 2});
}
});
} else if (menudata.id === 'reset_secret') {
// 重置密钥
layer.confirm('确定重置该应用的密钥吗?重置后原密钥将失效!', {icon: 3, title: '提示'}, function(index) {
// 发送重置密钥请求
$.ajax({
url: '/admin/api/apps/reset_secret',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
uuid: obj.data.uuid
}),
success: function(res) {
if (res.code === 0) {
layer.msg('密钥重置成功!', {
icon: 1,
time: 2000 // 显示2秒
});
// 刷新表格数据
table.reload('appsTable');
} else {
layer.msg(res.msg || '重置密钥失败', {icon: 2});
}
},
error: function(xhr) {
let errorMsg = '重置密钥失败';
if (xhr.responseText) {
try {
const errorRes = JSON.parse(xhr.responseText);
errorMsg = errorRes.msg || errorMsg;
} catch (e) {
errorMsg = xhr.responseText;
}
}
layer.msg(errorMsg, {icon: 2});
}
});
layer.close(index);
});
}
},
align: 'right', // 右对齐弹出
style: 'box-shadow: 1px 1px 10px rgb(0 0 0 / 12%);' // 设置额外样式
});
}
});

View File

@@ -1,415 +0,0 @@
{{ define "card_types.html" }}
<section>
<h2>卡密类型管理</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddCardType"><i class="layui-icon layui-icon-add-1"></i> 新增类型</button>
<button class="layui-btn layui-btn-normal" id="btnBatchEnableCardTypes"><i class="layui-icon layui-icon-ok-circle"></i> 批量启用</button>
<button class="layui-btn layui-btn-warm" id="btnBatchDisableCardTypes"><i class="layui-icon layui-icon-close-fill"></i> 批量禁用</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteCardTypes"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">筛选</div>
<div class="layui-card-body">
<form class="layui-form layui-form-pane" id="cardTypeFilterForm" lay-filter="cardTypeFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">名称</label>
<div class="layui-input-inline">
<input type="text" name="keyword" placeholder="卡密类型名称" autocomplete="off" class="layui-input" />
</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">
<button type="button" class="layui-btn" id="btnSearchCardTypes">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetCardTypes">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">卡密类型列表</div>
<div class="layui-card-body">
<table id="cardTypesTable" lay-filter="cardTypesTableFilter"></table>
<script type="text/html" id="tpl-cardtypes-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
</div>
<!-- 隐藏的表单弹层内容 -->
<div id="cardTypeFormModal" style="display:none;padding:16px">
<!-- 参考demo表单2样式添加layui-form-pane类实现方框风格 -->
<form class="layui-form layui-form-pane" id="cardTypeForm">
<input type="hidden" name="id" />
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input type="text" name="name" required lay-verify="required" placeholder="请输入类型名称" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">登录</label>
<div class="layui-input-block">
<input type="text" name="login_types" placeholder="请输入登录方式名称,多个用逗号分隔" class="layui-input" />
</div>
</div>
<!-- 操作按钮已移除,统一由 layer.open 的 btn 控制“提交/取消” -->
<!-- 操作按钮移除:统一由 layer.open 的 btn 控制“提交/取消” -->
</form>
</div>
</section>
<script>
layui.use(['table', 'form', 'layer'], () => {
const { table, form, layer, $ } = layui;
let currentFormLayerIndex; // 保存当前表单弹窗的索引
// 渲染表格
const cardTypesTable = table.render({
elem: '#cardTypesTable',
url: '/admin/api/card_types/list',
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
cols: [[
{ type: 'checkbox' },
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'name', title: '名称' },
{
field: 'status',
title: '状态',
width: 100,
templet: (d) => {
return d.status === 1
? '<span class="layui-badge layui-bg-green">启用</span>'
: '<span class="layui-badge">禁用</span>';
}
},
{ field: 'login_types', title: '登录方式' },
{
field: 'created_at',
title: '创建时间',
width: 180,
templet: (d) => {
return formatDateTime(d.created_at);
}
},
{
field: 'updated_at',
title: '更新时间',
width: 180,
templet: (d) => {
return formatDateTime(d.updated_at);
}
},
{ title: '操作', toolbar: '#tpl-cardtypes-ops', width: 150, fixed: 'right' }
]],
parseData: (res) => {
// 后端已返回正确格式,直接使用
return {
"code": res.code,
"msg": res.msg || '',
"count": res.data ? res.data.total : 0,
"data": res.data ? res.data.items : []
};
},
request: {
pageName: 'page',
limitName: 'page_size'
},
where: {}
});
// 格式化日期时间
const formatDateTime = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0') + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0') + ':' +
String(date.getSeconds()).padStart(2, '0');
};
// 监听表格工具条
table.on('tool(cardTypesTableFilter)', (obj) => {
const { data, event } = obj;
if (event === 'edit') {
editCardType(data);
} else if (event === 'del') {
deleteCardType(data.id);
}
});
// 新增卡密类型
$('#btnAddCardType').on('click', () => {
showCardTypeForm();
});
// 显示表单弹窗(统一使用 layer.open 的按钮作为确认/取消)
const showCardTypeForm = (data = null) => {
const title = data ? '编辑卡密类型' : '新增卡密类型';
currentFormLayerIndex = layer.open({
type: 1,
title: title,
content: $('#cardTypeFormModal'),
area: ['500px', '300px'],
btn: ['提交', '取消'],
btnAlign: 'c',
yes: () => {
// 点击“提交”时,执行统一的提交方法
doCardTypeSubmit();
},
btn2: (index) => {
// 点击“取消”时,关闭当前弹窗
layer.close(index);
},
success: () => {
// 成功打开弹窗后渲染表单,并根据是否为编辑模式进行回填
form.render();
if (data) {
// 编辑模式,填充数据
$('#cardTypeForm input[name="id"]').val(data.id);
$('#cardTypeForm input[name="name"]').val(data.name);
$('#cardTypeForm select[name="status"]').val(data.status);
$('#cardTypeForm input[name="login_types"]').val(data.login_types);
form.render('select');
} else {
// 新增模式,清空表单
$('#cardTypeForm')[0].reset();
$('#cardTypeForm input[name="id"]').val('');
form.render();
}
}
});
};
// 编辑卡密类型
const editCardType = (data) => {
showCardTypeForm(data);
};
// 提交表单(通过 layer.open 的“提交”按钮触发)
const doCardTypeSubmit = () => {
// 读取表单数据
const idValue = $('#cardTypeForm input[name="id"]').val();
const formData = {
id: idValue ? parseInt(idValue) : 0,
name: $('#cardTypeForm input[name="name"]').val().trim(),
status: parseInt($('#cardTypeForm select[name="status"]').val()),
login_types: $('#cardTypeForm input[name="login_types"]').val().trim()
};
// 校验必填项
if (!formData.name) {
layer.msg('请输入类型名称', { icon: 2 });
return;
}
// 根据是否存在 id 判断是创建还是更新
const url = formData.id ? '/admin/api/card_types/update' : '/admin/api/card_types/create';
const loadIndex = layer.load(2);
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(res => res.json())
.then(res => {
layer.close(loadIndex);
if (res.code === 0) {
layer.msg(res.msg || (formData.id ? '更新成功' : '创建成功'), { icon: 1 });
layer.close(currentFormLayerIndex);
cardTypesTable.reload();
} else {
layer.msg(res.msg || '操作失败', { icon: 2 });
}
})
.catch(() => {
layer.close(loadIndex);
layer.msg('网络错误,请重试', { icon: 2 });
});
};
// 取消按钮已移除,统一由 layer.open 的“取消”按钮处理
// 取消按钮已移除:统一由 layer.open 的“取消”按钮处理
// 删除卡密类型
const deleteCardType = (id) => {
layer.confirm('确定要删除这个卡密类型吗?', {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
const loadIndex = layer.load(2);
fetch('/admin/api/card_types/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: id })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardTypesTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
};
// 批量删除
$('#btnBatchDeleteCardTypes').on('click', () => {
const checkStatus = table.checkStatus('cardTypesTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要删除的数据', { icon: 2 });
return;
}
layer.confirm(`确定要删除选中的 ${checkStatus.data.length} 条数据吗?`, {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
const ids = checkStatus.data.map(item => item.id);
const loadIndex = layer.load(2);
fetch('/admin/api/card_types/batch_delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardTypesTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
});
// 批量启用
$('#btnBatchEnableCardTypes').on('click', () => {
batchUpdateStatus('/admin/api/card_types/batch_enable', '启用');
});
// 批量禁用
$('#btnBatchDisableCardTypes').on('click', () => {
batchUpdateStatus('/admin/api/card_types/batch_disable', '禁用');
});
// 批量更新状态的通用函数
const batchUpdateStatus = (url, action) => {
const checkStatus = table.checkStatus('cardTypesTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要操作的数据', { icon: 2 });
return;
}
layer.confirm(`确定要${action}选中的 ${checkStatus.data.length} 条数据吗?`, {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
const ids = checkStatus.data.map(item => item.id);
const loadIndex = layer.load(2);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardTypesTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
};
// 搜索功能
$('#btnSearchCardTypes').on('click', () => {
const keyword = $('#cardTypeFilterForm input[name="keyword"]').val();
const status = $('#cardTypeFilterForm select[name="status"]').val();
cardTypesTable.reload({
where: {
keyword: keyword,
status: status
},
page: {
curr: 1
}
});
});
// 重置搜索
$('#btnResetCardTypes').on('click', () => {
$('#cardTypeFilterForm')[0].reset();
form.render();
cardTypesTable.reload({
where: {},
page: {
curr: 1
}
});
});
});
</script>
{{ end }}

View File

@@ -1,771 +0,0 @@
{{ define "cards.html" }}
<section>
<h2>卡密管理</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddCard"><i class="layui-icon layui-icon-add-1"></i> 新增卡密</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteCards"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
<button class="layui-btn layui-btn-normal" id="btnBatchEnableCards"><i class="layui-icon layui-icon-ok-circle"></i> 设为未用</button>
<button class="layui-btn layui-btn-warm" id="btnBatchDisableCards"><i class="layui-icon layui-icon-close-fill"></i> 设为已用</button>
<!-- 新增:导出卡密按钮 -->
<button class="layui-btn layui-btn-primary" id="btnExportCards"><i class="layui-icon layui-icon-export"></i> 导出卡密</button>
<!-- 新增:导出选中卡密按钮 -->
<button class="layui-btn layui-btn-primary" id="btnExportSelectedCards"><i class="layui-icon layui-icon-export"></i> 导出选中</button>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">筛选</div>
<div class="layui-card-body">
<form class="layui-form layui-form-pane" id="cardFilterForm" lay-filter="cardFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">卡密</label>
<div class="layui-input-inline">
<input type="text" name="keyword" placeholder="卡号/批次/备注/任务号" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">类型</label>
<div class="layui-input-inline">
<select name="card_type" id="filterCardTypeSelect">
<option value="">全部</option>
</select>
</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="0">未使用</option>
<option value="1">已使用</option>
</select>
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchCards">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetCards">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">卡密列表</div>
<div class="layui-card-body">
<table id="cardsTable" lay-filter="cardsTableFilter"></table>
<script type="text/html" id="tpl-cards-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
</div>
<!-- 隐藏的表单弹层内容:新增卡密 -->
<div id="cardFormModal" style="display:none;padding:16px">
<form class="layui-form layui-form-pane" id="cardForm">
<input type="hidden" name="id" />
<div class="layui-form-item">
<label class="layui-form-label">前缀</label>
<div class="layui-input-block">
<input type="text" name="prefix" placeholder="可选,生成卡号时使用的前缀" autocomplete="off" class="layui-input" />
</div>
</div>
<!-- 新增生成数量默认1最大500位置在前缀之后、大小写之前 -->
<div class="layui-form-item">
<label class="layui-form-label">数量</label>
<div class="layui-input-block">
<input type="number" name="count" min="1" max="500" value="1" placeholder="一次生成的数量默认1最大500" class="layui-input" />
</div>
</div>
<!-- 新增:生成大小写选项(默认小写),位置在长度之前 -->
<div class="layui-form-item">
<label class="layui-form-label">规则</label>
<div class="layui-input-block">
<select name="uppercase">
<option value="lower" selected>小写</option>
<option value="upper">大写</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">长度</label>
<div class="layui-input-block">
<input type="number" name="length" min="1" max="64" value="18" placeholder="生成卡号的总长度包含前缀默认18" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">类型</label>
<div class="layui-input-block">
<select name="card_type" id="cardTypeSelect">
<option value="">请选择类型</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="0">未使用</option>
<option value="1">已使用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea name="remark" placeholder="可填写备注信息" class="layui-textarea"></textarea>
</div>
</div>
<!-- 移除:内置“操作/提交/取消”按钮,统一由 layer.open 的 btn 控制 -->
</form>
</div>
<!-- 隐藏的表单弹层内容:编辑卡密 -->
<div id="cardEditFormModal" style="display:none;padding:16px">
<form class="layui-form layui-form-pane" id="cardEditForm">
<input type="hidden" name="id" />
<div class="layui-form-item">
<label class="layui-form-label">类型</label>
<div class="layui-input-block">
<select name="card_type" id="cardEditTypeSelect">
<option value="">请选择类型</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="0">未使用</option>
<option value="1">已使用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">任务号</label>
<div class="layui-input-block">
<input type="text" name="task_no" placeholder="可选,支持填写/清空任务号" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea name="remark" placeholder="可填写备注信息" class="layui-textarea"></textarea>
</div>
</div>
<!-- 移除:内置“操作/提交/取消”按钮,统一由 layer.open 的 btn 控制 -->
</form>
</div>
<!-- 新增:导出条件弹窗(隐藏) -->
<div id="cardExportModal" style="display:none;padding:16px">
<form class="layui-form layui-form-pane" id="cardExportForm" lay-filter="cardExportForm">
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="">全部</option>
<option value="0">未使用</option>
<option value="1">已使用</option>
<option value="2">禁用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">类型</label>
<div class="layui-input-block">
<select name="card_type" id="exportCardTypeSelect">
<option value="">全部</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">批次</label>
<div class="layui-input-block">
<input type="text" name="batch" 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="text" name="remark" placeholder="按备注模糊匹配" autocomplete="off" class="layui-input" />
</div>
</div>
</form>
</div>
</section>
<script>
layui.use(['table', 'form', 'layer'], () => {
const { table, form, layer, $ } = layui;
let currentFormLayerIndex; // 保存当前表单弹窗的索引
let cardTypes = []; // 存储卡密类型数据
// 中文注释:以下三个标志用于协调类型列表和表格渲染的先后关系,避免出现“未知类型”
let cardTypesLoaded = false; // 类型列表是否已加载完成
let tableFirstRendered = false; // 表格是否已完成首次渲染
let tableReloadedAfterTypes = false; // 类型加载后是否已触发表格的二次渲染
// 格式化时间的辅助函数
const formatDateTime = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return String(date.getFullYear()) + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0') + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0') + ':' +
String(date.getSeconds()).padStart(2, '0');
};
// 获取卡密类型名称
// 中文注释:根据 card_type_id 在缓存的 cardTypes 中查找对应的类型名称;
// 为避免后端返回的 id 与前端数据在类型上不一致(字符串/数字)导致匹配失败,这里统一转换为数字再比较
const getCardTypeName = (cardTypeId) => {
const idNum = Number(cardTypeId);
const cardType = cardTypes.find(type => Number(type.id) === idNum);
return cardType ? cardType.name : '未知类型';
};
// 渲染表格
const cardsTable = table.render({
elem: '#cardsTable',
id: 'cardsTable',
url: '/admin/api/cards/list',
parseData: function(res) {
// 后端返回的数据结构:{items, total, page, page_size, pages}
return {
code: res.code,
msg: res.msg || '',
count: res.data ? res.data.total : 0,
data: res.data ? res.data.items : []
};
},
request: {
pageName: 'page', // 页码的参数名称默认page
limitName: 'page_size' // 每页数据量的参数名称默认limit
},
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
// 中文注释:表格首次渲染完成后,如果类型已经加载,则进行一次刷新以正确显示类型名称
done: function() {
if (!tableFirstRendered) {
tableFirstRendered = true;
if (cardTypesLoaded && !tableReloadedAfterTypes) {
tableReloadedAfterTypes = true;
cardsTable.reload();
}
}
},
cols: [[
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'card_number', title: '卡号', minWidth: 150 },
{
field: 'card_type_id',
title: '类型',
width: 100,
templet: (d) => d.card_type_name || getCardTypeName(d.card_type_id)
},
{
field: 'status',
title: '状态',
width: 80,
templet: (d) => {
if (d.status === 0) return '<span style="color: #5FB878;">未使用</span>';
if (d.status === 1) return '<span style="color: #FF5722;">已使用</span>';
return '<span style="color: #999;">禁用</span>';
}
},
{
field: 'task_no',
title: '任务号',
minWidth: 140,
templet: (d) => d.task_no || '-'
},
{ field: 'batch', title: '批次', minWidth: 60 },
{ field: 'remark', title: '备注', minWidth: 100 },
{
field: 'used_at',
title: '使用时间',
width: 180,
templet: (d) => formatDateTime(d.used_at)
},
{
field: 'created_at',
title: '创建时间',
width: 180,
templet: (d) => formatDateTime(d.created_at)
},
{ fixed: 'right', title: '操作', toolbar: '#tpl-cards-ops', width: 120 }
]]
});
// 加载卡密类型数据
const loadCardTypes = () => {
fetch('/admin/api/cards/types?all=1')
.then(response => response.json())
.then(data => {
if (data.code === 0) {
cardTypes = data.data || [];
// 填充筛选下拉框
const filterSelect = $('#filterCardTypeSelect');
filterSelect.empty().append('<option value="">全部</option>');
cardTypes.forEach(type => {
if (type.status === 1) { // 只显示启用的类型
filterSelect.append(`<option value="${type.id}">${type.name}</option>`);
}
});
// 填充新增表单下拉框
const cardTypeSelect = $('#cardTypeSelect');
cardTypeSelect.empty().append('<option value="">请选择类型</option>');
cardTypes.forEach(type => {
if (type.status === 1) { // 只显示启用的类型
cardTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
}
});
// 填充编辑表单下拉框
const cardEditTypeSelect = $('#cardEditTypeSelect');
cardEditTypeSelect.empty().append('<option value="">请选择类型</option>');
cardTypes.forEach(type => {
if (type.status === 1) { // 只显示启用的类型
cardEditTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
}
});
// 新增:填充导出弹窗下拉框(显示全部状态的类型,方便条件筛选)
const exportTypeSelect = $('#exportCardTypeSelect');
exportTypeSelect.empty().append('<option value="">全部</option>');
cardTypes.forEach(type => {
exportTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
});
form.render('select');
// 中文注释:标记类型加载完成;如表格已首次渲染,则进行一次性刷新以正确显示类型名称
cardTypesLoaded = true;
if (tableFirstRendered && !tableReloadedAfterTypes) {
tableReloadedAfterTypes = true;
cardsTable.reload();
}
// 卡密类型加载完成,表格会根据需要自动进行一次刷新
}
})
.catch(error => {
console.error('加载卡密类型失败:', error);
});
};
// 初始化加载卡密类型
loadCardTypes();
// 监听表格工具条
table.on('tool(cardsTableFilter)', (obj) => {
const { data, event } = obj;
if (event === 'edit') {
editCard(data);
} else if (event === 'del') {
deleteCard(data.id);
}
});
// 新增卡密
$('#btnAddCard').on('click', () => {
showCardForm();
});
// 显示新增卡密表单弹窗
// 中文注释:弹出新增/编辑表单的公共方法,采用 layer.open + btn/yes/btn2 的“确认框风格”
// data 为空表示新增;存在表示编辑。通过 yes 回调直接调用提交流程函数。
const showCardForm = (data = null) => {
const title = data ? '编辑卡密' : '新增卡密';
const modalId = data ? '#cardEditFormModal' : '#cardFormModal';
const formId = data ? '#cardEditForm' : '#cardForm';
const areaHeight = data ? '420px' : '600px';
currentFormLayerIndex = layer.open({
type: 1,
title: title,
content: $(modalId),
area: ['500px', areaHeight],
btn: ['提交', '取消'],
btnAlign: 'c',
yes: () => {
if (data) {
doEditCardSubmit();
} else {
doCreateCardSubmit();
}
return false;
},
btn2: (index) => {
layer.close(index);
},
success: () => {
form.render();
if (data) {
$(formId + ' input[name="id"]').val(data.id);
$(formId + ' select[name="card_type"]').val(data.card_type_id);
$(formId + ' select[name="status"]').val(data.status);
$(formId + ' textarea[name="remark"]').val(data.remark || '');
// 中文注释:编辑模式下,回填已有的任务号(若无则为空字符串)
$(formId + ' input[name="task_no"]').val(data.task_no || '');
form.render('select');
} else {
$(formId)[0].reset();
$(formId + ' input[name="id"]').val('');
// 中文注释:新增模式下显式清空任务号,避免出现上一次编辑残留
$(formId + ' input[name="task_no"]').val('');
form.render();
}
}
});
};
// 编辑卡密
const editCard = (data) => {
showCardForm(data);
};
// 提交新增卡密表单
// 提交逻辑函数化,供弹窗按钮直接调用,避免依赖模板内按钮
// 中文注释:提交“新增卡密”表单,完成校验、请求与反馈
const doCreateCardSubmit = () => {
const uppercaseValue = $('#cardForm select[name="uppercase"]').val();
const formData = {
prefix: $('#cardForm input[name="prefix"]').val() || '',
count: parseInt($('#cardForm input[name="count"]').val()) || 1,
uppercase: uppercaseValue === 'upper',
length: parseInt($('#cardForm input[name="length"]').val()) || 18,
card_type_id: parseInt($('#cardForm select[name="card_type"]').val()),
status: parseInt($('#cardForm select[name="status"]').val()),
remark: $('#cardForm textarea[name="remark"]').val() || ''
};
// 校验
if (!formData.card_type_id) {
layer.msg('请选择卡密类型', { icon: 2 });
return;
}
if (formData.count < 1 || formData.count > 500) {
layer.msg('生成数量必须在1-500之间', { icon: 2 });
return;
}
if (formData.length < 1 || formData.length > 64) {
layer.msg('卡号长度必须在1-64之间', { icon: 2 });
return;
}
const loadIndex = layer.load(2);
fetch('/admin/api/cards/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
layer.close(currentFormLayerIndex);
cardsTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error('新增卡密失败:', error);
layer.msg('网络错误,请重试', { icon: 2 });
});
};
// 中文注释:提交“编辑卡密”表单,完成校验、请求与反馈
const doEditCardSubmit = () => {
const idValue = $('#cardEditForm input[name="id"]').val();
const taskNoRaw = $('#cardEditForm input[name="task_no"]').val();
const hasTaskNoField = true; // 中文注释:该字段始终存在,通过值是否为空决定清空或设置
const formData = {
id: idValue ? parseInt(idValue) : 0,
card_type_id: parseInt($('#cardEditForm select[name="card_type"]').val()),
status: parseInt($('#cardEditForm select[name="status"]').val()),
remark: $('#cardEditForm textarea[name="remark"]').val() || ''
};
// 当任务号输入框有值或被清空时,也要传递 task_no 字段(允许清空)
if (hasTaskNoField) {
formData.task_no = (taskNoRaw || '').trim();
}
// 校验
if (!formData.id) {
layer.msg('卡密ID不能为空', { icon: 2 });
return;
}
if (!formData.card_type_id) {
layer.msg('请选择卡密类型', { icon: 2 });
return;
}
const loadIndex = layer.load(2);
fetch('/admin/api/cards/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
layer.close(currentFormLayerIndex);
cardsTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error('编辑卡密失败:', error);
layer.msg('网络错误,请重试', { icon: 2 });
});
};
// 删除卡密
const deleteCard = (id) => {
layer.confirm('确定要删除这个卡密吗?', {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
const loadIndex = layer.load(2);
fetch('/admin/api/cards/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: parseInt(id) })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardsTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error('删除卡密失败:', error);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
};
// 批量删除卡密
$('#btnBatchDeleteCards').on('click', () => {
const checkStatus = table.checkStatus('cardsTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要删除的卡密', { icon: 2 });
return;
}
layer.confirm(`确定要删除选中的 ${data.length} 个卡密吗?`, {
icon: 3,
title: '批量删除确认'
}, (index) => {
layer.close(index);
const loadIndex = layer.load(2);
const ids = data.map(item => item.id);
fetch('/admin/api/cards/batch_delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardsTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error('批量删除卡密失败:', error);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
});
// 批量启用卡密
$('#btnBatchEnableCards').on('click', () => {
batchUpdateStatus(0, '设为未用');
});
// 批量禁用卡密
$('#btnBatchDisableCards').on('click', () => {
batchUpdateStatus(1, '设为已用');
});
// 批量更新状态
const batchUpdateStatus = (status, statusText) => {
const checkStatus = table.checkStatus('cardsTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要操作的卡密', { icon: 2 });
return;
}
layer.confirm(`确定要${statusText}选中的 ${data.length} 个卡密吗?`, {
icon: 3,
title: `批量${statusText}确认`
}, (index) => {
layer.close(index);
const loadIndex = layer.load(2);
const ids = data.map(item => item.id);
fetch('/admin/api/cards/batch_update_status', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: ids, status: status })
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
layer.msg(data.msg, { icon: 1 });
cardsTable.reload();
} else {
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error(`批量${statusText}卡密失败:`, error);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
};
// 搜索功能
$('#btnSearchCards').on('click', () => {
const formData = form.val('cardFilterForm');
const where = {};
if (formData.card_type) {
where.card_type_id = formData.card_type;
}
if (formData.status !== '') {
where.status = formData.status;
}
if (formData.keyword && formData.keyword.trim() !== '') {
// 中文注释:将关键字作为 keyword 传给后端,由后端在 card_number、remark、batch 三个字段中进行模糊匹配
where.keyword = formData.keyword.trim();
}
table.reload('cardsTable', { where, page: { curr: 1 } });
});
$('#btnResetCards').on('click', () => {
$('#cardFilterForm')[0].reset();
form.render();
table.reload('cardsTable', { where: {}, page: { curr: 1 } });
});
// =============== 导出卡密逻辑 ===============
// 显示“导出卡密”弹窗
// 中文注释:弹出导出条件选择弹窗,允许管理员按状态/类型/批次/备注筛选导出
const showExportDialog = () => {
layer.open({
type: 1,
title: '导出卡密',
content: $('#cardExportModal'),
area: ['520px', '360px'],
btn: ['导出', '取消'],
btnAlign: 'c',
yes: (index) => {
doExportCards();
layer.close(index); // 关闭导出弹窗
layer.msg('卡密导出中...', { icon: 1 });
return false;
},
success: () => {
form.render();
}
});
};
// 绑定按钮事件
$('#btnExportCards').on('click', () => {
showExportDialog();
});
// 导出选中卡密
// 中文注释导出当前表格中选中的卡密无需弹窗筛选直接根据选中的卡密ID进行导出
$('#btnExportSelectedCards').on('click', () => {
const checkStatus = table.checkStatus('cardsTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要导出的卡密', { icon: 2 });
return;
}
layer.confirm(`确定要导出选中的 ${data.length} 个卡密吗?`, {
icon: 3,
title: '导出选中确认'
}, (index) => {
layer.close(index);
const ids = data.map(item => item.id);
const params = new URLSearchParams();
params.set('ids', ids.join(','));
const url = '/admin/api/cards/export_selected?' + params.toString();
triggerDownload(url);
layer.msg(`正在导出 ${data.length} 个卡密...`, { icon: 1 });
});
});
// 执行导出
// 中文注释根据表单条件拼接导出URL并以下载方式触发导出CSV 文件)
const doExportCards = () => {
const formData = form.val('cardExportForm');
const params = new URLSearchParams();
if (formData.status !== '') params.set('status', formData.status);
if (formData.card_type) params.set('card_type_id', formData.card_type);
if (formData.batch && formData.batch.trim() !== '') params.set('batch', formData.batch.trim());
if (formData.remark && formData.remark.trim() !== '') params.set('remark', formData.remark.trim());
const url = '/admin/api/cards/export' + (params.toString() ? ('?' + params.toString()) : '');
triggerDownload(url);
};
// 触发下载
// 中文注释:通过创建临时 <a> 元素点击,保持当前页面不跳转,触发后端附件下载
const triggerDownload = (url) => {
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
});
</script>
{{ end }}

View File

@@ -39,41 +39,7 @@
</div>
</section>
<!-- 卡密统计区域 -->
<section style="margin-top:16px">
<div class="layui-row layui-col-space15">
<!-- 当日卡密统计 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">当日卡密统计 <span class="layui-badge layui-bg-blue" style="margin-left:8px">总数:<span id="today-total">-</span></span></div>
<div class="layui-card-body">
<div id="chart-today-by-status" style="width:100%;height:320px"></div>
</div>
</div>
</div>
<!-- 所有卡密统计 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">所有卡密统计 <span class="layui-badge layui-bg-blue" style="margin-left:8px">总数:<span id="all-total">-</span></span></div>
<div class="layui-card-body">
<div id="chart-all-by-status" style="width:100%;height:320px"></div>
</div>
</div>
</div>
</div>
<!-- 30天走势图 -->
<div class="layui-row layui-col-space15" style="margin-top:16px">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">近30天卡密走势</div>
<div class="layui-card-body">
<div id="chart-trend-30days" style="width:100%;height:360px"></div>
</div>
</div>
</div>
</div>
</section>
<script>
// 仪表盘统计脚本(采用箭头函数与中文注释)
@@ -100,107 +66,7 @@ layui.use(['layer', 'util'], function(){
}
};
// 工具函数:状态码 -> 名称 映射
// 说明卡密状态映射0=未使用1=已使用2=禁用
const getStatusText = (s) => {
const map = {0:'未使用',1:'已使用',2:'禁用'};
const k = Number(s);
return map[k] ?? String(s);
};
// 工具函数:状态码 -> 颜色 映射(与徽章风格一致,尽量贴近 Layui 配色)
const getStatusColor = (s) => {
switch (Number(s)) {
case 0: return '#1E9FFF'; // 蓝色 - 未使用
case 1: return '#5FB878'; // 绿色 - 已使用
case 2: return '#FF5722'; // 红色 - 禁用
default: return '#909399'; // 灰色 - 默认
}
};
// 函数:渲染饼图
// 说明:接收状态分布对象(键为状态码,值为数量),绘制环形图
const renderPie = (domId, byStatus) => {
const el = document.getElementById(domId);
if (!el) return;
const chart = echarts.init(el);
const codes = [0,1,2]; // 卡密状态0=未使用1=已使用2=禁用
const data = codes.map(code => ({
name: getStatusText(code),
value: Number((byStatus && byStatus[code]) || 0),
itemStyle: { color: getStatusColor(code) }
}));
chart.setOption({
tooltip: { trigger: 'item' },
legend: { top: 'bottom' },
series: [{
name: '按状态分布',
type: 'pie',
radius: ['38%', '68%'],
avoidLabelOverlap: true,
label: { formatter: '{b}: {c} ({d}%)' },
data
}]
});
// 自适应
window.addEventListener('resize', () => chart.resize());
return chart;
};
// 函数:渲染 30 天折线图
// 说明三条序列total/used/unused对应后台返回的数组
const renderTrend = (domId, trend) => {
const el = document.getElementById(domId);
if (!el) return;
const chart = echarts.init(el);
const dates = (trend && trend.dates) || [];
const total = (trend && trend.total) || [];
const used = (trend && trend.used) || [];
const unused = (trend && trend.unused) || [];
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['总数', '已使用', '未使用'] },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: dates },
yAxis: { type: 'value' },
series: [
{ name: '总数', type: 'line', smooth: true, data: total, itemStyle: { color: '#909399' } },
{ name: '已使用', type: 'line', smooth: true, data: used, itemStyle: { color: getStatusColor(1) } },
{ name: '未使用', type: 'line', smooth: true, data: unused, itemStyle: { color: getStatusColor(0) } }
]
});
window.addEventListener('resize', () => chart.resize());
return chart;
};
// 函数:拉取概览并渲染
// 说明:请求 /admin/api/cards/stats_overview更新总数文本并渲染两个饼图
const loadAndRenderOverview = () => {
$.get('/admin/api/cards/stats_overview', (res) => {
if (!res || res.code !== 0) { layer.msg(res && res.msg ? res.msg : '获取统计概览失败'); return; }
const data = res.data || {};
$('#today-total').text((data.today && data.today.total) ?? '-');
$('#all-total').text((data.all && data.all.total) ?? '-');
// 渲染饼图
renderPie('chart-today-by-status', data.today ? data.today.by_status : {});
renderPie('chart-all-by-status', data.all ? data.all.by_status : {});
});
};
// 函数:拉取 30 天数据并渲染折线图
// 说明:请求 /admin/api/cards/trend_30days渲染趋势图
const loadAndRenderTrend = () => {
$.get('/admin/api/cards/trend_30days', (res) => {
if (!res || res.code !== 0) { layer.msg(res && res.msg ? res.msg : '获取30天趋势失败'); return; }
renderTrend('chart-trend-30days', res.data || {});
});
};
// 函数:刷新基本信息和运行状态
// 说明:请求后台获取最新的系统信息并更新页面显示
@@ -223,14 +89,8 @@ layui.use(['layer', 'util'], function(){
});
};
// 入口:确保 ECharts 已加载后开始渲染
ensureECharts(() => {
loadAndRenderOverview();
loadAndRenderTrend();
// 立即刷新一次系统信息
refreshSystemInfo();
});
// 立即刷新一次系统信息
refreshSystemInfo();
});
</script>
{{ end }}

View File

@@ -54,14 +54,6 @@
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">卡密管理</a>
<dl class="layui-nav-child">
<dd><a data-path="logintypes" href="javascript:;">登录类型</a></dd>
<dd><a data-path="cardtypes" href="javascript:;">卡密类型</a></dd>
<dd><a data-path="cards" href="javascript:;">卡密列表</a></dd>
</dl>
</li>
</ul>
</div>
</div>

View File

@@ -1,422 +0,0 @@
{{ define "login_types.html" }}
<section>
<h2>登录方式管理</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddLoginType"><i class="layui-icon layui-icon-add-1"></i> 新增方式</button>
<button class="layui-btn layui-btn-normal" id="btnBatchEnableLoginTypes"><i class="layui-icon layui-icon-ok-circle"></i> 批量启用</button>
<button class="layui-btn layui-btn-warm" id="btnBatchDisableLoginTypes"><i class="layui-icon layui-icon-close-fill"></i> 批量禁用</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteLoginTypes"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">筛选</div>
<div class="layui-card-body">
<form class="layui-form layui-form-pane" id="loginTypeFilterForm" lay-filter="loginTypeFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">名称</label>
<div class="layui-input-inline">
<input type="text" name="keyword" placeholder="登录方式名称" autocomplete="off" class="layui-input" />
</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">
<button type="button" class="layui-btn" id="btnSearchLoginTypes">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetLoginTypes">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">登录方式列表</div>
<div class="layui-card-body">
<table id="loginTypesTable" lay-filter="loginTypesTableFilter"></table>
<script type="text/html" id="tpl-logintypes-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
</div>
<!-- 隐藏的表单弹层内容 -->
<div id="loginTypeFormModal" style="display:none;padding:16px">
<!-- 参考demo表单2样式添加layui-form-pane类实现方框风格 -->
<form class="layui-form layui-form-pane" id="loginTypeForm" lay-filter="loginTypeForm">
<input type="hidden" name="id" />
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input type="text" name="name" required lay-verify="required" placeholder="请输入登录方式名称" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">验证类型</label>
<div class="layui-input-block">
<input type="text" name="verify_types" placeholder="请输入验证类型,多个用逗号分隔" autocomplete="off" class="layui-input" />
</div>
</div>
<!-- 操作按钮移除:统一由 layer.open 的 btn 控制“提交/取消” -->
</form>
</div>
</section>
<script>
// 登录类型管理页面的JavaScript脚本
layui.use(['table', 'form', 'layer'], function(){
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
// 表格实例
let tableIns;
// 初始化表格
const initTable = () => {
tableIns = table.render({
elem: '#loginTypesTable',
url: '/admin/api/login_types/list',
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
cols: [[
{type: 'checkbox'},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'name', title: '名称'},
{field: 'status', title: '状态', width: 100, templet: function(d){
return d.status === 1 ? '<span class="layui-badge layui-bg-green">启用</span>' : '<span class="layui-badge">禁用</span>';
}},
{field: 'verify_types', title: '验证类型'},
{field: 'created_at', title: '创建时间', width: 180, templet: function(d){
return formatDateTime(d.created_at);
}},
{field: 'updated_at', title: '更新时间', width: 180, templet: function(d){
return formatDateTime(d.updated_at);
}},
{title: '操作', toolbar: '#tpl-logintypes-ops', width: 150, fixed: 'right'}
]],
parseData: function(res){
// 后端已返回正确格式,直接使用
return {
"code": res.code,
"msg": res.msg || '',
"count": res.data ? res.data.total : 0,
"data": res.data ? res.data.items : []
};
},
request: {
pageName: 'page',
limitName: 'page_size'
},
where: {}
});
};
// 格式化日期时间
const formatDateTime = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0') + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0') + ':' +
String(date.getSeconds()).padStart(2, '0');
};
// 重载表格数据
const reloadTable = (where = {}) => {
tableIns.reload({
where: where,
page: {
curr: 1
}
});
};
// 获取选中的行数据
const getCheckData = () => {
const checkStatus = table.checkStatus('loginTypesTable');
return checkStatus.data;
};
// 当前表单弹窗索引
let currentFormLayerIndex = null;
// 显示表单弹层(统一使用 layer.open 的按钮作为确认/取消)
const showFormModal = (title, data = {}) => {
// 重置表单
$('#loginTypeForm')[0].reset();
// 填充表单数据
if (data.id) {
$('input[name="id"]').val(data.id);
$('input[name="name"]').val(data.name);
$('select[name="status"]').val(data.status);
$('input[name="verify_types"]').val(data.verify_types);
}
// 刷新表单渲染
form.render();
// 显示弹层并保存索引
currentFormLayerIndex = layer.open({
type: 1,
title: title,
content: $('#loginTypeFormModal'),
area: ['500px', '300px'],
btn: ['提交', '取消'],
btnAlign: 'c',
yes: () => {
// 点击“提交”时执行提交
doLoginTypeSubmit();
},
btn2: (index) => {
// 点击“取消”时关闭弹层
layer.close(index);
},
closeBtn: 1
});
};
// 提交表单(通过 layer.open 的“提交”按钮触发)
const doLoginTypeSubmit = () => {
// 读取表单数据并校验
const idVal = $('input[name="id"]').val();
const isEdit = idVal && idVal !== '';
const name = $('input[name="name"]').val().trim();
const status = parseInt($('select[name="status"]').val() || '0');
const verifyTypes = $('input[name="verify_types"]').val().trim();
if (!name) {
layer.msg('请输入登录方式名称', { icon: 2 });
return;
}
const url = isEdit ? '/admin/api/login_types/update' : '/admin/api/login_types/create';
const requestData = {
name: name,
status: status,
verify_types: verifyTypes
};
if (isEdit) requestData.id = parseInt(idVal);
$.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
success: function(res) {
if (res.code === 0) {
layer.msg(isEdit ? '更新成功' : '创建成功', { icon: 1, time: 1000 });
layer.close(currentFormLayerIndex);
reloadTable();
} else {
// 失败时对提示信息做截断,避免过长
const raw = res.msg || '操作失败';
const shortMsg = raw.length > 100 ? (raw.slice(0, 100) + '...') : raw;
layer.msg(shortMsg, { icon: 2 });
}
},
// 优先展示后端返回的业务错误信息,避免统一显示“网络错误”
error: (xhr) => {
// 失败时对提示信息做截断,避免过长
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
const shortMsg = raw.length > 100 ? (raw.slice(0, 100) + '...') : raw;
layer.msg(shortMsg, { icon: 2 });
}
});
};
// 删除单个记录
// 说明删除前二次确认后端返回400/500也能显示具体错误信息
const deleteItem = (id) => {
layer.confirm('确定要删除这条记录吗?', {icon: 3, title: '提示'}, function(index){
$.ajax({
url: '/admin/api/login_types/delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: id}),
success: function(res) {
if (res.code === 0) {
layer.msg('删除成功', { icon: 1, time: 3000 });
reloadTable();
} else {
// 删除失败:使用折行展示错误信息,便于阅读(不再截断)
const raw = res.msg || '删除失败';
const safe = String(raw)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// 将常见分隔符替换为换行,结合 white-space: pre-wrap 生效
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,;]/g, '\n')}</div>`;
layer.msg(content, { icon: 2 });
}
},
// 解析后端JSON错误响应展示msg内容支持折行
error: (xhr) => {
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
const safe = String(raw)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,;]/g, '\n')}</div>`;
layer.msg(content, { icon: 2 });
}
});
layer.close(index);
});
};
// 批量操作
// 参数operation 用于提示文案url 为接口地址confirmMsg 为确认提示语
const batchOperation = (operation, url, confirmMsg) => {
const checkData = getCheckData();
if (checkData.length === 0) {
layer.msg('请选择要操作的数据', { icon: 2 });
return;
}
const ids = checkData.map(item => item.id);
layer.confirm(confirmMsg, {icon: 3, title: '提示'}, function(index){
$.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ids: ids}),
// 统一成功/失败提示移除残留的diff标记
success: function(res) {
if (res.code === 0) {
layer.msg(operation + '成功', { icon: 1 });
reloadTable();
} else {
// 批量失败:使用折行展示长信息(例如占用明细),便于阅读
const raw = res.msg || operation + '失败';
const safe = String(raw)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,;]/g, '\n')}</div>`;
layer.msg(content, { icon: 2 });
}
},
// 出错时同样尝试展示后端返回的msg支持折行
error: (xhr) => {
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
const safe = String(raw)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,;]/g, '\n')}</div>`;
layer.msg(content, { icon: 2 });
}
});
layer.close(index);
});
};
// 事件绑定
// 新增按钮
$('#btnAddLoginType').on('click', function(){
showFormModal('新增登录方式');
});
// 批量启用按钮
$('#btnBatchEnableLoginTypes').on('click', function(){
batchOperation('批量启用', '/admin/api/login_types/batch_enable', '确定要启用选中的登录方式吗?');
});
// 批量禁用按钮
$('#btnBatchDisableLoginTypes').on('click', function(){
batchOperation('批量禁用', '/admin/api/login_types/batch_disable', '确定要禁用选中的登录方式吗?');
});
// 批量删除按钮
$('#btnBatchDeleteLoginTypes').on('click', function(){
batchOperation('批量删除', '/admin/api/login_types/batch_delete', '确定要删除选中的登录方式吗?删除后不可恢复!');
});
// 查询按钮
$('#btnSearchLoginTypes').on('click', function(){
const formData = form.val('loginTypeFilterForm');
const where = {};
if (formData.keyword && formData.keyword.trim() !== '') {
where.keyword = formData.keyword.trim();
}
if (formData.status && formData.status !== '') {
where.status = formData.status;
}
reloadTable(where);
});
// 重置按钮
$('#btnResetLoginTypes').on('click', function(){
$('#loginTypeFilterForm')[0].reset();
form.render();
reloadTable();
});
// Layui表单提交事件
// 删除 Layui 表单提交监听(由 layer.open 的“提交”按钮统一触发)
// form.on('submit(loginTypeSubmit)', function(data){
// submitForm();
// return false; // 阻止表单跳转
// });
// 删除表单取消按钮事件(由 layer.open 的“取消”按钮统一处理)
// $('#btnCancelLoginType').on('click', function(){
// layer.close(currentFormLayerIndex);
// });
// 表格工具栏事件
table.on('tool(loginTypesTableFilter)', function(obj){
const data = obj.data;
const layEvent = obj.event;
if (layEvent === 'edit') {
showFormModal('编辑登录方式', data);
} else if (layEvent === 'del') {
deleteItem(data.id);
}
});
// 初始化页面
initTable();
});
</script>
{{ end }}