408 lines
20 KiB
JavaScript
408 lines
20 KiB
JavaScript
/**
|
||
* 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></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>' + 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="10" 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><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 () {});
|
||
});
|