Files
superlink/static/js/channel.js
T

410 lines
21 KiB
JavaScript
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.
/**
* channel.js - 渠道管理逻辑(v1.0.18 重构)
* 目标:记录企业/人员数据的来源渠道,并根据以往数据有计划地拓展高效渠道
* 两部分:
* 1) 渠道效能分析:企业(左)/人员(右)两个板块,右上角年份下拉(默认全部)
* - 按「渠道」字段饼状图按百分比排序;鼠标悬停饼图区域显示该渠道 source_detail TOP10 表格
* - 表格字段:来源(只显示前 8 字,超出用…)、数量;默认显示渠道第一名
* 2) 渠道新增计划:列表(渠道类别/来源详情/所属行业/时间窗口/剩余天数/备注说明/状态反馈)
*/
$(function () {
renderShell('渠道管理', '业务模块 / 渠道管理');
renderPage();
initPage('channel', function (user) {
if (!checkPagePermission('channel')) return;
loadAnalysis();
loadPlans(1);
});
var currentPage = 1;
var pageRows = [];
var analysisData = null;
var charts = { company: null, person: null };
/** 渲染页面内容:渠道效能分析(左右两板块)+ 渠道新增计划 */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">' +
' <b style="font-size:15px;color:#1e2a3a;">渠道效能分析</b>' +
' <span style="font-size:12px;color:#8a94a6;">按「渠道」字段统计,鼠标悬停饼图查看该渠道来源详情 TOP10</span>' +
' </div>' +
' <div style="display:flex;gap:14px;flex-wrap:wrap;">' +
' <div class="ana-panel">' +
' <div class="ana-head"><span>企业数据</span>' +
' <select id="ana-year-company" class="ana-year"><option value="">全部年份</option></select></div>' +
' <div class="ana-body">' +
' <div id="ana-chart-company" class="ana-chart"></div>' +
' <div class="ana-detail"><div class="ana-detail-title" id="ana-detail-title-c">渠道明细</div>' +
' <table class="grid" style="font-size:12px;"><thead><tr><th>来源</th><th>数量</th></tr></thead>' +
' <tbody id="ana-detail-c"></tbody></table></div>' +
' </div>' +
' </div>' +
' <div class="ana-panel">' +
' <div class="ana-head"><span>人员数据</span>' +
' <select id="ana-year-person" class="ana-year"><option value="">全部年份</option></select></div>' +
' <div class="ana-body">' +
' <div id="ana-chart-person" class="ana-chart"></div>' +
' <div class="ana-detail"><div class="ana-detail-title" id="ana-detail-title-p">渠道明细</div>' +
' <table class="grid" style="font-size:12px;"><thead><tr><th>来源</th><th>数量</th></tr></thead>' +
' <tbody id="ana-detail-p"></tbody></table></div>' +
' </div>' +
' </div>' +
' </div>' +
'</div>' +
'<div class="card" style="margin-top:14px;">' +
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">' +
' <b style="font-size:15px;color:#1e2a3a;">渠道新增计划</b>' +
' <span style="font-size:12px;color:#8a94a6;">根据历史数据有计划地拓展高效渠道</span>' +
' </div>' +
' <div class="toolbar">' +
' <select data-filter="channel_type"><option value="">全部渠道类别</option></select>' +
' <select data-filter="status"><option value="">全部状态</option>' +
' <option value="待启动">待启动</option><option value="已执行">已执行</option><option value="错过">错过</option></select>' +
' <button class="btn btn-primary" id="btn-plan-search">搜索</button>' +
' <button class="btn" id="btn-plan-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-plan-add">+ 新增计划</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>渠道类别</th><th>来源详情</th><th>发生地址</th><th>所属行业</th><th>时间窗口</th><th>剩余天数</th><th>备注说明</th><th>状态反馈</th><th>费用(元)</th><th>创建时间</th></tr></thead>' +
' <tbody id="plan-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="plan-pagination"></div>' +
'</div>'
);
}
/* ============================================================
* 1) 渠道效能分析
* ============================================================ */
function loadAnalysis() {
var year = null;
var $c = $('#ana-year-company');
var $p = $('#ana-year-person');
if ($c.length && $p.length) {
var yc = $c.val();
var yp = $p.val();
year = (yc === yp) ? yc : ''; // 两板块年份不同时,分别请求
}
if (year !== null && year === '' && $c.length && $p.length && $c.val() !== $p.val()) {
// 两板块各自请求
httpGet('channel/analysis.php', { year: $c.val() || '' }).then(function (d) {
analysisData = d;
renderAnalysis(d, 'company');
fillYears(d, 'company');
});
httpGet('channel/analysis.php', { year: $p.val() || '' }).then(function (d) {
renderAnalysis(d, 'person');
fillYears(d, 'person');
});
return;
}
httpGet('channel/analysis.php', { year: year || '' }).then(function (d) {
analysisData = d;
fillYears(d, 'company');
fillYears(d, 'person');
renderAnalysis(d, 'company');
renderAnalysis(d, 'person');
});
}
function fillYears(d, side) {
var $sel = side === 'company' ? $('#ana-year-company') : $('#ana-year-person');
var cur = $sel.val();
var html = '<option value="">全部年份</option>';
(d.years || []).forEach(function (y) {
html += '<option value="' + y + '"' + (String(y) === String(cur) ? ' selected' : '') + '>' + y + '年</option>';
});
$sel.html(html);
}
function renderAnalysis(d, side) {
var rows = side === 'company' ? (d.company || []) : (d.person || []);
var chartId = side === 'company' ? 'ana-chart-company' : 'ana-chart-person';
var detailBodyId = side === 'company' ? 'ana-detail-c' : 'ana-detail-p';
var detailTitleId = side === 'company' ? 'ana-detail-title-c' : 'ana-detail-title-p';
// 饼图数据(按百分比排序)
var pieData = rows.map(function (r) {
return { name: r.channel, value: r.count, percent: r.percent };
}).sort(function (a, b) { return b.value - a.value; });
if (!charts[side]) {
charts[side] = echarts.init(document.getElementById(chartId));
}
var chart = charts[side];
chart.clear();
chart.setOption({
tooltip: {
trigger: 'item',
formatter: function (p) {
if (p.data && p.data.percent !== undefined) {
return p.data.name + '<br/>数量:' + p.data.value + '(' + p.data.percent + '%)';
}
return p.name + '<br/>数量:' + p.value;
}
},
legend: { bottom: 0, type: 'scroll', textStyle: { fontSize: 11 } },
series: [{
type: 'pie',
radius: ['38%', '62%'],
center: ['50%', '44%'],
data: pieData,
label: { show: true, formatter: '{b}\n{d}%', fontSize: 11 },
emphasis: { itemStyle: { shadowBlur: 8, shadowColor: 'rgba(0,0,0,0.2)' } }
}]
});
// 默认显示第一名渠道的 source_detail TOP10
function renderDetail(index) {
var r = rows[index];
if (!r) {
$('#' + detailBodyId).html('<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无数据</td></tr>');
return;
}
$('#' + detailTitleId).text(r.channel + ' 来源明细(TOP' + Math.min(10, (r.details || []).length) + ',共 ' + r.count + ' 条)');
var html = '';
(r.details || []).forEach(function (item) {
var src = item.source || '';
html += '<tr><td class="src-cell" title="' + escHtml(src) + '">' + escHtml(src) + '</td><td>' + (item.count !== undefined ? item.count : item.cnt) + '</td></tr>';
});
if (!(r.details || []).length) {
html = '<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无明细</td></tr>';
}
$('#' + detailBodyId).html(html);
}
renderDetail(0);
// 悬停饼图区域 -> 显示该渠道 TOP10 表格
chart.off('mouseover');
chart.on('mouseover', function (p) {
if (p.dataIndex !== undefined) {
renderDetail(p.dataIndex);
}
});
chart.off('mouseout');
chart.on('mouseout', function () {
renderDetail(0);
});
// 空数据提示
if (!rows.length) {
$('#' + detailBodyId).html('<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无渠道数据(企业/人员需填写来源渠道)</td></tr>');
}
}
$('#ana-year-company, #ana-year-person').on('change', function () {
loadAnalysis();
});
/* ============================================================
* 2) 渠道新增计划
* ============================================================ */
function loadPlans(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('channel/plan_list.php', params).then(function (data) {
var rows = data.list || [];
pageRows = rows;
var html = '';
rows.forEach(function (r) {
var win = r.start_date && r.end_date
? fmtWin(r.start_date) + ' 至 ' + fmtWin(r.end_date)
: (r.start_date || r.end_date || '-');
var statusSel = '<select class="plan-status" data-id="' + r.id + '" style="height:28px;padding:0 6px;font-size:12px;border:1px solid #d9dee8;border-radius:4px;">' +
'<option value="待启动"' + (r.status === '待启动' ? ' selected' : '') + '>待启动</option>' +
'<option value="已执行"' + (r.status === '已执行' ? ' selected' : '') + '>已执行</option>' +
'<option value="错过"' + (r.status === '错过' ? ' selected' : '') + '>错过</option></select>';
var costInput = '<input type="text" class="plan-cost" data-id="' + r.id + '" value="' + escHtml(r.cost !== null && r.cost !== undefined ? r.cost : '') + '" placeholder="0.00"' +
(r.status === '已执行' ? '' : ' disabled') + ' style="width:90px;height:28px;padding:0 6px;font-size:12px;border:1px solid #d9dee8;border-radius:4px;text-align:right;">';
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.channel_type) + '</td>' +
'<td title="' + escHtml(r.source_detail || '') + '">' + escHtml((r.source_detail || '-').substring(0, 16)) + '</td>' +
'<td title="' + escHtml(r.occurrence_address || '') + '">' + escHtml((r.occurrence_address || '-').substring(0, 20)) + '</td>' +
'<td>' + escHtml(r.industry || '-') + '</td>' +
'<td>' + escHtml(win) + '</td>' +
'<td>' + (r.remaining_days > 0 ? '<span class="tag' + (r.remaining_days <= 7 ? ' tag-red' : '') + '">' + r.remaining_days + ' 天</span>' : '<span class="tag tag-gray">已到期</span>') + '</td>' +
'<td title="' + escHtml(r.remark || '') + '">' + escHtml((r.remark || '-').substring(0, 20)) + '</td>' +
'<td>' + statusSel + '</td>' +
'<td>' + costInput + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无渠道新增计划</td></tr>';
}
$('#plan-tbody').html(html);
bindPlanInlineEdit();
renderPagination($('#plan-pagination'), data, loadPlans);
});
}
/** 行内实时编辑:状态下拉随时改;费用(元)仅在「已执行」时激活可输入 */
function bindPlanInlineEdit() {
$('.plan-status').off('change.plan').on('change.plan', function () {
var id = $(this).data('id');
var status = $(this).val();
var $row = $(this).closest('tr');
var $cost = $row.find('.plan-cost');
// 仅控制可编辑性,不改 CSS 样式(保持「已执行」的样式呈现)
$cost.prop('disabled', status !== '已执行');
httpPost('channel/plan_save.php', { id: id, status: status }).then(function () {
Dialog.toast('状态已更新为「' + status + '」');
}).catch(function () {
loadPlans(currentPage);
});
});
$('.plan-cost').off('change.plan').on('change.plan', function () {
var id = $(this).data('id');
var val = $.trim($(this).val());
if (val === '') return;
if (isNaN(parseFloat(val))) {
Dialog.error('费用(元)请输入数字');
return;
}
var cost = Math.round(parseFloat(val) * 100) / 100;
$(this).val(cost);
httpPost('channel/plan_save.php', { id: id, cost: cost }).then(function () {
Dialog.toast('费用已更新:¥' + cost.toFixed(2));
}).catch(function () {
loadPlans(currentPage);
});
});
}
/** 时间窗口显示格式:2025-4-1 */
function fmtWin(d) {
if (!d) return '';
var parts = String(d).split('-');
if (parts.length === 3) {
return parseInt(parts[0], 10) + '-' + parseInt(parts[1], 10) + '-' + parseInt(parts[2], 10);
}
return d;
}
function statusTag(s) {
if (s === '已执行') return '<span class="tag tag-green">已执行</span>';
if (s === '错过') return '<span class="tag tag-red">错过</span>';
return '<span class="tag">待启动</span>';
}
$('#btn-plan-search').on('click', function () { loadPlans(1); });
$('#btn-plan-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadPlans(1);
});
/* ---------- 新增/编辑计划 ---------- */
$('#btn-plan-add').on('click', function () { openPlanForm(null); });
window.editPlan = function (id) {
var row = null;
pageRows.forEach(function (r) { if (r.id == id) row = r; });
openPlanForm(row);
};
function openPlanForm(pl) {
var isEdit = !!pl;
var r = pl || {};
// 渠道类别下拉(来自 source_channels 字典)
var chHtml = '<option value="">请选择渠道类别</option>';
var hasCur = false;
if (window._channelTypes && window._channelTypes.length) {
window._channelTypes.forEach(function (c) {
var sel = (c === r.channel_type) ? ' selected' : '';
if (sel) hasCur = true;
chHtml += '<option value="' + escHtml(c) + '"' + sel + '>' + escHtml(c) + '</option>';
});
}
if (r.channel_type && !hasCur) {
chHtml += '<option value="' + escHtml(r.channel_type) + '" selected>' + escHtml(r.channel_type) + '</option>';
}
var content =
'<form id="plan-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>渠道类别<span class="req">*</span></label><select name="channel_type">' + chHtml + '</select></div>' +
' <div class="form-item"><label>所属行业</label><input type="text" name="industry" value="' + escHtml(r.industry || '') + '"></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(r.source_detail || '') + '" placeholder="如:华南工博会"></div>' +
' <div class="form-item"><label>发生地址</label><input type="text" name="occurrence_address" value="' + escHtml(r.occurrence_address || '') + '" placeholder="详细到门牌号,如:深圳市南山区科技园路1号A栋101"></div>' +
' <div class="form-item"><label>状态反馈</label><select name="status">' +
' <option value="待启动"' + (!r.status || r.status === '待启动' ? ' selected' : '') + '>待启动</option>' +
' <option value="已执行"' + (r.status === '已执行' ? ' selected' : '') + '>已执行</option>' +
' <option value="错过"' + (r.status === '错过' ? ' selected' : '') + '>错过</option></select></div>' +
' <div class="form-item"><label>费用(元)</label><input type="text" name="cost" value="' + escHtml(r.cost !== null && r.cost !== undefined ? r.cost : '') + '" placeholder="仅已执行时可填写"' +
(r.status === '已执行' ? '' : ' disabled') + '></div>' +
' <div class="form-item"><label>开始日期</label><input type="date" name="start_date" value="' + escHtml(r.start_date || '') + '"></div>' +
' <div class="form-item"><label>结束日期</label><input type="date" name="end_date" value="' + escHtml(r.end_date || '') + '"></div>' +
' <div class="form-item full"><label>备注说明</label><input type="text" name="remark" value="' + escHtml(r.remark || '') + '"></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="plan-form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑计划' : '新增计划',
area: ['680px', 'auto'],
content: content,
btn: false
});
$('#plan-form-cancel').on('click', function () { Dialog.close(idx); });
// 状态反馈 → 费用(元) 联动:仅「已执行」可填写
$('#plan-form select[name="status"]').on('change', function () {
var enabled = $(this).val() === '已执行';
var $cost = $('#plan-form input[name="cost"]');
// 仅控制可编辑性,不改 CSS 样式
$cost.prop('disabled', !enabled);
if (!enabled) $cost.val('');
});
$('#plan-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select').each(function () {
var name = $(this).attr('name');
if (!name) return;
data[name] = $(this).val();
});
if (!data.channel_type) { Dialog.error('渠道类别为必填项'); return; }
if (data.start_date && data.end_date && data.start_date > data.end_date) {
Dialog.error('开始日期不能晚于结束日期');
return;
}
if (isEdit) data.id = r.id;
httpPost('channel/plan_save.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadPlans(currentPage);
});
});
});
}
window.delPlan = function (id) {
Dialog.confirm('确定删除该计划吗?', function () {
httpPost('channel/plan_delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () {
loadPlans(currentPage);
});
});
});
};
/* ---------- 初始化渠道类别下拉(新增计划用 + 列表筛选) ---------- */
httpGet('common/dicts.php', { type: 'source_channel' }).then(function (d) {
var types = (d.source_channels || []).map(function (s) { return s.name; });
window._channelTypes = types;
var html = '<option value="">全部渠道类别</option>';
types.forEach(function (t) {
html += '<option value="' + escHtml(t) + '">' + escHtml(t) + '</option>';
});
$('.toolbar select[data-filter="channel_type"]').html(html);
}).catch(function () {});
});