765 lines
26 KiB
JavaScript
765 lines
26 KiB
JavaScript
|
/**
|
|||
|
* 行业估值分析工具前端JS
|
|||
|
*/
|
|||
|
|
|||
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
// 获取DOM元素
|
|||
|
const industryForm = document.getElementById('industryForm');
|
|||
|
const industryNameSelect = document.getElementById('industryName');
|
|||
|
const startDateInput = document.getElementById('startDate');
|
|||
|
const metricSelect = document.getElementById('metric');
|
|||
|
const showCrowdingCheckbox = document.getElementById('showCrowding');
|
|||
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
|||
|
const resetBtn = document.getElementById('resetBtn');
|
|||
|
const loadingSpinner = document.getElementById('loadingSpinner');
|
|||
|
const resultCard = document.getElementById('resultCard');
|
|||
|
const resultTitle = document.getElementById('resultTitle');
|
|||
|
const errorAlert = document.getElementById('errorAlert');
|
|||
|
const errorMessage = document.getElementById('errorMessage');
|
|||
|
const percentileTable = document.getElementById('percentileTable');
|
|||
|
const crowdingStats = document.getElementById('crowdingStats');
|
|||
|
const crowdingTable = document.getElementById('crowdingTable');
|
|||
|
const industryStatsTable = document.getElementById('industryStatsTable');
|
|||
|
const crowdingChartRow = document.getElementById('crowdingChartRow');
|
|||
|
|
|||
|
// 定义图表实例
|
|||
|
let valuationChart = null;
|
|||
|
let crowdingChart = null;
|
|||
|
|
|||
|
// 初始化 - 加载行业列表
|
|||
|
loadIndustryList();
|
|||
|
|
|||
|
// 监听表单提交事件
|
|||
|
industryForm.addEventListener('submit', function(event) {
|
|||
|
event.preventDefault();
|
|||
|
analyzeIndustry();
|
|||
|
});
|
|||
|
|
|||
|
// 监听重置按钮点击事件
|
|||
|
resetBtn.addEventListener('click', function() {
|
|||
|
resetForm();
|
|||
|
});
|
|||
|
|
|||
|
// 拥挤度显示控制
|
|||
|
showCrowdingCheckbox.addEventListener('change', function() {
|
|||
|
if (crowdingStats.classList.contains('d-none')) {
|
|||
|
// 如果还没有数据,不做任何操作
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (this.checked) {
|
|||
|
// 显示拥挤度
|
|||
|
crowdingStats.classList.remove('d-none');
|
|||
|
crowdingChartRow.classList.remove('d-none');
|
|||
|
} else {
|
|||
|
// 隐藏拥挤度
|
|||
|
crowdingStats.classList.add('d-none');
|
|||
|
crowdingChartRow.classList.add('d-none');
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* 加载行业列表
|
|||
|
*/
|
|||
|
function loadIndustryList() {
|
|||
|
showLoading(true);
|
|||
|
|
|||
|
fetch('/api/industry/list')
|
|||
|
.then(response => {
|
|||
|
if (!response.ok) {
|
|||
|
return response.json().then(data => {
|
|||
|
throw new Error(data.message || '请求失败');
|
|||
|
});
|
|||
|
}
|
|||
|
return response.json();
|
|||
|
})
|
|||
|
.then(data => {
|
|||
|
if (data.status === 'success') {
|
|||
|
// 填充行业下拉列表
|
|||
|
populateIndustrySelect(data.data);
|
|||
|
} else {
|
|||
|
showError(data.message || '获取行业列表失败');
|
|||
|
}
|
|||
|
})
|
|||
|
.catch(error => {
|
|||
|
showError(error.message || '请求失败,请检查网络连接');
|
|||
|
})
|
|||
|
.finally(() => {
|
|||
|
showLoading(false);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 填充行业下拉列表
|
|||
|
*/
|
|||
|
function populateIndustrySelect(industries) {
|
|||
|
// 清空选项(保留第一个默认选项)
|
|||
|
industryNameSelect.innerHTML = '<option value="" selected disabled>请选择行业</option>';
|
|||
|
|
|||
|
// 排序行业列表(按名称)
|
|||
|
industries.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
|||
|
|
|||
|
// 添加行业选项
|
|||
|
industries.forEach(industry => {
|
|||
|
const option = document.createElement('option');
|
|||
|
option.value = industry.name;
|
|||
|
option.textContent = industry.name;
|
|||
|
industryNameSelect.appendChild(option);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 分析行业估值
|
|||
|
*/
|
|||
|
function analyzeIndustry() {
|
|||
|
// 显示加载中状态
|
|||
|
showLoading(true);
|
|||
|
|
|||
|
// 隐藏之前的结果和错误信息
|
|||
|
resultCard.classList.add('d-none');
|
|||
|
errorAlert.classList.add('d-none');
|
|||
|
|
|||
|
// 获取表单数据
|
|||
|
const industryName = industryNameSelect.value;
|
|||
|
const startDate = startDateInput.value;
|
|||
|
const metric = metricSelect.value;
|
|||
|
|
|||
|
// 构建请求URL
|
|||
|
let url = `/api/industry/analysis?industry_name=${encodeURIComponent(industryName)}&metric=${metric}`;
|
|||
|
|
|||
|
if (startDate) {
|
|||
|
url += `&start_date=${startDate}`;
|
|||
|
}
|
|||
|
|
|||
|
// 发送API请求
|
|||
|
fetch(url)
|
|||
|
.then(response => {
|
|||
|
if (!response.ok) {
|
|||
|
return response.json().then(data => {
|
|||
|
throw new Error(data.message || '请求失败');
|
|||
|
});
|
|||
|
}
|
|||
|
return response.json();
|
|||
|
})
|
|||
|
.then(data => {
|
|||
|
if (data.status === 'success') {
|
|||
|
// 渲染分析结果
|
|||
|
renderIndustryResults(data);
|
|||
|
} else {
|
|||
|
showError(data.message || '分析失败,请稍后再试');
|
|||
|
}
|
|||
|
})
|
|||
|
.catch(error => {
|
|||
|
showError(error.message || '请求失败,请检查网络连接');
|
|||
|
})
|
|||
|
.finally(() => {
|
|||
|
showLoading(false);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染行业分析结果
|
|||
|
*/
|
|||
|
function renderIndustryResults(data) {
|
|||
|
// 显示结果卡片(在渲染图表前确保容器可见)
|
|||
|
resultCard.classList.remove('d-none');
|
|||
|
|
|||
|
// 更新标题
|
|||
|
resultTitle.textContent = data.data.title.text;
|
|||
|
|
|||
|
// 渲染分位数数据表格
|
|||
|
renderPercentileTable(data.data.percentiles, data.data.yAxis[0].name);
|
|||
|
|
|||
|
// 渲染行业基本统计
|
|||
|
renderIndustryStats(data.data.percentiles);
|
|||
|
|
|||
|
// 渲染拥挤度数据(如果有)
|
|||
|
if (data.data.crowding) {
|
|||
|
renderCrowdingStats(data.data.crowding);
|
|||
|
|
|||
|
// 根据复选框状态决定是否显示拥挤度
|
|||
|
if (showCrowdingCheckbox.checked) {
|
|||
|
crowdingStats.classList.remove('d-none');
|
|||
|
crowdingChartRow.classList.remove('d-none');
|
|||
|
} else {
|
|||
|
crowdingStats.classList.add('d-none');
|
|||
|
crowdingChartRow.classList.add('d-none');
|
|||
|
}
|
|||
|
} else {
|
|||
|
crowdingStats.classList.add('d-none');
|
|||
|
crowdingChartRow.classList.add('d-none');
|
|||
|
}
|
|||
|
|
|||
|
// 渲染图表(等待DOM更新后)
|
|||
|
setTimeout(() => {
|
|||
|
renderValuationChart(data.data);
|
|||
|
|
|||
|
// 如果有拥挤度数据且复选框被选中,渲染拥挤度图表
|
|||
|
if (data.data.crowding && showCrowdingCheckbox.checked) {
|
|||
|
renderCrowdingChart(data.data);
|
|||
|
}
|
|||
|
|
|||
|
// 滚动到结果区域
|
|||
|
resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|||
|
}, 100);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染估值分位数ECharts图表
|
|||
|
*/
|
|||
|
function renderValuationChart(chartData) {
|
|||
|
// 初始化图表
|
|||
|
const chartContainer = document.getElementById('valuationChart');
|
|||
|
|
|||
|
// 确保容器可见
|
|||
|
resultCard.classList.remove('d-none');
|
|||
|
|
|||
|
// 如果已经存在图表实例,则销毁
|
|||
|
if (valuationChart) {
|
|||
|
valuationChart.dispose();
|
|||
|
}
|
|||
|
|
|||
|
// 创建新的图表实例
|
|||
|
valuationChart = echarts.init(chartContainer);
|
|||
|
|
|||
|
// 处理图例数据
|
|||
|
let legendData = chartData.legend.data.filter(item => !item.includes('拥挤度'));
|
|||
|
|
|||
|
// 处理系列数据
|
|||
|
let seriesData = chartData.series.filter(series =>
|
|||
|
!series.name.includes('拥挤度') && series.yAxisIndex !== 1
|
|||
|
);
|
|||
|
|
|||
|
// 去掉所有折线上的圆圈标记
|
|||
|
seriesData.forEach(series => {
|
|||
|
series.symbol = 'none'; // 不显示折线上的圆点标记
|
|||
|
});
|
|||
|
|
|||
|
// 检查数据有效性
|
|||
|
// 注意:现在所有图例都是存在的,因为我们使用行业平均值的历史统计生成它们
|
|||
|
// 不再需要检查q1_values, q3_values等是否存在
|
|||
|
|
|||
|
// 计算Y轴范围
|
|||
|
const percentiles = chartData.percentiles;
|
|||
|
const avgValues = chartData.valuation.avg_values.filter(v => v !== null && v !== undefined);
|
|||
|
|
|||
|
// 找出数据的最小值和最大值
|
|||
|
let minValue = percentiles.min;
|
|||
|
let maxValue = percentiles.max;
|
|||
|
|
|||
|
// 如果有avgValues,也考虑其最小值和最大值
|
|||
|
if (avgValues.length > 0) {
|
|||
|
const avgMin = Math.min(...avgValues);
|
|||
|
const avgMax = Math.max(...avgValues);
|
|||
|
minValue = Math.min(minValue, avgMin);
|
|||
|
maxValue = Math.max(maxValue, avgMax);
|
|||
|
}
|
|||
|
|
|||
|
// 计算Y轴范围,给出更好的视觉展示
|
|||
|
// 下限:取最小值的80%或最小值减去范围的20%,但不小于0
|
|||
|
// 上限:取最大值的120%或最大值加上范围的20%
|
|||
|
const range = maxValue - minValue;
|
|||
|
const padding = range * 0.2;
|
|||
|
let yMinValue = Math.max(0, minValue - padding);
|
|||
|
let yMaxValue = maxValue + padding;
|
|||
|
|
|||
|
// 如果最小值与最大值之间的差距太小,进一步扩大范围以便更好地展示
|
|||
|
if (range / maxValue < 0.1) { // 如果范围小于最大值的10%
|
|||
|
yMinValue = Math.max(0, minValue * 0.5); // 下限可以更低,但不低于0
|
|||
|
yMaxValue = maxValue * 1.3; // 上限可以更高
|
|||
|
}
|
|||
|
|
|||
|
// 设置图表选项
|
|||
|
const option = {
|
|||
|
title: {
|
|||
|
text: chartData.title.text,
|
|||
|
subtext: chartData.title.subtext,
|
|||
|
left: 'center',
|
|||
|
top: 5
|
|||
|
},
|
|||
|
tooltip: {
|
|||
|
trigger: 'axis',
|
|||
|
axisPointer: {
|
|||
|
type: 'cross',
|
|||
|
label: {
|
|||
|
backgroundColor: '#6a7985'
|
|||
|
}
|
|||
|
},
|
|||
|
formatter: function(params) {
|
|||
|
const date = params[0].axisValue;
|
|||
|
let result = `${date}<br/>`;
|
|||
|
|
|||
|
params.forEach(param => {
|
|||
|
const value = param.value;
|
|||
|
const color = param.color;
|
|||
|
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
|||
|
const name = param.seriesName;
|
|||
|
|
|||
|
if (name.includes('历史')) {
|
|||
|
// 对于历史统计线,使用固定值显示
|
|||
|
result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}<br/>`;
|
|||
|
} else {
|
|||
|
// 对于当前值,显示更多小数位
|
|||
|
result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}<br/>`;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
return result;
|
|||
|
}
|
|||
|
},
|
|||
|
legend: {
|
|||
|
data: legendData,
|
|||
|
top: 60
|
|||
|
},
|
|||
|
grid: chartData.grid[0],
|
|||
|
toolbox: chartData.toolbox,
|
|||
|
xAxis: chartData.xAxis[0],
|
|||
|
yAxis: {
|
|||
|
type: 'value',
|
|||
|
name: chartData.yAxis[0].name,
|
|||
|
min: yMinValue,
|
|||
|
max: yMaxValue,
|
|||
|
axisLabel: {
|
|||
|
formatter: function(value) { return value.toFixed(1); }
|
|||
|
},
|
|||
|
splitLine: {
|
|||
|
show: true,
|
|||
|
lineStyle: {
|
|||
|
type: 'dashed'
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
dataZoom: [
|
|||
|
{
|
|||
|
type: 'inside',
|
|||
|
start: 0,
|
|||
|
end: 100,
|
|||
|
xAxisIndex: [0]
|
|||
|
},
|
|||
|
{
|
|||
|
start: 0,
|
|||
|
end: 100,
|
|||
|
xAxisIndex: [0]
|
|||
|
}
|
|||
|
],
|
|||
|
series: seriesData
|
|||
|
};
|
|||
|
|
|||
|
// 设置图表选项
|
|||
|
valuationChart.setOption(option);
|
|||
|
|
|||
|
// 手动触发一次resize,确保图表正确渲染
|
|||
|
setTimeout(() => {
|
|||
|
valuationChart.resize();
|
|||
|
}, 50);
|
|||
|
|
|||
|
// 响应式调整
|
|||
|
window.addEventListener('resize', function() {
|
|||
|
if (valuationChart) {
|
|||
|
valuationChart.resize();
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染拥挤度ECharts图表
|
|||
|
*/
|
|||
|
function renderCrowdingChart(chartData) {
|
|||
|
// 初始化图表
|
|||
|
const chartContainer = document.getElementById('crowdingChart');
|
|||
|
|
|||
|
// 确保容器可见
|
|||
|
crowdingChartRow.classList.remove('d-none');
|
|||
|
|
|||
|
// 如果已经存在图表实例,则销毁
|
|||
|
if (crowdingChart) {
|
|||
|
crowdingChart.dispose();
|
|||
|
}
|
|||
|
|
|||
|
// 创建新的图表实例
|
|||
|
crowdingChart = echarts.init(chartContainer);
|
|||
|
|
|||
|
// 使用拥挤度的完整数据,不受其他时间筛选影响
|
|||
|
// 拥挤度数据在后端已经固定为3年数据
|
|||
|
const crowdingDates = chartData.crowding.dates;
|
|||
|
const crowdingPercentiles = chartData.crowding.percentiles;
|
|||
|
|
|||
|
// 筛选拥挤度相关的系列
|
|||
|
const crowdingSeries = {
|
|||
|
name: '行业交易拥挤度历史百分位',
|
|||
|
type: 'line',
|
|||
|
data: crowdingPercentiles,
|
|||
|
symbol: 'none', // 不显示折线上的圆点标记
|
|||
|
lineStyle: {width: 2, color: '#ff7f50'},
|
|||
|
areaStyle: {opacity: 0.2, color: '#ff7f50'},
|
|||
|
markLine: {
|
|||
|
data: [
|
|||
|
{name: "20%", yAxis: 20, lineStyle: {color: "#28a745", type: "dashed"}},
|
|||
|
{name: "80%", yAxis: 80, lineStyle: {color: "#dc3545", type: "dashed"}}
|
|||
|
]
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 获取拥挤度数据,用于计算Y轴范围
|
|||
|
// 使用百分位数值替代原始比例
|
|||
|
let percentileValues = crowdingPercentiles.filter(v => v !== null && v !== undefined);
|
|||
|
|
|||
|
// 计算Y轴范围
|
|||
|
let yMinValue = 0; // 默认最小值为0
|
|||
|
let yMaxValue = 100; // 默认最大值为100(百分比)
|
|||
|
|
|||
|
// 如果有实际数据,可以根据数据调整范围
|
|||
|
if (percentileValues.length > 0) {
|
|||
|
const minPercentile = Math.min(...percentileValues);
|
|||
|
const maxPercentile = Math.max(...percentileValues);
|
|||
|
|
|||
|
// 但拥挤度图表通常应该保持0-100的范围,所以只在必要时调整
|
|||
|
if (minPercentile > 20 && maxPercentile < 80) {
|
|||
|
// 如果数据都集中在20-80之间,可以适当缩小范围
|
|||
|
yMinValue = Math.max(0, Math.floor(minPercentile / 10) * 10 - 10); // 向下取整到最接近的10的倍数再减10
|
|||
|
yMaxValue = Math.min(100, Math.ceil(maxPercentile / 10) * 10 + 10); // 向上取整到最接近的10的倍数再加10
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 设置图表选项
|
|||
|
const option = {
|
|||
|
title: {
|
|||
|
text: '行业交易拥挤度分析',
|
|||
|
subtext: `当前拥挤度历史百分位: ${chartData.crowding.current_percentile.toFixed(2)}%`,
|
|||
|
left: 'center',
|
|||
|
top: 5
|
|||
|
},
|
|||
|
tooltip: {
|
|||
|
trigger: 'axis',
|
|||
|
axisPointer: {
|
|||
|
type: 'cross',
|
|||
|
label: {
|
|||
|
backgroundColor: '#6a7985'
|
|||
|
}
|
|||
|
},
|
|||
|
formatter: function(params) {
|
|||
|
const date = params[0].axisValue;
|
|||
|
let result = `${date}<br/>`;
|
|||
|
|
|||
|
params.forEach(param => {
|
|||
|
const value = param.value;
|
|||
|
const color = param.color;
|
|||
|
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
|||
|
|
|||
|
result += `${marker}${param.seriesName}: ${value ? value.toFixed(2) : 'N/A'}%<br/>`;
|
|||
|
});
|
|||
|
|
|||
|
return result;
|
|||
|
}
|
|||
|
},
|
|||
|
legend: {
|
|||
|
data: ['行业交易拥挤度历史百分位'],
|
|||
|
top: 60
|
|||
|
},
|
|||
|
grid: {
|
|||
|
left: '3%',
|
|||
|
right: '4%',
|
|||
|
bottom: '10%',
|
|||
|
top: '100',
|
|||
|
containLabel: true
|
|||
|
},
|
|||
|
xAxis: {
|
|||
|
type: 'category',
|
|||
|
boundaryGap: false,
|
|||
|
data: crowdingDates,
|
|||
|
axisLabel: {
|
|||
|
rotate: 45
|
|||
|
}
|
|||
|
},
|
|||
|
yAxis: {
|
|||
|
type: 'value',
|
|||
|
name: '历史百分位 (%)',
|
|||
|
min: yMinValue,
|
|||
|
max: yMaxValue,
|
|||
|
axisLine: {
|
|||
|
show: true
|
|||
|
},
|
|||
|
axisLabel: {
|
|||
|
formatter: function(value) { return value.toFixed(1) + '%'; }
|
|||
|
},
|
|||
|
splitLine: {
|
|||
|
show: true,
|
|||
|
lineStyle: {
|
|||
|
type: 'dashed'
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
dataZoom: [
|
|||
|
{
|
|||
|
type: 'inside',
|
|||
|
start: 0,
|
|||
|
end: 100
|
|||
|
},
|
|||
|
{
|
|||
|
start: 0,
|
|||
|
end: 100
|
|||
|
}
|
|||
|
],
|
|||
|
series: [crowdingSeries],
|
|||
|
visualMap: {
|
|||
|
show: false,
|
|||
|
dimension: 1,
|
|||
|
pieces: [
|
|||
|
{min: 0, max: 20, color: '#91cc75'}, // 绿色 - 不拥挤
|
|||
|
{min: 20, max: 40, color: '#91cc75'}, // 绿色 - 较不拥挤
|
|||
|
{min: 40, max: 60, color: '#fac858'}, // 黄色 - 中性
|
|||
|
{min: 60, max: 80, color: '#ee6666'}, // 红色 - 较为拥挤
|
|||
|
{min: 80, max: 100, color: '#ee6666'} // 红色 - 极度拥挤
|
|||
|
]
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 设置图表选项
|
|||
|
crowdingChart.setOption(option);
|
|||
|
|
|||
|
// 手动触发一次resize,确保图表正确渲染
|
|||
|
setTimeout(() => {
|
|||
|
crowdingChart.resize();
|
|||
|
}, 50);
|
|||
|
|
|||
|
// 响应式调整
|
|||
|
window.addEventListener('resize', function() {
|
|||
|
if (crowdingChart) {
|
|||
|
crowdingChart.resize();
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染分位数表格
|
|||
|
*/
|
|||
|
function renderPercentileTable(percentiles, metricName) {
|
|||
|
// 清空表格
|
|||
|
percentileTable.innerHTML = '';
|
|||
|
|
|||
|
// 格式化函数
|
|||
|
const formatValue = value => value ? value.toFixed(2) : 'N/A';
|
|||
|
|
|||
|
// 获取百分位类别
|
|||
|
const getPercentileClass = value => {
|
|||
|
if (value <= 33) return 'percent-low';
|
|||
|
if (value <= 66) return 'percent-medium';
|
|||
|
return 'percent-high';
|
|||
|
};
|
|||
|
|
|||
|
// 创建表格行
|
|||
|
const rows = [
|
|||
|
{
|
|||
|
label: '当前行业均值',
|
|||
|
value: formatValue(percentiles.current),
|
|||
|
class: 'current-value' // 添加特殊样式类
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史百分位',
|
|||
|
value: `${formatValue(percentiles.percentile)}%`,
|
|||
|
class: getPercentileClass(percentiles.percentile)
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史最小值',
|
|||
|
value: formatValue(percentiles.min),
|
|||
|
class: 'percent-low' // 绿色
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史最大值',
|
|||
|
value: formatValue(percentiles.max),
|
|||
|
class: 'percent-high' // 红色
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史均值',
|
|||
|
value: formatValue(percentiles.mean)
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史中位数',
|
|||
|
value: formatValue(percentiles.median)
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史第一四分位',
|
|||
|
value: formatValue(percentiles.q1),
|
|||
|
class: 'percent-low' // 绿色
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史第三四分位',
|
|||
|
value: formatValue(percentiles.q3),
|
|||
|
class: 'percent-high' // 红色
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
// 添加行到表格
|
|||
|
rows.forEach(row => {
|
|||
|
const tr = document.createElement('tr');
|
|||
|
|
|||
|
// 添加特殊类以突出当前值行
|
|||
|
if (row.label === '当前行业均值' || row.label === '历史百分位') {
|
|||
|
tr.classList.add('highlight-row');
|
|||
|
}
|
|||
|
|
|||
|
const tdLabel = document.createElement('td');
|
|||
|
tdLabel.textContent = row.label;
|
|||
|
tr.appendChild(tdLabel);
|
|||
|
|
|||
|
const tdValue = document.createElement('td');
|
|||
|
tdValue.textContent = row.value;
|
|||
|
|
|||
|
if (row.class) {
|
|||
|
tdValue.classList.add('percent-value', row.class);
|
|||
|
}
|
|||
|
|
|||
|
tr.appendChild(tdValue);
|
|||
|
percentileTable.appendChild(tr);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染行业统计数据
|
|||
|
*/
|
|||
|
function renderIndustryStats(percentiles) {
|
|||
|
// 清空表格
|
|||
|
industryStatsTable.innerHTML = '';
|
|||
|
|
|||
|
// 获取用户选择的开始日期,如果没有选择则显示"最近三年"
|
|||
|
const userStartDate = startDateInput.value ? startDateInput.value : "最近三年";
|
|||
|
|
|||
|
// 创建表格行
|
|||
|
const rows = [
|
|||
|
{ label: '行业成份股数量', value: percentiles.stock_count + '只' },
|
|||
|
{ label: '分析开始日期', value: userStartDate }
|
|||
|
];
|
|||
|
|
|||
|
// 添加行到表格
|
|||
|
rows.forEach(row => {
|
|||
|
const tr = document.createElement('tr');
|
|||
|
|
|||
|
const tdLabel = document.createElement('td');
|
|||
|
tdLabel.textContent = row.label;
|
|||
|
tr.appendChild(tdLabel);
|
|||
|
|
|||
|
const tdValue = document.createElement('td');
|
|||
|
tdValue.textContent = row.value;
|
|||
|
tr.appendChild(tdValue);
|
|||
|
|
|||
|
industryStatsTable.appendChild(tr);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 渲染拥挤度统计数据
|
|||
|
*/
|
|||
|
function renderCrowdingStats(crowding) {
|
|||
|
// 清空表格
|
|||
|
crowdingTable.innerHTML = '';
|
|||
|
|
|||
|
// 获取拥挤度级别的颜色类
|
|||
|
const getCrowdingLevelClass = level => {
|
|||
|
switch(level) {
|
|||
|
case '不拥挤':
|
|||
|
case '较不拥挤':
|
|||
|
return 'percent-low'; // 绿色
|
|||
|
case '中性':
|
|||
|
return 'percent-medium'; // 黄色
|
|||
|
case '较为拥挤':
|
|||
|
case '极度拥挤':
|
|||
|
return 'percent-high'; // 红色
|
|||
|
default:
|
|||
|
return '';
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 创建表格行
|
|||
|
const rows = [
|
|||
|
{
|
|||
|
label: '当前成交占比',
|
|||
|
value: crowding.current_ratio.toFixed(2) + '%',
|
|||
|
tooltip: '行业成交额占全市场成交额的百分比'
|
|||
|
},
|
|||
|
{
|
|||
|
label: '历史百分位',
|
|||
|
value: crowding.current_percentile.toFixed(2) + '%',
|
|||
|
class: getCrowdingLevelClass(crowding.level),
|
|||
|
tooltip: '当前行业成交占比在历史分布中的位置'
|
|||
|
},
|
|||
|
{
|
|||
|
label: '拥挤度评级',
|
|||
|
value: crowding.level,
|
|||
|
class: getCrowdingLevelClass(crowding.level),
|
|||
|
tooltip: '根据历史百分位划分的拥挤度等级'
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
// 添加行到表格
|
|||
|
rows.forEach(row => {
|
|||
|
const tr = document.createElement('tr');
|
|||
|
|
|||
|
const tdLabel = document.createElement('td');
|
|||
|
tdLabel.textContent = row.label;
|
|||
|
|
|||
|
// 添加工具提示
|
|||
|
if (row.tooltip) {
|
|||
|
tdLabel.title = row.tooltip;
|
|||
|
tdLabel.style.cursor = 'help';
|
|||
|
}
|
|||
|
|
|||
|
tr.appendChild(tdLabel);
|
|||
|
|
|||
|
const tdValue = document.createElement('td');
|
|||
|
tdValue.textContent = row.value;
|
|||
|
|
|||
|
if (row.class) {
|
|||
|
tdValue.classList.add('percent-value', row.class);
|
|||
|
}
|
|||
|
|
|||
|
tr.appendChild(tdValue);
|
|||
|
crowdingTable.appendChild(tr);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 显示错误信息
|
|||
|
*/
|
|||
|
function showError(message) {
|
|||
|
errorMessage.textContent = message;
|
|||
|
errorAlert.classList.remove('d-none');
|
|||
|
|
|||
|
// 滚动到错误信息
|
|||
|
errorAlert.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 重置表单
|
|||
|
*/
|
|||
|
function resetForm() {
|
|||
|
industryForm.reset();
|
|||
|
|
|||
|
// 隐藏结果和错误信息
|
|||
|
resultCard.classList.add('d-none');
|
|||
|
errorAlert.classList.add('d-none');
|
|||
|
|
|||
|
// 销毁图表
|
|||
|
if (valuationChart) {
|
|||
|
valuationChart.dispose();
|
|||
|
valuationChart = null;
|
|||
|
}
|
|||
|
|
|||
|
if (crowdingChart) {
|
|||
|
crowdingChart.dispose();
|
|||
|
crowdingChart = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 显示/隐藏加载状态
|
|||
|
*/
|
|||
|
function showLoading(isLoading) {
|
|||
|
if (isLoading) {
|
|||
|
loadingSpinner.classList.remove('d-none');
|
|||
|
analyzeBtn.disabled = true;
|
|||
|
} else {
|
|||
|
loadingSpinner.classList.add('d-none');
|
|||
|
analyzeBtn.disabled = false;
|
|||
|
}
|
|||
|
}
|
|||
|
});
|