删除旧版 dashboard,已迁移到 dashboard-v2
This commit is contained in:
parent
14607fec28
commit
6ca98f4520
@ -1,214 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>任务看板 · 糖心v2.0</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --surface: #161b22; --surface2: #21262d;
|
||||
--border: #30363d; --text: #e6edf3; --text2: #8b949e; --text3: #6e7681;
|
||||
--done: #3fb950; --doing: #58a6ff; --todo: #6e7681; --blocked: #f85149;
|
||||
--p0: #f85149; --p1: #e3b341; --p2: #3fb950;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; }
|
||||
|
||||
.nav {
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
height: 52px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 20px; position: sticky; top: 0; z-index: 200;
|
||||
}
|
||||
.nav-brand { font-weight: 600; font-size: 15px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-link {
|
||||
color: var(--text2); text-decoration: none; padding: 6px 14px;
|
||||
border-radius: 6px; font-size: 13px; transition: all .15s;
|
||||
}
|
||||
.nav-link:hover { background: var(--surface2); color: var(--text); }
|
||||
.nav-link.active { color: var(--doing); background: rgba(88,166,255,.1); }
|
||||
|
||||
.main { padding: 20px; }
|
||||
.page-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; }
|
||||
.stats { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.stat {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 6px 14px; font-size: 12px; color: var(--text2); display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.sv { font-size: 17px; font-weight: 700; }
|
||||
.stat.s-total .sv { color: var(--text); }
|
||||
.stat.s-done .sv { color: var(--done); }
|
||||
.stat.s-doing .sv { color: var(--doing); }
|
||||
.stat.s-blocked .sv { color: var(--blocked); }
|
||||
|
||||
/* Board grid */
|
||||
.board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; align-items: start; }
|
||||
|
||||
/* Column */
|
||||
.col { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||
.col-hd {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 16px 12px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.col-title {
|
||||
font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.col-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.col-count {
|
||||
font-size: 11px; font-weight: 600; padding: 2px 8px;
|
||||
border-radius: 10px; background: var(--surface2); color: var(--text2);
|
||||
}
|
||||
.cards { padding: 10px; display: flex; flex-direction: column; gap: 8px; min-height: 60px; }
|
||||
|
||||
/* Card */
|
||||
.card {
|
||||
background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 12px; transition: border-color .15s, transform .1s;
|
||||
cursor: default;
|
||||
}
|
||||
.card:hover { border-color: rgba(88,166,255,.4); transform: translateY(-1px); }
|
||||
.card-top { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.prio {
|
||||
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; letter-spacing: .3px;
|
||||
}
|
||||
.prio.P0 { background: rgba(248,81,73,.15); color: var(--p0); border: 1px solid rgba(248,81,73,.2); }
|
||||
.prio.P1 { background: rgba(227,179,65,.12); color: var(--p1); border: 1px solid rgba(227,179,65,.2); }
|
||||
.prio.P2 { background: rgba(63,185,80,.12); color: var(--p2); border: 1px solid rgba(63,185,80,.2); }
|
||||
.day-tag {
|
||||
font-size: 10px; color: var(--text3); background: rgba(255,255,255,.05);
|
||||
padding: 2px 6px; border-radius: 4px;
|
||||
}
|
||||
.card-title { font-size: 13px; font-weight: 500; line-height: 1.4; margin-bottom: 6px; color: var(--text); }
|
||||
.card-note { font-size: 11px; color: var(--text3); margin-bottom: 8px; line-height: 1.5; }
|
||||
.card-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||
.assignee {
|
||||
font-size: 11px; color: var(--text2);
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.assignee::before {
|
||||
content: ''; display: inline-block; width: 18px; height: 18px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #58a6ff, #3fb950); font-size: 10px;
|
||||
line-height: 18px; text-align: center; flex-shrink: 0;
|
||||
}
|
||||
.blocked-note {
|
||||
font-size: 11px; color: var(--blocked); margin-top: 6px;
|
||||
display: flex; align-items: flex-start; gap: 4px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty { text-align: center; padding: 24px 12px; color: var(--text3); font-size: 12px; }
|
||||
|
||||
.error-box {
|
||||
margin: 24px; padding: 20px;
|
||||
background: rgba(248,81,73,.08); border: 1px solid rgba(248,81,73,.25);
|
||||
border-radius: 8px; color: var(--text2); line-height: 2;
|
||||
}
|
||||
.error-box code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; color: var(--text); font-size: 12px; }
|
||||
|
||||
@media (max-width: 1100px) { .board { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 600px) {
|
||||
.board { grid-template-columns: 1fr; }
|
||||
.main { padding: 12px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ 糖心v2.0</span>
|
||||
<div class="nav-links">
|
||||
<a href="gantt.html" class="nav-link">甘特图</a>
|
||||
<a href="board.html" class="nav-link active">看板</a>
|
||||
<a href="progress.html" class="nav-link">进度</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="page-hd">
|
||||
<h1 class="page-title">任务看板</h1>
|
||||
<div class="stats" id="stats"></div>
|
||||
</div>
|
||||
<div class="board" id="board"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const COL_CFG = {
|
||||
'待开发': { color: '#6e7681', dot: '#6e7681' },
|
||||
'进行中': { color: '#58a6ff', dot: '#58a6ff' },
|
||||
'已完成': { color: '#3fb950', dot: '#3fb950' },
|
||||
'阻塞': { color: '#f85149', dot: '#f85149' },
|
||||
};
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const r = await fetch('tasks.json');
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
render(await r.json());
|
||||
} catch(e) {
|
||||
document.getElementById('board').innerHTML =
|
||||
`<div class="error-box" style="grid-column:1/-1">
|
||||
<strong>⚠️ 无法加载 tasks.json</strong><br>
|
||||
请通过本地服务器访问:<br>
|
||||
<code>python3 -m http.server 8080</code> 然后打开 <code>http://localhost:8080/board.html</code>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const { columns, tasks } = data;
|
||||
|
||||
// Stats
|
||||
const byCol = col => tasks.filter(t => t.column === col).length;
|
||||
document.getElementById('stats').innerHTML =
|
||||
`<div class="stat s-total"><span class="sv">${tasks.length}</span>总计</div>
|
||||
<div class="stat s-done"><span class="sv">${byCol('已完成')}</span>已完成</div>
|
||||
<div class="stat s-doing"><span class="sv">${byCol('进行中')}</span>进行中</div>
|
||||
<div class="stat s-blocked"><span class="sv">${byCol('阻塞')}</span>阻塞</div>`;
|
||||
|
||||
// Board columns
|
||||
let html = '';
|
||||
columns.forEach(col => {
|
||||
const cfg = COL_CFG[col] || { color: '#6e7681', dot: '#6e7681' };
|
||||
const colTasks = tasks.filter(t => t.column === col);
|
||||
|
||||
const cardsHTML = colTasks.length === 0
|
||||
? '<div class="empty">暂无任务</div>'
|
||||
: colTasks.map(t => {
|
||||
const noteHTML = t.note
|
||||
? `<div class="card-note">${t.note}</div>` : '';
|
||||
const blockedHTML = t.column === '阻塞' && t.note
|
||||
? `<div class="blocked-note">⛔ ${t.note}</div>` : '';
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
<span class="prio ${t.priority}">${t.priority}</span>
|
||||
<span class="day-tag">${t.day}</span>
|
||||
</div>
|
||||
<div class="card-title">${t.title}</div>
|
||||
${blockedHTML || noteHTML}
|
||||
<div class="card-footer">
|
||||
<span class="assignee">${t.assignee}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
html += `
|
||||
<div class="col">
|
||||
<div class="col-hd">
|
||||
<div class="col-title" style="color:${cfg.color}">
|
||||
<span class="col-dot" style="background:${cfg.dot}"></span>${col}
|
||||
</div>
|
||||
<span class="col-count">${colTasks.length}</span>
|
||||
</div>
|
||||
<div class="cards">${cardsHTML}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
document.getElementById('board').innerHTML = html;
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,74 +0,0 @@
|
||||
{
|
||||
"days": [
|
||||
{
|
||||
"date": "2026-04-01",
|
||||
"day": "Day 1",
|
||||
"weekday": "周三",
|
||||
"progress": [
|
||||
{"name": "明诚", "planned": "项目初始化+注册登录(4个接口)", "actual": "完成,含JWT+手机号验证码登录", "status": "done"},
|
||||
{"name": "青枫", "planned": "后台初始化+登录认证(2个接口)", "actual": "完成,含管理员token体系", "status": "done"},
|
||||
{"name": "远舟", "planned": "React+AntDesign项目搭建", "actual": "完成,路由+权限框架初始化", "status": "done"},
|
||||
{"name": "微冰", "planned": "公共组件开发", "actual": "完成,Button/Table/Modal等8个基础组件", "status": "done"},
|
||||
{"name": "徐天杰","planned": "iOS工程搭建", "actual": "完成,CocoaPods环境OK", "status": "done"},
|
||||
{"name": "聂军", "planned": "安卓工程搭建", "actual": "完成,Gradle+依赖配置OK", "status": "done"}
|
||||
],
|
||||
"notes": "Day1全员完成✅ 技术栈确认:管理后台React+AntDesign;徐天杰对接CDFlow返回401,IM先往后放"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-02",
|
||||
"day": "Day 2",
|
||||
"weekday": "周四",
|
||||
"progress": [
|
||||
{"name": "明诚", "planned": "第三方登录+钱包基础", "actual": "Google/Apple登录完成,钱包开发中", "status": "done"},
|
||||
{"name": "青枫", "planned": "管理员管理(3个接口)", "actual": "管理员CRUD完成", "status": "done"},
|
||||
{"name": "远舟", "planned": "登录页+Token管理", "actual": "登录页完成,Token刷新逻辑调试中", "status": "done"},
|
||||
{"name": "微冰", "planned": "登录页+菜单布局", "actual": "登录页+侧边菜单完成", "status": "done"},
|
||||
{"name": "徐天杰","planned": "iOS登录注册页面", "actual": "登录注册UI完成,接口联调中", "status": "done"},
|
||||
{"name": "聂军", "planned": "安卓登录注册页面", "actual": "登录注册UI完成,接口联调中", "status": "done"}
|
||||
],
|
||||
"notes": "CDFlow 401问题光明在跟进,暂不影响主线开发"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-03",
|
||||
"day": "Day 3",
|
||||
"weekday": "周五",
|
||||
"progress": [
|
||||
{"name": "明诚", "planned": "媒体上传处理", "actual": "", "status": "todo"},
|
||||
{"name": "青枫", "planned": "角色管理(5个接口)", "actual": "", "status": "todo"},
|
||||
{"name": "远舟", "planned": "权限框架", "actual": "", "status": "todo"},
|
||||
{"name": "微冰", "planned": "权限组件", "actual": "", "status": "todo"},
|
||||
{"name": "徐天杰","planned": "苹果登录对接", "actual": "", "status": "todo"},
|
||||
{"name": "聂军", "planned": "谷歌登录对接", "actual": "", "status": "todo"}
|
||||
],
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"date": "2026-04-04",
|
||||
"day": "Day 4",
|
||||
"weekday": "周六",
|
||||
"progress": [
|
||||
{"name": "明诚", "planned": "内容发布+标签", "actual": "", "status": "todo"},
|
||||
{"name": "青枫", "planned": "内容审核(上)", "actual": "", "status": "todo"},
|
||||
{"name": "远舟", "planned": "管理员管理页(上)", "actual": "", "status": "todo"},
|
||||
{"name": "微冰", "planned": "角色管理页(上)", "actual": "", "status": "todo"},
|
||||
{"name": "徐天杰","planned": "登录联调+Tab框架", "actual": "", "status": "todo"},
|
||||
{"name": "聂军", "planned": "登录联调+Tab框架", "actual": "", "status": "todo"}
|
||||
],
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"date": "2026-04-06",
|
||||
"day": "Day 5",
|
||||
"weekday": "周一",
|
||||
"progress": [
|
||||
{"name": "明诚", "planned": "联调+Bug修复", "actual": "", "status": "todo"},
|
||||
{"name": "青枫", "planned": "内容审核(下)+举报", "actual": "", "status": "todo"},
|
||||
{"name": "远舟", "planned": "管理员管理页(下)", "actual": "", "status": "todo"},
|
||||
{"name": "微冰", "planned": "角色管理页(下)", "actual": "", "status": "todo"},
|
||||
{"name": "徐天杰","planned": "登录联调+Tab框架", "actual": "", "status": "todo"},
|
||||
{"name": "聂军", "planned": "登录联调+Tab框架", "actual": "", "status": "todo"}
|
||||
],
|
||||
"notes": "第1周收尾,周会对齐W2计划"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,344 +0,0 @@
|
||||
# Open Source PM / Gantt / Kanban Libraries for React — Research Report
|
||||
|
||||
> Date: 2026-04-02
|
||||
> Purpose: Technology selection for the 糖心v2.0 operations dashboard (React 18 + TypeScript + Ant Design 5)
|
||||
|
||||
---
|
||||
|
||||
## 1. Gantt Chart Libraries (React)
|
||||
|
||||
### 1.1 gantt-task-react
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~150,000 |
|
||||
| Last commit | 2023 (maintenance mode, issues still triaged) |
|
||||
| React compatibility | React 16–18 |
|
||||
| TypeScript support | Yes — ships full `.d.ts` types |
|
||||
| Chinese locale | Manual — date labels are customizable via `locale` prop |
|
||||
| Bundle size | ~45 kB minified + gzip |
|
||||
| npm package | `gantt-task-react` |
|
||||
|
||||
**Summary:** The most popular standalone React Gantt component. Renders tasks as horizontal bars with dependency arrows, drag-to-resize bars, and a left-side task list. The API is straightforward — pass an array of `Task` objects with `id`, `name`, `start`, `end`, `type`, and `progress`. Supports `ViewMode` switching (Hour / Day / Week / Month). TypeScript types are bundled. Chinese locale requires passing a custom `locale` object but the library documents this clearly.
|
||||
|
||||
**Strengths:** High download volume means broad community knowledge; MIT; zero heavyweight dependencies; works well with Ant Design theming via CSS variables.
|
||||
**Weaknesses:** No official paid support; dependency arrows limited to finish-to-start only; the library is in maintenance mode (no major new features since 2023).
|
||||
|
||||
---
|
||||
|
||||
### 1.2 frappe-gantt (+ React wrapper)
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads (core) | ~20,000 |
|
||||
| Last commit | Active (2024) |
|
||||
| React compatibility | Via community wrapper `frappe-gantt-react` |
|
||||
| TypeScript support | Partial — community types only |
|
||||
| Chinese locale | Not built-in; SVG labels require manual override |
|
||||
| Bundle size | ~30 kB (vanilla JS core) + wrapper overhead |
|
||||
| npm package | `frappe-gantt` + `frappe-gantt-react` |
|
||||
|
||||
**Summary:** A clean vanilla JS Gantt implemented in SVG. The React wrapper is a thin imperative bridge, not a true React component. State management is external to React, which makes integration awkward when other React state must stay in sync with the chart.
|
||||
|
||||
**Strengths:** Beautiful default styling; active upstream development; MIT license.
|
||||
**Weaknesses:** The React integration is not idiomatic; TypeScript types are community-maintained and incomplete; Chinese text in SVG requires custom fonts; not recommended for TypeScript-heavy projects.
|
||||
|
||||
---
|
||||
|
||||
### 1.3 dhtmlxGantt
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | GPL v2 (free) / Commercial (paid) |
|
||||
| npm weekly downloads | ~5,000 |
|
||||
| Last commit | Active (maintained by DHTMLX team) |
|
||||
| React compatibility | Documented React integration via `useEffect` imperative init |
|
||||
| TypeScript support | Yes — ships `.d.ts` |
|
||||
| Chinese locale | Yes — official `locale_cn` |
|
||||
| Bundle size | ~200 kB+ (full feature set) |
|
||||
| npm package | `dhtmlx-gantt` |
|
||||
|
||||
**Summary:** Enterprise-grade Gantt with a rich feature set: baselines, resource view, critical path, PDF export, inline editing. Widely used in enterprise project management tools. The free GPL v2 tier is functional but any commercial closed-source product requires a paid license.
|
||||
|
||||
**Strengths:** Most feature-complete option; official Chinese locale; active commercial support.
|
||||
**Weaknesses:** GPL v2 is incompatible with closed-source commercial products without purchasing a license; imperative DOM-based API does not integrate naturally with React/Redux patterns; large bundle.
|
||||
|
||||
---
|
||||
|
||||
### 1.4 react-gantt-chart
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~3,000 |
|
||||
| Last commit | 2022 (inactive / abandoned) |
|
||||
| React compatibility | React 16–17; React 18 untested |
|
||||
| TypeScript support | Yes |
|
||||
| Chinese locale | No |
|
||||
| Bundle size | ~35 kB |
|
||||
| npm package | `react-gantt-chart` |
|
||||
|
||||
**Summary:** A lightweight React-native Gantt library. While MIT and TypeScript-enabled, development appears to have stalled in 2022. React 18 strict mode compatibility is not guaranteed. Not recommended for new projects.
|
||||
|
||||
---
|
||||
|
||||
### 1.5 bryntum-gantt
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | Commercial only (no free tier) |
|
||||
| npm weekly downloads | N/A (private registry) |
|
||||
| Last commit | Active (Bryntum team) |
|
||||
| React compatibility | Official React wrapper |
|
||||
| TypeScript support | Yes |
|
||||
| Chinese locale | Yes |
|
||||
| Bundle size | ~500 kB+ |
|
||||
| npm package | Private npm registry (requires license) |
|
||||
|
||||
**Summary:** The most powerful commercial Gantt available. Supports resource allocation, baselines, calendars, undo/redo, PDF export, and more. Requires a paid license per developer seat (~$1,500–$3,000/year). Completely off-limits for budget-constrained internal tools.
|
||||
|
||||
**Strengths:** Best-in-class features; excellent support.
|
||||
**Weaknesses:** Commercial license mandatory; extremely large bundle; overkill for a project tracking dashboard.
|
||||
|
||||
---
|
||||
|
||||
### Gantt Recommendation Summary
|
||||
|
||||
| Library | License | Downloads | Recommended |
|
||||
|---------|---------|-----------|-------------|
|
||||
| gantt-task-react | MIT | ~150k/wk | **Yes — primary choice** |
|
||||
| frappe-gantt | MIT | ~20k/wk | No (awkward React bridge) |
|
||||
| dhtmlxGantt | GPL/Commercial | ~5k/wk | No (license issue) |
|
||||
| react-gantt-chart | MIT | ~3k/wk | No (abandoned) |
|
||||
| bryntum-gantt | Commercial | N/A | No (paid) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Kanban Board Libraries (React)
|
||||
|
||||
### 2.1 @hello-pangea/dnd
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | Apache 2.0 |
|
||||
| npm weekly downloads | ~700,000 |
|
||||
| Maintenance status | Actively maintained (community fork, regular releases) |
|
||||
| TypeScript support | Yes — bundled types |
|
||||
| Animation quality | Smooth physics-based drag animation (inherited from react-beautiful-dnd) |
|
||||
| npm package | `@hello-pangea/dnd` |
|
||||
|
||||
**Summary:** The maintained community fork of the now-deprecated `react-beautiful-dnd`. After Atlassian stopped maintaining the original library, the community forked it as `@hello-pangea/dnd`. It has full React 18 support, Concurrent Mode compatibility, and TypeScript types. The API is backward-compatible with `react-beautiful-dnd`, making migration trivial.
|
||||
|
||||
**Strengths:** Drop-in replacement for react-beautiful-dnd; battle-tested drag-and-drop model; high download count; smooth animations; Apache 2.0 license.
|
||||
**Weaknesses:** Drag model is opinionated (list-based); less flexible than @dnd-kit for arbitrary 2D drag targets.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 react-beautiful-dnd
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | Apache 2.0 |
|
||||
| npm weekly downloads | ~2,000,000 (legacy installs) |
|
||||
| Maintenance status | **Deprecated by Atlassian** — no new features, security patches only |
|
||||
| TypeScript support | Yes |
|
||||
| Animation quality | Excellent |
|
||||
| npm package | `react-beautiful-dnd` |
|
||||
|
||||
**Summary:** The original Atlassian drag-and-drop library. Still heavily downloaded due to existing projects, but Atlassian officially deprecated it in 2023 and recommends migrating to `@hello-pangea/dnd` or `@dnd-kit`. Not recommended for new projects.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 @dnd-kit/core + @dnd-kit/sortable
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~3,000,000 (@dnd-kit/core) |
|
||||
| Maintenance status | Actively maintained |
|
||||
| TypeScript support | Yes — first-class TypeScript |
|
||||
| Animation quality | Good — CSS transitions, customizable spring physics |
|
||||
| npm package | `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` |
|
||||
|
||||
**Summary:** A modern, modular drag-and-drop toolkit. More low-level than `@hello-pangea/dnd` but significantly more flexible. Supports touch, pointer, keyboard, and mouse sensors. Excellent accessibility support. The modular design means you only import what you need, keeping bundle size small. Building a Kanban board requires combining `@dnd-kit/core` with `@dnd-kit/sortable` and writing slightly more boilerplate than with @hello-pangea/dnd.
|
||||
|
||||
**Strengths:** Most actively maintained; highest download count; MIT license; accessibility-first; very flexible; works for complex 2D drag scenarios.
|
||||
**Weaknesses:** Lower-level API requires more implementation code; animation setup is manual.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 react-kanban
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~5,000 |
|
||||
| Maintenance status | Low activity (last release 2022) |
|
||||
| TypeScript support | Partial (community types) |
|
||||
| Animation quality | Basic CSS transitions |
|
||||
| npm package | `@asseinfo/react-kanban` |
|
||||
|
||||
**Summary:** A high-level Kanban board component that provides the full board UI out of the box. Very easy to start with but highly opinionated about structure. Limited customization and TypeScript support is poor. Low download numbers and infrequent updates make it a risky choice.
|
||||
|
||||
---
|
||||
|
||||
### Kanban Recommendation Summary
|
||||
|
||||
| Library | License | Downloads | Recommended |
|
||||
|---------|---------|-----------|-------------|
|
||||
| @hello-pangea/dnd | Apache 2.0 | ~700k/wk | **Yes — primary choice** |
|
||||
| @dnd-kit/core | MIT | ~3M/wk | Yes — alternative if flexibility needed |
|
||||
| react-beautiful-dnd | Apache 2.0 | ~2M/wk (legacy) | No (deprecated) |
|
||||
| react-kanban | MIT | ~5k/wk | No (abandoned) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Chart Libraries (React)
|
||||
|
||||
### 3.1 recharts
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~4,500,000 |
|
||||
| React compatibility | React 16–18 (Concurrent Mode supported) |
|
||||
| Chart types | Line, Bar, Area, Pie, Radar, Scatter, Composed, Treemap, Funnel, Sankey |
|
||||
| TypeScript support | Yes — bundled types |
|
||||
| Bundle size | ~300 kB minified (~100 kB gzip) |
|
||||
| npm package | `recharts` |
|
||||
|
||||
**Summary:** The most popular React-native chart library. Built entirely on React + D3 scale functions. Renders SVG. Excellent composable API — charts are built by composing components like `<LineChart>`, `<XAxis>`, `<Tooltip>`, `<Legend>`. Integrates naturally with React state and Ant Design theming. The component model makes recharts easy to reason about for TypeScript developers.
|
||||
|
||||
**Strengths:** Most widely used React chart library; MIT; great docs; composable; TypeScript native; no imperative API.
|
||||
**Weaknesses:** SVG rendering can be slow with very large datasets (>10,000 points); 3D charts not supported; less powerful than ECharts for complex visualizations.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 echarts-for-react (Apache ECharts + React wrapper)
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT (wrapper) + Apache 2.0 (ECharts core) |
|
||||
| npm weekly downloads | ~300,000 (`echarts-for-react`) |
|
||||
| React compatibility | React 16–18 |
|
||||
| Chart types | 20+ types including heatmap, graph, globe (3D), parallel coordinates, custom series |
|
||||
| TypeScript support | Yes (ECharts has full types; wrapper has partial types) |
|
||||
| Bundle size | ECharts core ~900 kB minified (~300 kB gzip); tree-shaking available |
|
||||
| npm package | `echarts` + `echarts-for-react` |
|
||||
|
||||
**Summary:** Apache ECharts is the most powerful open-source chart library in existence, originally developed by Baidu. It has extraordinary chart type coverage, excellent Chinese locale support (because it was built for Chinese products), and a robust option API. The `echarts-for-react` wrapper bridges it to React via an imperative `ref`-based API.
|
||||
|
||||
**Strengths:** Excellent Chinese locale and i18n; widest chart type coverage; Canvas rendering for large datasets; Apache 2.0 license; very popular in Chinese enterprise projects.
|
||||
**Weaknesses:** Option API is JSON-based (not React-idiomatic); integration with React state requires care; large bundle (mitigated by tree-shaking `echarts/core`); the wrapper library is less maintained than the core.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 chart.js + react-chartjs-2
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~3,000,000 (chart.js); ~1,500,000 (react-chartjs-2) |
|
||||
| React compatibility | React 16–18 |
|
||||
| Chart types | Line, Bar, Radar, Doughnut, Pie, Polar Area, Bubble, Scatter |
|
||||
| TypeScript support | Yes — Chart.js ships full types |
|
||||
| Bundle size | Chart.js ~200 kB minified (~60 kB gzip) |
|
||||
| npm package | `chart.js` + `react-chartjs-2` |
|
||||
|
||||
**Summary:** Chart.js is a Canvas-based charting library popular for its simplicity. `react-chartjs-2` wraps it with React lifecycle management. While not as powerful as ECharts, it covers the most common chart types with a simpler configuration API. Good performance for moderate dataset sizes.
|
||||
|
||||
**Strengths:** Simple API; good performance; MIT; widely documented.
|
||||
**Weaknesses:** Chart.js v4 has a somewhat verbose configuration; not as React-idiomatic as recharts; fewer chart types than ECharts.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 victory
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| License | MIT |
|
||||
| npm weekly downloads | ~200,000 |
|
||||
| React compatibility | React 16–18 |
|
||||
| Chart types | Line, Bar, Area, Pie, Scatter, Histogram, Candlestick, Voronoi |
|
||||
| TypeScript support | Yes — bundled types |
|
||||
| Bundle size | ~350 kB minified (~110 kB gzip) |
|
||||
| npm package | `victory` |
|
||||
|
||||
**Summary:** Maintained by Formidable Labs. Victory uses a fully composable React component model (similar to recharts) and has strong support for animations via `victory-animation`. It also has a React Native version (`victory-native`), useful for cross-platform projects. However, it has a smaller community than recharts and is noticeably larger in bundle size for similar functionality.
|
||||
|
||||
**Strengths:** Composable API; React Native compatible; good animation support; MIT.
|
||||
**Weaknesses:** Smaller community than recharts; heavier bundle; fewer chart types than ECharts.
|
||||
|
||||
---
|
||||
|
||||
### Chart Library Recommendation Summary
|
||||
|
||||
| Library | License | Downloads | Recommended |
|
||||
|---------|---------|-----------|-------------|
|
||||
| recharts | MIT | ~4.5M/wk | **Yes — primary choice** |
|
||||
| echarts-for-react | MIT/Apache 2.0 | ~300k/wk | Yes — for complex/large-data charts |
|
||||
| chart.js + react-chartjs-2 | MIT | ~1.5M/wk | Acceptable alternative |
|
||||
| victory | MIT | ~200k/wk | No (heavier, smaller community) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Complete PM Solutions
|
||||
|
||||
### 4.1 plane.so
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| License | AGPL 3.0 (self-hosted); SaaS commercial |
|
||||
| Type | **Standalone web application** — not an embeddable component |
|
||||
| Tech stack | Next.js + Django + PostgreSQL |
|
||||
| Features | Issues, Cycles (sprints), Modules, Gantt view, Kanban, Spreadsheet view, Pages |
|
||||
| Embeddable? | No. The entire product is a web app. The front-end codebase can be studied for patterns but cannot be imported as a component library. |
|
||||
|
||||
**Assessment:** Plane is a full-featured GitHub/Linear alternative with excellent UI design. It could replace the custom dashboard entirely if the team is willing to self-host. However, integrating its Gantt/Kanban components into an existing React app is not practical — the codebase is tightly coupled to Next.js and the Plane API. AGPL license means any modifications to the server-side code must be open-sourced.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 focalboard (Mattermost)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| License | AGPL 3.0 / MIT (personal edition) |
|
||||
| Type | **Standalone web application** — not embeddable |
|
||||
| Tech stack | React + Go (Golang) |
|
||||
| Features | Kanban, Table, Gallery, Calendar views; Mattermost integration |
|
||||
| Embeddable? | No. Focalboard is designed as a standalone app or Mattermost plugin. |
|
||||
|
||||
**Assessment:** Focalboard is a Notion/Trello-like project management tool built by Mattermost. It was archived/deprecated as a standalone project in 2023 when Mattermost integrated its functionality into the core product. Not recommended for new projects as a standalone tool.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 taiga
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| License | AGPL 3.0 (self-hosted); SaaS commercial |
|
||||
| Type | **Standalone web application** — not embeddable |
|
||||
| Tech stack | Angular (front-end) + Django (back-end) + PostgreSQL |
|
||||
| Features | Scrum, Kanban, Backlog, Sprints, Wiki, Issues |
|
||||
| Embeddable? | No. Taiga is a complete Angular application. |
|
||||
|
||||
**Assessment:** Taiga is a mature, battle-tested project management platform. The Angular front-end makes it entirely unusable as a React component. The back-end REST API could theoretically be used to drive a custom React UI, but this would be a complete reimplementation. Not relevant to this project.
|
||||
|
||||
---
|
||||
|
||||
### Complete PM Solutions Summary
|
||||
|
||||
All three evaluated solutions (Plane, Focalboard, Taiga) are **standalone web applications** and are **not embeddable as React component libraries**. They can serve as reference implementations for UX design patterns, but for embedding Gantt/Kanban functionality inside an existing React application, dedicated component libraries must be used.
|
||||
|
||||
---
|
||||
|
||||
## Final Recommendations
|
||||
|
||||
| Category | Selected Library | Reason |
|
||||
|----------|-----------------|--------|
|
||||
| Gantt | `gantt-task-react` | MIT, ~150k downloads/wk, TypeScript native, easiest React integration |
|
||||
| Kanban | `@hello-pangea/dnd` | Maintained fork of react-beautiful-dnd, Apache 2.0, smooth animations |
|
||||
| Charts | `recharts` | Most popular React chart library, MIT, composable, TypeScript native |
|
||||
| Charts (supplemental) | `echarts-for-react` | For data-heavy charts where recharts performance is insufficient |
|
||||
@ -1,430 +0,0 @@
|
||||
# Technology Decision Document — 糖心v2.0 Operations Dashboard
|
||||
|
||||
> Date: 2026-04-02
|
||||
> Based on: research.md in this directory
|
||||
|
||||
---
|
||||
|
||||
## Selected Stack
|
||||
|
||||
| Concern | Choice | Version | Reason |
|
||||
|---------|--------|---------|--------|
|
||||
| Framework | React | 18.x | Concurrent Mode, latest hooks API |
|
||||
| Language | TypeScript | 5.x | Type safety for complex data schemas |
|
||||
| Build tool | Vite | 5.x | Fastest HMR, ESM-native, excellent TS support |
|
||||
| UI library | Ant Design | 5.x | Design system with dark theme token API, rich component set |
|
||||
| Package manager | pnpm | 8.x | Faster installs, strict hoisting, disk-efficient |
|
||||
| Gantt | gantt-task-react | ^2.x | MIT, ~150k weekly downloads, TypeScript bundled, React 18 compatible |
|
||||
| Kanban | @hello-pangea/dnd | ^16.x | Maintained fork of react-beautiful-dnd, Apache 2.0, smooth animations |
|
||||
| Charts | recharts | ^2.x | Most popular React chart library, MIT, composable component API |
|
||||
| Routing | react-router-dom | v6.x | Declarative routing, loader API, nested routes |
|
||||
|
||||
### Gantt: gantt-task-react — Decision Rationale
|
||||
|
||||
`gantt-task-react` is the clear choice among React Gantt libraries:
|
||||
- MIT license with no commercial restrictions
|
||||
- ~150,000 weekly npm downloads (10x the nearest competitor)
|
||||
- Ships TypeScript definitions (`Task`, `ViewMode`, `GanttProps`)
|
||||
- Pure React component — no imperative DOM manipulation
|
||||
- Supports bar click/double-click events, drag-to-resize, view mode switching
|
||||
- Chinese text works with any UTF-8 string in the `name` field
|
||||
- The `locale` prop accepts a custom locale object for date formatting in Chinese
|
||||
|
||||
The only alternative seriously considered was `dhtmlxGantt`, which has better features but requires a paid commercial license for closed-source projects.
|
||||
|
||||
### Kanban: @hello-pangea/dnd — Decision Rationale
|
||||
|
||||
`@hello-pangea/dnd` is the maintained community continuation of Atlassian's `react-beautiful-dnd`, which Atlassian deprecated in 2023:
|
||||
- Apache 2.0 license
|
||||
- ~700,000 weekly downloads (actively growing)
|
||||
- Full React 18 + Concurrent Mode support
|
||||
- Drop-in replacement API — extensive docs and examples available
|
||||
- Physics-based drag animation is the smoothest among all evaluated options
|
||||
- Accessible by default (keyboard and screen reader support)
|
||||
|
||||
`@dnd-kit` was considered as an alternative. It has higher raw download counts and MIT license, but requires more boilerplate to construct a Kanban board (no high-level column/item abstraction). `@hello-pangea/dnd` is the better choice when the goal is a standard Kanban board without unusual drag interactions.
|
||||
|
||||
### Charts: recharts — Decision Rationale
|
||||
|
||||
Recharts is the most popular React chart library with ~4.5M weekly downloads:
|
||||
- MIT license
|
||||
- Fully declarative component API (no `useEffect` / imperative calls)
|
||||
- TypeScript definitions bundled
|
||||
- Supports all required chart types: Bar, Line, Area, Pie, Progress indicators
|
||||
- Integrates naturally with React state and Ant Design color tokens
|
||||
- ~100 kB gzip (acceptable)
|
||||
|
||||
`echarts-for-react` is retained as a supplemental option if a chart type or dataset size exceeds recharts' capabilities (e.g., heatmaps, large time-series data).
|
||||
|
||||
---
|
||||
|
||||
## Project Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── pages/
|
||||
│ ├── GanttPage.tsx # Gantt chart view (schedule.json)
|
||||
│ ├── KanbanPage.tsx # Kanban board view (tasks.json)
|
||||
│ └── ProgressPage.tsx # Daily progress table (daily.json)
|
||||
├── components/
|
||||
│ ├── layout/
|
||||
│ │ ├── AppLayout.tsx # Ant Design Layout wrapper with dark sidebar
|
||||
│ │ └── PageHeader.tsx # Consistent page title + toolbar
|
||||
│ ├── gantt/
|
||||
│ │ └── MemberGantt.tsx # Wrapper around gantt-task-react
|
||||
│ ├── kanban/
|
||||
│ │ ├── KanbanBoard.tsx # DragDropContext + board grid
|
||||
│ │ ├── KanbanColumn.tsx # Droppable column
|
||||
│ │ └── KanbanCard.tsx # Draggable task card
|
||||
│ ├── progress/
|
||||
│ │ ├── DayAccordion.tsx # Collapsible day section
|
||||
│ │ └── ProgressRow.tsx # Single member progress row
|
||||
│ └── shared/
|
||||
│ ├── StatusBadge.tsx # done / doing / todo / blocked badge
|
||||
│ └── PriorityTag.tsx # P0 / P1 / P2 tag
|
||||
├── hooks/
|
||||
│ ├── useSchedule.ts # Load + parse schedule.json
|
||||
│ ├── useTasks.ts # Load + parse tasks.json
|
||||
│ └── useDaily.ts # Load + parse daily.json
|
||||
├── types/
|
||||
│ ├── schedule.ts # TypeScript interfaces for schedule.json
|
||||
│ ├── tasks.ts # TypeScript interfaces for tasks.json
|
||||
│ └── daily.ts # TypeScript interfaces for daily.json
|
||||
├── theme/
|
||||
│ └── antd-theme.ts # Ant Design 5 dark theme token config
|
||||
├── App.tsx # Router + layout root
|
||||
└── main.tsx # Vite entry point
|
||||
|
||||
public/
|
||||
└── data/
|
||||
├── schedule.json
|
||||
├── tasks.json
|
||||
└── daily.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Schema — TypeScript Interfaces
|
||||
|
||||
### schedule.json (`src/types/schedule.ts`)
|
||||
|
||||
```typescript
|
||||
/** Status of a single task bar */
|
||||
export type TaskStatus = 'done' | 'doing' | 'todo' | 'blocked';
|
||||
|
||||
/** A single task assigned to a team member */
|
||||
export interface MemberTask {
|
||||
name: string; // Task display name (Chinese)
|
||||
start: string; // ISO date string "YYYY-MM-DD"
|
||||
end: string; // ISO date string "YYYY-MM-DD"
|
||||
status: TaskStatus;
|
||||
}
|
||||
|
||||
/** A team member with their role and task list */
|
||||
export interface TeamMember {
|
||||
name: string; // Member display name (Chinese)
|
||||
role: string; // Role label e.g. "后端A", "iOS"
|
||||
tasks: MemberTask[];
|
||||
}
|
||||
|
||||
/** Root shape of schedule.json */
|
||||
export interface ProjectSchedule {
|
||||
project: string; // Project name e.g. "糖心v2.0"
|
||||
startDate: string; // ISO date string "YYYY-MM-DD"
|
||||
endDate: string; // ISO date string "YYYY-MM-DD"
|
||||
members: TeamMember[];
|
||||
}
|
||||
```
|
||||
|
||||
### tasks.json (`src/types/tasks.ts`)
|
||||
|
||||
```typescript
|
||||
/** Priority level for a Kanban task */
|
||||
export type Priority = 'P0' | 'P1' | 'P2';
|
||||
|
||||
/** Column names matching the columns array in tasks.json */
|
||||
export type KanbanColumnName = '待开发' | '进行中' | '已完成' | '阻塞';
|
||||
|
||||
/** A single Kanban task card */
|
||||
export interface KanbanTask {
|
||||
title: string; // Task card title (Chinese)
|
||||
assignee: string; // Assignee name (Chinese)
|
||||
column: KanbanColumnName; // Which column this card belongs to
|
||||
priority: Priority; // Priority level
|
||||
day: string; // Schedule day label e.g. "Day2", "TBD"
|
||||
note?: string; // Optional blocking note
|
||||
}
|
||||
|
||||
/** Root shape of tasks.json */
|
||||
export interface KanbanData {
|
||||
columns: KanbanColumnName[]; // Ordered column definitions
|
||||
tasks: KanbanTask[];
|
||||
}
|
||||
```
|
||||
|
||||
### daily.json (`src/types/daily.ts`)
|
||||
|
||||
```typescript
|
||||
/** Progress status for a single member on a single day */
|
||||
export type ProgressStatus = 'done' | 'in_progress' | 'todo' | 'blocked';
|
||||
|
||||
/** One member's planned vs. actual progress for a day */
|
||||
export interface DayProgress {
|
||||
name: string; // Member name (Chinese)
|
||||
planned: string; // What was planned (Chinese description)
|
||||
actual: string; // What was actually done (may be empty for future days)
|
||||
status: ProgressStatus;
|
||||
}
|
||||
|
||||
/** All progress data for a single working day */
|
||||
export interface WorkDay {
|
||||
date: string; // ISO date string "YYYY-MM-DD"
|
||||
day: string; // Display label e.g. "Day 1"
|
||||
weekday: string; // Chinese weekday e.g. "周三"
|
||||
progress: DayProgress[]; // One entry per team member
|
||||
notes: string; // Daily notes / blockers (may be empty)
|
||||
}
|
||||
|
||||
/** Root shape of daily.json */
|
||||
export interface DailyReport {
|
||||
days: WorkDay[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theme Configuration
|
||||
|
||||
### Ant Design 5 Dark Theme Tokens (`src/theme/antd-theme.ts`)
|
||||
|
||||
```typescript
|
||||
import type { ThemeConfig } from 'antd';
|
||||
|
||||
export const darkTheme: ThemeConfig = {
|
||||
token: {
|
||||
// Color palette
|
||||
colorPrimary: '#6366f1', // Indigo accent
|
||||
colorBgBase: '#0f0f1a', // Page background
|
||||
colorBgContainer: '#161627', // Card / panel background
|
||||
colorBgElevated: '#1e1e35', // Dropdown / modal background
|
||||
colorBorder: '#2a2a4a', // Dividers and borders
|
||||
colorText: '#e2e8f0', // Primary text
|
||||
colorTextSecondary: '#94a3b8', // Secondary / muted text
|
||||
colorTextDisabled: '#4a4a6a', // Disabled state text
|
||||
|
||||
// Typography
|
||||
fontFamily: "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
|
||||
fontSize: 14,
|
||||
borderRadius: 8,
|
||||
|
||||
// Shadows
|
||||
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.4)',
|
||||
boxShadowSecondary: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
components: {
|
||||
Table: {
|
||||
headerBg: '#1e1e35',
|
||||
rowHoverBg: '#1e1e35',
|
||||
},
|
||||
Card: {
|
||||
colorBgContainer: '#161627',
|
||||
},
|
||||
Layout: {
|
||||
siderBg: '#0f0f1a',
|
||||
headerBg: '#0f0f1a',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#0f0f1a',
|
||||
darkItemSelectedBg: '#6366f1',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Status Color Constants
|
||||
|
||||
```typescript
|
||||
/** CSS color values for task status indicators */
|
||||
export const STATUS_COLORS: Record<string, string> = {
|
||||
done: '#22c55e', // Green
|
||||
doing: '#f59e0b', // Amber
|
||||
in_progress: '#f59e0b', // Amber (alias)
|
||||
todo: '#6b7280', // Gray
|
||||
blocked: '#ef4444', // Red
|
||||
} as const;
|
||||
|
||||
/** Priority badge colors */
|
||||
export const PRIORITY_COLORS: Record<string, string> = {
|
||||
P0: '#ef4444', // Red — critical
|
||||
P1: '#f59e0b', // Amber — high
|
||||
P2: '#6b7280', // Gray — normal
|
||||
} as const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Hooks Pattern
|
||||
|
||||
### `useSchedule` hook (`src/hooks/useSchedule.ts`)
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { ProjectSchedule } from '../types/schedule';
|
||||
|
||||
interface UseScheduleResult {
|
||||
data: ProjectSchedule | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useSchedule(): UseScheduleResult {
|
||||
const [data, setData] = useState<ProjectSchedule | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/schedule.json')
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json() as Promise<ProjectSchedule>;
|
||||
})
|
||||
.then((json) => {
|
||||
setData(json);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(`Failed to load schedule: ${message}`);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { data, loading, error };
|
||||
}
|
||||
```
|
||||
|
||||
The same pattern applies to `useTasks` (loads `tasks.json` → `KanbanData`) and `useDaily` (loads `daily.json` → `DailyReport`).
|
||||
|
||||
---
|
||||
|
||||
## Routing Structure
|
||||
|
||||
```typescript
|
||||
// App.tsx — react-router-dom v6
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { AppLayout } from './components/layout/AppLayout';
|
||||
import { GanttPage } from './pages/GanttPage';
|
||||
import { KanbanPage } from './pages/KanbanPage';
|
||||
import { ProgressPage } from './pages/ProgressPage';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{ index: true, element: <GanttPage /> },
|
||||
{ path: 'gantt', element: <GanttPage /> },
|
||||
{ path: 'kanban', element: <KanbanPage /> },
|
||||
{ path: 'progress', element: <ProgressPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency List (package.json excerpt)
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"antd": "^5.21.0",
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"gantt-task-react": "^0.3.9",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"recharts": "^2.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.0",
|
||||
"@vitejs/plugin-react": "^4.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Implementation Notes
|
||||
|
||||
### gantt-task-react Integration
|
||||
|
||||
The `schedule.json` data must be transformed from the project's `MemberTask[]` format into `gantt-task-react`'s `Task[]` format before rendering:
|
||||
|
||||
```typescript
|
||||
import type { Task } from 'gantt-task-react';
|
||||
import type { TeamMember } from '../types/schedule';
|
||||
|
||||
export function memberToGanttTasks(member: TeamMember): Task[] {
|
||||
return member.tasks.map((task, index) => ({
|
||||
id: `${member.name}-${index}`,
|
||||
name: task.name,
|
||||
start: new Date(task.start),
|
||||
end: new Date(task.end + 'T23:59:59'), // inclusive end date
|
||||
type: 'task',
|
||||
progress: task.status === 'done' ? 100 : task.status === 'doing' ? 50 : 0,
|
||||
isDisabled: false,
|
||||
styles: {
|
||||
progressColor: STATUS_COLORS[task.status],
|
||||
progressSelectedColor: STATUS_COLORS[task.status],
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### @hello-pangea/dnd Kanban Column Grouping
|
||||
|
||||
The `tasks.json` tasks array must be grouped by `column` before rendering. Use an immutable transformation:
|
||||
|
||||
```typescript
|
||||
import type { KanbanTask, KanbanColumnName } from '../types/tasks';
|
||||
|
||||
export function groupTasksByColumn(
|
||||
columns: KanbanColumnName[],
|
||||
tasks: KanbanTask[]
|
||||
): Record<KanbanColumnName, KanbanTask[]> {
|
||||
const initial = Object.fromEntries(
|
||||
columns.map((col) => [col, [] as KanbanTask[]])
|
||||
) as Record<KanbanColumnName, KanbanTask[]>;
|
||||
|
||||
return tasks.reduce((acc, task) => ({
|
||||
...acc,
|
||||
[task.column]: [...acc[task.column], task],
|
||||
}), initial);
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Custom Properties for gantt-task-react Theming
|
||||
|
||||
`gantt-task-react` uses CSS class names that can be overridden via a stylesheet:
|
||||
|
||||
```css
|
||||
/* Override gantt-task-react default colors to match dark theme */
|
||||
.gantt-task-react-grid-row:nth-child(even) {
|
||||
fill: #161627;
|
||||
}
|
||||
.gantt-task-react-grid-row:nth-child(odd) {
|
||||
fill: #0f0f1a;
|
||||
}
|
||||
.gantt-task-react-tick {
|
||||
stroke: #2a2a4a;
|
||||
}
|
||||
```
|
||||
@ -1,285 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>甘特图 · 糖心v2.0</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --surface: #161b22; --surface2: #21262d;
|
||||
--border: #30363d; --text: #e6edf3; --text2: #8b949e; --text3: #6e7681;
|
||||
--done: #3fb950; --doing: #58a6ff; --todo: #6e7681; --blocked: #f85149;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
||||
|
||||
.nav {
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
height: 52px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 20px; position: sticky; top: 0; z-index: 200;
|
||||
}
|
||||
.nav-brand { font-weight: 600; font-size: 15px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-link {
|
||||
color: var(--text2); text-decoration: none; padding: 6px 14px;
|
||||
border-radius: 6px; font-size: 13px; transition: all .15s;
|
||||
}
|
||||
.nav-link:hover { background: var(--surface2); color: var(--text); }
|
||||
.nav-link.active { color: var(--doing); background: rgba(88,166,255,.1); }
|
||||
|
||||
.main { padding: 20px; }
|
||||
.page-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; }
|
||||
|
||||
.stats { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.stat {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 6px 14px; font-size: 12px; color: var(--text2);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.sv { font-size: 17px; font-weight: 700; }
|
||||
.stat.s-done .sv { color: var(--done); }
|
||||
.stat.s-doing .sv { color: var(--doing); }
|
||||
.stat.s-todo .sv { color: var(--text3); }
|
||||
.stat.s-blocked .sv { color: var(--blocked); }
|
||||
|
||||
.legend {
|
||||
display: flex; gap: 16px; margin-bottom: 14px; flex-wrap: wrap; align-items: center;
|
||||
font-size: 12px; color: var(--text2);
|
||||
}
|
||||
.leg { display: flex; align-items: center; gap: 6px; }
|
||||
.ld { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }
|
||||
.ld.done { background: rgba(63,185,80,.75); }
|
||||
.ld.doing { background: rgba(88,166,255,.75); }
|
||||
.ld.todo { background: rgba(110,118,129,.35); border: 1px solid rgba(110,118,129,.4); }
|
||||
.ld.blocked { background: rgba(248,81,73,.75); }
|
||||
.today-badge {
|
||||
margin-left: auto; display: flex; align-items: center; gap: 6px;
|
||||
color: rgba(88,166,255,.85);
|
||||
}
|
||||
.today-badge::before {
|
||||
content: ''; display: inline-block; width: 2px; height: 14px;
|
||||
background: rgba(88,166,255,.6); border-radius: 2px;
|
||||
}
|
||||
|
||||
.gantt-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 10px; }
|
||||
.gantt-table { border-collapse: collapse; }
|
||||
|
||||
.gantt-table thead th {
|
||||
position: sticky; top: 52px; background: var(--surface2); z-index: 40;
|
||||
font-weight: 500; font-size: 11px; color: var(--text3);
|
||||
height: 34px; border-bottom: 1px solid var(--border); white-space: nowrap;
|
||||
}
|
||||
.corner-th {
|
||||
position: sticky !important; left: 0 !important; top: 52px !important;
|
||||
z-index: 100 !important; background: var(--surface2) !important;
|
||||
min-width: 88px; max-width: 88px; width: 88px;
|
||||
text-align: left; padding: 0 12px;
|
||||
border-right: 1px solid var(--border); font-size: 12px; color: var(--text3);
|
||||
}
|
||||
.day-th { width: 32px; min-width: 32px; max-width: 32px; text-align: center; padding: 0; }
|
||||
.day-th.wkd { opacity: .4; }
|
||||
.day-th.wb { border-left: 1px solid rgba(48,54,61,.9); }
|
||||
|
||||
.member-td {
|
||||
position: sticky; left: 0; background: var(--surface); z-index: 10;
|
||||
min-width: 88px; max-width: 88px; width: 88px;
|
||||
padding: 0 12px; font-size: 13px; font-weight: 500; white-space: nowrap;
|
||||
border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
|
||||
height: 52px; vertical-align: middle;
|
||||
}
|
||||
.member-role { font-size: 10px; color: var(--text3); font-weight: 400; display: block; margin-top: 1px; }
|
||||
.tasks-td { border-bottom: 1px solid var(--border); padding: 0; vertical-align: top; }
|
||||
|
||||
.tasks-canvas { position: relative; height: 52px; }
|
||||
.wkd-bg { position: absolute; top: 0; bottom: 0; width: 32px; background: rgba(255,255,255,.018); pointer-events: none; }
|
||||
.today-line {
|
||||
position: absolute; top: 0; bottom: 0; width: 2px;
|
||||
background: rgba(88,166,255,.5); z-index: 3; pointer-events: none;
|
||||
}
|
||||
.today-lbl {
|
||||
position: absolute; top: 2px; left: 4px;
|
||||
font-size: 9px; color: rgba(88,166,255,.8); white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-bar {
|
||||
position: absolute; top: 10px; height: 32px; border-radius: 5px;
|
||||
display: flex; align-items: center; padding: 0 8px;
|
||||
font-size: 11px; font-weight: 500; overflow: hidden; white-space: nowrap;
|
||||
cursor: default; transition: filter .12s; z-index: 1;
|
||||
}
|
||||
.task-bar:hover { filter: brightness(1.3); z-index: 5; }
|
||||
.task-bar.s-done { background: rgba(63,185,80,.62); color: #d4f0da; }
|
||||
.task-bar.s-doing { background: rgba(88,166,255,.62); color: #d8ecff; }
|
||||
.task-bar.s-todo { background: rgba(110,118,129,.25); color: var(--text2); border: 1px solid rgba(110,118,129,.22); }
|
||||
.task-bar.s-blocked { background: rgba(248,81,73,.62); color: #ffd8d6; }
|
||||
|
||||
#tip {
|
||||
position: fixed; background: #1c2128; color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 10px 14px; font-size: 12px; line-height: 1.8;
|
||||
pointer-events: none; z-index: 9999; opacity: 0;
|
||||
transition: opacity .1s; max-width: 280px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,.5);
|
||||
}
|
||||
.tip-name { font-weight: 600; font-size: 13px; }
|
||||
.tip-meta { color: var(--text2); font-size: 11px; margin-top: 2px; }
|
||||
|
||||
.error-box {
|
||||
margin: 24px; padding: 20px;
|
||||
background: rgba(248,81,73,.08); border: 1px solid rgba(248,81,73,.25);
|
||||
border-radius: 8px; color: var(--text2); line-height: 2;
|
||||
}
|
||||
.error-box code {
|
||||
background: var(--surface2); padding: 2px 6px;
|
||||
border-radius: 4px; color: var(--text); font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main { padding: 12px; }
|
||||
.page-hd { flex-direction: column; align-items: flex-start; }
|
||||
.today-badge { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ 糖心v2.0</span>
|
||||
<div class="nav-links">
|
||||
<a href="gantt.html" class="nav-link active">甘特图</a>
|
||||
<a href="board.html" class="nav-link">看板</a>
|
||||
<a href="progress.html" class="nav-link">进度</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="page-hd">
|
||||
<h1 class="page-title">开发甘特图</h1>
|
||||
<div class="stats" id="stats"></div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span class="leg"><span class="ld done"></span>已完成</span>
|
||||
<span class="leg"><span class="ld doing"></span>进行中</span>
|
||||
<span class="leg"><span class="ld todo"></span>待开发</span>
|
||||
<span class="leg"><span class="ld blocked"></span>阻塞</span>
|
||||
<span class="today-badge">今日 4/2(周四)</span>
|
||||
</div>
|
||||
|
||||
<div class="gantt-wrap" id="gantt-wrap">
|
||||
<table class="gantt-table">
|
||||
<thead><tr id="gantt-head"></tr></thead>
|
||||
<tbody id="gantt-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="tip"></div>
|
||||
|
||||
<script>
|
||||
const START = new Date('2026-04-01');
|
||||
const NDAYS = 32; // 4/1 ~ 5/2 inclusive
|
||||
const DAY_W = 32; // px per day
|
||||
const SL = { done:'✅ 已完成', doing:'🔵 进行中', todo:'⬜ 待开发', blocked:'🔴 阻塞' };
|
||||
|
||||
const dIdx = s => Math.round((new Date(s) - START) / 86400000);
|
||||
const dDur = (s,e) => Math.round((new Date(e)-new Date(s))/86400000)+1;
|
||||
const isWkd = i => { const d=new Date(START); d.setDate(d.getDate()+i); return d.getDay()===0||d.getDay()===6; };
|
||||
const fmtD = i => { const d=new Date(START); d.setDate(d.getDate()+i); return `${d.getMonth()+1}/${d.getDate()}`; };
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const r = await fetch('schedule.json');
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
render(await r.json());
|
||||
} catch(e) {
|
||||
document.getElementById('gantt-wrap').innerHTML =
|
||||
`<div class="error-box">
|
||||
<strong>⚠️ 无法加载 schedule.json</strong><br>
|
||||
请通过本地服务器访问本页面:<br>
|
||||
<code>python3 -m http.server 8080</code> 然后打开 <code>http://localhost:8080/gantt.html</code>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const members = data.members;
|
||||
|
||||
// Stats
|
||||
let cnt = {done:0, doing:0, todo:0, blocked:0};
|
||||
members.forEach(m => m.tasks.forEach(t => (cnt[t.status]!=null ? cnt[t.status]++ : cnt.todo++)));
|
||||
document.getElementById('stats').innerHTML =
|
||||
`<div class="stat s-done"><span class="sv">${cnt.done}</span>已完成</div>
|
||||
<div class="stat s-doing"><span class="sv">${cnt.doing}</span>进行中</div>
|
||||
<div class="stat s-todo"><span class="sv">${cnt.todo}</span>待开发</div>
|
||||
<div class="stat s-blocked"><span class="sv">${cnt.blocked}</span>阻塞</div>`;
|
||||
|
||||
// Date header
|
||||
let hd = `<th class="corner-th">成员</th>`;
|
||||
for (let i = 0; i < NDAYS; i++) {
|
||||
const wb = i > 0 && i % 7 === 0;
|
||||
hd += `<th class="day-th${isWkd(i)?' wkd':''}${wb?' wb':''}">${fmtD(i)}</th>`;
|
||||
}
|
||||
document.getElementById('gantt-head').innerHTML = hd;
|
||||
|
||||
// Rows
|
||||
const todayI = dIdx('2026-04-02');
|
||||
let body = '';
|
||||
|
||||
members.forEach(m => {
|
||||
// Background: weekend shading + today line
|
||||
let bg = '';
|
||||
for (let i = 0; i < NDAYS; i++) {
|
||||
if (isWkd(i)) bg += `<div class="wkd-bg" style="left:${i*DAY_W}px"></div>`;
|
||||
}
|
||||
bg += `<div class="today-line" style="left:${todayI*DAY_W}px"><span class="today-lbl">今</span></div>`;
|
||||
|
||||
// Task bars
|
||||
let bars = '';
|
||||
m.tasks.forEach(t => {
|
||||
const si = dIdx(t.start);
|
||||
if (si < 0 || si >= NDAYS) return;
|
||||
const dur = Math.min(dDur(t.start, t.end), NDAYS - si);
|
||||
const left = si * DAY_W + 2;
|
||||
const width = Math.max(dur * DAY_W - 4, 6);
|
||||
const maxCh = Math.max(0, Math.floor((width - 16) / 6.0));
|
||||
const label = width >= 22 ? (t.name.length > maxCh ? t.name.slice(0, maxCh-1) + '…' : t.name) : '';
|
||||
const tip = encodeURIComponent(
|
||||
[t.name, t.start + (t.start!==t.end?' → '+t.end:''), SL[t.status]||t.status].join('|')
|
||||
);
|
||||
bars += `<div class="task-bar s-${t.status}" style="left:${left}px;width:${width}px" data-tip="${tip}">${label}</div>`;
|
||||
});
|
||||
|
||||
body += `<tr>
|
||||
<td class="member-td">${m.name}<span class="member-role">${m.role}</span></td>
|
||||
<td class="tasks-td" colspan="${NDAYS}">
|
||||
<div class="tasks-canvas" style="width:${NDAYS*DAY_W}px">${bg}${bars}</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
document.getElementById('gantt-body').innerHTML = body;
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
const tipEl = document.getElementById('tip');
|
||||
document.addEventListener('mouseover', e => {
|
||||
const el = e.target.closest('[data-tip]');
|
||||
if (!el) return;
|
||||
const [name, dates, status] = decodeURIComponent(el.dataset.tip).split('|');
|
||||
tipEl.innerHTML = `<div class="tip-name">${name}</div><div class="tip-meta">${dates}<br>${status}</div>`;
|
||||
tipEl.style.opacity = '1';
|
||||
});
|
||||
document.addEventListener('mouseout', e => {
|
||||
if (e.target.closest('[data-tip]')) tipEl.style.opacity = '0';
|
||||
});
|
||||
document.addEventListener('mousemove', e => {
|
||||
tipEl.style.left = Math.min(e.clientX + 14, window.innerWidth - 295) + 'px';
|
||||
tipEl.style.top = Math.max(e.clientY - 55, 60) + 'px';
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,256 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>每日进度 · 糖心v2.0</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --surface: #161b22; --surface2: #21262d;
|
||||
--border: #30363d; --text: #e6edf3; --text2: #8b949e; --text3: #6e7681;
|
||||
--done: #3fb950; --doing: #58a6ff; --todo: #6e7681; --blocked: #f85149;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
||||
|
||||
.nav {
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
height: 52px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 20px; position: sticky; top: 0; z-index: 200;
|
||||
}
|
||||
.nav-brand { font-weight: 600; font-size: 15px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-link {
|
||||
color: var(--text2); text-decoration: none; padding: 6px 14px;
|
||||
border-radius: 6px; font-size: 13px; transition: all .15s;
|
||||
}
|
||||
.nav-link:hover { background: var(--surface2); color: var(--text); }
|
||||
.nav-link.active { color: var(--doing); background: rgba(88,166,255,.1); }
|
||||
|
||||
.main { padding: 20px; max-width: 1200px; margin: 0 auto; }
|
||||
.page-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; }
|
||||
|
||||
/* Summary bar */
|
||||
.summary-bar {
|
||||
display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap;
|
||||
}
|
||||
.sum-card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||||
padding: 14px 20px; flex: 1; min-width: 140px;
|
||||
}
|
||||
.sum-label { font-size: 12px; color: var(--text2); margin-bottom: 4px; }
|
||||
.sum-val { font-size: 22px; font-weight: 700; }
|
||||
.sum-card.c-done .sum-val { color: var(--done); }
|
||||
.sum-card.c-doing .sum-val { color: var(--doing); }
|
||||
.sum-card.c-todo .sum-val { color: var(--text3); }
|
||||
.sum-card.c-blocked .sum-val { color: var(--blocked); }
|
||||
|
||||
/* Day card */
|
||||
.day-card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||||
margin-bottom: 16px; overflow: hidden;
|
||||
}
|
||||
.day-hd {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.day-info { display: flex; align-items: baseline; gap: 10px; }
|
||||
.day-label { font-size: 15px; font-weight: 700; }
|
||||
.day-date { font-size: 13px; color: var(--text2); }
|
||||
.day-badge {
|
||||
font-size: 12px; padding: 3px 10px; border-radius: 20px; font-weight: 500;
|
||||
}
|
||||
.day-badge.all-done { background: rgba(63,185,80,.15); color: var(--done); border: 1px solid rgba(63,185,80,.2); }
|
||||
.day-badge.in-prog { background: rgba(88,166,255,.12); color: var(--doing); border: 1px solid rgba(88,166,255,.2); }
|
||||
.day-badge.planned { background: rgba(110,118,129,.12); color: var(--text3); border: 1px solid rgba(110,118,129,.2); }
|
||||
.day-badge.blocked { background: rgba(248,81,73,.12); color: var(--blocked); border: 1px solid rgba(248,81,73,.2); }
|
||||
|
||||
/* Progress table */
|
||||
.progress-table { width: 100%; border-collapse: collapse; }
|
||||
.progress-table th {
|
||||
text-align: left; padding: 10px 16px; font-size: 11px; font-weight: 600;
|
||||
color: var(--text3); text-transform: uppercase; letter-spacing: .5px;
|
||||
background: rgba(255,255,255,.02); border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.progress-table td {
|
||||
padding: 12px 16px; font-size: 13px; border-bottom: 1px solid rgba(48,54,61,.6);
|
||||
vertical-align: top; line-height: 1.5;
|
||||
}
|
||||
.progress-table tr:last-child td { border-bottom: none; }
|
||||
.progress-table tr:hover td { background: rgba(255,255,255,.02); }
|
||||
|
||||
.m-name { font-weight: 600; white-space: nowrap; width: 64px; color: var(--text); }
|
||||
.m-role { font-size: 10px; color: var(--text3); font-weight: 400; display: block; }
|
||||
.actual-cell { color: var(--text2); }
|
||||
.actual-cell.has-val { color: var(--text); }
|
||||
|
||||
/* Status badge */
|
||||
.status {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-size: 12px; font-weight: 500; white-space: nowrap;
|
||||
}
|
||||
.status.done { color: var(--done); }
|
||||
.status.in_progress { color: var(--doing); }
|
||||
.status.todo { color: var(--text3); }
|
||||
.status.blocked { color: var(--blocked); }
|
||||
|
||||
/* Notes */
|
||||
.day-notes {
|
||||
padding: 10px 20px; font-size: 12px; color: var(--text2);
|
||||
border-top: 1px solid var(--border); background: rgba(255,255,255,.018);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Col widths */
|
||||
.col-name { width: 80px; }
|
||||
.col-plan { width: 35%; }
|
||||
.col-actual { }
|
||||
.col-status { width: 96px; }
|
||||
|
||||
.error-box {
|
||||
padding: 24px; background: rgba(248,81,73,.08);
|
||||
border: 1px solid rgba(248,81,73,.25); border-radius: 8px;
|
||||
color: var(--text2); line-height: 2;
|
||||
}
|
||||
.error-box code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; color: var(--text); font-size: 12px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main { padding: 12px; }
|
||||
.progress-table th, .progress-table td { padding: 8px 10px; }
|
||||
.col-plan, .col-actual { display: none; }
|
||||
.progress-table .plan-col, .progress-table .actual-col { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ 糖心v2.0</span>
|
||||
<div class="nav-links">
|
||||
<a href="gantt.html" class="nav-link">甘特图</a>
|
||||
<a href="board.html" class="nav-link">看板</a>
|
||||
<a href="progress.html" class="nav-link active">进度</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="page-hd">
|
||||
<h1 class="page-title">每日进度汇总</h1>
|
||||
<div id="date-range" style="font-size:13px;color:var(--text2)"></div>
|
||||
</div>
|
||||
<div class="summary-bar" id="summary-bar"></div>
|
||||
<div id="progress-list"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const STATUS_CFG = {
|
||||
done: { icon: '✅', label: '完成', cls: 'done' },
|
||||
in_progress: { icon: '⚠️', label: '进行中', cls: 'in_progress' },
|
||||
todo: { icon: '⏳', label: '计划中', cls: 'todo' },
|
||||
blocked: { icon: '❌', label: '阻塞', cls: 'blocked' },
|
||||
};
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const r = await fetch('daily.json');
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
render(await r.json());
|
||||
} catch(e) {
|
||||
document.getElementById('progress-list').innerHTML =
|
||||
`<div class="error-box">
|
||||
<strong>⚠️ 无法加载 daily.json</strong><br>
|
||||
请通过本地服务器访问:<br>
|
||||
<code>python3 -m http.server 8080</code> 然后打开 <code>http://localhost:8080/progress.html</code>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const days = data.days;
|
||||
|
||||
// Summary counts
|
||||
let total = 0, done = 0, inProg = 0, blocked = 0, todo = 0;
|
||||
days.forEach(day => {
|
||||
day.progress.forEach(p => {
|
||||
total++;
|
||||
if (p.status === 'done') done++;
|
||||
else if (p.status === 'in_progress') inProg++;
|
||||
else if (p.status === 'blocked') blocked++;
|
||||
else todo++;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('date-range').textContent =
|
||||
`${days[0].date} — ${days[days.length-1].date}(共${days.length}天)`;
|
||||
|
||||
document.getElementById('summary-bar').innerHTML =
|
||||
`<div class="sum-card c-done"><div class="sum-label">已完成</div><div class="sum-val">${done}</div></div>
|
||||
<div class="sum-card c-doing"><div class="sum-label">进行中</div><div class="sum-val">${inProg}</div></div>
|
||||
<div class="sum-card c-todo"><div class="sum-label">计划中</div><div class="sum-val">${todo}</div></div>
|
||||
<div class="sum-card c-blocked"><div class="sum-label">阻塞</div><div class="sum-val">${blocked}</div></div>`;
|
||||
|
||||
// Day cards
|
||||
let html = '';
|
||||
days.forEach(day => {
|
||||
const doneC = day.progress.filter(p => p.status === 'done').length;
|
||||
const inProgC = day.progress.filter(p => p.status === 'in_progress').length;
|
||||
const blockedC = day.progress.filter(p => p.status === 'blocked').length;
|
||||
const total = day.progress.length;
|
||||
const allDone = doneC === total;
|
||||
const hasBlocked = blockedC > 0;
|
||||
|
||||
let badgeCls = 'planned', badgeTxt = '计划中';
|
||||
if (allDone) { badgeCls = 'all-done'; badgeTxt = `🎉 全员完成`; }
|
||||
else if (hasBlocked){ badgeCls = 'blocked'; badgeTxt = `⛔ 有阻塞`; }
|
||||
else if (inProgC > 0){ badgeCls = 'in-prog'; badgeTxt = `${doneC}/${total} 完成`; }
|
||||
|
||||
const rows = day.progress.map(p => {
|
||||
const sc = STATUS_CFG[p.status] || STATUS_CFG.todo;
|
||||
const hasActual = p.actual && p.actual.trim();
|
||||
return `<tr>
|
||||
<td class="m-name col-name">${p.name}</td>
|
||||
<td class="plan-col col-plan">${p.planned}</td>
|
||||
<td class="actual-col ${hasActual ? 'has-val' : 'actual-cell'} col-actual">${hasActual ? p.actual : '—'}</td>
|
||||
<td class="col-status"><span class="status ${sc.cls}">${sc.icon} ${sc.label}</span></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const notesHTML = day.notes
|
||||
? `<div class="day-notes">📌 ${day.notes}</div>` : '';
|
||||
|
||||
html += `
|
||||
<div class="day-card">
|
||||
<div class="day-hd">
|
||||
<div class="day-info">
|
||||
<span class="day-label">${day.day}</span>
|
||||
<span class="day-date">${day.date} ${day.weekday}</span>
|
||||
</div>
|
||||
<span class="day-badge ${badgeCls}">${badgeTxt}</span>
|
||||
</div>
|
||||
<table class="progress-table">
|
||||
<colgroup>
|
||||
<col class="col-name"><col class="col-plan"><col class="col-actual"><col class="col-status">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>成员</th>
|
||||
<th class="plan-col">计划任务</th>
|
||||
<th class="actual-col">实际完成</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
${notesHTML}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
document.getElementById('progress-list').innerHTML = html;
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,131 +0,0 @@
|
||||
{
|
||||
"project": "糖心v2.0",
|
||||
"startDate": "2026-04-01",
|
||||
"endDate": "2026-05-02",
|
||||
"members": [
|
||||
{
|
||||
"name": "明诚", "role": "后端A",
|
||||
"tasks": [
|
||||
{"name": "项目初始化+注册登录", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "第三方登录+钱包基础", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "媒体上传处理", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "内容发布+标签", "start": "2026-04-04", "end": "2026-04-04", "status": "todo"},
|
||||
{"name": "联调+Bug修复", "start": "2026-04-06", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "充值核心", "start": "2026-04-07", "end": "2026-04-07", "status": "todo"},
|
||||
{"name": "苹果IAP+Feed", "start": "2026-04-08", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "内容详情+互动+订阅", "start": "2026-04-09", "end": "2026-04-09", "status": "todo"},
|
||||
{"name": "购买订阅+创作者主页", "start": "2026-04-10", "end": "2026-04-10", "status": "todo"},
|
||||
{"name": "关注+CDFlow对接", "start": "2026-04-11", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "用户信息+收款方式", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "PPV核心+注销", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "提现+拉黑", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "创作者内容管理+会话", "start": "2026-04-17", "end": "2026-04-17", "status": "todo"},
|
||||
{"name": "第3周全流程联调", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "第4周联调+Bug修复", "start": "2026-04-22", "end": "2026-04-25", "status": "todo"},
|
||||
{"name": "压测+上线部署", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "青枫", "role": "后端B",
|
||||
"tasks": [
|
||||
{"name": "后台初始化+登录认证", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "管理员管理", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "角色管理", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "内容审核(上)", "start": "2026-04-04", "end": "2026-04-04", "status": "todo"},
|
||||
{"name": "内容审核(下)+举报", "start": "2026-04-06", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "用户管理", "start": "2026-04-07", "end": "2026-04-07", "status": "todo"},
|
||||
{"name": "财务管理(上)", "start": "2026-04-08", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "财务管理(中)", "start": "2026-04-09", "end": "2026-04-09", "status": "todo"},
|
||||
{"name": "财务管理(下)", "start": "2026-04-10", "end": "2026-04-10", "status": "todo"},
|
||||
{"name": "数据注水", "start": "2026-04-11", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "推荐位+配置(上)", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "配置(中)", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "配置(下)", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "数据统计", "start": "2026-04-17", "end": "2026-04-17", "status": "todo"},
|
||||
{"name": "客服工具+联调", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "支援C端开发", "start": "2026-04-22", "end": "2026-04-28", "status": "todo"},
|
||||
{"name": "灰度发布+修复", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "远舟", "role": "前端A",
|
||||
"tasks": [
|
||||
{"name": "后台项目搭建", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "登录页+Token", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "权限框架", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "管理员管理页", "start": "2026-04-04", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "用户列表+详情", "start": "2026-04-07", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "充值订单+封禁", "start": "2026-04-09", "end": "2026-04-09", "status": "todo"},
|
||||
{"name": "提现审核", "start": "2026-04-10", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "充值档位配置", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "提现+充值联调", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "订阅管理", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "支付通道管理", "start": "2026-04-17", "end": "2026-04-17", "status": "todo"},
|
||||
{"name": "第3周联调", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "后台联调+验收", "start": "2026-04-22", "end": "2026-04-25", "status": "todo"},
|
||||
{"name": "上线准备+发布", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "微冰", "role": "前端B",
|
||||
"tasks": [
|
||||
{"name": "公共组件开发", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "登录页+菜单布局", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "权限组件", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "角色管理页", "start": "2026-04-04", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "内容审核列表", "start": "2026-04-07", "end": "2026-04-07", "status": "todo"},
|
||||
{"name": "审核操作+预览", "start": "2026-04-08", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "创作者审核", "start": "2026-04-09", "end": "2026-04-09", "status": "todo"},
|
||||
{"name": "头像审核", "start": "2026-04-10", "end": "2026-04-10", "status": "todo"},
|
||||
{"name": "联调+审核测试", "start": "2026-04-11", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "标签管理", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "PPV审核", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "平台配置", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "数据看板", "start": "2026-04-17", "end": "2026-04-17", "status": "todo"},
|
||||
{"name": "联调+回归", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "后台优化+联调", "start": "2026-04-22", "end": "2026-04-25", "status": "todo"},
|
||||
{"name": "上线准备+发布", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "徐天杰", "role": "iOS",
|
||||
"tasks": [
|
||||
{"name": "iOS工程搭建", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "登录注册页面", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "苹果登录对接", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "登录联调+Tab框架", "start": "2026-04-04", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "广场Feed瀑布流", "start": "2026-04-07", "end": "2026-04-07", "status": "todo"},
|
||||
{"name": "Feed图片加载", "start": "2026-04-08", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "创作者主页", "start": "2026-04-09", "end": "2026-04-10", "status": "todo"},
|
||||
{"name": "Feed联调", "start": "2026-04-11", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "充值页", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "IAP内购", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "订阅购买", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "IM SDK对接", "start": "2026-04-17", "end": "2026-04-17", "status": "blocked"},
|
||||
{"name": "IM联调+PPV", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "功能完善+测试", "start": "2026-04-22", "end": "2026-04-26", "status": "todo"},
|
||||
{"name": "提审+应对审核", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "聂军", "role": "安卓",
|
||||
"tasks": [
|
||||
{"name": "安卓工程搭建", "start": "2026-04-01", "end": "2026-04-01", "status": "done"},
|
||||
{"name": "登录注册页面", "start": "2026-04-02", "end": "2026-04-02", "status": "doing"},
|
||||
{"name": "谷歌登录对接", "start": "2026-04-03", "end": "2026-04-03", "status": "todo"},
|
||||
{"name": "登录联调+Tab框架", "start": "2026-04-04", "end": "2026-04-06", "status": "todo"},
|
||||
{"name": "广场Feed瀑布流", "start": "2026-04-07", "end": "2026-04-07", "status": "todo"},
|
||||
{"name": "Feed图片加载", "start": "2026-04-08", "end": "2026-04-08", "status": "todo"},
|
||||
{"name": "创作者主页", "start": "2026-04-09", "end": "2026-04-10", "status": "todo"},
|
||||
{"name": "Feed联调", "start": "2026-04-11", "end": "2026-04-11", "status": "todo"},
|
||||
{"name": "充值页", "start": "2026-04-14", "end": "2026-04-14", "status": "todo"},
|
||||
{"name": "聚合支付对接", "start": "2026-04-15", "end": "2026-04-15", "status": "todo"},
|
||||
{"name": "订阅购买", "start": "2026-04-16", "end": "2026-04-16", "status": "todo"},
|
||||
{"name": "IM SDK对接", "start": "2026-04-17", "end": "2026-04-17", "status": "todo"},
|
||||
{"name": "IM联调+PPV", "start": "2026-04-18", "end": "2026-04-18", "status": "todo"},
|
||||
{"name": "功能完善+测试", "start": "2026-04-22", "end": "2026-04-26", "status": "todo"},
|
||||
{"name": "发布+线上验证", "start": "2026-04-29", "end": "2026-05-02", "status": "todo"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"columns": ["待开发", "进行中", "已完成", "阻塞"],
|
||||
"tasks": [
|
||||
{"title": "第三方登录+钱包基础", "assignee": "明诚", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "管理员管理(3个接口)", "assignee": "青枫", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "登录页+Token管理", "assignee": "远舟", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "登录页+菜单布局", "assignee": "微冰", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "iOS登录注册页面", "assignee": "徐天杰", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "安卓登录注册页面", "assignee": "聂军", "column": "已完成", "priority": "P0", "day": "Day2"},
|
||||
{"title": "项目初始化+注册登录", "assignee": "明诚", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "后台初始化+登录认证", "assignee": "青枫", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "React+AntDesign搭建", "assignee": "远舟", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "公共组件开发", "assignee": "微冰", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "iOS工程搭建", "assignee": "徐天杰", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "安卓工程搭建", "assignee": "聂军", "column": "已完成", "priority": "P0", "day": "Day1"},
|
||||
{"title": "CDFlow IM对接", "assignee": "徐天杰", "column": "阻塞", "priority": "P1", "day": "TBD", "note": "401错误,光明明天看"},
|
||||
{"title": "媒体上传处理", "assignee": "明诚", "column": "待开发", "priority": "P0", "day": "Day3"},
|
||||
{"title": "角色管理(5个接口)", "assignee": "青枫", "column": "待开发", "priority": "P0", "day": "Day3"},
|
||||
{"title": "苹果登录对接", "assignee": "徐天杰", "column": "待开发", "priority": "P0", "day": "Day3"},
|
||||
{"title": "谷歌登录对接", "assignee": "聂军", "column": "待开发", "priority": "P0", "day": "Day3"}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user