feat(phase-3): add hot/cold number analysis with period filter
This commit is contained in:
@@ -22,7 +22,7 @@ class History extends Backend
|
||||
* 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录)
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedRight = ['missingNum', 'trendData'];
|
||||
protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers'];
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
@@ -72,5 +72,25 @@ class History extends Backend
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冷热号码
|
||||
* @return void
|
||||
*/
|
||||
public function hotColdNumbers()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$periods = $this->request->get('periods', 30, 'intval');
|
||||
if ($periods < 10 || $periods > 100) {
|
||||
$this->error('期数范围必须在 10-100 之间');
|
||||
}
|
||||
$type = $this->request->get('type', 'all');
|
||||
if (!in_array($type, ['all', 'special'])) {
|
||||
$this->error('查询类型不正确');
|
||||
}
|
||||
$result = $this->model->getHotColdNumbers($periods, $type);
|
||||
$this->success('查询成功', null, $result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -17,4 +17,5 @@ return [
|
||||
'Special Only' => '仅特码',
|
||||
'Trend Chart' => '走势图',
|
||||
'No data available' => '暂无数据',
|
||||
'Hot/Cold Analysis' => '冷热分析',
|
||||
];
|
||||
|
||||
@@ -74,6 +74,69 @@ class History extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冷热号码
|
||||
* @param int $periods 查询最近多少期
|
||||
* @param string $type 查询类型 all=全部号码 special=仅特码
|
||||
* @return array {hot: [], cold: [], all: []}
|
||||
*/
|
||||
public function getHotColdNumbers($periods = 30, $type = 'all')
|
||||
{
|
||||
$num_model = new Num();
|
||||
$colorMap = $num_model->column('color', 'num');
|
||||
$animalMap = $num_model->column('animal', 'num');
|
||||
|
||||
$history = $this
|
||||
->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime')
|
||||
->order('openTime', 'desc')
|
||||
->limit($periods)
|
||||
->select();
|
||||
|
||||
if (empty($history)) {
|
||||
return ['hot' => [], 'cold' => [], 'all' => []];
|
||||
}
|
||||
|
||||
$fields = ($type === 'special') ? ['num7'] : ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7'];
|
||||
|
||||
// 统计每个号码的出现次数
|
||||
$count = array_fill(1, 49, 0);
|
||||
$totalAppearances = 0;
|
||||
foreach ($history as $row) {
|
||||
foreach ($fields as $field) {
|
||||
$num = (int)$row[$field];
|
||||
if ($num >= 1 && $num <= 49) {
|
||||
$count[$num]++;
|
||||
$totalAppearances++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$all = [];
|
||||
for ($num = 1; $num <= 49; $num++) {
|
||||
$percent = $totalAppearances > 0 ? round($count[$num] / $totalAppearances * 100, 1) : 0;
|
||||
$all[] = [
|
||||
'num' => $num,
|
||||
'count' => $count[$num],
|
||||
'percent' => $percent,
|
||||
'color' => $colorMap[$num] ?? '—',
|
||||
'animal' => $animalMap[$num] ?? '—'
|
||||
];
|
||||
}
|
||||
|
||||
// 按出现次数降序排序
|
||||
$sorted = $all;
|
||||
usort($sorted, function ($a, $b) {
|
||||
return $b['count'] - $a['count'];
|
||||
});
|
||||
|
||||
// 热号: top 10, 冷号: bottom 10
|
||||
$hot = array_slice($sorted, 0, 10);
|
||||
$cold = array_slice($sorted, -10);
|
||||
$cold = array_reverse($cold);
|
||||
|
||||
return ['hot' => $hot, 'cold' => $cold, 'all' => $all];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算遗漏号码
|
||||
* @param int $periods 查询最近多少期
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
|
||||
<a href="javascript:;" class="btn btn-info btn-trend" title="{:__('Trend Chart')}"><i class="fa fa-area-chart"></i> {:__('Trend Chart')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-hotcold" title="{:__('Hot/Cold Analysis')}"><i class="fa fa-fire"></i> {:__('Hot/Cold Analysis')}</a>
|
||||
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
|
||||
@@ -52,6 +52,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
$(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();
|
||||
});
|
||||
},
|
||||
add: function () {
|
||||
Controller.api.bindevent();
|
||||
@@ -145,6 +150,112 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示冷热分析弹窗
|
||||
*/
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示走势图弹窗
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user