Change layout

This commit is contained in:
jackyu66git
2025-04-29 12:06:20 +08:00
parent d2fd7bc32a
commit 645c66d23d
+570 -31
View File
@@ -18,7 +18,7 @@
body {
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
padding: 10px;
background-color: #f8f9fa;
color: #333;
}
@@ -26,18 +26,17 @@
max-width: 100%;
margin: 0 auto;
background-color: white;
padding: 20px;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
margin-bottom: 0px;
padding-bottom: 0px;
}
.controls {
margin-bottom: 20px;
padding: 20px;
margin-bottom: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
@@ -50,7 +49,7 @@
.chart-container {
width: 100%;
height: 600px;
margin-top: 20px;
margin-top: 10px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
@@ -74,10 +73,10 @@
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.data-container {
margin-top: 30px;
margin-top: 10px;
}
.nav-tabs {
margin-bottom: 15px;
margin-bottom: 10px;
}
.table-container {
overflow-x: auto;
@@ -215,12 +214,22 @@
margin-left: 8px;
vertical-align: middle;
}
/* 数据回放相关样式 */
#replayStatus {
min-width: 120px;
}
#replayProgress {
border-radius: 4px;
overflow: hidden;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 class="my-4">缠论分析系统</h1>
<h5 class="my-1">缠论分析系统</h5>
</div>
<div class="controls">
@@ -266,7 +275,7 @@
</div>
<div class="row mt-3">
<div class="col-md-5">
<div class="col-md-7">
<div class="d-flex align-items-center">
<label class="form-label me-3 mb-0">基础显示:</label>
<div class="form-check form-check-inline">
@@ -286,7 +295,7 @@
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showVolume" checked>
<input class="form-check-input" type="checkbox" id="showVolume">
<label class="form-check-label" for="showVolume">成交量</label>
</div>
@@ -294,11 +303,6 @@
<input class="form-check-input" type="checkbox" id="showMacd" checked>
<label class="form-check-label" for="showMacd">MACD</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showTradePoints" checked>
<label class="form-check-label" for="showTradePoints">买卖点</label>
</div>
</div>
<div class="d-flex align-items-center mt-2">
<label class="form-label me-3 mb-0">主周期:</label>
@@ -307,30 +311,30 @@
<label class="form-check-label" for="showMainBi"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainSeg" checked>
<input class="form-check-input" type="checkbox" id="showMainSeg">
<label class="form-check-label" for="showMainSeg">线段</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainZs" checked>
<input class="form-check-input" type="checkbox" id="showMainZs">
<label class="form-check-label" for="showMainZs">中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs" checked>
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs">
<label class="form-check-label" for="showMainUncompletedZs">未完成中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainMacdDiv" checked>
<input class="form-check-input" type="checkbox" id="showMainMacdDiv">
<label class="form-check-label" for="showMainMacdDiv">MACD背离</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showKlcFxType">
<label class="form-check-label" for="showKlcFxType">分型类型</label>
<input class="form-check-input" type="checkbox" id="showKlcFxType" checked>
<label class="form-check-label" for="showKlcFxType">分型</label>
</div>
</div>
<div class="d-flex align-items-center mt-2">
<label class="form-label me-3 mb-0">次周期:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBi" checked>
<input class="form-check-input" type="checkbox" id="showElementBi">
<label class="form-check-label" for="showElementBi"></label>
</div>
<div class="form-check form-check-inline">
@@ -338,24 +342,24 @@
<label class="form-check-label" for="showElementSeg">线段</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementZs" checked>
<input class="form-check-input" type="checkbox" id="showElementZs">
<label class="form-check-label" for="showElementZs">中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs" checked>
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs">
<label class="form-check-label" for="showElementUncompletedZs">未完成中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementMacdDiv" checked>
<input class="form-check-input" type="checkbox" id="showElementMacdDiv">
<label class="form-check-label" for="showElementMacdDiv">MACD背离</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementKlcFxType" checked>
<label class="form-check-label" for="showElementKlcFxType">分型类型</label>
<input class="form-check-input" type="checkbox" id="showElementKlcFxType">
<label class="form-check-label" for="showElementKlcFxType">分型</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="col-md-5">
<div class="d-flex align-items-center">
<label for="elementTimeframe" class="form-label me-3 mb-0">次级别时间周期:</label>
<select id="elementTimeframe" class="form-select form-select-sm" style="width: auto;">
@@ -386,6 +390,33 @@
<span id="nextRefreshTime" class="ms-2 text-muted" style="display:none;font-size:0.85rem;"></span>
<div id="refreshLoadingSpinner" class="loading-spinner ms-2" style="display:none;"></div>
</div>
<!-- 添加数据回放控制面板 -->
<div class="d-flex align-items-center mt-2">
<label for="replayInterval" class="form-label me-3 mb-0">数据回放:</label>
<select id="replayInterval" class="form-select form-select-sm me-2" style="width: auto;">
<option value="0.5">0.5秒</option>
<option value="1" selected>1秒</option>
<option value="2">2秒</option>
<option value="3">3秒</option>
<option value="5">5秒</option>
</select>
<div class="btn-group me-2">
<button id="startReplay" class="btn btn-sm btn-success">
<i class="bi bi-play-fill"></i> 开始回放
</button>
<button id="pauseReplay" class="btn btn-sm btn-warning" style="display:none;">
<i class="bi bi-pause-fill"></i> 暂停
</button>
<button id="stopReplay" class="btn btn-sm btn-danger" style="display:none;">
<i class="bi bi-stop-fill"></i> 停止
</button>
</div>
<span id="replayStatus" class="ms-2 text-muted" style="font-size:0.85rem;"></span>
<div id="replayProgress" class="progress ms-2" style="width: 100px; height: 8px; display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
@@ -3871,6 +3902,514 @@
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// 数据回放相关变量
let replayTimer = null;
let replayData = null;
let currentReplayIndex = 0;
let totalReplaySteps = 0;
let isReplaying = false;
let replayStartTime = null;
let replayEndTime = null;
// 数据回放功能初始化
$(document).ready(function() {
// 开始回放按钮
$('#startReplay').click(function() {
startDataReplay();
});
// 暂停回放按钮
$('#pauseReplay').click(function() {
pauseDataReplay();
});
// 停止回放按钮
$('#stopReplay').click(function() {
stopDataReplay();
});
});
// 开始数据回放
function startDataReplay() {
// 如果自动刷新开启,需要先关闭
if ($('#autoRefresh').is(':checked')) {
$('#autoRefresh').prop('checked', false).change();
}
// 检查是否设置了时间范围
if (!$('#start_time').val() || !$('#end_time').val()) {
alert('请先设置开始时间和结束时间');
return;
}
// 如果已经在回放中,且是暂停状态,则继续回放
if (isReplaying && replayTimer === null) {
continueDataReplay();
return;
}
// 停止任何现有的回放
stopDataReplay();
// 获取回放参数
replayStartTime = new Date($('#start_time').val()).getTime();
replayEndTime = new Date($('#end_time').val()).getTime();
// 检查时间范围是否有效
if (replayStartTime >= replayEndTime) {
alert('开始时间必须早于结束时间');
return;
}
// 获取当前参数
const symbol = $('#symbol').val() || 'SOL/USDT:USDT';
const timeframe = $('#timeframe').val() || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 显示回放状态
$('#replayStatus').text('正在加载数据...');
$('#replayProgress').show();
$('.progress-bar').css('width', '0%');
// 切换按钮状态
$('#startReplay').hide();
$('#stopReplay').show();
// 尝试使用当前已有数据
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const currentStart = new Date(currentData.kline_data[0].date).getTime();
const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime();
// 检查当前数据是否覆盖所需的时间范围
if (currentStart <= replayStartTime && currentEnd >= replayEndTime) {
console.log('使用当前数据进行回放,无需重新请求');
replayData = currentData;
initReplay();
return;
}
}
// 请求整个时间范围的数据
$.ajax({
url: '/api/analyze',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
start_time: replayStartTime,
end_time: replayEndTime,
elements_only: false
},
success: function(data) {
// 检查是否有有效数据
if (!data || !data.kline_data || data.kline_data.length === 0) {
$('#replayStatus').text('服务器返回的数据无效');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
console.error('服务器返回的数据无效:', data);
return;
}
// 保存完整数据
replayData = data;
// 初始化回放
initReplay();
},
error: function(jqXHR, textStatus, errorThrown) {
$('#replayStatus').text('加载数据失败');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化回放
function initReplay() {
if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
return;
}
// 设置回放变量
isReplaying = true;
currentReplayIndex = 0;
totalReplaySteps = replayData.kline_data.length;
// 更新回放状态
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
$('.progress-bar').css('width', '0%');
// 准备初始数据
prepareInitialReplayData();
// 开始回放
continueDataReplay();
}
// 准备初始回放数据
function prepareInitialReplayData() {
// 创建初始数据集,只包含第一根K线
const initialData = $.extend(true, {}, replayData);
// 确保第一根K线数据有效
if (!replayData.kline_data || replayData.kline_data.length === 0) {
console.error('无有效K线数据用于回放');
$('#replayStatus').text('无有效数据用于回放');
$('#replayProgress').hide();
return;
}
initialData.kline_data = [replayData.kline_data[0]];
// 确保初始K线数据的所有字段都有值
const firstKline = initialData.kline_data[0];
if (firstKline) {
// 确保K线数据的每个字段都是有效的数值
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) {
console.warn(`K线数据的${field}字段无效,设置为默认值`);
firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0;
}
});
}
// 清空其他数据列表,因为还没有处理
initialData.bi_list = [];
initialData.seg_list = [];
initialData.zs_list = [];
initialData.uncompleted_zs_list = [];
initialData.element_bi_list = [];
initialData.element_seg_list = [];
initialData.element_zs_list = [];
initialData.element_uncompleted_zs_list = [];
initialData.macd_divergence = [];
initialData.klc_fx_type = [];
// 初始化MACD数据
if (initialData.macd && initialData.macd.length > 0) {
initialData.macd = [initialData.macd[0]];
// 确保MACD数据有效
const firstMacd = initialData.macd[0];
if (firstMacd) {
['dif', 'dea', 'macd'].forEach(field => {
if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) {
console.warn(`MACD数据的${field}字段无效,设置为0`);
firstMacd[field] = 0;
}
});
}
}
// 更新图表,固定坐标轴范围
currentData = initialData;
// 清除当前图表
if (tvWidget.state.isInitialized) {
// 保存当前的图表可见范围以供后续使用
try {
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.error('保存图表范围失败', e);
}
}
// 刷新图表
refreshChart(initialData);
// 固定y轴范围
fixYAxisRange();
}
// 固定y轴范围,保持图表不因数据变化而变化
function fixYAxisRange() {
if (!tvWidget.mainChart) return;
// 获取全部数据的最高价和最低价,为了适当的范围
let minPrice = Infinity;
let maxPrice = -Infinity;
// 计算整个数据集的价格范围
replayData.kline_data.forEach(kline => {
const high = parseFloat(kline.high);
const low = parseFloat(kline.low);
if (high > maxPrice) maxPrice = high;
if (low < minPrice) minPrice = low;
});
// 添加一些边距(约5%
const priceRange = maxPrice - minPrice;
maxPrice += priceRange * 0.05;
minPrice -= priceRange * 0.05;
// 修复: 使用正确的API设置固定价格范围
try {
// 禁用自动缩放
tvWidget.mainChart.priceScale().applyOptions({
autoScale: false,
scaleMargins: {
top: 0.1,
bottom: 0.2
},
// 直接在options中设置最小和最大值
minimumValue: minPrice,
maximumValue: maxPrice
});
} catch (e) {
console.error('设置Y轴范围失败:', e);
}
}
// 继续回放
function continueDataReplay() {
if (!isReplaying || !replayData) return;
// 获取回放速度
const interval = parseFloat($('#replayInterval').val()) * 1000 || 1000;
// 切换按钮状态
$('#startReplay').hide();
$('#pauseReplay').show();
$('#stopReplay').show();
// 开始回放定时器
replayTimer = setInterval(replayNextStep, interval);
}
// 回放下一步
function replayNextStep() {
if (!isReplaying || currentReplayIndex >= totalReplaySteps) {
stopDataReplay();
return;
}
// 更新回放状态
currentReplayIndex++;
updateReplayStatus();
// 创建截止到当前索引的数据子集
const subsetData = createDataSubset(currentReplayIndex);
// 更新图表
currentData = subsetData;
refreshChart(subsetData);
// 固定Y轴范围
fixYAxisRange();
// 保持最新数据可见
keepLatestDataVisible();
// 如果到达最后一步,停止回放
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
pauseDataReplay();
}
}
// 保持最新数据可见
function keepLatestDataVisible() {
if (!tvWidget.mainChart) return;
try {
// 获取当前数据的最后一个时间点
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
// 计算时间范围
const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
// 设置时间轴范围
tvWidget.mainChart.timeScale().setVisibleRange({
from: firstTime,
to: lastTime
});
}
} catch (e) {
console.error('保持最新数据可见失败:', e);
}
}
// 创建数据子集
function createDataSubset(endIndex) {
// 创建数据副本
const subsetData = $.extend(true, {}, replayData);
// 截取K线数据
subsetData.kline_data = replayData.kline_data.slice(0, endIndex);
// 验证K线数据
subsetData.kline_data.forEach((kline, index) => {
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) {
console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`);
// 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值
if (field !== 'volume') {
if (field === 'open' && kline.close !== null && !isNaN(kline.close)) {
kline[field] = kline.close;
} else if (index > 0) {
kline[field] = subsetData.kline_data[index-1][field] || 0;
} else {
kline[field] = 0;
}
} else {
kline[field] = 0; // 对于成交量,设为0
}
}
});
});
// 处理其他数据结构(笔、线段、中枢等)
// 我们需要根据当前K线的时间来过滤其他数据结构
try {
const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime();
// 过滤笔列表
if (subsetData.bi_list && subsetData.bi_list.length) {
subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime);
}
// 过滤线段列表
if (subsetData.seg_list && subsetData.seg_list.length) {
subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime);
}
// 过滤中枢列表
if (subsetData.zs_list && subsetData.zs_list.length) {
subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime);
}
// 过滤未完成中枢列表
if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) {
subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime);
}
// 过滤小周期数据
if (subsetData.element_bi_list && subsetData.element_bi_list.length) {
subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime);
}
if (subsetData.element_seg_list && subsetData.element_seg_list.length) {
subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime);
}
if (subsetData.element_zs_list && subsetData.element_zs_list.length) {
subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime);
}
if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) {
subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime);
}
// 过滤MACD
if (subsetData.macd && subsetData.macd.length) {
subsetData.macd = replayData.macd.slice(0, endIndex);
// 验证MACD数据
subsetData.macd.forEach((item, index) => {
['dif', 'dea', 'macd'].forEach(field => {
if (item[field] === null || item[field] === undefined || isNaN(item[field])) {
console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`);
item[field] = 0;
}
});
});
}
// 过滤MACD背离
if (subsetData.macd_divergence && subsetData.macd_divergence.length) {
subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime);
}
// 过滤分型类型
if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) {
subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time');
}
} catch (e) {
console.error('过滤数据时出错:', e);
}
return subsetData;
}
// 根据时间过滤数据
function filterDataByTime(dataList, endTime, timeField = 'end_time') {
if (!dataList || !Array.isArray(dataList)) return [];
return dataList.filter(item => {
const itemTime = new Date(item[timeField]).getTime();
return itemTime <= endTime;
});
}
// 更新回放状态显示
function updateReplayStatus() {
const percent = Math.floor((currentReplayIndex / totalReplaySteps) * 100);
$('#replayStatus').text(`回放中 (${currentReplayIndex}/${totalReplaySteps})`);
$('.progress-bar').css('width', `${percent}%`);
}
// 暂停回放
function pauseDataReplay() {
if (replayTimer) {
clearInterval(replayTimer);
replayTimer = null;
}
// 切换按钮状态
$('#pauseReplay').hide();
$('#startReplay').show();
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
} else {
$('#replayStatus').text(`已暂停 (${currentReplayIndex}/${totalReplaySteps})`);
}
}
// 停止回放
function stopDataReplay() {
// 清除定时器
if (replayTimer) {
clearInterval(replayTimer);
replayTimer = null;
}
// 重置回放状态
isReplaying = false;
currentReplayIndex = 0;
replayData = null;
// 更新UI
$('#replayStatus').text('');
$('#replayProgress').hide();
$('#pauseReplay').hide();
$('#stopReplay').hide();
$('#startReplay').show();
// 恢复图表自动缩放
if (tvWidget.mainChart) {
tvWidget.mainChart.priceScale().applyOptions({
autoScale: true
});
}
}
</script>
</body>
</html>