1514 lines
74 KiB
HTML
1514 lines
74 KiB
HTML
<!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> |