Trae Assistant commited on
Commit
253cabf
·
1 Parent(s): 76ee95f

Enhance: Local assets, default data, gunicorn support

Browse files
Files changed (5) hide show
  1. Dockerfile +12 -2
  2. README.md +40 -39
  3. app.py +60 -73
  4. requirements.txt +2 -2
  5. templates/index.html +395 -623
Dockerfile CHANGED
@@ -2,12 +2,22 @@ FROM python:3.9-slim
2
 
3
  WORKDIR /app
4
 
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
 
8
  COPY . .
9
 
10
- # Create data directory and set permissions
11
- RUN mkdir -p data && chmod 777 data
 
 
12
 
 
 
 
 
 
 
13
  CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
 
2
 
3
  WORKDIR /app
4
 
5
+ # Copy requirements and install
6
  COPY requirements.txt .
7
  RUN pip install --no-cache-dir -r requirements.txt
8
 
9
+ # Copy application code
10
  COPY . .
11
 
12
+ # Create a non-root user
13
+ RUN useradd -m -u 1000 user && \
14
+ chown -R user:user /app && \
15
+ chmod -R 777 /app
16
 
17
+ USER user
18
+
19
+ # Expose port
20
+ EXPOSE 7860
21
+
22
+ # Run the application
23
  CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,61 +1,62 @@
1
  ---
2
- title: 创业估值与融资模拟引擎
3
- emoji: 💰
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
- short_description: 创业项目早期估值、融资稀释模拟及投资意向书生成工具
 
9
  ---
10
 
11
- # 创业估值与融资模拟引擎 (Startup Valuation & Deal Structuring Engine)
12
 
13
- 一个专为早期创业设计的综合工具,帮助你科学估值、模拟融资稀释并生成标准的投资意向书摘要
14
 
15
- ## 核心功能
16
 
17
- 1. **多维度估值模型**
18
- * **Berkus Method**: 针对早期无收入项目经典估值法,通过评估5个关键风险维度(意、原型、团队、战略关系、销售)来计算估值
19
- * **Scorecard Method (记分卡法)**: 通过对比行平均估值,并根据团队、市场、产品等7个维度进行加权调整,得出更符合市场行情的估值。
 
20
 
21
- 2. **融资与股权稀释模拟 (Cap Table Simulator)**
22
- * 直观的股权结构表计算
23
- * 支持 Pre-money 估值融资金额期权池 (Option Pool) 设置
24
- * 自动计算融资后 Post-money 估值、每股价格以及创始人、投资人、期权池的持股比例
25
- * 可视化饼图展示。
26
 
27
- 3. **投资意向书生成器 (Term Sheet Lite)**
28
- * 快速生成一份简易的投资条款清单 (Term Sheet)
29
- * 支持自定义清算优先权 (Liquidation Preference)董事会席位分配关键条款
30
- * 一键复制内容
31
 
32
- 4. **专业报告导出**
33
- * 生成包含项目概况、估值分析、拟议融资构的专业报告
34
- * 支持打印为 PDF 格式,方便发送给投资人或合伙人
 
35
 
36
  ## 技术栈
37
 
 
38
  * **Backend**: Python Flask
39
- * **Frontend**: Vue.js 3, Tailwind CSS, Chart.js
40
- * **Deployment**: Docker (Compatible with Hugging Face Spaces)
41
 
42
- ## 本地运行
43
 
44
- ```bash
45
- # 构建镜像
46
- docker build -t valuation-engine .
 
 
 
 
 
47
 
48
- # 运行容器
49
- docker run -p 7860:7860 valuation-engine
50
- ```
51
 
52
- 或者直接运行 Python:
53
 
54
  ```bash
55
- pip install -r requirements.txt
56
- python app.py
57
  ```
58
-
59
- ## 许可证
60
-
61
- MIT License
 
1
  ---
2
+ title: 创业估值引擎 (Startup Valuation Engine)
3
+ emoji: 📈
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ short_description: 多模型早期项目估值工具 (Berkus/Scorecard/RiskFactor)
9
+ app_port: 7860
10
  ---
11
 
12
+ # 创业估值引擎 (Startup Valuation Engine)
13
 
14
+ 这是一个专为早期创业项目设计的估值计算工具,集成了三种主流的早期项目估值方法帮助创业者和投资人快速进行估值测算
15
 
16
+ ## 主要功能
17
 
18
+ 1. **Berkus 方法 (Berkus Method)**
19
+ * 适用于极早期的公司
20
+ * 基于5个关键成功驱动因素(商创意、原型、团队、战略关系、产品推出)进行评估
21
+ * 每个因素最高可贡献 $500,000 (可配置)。
22
 
23
+ 2. **记分卡方法 (Scorecard Valuation Method)**
24
+ * 基于行业平均估值(Pre-money Valuation)
25
+ * 根据7个加权因素(团队市场机会产品技术、竞争环境等)对平均估值进行调整
26
+ * 直观展示相对于行业平均水平优劣势
 
27
 
28
+ 3. **风险因子求和法 (Risk Factor Summation Method)**
29
+ * 从基础估值出发
30
+ * 综合考虑12个风险维度(管理政策、制造、销售、融资、竞争、技术
31
+ * 每个风险等级对应增减 $250,000 的估值调整
32
 
33
+ 4. **综合分析与导出**
34
+ * **可视化图表**: 自动生成柱状图,对比三种方法的估值结果及平均值
35
+ * **历史记录**: 本地保存和加载估值记录(基于 LocalStorage/JSON)
36
+ * **报告导出**: 一键生成 PNG 格式的估值报告,便于分享和演示。
37
 
38
  ## 技术栈
39
 
40
+ * **Frontend**: Vue 3 (Composition API), Tailwind CSS, Chart.js
41
  * **Backend**: Python Flask
42
+ * **Deployment**: Docker (Hugging Face Spaces compatible)
 
43
 
44
+ ## 使用说明
45
 
46
+ 1. 输入项目名称和行业信息。
47
+ 2. 在不同标签页中调整各模型的参数。
48
+ * **Berkus**: 拖动滑块设置每个维度的价值。
49
+ * **Scorecard**: 设置行业基准估值,调整各项权重表现。
50
+ * **Risk**: 对各项风险进行评级(从非常消极到非常积极)。
51
+ 3. 查看顶部的综合估值面板和底部的可视化图表。
52
+ 4. 点击“保存记录”将当前数据保存到服务器。
53
+ 5. 点击“导出报告”下载图片格式的估值报告。
54
 
55
+ ## 部署
 
 
56
 
57
+ 本项目支持通过 Docker 部署,已配置 `Dockerfile`,可直接部署至 Hugging Face Spaces。
58
 
59
  ```bash
60
+ docker build -t startup-valuation-engine .
61
+ docker run -p 7860:7860 startup-valuation-engine
62
  ```
 
 
 
 
app.py CHANGED
@@ -1,99 +1,86 @@
1
- from flask import Flask, render_template, request, jsonify, send_file
2
- from werkzeug.exceptions import RequestEntityTooLarge
3
  import os
4
  import json
5
- import datetime
 
 
 
 
 
 
6
 
7
  app = Flask(__name__)
8
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
 
9
 
10
- # Data storage path
11
- DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
12
- os.makedirs(DATA_DIR, exist_ok=True)
13
 
14
- @app.errorhandler(RequestEntityTooLarge)
15
- def handle_file_too_large(e):
16
- return jsonify({"success": False, "message": "File too large. Maximum size is 16MB."}), 413
 
 
 
 
 
 
17
 
18
- @app.errorhandler(404)
19
- def page_not_found(e):
20
- return render_template('index.html'), 404
 
 
 
 
 
21
 
22
  @app.route('/')
23
  def index():
24
  return render_template('index.html')
25
 
26
- @app.route('/health')
27
- def health_check():
28
- return "OK", 200
29
 
30
- @app.route('/api/save', methods=['POST'])
31
- def save_project():
32
  try:
33
- data = request.json
34
- if not data:
35
- return jsonify({"success": False, "message": "No data provided"}), 400
36
-
37
- project_name = data.get('projectName', 'untitled')
38
- # Sanitize filename
39
- safe_name = "".join([c for c in project_name if c.isalpha() or c.isdigit() or c in (' ', '-', '_')]).strip()
40
- if not safe_name:
41
- safe_name = 'untitled'
42
 
43
- filename = f"{safe_name}.json"
44
- filepath = os.path.join(DATA_DIR, filename)
 
45
 
46
- with open(filepath, 'w', encoding='utf-8') as f:
47
- json.dump(data, f, ensure_ascii=False, indent=2)
 
 
 
 
48
 
49
- return jsonify({"success": True, "message": "项目已保存", "filename": filename})
 
50
  except Exception as e:
51
- return jsonify({"success": False, "message": str(e)}), 500
 
52
 
53
- @app.route('/api/load', methods=['GET'])
54
- def load_projects():
55
  try:
56
- if not os.path.exists(DATA_DIR):
57
- os.makedirs(DATA_DIR, exist_ok=True)
58
-
59
- files = [f for f in os.listdir(DATA_DIR) if f.endswith('.json')]
60
- projects = []
61
- for f in files:
62
- filepath = os.path.join(DATA_DIR, f)
63
- try:
64
- with open(filepath, 'r', encoding='utf-8') as file:
65
- data = json.load(file)
66
- projects.append({
67
- "filename": f,
68
- "projectName": data.get('projectName', f.replace('.json', '')),
69
- "updatedAt": datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %H:%M:%S')
70
- })
71
- except:
72
- continue
73
- # Sort by update time desc
74
- projects.sort(key=lambda x: x['updatedAt'], reverse=True)
75
- return jsonify({"success": True, "projects": projects})
76
  except Exception as e:
77
- return jsonify({"success": False, "message": str(e)}), 500
 
78
 
79
- @app.route('/api/load/<filename>', methods=['GET'])
80
- def load_project_detail(filename):
81
- try:
82
- # Security check
83
- if '..' in filename or not filename.endswith('.json'):
84
- return jsonify({"success": False, "message": "Invalid filename"}), 400
85
-
86
- filepath = os.path.join(DATA_DIR, filename)
87
- if not os.path.exists(filepath):
88
- return jsonify({"success": False, "message": "File not found"}), 404
89
-
90
- with open(filepath, 'r', encoding='utf-8') as f:
91
- data = json.load(f)
92
- return jsonify({"success": True, "data": data})
93
- except Exception as e:
94
- return jsonify({"success": False, "message": str(e)}), 500
95
 
96
  if __name__ == '__main__':
97
- # Use 7860 for Hugging Face Spaces default
98
  port = int(os.environ.get('PORT', 7860))
99
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
 
1
  import os
2
  import json
3
+ import logging
4
+ from datetime import datetime
5
+ from flask import Flask, render_template, jsonify, request, send_from_directory
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
 
11
  app = Flask(__name__)
12
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
13
+ app.config['JSON_AS_ASCII'] = False
14
 
15
+ # Data storage
16
+ DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'valuations.json')
 
