Files
coruna-lab/resources/views/admin/channels/index.blade.php
T
root 5b677d4d1f fix(admin): 渠道列表脚本因 \$input 无法执行
Blade 原样输出反斜杠,整段 layui 脚本语法错误,表格不会请求数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 12:19:13 +00:00

365 lines
17 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">
@include('admin.partials.filter_channel_select')
@include('admin.partials.filter_agent_select')
<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">
<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($agentOptions ?? []);
var isAdmin = portal === 'admin';
var maxPerAgent = @json($maxPerAgent ?? 5);
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
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) {
if (d.builder_type === 'app') return '<span style="color:#ea580c;font-weight:600;">App</span>';
return d.builder_type === 'new' ? '新版' : '旧版';
}},
];
if (isAdmin) {
cols.push({ field: 'agent_username', title: '代理', width: 120 });
}
cols = cols.concat([
{ field: 'app_name', title: 'App', width: 90, templet: function (d) { return d.app_name || '—'; } },
{ field: 'bundle_id', title: 'Bundle ID', minWidth: 200, templet: function (d) { return d.bundle_id ? '<code>' + d.bundle_id + '</code>' : '—'; } },
{ 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 ? 220 : 160, 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.label || 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 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.channel_dir) {
html += '<div style="margin-bottom:10px;"><b>资源目录</b> <code>' +
String(data.channel_dir).replace(/</g, '&lt;') + '</code></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
});
}
function openForm(title, values, creating) {
values = values || {};
var isApp = values.builder_type === 'app';
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>'
: '';
// Version selector (only on create for admin)
var versionBlock = (isAdmin && creating)
? '<div class="layui-form-item"><label class="layui-form-label">版本</label><div class="layui-input-block">' +
'<select name="builder_type" id="LAY-ch-builder-type" lay-filter="LAY-ch-builder-type">' +
'<option value="new"' + (!isApp ? ' selected' : '') + '>新版(生成静态资源)</option>' +
'<option value="app"' + (isApp ? ' selected' : '') + '>App(仅记录,UUID 渠道 ID)</option>' +
'</select></div></div>'
: '';
// Channel ID block: App uses auto-generated UUID (read-only); new uses X.Y.ZZ input
var channelBlock = creating
? (isApp
? '<div class="layui-form-item"><label class="layui-form-label">渠道 ID</label><div class="layui-input-block">' +
'<input name="channel_id" class="layui-input" readonly id="LAY-ch-id-input" value="' + (values.channel_id || '') + '" placeholder="自动生成 UUID">' +
'<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 name="channel_id" class="layui-input" maxlength="6" id="LAY-ch-id-input" style="width:70%;display:inline-block;" value="' + (values.channel_id || '') + '" placeholder="例如 A.B.C1 或 3.1.07">' +
'<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>';
// App-specific fields: app_name + bundle_id
var appFieldsBlock = (isApp || (creating && isAdmin))
? '<div class="layui-form-item LAY-ch-app-only" style="' + (isApp ? '' : 'display:none;') + '"><label class="layui-form-label">App 名称</label><div class="layui-input-block">' +
'<input name="app_name" class="layui-input" value="' + (values.app_name || '').replace(/"/g, '&quot;') + '" placeholder="例如 Ai"></div></div>' +
'<div class="layui-form-item LAY-ch-app-only" style="' + (isApp ? '' : 'display:none;') + '"><label class="layui-form-label">Bundle ID</label><div class="layui-input-block">' +
'<input name="bundle_id" class="layui-input" value="' + (values.bundle_id || '').replace(/"/g, '&quot;') + '" placeholder="例如 aai.AiAi168168AiAi.app"></div></div>'
: '';
var templateBlock = (isAdmin && creating)
? '<div class="layui-form-item LAY-ch-new-only" id="LAY-ch-template-item"' + (isApp ? ' style="display:none;"' : '') + '><label class="layui-form-label">页面模板</label><div class="layui-input-block">' +
'<select name="support_template">' +
'<option value="blank"' + ((values.support_template || 'blank') === 'blank' ? ' selected' : '') + '>blank(空白页)</option>' +
'<option value="test"' + (values.support_template === 'test' ? ' selected' : '') + '>test(加载页 / 15s 倒计时)</option>' +
'</select>' +
'<div class="layui-form-mid layui-word-aux">weifile.html:test=大圆圈加载+15s 倒计时;blank=空白页。路径 /channel/X.Y.ZZ/(c 取自 XXBB_CHANNEL_C)</div></div></div>'
: '';
layer.open({
type: 1,
title: title,
area: ['560px', creating ? (isAdmin ? '560px' : '380px') : '420px'],
content: '<form class="layui-form" style="padding:16px;" id="LAY-ch-form" lay-filter="LAY-ch-form">' +
versionBlock + channelBlock + agentBlock + templateBlock + appFieldsBlock +
'<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 生成静态资源;App 仅创建数据库记录。代理最多 ' + maxPerAgent + ' 条。</div></div>' : '') +
'</form>',
success: function () {
form.render();
$('#LAY-ch-rand').on('click', function () {
var bt = $('#LAY-ch-builder-type').val();
$.getJSON(@json(route('admin.channels.randomId')) + '?builder_type=' + bt, function (res) {
if (res.code === 0) $('#LAY-ch-form input[name=channel_id]').val(res.data.channel_id);
});
});
// Auto-generate UUID when switching to App
form.on('select(LAY-ch-builder-type)', function (data) {
var isAppNow = data.value === 'app';
$('.LAY-ch-app-only').toggle(isAppNow);
$('.LAY-ch-new-only').toggle(!isAppNow);
var idInput = $('#LAY-ch-id-input');
if (isAppNow) {
idInput.attr('readonly', true).attr('maxlength', '').attr('placeholder', '自动生成 UUID');
if (!idInput.val()) {
$.getJSON(@json(route('admin.channels.randomId')) + '?builder_type=app', function (res) {
if (res.code === 0) idInput.val(res.data.channel_id);
});
}
} else {
idInput.removeAttr('readonly').attr('maxlength', '6').attr('placeholder', '例如 A.B.C1 或 3.1.07').val('');
}
});
},
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);
if (window.CorunaFilterOptions) {
CorunaFilterOptions.bust();
CorunaFilterOptions.apply(form);
}
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);
if (window.CorunaFilterOptions) {
CorunaFilterOptions.bust();
CorunaFilterOptions.apply(form);
}
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 === 'del') {
var pathHint = obj.data.builder_type === 'new'
? ('数据库记录与 channel/' + obj.data.channel_id + '/')
: ('数据库记录与 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