Files
invest-mind-store/packages/design-ui/script.js
2026-02-11 16:01:42 +08:00

852 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// 思投录 - 投资决策与复盘工具
class VestMindApp {
constructor() {
this.currentTab = 'portfolio';
this.modal = document.getElementById('modal');
this.modalTitle = document.getElementById('modal-title');
this.modalBody = document.getElementById('modal-body');
this.closeModalBtn = document.getElementById('close-modal');
this.init();
}
init() {
this.bindEvents();
this.showPage('portfolio');
this.loadData();
}
bindEvents() {
// 底部导航栏事件
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
const tab = e.currentTarget.dataset.tab;
this.switchTab(tab);
});
});
// 模态框关闭事件
this.closeModalBtn.addEventListener('click', () => {
this.hideModal();
});
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.hideModal();
}
});
// 工具卡片点击事件
document.querySelectorAll('.tool-card').forEach(card => {
card.addEventListener('click', (e) => {
const tool = e.currentTarget.dataset.tool;
this.openTool(tool);
});
});
// 添加按钮事件
document.querySelectorAll('.add-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const page = e.currentTarget.closest('.page').id;
this.showAddModal(page);
});
});
}
switchTab(tab) {
// 更新导航栏状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
// 显示对应页面
this.showPage(tab);
this.currentTab = tab;
}
showPage(pageId) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(page => {
page.classList.add('hidden');
});
// 显示目标页面
const targetPage = document.getElementById(`${pageId}-page`);
if (targetPage) {
targetPage.classList.remove('hidden');
}
}
loadData() {
// 模拟数据加载
this.loadPortfolioData();
this.loadPlansData();
this.loadRecordsData();
}
loadPortfolioData() {
// 模拟持仓数据
const holdings = [
{
name: '贵州茅台',
code: '600519',
shares: 100,
currentPrice: 1850.00,
profit: 2500.00,
profitRate: 15.6
},
{
name: '腾讯控股',
code: '00700',
shares: 200,
currentPrice: 320.00,
profit: -800.00,
profitRate: -1.2
},
{
name: '招商银行',
code: '600036',
shares: 500,
currentPrice: 42.50,
profit: 1250.00,
profitRate: 6.2
}
];
this.renderHoldings(holdings);
}
renderHoldings(holdings) {
const container = document.querySelector('.holdings-list');
if (!container) return;
container.innerHTML = holdings.map(holding => `
<div class="holding-item">
<div class="stock-info">
<div class="stock-name">${holding.name}</div>
<div class="stock-code">${holding.code}</div>
</div>
<div class="holding-details">
<div class="shares">${holding.shares}股</div>
<div class="current-price">¥${holding.currentPrice.toFixed(2)}</div>
<div class="profit ${holding.profit >= 0 ? 'positive' : 'negative'}">
${holding.profit >= 0 ? '+' : ''}¥${holding.profit.toFixed(2)} (${holding.profitRate >= 0 ? '+' : ''}${holding.profitRate}%)
</div>
</div>
</div>
`).join('');
}
loadPlansData() {
// 模拟交易计划数据
const plans = [
{
name: '招商银行',
code: '600036',
targetPrice: 45.00,
amount: 10000,
deadline: '2024-03-15',
progress: 60,
status: 'pending'
},
{
name: '中国平安',
code: '601318',
targetPrice: 55.00,
amount: 15000,
deadline: '2024-04-20',
progress: 30,
status: 'pending'
}
];
this.renderPlans(plans);
}
renderPlans(plans) {
const container = document.querySelector('.plans-list');
if (!container) return;
container.innerHTML = plans.map(plan => `
<div class="plan-item">
<div class="plan-header">
<div class="stock-info">
<div class="stock-name">${plan.name}</div>
<div class="stock-code">${plan.code}</div>
</div>
<div class="plan-status ${plan.status}">进行中</div>
</div>
<div class="plan-details">
<div class="plan-row">
<span class="label">目标价格:</span>
<span class="value">¥${plan.targetPrice.toFixed(2)}</span>
</div>
<div class="plan-row">
<span class="label">计划金额:</span>
<span class="value">¥${plan.amount.toLocaleString()}</span>
</div>
<div class="plan-row">
<span class="label">截止时间:</span>
<span class="value">${plan.deadline}</span>
</div>
</div>
<div class="plan-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${plan.progress}%"></div>
</div>
<span class="progress-text">已完成 ${plan.progress}%</span>
</div>
</div>
`).join('');
}
loadRecordsData() {
// 模拟交易记录数据
const records = [
{
date: '2024-01-15',
type: 'buy',
name: '贵州茅台',
code: '600519',
shares: 100,
price: 1600.00,
thoughts: '基于茅台品牌价值和长期增长潜力,认为当前价格具有投资价值。白酒行业龙头地位稳固,现金流优秀。'
},
{
date: '2024-01-10',
type: 'sell',
name: '比亚迪',
code: '002594',
shares: 200,
price: 280.00,
thoughts: '新能源汽车行业竞争加剧,估值偏高,选择获利了结。'
}
];
this.renderRecords(records);
}
renderRecords(records) {
const container = document.querySelector('.timeline');
if (!container) return;
container.innerHTML = records.map(record => `
<div class="timeline-item">
<div class="timeline-date">${record.date}</div>
<div class="timeline-content">
<div class="transaction-card">
<div class="transaction-header">
<div class="stock-info">
<div class="stock-name">${record.name}</div>
<div class="stock-code">${record.code}</div>
</div>
<div class="transaction-type ${record.type}">${record.type === 'buy' ? '买入' : '卖出'}</div>
</div>
<div class="transaction-details">
<div class="detail-row">
<span>数量:</span>
<span>${record.shares}股</span>
</div>
<div class="detail-row">
<span>价格:</span>
<span>¥${record.price.toFixed(2)}</span>
</div>
</div>
<div class="transaction-thoughts">
<h4>交易思考:</h4>
<p>${record.thoughts}</p>
</div>
</div>
</div>
</div>
`).join('');
}
openTool(tool) {
switch (tool) {
case 'checklist':
this.showChecklistModal();
break;
case 'calculator':
this.showCalculatorModal();
break;
case 'valuation':
this.showValuationModal();
break;
case 'freedom':
this.showFreedomModal();
break;
}
}
showChecklistModal() {
this.modalTitle.textContent = '投资检查清单';
this.modalBody.innerHTML = `
<div class="checklist-container">
<div class="checklist-section">
<h4>买入检查清单</h4>
<div class="checklist-items">
<label class="checklist-item">
<input type="checkbox">
<span>企业基本面是否优秀?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>估值是否合理?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>行业前景如何?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>管理层是否可信?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>现金流是否健康?</span>
</label>
</div>
</div>
<div class="checklist-section">
<h4>卖出检查清单</h4>
<div class="checklist-items">
<label class="checklist-item">
<input type="checkbox">
<span>基本面是否恶化?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>估值是否过高?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>是否有更好的投资机会?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>是否需要资金配置?</span>
</label>
</div>
</div>
</div>
`;
this.showModal();
}
showCalculatorModal() {
this.modalTitle.textContent = '复利计算器';
this.modalBody.innerHTML = `
<div class="calculator-container">
<div class="input-group">
<label>初始金额(元)</label>
<input type="number" id="initial-amount" placeholder="100000" value="100000">
</div>
<div class="input-group">
<label>每年投入(元)</label>
<input type="number" id="annual-investment" placeholder="50000" value="50000">
</div>
<div class="input-group">
<label>年复合增长率(%</label>
<input type="number" id="growth-rate" placeholder="10" value="10" step="0.1">
</div>
<div class="input-group">
<label>投资年限</label>
<input type="number" id="years" placeholder="10" value="10">
</div>
<button class="calculate-btn" onclick="app.calculateCompound()">计算</button>
<div class="result-container" id="calculator-result" style="display: none;">
<h4>计算结果</h4>
<div class="result-item">
<span>总投入:</span>
<span id="total-investment">¥0</span>
</div>
<div class="result-item">
<span>最终金额:</span>
<span id="final-amount">¥0</span>
</div>
<div class="result-item">
<span>总收益:</span>
<span id="total-profit">¥0</span>
</div>
</div>
</div>
`;
this.showModal();
}
calculateCompound() {
const initialAmount = parseFloat(document.getElementById('initial-amount').value) || 0;
const annualInvestment = parseFloat(document.getElementById('annual-investment').value) || 0;
const growthRate = parseFloat(document.getElementById('growth-rate').value) || 0;
const years = parseInt(document.getElementById('years').value) || 0;
const rate = growthRate / 100;
let totalInvestment = initialAmount + annualInvestment * years;
let finalAmount = initialAmount * Math.pow(1 + rate, years);
// 计算每年投入的复利
for (let i = 1; i <= years; i++) {
finalAmount += annualInvestment * Math.pow(1 + rate, years - i);
}
const totalProfit = finalAmount - totalInvestment;
document.getElementById('total-investment').textContent = `¥${totalInvestment.toLocaleString()}`;
document.getElementById('final-amount').textContent = `¥${finalAmount.toLocaleString()}`;
document.getElementById('total-profit').textContent = `¥${totalProfit.toLocaleString()}`;
document.getElementById('calculator-result').style.display = 'block';
}
showValuationModal() {
this.modalTitle.textContent = '估值工具';
this.modalBody.innerHTML = `
<div class="valuation-container">
<div class="valuation-tabs">
<button class="tab-btn active" data-method="tang">老唐估值法</button>
<button class="tab-btn" data-method="dcf">现金流折现</button>
</div>
<div class="valuation-content">
<div class="method-content" id="tang-method">
<div class="input-group">
<label>净利润(亿元)</label>
<input type="number" id="net-profit" placeholder="100" value="100">
</div>
<div class="input-group">
<label>无风险收益率(%</label>
<input type="number" id="risk-free-rate" placeholder="3" value="3" step="0.1">
</div>
<div class="input-group">
<label>合理PE倍数</label>
<input type="number" id="pe-ratio" placeholder="25" value="25">
</div>
<button class="calculate-btn" onclick="app.calculateTangValuation()">计算估值</button>
<div class="result-container" id="tang-result" style="display: none;">
<h4>估值结果</h4>
<div class="result-item">
<span>合理估值:</span>
<span id="tang-value">¥0亿</span>
</div>
</div>
</div>
</div>
</div>
`;
this.showModal();
}
calculateTangValuation() {
const netProfit = parseFloat(document.getElementById('net-profit').value) || 0;
const riskFreeRate = parseFloat(document.getElementById('risk-free-rate').value) || 0;
const peRatio = parseFloat(document.getElementById('pe-ratio').value) || 0;
const reasonablePE = 1 / (riskFreeRate / 100);
const valuation = netProfit * Math.min(peRatio, reasonablePE);
document.getElementById('tang-value').textContent = `¥${valuation.toFixed(2)}亿`;
document.getElementById('tang-result').style.display = 'block';
}
showFreedomModal() {
this.modalTitle.textContent = '自由目标';
this.modalBody.innerHTML = `
<div class="freedom-container">
<div class="input-group">
<label>目标资产(万元)</label>
<input type="number" id="target-assets" placeholder="1000" value="1000">
</div>
<div class="input-group">
<label>当前资产(万元)</label>
<input type="number" id="current-assets" placeholder="100" value="100">
</div>
<div class="input-group">
<label>年复合增长率(%</label>
<input type="number" id="freedom-growth-rate" placeholder="10" value="10" step="0.1">
</div>
<div class="input-group">
<label>每年投入(万元)</label>
<input type="number" id="freedom-annual-investment" placeholder="20" value="20">
</div>
<button class="calculate-btn" onclick="app.calculateFreedom()">计算达成时间</button>
<div class="result-container" id="freedom-result" style="display: none;">
<h4>自由目标分析</h4>
<div class="result-item">
<span>预计达成时间:</span>
<span id="freedom-years">0年</span>
</div>
<div class="result-item">
<span>届时年龄:</span>
<span id="freedom-age">0岁</span>
</div>
<div class="result-item">
<span>总投入:</span>
<span id="freedom-total-investment">¥0万</span>
</div>
</div>
</div>
`;
this.showModal();
}
calculateFreedom() {
const targetAssets = parseFloat(document.getElementById('target-assets').value) || 0;
const currentAssets = parseFloat(document.getElementById('current-assets').value) || 0;
const growthRate = parseFloat(document.getElementById('freedom-growth-rate').value) || 0;
const annualInvestment = parseFloat(document.getElementById('freedom-annual-investment').value) || 0;
const rate = growthRate / 100;
let years = 0;
let assets = currentAssets;
// 模拟逐年增长
while (assets < targetAssets && years < 50) {
assets = assets * (1 + rate) + annualInvestment;
years++;
}
const totalInvestment = currentAssets + annualInvestment * years;
const currentAge = 30; // 假设当前年龄
const targetAge = currentAge + years;
document.getElementById('freedom-years').textContent = `${years}`;
document.getElementById('freedom-age').textContent = `${targetAge}`;
document.getElementById('freedom-total-investment').textContent = `¥${totalInvestment.toFixed(2)}`;
document.getElementById('freedom-result').style.display = 'block';
}
showAddModal(page) {
switch (page) {
case 'portfolio-page':
this.showAddHoldingModal();
break;
case 'plans-page':
this.showAddPlanModal();
break;
case 'records-page':
this.showAddRecordModal();
break;
}
}
showAddHoldingModal() {
this.modalTitle.textContent = '添加持仓';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>股票名称</label>
<input type="text" id="stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>持股数量</label>
<input type="number" id="shares" placeholder="请输入持股数量">
</div>
<div class="input-group">
<label>成本价格</label>
<input type="number" id="cost-price" placeholder="请输入成本价格" step="0.01">
</div>
<button class="submit-btn" onclick="app.addHolding()">添加持仓</button>
</div>
`;
this.showModal();
}
showAddPlanModal() {
this.modalTitle.textContent = '新建交易计划';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>股票名称</label>
<input type="text" id="plan-stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="plan-stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>目标价格</label>
<input type="number" id="target-price" placeholder="请输入目标价格" step="0.01">
</div>
<div class="input-group">
<label>计划金额</label>
<input type="number" id="plan-amount" placeholder="请输入计划金额">
</div>
<div class="input-group">
<label>截止时间</label>
<input type="date" id="deadline">
</div>
<button class="submit-btn" onclick="app.addPlan()">创建计划</button>
</div>
`;
this.showModal();
}
showAddRecordModal() {
this.modalTitle.textContent = '记录交易';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>交易类型</label>
<select id="transaction-type">
<option value="buy">买入</option>
<option value="sell">卖出</option>
</select>
</div>
<div class="input-group">
<label>股票名称</label>
<input type="text" id="record-stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="record-stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>交易数量</label>
<input type="number" id="record-shares" placeholder="请输入交易数量">
</div>
<div class="input-group">
<label>交易价格</label>
<input type="number" id="record-price" placeholder="请输入交易价格" step="0.01">
</div>
<div class="input-group">
<label>交易思考</label>
<textarea id="transaction-thoughts" placeholder="请记录您的交易思考..." rows="4"></textarea>
</div>
<button class="submit-btn" onclick="app.addRecord()">记录交易</button>
</div>
`;
this.showModal();
}
addHolding() {
// 模拟添加持仓
this.showToast('持仓添加成功!');
this.hideModal();
}
addPlan() {
// 模拟添加计划
this.showToast('交易计划创建成功!');
this.hideModal();
}
addRecord() {
// 模拟添加记录
this.showToast('交易记录添加成功!');
this.hideModal();
}
showModal() {
this.modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
hideModal() {
this.modal.classList.remove('show');
document.body.style.overflow = 'auto';
}
showToast(message) {
// 创建提示框
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(toast);
// 显示动画
setTimeout(() => {
toast.style.opacity = '1';
}, 100);
// 自动隐藏
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 2000);
}
}
// 初始化应用
const app = new VestMindApp();
// 添加一些额外的样式
const additionalStyles = `
<style>
.checklist-container {
max-height: 400px;
overflow-y: auto;
}
.checklist-section {
margin-bottom: 20px;
}
.checklist-section h4 {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.checklist-item {
display: flex;
align-items: center;
padding: 8px 0;
cursor: pointer;
font-size: 14px;
color: #374151;
}
.checklist-item input[type="checkbox"] {
margin-right: 12px;
width: 16px;
height: 16px;
accent-color: #8b5cf6;
}
.calculator-container,
.valuation-container,
.freedom-container,
.form-container {
max-height: 400px;
overflow-y: auto;
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.input-group input,
.input-group select,
.input-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.calculate-btn,
.submit-btn {
width: 100%;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.calculate-btn:hover,
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.result-container {
margin-top: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #8b5cf6;
}
.result-container h4 {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.result-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.result-item span:first-child {
color: #6b7280;
}
.result-item span:last-child {
font-weight: 600;
color: #374151;
}
.valuation-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.tab-btn {
flex: 1;
padding: 10px;
background: none;
border: none;
font-size: 14px;
font-weight: 500;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.tab-btn.active {
color: #8b5cf6;
border-bottom-color: #8b5cf6;
}
.tab-btn:hover {
color: #8b5cf6;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', additionalStyles);