PharC commited on
Commit
e73bd40
·
verified ·
1 Parent(s): ec011db

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +26 -0
  2. README.md +108 -12
  3. app.js +347 -0
  4. app.py +286 -0
  5. index.html +536 -0
  6. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方轻量级 Python 镜像
2
+ FROM python:3.12-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装必要的系统库(针对 primer3 和 biopython 可能需要的依赖)
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # 先复制 requirements.txt 以利用 Docker 缓存机制
13
+ COPY requirements.txt .
14
+
15
+ # 安装 Python 依赖
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # 复制项目所有文件到容器中
19
+ COPY . .
20
+
21
+ # 暴露 Flask 端口(需与 app.py 中的 port 一致)
22
+ EXPOSE 5000
23
+
24
+ # 启动程序
25
+ # 注意:host 必须设为 0.0.0.0 才能让外部访问
26
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,12 +1,108 @@
1
- ---
2
- title: FasterPrimer
3
- emoji: 📈
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: Fast & Hassle-Free RT-PCR Primer Design Website for Sybr Gre
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Primer Design Tool
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 5000
8
+ pinned: false
9
+ license: apache-2.0
10
+ ---
11
+ # qPCR引物设计工具
12
+
13
+ 一个基于Web的qPCR引物设计工具,可以根据基因名称和物种自动设计跨越外显子交界点的引物序列。
14
+
15
+ ## 功能特点
16
+
17
+ - 🧬 支持多种模式生物(人类、小鼠、大鼠、果蝇、线虫、斑马鱼)
18
+ - 🎯 自动设计跨越外显子交界点的引物,避免基因组DNA扩增
19
+ - 📊 提供详细的引物信息(Tm值、产物长度、序列等)
20
+ - 💻 现代化的Web界面,支持移动端
21
+ - ⚡ 基于NCBI数据库的实时查询
22
+ - 📋 **批量处理功能** - 支持一次性处理多个基因
23
+ - 📥 **多格式导出** - 支持Excel、CSV、JSON格式导出
24
+
25
+ ## 新增功能
26
+
27
+ ### 批量处理
28
+ - 支持多种输入格式:每行一个基因、逗号分隔、空格分隔
29
+ - 实时进度显示
30
+ - 批量结果统计和汇总
31
+ - 失败基因的详细错误信息
32
+
33
+ ### 导出功能
34
+ - **Excel格式**: 完整的表格数据,包含所有引物信息
35
+ - **CSV格式**: 兼容各种数据分析软件
36
+ - **JSON格式**: 程序化处理的结构化数据
37
+ - 自动生成带时间戳的文件名
38
+
39
+ ## 安装和运行
40
+
41
+ ### 1. 安装依赖
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ ### 2. 运行应用
48
+
49
+ ```bash
50
+ python app.py
51
+ ```
52
+
53
+ ### 3. 访问网页
54
+
55
+ 打开浏览器访问:http://localhost:5000
56
+
57
+ ## 使用方法
58
+
59
+ ### 单个基因设计
60
+ 1. 选择"单个基因"标签
61
+ 2. 输入基因名称(如:GAPDH, Gpr34)
62
+ 3. 选择目标物种
63
+ 4. 点击"设计引物"按钮
64
+ 5. 查看结果并可选择导出
65
+
66
+ ### 批量基因设计
67
+ 1. 选择"批量处理"标签
68
+ 2. 输入基因列表,支持多种格式:
69
+ - 每行一个:`GAPDH\nACTB\nTUBB3`
70
+ - 逗号分隔:`GAPDH, ACTB, TUBB3`
71
+ - 空格分隔:`GAPDH ACTB TUBB3`
72
+ 3. 选择目标物种
73
+ 4. 点击"批量设计引物"按钮
74
+ 5. 查看批量结果统计和详细信息
75
+ 6. 导出完整的批量结果
76
+
77
+ ## 导出格式说明
78
+
79
+ ### Excel格式 (.xlsx)
80
+ 包含完整的引物信息表格,字段包括:
81
+ - 基因名称、RefSeq ID
82
+ - 引物对编号、正反向引物序列
83
+ - Tm值、产物长度
84
+ - 外显子交界点信息、设计时间
85
+
86
+ ### CSV格式 (.csv)
87
+ 与Excel相同的数据,但以逗号分隔值格式保存,便于导入其他软件。
88
+
89
+ ### JSON格式 (.json)
90
+ 结构化的数据格式,包含:
91
+ - 导出时间和统计信息
92
+ - 完整的原始结果数据
93
+ - 适合程序化处理
94
+
95
+ ## 技术栈
96
+
97
+ - **后端**: Flask + Biopython + Primer3 + Pandas
98
+ - **前端**: HTML5 + CSS3 + JavaScript
99
+ - **数据源**: NCBI Entrez数据库
100
+ - **导出**: openpyxl (Excel) + pandas (CSV/JSON)
101
+
102
+ ## 注意事项
103
+
104
+ - 需要稳定的网络连接访问NCBI数据库
105
+ - 批量处理时建议每次不超过20个基因,避免超时
106
+ - 首次查询某个基因可能需要较长时间
107
+ - 建议使用标准的基因符号进行查询
108
+ - 导出的文件会自动添加时间戳避免重名
app.js ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let currentResults = null;
2
+ let currentBatchResults = null;
3
+
4
+ // 标签页切换
5
+ function switchTab(tabName) {
6
+ // 隐藏所有标签内容
7
+ document.querySelectorAll('.tab-content').forEach(tab => {
8
+ tab.classList.remove('active');
9
+ });
10
+ document.querySelectorAll('.tab-button').forEach(btn => {
11
+ btn.classList.remove('active');
12
+ });
13
+
14
+ // 显示选中的标签
15
+ document.getElementById(tabName + '-tab').classList.add('active');
16
+ event.target.classList.add('active');
17
+
18
+ // 隐藏结果区域
19
+ document.getElementById('results').style.display = 'none';
20
+ document.getElementById('batchResults').style.display = 'none';
21
+ }
22
+
23
+ // 单个基因设计
24
+ document.getElementById('primerForm').addEventListener('submit', async function(e) {
25
+ e.preventDefault();
26
+
27
+ const geneSymbol = document.getElementById('geneSymbol').value.trim();
28
+ const species = document.getElementById('species').value;
29
+
30
+ if (!geneSymbol) {
31
+ alert('请输入基因名称');
32
+ return;
33
+ }
34
+
35
+ // 显示加载状态
36
+ document.getElementById('loading').style.display = 'block';
37
+ document.getElementById('results').style.display = 'none';
38
+ document.getElementById('batchResults').style.display = 'none';
39
+ document.getElementById('submitBtn').disabled = true;
40
+
41
+ try {
42
+ const response = await fetch('/design_primers', {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ body: JSON.stringify({
48
+ gene_symbol: geneSymbol,
49
+ species: species
50
+ })
51
+ });
52
+
53
+ const data = await response.json();
54
+
55
+ if (data.error) {
56
+ showError(data.error);
57
+ } else {
58
+ currentResults = [{ gene: geneSymbol, status: 'success', data: data }];
59
+ showResults(data);
60
+ }
61
+ } catch (error) {
62
+ showError('网络错误,请检查连接后重试');
63
+ } finally {
64
+ document.getElementById('loading').style.display = 'none';
65
+ document.getElementById('submitBtn').disabled = false;
66
+ }
67
+ });
68
+
69
+ // 批量基因设计
70
+ document.getElementById('batchForm').addEventListener('submit', async function(e) {
71
+ e.preventDefault();
72
+
73
+ const batchGenesText = document.getElementById('batchGenes').value.trim();
74
+ const species = document.getElementById('batchSpecies').value;
75
+
76
+ if (!batchGenesText) {
77
+ alert('请输入基因列表');
78
+ return;
79
+ }
80
+
81
+ // 解析基因列表
82
+ const geneList = parseGeneList(batchGenesText);
83
+
84
+ if (geneList.length === 0) {
85
+ alert('未找到有效的基因名称');
86
+ return;
87
+ }
88
+
89
+ // 显示进度条
90
+ document.getElementById('batchProgress').style.display = 'block';
91
+ document.getElementById('results').style.display = 'none';
92
+ document.getElementById('batchResults').style.display = 'none';
93
+ document.getElementById('batchSubmitBtn').disabled = true;
94
+
95
+ try {
96
+ const response = await fetch('/batch_design_primers', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ },
101
+ body: JSON.stringify({
102
+ gene_list: geneList,
103
+ species: species
104
+ })
105
+ });
106
+
107
+ const data = await response.json();
108
+ currentBatchResults = data.results;
109
+ showBatchResults(data.results);
110
+
111
+ } catch (error) {
112
+ showError('网络错误,请检查连接后重试');
113
+ } finally {
114
+ document.getElementById('batchProgress').style.display = 'none';
115
+ document.getElementById('batchSubmitBtn').disabled = false;
116
+ }
117
+ });
118
+
119
+ function parseGeneList(text) {
120
+ // 支持多种分隔符:换行、逗号、空格
121
+ return text
122
+ .split(/[\n,\s]+/)
123
+ .map(gene => gene.trim())
124
+ .filter(gene => gene.length > 0);
125
+ }
126
+
127
+ function showError(message) {
128
+ document.getElementById('results').innerHTML = `
129
+ <div class="error">
130
+ ❌ ${message}
131
+ </div>
132
+ `;
133
+ document.getElementById('results').style.display = 'block';
134
+ }
135
+
136
+ function showResults(data) {
137
+ const geneInfo = data.gene_info;
138
+ const primers = data.primers;
139
+
140
+ // 显示基因信息
141
+ document.getElementById('geneInfo').innerHTML = `
142
+ <h3>📋 基因信息</h3>
143
+ <p><strong>基因名称:</strong> ${geneInfo.symbol}</p>
144
+ <p><strong>RefSeq ID:</strong> ${geneInfo.nm_id}</p>
145
+ <p><strong>外显子交界点:</strong> ${geneInfo.junctions.join(', ')}</p>
146
+ `;
147
+
148
+ // 显示引物结果
149
+ let primerHtml = '<h3>🎯 推荐引物序列</h3>';
150
+
151
+ primers.forEach(primer => {
152
+ primerHtml += `
153
+ <div class="primer-card">
154
+ <div class="primer-header">
155
+ <span class="primer-id">引物对 ${primer.id}</span>
156
+ <span class="product-size">${primer.product_size} bp</span>
157
+ </div>
158
+
159
+ <div class="primer-sequences">
160
+ <div class="sequence-box">
161
+ <div class="sequence-label">正向引物 (Forward)</div>
162
+ <div class="sequence">${primer.forward}</div>
163
+ </div>
164
+ <div class="sequence-box">
165
+ <div class="sequence-label">反向引物 (Reverse)</div>
166
+ <div class="sequence">${primer.reverse}</div>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="tm-info">
171
+ <span>正向Tm: ${primer.f_tm}°C</span>
172
+ <span>反向Tm: ${primer.r_tm}°C</span>
173
+ <span>${primer.junction_info}</span>
174
+ </div>
175
+ </div>
176
+ `;
177
+ });
178
+
179
+ document.getElementById('primerResults').innerHTML = primerHtml;
180
+ document.getElementById('exportSection').style.display = 'block';
181
+ document.getElementById('results').style.display = 'block';
182
+ }
183
+
184
+ function showBatchResults(results) {
185
+ const successCount = results.filter(r => r.status === 'success').length;
186
+ const failedCount = results.filter(r => r.status === 'failed').length;
187
+ const totalPrimers = results
188
+ .filter(r => r.status === 'success')
189
+ .reduce((sum, r) => sum + r.data.primers.length, 0);
190
+
191
+ // 显示统计信息
192
+ document.getElementById('resultSummary').innerHTML = `
193
+ <div class="summary-stats">
194
+ <div class="stat-item">
195
+ <div class="stat-number">${results.length}</div>
196
+ <div class="stat-label">总基因数</div>
197
+ </div>
198
+ <div class="stat-item">
199
+ <div class="stat-number">${successCount}</div>
200
+ <div class="stat-label">成功设计</div>
201
+ </div>
202
+ <div class="stat-item">
203
+ <div class="stat-number">${failedCount}</div>
204
+ <div class="stat-label">设计失败</div>
205
+ </div>
206
+ <div class="stat-item">
207
+ <div class="stat-number">${totalPrimers}</div>
208
+ <div class="stat-label">引物对总数</div>
209
+ </div>
210
+ </div>
211
+ `;
212
+
213
+ // 显示失败的基因
214
+ const failedGenes = results.filter(r => r.status === 'failed');
215
+ if (failedGenes.length > 0) {
216
+ let failedHtml = '<div class="failed-genes"><h4>⚠️ 设计失败的基因</h4>';
217
+ failedGenes.forEach(gene => {
218
+ failedHtml += `
219
+ <div class="failed-gene">
220
+ <strong>${gene.gene}:</strong> ${gene.error}
221
+ </div>
222
+ `;
223
+ });
224
+ failedHtml += '</div>';
225
+ document.getElementById('failedGenes').innerHTML = failedHtml;
226
+ } else {
227
+ document.getElementById('failedGenes').innerHTML = '';
228
+ }
229
+
230
+ // 显示成功的引物结果
231
+ let primerHtml = '<h3>🎯 批量引物设计结果</h3>';
232
+
233
+ results.filter(r => r.status === 'success').forEach(result => {
234
+ const geneInfo = result.data.gene_info;
235
+ const primers = result.data.primers;
236
+
237
+ primerHtml += `<div style="margin-bottom: 40px; border: 2px solid #e1e5e9; border-radius: 8px; padding: 20px;">`;
238
+ primerHtml += `<h4 style="color: #4facfe; margin-bottom: 15px;">📋 ${geneInfo.symbol} (${geneInfo.nm_id})</h4>`;
239
+
240
+ primers.forEach(primer => {
241
+ primerHtml += `
242
+ <div class="primer-card">
243
+ <div class="primer-header">
244
+ <span class="primer-id">引物对 ${primer.id}</span>
245
+ <span class="product-size">${primer.product_size} bp</span>
246
+ </div>
247
+
248
+ <div class="primer-sequences">
249
+ <div class="sequence-box">
250
+ <div class="sequence-label">正向引物 (Forward)</div>
251
+ <div class="sequence">${primer.forward}</div>
252
+ </div>
253
+ <div class="sequence-box">
254
+ <div class="sequence-label">反向引物 (Reverse)</div>
255
+ <div class="sequence">${primer.reverse}</div>
256
+ </div>
257
+ </div>
258
+
259
+ <div class="tm-info">
260
+ <span>正向Tm: ${primer.f_tm}°C</span>
261
+ <span>反向Tm: ${primer.r_tm}°C</span>
262
+ <span>${primer.junction_info}</span>
263
+ </div>
264
+ </div>
265
+ `;
266
+ });
267
+
268
+ primerHtml += '</div>';
269
+ });
270
+
271
+ document.getElementById('batchPrimerResults').innerHTML = primerHtml;
272
+ document.getElementById('batchResults').style.display = 'block';
273
+ }
274
+
275
+ // 导出单个结果
276
+ async function exportResults(format) {
277
+ if (!currentResults) {
278
+ alert('没有可导出的结果');
279
+ return;
280
+ }
281
+
282
+ try {
283
+ const response = await fetch('/export_primers', {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Content-Type': 'application/json',
287
+ },
288
+ body: JSON.stringify({
289
+ format: format,
290
+ data: currentResults
291
+ })
292
+ });
293
+
294
+ if (response.ok) {
295
+ const blob = await response.blob();
296
+ const url = window.URL.createObjectURL(blob);
297
+ const a = document.createElement('a');
298
+ a.href = url;
299
+ a.download = response.headers.get('Content-Disposition')?.split('filename=')[1] || `primers.${format}`;
300
+ document.body.appendChild(a);
301
+ a.click();
302
+ window.URL.revokeObjectURL(url);
303
+ document.body.removeChild(a);
304
+ } else {
305
+ alert('导出失败,请重试');
306
+ }
307
+ } catch (error) {
308
+ alert('导出失败:' + error.message);
309
+ }
310
+ }
311
+
312
+ // 导出批量结果
313
+ async function exportBatchResults(format) {
314
+ if (!currentBatchResults) {
315
+ alert('没有可导出的结果');
316
+ return;
317
+ }
318
+
319
+ try {
320
+ const response = await fetch('/export_primers', {
321
+ method: 'POST',
322
+ headers: {
323
+ 'Content-Type': 'application/json',
324
+ },
325
+ body: JSON.stringify({
326
+ format: format,
327
+ data: currentBatchResults
328
+ })
329
+ });
330
+
331
+ if (response.ok) {
332
+ const blob = await response.blob();
333
+ const url = window.URL.createObjectURL(blob);
334
+ const a = document.createElement('a');
335
+ a.href = url;
336
+ a.download = response.headers.get('Content-Disposition')?.split('filename=')[1] || `batch_primers.${format}`;
337
+ document.body.appendChild(a);
338
+ a.click();
339
+ window.URL.revokeObjectURL(url);
340
+ document.body.removeChild(a);
341
+ } else {
342
+ alert('导出失败,请重试');
343
+ }
344
+ } catch (error) {
345
+ alert('导出失败:' + error.message);
346
+ }
347
+ }
app.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, send_file
2
+ from Bio import Entrez, SeqIO
3
+ import primer3
4
+ import ssl
5
+ import pandas as pd
6
+ import io
7
+ import json
8
+ from datetime import datetime
9
+
10
+ app = Flask(__name__)
11
+ ssl._create_default_https_context = ssl._create_unverified_context
12
+ def get_ready_for_primers(gene_symbol, species="human"):
13
+ """获取基因序列和外显子交界点信息"""
14
+ Entrez.email = "your_email@example.com"
15
+
16
+ # 1. 搜索基因并获取 NCBI 内部 ID
17
+ search_term = f"{gene_symbol}[Gene Name] AND {species}[Organism]"
18
+ handle = Entrez.esearch(db="gene", term=search_term)
19
+ record = Entrez.read(handle)
20
+ if not record["IdList"]:
21
+ return {"error": "未找到该基因"}
22
+ gene_id = record["IdList"][0]
23
+
24
+ # 2. 获取该基因关联的 NM_ 编号
25
+ link_handle = Entrez.elink(dbfrom="gene", db="nucleotide", id=gene_id, term="srcdb_refseq[prop] AND mRNA[filter]")
26
+ link_record = Entrez.read(link_handle)
27
+
28
+ try:
29
+ # 获取第一个关联的核苷酸 UID
30
+ nucl_id = link_record[0]["LinkSetDb"][0]["Link"][0]["Id"]
31
+
32
+ # 3. 下载完整的 GenBank 格式数据
33
+ handle = Entrez.efetch(db="nucleotide", id=nucl_id, rettype="gb", retmode="text")
34
+ seq_record = SeqIO.read(handle, "genbank")
35
+
36
+ # 提取外显子分界点
37
+ junctions = []
38
+ current_pos = 0
39
+ for feature in seq_record.features:
40
+ if feature.type == "exon":
41
+ start, end = feature.location.start, feature.location.end
42
+ current_pos += (end - start)
43
+ junctions.append(int(current_pos))
44
+
45
+ if junctions:
46
+ junctions.pop() # 移除最后一个边界
47
+
48
+ return {
49
+ "symbol": gene_symbol,
50
+ "nm_id": seq_record.id,
51
+ "sequence": str(seq_record.seq),
52
+ "junctions": junctions
53
+ }
54
+ except Exception as e:
55
+ return {"error": f"获取基因信息失败: {str(e)}"}
56
+
57
+ def design_qpcr_primers(gene_data):
58
+ """设计qPCR引物"""
59
+ if "error" in gene_data:
60
+ return gene_data
61
+
62
+ seq_args = {
63
+ 'SEQUENCE_ID': gene_data['nm_id'],
64
+ 'SEQUENCE_TEMPLATE': gene_data['sequence'],
65
+ 'SEQUENCE_OVERLAP_JUNCTION_LIST': gene_data['junctions'],
66
+ }
67
+
68
+ global_args = {
69
+ 'PRIMER_OPT_SIZE': 20,
70
+ 'PRIMER_MIN_SIZE': 18,
71
+ 'PRIMER_MAX_SIZE': 25,
72
+ 'PRIMER_OPT_TM': 60.0,
73
+ 'PRIMER_MIN_TM': 57.0,
74
+ 'PRIMER_MAX_TM': 63.0,
75
+ 'PRIMER_TM_MAX_DIFF': 1.0,
76
+ 'PRIMER_MIN_GC': 40.0,
77
+ 'PRIMER_MAX_GC': 60.0,
78
+ 'PRIMER_PRODUCT_SIZE_RANGE': [120, 300],
79
+ 'PRIMER_NUM_RETURN': 5,
80
+ }
81
+
82
+ try:
83
+ results = primer3.bindings.design_primers(seq_args, global_args)
84
+
85
+ primer_pairs = []
86
+ for i in range(global_args['PRIMER_NUM_RETURN']):
87
+ try:
88
+ pair = {
89
+ "id": i + 1,
90
+ "forward": results[f'PRIMER_LEFT_{i}_SEQUENCE'],
91
+ "reverse": results[f'PRIMER_RIGHT_{i}_SEQUENCE'],
92
+ "f_tm": f"{results[f'PRIMER_LEFT_{i}_TM']:.2f}",
93
+ "r_tm": f"{results[f'PRIMER_RIGHT_{i}_TM']:.2f}",
94
+ "product_size": results[f'PRIMER_PAIR_{i}_PRODUCT_SIZE'],
95
+ "junction_info": "跨越外显子交界点"
96
+ }
97
+ primer_pairs.append(pair)
98
+ except KeyError:
99
+ break
100
+
101
+ return {"primers": primer_pairs, "gene_info": gene_data}
102
+ except Exception as e:
103
+ return {"error": f"引物设计失败: {str(e)}"}
104
+
105
+ @app.route('/')
106
+ def index():
107
+ return render_template('index.html')
108
+
109
+ @app.route('/design_primers', methods=['POST'])
110
+ def design_primers_api():
111
+ data = request.json
112
+ gene_symbol = data.get('gene_symbol', '').strip()
113
+ species = data.get('species', 'human')
114
+
115
+ if not gene_symbol:
116
+ return jsonify({"error": "请输入基因名称"})
117
+
118
+ # 获取基因信息
119
+ gene_data = get_ready_for_primers(gene_symbol, species)
120
+
121
+ # 设计引物
122
+ result = design_qpcr_primers(gene_data)
123
+
124
+ return jsonify(result)
125
+
126
+ @app.route('/batch_design_primers', methods=['POST'])
127
+ def batch_design_primers_api():
128
+ data = request.json
129
+ gene_list = data.get('gene_list', [])
130
+ species = data.get('species', 'human')
131
+
132
+ if not gene_list:
133
+ return jsonify({"error": "请输入基因列表"})
134
+
135
+ results = []
136
+ for gene_symbol in gene_list:
137
+ gene_symbol = gene_symbol.strip()
138
+ if not gene_symbol:
139
+ continue
140
+
141
+ # 获取基因信息
142
+ gene_data = get_ready_for_primers(gene_symbol, species)
143
+
144
+ # 设计引物
145
+ result = design_qpcr_primers(gene_data)
146
+
147
+ if "error" in result:
148
+ results.append({
149
+ "gene": gene_symbol,
150
+ "status": "failed",
151
+ "error": result["error"]
152
+ })
153
+ else:
154
+ results.append({
155
+ "gene": gene_symbol,
156
+ "status": "success",
157
+ "data": result
158
+ })
159
+
160
+ return jsonify({"results": results})
161
+
162
+ @app.route('/export_primers', methods=['POST'])
163
+ def export_primers():
164
+ data = request.json
165
+ export_format = data.get('format', 'excel') # excel, csv, json
166
+ results_data = data.get('data', [])
167
+
168
+ if export_format == 'excel':
169
+ return export_to_excel(results_data)
170
+ elif export_format == 'csv':
171
+ return export_to_csv(results_data)
172
+ elif export_format == 'json':
173
+ return export_to_json(results_data)
174
+ else:
175
+ return jsonify({"error": "不支持的导出格式"})
176
+
177
+ def export_to_excel(results_data):
178
+ """导出为Excel格式"""
179
+ rows = []
180
+
181
+ for result in results_data:
182
+ if result.get('status') == 'success':
183
+ gene_info = result['data']['gene_info']
184
+ primers = result['data']['primers']
185
+
186
+ for primer in primers:
187
+ rows.append({
188
+ '基因名称': gene_info['symbol'],
189
+ 'RefSeq ID': gene_info['nm_id'],
190
+ '引物对编号': primer['id'],
191
+ '正向引物序列': primer['forward'],
192
+ '反向引物序列': primer['reverse'],
193
+ '正向引物Tm(°C)': primer['f_tm'],
194
+ '反向引物Tm(°C)': primer['r_tm'],
195
+ '产物长度(bp)': primer['product_size'],
196
+ '外显子交界点': ', '.join(map(str, gene_info['junctions'])),
197
+ '设计时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
198
+ })
199
+ else:
200
+ rows.append({
201
+ '基因名称': result['gene'],
202
+ 'RefSeq ID': 'N/A',
203
+ '引物对编号': 'N/A',
204
+ '正向引物序列': 'N/A',
205
+ '反向引物序列': 'N/A',
206
+ '正向引物Tm(°C)': 'N/A',
207
+ '反向引物Tm(°C)': 'N/A',
208
+ '产物长度(bp)': 'N/A',
209
+ '外显子交界点': 'N/A',
210
+ '设计时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
211
+ '错误信息': result.get('error', '未知错误')
212
+ })
213
+
214
+ df = pd.DataFrame(rows)
215
+
216
+ # 创建Excel文件
217
+ output = io.BytesIO()
218
+ with pd.ExcelWriter(output, engine='openpyxl') as writer:
219
+ df.to_excel(writer, sheet_name='引物设计结果', index=False)
220
+
221
+ output.seek(0)
222
+ filename = f"qPCR_primers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
223
+
224
+ return send_file(
225
+ output,
226
+ mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
227
+ as_attachment=True,
228
+ download_name=filename
229
+ )
230
+
231
+ def export_to_csv(results_data):
232
+ """导出为CSV格式"""
233
+ rows = []
234
+
235
+ for result in results_data:
236
+ if result.get('status') == 'success':
237
+ gene_info = result['data']['gene_info']
238
+ primers = result['data']['primers']
239
+
240
+ for primer in primers:
241
+ rows.append({
242
+ '基因名称': gene_info['symbol'],
243
+ 'RefSeq ID': gene_info['nm_id'],
244
+ '引物对编号': primer['id'],
245
+ '正向引物序列': primer['forward'],
246
+ '反向引物序列': primer['reverse'],
247
+ '正向引物Tm(°C)': primer['f_tm'],
248
+ '反向引物Tm(°C)': primer['r_tm'],
249
+ '产物长度(bp)': primer['product_size'],
250
+ '外显子交界点': ', '.join(map(str, gene_info['junctions'])),
251
+ '设计时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
252
+ })
253
+
254
+ df = pd.DataFrame(rows)
255
+
256
+ output = io.StringIO()
257
+ df.to_csv(output, index=False, encoding='utf-8-sig')
258
+
259
+ filename = f"qPCR_primers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
260
+
261
+ return send_file(
262
+ io.BytesIO(output.getvalue().encode('utf-8-sig')),
263
+ mimetype='text/csv',
264
+ as_attachment=True,
265
+ download_name=filename
266
+ )
267
+
268
+ def export_to_json(results_data):
269
+ """导出为JSON格式"""
270
+ export_data = {
271
+ "export_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
272
+ "total_genes": len(results_data),
273
+ "results": results_data
274
+ }
275
+
276
+ filename = f"qPCR_primers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
277
+
278
+ return send_file(
279
+ io.BytesIO(json.dumps(export_data, ensure_ascii=False, indent=2).encode('utf-8')),
280
+ mimetype='application/json',
281
+ as_attachment=True,
282
+ download_name=filename
283
+ )
284
+
285
+ if __name__ == '__main__':
286
+ app.run(debug=True, host='0.0.0.0', port=5000)
index.html ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>qPCR引物设计工具</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 15px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5em;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 1.1em;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .form-section {
48
+ padding: 40px;
49
+ }
50
+
51
+ .tab-container {
52
+ margin-bottom: 30px;
53
+ }
54
+
55
+ .tab-buttons {
56
+ display: flex;
57
+ border-bottom: 2px solid #e1e5e9;
58
+ margin-bottom: 20px;
59
+ }
60
+
61
+ .tab-button {
62
+ padding: 12px 24px;
63
+ background: none;
64
+ border: none;
65
+ cursor: pointer;
66
+ font-size: 16px;
67
+ font-weight: 600;
68
+ color: #666;
69
+ border-bottom: 3px solid transparent;
70
+ transition: all 0.3s;
71
+ }
72
+
73
+ .tab-button.active {
74
+ color: #4facfe;
75
+ border-bottom-color: #4facfe;
76
+ }
77
+
78
+ .tab-content {
79
+ display: none;
80
+ }
81
+
82
+ .tab-content.active {
83
+ display: block;
84
+ }
85
+
86
+ .batch-input {
87
+ margin-bottom: 20px;
88
+ }
89
+
90
+ .batch-textarea {
91
+ width: 100%;
92
+ min-height: 150px;
93
+ padding: 15px;
94
+ border: 2px solid #e1e5e9;
95
+ border-radius: 8px;
96
+ font-size: 14px;
97
+ font-family: 'Courier New', monospace;
98
+ resize: vertical;
99
+ }
100
+
101
+ .batch-help {
102
+ background: #f8f9fa;
103
+ padding: 15px;
104
+ border-radius: 8px;
105
+ margin-bottom: 20px;
106
+ font-size: 14px;
107
+ color: #666;
108
+ }
109
+
110
+ .export-section {
111
+ background: #f8f9fa;
112
+ padding: 20px;
113
+ border-radius: 8px;
114
+ margin-top: 20px;
115
+ }
116
+
117
+ .export-buttons {
118
+ display: flex;
119
+ gap: 10px;
120
+ flex-wrap: wrap;
121
+ }
122
+
123
+ .export-btn {
124
+ background: #28a745;
125
+ color: white;
126
+ padding: 10px 20px;
127
+ border: none;
128
+ border-radius: 6px;
129
+ cursor: pointer;
130
+ font-size: 14px;
131
+ font-weight: 600;
132
+ transition: background 0.3s;
133
+ }
134
+
135
+ .export-btn:hover {
136
+ background: #218838;
137
+ }
138
+
139
+ .export-btn:disabled {
140
+ background: #6c757d;
141
+ cursor: not-allowed;
142
+ }
143
+
144
+ .batch-progress {
145
+ display: none;
146
+ margin: 20px 0;
147
+ }
148
+
149
+ .progress-bar {
150
+ width: 100%;
151
+ height: 20px;
152
+ background: #e1e5e9;
153
+ border-radius: 10px;
154
+ overflow: hidden;
155
+ }
156
+
157
+ .progress-fill {
158
+ height: 100%;
159
+ background: linear-gradient(90deg, #4facfe, #00f2fe);
160
+ width: 0%;
161
+ transition: width 0.3s;
162
+ }
163
+
164
+ .progress-text {
165
+ text-align: center;
166
+ margin-top: 10px;
167
+ font-weight: 600;
168
+ }
169
+
170
+ .batch-results {
171
+ display: none;
172
+ }
173
+
174
+ .result-summary {
175
+ background: white;
176
+ padding: 20px;
177
+ border-radius: 8px;
178
+ margin-bottom: 20px;
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: center;
182
+ }
183
+
184
+ .summary-stats {
185
+ display: flex;
186
+ gap: 30px;
187
+ }
188
+
189
+ .stat-item {
190
+ text-align: center;
191
+ }
192
+
193
+ .stat-number {
194
+ font-size: 24px;
195
+ font-weight: bold;
196
+ color: #4facfe;
197
+ }
198
+
199
+ .stat-label {
200
+ font-size: 12px;
201
+ color: #666;
202
+ text-transform: uppercase;
203
+ }
204
+
205
+ .failed-genes {
206
+ background: #fff3cd;
207
+ border: 1px solid #ffeaa7;
208
+ padding: 15px;
209
+ border-radius: 8px;
210
+ margin-bottom: 20px;
211
+ }
212
+
213
+ .failed-gene {
214
+ margin-bottom: 10px;
215
+ padding: 8px;
216
+ background: white;
217
+ border-radius: 4px;
218
+ border-left: 3px solid #e17055;
219
+ }
220
+
221
+ .form-group {
222
+ margin-bottom: 25px;
223
+ }
224
+
225
+ label {
226
+ display: block;
227
+ margin-bottom: 8px;
228
+ font-weight: 600;
229
+ color: #333;
230
+ }
231
+
232
+ input, select {
233
+ width: 100%;
234
+ padding: 12px 15px;
235
+ border: 2px solid #e1e5e9;
236
+ border-radius: 8px;
237
+ font-size: 16px;
238
+ transition: border-color 0.3s;
239
+ }
240
+
241
+ input:focus, select:focus {
242
+ outline: none;
243
+ border-color: #4facfe;
244
+ }
245
+
246
+ .btn {
247
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
248
+ color: white;
249
+ padding: 15px 30px;
250
+ border: none;
251
+ border-radius: 8px;
252
+ font-size: 16px;
253
+ font-weight: 600;
254
+ cursor: pointer;
255
+ transition: transform 0.2s;
256
+ width: 100%;
257
+ }
258
+
259
+ .btn:hover {
260
+ transform: translateY(-2px);
261
+ }
262
+
263
+ .btn:disabled {
264
+ opacity: 0.6;
265
+ cursor: not-allowed;
266
+ transform: none;
267
+ }
268
+
269
+ .loading {
270
+ display: none;
271
+ text-align: center;
272
+ padding: 20px;
273
+ }
274
+
275
+ .spinner {
276
+ border: 4px solid #f3f3f3;
277
+ border-top: 4px solid #4facfe;
278
+ border-radius: 50%;
279
+ width: 40px;
280
+ height: 40px;
281
+ animation: spin 1s linear infinite;
282
+ margin: 0 auto 10px;
283
+ }
284
+
285
+ @keyframes spin {
286
+ 0% { transform: rotate(0deg); }
287
+ 100% { transform: rotate(360deg); }
288
+ }
289
+
290
+ .results {
291
+ display: none;
292
+ padding: 40px;
293
+ background: #f8f9fa;
294
+ }
295
+
296
+ .gene-info {
297
+ background: white;
298
+ padding: 20px;
299
+ border-radius: 8px;
300
+ margin-bottom: 30px;
301
+ border-left: 4px solid #4facfe;
302
+ }
303
+
304
+ .primer-card {
305
+ background: white;
306
+ border-radius: 8px;
307
+ padding: 20px;
308
+ margin-bottom: 20px;
309
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
310
+ }
311
+
312
+ .primer-header {
313
+ display: flex;
314
+ justify-content: space-between;
315
+ align-items: center;
316
+ margin-bottom: 15px;
317
+ }
318
+
319
+ .primer-id {
320
+ background: #4facfe;
321
+ color: white;
322
+ padding: 5px 15px;
323
+ border-radius: 20px;
324
+ font-weight: 600;
325
+ }
326
+
327
+ .product-size {
328
+ background: #e8f5e8;
329
+ color: #2d5a2d;
330
+ padding: 5px 15px;
331
+ border-radius: 20px;
332
+ font-weight: 600;
333
+ }
334
+
335
+ .primer-sequences {
336
+ display: grid;
337
+ grid-template-columns: 1fr 1fr;
338
+ gap: 20px;
339
+ margin-bottom: 15px;
340
+ }
341
+
342
+ .sequence-box {
343
+ background: #f8f9fa;
344
+ padding: 15px;
345
+ border-radius: 6px;
346
+ border-left: 3px solid #4facfe;
347
+ }
348
+
349
+ .sequence-label {
350
+ font-weight: 600;
351
+ color: #666;
352
+ margin-bottom: 5px;
353
+ }
354
+
355
+ .sequence {
356
+ font-family: 'Courier New', monospace;
357
+ font-size: 14px;
358
+ word-break: break-all;
359
+ background: white;
360
+ padding: 8px;
361
+ border-radius: 4px;
362
+ border: 1px solid #e1e5e9;
363
+ }
364
+
365
+ .tm-info {
366
+ display: flex;
367
+ justify-content: space-between;
368
+ font-size: 14px;
369
+ color: #666;
370
+ }
371
+
372
+ .error {
373
+ background: #fee;
374
+ color: #c33;
375
+ padding: 15px;
376
+ border-radius: 8px;
377
+ border-left: 4px solid #c33;
378
+ margin: 20px 0;
379
+ }
380
+
381
+ @media (max-width: 768px) {
382
+ .primer-sequences {
383
+ grid-template-columns: 1fr;
384
+ }
385
+
386
+ .primer-header {
387
+ flex-direction: column;
388
+ gap: 10px;
389
+ }
390
+ }
391
+ </style>
392
+ </head>
393
+ <body>
394
+ <div class="container">
395
+ <div class="header">
396
+ <h1>🧬 qPCR引物设计工具</h1>
397
+ <p>输入基因名称和物种,自动设计跨越外显子交界点的qPCR引物</p>
398
+ <p style="margin-top: 15px; font-size: 0.9em; opacity: 0.8;">Developed by PharC | Contact: pc794797023@163.com</p>
399
+ </div>
400
+
401
+ <div class="form-section">
402
+ <div class="tab-container">
403
+ <div class="tab-buttons">
404
+ <button type="button" class="tab-button active" onclick="switchTab('single')">
405
+ 🧬 单个基因
406
+ </button>
407
+ <button type="button" class="tab-button" onclick="switchTab('batch')">
408
+ 📋 批量处理
409
+ </button>
410
+ </div>
411
+
412
+ <!-- 单个基因设计 -->
413
+ <div id="single-tab" class="tab-content active">
414
+ <form id="primerForm">
415
+ <div class="form-group">
416
+ <label for="geneSymbol">基因名称 *</label>
417
+ <input type="text" id="geneSymbol" name="geneSymbol" placeholder="例如: GAPDH, Gpr34" required>
418
+ </div>
419
+
420
+ <div class="form-group">
421
+ <label for="species">物种</label>
422
+ <select id="species" name="species">
423
+ <option value="human">人类 (Human)</option>
424
+ <option value="Mus musculus">小鼠 (Mouse)</option>
425
+ <option value="Rattus norvegicus">大鼠 (Rat)</option>
426
+ <option value="Drosophila melanogaster">果蝇 (Drosophila)</option>
427
+ <option value="Caenorhabditis elegans">线虫 (C. elegans)</option>
428
+ <option value="Danio rerio">斑马鱼 (Zebrafish)</option>
429
+ </select>
430
+ </div>
431
+
432
+ <button type="submit" class="btn" id="submitBtn">
433
+ 🔬 设计引物
434
+ </button>
435
+ </form>
436
+ </div>
437
+
438
+ <!-- 批量基因设计 -->
439
+ <div id="batch-tab" class="tab-content">
440
+ <form id="batchForm">
441
+ <div class="batch-help">
442
+ 💡 <strong>使用说明:</strong>每行输入一个基因名称,支持多种格式:
443
+ <br>• 一行一个基因:GAPDH
444
+ <br>• 逗号分隔:GAPDH, ACTB, TUBB3
445
+ <br>• 空格分隔:GAPDH ACTB TUBB3
446
+ </div>
447
+
448
+ <div class="form-group">
449
+ <label for="batchGenes">基因列表 *</label>
450
+ <textarea
451
+ id="batchGenes"
452
+ class="batch-textarea"
453
+ placeholder="请输入基因名称,每行一个或用逗号/空格分隔:&#10;GAPDH&#10;ACTB&#10;TUBB3&#10;或者:GAPDH, ACTB, TUBB3"
454
+ required
455
+ ></textarea>
456
+ </div>
457
+
458
+ <div class="form-group">
459
+ <label for="batchSpecies">物种</label>
460
+ <select id="batchSpecies" name="batchSpecies">
461
+ <option value="human">人类 (Human)</option>
462
+ <option value="Mus musculus">小鼠 (Mouse)</option>
463
+ <option value="Rattus norvegicus">大鼠 (Rat)</option>
464
+ <option value="Drosophila melanogaster">果蝇 (Drosophila)</option>
465
+ <option value="Caenorhabditis elegans">线虫 (C. elegans)</option>
466
+ <option value="Danio rerio">斑马鱼 (Zebrafish)</option>
467
+ </select>
468
+ </div>
469
+
470
+ <button type="submit" class="btn" id="batchSubmitBtn">
471
+ 🚀 批量设计引物
472
+ </button>
473
+ </form>
474
+
475
+ <div class="batch-progress" id="batchProgress">
476
+ <div class="progress-bar">
477
+ <div class="progress-fill" id="progressFill"></div>
478
+ </div>
479
+ <div class="progress-text" id="progressText">正在处理: 0/0</div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <div class="loading" id="loading">
486
+ <div class="spinner"></div>
487
+ <p>正在设计引物,请稍候...</p>
488
+ </div>
489
+
490
+ <div class="results" id="results">
491
+ <div class="gene-info" id="geneInfo"></div>
492
+ <div id="primerResults"></div>
493
+
494
+ <div class="export-section" id="exportSection" style="display: none;">
495
+ <h4>📥 导出结果</h4>
496
+ <p>选择导出格式:</p>
497
+ <div class="export-buttons">
498
+ <button class="export-btn" onclick="exportResults('excel')">
499
+ 📊 Excel格式
500
+ </button>
501
+ <button class="export-btn" onclick="exportResults('csv')">
502
+ 📄 CSV格式
503
+ </button>
504
+ <button class="export-btn" onclick="exportResults('json')">
505
+ 🔧 JSON格式
506
+ </button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <div class="batch-results" id="batchResults">
512
+ <div class="result-summary" id="resultSummary"></div>
513
+ <div id="failedGenes"></div>
514
+ <div id="batchPrimerResults"></div>
515
+
516
+ <div class="export-section">
517
+ <h4>📥 导出批量结果</h4>
518
+ <p>选择导出格式:</p>
519
+ <div class="export-buttons">
520
+ <button class="export-btn" onclick="exportBatchResults('excel')">
521
+ 📊 Excel格式
522
+ </button>
523
+ <button class="export-btn" onclick="exportBatchResults('csv')">
524
+ 📄 CSV格式
525
+ </button>
526
+ <button class="export-btn" onclick="exportBatchResults('json')">
527
+ 🔧 JSON格式
528
+ </button>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+
534
+ <script src="{{ url_for('static', filename='app.js') }}"></script>
535
+ </body>
536
+ </html>
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask
2
+ biopython
3
+ primer3-py
4
+ pandas
5
+ openpyxl