17
 
18
+ def load_data():
19
+ if os.path.exists(DATA_FILE):
20
+ try:
21
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
22
+ return json.load(f)
23
+ except Exception as e:
24
+ logger.error(f"Error loading data: {e}")
25
+ return []
26
+ return []
27
 
28
+ def save_data(data):
29
+ try:
30
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
31
+ json.dump(data, f, ensure_ascii=False, indent=2)
32
+ return True
33
+ except Exception as e:
34
+ logger.error(f"Error saving data: {e}")
35
+ return False
36
 
37
  @app.route('/')
38
  def index():
39
  return render_template('index.html')
40
 
41
+ @app.route('/api/valuations', methods=['GET'])
42
+ def get_valuations():
43
+ return jsonify(load_data())
44
 
45
+ @app.route('/api/valuations', methods=['POST'])
46
+ def save_valuation():
47
  try:
48
+ new_item = request.json
49
+ if not new_item:
50
+ return jsonify({"error": "No data provided"}), 400
 
 
 
 
 
 
51
 
52
+ # Add metadata
53
+ new_item['id'] = str(int(datetime.now().timestamp() * 1000))
54
+ new_item['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
55
 
56
+ data = load_data()
57
+ data.insert(0, new_item) # Newest first
58
+
59
+ # Limit history to 50 items
60
+ if len(data) > 50:
61
+ data = data[:50]
62
 
63
+ save_data(data)
64
+ return jsonify({"success": True, "data": new_item})
65
  except Exception as e:
66
+ logger.error(f"Error saving valuation: {e}")
67
+ return jsonify({"error": str(e)}), 500
68
 
69
+ @app.route('/api/valuations/<id>', methods=['DELETE'])
70
+ def delete_valuation(id):
71
  try:
72
+ data = load_data()
73
+ data = [item for item in data if item.get('id') != id]
74
+ save_data(data)
75
+ return jsonify({"success": True})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  except Exception as e:
77
+ logger.error(f"Error deleting valuation: {e}")
78
+ return jsonify({"error": str(e)}), 500
79
 
80
+ @app.route('/health')
81
+ def health():
82
+ return jsonify({"status": "healthy"})
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  if __name__ == '__main__':
 
85
  port = int(os.environ.get('PORT', 7860))
86
+ app.run(host='0.0.0.0', port=port)
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
- flask==3.0.0
2
- gunicorn==21.2.0
 
1
+ flask
2
+ gunicorn
templates/index.html CHANGED
@@ -3,20 +3,15 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>创业估值与融资模拟引擎 | Startup Valuation Engine</title>
7
  <script src="/static/js/tailwindcss.js"></script>
8
  <script src="/static/js/vue.global.js"></script>
9
  <script src="/static/js/chart.js"></script>
10
  <script src="/static/js/html2canvas.min.js"></script>
11
- <!-- Icons -->
12
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
13
  <style>
14
- @media print {
15
- .no-print { display: none !important; }
16
- .print-only { display: block !important; }
17
- body { background: white; }
18
- .card { box-shadow: none; border: 1px solid #ddd; }
19
- }
20
  .slider-thumb::-webkit-slider-thumb {
21
  -webkit-appearance: none;
22
  appearance: none;
@@ -26,367 +21,227 @@
26
  cursor: pointer;
27
  border-radius: 50%;
28
  }
29
- /* Custom scrollbar */
30
- ::-webkit-scrollbar { width: 8px; }
31
- ::-webkit-scrollbar-track { background: #f1f1f1; }
32
- ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
33
- ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
34
-
35
- [v-cloak] { display: none; }
36
  </style>
37
  </head>
38
- <body class="bg-slate-50 text-slate-800 font-sans">
39
- <div id="app" v-cloak>
40
  <!-- Header -->
41
- <header class="bg-indigo-600 text-white shadow-lg no-print">
42
- <div class="container mx-auto px-4 py-4 flex justify-between items-center">
43
- <div class="flex items-center space-x-3">
44
- <i class="fa-solid fa-chart-line text-2xl"></i>
45
- <h1 class="text-xl font-bold">创业估值与融资模拟引擎</h1>
46
- </div>
47
- <div class="flex items-center space-x-4">
48
- <button @click="loadDemoData" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-400 rounded-md text-sm transition flex items-center">
49
- <i class="fa-solid fa-magic mr-2"></i> 演示数据
50
- </button>
51
- <button @click="saveProject" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-400 rounded-md text-sm transition flex items-center">
52
- <i class="fa-solid fa-save mr-2"></i> 保存
53
- </button>
54
- <button @click="showLoadModal = true" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-400 rounded-md text-sm transition flex items-center">
55
- <i class="fa-solid fa-folder-open mr-2"></i> 打开
56
- </button>
57
- <button @click="printReport" class="px-4 py-2 bg-white text-indigo-600 hover:bg-gray-100 rounded-md text-sm font-semibold transition flex items-center">
58
- <i class="fa-solid fa-print mr-2"></i> 打印报告
59
- </button>
60
- <button @click="exportImage" class="px-4 py-2 bg-green-500 hover:bg-green-400 text-white rounded-md text-sm font-semibold transition flex items-center">
61
- <i class="fa-solid fa-image mr-2"></i> 导出图片
62
- </button>
63
  </div>
64
  </div>
65
- </header>
66
-
67
- <!-- Main Content -->
68
- <main class="container mx-auto px-4 py-8">
69
-
70
- <!-- Project Info Card -->
71
- <div class="bg-white rounded-xl shadow-sm p-6 mb-6 border border-slate-200">
72
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
73
- <div>
74
- <label class="block text-sm font-medium text-slate-500 mb-1">项目名称</label>
75
- <input type="text" v-model="project.projectName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="例如:未来科技">
76
- </div>
77
- <div>
78
- <label class="block text-sm font-medium text-slate-500 mb-1">所属行业</label>
79
- <select v-model="project.industry" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none bg-white">
80
- <option value="SaaS">SaaS / 软件</option>
81
- <option value="E-commerce">电商 / 消费品</option>
82
- <option value="AI">人工智能 / 大数据</option>
83
- <option value="Hardware">智能硬件 / IoT</option>
84
- <option value="Biotech">生物科技 / 医疗</option>
85
- <option value="Other">其他</option>
86
- </select>
87
- </div>
88
- <div>
89
- <label class="block text-sm font-medium text-slate-500 mb-1">当前阶段</label>
90
- <select v-model="project.stage" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none bg-white">
91
- <option value="Idea">创意阶段 (Idea)</option>
92
- <option value="MVP">原型阶段 (MVP)</option>
93
- <option value="Early Revenue">早期收入 (Early Revenue)</option>
94
- <option value="Growth">成长期 (Growth)</option>
95
- </select>
96
- </div>
97
- </div>
98
- </div>
99
-
100
- <!-- Tabs Navigation -->
101
- <div class="flex space-x-1 bg-slate-200 p-1 rounded-lg mb-6 no-print overflow-x-auto">
102
- <button v-for="tab in tabs" :key="tab.id"
103
- @click="currentTab = tab.id"
104
- :class="['flex-1 py-2 px-4 rounded-md text-sm font-medium transition whitespace-nowrap', currentTab === tab.id ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-600 hover:text-indigo-600']">
105
- <i :class="tab.icon + ' mr-2'"></i>${ tab.name }
106
  </button>
107
  </div>
 
108
 
109
- <!-- Tab 1: Berkus Method -->
110
- <div v-show="currentTab === 'berkus'" class="space-y-6">
111
- <div class="bg-white rounded-xl shadow-sm p-6 border border-slate-200">
112
- <div class="flex justify-between items-start mb-4">
113
- <div>
114
- <h2 class="text-xl font-bold text-slate-800">Berkus 估值法</h2>
115
- <p class="text-sm text-slate-500 mt-1">适用于尚未产生收入的早期初创企业。通过评估5个关键风险维度的降低程度来计算估值。</p>
116
- </div>
117
- <div class="text-right">
118
- <div class="text-sm text-slate-500">当前估值 (Pre-money)</div>
119
- <div class="text-3xl font-bold text-indigo-600">$${ formatNumber(berkusValuation) }</div>
 
 
 
 
 
 
 
 
120
  </div>
 
 
 
121
  </div>
 
 
122
 
123
- <div class="space-y-6 mt-8">
124
- <div v-for="(factor, index) in project.berkusFactors" :key="index" class="bg-slate-50 p-4 rounded-lg">
125
- <div class="flex justify-between mb-2">
126
- <label class="font-semibold text-slate-700">${ factor.name }</label>
127
- <span class="font-mono text-indigo-600">$${ formatNumber(factor.value) }</span>
 
 
 
 
 
 
 
 
 
128
  </div>
129
- <input type="range" v-model.number="factor.value" min="0" max="500000" step="50000" class="w-full h-2 bg-slate-300 rounded-lg appearance-none cursor-pointer">
130
- <p class="text-xs text-slate-500 mt-2">${ factor.desc }</p>
131
  </div>
132
  </div>
133
- </div>
134
- </div>
135
 
136
- <!-- Tab 2: Scorecard Method -->
137
- <div v-show="currentTab === 'scorecard'" class="space-y-6">
138
- <div class="bg-white rounded-xl shadow-sm p-6 border border-slate-200">
139
- <div class="flex justify-between items-start mb-4">
140
- <div>
141
- <h2 class="text-xl font-bold text-slate-800">记分卡估值法 (Scorecard Method)</h2>
142
- <p class="text-sm text-slate-500 mt-1">通过与同行业类似初创企业的平均估值进行对比,根据团队、市场等加权因子调整估值。</p>
143
  </div>
144
- <div class="text-right">
145
- <div class="text-sm text-slate-500">当前估值 (Pre-money)</div>
146
- <div class="text-3xl font-bold text-purple-600">$${ formatNumber(scorecardValuation) }</div>
147
  </div>
148
- </div>
149
-
150
- <div class="mb-8">
151
- <label class="block text-sm font-medium text-slate-700 mb-2">行业平均 Pre-money 估值</label>
152
- <div class="flex items-center">
153
- <span class="text-slate-500 mr-2">$</span>
154
- <input type="number" v-model.number="project.industryAverageValuation" class="w-48 px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none">
155
  </div>
156
  </div>
157
 
158
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
159
- <div v-for="(factor, index) in project.scorecardFactors" :key="index" class="bg-slate-50 p-4 rounded-lg border border-slate-100">
160
- <div class="flex justify-between items-center mb-2">
161
- <span class="font-semibold text-slate-700 text-sm">${ factor.name } (权重: ${ factor.weight }%)</span>
162
- <span class="text-xs font-bold px-2 py-1 rounded"
163
- :class="factor.percentage >= 100 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'">
164
- ${ factor.percentage }%
165
- </span>
166
- </div>
167
- <input type="range" v-model.number="factor.percentage" min="50" max="150" step="5" class="w-full h-2 bg-slate-300 rounded-lg appearance-none cursor-pointer">
168
- <div class="flex justify-between text-xs text-slate-400 mt-1">
169
- <span>弱 (50%)</span>
170
- <span>平均 (100%)</span>
171
- <span>强 (150%)</span>
172
- </div>
173
  </div>
174
- </div>
175
- </div>
176
- </div>
177
 
178
- <!-- Tab 3: Funding & Dilution -->
179
- <div v-show="currentTab === 'dilution'" class="space-y-6">
180
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
181
- <!-- Inputs -->
182
- <div class="bg-white rounded-xl shadow-sm p-6 border border-slate-200 lg:col-span-1">
183
- <h3 class="text-lg font-bold mb-4">融资参数设定</h3>
184
-
185
- <div class="space-y-4">
186
- <div>
187
- <label class="block text-sm font-medium text-slate-700 mb-1">选定 Pre-money 估值</label>
188
- <div class="flex space-x-2 mb-2">
189
- <button @click="project.selectedPreMoney = berkusValuation" class="text-xs bg-indigo-100 text-indigo-700 px-2 py-1 rounded hover:bg-indigo-200">使用 Berkus</button>
190
- <button @click="project.selectedPreMoney = scorecardValuation" class="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded hover:bg-purple-200">使用 Scorecard</button>
191
  </div>
192
- <div class="relative">
193
- <span class="absolute left-3 top-2 text-slate-500">$</span>
194
- <input type="number" v-model.number="project.selectedPreMoney" class="w-full pl-8 pr-4 py-2 border border-slate-300 rounded-lg">
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
  </div>
197
 
198
- <div>
199
- <label class="block text-sm font-medium text-slate-700 mb-1">融资金额 (Investment)</label>
200
- <div class="relative">
201
- <span class="absolute left-3 top-2 text-slate-500">$</span>
202
- <input type="number" v-model.number="project.investmentAmount" class="w-full pl-8 pr-4 py-2 border border-slate-300 rounded-lg">
 
 
 
 
 
 
203
  </div>
204
- </div>
205
-
206
- <div>
207
- <label class="block text-sm font-medium text-slate-700 mb-1">期权池 (Option Pool %)</label>
208
- <div class="relative">
209
- <input type="number" v-model.number="project.optionPool" min="0" max="30" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
210
- <span class="absolute right-3 top-2 text-slate-500">%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </div>
212
- <p class="text-xs text-slate-500 mt-1">通常为 Post-money 的 10-20%</p>
213
  </div>
214
-
215
- <div>
216
- <label class="block text-sm font-medium text-slate-700 mb-1">当前总股数</label>
217
- <input type="number" v-model.number="project.totalShares" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
218
- </div>
219
- </div>
220
- </div>
221
 
222
- <!-- Results -->
223
- <div class="bg-white rounded-xl shadow-sm p-6 border border-slate-200 lg:col-span-2">
224
- <h3 class="text-lg font-bold mb-4">融资后股权结构 (Post-money Cap Table)</h3>
225
-
226
- <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
227
- <div>
228
- <div class="mb-6">
229
- <div class="text-sm text-slate-500">Post-money 估值</div>
230
- <div class="text-2xl font-bold text-green-600">$${ formatNumber(postMoneyValuation) }</div>
 
 
231
  </div>
232
- <div class="mb-6">
233
- <div class="text-sm text-slate-500">每股价格</div>
234
- <div class="text-xl font-bold text-slate-800">$${ formatNumber(pricePerShare, 2) }</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </div>
236
-
237
- <table class="w-full text-sm">
238
- <thead>
239
- <tr class="border-b">
240
- <th class="text-left py-2">股东</th>
241
- <th class="text-right py-2">股份 %</th>
242
- <th class="text-right py-2">价值</th>
243
- </tr>
244
- </thead>
245
- <tbody>
246
- <tr class="text-slate-700">
247
- <td class="py-2">创始人团队</td>
248
- <td class="text-right font-mono">${ formatNumber(foundersPercent, 1) }%</td>
249
- <td class="text-right font-mono">$${ formatNumber(foundersValue) }</td>
250
- </tr>
251
- <tr class="text-indigo-600 font-semibold">
252
- <td class="py-2">新投资人</td>
253
- <td class="text-right font-mono">${ formatNumber(investorsPercent, 1) }%</td>
254
- <td class="text-right font-mono">$${ formatNumber(project.investmentAmount) }</td>
255
- </tr>
256
- <tr class="text-slate-500">
257
- <td class="py-2">期权池 (ESOP)</td>
258
- <td class="text-right font-mono">${ formatNumber(project.optionPool, 1) }%</td>
259
- <td class="text-right font-mono">$${ formatNumber(optionPoolValue) }</td>
260
- </tr>
261
- </tbody>
262
- </table>
263
- </div>
264
-
265
- <div class="flex items-center justify-center h-64">
266
- <canvas id="dilutionChart"></canvas>
267
- </div>
268
- </div>
269
- </div>
270
- </div>
271
- </div>
272
-
273
- <!-- Tab 4: Term Sheet Generator -->
274
- <div v-show="currentTab === 'termsheet'" class="space-y-6">
275
- <div class="bg-white rounded-xl shadow-sm p-6 border border-slate-200">
276
- <h2 class="text-xl font-bold mb-6">投资意向书生成器 (Lite)</h2>
277
-
278
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
279
- <div>
280
- <label class="block text-sm font-medium text-slate-700 mb-1">投资方名称</label>
281
- <input type="text" v-model="project.investorName" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
282
- </div>
283
- <div>
284
- <label class="block text-sm font-medium text-slate-700 mb-1">交割日期</label>
285
- <input type="date" v-model="project.closingDate" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
286
- </div>
287
- <div>
288
- <label class="block text-sm font-medium text-slate-700 mb-1">清算优先权 (Liquidation Preference)</label>
289
- <select v-model="project.liquidationPref" class="w-full px-4 py-2 border border-slate-300 rounded-lg bg-white">
290
- <option value="1x Non-Participating">1x 不参与分配 (标准)</option>
291
- <option value="1x Participating">1x 参与分配 (对投资人有利)</option>
292
- <option value="2x Non-Participating">2x 不参与分配 (苛刻)</option>
293
- </select>
294
- </div>
295
- <div>
296
- <label class="block text-sm font-medium text-slate-700 mb-1">董事会席位 (Board Seats)</label>
297
- <div class="flex items-center space-x-2">
298
- <input type="number" v-model="project.boardSeatsFounder" placeholder="创始人" class="w-1/3 px-4 py-2 border border-slate-300 rounded-lg">
299
- <span>+</span>
300
- <input type="number" v-model="project.boardSeatsInvestor" placeholder="投资人" class="w-1/3 px-4 py-2 border border-slate-300 rounded-lg">
301
- <span>+</span>
302
- <input type="number" v-model="project.boardSeatsIndependent" placeholder="独立" class="w-1/3 px-4 py-2 border border-slate-300 rounded-lg">
303
  </div>
304
  </div>
305
  </div>
306
-
307
- <div class="bg-slate-50 p-6 rounded-lg border border-slate-200 font-mono text-sm whitespace-pre-wrap leading-relaxed">${ termSheetText }</div>
308
-
309
- <div class="mt-4 text-right">
310
- <button @click="copyTermSheet" class="text-indigo-600 hover:text-indigo-800 font-medium">
311
- <i class="fa-solid fa-copy mr-1"></i> 复制内容
312
- </button>
313
- </div>
314
- </div>
315
- </div>
316
-
317
- <!-- Tab 5: Report Preview (For Print) -->
318
- <div v-show="currentTab === 'report'" class="bg-white rounded-xl shadow-sm p-8 border border-slate-200 print-section">
319
- <div class="text-center border-b pb-6 mb-6">
320
- <h1 class="text-3xl font-bold text-slate-800">${ project.projectName }</h1>
321
- <p class="text-slate-500 mt-2">估值与融资分析报告</p>
322
- <p class="text-sm text-slate-400 mt-1">${ new Date().toLocaleDateString() }</p>
323
- </div>
324
-
325
- <div class="grid grid-cols-2 gap-8 mb-8">
326
- <div>
327
- <h3 class="font-bold text-slate-700 border-b mb-2">项目概况</h3>
328
- <p><span class="text-slate-500 w-24 inline-block">行业:</span> ${ project.industry }</p>
329
- <p><span class="text-slate-500 w-24 inline-block">阶段:</span> ${ project.stage }</p>
330
- </div>
331
- <div>
332
- <h3 class="font-bold text-slate-700 border-b mb-2">估值综述</h3>
333
- <p><span class="text-slate-500 w-32 inline-block">Berkus 估值:</span> $${ formatNumber(berkusValuation) }</p>
334
- <p><span class="text-slate-500 w-32 inline-block">Scorecard 估值:</span> $${ formatNumber(scorecardValuation) }</p>
335
- </div>
336
- </div>
337
 
338
- <div class="mb-8">
339
- <h3 class="font-bold text-slate-700 border-b mb-4">拟议融资结构</h3>
340
- <div class="grid grid-cols-3 gap-4 text-center">
341
- <div class="bg-slate-50 p-4 rounded">
342
- <div class="text-xs text-slate-500">Pre-money</div>
343
- <div class="font-bold text-lg">$${ formatNumber(project.selectedPreMoney) }</div>
344
- </div>
345
- <div class="bg-slate-50 p-4 rounded">
346
- <div class="text-xs text-slate-500">融资金额</div>
347
- <div class="font-bold text-lg text-indigo-600">$${ formatNumber(project.investmentAmount) }</div>
348
- </div>
349
- <div class="bg-slate-50 p-4 rounded">
350
- <div class="text-xs text-slate-500">Post-money</div>
351
- <div class="font-bold text-lg text-green-600">$${ formatNumber(postMoneyValuation) }</div>
352
  </div>
353
  </div>
354
- </div>
355
-
356
- <div class="text-center text-xs text-slate-400 mt-12">
357
- Generated by Startup Valuation Engine. This document is for simulation purposes only.
358
- </div>
359
- </div>
360
-
361
- </main>
362
 
363
- <!-- Load Project Modal -->
364
- <div v-if="showLoadModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 no-print">
365
- <div class="bg-white rounded-lg p-6 w-full max-w-md">
366
- <h3 class="text-lg font-bold mb-4">加载项目</h3>
367
- <div v-if="savedProjects.length === 0" class="text-center text-slate-500 py-4">
368
- 暂无已保存的项目
369
- </div>
370
- <ul class="space-y-2 max-h-60 overflow-y-auto">
371
- <li v-for="p in savedProjects" :key="p.filename"
372
- class="flex justify-between items-center p-3 hover:bg-slate-50 rounded cursor-pointer border border-transparent hover:border-slate-200"
373
- @click="loadProject(p.filename)">
374
- <div>
375
- <div class="font-medium">${ p.projectName }</div>
376
- <div class="text-xs text-slate-400">${ p.updatedAt }</div>
377
- </div>
378
- <i class="fa-solid fa-chevron-right text-slate-300"></i>
379
- </li>
380
- </ul>
381
- <div class="mt-4 text-right">
382
- <button @click="showLoadModal = false" class="text-slate-500 hover:text-slate-700">关闭</button>
383
  </div>
384
- </div>
385
- </div>
386
-
387
- <!-- Toast Notification -->
388
- <div v-if="toast.show" class="fixed bottom-4 right-4 bg-slate-800 text-white px-4 py-2 rounded shadow-lg transition-opacity duration-300 z-50">
389
- ${ toast.message }
390
  </div>
391
  </div>
392
 
@@ -397,354 +252,271 @@
397
  delimiters: ['${', '}'],
398
  setup() {
399
  // State
 
400
  const currentTab = ref('berkus');
401
- const showLoadModal = ref(false);
402
- const savedProjects = ref([]);
403
- const toast = ref({ show: false, message: '' });
404
- let chartInstance = null;
405
-
406
  const tabs = [
407
- { id: 'berkus', name: 'Berkus 估值', icon: 'fa-solid fa-gavel' },
408
- { id: 'scorecard', name: '记分卡估值', icon: 'fa-solid fa-clipboard-check' },
409
- { id: 'dilution', name: '融资稀释', icon: 'fa-solid fa-chart-pie' },
410
- { id: 'termsheet', name: '投资意向书', icon: 'fa-solid fa-file-contract' },
411
- { id: 'report', name: '报告预览', icon: 'fa-solid fa-file-pdf' }
412
  ];
413
 
414
- const project = ref({
415
- projectName: '',
416
- industry: 'SaaS',
417
- stage: 'MVP',
418
- // Berkus Data
419
- berkusFactors: [
420
- { name: '创意价值 (Sound Idea)', value: 0, desc: '产品创意是否存在基本价值和风险规避?(最高 $500k)' },
421
- { name: '原型验证 (Prototype)', value: 0, desc: '是否有可工作的原型来降低技术风险?(最高 $500k)' },
422
- { name: '管理团队 (Quality Team)', value: 0, desc: '是否有高质量的管理团队来降低执行风险?(最高 $500k)' },
423
- { name: '战略关系 (Relationships)', value: 0, desc: '是否有战略合作伙伴/早期客户来降低市场风险?(最高 $500k)' },
424
- { name: '产品发布/销售 (Rollout)', value: 0, desc: '产品是否已发布或产生销售来降低生产风险?(最高 $500k)' }
425
- ],
426
- // Scorecard Data
427
- industryAverageValuation: 2000000,
428
- scorecardFactors: [
429
- { name: '团队实力', weight: 30, percentage: 100 },
430
- { name: '市场机会大小', weight: 25, percentage: 100 },
431
- { name: '产品/技术壁垒', weight: 15, percentage: 100 },
432
- { name: '竞争环境', weight: 10, percentage: 100 },
433
- { name: '市场推广/销售渠道', weight: 10, percentage: 100 },
434
- { name: '额外资金需求', weight: 5, percentage: 100 },
435
- { name: '其他因素', weight: 5, percentage: 100 }
436
- ],
437
- // Dilution Data
438
- selectedPreMoney: 0,
439
- investmentAmount: 500000,
440
- optionPool: 15, // percent
441
- totalShares: 1000000,
442
- // Term Sheet Data
443
- investorName: '',
444
- closingDate: new Date().toISOString().split('T')[0],
445
- liquidationPref: '1x Non-Participating',
446
- boardSeatsFounder: 2,
447
- boardSeatsInvestor: 1,
448
- boardSeatsIndependent: 0
 
 
 
 
 
 
449
  });
450
 
451
- // Computed: Valuations
452
- const berkusValuation = computed(() => {
453
- return project.value.berkusFactors.reduce((sum, f) => sum + f.value, 0);
454
  });
455
 
456
  const scorecardValuation = computed(() => {
457
- let multiplier = 0;
458
- project.value.scorecardFactors.forEach(f => {
459
- multiplier += (f.weight / 100) * (f.percentage / 100);
460
- });
461
- return Math.round(project.value.industryAverageValuation * multiplier);
462
  });
463
 
464
- // Auto-update selectedPreMoney if it's 0 or matches one of the calc methods
465
- watch([berkusValuation, scorecardValuation], ([newB, newS], [oldB, oldS]) => {
466
- if (project.value.selectedPreMoney === 0) {
467
- project.value.selectedPreMoney = newB > 0 ? newB : newS;
468
- }
469
  });
470
 
471
- // Computed: Dilution
472
- const postMoneyValuation = computed(() => {
473
- return project.value.selectedPreMoney + project.value.investmentAmount;
474
  });
475
 
476
- const optionPoolValue = computed(() => {
477
- // Option pool is typically calculated on post-money basis
478
- // Value = PostMoney * (OptionPool% / 100)
479
- return Math.round(postMoneyValuation.value * (project.value.optionPool / 100));
480
- });
481
 
482
- // Effective Pre-Money (Pre-Money - Option Pool increase usually comes from pre-money side effectively)
483
- // But for simplicity here:
484
- // Founders own: (PreMoney / PostMoney) - OptionPool%?
485
- // Let's use standard VC math:
486
- // PostMoney = PreMoney + Investment
487
- // Investor% = Investment / PostMoney
488
- // OptionPool% is fixed at PostMoney
489
- // Founders% = 100% - Investor% - OptionPool%
490
-
491
- const investorsPercent = computed(() => {
492
- if (postMoneyValuation.value === 0) return 0;
493
- return (project.value.investmentAmount / postMoneyValuation.value) * 100;
494
- });
495
 
496
- const foundersPercent = computed(() => {
497
- return 100 - investorsPercent.value - project.value.optionPool;
498
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- const foundersValue = computed(() => {
501
- return Math.round(postMoneyValuation.value * (foundersPercent.value / 100));
 
502
  });
503
-
504
- const pricePerShare = computed(() => {
505
- // If we assume totalShares (founders) represents the foundersPercent
506
- // FoundersShares / Founders% = TotalPostShares
507
- // Price = PostMoney / TotalPostShares
508
- if (foundersPercent.value <= 0) return 0;
509
- const totalPostShares = project.value.totalShares / (foundersPercent.value / 100);
510
- return postMoneyValuation.value / totalPostShares;
511
- });
512
-
513
- // Computed: Term Sheet Text
514
- const termSheetText = computed(() => {
515
- return `INVESTMENT TERM SHEET (SUMMARY)
516
-
517
- Company: ${project.value.projectName || '[Company Name]'}
518
- Investor: ${project.value.investorName || '[Investor Name]'}
519
- Date: ${project.value.closingDate}
520
-
521
- 1. Valuation & Investment
522
- - Pre-money Valuation: $${formatNumber(project.value.selectedPreMoney)}
523
- - Investment Amount: $${formatNumber(project.value.investmentAmount)}
524
- - Post-money Valuation: $${formatNumber(postMoneyValuation.value)}
525
-
526
- 2. Capitalization
527
- - Employee Option Pool: ${project.value.optionPool}% (Post-money basis)
528
- - Investor Ownership: ${formatNumber(investorsPercent.value, 1)}%
529
 
530
- 3. Liquidation Preference
531
- - ${project.value.liquidationPref}
532
- - In the event of a sale or liquidation, investors receive their preference amount before common shareholders.
533
-
534
- 4. Board of Directors
535
- - The Board shall consist of ${project.value.boardSeatsFounder + project.value.boardSeatsInvestor + project.value.boardSeatsIndependent} members:
536
- - ${project.value.boardSeatsFounder} Founder representative(s)
537
- - ${project.value.boardSeatsInvestor} Investor representative(s)
538
- - ${project.value.boardSeatsIndependent} Independent member(s)
539
-
540
- 5. Dividends
541
- - Non-cumulative dividends will be paid on the Preferred Stock when and if declared by the Board.
542
 
543
- [This is a non-binding summary for simulation purposes only.]`;
544
- });
 
 
 
 
 
 
545
 
546
- // Methods
547
- const formatNumber = (num, decimals = 0) => {
548
- if (!num && num !== 0) return '0';
549
- return num.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
550
  };
551
 
552
- const showToast = (msg) => {
553
- toast.value.message = msg;
554
- toast.value.show = true;
555
- setTimeout(() => toast.value.show = false, 3000);
 
 
 
 
 
556
  };
557
 
558
- const saveProject = async () => {
559
- try {
560
- const res = await fetch('/api/save', {
561
- method: 'POST',
562
- headers: { 'Content-Type': 'application/json' },
563
- body: JSON.stringify(project.value)
564
- });
565
- const data = await res.json();
566
- if (data.success) {
567
- showToast('保存成功');
568
- loadProjectList();
569
- } else {
570
- showToast('保存失败: ' + data.message);
571
- }
572
- } catch (e) {
573
- showToast('保存错误');
574
- }
575
  };
576
 
577
- const loadProjectList = async () => {
 
578
  try {
579
- const res = await fetch('/api/load');
580
  const data = await res.json();
581
- if (data.success) {
582
- savedProjects.value = data.projects;
583
- }
584
  } catch (e) {
585
- console.error(e);
586
  }
587
  };
588
 
589
- const loadProject = async (filename) => {
 
 
 
 
 
 
 
 
 
 
 
 
590
  try {
591
- const res = await fetch('/api/load/' + filename);
592
- const data = await res.json();
593
- if (data.success) {
594
- project.value = data.data;
595
- showLoadModal.value = false;
596
- showToast('加载成功');
 
 
 
597
  }
598
  } catch (e) {
599
- showToast('加载失败');
600
  }
601
  };
602
 
603
- const copyTermSheet = () => {
604
- navigator.clipboard.writeText(termSheetText.value);
605
- showToast('已复制到剪贴板');
606
- };
607
-
608
- const printReport = () => {
609
- currentTab.value = 'report';
610
- setTimeout(() => {
611
- window.print();
612
- }, 500);
 
 
613
  };
614
 
615
- const exportImage = async () => {
616
- currentTab.value = 'report';
617
- // Wait for render
618
- await new Promise(r => setTimeout(r, 800));
619
-
620
- const element = document.querySelector('.print-section');
621
- if (!element) return;
622
-
623
  try {
624
- showToast('正在生成图片...', 5000);
625
- const canvas = await html2canvas(element, {
626
- scale: 2,
627
- backgroundColor: '#ffffff',
628
- useCORS: true
629
- });
630
-
631
- const link = document.createElement('a');
632
- link.download = `${project.value.projectName || 'project'}-valuation-report.png`;
633
- link.href = canvas.toDataURL('image/png');
634
- link.click();
635
- showToast('图片导出成功');
636
  } catch (e) {
637
  console.error(e);
638
- showToast('导出失败,请重试');
639
  }
640
  };
641
-
642
- const loadDemoData = () => {
643
- project.value = {
644
- projectName: '演示科技 (DemoTech)',
645
- industry: 'AI',
646
- stage: 'MVP',
647
- berkusFactors: [
648
- { name: '创意价值 (Sound Idea)', value: 350000, desc: '产品创意是否存在基本价值和风险规避?(最高 $500k)' },
649
- { name: '原型验证 (Prototype)', value: 400000, desc: '是否有可工作的原型来降低技术风险?(最高 $500k)' },
650
- { name: '管理团队 (Quality Team)', value: 250000, desc: '是否有高质量的管理团队来降低执行风险?(最高 $500k)' },
651
- { name: '战略关系 (Relationships)', value: 100000, desc: '是否有战略合作伙伴/早期客户来降低市场风险?(最高 $500k)' },
652
- { name: '产品发布/销售 (Rollout)', value: 0, desc: '产品是否已发布或产生销售来降低生产风险?(最高 $500k)' }
653
- ],
654
- industryAverageValuation: 3000000,
655
- scorecardFactors: [
656
- { name: '团队实力', weight: 30, percentage: 120 },
657
- { name: '市场机会大小', weight: 25, percentage: 110 },
658
- { name: '产品/技术壁垒', weight: 15, percentage: 115 },
659
- { name: '竞争环境', weight: 10, percentage: 90 },
660
- { name: '市场推广/销售渠道', weight: 10, percentage: 80 },
661
- { name: '额外资金需求', weight: 5, percentage: 100 },
662
- { name: '其他因素', weight: 5, percentage: 100 }
663
- ],
664
- selectedPreMoney: 0,
665
- investmentAmount: 800000,
666
- optionPool: 15,
667
- totalShares: 1000000,
668
- investorName: '红杉资本 (Sequoia)',
669
- closingDate: new Date().toISOString().split('T')[0],
670
- liquidationPref: '1x Non-Participating',
671
- boardSeatsFounder: 2,
672
- boardSeatsInvestor: 1,
673
- boardSeatsIndependent: 0
674
- };
675
- showToast('演示数据已加载');
676
- };
677
- const updateChart = () => {
678
- if (!document.getElementById('dilutionChart')) return;
679
-
680
- const ctx = document.getElementById('dilutionChart').getContext('2d');
681
-
682
- if (chartInstance) {
683
- chartInstance.destroy();
684
- }
685
-
686
- chartInstance = new Chart(ctx, {
687
- type: 'doughnut',
688
- data: {
689
- labels: ['创始人团队', '新投资人', '期权池'],
690
- datasets: [{
691
- data: [foundersPercent.value, investorsPercent.value, project.value.optionPool],
692
- backgroundColor: ['#94a3b8', '#4f46e5', '#cbd5e1'],
693
- borderWidth: 0
694
- }]
695
- },
696
- options: {
697
- responsive: true,
698
- maintainAspectRatio: false,
699
- plugins: {
700
- legend: { position: 'bottom' }
701
- }
702
- }
703
  });
704
  };
705
 
706
- // Watch for chart updates
707
- watch([foundersPercent, investorsPercent, () => project.value.optionPool, currentTab], () => {
708
- if (currentTab.value === 'dilution') {
709
- // Small delay to ensure DOM is ready
710
- setTimeout(updateChart, 100);
711
- }
712
- });
713
-
714
  onMounted(() => {
715
- loadProjectList();
716
- // Set default Berkus values to non-zero for demo
717
- project.value.berkusFactors[0].value = 150000;
718
- project.value.berkusFactors[1].value = 100000;
719
-
720
- if (currentTab.value === 'dilution') {
721
- setTimeout(updateChart, 100);
722
- }
723
  });
724
 
725
  return {
726
- currentTab,
727
- tabs,
728
- project,
729
- berkusValuation,
730
- scorecardValuation,
731
- postMoneyValuation,
732
- investorsPercent,
733
- foundersPercent,
734
- foundersValue,
735
- optionPoolValue,
736
- pricePerShare,
737
- termSheetText,
738
- formatNumber,
739
- saveProject,
740
- showLoadModal,
741
- savedProjects,
742
- loadProject,
743
- copyTermSheet,
744
- printReport,
745
- toast,
746
- loadDemoData,
747
- exportImage
748
  };
749
  }
750
  }).mount('#app');
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>创业估值引擎 (Startup Valuation Engine)</title>
7
  <script src="/static/js/tailwindcss.js"></script>
8
  <script src="/static/js/vue.global.js"></script>
9
  <script src="/static/js/chart.js"></script>
10
  <script src="/static/js/html2canvas.min.js"></script>
 
11
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
12
  <style>
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
14
+ body { font-family: 'Inter', sans-serif; }
 
 
 
 
15
  .slider-thumb::-webkit-slider-thumb {
16
  -webkit-appearance: none;
17
  appearance: none;
 
21
  cursor: pointer;
22
  border-radius: 50%;
23
  }
24
+ .gradient-bg {
25
+ background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%);
26
+ }
 
 
 
 
27
  </style>
28
  </head>
29
+ <body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden">
30
+ <div id="app" class="flex flex-col h-full">
31
  <!-- Header -->
32
+ <header class="gradient-bg text-white p-4 shadow-lg flex justify-between items-center z-10">
33
+ <div class="flex items-center gap-3">
34
+ <i class="fas fa-chart-line text-2xl"></i>
35
+ <div>
36
+ <h1 class="text-xl font-bold tracking-tight">创业估值引擎</h1>
37
+ <p class="text-xs text-indigo-100 opacity-80">Startup Valuation Engine - 多模型早期项目估值工具</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
  </div>
40
+ <div class="flex gap-3">
41
+ <button @click="saveValuation" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg text-sm transition flex items-center gap-2">
42
+ <i class="fas fa-save"></i> 保存记录
43
+ </button>
44
+ <button @click="exportReport" class="bg-white text-indigo-600 hover:bg-indigo-50 px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2 shadow-sm">
45
+ <i class="fas fa-download"></i> 导出报告
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </button>
47
  </div>
48
+ </header>
49
 
50
+ <div class="flex flex-1 overflow-hidden">
51
+ <!-- Sidebar (History) -->
52
+ <aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex z-0">
53
+ <div class="p-4 border-b border-gray-100">
54
+ <h2 class="font-semibold text-gray-700 flex items-center gap-2">
55
+ <i class="fas fa-history text-indigo-500"></i> 历史记录
56
+ </h2>
57
+ </div>
58
+ <div class="flex-1 overflow-y-auto p-2 space-y-2">
59
+ <div v-if="history.length === 0" class="text-center text-gray-400 text-sm py-8">
60
+ 暂无记录
61
+ </div>
62
+ <div v-for="item in history" :key="item.id"
63
+ @click="loadValuation(item)"
64
+ class="p-3 rounded-lg border border-gray-100 hover:border-indigo-200 hover:bg-indigo-50 cursor-pointer transition group relative">
65
+ <div class="font-medium text-gray-800 truncate text-sm">${ item.projectName || '未命名项目' }</div>
66
+ <div class="text-xs text-gray-500 mt-1">${ item.created_at }</div>
67
+ <div class="text-xs font-semibold text-indigo-600 mt-1">
68
+ 平均估值: $${ formatCurrency(item.averageValuation) }
69
  </div>
70
+ <button @click.stop="deleteValuation(item.id)" class="absolute top-2 right-2 text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition">
71
+ <i class="fas fa-trash-alt"></i>
72
+ </button>
73
  </div>
74
+ </div>
75
+ </aside>
76
 
77
+ <!-- Main Content -->
78
+ <main class="flex-1 overflow-y-auto p-6 bg-gray-50 relative" id="report-content">
79
+ <div class="max-w-6xl mx-auto space-y-6">
80
+
81
+ <!-- Project Info -->
82
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
83
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
84
+ <div>
85
+ <label class="block text-sm font-medium text-gray-700 mb-1">项目名称</label>
86
+ <input v-model="project.name" type="text" class="w-full rounded-lg border-gray-300 border px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="例如: NextGen AI Platform">
87
+ </div>
88
+ <div>
89
+ <label class="block text-sm font-medium text-gray-700 mb-1">所在行业 / 阶段</label>
90
+ <input v-model="project.industry" type="text" class="w-full rounded-lg border-gray-300 border px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="例如: SaaS / Seed Stage">
91
  </div>
 
 
92
  </div>
93
  </div>
 
 
94
 
95
+ <!-- Valuation Summary Cards -->
96
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
97
+ <div class="bg-white p-5 rounded-xl shadow-sm border-l-4 border-blue-500">
98
+ <div class="text-gray-500 text-xs font-medium uppercase tracking-wider">Berkus 方法</div>
99
+ <div class="text-2xl font-bold text-gray-800 mt-1">$${ formatCurrency(berkusValuation) }</div>
 
 
100
  </div>
101
+ <div class="bg-white p-5 rounded-xl shadow-sm border-l-4 border-green-500">
102
+ <div class="text-gray-500 text-xs font-medium uppercase tracking-wider">Scorecard 方法</div>
103
+ <div class="text-2xl font-bold text-gray-800 mt-1">$${ formatCurrency(scorecardValuation) }</div>
104
  </div>
105
+ <div class="bg-white p-5 rounded-xl shadow-sm border-l-4 border-purple-500">
106
+ <div class="text-gray-500 text-xs font-medium uppercase tracking-wider">风险因子求和法</div>
107
+ <div class="text-2xl font-bold text-gray-800 mt-1">$${ formatCurrency(riskFactorValuation) }</div>
108
+ </div>
109
+ <div class="bg-gradient-to-br from-indigo-600 to-purple-700 p-5 rounded-xl shadow-md text-white">
110
+ <div class="text-indigo-100 text-xs font-medium uppercase tracking-wider">平均估值 (Average)</div>
111
+ <div class="text-3xl font-bold mt-1">$${ formatCurrency(averageValuation) }</div>
112
  </div>
113
  </div>
114
 
115
+ <!-- Main Logic Tabs -->
116
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
117
+ <div class="flex border-b border-gray-100">
118
+ <button v-for="tab in tabs" :key="tab.id"
119
+ @click="currentTab = tab.id"
120
+ :class="['px-6 py-4 text-sm font-medium transition-colors flex items-center gap-2',
121
+ currentTab === tab.id ? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50/50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50']">
122
+ <i :class="tab.icon"></i> ${ tab.name }
123
+ </button>
 
 
 
 
 
 
124
  </div>
 
 
 
125
 
126
+ <div class="p-6">
127
+ <!-- Berkus Method -->
128
+ <div v-if="currentTab === 'berkus'" class="animate-fade-in">
129
+ <div class="mb-6 bg-blue-50 text-blue-800 p-4 rounded-lg text-sm border border-blue-100">
130
+ <i class="fas fa-info-circle mr-2"></i>
131
+ <strong>Berkus 方法:</strong> 适用于早期初创公司。为5个关键成功驱动因素分配价值(每个最高 $500k)。
 
 
 
 
 
 
 
132
  </div>
133
+ <div class="space-y-6">
134
+ <div v-for="(driver, index) in berkusDrivers" :key="index" class="flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-100">
135
+ <div class="flex-1">
136
+ <h4 class="font-medium text-gray-800">${ driver.name }</h4>
137
+ <p class="text-xs text-gray-500 mt-1">${ driver.desc }</p>
138
+ </div>
139
+ <div class="w-1/3 px-4">
140
+ <input type="range" v-model.number="driver.value" min="0" max="500000" step="50000" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
141
+ </div>
142
+ <div class="w-24 text-right font-mono font-medium text-indigo-600">
143
+ $${ formatCurrency(driver.value) }
144
+ </div>
145
+ </div>
146
  </div>
147
  </div>
148
 
149
+ <!-- Scorecard Method -->
150
+ <div v-if="currentTab === 'scorecard'" class="animate-fade-in">
151
+ <div class="mb-6 bg-green-50 text-green-800 p-4 rounded-lg text-sm border border-green-100 flex items-center justify-between">
152
+ <div>
153
+ <i class="fas fa-info-circle mr-2"></i>
154
+ <strong>Scorecard 方法:</strong> 基于行业平均估值,根据7个加权因素进行调整。
155
+ </div>
156
+ <div class="flex items-center gap-2">
157
+ <label class="text-xs font-semibold uppercase">行业平均估值 ($):</label>
158
+ <input type="number" v-model.number="scorecardBase" class="w-32 text-sm border-green-300 border rounded px-2 py-1 focus:ring-2 focus:ring-green-500 outline-none">
159
+ </div>
160
  </div>
161
+ <div class="overflow-x-auto">
162
+ <table class="w-full text-sm text-left text-gray-500">
163
+ <thead class="text-xs text-gray-700 uppercase bg-gray-50">
164
+ <tr>
165
+ <th class="px-6 py-3">评估因素</th>
166
+ <th class="px-6 py-3 w-24">权重 (%)</th>
167
+ <th class="px-6 py-3 w-48">项目表现 (相对于平均水平)</th>
168
+ <th class="px-6 py-3 text-right">调整系数</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody>
172
+ <tr v-for="(factor, index) in scorecardFactors" :key="index" class="bg-white border-b hover:bg-gray-50">
173
+ <td class="px-6 py-4 font-medium text-gray-900">${ factor.name }</td>
174
+ <td class="px-6 py-4">${ factor.weight * 100 }%</td>
175
+ <td class="px-6 py-4">
176
+ <div class="flex items-center gap-2">
177
+ <input type="range" v-model.number="factor.percentage" min="50" max="150" step="5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600">
178
+ <span class="text-xs w-10">${ factor.percentage }%</span>
179
+ </div>
180
+ </td>
181
+ <td class="px-6 py-4 text-right font-mono text-green-600">
182
+ ${ (factor.weight * (factor.percentage / 100)).toFixed(4) }
183
+ </td>
184
+ </tr>
185
+ </tbody>
186
+ <tfoot>
187
+ <tr class="bg-green-50 font-bold text-gray-900">
188
+ <td colspan="3" class="px-6 py-4 text-right">总调整系数:</td>
189
+ <td class="px-6 py-4 text-right text-green-700">${ scorecardMultiplier.toFixed(4) }</td>
190
+ </tr>
191
+ </tfoot>
192
+ </table>
193
  </div>
 
194
  </div>
 
 
 
 
 
 
 
195
 
196
+ <!-- Risk Factor Method -->
197
+ <div v-if="currentTab === 'risk'" class="animate-fade-in">
198
+ <div class="mb-6 bg-purple-50 text-purple-800 p-4 rounded-lg text-sm border border-purple-100 flex items-center justify-between">
199
+ <div>
200
+ <i class="fas fa-info-circle mr-2"></i>
201
+ <strong>风险因子求和法:</strong> 基础估值 + 12个风险因子的调整。
202
+ </div>
203
+ <div class="flex items-center gap-2">
204
+ <label class="text-xs font-semibold uppercase">基础估值 ($):</label>
205
+ <input type="number" v-model.number="riskBase" class="w-32 text-sm border-purple-300 border rounded px-2 py-1 focus:ring-2 focus:ring-purple-500 outline-none">
206
+ </div>
207
  </div>
208
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
209
+ <div v-for="(factor, index) in riskFactors" :key="index" class="p-4 bg-white border rounded-lg hover:shadow-md transition">
210
+ <div class="flex justify-between items-start mb-2">
211
+ <span class="font-medium text-gray-700 text-sm">${ factor.name }</span>
212
+ <span :class="['text-xs font-bold px-2 py-0.5 rounded', getRiskColor(factor.rating)]">
213
+ ${ getRiskLabel(factor.rating) }
214
+ </span>
215
+ </div>
216
+ <div class="flex items-center gap-2 mt-2">
217
+ <button @click="factor.rating = Math.max(-2, factor.rating - 1)" class="w-6 h-6 rounded bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-gray-600">-</button>
218
+ <div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
219
+ <div class="h-full transition-all duration-300"
220
+ :class="getRiskBarColor(factor.rating)"
221
+ :style="{ width: getRiskBarWidth(factor.rating), marginLeft: getRiskBarMargin(factor.rating) }">
222
+ </div>
223
+ </div>
224
+ <button @click="factor.rating = Math.min(2, factor.rating + 1)" class="w-6 h-6 rounded bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-gray-600">+</button>
225
+ </div>
226
+ <div class="text-right text-xs text-gray-400 mt-2">
227
+ 调整: $${ formatCurrency(factor.rating * 250000) }
228
+ </div>
229
+ </div>
230
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  </div>
232
  </div>
233
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ <!-- Charts Section -->
236
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-10">
237
+ <h3 class="text-lg font-bold text-gray-800 mb-4">估值可视化分析</h3>
238
+ <div class="h-64">
239
+ <canvas id="valuationChart"></canvas>
 
 
 
 
 
 
 
 
 
240
  </div>
241
  </div>
 
 
 
 
 
 
 
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  </div>
244
+ </main>
 
 
 
 
 
245
  </div>
246
  </div>
247
 
 
252
  delimiters: ['${', '}'],
253
  setup() {
254
  // State
255
+ const project = ref({ name: 'TechFlow AI (示例项目)', industry: 'SaaS / Seed Stage' });
256
  const currentTab = ref('berkus');
257
+ const history = ref([]);
 
 
 
 
258
  const tabs = [
259
+ { id: 'berkus', name: 'Berkus 方法', icon: 'fas fa-shield-alt' },
260
+ { id: 'scorecard', name: 'Scorecard 方法', icon: 'fas fa-clipboard-check' },
261
+ { id: 'risk', name: '风险因子求和', icon: 'fas fa-exclamation-triangle' }
 
 
262
  ];
263
 
264
+ // Berkus Data
265
+ const berkusDrivers = ref([
266
+ { name: '商业创意 (Sound Idea)', desc: '提供基本价值', value: 350000 },
267
+ { name: '原型/MVP (Prototype)', desc: '降低技术风险', value: 250000 },
268
+ { name: '管理团队 (Management)', desc: '降低执行风险', value: 400000 },
269
+ { name: '战略关系 (Strategic Relationships)', desc: '降低市场风险', value: 150000 },
270
+ { name: '产品推出/销售 (Product Rollout)', desc: '降低生产风险', value: 0 }
271
+ ]);
272
+
273
+ // Scorecard Data
274
+ const scorecardBase = ref(1500000); // Industry avg
275
+ const scorecardFactors = ref([
276
+ { name: '团队实力', weight: 0.30, percentage: 120 },
277
+ { name: '市场机会规模', weight: 0.25, percentage: 110 },
278
+ { name: '产品/技术', weight: 0.15, percentage: 105 },
279
+ { name: '竞争环境', weight: 0.10, percentage: 90 },
280
+ { name: '营销/销售渠道', weight: 0.10, percentage: 80 },
281
+ { name: '额外资金需求', weight: 0.05, percentage: 100 },
282
+ { name: '其他', weight: 0.05, percentage: 100 }
283
+ ]);
284
+
285
+ // Risk Factor Data
286
+ const riskBase = ref(2000000);
287
+ const riskFactors = ref([
288
+ { name: '管理风险', rating: 1 },
289
+ { name: '业务阶段', rating: 1 },
290
+ { name: '政策/立法风险', rating: 0 },
291
+ { name: '制造/供应链风险', rating: 0 },
292
+ { name: '销售/营销风险', rating: -1 },
293
+ { name: '融资风险', rating: 0 },
294
+ { name: '竞争风险', rating: 1 },
295
+ { name: '技术风险', rating: 1 },
296
+ { name: '法律诉讼风险', rating: 0 },
297
+ { name: '国际化风险', rating: 0 },
298
+ { name: '声誉风险', rating: 0 },
299
+ { name: '潜在退出回报', rating: 1 }
300
+ ]);
301
+
302
+ // Computed Valuations
303
+ const berkusValuation = computed(() => {
304
+ return berkusDrivers.value.reduce((sum, item) => sum + item.value, 0);
305
  });
306
 
307
+ const scorecardMultiplier = computed(() => {
308
+ return scorecardFactors.value.reduce((sum, item) => sum + (item.weight * (item.percentage / 100)), 0);
 
309
  });
310
 
311
  const scorecardValuation = computed(() => {
312
+ return scorecardBase.value * scorecardMultiplier.value;
 
 
 
 
313
  });
314
 
315
+ const riskFactorValuation = computed(() => {
316
+ // Each +1 rating adds $250k, -1 removes $250k
317
+ const adjustment = riskFactors.value.reduce((sum, item) => sum + (item.rating * 250000), 0);
318
+ return Math.max(0, riskBase.value + adjustment);
 
319
  });
320
 
321
+ const averageValuation = computed(() => {
322
+ return (berkusValuation.value + scorecardValuation.value + riskFactorValuation.value) / 3;
 
323
  });
324
 
325
+ // Chart Management
326
+ let chartInstance = null;
327
+ const updateChart = () => {
328
+ const ctx = document.getElementById('valuationChart');
329
+ if (!ctx) return;
330
 
331
+ if (chartInstance) chartInstance.destroy();
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ chartInstance = new Chart(ctx, {
334
+ type: 'bar',
335
+ data: {
336
+ labels: ['Berkus 方法', 'Scorecard 方法', '风险因子求和法', '平均估值'],
337
+ datasets: [{
338
+ label: '估值 ($)',
339
+ data: [
340
+ berkusValuation.value,
341
+ scorecardValuation.value,
342
+ riskFactorValuation.value,
343
+ averageValuation.value
344
+ ],
345
+ backgroundColor: [
346
+ 'rgba(59, 130, 246, 0.6)',
347
+ 'rgba(16, 185, 129, 0.6)',
348
+ 'rgba(139, 92, 246, 0.6)',
349
+ 'rgba(79, 70, 229, 0.8)'
350
+ ],
351
+ borderColor: [
352
+ 'rgb(59, 130, 246)',
353
+ 'rgb(16, 185, 129)',
354
+ 'rgb(139, 92, 246)',
355
+ 'rgb(79, 70, 229)'
356
+ ],
357
+ borderWidth: 1,
358
+ borderRadius: 6
359
+ }]
360
+ },
361
+ options: {
362
+ responsive: true,
363
+ maintainAspectRatio: false,
364
+ plugins: {
365
+ legend: { display: false },
366
+ tooltip: {
367
+ callbacks: {
368
+ label: function(context) {
369
+ return '$ ' + context.raw.toLocaleString();
370
+ }
371
+ }
372
+ }
373
+ },
374
+ scales: {
375
+ y: {
376
+ beginAtZero: true,
377
+ ticks: {
378
+ callback: function(value) {
379
+ return '$' + (value / 1000000).toFixed(1) + 'M';
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ });
386
+ };
387
 
388
+ // Watchers for Chart updates
389
+ watch([berkusValuation, scorecardValuation, riskFactorValuation], () => {
390
+ updateChart();
391
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
+ // Helper Functions
394
+ const formatCurrency = (val) => {
395
+ return val.toLocaleString('en-US', { maximumFractionDigits: 0 });
396
+ };
 
 
 
 
 
 
 
 
397
 
398
+ const getRiskLabel = (rating) => {
399
+ if (rating === 2) return '非常积极 (Very Positive)';
400
+ if (rating === 1) return '积极 (Positive)';
401
+ if (rating === 0) return '中性 (Neutral)';
402
+ if (rating === -1) return '消极 (Negative)';
403
+ if (rating === -2) return '非常消极 (Very Negative)';
404
+ return '';
405
+ };
406
 
407
+ const getRiskColor = (rating) => {
408
+ if (rating > 0) return 'bg-green-100 text-green-700';
409
+ if (rating < 0) return 'bg-red-100 text-red-700';
410
+ return 'bg-gray-100 text-gray-600';
411
  };
412
 
413
+ const getRiskBarColor = (rating) => {
414
+ if (rating > 0) return 'bg-green-500';
415
+ if (rating < 0) return 'bg-red-500';
416
+ return 'bg-gray-300';
417
+ };
418
+
419
+ const getRiskBarWidth = (rating) => {
420
+ if (rating === 0) return '0%';
421
+ return (Math.abs(rating) / 2 * 50) + '%';
422
  };
423
 
424
+ const getRiskBarMargin = (rating) => {
425
+ // Center is 50%.
426
+ // If positive, margin-left is 50%.
427
+ // If negative, margin-left is 50% - width.
428
+ if (rating >= 0) return '50%';
429
+ return (50 - (Math.abs(rating) / 2 * 50)) + '%';
 
 
 
 
 
 
 
 
 
 
 
430
  };
431
 
432
+ // API Actions
433
+ const loadHistory = async () => {
434
  try {
435
+ const res = await fetch('/api/valuations');
436
  const data = await res.json();
437
+ history.value = data;
 
 
438
  } catch (e) {
439
+ console.error('Failed to load history', e);
440
  }
441
  };
442
 
443
+ const saveValuation = async () => {
444
+ const payload = {
445
+ projectName: project.value.name,
446
+ industry: project.value.industry,
447
+ berkusData: berkusDrivers.value,
448
+ scorecardData: { base: scorecardBase.value, factors: scorecardFactors.value },
449
+ riskData: { base: riskBase.value, factors: riskFactors.value },
450
+ berkusValuation: berkusValuation.value,
451
+ scorecardValuation: scorecardValuation.value,
452
+ riskFactorValuation: riskFactorValuation.value,
453
+ averageValuation: averageValuation.value
454
+ };
455
+
456
  try {
457
+ const res = await fetch('/api/valuations', {
458
+ method: 'POST',
459
+ headers: { 'Content-Type': 'application/json' },
460
+ body: JSON.stringify(payload)
461
+ });
462
+ const result = await res.json();
463
+ if (result.success) {
464
+ alert('保存成功!');
465
+ loadHistory();
466
  }
467
  } catch (e) {
468
+ alert('保存失败: ' + e.message);
469
  }
470
  };
471
 
472
+ const loadValuation = (item) => {
473
+ project.value.name = item.projectName;
474
+ project.value.industry = item.industry;
475
+ if (item.berkusData) berkusDrivers.value = item.berkusData;
476
+ if (item.scorecardData) {
477
+ scorecardBase.value = item.scorecardData.base;
478
+ scorecardFactors.value = item.scorecardData.factors;
479
+ }
480
+ if (item.riskData) {
481
+ riskBase.value = item.riskData.base;
482
+ riskFactors.value = item.riskData.factors;
483
+ }
484
  };
485
 
486
+ const deleteValuation = async (id) => {
487
+ if (!confirm('确定要删除这条记录吗?')) return;
 
 
 
 
 
 
488
  try {
489
+ await fetch(`/api/valuations/${id}`, { method: 'DELETE' });
490
+ loadHistory();
 
 
 
 
 
 
 
 
 
 
491
  } catch (e) {
492
  console.error(e);
 
493
  }
494
  };
495
+
496
+ const exportReport = () => {
497
+ const element = document.getElementById('report-content');
498
+ html2canvas(element, { scale: 2, useCORS: true }).then(canvas => {
499
+ const link = document.createElement('a');
500
+ link.download = `valuation-report-${project.value.name || 'unnamed'}.png`;
501
+ link.href = canvas.toDataURL();
502
+ link.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  });
504
  };
505
 
506
+ // Default data init
 
 
 
 
 
 
 
507
  onMounted(() => {
508
+ loadHistory();
509
+ updateChart();
 
 
 
 
 
 
510
  });
511
 
512
  return {
513
+ project, currentTab, tabs, history,
514
+ berkusDrivers, berkusValuation,
515
+ scorecardBase, scorecardFactors, scorecardMultiplier, scorecardValuation,
516
+ riskBase, riskFactors, riskFactorValuation,
517
+ averageValuation,
518
+ getRiskLabel, getRiskColor, getRiskBarColor, getRiskBarWidth, getRiskBarMargin,
519
+ saveValuation, deleteValuation, loadValuation, exportReport, formatCurrency
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  };
521
  }
522
  }).mount('#app');