Files

508 lines
27 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{$title | default='网址管理'}</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
<script type="text/javascript">
var config = { root: "/" };
</script>
</head>
<body>
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-form-item">
<div class="layui-inline">
<input type="text" name="title" placeholder="请输入标题" autocomplete="off" class="layui-input">
</div>
<div class="layui-inline">
<input type="text" name="url" placeholder="请输入URL" autocomplete="off" class="layui-input">
</div>
<div class="layui-inline">
<input type="text" name="keywords" placeholder="请输入关键词" autocomplete="off" class="layui-input">
</div>
<div class="layui-inline">
<input type="text" name="description" placeholder="请输入描述" autocomplete="off" class="layui-input">
</div>
<div class="layui-inline">
<select name="status" lay-filter="statusFilter">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
<option value="2">待审核</option>
<option value="dead">死链(检测异常)</option>
</select>
</div>
<div class="layui-inline">
<button class="layui-btn layuiadmin-btn-useradmin" lay-submit lay-filter="tableSearchButton">
<i class="layui-icon layui-icon-search layuiadmin-button-btn"></i>
</button>
</div>
</div>
</div>
<div class="layui-card-body">
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<!-- 顶部工具栏 -->
<script type="text/html" id="tableBar">
<button class="layui-btn layui-btn-sm" lay-event="checkLinks">检测死链</button>
<button class="layui-btn layui-btn-sm" lay-event="refreshFavicon">刷新图标</button>
<button class="layui-btn layui-btn-sm" lay-event="import">导入书签</button>
<button class="layui-btn layui-btn-sm" lay-event="export">导出书签</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="batchEnable">批量启用</button>
<button class="layui-btn layui-btn-sm layui-btn-warm" lay-event="batchDisable">批量禁用</button>
<button class="layui-btn layui-btn-sm" lay-event="recoverdead">恢复死链</button>
<button class="layui-btn layui-btn-sm layui-btn-primary" lay-event="batchMove">批量移动分类</button>
</script>
<script type="text/html" id="dataBar">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm layui-btn-primary" title="编辑" lay-event="update"><i class="layui-icon layui-icon-edit"></i></a>
{{# if(!d.is_super) { }}
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除" lay-event="delete"><i class="layui-icon layui-icon-delete"></i></a>
{{# } }}
<a class="layui-btn layui-btn-sm layui-btn-normal" title="通过" lay-event="approve" {{# if(d.status != 2){ }}style="display:none"{{# } }}><i class="layui-icon layui-icon-ok"></i></a>
<a class="layui-btn layui-btn-sm layui-btn-danger" title="拒绝" lay-event="reject" {{# if(d.status != 2){ }}style="display:none"{{# } }}><i class="layui-icon layui-icon-close"></i></a>
</div>
</script>
<script type="text/html" id="dataRecybinTpl">
<div class="layui-card">
<div class="layui-card-header"></div>
<div class="layui-card-body">
<table class="layui-hide" id="dataRecybinTable" lay-filter="dataRecybinTable"></table>
</div>
</div>
</script>
<script type="text/html" id="dataRecybinBarTpl">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm layui-btn-primary" title="恢复数据" lay-event="update"><i class="layui-icon layui-icon-edit"></i></a>
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete"><i class="layui-icon layui-icon-delete"></i></a>
</div>
</script>
<script type="text/html" id="statusTpl">
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-text="启用|禁用" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
</script>
<script type="text/html" id="hotspotTpl">
<input type="checkbox" name="is_hot" value="{{d.id}}" lay-skin="switch" lay-text="启用|禁用" lay-filter="hotspotSwitch" {{ d.is_hot == 1 ? 'checked' : '' }}>
</script>
<!-- 导入弹层 -->
<script type="text/html" id="importTpl">
<form class="layui-form" style="padding:20px;">
<div class="layui-form-item">
<label class="layui-form-label">目标分类</label>
<div class="layui-input-block">
<select name="cid" id="importCid">
<option value="">默认首个分类</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">书签文件</label>
<div class="layui-input-block">
<input type="file" name="file" id="importFile" accept=".html,.htm,.json" class="layui-input">
<div style="color:#999;font-size:12px;margin-top:6px;">支持浏览器导出的 Netscape 书签 HTML JSON 数组</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn" id="doImport">开始导入</button>
</div>
</div>
</form>
</script>
<!-- 添加/编辑表单 -->
<script type="text/html" id="dataFormTpl">
<form class="layui-form" style="padding: 20px 20px 0;">
<input type="hidden" name="id" value="{{d.id||''}}">
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input type="text" name="title" required lay-verify="required|account" placeholder="请输入标题" autocomplete="off" class="layui-input" value="{{d.title||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="cid" lay-verify="required">
<option value="">请选择分类</option>
{{# d.cates.forEach(function(item) { }}
<option value="{{item.id}}" {{ d.cid == item.id ? 'selected' : '' }}>{{item.title}}</option>
{{# }); }}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">链接</label>
<div class="layui-input-block">
<input type="text" name="url" lay-verify="url" placeholder="请输入链接" autocomplete="off" class="layui-input" value="{{d.url||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">返利PID</label>
<div class="layui-input-block">
<input type="text" name="pid" placeholder="选填,如淘宝/京东联盟推广位PID,点击跳转自动拼接返利参数" autocomplete="off" class="layui-input" value="{{d.pid||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">图标</label>
<div class="layui-input-block">
<input type="text" name="icon" placeholder="请输入图标链接" autocomplete="off" class="layui-input" value="{{d.icon||''}}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">关键词</label>
<div class="layui-input-block">
<textarea name="keywords" placeholder="请输入关键词" class="layui-textarea">{{d.keywords||''}}</textarea>
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">描述</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入描述信息" class="layui-textarea">{{d.description||''}}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" {{ d.status==undefined || d.status==1 ? 'checked' : '' }}>
<input type="radio" name="status" value="0" title="禁用" {{ d.status==0 ? 'checked' : '' }}>
<input type="radio" name="status" value="2" title="待审核" {{ d.status==2 ? 'checked' : '' }}>
</div>
</div>
<div class="layui-form-item layui-hide">
<button class="layui-btn" lay-submit="" lay-filter="wxapp-form-submit" id="wxapp-form-submit">提交</button>
</div>
</form>
</script>
<script src="/assets/layui/layui.js"></script>
<script src="/assets/ywxapp/ywxapp.js" module="backend"></script>
<script>
layui.use(['http', 'util'], function () {
var $ = layui.$, table = layui.table, form = layui.form, laytpl = layui.laytpl, http = layui.http, layer = layui.layer, util = layui.util;
var cates = [];
http.get('/haonav/backend/links/create').then((res) => {
if (res.code === 0) { cates = res.data || []; } else { layer.msg(res.msg || '数据加载失败'); }
});
var dataTable = table.render({
elem: '#dataTable',
defaultToolbar: [{
title: '创建资源', layEvent: 'dataCreate', icon: 'layui-icon-add-1'
}, {
title: '删除资源', layEvent: 'dataDelete', icon: 'layui-icon-delete'
}, {
title: '资源回收站', layEvent: 'dataRecybin', icon: 'layui-icon-home'
}, 'filter', 'exports', 'print'
]
, url: "/haonav/backend/links/index"
, page: true,
limit: 20,
limits: [10, 15, 20, 30],
parseData: function (res) {
return { "code": res.code, "msg": res.message || '', "count": res.count || 0, "data": res.data || [] };
},
toolbar: '#tableBar'
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', title: 'ID', width: 72, sort: true, fixed: 'left' }
, { field: 'title', title: '名称', width: 130, fixed: 'left' }
, { field: 'cate', title: '分类', width: 100, sort: true }
, { field: 'url', title: '链接', minWidth: 160, templet: function (d) { return '<a href="' + (d.url || '') + '" target="_blank" style="color:#01AAED;">' + (d.url || '') + '</a>'; } }
, { field: 'pid', title: '返利PID', width: 130, align: 'center', templet: function (d) { return d.pid ? '<span style="color:#e67e22;">' + d.pid + '</span>' : '-'; } }
, { field: 'icon', title: '图标', width: 90, align: 'center', templet: function (d) { return d.icon ? '<img src="' + d.icon + '" style="width:24px;height:24px;">' : '-'; } }
, { field: 'status_code', title: '检测码', width: 90, align: 'center', templet: function (d) { return d.status_code === null || d.status_code === '' ? '-' : (d.status_code >= 200 && d.status_code < 400 ? '<span style="color:#2ecc71">' + d.status_code + '</span>' : '<span style="color:#e74c3c">' + d.status_code + '</span>'); } }
, { field: 'fail_count', title: '连败', width: 70, align: 'center', templet: function (d) { return (d.fail_count && d.fail_count > 0) ? '<span style="color:#e67e22;">' + d.fail_count + '</span>' : '0'; } }
, { field: 'dead_at', title: '死链时间', width: 115, align: 'center', templet: function (d) { return (d.dead_at && d.dead_at > 0) ? '<span style="color:#e74c3c;">' + util.toDateString(d.dead_at * 1000, 'yyyy-MM-dd') + '</span>' : '-'; } }
, { field: 'keywords', title: '关键词', width: 120, align: 'center' }
, { field: 'description', title: '描述', minWidth: 160 }
, { field: 'status', title: '状态', width: 90, align: 'center', templet: '#statusTpl', unresize: true }
, { field: 'is_hot', title: '热点', width: 90, align: 'center', templet: '#hotspotTpl', unresize: true }
, { field: 'click_count', title: '点击', width: 90, sort: true }
, { field: 'sort', title: '排序', width: 80, sort: true, edit: 'text' }
, { field: 'create_at', title: '创建时间', width: 160, align: 'center', sort: true }
, { title: '操作', width: 150, align: 'left', toolbar: '#dataBar', fixed: 'right', unresize: true }
]],
done: function () { }
});
table.on('toolbar(dataTable)', function (obj) {
var options = obj.config;
switch (obj.event) {
case 'dataCreate': active.dataCreate({ pid: 0, type: 1 }); break;
case 'dataDelete':
var cs = table.checkStatus('dataTable'), cd = cs.data;
active.dataDelete(cd.map((i) => i.id));
break;
case 'dataRecybin': active.dataRecybin(); break;
case 'checkLinks':
layer.confirm('将逐一检测全部链接可达性(可能耗时较长),确定开始?', function (index) {
layer.close(index);
var load = layer.load(2);
http.post('checkLinks', {}).then(function (res) {
layer.close(load);
if (res.code == 0) {
var msg = '检测完成:共 ' + res.data.total + ' 条,正常 ' + res.data.ok + ' 条,异常 ' + res.data.dead + ' 条';
if (res.data.offlined) { msg += ',已自动下线 ' + res.data.offlined + ' 条'; }
if (res.data.recovered) { msg += ',已自动恢复 ' + res.data.recovered + ' 条'; }
layer.alert(msg);
table.reload('dataTable', {}, true);
} else { layer.msg(res.msg || '检测失败'); }
}).catch(function () { layer.close(load); layer.msg('请求失败'); });
});
break;
case 'refreshFavicon':
var cs = table.checkStatus('dataTable'), cd = cs.data;
if (cd.length === 0) { return layer.msg('请先勾选要刷新的链接'); }
http.post('refreshFavicon', { ids: cd.map((i) => i.id).join(',') }).then(function (res) {
if (res.code == 0) { layer.msg(res.msg || '已刷新'); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '失败'); }
});
break;
case 'import': active.dataImport(); break;
case 'export': window.open('export'); break;
case 'batchEnable': active.batchOp('enable', '确定批量启用选中链接?'); break;
case 'batchDisable': active.batchOp('disable', '确定批量禁用选中链接?'); break;
case 'batchMove': active.batchMove(); break;
case 'recoverdead': active.batchOp('recoverdead', '确定恢复选中死链(重新启用并清空失败计数)?'); break;
};
});
// 排序列行内编辑:失焦即保存
table.on('edit(dataTable)', function (obj) {
if (obj.field === 'sort') {
http.post('batch', { op: 'sort', ids: String(obj.data.id), sort: obj.value }).then(function (res) {
if (res.code === 0) { layer.msg(res.message || '排序已保存'); }
else { layer.msg(res.message || res.msg || '保存失败'); table.reload('dataTable', {}, true); }
});
}
});
table.on('tool(dataTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'update': active.dataEdit(data); break;
case 'create': active.dataCreate({ pid: data.id }); break;
case 'delete': active.dataDelete([data.id]); break;
case 'approve':
http.post('approve', { id: data.id }).then(function (res) {
if (res.code == 0) { layer.msg('已通过'); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
break;
case 'reject':
layer.confirm('拒绝后该投稿将被禁用,确定?', function (index) {
layer.close(index);
http.post('reject', { id: data.id }).then(function (res) {
if (res.code == 0) { layer.msg('已拒绝'); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
break;
default: break;
}
});
form.on('submit(tableSearchButton)', function (data) {
table.reload('dataTable', { where: data.field });
});
form.on('switch(statusSwitch)', function (obj) {
var id = this.value, status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该数据吗?', function (index) {
layui.http.put('update', { id: id, status: status }).then(function (res) {
if (res.code == 0) { layer.msg(res.msg, { icon: 1 }); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg, { icon: 2 }); obj.elem.checked = !obj.elem.checked; form.render('checkbox'); }
});
layer.close(index);
}, function () {
obj.elem.checked = !obj.elem.checked; form.render('checkbox');
});
});
form.on('switch(hotspotSwitch)', function (obj) {
var id = this.value, is_hot = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (is_hot ? '推荐' : '不推荐') + '该数据吗?', function (index) {
layui.http.put('update', { id: id, is_hot: is_hot }).then(function (res) {
if (res.code == 0) { layer.msg(res.msg, { icon: 1 }); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg, { icon: 2 }); obj.elem.checked = !obj.elem.checked; form.render('checkbox'); }
});
layer.close(index);
}, function () {
obj.elem.checked = !obj.elem.checked; form.render('checkbox');
});
});
var dataFromFun = function (data, callback, done) {
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
layer.open({
title: data.id ? '编辑数据' : '添加数据',
content: formHtml, anim: "slideLeft", offset: "r",
btnAlign: "l", area: [window.innerWidth > 1200 ? '30%' : (window.innerWidth > 768 ? '50%' : '99%'), '99%'],
shade: 0.1, shadeClose: true,
btn: ['确定', '取消'],
success: function (layero, index) { callback(layero, index); form.render(); },
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem); layui.off('submit(wxapp-form)', 'from'); return false;
});
layero.contents().find("#wxapp-form-submit").trigger('click');
}
});
};
var active = {
dataCreate: function (data = {}) {
data.cates = cates;
dataFromFun(data, function () { }, function (layero, index, elem) {
var field = elem.field; field.status = field.status ? 1 : 0;
http.post('save', field).then((res) => {
if (res.code === 0) { layer.close(index); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
},
dataEdit: function (data = {}) {
data.cates = cates;
dataFromFun(data, function () { }, function (layero, index, elem) {
var field = elem.field; field.status = field.status ? 1 : 0;
http.put('update', field).then((res) => {
if (res.code === 0) { layer.close(index); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
},
dataImport: function () {
var html = laytpl($('#importTpl').html()).render({});
layer.open({
title: '导入书签', content: html, area: ['480px', '320px'],
success: function (layero) {
var sel = layero.find('#importCid');
cates.forEach(function (c) { sel.append('<option value="' + c.id + '">' + c.title + '</option>'); });
form.render('select');
layero.find('#doImport').on('click', function () {
var fileInput = layero.find('#importFile')[0];
if (!fileInput.files.length) { return layer.msg('请选择书签文件'); }
var fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('cid', layero.find('#importCid').val());
layer.msg('导入中...', { icon: 16, time: 0 });
fetch('import', { method: 'POST', body: fd }).then(r => r.json()).then(function (res) {
layer.closeAll();
if (res.code == 0) { layer.msg(res.msg || '导入成功'); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '导入失败'); }
}).catch(function () { layer.closeAll(); layer.msg('导入失败'); });
});
}
});
},
dataDelete: function (ids, force = 0) {
if (ids.length === 0) { return layer.msg('请选择数据'); }
layer.prompt({ formType: 1, title: '敏感操作,请验证口令' }, function (value, index) {
layer.close(index);
layer.confirm('确定删除吗?', function (index) {
http.delete('delete', { ids: ids.join(','), force: force }).then((res) => {
if (res.code === 0) { layer.close(index); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
layer.msg('已删除'); table.reload('dataTable', {}, true);
});
});
},
dataRecybin: function (data = {}) {
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
layer.open({
title: '资源回收站', content: formHtml, anim: "slideLeft", offset: "r",
area: ['60%', '99%'], shade: 0.1, shadeClose: true,
success: function (layero, index) {
layui.table.render({
elem: '#dataRecybinTable', url: 'recyclebin', height: 'full-100',
defaultToolbar: [{
title: '批量删除数据', layEvent: 'dataDelete', icon: 'layui-icon-delete',
onClick: function (obj) {
var cs = table.checkStatus('dataRecybinTable'), cd = cs.data;
active.dataDelete(cd.map((i) => i.id), 1);
}
}, 'filter', 'exports', 'print'],
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: 'title', width: 180, fixed: 'left' },
{
fixed: "right", title: "操作", width: 120, align: "center", templet: function (d) {
return `<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" title="恢复数据" lay-event="restore" > <i class="layui-icon layui-icon-edit"></i> </a >
<a class="layui-btn layui-btn-sm" title="删除数据" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
</div >`;
}
}
]],
page: true,
done: function () {
layui.table.on('tool(dataRecybinTable)', function (elem) {
var data = elem.data;
if (elem.event === 'restore') { active.dataRestore([data.id]); }
else if (elem.event === 'delete') { active.dataDelete([data.id], 1); }
});
}
});
}
});
},
batchOp: function (op, confirmText, extra) {
var cd = table.checkStatus('dataTable').data;
if (!cd.length) { return layer.msg('请先勾选要操作的链接'); }
var ids = cd.map(function (i) { return i.id; }).join(',');
layer.confirm(confirmText || '确定执行批量操作?', function (index) {
layer.close(index);
var params = Object.assign({ op: op, ids: ids }, extra || {});
http.post('batch', params).then(function (res) {
if (res.code === 0) { layer.msg(res.message || '操作成功'); table.reload('dataTable', {}, true); }
else { layer.msg(res.message || res.msg || '操作失败'); }
});
});
},
batchMove: function () {
var cd = table.checkStatus('dataTable').data;
if (!cd.length) { return layer.msg('请先勾选要移动的链接'); }
var opts = cates.map(function (c) { return '<option value="' + c.id + '">' + c.title + '</option>'; }).join('');
layer.open({
title: '批量移动分类(已选 ' + cd.length + ' 条)',
area: ['420px', '220px'],
content: '<div class="layui-form" style="padding:24px;"><div class="layui-form-item"><label class="layui-form-label">目标分类</label><div class="layui-input-block"><select id="batchMoveCid" lay-filter="batchMoveCid"><option value="">请选择分类</option>' + opts + '</select></div></div></div>',
btn: ['确定移动', '取消'],
success: function () { form.render('select'); },
yes: function (index) {
var cid = $('#batchMoveCid').val();
if (!cid) { return layer.msg('请选择目标分类'); }
var ids = cd.map(function (i) { return i.id; }).join(',');
http.post('batch', { op: 'move', ids: ids, cid: cid }).then(function (res) {
if (res.code === 0) { layer.close(index); layer.msg(res.message || '已移动'); table.reload('dataTable', {}, true); }
else { layer.msg(res.message || res.msg || '移动失败'); }
});
}
});
},
dataRestore: function (ids) {
if (ids.length === 0) { return layer.msg('请选择数据'); }
http.put('restore', { ids: ids.join(',') }).then((res) => {
if (res.code === 0) { layer.msg('恢复成功'); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '恢复失败'); }
});
}
};
});
</script>
</body>
</html>