| {% extends "layout.html" %}
|
|
|
| {% block title %}行业分析 - 智能分析系统{% endblock %}
|
|
|
| {% block content %}
|
| <div class="container-fluid py-3">
|
| <div id="alerts-container"></div>
|
|
|
| <div class="row mb-3">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">行业资金流向分析</h5>
|
| </div>
|
| <div class="card-body py-2">
|
| <form id="industry-form" class="row g-2">
|
| <div class="col-md-3">
|
| <div class="input-group input-group-sm">
|
| <span class="input-group-text">周期</span>
|
| <select class="form-select" id="fund-flow-period">
|
| <option value="即时" selected>即时</option>
|
| <option value="3日排行">3日排行</option>
|
| <option value="5日排行">5日排行</option>
|
| <option value="10日排行">10日排行</option>
|
| <option value="20日排行">20日排行</option>
|
| </select>
|
| </div>
|
| </div>
|
| <div class="col-md-3">
|
| <div class="input-group input-group-sm">
|
| <span class="input-group-text">行业</span>
|
| <select class="form-select" id="industry-selector">
|
| <option value="">-- 选择行业 --</option>
|
|
|
| </select>
|
| </div>
|
| </div>
|
| <div class="col-md-2">
|
| <button type="submit" class="btn btn-primary btn-sm w-100">
|
| <i class="fas fa-search"></i> 分析
|
| </button>
|
| </div>
|
| </form>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div id="loading-panel" class="text-center py-5" style="display: none;">
|
| <div class="spinner-border text-primary" role="status">
|
| <span class="visually-hidden">Loading...</span>
|
| </div>
|
| <p class="mt-3 mb-0">正在获取行业数据...</p>
|
| </div>
|
|
|
|
|
| <div id="industry-overview" class="row g-3 mb-3" style="display: none;">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-2 d-flex justify-content-between align-items-center">
|
| <h5 class="mb-0">行业资金流向概览</h5>
|
| <div class="d-flex">
|
| <span id="period-badge" class="badge bg-primary ms-2">即时</span>
|
| <button class="btn btn-sm btn-outline-primary ms-2" id="export-btn">
|
| <i class="fas fa-download"></i> 导出数据
|
| </button>
|
| </div>
|
| </div>
|
| <div class="card-body p-0">
|
| <div class="table-responsive">
|
| <table class="table table-sm table-striped table-hover mb-0">
|
| <thead>
|
| <tr>
|
| <th>序号</th>
|
| <th>行业</th>
|
| <th>行业指数</th>
|
| <th>涨跌幅</th>
|
| <th>流入资金(亿)</th>
|
| <th>流出资金(亿)</th>
|
| <th>净额(亿)</th>
|
| <th>公司家数</th>
|
| <th>领涨股</th>
|
| <th>操作</th>
|
| </tr>
|
| </thead>
|
| <tbody id="industry-table">
|
|
|
| </tbody>
|
| </table>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="industry-detail" class="row g-3 mb-3" style="display: none;">
|
| <div class="col-md-6">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 id="industry-name" class="mb-0">行业详情</h5>
|
| </div>
|
| <div class="card-body">
|
| <div class="row">
|
| <div class="col-md-6">
|
| <h6>行业概况</h6>
|
| <p><span class="text-muted">行业指数:</span> <span id="industry-index" class="fw-bold"></span></p>
|
| <p><span class="text-muted">涨跌幅:</span> <span id="industry-change" class="fw-bold"></span></p>
|
| <p><span class="text-muted">公司家数:</span> <span id="industry-company-count" class="fw-bold"></span></p>
|
| </div>
|
| <div class="col-md-6">
|
| <h6>资金流向</h6>
|
| <p><span class="text-muted">流入资金:</span> <span id="industry-inflow" class="fw-bold"></span></p>
|
| <p><span class="text-muted">流出资金:</span> <span id="industry-outflow" class="fw-bold"></span></p>
|
| <p><span class="text-muted">净额:</span> <span id="industry-net-flow" class="fw-bold"></span></p>
|
| </div>
|
| </div>
|
| <div class="mt-3">
|
| <div id="industry-flow-chart" style="height: 200px;"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="col-md-6">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">行业评分</h5>
|
| </div>
|
| <div class="card-body">
|
| <div class="row">
|
| <div class="col-md-5">
|
| <div id="industry-score-chart" style="height: 150px;"></div>
|
| <h4 id="industry-score" class="text-center mt-2">--</h4>
|
| <p class="text-muted text-center">综合评分</p>
|
| </div>
|
| <div class="col-md-7">
|
| <div class="mb-3">
|
| <div class="d-flex justify-content-between mb-1">
|
| <span>技术面</span>
|
| <span id="technical-score">--/40</span>
|
| </div>
|
| <div class="progress">
|
| <div id="technical-progress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
|
| </div>
|
| </div>
|
| <div class="mb-3">
|
| <div class="d-flex justify-content-between mb-1">
|
| <span>基本面</span>
|
| <span id="fundamental-score">--/40</span>
|
| </div>
|
| <div class="progress">
|
| <div id="fundamental-progress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
| </div>
|
| </div>
|
| <div class="mb-3">
|
| <div class="d-flex justify-content-between mb-1">
|
| <span>资金面</span>
|
| <span id="capital-flow-score">--/20</span>
|
| </div>
|
| <div class="progress">
|
| <div id="capital-flow-progress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="mt-3">
|
| <h6>投资建议</h6>
|
| <p id="industry-recommendation" class="mb-0"></p>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="industry-stocks" class="row g-3 mb-3" style="display: none;">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">行业成分股表现</h5>
|
| </div>
|
| <div class="card-body p-0">
|
| <div class="table-responsive">
|
| <table class="table table-sm table-striped table-hover mb-0">
|
| <thead>
|
| <tr>
|
| <th>代码</th>
|
| <th>名称</th>
|
| <th>最新价</th>
|
| <th>涨跌幅</th>
|
| <th>成交量</th>
|
| <th>成交额(万)</th>
|
| <th>换手率</th>
|
| <th>评分</th>
|
| <th>操作</th>
|
| </tr>
|
| </thead>
|
| <tbody id="industry-stocks-table">
|
|
|
| </tbody>
|
| </table>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="row g-3 mb-3">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">行业对比分析</h5>
|
| </div>
|
| <div class="card-body">
|
| <ul class="nav nav-tabs" id="industry-compare-tabs" role="tablist">
|
| <li class="nav-item" role="presentation">
|
| <button class="nav-link active" id="fund-flow-tab" data-bs-toggle="tab" data-bs-target="#fund-flow" type="button" role="tab" aria-controls="fund-flow" aria-selected="true">资金流向</button>
|
| </li>
|
| <li class="nav-item" role="presentation">
|
| <button class="nav-link" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance" type="button" role="tab" aria-controls="performance" aria-selected="false">行业涨跌幅</button>
|
| </li>
|
| </ul>
|
| <div class="tab-content mt-3" id="industry-compare-tabs-content">
|
| <div class="tab-pane fade show active" id="fund-flow" role="tabpanel" aria-labelledby="fund-flow-tab">
|
| <div class="row">
|
| <div class="col-md-6">
|
| <h6>资金净流入前10行业</h6>
|
| <div id="top-inflow-chart" style="height: 300px;"></div>
|
| </div>
|
| <div class="col-md-6">
|
| <h6>资金净流出前10行业</h6>
|
| <div id="top-outflow-chart" style="height: 300px;"></div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="tab-pane fade" id="performance" role="tabpanel" aria-labelledby="performance-tab">
|
| <div class="row">
|
| <div class="col-md-6">
|
| <h6>涨幅前10行业</h6>
|
| <div id="top-gainers-chart" style="height: 300px;"></div>
|
| </div>
|
| <div class="col-md-6">
|
| <h6>跌幅前10行业</h6>
|
| <div id="top-losers-chart" style="height: 300px;"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| {% endblock %}
|
|
|
| {% block scripts %}
|
| <script>
|
|
|
| $(document).ready(function() {
|
|
|
| loadIndustryFundFlow();
|
|
|
| $('#industry-form').submit(function(e) {
|
| e.preventDefault();
|
|
|
|
|
| const industry = $('#industry-selector').val();
|
|
|
| if (!industry) {
|
| showError('请选择行业名称');
|
| return;
|
| }
|
|
|
|
|
| loadIndustryDetail(industry);
|
| });
|
|
|
|
|
| $('#fund-flow-period').change(function() {
|
| const period = $(this).val();
|
| loadIndustryFundFlow(period);
|
| });
|
|
|
|
|
| $('#export-btn').click(function() {
|
| exportToCSV();
|
| });
|
| });
|
|
|
| function analyzeIndustry(industry) {
|
| $('#loading-panel').show();
|
| $('#industry-result').hide();
|
|
|
|
|
| $.ajax({
|
| url: `/api/industry_detail?industry=${encodeURIComponent(industry)}`,
|
| type: 'GET',
|
| success: function(industryDetail) {
|
| console.log("Industry detail loaded successfully:", industryDetail);
|
|
|
|
|
| $.ajax({
|
| url: `/api/industry_stocks?industry=${encodeURIComponent(industry)}`,
|
| type: 'GET',
|
| success: function(stocksResponse) {
|
| console.log("Industry stocks loaded successfully:", stocksResponse);
|
|
|
| $('#loading-panel').hide();
|
|
|
|
|
| renderIndustryDetail(industryDetail);
|
| renderIndustryStocks(stocksResponse);
|
|
|
| $('#industry-detail').show();
|
| $('#industry-stocks').show();
|
| },
|
| error: function(xhr, status, error) {
|
| $('#loading-panel').hide();
|
| console.error("Error loading industry stocks:", error);
|
| showError('获取行业成分股失败: ' + (xhr.responseJSON?.error || error));
|
| }
|
| });
|
| },
|
| error: function(xhr, status, error) {
|
| $('#loading-panel').hide();
|
| console.error("Error loading industry detail:", error);
|
| showError('获取行业详情失败: ' + (xhr.responseJSON?.error || error));
|
| }
|
| });
|
| }
|
|
|
|
|
| function loadIndustryFundFlow(period = '即时') {
|
| $('#loading-panel').show();
|
| $('#industry-overview').hide();
|
| $('#industry-detail').hide();
|
| $('#industry-stocks').hide();
|
|
|
| $.ajax({
|
| url: `/api/industry_fund_flow?symbol=${encodeURIComponent(period)}`,
|
| type: 'GET',
|
| dataType: 'json',
|
| success: function(response) {
|
| if (Array.isArray(response) && response.length > 0) {
|
| renderIndustryFundFlow(response, period);
|
| populateIndustrySelector(response);
|
|
|
|
|
| loadIndustryCompare();
|
|
|
| $('#loading-panel').hide();
|
| $('#industry-overview').show();
|
| } else {
|
| showError('获取行业资金流向数据失败:返回数据为空');
|
| $('#loading-panel').hide();
|
| }
|
| },
|
| error: function(xhr, status, error) {
|
| $('#loading-panel').hide();
|
| let errorMsg = '获取行业资金流向数据失败';
|
| if (xhr.responseJSON && xhr.responseJSON.error) {
|
| errorMsg += ': ' + xhr.responseJSON.error;
|
| } else if (error) {
|
| errorMsg += ': ' + error;
|
| }
|
| showError(errorMsg);
|
| }
|
| });
|
| }
|
|
|
|
|
|
|
| function loadIndustryDetail(industry) {
|
| console.log(`Loading industry detail for: ${industry}`);
|
| $('#loading-panel').show();
|
| $('#industry-overview').hide();
|
| $('#industry-detail').hide();
|
| $('#industry-stocks').hide();
|
|
|
|
|
| $.when(
|
|
|
| $.ajax({
|
| url: `/api/industry_detail?industry=${encodeURIComponent(industry)}`,
|
| type: 'GET',
|
| dataType: 'json'
|
| }),
|
|
|
| $.ajax({
|
| url: `/api/industry_stocks?industry=${encodeURIComponent(industry)}`,
|
| type: 'GET',
|
| dataType: 'json'
|
| })
|
| ).done(function(detailResponse, stocksResponse) {
|
|
|
| const industryData = detailResponse[0];
|
|
|
|
|
| const stocksData = stocksResponse[0];
|
|
|
| console.log("Industry detail loaded:", industryData);
|
| console.log("Industry stocks loaded:", stocksData);
|
|
|
| renderIndustryDetail(industryData);
|
| renderIndustryStocks(stocksData);
|
|
|
| $('#loading-panel').hide();
|
| $('#industry-detail').show();
|
| $('#industry-stocks').show();
|
| }).fail(function(jqXHR, textStatus, errorThrown) {
|
| $('#loading-panel').hide();
|
| console.error("Error loading industry data:", textStatus, errorThrown);
|
| let errorMsg = '获取行业数据失败';
|
| try {
|
| if (jqXHR.responseJSON && jqXHR.responseJSON.error) {
|
| errorMsg += ': ' + jqXHR.responseJSON.error;
|
| } else if (errorThrown) {
|
| errorMsg += ': ' + errorThrown;
|
| }
|
| } catch (e) {
|
| console.error("Error parsing error response:", e);
|
| }
|
| showError(errorMsg);
|
| });
|
| }
|
|
|
|
|
| function loadIndustryCompare() {
|
| $.ajax({
|
| url: '/api/industry_compare',
|
| type: 'GET',
|
| dataType: 'json',
|
| success: function(response) {
|
| try {
|
| if (response && response.results) {
|
|
|
| const sortedByNetFlow = [...response.results]
|
| .filter(item => item.netFlow !== undefined)
|
| .sort((a, b) => parseFloat(b.netFlow || 0) - parseFloat(a.netFlow || 0));
|
|
|
|
|
| const sortedByChange = [...response.results]
|
| .filter(item => item.change !== undefined)
|
| .sort((a, b) => parseFloat(b.change || 0) - parseFloat(a.change || 0));
|
|
|
|
|
| const topInflow = sortedByNetFlow.slice(0, 10);
|
| renderBarChart('top-inflow-chart',
|
| topInflow.map(item => item.industry),
|
| topInflow.map(item => parseFloat(item.netFlow || 0)),
|
| '资金净流入(亿)',
|
| '#00E396');
|
|
|
|
|
| const bottomInflow = [...sortedByNetFlow].reverse().slice(0, 10);
|
| renderBarChart('top-outflow-chart',
|
| bottomInflow.map(item => item.industry),
|
| bottomInflow.map(item => Math.abs(parseFloat(item.netFlow || 0))),
|
| '资金净流出(亿)',
|
| '#FF4560');
|
|
|
|
|
| const topGainers = sortedByChange.slice(0, 10);
|
| renderBarChart('top-gainers-chart',
|
| topGainers.map(item => item.industry),
|
| topGainers.map(item => parseFloat(item.change || 0)),
|
| '涨幅(%)',
|
| '#00E396');
|
|
|
|
|
| const topLosers = [...sortedByChange].reverse().slice(0, 10);
|
| renderBarChart('top-losers-chart',
|
| topLosers.map(item => item.industry),
|
| topLosers.map(item => Math.abs(parseFloat(item.change || 0))),
|
| '跌幅(%)',
|
| '#FF4560');
|
| }
|
| } catch (e) {
|
| console.error("Error processing industry comparison data:", e);
|
| }
|
| },
|
| error: function(xhr, status, error) {
|
| console.error('获取行业对比数据失败:', error);
|
| }
|
| });
|
| }
|
|
|
|
|
| function renderIndustryFundFlow(data, period) {
|
| $('#period-badge').text(period);
|
|
|
| let html = '';
|
| if (data.length === 0) {
|
| html = '<tr><td colspan="10" class="text-center">暂无数据</td></tr>';
|
| } else {
|
| data.forEach((item, index) => {
|
| const changeClass = parseFloat(item.change) >= 0 ? 'trend-up' : 'trend-down';
|
| const changeIcon = parseFloat(item.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
|
|
|
| const netFlowClass = parseFloat(item.netFlow) >= 0 ? 'trend-up' : 'trend-down';
|
| const netFlowIcon = parseFloat(item.netFlow) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
|
|
|
| html += `
|
| <tr>
|
| <td>${item.rank}</td>
|
| <td>
|
| <a href="javascript:void(0)" onclick="loadIndustryDetail('${item.industry}')" class="industry-link">
|
| ${item.industry}
|
| </a>
|
| </td>
|
| <td>${formatNumber(item.index, 2)}</td>
|
| <td class="${changeClass}">${changeIcon} ${item.change}%</td>
|
| <td>${formatNumber(item.inflow, 2)}</td>
|
| <td>${formatNumber(item.outflow, 2)}</td>
|
| <td class="${netFlowClass}">${netFlowIcon} ${formatNumber(item.netFlow, 2)}</td>
|
| <td>${item.companyCount}</td>
|
| <td>${item.leadingStock || '-'}</td>
|
| <td>
|
| <button class="btn btn-sm btn-outline-primary" onclick="loadIndustryDetail('${item.industry}')">
|
| <i class="fas fa-search"></i>
|
| </button>
|
| </td>
|
| </tr>
|
| `;
|
| });
|
| }
|
|
|
| $('#industry-table').html(html);
|
| }
|
|
|
|
|
| function renderIndustryDetail(data) {
|
| if (!data) {
|
| console.error("renderIndustryDetail: No data provided");
|
| return;
|
| }
|
|
|
| console.log("Rendering industry detail:", data);
|
|
|
|
|
| $('#industry-name').text(data.industry);
|
|
|
|
|
| const scoreClass = getScoreColorClass(data.score);
|
| $('#industry-score').text(data.score).removeClass().addClass(scoreClass);
|
|
|
|
|
| const technicalScore = Math.round(data.score * 0.4);
|
| const fundamentalScore = Math.round(data.score * 0.4);
|
| const capitalFlowScore = Math.round(data.score * 0.2);
|
|
|
| $('#technical-score').text(`${technicalScore}/40`);
|
| $('#fundamental-score').text(`${fundamentalScore}/40`);
|
| $('#capital-flow-score').text(`${capitalFlowScore}/20`);
|
|
|
| $('#technical-progress').css('width', `${technicalScore / 40 * 100}%`);
|
| $('#fundamental-progress').css('width', `${fundamentalScore / 40 * 100}%`);
|
| $('#capital-flow-progress').css('width', `${capitalFlowScore / 20 * 100}%`);
|
|
|
|
|
| $('#industry-index').text(formatNumber(data.index, 2));
|
| $('#industry-company-count').text(data.companyCount);
|
|
|
|
|
| const changeClass = parseFloat(data.change) >= 0 ? 'trend-up' : 'trend-down';
|
| const changeIcon = parseFloat(data.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
|
| $('#industry-change').html(`<span class="${changeClass}">${changeIcon} ${data.change}%</span>`);
|
|
|
|
|
| $('#industry-inflow').text(formatNumber(data.inflow, 2) + ' 亿');
|
| $('#industry-outflow').text(formatNumber(data.outflow, 2) + ' 亿');
|
|
|
| const netFlowClass = parseFloat(data.netFlow) >= 0 ? 'trend-up' : 'trend-down';
|
| const netFlowIcon = parseFloat(data.netFlow) >= 0 ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
|
| $('#industry-net-flow').html(`<span class="${netFlowClass}">${netFlowIcon} ${formatNumber(data.netFlow, 2)} 亿</span>`);
|
|
|
|
|
| $('#industry-recommendation').text(data.recommendation);
|
|
|
|
|
| renderIndustryScoreChart(data.score);
|
|
|
|
|
| renderIndustryFlowChart(data.flowHistory);
|
| }
|
|
|
|
|
|
|
| function renderIndustryStocks(data) {
|
| if (!data) {
|
| console.error("renderIndustryStocks: No data provided");
|
| return;
|
| }
|
|
|
| console.log("Rendering industry stocks:", data);
|
|
|
| let html = '';
|
|
|
| if (!Array.isArray(data) || data.length === 0) {
|
| html = '<tr><td colspan="9" class="text-center">暂无成分股数据</td></tr>';
|
| } else {
|
| data.forEach(stock => {
|
| const changeClass = parseFloat(stock.change) >= 0 ? 'trend-up' : 'trend-down';
|
| const changeIcon = parseFloat(stock.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
|
|
|
| html += `
|
| <tr>
|
| <td>${stock.code}</td>
|
| <td>${stock.name}</td>
|
| <td>${formatNumber(stock.price, 2)}</td>
|
| <td class="${changeClass}">${changeIcon} ${formatNumber(stock.change, 2)}%</td>
|
| <td>${formatNumber(stock.volume, 0)}</td>
|
| <td>${formatMoney(stock.turnover)}</td>
|
| <td>${formatNumber(stock.turnover_rate || stock.turnoverRate, 2)}%</td>
|
| <td>${stock.score ? formatNumber(stock.score, 0) : '-'}</td>
|
| <td>
|
| <a href="/stock_detail/${stock.code}" class="btn btn-sm btn-outline-primary">
|
| <i class="fas fa-chart-line"></i>
|
| </a>
|
| </td>
|
| </tr>
|
| `;
|
| });
|
| }
|
|
|
| $('#industry-stocks-table').html(html);
|
| }
|
|
|
| function renderCapitalFlowChart(flowHistory) {
|
|
|
|
|
| if (!flowHistory || !Array.isArray(flowHistory) || flowHistory.length === 0) {
|
|
|
| document.querySelector("#industry-flow-chart").innerHTML =
|
| '<div class="text-center text-muted py-5">暂无资金流向历史数据</div>';
|
| return;
|
| }
|
|
|
| const dates = flowHistory.map(item => item.date);
|
| const netFlows = flowHistory.map(item => parseFloat(item.netFlow));
|
| const changes = flowHistory.map(item => parseFloat(item.change));
|
|
|
|
|
| if (dates.length === 0 || netFlows.length === 0 || changes.length === 0) {
|
| document.querySelector("#industry-flow-chart").innerHTML =
|
| '<div class="text-center text-muted py-5">资金流向数据格式不正确</div>';
|
| return;
|
| }
|
|
|
| const options = {
|
| series: [
|
| {
|
| name: '净流入(亿)',
|
| type: 'column',
|
| data: netFlows
|
| },
|
| {
|
| name: '涨跌幅(%)',
|
| type: 'line',
|
| data: changes
|
| }
|
| ],
|
| chart: {
|
| height: 265,
|
| type: 'line',
|
| toolbar: {
|
| show: false
|
| }
|
| },
|
| plotOptions: {
|
| bar: {
|
| borderRadius: 2,
|
| dataLabels: {
|
| position: 'top'
|
| }
|
| }
|
| },
|
| dataLabels: {
|
| enabled: false
|
| },
|
| stroke: {
|
| width: [0, 3]
|
| },
|
| colors: ['#0d6efd', '#dc3545'],
|
| xaxis: {
|
| categories: dates,
|
| labels: {
|
| formatter: function(value) {
|
| return value.slice(5);
|
| }
|
| }
|
| },
|
| yaxis: [
|
| {
|
| title: {
|
| text: '净流入(亿)',
|
| style: {
|
| fontSize: '12px'
|
| }
|
| },
|
| labels: {
|
| formatter: function(val) {
|
| return val.toFixed(2);
|
| }
|
| }
|
| },
|
| {
|
| opposite: true,
|
| title: {
|
| text: '涨跌幅(%)',
|
| style: {
|
| fontSize: '12px'
|
| }
|
| },
|
| labels: {
|
| formatter: function(val) {
|
| return val.toFixed(2);
|
| }
|
| }
|
| }
|
| ],
|
| tooltip: {
|
| shared: true,
|
| intersect: false,
|
| y: {
|
| formatter: function(value, { seriesIndex }) {
|
| if (seriesIndex === 0) {
|
| return value.toFixed(2) + ' 亿';
|
| }
|
| return value.toFixed(2) + '%';
|
| }
|
| }
|
| },
|
| legend: {
|
| position: 'top'
|
| }
|
| };
|
|
|
|
|
| document.querySelector("#industry-flow-chart").innerHTML = '';
|
| const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
|
| chart.render();
|
| }
|
|
|
|
|
| function populateIndustrySelector(data) {
|
| let options = '<option value="">-- 选择行业 --</option>';
|
| const industries = data.map(item => item.industry);
|
|
|
| industries.forEach(industry => {
|
| options += `<option value="${industry}">${industry}</option>`;
|
| });
|
|
|
| $('#industry-selector').html(options);
|
| }
|
|
|
|
|
| function getScoreColorClass(score) {
|
| if (score >= 80) return 'badge rounded-pill bg-success';
|
| if (score >= 60) return 'badge rounded-pill bg-primary';
|
| if (score >= 40) return 'badge rounded-pill bg-warning text-dark';
|
| return 'badge rounded-pill bg-danger';
|
| }
|
|
|
|
|
| function getScoreColor(score) {
|
| if (score >= 80) return '#28a745';
|
| if (score >= 60) return '#007bff';
|
| if (score >= 40) return '#ffc107';
|
| return '#dc3545';
|
| }
|
|
|
|
|
| function formatMoney(value) {
|
| if (value === undefined || value === null) {
|
| return '--';
|
| }
|
|
|
| value = parseFloat(value);
|
| if (isNaN(value)) {
|
| return '--';
|
| }
|
|
|
| if (value >= 100000000) {
|
| return (value / 100000000).toFixed(2) + ' 亿';
|
| } else if (value >= 10000) {
|
| return (value / 10000).toFixed(2) + ' 万';
|
| } else {
|
| return value.toFixed(2);
|
| }
|
| }
|
|
|
|
|
| function renderIndustryFlowChart(data) {
|
| const options = {
|
| series: [
|
| {
|
| name: '流入资金',
|
| data: data.flowHistory.map(item => item.inflow)
|
| },
|
| {
|
| name: '流出资金',
|
| data: data.flowHistory.map(item => item.outflow)
|
| },
|
| {
|
| name: '净流入',
|
| data: data.flowHistory.map(item => item.netFlow)
|
| }
|
| ],
|
| chart: {
|
| type: 'bar',
|
| height: 200,
|
| toolbar: {
|
| show: false
|
| }
|
| },
|
| plotOptions: {
|
| bar: {
|
| horizontal: false,
|
| columnWidth: '55%',
|
| endingShape: 'rounded'
|
| },
|
| },
|
| dataLabels: {
|
| enabled: false
|
| },
|
| stroke: {
|
| show: true,
|
| width: 2,
|
| colors: ['transparent']
|
| },
|
| xaxis: {
|
| categories: data.flowHistory.map(item => item.date)
|
| },
|
| yaxis: {
|
| title: {
|
| text: '亿元'
|
| }
|
| },
|
| fill: {
|
| opacity: 1
|
| },
|
| tooltip: {
|
| y: {
|
| formatter: function(val) {
|
| return val + " 亿元";
|
| }
|
| }
|
| },
|
| colors: ['#00E396', '#FF4560', '#008FFB']
|
| };
|
|
|
| const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
|
| chart.render();
|
| }
|
|
|
|
|
| function renderIndustryScoreChart(score) {
|
| const options = {
|
| series: [score],
|
| chart: {
|
| height: 150,
|
| type: 'radialBar',
|
| },
|
| plotOptions: {
|
| radialBar: {
|
| hollow: {
|
| size: '70%',
|
| },
|
| dataLabels: {
|
| show: false
|
| }
|
| }
|
| },
|
| colors: [getScoreColor(score)],
|
| stroke: {
|
| lineCap: 'round'
|
| }
|
| };
|
|
|
|
|
| $('#industry-score-chart').empty();
|
| const chart = new ApexCharts(document.querySelector("#industry-score-chart"), options);
|
| chart.render();
|
| }
|
|
|
|
|
| function renderIndustryFlowChart(flowHistory) {
|
| if (!flowHistory || !Array.isArray(flowHistory) || flowHistory.length === 0) {
|
| console.error("renderIndustryFlowChart: Invalid flow history data");
|
| return;
|
| }
|
|
|
| console.log("Rendering flow chart with data:", flowHistory);
|
|
|
| const dates = flowHistory.map(item => item.date);
|
| const netFlows = flowHistory.map(item => parseFloat(item.netFlow));
|
| const changes = flowHistory.map(item => parseFloat(item.change));
|
|
|
| const options = {
|
| series: [
|
| {
|
| name: '净流入(亿)',
|
| type: 'column',
|
| data: netFlows
|
| },
|
| {
|
| name: '涨跌幅(%)',
|
| type: 'line',
|
| data: changes
|
| }
|
| ],
|
| chart: {
|
| height: 200,
|
| type: 'line',
|
| toolbar: {
|
| show: false
|
| }
|
| },
|
| plotOptions: {
|
| bar: {
|
| borderRadius: 2,
|
| dataLabels: {
|
| position: 'top'
|
| }
|
| }
|
| },
|
| dataLabels: {
|
| enabled: false
|
| },
|
| stroke: {
|
| width: [0, 3]
|
| },
|
| colors: ['#0d6efd', '#dc3545'],
|
| xaxis: {
|
| categories: dates,
|
| labels: {
|
| formatter: function(value) {
|
|
|
| if (typeof value === 'string' && value.includes('-')) {
|
| return value.slice(5);
|
| }
|
| return value;
|
| }
|
| }
|
| },
|
| yaxis: [
|
| {
|
| title: {
|
| text: '净流入(亿)',
|
| style: {
|
| fontSize: '12px'
|
| }
|
| },
|
| labels: {
|
| formatter: function(val) {
|
| return val.toFixed(2);
|
| }
|
| }
|
| },
|
| {
|
| opposite: true,
|
| title: {
|
| text: '涨跌幅(%)',
|
| style: {
|
| fontSize: '12px'
|
| }
|
| },
|
| labels: {
|
| formatter: function(val) {
|
| return val.toFixed(2);
|
| }
|
| }
|
| }
|
| ],
|
| tooltip: {
|
| shared: true,
|
| intersect: false,
|
| y: {
|
| formatter: function(value, { seriesIndex }) {
|
| if (seriesIndex === 0) {
|
| return value.toFixed(2) + ' 亿';
|
| }
|
| return value.toFixed(2) + '%';
|
| }
|
| }
|
| },
|
| legend: {
|
| position: 'top'
|
| }
|
| };
|
|
|
|
|
| $('#industry-flow-chart').empty();
|
| try {
|
| const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
|
| chart.render();
|
| } catch (e) {
|
| console.error("Error rendering flow chart:", e);
|
| }
|
| }
|
|
|
|
|
| function renderIndustryCompareCharts(data) {
|
|
|
| const sortedByNetFlow = [...data].sort((a, b) => b.netFlow - a.netFlow);
|
|
|
|
|
| const topInflow = sortedByNetFlow.slice(0, 10);
|
| renderBarChart('top-inflow-chart', topInflow.map(item => item.industry), topInflow.map(item => item.netFlow), '资金净流入(亿元)', '#00E396');
|
|
|
|
|
| const bottomInflow = [...sortedByNetFlow].reverse().slice(0, 10);
|
| renderBarChart('top-outflow-chart', bottomInflow.map(item => item.industry), bottomInflow.map(item => Math.abs(item.netFlow)), '资金净流出(亿元)', '#FF4560');
|
|
|
|
|
| const sortedByChange = [...data].sort((a, b) => parseFloat(b.change) - parseFloat(a.change));
|
|
|
|
|
| const topGainers = sortedByChange.slice(0, 10);
|
| renderBarChart('top-gainers-chart', topGainers.map(item => item.industry), topGainers.map(item => parseFloat(item.change)), '涨幅(%)', '#00E396');
|
|
|
|
|
| const topLosers = [...sortedByChange].reverse().slice(0, 10);
|
| renderBarChart('top-losers-chart', topLosers.map(item => item.industry), topLosers.map(item => Math.abs(parseFloat(item.change))), '跌幅(%)', '#FF4560');
|
| }
|
|
|
|
|
| function renderBarChart(elementId, categories, data, title, color) {
|
| if (!categories || !data || categories.length === 0 || data.length === 0) {
|
| console.error(`renderBarChart: Invalid data for ${elementId}`);
|
| return;
|
| }
|
|
|
| const options = {
|
| series: [{
|
| name: title,
|
| data: data
|
| }],
|
| chart: {
|
| type: 'bar',
|
| height: 300,
|
| toolbar: {
|
| show: false
|
| }
|
| },
|
| plotOptions: {
|
| bar: {
|
| horizontal: true,
|
| dataLabels: {
|
| position: 'top',
|
| },
|
| }
|
| },
|
| dataLabels: {
|
| enabled: true,
|
| offsetX: -6,
|
| style: {
|
| fontSize: '12px',
|
| colors: ['#fff']
|
| },
|
| formatter: function(val) {
|
| return val.toFixed(2);
|
| }
|
| },
|
| stroke: {
|
| show: true,
|
| width: 1,
|
| colors: ['#fff']
|
| },
|
| xaxis: {
|
| categories: categories
|
| },
|
| yaxis: {
|
| title: {
|
| text: title
|
| }
|
| },
|
| fill: {
|
| opacity: 1
|
| },
|
| colors: [color]
|
| };
|
|
|
|
|
| $(`#${elementId}`).empty();
|
| try {
|
| const chart = new ApexCharts(document.querySelector(`#${elementId}`), options);
|
| chart.render();
|
| } catch (e) {
|
| console.error(`Error rendering chart ${elementId}:`, e);
|
| }
|
| }
|
|
|
|
|
| function exportToCSV() {
|
|
|
| const table = document.querySelector('#industry-overview table');
|
| let csv = [];
|
| let rows = table.querySelectorAll('tr');
|
|
|
| for (let i = 0; i < rows.length; i++) {
|
| let row = [], cols = rows[i].querySelectorAll('td, th');
|
|
|
| for (let j = 0; j < cols.length - 1; j++) {
|
|
|
| let text = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/,/g, ',');
|
| row.push(text);
|
| }
|
|
|
| csv.push(row.join(','));
|
| }
|
|
|
|
|
| const period = $('#period-badge').text();
|
| const csvString = csv.join('\n');
|
| const filename = `行业资金流向_${period}_${new Date().toISOString().slice(0, 10)}.csv`;
|
|
|
| const blob = new Blob(['\uFEFF' + csvString], { type: 'text/csv;charset=utf-8;' });
|
| const link = document.createElement('a');
|
| link.href = URL.createObjectURL(blob);
|
| link.download = filename;
|
|
|
| link.style.display = 'none';
|
| document.body.appendChild(link);
|
|
|
| link.click();
|
|
|
| document.body.removeChild(link);
|
| }
|
|
|
| </script>
|
| {% endblock %} |