初始化版本
This commit is contained in:
334
404.html
Normal file
334
404.html
Normal 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
632
admin.css
Normal 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
849
admin.js
Normal 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
243
admin.php
Normal 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()">×</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
917
admin_api.php
Normal 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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
5
composer.json
Normal file
5
composer.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"ccxt/ccxt": "^1.53"
|
||||||
|
}
|
||||||
|
}
|
||||||
446
composer.lock
generated
Normal file
446
composer.lock
generated
Normal 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
1
data/cache/fund_001071.json
vendored
Normal 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
1
data/cache/fund_003567.json
vendored
Normal 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
1
data/cache/fund_003598.json
vendored
Normal 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
1
data/cache/fund_003766.json
vendored
Normal 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
1
data/cache/fund_004206.json
vendored
Normal 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
1
data/cache/fund_005965.json
vendored
Normal 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
1
data/cache/fund_008327.json
vendored
Normal 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
1
data/cache/fund_011103.json
vendored
Normal 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
1
data/cache/fund_011815.json
vendored
Normal 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
1
data/cache/fund_012863.json
vendored
Normal 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
1
data/cache/fund_014320.json
vendored
Normal 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
1
data/cache/fund_017560.json
vendored
Normal 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
1
data/cache/fund_018994.json
vendored
Normal 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
1
data/cache/fund_019432.json
vendored
Normal 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
1
data/cache/fund_022364.json
vendored
Normal 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
1
data/cache/fund_022365.json
vendored
Normal 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
1
data/cache/fund_023350.json
vendored
Normal 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
70
data/fund_config.json
Normal 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
417
data/fund_daily_data.json
Normal 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
119
data/fund_history.json
Normal 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
22
data/fund_names.json
Normal 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
1
data/last_cleanup.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1762443884
|
||||||
269
data/operation_log.json
Normal file
269
data/operation_log.json
Normal 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
9
data/recommend_logs.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
11
data/recommended_funds.json
Normal file
11
data/recommended_funds.json
Normal 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
93
data/stats.json
Normal 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
4293
data/visits.json
Normal file
File diff suppressed because it is too large
Load Diff
82
index.php
Normal file
82
index.php
Normal 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 © 东方财富</p>
|
||||||
|
<p><i class="fas fa-info-circle"></i> © 2025 Tsama</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
241
login.php
Normal file
241
login.php
Normal 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
711
recommend_fund.css
Normal 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
657
recommend_fund.js
Normal 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
566
recommend_fund.php
Normal 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> © 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>
|
||||||
967
styles.css
Normal file
967
styles.css
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user