feat: 任务树QC按审核记录分组(同任务多轮复审=组节点)+rev:轮次叶子详情(MdWidget渲染审核说明/改进意见)
This commit is contained in:
parent
7039c4980e
commit
475ea79f6c
@ -27,11 +27,74 @@ if not raw_id:
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# ── 步骤/交付件节点:先按自身查,查到就展示;查不到回落任务视图 ──
|
||||
# ── 步骤/交付件/QC审核轮次节点:先按自身查,查到就展示;查不到回落任务视图 ──
|
||||
task_id = raw_id
|
||||
step_focus = ''
|
||||
del_focus = ''
|
||||
if raw_id.startswith('step:'):
|
||||
if raw_id.startswith('rev:'):
|
||||
rid = raw_id[4:]
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, task_id, qc_type, round, reviewer, fit_score, pass_score, passed, "
|
||||
"improvement, comments, created_at FROM bid_qc_reviews WHERE id=${r}$", {"r": rid})
|
||||
if recs:
|
||||
rv = recs[0]
|
||||
passed = str(getattr(rv, 'passed', '0')) == '1'
|
||||
sc = float(getattr(rv, 'fit_score', 0) or 0)
|
||||
ps = float(getattr(rv, 'pass_score', 0) or 0)
|
||||
task_id = getattr(rv, 'task_id', '') or ''
|
||||
subwidgets = [
|
||||
{"widgettype": "Text",
|
||||
"options": {"text": "第%s轮 %s QC 契合度审核" % (
|
||||
getattr(rv, 'round', ''), getattr(rv, 'qc_type', '') or ''),
|
||||
"halign": "left", "cfontsize": 1.1,
|
||||
"style": {"fontWeight": "bold", "color": "#1e293b"}}},
|
||||
{"widgettype": "HBox", "options": {"gap": "12px", "width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {
|
||||
"text": ("%.1f / %.1f %s" % (sc, ps, '→ 通过' if passed else '→ 未通过')),
|
||||
"cfontsize": 0.85, "color": "#ffffff", "padding": "1px 10px",
|
||||
"style": {"background": '#16a34a' if passed else '#dc2626',
|
||||
"borderRadius": "12px", "fontWeight": "bold"}}},
|
||||
{"widgettype": "Text", "options": {
|
||||
"text": "审核: " + (getattr(rv, 'reviewer', '') or ''),
|
||||
"cfontsize": 0.75, "color": "#94a3b8"}},
|
||||
{"widgettype": "Text", "options": {
|
||||
"text": str(getattr(rv, 'created_at', '') or '')[:16],
|
||||
"cfontsize": 0.75, "color": "#94a3b8"}},
|
||||
{"widgettype": "Filler"},
|
||||
]},
|
||||
{"widgettype": "Text",
|
||||
"options": {"text": "\u2500\u2500 审核说明 \u2500\u2500", "cfontsize": 0.85,
|
||||
"style": {"fontWeight": "bold", "color": "#8b5cf6", "marginTop": "8px"}}},
|
||||
{"widgettype": "VScrollPanel",
|
||||
"options": {"css": "filler", "width": "100%", "minHeight": "100px",
|
||||
"maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff",
|
||||
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
|
||||
"subwidgets": [{"widgettype": "MdWidget",
|
||||
"options": {"mdtext": getattr(rv, 'comments', '') or '(无)',
|
||||
"width": "100%"}}]},
|
||||
]
|
||||
imp = getattr(rv, 'improvement', '') or ''
|
||||
if imp:
|
||||
subwidgets.append({"widgettype": "Text",
|
||||
"options": {"text": "\u2500\u2500 改进意见 \u2500\u2500", "cfontsize": 0.85,
|
||||
"style": {"fontWeight": "bold", "color": "#b45309", "marginTop": "8px"}}})
|
||||
subwidgets.append({"widgettype": "VScrollPanel",
|
||||
"options": {"css": "filler", "width": "100%", "minHeight": "100px",
|
||||
"maxHeight": "360px", "padding": "6px 12px", "bgcolor": "#ffffff",
|
||||
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
|
||||
"subwidgets": [{"widgettype": "MdWidget",
|
||||
"options": {"mdtext": imp, "width": "100%"}}]})
|
||||
subwidgets.append({"widgettype": "Text",
|
||||
"options": {"text": "(以下为该轮审核所属任务详情)", "cfontsize": 0.75,
|
||||
"color": "#94a3b8", "marginTop": "8px"}})
|
||||
return json.dumps({
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "padding": "12px", "gap": "6px", "overflowY": "auto"},
|
||||
"subwidgets": subwidgets,
|
||||
}, ensure_ascii=False)
|
||||
task_id = raw_id
|
||||
elif raw_id.startswith('step:'):
|
||||
sid = raw_id[5:]
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, task_id, step_name, display_name, step_type, state, error_msg, "
|
||||
|
||||
@ -150,28 +150,19 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
role_tasks.append(t)
|
||||
|
||||
# ── 维度/类型分组 ──
|
||||
if role in ('agent.tender_analyst', 'agent.qc'):
|
||||
if role == 'agent.tender_analyst':
|
||||
groups = {}
|
||||
for t in role_tasks:
|
||||
if role == 'agent.tender_analyst':
|
||||
key = (_params_of(t).get('analysis_dim') or '').strip() or '__legacy__'
|
||||
else:
|
||||
key = qc_map.get(getattr(t, 'id', ''), ('', 0))[0] or '__none__'
|
||||
key = (_params_of(t).get('analysis_dim') or '').strip() or '__legacy__'
|
||||
groups.setdefault(key, []).append(t)
|
||||
for key in groups:
|
||||
groups[key].sort(key=lambda x: str(getattr(x, 'created_at', '') or ''))
|
||||
|
||||
dim_labels = {
|
||||
'scoring': '评分项与得分规则', 'quals': '资质清单与要求',
|
||||
'reqs_outline': '投标文件要求与章节骨架', 'cost_benefit': '成本收益分析',
|
||||
'__legacy__': '全量解析(维度拆分前)',
|
||||
'scoring_items': '评分项 QC', 'qualifications': '资质 QC',
|
||||
'doc_requirements': '投标文件要求 QC', 'chapter_outline': '章节骨架 QC',
|
||||
'__none__': 'QC 审核',
|
||||
}
|
||||
order = ['scoring', 'quals', 'reqs_outline', 'cost_benefit', '__legacy__',
|
||||
'scoring_items', 'qualifications', 'doc_requirements',
|
||||
'chapter_outline', '__none__']
|
||||
order = ['scoring', 'quals', 'reqs_outline', 'cost_benefit', '__legacy__']
|
||||
nodes = []
|
||||
for key in order + [k for k in groups if k not in order]:
|
||||
if key not in groups:
|
||||
@ -179,7 +170,6 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
ts = groups[key]
|
||||
label = dim_labels.get(key, key)
|
||||
if len(ts) == 1:
|
||||
# 一次过:叶子节点
|
||||
t = ts[0]
|
||||
st = getattr(t, 'state', '') or ''
|
||||
nodes.append({
|
||||
@ -190,7 +180,6 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
del_cnt.get(getattr(t, 'id', ''), 0) > 0),
|
||||
})
|
||||
else:
|
||||
# 多轮:组节点 + 轮次升序子节点
|
||||
final_state = getattr(ts[-1], 'state', '') or ''
|
||||
nodes.append({
|
||||
"id": "grp:" + role + ":" + key,
|
||||
@ -200,6 +189,66 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
})
|
||||
return json.dumps(nodes, ensure_ascii=False)
|
||||
|
||||
if role == 'agent.qc':
|
||||
# QC 按审核记录分组(同一任务可多轮复审):一次过=叶子,多轮=组节点
|
||||
revs = await sor.sqlExe(
|
||||
"SELECT id, task_id, qc_type, round, fit_score, passed FROM bid_qc_reviews "
|
||||
"WHERE project_id=${p}$ ORDER BY qc_type, round ASC", {"p": pid}) or []
|
||||
groups = {}
|
||||
for rv in revs:
|
||||
groups.setdefault(getattr(rv, 'qc_type', '') or '__none__', []).append(rv)
|
||||
reviewed_tids = set(getattr(rv, 'task_id', '') for rv in revs)
|
||||
legacy = [t for t in role_tasks if getattr(t, 'id', '') not in reviewed_tids]
|
||||
if legacy:
|
||||
groups.setdefault('__legacy__', [])
|
||||
qc_labels = {
|
||||
'scoring_items': '评分项 QC', 'qualifications': '资质 QC',
|
||||
'doc_requirements': '投标文件要求 QC', 'chapter_outline': '章节骨架 QC',
|
||||
'__none__': 'QC 审核', '__legacy__': 'QC 审核(早期)',
|
||||
}
|
||||
order = ['scoring_items', 'qualifications', 'doc_requirements',
|
||||
'chapter_outline', '__none__', '__legacy__']
|
||||
nodes = []
|
||||
for key in order + [k for k in groups if k not in order]:
|
||||
if key not in groups:
|
||||
continue
|
||||
rs = groups[key]
|
||||
label = qc_labels.get(key, key)
|
||||
if legacy and key == '__legacy__':
|
||||
if not rs and legacy:
|
||||
# 只有遗留任务无审核记录:多任务=组,单任务=叶
|
||||
if len(legacy) == 1:
|
||||
t = legacy[0]
|
||||
st = getattr(t, 'state', '') or ''
|
||||
nodes.append({"id": getattr(t, 'id', ''),
|
||||
"label": "{} [{}] {}".format(_state_icons.get(st, '\u2b1c'),
|
||||
_state_zh.get(st, st), label),
|
||||
"is_leaf": True})
|
||||
else:
|
||||
nodes.append({"id": "grp:" + role + ":" + key,
|
||||
"label": "{}({} 个早期任务)".format(label, len(legacy)),
|
||||
"is_leaf": False})
|
||||
continue
|
||||
if len(rs) == 1 and str(getattr(rs[0], 'passed', '0')) == '1':
|
||||
# 一次过:叶子
|
||||
t_id = getattr(rs[0], 'task_id', '')
|
||||
sc = getattr(rs[0], 'fit_score', '')
|
||||
nodes.append({
|
||||
"id": t_id,
|
||||
"label": "\u2705 [通过] {}({:.1f} 分)".format(label, float(sc or 0)),
|
||||
"is_leaf": not (del_cnt.get(t_id, 0) > 0),
|
||||
})
|
||||
else:
|
||||
n_rounds = len(rs) if rs else len(legacy)
|
||||
last_passed = str(getattr(rs[-1], 'passed', '0')) == '1' if rs else False
|
||||
nodes.append({
|
||||
"id": "grp:" + role + ":" + key,
|
||||
"label": "{}({} 轮)最新[{}]".format(label, n_rounds,
|
||||
'通过' if last_passed else '未通过'),
|
||||
"is_leaf": False,
|
||||
})
|
||||
return json.dumps(nodes, ensure_ascii=False)
|
||||
|
||||
# ── 非维度型角色:平铺任务列表(原逻辑)──
|
||||
nodes = []
|
||||
for t in role_tasks:
|
||||
@ -227,23 +276,50 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
qc_map = {}
|
||||
if role == 'agent.qc':
|
||||
for r in await sor.sqlExe(
|
||||
"SELECT task_id, qc_type, round FROM bid_qc_reviews WHERE project_id=${p}$",
|
||||
{"p": pid}) or []:
|
||||
qc_map[getattr(r, 'task_id', '')] = (getattr(r, 'qc_type', '') or '',
|
||||
int(getattr(r, 'round', 0) or 0))
|
||||
# QC 组:按审核记录轮次升序展开(同一任务多轮复审各占一行)
|
||||
revs = await sor.sqlExe(
|
||||
"SELECT id, task_id, qc_type, round, fit_score, passed, created_at "
|
||||
"FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${k}$ "
|
||||
"ORDER BY round ASC", {"p": pid, "k": key}) or []
|
||||
if revs:
|
||||
nodes = []
|
||||
for rv in revs:
|
||||
rnd = int(getattr(rv, 'round', 0) or 0)
|
||||
passed = str(getattr(rv, 'passed', '0')) == '1'
|
||||
sc = float(getattr(rv, 'fit_score', 0) or 0)
|
||||
upd = str(getattr(rv, 'created_at', '') or '')[5:16]
|
||||
icon = '\u2705' if passed else '\U0001f6ab'
|
||||
nodes.append({
|
||||
"id": "rev:" + getattr(rv, 'id', ''),
|
||||
"label": "第%d轮 %s [%s] %.1f 分 %s" % (
|
||||
rnd, icon, '通过' if passed else '未通过', sc, upd),
|
||||
"is_leaf": True,
|
||||
})
|
||||
return json.dumps(nodes, ensure_ascii=False)
|
||||
# 无审核记录的早期 QC 任务:平铺
|
||||
nodes = []
|
||||
for t in all_tasks:
|
||||
if (getattr(t, 'role', '') or '').strip() != role:
|
||||
continue
|
||||
if (getattr(t, 'parent_id', '') or '').strip():
|
||||
continue
|
||||
st = getattr(t, 'state', '') or ''
|
||||
nodes.append({"id": getattr(t, 'id', ''),
|
||||
"label": "{} [{}] {}".format(_state_icons.get(st, '\u2b1c'),
|
||||
_state_zh.get(st, st),
|
||||
str(getattr(t, 'updated_at', '') or '')[5:16]),
|
||||
"is_leaf": not (del_cnt.get(getattr(t, 'id', ''), 0) > 0)})
|
||||
return json.dumps(nodes, ensure_ascii=False)
|
||||
|
||||
# analyst 组:按任务创建序(=轮次升序)展开
|
||||
ts = []
|
||||
for t in all_tasks:
|
||||
if (getattr(t, 'role', '') or '').strip() != role:
|
||||
continue
|
||||
if (getattr(t, 'parent_id', '') or '').strip():
|
||||
continue
|
||||
if role == 'agent.tender_analyst':
|
||||
k = (_params_of2(t).get('analysis_dim') or '').strip() or '__legacy__'
|
||||
else:
|
||||
k = qc_map.get(getattr(t, 'id', ''), ('', 0))[0] or '__none__'
|
||||
k = (_params_of2(t).get('analysis_dim') or '').strip() or '__legacy__'
|
||||
if k == key:
|
||||
ts.append(t)
|
||||
ts.sort(key=lambda x: str(getattr(x, 'created_at', '') or ''))
|
||||
@ -251,12 +327,10 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
for i, t in enumerate(ts):
|
||||
tid = getattr(t, 'id', '')
|
||||
st = getattr(t, 'state', '') or ''
|
||||
rnd = qc_map.get(tid, ('', 0))[1] if role == 'agent.qc' else 0
|
||||
rnd_label = "第%d轮" % (rnd or (i + 1))
|
||||
upd = str(getattr(t, 'updated_at', '') or '')[5:16]
|
||||
nodes.append({
|
||||
"id": tid,
|
||||
"label": "{} {} [{}] {}".format(rnd_label, _state_icons.get(st, '\u2b1c'),
|
||||
"label": "第%d轮 %s [%s] %s" % (i + 1, _state_icons.get(st, '\u2b1c'),
|
||||
_state_zh.get(st, st), upd),
|
||||
"is_leaf": not (step_cnt.get(tid, 0) > 0 or del_cnt.get(tid, 0) > 0),
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user