初始化版本

This commit is contained in:
LL
2025-11-18 14:25:00 +08:00
commit d6640719bd
42 changed files with 14150 additions and 0 deletions

334
404.html Normal file
View File

@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面未找到</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 炫酷背景 */
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* 粒子效果背景 */
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px);
background-size: 30px 30px;
z-index: 1;
animation: moveBackground 30s linear infinite;
}
@keyframes moveBackground {
0% {
background-position: 0 0;
}
100% {
background-position: 300px 300px;
}
}
/* 404容器 */
.container {
text-align: center;
color: white;
z-index: 2;
position: relative;
max-width: 800px;
padding: 2rem;
}
/* 404数字动画 */
.error-number {
font-size: 180px;
font-weight: 900;
margin: 0;
line-height: 1;
position: relative;
display: inline-block;
text-shadow: 0 0 50px rgba(255, 255, 255, 0.5);
animation: glitch 2s infinite;
}
@keyframes glitch {
0%, 100% {
transform: translate(0);
text-shadow: 0 0 50px rgba(255, 255, 255, 0.5);
}
20% {
transform: translate(-5px, 5px);
text-shadow: 0 0 30px rgba(255, 0, 255, 0.8), 0 0 60px rgba(255, 0, 255, 0.5);
}
40% {
transform: translate(-5px, -5px);
text-shadow: 0 0 30px rgba(0, 255, 255, 0.8), 0 0 60px rgba(0, 255, 255, 0.5);
}
60% {
transform: translate(5px, 5px);
text-shadow: 0 0 30px rgba(255, 255, 0, 0.8), 0 0 60px rgba(255, 255, 0, 0.5);
}
80% {
transform: translate(5px, -5px);
text-shadow: 0 0 30px rgba(0, 255, 0, 0.8), 0 0 60px rgba(0, 255, 0, 0.5);
}
}
/* 404数字内部动画效果 */
.error-number::after {
content: '404';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: transparent;
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.8) 50%, transparent 70%);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
animation: shine 3s infinite linear;
}
@keyframes shine {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* 标题和描述 */
h2 {
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
animation: fadeInUp 1s ease-out;
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
opacity: 0.9;
animation: fadeInUp 1s ease-out 0.3s both;
}
/* 返回首页按钮 */
.btn {
display: inline-block;
padding: 12px 30px;
font-size: 1.1rem;
font-weight: 600;
text-decoration: none;
color: #667eea;
background: white;
border-radius: 50px;
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
animation: fadeInUp 1s ease-out 0.6s both;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 15px 30px rgba(102, 126, 234, 0.5);
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
transition: all 0.6s;
}
.btn:hover::before {
left: 100%;
}
/* 动画效果 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 漂浮图标效果 */
.floating-icons {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.icon {
position: absolute;
font-size: 2rem;
color: rgba(255, 255, 255, 0.3);
animation: float 15s infinite ease-in-out;
}
.icon:nth-child(1) { top: 10%; left: 10%; animation-duration: 15s; }
.icon:nth-child(2) { top: 20%; right: 15%; animation-duration: 18s; animation-delay: 2s; }
.icon:nth-child(3) { bottom: 15%; left: 20%; animation-duration: 12s; animation-delay: 1s; }
.icon:nth-child(4) { bottom: 25%; right: 25%; animation-duration: 20s; animation-delay: 3s; }
.icon:nth-child(5) { top: 40%; left: 35%; animation-duration: 14s; animation-delay: 0.5s; }
@keyframes float {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
opacity: 0.3;
}
25% {
transform: translate(20px, -30px) rotate(15deg);
opacity: 0.5;
}
50% {
transform: translate(-10px, 20px) rotate(-10deg);
opacity: 0.4;
}
75% {
transform: translate(30px, 10px) rotate(5deg);
opacity: 0.6;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.error-number {
font-size: 120px;
}
h2 {
font-size: 2rem;
}
p {
font-size: 1rem;
}
.btn {
padding: 10px 25px;
font-size: 1rem;
}
}
@media (max-width: 480px) {
.error-number {
font-size: 80px;
}
h2 {
font-size: 1.5rem;
}
.container {
padding: 1rem;
}
}
</style>
</head>
<body>
<!-- 粒子背景 -->
<!-- 漂浮图标 -->
<div class="floating-icons">
<i class="fas fa-chart-line icon"></i>
<i class="fas fa-rocket icon"></i>
<i class="fas fa-cog icon"></i>
<i class="fas fa-code icon"></i>
<i class="fas fa-terminal icon"></i>
</div>
<!-- 主要内容 -->
<div class="container">
<div class="error-number">404</div>
<h2>页面未找到</h2>
<p>您访问的页面可能已经移动、删除或从未存在。请返回首页继续浏览我们的基金监控系统。</p>
<a href="index.php" class="btn">
<i class="fas fa-home mr-2"></i> 返回首页
</a>
</div>
<!-- JavaScript增强效果 -->
<script>
// 添加鼠标跟随效果
document.addEventListener('mousemove', function(e) {
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
// 轻微移动背景渐变
document.body.style.backgroundPosition = `${x * 50}px ${y * 50}px`;
// 轻微移动404数字
const errorNumber = document.querySelector('.error-number');
if (errorNumber) {
errorNumber.style.transform = `translate(${x * 10 - 5}px, ${y * 10 - 5}px)`;
}
});
// 随机生成更多漂浮图标
function createMoreIcons() {
const icons = ['fa-star', 'fa-globe', 'fa-bolt', 'fa-lightbulb', 'fa-puzzle-piece'];
const container = document.querySelector('.floating-icons');
for (let i = 0; i < 5; i++) {
const icon = document.createElement('i');
const randomIcon = icons[Math.floor(Math.random() * icons.length)];
icon.className = `fas ${randomIcon} icon`;
// 随机位置
icon.style.top = `${Math.random() * 100}%`;
icon.style.left = `${Math.random() * 100}%`;
// 随机动画持续时间和延迟
const duration = 10 + Math.random() * 20;
const delay = Math.random() * 5;
icon.style.animationDuration = `${duration}s`;
icon.style.animationDelay = `${delay}s`;
container.appendChild(icon);
}
}
// 页面加载后创建更多图标
window.addEventListener('load', function() {
createMoreIcons();
});
</script>
</body>
</html>

632
admin.css Normal file
View File

@@ -0,0 +1,632 @@
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--dark: #1f2937;
--light: #f8fafc;
--gray: #6b7280;
--border: #e5e7eb;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #334155;
line-height: 1.5;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 15px;
}
/* 头部样式 */
.header {
text-align: center;
margin-bottom: 25px;
color: white;
}
.header-content {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(45deg, #fff, #e0f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 1rem;
opacity: 0.9;
margin-bottom: 15px;
}
/* 控制按钮 */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 10px 18px;
border: none;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.btn-success {
background: linear-gradient(135deg, var(--success), #059669);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), #dc2626);
color: white;
}
.btn-outline {
background: rgba(134, 63, 235);
color: white;
border: 2px solid rgba(134, 63, 235, 0.3);
}
.btn-outline:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
/* 内容区域 */
.content {
background: white;
border-radius: 16px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-bottom: 25px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
/* 搜索框样式 */
.search-box {
position: relative;
max-width: 300px;
width: 100%;
margin-bottom: 20px;
}
.search-box input {
width: 100%;
padding: 10px 40px 10px 16px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.search-box::after {
content: '🔍';
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--gray);
pointer-events: none;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--dark);
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
background: white;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px;
padding-right: 40px;
}
/* 表格样式 */
.table-container {
overflow-x: auto;
border-radius: 12px;
border: 1px solid var(--border);
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.funds-table,
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.funds-table th,
.funds-table td,
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
transition: all 0.3s ease;
}
.funds-table th,
.data-table th {
background: linear-gradient(90deg, #f8fafc, #f1f5f9);
font-weight: 600;
color: var(--dark);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
z-index: 10;
}
.funds-table tr:hover,
.data-table tr:hover {
background: #f8fafc;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.funds-table tr:last-child td,
.data-table tr:last-child td {
border-bottom: none;
}
/* 推荐基金表格特殊样式 */
.fund-row {
transition: all 0.3s ease;
}
.fund-code-cell {
font-weight: 700;
color: var(--primary);
}
.fund-ip-cell {
font-family: 'Courier New', monospace;
}
.fund-time-cell {
font-size: 0.85rem;
color: var(--gray);
}
.fund-amount-cell {
font-weight: 600;
}
.fund-status-cell {
font-weight: 600;
}
.fund-actions-cell {
text-align: right;
}
/* 状态标签样式优化 */
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
color: white;
display: inline-block;
transition: all 0.3s ease;
}
.status-pending {
background: linear-gradient(135deg, var(--warning), #d97706);
}
.status-approved {
background: linear-gradient(135deg, var(--success), #059669);
}
.status-rejected {
background: linear-gradient(135deg, var(--danger), #dc2626);
}
/* 渠道标签 */
.channel-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
color: white;
}
.channel-cmb {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.channel-tt {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.channel-zfb {
background: linear-gradient(135deg, #27ae60, #229954);
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.8rem;
border-radius: 6px;
transition: all 0.3s ease;
}
.btn-sm:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn-sm:active {
transform: translateY(0);
}
/* 消息提示 */
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert-success {
background: #dcfce7;
border: 1px solid #bbf7d0;
color: #166534;
}
.alert-error {
background: #fee2e2;
border: 1px solid #fecaca;
color: #dc2626;
}
/* 模态框 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 16px;
padding: 30px;
width: 90%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 1.3rem;
font-weight: 700;
color: var(--dark);
}
.close-modal {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--gray);
padding: 5px;
}
.close-modal:hover {
color: var(--dark);
}
/* 页脚 */
.footer {
text-align: center;
color: white;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
/* 标签页样式 */
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
background: #f8fafc;
border-radius: 10px 10px 0 0;
overflow: hidden;
}
.tab {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 3px solid transparent;
font-size: 1rem;
font-weight: 600;
color: var(--gray);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
z-index: 1;
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
background: white;
box-shadow: 0 -2px 5px rgba(0,0,0,0.05);
}
.tab:hover {
color: var(--primary-dark);
background: rgba(255,255,255,0.5);
}
.tab-content {
display: none;
padding: 20px;
background: white;
border-radius: 0 0 10px 10px;
animation: fadeIn 0.5s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 操作日志样式 */
.operation-log {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 8px;
padding: 15px;
}
.operation-item {
padding: 12px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.operation-item:last-child {
border-bottom: none;
}
.operation-details {
flex: 1;
}
.operation-type {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
margin-right: 10px;
}
.type-add {
background: #dcfce7;
color: #166534;
}
.type-update {
background: #fef3c7;
color: #92400e;
}
.type-delete {
background: #fee2e2;
color: #dc2626;
}
.operation-time {
color: var(--gray);
font-size: 0.85rem;
}
/* 图表容器样式 */
.chart-container {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.chart-title {
font-size: 1.2rem;
font-weight: 700;
color: var(--dark);
}
.fund-selector {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.9rem;
}
.chart-wrapper {
position: relative;
height: 300px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 1.8rem;
}
.content {
padding: 15px;
}
.funds-table {
font-size: 0.85rem;
}
.funds-table th,
.funds-table td {
padding: 8px 12px;
}
.action-buttons {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
.tabs {
flex-direction: column;
}
.tab {
border-bottom: none;
border-left: 3px solid transparent;
}
.tab.active {
border-left-color: var(--primary);
}
.operation-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
@media (max-width: 480px) {
.modal-content {
padding: 20px;
margin: 10px;
}
}
/* 加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

849
admin.js Normal file
View File

@@ -0,0 +1,849 @@
// 全局变量
let fundsData = [];
let fundChart = null;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadFundsData();
loadOperationLog();
populateFundSelector();
// 表单提交事件
document.getElementById('fundForm').addEventListener('submit', function(e) {
e.preventDefault();
saveFund();
});
});
// 加载推荐基金数据
async function loadRecommendedFunds() {
try {
showLoading();
const response = await fetch('admin_api.php?action=get_recommended_funds');
const result = await response.json();
if (result.success) {
renderRecommendedFundsTable(result.data);
} else {
throw new Error(result.message || '加载数据失败');
}
} catch (error) {
console.error('加载推荐基金数据失败:', error);
showMessage('数据加载失败: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// 渲染推荐基金表格
function renderRecommendedFundsTable(data) {
const tbody = document.getElementById('recommendedFundsList');
if (!data || data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" style="text-align: center; padding: 40px; color: var(--gray);">
<i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 10px; display: block; opacity: 0.5;"></i>
暂无推荐基金数据
</td>
</tr>
`;
return;
}
let html = '';
data.forEach((fund, index) => {
// 处理状态显示,支持多种可能的状态值
const getStatusText = (status) => {
const normalizedStatus = (status || '').toLowerCase().trim();
if (normalizedStatus.includes('approve') || normalizedStatus === '已批准') {
return '已批准';
} else if (normalizedStatus.includes('pend') || normalizedStatus === '待审核') {
return '待审核';
} else if (normalizedStatus.includes('reject') || normalizedStatus === '已拒绝') {
return '已拒绝';
}
return '未知';
};
const getStatusClass = (status) => {
const normalizedStatus = (status || '').toLowerCase().trim();
if (normalizedStatus.includes('approve') || normalizedStatus === '已批准') {
return 'status-approved';
} else if (normalizedStatus.includes('pend') || normalizedStatus === '待审核') {
return 'status-pending';
} else if (normalizedStatus.includes('reject') || normalizedStatus === '已拒绝') {
return 'status-rejected';
}
return 'status-unknown';
};
const statusText = getStatusText(fund.status);
const statusClass = getStatusClass(fund.status);
// 根据状态决定操作按钮
let actionButtons = '';
if (fund.status === 'pending') {
actionButtons = `
<button class="btn btn-primary btn-sm" onclick="approveRecommendedFund('${fund.fund_code}')">
<i class="fas fa-check"></i> 批准
</button>
<button class="btn btn-danger btn-sm" onclick="deleteRecommendedFund('${fund.fund_code}')">
<i class="fas fa-trash"></i> 删除
</button>
`;
} else {
// 已批准或已拒绝状态只显示删除按钮
actionButtons = `
<button class="btn btn-danger btn-sm" onclick="deleteRecommendedFund('${fund.fund_code}')">
<i class="fas fa-trash"></i> 删除
</button>
`;
}
html += `
<tr class="fund-row">
<td class="fund-code-cell"><strong>${fund.fund_code}</strong></td>
<td class="fund-ip-cell">${fund.ip}</td>
<td class="fund-time-cell">${fund.timestamp}</td>
<td class="fund-channel-cell">${fund.channel}</td>
<td class="fund-amount-cell">${parseFloat(fund.amount).toFixed(2)}</td>
<td class="fund-status-cell">
<span class="status-badge ${statusClass}">
${statusText}
</span>
</td>
<td class="fund-actions-cell">
<div class="action-buttons">
${actionButtons}
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
// 搜索推荐基金
function searchRecommendedFunds(keyword) {
const rows = document.querySelectorAll('#recommendedFundsList tr');
keyword = keyword.toLowerCase();
rows.forEach(row => {
const fundCode = row.querySelector('td:first-child').textContent.toLowerCase();
if (fundCode.includes(keyword)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
// 批准推荐基金
async function approveRecommendedFund(fundCode) {
if (!confirm(`确定要批准基金 ${fundCode} 吗?`)) {
return;
}
try {
showLoading();
const response = await fetch('admin_api.php?action=approve_recommended_fund&fund_code=' + fundCode);
const result = await response.json();
if (result.success) {
showMessage('基金批准成功', 'success');
loadRecommendedFunds();
} else {
throw new Error(result.message || '操作失败');
}
} catch (error) {
console.error('批准推荐基金失败:', error);
showMessage('操作失败: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// 删除推荐基金
async function deleteRecommendedFund(fundCode) {
if (!confirm(`确定要删除推荐基金 ${fundCode} 吗?此操作不可恢复。`)) {
return;
}
try {
showLoading();
const response = await fetch('admin_api.php?action=delete_recommended_fund&fund_code=' + fundCode);
const result = await response.json();
if (result.success) {
showMessage('基金删除成功', 'success');
loadRecommendedFunds();
} else {
throw new Error(result.message || '操作失败');
}
} catch (error) {
console.error('删除推荐基金失败:', error);
showMessage('操作失败: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// 切换标签页
function switchTab(tabName) {
// 隐藏所有标签内容
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// 移除所有标签的激活状态
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示选中的标签内容
document.getElementById(tabName).classList.add('active');
// 激活选中的标签
event.target.classList.add('active');
// 如果是图表标签,加载默认图表
if (tabName === 'fundCharts') {
const fundSelector = document.getElementById('fundSelector');
if (fundSelector.value) {
loadFundChart(fundSelector.value);
}
} else if (tabName === 'recommendedFunds') {
// 加载推荐基金数据
loadRecommendedFunds();
// 添加搜索事件监听
const searchInput = document.getElementById('searchRecommendedFund');
searchInput.oninput = function() {
searchRecommendedFunds(this.value);
};
}
}
// 加载基金数据
async function loadFundsData() {
try {
showLoading();
const response = await fetch('admin_api.php?action=get_funds');
const result = await response.json();
if (result.success) {
fundsData = result.data;
renderFundsTable();
populateFundSelector();
} else {
throw new Error(result.message || '加载数据失败');
}
} catch (error) {
console.error('加载基金数据失败:', error);
showMessage('数据加载失败: ' + error.message, 'error');
}
}
// 渲染基金表格
function renderFundsTable() {
const tbody = document.getElementById('fundsTableBody');
if (fundsData.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; padding: 40px; color: var(--gray);">
<i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 10px; display: block; opacity: 0.5;"></i>
暂无基金数据,点击"添加基金"开始配置
</td>
</tr>
`;
return;
}
let html = '';
fundsData.forEach((fund, index) => {
const channelName = getChannelName(fund.channel);
const channelClass = getChannelClass(fund.channel);
html += `
<tr>
<td><strong>${fund.fund_code}</strong></td>
<td>${fund.name || '加载中...'}</td>
<td>
<span class="channel-badge ${channelClass}">
${getChannelIcon(fund.channel)} ${channelName}
</span>
</td>
<td>${parseFloat(fund.investment).toFixed(2)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-outline btn-sm" onclick="editFund(${index})">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-danger btn-sm" onclick="deleteFund(${index})">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
// 加载基金名称
loadFundNames();
}
// 加载基金名称
async function loadFundNames() {
const nameCells = document.querySelectorAll('#fundsTableBody td:nth-child(2)');
// 先尝试从localStorage获取缓存的基金名称
const fundNameCache = JSON.parse(localStorage.getItem('fundNameCache') || '{}');
// 收集所有需要获取名称的基金代码
const fundsWithoutNames = fundsData.filter(fund => !fund.name && !fundNameCache[fund.fund_code]);
// 先应用缓存的名称
fundsData.forEach((fund, index) => {
if (fundNameCache[fund.fund_code]) {
nameCells[index].textContent = fundNameCache[fund.fund_code];
fundsData[index].name = fundNameCache[fund.fund_code];
}
});
// 如果有需要获取名称的基金,通过服务器端代理获取
if (fundsWithoutNames.length > 0) {
try {
const fundCodes = fundsWithoutNames.map(fund => fund.fund_code).join(',');
const response = await fetch(`admin_api.php?action=get_fund_names&fund_codes=${fundCodes}`);
const result = await response.json();
if (result.success && result.data) {
// 更新页面显示和缓存
fundsData.forEach((fund, index) => {
if (result.data[fund.fund_code]) {
const fundName = result.data[fund.fund_code];
nameCells[index].textContent = fundName;
fundsData[index].name = fundName;
fundNameCache[fund.fund_code] = fundName;
}
});
// 保存缓存
localStorage.setItem('fundNameCache', JSON.stringify(fundNameCache));
}
} catch (error) {
console.error('获取基金名称失败:', error);
// 即使失败也给每个基金一个默认名称
fundsData.forEach((fund, index) => {
if (!fund.name && !fundNameCache[fund.fund_code]) {
nameCells[index].textContent = `基金${fund.fund_code}`;
}
});
}
}
}
// 填充基金选择器
function populateFundSelector() {
const selector = document.getElementById('fundSelector');
selector.innerHTML = '<option value="">请选择基金</option>';
fundsData.forEach(fund => {
const option = document.createElement('option');
option.value = fund.fund_code;
option.textContent = `${fund.fund_code} - ${fund.name || '加载中...'}`;
selector.appendChild(option);
});
}
// 加载操作日志
async function loadOperationLog() {
try {
const response = await fetch('admin_api.php?action=get_operation_log&limit=50');
const result = await response.json();
if (result.success) {
renderOperationLog(result.data);
} else {
throw new Error(result.message || '加载操作日志失败');
}
} catch (error) {
console.error('加载操作日志失败:', error);
document.getElementById('operationLogContent').innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--gray);">
<i class="fas fa-exclamation-triangle"></i> 加载操作日志失败: ${error.message}
</div>
`;
}
}
// 渲染操作日志
function renderOperationLog(operations) {
const container = document.getElementById('operationLogContent');
if (operations.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--gray);">
<i class="fas fa-inbox"></i> 暂无操作记录
</div>
`;
return;
}
let html = '';
operations.forEach(operation => {
const typeClass = `type-${operation.type}`;
const typeText = {
'add': '添加',
'update': '更新',
'delete': '删除'
}[operation.type] || operation.type;
const channelName = getChannelName(operation.channel);
html += `
<div class="operation-item">
<div class="operation-details">
<span class="operation-type ${typeClass}">${typeText}</span>
<strong>${operation.fund_code}</strong>
${operation.channel ? ` - ${channelName}` : ''}
${operation.investment ? ` - ${operation.investment}` : ''}
${operation.details ? ` - ${operation.details}` : ''}
</div>
<div class="operation-time">${formatTime(operation.date)}</div>
</div>
`;
});
container.innerHTML = html;
}
// 加载基金图表
async function loadFundChart(fundCode) {
if (!fundCode) {
document.getElementById('chartTitle').textContent = '请选择基金查看净值变化';
if (fundChart) {
fundChart.destroy();
}
return;
}
try {
const response = await fetch(`api.php?action=get_fund_chart&fund_code=${fundCode}`);
const result = await response.json();
if (result.success) {
renderFundChart(fundCode, result.data);
} else {
throw new Error(result.message || '加载图表数据失败');
}
} catch (error) {
console.error('加载基金图表失败:', error);
document.getElementById('chartTitle').textContent = `加载图表失败: ${error.message}`;
}
}
// 渲染基金图表
function renderFundChart(fundCode, chartData) {
const ctx = document.getElementById('fundChart').getContext('2d');
const fundName = fundsData.find(f => f.fund_code === fundCode)?.name || fundCode;
document.getElementById('chartTitle').textContent = `${fundName} - 近5日净值变化`;
// 销毁现有图表
if (fundChart) {
fundChart.destroy();
}
// 创建新图表
fundChart = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: [
{
label: '估算净值',
data: chartData.netValues,
borderColor: '#6366f1',
backgroundColor: 'rgba(99, 102, 241, 0.2)',
borderWidth: 3,
fill: true,
tension: 1, // 最大张力,使曲线更平滑
pointRadius: 5,
pointHoverRadius: 8,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#6366f1',
pointBorderWidth: 2,
pointHoverBackgroundColor: '#6366f1',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2
},
{
label: '涨跌幅 (%)',
data: chartData.changes,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.2)',
borderWidth: 3,
fill: false,
tension: 1, // 最大张力,使曲线更平滑
pointRadius: 5,
pointHoverRadius: 8,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#10b981',
pointBorderWidth: 2,
pointHoverBackgroundColor: '#10b981',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
axis: 'x'
},
animation: {
duration: 1500,
easing: 'easeInOutQuart'
},
scales: {
x: {
grid: {
display: false
},
ticks: {
font: {
size: 12
}
}
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '估算净值',
font: {
size: 14,
weight: 'bold'
}
},
grid: {
color: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
font: {
size: 11
},
callback: function(value) {
return value.toFixed(4);
}
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '涨跌幅 (%)',
font: {
size: 14,
weight: 'bold'
}
},
grid: {
drawOnChartArea: false,
},
ticks: {
font: {
size: 11
},
callback: function(value) {
return value > 0 ? '+' + value.toFixed(2) + '%' : value.toFixed(2) + '%';
}
}
}
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 20,
font: {
size: 13
}
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: {
size: 14,
weight: 'bold'
},
bodyFont: {
size: 13
},
borderColor: '#6366f1',
borderWidth: 1,
displayColors: true,
callbacks: {
title: function(tooltipItems) {
return tooltipItems[0].label;
},
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.datasetIndex === 0) {
label += context.parsed.y.toFixed(4);
} else {
const value = context.parsed.y;
label += (value > 0 ? '+' : '') + value.toFixed(2) + '%';
}
return label;
},
afterBody: function(context) {
// 添加额外信息或空行以美化显示
return '';
}
}
}
}
}
});
}
// 显示添加模态框
function showAddModal() {
document.getElementById('modalTitle').textContent = '添加基金';
document.getElementById('fundForm').reset();
document.getElementById('editIndex').value = '';
document.getElementById('fundModal').style.display = 'flex';
}
// 编辑基金
function editFund(index) {
const fund = fundsData[index];
document.getElementById('modalTitle').textContent = '编辑基金';
document.getElementById('fundCode').value = fund.fund_code;
document.getElementById('channel').value = fund.channel;
document.getElementById('investment').value = fund.investment;
document.getElementById('editIndex').value = index;
document.getElementById('fundModal').style.display = 'flex';
}
// 关闭模态框
function closeModal() {
document.getElementById('fundModal').style.display = 'none';
}
// 保存基金
async function saveFund() {
const editIndex = document.getElementById('editIndex').value;
const fundCode = document.getElementById('fundCode').value.trim();
const channel = document.getElementById('channel').value;
const investment = parseFloat(document.getElementById('investment').value);
// 验证表单数据
if (!fundCode) {
showMessage('请输入基金代码', 'error');
return;
}
if (!/^\d{6}$/.test(fundCode)) {
showMessage('基金代码必须是6位数字', 'error');
return;
}
if (!channel) {
showMessage('请选择渠道', 'error');
return;
}
if (!investment || investment <= 0) {
showMessage('投资金额必须大于0', 'error');
return;
}
// 检查重复(添加时检查,编辑时不检查自身)
if (editIndex === '' || fundsData[editIndex].fund_code !== fundCode) {
const exists = fundsData.some((fund, index) =>
fund.fund_code === fundCode && fund.channel === channel &&
(editIndex === '' || index !== parseInt(editIndex))
);
if (exists) {
showMessage('该渠道下已存在相同的基金代码', 'error');
return;
}
}
try {
const action = editIndex === '' ? 'add_fund' : 'update_fund';
const payload = {
fund_code: fundCode,
channel: channel,
investment: investment
};
if (editIndex !== '') {
payload.index = parseInt(editIndex);
}
console.log('发送请求:', action, payload);
const response = await fetch('admin_api.php?action=' + action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const result = await response.json();
console.log('响应结果:', result);
if (result.success) {
showMessage(result.message, 'success');
closeModal();
loadFundsData();
loadOperationLog(); // 刷新操作日志
} else {
throw new Error(result.message || '保存失败');
}
} catch (error) {
console.error('保存基金失败:', error);
showMessage('保存失败: ' + error.message, 'error');
}
}
// 删除基金
async function deleteFund(index) {
if (!confirm('确定要删除这只基金吗?')) {
return;
}
try {
const response = await fetch('admin_api.php?action=delete_fund', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ index: index })
});
const result = await response.json();
if (result.success) {
showMessage(result.message, 'success');
loadFundsData();
loadOperationLog(); // 刷新操作日志
} else {
throw new Error(result.message || '删除失败');
}
} catch (error) {
console.error('删除基金失败:', error);
showMessage('删除失败: ' + error.message, 'error');
}
}
// 工具函数
function getChannelName(channel) {
const channels = {
'0': '招商银行',
'1': '天天基金',
'2': '支付宝'
};
return channels[channel] || '未知渠道';
}
function getChannelClass(channel) {
const classMap = {
'0': 'channel-cmb',
'1': 'channel-tt',
'2': 'channel-zfb'
};
return classMap[channel] || 'channel-cmb';
}
function getChannelIcon(channel) {
const iconMap = {
'0': '🏦',
'1': '📱',
'2': '💙'
};
return iconMap[channel] || '🏦';
}
// 显示消息
function showMessage(message, type) {
const messageEl = document.getElementById('message');
messageEl.textContent = message;
messageEl.className = `alert alert-${type === 'success' ? 'success' : 'error'}`;
messageEl.style.display = 'block';
// 自动隐藏成功消息,错误消息保持显示直到用户操作
if (type === 'success') {
setTimeout(() => {
messageEl.style.display = 'none';
}, 3000);
}
}
// 显示加载状态
function showLoading() {
const tbody = document.getElementById('fundsTableBody');
tbody.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; padding: 40px; color: var(--gray);">
<div style="display: inline-block; width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite;"></div>
加载中...
</td>
</tr>
`;
}
// 格式化时间
function formatTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return '刚刚';
} else if (diff < 3600000) {
return Math.floor(diff / 60000) + '分钟前';
} else if (diff < 86400000) {
return Math.floor(diff / 3600000) + '小时前';
} else {
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modal = document.getElementById('fundModal');
if (event.target === modal) {
closeModal();
}
}

243
admin.php Normal file
View File

@@ -0,0 +1,243 @@
<?php
// 基金监控系统管理页面
// 启动会话
session_start();
// 检查是否已登录
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
// 未登录,重定向到登录页面
header('Location: login.php');
exit;
}
// 可选检查登录时间设置会话超时例如30分钟
$session_timeout = 30 * 60; // 30分钟
if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time']) > $session_timeout) {
// 会话超时,销毁会话并重定向到登录页面
session_unset();
session_destroy();
header('Location: login.php');
exit;
}
// 更新最后活动时间
$_SESSION['last_activity'] = time();
// 注销功能
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
// 记录注销日志
$log_entry = [
'timestamp' => date('Y-m-d H:i:s'),
'action' => '管理员注销',
'ip' => $_SERVER['REMOTE_ADDR']
];
// 保存注销日志
$log_file = 'data/operation_log.json';
if (file_exists($log_file)) {
$logs = json_decode(file_get_contents($log_file), true);
if (!is_array($logs)) {
$logs = [];
}
array_unshift($logs, $log_entry);
// 限制日志数量
if (count($logs) > 500) {
$logs = array_slice($logs, 0, 500);
}
file_put_contents($log_file, json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
// 销毁会话
session_unset();
session_destroy();
header('Location: login.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚀 基金监控 - 管理后台</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="admin.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<h1><i class="fas fa-cogs"></i> 基金监控管理后台</h1>
<p>管理基金代码和虚拟投资金额</p>
<div class="controls">
<button class="btn btn-outline" onclick="window.location.href='index.php'">
<i class="fas fa-arrow-left"></i> 返回监控页面
</button>
<button class="btn btn-danger" onclick="window.location.href='admin.php?action=logout'">
<i class="fas fa-sign-out-alt"></i> 注销
</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<div id="message" class="alert"></div>
<div class="content">
<!-- 标签页 -->
<div class="tabs">
<button class="tab active" onclick="switchTab('fundManagement')">基金配置</button>
<button class="tab" onclick="switchTab('recommendedFunds')">推荐基金管理</button>
<button class="tab" onclick="switchTab('operationLog')">操作日志</button>
<button class="tab" onclick="switchTab('fundCharts')">净值图表</button>
</div>
<!-- 推荐基金管理标签页 -->
<div id="recommendedFunds" class="tab-content">
<div class="section-header">
<h2>推荐基金管理</h2>
<div class="search-box">
<input type="text" id="searchRecommendedFund" placeholder="搜索基金代码...">
</div>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>基金代码</th>
<th>推荐IP</th>
<th>推荐时间</th>
<th>渠道</th>
<th>金额</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="recommendedFundsList">
<!-- 推荐基金列表将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<div class="pagination" id="recommendedFundsPagination">
<!-- 分页控件将通过JavaScript动态加载 -->
</div>
</div>
<!-- 基金配置标签页 -->
<div id="fundManagement" class="tab-content active">
<div class="section-header">
<h2>基金配置管理</h2>
<button class="btn btn-primary" onclick="showAddModal()">
<i class="fas fa-plus"></i> 添加基金
</button>
</div>
<!-- 基金列表表格 -->
<div class="table-container">
<table class="funds-table">
<thead>
<tr>
<th>基金代码</th>
<th>基金名称</th>
<th>所属渠道</th>
<th>投资金额(元)</th>
<th>操作</th>
</tr>
</thead>
<tbody id="fundsTableBody">
<!-- 动态加载数据 -->
</tbody>
</table>
</div>
</div>
<!-- 操作日志标签页 -->
<div id="operationLog" class="tab-content">
<div class="section-header">
<h2>操作日志</h2>
<button class="btn btn-outline" onclick="loadOperationLog()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
<div class="operation-log" id="operationLogContent">
<!-- 操作日志内容 -->
</div>
</div>
<!-- 净值图表标签页 -->
<div id="fundCharts" class="tab-content">
<div class="section-header">
<h2>基金净值变化</h2>
<div>
<select class="fund-selector" id="fundSelector" onchange="loadFundChart(this.value)">
<option value="">请选择基金</option>
</select>
</div>
</div>
<div class="chart-container">
<div class="chart-header">
<div class="chart-title" id="chartTitle">请选择基金查看净值变化</div>
</div>
<div class="chart-wrapper">
<canvas id="fundChart"></canvas>
</div>
</div>
</div>
</div>
<div class="footer">
<p><i class="fas fa-info-circle"></i> 基金监控系统管理后台</p>
</div>
</div>
<!-- 添加/编辑基金模态框 -->
<div class="modal" id="fundModal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="modalTitle">添加基金</h3>
<button class="close-modal" onclick="closeModal()">&times;</button>
</div>
<form id="fundForm">
<input type="hidden" id="editIndex">
<div class="form-group">
<label class="form-label" for="fundCode">基金代码</label>
<input type="text" class="form-control" id="fundCode" required
placeholder="请输入6位基金代码" pattern="[0-9]{6}" maxlength="6">
</div>
<div class="form-group">
<label class="form-label" for="channel">所属渠道</label>
<select class="form-control form-select" id="channel" required>
<option value="">请选择渠道</option>
<option value="0">招商银行</option>
<option value="1">天天基金</option>
<option value="2">支付宝</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="investment">投资金额(元)</label>
<input type="number" class="form-control" id="investment" required
placeholder="请输入投资金额" min="1" step="0.01" value="1000">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeModal()">
取消
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存
</button>
</div>
</form>
</div>
</div>
<script src="admin.js"></script>
</body>
</html>

917
admin_api.php Normal file
View File

@@ -0,0 +1,917 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
class FundAdminAPI {
private $configFile = 'data/fund_config.json';
private $operationFile = 'data/operation_log.json';
private $fundNamesFile = 'data/fund_names.json';
private $recommendedFundsFile = 'data/recommended_funds.json';
public function __construct() {
// 确保数据目录存在
if (!is_dir('data')) {
mkdir('data', 0755, true);
}
// 初始化配置文件(如果不存在)
if (!file_exists($this->configFile)) {
$this->initConfig();
}
// 初始化操作日志文件(如果不存在)
if (!file_exists($this->operationFile)) {
$this->initOperationLog();
}
// 初始化基金名称文件(如果不存在)
if (!file_exists($this->fundNamesFile)) {
$this->initFundNames();
}
// 初始化推荐基金文件(如果不存在)
if (!file_exists($this->recommendedFundsFile)) {
$this->initRecommendedFunds();
}
}
/**
* 初始化配置文件
*/
private function initConfig() {
$defaultConfig = [
[
"channel" => "0",
"fund_code" => "003766",
"investment" => 1000,
"name" => "汇添富医疗服务混合"
],
[
"channel" => "0",
"fund_code" => "004206",
"investment" => 1000,
"name" => "基金004206"
],
[
"channel" => "0",
"fund_code" => "019432",
"investment" => 1000,
"name" => "基金019432"
],
[
"channel" => "1",
"fund_code" => "003766",
"investment" => 1000,
"name" => "汇添富医疗服务混合"
],
[
"channel" => "1",
"fund_code" => "008327",
"investment" => 1000,
"name" => "基金008327"
],
[
"channel" => "2",
"fund_code" => "017811",
"investment" => 1000,
"name" => "基金017811"
]
];
file_put_contents($this->configFile, json_encode($defaultConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* 初始化操作日志
*/
private function initOperationLog() {
$initialLog = [
'operations' => []
];
file_put_contents($this->operationFile, json_encode($initialLog, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* 初始化推荐基金文件
*/
private function initRecommendedFunds() {
$initialData = [];
file_put_contents($this->recommendedFundsFile, json_encode($initialData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* 初始化基金名称文件
*/
private function initFundNames() {
// 从配置中提取已有的基金名称作为初始数据
$defaultFundNames = [];
$config = $this->loadConfig();
foreach ($config as $fund) {
if (isset($fund['fund_code']) && isset($fund['name']) && $fund['name'] !== "基金{$fund['fund_code']}") {
$defaultFundNames[$fund['fund_code']] = $fund['name'];
}
}
// 添加一些常用基金名称
$commonFunds = [
'003766' => '汇添富医疗服务混合',
'110022' => '易方达消费行业股票',
'001714' => '工银文体产业股票',
'000241' => '中欧时代先锋股票',
'001475' => '易方达国防军工混合',
'161725' => '招商中证白酒指数',
'000689' => '前海开源新经济混合',
'001102' => '前海开源国家比较优势混合',
'001593' => '天弘创业板ETF联接',
'000971' => '高升沪深300指数增强'
];
// 合并默认基金名称
$fundNames = array_merge($defaultFundNames, $commonFunds);
file_put_contents($this->fundNamesFile, json_encode($fundNames, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* 加载基金名称映射
*/
private function loadFundNames() {
if (!file_exists($this->fundNamesFile)) {
$this->initFundNames();
}
$data = file_get_contents($this->fundNamesFile);
if (!$data) {
return [];
}
$fundNames = json_decode($data, true);
return is_array($fundNames) ? $fundNames : [];
}
/**
* 保存基金名称映射
*/
private function saveFundNames($fundNames) {
$result = file_put_contents($this->fundNamesFile, json_encode($fundNames, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
if ($result === false) {
error_log('无法写入基金名称文件,请检查目录权限');
return false;
}
return true;
}
/**
* 记录操作日志
*/
private function logOperation($type, $fundCode, $channel = null, $investment = null, $details = '') {
$log = $this->loadOperationLog();
$operation = [
'id' => uniqid(),
'type' => $type, // 'add', 'update', 'delete'
'fund_code' => $fundCode,
'channel' => $channel,
'investment' => $investment,
'details' => $details,
'timestamp' => time(),
'date' => date('Y-m-d H:i:s')
];
$log['operations'][] = $operation;
// 只保留最近100条操作记录
if (count($log['operations']) > 100) {
$log['operations'] = array_slice($log['operations'], -100);
}
file_put_contents($this->operationFile, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $operation;
}
/**
* 加载操作日志
*/
private function loadOperationLog() {
if (!file_exists($this->operationFile)) {
$this->initOperationLog();
}
$data = file_get_contents($this->operationFile);
if (!$data) {
return ['operations' => []];
}
$log = json_decode($data, true);
return is_array($log) ? $log : ['operations' => []];
}
/**
* 获取基金名称优先从文件读取不存在时从API获取并保存
*/
public function getFundName($fundCode) {
try {
// 优先从文件中查找
$fundNames = $this->loadFundNames();
if (isset($fundNames[$fundCode])) {
return [
'success' => true,
'name' => $fundNames[$fundCode]
];
} else {
// 文件中没有尝试从API获取
$fundName = $this->fetchFundNameFromAPI($fundCode);
if ($fundName) {
// 将新获取的基金名称保存到文件中
$fundNames[$fundCode] = $fundName;
$this->saveFundNames($fundNames);
return [
'success' => true,
'name' => $fundName
];
} else {
// 如果API也获取失败尝试从本地数据查找
$config = $this->loadConfig();
$fund = array_filter($config, function($item) use ($fundCode) {
return $item['fund_code'] == $fundCode;
});
$fund = reset($fund);
if ($fund && isset($fund['name'])) {
// 保存到文件中供以后使用
$fundNames[$fundCode] = $fund['name'];
$this->saveFundNames($fundNames);
return [
'success' => true,
'name' => $fund['name']
];
} else {
// 如果都没有,返回默认名称
return [
'success' => true,
'name' => "基金$fundCode"
];
}
}
}
} catch (Exception $e) {
return [
'success' => false,
'message' => '获取基金名称失败: ' . $e->getMessage()
];
}
}
/**
* 获取操作日志
*/
public function getOperationLog($limit = 20) {
try {
$log = $this->loadOperationLog();
$operations = $log['operations'];
// 按时间倒序排列
usort($operations, function($a, $b) {
return $b['timestamp'] - $a['timestamp'];
});
// 限制返回数量
$operations = array_slice($operations, 0, $limit);
return [
'success' => true,
'data' => $operations
];
} catch (Exception $e) {
return [
'success' => false,
'message' => '获取操作日志失败: ' . $e->getMessage()
];
}
}
/**
* 加载基金配置
*/
private function loadConfig() {
if (!file_exists($this->configFile)) {
$this->initConfig();
}
$data = file_get_contents($this->configFile);
if (!$data) {
return [];
}
$config = json_decode($data, true);
return is_array($config) ? $config : [];
}
/**
* 保存基金配置
*/
private function saveConfig($config) {
$result = file_put_contents($this->configFile, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
if ($result === false) {
throw new Exception('无法写入配置文件,请检查目录权限');
}
return true;
}
/**
* 获取所有基金配置
*/
public function getFunds() {
try {
$config = $this->loadConfig();
return [
'success' => true,
'data' => $config
];
} catch (Exception $e) {
return [
'success' => false,
'message' => '获取基金配置失败: ' . $e->getMessage()
];
}
}
/**
* 添加基金
*/
public function addFund($fundData) {
try {
// 调试:记录接收到的数据
error_log("addFund received: " . print_r($fundData, true));
$config = $this->loadConfig();
// 验证数据
if (!isset($fundData['fund_code']) || trim($fundData['fund_code']) === '') {
return [
'success' => false,
'message' => '基金代码不能为空'
];
}
if (!isset($fundData['channel']) || trim($fundData['channel']) === '') {
return [
'success' => false,
'message' => '渠道不能为空'
];
}
$fundCode = trim($fundData['fund_code']);
$channel = trim($fundData['channel']);
// 验证基金代码格式
if (!preg_match('/^\d{6}$/', $fundCode)) {
return [
'success' => false,
'message' => '基金代码必须是6位数字'
];
}
// 检查是否已存在
foreach ($config as $item) {
if ($item['fund_code'] === $fundCode && $item['channel'] === $channel) {
return [
'success' => false,
'message' => '该渠道下已存在相同的基金代码'
];
}
}
// 添加新基金
// 获取基金名称
$fundName = "基金{$fundCode}";
$fundNames = [
'003766' => '汇添富医疗服务混合',
'110022' => '易方达消费行业股票'
];
if (isset($fundNames[$fundCode])) {
$fundName = $fundNames[$fundCode];
}
$newFund = [
'channel' => $channel,
'fund_code' => $fundCode,
'investment' => isset($fundData['investment']) ? floatval($fundData['investment']) : 1000,
'name' => $fundName
];
$config[] = $newFund;
if ($this->saveConfig($config)) {
// 记录操作日志
$this->logOperation('add', $fundCode, $channel, $newFund['investment'], '添加基金');
return [
'success' => true,
'message' => '基金添加成功'
];
} else {
return [
'success' => false,
'message' => '保存配置失败'
];
}
} catch (Exception $e) {
return [
'success' => false,
'message' => '添加基金失败: ' . $e->getMessage()
];
}
}
/**
* 更新基金
*/
public function updateFund($fundData) {
try {
// 调试:记录接收到的数据
error_log("updateFund received: " . print_r($fundData, true));
$config = $this->loadConfig();
$index = isset($fundData['index']) ? intval($fundData['index']) : -1;
// 验证索引
if ($index < 0 || !isset($config[$index])) {
return [
'success' => false,
'message' => '基金不存在'
];
}
// 验证数据
if (!isset($fundData['fund_code']) || trim($fundData['fund_code']) === '') {
return [
'success' => false,
'message' => '基金代码不能为空'
];
}
if (!isset($fundData['channel']) || trim($fundData['channel']) === '') {
return [
'success' => false,
'message' => '渠道不能为空'
];
}
$fundCode = trim($fundData['fund_code']);
$channel = trim($fundData['channel']);
// 获取旧数据用于日志
$oldFund = $config[$index];
// 检查重复(排除自身)
foreach ($config as $i => $item) {
if ($i !== $index && $item['fund_code'] === $fundCode && $item['channel'] === $channel) {
return [
'success' => false,
'message' => '该渠道下已存在相同的基金代码'
];
}
}
// 更新基金信息
$config[$index] = [
'channel' => $channel,
'fund_code' => $fundCode,
'investment' => isset($fundData['investment']) ? floatval($fundData['investment']) : 1000,
'name' => $oldFund['name'] ?? "基金{$fundCode}"
];
if ($this->saveConfig($config)) {
// 记录操作日志
$details = sprintf('更新基金: 投资金额 %s -> %s',
$oldFund['investment'],
$config[$index]['investment']
);
$this->logOperation('update', $fundCode, $channel, $config[$index]['investment'], $details);
return [
'success' => true,
'message' => '基金更新成功'
];
} else {
return [
'success' => false,
'message' => '保存配置失败'
];
}
} catch (Exception $e) {
return [
'success' => false,
'message' => '更新基金失败: ' . $e->getMessage()
];
}
}
/**
* 删除基金
*/
public function deleteFund($index) {
try {
$config = $this->loadConfig();
$index = intval($index);
// 验证索引
if (!isset($config[$index])) {
return [
'success' => false,
'message' => '基金不存在'
];
}
// 获取要删除的基金信息用于日志
$deletedFund = $config[$index];
// 删除基金
array_splice($config, $index, 1);
if ($this->saveConfig($config)) {
// 记录操作日志
$this->logOperation('delete', $deletedFund['fund_code'], $deletedFund['channel'], $deletedFund['investment'], '删除基金');
return [
'success' => true,
'message' => '基金删除成功'
];
} else {
return [
'success' => false,
'message' => '保存配置失败'
];
}
} catch (Exception $e) {
return [
'success' => false,
'message' => '删除基金失败: ' . $e->getMessage()
];
}
}
/**
* 获取基金名称列表优先从文件读取不存在时从API获取并保存
*/
public function getFundNames() {
$fundCodes = $_GET['fund_codes'] ?? '';
if (empty($fundCodes)) {
return [
'success' => false,
'message' => '基金代码不能为空'
];
}
$codes = explode(',', $fundCodes);
$results = [];
$fundNames = $this->loadFundNames();
foreach ($codes as $code) {
$code = trim($code);
if (!empty($code)) {
// 先从文件中查找
if (isset($fundNames[$code])) {
$results[$code] = $fundNames[$code];
} else {
// 文件中没有则从API获取
$fundName = $this->fetchFundNameFromAPI($code);
if ($fundName) {
$results[$code] = $fundName;
// 将新获取的基金名称保存到文件中
$fundNames[$code] = $fundName;
$this->saveFundNames($fundNames);
} else {
// 如果API也获取失败使用默认名称
$results[$code] = "基金$code";
}
}
}
}
return [
'success' => true,
'data' => $results,
'message' => '获取基金名称成功'
];
}
/**
* 获取推荐基金列表
*/
private function getRecommendedFunds() {
try {
$recommendedFunds = [];
if (file_exists($this->recommendedFundsFile)) {
$data = file_get_contents($this->recommendedFundsFile);
$recommendedFunds = json_decode($data, true);
if (!is_array($recommendedFunds)) {
$recommendedFunds = [];
}
}
// 按时间倒序排序
usort($recommendedFunds, function($a, $b) {
return strtotime($b['timestamp']) - strtotime($a['timestamp']);
});
echo json_encode(['success' => true, 'data' => $recommendedFunds], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '获取推荐基金失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
}
/**
* 批准推荐基金
*/
private function approveRecommendedFund() {
try {
$fundCode = isset($_GET['fund_code']) ? $_GET['fund_code'] : '';
if (empty($fundCode)) {
echo json_encode(['success' => false, 'message' => '基金代码不能为空'], JSON_UNESCAPED_UNICODE);
return;
}
// 加载推荐基金数据
$recommendedFunds = [];
if (file_exists($this->recommendedFundsFile)) {
$data = file_get_contents($this->recommendedFundsFile);
$recommendedFunds = json_decode($data, true);
if (!is_array($recommendedFunds)) {
$recommendedFunds = [];
}
}
// 查找并更新基金状态
$found = false;
foreach ($recommendedFunds as &$fund) {
if ($fund['fund_code'] == $fundCode) {
$fund['status'] = 'approved';
$found = true;
break;
}
}
if (!$found) {
echo json_encode(['success' => false, 'message' => '未找到该推荐基金'], JSON_UNESCAPED_UNICODE);
return;
}
// 保存更新后的数据
file_put_contents($this->recommendedFundsFile, json_encode($recommendedFunds, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 添加到基金配置中
$config = $this->loadConfig();
$fundExists = false;
foreach ($config as $existingFund) {
if ($existingFund['fund_code'] == $fundCode && $existingFund['channel'] == '0') {
$fundExists = true;
break;
}
}
if (!$fundExists) {
$newFund = [
'channel' => '2', // 2表示支付宝渠道
'fund_code' => $fundCode,
'investment' => 1000,
'name' => '基金' . $fundCode
];
$config[] = $newFund;
$this->saveConfig($config);
}
// 记录操作日志
$this->logOperation('approve_recommended_fund', ['fund_code' => $fundCode]);
echo json_encode(['success' => true, 'message' => '基金批准成功'], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '批准基金失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
}
/**
* 删除推荐基金
*/
private function deleteRecommendedFund() {
try {
$fundCode = isset($_GET['fund_code']) ? $_GET['fund_code'] : '';
if (empty($fundCode)) {
echo json_encode(['success' => false, 'message' => '基金代码不能为空'], JSON_UNESCAPED_UNICODE);
return;
}
// 加载推荐基金数据
$recommendedFunds = [];
if (file_exists($this->recommendedFundsFile)) {
$data = file_get_contents($this->recommendedFundsFile);
$recommendedFunds = json_decode($data, true);
if (!is_array($recommendedFunds)) {
$recommendedFunds = [];
}
}
// 过滤掉要删除的基金
$filteredFunds = array_filter($recommendedFunds, function($fund) use ($fundCode) {
return $fund['fund_code'] != $fundCode;
});
// 保存过滤后的数据
file_put_contents($this->recommendedFundsFile, json_encode(array_values($filteredFunds), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 记录操作日志
$this->logOperation('delete_recommended_fund', ['fund_code' => $fundCode]);
echo json_encode(['success' => true, 'message' => '基金删除成功'], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '删除基金失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
}
/**
* 从API获取基金名称只在文件中不存在时调用
*/
private function fetchFundNameFromAPI($fundCode) {
// 尝试多个API源
$apiSources = [
"https://fundgz.1234567.com.cn/js/{$fundCode}.js?rt=" . time(),
"https://api.doctorxiong.club/v1/fund/detail?code={$fundCode}"
];
foreach ($apiSources as $apiUrl) {
try {
// 设置超时时间
$context = stream_context_create([
'http' => [
'timeout' => 5,
'header' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
]
]);
$response = file_get_contents($apiUrl, false, $context);
if ($response) {
// 处理不同API的响应格式
if (strpos($apiUrl, 'fundgz.1234567.com.cn') !== false) {
// 处理JSONP格式
if (preg_match('/jsonpgz\((.*)\)/', $response, $matches)) {
$data = json_decode($matches[1], true);
if (isset($data['name']) && !empty($data['name'])) {
return $data['name'];
}
}
} else if (strpos($apiUrl, 'doctorxiong.club') !== false) {
// 处理JSON格式
$data = json_decode($response, true);
if (isset($data['data']['name']) && !empty($data['data']['name'])) {
return $data['data']['name'];
}
}
}
} catch (Exception $e) {
// 记录错误但继续尝试下一个API源
error_log("获取基金{$fundCode}名称失败: " . $e->getMessage());
continue;
}
}
// 所有API都失败返回false
return false;
}
/**
* 处理API请求
*/
public function handleRequest() {
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'OPTIONS') {
exit(0);
}
// 解析请求参数
$queryString = $_SERVER['QUERY_STRING'] ?? '';
parse_str($queryString, $params);
$action = $params['action'] ?? '';
if ($method === 'GET') {
switch ($action) {
case 'get_funds':
$result = $this->getFunds();
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
case 'get_operation_log':
$limit = isset($params['limit']) ? intval($params['limit']) : 20;
$result = $this->getOperationLog($limit);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
case 'get_fund_name':
$fundCode = $params['fund_code'] ?? '';
if ($fundCode) {
$result = $this->getFundName($fundCode);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
} else {
echo json_encode(['success' => false, 'message' => '基金代码不能为空']);
}
break;
case 'get_fund_names':
$result = $this->getFundNames();
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
case 'get_recommended_funds':
$this->getRecommendedFunds();
break;
case 'approve_recommended_fund':
$this->approveRecommendedFund();
break;
case 'delete_recommended_fund':
$this->deleteRecommendedFund();
break;
default:
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Endpoint not found']);
}
} elseif ($method === 'POST') {
// 获取POST数据
$input = file_get_contents('php://input');
// 调试:记录原始输入
error_log("Raw input: " . $input);
$postData = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '无效的JSON数据: ' . json_last_error_msg(),
'debug_input' => $input
]);
return;
}
// 调试:记录解析后的数据
error_log("Parsed data: " . print_r($postData, true));
switch ($action) {
case 'add_fund':
$result = $this->addFund($postData);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
case 'update_fund':
$result = $this->updateFund($postData);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
case 'delete_fund':
$result = $this->deleteFund($postData['index']);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
break;
default:
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Endpoint not found']);
}
} else {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
}
}
}
// 错误处理
try {
$api = new FundAdminAPI();
$api->handleRequest();
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
?>

1165
api.php Normal file

File diff suppressed because it is too large Load Diff

5
composer.json Normal file
View File

@@ -0,0 +1,5 @@
{
"require": {
"ccxt/ccxt": "^1.53"
}
}

446
composer.lock generated Normal file
View File

@@ -0,0 +1,446 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7fe6ed9149491f865075a222db8f3a4a",
"packages": [
{
"name": "ccxt/ccxt",
"version": "1.53.1",
"source": {
"type": "git",
"url": "https://github.com/ccxt/ccxt.git",
"reference": "df03acf6997ff35ee84abadc80bebcbe13e5efdc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ccxt/ccxt/zipball/df03acf6997ff35ee84abadc80bebcbe13e5efdc",
"reference": "df03acf6997ff35ee84abadc80bebcbe13e5efdc",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"ext-curl": "*",
"ext-iconv": "*",
"ext-json": "*",
"ext-openssl": "*",
"ext-pcre": "*",
"pear/console_table": "1.3.1",
"php": ">=5.4.0",
"symfony/polyfill-mbstring": "^1.7"
},
"suggest": {
"clue/buzz-react": "Required for asynchronous API calls to exchanges with PHP",
"clue/http-proxy-react": "Required for using a proxy when doing asynchronous API calls to exchanges with PHP",
"react/event-loop": "Required for asynchronous API calls to exchanges with PHP",
"recoil/react": "Required for asynchronous API calls to exchanges with PHP",
"recoil/recoil": "Required for asynchronous API calls to exchanges with PHP"
},
"type": "library",
"autoload": {
"files": [
"ccxt.php"
],
"psr-4": {
"ccxt\\": "php/",
"ccxt_async\\": "php/async/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Kroitor",
"email": "igor.kroitor@gmail.com",
"homepage": "https://github.com/kroitor",
"role": "Developer"
},
{
"name": "Vitaly Gordon",
"email": "rocket.mind@gmail.com",
"homepage": "https://github.com/xpl",
"role": "Developer"
},
{
"name": "Carlo Revelli",
"email": "carlo.revelli@berkeley.edu",
"homepage": "https://github.com/frosty00",
"role": "Junior Developer"
}
],
"description": "A JavaScript / Python / PHP cryptocurrency trading library with support for more than 90 bitcoin/altcoin exchanges",
"homepage": "https://github.com/ccxt/ccxt",
"keywords": [
"1BTCXE",
"1Broker",
"ACX",
"ANX",
"ANXPro",
"BL3P",
"BTC Markets",
"BTC Trade UA",
"BTCC",
"BTCChina",
"BTCExchange",
"BTCTrader",
"BTCTurk",
"BTCX",
"BX.in.th",
"Bit2C",
"BitBay",
"BitBays",
"BitMEX",
"BitMarket",
"Bitcoin.co.id",
"Bleutrade",
"BlinkTrade",
"BtcBox",
"Bter.com",
"C-CEX",
"CEX.IO",
"CHBTC",
"CNY",
"ChileBit",
"CoinMate",
"CoinSpot",
"Coinsecure",
"Crypto Capital",
"DOGE",
"EUR",
"EXMO",
"FYB-SE",
"FYB-SG",
"FoxBit",
"Gatecoin",
"Gemini",
"Huobi",
"HuobiPRO",
"LakeBTC",
"Liqui",
"LiveCoin",
"OKCoin",
"OKCoin.cn",
"OKCoin.com",
"OKEX",
"Paymium",
"Poloniex",
"QUOINE",
"Qryptos",
"QuadrigaCX",
"Southxchange",
"SurBitcoin",
"TheRockTrading",
"Tidex",
"USD",
"UrduBit",
"VBTC",
"Vaultoro",
"VirWoX",
"Wex",
"YoBit",
"Zaif",
"acx.io",
"algorithmic",
"algotrading",
"altcoin",
"altcoins",
"api",
"arbitrage",
"backtest",
"backtesting",
"binance",
"binance.com",
"bit2c.co.il",
"bitcoin",
"bitcoincoid",
"bitfinex",
"bitflyer",
"bitflyer.jp",
"bithumb",
"bithumb.com",
"bitlish",
"bitso",
"bitstamp",
"bittrex",
"bleutrade.com",
"bot",
"btc",
"btc-e",
"btc-trade.com.ua",
"btc-x",
"btcbox.co.jp",
"btce",
"btcexchange.ph",
"btcmarkets",
"btcmarkets.net",
"btctrader.com",
"btcturk.com",
"bter",
"ccex",
"cex",
"chilebit.net",
"coin",
"coincheck",
"coingi",
"coingi.com",
"coinmarketcap",
"coins",
"coinspot.com.au",
"crypto",
"crypto currency",
"crypto market",
"cryptocapital.co",
"cryptocurrency",
"currencies",
"currency",
"darkcoin",
"dash",
"digital currency",
"dogecoin",
"dsx",
"dsx.uk",
"e-commerce",
"etc",
"eth",
"ether",
"ethereum",
"exchange",
"exchanges",
"flowBTC",
"flowbtc.com",
"foxbit.exchange",
"framework",
"gdax",
"hitbtc",
"huobi.pro",
"independent reserve",
"independentreserve.com",
"invest",
"investing",
"investor",
"itBit",
"jubi.com",
"kraken",
"lakebtc.com",
"library",
"light",
"liqui.io",
"litecoin",
"ltc",
"luno",
"market",
"market data",
"markets",
"mercado",
"mercadobitcoin",
"mercadobitcoin.br",
"merchandise",
"merchant",
"minimal",
"mixcoins",
"mixcoins.com",
"nova",
"novaexchange",
"novaexchange.com",
"okex.com",
"order",
"order book",
"orderbook",
"price",
"price data",
"pricefeed",
"private",
"public",
"ripple",
"strategy",
"surbitcoin.com",
"tidex.com",
"toolkit",
"trade",
"trader",
"trading",
"urdubit.com",
"vbtc.exchange",
"vbtc.vn",
"volume",
"wex.nz",
"xBTCe",
"xbt",
"xbtce.com",
"xrp",
"yobit.net",
"yunbi",
"zec",
"zerocoin"
],
"support": {
"issues": "https://github.com/ccxt/ccxt/issues",
"source": "https://github.com/ccxt/ccxt/tree/1.53.1"
},
"funding": [
{
"url": "https://opencollective.com/ccxt",
"type": "open_collective"
}
],
"time": "2021-07-13T22:50:19+00:00"
},
{
"name": "pear/console_table",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/pear/Console_Table.git",
"reference": "1930c11897ca61fd24b95f2f785e99e0f36dcdea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pear/Console_Table/zipball/1930c11897ca61fd24b95f2f785e99e0f36dcdea",
"reference": "1930c11897ca61fd24b95f2f785e99e0f36dcdea",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"suggest": {
"pear/Console_Color2": ">=0.1.2"
},
"type": "library",
"autoload": {
"classmap": [
"Table.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Jan Schneider",
"homepage": "http://pear.php.net/user/yunosh"
},
{
"name": "Tal Peer",
"homepage": "http://pear.php.net/user/tal"
},
{
"name": "Xavier Noguer",
"homepage": "http://pear.php.net/user/xnoguer"
},
{
"name": "Richard Heyes",
"homepage": "http://pear.php.net/user/richard"
}
],
"description": "Library that makes it easy to build console style tables.",
"homepage": "http://pear.php.net/package/Console_Table/",
"keywords": [
"console"
],
"support": {
"issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Table",
"source": "https://github.com/pear/Console_Table"
},
"time": "2018-01-25T20:47:17+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

1
data/cache/fund_001071.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"001071","name":"华安媒体互联网混合A","jzrq":"2025-11-04","dwjz":"4.0040","gsz":"4.0034","gszzl":"-0.01","gztime":"2025-11-05 14:32"}

1
data/cache/fund_003567.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"003567","name":"华夏行业景气混合A","jzrq":"2025-11-04","dwjz":"4.7305","gsz":"4.7463","gszzl":"0.33","gztime":"2025-11-05 14:31"}

1
data/cache/fund_003598.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"003598","name":"华商润丰灵活配置混合A","jzrq":"2025-11-17","dwjz":"4.4470","gsz":"4.4728","gszzl":"0.58","gztime":"2025-11-18 11:30"}

1
data/cache/fund_003766.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"003766","name":"广发创业板ETF发起式联接C","jzrq":"2025-11-17","dwjz":"1.6787","gsz":"1.6715","gszzl":"-0.43","gztime":"2025-11-18 11:30"}

1
data/cache/fund_004206.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"004206","name":"华商元亨混合A","jzrq":"2025-11-17","dwjz":"2.8457","gsz":"2.8656","gszzl":"0.70","gztime":"2025-11-18 11:30"}

1
data/cache/fund_005965.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"005965","name":"安信中证500指数增强A","jzrq":"2025-11-05","dwjz":"2.2797","gsz":"2.3164","gszzl":"1.61","gztime":"2025-11-06 15:00"}

1
data/cache/fund_008327.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"008327","name":"东财通信C","jzrq":"2025-11-17","dwjz":"2.2664","gsz":"2.3015","gszzl":"1.55","gztime":"2025-11-18 11:30"}

1
data/cache/fund_011103.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"011103","name":"天弘中证光伏产业指数C","jzrq":"2025-11-17","dwjz":"0.8674","gsz":"0.8492","gszzl":"-2.10","gztime":"2025-11-18 11:30"}

1
data/cache/fund_011815.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"011815","name":"恒越优势精选混合","jzrq":"2025-11-17","dwjz":"1.4577","gsz":"1.4411","gszzl":"-1.14","gztime":"2025-11-18 11:30"}

1
data/cache/fund_012863.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"012863","name":"汇添富中证电池主题ETF发起式联接C","jzrq":"2025-11-17","dwjz":"0.8622","gsz":"0.8287","gszzl":"-3.89","gztime":"2025-11-18 11:30"}

1
data/cache/fund_014320.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"014320","name":"德邦半导体产业混合发起式C","jzrq":"2025-11-04","dwjz":"1.8891","gsz":"1.8863","gszzl":"-0.15","gztime":"2025-11-05 14:30"}

1
data/cache/fund_017560.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"017560","name":"华安上证科创板芯片ETF发起式联接C","jzrq":"2025-11-17","dwjz":"2.0604","gsz":"2.1035","gszzl":"2.09","gztime":"2025-11-18 11:30"}

1
data/cache/fund_018994.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"018994","name":"中欧数字经济混合发起C","jzrq":"2025-11-04","dwjz":"2.8974","gsz":"2.8941","gszzl":"-0.11","gztime":"2025-11-05 14:31"}

1
data/cache/fund_019432.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"019432","name":"永赢睿信混合C","jzrq":"2025-11-04","dwjz":"2.0302","gsz":"2.0390","gszzl":"0.43","gztime":"2025-11-05 14:31"}

1
data/cache/fund_022364.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"022364","name":"永赢科技智选混合发起A","jzrq":"2025-11-17","dwjz":"3.2462","gsz":"3.2874","gszzl":"1.27","gztime":"2025-11-18 11:30"}

1
data/cache/fund_022365.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"022365","name":"永赢科技智选混合发起C","jzrq":"2025-11-17","dwjz":"3.2251","gsz":"3.2661","gszzl":"1.27","gztime":"2025-11-18 11:30"}

1
data/cache/fund_023350.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fundcode":"023350","name":"诺安多策略混合C","jzrq":"2025-11-17","dwjz":"3.4440","gsz":"3.4001","gszzl":"-1.27","gztime":"2025-11-18 11:30"}

70
data/fund_config.json Normal file
View File

@@ -0,0 +1,70 @@
[
{
"channel": "1",
"fund_code": "003766",
"investment": 1000
},
{
"channel": "1",
"fund_code": "008327",
"investment": 1000
},
{
"channel": "1",
"fund_code": "012863",
"investment": 10000
},
{
"channel": "1",
"fund_code": "023350",
"investment": 1000
},
{
"channel": "1",
"fund_code": "017560",
"investment": 1000,
"name": "基金017560"
},
{
"channel": "2",
"fund_code": "011815",
"investment": 1000,
"name": "基金011815"
},
{
"channel": "2",
"fund_code": "003598",
"investment": 1000,
"name": "基金003598"
},
{
"channel": "2",
"fund_code": "004206",
"investment": 1000,
"name": "基金004206"
},
{
"channel": "2",
"fund_code": "022365",
"investment": 1000,
"name": "基金022365"
},
{
"channel": "2",
"fund_code": "022364",
"investment": 1000,
"name": "基金022364"
},
{
"channel": "2",
"fund_code": "011103",
"investment": 1000,
"name": "基金011103"
},
{
"channel": "0",
"fund_code": "025825",
"investment": 1000,
"name": "基金025825"
}
]

417
data/fund_daily_data.json Normal file
View File

@@ -0,0 +1,417 @@
{
"last_update": "2025-11-18",
"funds": {
"003766": [
{
"date": "2025-11-13",
"dwjz": "1.6881",
"gsz": "1.7333",
"gszzl": 2.68,
"name": "广发创业板ETF发起式联接C"
},
{
"date": "2025-11-14",
"dwjz": "1.6881",
"gsz": "1.7311",
"gszzl": 2.55,
"name": "广发创业板ETF发起式联接C"
},
{
"date": "2025-11-15",
"dwjz": "1.7298",
"gsz": "1.6810",
"gszzl": -2.82,
"name": "广发创业板ETF发起式联接C"
},
{
"date": "2025-11-17",
"dwjz": "1.6821",
"gsz": "1.6687",
"gszzl": -0.8,
"name": "广发创业板ETF发起式联接C"
},
{
"date": "2025-11-18",
"dwjz": "1.6787",
"gsz": "1.6715",
"gszzl": -0.43,
"name": "广发创业板ETF发起式联接C"
}
],
"004206": [
{
"date": "2025-11-13",
"dwjz": "2.8892",
"gsz": "2.8806",
"gszzl": -0.3,
"name": "华商元亨混合A"
},
{
"date": "2025-11-14",
"dwjz": "2.8892",
"gsz": "2.8838",
"gszzl": -0.19,
"name": "华商元亨混合A"
},
{
"date": "2025-11-15",
"dwjz": "2.8883",
"gsz": "2.8402",
"gszzl": -1.67,
"name": "华商元亨混合A"
},
{
"date": "2025-11-17",
"dwjz": "2.8446",
"gsz": "2.8475",
"gszzl": 0.1,
"name": "华商元亨混合A"
},
{
"date": "2025-11-18",
"dwjz": "2.8457",
"gsz": "2.8656",
"gszzl": 0.7,
"name": "华商元亨混合A"
}
],
"019432": [],
"003598": [
{
"date": "2025-11-13",
"dwjz": "4.5150",
"gsz": "4.5074",
"gszzl": -0.17,
"name": "华商润丰灵活配置混合A"
},
{
"date": "2025-11-14",
"dwjz": "4.5150",
"gsz": "4.5110",
"gszzl": -0.09,
"name": "华商润丰灵活配置混合A"
},
{
"date": "2025-11-15",
"dwjz": "4.5140",
"gsz": "4.4384",
"gszzl": -1.68,
"name": "华商润丰灵活配置混合A"
},
{
"date": "2025-11-17",
"dwjz": "4.4460",
"gsz": "4.4501",
"gszzl": 0.09,
"name": "华商润丰灵活配置混合A"
},
{
"date": "2025-11-18",
"dwjz": "4.4470",
"gsz": "4.4728",
"gszzl": 0.58,
"name": "华商润丰灵活配置混合A"
}
],
"001071": [],
"018994": [],
"003567": [],
"008327": [
{
"date": "2025-11-13",
"dwjz": "2.3307",
"gsz": "2.3179",
"gszzl": -0.55,
"name": "东财通信C"
},
{
"date": "2025-11-14",
"dwjz": "2.3307",
"gsz": "2.3211",
"gszzl": -0.41,
"name": "东财通信C"
},
{
"date": "2025-11-15",
"dwjz": "2.3215",
"gsz": "2.2421",
"gszzl": -3.42,
"name": "东财通信C"
},
{
"date": "2025-11-17",
"dwjz": "2.2458",
"gsz": "2.2503",
"gszzl": 0.2,
"name": "东财通信C"
},
{
"date": "2025-11-18",
"dwjz": "2.2664",
"gsz": "2.3015",
"gszzl": 1.55,
"name": "东财通信C"
}
],
"012863": [
{
"date": "2025-11-13",
"dwjz": "0.8305",
"gsz": "0.8876",
"gszzl": 6.88,
"name": "汇添富中证电池主题ETF发起式联接C"
},
{
"date": "2025-11-14",
"dwjz": "0.8305",
"gsz": "0.8790",
"gszzl": 5.84,
"name": "汇添富中证电池主题ETF发起式联接C"
},
{
"date": "2025-11-15",
"dwjz": "0.8749",
"gsz": "0.8536",
"gszzl": -2.43,
"name": "汇添富中证电池主题ETF发起式联接C"
},
{
"date": "2025-11-17",
"dwjz": "0.8554",
"gsz": "0.8533",
"gszzl": -0.25,
"name": "汇添富中证电池主题ETF发起式联接C"
},
{
"date": "2025-11-18",
"dwjz": "0.8622",
"gsz": "0.8287",
"gszzl": -3.89,
"name": "汇添富中证电池主题ETF发起式联接C"
}
],
"017560": [
{
"date": "2025-11-13",
"dwjz": "2.0965",
"gsz": "2.1388",
"gszzl": 2.02,
"name": "华安上证科创板芯片ETF发起式联接C"
},
{
"date": "2025-11-14",
"dwjz": "2.0965",
"gsz": "2.1298",
"gszzl": 1.59,
"name": "华安上证科创板芯片ETF发起式联接C"
},
{
"date": "2025-11-15",
"dwjz": "2.1276",
"gsz": "2.0595",
"gszzl": -3.2,
"name": "华安上证科创板芯片ETF发起式联接C"
},
{
"date": "2025-11-17",
"dwjz": "2.0636",
"gsz": "2.0543",
"gszzl": -0.45,
"name": "华安上证科创板芯片ETF发起式联接C"
},
{
"date": "2025-11-18",
"dwjz": "2.0604",
"gsz": "2.1035",
"gszzl": 2.09,
"name": "华安上证科创板芯片ETF发起式联接C"
}
],
"014320": [],
"023350": [
{
"date": "2025-11-13",
"dwjz": "3.3700",
"gsz": "3.3903",
"gszzl": 0.6,
"name": "诺安多策略混合C"
},
{
"date": "2025-11-14",
"dwjz": "3.3700",
"gsz": "3.3953",
"gszzl": 0.75,
"name": "诺安多策略混合C"
},
{
"date": "2025-11-15",
"dwjz": "3.3960",
"gsz": "3.3999",
"gszzl": 0.12,
"name": "诺安多策略混合C"
},
{
"date": "2025-11-17",
"dwjz": "3.4210",
"gsz": "3.4273",
"gszzl": 0.19,
"name": "诺安多策略混合C"
},
{
"date": "2025-11-18",
"dwjz": "3.4440",
"gsz": "3.4001",
"gszzl": -1.27,
"name": "诺安多策略混合C"
}
],
"011815": [
{
"date": "2025-11-13",
"dwjz": "1.4696",
"gsz": "1.4957",
"gszzl": 1.77,
"name": "恒越优势精选混合"
},
{
"date": "2025-11-14",
"dwjz": "1.4696",
"gsz": "1.4972",
"gszzl": 1.88,
"name": "恒越优势精选混合"
},
{
"date": "2025-11-15",
"dwjz": "1.5174",
"gsz": "1.4674",
"gszzl": -3.3,
"name": "恒越优势精选混合"
},
{
"date": "2025-11-17",
"dwjz": "1.4704",
"gsz": "1.4620",
"gszzl": -0.57,
"name": "恒越优势精选混合"
},
{
"date": "2025-11-18",
"dwjz": "1.4577",
"gsz": "1.4411",
"gszzl": -1.14,
"name": "恒越优势精选混合"
}
],
"022365": [
{
"date": "2025-11-13",
"dwjz": "3.3255",
"gsz": "3.3140",
"gszzl": -0.35,
"name": "永赢科技智选混合发起C"
},
{
"date": "2025-11-14",
"dwjz": "3.3255",
"gsz": "3.3135",
"gszzl": -0.36,
"name": "永赢科技智选混合发起C"
},
{
"date": "2025-11-15",
"dwjz": "3.3157",
"gsz": "3.2108",
"gszzl": -3.16,
"name": "永赢科技智选混合发起C"
},
{
"date": "2025-11-17",
"dwjz": "3.2003",
"gsz": "3.1977",
"gszzl": -0.08,
"name": "永赢科技智选混合发起C"
},
{
"date": "2025-11-18",
"dwjz": "3.2251",
"gsz": "3.2661",
"gszzl": 1.27,
"name": "永赢科技智选混合发起C"
}
],
"022364": [
{
"date": "2025-11-13",
"dwjz": "3.3470",
"gsz": "3.3354",
"gszzl": -0.35,
"name": "永赢科技智选混合发起A"
},
{
"date": "2025-11-14",
"dwjz": "3.3470",
"gsz": "3.3349",
"gszzl": -0.36,
"name": "永赢科技智选混合发起A"
},
{
"date": "2025-11-15",
"dwjz": "3.3372",
"gsz": "3.2316",
"gszzl": -3.16,
"name": "永赢科技智选混合发起A"
},
{
"date": "2025-11-17",
"dwjz": "3.2210",
"gsz": "3.2184",
"gszzl": -0.08,
"name": "永赢科技智选混合发起A"
},
{
"date": "2025-11-18",
"dwjz": "3.2462",
"gsz": "3.2874",
"gszzl": 1.27,
"name": "永赢科技智选混合发起A"
}
],
"011103": [
{
"date": "2025-11-13",
"dwjz": "0.8716",
"gsz": "0.8954",
"gszzl": 2.73,
"name": "天弘中证光伏产业指数C"
},
{
"date": "2025-11-14",
"dwjz": "0.8716",
"gsz": "0.8887",
"gszzl": 1.96,
"name": "天弘中证光伏产业指数C"
},
{
"date": "2025-11-15",
"dwjz": "0.8875",
"gsz": "0.8779",
"gszzl": -1.08,
"name": "天弘中证光伏产业指数C"
},
{
"date": "2025-11-17",
"dwjz": "0.8786",
"gsz": "0.8612",
"gszzl": -1.98,
"name": "天弘中证光伏产业指数C"
},
{
"date": "2025-11-18",
"dwjz": "0.8674",
"gsz": "0.8492",
"gszzl": -2.1,
"name": "天弘中证光伏产业指数C"
}
]
}
}

119
data/fund_history.json Normal file
View File

@@ -0,0 +1,119 @@
{
"last_update": "2025-10-29",
"data": [],
"yesterday_data": {
"003766": {
"date": "2025-11-17",
"change": -0.07,
"name": "广发创业板ETF发起式联接C"
},
"008327": {
"date": "2025-11-17",
"change": 1.14,
"name": "东财通信C"
},
"012863": {
"date": "2025-11-17",
"change": 0.81,
"name": "汇添富中证电池主题ETF发起式联接C"
},
"023350": {
"date": "2025-11-17",
"change": 0.11,
"name": "诺安多策略混合C"
},
"017560": {
"date": "2025-11-17",
"change": 0.27,
"name": "华安上证科创板芯片ETF发起式联接C"
},
"011815": {
"date": "2025-11-17",
"change": -0.36,
"name": "恒越优势精选混合"
},
"003598": {
"date": "2025-11-17",
"change": 0.57,
"name": "华商润丰灵活配置混合A"
},
"004206": {
"date": "2025-11-17",
"change": 0.58,
"name": "华商元亨混合A"
},
"022365": {
"date": "2025-11-17",
"change": 0.62,
"name": "永赢科技智选混合发起C"
},
"022364": {
"date": "2025-11-17",
"change": 0.62,
"name": "永赢科技智选混合发起A"
},
"011103": {
"date": "2025-11-17",
"change": -1.26,
"name": "天弘中证光伏产业指数C"
}
},
"today_data": {
"003766": {
"date": "2025-11-18",
"change": -0.43,
"name": "广发创业板ETF发起式联接C"
},
"008327": {
"date": "2025-11-18",
"change": 1.55,
"name": "东财通信C"
},
"012863": {
"date": "2025-11-18",
"change": -3.89,
"name": "汇添富中证电池主题ETF发起式联接C"
},
"023350": {
"date": "2025-11-18",
"change": -1.27,
"name": "诺安多策略混合C"
},
"017560": {
"date": "2025-11-18",
"change": 2.09,
"name": "华安上证科创板芯片ETF发起式联接C"
},
"011815": {
"date": "2025-11-18",
"change": -1.14,
"name": "恒越优势精选混合"
},
"003598": {
"date": "2025-11-18",
"change": 0.58,
"name": "华商润丰灵活配置混合A"
},
"004206": {
"date": "2025-11-18",
"change": 0.7,
"name": "华商元亨混合A"
},
"022365": {
"date": "2025-11-18",
"change": 1.27,
"name": "永赢科技智选混合发起C"
},
"022364": {
"date": "2025-11-18",
"change": 1.27,
"name": "永赢科技智选混合发起A"
},
"011103": {
"date": "2025-11-18",
"change": -2.1,
"name": "天弘中证光伏产业指数C"
}
},
"last_update_date": "2025-11-18"
}

22
data/fund_names.json Normal file
View File

@@ -0,0 +1,22 @@
{
"003766": "汇添富医疗服务混合",
"0": "易方达消费行业股票",
"001714": "工银文体产业股票",
"000241": "中欧时代先锋股票",
"001475": "易方达国防军工混合",
"1": "招商中证白酒指数",
"000689": "前海开源新经济混合",
"001102": "前海开源国家比较优势混合",
"001593": "天弘创业板ETF联接",
"000971": "高升沪深300指数增强",
"004206": "华商元亨混合A",
"019432": "永赢睿信混合C",
"003598": "华商润丰灵活配置混合A",
"001071": "华安媒体互联网混合A",
"018994": "中欧数字经济混合发起C",
"003567": "华夏行业景气混合A",
"008327": "东财通信C",
"012863": "汇添富中证电池主题ETF发起式联接C",
"014320": "德邦半导体产业混合发起式C",
"023350": "诺安多策略混合C"
}

1
data/last_cleanup.txt Normal file
View File

@@ -0,0 +1 @@
1762443884

269
data/operation_log.json Normal file
View File

@@ -0,0 +1,269 @@
{
"0": {
"timestamp": "2025-11-11 13:41:58",
"action": "管理员登录",
"ip": "113.87.139.184"
},
"1": {
"timestamp": "2025-11-06 08:31:27",
"action": "管理员登录",
"ip": "113.87.138.191"
},
"2": {
"timestamp": "2025-11-05 14:45:57",
"action": "管理员登录",
"ip": "113.87.138.191"
},
"3": {
"timestamp": "2025-11-05 14:34:30",
"action": "管理员登录",
"ip": "113.87.138.191"
},
"4": {
"timestamp": "2025-11-05 09:47:16",
"action": "管理员登录",
"ip": "113.87.138.191"
},
"5": {
"timestamp": "2025-11-01 23:18:46",
"action": "管理员登录",
"ip": "113.87.139.206"
},
"6": {
"timestamp": "2025-10-31 23:11:13",
"action": "管理员登录",
"ip": "113.84.8.124"
},
"7": {
"timestamp": "2025-10-31 10:20:41",
"action": "管理员注销",
"ip": "113.87.137.255"
},
"8": {
"timestamp": "2025-10-31 10:18:31",
"action": "管理员登录",
"ip": "113.87.137.255"
},
"9": {
"timestamp": "2025-10-31 09:08:47",
"action": "管理员登录",
"ip": "113.87.137.255"
},
"10": {
"timestamp": "2025-10-30 21:05:23",
"action": "管理员登录",
"ip": "113.87.139.170"
},
"11": {
"timestamp": "2025-10-30 17:28:24",
"action": "管理员注销",
"ip": "113.87.137.255"
},
"12": {
"timestamp": "2025-10-30 17:14:21",
"action": "管理员登录",
"ip": "113.87.137.255"
},
"operations": [
{
"id": "6902c8ca52d34",
"type": "add",
"fund_code": "123123",
"channel": "0",
"investment": 1000,
"details": "添加基金",
"timestamp": 1761790154,
"date": "2025-10-30 10:09:14"
},
{
"id": "6902c8d11928c",
"type": "delete",
"fund_code": "123123",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1761790161,
"date": "2025-10-30 10:09:21"
},
{
"id": "690303ba12e0b",
"type": "delete",
"fund_code": "017560",
"channel": "1",
"investment": 1000,
"details": "删除基金",
"timestamp": 1761805242,
"date": "2025-10-30 14:20:42"
},
{
"id": "690aeffc99bf3",
"type": "delete",
"fund_code": "003766",
"channel": "0",
"investment": 10000,
"details": "删除基金",
"timestamp": 1762324476,
"date": "2025-11-05 14:34:36"
},
{
"id": "690af008d8578",
"type": "delete",
"fund_code": "018994",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324488,
"date": "2025-11-05 14:34:48"
},
{
"id": "690af00b6106c",
"type": "delete",
"fund_code": "003567",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324491,
"date": "2025-11-05 14:34:51"
},
{
"id": "690af00e849e6",
"type": "delete",
"fund_code": "014320",
"channel": "1",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324494,
"date": "2025-11-05 14:34:54"
},
{
"id": "690af0623f151",
"type": "delete",
"fund_code": "001071",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324578,
"date": "2025-11-05 14:36:18"
},
{
"id": "690af06459fbc",
"type": "delete",
"fund_code": "004206",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324580,
"date": "2025-11-05 14:36:20"
},
{
"id": "690af06641d8c",
"type": "delete",
"fund_code": "019432",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324582,
"date": "2025-11-05 14:36:22"
},
{
"id": "690af0683bed6",
"type": "delete",
"fund_code": "003598",
"channel": "0",
"investment": 1000,
"details": "删除基金",
"timestamp": 1762324584,
"date": "2025-11-05 14:36:24"
},
{
"id": "690becaa0e959",
"type": "add",
"fund_code": "017560",
"channel": "1",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762389162,
"date": "2025-11-06 08:32:42"
},
{
"id": "6912cce74b4c7",
"type": "add",
"fund_code": "011815",
"channel": "1",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839783,
"date": "2025-11-11 13:43:03"
},
{
"id": "6912cd0a18557",
"type": "update",
"fund_code": "011815",
"channel": "2",
"investment": 1000,
"details": "更新基金: 投资金额 1000 -> 1000",
"timestamp": 1762839818,
"date": "2025-11-11 13:43:38"
},
{
"id": "6912cd214bbaa",
"type": "add",
"fund_code": "003598",
"channel": "2",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839841,
"date": "2025-11-11 13:44:01"
},
{
"id": "6912cd2e79b67",
"type": "add",
"fund_code": "004206",
"channel": "2",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839854,
"date": "2025-11-11 13:44:14"
},
{
"id": "6912cd3839737",
"type": "add",
"fund_code": "022365",
"channel": "2",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839864,
"date": "2025-11-11 13:44:24"
},
{
"id": "6912cd4373db8",
"type": "add",
"fund_code": "022364",
"channel": "2",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839875,
"date": "2025-11-11 13:44:35"
},
{
"id": "6912cd51cdd4c",
"type": "add",
"fund_code": "011103",
"channel": "2",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839889,
"date": "2025-11-11 13:44:49"
},
{
"id": "6912cd644111a",
"type": "add",
"fund_code": "025825",
"channel": "0",
"investment": 1000,
"details": "添加基金",
"timestamp": 1762839908,
"date": "2025-11-11 13:45:08"
}
]
}

9
data/recommend_logs.json Normal file
View File

@@ -0,0 +1,9 @@
[
{
"action": "recommend_fund",
"timestamp": "2025-10-31 16:33:53",
"fund_code": "005965",
"fund_name": "\u5b89\u4fe1\u4e2d\u8bc1500\u6307\u6570\u589e\u5f3aA",
"ip": "113.87.137.255"
}
]

View File

@@ -0,0 +1,11 @@
[
{
"fund_code": "005965",
"ip": "113.87.137.255",
"timestamp": "2025-10-31 16:33:53",
"channel": "网站推荐",
"amount": 1000,
"status": "pending",
"fund_name": "安信中证500指数增强A"
}
]

93
data/stats.json Normal file
View File

@@ -0,0 +1,93 @@
{
"total_visits": 613,
"today_visits": 1,
"unique_visitors": 63,
"last_reset": "2025-11-18",
"daily_stats": {
"2025-10-28": 55,
"2025-10-29": 195,
"2025-10-30": 206,
"2025-10-31": 18,
"2025-11-01": 4,
"2025-11-02": 2,
"2025-11-03": 12,
"2025-11-04": 6,
"2025-11-05": 11,
"2025-11-06": 9,
"2025-11-07": 7,
"2025-11-10": 5,
"2025-11-11": 11,
"2025-11-12": 10,
"2025-11-13": 10,
"2025-11-14": 10,
"2025-11-15": 1,
"2025-11-17": 5,
"2025-11-18": 1
},
"unique_ips": [
"113.87.138.21",
"113.84.209.129",
"113.87.137.255",
"23.27.145.18",
"112.97.82.77",
"113.84.82.28",
"178.239.124.106",
"23.27.145.162",
"14.116.140.45",
"18.207.197.68",
"14.116.140.24",
"183.211.90.28",
"113.201.15.72",
"42.232.246.99",
"81.71.101.51",
"113.84.138.211",
"14.116.140.135",
"113.84.42.84",
"113.87.139.170",
"180.101.245.251",
"220.196.160.101",
"103.219.192.68",
"112.97.84.103",
"149.57.180.103",
"113.84.8.124",
"23.27.145.45",
"66.249.75.226",
"27.38.135.37",
"205.169.39.29",
"34.123.170.104",
"205.169.39.25",
"113.87.139.206",
"205.169.39.148",
"112.97.87.32",
"113.84.192.103",
"113.87.138.191",
"146.70.185.32",
"14.116.140.239",
"113.87.137.193",
"220.196.160.146",
"110.249.201.169",
"113.84.33.37",
"14.116.140.86",
"113.87.136.77",
"180.101.244.13",
"180.101.244.15",
"14.116.140.42",
"113.84.83.15",
"113.87.139.184",
"113.84.193.224",
"129.211.162.158",
"113.87.139.12",
"113.84.169.220",
"14.31.17.219",
"220.196.160.144",
"14.31.17.35",
"112.97.80.134",
"66.249.77.193",
"35.165.215.140",
"113.87.139.101",
"113.87.136.32",
"205.169.39.16",
"113.84.33.13"
],
"today_unique_visitors": 1
}

4293
data/visits.json Normal file

File diff suppressed because it is too large Load Diff

82
index.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
// 基金监控系统主页面
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚀 组合基金监控</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<h1><i class="fas fa-chart-line"></i> 基金监控</h1>
<p>实时投资组合追踪</p>
<div class="controls">
<button class="btn btn-primary" id="refreshBtn">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="btn btn-outline" id="themeBtn">
<i class="fas fa-palette"></i> 主题
</button>
<button class="btn btn-outline" id="statsBtn">
<i class="fas fa-chart-bar"></i> 统计
</button>
<button class="btn btn-primary" id="recommendBtn" onclick="window.location.href='recommend_fund.php'">
<i class="fas fa-thumbs-up"></i> 推荐基金
</button>
<button class="btn btn-outline" id="adminBtn" onclick="window.location.href='admin.php'">
<i class="fas fa-cog"></i> 后台管理
</button>
</div>
</div>
</div>
<!-- 访问统计面板 -->
<div class="stats-panel" id="statsPanel" style="display: none;">
<div class="stats-header">
<div class="stats-title">
<i class="fas fa-users"></i> 访问统计
</div>
<button class="stats-toggle" onclick="toggleStats()">
<i class="fas fa-times"></i> 关闭
</button>
</div>
<div class="stats-grid" id="statsGrid">
<!-- 统计内容将通过JavaScript动态加载 -->
</div>
<div class="recent-visits" id="recentVisits">
<!-- 最近访问记录将通过JavaScript动态加载 -->
</div>
</div>
<div id="content">
<div class="loading">
<div class="loading-spinner"></div>
<p>正在加载基金数据...</p>
</div>
</div>
<div class="footer">
<div class="last-update" id="lastUpdate">
<strong><i class="fas fa-clock"></i> 最后更新:</strong> 加载中...
</div>
<div style="margin-top: 10px; font-size: 0.9rem; opacity: 0.8;">
<i class="fas fa-eye"></i> 今日访问: <span id="todayVisits">0</span> |
三天访问: <span id="totalVisits">0</span> |
独立访客: <span id="uniqueVisitors">0</span>
</div>
<p><i class="fas fa-info-circle"></i> 数据仅供参考,实际净值以基金公司公布为准</p>
<p><i class="fas fa-info-circle"></i> Tsama &copy; 东方财富</p>
<p><i class="fas fa-info-circle"></i> &copy; 2025 Tsama</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

241
login.php Normal file
View File

@@ -0,0 +1,241 @@
<?php
// 基金监控系统登录页面
// 初始化错误信息
$error = '';
// 处理登录请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 获取表单提交的密码
$password = $_POST['password'] ?? '';
// 简单的密码验证(实际项目中应使用更安全的验证方式)
$correct_password = 'qiyu123123';
if ($password === $correct_password) {
// 登录成功,创建会话
session_start();
$_SESSION['admin_logged_in'] = true;
$_SESSION['login_time'] = time();
// 记录登录日志
$log_entry = [
'timestamp' => date('Y-m-d H:i:s'),
'action' => '管理员登录',
'ip' => $_SERVER['REMOTE_ADDR']
];
// 保存登录日志
saveLog($log_entry);
// 跳转到管理页面
header('Location: admin.php');
exit;
} else {
// 登录失败
$error = '密码错误,请重试';
}
}
// 保存日志函数
function saveLog($log_entry) {
$log_file = 'data/operation_log.json';
// 检查目录是否存在,不存在则创建
if (!file_exists(dirname($log_file))) {
mkdir(dirname($log_file), 0777, true);
}
// 读取现有日志
$logs = [];
if (file_exists($log_file)) {
$content = file_get_contents($log_file);
if (!empty($content)) {
$logs = json_decode($content, true);
}
}
// 添加新日志
array_unshift($logs, $log_entry);
// 限制日志数量只保留最新的500条
if (count($logs) > 500) {
$logs = array_slice($logs, 0, 500);
}
// 保存日志
file_put_contents($log_file, json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚀 基金监控 - 登录</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
transform: translateY(0);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
color: #333;
font-size: 24px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.logo p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #667eea;
color: white;
}
.btn-primary:hover {
background-color: #5a67d8;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background-color: #fee2e2;
color: #dc2626;
border: 1px solid #fecaca;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: #9ca3af;
}
@media (max-width: 480px) {
.login-container {
padding: 30px 20px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<h1><i class="fas fa-lock"></i> 基金监控管理后台</h1>
<p>请输入密码进行登录</p>
</div>
<?php if (!empty($error)): ?>
<div class="alert alert-error">
<i class="fas fa-exclamation-circle"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="post" action="login.php">
<div class="form-group">
<label class="form-label" for="password">管理员密码</label>
<input type="password" class="form-control" id="password" name="password" required
placeholder="请输入密码" autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> 登录
</button>
<button type="button" class="btn" style="background-color: #f3f4f6; color: #4b5563; margin-top: 15px;" onclick="window.location.href='index.php'">
<i class="fas fa-home"></i> 返回主界面
</button>
</form>
<div class="footer">
<p>© 2025 基金监控系统 - Tsama</p>
</div>
</div>
</body>
</html>

711
recommend_fund.css Normal file
View File

@@ -0,0 +1,711 @@
/* 推荐基金页面专用样式 */
/* 容器样式 */
.recommend-container {
max-width: 880px;
margin: 0 auto;
padding: 20px;
}
/* 表单样式 */
.recommend-form {
background: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.recommend-form h2 {
margin-top: 0;
margin-bottom: 25px;
color: #374151;
font-size: 1.4rem;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #4b5563;
font-size: 1rem;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
background: #f9fafb;
}
.form-group input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
background: white;
}
.submit-btn {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
}
/* 消息提示样式 */
.message {
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 10px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.message.success {
background: linear-gradient(135deg, #d1fae5, #06d6a0);
color: #065f46;
border: none;
}
.message.error {
background: linear-gradient(135deg, #fee2e2, #ef4444);
color: #991b1b;
border: none;
}
/* 统计显示样式 */
.stats-display {
display: flex;
justify-content: space-between;
margin-bottom: 25px;
gap: 15px;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
flex: 1;
min-width: 180px;
padding: 20px 15px;
background: white;
border-radius: 12px;
margin: 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
}
.stat-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(180deg, #6366f1, #8b5cf6);
}
.stat-number {
font-size: 2.2rem;
font-weight: 700;
color: #6366f1;
margin-bottom: 8px;
display: block;
}
.stat-label {
font-size: 0.95rem;
color: #6b7280;
font-weight: 500;
}
/* 信息框样式 */
.info-box {
background: linear-gradient(135deg, #dbeafe, #3b82f6);
color: #1e40af;
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.info-box strong {
display: block;
margin-bottom: 12px;
font-size: 1.1rem;
}
.info-box ul {
margin: 0;
padding-left: 25px;
}
.info-box li {
margin-bottom: 6px;
font-weight: 500;
}
/* 警告框样式 */
.warning-box {
background: linear-gradient(135deg, #fef3c7, #f59e0b);
color: #92400e;
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.warning-box strong {
display: block;
margin-bottom: 12px;
font-size: 1.1rem;
}
/* 基金列表样式 */
.fund-list {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
margin-top: 30px;
}
.fund-list h3 {
margin-top: 0;
color: #374151;
margin-bottom: 25px;
font-size: 1.3rem;
position: relative;
padding-bottom: 12px;
}
.fund-list h3::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 4px;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
border-radius: 2px;
}
/* 下拉框容器 */
.dropdown-container {
margin-top: 0;
}
/* 下拉框项样式 */
.dropdown-item {
margin-bottom: 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
background: white;
}
.dropdown-item:hover {
border-color: #d1d5db;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.dropdown-item.user-ip {
border-color: #6366f1;
background-color: #f8fafc;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
}
/* 下拉框头部 */
.dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.dropdown-header:hover {
background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
}
.dropdown-item.user-ip .dropdown-header {
background: linear-gradient(135deg, #eff6ff, #dbeafe);
}
.dropdown-item.user-ip .dropdown-header:hover {
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
}
/* 下拉框信息 */
.dropdown-info {
display: flex;
flex-wrap: wrap;
gap: 25px;
align-items: center;
}
.dropdown-info > strong {
color: #6366f1;
font-size: 1.1rem;
}
.dropdown-time,
.dropdown-count,
.dropdown-total-change {
font-size: 0.95rem;
color: #4b5563;
font-weight: 500;
}
.dropdown-time strong,
.dropdown-count strong {
color: #374151;
}
.dropdown-total-change {
font-weight: 700;
padding: 6px 12px;
border-radius: 6px;
background: #f3f4f6;
transition: all 0.3s ease;
}
.dropdown-total-change.positive {
background: #dcfce7;
color: #166534;
}
.dropdown-total-change.negative {
background: #fee2e2;
color: #991b1b;
}
/* 下拉框箭头 */
.dropdown-arrow i {
transition: transform 0.3s ease;
font-size: 1.1rem;
color: #6366f1;
}
.dropdown-item.open .dropdown-arrow i {
transform: rotate(180deg);
}
/* 下拉框内容 */
.dropdown-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background-color: white;
}
.dropdown-item.open .dropdown-content {
max-height: 800px;
}
/* 基金详情表格 */
.fund-details-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
.fund-details-table th,
.fund-details-table td {
padding: 15px 20px;
text-align: left;
border-bottom: 1px solid #f1f5f9;
transition: all 0.3s ease;
}
.fund-details-table th {
background: #f8fafc;
font-weight: 600;
color: #374151;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.fund-details-table tr:hover {
background: #f8fafc;
transform: translateX(2px);
}
.fund-details-table tr:last-child td {
border-bottom: none;
}
/* 基金涨幅样式 */
.fund-change {
font-weight: 700;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.fund-change.positive {
background: #fee2e2;
color: #dc2626;
}
.fund-change.negative {
background: #dcfce7;
color: #16a34a;
}
/* 错误状态样式 */
.fund-change.error {
background: #f3f4f6;
color: #6b7280;
font-style: italic;
box-shadow: 0 2px 5px rgba(107, 114, 128, 0.1);
}
.error-text {
color: #6b7280;
font-style: italic;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 5px;
}
.error-text::before {
content: '⚠️';
font-size: 1em;
}
.error-row {
background: linear-gradient(90deg, #fffbeb, #fef3c7);
border-left: 3px solid #f59e0b;
transition: all 0.3s ease;
}
.error-row:hover {
background: linear-gradient(90deg, #fef3c7, #fde68a);
transform: translateX(2px);
}
.error-icon {
color: #f59e0b;
font-weight: bold;
font-size: 1.2em;
}
/* 返回按钮样式 */
.back-btn {
display: inline-block;
margin-bottom: 25px;
color: #000000ff;
text-decoration: none;
padding: 10px 20px;
border: 2px solid #0004d3;
border-radius: 10px;
transition: all 0.3s ease;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.back-btn:hover {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
}
/* 加载指示器样式 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(3px);
}
.loading-spinner {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: fadeIn 0.3s ease;
}
.loading-spinner i {
font-size: 48px;
color: #6366f1;
margin-bottom: 20px;
display: block;
animation: spin 1s linear infinite;
}
.loading-spinner span {
font-size: 18px;
font-weight: 600;
color: #495057;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 改进错误显示效果 */
.error-container {
background: linear-gradient(135deg, #fee2e2, #fecaca);
border: 2px solid #ef4444;
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.15);
position: relative;
}
.error-container::before {
content: '⚠️';
position: absolute;
top: 15px;
right: 20px;
font-size: 24px;
}
.error-container h4 {
color: #991b1b;
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 8px;
}
.error-container p {
color: #7f1d1d;
margin: 0;
font-weight: 500;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
background: linear-gradient(135deg, #f9fafb, #f3f4f6);
border-radius: 12px;
border: 2px dashed #d1d5db;
}
.empty-state i {
font-size: 72px;
margin-bottom: 20px;
opacity: 0.3;
color: #6366f1;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
color: #4b5563;
}
.empty-state p {
font-size: 16px;
max-width: 400px;
margin: 0 auto;
}
/* 高亮动画样式 */
.highlight {
animation: highlight 0.3s ease-in-out;
}
@keyframes highlight {
0% { background-color: transparent; }
50% { background-color: rgba(255, 255, 0, 0.3); }
100% { background-color: transparent; }
}
/* ===== UI优化追加样式覆盖同名规则 ===== */
.stat-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: #fff;
margin-bottom: 10px;
box-shadow: 0 6px 18px rgba(99, 102, 241, 0.35);
}
.dropdown-total-change.neutral {
background: #f3f4f6;
color: #374151;
}
.fund-change.neutral {
background: #f3f4f6;
color: #374151;
}
.fund-change.highlight,
.dropdown-total-change.highlight {
box-shadow: 0 0 0 6px rgba(99,102,241,0.08) inset;
}
.back-btn {
color: var(--dark);
border: 2px solid var(--primary);
}
.ip-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background: linear-gradient(135deg, var(--info), var(--primary));
color: #fff;
font-size: 0.85rem;
font-weight: 600;
}
@media (max-width: 680px) {
.fund-details-table thead { display: none; }
.fund-details-table,
.fund-details-table tbody,
.fund-details-table tr,
.fund-details-table td { display: block; width: 100%; }
.fund-details-table tr {
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
padding: 6px 0;
}
.fund-details-table td {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: none;
}
.fund-details-table td:nth-child(1)::before { content: '基金代码'; color: #6b7280; font-weight: 600; margin-right: 12px; }
.fund-details-table td:nth-child(2)::before { content: '推荐时间'; color: #6b7280; font-weight: 600; margin-right: 12px; }
.fund-details-table td:nth-child(3)::before { content: '基金名称'; color: #6b7280; font-weight: 600; margin-right: 12px; }
.fund-details-table td:nth-child(4)::before { content: '当前涨幅'; color: #6b7280; font-weight: 600; margin-right: 12px; }
}
/* 排序工具条样式 */
.sort-toolbar {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.sort-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-radius: 10px;
border: 2px solid var(--border);
background: #fff;
color: var(--dark);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.sort-btn .order-indicator {
font-size: 0.9rem;
color: var(--gray);
}
.sort-btn:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
}
.sort-btn.active {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: #fff;
border-color: transparent;
}
.sort-btn.active .order-indicator {
color: #fff;
}
/* 重置按钮样式 */
.sort-reset {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-radius: 10px;
border: 2px solid var(--border);
background: #fff;
color: var(--dark);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.sort-reset:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
}

657
recommend_fund.js Normal file
View File

@@ -0,0 +1,657 @@
/**
* 推荐基金页面JavaScript功能
*/
// 配置常量
const API_TIMEOUT = 5000; // API 请求超时时间(毫秒)
const MAX_RETRY_COUNT = 3; // 最大重试次数
const RETRY_DELAY = 1000; // 重试延迟(毫秒)
const BATCH_SIZE = 10; // 批量请求数量
const UPDATE_INTERVAL = 60000; // 自动更新间隔(毫秒)
const HIGHLIGHT_DURATION = 300; // 高亮效果持续时间
// 全局状态
let updateIntervalId = null;
let isLoading = false;
let fundDataCache = {}; // 缓存基金数据
let lastData = {}; // 上一次的数据,用于比较变化
let errorCount = 0;
const MAX_ERROR_COUNT = 5; // 连续错误次数上限
/**
* 格式化数字(保留两位小数)
* @param {number} num - 要格式化的数字
* @returns {string} 格式化后的字符串
*/
function formatNumber(num) {
if (typeof num !== 'number' || isNaN(num)) {
return '--';
}
return num.toFixed(2);
}
/**
* 显示加载状态
* @param {boolean} show - 是否显示
*/
function showLoading(show) {
const loadingElement = document.getElementById('loading-indicator');
if (!loadingElement && show) {
// 创建加载指示器
const div = document.createElement('div');
div.id = 'loading-indicator';
div.className = 'loading-overlay';
div.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-circle-notch fa-spin"></i>
<span>加载基金数据中...</span>
</div>
`;
document.body.appendChild(div);
} else if (loadingElement) {
loadingElement.style.display = show ? 'flex' : 'none';
}
}
/**
* 清理JSONP请求的资源
* @param {string} scriptId - script标签ID
* @param {string} callbackName - 回调函数名
*/
function cleanupResources(scriptId, callbackName) {
// 清理script标签
const script = document.getElementById(scriptId);
if (script && script.parentNode) {
script.parentNode.removeChild(script);
}
// 清理回调函数
if (window[callbackName]) {
delete window[callbackName];
}
}
/**
* 处理API错误
* @param {string} errorMessage - 错误消息
*/
function handleApiError(errorMessage) {
errorCount++;
console.error(errorMessage);
// 如果连续错误次数过多,显示错误提示
if (errorCount >= MAX_ERROR_COUNT) {
updateErrorDisplay('获取基金数据时遇到问题,请稍后再试', true);
}
}
/**
* 获取单个基金数据(使用 JSONP 方式,支持预加载)
* @param {string} fundCode - 基金代码
* @param {number} retryCount - 当前重试次数
* @returns {Promise<Object>} 基金数据对象
*/
function fetchFundData(fundCode, retryCount = 0) {
// 清理基金代码,确保只包含数字和字母
const cleanFundCode = String(fundCode).replace(/[^\dA-Za-z]/g, '').trim();
if (!cleanFundCode) {
console.error('无效的基金代码:', fundCode);
return Promise.resolve({
success: false,
error: '无效的基金代码',
fundCode: fundCode
});
}
// 1. 检查是否有预加载数据
if (window.preloadedFundData && window.preloadedFundData[cleanFundCode]) {
console.log(`使用预加载数据: ${cleanFundCode}`);
return Promise.resolve({
success: true,
data: window.preloadedFundData[cleanFundCode],
fromCache: true
});
}
// 2. 检查缓存
const cachedData = fundDataCache[cleanFundCode];
if (cachedData && (Date.now() - cachedData.timestamp) < 30000) { // 30秒缓存
return Promise.resolve({
success: true,
data: cachedData.data,
fromCache: true
});
}
return new Promise((resolve, reject) => {
// 设置超时
const timeoutId = setTimeout(() => {
reject(new Error('请求超时'));
}, API_TIMEOUT);
// 创建唯一的回调函数名
const callbackName = `jsonp_callback_${cleanFundCode}_${Date.now()}`;
const scriptId = `jsonp_script_${cleanFundCode}`;
// 定义全局回调函数
window[callbackName] = function(data) {
clearTimeout(timeoutId);
// 检查数据有效性
if (!data || !data.fundcode || data.fundcode !== cleanFundCode) {
cleanupResources(scriptId, callbackName);
handleApiError(`基金${cleanFundCode}数据格式错误`);
resolve({
success: false,
error: '数据格式错误',
fundCode: cleanFundCode
});
return;
}
// 缓存数据
fundDataCache[cleanFundCode] = {
data: data,
timestamp: Date.now()
};
// 重置错误计数
errorCount = 0;
// 清理资源并返回结果
cleanupResources(scriptId, callbackName);
resolve({
success: true,
data: data
});
};
// 创建脚本元素
const script = document.createElement('script');
script.id = scriptId;
script.src = `http://fundgz.1234567.com.cn/js/${cleanFundCode}.js?callback=${callbackName}`;
script.onerror = function() {
clearTimeout(timeoutId);
cleanupResources(scriptId, callbackName);
reject(new Error('脚本加载失败'));
};
// 添加到文档中
document.body.appendChild(script);
}).catch(error => {
console.error(`获取基金 ${cleanFundCode} 数据失败:`, error.message);
// 重试逻辑
if (retryCount < MAX_RETRY_COUNT) {
console.log(`尝试重试 ${cleanFundCode}, 第 ${retryCount + 1}`);
return new Promise(resolve => {
setTimeout(() => {
resolve(fetchFundData(cleanFundCode, retryCount + 1));
}, RETRY_DELAY * (retryCount + 1));
});
}
handleApiError(`获取基金 ${cleanFundCode} 数据失败`);
return {
success: false,
error: error.message,
fundCode: cleanFundCode
};
});
}
/**
* 批量获取基金数据
* @param {Array<string>} fundCodes - 基金代码数组
* @returns {Promise<Array>} 基金数据结果数组
*/
async function fetchFundsDataInBatch(fundCodes) {
// 分批处理,避免一次请求过多
const batches = [];
for (let i = 0; i < fundCodes.length; i += BATCH_SIZE) {
batches.push(fundCodes.slice(i, i + BATCH_SIZE));
}
const allResults = [];
// 逐批处理
for (const batch of batches) {
// 使用Promise.allSettled确保所有请求都完成
const batchPromises = batch.map(code => fetchFundData(code));
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
allResults.push(result.value);
} else {
console.error(`处理批处理中基金 ${batch[index]} 时出错:`, result.reason);
allResults.push({
success: false,
error: result.reason?.message || '未知错误',
fundCode: batch[index]
});
}
});
// 批处理之间添加短暂延迟,避免请求过于密集
if (batches.indexOf(batch) < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return allResults;
}
/**
* 更新基金错误状态显示
* @param {string} fundCode - 基金代码
*/
function updateFundError(fundCode) {
// 更新基金名称显示
document.querySelectorAll(`.fund-name[data-fund-code="${fundCode}"]`).forEach(el => {
if (el.textContent === '加载中...') {
el.textContent = `${fundCode} (数据获取失败)`;
el.classList.add('error-text');
}
});
// 更新涨幅显示
document.querySelectorAll(`.fund-change[data-fund-code="${fundCode}"]`).forEach(el => {
el.textContent = '--';
el.classList.add('error');
});
}
/**
* 加载所有基金信息
*/
async function loadAllFundInfo() {
if (isLoading) return;
isLoading = true;
showLoading(true);
try {
// 保存旧数据用于比较
lastData = {};
for (const fundCode in fundDataCache) {
lastData[fundCode] = fundDataCache[fundCode].data;
}
// 获取所有需要的基金代码
const fundElements = document.querySelectorAll('[data-fund-code]');
const fundCodes = Array.from(new Set(Array.from(fundElements).map(el => {
const code = el.getAttribute('data-fund-code');
// 清理基金代码
return String(code || '').replace(/[^\dA-Za-z]/g, '').trim();
}))).filter(code => code.length > 0); // 过滤空代码
if (fundCodes.length === 0) {
console.log('没有找到需要加载的基金');
return;
}
console.log(`开始加载 ${fundCodes.length} 支基金数据`, fundCodes);
// 批量获取基金数据
const results = await fetchFundsDataInBatch(fundCodes);
// 更新UI
let successCount = 0;
results.forEach(result => {
if (result.success && result.data) {
updateFundDisplay(result.data);
successCount++;
} else {
// 对于失败的请求,尝试更新为错误状态
if (result.fundCode) {
updateFundError(result.fundCode);
}
}
});
// 更新IP组的总涨幅
updateIpTotalChanges();
console.log(`基金数据加载完成: 成功${successCount}/${fundCodes.length}`);
} catch (error) {
console.error('加载基金信息时发生错误:', error);
updateErrorDisplay('加载基金数据失败,请稍后重试', true);
} finally {
isLoading = false;
showLoading(false);
}
}
/**
* 更新单个基金的显示信息
* @param {Object} fundData - 基金数据
*/
function updateFundDisplay(fundData) {
if (!fundData || !fundData.fundcode) return;
const fundCode = fundData.fundcode;
const name = fundData.name || '未知名称';
const gszzl = parseFloat(fundData.gszzl) || 0;
// 检查是否有数据变化
const oldData = lastData[fundCode];
const hasChanged = !oldData || oldData.gszzl !== fundData.gszzl;
// 更新基金名称
document.querySelectorAll(`.fund-name[data-fund-code="${fundCode}"]`).forEach(el => {
el.textContent = name;
});
// 更新基金涨幅
document.querySelectorAll(`.fund-change[data-fund-code="${fundCode}"]`).forEach(el => {
const oldValue = el.textContent;
el.textContent = `${formatNumber(gszzl)}%`;
// 根据涨跌设置颜色
el.classList.remove('positive', 'negative', 'neutral', 'highlight');
if (gszzl > 0) {
el.classList.add('positive');
} else if (gszzl < 0) {
el.classList.add('negative');
} else {
el.classList.add('neutral');
}
// 如果有变化,添加高亮效果
if (hasChanged) {
el.classList.add('highlight');
setTimeout(() => {
el.classList.remove('highlight');
}, HIGHLIGHT_DURATION);
}
});
}
/**
* 更新IP组的总涨幅
*/
function updateIpTotalChanges() {
// 获取所有IP分组
const ipGroups = document.querySelectorAll('[id^="ip-"]');
ipGroups.forEach(group => {
const ipId = group.id;
const changeElements = group.querySelectorAll('.fund-change');
let totalChange = 0;
let validCount = 0;
changeElements.forEach(el => {
const text = el.textContent.trim();
if (text !== '--' && text !== '') {
const change = parseFloat(text.replace('%', ''));
if (!isNaN(change)) {
totalChange += change;
validCount++;
}
}
});
// 计算平均涨幅
const avgChange = validCount > 0 ? totalChange / validCount : 0;
// 更新显示
const totalChangeElement = document.querySelector(`#total-change-${ipId.split('-')[1]}`);
if (totalChangeElement) {
const oldValue = totalChangeElement.textContent;
const newValue = `${formatNumber(avgChange)}%`;
totalChangeElement.textContent = newValue;
// 如果有变化,添加高亮效果
if (oldValue !== newValue) {
totalChangeElement.classList.add('highlight');
setTimeout(() => {
totalChangeElement.classList.remove('highlight');
}, HIGHLIGHT_DURATION);
}
// 根据涨跌设置颜色
totalChangeElement.classList.remove('positive', 'negative', 'neutral');
if (avgChange > 0) {
totalChangeElement.classList.add('positive');
} else if (avgChange < 0) {
totalChangeElement.classList.add('negative');
} else {
totalChangeElement.classList.add('neutral');
}
// 将平均涨幅写入分组容器的data属性便于排序
const dropdownItem = document.getElementById(ipId)?.closest('.dropdown-item');
if (dropdownItem) {
dropdownItem.dataset.avgChange = String(avgChange);
}
}
});
}
/**
* 更新错误显示
* @param {string} message - 错误信息
* @param {boolean} show - 是否显示错误
*/
function updateErrorDisplay(message, show = true) {
let errorElement = document.getElementById('api-error');
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = 'api-error';
errorElement.className = 'error-message';
document.body.prepend(errorElement);
}
if (show) {
errorElement.textContent = message;
errorElement.style.display = 'block';
// 5秒后自动隐藏错误信息
setTimeout(() => {
errorElement.style.display = 'none';
}, 5000);
} else {
errorElement.style.display = 'none';
}
}
/**
* 切换下拉菜单显示/隐藏
* @param {string} id - 要切换的元素ID
*/
function toggleDropdown(id) {
const element = document.getElementById(id);
if (element) {
element.classList.toggle('show');
// 切换箭头方向
const header = element.previousElementSibling;
if (header) {
const arrow = header.querySelector('.dropdown-arrow i');
if (arrow) {
arrow.classList.toggle('fa-chevron-down');
arrow.classList.toggle('fa-chevron-up');
}
}
}
}
/**
* 初始化函数
*/
function init() {
// 初始加载基金数据
loadAllFundInfo();
// 设置自动更新
updateIntervalId = setInterval(loadAllFundInfo, UPDATE_INTERVAL);
// 清理函数
window.addEventListener('beforeunload', cleanup);
// 为所有下拉菜单添加点击事件委托
document.addEventListener('click', function(e) {
// 检查是否点击了下拉菜单标题
const dropdownHeader = e.target.closest('.dropdown-header');
if (dropdownHeader) {
// 获取父级dropdown-item元素
const dropdownItem = dropdownHeader.closest('.dropdown-item');
if (dropdownItem) {
// 关闭所有其他下拉菜单
document.querySelectorAll('.dropdown-item').forEach(item => {
if (item !== dropdownItem) {
item.classList.remove('open');
const arrow = item.querySelector('.dropdown-arrow i');
if (arrow) {
arrow.classList.remove('fa-chevron-up');
arrow.classList.add('fa-chevron-down');
}
}
});
// 切换当前下拉菜单
dropdownItem.classList.toggle('open');
// 切换箭头方向
const arrow = dropdownHeader.querySelector('.dropdown-arrow i');
if (arrow) {
arrow.classList.toggle('fa-chevron-down');
arrow.classList.toggle('fa-chevron-up');
}
}
} else {
// 点击其他地方,关闭所有下拉菜单
document.querySelectorAll('.dropdown-item').forEach(item => {
item.classList.remove('open');
const arrow = item.querySelector('.dropdown-arrow i');
if (arrow) {
arrow.classList.remove('fa-chevron-up');
arrow.classList.add('fa-chevron-down');
}
});
}
});
// 初始化排序工具条
initSortToolbar();
}
/**
* 清理函数
*/
function cleanup() {
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
// 清理所有可能的JSONP回调函数
Object.keys(window).forEach(key => {
if (key.startsWith('jsonp_callback_')) {
delete window[key];
}
});
// 清理所有JSONP脚本标签
document.querySelectorAll('script[id^="jsonp_script_"]').forEach(script => {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
});
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
/**
* 初始化排序工具条
*/
function initSortToolbar() {
const container = document.querySelector('.dropdown-container');
const buttons = document.querySelectorAll('.sort-btn');
const resetBtn = document.querySelector('.sort-reset');
if (!container || buttons.length === 0) return;
const applyActive = (activeBtn) => {
buttons.forEach(btn => btn.classList.toggle('active', btn === activeBtn));
// 更新箭头指示符
buttons.forEach(btn => {
const indicator = btn.querySelector('.order-indicator');
if (indicator) {
indicator.textContent = btn.dataset.order === 'asc' ? '↑' : '↓';
}
});
};
const sortIpGroups = (key, order) => {
const items = Array.from(container.querySelectorAll('.dropdown-item'));
const getVal = (el) => {
switch (key) {
case 'time': {
const t = el.dataset.latestTime || '';
// 兼容 YYYY-MM-DD HH:mm:ss
const timeNum = Date.parse(t.replace(/-/g, '/')) || 0;
return timeNum;
}
case 'count':
return parseInt(el.dataset.fundCount || '0', 10);
case 'avg':
return parseFloat(el.dataset.avgChange || '0');
default:
return 0;
}
};
items.sort((a, b) => {
const av = getVal(a);
const bv = getVal(b);
if (av === bv) return 0;
return order === 'asc' ? (av - bv) : (bv - av);
});
// 重新插入排序后的元素
items.forEach(el => container.appendChild(el));
};
// 绑定按钮事件
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const sortKey = btn.dataset.sort;
const currentOrder = btn.dataset.order || 'desc';
// 如果重复点击同一按钮,切换升降序
const newOrder = (btn.classList.contains('active') && currentOrder === 'desc') ? 'asc' : 'desc';
btn.dataset.order = newOrder;
applyActive(btn);
sortIpGroups(sortKey, newOrder);
});
});
// 重置排序:恢复时间倒序
if (resetBtn) {
resetBtn.addEventListener('click', () => {
// 所有按钮恢复箭头为倒序
buttons.forEach(b => { b.dataset.order = 'desc'; });
const defaultBtn = document.querySelector('.sort-btn[data-sort="time"]');
if (defaultBtn) {
defaultBtn.dataset.order = 'desc';
applyActive(defaultBtn);
sortIpGroups('time', 'desc');
}
});
}
// 默认执行一次“按时间倒序”排序,保持与服务端一致
const defaultBtn = document.querySelector('.sort-btn[data-sort="time"]');
if (defaultBtn) {
applyActive(defaultBtn);
sortIpGroups('time', defaultBtn.dataset.order || 'desc');
}
}

566
recommend_fund.php Normal file
View File

@@ -0,0 +1,566 @@
<?php
/**
* 基金推荐页面 - 优化版
* 功能:基金推荐、真实数据展示、用户体验优化
*/
// 设置响应头
header('Content-Type: text/html; charset=utf-8');
// 设置时区
date_default_timezone_set('Asia/Shanghai');
// 启动会话以保存消息
session_start();
// 配置常量
const DATA_DIR = __DIR__ . '/data';
const CACHE_DIR = DATA_DIR . '/cache';
const FUND_CACHE_TTL = 300; // 基金数据缓存5分钟
const MAX_FUNDS_PER_IP = 3; // 每IP最大推荐数量
const MAX_TOTAL_FUNDS = 3000; // 系统最大基金数量
// 确保目录存在
function ensureDirsExist() {
if (!file_exists(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
if (!file_exists(CACHE_DIR)) {
mkdir(CACHE_DIR, 0755, true);
}
}
ensureDirsExist();
/**
* 获取用户真实IP
* @return string 用户IP地址
*/
function getUserIP() {
$ip = $_SERVER['REMOTE_ADDR'];
// 检查代理IP
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// 处理多个IP的情况逗号分隔
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ipList[0]);
}
return $ip;
}
/**
* 加载已推荐基金数据
* @return array 推荐基金数据
*/
function loadRecommendedFunds() {
$file = DATA_DIR . '/recommended_funds.json';
if (file_exists($file)) {
$data = file_get_contents($file);
return json_decode($data, true) ?? [];
}
return [];
}
/**
* 清理不存在的基金
* @return int 移除的基金数量
*/
function cleanNonExistentFunds() {
$funds = loadRecommendedFunds();
$fundsToKeep = [];
$fundsRemoved = 0;
foreach ($funds as $fund) {
if (isset($fund['fund_code']) && checkFundExists($fund['fund_code'])) {
$fundsToKeep[] = $fund;
} else {
$fundsRemoved++;
}
}
// 如果有基金被移除,保存更新后的列表
if ($fundsRemoved > 0) {
$file = DATA_DIR . '/recommended_funds.json';
file_put_contents($file, json_encode($fundsToKeep, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 记录清理日志
logAction('clean_nonexistent_funds', ['funds_removed' => $fundsRemoved]);
}
return $fundsRemoved;
}
/**
* 记录操作日志
* @param string $action 操作类型
* @param array $data 操作数据
*/
function logAction($action, $data = []) {
$logFile = DATA_DIR . '/recommend_logs.json';
$logs = [];
if (file_exists($logFile)) {
$logs = json_decode(file_get_contents($logFile), true) ?? [];
}
$logs[] = array_merge([
'action' => $action,
'timestamp' => date('Y-m-d H:i:s')
], $data);
file_put_contents($logFile, json_encode($logs, JSON_PRETTY_PRINT));
}
/**
* 获取基金缓存文件路径
* @param string $fundCode 基金代码
* @return string 缓存文件路径
*/
function getFundCachePath($fundCode) {
return CACHE_DIR . "/fund_{$fundCode}.json";
}
/**
* 从缓存获取基金数据
* @param string $fundCode 基金代码
* @return array|false 基金数据或false
*/
function getFundFromCache($fundCode) {
$cacheFile = getFundCachePath($fundCode);
if (!file_exists($cacheFile)) {
return false;
}
// 检查缓存是否过期
if (time() - filemtime($cacheFile) > FUND_CACHE_TTL) {
return false;
}
$data = file_get_contents($cacheFile);
if ($data === false) {
return false;
}
$fundData = json_decode($data, true);
return is_array($fundData) ? $fundData : false;
}
/**
* 缓存基金数据
* @param string $fundCode 基金代码
* @param array $fundData 基金数据
*/
function cacheFundData($fundCode, $fundData) {
$cacheFile = getFundCachePath($fundCode);
file_put_contents($cacheFile, json_encode($fundData, JSON_UNESCAPED_UNICODE));
}
/**
* 从API获取基金信息
* @param string $fundCode 基金代码
* @return array|false 基金数据或false
*/
function fetchFundDataFromAPI($fundCode) {
$apiUrl = "http://fundgz.1234567.com.cn/js/{$fundCode}.js?" . time();
$context = stream_context_create([
'http' => [
'timeout' => 5,
'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n",
'ignore_errors' => true
]
]);
$response = @file_get_contents($apiUrl, false, $context);
if (!$response) {
return false;
}
// 解析返回的数据
if (preg_match('/jsonpgz\d*\((.*)\);?/', $response, $matches)) {
$data = json_decode($matches[1], true);
// 检查数据是否有效
if (is_array($data) && isset($data['fundcode']) && $data['fundcode'] == $fundCode) {
return $data;
}
}
return false;
}
/**
* 检查基金是否存在
* @param string $fundCode 基金代码
* @return bool 基金是否存在
*/
function checkFundExists($fundCode) {
// 基金代码格式验证
if (!preg_match('/^\d{6}$/', $fundCode)) {
return false;
}
// 尝试从缓存获取
$cachedData = getFundFromCache($fundCode);
if ($cachedData) {
return true;
}
// 从API获取
$fundData = fetchFundDataFromAPI($fundCode);
if ($fundData) {
// 缓存基金数据
cacheFundData($fundCode, $fundData);
return true;
}
return false;
}
/**
* 获取基金详细信息
* @param string $fundCode 基金代码
* @return array|false 基金详细数据或false
*/
function getFundInfo($fundCode) {
// 先尝试从缓存获取
$cachedData = getFundFromCache($fundCode);
if ($cachedData) {
return $cachedData;
}
// 从API获取
$fundData = fetchFundDataFromAPI($fundCode);
if ($fundData) {
// 缓存基金数据
cacheFundData($fundCode, $fundData);
return $fundData;
}
return false;
}
/**
* 保存推荐基金
* @param string $fundCode 基金代码
* @param string $ip 用户IP
* @return array 操作结果
*/
function saveRecommendedFund($fundCode, $ip) {
// 首先检查基金号是否存在
if (!checkFundExists($fundCode)) {
return ['success' => false, 'message' => '基金号不存在请输入正确的6位基金代码'];
}
// 检查数量限制
$funds = loadRecommendedFunds();
$timestamp = date('Y-m-d H:i:s');
// 检查基金总数是否已达到上限
if (count($funds) >= MAX_TOTAL_FUNDS) {
return ['success' => false, 'message' => "推荐基金数量已达到上限({MAX_TOTAL_FUNDS}支)"];
}
// 检查IP推荐次数和基金是否已被推荐
$ipCount = 0;
foreach ($funds as $fund) {
if ($fund['ip'] == $ip) {
$ipCount++;
}
if ($fund['fund_code'] == $fundCode) {
return ['success' => false, 'message' => '该基金已被推荐过'];
}
}
if ($ipCount >= MAX_FUNDS_PER_IP) {
return ['success' => false, 'message' => "每个IP最多只能推荐{MAX_FUNDS_PER_IP}只基金"];
}
// 获取基金信息
$fundInfo = getFundInfo($fundCode);
$fundName = $fundInfo ? ($fundInfo['name'] ?? '未知基金') : '未知基金';
// 添加新基金
$newFund = [
'fund_code' => $fundCode,
'ip' => $ip,
'timestamp' => $timestamp,
'channel' => '网站推荐',
'amount' => 1000,
'status' => 'pending', // 默认设为待审核,需要管理员批准
'fund_name' => $fundName // 保存基金名称
];
$funds[] = $newFund;
// 保存到文件
$file = DATA_DIR . '/recommended_funds.json';
file_put_contents($file, json_encode($funds, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 记录操作日志
logAction('recommend_fund', [
'fund_code' => $fundCode,
'fund_name' => $fundName,
'ip' => $ip
]);
return ['success' => true, 'message' => '基金推荐成功!'];
}
// 处理推荐请求
// 从session中获取消息如果有
$message = isset($_SESSION['message']) ? $_SESSION['message'] : '';
$messageType = isset($_SESSION['messageType']) ? $_SESSION['messageType'] : '';
// 清除session中的消息防止刷新页面再次显示
unset($_SESSION['message'], $_SESSION['messageType']);
// 定期清理不存在的基金每10分钟执行一次
$cleanupFile = DATA_DIR . '/last_cleanup.txt';
$lastCleanup = @file_get_contents($cleanupFile);
$currentTime = time();
// 如果距离上次清理超过10分钟执行清理
if (!$lastCleanup || ($currentTime - $lastCleanup) > 600) {
$removedCount = cleanNonExistentFunds();
file_put_contents($cleanupFile, $currentTime);
// 如果有基金被清理,记录日志但不显示给用户
if ($removedCount > 0) {
error_log("清理了{$removedCount}个不存在的基金");
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['fund_code'])) {
$fundCode = trim($_POST['fund_code']);
$ip = getUserIP();
// 简单验证基金代码格式假设为6位数字
if (!preg_match('/^\d{6}$/', $fundCode)) {
$_SESSION['message'] = '基金代码格式不正确请输入6位数字';
$_SESSION['messageType'] = 'error';
} else {
$result = saveRecommendedFund($fundCode, $ip);
$_SESSION['message'] = $result['message'];
$_SESSION['messageType'] = $result['success'] ? 'success' : 'error';
}
// 重定向到同一页面实现POST-REDIRECT-GET模式
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// 获取IP推荐次数
$userIp = getUserIP();
$recommendedFunds = loadRecommendedFunds();
$userRecommendCount = 0;
foreach ($recommendedFunds as $fund) {
if (isset($fund['ip']) && $fund['ip'] == $userIp) {
$userRecommendCount++;
}
}
$totalFunds = count($recommendedFunds);
// 预加载部分基金数据到页面中,提高初始加载速度
$preloadedFundData = [];
$preloadLimit = 10; // 预加载10个基金的数据
$preloadCount = 0;
foreach ($recommendedFunds as $fund) {
if ($preloadCount >= $preloadLimit) break;
$fundCode = $fund['fund_code'] ?? '';
if (!empty($fundCode)) {
$fundInfo = getFundInfo($fundCode);
if ($fundInfo) {
$preloadedFundData[$fundCode] = $fundInfo;
$preloadCount++;
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基金推荐 - 组合基金监控</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="recommend_fund.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<h1><i class="fas fa-chart-line"></i> 基金推荐</h1>
<p>推荐您关注的优质基金</p>
</div>
</div>
<div class="recommend-container">
<a href="index.php" class="back-btn"><i class="fas fa-arrow-left"></i> 返回主页</a>
<div class="stats-display">
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-user-check"></i></div>
<div class="stat-number"><?php echo $userRecommendCount; ?></div>
<div class="stat-label">您已推荐基金数</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-rotate-left"></i></div>
<div class="stat-number"><?php echo 3 - $userRecommendCount; ?></div>
<div class="stat-label">剩余推荐次数</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-list-ul"></i></div>
<div class="stat-number"><?php echo $totalFunds; ?></div>
<div class="stat-label">总推荐基金数</div>
</div>
</div>
<?php if ($message): ?>
<div class="message <?php echo $messageType; ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>
<div class="info-box">
<strong><i class="fas fa-info-circle"></i> 推荐规则:</strong>
<ul>
<li>每个IP地址最多只能推荐3只基金</li>
<li>系统最多接受3000支基金推荐</li>
<li>基金代码为6位数字</li>
<li>推荐的基金将由管理员审核</li>
</ul>
</div>
<?php if ($userRecommendCount >= 3 || $totalFunds >= 3000): ?>
<div class="warning-box">
<strong><i class="fas fa-exclamation-triangle"></i> 提示:</strong>
<?php if ($userRecommendCount >= 3): ?>
您已达到每个IP最多推荐3只基金的限制。
<?php else: ?>
系统已达到最大推荐基金数量(3000支)。
<?php endif; ?>
</div>
<?php else: ?>
<form class="recommend-form" method="POST" action="">
<h2><i class="fas fa-plus-circle"></i> 推荐新基金</h2>
<div class="form-group">
<label for="fund_code">基金代码 (6位数字)</label>
<input type="text" id="fund_code" name="fund_code" placeholder="请输入基金代码" required>
</div>
<button type="submit" class="submit-btn">
<i class="fas fa-paper-plane"></i> 提交推荐
</button>
</form>
<?php endif; ?>
<?php if (!empty($recommendedFunds)): ?>
<div class="fund-list">
<h3><i class="fas fa-list"></i> 已推荐基金列表</h3>
<!-- 排序工具条 -->
<div class="sort-toolbar" aria-label="排序工具条">
<button class="sort-btn active" data-sort="time" data-order="desc" title="按最新推荐时间排序">
<i class="fas fa-clock"></i> 推荐时间
<span class="order-indicator">↓</span>
</button>
<button class="sort-btn" data-sort="count" data-order="desc" title="按基金数量排序">
<i class="fas fa-layer-group"></i> 基金数
<span class="order-indicator">↓</span>
</button>
<button class="sort-btn" data-sort="avg" data-order="desc" title="按平均涨幅排序">
<i class="fas fa-chart-line"></i> 平均涨幅
<span class="order-indicator">↓</span>
</button>
<button class="sort-reset" type="button" title="重置为推荐时间倒序">
<i class="fas fa-rotate-right"></i> 重置排序
</button>
</div>
<!-- IP分组下拉框 -->
<div class="dropdown-container">
<?php
// 按IP分组
$ipGroups = [];
foreach ($recommendedFunds as $fund) {
if (!isset($ipGroups[$fund['ip']])) {
$ipGroups[$fund['ip']] = ['funds' => [], 'latest_time' => ''];
}
$ipGroups[$fund['ip']]['funds'][] = $fund;
// 记录最新时间
if ($fund['timestamp'] > $ipGroups[$fund['ip']]['latest_time']) {
$ipGroups[$fund['ip']]['latest_time'] = $fund['timestamp'];
}
}
// 按最新时间排序
uasort($ipGroups, function($a, $b) {
return strcmp($b['latest_time'], $a['latest_time']);
});
foreach ($ipGroups as $ip => $group):
$isUserIp = ($ip == $userIp);
$ipClass = $isUserIp ? 'user-ip' : '';
?>
<div class="dropdown-item <?php echo $ipClass; ?>" data-ip="<?php echo htmlspecialchars($ip); ?>" data-latest-time="<?php echo htmlspecialchars($group['latest_time']); ?>" data-fund-count="<?php echo count($group['funds']); ?>" data-avg-change="">
<div class="dropdown-header" data-dropdown-id="ip-<?php echo md5($ip); ?>">
<div class="dropdown-info">
<strong>IP</strong><?php echo $ip; ?>
<?php if ($isUserIp): ?>
<span class="ip-badge"><i class="fas fa-user"></i> 我的IP</span>
<?php endif; ?>
<span class="dropdown-time"><strong>最新推荐时间:</strong><?php echo $group['latest_time']; ?></span>
<span class="dropdown-count"><strong>推荐基金数:</strong><?php echo count($group['funds']); ?></span>
<span class="dropdown-total-change">平均涨幅:<span id="total-change-<?php echo md5($ip); ?>">--</span></span>
</div>
<div class="dropdown-arrow">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div class="dropdown-content" id="ip-<?php echo md5($ip); ?>">
<table class="fund-details-table">
<thead>
<tr>
<th>基金代码</th>
<th>推荐时间</th>
<th>基金名称</th>
<th>当前涨幅</th>
</tr>
</thead>
<tbody>
<?php foreach ($group['funds'] as $fund): ?>
<tr>
<td><?php echo $fund['fund_code']; ?></td>
<td><?php echo $fund['timestamp']; ?></td>
<td><span class="fund-name" data-fund-code="<?php echo $fund['fund_code']; ?>">加载中...</span></td>
<td><span class="fund-change" data-fund-code="<?php echo $fund['fund_code']; ?>">--</span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="footer">
<p><i class="fas fa-info-circle"></i> 数据仅供参考,实际净值以基金公司公布为准</p>
<p><i class="fas fa-info-circle"></i> &copy; 2025 Tsama</p>
</div>
</div>
<script>
// 预加载的基金数据
const preloadedFundData = <?php echo json_encode($preloadedFundData, JSON_UNESCAPED_UNICODE); ?>;
// 将预加载数据传递给recommend_fund.js
window.preloadedFundData = preloadedFundData;
</script>
<script src="recommend_fund.js"></script>
</body>
</html>

1014
script.js Normal file

File diff suppressed because it is too large Load Diff

967
styles.css Normal file
View File

@@ -0,0 +1,967 @@
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--dark: #1f2937;
--light: #f8fafc;
--gray: #6b7280;
--border: #e5e7eb;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #334155;
line-height: 1.5;
}
/* 暗色主题背景与基础颜色 */
body.theme-dark {
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
color: #e5e7eb;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 15px;
}
/* 头部样式 */
.header {
text-align: center;
margin-bottom: 25px;
color: white;
}
.header-content {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(45deg, #fff, #e0f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 1rem;
opacity: 0.9;
margin-bottom: 15px;
}
/* 控制按钮 */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 10px 18px;
border: none;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.btn-outline {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-outline:hover {
background: rgba(255, 255, 255, 0.2);
}
/* 暗色主题下的卡片与边框微调 */
body.theme-dark .stat-card,
body.theme-dark .fund-card,
body.theme-dark .channel-stat-card,
body.theme-dark .content {
background: #111827;
border-color: rgba(255, 255, 255, 0.08);
color: #e5e7eb;
}
body.theme-dark .stat-label { opacity: 0.85; }
body.theme-dark .last-update { color: #e5e7eb; }
/* 统计面板样式 */
.stats-panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 20px;
margin-bottom: 25px;
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
}
.stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.stats-title {
font-size: 1.3rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.stat-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 12px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-value {
font-size: 1.8rem;
font-weight: 800;
margin-bottom: 5px;
background: linear-gradient(45deg, #fff, #e0f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
font-size: 0.85rem;
opacity: 0.9;
}
.stats-toggle {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.stats-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}
.recent-visits {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.visit-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.85rem;
}
.visit-item:last-child {
border-bottom: none;
}
.visit-ip {
font-family: 'Monaco', 'Consolas', monospace;
background: rgba(255, 255, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.visit-time {
opacity: 0.8;
}
/* 仪表盘样式 */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 25px;
}
.stat-card {
background: white;
padding: 20px 15px;
border-radius: 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
text-align: center;
transition: all 0.3s ease;
border: 1px solid var(--border);
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
.stat-icon {
font-size: 1.8rem;
margin-bottom: 10px;
opacity: 0.8;
}
.stat-number {
font-size: 1.6rem;
font-weight: 800;
margin-bottom: 5px;
line-height: 1.2;
}
.stat-label {
font-size: 0.85rem;
color: var(--gray);
font-weight: 600;
}
.stat-subtext {
font-size: 0.75rem;
color: var(--gray);
margin-top: 4px;
opacity: 0.7;
}
/* 特殊卡片样式 */
.total-funds .stat-number { color: var(--info); }
.up-funds .stat-number { color: var(--danger); }
.down-funds .stat-number { color: var(--success); }
.avg-change .stat-number { color: var(--warning); }
.total-investment .stat-number { color: var(--primary); }
.total-profit .stat-number {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.profit-badge, .loss-badge {
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.profit-badge {
background: linear-gradient(135deg, var(--success), #059669);
}
.loss-badge {
background: linear-gradient(135deg, var(--danger), #dc2626);
}
/* 渠道统计 */
.channel-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 25px;
}
.channel-stat-card {
background: white;
padding: 18px;
border-radius: 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border-left: 4px solid;
}
.channel-stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
.channel-stat-card.cmb { border-left-color: #e74c3c; }
.channel-stat-card.tt { border-left-color: #3498db; }
.channel-stat-card.zfb { border-left-color: #27ae60; }
.channel-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.channel-icon {
font-size: 1.5rem;
margin-right: 10px;
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.channel-name {
font-size: 1.1rem;
font-weight: 700;
color: var(--dark);
}
.channel-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
}
.channel-detail-item {
text-align: center;
padding: 8px;
background: #f8fafc;
border-radius: 8px;
}
.channel-detail-label {
font-size: 0.75rem;
color: var(--gray);
margin-bottom: 4px;
font-weight: 600;
}
.channel-detail-value {
font-size: 0.9rem;
font-weight: 700;
color: var(--dark);
}
.channel-profit-section {
text-align: center;
padding: 10px;
border-radius: 8px;
font-weight: 700;
font-size: 0.9rem;
margin-top: 8px;
}
.profit-positive {
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
color: #166534;
}
.profit-negative {
background: linear-gradient(135deg, #fee2e2, #fecaca);
color: #dc2626;
}
/* 基金卡片网格 */
.funds-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
margin-bottom: 25px;
}
.fund-card {
background: white;
border-radius: 14px;
padding: 18px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border-left: 4px solid var(--info);
}
.fund-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
.fund-card.positive {
border-left-color: var(--danger);
background: linear-gradient(135deg, #fff, #fef2f2);
}
.fund-card.negative {
border-left-color: var(--success);
background: linear-gradient(135deg, #fff, #f0fdf4);
}
.fund-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.fund-name {
font-size: 1rem;
font-weight: 700;
color: var(--dark);
flex: 1;
line-height: 1.3;
}
.fund-code {
background: #eef2ff;
padding: 4px 8px;
border-radius: 10px;
font-size: 0.8rem;
color: var(--primary);
font-weight: 600;
font-family: 'Monaco', 'Consolas', monospace;
}
.fund-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
.detail-item {
text-align: center;
padding: 10px;
background: #f8fafc;
border-radius: 8px;
}
.detail-label {
font-size: 0.75rem;
color: var(--gray);
margin-bottom: 4px;
font-weight: 600;
}
.detail-value {
font-size: 0.9rem;
font-weight: 700;
color: var(--dark);
}
/* 昨天涨幅显示样式 */
.yesterday-change {
font-weight: 800;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.change-display {
text-align: center;
padding: 10px;
border-radius: 10px;
font-size: 1.1rem;
font-weight: 800;
margin-bottom: 12px;
}
.change-positive {
background: linear-gradient(135deg, #fef2f2, #fee2e2);
color: var(--danger);
}
.change-negative {
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
color: var(--success);
}
.change-icon {
font-size: 1rem;
margin-right: 5px;
}
.profit-section {
background: #f8fafc;
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
}
.profit-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 0.85rem;
}
.profit-row:last-child {
margin-bottom: 0;
padding-top: 6px;
border-top: 1px solid var(--border);
font-weight: 800;
}
.profit-label {
color: var(--gray);
font-weight: 600;
}
.profit-value {
font-weight: 700;
}
.profit-positive {
color: var(--danger);
}
.profit-negative {
color: var(--success);
}
.channel-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.channel-badge {
padding: 5px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
color: red;
display: flex;
align-items: center;
gap: 4px;
}
.channel-cmb {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.channel-tt {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.channel-zfb {
background: linear-gradient(135deg, #27ae60, #229954);
}
.update-time {
font-size: 0.75rem;
color: var(--gray);
font-weight: 600;
}
.rank-badge {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 700;
margin-left: 6px;
}
.buy-hint {
font-size: 0.75rem;
color: #000000;
margin-top: 8px;
padding: 8px;
background: #f8fafc;
border-radius: 6px;
border-left: 3px solid var(--warning);
}
/* 错误信息 */
.error-section {
background: linear-gradient(135deg, #fef3c7, #fef3c7);
border: 1px solid #f59e0b;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.error-section h3 {
color: #92400e;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
font-size: 1rem;
}
.error-list {
list-style: none;
}
.error-list li {
padding: 4px 0;
color: #92400e;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
}
/* 页脚 */
.footer {
text-align: center;
color: white;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
.last-update {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 12px;
border-radius: 10px;
margin-bottom: 12px;
font-size: 0.9rem;
}
/* 动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fund-card, .stat-card, .channel-stat-card {
animation: fadeInUp 0.4s ease-out forwards;
opacity: 0;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40px;
color: white;
font-size: 1.2rem;
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-bottom: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 遵循系统减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 1.8rem;
}
.dashboard {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.channel-dashboard {
grid-template-columns: 1fr;
}
.funds-grid {
grid-template-columns: 1fr;
}
.fund-details {
grid-template-columns: 1fr;
}
.channel-details {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
.channel-info {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.profit-row {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.stats-header {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.dashboard {
grid-template-columns: 1fr;
}
.stat-card {
padding: 15px 10px;
}
.fund-card {
padding: 15px;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
/* 昨日涨幅样式 */
.yesterday-change {
font-weight: 700;
font-size: 0.9rem;
}
.yesterday-change.profit-positive {
color: var(--danger);
font-weight: 800;
}
.yesterday-change.profit-negative {
color: var(--success);
font-weight: 800;
}
/* 响应式调整 */
@media (max-width: 768px) {
.fund-details {
grid-template-columns: 1fr;
}
}
/* 暗黑主题:变量覆盖与文字对比度修复 */
body.theme-dark {
/* 提升暗黑模式下的全局文字可读性 */
--dark: #e5e7eb; /* 原为深灰,暗黑下改为浅灰 */
--gray: #cbd5e1; /* 次级文字颜色变浅 */
--border: #1f2937; /* 边框在暗色背景下的可见性 */
}
/* 基金卡片整体与常见文本 */
body.theme-dark .fund-card {
background: #111827;
color: #e5e7eb;
border-color: var(--border);
}
/* 正/负卡片在暗黑主题下不再使用浅色渐变,保持一致暗底 */
body.theme-dark .fund-card.positive,
body.theme-dark .fund-card.negative {
background: #0b1320;
}
/* 卡片内标题与数值文字提升对比度 */
body.theme-dark .fund-name,
body.theme-dark .detail-value,
body.theme-dark .channel-name,
body.theme-dark .channel-detail-value {
color: #74a2ff;
}
/* 次要说明文字颜色适配暗黑主题 */
body.theme-dark .detail-label,
body.theme-dark .update-time,
body.theme-dark .profit-label,
body.theme-dark .stat-label,
body.theme-dark .stat-subtext {
color: #cbd5e1;
}
/* 代码徽章在暗色背景下的可读性提升 */
body.theme-dark .fund-code {
background: #1f2937;
color: #93c5fd; /* 浅蓝提高对比 */
}
/* 细节块与收益块背景统一暗色,并提升边框对比 */
body.theme-dark .channel-detail-item,
body.theme-dark .detail-item,
body.theme-dark .profit-section {
background: #0b1320;
border-color: var(--border);
}
/* 渠道统计卡片暗黑配色 */
body.theme-dark .channel-dashboard .channel-stat-card {
background: #111827;
color: #e5e7eb;
}
/* 涨跌显示在暗底下采用更清晰的配色 */
body.theme-dark .change-positive {
background: linear-gradient(135deg, #1f2937, #0b1320);
color: #fca5a5; /* 浅红 */
}
body.theme-dark .change-negative {
background: linear-gradient(135deg, #1f2937, #0b1320);
color: #86efac; /* 浅绿 */
}
/* 卡片翻转动画样式 */
.flip-card {
perspective: 1000px;
}
.flip-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s ease;
}
.flip-card.flipped .flip-inner {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
top: 0;
left: 0;
width: 100%;
backface-visibility: hidden;
}
.card-front {
position: relative;
}
.card-back {
transform: rotateY(180deg);
padding: 10px;
}
/* 卡片背面图表容器 */
.card-back .chart-container {
width: 100%;
height: 220px;
}
.card-back canvas {
width: 100% !important;
height: 220px !important;
}
/* 暗色主题下卡片背面 */
body.theme-dark .card-back {
background: #111827;
}
/* 移动端适配:翻转卡片与图表尺寸 */
@media (max-width: 480px) {
.flip-card {
touch-action: manipulation;
}
.card-back .chart-container {
height: 160px;
}
.card-back canvas {
height: 160px !important;
}
}
/* 卡片背面关闭按钮 */
.flip-close {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0,0,0,0.05);
border: none;
color: #374151;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
line-height: 1;
}
.flip-close:hover {
background: rgba(0,0,0,0.1);
}
body.theme-dark .flip-close {
background: rgba(255,255,255,0.06);
color: #e5e7eb;
}
body.theme-dark .flip-close:hover {
background: rgba(255,255,255,0.12);
}