feat: 操作日志系统 + 日期修复 + 协同去重 + 主题持久化 + 计划自动完成

本次累积提交包含以下功能:

1. 修复:el-date-picker 缺失 value-format 导致日期保存偏移(8处)
2. 新增:协同拜访 visit_group_id 去重机制(完整副本 + 周报合并 + 亮灯去重)
3. 新增:计划自动完成(创建计划检测已有拜访 / 编辑拜访触发)
4. 新增:拜访表单选择客户后自动预填过期计划内容
5. 新增:移动端主题切换按钮 + 后端持久化
6. 新增:操作日志系统 (audit_logs),全覆盖 8 个模块 CRUD
7. 新增:PC端「我的数据」支局长/领导视角客户经理列

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-30 12:01:14 +08:00
parent 9b5805447e
commit 2187be7aeb
35 changed files with 839 additions and 80 deletions
+46 -24
View File
@@ -28,7 +28,8 @@ async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = No
today = date.today()
visits_count = (await db.execute(
select(func.count(Visit.id)).where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
select(func.count(func.distinct(func.coalesce(Visit.visit_group_id, Visit.id))))
.where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
)).scalar() or 0
plans_count = (await db.execute(
@@ -171,31 +172,52 @@ async def get_weekly_report(
visits_result = await db.execute(visit_query)
visits = visits_result.scalars().all()
visits_data = []
# Group visits by visit_group_id (or id for solo visits)
groups: dict[str, list] = {}
for v in visits:
# Resolve companion names: system users → names, external → direct
companion_names_resolved = [user_map.get(c, str(c)) for c in (v.companions or [])]
companion_names_resolved.extend(v.companion_names or [])
visits_data.append({
"id": str(v.id),
"customer_id": str(v.customer_id),
"customer_name": customer_map.get(v.customer_id, ""),
"visit_date": str(v.visit_date),
"visit_method": v.visit_method,
"time_range": v.time_range,
"visitor_name": v.visitor_name or "",
"visitor_phone": v.visitor_phone or "",
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"companions": [str(c) for c in (v.companions or [])],
"companion_names": v.companion_names or [],
gid = str(v.visit_group_id) if v.visit_group_id else str(v.id)
groups.setdefault(gid, []).append(v)
visits_data = []
for gid, gvisits in groups.items():
# Determine the "primary" — first record in group (the creator's)
primary = gvisits[0]
companion_names_resolved = [user_map.get(c, str(c)) for c in (primary.companions or [])]
companion_names_resolved.extend(primary.companion_names or [])
merged = {
"id": str(primary.id),
"customer_id": str(primary.customer_id),
"customer_name": customer_map.get(primary.customer_id, ""),
"visit_date": str(primary.visit_date),
"visit_method": primary.visit_method,
"time_range": primary.time_range,
"visitor_name": primary.visitor_name or "",
"visitor_phone": primary.visitor_phone or "",
"visit_group_id": gid if len(gvisits) > 1 else None,
"companions": [str(c) for c in (primary.companions or [])],
"companion_names": primary.companion_names or [],
"companion_names_resolved": companion_names_resolved,
"photos": v.photos or [],
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"edit_log": v.edit_log or [],
"created_at": str(v.created_at),
})
"communication_content": primary.communication_content,
"customer_demand": primary.customer_demand,
"photos": primary.photos or [],
"manager_id": str(primary.manager_id),
"manager_name": user_map.get(primary.manager_id, ""),
"edit_log": primary.edit_log or [],
"created_at": str(primary.created_at),
"participants": [
{
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"role": "primary" if v is primary else "companion",
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"photos": v.photos or [],
}
for v in gvisits
],
}
visits_data.append(merged)
# ── Work Plans ──
wp_query = select(WorkPlan)