402 lines
18 KiB
PHP
402 lines
18 KiB
PHP
@extends('admin.content')
|
||
|
||
@section('title', '渠道链接')
|
||
|
||
@section('content')
|
||
@php $portal = $portal ?? 'admin'; @endphp
|
||
<div class="layui-card">
|
||
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-ch-search">
|
||
<div class="layui-form-item">
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label">渠道 ID</label>
|
||
<div class="layui-input-block">
|
||
<input type="text" name="channel_id" class="layui-input" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
@if ($portal === 'admin')
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label">代理</label>
|
||
<div class="layui-input-block">
|
||
<select name="agent_user_id">
|
||
<option value="">全部</option>
|
||
<option value="0">官方</option>
|
||
@foreach ($agents as $a)
|
||
<option value="{{ $a->id }}">{{ $a->username }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label">状态</label>
|
||
<div class="layui-input-block">
|
||
<select name="status">
|
||
<option value="">全部</option>
|
||
<option value="1">启用</option>
|
||
<option value="0">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline">
|
||
<button class="layui-btn" lay-submit lay-filter="LAY-ch-search">搜索</button>
|
||
@if ($portal === 'admin')
|
||
<button type="button" class="layui-btn layui-btn-normal" id="LAY-ch-create">新建</button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</form>
|
||
<div class="layui-card-body">
|
||
<table id="LAY-ch-list" lay-filter="LAY-ch-list"></table>
|
||
<script type="text/html" id="LAY-ch-ops">
|
||
@{{# if(d.builder_type === 'new'){ }}
|
||
@{{# if(d.download_url){ }}
|
||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="download">下载代码</a>
|
||
@{{# } }}
|
||
@{{# } else { }}
|
||
<a class="layui-btn layui-btn-xs" lay-event="links">查看链接</a>
|
||
@{{# } }}
|
||
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="edit">编辑</a>
|
||
@{{# if(d._isAdmin){ }}
|
||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||
@{{# } }}
|
||
</script>
|
||
</div>
|
||
</div>
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
<script>
|
||
layui.use(['table', 'form', 'layer'], function () {
|
||
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
|
||
var portal = @json($portal);
|
||
var token = @json(csrf_token());
|
||
var agents = @json(($agents ?? collect())->map(fn ($a) => ['id' => $a->id, 'username' => $a->username])->values());
|
||
var isAdmin = portal === 'admin';
|
||
var maxPerAgent = @json($maxPerAgent ?? 5);
|
||
|
||
var cols = [
|
||
{ field: 'id', title: 'ID', width: 70, sort: true },
|
||
{ field: 'channel_id', title: '渠道 ID', minWidth: 220, sort: true },
|
||
{ field: 'builder_type', title: '类型', width: 80, templet: function (d) {
|
||
return d.builder_type === 'new' ? '新版' : '旧版';
|
||
}},
|
||
];
|
||
if (isAdmin) {
|
||
cols.push({ field: 'agent_username', title: '代理', width: 120 });
|
||
}
|
||
cols = cols.concat([
|
||
{ field: 'remark', title: '备注', minWidth: 140, templet: function (d) { return d.remark || '—'; } },
|
||
{ field: 'status', title: '状态', width: 90, templet: function (d) {
|
||
return d.status == 1
|
||
? '<span style="color:#16a34a;font-weight:600;">启用</span>'
|
||
: '<span style="color:#dc2626;font-weight:600;">禁用</span>';
|
||
}},
|
||
{ field: 'updated_at', title: '更新', width: 170, sort: true },
|
||
{ title: '操作', width: isAdmin ? 250 : 190, align: 'center', fixed: 'right', toolbar: '#LAY-ch-ops' }
|
||
]);
|
||
|
||
table.render({
|
||
elem: '#LAY-ch-list',
|
||
id: 'LAY-ch-list',
|
||
url: @json(route($portal.'.channels.data')),
|
||
cols: [cols],
|
||
page: true, limit: 20, limits: [10, 20, 30, 50],
|
||
parseData: function (res) {
|
||
(res.data || []).forEach(function (row) { row._isAdmin = isAdmin; });
|
||
return res;
|
||
},
|
||
request: { pageName: 'page', limitName: 'limit' },
|
||
response: { statusName: 'code', statusCode: 0, msgName: 'msg', countName: 'count', dataName: 'data' }
|
||
});
|
||
|
||
form.on('submit(LAY-ch-search)', function (data) {
|
||
table.reload('LAY-ch-list', { where: data.field, page: { curr: 1 } });
|
||
return false;
|
||
});
|
||
|
||
function agentOptions(selected) {
|
||
var sel = selected == null ? 0 : selected;
|
||
var html = '<option value="0"' + (String(sel) === '0' ? ' selected' : '') + '>官方</option>';
|
||
agents.forEach(function (a) {
|
||
html += '<option value="' + a.id + '"' + (String(sel) === String(a.id) ? ' selected' : '') + '>' + a.username + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function copyText(text) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
return navigator.clipboard.writeText(text);
|
||
}
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
return Promise.resolve();
|
||
}
|
||
|
||
function promoIframe(url) {
|
||
return '<iframe src="' + url + '" style="position:fixed;top:0;left:-1000px;pointer-events:none;border:0"></iframe>';
|
||
}
|
||
|
||
function weifileIframeSnippet() {
|
||
return '<iframe src="/weifile/weifile.html" style="position:fixed;top:0;width:0;height:0;left:-1000px;border:0"></iframe>';
|
||
}
|
||
|
||
function openDownloadGuide(row) {
|
||
var url = row && row.download_url;
|
||
if (!url) return layer.msg('安装包不存在');
|
||
var snippet = weifileIframeSnippet();
|
||
var html = '<div style="padding:18px 20px;line-height:1.75;font-size:13px;color:#334155;">' +
|
||
'<p style="margin:0 0 10px;">下载后按下面步骤放到自己的落地页项目中:</p>' +
|
||
'<p style="margin:0 0 8px;"><b>1.</b> 解压 zip,把得到的 <code>weifile/</code> 目录放到落地页网站根目录,保证能访问到 <code>/weifile/weifile.html</code>。</p>' +
|
||
'<p style="margin:0 0 14px;"><b>2.</b> 在自己的 HTML 里(建议放在 <code><body></code> 开头)加入一段不可见 iframe:</p>' +
|
||
'<pre style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px;overflow:auto;font-size:12px;margin:0 0 14px;white-space:pre-wrap;word-break:break-all;">' +
|
||
snippet.replace(/</g, '<') + '</pre>' +
|
||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
|
||
'<a class="layui-btn layui-btn-normal" href="' + url.replace(/"/g, '"') + '">下载代码</a>' +
|
||
'<button type="button" class="layui-btn layui-btn-primary LAY-ch-copy-iframe">复制 iframe 代码</button>' +
|
||
'</div>' +
|
||
'<p style="margin:12px 0 0;color:#64748b;font-size:12px;">iframe 不可见,不影响落地页展示;用户打开落地页时会加载该渠道代码。</p>' +
|
||
'</div>';
|
||
layer.open({
|
||
type: 1,
|
||
title: '下载代码 — ' + (row.channel_id || ''),
|
||
area: ['640px', '430px'],
|
||
content: html,
|
||
success: function (layero) {
|
||
layero.find('.LAY-ch-copy-iframe').on('click', function () {
|
||
copyText(snippet).then(function () { layer.msg('已复制 iframe 代码'); });
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function openLinks(row) {
|
||
var links = row.links || [];
|
||
if (!links.length) {
|
||
return layer.msg('未生成投放链接(请配置 CORUNA_LAB_CHANNEL_DOMAINS 或系统设置→投放域名)');
|
||
}
|
||
var html = '<div style="padding:16px;">';
|
||
links.forEach(function (u, i) {
|
||
html += '<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px;">' +
|
||
'<input class="layui-input" readonly id="LAY-ch-link-' + i + '" value="' + u.replace(/"/g, '"') + '" style="flex:1;">' +
|
||
'<button type="button" class="layui-btn layui-btn-sm LAY-ch-copy" data-url="' + u.replace(/"/g, '"') + '">复制链接</button>' +
|
||
'<button type="button" class="layui-btn layui-btn-normal layui-btn-sm LAY-ch-promo" data-url="' + u.replace(/"/g, '"') + '">复制推广代码</button>' +
|
||
'</div>';
|
||
});
|
||
html += '</div>';
|
||
layer.open({
|
||
type: 1,
|
||
title: '渠道链接 — ' + row.channel_id,
|
||
area: ['720px', '360px'],
|
||
content: html,
|
||
success: function (layero) {
|
||
layero.find('.LAY-ch-copy').on('click', function () {
|
||
var url = $(this).data('url');
|
||
copyText(url).then(function () { layer.msg('已复制链接'); });
|
||
});
|
||
layero.find('.LAY-ch-promo').on('click', function () {
|
||
var url = $(this).data('url');
|
||
copyText(promoIframe(url)).then(function () { layer.msg('推广代码已复制'); });
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function showCreateResult(res, channelId) {
|
||
var data = res.data || {};
|
||
var links = data.links || [];
|
||
var html = '<div style="padding:16px;line-height:1.6;font-size:13px;">';
|
||
html += '<div style="margin-bottom:10px;"><b>渠道</b> ' + channelId + '</div>';
|
||
if (data.weifile_path || data.support_path) {
|
||
html += '<div style="margin-bottom:10px;"><b>落地页</b> <code>' +
|
||
(data.weifile_path || data.support_path).replace(/</g, '<') + '</code></div>';
|
||
}
|
||
if (data.builder_type === 'new' && data.zip_path) {
|
||
html += '<div style="margin-bottom:10px;"><b>安装包</b> <code>' +
|
||
String(data.zip_path).replace(/</g, '<') + '</code></div>';
|
||
if (data.download_url) {
|
||
html += '<div style="margin-bottom:10px;"><button type="button" class="layui-btn layui-btn-normal layui-btn-sm LAY-ch-dl-guide">下载代码</button></div>';
|
||
}
|
||
}
|
||
if (links.length) {
|
||
html += '<div style="margin-bottom:6px;"><b>投放链接</b></div>';
|
||
links.forEach(function (u) {
|
||
html += '<div style="word-break:break-all;margin-bottom:6px;"><code>' + u.replace(/</g, '<') + '</code></div>';
|
||
});
|
||
}
|
||
if (data.domains && ((data.domains.deployment || []).length || (data.domains.reporting || []).length)) {
|
||
html += '<div style="margin:12px 0 6px;"><b>' +
|
||
(data.seeds_initialized ? '首次 DGA 域名(请去注册/绑源站)' : 'DGA 域名') +
|
||
'</b></div>';
|
||
html += '<div><b>Deployment</b></div><ul style="margin:4px 0 8px 18px;">';
|
||
(data.domains.deployment || []).forEach(function (d) { html += '<li>' + d + '</li>'; });
|
||
html += '</ul><div><b>Reporting</b></div><ul style="margin:4px 0 0 18px;">';
|
||
(data.domains.reporting || []).forEach(function (d) { html += '<li>' + d + '</li>'; });
|
||
html += '</ul>';
|
||
}
|
||
html += '</div>';
|
||
layer.open({
|
||
type: 1,
|
||
title: '创建成功',
|
||
area: ['560px', '420px'],
|
||
content: html,
|
||
success: function (layero) {
|
||
layero.find('.LAY-ch-dl-guide').on('click', function () {
|
||
openDownloadGuide({ channel_id: channelId, download_url: data.download_url });
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function openForm(title, values, creating) {
|
||
values = values || {};
|
||
var agentBlock = isAdmin
|
||
? '<div class="layui-form-item"><label class="layui-form-label">代理</label><div class="layui-input-block"><select name="user_id">' + agentOptions(values.user_id) + '</select></div></div>'
|
||
: '';
|
||
var channelBlock = creating
|
||
? '<div class="layui-form-item"><label class="layui-form-label">渠道 ID</label><div class="layui-input-block">' +
|
||
'<input name="channel_id" class="layui-input" maxlength="32" id="LAY-ch-id-input" style="width:70%;display:inline-block;" value="' + (values.channel_id || '') + '" placeholder="旧版 32 位 / 新版 8 位">' +
|
||
'<button type="button" class="layui-btn layui-btn-primary" id="LAY-ch-rand" style="margin-left:6px;">随机</button></div></div>'
|
||
: '<div class="layui-form-item"><label class="layui-form-label">渠道 ID</label><div class="layui-input-block"><input class="layui-input" readonly value="' + (values.channel_id || '') + '"></div></div>';
|
||
|
||
var typeBlock = (isAdmin && creating)
|
||
? '<div class="layui-form-item"><label class="layui-form-label">类型</label><div class="layui-input-block">' +
|
||
'<select name="builder_type" lay-filter="LAY-ch-builder-type">' +
|
||
'<option value="old"' + ((values.builder_type || 'old') === 'old' ? ' selected' : '') + '>旧版</option>' +
|
||
'<option value="new"' + (values.builder_type === 'new' ? ' selected' : '') + '>新版</option>' +
|
||
'</select>' +
|
||
'<div class="layui-form-mid layui-word-aux">旧版=/web/id/support.html;新版=8 位渠道码,打包已构建的 weifile(需先 php artisan xxbb:build)</div></div></div>'
|
||
: '';
|
||
|
||
var templateBlock = (isAdmin && creating)
|
||
? '<div class="layui-form-item" id="LAY-ch-template-item"><label class="layui-form-label">页面模板</label><div class="layui-input-block">' +
|
||
'<select name="support_template">' +
|
||
'<option value="test"' + ((values.support_template || 'test') === 'test' ? ' selected' : '') + '>test(含 HUD 进度页)</option>' +
|
||
'<option value="blank"' + (values.support_template === 'blank' ? ' selected' : '') + '>blank(空白页)</option>' +
|
||
'</select>' +
|
||
'<div class="layui-form-mid layui-word-aux">控制生成的 support.html:test=HUD;blank=空白</div></div></div>'
|
||
: '';
|
||
|
||
layer.open({
|
||
type: 1,
|
||
title: title,
|
||
area: ['560px', creating ? (isAdmin ? '580px' : '380px') : '420px'],
|
||
content: '<form class="layui-form" style="padding:16px;" id="LAY-ch-form" lay-filter="LAY-ch-form">' +
|
||
channelBlock + typeBlock + agentBlock + templateBlock +
|
||
'<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-block">' +
|
||
'<input name="remark" class="layui-input" value="' + (values.remark || '').replace(/"/g, '"') + '"></div></div>' +
|
||
'<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-block">' +
|
||
'<input type="checkbox" name="status_switch" lay-skin="switch" lay-text="启用|禁用" ' + ((values.status == null || values.status == 1) ? 'checked' : '') + '>' +
|
||
'</div></div>' +
|
||
(creating && isAdmin ? '<div class="layui-form-item"><div class="layui-input-block" style="color:#888;font-size:12px;">新建将调用对应 builder 生成静态资源。代理最多 ' + maxPerAgent + ' 条。</div></div>' : '') +
|
||
'</form>',
|
||
success: function () {
|
||
form.render();
|
||
function syncTemplateVisibility() {
|
||
var type = $('#LAY-ch-form select[name=builder_type]').val() || 'old';
|
||
var $id = $('#LAY-ch-id-input');
|
||
if (type === 'new') {
|
||
$('#LAY-ch-template-item').hide();
|
||
$id.attr('maxlength', 8).attr('placeholder', '8位,例如 FAFA9988');
|
||
if (($id.val() || '').length !== 8) $id.val('');
|
||
} else {
|
||
$('#LAY-ch-template-item').show();
|
||
$id.attr('maxlength', 32).attr('placeholder', '32位字母数字');
|
||
}
|
||
}
|
||
syncTemplateVisibility();
|
||
form.on('select(LAY-ch-builder-type)', function () { syncTemplateVisibility(); });
|
||
$('#LAY-ch-rand').on('click', function () {
|
||
var type = $('#LAY-ch-form select[name=builder_type]').val() || 'old';
|
||
$.getJSON(@json(route('admin.channels.randomId')), { builder_type: type }, function (res) {
|
||
if (res.code === 0) $('#LAY-ch-form input[name=channel_id]').val(res.data.channel_id);
|
||
});
|
||
});
|
||
},
|
||
btn: ['保存', '取消'],
|
||
yes: function (index) {
|
||
var data = {};
|
||
$('#LAY-ch-form').serializeArray().forEach(function (x) { data[x.name] = x.value; });
|
||
data.status = $('#LAY-ch-form input[name=status_switch]').is(':checked') ? 1 : 0;
|
||
delete data.status_switch;
|
||
if (creating) {
|
||
var load = layer.load(2);
|
||
$.ajax({
|
||
url: @json(route('admin.channels.store')),
|
||
method: 'POST',
|
||
data: Object.assign(data, { _token: token }),
|
||
success: function (res) {
|
||
layer.close(load);
|
||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||
layer.close(index);
|
||
table.reload('LAY-ch-list');
|
||
showCreateResult(res, data.channel_id);
|
||
},
|
||
error: function (xhr) {
|
||
layer.close(load);
|
||
var msg = (xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '创建失败';
|
||
if (xhr.responseJSON && xhr.responseJSON.errors) {
|
||
msg = Object.values(xhr.responseJSON.errors).flat().join('; ');
|
||
}
|
||
layer.msg(msg);
|
||
}
|
||
});
|
||
} else {
|
||
$.ajax({
|
||
url: @json(url('/'.$portal.'/channels')) + '/' + values.id,
|
||
method: 'POST',
|
||
data: Object.assign(data, { _method: 'PUT', _token: token }),
|
||
success: function (res) {
|
||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||
layer.close(index);
|
||
table.reload('LAY-ch-list');
|
||
layer.msg('已保存');
|
||
},
|
||
error: function (xhr) {
|
||
var msg = (xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '保存失败';
|
||
if (xhr.responseJSON && xhr.responseJSON.errors) {
|
||
msg = Object.values(xhr.responseJSON.errors).flat().join('; ');
|
||
}
|
||
layer.msg(msg);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (isAdmin) {
|
||
$('#LAY-ch-create').on('click', function () { openForm('新建渠道链接', { user_id: 0, status: 1 }, true); });
|
||
}
|
||
|
||
table.on('tool(LAY-ch-list)', function (obj) {
|
||
if (obj.event === 'edit') openForm('编辑渠道链接', obj.data, false);
|
||
if (obj.event === 'links') openLinks(obj.data);
|
||
if (obj.event === 'download') {
|
||
openDownloadGuide(obj.data);
|
||
}
|
||
if (obj.event === 'del') {
|
||
var pathHint = obj.data.builder_type === 'new'
|
||
? ('数据库记录与 channel-source-new/' + obj.data.channel_id + '.zip')
|
||
: ('数据库记录与 web/' + obj.data.channel_id + ' 资源');
|
||
layer.confirm('删除后将移除 ' + pathHint + ',确认?', function (idx) {
|
||
$.ajax({
|
||
url: @json(url('/admin/channels')) + '/' + obj.data.id,
|
||
method: 'POST',
|
||
data: { _method: 'DELETE', _token: token },
|
||
success: function (res) {
|
||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||
layer.close(idx);
|
||
table.reload('LAY-ch-list');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
@endpush
|