919fbbc148
在history页面工具栏添加"尾首概率"按钮,弹窗显示: - 下一期尾数与前一期尾数相同的概率 - 下一期首位与前一期首位相同的概率 支持切换统计期数(30/50/100/200期)
2237 lines
131 KiB
JavaScript
2237 lines
131 KiB
JavaScript
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||
|
||
var Controller = {
|
||
index: function () {
|
||
Table.api.init({
|
||
extend: {
|
||
index_url: 'history/index' + location.search,
|
||
add_url: 'history/add',
|
||
edit_url: 'history/edit',
|
||
del_url: 'history/del',
|
||
multi_url: 'history/multi',
|
||
import_url: 'history/import',
|
||
table: 'history',
|
||
}
|
||
});
|
||
|
||
var table = $("#table");
|
||
|
||
// 从后端获取颜色和生肖映射
|
||
Controller.api.loadColorMap(function () {
|
||
Controller.api.loadAnimalMap(function () {
|
||
table.bootstrapTable({
|
||
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||
pk: 'expect',
|
||
sortName: 'expect',
|
||
columns: [
|
||
[
|
||
{checkbox: true},
|
||
{field: 'expect', title: __('Expect')},
|
||
{field: 'num1', title: __('Num1'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num2', title: __('Num2'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num3', title: __('Num3'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num4', title: __('Num4'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num5', title: __('Num5'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num6', title: __('Num6'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'num7', title: __('Num7'), formatter: Controller.api.formatter.numBall},
|
||
{field: 'openTime', title: __('OpenTime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false},
|
||
{field: 'search_day', title: '每月日号', visible: false, searchable: true, operate: '=', style: 'width:80px;', extend: 'min="1" max="31" placeholder="1-31"'}
|
||
]
|
||
]
|
||
});
|
||
|
||
Table.api.bindevent(table);
|
||
});
|
||
});
|
||
|
||
// 遗漏号码按钮事件
|
||
$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', function () {
|
||
Controller.api.showMissingNumDialog();
|
||
});
|
||
|
||
// 走势图按钮事件
|
||
$(document).off('click', '.btn-trend').on('click', '.btn-trend', function () {
|
||
Controller.api.showTrendDialog();
|
||
});
|
||
|
||
// 冷热分析按钮事件
|
||
$(document).off('click', '.btn-hotcold').on('click', '.btn-hotcold', function () {
|
||
Controller.api.showHotColdDialog();
|
||
});
|
||
|
||
// 波色分析按钮事件
|
||
$(document).off('click', '.btn-colorwave').on('click', '.btn-colorwave', function () {
|
||
Controller.api.showAnalysisDialog('colorWave');
|
||
});
|
||
// 生肖分析按钮事件
|
||
$(document).off('click', '.btn-zodiac').on('click', '.btn-zodiac', function () {
|
||
Controller.api.showAnalysisDialog('zodiac');
|
||
});
|
||
// 奇偶分析按钮事件
|
||
$(document).off('click', '.btn-oddeven').on('click', '.btn-oddeven', function () {
|
||
Controller.api.showAnalysisDialog('oddEven');
|
||
});
|
||
// 大小分析按钮事件
|
||
$(document).off('click', '.btn-bigsmall').on('click', '.btn-bigsmall', function () {
|
||
Controller.api.showAnalysisDialog('bigSmall');
|
||
});
|
||
// 和值分析按钮事件
|
||
$(document).off('click', '.btn-sumchart').on('click', '.btn-sumchart', function () {
|
||
Controller.api.showSumDialog();
|
||
});
|
||
// 连号分析按钮事件
|
||
$(document).off('click', '.btn-consecutive').on('click', '.btn-consecutive', function () {
|
||
Controller.api.showConsecutiveDialog();
|
||
});
|
||
// 尾数分析按钮事件
|
||
$(document).off('click', '.btn-tailnums').on('click', '.btn-tailnums', function () {
|
||
Controller.api.showAnalysisDialog('tailNumbers');
|
||
});
|
||
|
||
// 特码冷热按钮事件
|
||
$(document).off('click', '.btn-specialhotcold').on('click', '.btn-specialhotcold', function () {
|
||
Controller.api.showSpecialHotColdDialog();
|
||
});
|
||
|
||
// 综合统计面板按钮事件
|
||
$(document).off('click', '.btn-dashboard').on('click', '.btn-dashboard', function () {
|
||
Controller.api.showDashboard();
|
||
});
|
||
|
||
// 筛号器按钮事件
|
||
$(document).off('click', '.btn-numberfilter').on('click', '.btn-numberfilter', function () {
|
||
Controller.api.showNumberFilterDialog();
|
||
});
|
||
|
||
// 预测号码按钮事件
|
||
$(document).off('click', '.btn-predict').on('click', '.btn-predict', function () {
|
||
Controller.api.showPredictDialog();
|
||
});
|
||
|
||
// 正码关联预测按钮事件
|
||
$(document).off('click', '.btn-normal-relation').on('click', '.btn-normal-relation', function () {
|
||
Controller.api.showNormalRelationDialog();
|
||
});
|
||
|
||
// 尾首概率按钮事件
|
||
$(document).off('click', '.btn-tailheadprob').on('click', '.btn-tailheadprob', function () {
|
||
Controller.api.showTailHeadProbabilityDialog();
|
||
});
|
||
},
|
||
add: function () {
|
||
Controller.api.bindevent();
|
||
},
|
||
edit: function () {
|
||
Controller.api.bindevent();
|
||
},
|
||
api: {
|
||
colorMap: {},
|
||
colorMapLoaded: false,
|
||
animalMap: {},
|
||
animalMapLoaded: false,
|
||
|
||
/**
|
||
* 从后端加载颜色映射并缓存
|
||
*/
|
||
loadColorMap: function (callback) {
|
||
if (Controller.api.colorMapLoaded) {
|
||
callback();
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: 'num/getColorMap',
|
||
type: 'GET',
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.colorMap = ret.msg;
|
||
}
|
||
Controller.api.colorMapLoaded = true;
|
||
callback();
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 从后端加载生肖映射并缓存
|
||
*/
|
||
loadAnimalMap: function (callback) {
|
||
if (Controller.api.animalMapLoaded) {
|
||
callback();
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: 'num/getAnimalMap',
|
||
type: 'GET',
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.animalMap = ret.msg;
|
||
}
|
||
Controller.api.animalMapLoaded = true;
|
||
callback();
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 根据数字从映射表中获取生肖
|
||
*/
|
||
getAnimalByNum: function (num) {
|
||
return Controller.api.animalMap[num] || '';
|
||
},
|
||
|
||
/**
|
||
* 根据数字从映射表中获取颜色
|
||
*/
|
||
getColorByNum: function (num) {
|
||
var color = Controller.api.colorMap[num];
|
||
if (!color) return '#95a5a6';
|
||
// 后端返回中文波色,前端映射为CSS颜色
|
||
if (color.indexOf('红') !== -1) return '#e74c3c';
|
||
if (color.indexOf('蓝') !== -1) return '#3498db';
|
||
if (color.indexOf('绿') !== -1) return '#2ecc71';
|
||
return '#95a5a6';
|
||
},
|
||
|
||
formatter: {
|
||
numBall: function (value, row, index) {
|
||
if (value === null || value === undefined || value === '') return '';
|
||
var num = parseInt(value);
|
||
var color = Controller.api.getColorByNum(num);
|
||
var animal = Controller.api.getAnimalByNum(num);
|
||
var html = '<div style="text-align:center;">' +
|
||
'<span class="num-ball" style="display:inline-block;width:32px;height:32px;line-height:32px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;">' + value + '</span>';
|
||
if (animal) {
|
||
html += '<div style="font-size:10px;color:#666;line-height:1.2;">' + animal + '</div>';
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 显示冷热分析弹窗
|
||
*/
|
||
showHotColdDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:15px;">' + __('Query Type') + ':</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;">' +
|
||
' <input type="radio" name="hc-type" value="all" checked> ' + __('All Numbers') +
|
||
' </label>' +
|
||
' <label class="radio-inline">' +
|
||
' <input type="radio" name="hc-type" value="special"> ' + __('Special Only') +
|
||
' </label>' +
|
||
'</div>' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="hc-periods" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-hc-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="hc-result" style="margin-top:15px;overflow-x:auto;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Hot/Cold Analysis'),
|
||
area: ['700px', '600px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-hc-query', layero).on('click', function () {
|
||
var periods = parseInt($('#hc-periods', layero).val()) || 30;
|
||
var type = $('input[name="hc-type"]:checked', layero).val();
|
||
Controller.api.queryHotCold(periods, type, layero);
|
||
});
|
||
$('input[name="hc-type"]', layero).on('change', function () {
|
||
var periods = parseInt($('#hc-periods', layero).val()) || 30;
|
||
Controller.api.queryHotCold(periods, $(this).val(), layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
queryHotCold: function (periods, type, layero) {
|
||
var $btn = $('#btn-hc-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#hc-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/hotColdNumbers',
|
||
type: 'GET',
|
||
data: {periods: periods, type: type},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderHotCold(ret.data, layero);
|
||
} else {
|
||
$('#hc-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#hc-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderHotCold: function (data, layero) {
|
||
var getColor = function (num) {
|
||
var color = data.all.find(function (item) { return item.num === num; });
|
||
if (!color || !color.color) return '#95a5a6';
|
||
if (color.color.indexOf('红') !== -1) return '#e74c3c';
|
||
if (color.color.indexOf('蓝') !== -1) return '#3498db';
|
||
if (color.color.indexOf('绿') !== -1) return '#2ecc71';
|
||
return '#95a5a6';
|
||
};
|
||
|
||
var getAnimal = function (num) {
|
||
var item = data.all.find(function (item) { return item.num === num; });
|
||
return item ? (item.animal || '') : '';
|
||
};
|
||
|
||
var renderSection = function (title, items, icon) {
|
||
var html = '<div style="margin-bottom:15px;"><h4 style="margin:0 0 8px 0;border-bottom:1px solid #eee;padding-bottom:5px;">' + icon + ' ' + title + '</h4>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:8px;">';
|
||
for (var i = 0; i < items.length; i++) {
|
||
var item = items[i];
|
||
var color = getColor(item.num);
|
||
var animal = getAnimal(item.num);
|
||
html += '<div style="text-align:center;background:#f9f9f9;padding:8px;border-radius:6px;min-width:70px;">' +
|
||
'<span style="display:inline-block;width:36px;height:36px;line-height:36px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;">' + item.num + '</span>' +
|
||
'<div style="margin-top:4px;font-size:10px;color:#666;">' + (animal ? animal + '<br>' : '') + '<b>' + item.count + '</b> (' + item.percent + '%)</div>' +
|
||
'</div>';
|
||
}
|
||
html += '</div></div>';
|
||
return html;
|
||
};
|
||
|
||
var html = '<div style="padding:10px;">' +
|
||
renderSection('热号 Top 10', data.hot, '<span style="color:#e74c3c;">🔥</span>') +
|
||
renderSection('冷号 Top 10', data.cold, '<span style="color:#3498db;">❄</span>') +
|
||
'</div>';
|
||
|
||
$('#hc-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 显示走势图弹窗
|
||
*/
|
||
showTrendDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:15px;">' + __('Query Type') + ':</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;">' +
|
||
' <input type="radio" name="trend-type" value="all" checked> ' + __('All Numbers') +
|
||
' </label>' +
|
||
' <label class="radio-inline">' +
|
||
' <input type="radio" name="trend-type" value="special"> ' + __('Special Only') +
|
||
' </label>' +
|
||
'</div>' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="trend-periods" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-trend-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="trend-result" style="margin-top:15px;overflow-x:auto;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Trend Chart'),
|
||
area: ['90%', '80%'],
|
||
content: html,
|
||
shadeClose: false,
|
||
maxmin: true,
|
||
success: function (layero, index) {
|
||
$('#btn-trend-query', layero).on('click', function () {
|
||
var periods = parseInt($('#trend-periods', layero).val()) || 30;
|
||
var type = $('input[name="trend-type"]:checked', layero).val();
|
||
Controller.api.queryTrend(periods, type, layero);
|
||
});
|
||
$('input[name="trend-type"]', layero).on('change', function () {
|
||
var periods = parseInt($('#trend-periods', layero).val()) || 30;
|
||
Controller.api.queryTrend(periods, $(this).val(), layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 查询走势图
|
||
*/
|
||
queryTrend: function (periods, type, layero) {
|
||
var $btn = $('#btn-trend-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#trend-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/trendData',
|
||
type: 'GET',
|
||
data: {periods: periods, type: type},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderTrend(ret.data, type, layero);
|
||
} else {
|
||
$('#trend-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#trend-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 渲染走势图(ECharts 折线图)
|
||
*/
|
||
renderTrend: function (data, type, layero) {
|
||
var expects = data.expects;
|
||
var rows = data.data;
|
||
var colorMap = data.colorMap;
|
||
|
||
if (!expects || expects.length === 0) {
|
||
$('#trend-result', layero).html('<div class="alert alert-info">' + __('No data available') + '</div>');
|
||
return;
|
||
}
|
||
|
||
var getColor = function (num) {
|
||
var color = colorMap[num];
|
||
if (!color) return '#95a5a6';
|
||
if (color.indexOf('红') !== -1) return '#e74c3c';
|
||
if (color.indexOf('蓝') !== -1) return '#3498db';
|
||
if (color.indexOf('绿') !== -1) return '#2ecc71';
|
||
return '#95a5a6';
|
||
};
|
||
|
||
$('#trend-result', layero).html('<div id="trend-chart" style="width:100%;height:500px;"></div>');
|
||
|
||
var chartDom = document.getElementById('trend-chart');
|
||
var myChart = echarts.init(chartDom);
|
||
|
||
var series = [];
|
||
if (type === 'special') {
|
||
series = [{
|
||
name: '特码',
|
||
type: 'line',
|
||
data: rows.map(function (r) { return r.num7; }),
|
||
smooth: false,
|
||
symbol: 'circle',
|
||
symbolSize: 8,
|
||
lineStyle: { width: 2 },
|
||
itemStyle: {
|
||
color: function (params) {
|
||
var num = params.data;
|
||
return getColor(num);
|
||
}
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
fontSize: 11,
|
||
color: '#333'
|
||
}
|
||
}];
|
||
} else {
|
||
var numFields = [];
|
||
for (var c = 1; c <= 7; c++) {
|
||
numFields.push('num' + c);
|
||
}
|
||
var colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22'];
|
||
for (var f = 0; f < numFields.length; f++) {
|
||
(function (idx) {
|
||
series.push({
|
||
name: '第' + (idx + 1) + '码',
|
||
type: 'line',
|
||
data: rows.map(function (r) { return r[numFields[idx]]; }),
|
||
smooth: false,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: { width: 2 },
|
||
itemStyle: { color: colors[idx] },
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
fontSize: 10,
|
||
color: '#333'
|
||
}
|
||
});
|
||
})(f);
|
||
}
|
||
}
|
||
|
||
var option = {
|
||
title: { text: type === 'special' ? '特码走势' : '全部号码走势', left: 'center', textStyle: { fontSize: 14 } },
|
||
tooltip: { trigger: 'axis', formatter: function (params) {
|
||
var tip = '期号: ' + params[0].axisValueLabel + '<br/>';
|
||
for (var i = 0; i < params.length; i++) {
|
||
tip += params[i].seriesName + ': <b>' + params[i].data + '</b><br/>';
|
||
}
|
||
return tip;
|
||
}},
|
||
legend: { bottom: 10, data: series.map(function (s) { return s.name; }) },
|
||
grid: { left: 40, right: 20, bottom: 50, top: 40 },
|
||
xAxis: { type: 'category', data: expects, axisLabel: { rotate: 45, fontSize: 10 } },
|
||
yAxis: { type: 'value', min: 0, max: 50, splitLine: { show: true, lineStyle: { type: 'dashed' } } },
|
||
dataZoom: [{ type: 'slider', bottom: 30, height: 20, start: 0, end: 100 }],
|
||
series: series
|
||
};
|
||
|
||
myChart.setOption(option);
|
||
window.addEventListener('resize', function () { myChart.resize(); });
|
||
},
|
||
|
||
/**
|
||
* 显示遗漏号码分析弹窗
|
||
*/
|
||
showMissingNumDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:15px;">' + __('Query Type') + ':</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;">' +
|
||
' <input type="radio" name="missing-type" value="all" checked> ' + __('All Numbers') +
|
||
' </label>' +
|
||
' <label class="radio-inline">' +
|
||
' <input type="radio" name="missing-type" value="special"> ' + __('Special Only') +
|
||
' </label>' +
|
||
'</div>' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-missing-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="missing-result" style="margin-top:15px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Missing Number Analysis'),
|
||
area: ['650px', '550px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
// 绑定查询按钮事件
|
||
$('#btn-missing-query', layero).on('click', function () {
|
||
var periods = parseInt($('#missing-periods', layero).val()) || 10;
|
||
var type = $('input[name="missing-type"]:checked', layero).val();
|
||
Controller.api.queryMissingNum(periods, type, layero);
|
||
});
|
||
// 切换类型时自动查询
|
||
$('input[name="missing-type"]', layero).on('change', function () {
|
||
var periods = parseInt($('#missing-periods', layero).val()) || 10;
|
||
Controller.api.queryMissingNum(periods, $(this).val(), layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 查询遗漏号码
|
||
*/
|
||
queryMissingNum: function (periods, type, layero) {
|
||
// 确保颜色映射已加载
|
||
if (!Controller.api.colorMapLoaded) {
|
||
Controller.api.loadColorMap(function () {
|
||
Controller.api._doQueryMissingNum(periods, type, layero);
|
||
});
|
||
} else {
|
||
Controller.api._doQueryMissingNum(periods, type, layero);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 执行遗漏号码查询(内部方法)
|
||
*/
|
||
_doQueryMissingNum: function (periods, type, layero) {
|
||
var $btn = $('#btn-missing-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/missingNum',
|
||
type: 'GET',
|
||
data: {periods: periods, type: type},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderMissingNum(ret.data, periods, layero);
|
||
} else {
|
||
$('#missing-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#missing-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 渲染遗漏号码结果
|
||
*/
|
||
renderMissingNum: function (data, periods, layero) {
|
||
if (!data || data.length === 0) {
|
||
$('#missing-result', layero).html('<div class="alert alert-info">' + __('No missing numbers found') + '</div>');
|
||
return;
|
||
}
|
||
var container = $('<div style="display:flex;flex-wrap:wrap;gap:12px;"></div>');
|
||
for (var i = 0; i < data.length; i++) {
|
||
var color = Controller.api.getColorByNum(data[i].num);
|
||
var animal = Controller.api.getAnimalByNum(data[i].num);
|
||
var $item = $('<div style="text-align:center;"></div>');
|
||
var $ball = $('<span class="num-ball"></span>').css({
|
||
'display': 'inline-block',
|
||
'width': '48px',
|
||
'height': '48px',
|
||
'line-height': '48px',
|
||
'text-align': 'center',
|
||
'border-radius': '50%',
|
||
'color': '#fff',
|
||
'background-color': color,
|
||
'font-weight': 'bold',
|
||
'font-size': '18px'
|
||
}).text(data[i].num);
|
||
var $content = $('<div></div>').append($ball);
|
||
if (animal) {
|
||
$content.append($('<div style="margin-top:3px;font-size:11px;color:#666;line-height:1.2;"></div>').text(animal));
|
||
}
|
||
$content.append($('<div style="margin-top:3px;font-size:12px;color:#666;"></div>').text(
|
||
__('Missing') + ' ' + data[i].omit + ' ' + __('periods')
|
||
));
|
||
$item.append($content);
|
||
container.append($item);
|
||
}
|
||
$('#missing-result', layero).html('').append(container);
|
||
},
|
||
|
||
/**
|
||
* 特码冷热列表(每期相对于前N期的冷热状态)
|
||
*/
|
||
showSpecialHotColdDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label>向前期数:</label>' +
|
||
' <input type="number" id="shc-lookback" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-shc-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="shc-result" style="margin-top:10px;max-height:500px;overflow-y:auto;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: '特码冷热列表',
|
||
area: ['650px', '600px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-shc-query', layero).on('click', function () {
|
||
var lookback = parseInt($('#shc-lookback', layero).val()) || 30;
|
||
Controller.api.querySpecialHotCold(lookback, layero);
|
||
});
|
||
// 打开时自动查询
|
||
var lookback = parseInt($('#shc-lookback', layero).val()) || 30;
|
||
Controller.api.querySpecialHotCold(lookback, layero);
|
||
}
|
||
});
|
||
},
|
||
|
||
querySpecialHotCold: function (lookback, layero) {
|
||
var $btn = $('#btn-shc-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#shc-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/specialHotColdAction',
|
||
type: 'GET',
|
||
data: {lookback: lookback},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderSpecialHotCold(ret.data, lookback, layero);
|
||
} else {
|
||
$('#shc-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#shc-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderSpecialHotCold: function (data, lookback, layero) {
|
||
var getColor = function (num) {
|
||
return Controller.api.getColorByNum(num);
|
||
};
|
||
|
||
var statusTag = function (status) {
|
||
var map = {
|
||
'hot': '<span style="color:#e74c3c;font-weight:bold;">🔥 热号</span>',
|
||
'cold': '<span style="color:#3498db;font-weight:bold;">❄ 冷号</span>',
|
||
'normal': '<span style="color:#f39c12;font-weight:bold;">➜ 温号</span>',
|
||
'unknown': '<span style="color:#999;">数据不足</span>'
|
||
};
|
||
return map[status] || '';
|
||
};
|
||
|
||
// 新接口返回 {list: [...], current: {hot: [], cold: [], warm: []}}
|
||
var listData = data.list || data;
|
||
var current = data.current || null;
|
||
|
||
if (!listData || listData.length === 0) {
|
||
$('#shc-result', layero).html('<div class="alert alert-info">暂无数据</div>');
|
||
return;
|
||
}
|
||
|
||
var html = '<div style="padding:0 5px;">';
|
||
|
||
// 当前冷热号汇总区域
|
||
if (current && (current.hot.length > 0 || current.cold.length > 0)) {
|
||
var renderNumBall = function (item) {
|
||
var color = getColor(item.num);
|
||
return '<span style="display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;font-size:14px;" title="' + item.num + ' ×' + item.count + '">' + item.num + '</span>';
|
||
};
|
||
|
||
html += '<div style="background:#fafafa;border:1px solid #e0e0e0;border-radius:6px;padding:12px;margin-bottom:12px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;color:#333;"><i class="fa fa-bullseye"></i> 当前热号</div>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;">';
|
||
for (var h = 0; h < current.hot.length; h++) {
|
||
html += renderNumBall(current.hot[h]);
|
||
}
|
||
if (current.hot.length === 0) {
|
||
html += '<span style="color:#999;font-size:12px;">暂无热号</span>';
|
||
}
|
||
html += '</div>';
|
||
html += '<div style="height:8px;"></div>';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;color:#333;"><i class="fa fa-snowflake-o"></i> 当前冷号</div>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;">';
|
||
for (var c = 0; c < current.cold.length; c++) {
|
||
html += renderNumBall(current.cold[c]);
|
||
}
|
||
if (current.cold.length === 0) {
|
||
html += '<span style="color:#999;font-size:12px;">暂无冷号</span>';
|
||
}
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 历史记录表格
|
||
html += '<table class="table table-striped table-bordered table-hover" style="font-size:13px;">' +
|
||
'<thead><tr>' +
|
||
'<th style="text-align:center;width:120px;">期号</th>' +
|
||
'<th style="text-align:center;width:60px;">特码</th>' +
|
||
'<th style="text-align:center;width:80px;">冷热</th>' +
|
||
'<th style="text-align:center;width:60px;">次数</th>' +
|
||
'<th style="text-align:center;width:60px;">平均</th>' +
|
||
'<th style="text-align:center;width:60px;">排名</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
for (var i = 0; i < listData.length; i++) {
|
||
var item = listData[i];
|
||
var rowClass = '';
|
||
if (item.status === 'hot') rowClass = 'style="background:#fff5f5;"';
|
||
else if (item.status === 'cold') rowClass = 'style="background:#f5f8ff;"';
|
||
|
||
html += '<tr ' + rowClass + '>' +
|
||
'<td style="text-align:center;">' + item.expect + '</td>' +
|
||
'<td style="text-align:center;">' +
|
||
'<span style="display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;border-radius:50%;color:#fff;background-color:' + getColor(item.specialNum) + ';font-weight:bold;font-size:14px;">' + item.specialNum + '</span>' +
|
||
'</td>' +
|
||
'<td style="text-align:center;">' + statusTag(item.status) + '</td>' +
|
||
'<td style="text-align:center;">' + item.count + '</td>' +
|
||
'<td style="text-align:center;">' + item.avgCount + '</td>' +
|
||
'<td style="text-align:center;">' + item.rank + '</td>' +
|
||
'</tr>';
|
||
}
|
||
|
||
html += '</tbody></table></div>';
|
||
$('#shc-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 筛号器弹窗
|
||
*/
|
||
showNumberFilterDialog: function () {
|
||
var html = '<style>.btn-gray{background-color:#d2d2d2!important;border-color:#adadad!important;color:#999!important;}</style>' +
|
||
'<div style="padding:20px;">' +
|
||
'<div style="margin-bottom:15px;display:flex;gap:15px;align-items:center;flex-wrap:wrap;">' +
|
||
' <div class="form-group" style="margin:0;">' +
|
||
' <label>尾号筛选:</label>' +
|
||
' <button class="btn btn-xs btn-primary btn-nf-add-tail"><i class="fa fa-plus"></i> 新增</button>' +
|
||
' </div>' +
|
||
' <button class="btn btn-default btn-nf-reset" style="margin-left:auto;"><i class="fa fa-refresh"></i> 重置</button>' +
|
||
'</div>' +
|
||
'<div id="nf-tail-list" style="margin-bottom:15px;display:flex;flex-wrap:wrap;gap:6px;"></div>' +
|
||
'<div style="margin-bottom:15px;">' +
|
||
' <label style="margin-right:10px;">生肖:</label>' +
|
||
' <div id="nf-zodiac" style="display:inline-flex;gap:6px;flex-wrap:wrap;">' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="鼠">鼠</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="牛">牛</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="虎">虎</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="兔">兔</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="龙">龙</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="蛇">蛇</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="马">马</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="羊">羊</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="猴">猴</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="鸡">鸡</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="狗">狗</button>' +
|
||
' <button class="btn btn-default btn-xs nf-zodiac" data-zodiac="猪">猪</button>' +
|
||
' </div>' +
|
||
'</div>' +
|
||
'<div style="margin-bottom:15px;">' +
|
||
' <label style="margin-right:10px;">波色:</label>' +
|
||
' <div id="nf-colorwrap" style="display:inline-flex;gap:6px;">' +
|
||
' <button class="btn btn-default btn-xs nf-color-btn" data-color="红" style="color:#e74c3c;">红波</button>' +
|
||
' <button class="btn btn-default btn-xs nf-color-btn" data-color="蓝" style="color:#3498db;">蓝波</button>' +
|
||
' <button class="btn btn-default btn-xs nf-color-btn" data-color="绿" style="color:#2ecc71;">绿波</button>' +
|
||
' </div>' +
|
||
'</div>' +
|
||
'<div style="margin-bottom:15px;">' +
|
||
' <label style="margin-right:10px;">单双:</label>' +
|
||
' <button class="btn btn-default btn-xs nf-parity" data-parity="单">单</button>' +
|
||
' <button class="btn btn-default btn-xs nf-parity" data-parity="双">双</button>' +
|
||
'</div>' +
|
||
'<div style="margin-bottom:15px;">' +
|
||
' <label style="margin-right:10px;">区间:</label>' +
|
||
' <button class="btn btn-xs btn-primary btn-nf-add-range"><i class="fa fa-plus"></i> 新增</button>' +
|
||
'</div>' +
|
||
'<div id="nf-range-list" style="margin-bottom:15px;"></div>' +
|
||
'<div id="nf-numbers" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: '筛号器',
|
||
area: ['700px', '600px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
// 手动屏蔽的号码列表
|
||
var blockedNums = [];
|
||
|
||
// 渲染号码网格
|
||
Controller.api.renderNumberFilterGrid(layero);
|
||
|
||
// 号码点击屏蔽
|
||
$('#nf-numbers', layero).on('click', '.nf-number', function () {
|
||
var num = parseInt($(this).data('num'));
|
||
var idx = blockedNums.indexOf(num);
|
||
if (idx === -1) {
|
||
blockedNums.push(num);
|
||
} else {
|
||
blockedNums.splice(idx, 1);
|
||
}
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 新增尾号
|
||
$('.btn-nf-add-tail', layero).on('click', function () {
|
||
Controller.api.addTailRow(layero, 0);
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 尾号输入 & 删除事件委托
|
||
$('#nf-tail-list', layero).on('input change', '.nf-tail-select', function () {
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
$('#nf-tail-list', layero).on('click', '.nf-tail-del', function () {
|
||
$(this).closest('.nf-tail-row').remove();
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 生肖按钮点击
|
||
$('.nf-zodiac', layero).on('click', function () {
|
||
var $btn = $(this);
|
||
$btn.toggleClass('btn-default').toggleClass('btn-gray');
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 波色按钮点击
|
||
$('.nf-color-btn', layero).on('click', function () {
|
||
var $btn = $(this);
|
||
$btn.toggleClass('btn-default').toggleClass('btn-gray');
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 单双按钮点击
|
||
$('.nf-parity', layero).on('click', function () {
|
||
var $btn = $(this);
|
||
$btn.toggleClass('btn-default').toggleClass('btn-gray');
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 新增区间
|
||
$('.btn-nf-add-range', layero).on('click', function () {
|
||
Controller.api.addRangeRow(layero, 1, 49, 'include');
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 区间输入 & 删除事件委托
|
||
$('#nf-range-list', layero).on('input change', '.nf-range-min, .nf-range-max', function () {
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
$('#nf-range-list', layero).on('click', '.nf-range-mode', function () {
|
||
var $btn = $(this);
|
||
if ($btn.hasClass('btn-info')) {
|
||
$btn.removeClass('btn-info').addClass('btn-default');
|
||
} else {
|
||
$btn.removeClass('btn-default').addClass('btn-info');
|
||
}
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
$('#nf-range-list', layero).on('click', '.nf-range-del', function () {
|
||
$(this).closest('.nf-range-row').remove();
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
|
||
// 重置按钮
|
||
$('.btn-nf-reset', layero).on('click', function () {
|
||
blockedNums = [];
|
||
$('#nf-tail-list', layero).html('');
|
||
$('.nf-zodiac', layero).removeClass('btn-gray').addClass('btn-default');
|
||
$('.nf-color-btn', layero).removeClass('btn-gray').addClass('btn-default');
|
||
$('.nf-parity', layero).removeClass('btn-gray').addClass('btn-default');
|
||
$('#nf-range-list', layero).html('');
|
||
Controller.api.applyNumberFilters(layero, blockedNums);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 新增一行尾号筛选
|
||
*/
|
||
addTailRow: function (layero, value) {
|
||
var rowId = 'nf-tail-' + Date.now() + Math.random().toString(36).substr(2, 5);
|
||
var opts = '<option value="">尾号</option>';
|
||
for (var t = 0; t <= 9; t++) {
|
||
opts += '<option value="' + t + '"' + (t === value ? ' selected' : '') + '>' + t + '</option>';
|
||
}
|
||
var html = '<div class="nf-tail-row" id="' + rowId + '" style="display:inline-flex;align-items:center;gap:4px;">' +
|
||
' <select class="form-control nf-tail-select" style="width:80px;display:inline-block;">' + opts + '</select>' +
|
||
' <button class="btn btn-xs btn-danger nf-tail-del"><i class="fa fa-times"></i></button>' +
|
||
'</div>';
|
||
$('#nf-tail-list', layero).append(html);
|
||
},
|
||
|
||
/**
|
||
* 新增一行区间筛选
|
||
*/
|
||
addRangeRow: function (layero, min, max, mode) {
|
||
var rowId = 'nf-range-' + Date.now();
|
||
var isInclude = mode === 'include';
|
||
var html = '<div class="nf-range-row" id="' + rowId + '" style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">' +
|
||
' <span style="font-size:12px;color:#999;white-space:nowrap;">' + (isInclude ? '在区间' : '排除') + '</span>' +
|
||
' <input type="number" class="form-control nf-range-min" value="' + min + '" min="1" max="49" style="width:65px;display:inline-block;">' +
|
||
' <span style="margin:0 3px;">—</span>' +
|
||
' <input type="number" class="form-control nf-range-max" value="' + max + '" min="1" max="49" style="width:65px;display:inline-block;">' +
|
||
' <button class="btn btn-xs ' + (isInclude ? 'btn-info' : 'btn-default') + ' nf-range-mode" data-mode="' + mode + '">' + (isInclude ? '在区间' : '排除区间') + '</button>' +
|
||
' <button class="btn btn-xs btn-danger nf-range-del"><i class="fa fa-times"></i></button>' +
|
||
'</div>';
|
||
$('#nf-range-list', layero).append(html);
|
||
},
|
||
|
||
/**
|
||
* 渲染筛号器号码网格
|
||
*/
|
||
renderNumberFilterGrid: function (layero) {
|
||
var colorMap = Controller.api.colorMap;
|
||
var html = '';
|
||
for (var num = 1; num <= 49; num++) {
|
||
var colorHex = Controller.api.getColorByNum(num);
|
||
var colorRaw = colorMap[num] || '';
|
||
var animal = Controller.api.getAnimalByNum(num);
|
||
var colorLabel = '';
|
||
if (colorRaw.indexOf('红') !== -1) colorLabel = '红';
|
||
else if (colorRaw.indexOf('蓝') !== -1) colorLabel = '蓝';
|
||
else if (colorRaw.indexOf('绿') !== -1) colorLabel = '绿';
|
||
html += '<div class="nf-number" data-num="' + num + '" data-color="' + colorLabel + '" data-animal="' + animal + '" data-tail="' + (num % 10) + '" data-parity="' + (num % 2 === 1 ? '单' : '双') + '" style="text-align:center;background:#f9f9f9;padding:6px 4px;border-radius:6px;min-width:60px;transition:opacity 0.2s;cursor:pointer;" title="点击屏蔽/恢复">' +
|
||
'<span style="display:inline-block;width:36px;height:36px;line-height:36px;text-align:center;border-radius:50%;color:#fff;background-color:' + colorHex + ';font-weight:bold;">' + num + '</span>' +
|
||
'<div style="font-size:10px;color:#666;line-height:1.2;">' + animal + '</div>' +
|
||
'</div>';
|
||
}
|
||
$('#nf-numbers', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 应用筛号器过滤条件
|
||
* @param {object} layero Layer弹窗对象
|
||
* @param {Array} blockedNums 手动屏蔽的号码列表
|
||
*/
|
||
applyNumberFilters: function (layero, blockedNums) {
|
||
blockedNums = blockedNums || [];
|
||
// 收集所有选中的尾号
|
||
var excludedTails = [];
|
||
$('.nf-tail-select', layero).each(function () {
|
||
var val = $(this).val();
|
||
if (val !== '' && excludedTails.indexOf(parseInt(val)) === -1) {
|
||
excludedTails.push(parseInt(val));
|
||
}
|
||
});
|
||
// 收集被点击(置灰)的生肖
|
||
var excludedZodiacs = [];
|
||
$('.nf-zodiac.btn-gray', layero).each(function () {
|
||
excludedZodiacs.push($(this).data('zodiac'));
|
||
});
|
||
// 收集被点击(置灰)的波色
|
||
var excludedColors = [];
|
||
$('.nf-color-btn.btn-gray', layero).each(function () {
|
||
excludedColors.push($(this).data('color'));
|
||
});
|
||
// 收集被点击(置灰)的单双
|
||
var excludedParities = [];
|
||
$('.nf-parity.btn-gray', layero).each(function () {
|
||
excludedParities.push($(this).data('parity'));
|
||
});
|
||
// 收集所有区间
|
||
var ranges = [];
|
||
$('.nf-range-row', layero).each(function () {
|
||
var $row = $(this);
|
||
var min = parseInt($row.find('.nf-range-min').val()) || 1;
|
||
var max = parseInt($row.find('.nf-range-max').val()) || 49;
|
||
var mode = $row.find('.nf-range-mode').hasClass('btn-info') ? 'include' : 'exclude';
|
||
ranges.push({min: min, max: max, mode: mode});
|
||
});
|
||
|
||
$('.nf-number', layero).each(function () {
|
||
var $num = $(this);
|
||
var num = parseInt($num.data('num'));
|
||
var tail = $num.data('tail');
|
||
var animal = $num.data('animal');
|
||
var color = $num.data('color');
|
||
|
||
var parity = $num.data('parity');
|
||
|
||
var hidden = false;
|
||
|
||
// 尾号筛选:选中多个尾号,任一命中即屏蔽
|
||
if (excludedTails.length > 0 && excludedTails.indexOf(tail) !== -1) {
|
||
hidden = true;
|
||
}
|
||
// 单双筛选:选中单或双,匹配则屏蔽
|
||
if (excludedParities.indexOf(parity) !== -1) {
|
||
hidden = true;
|
||
}
|
||
// 区间筛选:在区间=白名单(OR)、排除区间=黑名单(OR)
|
||
if (!hidden) {
|
||
var includeRanges = [];
|
||
var excludeRanges = [];
|
||
for (var r = 0; r < ranges.length; r++) {
|
||
if (ranges[r].mode === 'include') {
|
||
includeRanges.push(ranges[r]);
|
||
} else {
|
||
excludeRanges.push(ranges[r]);
|
||
}
|
||
}
|
||
// 黑名单:任一排除区间命中则屏蔽
|
||
for (var r = 0; r < excludeRanges.length; r++) {
|
||
if (num >= excludeRanges[r].min && num <= excludeRanges[r].max) {
|
||
hidden = true;
|
||
break;
|
||
}
|
||
}
|
||
// 白名单:有在区间规则时,号码不在任一区间则屏蔽
|
||
if (!hidden && includeRanges.length > 0) {
|
||
var inAnyInclude = false;
|
||
for (var r = 0; r < includeRanges.length; r++) {
|
||
if (num >= includeRanges[r].min && num <= includeRanges[r].max) {
|
||
inAnyInclude = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!inAnyInclude) {
|
||
hidden = true;
|
||
}
|
||
}
|
||
}
|
||
// 排除的生肖
|
||
if (excludedZodiacs.indexOf(animal) !== -1) {
|
||
hidden = true;
|
||
}
|
||
// 排除的波色
|
||
if (excludedColors.indexOf(color) !== -1) {
|
||
hidden = true;
|
||
}
|
||
// 手动屏蔽的号码
|
||
if (blockedNums.indexOf(num) !== -1) {
|
||
hidden = true;
|
||
}
|
||
|
||
if (hidden) {
|
||
$num.css('opacity', '0.25').css('filter', 'grayscale(100%)');
|
||
} else {
|
||
$num.css('opacity', '1').css('filter', 'none');
|
||
}
|
||
});
|
||
},
|
||
|
||
bindevent: function () {
|
||
Form.api.bindevent($("form[role=form]"));
|
||
},
|
||
|
||
/**
|
||
* 通用分析弹窗(波色、生肖、奇偶、大小、尾数)
|
||
*/
|
||
showAnalysisDialog: function (type) {
|
||
var titles = {
|
||
colorWave: __('Color Wave'),
|
||
zodiac: __('Zodiac'),
|
||
oddEven: __('Odd/Even'),
|
||
bigSmall: __('Big/Small'),
|
||
tailNumbers: __('Tail Numbers')
|
||
};
|
||
var endpoints = {
|
||
colorWave: 'history/colorWaveAnalysis',
|
||
zodiac: 'history/zodiacAnalysis',
|
||
oddEven: 'history/oddEvenAnalysis',
|
||
bigSmall: 'history/bigSmallAnalysis',
|
||
tailNumbers: 'history/tailNumbers'
|
||
};
|
||
var hasType = ['colorWave', 'zodiac', 'oddEven', 'bigSmall', 'tailNumbers'].indexOf(type) !== -1;
|
||
|
||
var html = '<div style="padding:20px;">';
|
||
if (hasType) {
|
||
html += '<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:15px;">' + __('Query Type') + ':</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;">' +
|
||
' <input type="radio" name="analysis-type-' + type + '" value="all" checked> ' + __('All Numbers') +
|
||
' </label>' +
|
||
' <label class="radio-inline">' +
|
||
' <input type="radio" name="analysis-type-' + type + '" value="special"> ' + __('Special Only') +
|
||
' </label>' +
|
||
'</div>';
|
||
}
|
||
html += '<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="analysis-periods-' + type + '" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-analysis-' + type + '" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="analysis-result-' + type + '" style="margin-top:15px;overflow-x:auto;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: titles[type] || type,
|
||
area: ['650px', '550px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-analysis-' + type, layero).on('click', function () {
|
||
var periods = parseInt($('#analysis-periods-' + type, layero).val()) || 30;
|
||
var tp = hasType ? $('input[name="analysis-type-' + type + '"]:checked', layero).val() : 'all';
|
||
Controller.api.queryAnalysis(periods, tp, type, endpoints[type], layero);
|
||
});
|
||
if (hasType) {
|
||
$('input[name="analysis-type-' + type + '"]', layero).on('change', function () {
|
||
var periods = parseInt($('#analysis-periods-' + type, layero).val()) || 30;
|
||
Controller.api.queryAnalysis(periods, $(this).val(), type, endpoints[type], layero);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
queryAnalysis: function (periods, type, analysisType, endpoint, layero) {
|
||
var $btn = $('#btn-analysis-' + analysisType, layero);
|
||
$btn.prop('disabled', true);
|
||
$('#analysis-result-' + analysisType, layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: endpoint,
|
||
type: 'GET',
|
||
data: {periods: periods, type: type},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
var renderMap = {
|
||
colorWave: 'renderColorWaveAnalysis',
|
||
zodiac: 'renderZodiacAnalysis',
|
||
oddEven: 'renderOddEvenAnalysis',
|
||
bigSmall: 'renderBigSmallAnalysis',
|
||
tailNumbers: 'renderTailNumbers'
|
||
};
|
||
var renderFn = renderMap[analysisType];
|
||
if (renderFn && typeof Controller.api[renderFn] === 'function') {
|
||
Controller.api[renderFn](ret.data, layero);
|
||
} else {
|
||
$('#analysis-result-' + analysisType, layero).html('<div class="alert alert-danger">渲染方法不存在: ' + renderFn + '</div>');
|
||
}
|
||
} else {
|
||
$('#analysis-result-' + analysisType, layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#analysis-result-' + analysisType, layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderColorWaveAnalysis: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;justify-content:space-around;">';
|
||
var items = [
|
||
{label: '红波', value: data.red, pct: data.red_pct, color: '#e74c3c'},
|
||
{label: '蓝波', value: data.blue, pct: data.blue_pct, color: '#3498db'},
|
||
{label: '绿波', value: data.green, pct: data.green_pct, color: '#2ecc71'}
|
||
];
|
||
for (var i = 0; i < items.length; i++) {
|
||
var item = items[i];
|
||
html += '<div style="text-align:center;padding:20px;border-radius:8px;background:#f5f5f5;min-width:120px;">' +
|
||
'<div style="width:60px;height:60px;line-height:60px;border-radius:50%;background-color:' + item.color + ';color:#fff;font-size:24px;font-weight:bold;margin:0 auto;">' + item.value + '</div>' +
|
||
'<div style="margin-top:10px;font-size:14px;color:#333;">' + item.label + '</div>' +
|
||
'<div style="font-size:12px;color:#999;">' + item.pct + '%</div>' +
|
||
'</div>';
|
||
}
|
||
html += '</div><div style="margin-top:15px;color:#999;font-size:12px;text-align:center;">总计: ' + data.total + ' 个号码</div></div>';
|
||
$('#analysis-result-colorWave', layero).html(html);
|
||
},
|
||
|
||
renderZodiacAnalysis: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;flex-wrap:wrap;gap:8px;">';
|
||
for (var i = 0; i < data.list.length; i++) {
|
||
var item = data.list[i];
|
||
html += '<div style="text-align:center;background:#f9f9f9;padding:10px 15px;border-radius:6px;min-width:90px;">' +
|
||
'<div style="font-size:18px;font-weight:bold;color:#333;">' + item.animal + '</div>' +
|
||
'<div style="font-size:12px;color:#666;">' + item.count + ' (' + item.percent + '%)</div>' +
|
||
'</div>';
|
||
}
|
||
html += '</div></div>';
|
||
$('#analysis-result-zodiac', layero).html(html);
|
||
},
|
||
|
||
renderOddEvenAnalysis: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;justify-content:space-around;margin-bottom:15px;">';
|
||
html += '<div style="text-align:center;padding:15px;border-radius:8px;background:#f5f5f5;min-width:140px;">' +
|
||
'<div style="font-size:28px;font-weight:bold;color:#e74c3c;">' + data.odd + '</div>' +
|
||
'<div style="font-size:14px;color:#333;">奇数</div>' +
|
||
'<div style="font-size:12px;color:#999;">' + data.odd_pct + '%</div></div>';
|
||
html += '<div style="text-align:center;padding:15px;border-radius:8px;background:#f5f5f5;min-width:140px;">' +
|
||
'<div style="font-size:28px;font-weight:bold;color:#3498db;">' + data.even + '</div>' +
|
||
'<div style="font-size:14px;color:#333;">偶数</div>' +
|
||
'<div style="font-size:12px;color:#999;">' + data.even_pct + '%</div></div>';
|
||
html += '</div></div>';
|
||
$('#analysis-result-oddEven', layero).html(html);
|
||
},
|
||
|
||
renderBigSmallAnalysis: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;justify-content:space-around;margin-bottom:15px;">';
|
||
html += '<div style="text-align:center;padding:15px;border-radius:8px;background:#f5f5f5;min-width:140px;">' +
|
||
'<div style="font-size:28px;font-weight:bold;color:#f39c12;">' + data.big + '</div>' +
|
||
'<div style="font-size:14px;color:#333;">大数(25-49)</div>' +
|
||
'<div style="font-size:12px;color:#999;">' + data.big_pct + '%</div></div>';
|
||
html += '<div style="text-align:center;padding:15px;border-radius:8px;background:#f5f5f5;min-width:140px;">' +
|
||
'<div style="font-size:28px;font-weight:bold;color:#2ecc71;">' + data.small + '</div>' +
|
||
'<div style="font-size:14px;color:#333;">小数(1-24)</div>' +
|
||
'<div style="font-size:12px;color:#999;">' + data.small_pct + '%</div></div>';
|
||
html += '</div></div>';
|
||
$('#analysis-result-bigSmall', layero).html(html);
|
||
},
|
||
|
||
renderTailNumbers: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;flex-wrap:wrap;gap:8px;">';
|
||
for (var i = 0; i < data.all.length; i++) {
|
||
var item = data.all[i];
|
||
html += '<div style="text-align:center;background:#f9f9f9;padding:10px 15px;border-radius:6px;min-width:70px;">' +
|
||
'<div style="font-size:22px;font-weight:bold;color:#333;">' + item.tail + '</div>' +
|
||
'<div style="font-size:12px;color:#666;">' + item.count + ' (' + item.percent + '%)</div></div>';
|
||
}
|
||
html += '</div></div>';
|
||
$('#analysis-result-tailNumbers', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 和值分析弹窗
|
||
*/
|
||
showSumDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="sum-periods" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-sum-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="sum-result" style="margin-top:15px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Sum Chart'),
|
||
area: ['750px', '400px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-sum-query', layero).on('click', function () {
|
||
var periods = parseInt($('#sum-periods', layero).val()) || 30;
|
||
Controller.api.querySum(periods, layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
querySum: function (periods, layero) {
|
||
var $btn = $('#btn-sum-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#sum-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/sumAnalysis',
|
||
type: 'GET',
|
||
data: {periods: periods},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderSum(ret.data, layero);
|
||
} else {
|
||
$('#sum-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#sum-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderSum: function (data, layero) {
|
||
var html = '<div style="padding:15px;"><div style="display:flex;justify-content:space-around;margin-bottom:15px;">';
|
||
html += '<div style="text-align:center;padding:10px;"><div style="font-size:20px;font-weight:bold;color:#333;">' + data.avg + '</div><div style="font-size:12px;color:#999;">平均和值</div></div>';
|
||
html += '<div style="text-align:center;padding:10px;"><div style="font-size:20px;font-weight:bold;color:#e74c3c;">' + data.max + '</div><div style="font-size:12px;color:#999;">最大和值</div></div>';
|
||
html += '<div style="text-align:center;padding:10px;"><div style="font-size:20px;font-weight:bold;color:#3498db;">' + data.min + '</div><div style="font-size:12px;color:#999;">最小和值</div></div>';
|
||
html += '</div><div id="sum-chart" style="width:100%;height:250px;"></div></div>';
|
||
$('#sum-result', layero).html(html);
|
||
|
||
var chartDom = document.getElementById('sum-chart');
|
||
if (chartDom && typeof echarts !== 'undefined') {
|
||
var myChart = echarts.init(chartDom);
|
||
myChart.setOption({
|
||
xAxis: {type: 'category', data: data.expects, axisLabel: {rotate: 45, fontSize: 10}},
|
||
yAxis: {type: 'value'},
|
||
series: [{type: 'line', data: data.sums, smooth: true, itemStyle: {color: '#3498db'}, areaStyle: {color: 'rgba(52,152,219,0.1)'}}],
|
||
grid: {left: 40, right: 20, bottom: 40, top: 20}
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 连号分析弹窗
|
||
*/
|
||
showConsecutiveDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="consecutive-periods" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-consecutive-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="consecutive-result" style="margin-top:15px;overflow-x:auto;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Consecutive'),
|
||
area: ['600px', '500px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-consecutive-query', layero).on('click', function () {
|
||
var periods = parseInt($('#consecutive-periods', layero).val()) || 30;
|
||
Controller.api.queryConsecutive(periods, layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
queryConsecutive: function (periods, layero) {
|
||
var $btn = $('#btn-consecutive-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#consecutive-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/consecutiveNumbers',
|
||
type: 'GET',
|
||
data: {periods: periods},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderConsecutive(ret.data, layero);
|
||
} else {
|
||
$('#consecutive-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#consecutive-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderConsecutive: function (data, layero) {
|
||
var html = '<div style="padding:15px;">';
|
||
html += '<h4 style="margin:0 0 10px 0;border-bottom:1px solid #eee;padding-bottom:5px;">连号对</h4>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:15px;">';
|
||
var pairs = data.pairs;
|
||
if (pairs && Object.keys(pairs).length > 0) {
|
||
for (var pair in pairs) {
|
||
html += '<div style="background:#f5f5f5;padding:6px 12px;border-radius:4px;font-size:13px;">' + pair + ' <b>×' + pairs[pair] + '</b></div>';
|
||
}
|
||
} else {
|
||
html += '<div style="color:#999;font-size:13px;">暂无连号数据</div>';
|
||
}
|
||
html += '</div><h4 style="margin:0 0 10px 0;border-bottom:1px solid #eee;padding-bottom:5px;">三连号</h4>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;">';
|
||
var triples = data.triples;
|
||
if (triples && Object.keys(triples).length > 0) {
|
||
for (var triple in triples) {
|
||
html += '<div style="background:#f5f5f5;padding:6px 12px;border-radius:4px;font-size:13px;">' + triple + ' <b>×' + triples[triple] + '</b></div>';
|
||
}
|
||
} else {
|
||
html += '<div style="color:#999;font-size:13px;">暂无三连号数据</div>';
|
||
}
|
||
html += '</div></div>';
|
||
$('#consecutive-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 综合统计面板
|
||
*/
|
||
showDashboard: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div class="form-group">' +
|
||
' <label>' + __('Query Periods') + ':</label>' +
|
||
' <input type="number" id="dash-periods" class="form-control" value="30" min="10" max="100" style="width:120px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-dash-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
|
||
'</div>' +
|
||
'<div id="dash-result" style="margin-top:15px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: __('Dashboard'),
|
||
area: ['90%', '90%'],
|
||
content: html,
|
||
shadeClose: false,
|
||
maxmin: true,
|
||
success: function (layero, index) {
|
||
$('#btn-dash-query', layero).on('click', function () {
|
||
var periods = parseInt($('#dash-periods', layero).val()) || 30;
|
||
Controller.api.queryDashboard(periods, layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
queryDashboard: function (periods, layero) {
|
||
var $btn = $('#btn-dash-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#dash-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
|
||
$.ajax({
|
||
url: 'history/dashboard',
|
||
type: 'GET',
|
||
data: {periods: periods},
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderDashboard(ret.data, layero);
|
||
} else {
|
||
$('#dash-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#dash-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
renderDashboard: function (data, layero) {
|
||
var getColor = function (color) {
|
||
if (!color) return '#95a5a6';
|
||
if (color.indexOf('红') !== -1) return '#e74c3c';
|
||
if (color.indexOf('蓝') !== -1) return '#3498db';
|
||
if (color.indexOf('绿') !== -1) return '#2ecc71';
|
||
return '#95a5a6';
|
||
};
|
||
|
||
var hc = data.hotcold;
|
||
var cw = data.colorwave;
|
||
var zo = data.zodiac;
|
||
var oe = data.oddeven;
|
||
var bs = data.bigsmall;
|
||
var sm = data.sum;
|
||
var tn = data.tailnumbers;
|
||
|
||
var ballHtml = function (item) {
|
||
return '<div style="text-align:center;display:inline-block;margin:3px;"><span style="display:inline-block;width:28px;height:28px;line-height:28px;text-align:center;border-radius:50%;color:#fff;background-color:' + getColor(item.color) + ';font-weight:bold;font-size:12px;">' + item.num + '</span><div style="font-size:9px;color:#666;">' + item.count + '</div></div>';
|
||
};
|
||
|
||
var html = '<div style="padding:10px;max-height:75vh;overflow-y:auto;">';
|
||
|
||
// 冷热号码
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">🔥❄️ 冷热号码</h4>';
|
||
html += '<div style="display:flex;"><div style="flex:1;padding:5px;"><b style="color:#e74c3c;">热号 Top5</b><div>';
|
||
for (var i = 0; i < 5; i++) html += ballHtml(hc.hot[i]);
|
||
html += '</div></div><div style="flex:1;padding:5px;"><b style="color:#3498db;">冷号 Top5</b><div>';
|
||
for (var i = 0; i < 5; i++) html += ballHtml(hc.cold[i]);
|
||
html += '</div></div></div>';
|
||
|
||
// 波色分析
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">🎨 波色比例</h4>';
|
||
html += '<div style="display:flex;gap:15px;">';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#fce4ec;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#e74c3c;">' + cw.red + '</div><div>红波 ' + cw.red_pct + '%</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#e3f2fd;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#3498db;">' + cw.blue + '</div><div>蓝波 ' + cw.blue_pct + '%</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#e8f5e9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#2ecc71;">' + cw.green + '</div><div>绿波 ' + cw.green_pct + '%</div></div>';
|
||
html += '</div>';
|
||
|
||
// 生肖分析
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">⭐ 生肖排名</h4>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:5px;">';
|
||
for (var i = 0; i < Math.min(zo.list.length, 12); i++) {
|
||
var z = zo.list[i];
|
||
html += '<div style="text-align:center;background:#f5f5f5;padding:5px 10px;border-radius:4px;"><div style="font-size:14px;font-weight:bold;">' + z.animal + '</div><div style="font-size:10px;color:#666;">' + z.count + ' (' + z.percent + '%)</div></div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// 奇偶分析
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">⚖️ 奇偶分析</h4>';
|
||
html += '<div style="display:flex;gap:15px;">';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#e74c3c;">' + oe.odd + ' (' + oe.odd_pct + '%)</div><div>奇数</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#3498db;">' + oe.even + ' (' + oe.even_pct + '%)</div><div>偶数</div></div>';
|
||
html += '</div>';
|
||
|
||
// 大小分析
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">📊 大小分析</h4>';
|
||
html += '<div style="display:flex;gap:15px;">';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#f39c12;">' + bs.big + ' (' + bs.big_pct + '%)</div><div>大数(25-49)</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#2ecc71;">' + bs.small + ' (' + bs.small_pct + '%)</div><div>小数(1-24)</div></div>';
|
||
html += '</div>';
|
||
|
||
// 和值
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">📈 和值统计</h4>';
|
||
html += '<div style="display:flex;gap:15px;">';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;">' + sm.avg + '</div><div>平均</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#e74c3c;">' + sm.max + '</div><div>最大</div></div>';
|
||
html += '<div style="flex:1;text-align:center;padding:8px;background:#f9f9f9;border-radius:6px;"><div style="font-size:20px;font-weight:bold;color:#3498db;">' + sm.min + '</div><div>最小</div></div>';
|
||
html += '</div>';
|
||
|
||
// 尾数
|
||
html += '<h4 style="border-bottom:1px solid #eee;padding-bottom:5px;">🔢 尾数频率</h4>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:5px;">';
|
||
for (var i = 0; i < tn.all.length; i++) {
|
||
var t = tn.all[i];
|
||
html += '<div style="text-align:center;background:#f5f5f5;padding:5px 10px;border-radius:4px;"><div style="font-size:16px;font-weight:bold;">' + t.tail + '</div><div style="font-size:10px;color:#666;">' + t.count + ' (' + t.percent + '%)</div></div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
html += '</div>';
|
||
$('#dash-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 预测号码弹窗
|
||
*/
|
||
showPredictDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div style="margin-bottom:15px;background:#fff3cd;border:1px solid #ffc107;padding:10px;border-radius:6px;">' +
|
||
' <div style="font-size:13px;font-weight:bold;color:#856404;margin-bottom:8px;"><i class="fa fa-lightbulb-o"></i> 预测算法说明</div>' +
|
||
' <div style="font-size:12px;color:#666;">' +
|
||
' <b>V1版本</b>:基于转移概率分析(区域、生肖、尾号、首号、波色转移 + 冷热系数)<br>' +
|
||
' <b>V2版本</b>:基于统计回归分析(遗漏回归、频率回归、区域平衡、波色平衡等)+ 历史回测验证<br>' +
|
||
' <b>V3版本(推荐)</b>:多维度综合预测,新增转移概率(马尔可夫链)、单双规律、大小规律、走势方向分析' +
|
||
' </div>' +
|
||
'</div>' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">算法版本:</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;"><input type="radio" name="predict-version" value="v3" checked> <b>V3</b>(多维度综合)</label>' +
|
||
' <label class="radio-inline" style="margin-right:15px;"><input type="radio" name="predict-version" value="v2"> V2(统计回归)</label>' +
|
||
' <label class="radio-inline"><input type="radio" name="predict-version" value="v1"> V1(转移概率)</label>' +
|
||
'</div>' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">验证期号:</label>' +
|
||
' <input type="text" id="predict-target" class="form-control" placeholder="输入期号验证历史预测(可选)" style="width:180px;display:inline-block;">' +
|
||
' <span style="font-size:11px;color:#999;margin-left:5px;">留空则预测下一期</span>' +
|
||
'</div>' +
|
||
'<div class="form-group" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">统计期数:</label>' +
|
||
' <input type="number" id="predict-periods" class="form-control" value="200" min="30" max="500" style="width:120px;display:inline-block;">' +
|
||
' <span style="font-size:11px;color:#999;margin-left:5px;">建议200期以上</span>' +
|
||
'</div>' +
|
||
// V3权重配置
|
||
'<div id="predict-v3-weights" style="border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">V3权重配置:</label>' +
|
||
' <div style="display:flex;flex-wrap:wrap;gap:10px;font-size:12px;">' +
|
||
' <div>遗漏回归: <input type="number" class="predict-weight-v3" data-key="omit_regression" value="0.20" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>频率回归: <input type="number" class="predict-weight-v3" data-key="freq_regression" value="0.15" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>转移概率: <input type="number" class="predict-weight-v3" data-key="transition_prob" value="0.20" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>单双平衡: <input type="number" class="predict-weight-v3" data-key="oddeven_balance" value="0.10" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>大小平衡: <input type="number" class="predict-weight-v3" data-key="bigsmall_balance" value="0.10" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>走势方向: <input type="number" class="predict-weight-v3" data-key="trend_direction" value="0.15" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>区域平衡: <input type="number" class="predict-weight-v3" data-key="zone_balance" value="0.05" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>波色平衡: <input type="number" class="predict-weight-v3" data-key="color_balance" value="0.05" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' </div>' +
|
||
'</div>' +
|
||
// V2权重配置
|
||
'<div id="predict-v2-weights" style="display:none;border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">V2权重配置:</label>' +
|
||
' <div style="display:flex;flex-wrap:wrap;gap:10px;font-size:12px;">' +
|
||
' <div>遗漏回归: <input type="number" class="predict-weight-v2" data-key="omit_regression" value="0.30" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>频率回归: <input type="number" class="predict-weight-v2" data-key="freq_regression" value="0.25" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>近期趋势: <input type="number" class="predict-weight-v2" data-key="recent_trend" value="0.20" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>区域平衡: <input type="number" class="predict-weight-v2" data-key="zone_balance" value="0.10" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>波色平衡: <input type="number" class="predict-weight-v2" data-key="color_balance" value="0.08" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>统计显著性: <input type="number" class="predict-weight-v2" data-key="stat_significance" value="0.07" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' </div>' +
|
||
'</div>' +
|
||
// V1权重配置
|
||
'<div id="predict-v1-weights" style="display:none;border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:10px;">' +
|
||
' <label style="margin-right:10px;">V1权重配置:</label>' +
|
||
' <div style="display:flex;flex-wrap:wrap;gap:10px;font-size:12px;">' +
|
||
' <div>区域: <input type="number" class="predict-weight" data-key="zone" value="0.25" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>生肖: <input type="number" class="predict-weight" data-key="zodiac" value="0.20" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>尾号: <input type="number" class="predict-weight" data-key="tail" value="0.20" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>首号: <input type="number" class="predict-weight" data-key="head" value="0.15" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>波色: <input type="number" class="predict-weight" data-key="color" value="0.10" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' <div>冷热: <input type="number" class="predict-weight" data-key="hotcold" value="0.10" min="0" max="1" step="0.05" style="width:60px;"></div>' +
|
||
' </div>' +
|
||
'</div>' +
|
||
'<div style="text-align:center;margin-bottom:15px;">' +
|
||
' <button class="btn btn-primary" id="btn-predict-query"><i class="fa fa-magic"></i> 开始预测</button>' +
|
||
'</div>' +
|
||
'<div id="predict-result" style="margin-top:15px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: '🎯 智能预测号码',
|
||
area: ['850px', '750px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
// 切换版本时显示对应权重配置
|
||
$('input[name="predict-version"]', layero).on('change', function () {
|
||
var val = $(this).val();
|
||
$('#predict-v3-weights', layero).hide();
|
||
$('#predict-v2-weights', layero).hide();
|
||
$('#predict-v1-weights', layero).hide();
|
||
if (val === 'v3') {
|
||
$('#predict-v3-weights', layero).show();
|
||
$('#predict-periods', layero).val(200);
|
||
} else if (val === 'v2') {
|
||
$('#predict-v2-weights', layero).show();
|
||
$('#predict-periods', layero).val(200);
|
||
} else {
|
||
$('#predict-v1-weights', layero).show();
|
||
$('#predict-periods', layero).val(100);
|
||
}
|
||
});
|
||
$('#btn-predict-query', layero).on('click', function () {
|
||
Controller.api.queryPredict(layero);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 查询预测结果
|
||
*/
|
||
queryPredict: function (layero) {
|
||
var $btn = $('#btn-predict-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#predict-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> 正在分析历史数据...</div>');
|
||
|
||
var version = $('input[name="predict-version"]:checked', layero).val();
|
||
var periods = parseInt($('#predict-periods', layero).val()) || 200;
|
||
var targetExpect = $('#predict-target', layero).val().trim();
|
||
var weights = {};
|
||
|
||
// 根据版本获取权重
|
||
if (version === 'v3') {
|
||
$('.predict-weight-v3', layero).each(function () {
|
||
var key = $(this).data('key');
|
||
var val = parseFloat($(this).val()) || 0;
|
||
weights[key] = val;
|
||
});
|
||
} else if (version === 'v2') {
|
||
$('.predict-weight-v2', layero).each(function () {
|
||
var key = $(this).data('key');
|
||
var val = parseFloat($(this).val()) || 0;
|
||
weights[key] = val;
|
||
});
|
||
} else {
|
||
$('.predict-weight', layero).each(function () {
|
||
var key = $(this).data('key');
|
||
var val = parseFloat($(this).val()) || 0;
|
||
weights[key] = val;
|
||
});
|
||
}
|
||
|
||
// 根据版本选择URL
|
||
var url = 'history/predict';
|
||
if (version === 'v3') {
|
||
url = 'history/predictV3';
|
||
} else if (version === 'v2') {
|
||
url = 'history/predictV2';
|
||
}
|
||
|
||
$.ajax({
|
||
url: url,
|
||
type: 'GET',
|
||
data: { periods: periods, weights: JSON.stringify(weights), target_expect: targetExpect },
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderPredict(ret.data, layero, version);
|
||
} else {
|
||
$('#predict-result', layero).html('<div class="alert alert-danger">' + (ret.msg || '预测失败') + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#predict-result', layero).html('<div class="alert alert-danger">预测请求失败</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 渲染预测结果
|
||
*/
|
||
renderPredict: function (data, layero, version) {
|
||
var predictions = data.predictions || [];
|
||
var analysis = data.analysis || {};
|
||
var hitInfo = data.hit_info || null;
|
||
var actualResult = data.actual_result || null;
|
||
var backtest = data.backtest || null;
|
||
var confidence = data.confidence || null;
|
||
|
||
if (predictions.length === 0) {
|
||
$('#predict-result', layero).html('<div class="alert alert-info">暂无预测结果</div>');
|
||
return;
|
||
}
|
||
|
||
// 上期特码信息
|
||
var lastSpecial = analysis.last_special || 0;
|
||
var lastExpect = analysis.last_expect || '';
|
||
var lastColor = Controller.api.getColorByNum(lastSpecial);
|
||
var lastAnimal = Controller.api.getAnimalByNum(lastSpecial);
|
||
|
||
// 版本名称映射
|
||
var versionNames = {
|
||
'v1': 'V1(转移概率)',
|
||
'v2': 'V2(统计回归)',
|
||
'v3': 'V3(多维度综合)'
|
||
};
|
||
|
||
var html = '<div style="padding:10px;">';
|
||
|
||
// 基准期号标题
|
||
html += '<div style="font-size:12px;color:#666;margin-bottom:10px;">基于期号 <b>' + lastExpect + '</b>(特码 ' + lastSpecial + ')进行预测 | 算法版本: <b>' + versionNames[version] + '</b></div>';
|
||
|
||
// 置信度评估展示(V2和V3版本)
|
||
if (confidence && (version === 'v2' || version === 'v3')) {
|
||
html += '<div style="background:#fff8e1;border:1px solid #ffb300;border-radius:6px;padding:12px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;color:#ff8f00;margin-bottom:8px;"><i class="fa fa-star-half-o"></i> 预测置信度评估</div>';
|
||
|
||
// 数据警告提示(数据不足时显示)
|
||
if (confidence.data_warning) {
|
||
html += '<div style="font-size:11px;color:#d32f2f;background:#ffebee;padding:6px;border-radius:4px;margin-bottom:8px;"><i class="fa fa-exclamation-triangle"></i> ' + confidence.data_warning + '</div>';
|
||
}
|
||
|
||
html += '<div style="display:flex;gap:20px;align-items:center;">';
|
||
html += '<div style="text-align:center;"><div style="font-size:24px;font-weight:bold;color:#ff8f00;">' + confidence.overall_confidence + '%</div><div style="font-size:12px;color:#666;">整体置信度</div></div>';
|
||
|
||
// 各排名置信度(使用得分集中度维度)
|
||
if (confidence.confidence_scores && confidence.confidence_scores.length > 0) {
|
||
html += '<div style="display:flex;gap:8px;">';
|
||
for (var i = 0; i < confidence.confidence_scores.length; i++) {
|
||
var cs = confidence.confidence_scores[i];
|
||
// 阈值定义:>=70%高(绿)、50-70%中(橙)、<50%低(红)
|
||
var confLevel = cs.confidence >= 70 ? '高' : (cs.confidence >= 50 ? '中' : '低');
|
||
var confColor = cs.confidence >= 70 ? '#4caf50' : (cs.confidence >= 50 ? '#ff9800' : '#f44336');
|
||
html += '<div style="text-align:center;padding:5px;background:#fff;border-radius:4px;">';
|
||
html += '<div style="font-size:14px;font-weight:bold;color:' + confColor + ';">' + cs.confidence + '%</div>';
|
||
html += '<div style="font-size:10px;color:#999;">#' + cs.rank + '</div>';
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
html += '</div></div>';
|
||
}
|
||
|
||
// 回测验证结果(V2和V3版本)
|
||
if (backtest && (version === 'v2' || version === 'v3')) {
|
||
html += '<div style="background:#e3f2fd;border:1px solid #2196f3;border-radius:6px;padding:12px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;color:#1565c0;margin-bottom:8px;"><i class="fa fa-chart-line"></i> 历史回测验证(最近' + backtest.total_tests + '期)</div>';
|
||
|
||
// 回测数据警告提示
|
||
if (backtest.data_warning) {
|
||
html += '<div style="font-size:11px;color:#d32f2f;background:#ffebee;padding:6px;border-radius:4px;margin-bottom:8px;"><i class="fa fa-exclamation-triangle"></i> ' + backtest.data_warning + '</div>';
|
||
}
|
||
|
||
html += '<div style="display:flex;gap:15px;flex-wrap:wrap;">';
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#2196f3;">' + backtest.hit_rate + '%</div><div style="font-size:11px;color:#666;">命中率(Top5)</div></div>';
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#4caf50;">' + backtest.total_hits + '/' + backtest.total_tests + '</div><div style="font-size:11px;color:#666;">命中次数</div></div>';
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#ff9800;">' + (backtest.avg_rank || '—') + '</div><div style="font-size:11px;color:#666;">平均排名</div></div>';
|
||
|
||
// 新增指标:NDCG@5 和 MRR(百分比展示)
|
||
if (backtest.ndcg_5 !== undefined) {
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#9c27b0;">' + (backtest.ndcg_5 * 100).toFixed(1) + '%</div><div style="font-size:11px;color:#666;">NDCG@5</div></div>';
|
||
}
|
||
if (backtest.mrr !== undefined) {
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#00bcd4;">' + (backtest.mrr * 100).toFixed(1) + '%</div><div style="font-size:11px;color:#666;">MRR</div></div>';
|
||
}
|
||
|
||
// 转移概率阶数显示(来自analysis.transition_order字段)
|
||
if (analysis && analysis.transition_order !== undefined) {
|
||
html += '<div style="text-align:center;padding:8px;"><div style="font-size:22px;font-weight:bold;color:#607d8b;">' + analysis.transition_order + '阶</div><div style="font-size:11px;color:#666;">转移概率</div></div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// 命中分布柱状图(使用rank_1..rank_5键名)
|
||
if (backtest.hit_distribution && Object.keys(backtest.hit_distribution).length > 0) {
|
||
var distribution = backtest.hit_distribution;
|
||
var maxHit = 0;
|
||
// 找最大值用于计算柱状图高度比例
|
||
for (var r = 1; r <= 5; r++) {
|
||
var key = 'rank_' + r;
|
||
if (distribution[key] > maxHit) {
|
||
maxHit = distribution[key];
|
||
}
|
||
}
|
||
|
||
html += '<div style="margin-top:10px;font-size:11px;color:#666;">命中分布(各排名命中次数):</div>';
|
||
html += '<div style="display:flex;gap:8px;align-items:flex-end;height:60px;margin-top:5px;padding:5px;background:#f5f5f5;border-radius:4px;">';
|
||
for (var r = 1; r <= 5; r++) {
|
||
var key = 'rank_' + r;
|
||
var hitCount = distribution[key] || 0;
|
||
var barHeight = maxHit > 0 ? (hitCount / maxHit * 45) : 0;
|
||
var barColor = hitCount > 0 ? '#4caf50' : '#e0e0e0';
|
||
html += '<div style="text-align:center;min-width:50px;">';
|
||
html += '<div style="height:' + barHeight + 'px;background:' + barColor + ';border-radius:2px 2px 0 0;width:35px;margin:0 auto;"></div>';
|
||
html += '<div style="font-size:10px;color:#666;margin-top:2px;">#' + r + '</div>';
|
||
html += '<div style="font-size:11px;color:#333;font-weight:bold;">' + hitCount + '</div>';
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
// 回测详情
|
||
if (backtest.details && backtest.details.length > 0) {
|
||
html += '<div style="margin-top:10px;font-size:11px;color:#666;">最近' + backtest.details.length + '期验证详情:</div>';
|
||
html += '<div style="margin-top:5px;max-height:300px;overflow-y:auto;">';
|
||
for (var i = 0; i < backtest.details.length; i++) {
|
||
var bd = backtest.details[i];
|
||
var hitTag = bd.hit ? '<span style="color:#4caf50;">✓</span>' : '<span style="color:#f44336;">✗</span>';
|
||
html += '<div style="font-size:11px;padding:3px 0;border-bottom:1px dashed #eee;">期号' + bd.expect + ': 实际<b>' + bd.actual + '</b> ' + hitTag + ' 预测[' + bd.predictions.join(',') + ']</div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
// V3版本特有的分析信息
|
||
if (version === 'v3' && analysis) {
|
||
html += '<div style="background:#f3e5f5;border:1px solid #9c27b0;border-radius:6px;padding:10px;margin-bottom:15px;font-size:12px;">';
|
||
html += '<div style="font-weight:bold;color:#7b1fa2;margin-bottom:8px;"><i class="fa fa-chart-bar"></i> V3多维度分析</div>';
|
||
|
||
// 单双统计
|
||
if (analysis.oddeven_stats) {
|
||
var oe = analysis.oddeven_stats;
|
||
html += '<div style="margin-bottom:5px;"><b>单双规律:</b> 单号' + oe.odd_pct + '% / 双号' + oe.even_pct + '%';
|
||
if (oe.recent_streak >= 2) {
|
||
html += ' | 近期连续' + (oe.recent_type === 'odd' ? '单号' : '双号') + oe.recent_streak + '期';
|
||
html += '(平均连续' + (oe.recent_type === 'odd' ? oe.avg_odd_streak : oe.avg_even_streak) + '期)';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
// 大小统计
|
||
if (analysis.bigsmall_stats) {
|
||
var bs = analysis.bigsmall_stats;
|
||
html += '<div style="margin-bottom:5px;"><b>大小规律:</b> 大号' + bs.big_pct + '% / 小号' + bs.small_pct + '%';
|
||
if (bs.recent_streak >= 2) {
|
||
html += ' | 近期连续' + (bs.recent_type === 'big' ? '大号' : '小号') + bs.recent_streak + '期';
|
||
html += '(平均连续' + (bs.recent_type === 'big' ? bs.avg_big_streak : bs.avg_small_streak) + '期)';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
// 走势方向
|
||
if (analysis.trend_direction) {
|
||
var td = analysis.trend_direction;
|
||
var trendNames = { 'ascending': '上升(号码减小)', 'descending': '下降(号码增大)', 'jump': '跳跃震荡' };
|
||
html += '<div style="margin-bottom:5px;"><b>走势方向:</b> ' + trendNames[td.trend_type];
|
||
html += ' | 强度' + (td.trend_strength * 100).toFixed(0) + '% | 平均变化' + td.avg_change + '</div>';
|
||
}
|
||
|
||
// 上期属性
|
||
html += '<div><b>上期属性:</b> 区域[' + (analysis.last_zone || '') + '] 尾数[' + (analysis.last_tail || '') + '] 首号[' + (analysis.last_head || '') + ']</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 遗漏统计信息(V2和V3版本)
|
||
if (analysis.omit_stats && (version === 'v2' || version === 'v3')) {
|
||
html += '<div style="background:#fce4ec;border:1px solid #e91e63;border-radius:6px;padding:10px;margin-bottom:15px;font-size:12px;">';
|
||
html += '<b>遗漏值统计:</b> 平均遗漏 ' + (analysis.omit_stats.avg || 0).toFixed(1) + ' 期 | 最大遗漏 ' + (analysis.omit_stats.max || 0) + ' 期 | 期望频率 ' + analysis.expected_freq + '';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 命中结果(验证模式下显示)
|
||
if (hitInfo && actualResult) {
|
||
var hitBgColor = hitInfo.hit ? '#d4edda' : '#f8d7da';
|
||
var hitBorderColor = hitInfo.hit ? '#28a745' : '#dc3545';
|
||
var hitTitleColor = hitInfo.hit ? '#155724' : '#721c24';
|
||
var actualColorHex = Controller.api.getColorByNum(hitInfo.actual_num);
|
||
|
||
html += '<div style="background:' + hitBgColor + ';border:2px solid ' + hitBorderColor + ';border-radius:6px;padding:12px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:14px;font-weight:bold;color:' + hitTitleColor + ';margin-bottom:8px;">';
|
||
if (hitInfo.hit) {
|
||
html += '<i class="fa fa-check-circle"></i> 预测命中!排名第 ' + hitInfo.rank + ' 位';
|
||
} else {
|
||
html += '<i class="fa fa-times-circle"></i> 未命中(实际号码不在预测Top' + predictions.length + '中)';
|
||
}
|
||
html += '</div>';
|
||
html += '<div style="display:flex;align-items:center;gap:15px;">';
|
||
html += '<div style="font-size:12px;">期号 <b>' + hitInfo.actual_expect + '</b> 实际开奖:</div>';
|
||
html += '<span style="display:inline-block;width:40px;height:40px;line-height:40px;text-align:center;border-radius:50%;color:#fff;background-color:' + actualColorHex + ';font-weight:bold;font-size:18px;">' + hitInfo.actual_num + '</span>';
|
||
html += '<div style="font-size:12px;">' + hitInfo.actual_animal + ' / ' + hitInfo.actual_color + '</div>';
|
||
html += '</div></div>';
|
||
}
|
||
|
||
// 预测号码列表
|
||
var topCount = predictions.length;
|
||
html += '<div style="background:#e8f5e9;border:1px solid #4caf50;border-radius:6px;padding:12px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;color:#2e7d32;margin-bottom:10px;"><i class="fa fa-bullseye"></i> 预测推荐号码(Top ' + topCount + ')</div>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:10px;">';
|
||
|
||
for (var i = 0; i < predictions.length; i++) {
|
||
var p = predictions[i];
|
||
var colorHex = Controller.api.getColorByNum(p.num);
|
||
var animal = Controller.api.getAnimalByNum(p.num);
|
||
// 验证模式下,命中号码高亮
|
||
var isHit = hitInfo && hitInfo.hit && p.num === hitInfo.actual_num;
|
||
var cardBg = isHit ? '#fffacd' : '#fff';
|
||
var cardBorder = isHit ? '2px solid #f39c12' : 'none';
|
||
var rankBadge = i < 3 ? '<span style="position:absolute;top:-5px;right:-5px;background:#f39c12;color:#fff;font-size:10px;padding:2px 4px;border-radius:50%;">' + (i + 1) + '</span>' : '';
|
||
|
||
// 根据版本显示不同的详情信息
|
||
var detailInfo = '';
|
||
if (p.detail) {
|
||
if (version === 'v3') {
|
||
// V3版本显示更多维度信息
|
||
var omitInfo = p.detail.omit || 0;
|
||
var transScore = p.detail.trans_score || 0;
|
||
var oddevenScore = p.detail.oddeven_score || 0;
|
||
var bigsmallScore = p.detail.bigsmall_score || 0;
|
||
var trendScore = p.detail.trend_score || 0;
|
||
detailInfo = '<div style="font-size:9px;color:#999;line-height:1.3;">';
|
||
detailInfo += '遗漏:' + omitInfo + '期 | ';
|
||
detailInfo += '转移:' + transScore + ' | ';
|
||
detailInfo += (p.detail.is_odd ? '单' : '双') + ':' + oddevenScore + ' | ';
|
||
detailInfo += (p.detail.is_big ? '大' : '小') + ':' + bigsmallScore;
|
||
detailInfo += '</div>';
|
||
} else if (version === 'v2') {
|
||
var omitInfo = p.detail.omit || 0;
|
||
detailInfo = '<div style="font-size:9px;color:#999;">遗漏:' + omitInfo + '期</div>';
|
||
}
|
||
}
|
||
|
||
html += '<div style="text-align:center;background:' + cardBg + ';border:' + cardBorder + ';padding:8px;border-radius:8px;min-width:80px;position:relative;box-shadow:0 1px 3px rgba(0,0,0,0.1);">' + rankBadge;
|
||
html += '<span style="display:inline-block;width:36px;height:36px;line-height:36px;text-align:center;border-radius:50%;color:#fff;background-color:' + colorHex + ';font-weight:bold;font-size:16px;">' + p.num + '</span>';
|
||
html += '<div style="font-size:10px;color:#666;line-height:1.2;margin-top:2px;">' + animal + '</div>';
|
||
html += '<div style="font-size:11px;color:#2e7d32;font-weight:bold;">得分:' + p.score + '</div>';
|
||
|
||
// 显示置信度(V3版本)
|
||
if (version === 'v3' && confidence && confidence.confidence_scores) {
|
||
var csForNum = confidence.confidence_scores.find(function(c) { return c.num === p.num; });
|
||
if (csForNum) {
|
||
// 阈值定义:>=70%高(绿)、50-70%中(橙)、<50%低(红)
|
||
var confLevel = csForNum.confidence >= 70 ? '高' : (csForNum.confidence >= 50 ? '中' : '低');
|
||
var confColor = csForNum.confidence >= 70 ? '#4caf50' : (csForNum.confidence >= 50 ? '#ff9800' : '#f44336');
|
||
html += '<div style="font-size:10px;"><span style="color:' + confColor + ';font-weight:bold;">置信度:' + confLevel + '</span> <span style="color:#666;">(' + csForNum.confidence + '%)</span></div>';
|
||
}
|
||
}
|
||
|
||
html += detailInfo;
|
||
html += '</div>';
|
||
}
|
||
html += '</div></div>';
|
||
|
||
html += '</div>';
|
||
$('#predict-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 显示正码关联预测弹窗(修正版)
|
||
* 核心规律:上期正码 → 当期特码
|
||
*/
|
||
showNormalRelationDialog: function () {
|
||
var html = '<div style="padding:20px;">' +
|
||
'<div style="margin-bottom:15px;background:#e8f5e9;border:1px solid #4caf50;padding:10px;border-radius:6px;">' +
|
||
' <div style="font-size:13px;font-weight:bold;color:#2e7d32;margin-bottom:8px;"><i class="fa fa-link"></i> 正码关联预测算法(修正版)</div>' +
|
||
' <div style="font-size:12px;color:#666;">' +
|
||
' <b>核心逻辑</b>:用<b>上期正码(num1-6)</b>预测<b>当期特码(num7)</b><br>' +
|
||
' <b>1. 覆盖区间规律(91.44%)</b>:当期特码在上期正码覆盖的细区间内<br>' +
|
||
' <b>2. 特码区间转移(77.54%)</b>:基于上期特码区间预测当期特码大区间<br>' +
|
||
' <b>3. 双波色预测(69.52%)</b>:当期特码波色在上期正码前2种主导波色内<br>' +
|
||
' <b>4. 正码±3距离(59.36%)</b>:当期特码与上期正码某号码距离≤3<br>' +
|
||
' <b>5. 尾数±2(50%)</b>:上期正码和值尾数与当期特码尾数差≤2<br>' +
|
||
' <b>6. 平均值±10(41.98%)</b>:当期特码在上期正码平均值±10范围' +
|
||
' </div>' +
|
||
'</div>' +
|
||
'<div class="form-group">' +
|
||
' <label>回测期数:</label>' +
|
||
' <input type="number" id="nr-periods" class="form-control" value="100" min="30" max="500" style="width:120px;display:inline-block;">' +
|
||
' <label style="margin-left:15px;">目标期号(可选):</label>' +
|
||
' <input type="text" id="nr-target" class="form-control" placeholder="如2026120" style="width:150px;display:inline-block;">' +
|
||
' <button class="btn btn-primary" id="btn-nr-query" style="margin-left:10px;"><i class="fa fa-search"></i> 查询</button>' +
|
||
'</div>' +
|
||
'<div id="nr-result" style="margin-top:15px;"></div>' +
|
||
'</div>';
|
||
|
||
Layer.open({
|
||
type: 1,
|
||
title: '正码关联预测(上期正码→当期特码)',
|
||
area: ['900px', '750px'],
|
||
content: html,
|
||
shadeClose: true,
|
||
success: function (layero, index) {
|
||
$('#btn-nr-query', layero).on('click', function () {
|
||
Controller.api.queryNormalRelation(layero);
|
||
});
|
||
// 自动执行一次查询
|
||
Controller.api.queryNormalRelation(layero);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 查询正码关联预测
|
||
*/
|
||
queryNormalRelation: function (layero) {
|
||
var $btn = $('#btn-nr-query', layero);
|
||
$btn.prop('disabled', true);
|
||
$('#nr-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> 正在分析...</div>');
|
||
|
||
var periods = parseInt($('#nr-periods', layero).val()) || 100;
|
||
var targetExpect = $('#nr-target', layero).val().trim();
|
||
|
||
$.ajax({
|
||
url: 'history/predictByNormalRelation',
|
||
type: 'GET',
|
||
data: { periods: periods, target_expect: targetExpect },
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
Controller.api.renderNormalRelation(ret.data, layero);
|
||
} else {
|
||
$('#nr-result', layero).html('<div class="alert alert-danger">' + (ret.msg || '查询失败') + '</div>');
|
||
}
|
||
},
|
||
error: function () {
|
||
$('#nr-result', layero).html('<div class="alert alert-danger">查询失败</div>');
|
||
},
|
||
complete: function () {
|
||
$btn.prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 渲染正码关联预测结果
|
||
*/
|
||
renderNormalRelation: function (data, layero) {
|
||
if (!data || !data.predictions || data.predictions.length === 0) {
|
||
$('#nr-result', layero).html('<div class="alert alert-info">无预测结果</div>');
|
||
return;
|
||
}
|
||
|
||
var analysis = data.analysis || {};
|
||
var predictions = data.predictions;
|
||
var hitInfo = data.hit_info;
|
||
var backtest = data.backtest;
|
||
|
||
var html = '';
|
||
|
||
// 分析信息
|
||
html += '<div style="background:#f5f5f5;padding:10px;border-radius:6px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;"><i class="fa fa-info-circle"></i> 上期开奖信息(预测基准)</div>';
|
||
html += '<div style="font-size:12px;">';
|
||
html += '期号:<b>' + analysis.last_expect + '</b> | ';
|
||
html += '正码:<b>' + analysis.last_normals.join(', ') + '</b> | ';
|
||
html += '特码:<b>' + analysis.last_special + '</b><br>';
|
||
html += '正码范围:' + analysis.normal_min + ' ~ ' + analysis.normal_max + ' | ';
|
||
html += '平均值:' + analysis.normal_avg + ' | ';
|
||
html += '和值:' + analysis.normal_sum + '<br>';
|
||
html += '正码波色:<b>' + (analysis.top2_colors || analysis.normal_colors || []).join('/') + '</b> | ';
|
||
html += '覆盖区间:' + (analysis.normal_fine_zones || analysis.normal_zones || []).map(function(z) {
|
||
return ['1-10','11-20','21-30','31-40','41-49'][z];
|
||
}).join(',');
|
||
html += '</div></div>';
|
||
|
||
// 预测号码展示
|
||
html += '<div style="margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;"><i class="fa fa-star"></i> 推荐号码(Top 15)</div>';
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:8px;">';
|
||
|
||
for (var i = 0; i < predictions.length; i++) {
|
||
var p = predictions[i];
|
||
var colorHex = Controller.api.getColorByNum(p.num);
|
||
var animal = Controller.api.animalMap[p.num] || '';
|
||
|
||
var bgColor = '#fff';
|
||
var borderColor = '1px solid #ddd';
|
||
if (i < 5) {
|
||
bgColor = '#fff8e1';
|
||
borderColor = '2px solid #ffc107';
|
||
}
|
||
|
||
html += '<div style="background:' + bgColor + ';border:' + borderColor + ';padding:8px;border-radius:8px;min-width:90px;text-align:center;">';
|
||
html += '<span style="display:inline-block;width:36px;height:36px;line-height:36px;border-radius:50%;color:#fff;background:' + colorHex + ';font-weight:bold;font-size:16px;">' + p.num + '</span>';
|
||
html += '<div style="font-size:10px;color:#666;">' + animal + '</div>';
|
||
html += '<div style="font-size:11px;color:#2e7d32;font-weight:bold;">得分:' + p.score + '</div>';
|
||
html += '<div style="font-size:9px;color:#999;">';
|
||
html += (p.color_in_top2 || p.color_match) ? '✓波色 ' : '';
|
||
html += '距离' + (p.min_distance || 0);
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
html += '</div></div>';
|
||
|
||
// 规律命中情况(如果有回测)
|
||
if (backtest) {
|
||
html += '<div style="background:#e3f2fd;padding:10px;border-radius:6px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;"><i class="fa fa-chart-line"></i> 回测验证(最近' + backtest.periods + '期)</div>';
|
||
html += '<div style="font-size:12px;">';
|
||
html += '命中率(Top15内):<b style="color:#2e7d32;">' + backtest.hit_rate + '%</b> (' + backtest.hits + '/' + backtest.periods + ')<br>';
|
||
html += '平均排名:<b>' + backtest.avg_rank + '</b> / 49<br>';
|
||
html += '<span style="color:#666;font-size:11px;">注:命中率越高越好,平均排名越低越好</span>';
|
||
html += '</div>';
|
||
|
||
// 显示前50期命中详情表格
|
||
if (backtest.details && backtest.details.length > 0) {
|
||
html += '<div style="margin-top:10px;max-height:300px;overflow-y:auto;">';
|
||
html += '<table class="table table-striped table-bordered" style="font-size:11px;">';
|
||
html += '<thead><tr><th>期号</th><th>特码</th><th>排名</th><th>命中</th></tr></thead>';
|
||
html += '<tbody>';
|
||
for (var d = 0; d < backtest.details.length; d++) {
|
||
var det = backtest.details[d];
|
||
var hitBadge = det.hit ? '<span style="color:#2e7d32;font-weight:bold;">✓</span>' : '<span style="color:#c62828;">✗</span>';
|
||
var rankColor = det.rank <= 5 ? '#2e7d32' : (det.rank <= 15 ? '#ff9800' : '#c62828');
|
||
html += '<tr>';
|
||
html += '<td>' + det.expect + '</td>';
|
||
html += '<td>' + det.actual + '</td>';
|
||
html += '<td><b style="color:' + rankColor + ';">' + det.rank + '</b></td>';
|
||
html += '<td>' + hitBadge + '</td>';
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody></table>';
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
// 实际命中情况(如果有目标期号)
|
||
if (hitInfo) {
|
||
var hitBg = hitInfo.hit ? '#e8f5e9' : '#ffebee';
|
||
var hitColor = hitInfo.hit ? '#2e7d32' : '#c62828';
|
||
html += '<div style="background:' + hitBg + ';padding:10px;border-radius:6px;margin-bottom:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;color:' + hitColor + ';">';
|
||
html += hitInfo.hit ? '✓ 命中!排名:' + hitInfo.rank_in_top + ' / 15' : '✗ 未命中,排名:' + hitInfo.rank_in_all + ' / 49';
|
||
html += '</div>';
|
||
html += '<div style="font-size:12px;">实际特码:<b>' + hitInfo.actual_num + '</b> (' + hitInfo.actual_color + '/' + hitInfo.actual_animal + ') 期号:' + hitInfo.actual_expect + '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 规律说明表
|
||
if (analysis.rules) {
|
||
html += '<div style="margin-top:15px;">';
|
||
html += '<div style="font-size:13px;font-weight:bold;margin-bottom:8px;"><i class="fa fa-table"></i> 规律命中率表</div>';
|
||
html += '<table class="table table-bordered" style="font-size:12px;">';
|
||
html += '<tr><th>规律名称</th><th>命中率</th><th>说明</th></tr>';
|
||
for (var r = 0; r < analysis.rules.length; r++) {
|
||
var rule = analysis.rules[r];
|
||
html += '<tr><td>' + rule.name + '</td><td><b style="color:#2e7d32;">' + rule.rate + '</b></td><td>' + rule.desc + '</td></tr>';
|
||
}
|
||
html += '</table></div>';
|
||
}
|
||
|
||
$('#nr-result', layero).html(html);
|
||
},
|
||
|
||
/**
|
||
* 尾首概率弹窗
|
||
*/
|
||
showTailHeadProbabilityDialog: function () {
|
||
var content = '<div id="tailheadprob-content" style="padding:20px 30px;">';
|
||
content += '<div style="text-align:center;margin-bottom:20px;">';
|
||
content += '<label style="margin-right:10px;">统计期数:</label>';
|
||
content += '<select id="tailheadprob-periods" style="width:100px;padding:5px 10px;">';
|
||
content += '<option value="30">近30期</option>';
|
||
content += '<option value="50">近50期</option>';
|
||
content += '<option value="100" selected>近100期</option>';
|
||
content += '<option value="200">近200期</option>';
|
||
content += '</select>';
|
||
content += ' <button class="btn btn-primary btn-sm" id="btn-tailheadprob-query" style="margin-left:10px;"><i class="fa fa-search"></i> 查询</button>';
|
||
content += '</div>';
|
||
content += '<div id="tailheadprob-result"></div>';
|
||
content += '</div>';
|
||
|
||
layer.open({
|
||
type: 1,
|
||
title: '尾首概率分析',
|
||
area: ['500px', '380px'],
|
||
content: content,
|
||
success: function (layero) {
|
||
$('#btn-tailheadprob-query', layero).on('click', function () {
|
||
var periods = $('#tailheadprob-periods').val();
|
||
$.ajax({
|
||
url: 'history/tailHeadProb',
|
||
type: 'GET',
|
||
data: { periods: periods },
|
||
dataType: 'json',
|
||
success: function (ret) {
|
||
if (ret.code == 1) {
|
||
var d = ret.msg;
|
||
var html = '<div style="margin-top:15px;">';
|
||
html += '<div style="padding:15px;background:#f0f9ff;border-radius:8px;margin-bottom:12px;">';
|
||
html += '<div style="font-size:14px;font-weight:bold;color:#1565c0;margin-bottom:10px;"><i class="fa fa-bar-chart"></i> 概率统计</div>';
|
||
html += '<table class="table table-bordered" style="margin-bottom:0;">';
|
||
html += '<tr><th style="width:50%;">对比项</th><th>相同概率</th></tr>';
|
||
html += '<tr><td>下一期尾数与前一期尾数相同</td><td style="text-align:center;font-size:18px;font-weight:bold;color:#e65100;">' + d.tailProb + '%</td></tr>';
|
||
html += '<tr><td>下一期首位与前一期首位相同</td><td style="text-align:center;font-size:18px;font-weight:bold;color:#e65100;">' + d.headProb + '%</td></tr>';
|
||
html += '</table></div>';
|
||
html += '<div style="font-size:12px;color:#999;">共分析 ' + d.periodCount + ' 期数据,' + d.totalTransitions + ' 次转移</div>';
|
||
html += '</div>';
|
||
$('#tailheadprob-result').html(html);
|
||
} else {
|
||
Toastr.error(ret.msg);
|
||
}
|
||
},
|
||
error: function () {
|
||
Toastr.error('请求失败,请重试');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 初始加载
|
||
$('#btn-tailheadprob-query', layero).trigger('click');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
return Controller;
|
||
});
|