性能优化: 项目页/看板/DAG/知识库/交付 加载提速

- /api/tasks 列表接口改用轻量序列化,去掉 output_text/error 大字段
  (300KB -> 6KB, 详情接口 /api/tasks/<id> 仍返回完整数据)
- /api/tasks/trash 与 /api/projects/<id>/dag 同步轻量化
- 前端同一项目内切换 tab 不再重复拉取项目/任务/Worker/事件,
  复用 projCtx 缓存; 离开项目页时自动清缓存
- /api/projects/<id>/events 各来源加 LIMIT, 避免全表加载
This commit is contained in:
2026-08-16 19:26:13 +08:00
parent 7c4cbf8994
commit e5d664fb98
3 changed files with 31 additions and 7 deletions
+9 -6
View File
@@ -708,7 +708,7 @@ def tasks():
visible = _visible_project_ids() visible = _visible_project_ids()
if visible is not None: if visible is not None:
rows = [r for r in rows if r['project_id'] in visible] rows = [r for r in rows if r['project_id'] in visible]
return jsonify({'ok': True, 'data': [db.serialize_task(t) for t in rows]}) return jsonify({'ok': True, 'data': [db.serialize_task_light(t) for t in rows]})
@app.route('/api/tasks/<int:tid>', methods=['GET', 'PUT', 'DELETE']) @app.route('/api/tasks/<int:tid>', methods=['GET', 'PUT', 'DELETE'])
@@ -779,7 +779,7 @@ def tasks_trash():
visible = _visible_project_ids() visible = _visible_project_ids()
if visible is not None: if visible is not None:
rows = [r for r in rows if r['project_id'] in visible] rows = [r for r in rows if r['project_id'] in visible]
return jsonify({'ok': True, 'data': [db.serialize_task(t) for t in rows]}) return jsonify({'ok': True, 'data': [db.serialize_task_light(t) for t in rows]})
@app.route('/api/tasks/<int:tid>/trash', methods=['POST']) @app.route('/api/tasks/<int:tid>/trash', methods=['POST'])
@@ -1104,18 +1104,21 @@ def project_events(pid):
limit = min(int(request.args.get('limit', 200)), 500) limit = min(int(request.args.get('limit', 200)), 500)
rows = [] rows = []
# AI 主管动态 # AI 主管动态
for r in db.q('SELECT * FROM project_logs WHERE project_id=?', (pid,)): for r in db.q('SELECT * FROM project_logs WHERE project_id=? ORDER BY id DESC LIMIT ?',
(pid, limit)):
rows.append({'id': f'p{r["id"]}', 'kind': 'autolog', 'level': r['level'], rows.append({'id': f'p{r["id"]}', 'kind': 'autolog', 'level': r['level'],
'message': r['message'], 'created_at': r['created_at'], 'task_id': None, 'message': r['message'], 'created_at': r['created_at'], 'task_id': None,
'task_title': None}) 'task_title': None})
# 任务日志(带任务标题,可定位) # 任务日志(带任务标题,可定位)
for r in db.q('SELECT l.*, t.title AS task_title FROM task_logs l ' for r in db.q('SELECT l.*, t.title AS task_title FROM task_logs l '
'JOIN tasks t ON t.id=l.task_id WHERE t.project_id=? AND t.deleted=0', (pid,)): 'JOIN tasks t ON t.id=l.task_id WHERE t.project_id=? AND t.deleted=0 '
'ORDER BY l.id DESC LIMIT ?', (pid, limit)):
rows.append({'id': f'l{r["id"]}', 'kind': 'task', 'level': r['level'], rows.append({'id': f'l{r["id"]}', 'kind': 'task', 'level': r['level'],
'message': r['message'], 'created_at': r['created_at'], 'message': r['message'], 'created_at': r['created_at'],
'task_id': r['task_id'], 'task_title': r['task_title']}) 'task_id': r['task_id'], 'task_title': r['task_title']})
# 交付记录 # 交付记录
for r in db.q('SELECT * FROM project_deliverables WHERE project_id=?', (pid,)): for r in db.q('SELECT * FROM project_deliverables WHERE project_id=? ORDER BY id DESC LIMIT ?',
(pid, limit)):
rows.append({'id': f'd{r["id"]}', 'kind': 'deliver', 'level': 'success', rows.append({'id': f'd{r["id"]}', 'kind': 'deliver', 'level': 'success',
'message': f'📦 {r["note"]}{r["name"]}', 'created_at': r['created_at'], 'message': f'📦 {r["note"]}{r["name"]}', 'created_at': r['created_at'],
'task_id': None, 'task_title': None}) 'task_id': None, 'task_title': None})
@@ -1275,7 +1278,7 @@ def project_dag(pid):
rows = db.q('SELECT * FROM tasks WHERE project_id=? AND deleted=0 ORDER BY id', (pid,)) rows = db.q('SELECT * FROM tasks WHERE project_id=? AND deleted=0 ORDER BY id', (pid,))
nodes, edges, id_map = [], [], {} nodes, edges, id_map = [], [], {}
for t in rows: for t in rows:
node = db.serialize_task(t) node = db.serialize_task_light(t)
nodes.append(node) nodes.append(node)
id_map[t['id']] = node id_map[t['id']] = node
for t in nodes: for t in nodes:
+10
View File
@@ -631,6 +631,16 @@ def serialize_task(t):
return t return t
def serialize_task_light(t):
"""轻量序列化(列表接口用):去掉大字段 output_text / error
看板、DAG、回收站等列表视图不需要它们,可显著减小传输体积。
详情接口 /api/tasks/<id> 仍返回完整数据。"""
t = serialize_task(t)
t.pop('output_text', None)
t.pop('error', None)
return t
def get_setting(key, default=''): def get_setting(key, default=''):
r = q('SELECT value FROM settings WHERE key=?', (key,), one=True) r = q('SELECT value FROM settings WHERE key=?', (key,), one=True)
return r['value'] if r else default return r['value'] if r else default
+12 -1
View File
@@ -100,6 +100,8 @@ function router() {
const hash = location.hash.replace(/^#\//, '') || 'dashboard'; const hash = location.hash.replace(/^#\//, '') || 'dashboard';
const parts = hash.split('/'); const parts = hash.split('/');
const name = parts[0]; const name = parts[0];
// 离开项目页时丢弃项目缓存,避免下次进入用旧数据
if (name !== 'project') projCtx = null;
const fn = routes[name] || pageDashboard; const fn = routes[name] || pageDashboard;
const navMap = {project:'projects', api:'api', settings:'settings', alerts:'alerts'}; const navMap = {project:'projects', api:'api', settings:'settings', alerts:'alerts'};
$$('#sidebar nav a').forEach(a => a.classList.toggle('active', a.dataset.route === (navMap[name] || name))); $$('#sidebar nav a').forEach(a => a.classList.toggle('active', a.dataset.route === (navMap[name] || name)));
@@ -274,11 +276,20 @@ let projCtx = null; // {pid, p, tasks, workers, tab, autoLogs}
let projAutoTimer = null; let projAutoTimer = null;
async function pageProject([pid, tab]) { async function pageProject([pid, tab]) {
tab = tab || 'kanban';
// 同一项目内切 tab:复用已加载的数据,不再重新拉项目/任务/Worker/事件
if (projCtx && String(projCtx.pid) === String(pid)) {
projCtx.tab = tab;
renderProjectShell();
renderActiveTab();
startProjAutoPoll(); // 重启轮询(router 已清掉定时器)
return;
}
const p = (await api(`/api/projects/${pid}`)).data; const p = (await api(`/api/projects/${pid}`)).data;
const tasks = (await api(`/api/tasks?project_id=${pid}`)).data; const tasks = (await api(`/api/tasks?project_id=${pid}`)).data;
const workers = (await api('/api/workers')).data; const workers = (await api('/api/workers')).data;
const events = (await api(`/api/projects/${pid}/events`)).data; const events = (await api(`/api/projects/${pid}/events`)).data;
projCtx = {pid, p, tasks, workers, tab: tab || 'kanban', events}; projCtx = {pid, p, tasks, workers, tab, events};
renderProjectShell(); renderProjectShell();
renderActiveTab(); renderActiveTab();
startProjAutoPoll(); startProjAutoPoll();