feat: 第一次生产需求文档

This commit is contained in:
R524809
2025-10-10 13:30:16 +08:00
commit 8eb49f02b1
7 changed files with 2381 additions and 0 deletions

851
script.js Normal file
View File

@@ -0,0 +1,851 @@
// 思投录 - 投资决策与复盘工具
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);