Spaces:
Sleeping
Sleeping
Commit ·
1d666c5
1
Parent(s): cd70bf3
升级优化
Browse files- README.md +19 -1
- app.py +22 -2
- static/.gitkeep +0 -0
- static/js/chart.js +0 -0
- static/js/html2canvas.min.js +0 -0
- static/js/tailwindcss.js +0 -0
- static/js/vue.global.js +0 -0
- templates/index.html +55 -53
README.md
CHANGED
|
@@ -30,6 +30,16 @@ short_description: 智能决策矩阵
|
|
| 30 |
|
| 31 |
## 🚀 快速开始
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
### Docker 部署
|
| 34 |
|
| 35 |
```bash
|
|
@@ -40,13 +50,21 @@ docker run -p 7860:7860 smart-decision-matrix
|
|
| 40 |
### 本地运行
|
| 41 |
|
| 42 |
```bash
|
|
|
|
| 43 |
pip install -r requirements.txt
|
|
|
|
|
|
|
| 44 |
python app.py
|
| 45 |
```
|
| 46 |
|
| 47 |
打开浏览器访问 `http://localhost:7860` 即可。
|
| 48 |
|
| 49 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
* **产品规划**:在多个功能需求中决定优先级。
|
| 52 |
* **技术选型**:在 React、Vue、Angular 中选择最适合团队的框架。
|
|
|
|
| 30 |
|
| 31 |
## 🚀 快速开始
|
| 32 |
|
| 33 |
+
### Hugging Face Spaces 部署
|
| 34 |
+
|
| 35 |
+
1. 在 Hugging Face 创建一个新的 Space (SDK 选择 **Docker**)。
|
| 36 |
+
2. 将本项目代码上传/推送到 Space 的仓库。
|
| 37 |
+
```bash
|
| 38 |
+
git remote add space https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 39 |
+
git push space main
|
| 40 |
+
```
|
| 41 |
+
3. 等待构建完成即可访问。
|
| 42 |
+
|
| 43 |
### Docker 部署
|
| 44 |
|
| 45 |
```bash
|
|
|
|
| 50 |
### 本地运行
|
| 51 |
|
| 52 |
```bash
|
| 53 |
+
# 1. 安装依赖
|
| 54 |
pip install -r requirements.txt
|
| 55 |
+
|
| 56 |
+
# 2. 运行应用
|
| 57 |
python app.py
|
| 58 |
```
|
| 59 |
|
| 60 |
打开浏览器访问 `http://localhost:7860` 即可。
|
| 61 |
|
| 62 |
+
## �️ 技术栈优化
|
| 63 |
+
|
| 64 |
+
* **Local-First**: 核心依赖 (Vue, Tailwind, Chart.js, html2canvas) 已本地化,无需 CDN,支持离线运行。
|
| 65 |
+
* **Zero External CSS**: 移除 FontAwesome,改用内联 SVG 图标,提升加载速度。
|
| 66 |
+
* **Backend**: Flask (Python) with Gunicorn ready.
|
| 67 |
+
* **Deployment**: Docker optimized for Hugging Face Spaces.
|
| 68 |
|
| 69 |
* **产品规划**:在多个功能需求中决定优先级。
|
| 70 |
* **技术选型**:在 React、Vue、Angular 中选择最适合团队的框架。
|
app.py
CHANGED
|
@@ -1,15 +1,35 @@
|
|
| 1 |
-
from flask import Flask, render_template, send_from_directory
|
| 2 |
import os
|
| 3 |
|
| 4 |
app = Flask(__name__)
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
@app.route('/')
|
| 7 |
def index():
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
@app.route('/static/<path:path>')
|
| 11 |
def send_static(path):
|
| 12 |
return send_from_directory('static', path)
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
if __name__ == '__main__':
|
| 15 |
app.run(host='0.0.0.0', port=7860)
|
|
|
|
| 1 |
+
from flask import Flask, render_template, send_from_directory, jsonify
|
| 2 |
import os
|
| 3 |
|
| 4 |
app = Flask(__name__)
|
| 5 |
|
| 6 |
+
# Ensure static folder exists to prevent errors
|
| 7 |
+
if not os.path.exists('static'):
|
| 8 |
+
os.makedirs('static')
|
| 9 |
+
|
| 10 |
@app.route('/')
|
| 11 |
def index():
|
| 12 |
+
try:
|
| 13 |
+
return render_template('index.html')
|
| 14 |
+
except Exception as e:
|
| 15 |
+
return f"Error rendering template: {str(e)}", 500
|
| 16 |
+
|
| 17 |
+
@app.route('/health')
|
| 18 |
+
def health():
|
| 19 |
+
return jsonify({"status": "healthy"}), 200
|
| 20 |
|
| 21 |
@app.route('/static/<path:path>')
|
| 22 |
def send_static(path):
|
| 23 |
return send_from_directory('static', path)
|
| 24 |
|
| 25 |
+
# Error handlers
|
| 26 |
+
@app.errorhandler(404)
|
| 27 |
+
def page_not_found(e):
|
| 28 |
+
return render_template('index.html'), 404
|
| 29 |
+
|
| 30 |
+
@app.errorhandler(500)
|
| 31 |
+
def internal_server_error(e):
|
| 32 |
+
return jsonify({"error": "Internal Server Error", "details": str(e)}), 500
|
| 33 |
+
|
| 34 |
if __name__ == '__main__':
|
| 35 |
app.run(host='0.0.0.0', port=7860)
|
static/.gitkeep
ADDED
|
File without changes
|
static/js/chart.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/html2canvas.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/tailwindcss.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/vue.global.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
templates/index.html
CHANGED
|
@@ -4,11 +4,11 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>智能决策矩阵 - Smart Decision Matrix</title>
|
| 7 |
-
<
|
| 8 |
-
<script src="
|
| 9 |
-
<script src="
|
| 10 |
-
<script src="
|
| 11 |
-
<
|
| 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; }
|
|
@@ -25,21 +25,22 @@
|
|
| 25 |
</style>
|
| 26 |
</head>
|
| 27 |
<body class="bg-gray-50 text-gray-800 min-h-screen">
|
|
|
|
| 28 |
<div id="app" v-cloak class="min-h-screen flex flex-col">
|
| 29 |
<!-- Navbar -->
|
| 30 |
<nav class="bg-white shadow-sm border-b border-gray-200">
|
| 31 |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 32 |
<div class="flex justify-between h-16">
|
| 33 |
<div class="flex items-center">
|
| 34 |
-
<
|
| 35 |
<h1 class="text-xl font-bold text-gray-900">智能决策矩阵</h1>
|
| 36 |
</div>
|
| 37 |
<div class="flex items-center space-x-4">
|
| 38 |
-
<button @click="resetData" class="text-gray-500 hover:text-red-600 transition-colors text-sm">
|
| 39 |
-
<
|
| 40 |
</button>
|
| 41 |
-
<button @click="exportImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors shadow-sm">
|
| 42 |
-
<
|
| 43 |
</button>
|
| 44 |
</div>
|
| 45 |
</div>
|
|
@@ -71,8 +72,8 @@
|
|
| 71 |
<span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center text-xs mr-2">1</span>
|
| 72 |
评价标准 (Criteria)
|
| 73 |
</h2>
|
| 74 |
-
<button @click="addCriteria" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium">
|
| 75 |
-
<
|
| 76 |
</button>
|
| 77 |
</div>
|
| 78 |
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
|
@@ -85,7 +86,7 @@
|
|
| 85 |
<input type="range" v-model.number="crit.weight" min="1" max="10" class="w-24 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
|
| 86 |
</div>
|
| 87 |
<button @click="removeCriteria(index)" class="text-gray-300 hover:text-red-500 transition-colors" v-if="criteria.length > 1">
|
| 88 |
-
<
|
| 89 |
</button>
|
| 90 |
</div>
|
| 91 |
</div>
|
|
@@ -98,8 +99,8 @@
|
|
| 98 |
<span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center text-xs mr-2">2</span>
|
| 99 |
选项评分 (Options & Scoring)
|
| 100 |
</h2>
|
| 101 |
-
<button @click="addOption" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium">
|
| 102 |
-
<
|
| 103 |
</button>
|
| 104 |
</div>
|
| 105 |
|
|
@@ -133,7 +134,7 @@
|
|
| 133 |
</td>
|
| 134 |
<td class="px-2 py-4 whitespace-nowrap text-center">
|
| 135 |
<button @click="removeOption(optIndex)" class="text-gray-300 hover:text-red-500 transition-colors" v-if="options.length > 1">
|
| 136 |
-
<
|
| 137 |
</button>
|
| 138 |
</td>
|
| 139 |
</tr>
|
|
@@ -199,18 +200,19 @@
|
|
| 199 |
setup() {
|
| 200 |
// Initial Data
|
| 201 |
const defaultCriteria = [
|
| 202 |
-
{ id: 'c1', name: '
|
| 203 |
-
{ id: 'c2', name: '
|
| 204 |
-
{ id: 'c3', name: '
|
|
|
|
| 205 |
];
|
| 206 |
|
| 207 |
const defaultOptions = [
|
| 208 |
-
{ id: 'o1', name: '方案 A', scores: { 'c1':
|
| 209 |
-
{ id: 'o2', name: '方案 B', scores: { 'c1':
|
| 210 |
-
{ id: 'o3', name: '方案 C', scores: { 'c1':
|
| 211 |
];
|
| 212 |
|
| 213 |
-
const projectTitle = ref('
|
| 214 |
const criteria = ref([...defaultCriteria]);
|
| 215 |
const options = ref([...defaultOptions]);
|
| 216 |
const chartInstance = ref(null);
|
|
@@ -247,20 +249,11 @@
|
|
| 247 |
};
|
| 248 |
|
| 249 |
const resetData = () => {
|
| 250 |
-
if(confirm('确定要重置所有数据吗?')) {
|
| 251 |
-
projectTitle.value = '
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
];
|
| 256 |
-
options.value = [
|
| 257 |
-
{ id: Date.now() + 'o1', name: '选项 1', scores: {} },
|
| 258 |
-
{ id: Date.now() + 'o2', name: '选项 2', scores: {} }
|
| 259 |
-
];
|
| 260 |
-
// Initialize scores
|
| 261 |
-
options.value.forEach(o => {
|
| 262 |
-
criteria.value.forEach(c => o.scores[c.id] = 5);
|
| 263 |
-
});
|
| 264 |
}
|
| 265 |
};
|
| 266 |
|
|
@@ -319,27 +312,35 @@
|
|
| 319 |
|
| 320 |
// Chart
|
| 321 |
const renderChart = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
const ctx = document.getElementById('scoreChart');
|
| 323 |
if (!ctx) return;
|
| 324 |
|
| 325 |
-
|
| 326 |
-
chartInstance.value
|
| 327 |
-
|
|
|
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
| 340 |
}
|
| 341 |
-
}
|
| 342 |
-
})
|
|
|
|
|
|
|
| 343 |
};
|
| 344 |
|
| 345 |
const updateChart = () => {
|
|
@@ -410,5 +411,6 @@
|
|
| 410 |
}
|
| 411 |
}).mount('#app');
|
| 412 |
</script>
|
|
|
|
| 413 |
</body>
|
| 414 |
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>智能决策矩阵 - Smart Decision Matrix</title>
|
| 7 |
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚖️</text></svg>">
|
| 8 |
+
<script src="/static/js/tailwindcss.js"></script>
|
| 9 |
+
<script src="/static/js/vue.global.js"></script>
|
| 10 |
+
<script src="/static/js/chart.js"></script>
|
| 11 |
+
<script src="/static/js/html2canvas.min.js"></script>
|
| 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; }
|
|
|
|
| 25 |
</style>
|
| 26 |
</head>
|
| 27 |
<body class="bg-gray-50 text-gray-800 min-h-screen">
|
| 28 |
+
{% raw %}
|
| 29 |
<div id="app" v-cloak class="min-h-screen flex flex-col">
|
| 30 |
<!-- Navbar -->
|
| 31 |
<nav class="bg-white shadow-sm border-b border-gray-200">
|
| 32 |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 33 |
<div class="flex justify-between h-16">
|
| 34 |
<div class="flex items-center">
|
| 35 |
+
<svg class="w-8 h-8 text-indigo-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"></path></svg>
|
| 36 |
<h1 class="text-xl font-bold text-gray-900">智能决策矩阵</h1>
|
| 37 |
</div>
|
| 38 |
<div class="flex items-center space-x-4">
|
| 39 |
+
<button @click="resetData" class="text-gray-500 hover:text-red-600 transition-colors text-sm flex items-center">
|
| 40 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg> 重置
|
| 41 |
</button>
|
| 42 |
+
<button @click="exportImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors shadow-sm flex items-center">
|
| 43 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg> 导出图片
|
| 44 |
</button>
|
| 45 |
</div>
|
| 46 |
</div>
|
|
|
|
| 72 |
<span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center text-xs mr-2">1</span>
|
| 73 |
评价标准 (Criteria)
|
| 74 |
</h2>
|
| 75 |
+
<button @click="addCriteria" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium flex items-center">
|
| 76 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> 添加标准
|
| 77 |
</button>
|
| 78 |
</div>
|
| 79 |
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
|
|
|
| 86 |
<input type="range" v-model.number="crit.weight" min="1" max="10" class="w-24 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
|
| 87 |
</div>
|
| 88 |
<button @click="removeCriteria(index)" class="text-gray-300 hover:text-red-500 transition-colors" v-if="criteria.length > 1">
|
| 89 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
| 90 |
</button>
|
| 91 |
</div>
|
| 92 |
</div>
|
|
|
|
| 99 |
<span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center text-xs mr-2">2</span>
|
| 100 |
选项评分 (Options & Scoring)
|
| 101 |
</h2>
|
| 102 |
+
<button @click="addOption" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium flex items-center">
|
| 103 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> 添加选项
|
| 104 |
</button>
|
| 105 |
</div>
|
| 106 |
|
|
|
|
| 134 |
</td>
|
| 135 |
<td class="px-2 py-4 whitespace-nowrap text-center">
|
| 136 |
<button @click="removeOption(optIndex)" class="text-gray-300 hover:text-red-500 transition-colors" v-if="options.length > 1">
|
| 137 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
| 138 |
</button>
|
| 139 |
</td>
|
| 140 |
</tr>
|
|
|
|
| 200 |
setup() {
|
| 201 |
// Initial Data
|
| 202 |
const defaultCriteria = [
|
| 203 |
+
{ id: 'c1', name: '功能完备性 (Features)', weight: 5 },
|
| 204 |
+
{ id: 'c2', name: '易用性 (Usability)', weight: 4 },
|
| 205 |
+
{ id: 'c3', name: '成本 (Cost)', weight: 3 },
|
| 206 |
+
{ id: 'c4', name: '社区支持 (Community)', weight: 2 }
|
| 207 |
];
|
| 208 |
|
| 209 |
const defaultOptions = [
|
| 210 |
+
{ id: 'o1', name: '方案 A (React)', scores: { 'c1': 9, 'c2': 7, 'c3': 6, 'c4': 10 } },
|
| 211 |
+
{ id: 'o2', name: '方案 B (Vue)', scores: { 'c1': 8, 'c2': 9, 'c3': 7, 'c4': 9 } },
|
| 212 |
+
{ id: 'o3', name: '方案 C (Angular)', scores: { 'c1': 9, 'c2': 6, 'c3': 5, 'c4': 8 } }
|
| 213 |
];
|
| 214 |
|
| 215 |
+
const projectTitle = ref('技术选型决策矩阵 (示例)');
|
| 216 |
const criteria = ref([...defaultCriteria]);
|
| 217 |
const options = ref([...defaultOptions]);
|
| 218 |
const chartInstance = ref(null);
|
|
|
|
| 249 |
};
|
| 250 |
|
| 251 |
const resetData = () => {
|
| 252 |
+
if(confirm('确定要重置所有数据吗?这将恢复到默认示例。')) {
|
| 253 |
+
projectTitle.value = '技术选型决策矩阵 (示例)';
|
| 254 |
+
// Clone defaults to avoid reference issues
|
| 255 |
+
criteria.value = JSON.parse(JSON.stringify(defaultCriteria));
|
| 256 |
+
options.value = JSON.parse(JSON.stringify(defaultOptions));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
};
|
| 259 |
|
|
|
|
| 312 |
|
| 313 |
// Chart
|
| 314 |
const renderChart = () => {
|
| 315 |
+
if (typeof Chart === 'undefined') {
|
| 316 |
+
console.error('Chart.js not loaded');
|
| 317 |
+
return;
|
| 318 |
+
}
|
| 319 |
const ctx = document.getElementById('scoreChart');
|
| 320 |
if (!ctx) return;
|
| 321 |
|
| 322 |
+
try {
|
| 323 |
+
if (chartInstance.value) {
|
| 324 |
+
chartInstance.value.destroy();
|
| 325 |
+
}
|
| 326 |
|
| 327 |
+
chartInstance.value = new Chart(ctx, {
|
| 328 |
+
type: 'bar',
|
| 329 |
+
data: getChartData(),
|
| 330 |
+
options: {
|
| 331 |
+
responsive: true,
|
| 332 |
+
maintainAspectRatio: false,
|
| 333 |
+
plugins: {
|
| 334 |
+
legend: { display: false }
|
| 335 |
+
},
|
| 336 |
+
scales: {
|
| 337 |
+
y: { beginAtZero: true }
|
| 338 |
+
}
|
| 339 |
}
|
| 340 |
+
});
|
| 341 |
+
} catch (e) {
|
| 342 |
+
console.error('Error rendering chart:', e);
|
| 343 |
+
}
|
| 344 |
};
|
| 345 |
|
| 346 |
const updateChart = () => {
|
|
|
|
| 411 |
}
|
| 412 |
}).mount('#app');
|
| 413 |
</script>
|
| 414 |
+
{% endraw %}
|
| 415 |
</body>
|
| 416 |
</html>
|