stock-monitor/app/templates/index.html

1514 lines
74 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO 优化 -->
<title>价值投资盯盘系统 - 智能股票分析与监控平台</title>
<meta name="description" content="价值投资盯盘系统是一个智能股票分析工具提供实时股票监控、AI智能分析、财务指标分析、价值投资建议等功能助您做出更明智的投资决策。">
<meta name="keywords" content="价值投资,股票分析,股票监控,AI分析,财务分析,投资建议,股市行情,智能投资,量化分析,股票筛选">
<meta name="author" content="ZYJ">
<meta name="robots" content="index, follow">
<!-- Open Graph 标签 -->
<meta property="og:title" content="价值投资盯盘系统 - 智能股票分析与监控平台">
<meta property="og:description" content="智能股票分析工具提供实时股票监控、AI智能分析、财务指标分析、价值投资建议等功能。">
<meta property="og:type" content="website">
<!-- 其他元标签 -->
<meta name="application-name" content="价值投资盯盘系统">
<meta name="theme-color" content="#1e3c72">
<link rel="canonical" href="https://your-domain.com/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
<style>
body {
background-color: #f8f9fa;
margin-bottom: 60px;
}
.navbar {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
height: 50px;
padding: 0;
}
.navbar-brand {
color: white;
font-size: 16px;
text-decoration: none;
margin-right: 50px;
}
.navbar-nav {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
margin-right: 30px;
}
.nav-link {
color: white !important;
text-decoration: none;
font-size: 14px;
line-height: 50px;
padding: 0;
}
.nav-link.active {
font-weight: bold;
}
.iconfont {
font-size: 14px;
margin-right: 4px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.card-header {
background: white;
border-bottom: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
font-weight: 600;
color: #1e3c72;
}
.table {
margin-bottom: 0;
}
.table th {
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
white-space: nowrap;
}
.table td {
vertical-align: middle;
white-space: nowrap;
}
.alert-value {
background-color: rgba(255, 193, 7, 0.1);
}
.alert-name {
color: #dc3545;
font-weight: bold;
}
.btn-primary {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
border: none;
padding: 0.5rem 1.5rem;
}
.btn-primary:hover {
background: linear-gradient(135deg, #2a5298 0%, #1e3c72 100%);
}
.btn-outline-primary {
color: #1e3c72;
border-color: #1e3c72;
}
.btn-outline-primary:hover {
background-color: #1e3c72;
border-color: #1e3c72;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.form-control:focus {
border-color: #1e3c72;
box-shadow: 0 0 0 0.2rem rgba(30, 60, 114, 0.25);
}
.positive-value {
color: #dc3545; /* 红色 */
}
.negative-value {
color: #28a745; /* 绿色 */
}
.table-container {
overflow-x: auto;
border-radius: 10px;
position: relative;
}
.table {
margin-bottom: 0;
position: relative;
}
.table thead {
position: sticky;
top: 0;
z-index: 1;
background: white;
}
.table thead th {
position: sticky;
top: 0;
background: white;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
.table tfoot {
position: sticky;
bottom: 0;
background: #f8f9fa;
box-shadow: 0 -2px 2px -1px rgba(0, 0, 0, 0.1);
}
.table tbody tr:last-child td {
border-bottom: none;
}
.card-body.p-0 {
overflow: hidden;
}
.refresh-icon {
margin-right: 5px;
}
.sortable {
cursor: pointer;
user-select: none;
}
.sortable:hover {
background-color: rgba(0,0,0,0.05);
}
.sortable.asc .bi-arrow-down-up::before {
content: "\f12d"; /* bootstrap icon for up arrow */
}
.sortable.desc .bi-arrow-down-up::before {
content: "\f128"; /* bootstrap icon for down arrow */
}
.loading {
opacity: 0.5;
}
.loading-spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.data-loading {
color: #666;
font-size: 0.8rem;
}
.index-container {
background: #f8f9fa;
border-top: 1px solid #dee2e6;
}
.index-card {
background: white;
border-radius: 8px;
padding: 10px;
margin: 5px;
min-width: 200px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.index-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 5px;
}
.index-price {
font-size: 16px;
font-weight: bold;
}
.index-change {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
.index-chart {
width: 160px;
height: 60px;
margin-top: 5px;
}
.index-panel {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-top: 1px solid rgba(255,255,255,0.1);
}
.index-card {
background: rgba(255,255,255,0.1);
border-radius: 8px;
padding: 12px;
margin: 5px;
min-width: 220px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
}
.index-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 5px;
color: rgba(255,255,255,0.9);
}
.index-price {
font-size: 18px;
font-weight: bold;
color: white;
}
.index-change {
font-size: 13px;
padding: 2px 8px;
border-radius: 4px;
background: rgba(0,0,0,0.2);
}
.index-chart {
width: 200px;
height: 60px;
margin-top: 8px;
}
.navbar-toggler {
color: white;
border-color: rgba(255,255,255,0.3);
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.navbar-toggler:focus {
box-shadow: none;
}
.navbar-nav {
margin-left: 30px;
}
.nav-link {
color: rgba(255,255,255,0.8) !important;
padding: 0.5rem 1rem;
transition: color 0.2s;
}
.nav-link:hover {
color: white !important;
}
.nav-link.active {
color: white !important;
font-weight: 600;
}
/* 添加弹窗相关样式 */
.modal-body {
padding: 1.5rem;
}
.modal-body .table td {
padding: 0.75rem;
vertical-align: top;
word-break: break-word;
white-space: normal;
line-height: 1.5;
}
.modal-body .table td:first-child {
width: 140px;
min-width: 140px;
font-weight: 500;
color: #495057;
background-color: rgba(0,0,0,0.02);
}
.modal-body .table td:last-child {
width: auto;
overflow-wrap: break-word;
}
.modal-body .card {
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.1);
}
.modal-body .card-header {
background-color: #f8f9fa;
border-bottom: 1px solid rgba(0,0,0,0.1);
padding: 0.75rem 1rem;
}
.modal-body .card-body {
padding: 1rem;
}
.modal-body .table {
margin-bottom: 0;
}
.modal-body .table tr:last-child {
border-bottom: none;
}
.modal-body .table a {
color: #1e3c72;
text-decoration: none;
}
.modal-body .table a:hover {
text-decoration: underline;
}
.modal-body h6.text-muted {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.modal-body .alert {
margin-bottom: 1rem;
padding: 0.75rem;
}
.modal-body .alert-info {
background-color: rgba(30,60,114,0.1);
border: none;
color: #1e3c72;
}
@media (max-width: 768px) {
.modal-body .table td:first-child {
width: 120px;
min-width: 120px;
}
}
/* 美化滚动条 */
.modal-body::-webkit-scrollbar {
width: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.modal-body::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Footer 样式 */
.footer {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
text-align: center;
padding: 15px 0;
width: 100%;
font-size: 14px;
margin: 0;
}
.footer a {
color: white;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* 修改容器样式 */
.container {
margin-bottom: 0;
padding-bottom: 0;
}
.card:last-child {
margin-bottom: 0;
}
</style>
</head>
<body>
<nav class="navbar navbar-dark">
<div class="container-fluid px-4">
<span class="navbar-brand">
<i class="bi bi-graph-up"></i>
价值投资盯盘系统
</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#indexMarketPanel">
<i class="bi bi-arrows-angle-expand"></i> 指数行情
</button>
</div>
</nav>
<!-- 指数行情面板 -->
<div class="collapse" id="indexMarketPanel">
<div class="container-fluid index-panel">
<div id="indexList" class="d-flex flex-wrap justify-content-center py-2">
<!-- 指数数据将通过JavaScript动态添加 -->
</div>
</div>
</div>
<div class="container py-4">
<!-- 添加股票表单 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-plus-circle"></i> 添加监控股票</span>
</div>
<div class="card-body">
<form id="addStockForm" onsubmit="return addStock(event)">
<div class="row align-items-end">
<div class="col-md-3 mb-3">
<label for="stockCode" class="form-label">股票代码</label>
<input type="text" class="form-control" id="stockCode" name="stock_code" required pattern="\d{6}" title="请输入6位数字的股票代码" placeholder="请输入股票代码">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">市值目标 (亿元)</label>
<div class="input-group">
<input type="number" step="0.01" class="form-control" name="target_market_value_min" placeholder="最小值">
<span class="input-group-text">-</span>
<input type="number" step="0.01" class="form-control" name="target_market_value_max" placeholder="最大值">
</div>
</div>
<div class="col-md-3 mb-3">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 股票列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-table"></i> 监控列表</span>
<div>
<button class="btn btn-outline-primary btn-sm" onclick="refreshWatchlist(true)">
<i class="bi bi-arrow-repeat"></i> 强制刷新
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th>代码</th>
<th>名称</th>
<th>现价</th>
<th class="sortable" data-sort="change_percent">涨跌幅 <i class="bi bi-arrow-down-up"></i></th>
<th>监控目标</th>
<th class="sortable" data-sort="market_value">市值 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="pe_ratio">市盈率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="pb_ratio">市净率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="ps_ratio">市销率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="dividend_yield">股息率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="roe">ROE <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="gross_profit_margin">毛利率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="net_profit_margin">净利率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="debt_to_assets">资产负债率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="net_profit_yoy">净利润增长率 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="bps">每股净资产 <i class="bi bi-arrow-down-up"></i></th>
<th class="sortable" data-sort="ocfps">每股经营现金流 <i class="bi bi-arrow-down-up"></i></th>
<th>操作</th>
</tr>
</thead>
<tbody id="stockTableBody">
<!-- 初始数据将通过JavaScript动态加载 -->
</tbody>
<tfoot>
<tr class="table-secondary">
<td colspan="18"><strong>统计(<span id="stockCount">0</span>只)</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<!-- 公司详情弹窗 -->
<div class="modal fade" id="companyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">公司详情分析</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height: 85vh; overflow-y: auto;">
<div class="row g-3">
<!-- 基本信息 -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<i class="bi bi-building"></i> 公司基本信息
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="companyBasicInfo">
<!-- 基本信息将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 财务指标 -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<i class="bi bi-graph-up"></i> 财务指标
</div>
<div class="card-body">
<div class="row g-3">
<!-- 估值指标 -->
<div class="col-12">
<h6 class="text-muted mb-2">估值指标</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="valuationMetrics">
<!-- 估值指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 盈利能力 -->
<div class="col-12">
<h6 class="text-muted mb-2">盈利能力</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="profitabilityMetrics">
<!-- 盈利能力指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 成长能力 -->
<div class="col-12">
<h6 class="text-muted mb-2">成长能力</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="growthMetrics">
<!-- 成长能力指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 营运能力 -->
<div class="col-12">
<h6 class="text-muted mb-2">营运能力</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="operationMetrics">
<!-- 营运能力指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 偿债能力 -->
<div class="col-12">
<h6 class="text-muted mb-2">偿债能力</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="debtMetrics">
<!-- 偿债能力指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 现金流 -->
<div class="col-12">
<h6 class="text-muted mb-2">现金流</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="cashFlowMetrics">
<!-- 现金流指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 每股指标 -->
<div class="col-12">
<h6 class="text-muted mb-2">每股指标</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<tbody id="perShareMetrics">
<!-- 每股指标将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 股东信息 -->
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<i class="bi bi-people"></i> 前十大股东
</div>
<div class="card-body">
<div id="holdersSummary" class="mb-3">
<!-- 股东汇总信息将在这里动态加载 -->
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th>股东名称</th>
<th>持股数量(股)</th>
<th>持股比例</th>
<th>较上期变动(股)</th>
</tr>
</thead>
<tbody id="holdersList">
<!-- 股东数据将在这里动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- AI价值投资分析模块 -->
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<i class="bi bi-robot"></i> AI价值投资分析
</div>
<div class="card-body">
<div id="aiAnalysisError" class="alert alert-danger d-none">
<!-- 错误信息将在这里显示 -->
</div>
<div id="aiAnalysisContent">
<!-- 原始数据显示 -->
<div class="alert alert-secondary mb-4">
<h6 class="alert-heading mb-2">分析数据</h6>
<div class="row">
<div class="col-md-6">
<h6 class="text-muted">估值指标</h6>
<ul class="list-unstyled">
<li>市盈率(PE)<span id="rawPE"></span></li>
<li>市净率(PB)<span id="rawPB"></span></li>
<li>市销率(PS)<span id="rawPS"></span></li>
<li>股息率:<span id="rawDividend"></span></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted">盈利能力</h6>
<ul class="list-unstyled">
<li>ROE<span id="rawROE"></span></li>
<li>毛利率:<span id="rawGrossMargin"></span></li>
<li>净利率:<span id="rawNetMargin"></span></li>
<li>净利润增长率:<span id="rawProfitGrowth"></span></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted">运营指标</h6>
<ul class="list-unstyled">
<li>总资产周转率:<span id="rawAssetTurnover"></span></li>
<li>存货周转率:<span id="rawInventoryTurnover"></span></li>
<li>应收账款周转率:<span id="rawReceivableTurnover"></span></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted">偿债能力</h6>
<ul class="list-unstyled">
<li>资产负债率:<span id="rawDebtRatio"></span></li>
<li>流动比率:<span id="rawCurrentRatio"></span></li>
<li>速动比率:<span id="rawQuickRatio"></span></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted">现金流</h6>
<ul class="list-unstyled">
<li>每股经营现金流:<span id="rawOCFPS"></span></li>
<li>经营现金流/营收:<span id="rawOCFToRevenue"></span></li>
<li>经营现金流同比增长:<span id="rawOCFGrowth"></span></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted">每股指标</h6>
<ul class="list-unstyled">
<li>每股收益(EPS)<span id="rawEPS"></span></li>
<li>每股净资产:<span id="rawBPS"></span></li>
<li>每股现金流量:<span id="rawCFPS"></span></li>
</ul>
</div>
</div>
</div>
<!-- 投资建议 -->
<div class="alert alert-primary mb-4">
<h6 class="alert-heading mb-2">投资建议</h6>
<p class="mb-0" id="investmentSuggestion"></p>
</div>
<!-- 价格分析 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">合理价格区间</h6>
<p class="card-text" id="reasonablePriceRange"></p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">目标市值区间</h6>
<p class="card-text" id="targetMarketValue"></p>
</div>
</div>
</div>
</div>
<!-- 详细分析 -->
<div class="row">
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">估值分析</div>
<div class="card-body">
<p class="card-text" id="valuationAnalysis"></p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">财务健康状况</div>
<div class="card-body">
<p class="card-text" id="financialHealth"></p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">成长潜力</div>
<div class="card-body">
<p class="card-text" id="growthPotential"></p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">风险评估</div>
<div class="card-body">
<p class="card-text" id="riskAssessment"></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="container">
<span>© 2024 价值投资盯盘系统 | 开发者ZYJ</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
let currentSortField = '';
let currentSortDirection = 'asc';
let stockData = [];
let loadingStocks = new Set();
// 添加排序功能
document.querySelectorAll('.sortable').forEach(header => {
header.addEventListener('click', () => {
const field = header.dataset.sort;
// 清除其他列的排序状态
document.querySelectorAll('.sortable').forEach(h => {
if (h !== header) {
h.classList.remove('asc', 'desc');
}
});
// 切换排序方向
if (currentSortField === field) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
header.classList.toggle('asc');
header.classList.toggle('desc');
} else {
currentSortField = field;
currentSortDirection = 'asc';
header.classList.add('asc');
header.classList.remove('desc');
}
// 重新渲染表格
renderStockList(stockData);
});
});
// 格式化数字(带颜色)
function formatNumber(value, noColor = false) {
if (value === null || value === undefined || isNaN(value)) return '-';
const num = parseFloat(value);
if (num === 0) return '0';
const formatted = num.toFixed(2);
if (noColor) return formatted;
return `<span class="${num > 0 ? 'positive-value' : num < 0 ? 'negative-value' : ''}">${formatted}</span>`;
}
// 格式化百分比(带颜色)
function formatPercent(value, noColor = false, isNegative = false) {
if (value === null || value === undefined || isNaN(value)) return '-';
const num = parseFloat(value);
if (num === 0) return '0%';
const formatted = (num * 100).toFixed(2) + '%';
if (noColor) return formatted;
// 对于负向指标(如负债率),高值显示为绿色,低值显示为红色
if (isNegative) {
return `<span class="${num > 0.6 ? 'negative-value' : num < 0.4 ? 'positive-value' : ''}">${formatted}</span>`;
}
// 对于股息率,总是显示为红色(正值)
if (num > 0) {
return `<span class="positive-value">${formatted}</span>`;
}
return `<span class="negative-value">${formatted}</span>`;
}
// 格式化目标范围显示
function formatTargetRange(target) {
if (!target || (!target.min && !target.max)) return '-';
if (target.min && target.max) {
return `市值: ${formatNumber(target.min)} ~ ${formatNumber(target.max)}`;
} else if (target.min) {
return `市值: ≥ ${formatNumber(target.min)}`;
} else if (target.max) {
return `市值: ≤ ${formatNumber(target.max)}`;
}
return '-';
}
// 检查是否在目标范围内
function checkTargetRange(value, target) {
if (!target || (!target.min && !target.max)) return false;
const val = parseFloat(value);
if (isNaN(val)) return false;
if (target.min && target.max) {
return val >= parseFloat(target.min) && val <= parseFloat(target.max);
} else if (target.min) {
return val >= parseFloat(target.min);
} else if (target.max) {
return val <= parseFloat(target.max);
}
return false;
}
// 检查市值是否在目标范围内
function checkMarketValueStatus(marketValue, target) {
if (!target || (!target.min && !target.max)) return null;
const value = parseFloat(marketValue);
if (isNaN(value)) return null;
if (target.min && value < parseFloat(target.min)) {
return 'undervalued';
} else if (target.max && value > parseFloat(target.max)) {
return 'overvalued';
}
return 'normal';
}
// 获取市值状态图标
function getMarketValueIcon(status) {
if (status === 'overvalued') {
return '<i class="bi bi-arrow-up-circle-fill text-danger ms-1" title="高于目标市值"></i>';
} else if (status === 'undervalued') {
return '<i class="bi bi-arrow-down-circle-fill text-success ms-1" title="低于目标市值"></i>';
} else if (status === 'normal') {
return '<i class="bi bi-check-circle-fill text-success ms-1" title="处于合理市值区间"></i>';
}
return '';
}
// 渲染股票列表
function renderStockList(data) {
const stockTableBody = document.getElementById('stockTableBody');
if (!stockTableBody) return;
stockTableBody.innerHTML = '';
if (!data || data.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="18" class="text-center text-muted py-4">暂无监控的股票</td>';
stockTableBody.appendChild(emptyRow);
document.getElementById('stockCount').textContent = '0';
return;
}
// 排序数据
if (currentSortField) {
data.sort((a, b) => {
let aValue = parseFloat(a.stock_info[currentSortField]);
let bValue = parseFloat(b.stock_info[currentSortField]);
// 处理无效值
if (aValue === null || aValue === undefined || isNaN(aValue)) aValue = -Infinity;
if (bValue === null || bValue === undefined || isNaN(bValue)) bValue = -Infinity;
// 数值比较
const comparison = aValue - bValue;
return currentSortDirection === 'asc' ? comparison : -comparison;
});
}
data.forEach(item => {
const { stock_info, targets } = item;
const row = document.createElement('tr');
row.setAttribute('data-stock-code', stock_info.code);
// 检查市值状态
const marketValueStatus = checkMarketValueStatus(stock_info.market_value, targets.target_market_value);
const marketValueIcon = getMarketValueIcon(marketValueStatus);
row.innerHTML = `
<td>${stock_info.code}${marketValueIcon}</td>
<td>
<a href="javascript:void(0)" onclick="showCompanyDetail('${stock_info.code}')" class="text-primary">
${stock_info.name || '-'}
</a>
</td>
<td>${formatNumber(stock_info.price)}</td>
<td>${formatPercent(stock_info.change_percent)}</td>
<td>${formatTargetRange(targets.target_market_value)}</td>
<td>${formatNumber(stock_info.market_value, true)}</td>
<td>${formatNumber(stock_info.pe_ratio, true)}</td>
<td>${formatNumber(stock_info.pb_ratio, true)}</td>
<td>${formatNumber(stock_info.ps_ratio, true)}</td>
<td>${formatPercent(stock_info.dividend_yield)}</td>
<td>${formatPercent(stock_info.roe)}</td>
<td>${formatPercent(stock_info.gross_profit_margin)}</td>
<td>${formatPercent(stock_info.net_profit_margin)}</td>
<td>${formatPercent(stock_info.debt_to_assets, false, true)}</td>
<td>${formatPercent(stock_info.net_profit_yoy)}</td>
<td>${formatNumber(stock_info.bps, true)}</td>
<td>${formatNumber(stock_info.ocfps)}</td>
<td>
<button class="btn btn-sm btn-danger remove-btn" data-stock-code="${stock_info.code}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
stockTableBody.appendChild(row);
});
// 更新股票数量统计
document.getElementById('stockCount').textContent = data.length;
}
// 更新单个股票数据
function updateStockData(stockData) {
const row = document.querySelector(`tr[data-stock-code="${stockData.stock_info.code}"]`);
if (!row) return;
const { stock_info, targets } = stockData;
// 检查市值状态
const marketValueStatus = checkMarketValueStatus(stock_info.market_value, targets.target_market_value);
const marketValueIcon = getMarketValueIcon(marketValueStatus);
row.innerHTML = `
<td>${stock_info.code}${marketValueIcon}</td>
<td>
<a href="javascript:void(0)" onclick="showCompanyDetail('${stock_info.code}')" class="text-primary">
${stock_info.name || '-'}
</a>
</td>
<td>${formatNumber(stock_info.price)}</td>
<td>${formatPercent(stock_info.change_percent)}</td>
<td>${formatTargetRange(stockData.targets.target_market_value)}</td>
<td>${formatNumber(stock_info.market_value, true)}</td>
<td>${formatNumber(stock_info.pe_ratio, true)}</td>
<td>${formatNumber(stock_info.pb_ratio, true)}</td>
<td>${formatNumber(stock_info.ps_ratio, true)}</td>
<td>${formatPercent(stock_info.dividend_yield)}</td>
<td>${formatPercent(stock_info.roe)}</td>
<td>${formatPercent(stock_info.gross_profit_margin)}</td>
<td>${formatPercent(stock_info.net_profit_margin)}</td>
<td>${formatPercent(stock_info.debt_to_assets, false, true)}</td>
<td>${formatPercent(stock_info.net_profit_yoy)}</td>
<td>${formatNumber(stock_info.bps, true)}</td>
<td>${formatNumber(stock_info.ocfps)}</td>
<td>
<button class="btn btn-sm btn-danger remove-btn" data-stock-code="${stock_info.code}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
}
// 刷新监控列表
async function refreshWatchlist(forceRefresh = false) {
try {
const response = await fetch('/api/watchlist');
if (!response.ok) throw new Error('获取监控列表失败');
const data = await response.json();
stockData = data; // 更新全局数据
renderStockList(data);
// 异步加载每个股票的详细数据
data.forEach(item => {
loadStockData(item.stock_info.code, forceRefresh);
});
} catch (error) {
console.error('刷新数据失败:', error);
alert('刷新数据失败:' + error.message);
}
}
// 异步加载股票数据
async function loadStockData(stockCode, forceRefresh = false) {
try {
const url = `/api/stock_info/${stockCode}${forceRefresh ? '?force_refresh=true' : ''}`;
const response = await fetch(url);
if (!response.ok) throw new Error('获取股票数据失败');
const data = await response.json();
if (data.error) throw new Error(data.error);
updateStockData(data);
} catch (error) {
console.error(`加载股票 ${stockCode} 数据失败:`, error);
}
}
// 添加股票
async function addStock(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
const response = await fetch('/api/add_watch', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(await response.text() || '添加失败');
}
form.reset();
await refreshWatchlist(true);
} catch (error) {
console.error('添加股票失败:', error);
alert('添加股票失败:' + error.message);
}
}
// 删除股票
async function removeStock(stockCode) {
if (!confirm('确定要删除这只股票吗?')) {
return;
}
try {
const response = await fetch(`/api/remove_watch/${stockCode}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(await response.text() || '删除失败');
}
await refreshWatchlist(true);
} catch (error) {
console.error('删除股票失败:', error);
alert('删除股票失败:' + error.message);
}
}
// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', function() {
// 初始化加载数据
refreshWatchlist();
// 绑定删除按钮事件
document.addEventListener('click', function(e) {
if (e.target.closest('.remove-btn')) {
const stockCode = e.target.closest('.remove-btn').dataset.stockCode;
removeStock(stockCode);
}
});
});
// 获取指数数据
async function refreshIndexData() {
try {
const response = await fetch('/api/index_info');
const data = await response.json();
updateIndexDisplay(data);
} catch (error) {
console.error('获取指数数据失败:', error);
}
}
// 更新指数显示
function updateIndexDisplay(indexData) {
const indexList = document.getElementById('indexList');
indexList.innerHTML = '';
indexData.forEach(index => {
const card = document.createElement('div');
card.className = 'index-card';
const changeClass = index.change >= 0 ? 'positive-value' : 'negative-value';
const changeSign = index.change >= 0 ? '+' : '';
card.innerHTML = `
<div class="index-name">${index.name}</div>
<div class="d-flex justify-content-between align-items-center">
<div class="index-price">${index.price.toFixed(2)}</div>
<div class="index-change ${changeClass}">${changeSign}${index.change.toFixed(2)}%</div>
</div>
<div class="index-chart" id="chart_${index.code.replace('.', '_')}"></div>
`;
indexList.appendChild(card);
// 创建K线图
setTimeout(() => {
createKlineChart(index.code, index.kline_data);
}, 0);
});
}
// 创建K线图
function createKlineChart(code, klineData) {
const chartDom = document.getElementById(`chart_${code.replace('.', '_')}`);
const myChart = echarts.init(chartDom);
const dates = klineData.map(item => item.date);
const data = klineData.map(item => [item.open, item.close, item.low, item.high]);
const option = {
animation: false,
grid: {
left: '5%',
right: '5%',
top: '10%',
bottom: '10%'
},
xAxis: {
type: 'category',
data: dates,
show: false
},
yAxis: {
type: 'value',
show: false,
scale: true
},
series: [{
type: 'candlestick',
data: data,
itemStyle: {
color: '#ef5350',
color0: '#26a69a',
borderColor: '#ef5350',
borderColor0: '#26a69a'
}
}]
};
myChart.setOption(option);
// 监听面板展开/收起事件,重新渲染图表
document.getElementById('indexMarketPanel').addEventListener('shown.bs.collapse', () => {
myChart.resize();
});
// 监听窗口大小变化
window.addEventListener('resize', () => {
myChart.resize();
});
}
// 在页面加载和刷新数据时更新指数
document.addEventListener('DOMContentLoaded', refreshIndexData);
setInterval(refreshIndexData, 60000); // 每分钟更新一次
// 显示公司详情弹窗
async function showCompanyDetail(stockCode) {
const modal = new bootstrap.Modal(document.getElementById('companyModal'));
modal.show();
// 显示加载状态
document.getElementById('companyBasicInfo').innerHTML = '<tr><td colspan="2" class="text-center"><span class="loading-spinner"></span> 数据加载中...</td></tr>';
document.getElementById('valuationMetrics').innerHTML = '<tr><td colspan="2" class="text-center"><span class="loading-spinner"></span> 数据加载中...</td></tr>';
document.getElementById('profitabilityMetrics').innerHTML = '<tr><td colspan="2" class="text-center"><span class="loading-spinner"></span> 数据加载中...</td></tr>';
try {
// 获取公司基本信息
const response = await fetch(`/api/company_detail/${stockCode}`);
const data = await response.json();
if (data.error) {
document.getElementById('companyBasicInfo').innerHTML = `<tr><td colspan="2" class="text-danger">获取数据失败: ${data.error}</td></tr>`;
return;
}
// 更新基本信息
const basicInfo = document.getElementById('companyBasicInfo');
basicInfo.innerHTML = `
<tr><td>公司名称</td><td>${data.basic_info.name || '-'}</td></tr>
<tr><td>公司全称</td><td>${data.basic_info.com_name || '-'}</td></tr>
<tr><td>所属行业</td><td>${data.basic_info.industry || '-'}</td></tr>
<tr><td>法人代表</td><td>${data.basic_info.chairman || '-'}</td></tr>
<tr><td>总经理</td><td>${data.basic_info.manager || '-'}</td></tr>
<tr><td>董秘</td><td>${data.basic_info.secretary || '-'}</td></tr>
<tr><td>注册资本</td><td>${data.basic_info.reg_capital ? formatNumber(data.basic_info.reg_capital, true) + '万元' : '-'}</td></tr>
<tr><td>员工人数</td><td>${data.basic_info.employees ? formatNumber(data.basic_info.employees, true) + '人' : '-'}</td></tr>
<tr><td>注册日期</td><td>${data.basic_info.setup_date || '-'}</td></tr>
<tr><td>上市日期</td><td>${data.basic_info.list_date || '-'}</td></tr>
<tr><td>所在地区</td><td>${data.basic_info.province} ${data.basic_info.city}</td></tr>
<tr><td>办公地址</td><td>${data.basic_info.office || '-'}</td></tr>
<tr><td>公司网站</td><td>${data.basic_info.website ? `<a href="${data.basic_info.website}" target="_blank">${data.basic_info.website}</a>` : '-'}</td></tr>
<tr><td>电子邮箱</td><td>${data.basic_info.email ? `<a href="mailto:${data.basic_info.email}">${data.basic_info.email}</a>` : '-'}</td></tr>
<tr><td>公司介绍</td><td>${data.basic_info.introduction || '-'}</td></tr>
<tr><td>主要业务</td><td>${data.basic_info.main_business || '-'}</td></tr>
<tr><td>经营范围</td><td>${data.basic_info.business_scope || '-'}</td></tr>
`;
// 更新估值指标
document.getElementById('valuationMetrics').innerHTML = `
<tr><td>市盈率(PE)</td><td>${formatNumber(data.financial_info.pe_ratio, true)}</td></tr>
<tr><td>市净率(PB)</td><td>${formatNumber(data.financial_info.pb_ratio, true)}</td></tr>
<tr><td>市销率(PS)</td><td>${formatNumber(data.financial_info.ps_ratio, true)}</td></tr>
`;
// 更新盈利能力指标
document.getElementById('profitabilityMetrics').innerHTML = `
<tr><td>ROE</td><td>${formatPercent(data.financial_info.roe/100)}</td></tr>
<tr><td>ROE(扣非)</td><td>${formatPercent(data.financial_info.roe_dt/100)}</td></tr>
<tr><td>ROA</td><td>${formatPercent(data.financial_info.roa/100)}</td></tr>
<tr><td>毛利率</td><td>${formatPercent(data.financial_info.grossprofit_margin/100)}</td></tr>
<tr><td>净利率</td><td>${formatPercent(data.financial_info.netprofit_margin/100)}</td></tr>
`;
// 更新成长能力指标
document.getElementById('growthMetrics').innerHTML = `
<tr><td>净利润增长</td><td>${formatPercent(data.financial_info.netprofit_yoy/100)}</td></tr>
<tr><td>扣非净利润增长</td><td>${formatPercent(data.financial_info.dt_netprofit_yoy/100)}</td></tr>
<tr><td>营业总收入增长</td><td>${formatPercent(data.financial_info.tr_yoy/100)}</td></tr>
<tr><td>营业收入增长</td><td>${formatPercent(data.financial_info.or_yoy/100)}</td></tr>
`;
// 更新营运能力指标
document.getElementById('operationMetrics').innerHTML = `
<tr><td>总资产周转率</td><td>${formatNumber(data.financial_info.assets_turn)}</td></tr>
<tr><td>存货周转率</td><td>${formatNumber(data.financial_info.inv_turn)}</td></tr>
<tr><td>应收账款周转率</td><td>${formatNumber(data.financial_info.ar_turn)}</td></tr>
<tr><td>流动资产周转率</td><td>${formatNumber(data.financial_info.ca_turn)}</td></tr>
`;
// 更新偿债能力指标
document.getElementById('debtMetrics').innerHTML = `
<tr><td>流动比率</td><td>${formatNumber(data.financial_info.current_ratio, true)}</td></tr>
<tr><td>速动比率</td><td>${formatNumber(data.financial_info.quick_ratio, true)}</td></tr>
<tr><td>资产负债率</td><td>${formatPercent(data.financial_info.debt_to_assets/100, true)}</td></tr>
<tr><td>产权比率</td><td>${formatNumber(data.financial_info.debt_to_eqt, true)}</td></tr>
`;
// 更新现金流指标
document.getElementById('cashFlowMetrics').innerHTML = `
<tr><td>经营现金流/营收</td><td>${formatPercent(data.financial_info.ocf_to_or/100)}</td></tr>
<tr><td>经营现金流/经营利润</td><td>${formatPercent(data.financial_info.ocf_to_opincome/100)}</td></tr>
<tr><td>经营现金流同比增长</td><td>${formatPercent(data.financial_info.ocf_yoy/100)}</td></tr>
`;
// 更新每股指标
document.getElementById('perShareMetrics').innerHTML = `
<tr><td>每股收益(EPS)</td><td>${formatNumber(data.financial_info.eps)}</td></tr>
<tr><td>每股收益(扣非)</td><td>${formatNumber(data.financial_info.dt_eps)}</td></tr>
<tr><td>每股净资产</td><td>${formatNumber(data.financial_info.bps)}</td></tr>
<tr><td>每股经营现金流</td><td>${formatNumber(data.financial_info.ocfps)}</td></tr>
<tr><td>每股留存收益</td><td>${formatNumber(data.financial_info.retainedps)}</td></tr>
<tr><td>每股现金流量</td><td>${formatNumber(data.financial_info.cfps)}</td></tr>
<tr><td>每股息税前利润</td><td>${formatNumber(data.financial_info.ebit_ps)}</td></tr>
<tr><td>每股企业自由现金流</td><td>${formatNumber(data.financial_info.fcff_ps)}</td></tr>
<tr><td>每股股东自由现金流</td><td>${formatNumber(data.financial_info.fcfe_ps)}</td></tr>
`;
// 获取股东数据
const holdersResponse = await fetch(`/api/holders/${stockCode}`);
const holdersData = await holdersResponse.json();
if (holdersData.error) {
document.getElementById('holdersList').innerHTML = `<tr><td colspan="4" class="text-center text-muted">${holdersData.error}</td></tr>`;
document.getElementById('holdersSummary').innerHTML = '';
} else {
// 更新股东汇总信息
const holdersSummaryHtml = `
<div class="alert alert-info">
<div class="row">
<div class="col-md-6">
<strong>统计截止日期:</strong> ${holdersData.report_date}
</div>
<div class="col-md-6">
<strong>前十大股东合计持股:</strong> ${formatPercent(holdersData.total_ratio/100)}
</div>
</div>
</div>
`;
document.getElementById('holdersSummary').innerHTML = holdersSummaryHtml;
// 更新股东列表
if (holdersData.holders.length === 0) {
document.getElementById('holdersList').innerHTML = '<tr><td colspan="4" class="text-center text-muted">暂无股东数据</td></tr>';
} else {
const holdersHtml = holdersData.holders.map(holder => `
<tr>
<td>${holder.holder_name}</td>
<td>${formatNumber(holder.hold_amount)}</td>
<td>${formatPercent(holder.hold_ratio/100)}</td>
<td>${formatNumber(holder.hold_change)}</td>
</tr>
`).join('');
document.getElementById('holdersList').innerHTML = holdersHtml;
}
}
// 获取AI分析数据
document.getElementById('aiAnalysisError').classList.add('d-none');
document.getElementById('aiAnalysisContent').classList.add('d-none');
const aiAnalysisLoading = document.createElement('div');
aiAnalysisLoading.className = 'text-center py-4';
aiAnalysisLoading.innerHTML = `
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<div class="mt-2 text-muted">AI分析中请稍候...</div>
`;
document.getElementById('aiAnalysisContent').parentNode.insertBefore(aiAnalysisLoading, document.getElementById('aiAnalysisContent'));
try {
const aiAnalysisResponse = await fetch(`/api/ai_analysis/${stockCode}`);
const aiAnalysisText = await aiAnalysisResponse.text();
let aiAnalysisData;
try {
aiAnalysisData = JSON.parse(aiAnalysisText);
} catch (parseError) {
console.error('JSON解析失败:', parseError);
console.log('原始响应:', aiAnalysisText);
throw new Error('AI分析结果格式错误请稍后重试');
}
aiAnalysisLoading.remove();
if (aiAnalysisData.error) {
document.getElementById('aiAnalysisError').textContent = aiAnalysisData.error;
document.getElementById('aiAnalysisError').classList.remove('d-none');
document.getElementById('aiAnalysisContent').classList.add('d-none');
return;
}
if (!aiAnalysisData.investment_suggestion) {
throw new Error('AI分析结果格式不正确');
}
document.getElementById('aiAnalysisError').classList.add('d-none');
document.getElementById('aiAnalysisContent').classList.remove('d-none');
console.log('AI分析结果:', aiAnalysisData); // 调试输出
// 更新投资建议
const suggestionHtml = `
<div class="mb-2"><strong>总体建议:</strong>${aiAnalysisData.investment_suggestion.summary || '-'}</div>
<div class="mb-2"><strong>建议操作:</strong>${aiAnalysisData.investment_suggestion.action || '-'}</div>
<div><strong>关注重点:</strong>
<p class="mb-0">${aiAnalysisData.investment_suggestion.key_points || '-'}</p>
</div>
`;
document.getElementById('investmentSuggestion').innerHTML = suggestionHtml;
// 更新价格分析
const priceRange = aiAnalysisData.price_analysis?.合理价格区间 || [];
document.getElementById('reasonablePriceRange').innerHTML = `
<div class="mb-2">${priceRange[0] || '-'} - ${priceRange[1] || '-'} 元</div>
`;
const marketValue = aiAnalysisData.price_analysis?.目标市值区间 || [];
document.getElementById('targetMarketValue').innerHTML = `
<div class="mb-2">${marketValue[0] || '-'} - ${marketValue[1] || '-'} 亿元</div>
`;
// 更新估值分析
const valuationHtml = `
<div class="mb-2">${aiAnalysisData.analysis?.估值分析 || '-'}</div>
`;
document.getElementById('valuationAnalysis').innerHTML = valuationHtml;
// 更新财务健康状况
const financialHtml = `
<div>${aiAnalysisData.analysis?.财务健康状况 || '-'}</div>
`;
document.getElementById('financialHealth').innerHTML = financialHtml;
// 更新成长潜力
const growthHtml = `
<div>${aiAnalysisData.analysis?.成长潜力 || '-'}</div>
`;
document.getElementById('growthPotential').innerHTML = growthHtml;
// 更新风险评估
const riskHtml = `
<div>${aiAnalysisData.analysis?.风险评估 || '-'}</div>
`;
document.getElementById('riskAssessment').innerHTML = riskHtml;
// 更新原始数据显示
document.getElementById('rawPE').textContent = formatNumber(data.financial_info.pe_ratio, true);
document.getElementById('rawPB').textContent = formatNumber(data.financial_info.pb_ratio, true);
document.getElementById('rawPS').textContent = formatNumber(data.financial_info.ps_ratio, true);
document.getElementById('rawDividend').textContent = formatPercent(data.financial_info.dividend_yield, true);
document.getElementById('rawROE').textContent = formatPercent(data.financial_info.roe/100, true);
document.getElementById('rawGrossMargin').textContent = formatPercent(data.financial_info.grossprofit_margin/100, true);
document.getElementById('rawNetMargin').textContent = formatPercent(data.financial_info.netprofit_margin/100, true);
document.getElementById('rawProfitGrowth').textContent = formatPercent(data.financial_info.netprofit_yoy/100, true);
document.getElementById('rawAssetTurnover').textContent = formatNumber(data.financial_info.assets_turn, true);
document.getElementById('rawInventoryTurnover').textContent = formatNumber(data.financial_info.inv_turn, true);
document.getElementById('rawReceivableTurnover').textContent = formatNumber(data.financial_info.ar_turn, true);
document.getElementById('rawDebtRatio').textContent = formatPercent(data.financial_info.debt_to_assets/100, true);
document.getElementById('rawCurrentRatio').textContent = formatNumber(data.financial_info.current_ratio, true);
document.getElementById('rawQuickRatio').textContent = formatNumber(data.financial_info.quick_ratio, true);
document.getElementById('rawOCFPS').textContent = formatNumber(data.financial_info.ocfps, true);
document.getElementById('rawOCFToRevenue').textContent = formatPercent(data.financial_info.ocf_to_or/100, true);
document.getElementById('rawOCFGrowth').textContent = formatPercent(data.financial_info.ocf_yoy/100, true);
document.getElementById('rawEPS').textContent = formatNumber(data.financial_info.eps, true);
document.getElementById('rawBPS').textContent = formatNumber(data.financial_info.bps, true);
document.getElementById('rawCFPS').textContent = formatNumber(data.financial_info.cfps, true);
} catch (error) {
console.error('获取AI分析失败:', error);
aiAnalysisLoading.remove();
document.getElementById('aiAnalysisError').textContent = error.message || 'AI分析失败请稍后重试';
document.getElementById('aiAnalysisError').classList.remove('d-none');
document.getElementById('aiAnalysisContent').classList.add('d-none');
}
} catch (error) {
console.error('获取公司详情失败:', error);
document.getElementById('companyBasicInfo').innerHTML = '<tr><td colspan="2" class="text-danger">获取数据失败,请稍后重试</td></tr>';
}
}
// 初始化DataTable
const watchlistTable = $('#watchlistTable').DataTable({
paging: false,
searching: false,
info: false,
order: [[4, 'desc']], // 默认按涨跌幅降序排列
columnDefs: [
{ orderable: false, targets: [0, 1, 2] }, // 禁用股票代码、名称和股价的排序
{
targets: [2, 3, 4, 5, 6, 7, 8, 9, 10], // 数值列
render: function(data, type, row) {
if (type === 'display') {
return formatNumber(data);
}
return data;
}
},
{
targets: [4], // 涨跌幅列
render: function(data, type, row) {
if (type === 'display') {
return formatPercent(data);
}
return data;
}
}
],
language: {
zeroRecords: "暂无数据"
}
});
</script>
</body>
</html>