1.页面文件:index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>TAPD Bug 评估</title>
<style>
body { font-family: sans-serif; padding: 20px; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
input[type="number"] { width: 60px; }
</style>
</head>
<body>
<h2>Bug 评估计算器</h2>
<label for="month">选择月份:</label>
<input type="month" id="month" />
<div id="fixerInputs" style="margin-top: 20px;">
<!-- 动态渲染 fixer 和输入框 -->
</div>
<button id="calculateBtn">计算得分</button>
<div id="resultArea">
<!-- 显示结果 -->
</div>
<script src="assessmentScore.js" type="module"></script>
</body>
</html>
2.主要计算:assessmentScore.js
// 默认当前月
// document.getElementById('month').value = new Date().toISOString().slice(0, 7);
// 设置默认日期为上一个月
const monthInput = document.getElementById('month');
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); // 上一个月
const formatted = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}`;
monthInput.value = formatted;
const fixerRatioMap = {}; // 存储用户输入
let fixerList = []; // 所有人名
let rawBugData = []; // 所有原始数据
// 获取数据 & 渲染输入框
(async () => {
rawBugData = await fetchBugData();
fixerList = getFixers(rawBugData, getSelectedMonth());
renderFixerInputs(fixerList);
})();
function getSelectedMonth() {
return document.getElementById('month').value;
}
function getFixers(data, month) {
const fixers = new Set();
data.forEach(item => {
const created = item.Bug.created;
const fixer = item.Bug.fixer;
if (created.startsWith(month) && fixer) {
fixers.add(fixer);
}
});
return Array.from(fixers);
}
function renderFixerInputs(fixers) {
const container = document.getElementById('fixerInputs');
container.innerHTML = '<h3>输入任务占比(0–100%)</h3><table><tr><th>姓名</th><th>任务占比%</th></tr>' +
fixers.map(name => `
<tr>
<td>${name}</td>
<td><input type="number" min="0" max="100" data-fixer="${name}" /></td>
</tr>
`).join('') + '</table>';
}
// 绑定点击事件
document.getElementById('calculateBtn').addEventListener('click', () => {
// 读取输入框
document.querySelectorAll('input[data-fixer]').forEach(input => {
const fixer = input.dataset.fixer;
const val = parseFloat(input.value);
fixerRatioMap[fixer] = isNaN(val) ? null : val / 100;
});
const month = getSelectedMonth();
const result = calculateScores(rawBugData, month, fixerRatioMap);
renderResult(result);
});
function renderResult(data) {
const html = `
<h3>评估结果(总分:33.25分)</h3>
<table>
<tr><th>Fixer</th><th>normal</th><th>serious</th><th>fatal</th><th>bugScore</th><th>得分</th></tr>
${data.map(d => `
<tr>
<td>${d.fixer}</td>
<td>${d.normal}</td>
<td>${d.serious}</td>
<td>${d.fatal}</td>
<td>${d.bugScore}</td>
<td>${d.score}</td>
</tr>
`).join('')}
</table>
`;
document.getElementById('resultArea').innerHTML = html;
}
// === 以下是你已有逻辑的调整版 ===
async function fetchBugData() {
const token = 'iUXZdUZJTrJfmoM0';
const projectArray = [
{ name: 'xxx1', workspace_id: 'xx1', conf_id: 'x1' },
{ name: 'xxx2', workspace_id: 'xx2', conf_id: 'x2' },
{ name: xxx3', workspace_id: 'xx3', conf_id: 'x3' }
];
let merged = [];
for (let i = 1; i <= 10; i++) {
const promises = projectArray.map(item =>
fetch('/api/tapd', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workspace_id: item.workspace_id,
conf_id: item.conf_id,
sort_name: "",
confIdType: "URL",
order: "desc",
perpage: 50,
page: i,
selected_workspace_ids: "",
query_token: "",
location: "/bugtrace/bugreports/my_view",
target: `${item.workspace_id}/bug/normal`,
entity_types: ["bug"],
use_scene: "bug_list",
return_url: `https://www.tapd.cn/tapd_fe/${item.workspace_id}/bug/list?confId=${item.conf_id}`,
identifier: "app_for_list_tools,app_for_list_operation",
dsc_token: token
})
}).then(res => res.json().then(r => r?.data?.bugs_list_ret?.data?.bugs_list || []))
);
const res = await Promise.all(promises);
merged = merged.concat(...res);
}
return merged;
}
function calculateScores(data, time, ratioMap = {}) {
const filtered = data.filter(item => item.Bug.created.startsWith(time));
const fixerStats = {};
filtered.forEach(item => {
const { fixer, severity } = item.Bug;
const sev = severity?.toLowerCase();
if (!fixer || !['normal', 'serious', 'fatal'].includes(sev)) return;
fixerStats[fixer] ||= { normal: 0, serious: 0, fatal: 0, bugScore: 0 };
const scoreMap = { normal: 1, serious: 3, fatal: 5 };
fixerStats[fixer][sev]++;
fixerStats[fixer].bugScore += scoreMap[sev];
});
return Object.entries(fixerStats).map(([fixer, stats]) => {
const ratio = typeof ratioMap[fixer] === 'number' ? ratioMap[fixer] : 0.25;
const x = stats.bugScore;
const score = (100 - x * (1 - ratio)) * 35 * 0.95 / 100;
return {
fixer,
...stats,
score: score.toFixed(2),
};
});
}
3.处理跨域问题:proxy.js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
const PORT = 3000;
app.use(express.static('public'));
app.use(express.json());
app.post('/api/tapd', async (req, res) => {
try {
const response = await fetch('https://www.tapd.cn/api/aggregation/bug_aggregation/get_bugs_list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': 't_u=196aae59xxxxxxxxxxxxx;',
'Referer': 'https://www.tapd.cn/'
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.json(data);
} catch (err) {
res.status(500).json({ error: '代理请求失败', detail: err.message });
}
});
app.listen(PORT, () => {
console.log(`✅ 代理服务运行中:http://localhost:${PORT}`);
});
4.自行安装对应的依赖:package.json文件
{
"name": "project-folder",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "wang",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^5.1.0",
"node-fetch": "^3.3.2"
}
}