Files
BuYuHouDuan/AdminSrc/AdminHost/wwwroot/admin/dist/views/system/faqmanage.html
T
2026-10-12 16:22:04 +08:00

684 lines
28 KiB
HTML

<title>常见问题管理</title>
<link rel="stylesheet" href="/admin/component/pear/css/pear Module.css" />
<style>
.faq-container { display: flex; gap: 15px; height: calc(100vh - 180px); }
.faq-tree-panel { width: 280px; background: #fff; border-radius: 4px; padding: 15px; overflow: auto; }
.faq-question-panel { flex: 1; background: #fff; border-radius: 4px; padding: 15px; }
.tree-toolbar { margin-bottom: 15px; display: flex; gap: 8px; }
.tree-node-item {
padding: 8px 12px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid transparent;
}
.tree-node-item:hover { background: #f5f5f5; }
.tree-node-item.active { background: #e6f7f5; border-color: #009688; }
.tree-node-item .node-info { display: flex; align-items: center; gap: 8px; flex: 1; }
.tree-node-item .node-actions { display: flex; gap: 5px; opacity: 0; }
.tree-node-item:hover .node-actions { opacity: 1; }
.tree-node-item .node-actions a { color: #666; font-size: 12px; }
.tree-node-item .node-actions a:hover { color: #009688; }
.tree-node-item .node-actions a.delete:hover { color: #f56c6c; }
.tree-children { margin-left: 20px; }
.icon-preview { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
.edit-dialog .layui-form-item { margin-bottom: 15px; }
.edit-dialog .layui-form-label { width: 90px; }
.edit-dialog .layui-input-block { margin-left: 110px; }
.rich-editor { min-height: 200px; border: 1px solid #e6e6e6; border-radius: 4px; padding: 10px; }
.rich-editor-toolbar { border-bottom: 1px solid #e6e6e6; padding: 5px; background: #fafafa; }
.rich-editor-toolbar button { padding: 3px 8px; margin-right: 3px; border: 1px solid #ddd; background: #fff; border-radius: 3px; cursor: pointer; }
.rich-editor-toolbar button:hover { background: #eee; }
.rich-editor-content { min-height: 180px; padding: 10px; outline: none; }
/* 图标选择器样式 */
.icon-selector { position: relative; }
.icon-selector .icon-display {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
border: 1px solid #e6e6e6;
border-radius: 2px;
cursor: pointer;
background: #fff;
min-height: 38px;
}
.icon-selector .icon-display:hover { border-color: #009688; }
.icon-selector .icon-display .icon-preview { font-size: 18px; color: #666; }
.icon-selector .icon-display .icon-placeholder { color: #999; font-size: 12px; }
.icon-selector .icon-dropdown {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 9999;
padding: 10px;
max-height: 250px;
overflow-y: auto;
}
.icon-selector .icon-dropdown.show { display: block; }
.icon-selector .icon-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; }
.icon-selector .icon-item {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e6e6e6;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
color: #666;
}
.icon-selector .icon-item:hover { background: #f5f5f5; border-color: #009688; }
.icon-selector .icon-item.selected { background: #009688; color: #fff; border-color: #009688; }
</style>
<div class="layui-card layadmin-header">
<div class="layui-breadcrumb" lay-filter="breadcrumb">
<a lay-href="">主页</a>
<a><cite>系统</cite></a>
<a><cite>常见问题管理</cite></a>
</div>
</div>
<div class="layui-fluid">
<div class="faq-container">
<!-- 左侧:分类树 -->
<div class="faq-tree-panel">
<div class="tree-toolbar">
<button class="layui-btn layui-btn-sm" id="btn-add-root"><i class="layui-icon layui-icon-add-1"></i>添加分类</button>
</div>
<div id="categoryTree"></div>
</div>
<!-- 右侧:问题列表 -->
<div class="faq-question-panel">
<div class="layui-card-header">
<span id="currentCategoryName">请选择分类</span>
<button class="layui-btn layui-btn-sm" id="btn-add-question" style="float:right;"><i class="layui-icon layui-icon-add-1"></i>添加问题</button>
</div>
<table id="table-question" lay-filter="table-question"></table>
</div>
</div>
</div>
<script type="text/html" id="toolbar-question">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="batchdel"><i class="layui-icon layui-icon-delete"></i>批量删除</button>
</div>
</script>
<script type="text/html" id="table-operate">
<a class="layui-btn layui-btn-xs" lay-event="edit"><i class="layui-icon layui-icon-edit"></i>编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del"><i class="layui-icon layui-icon-delete"></i>删除</a>
</script>
<script type="text/html" id="statusTpl">
<input type="checkbox" name="isenable" value="{{d.id}}" lay-skin="switch" lay-text="启用|禁用" {{ d.isenable ? 'checked' : '' }} lay-filter="enable-switch">
</script>
<script>
layui.use(['admin', 'table', 'form', 'layer'], function () {
var $ = layui.$,
admin = layui.admin,
table = layui.table,
form = layui.form,
layer = layui.layer,
setter = layui.setter;
var currentCategoryId = null;
var categoryList = [];
// 加载分类树
function loadCategoryTree() {
admin.req({
url: setter.baseurl + 'adminapi/Faq/CategoryTree',
done: function (res) {
if (res.code === 0) {
categoryList = res.data || [];
renderTree(categoryList, $('#categoryTree'));
}
}
});
}
// 渲染树
function renderTree(nodes, $container) {
var html = '';
nodes.forEach(function(node) {
html += '<div class="tree-node-item' + (node.id === currentCategoryId ? ' active' : '') + '" data-id="' + node.id + '">';
html += '<div class="node-info">';
if (node.icon) {
html += '<i class="layui-icon ' + node.icon + ' icon-preview"></i>';
}
html += '<span>' + node.name + '</span>';
html += '<span style="color:#999;font-size:12px;">(' + (node.children ? node.children.length : 0) + ')</span>';
html += '</div>';
html += '<div class="node-actions">';
html += '<a href="javascript:;" class="add-child" title="添加子分类"><i class="layui-icon layui-icon-add-1"></i></a>';
html += '<a href="javascript:;" class="edit" title="编辑"><i class="layui-icon layui-icon-edit"></i></a>';
html += '<a href="javascript:;" class="delete" title="删除"><i class="layui-icon layui-icon-delete"></i></a>';
html += '</div>';
html += '</div>';
if (node.children && node.children.length > 0) {
html += '<div class="tree-children">';
html += renderTreeHtml(node.children);
html += '</div>';
}
});
$container.html(html);
}
function renderTreeHtml(nodes) {
var html = '';
nodes.forEach(function(node) {
html += '<div class="tree-node-item' + (node.id === currentCategoryId ? ' active' : '') + '" data-id="' + node.id + '">';
html += '<div class="node-info">';
if (node.icon) {
html += '<i class="layui-icon ' + node.icon + ' icon-preview"></i>';
}
html += '<span>' + node.name + '</span>';
html += '<span style="color:#999;font-size:12px;">(' + (node.children ? node.children.length : 0) + ')</span>';
html += '</div>';
html += '<div class="node-actions">';
html += '<a href="javascript:;" class="add-child" title="添加子分类"><i class="layui-icon layui-icon-add-1"></i></a>';
html += '<a href="javascript:;" class="edit" title="编辑"><i class="layui-icon layui-icon-edit"></i></a>';
html += '<a href="javascript:;" class="delete" title="删除"><i class="layui-icon layui-icon-delete"></i></a>';
html += '</div>';
html += '</div>';
if (node.children && node.children.length > 0) {
html += '<div class="tree-children">';
html += renderTreeHtml(node.children);
html += '</div>';
}
});
return html;
}
// 分类树点击
$(document).on('click', '.tree-node-item', function(e) {
if ($(e.target).closest('.node-actions').length > 0) return;
$('.tree-node-item').removeClass('active');
$(this).addClass('active');
currentCategoryId = $(this).data('id');
var nodeName = $(this).find('.node-info span').eq(0).text();
$('#currentCategoryName').text(nodeName);
loadQuestions();
});
// 添加根分类
$('#btn-add-root').on('click', function() {
showCategoryDialog(null, null);
});
// 添加子分类
$(document).on('click', '.add-child', function(e) {
e.stopPropagation();
var parentId = $(this).closest('.tree-node-item').data('id');
showCategoryDialog(null, parentId);
});
// 编辑分类
$(document).on('click', '.edit', function(e) {
e.stopPropagation();
var id = $(this).closest('.tree-node-item').data('id');
admin.req({
url: setter.baseurl + 'adminapi/Faq/CategoryDetail',
data: { id: id },
done: function(res) {
if (res.code === 0) {
showCategoryDialog(res.data, res.data.parentid);
}
}
});
});
// 删除分类
$(document).on('click', '.delete', function(e) {
e.stopPropagation();
var id = $(this).closest('.tree-node-item').data('id');
layer.confirm('确定删除该分类吗?', function(idx) {
admin.req({
url: setter.baseurl + 'adminapi/Faq/DeleteCategory',
data: { id: id },
done: function(res) {
layer.close(idx);
if (res.code === 0) {
layer.msg('删除成功');
loadCategoryTree();
} else {
layer.msg(res.info || '删除失败');
}
}
});
});
});
// 分类弹窗
function showCategoryDialog(data, parentId) {
data = data || {};
var iconOptions = [
{ value: 'layui-icon-user', text: '用户' },
{ value: 'layui-icon-set', text: '设置' },
{ value: 'layui-icon-help', text: '帮助' },
{ value: 'layui-icon-notice', text: '通知' },
{ value: 'layui-icon-util', text: '工具' },
{ value: 'layui-icon-release', text: '发布' },
{ value: 'layui-icon-survey', text: '问卷' },
{ value: 'layui-icon-senior', text: '高级' },
{ value: 'layui-icon-app', text: '应用' },
{ value: 'layui-icon-website', text: '网站' },
{ value: 'layui-icon-console', text: '控制台' },
{ value: 'layui-icon-home', text: '首页' },
{ value: 'layui-icon-reduce-circle', text: '减少' },
{ value: 'layui-icon-chart', text: '图表' },
{ value: 'layui-icon-dialogue', text: '对话' },
{ value: 'layui-icon-edit', text: '编辑' },
{ value: 'layui-icon-link', text: '链接' },
{ value: 'layui-icon-picture', text: '图片' },
{ value: 'layui-icon-file', text: '文件' },
{ value: 'layui-icon-find-fill', text: '发现' },
{ value: 'layui-icon-add-circle', text: '添加' },
{ value: 'layui-icon-cart-simple', text: '购物' },
{ value: 'layui-icon-cart', text: '购物车' },
{ value: 'layui-icon-group', text: '群组' },
{ value: 'layui-icon-fire', text: '热门' },
{ value: 'layui-icon-gift', text: '礼物' },
{ value: 'layui-icon-templeate', text: '模板' },
{ value: 'layui-icon-voice', text: '语音' },
{ value: 'layui-icon-mike', text: '麦克风' },
{ value: 'layui-icon-news', text: '新闻' }
];
var iconGridHtml = '';
iconOptions.forEach(function(icon) {
var selected = data.icon === icon.value ? 'selected' : '';
iconGridHtml += '<div class="icon-item ' + selected + '" data-value="' + icon.value + '" title="' + icon.text + '"><i class="layui-icon ' + icon.value + '"></i></div>';
});
var parentOptions = '<option value="">顶级分类</option>';
categoryList.forEach(function(cat) {
if (!data.id || cat.id !== data.id) {
var selected = data.parentid === cat.id ? 'selected' : '';
parentOptions += '<option value="' + cat.id + '" ' + selected + '>' + cat.name + '</option>';
}
});
var selectedIconHtml = data.icon
? '<i class="layui-icon ' + data.icon + ' icon-preview"></i><span>' + (iconOptions.find(function(o){return o.value===data.icon;})?.text || data.icon) + '</span>'
: '<span class="icon-placeholder">点击选择图标</span>';
var html = '<div class="edit-dialog" style="padding:20px;">' +
'<form class="layui-form">' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">上级分类</label>' +
'<div class="layui-input-block">' +
'<select name="parentid">' + parentOptions + '</select>' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">分类名称</label>' +
'<div class="layui-input-block">' +
'<input type="text" name="name" value="' + (data.name || '') + '" required lay-verify="required" class="layui-input">' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">分类图标</label>' +
'<div class="layui-input-block">' +
'<div class="icon-selector" id="iconSelector">' +
'<div class="icon-display">' + selectedIconHtml + '</div>' +
'<input type="hidden" name="icon" value="' + (data.icon || '') + '"/>' +
'<div class="icon-dropdown" id="iconDropdown">' +
'<div class="icon-grid">' + iconGridHtml + '</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">排序</label>' +
'<div class="layui-input-block">' +
'<input type="number" name="sort" value="' + (data.sort || 0) + '" class="layui-input">' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">启用</label>' +
'<div class="layui-input-block">' +
'<input type="checkbox" name="isenable" lay-skin="switch" lay-text="启用|禁用" ' + (data.isenable !== false ? 'checked' : '') + '>' +
'</div>' +
'</div>' +
'</form>' +
'</div>';
layer.open({
type: 1,
title: data.id ? '编辑分类' : '添加分类',
area: ['520px', '480px'],
content: html,
btn: ['保存', '取消'],
success: function(layero) {
form.render('select');
// 图标选择器交互
var $selector = layero.find('#iconSelector');
var $display = $selector.find('.icon-display');
var $dropdown = $selector.find('.icon-dropdown');
var $input = $selector.find('input[name="icon"]');
$display.on('click', function(e) {
e.stopPropagation();
$('.icon-dropdown').not($dropdown).removeClass('show');
$dropdown.toggleClass('show');
});
$dropdown.on('click', '.icon-item', function(e) {
e.stopPropagation();
var val = $(this).data('value');
var iconClass = $(this).find('i').attr('class');
var iconText = $(this).attr('title');
$input.val(val);
$dropdown.find('.icon-item').removeClass('selected');
$(this).addClass('selected');
$display.html('<i class="' + iconClass + ' icon-preview"></i><span>' + iconText + '</span>');
$dropdown.removeClass('show');
});
$(document).on('click', function() {
$dropdown.removeClass('show');
});
},
yes: function(index, layero) {
var name = layero.find('input[name="name"]').val();
if (!name) {
layer.msg('请输入分类名称');
return;
}
var param = {
name: name,
parentid: layero.find('select[name="parentid"]').val() || null,
icon: layero.find('select[name="icon"]').val() || null,
sort: parseInt(layero.find('input[name="sort"]').val()) || 0,
isenable: layero.find('input[name="isenable"]').prop('checked')
};
if (data.id) param.id = data.id;
var url = data.id ? 'adminapi/Faq/UpdateCategory' : 'adminapi/Faq/AddCategory';
admin.req({
url: setter.baseurl + url,
contentType: 'application/json',
method: 'post',
data: JSON.stringify(param),
done: function(res) {
layer.close(index);
if (res.code === 0) {
layer.msg('保存成功');
loadCategoryTree();
} else {
layer.msg(res.info || '保存失败');
}
}
});
}
});
}
// 添加问题
$('#btn-add-question').on('click', function() {
if (!currentCategoryId) {
return layer.msg('请先选择分类');
}
showQuestionDialog(null);
});
// 加载问题列表
function loadQuestions() {
if (!currentCategoryId) {
table.render({
elem: '#table-question',
data: [],
cols: [[{ title: '请选择左侧分类查看问题' }]]
});
return;
}
table.render({
elem: '#table-question',
url: setter.baseurl + 'adminapi/Faq/QuestionList',
method: 'post',
contentType: 'application/json',
headers: { 'token': layui.data(setter.tableName)[setter.request.tokenName] },
page: true,
where: {
categoryId: currentCategoryId
},
request: {
pageName: 'pageIndex',
limitName: 'pageSize'
},
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ type: 'numbers', title: '序号', width: 60 },
{ field: "title", title: "问题标题", minWidth: 200 },
{ field: "sort", title: "排序", width: 80, edit: 'text' },
{ field: "isenable", title: "启用", width: 100, templet: '#statusTpl' },
{ title: "操作", width: 120, fixed: 'right', toolbar: '#table-operate' }
]],
toolbar: '#toolbar-question',
defaultToolbar: ['filter', 'exports', 'print'],
response: {
statusName: 'code',
statusCode: 0,
msgName: 'info',
countName: 'count',
dataName: 'data'
}
});
}
// 表格工具栏
table.on('toolbar(table-question)', function(obj) {
if (obj.event === 'batchdel') {
var checkStatus = table.checkStatus('table-question');
var data = checkStatus.data;
if (data.length === 0) return layer.msg('请选择要删除的数据');
layer.confirm('确定删除选中的 ' + data.length + ' 条数据吗?', function(idx) {
var ids = data.map(function(item) { return item.id; });
var failCount = 0;
var promises = ids.map(function(id) {
return new Promise(function(resolve) {
admin.req({
url: setter.baseurl + 'adminapi/Faq/DeleteQuestion',
data: { id: id },
done: function(res) {
if (res.code !== 0) failCount++;
resolve();
}
});
});
});
Promise.all(promises).then(function() {
layer.close(idx);
if (failCount > 0) {
layer.msg('部分删除失败(' + failCount + '条)');
} else {
layer.msg('删除成功');
}
loadQuestions();
});
});
}
});
// 表格行工具栏
table.on('tool(table-question)', function(obj) {
var data = obj.data;
if (obj.event === 'edit') {
showQuestionDialog(data);
} else if (obj.event === 'del') {
layer.confirm('确定删除该问题吗?', function(idx) {
admin.req({
url: setter.baseurl + 'adminapi/Faq/DeleteQuestion',
data: { id: data.id },
done: function(res) {
layer.close(idx);
if (res.code === 0) {
layer.msg('删除成功');
loadQuestions();
} else {
layer.msg(res.info || '删除失败');
}
}
});
});
}
});
// 问题弹窗
function showQuestionDialog(data) {
data = data || {};
var html = '<div class="edit-dialog" style="padding:20px;">' +
'<form class="layui-form" id="questionForm">' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">问题标题</label>' +
'<div class="layui-input-block">' +
'<input type="text" name="title" value="' + (data.title || '') + '" required lay-verify="required" class="layui-input">' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">排序</label>' +
'<div class="layui-input-block">' +
'<input type="number" name="sort" value="' + (data.sort || 0) + '" class="layui-input">' +
'</div>' +
'</div>' +
'<div class="layui-form-item">' +
'<label class="layui-form-label">启用</label>' +
'<div class="layui-input-block">' +
'<input type="checkbox" name="isenable" lay-skin="switch" lay-text="启用|禁用" ' + (data.isenable !== false ? 'checked' : '') + '>' +
'</div>' +
'</div>' +
'<div class="layui-form-item layui-form-text">' +
'<label class="layui-form-label">问题内容</label>' +
'<div class="layui-input-block">' +
'<div class="rich-editor" id="richEditor">' +
'<div class="rich-editor-toolbar">' +
'<button type="button" onclick="execCmd(\'bold\')"><b>B</b></button>' +
'<button type="button" onclick="execCmd(\'italic\')"><i>I</i></button>' +
'<button type="button" onclick="execCmd(\'underline\')"><u>U</u></button>' +
'<button type="button" onclick="execCmd(\'insertUnorderedList\')">• 列表</button>' +
'<button type="button" onclick="execCmd(\'formatBlock\',\'<h3>\')">标题</button>' +
'<button type="button" onclick="execCmd(\'createLink\')">链接</button>' +
'<button type="button" onclick="execCmd(\'formatBlock\',\'<p>\')">段落</button>' +
'</div>' +
'<div class="rich-editor-content" contenteditable="true" id="editorContent">' + (data.content || '') + '</div>' +
'</div>' +
'</div>' +
'</div>' +
'</form>' +
'</div>';
layer.open({
type: 1,
title: data.id ? '编辑问题' : '添加问题',
area: ['700px', '550px'],
content: html,
btn: ['保存', '取消'],
success: function(layero) {
form.render();
},
yes: function(index, layero) {
var title = layero.find('input[name="title"]').val();
if (!title) {
layer.msg('请输入问题标题');
return;
}
var content = layero.find('#editorContent').html();
var param = {
categoryId: currentCategoryId,
title: title,
content: content,
sort: parseInt(layero.find('input[name="sort"]').val()) || 0,
isenable: layero.find('input[name="isenable"]').prop('checked')
};
if (data.id) param.id = data.id;
var url = data.id ? 'adminapi/Faq/UpdateQuestion' : 'adminapi/Faq/AddQuestion';
admin.req({
url: setter.baseurl + url,
contentType: 'application/json',
method: 'post',
data: JSON.stringify(param),
done: function(res) {
layer.close(index);
if (res.code === 0) {
layer.msg('保存成功');
loadQuestions();
} else {
layer.msg(res.info || '保存失败');
}
}
});
}
});
}
// 富文本命令
window.execCmd = function(cmd, value) {
document.execCommand(cmd, false, value || null);
$('#editorContent').focus();
};
// 启用/禁用
form.on('switch(enable-switch)', function(obj) {
var id = this.value;
var isenable = obj.elem.checked;
admin.req({
url: setter.baseurl + 'adminapi/Faq/UpdateQuestion',
contentType: 'application/json',
method: 'post',
data: JSON.stringify({ id: id, isenable: isenable }),
done: function(res) {
if (res.code === 0) {
layer.msg(isenable ? '已启用' : '已禁用');
} else {
layer.msg(res.info || '操作失败');
obj.elem.checked = !isenable; // 回滚开关状态
}
}
});
});
// 排序编辑
table.on('edit(table-question)', function(obj) {
var id = obj.data.id;
var sort = obj.value;
admin.req({
url: setter.baseurl + 'adminapi/Faq/UpdateQuestion',
contentType: 'application/json',
method: 'post',
data: JSON.stringify({ id: id, sort: parseInt(sort) || 0 }),
done: function(res) {
if (res.code === 0) {
layer.msg('排序已更新');
} else {
layer.msg(res.info || '操作失败');
}
}
});
});
// 初始化
loadCategoryTree();
});
</script>