feat: 项目层级树+合作方绑定KOC资源+客户群全量可选+精简编辑表单,移除复盘报表

- 项目支持父子层级:树形折叠展示、详情面板内联管理总群成员、推送聚合子项目任务、环检测与子项目删除守卫
- 外部KOC合作方绑定 KOC 资源账号(唯一占用、换绑/解绑、资源页一键设为合作方),列表直显联系方式
- 绑定客户群改为全量外部群缓存(含各群主),同步按钮强刷;member_count 缺失以 member_list 兜底
- 任务/项目编辑模式隐藏不可变字段(平台/内容类型/品牌/总群成员),详情面板维护成员时保留 parent_id
- 删除复盘报表整栏(导航/页面/task_report_list/导出路由/CSV lib 及测试)
This commit is contained in:
ABAPPLO
2026-09-23 11:13:17 +08:00
parent 8f1ebf7697
commit ab78b187c0
20 changed files with 1354 additions and 958 deletions

View File

@@ -375,6 +375,11 @@ export async function ensureSchema(database?: DatabaseClient) {
"wecom_external_user_id",
"wecom_external_user_id TEXT",
);
await ensureColumn(
"partners",
"account_id",
"account_id TEXT NOT NULL DEFAULT ''",
);
await ensureColumn(
"users",
"wecom_userid",
@@ -902,7 +907,17 @@ export async function getDashboardData(ownerUserid?: string) {
const [partnersResult, tasksResult, accountsResult, distributionsResult] =
await Promise.all([
ownerFilter === null
? db.prepare("SELECT * FROM partners ORDER BY created_at DESC").all()
? db
.prepare(
`SELECT p.*, a.nickname AS account_nickname,
a.public_account_id AS account_public_id,
a.current_contact AS account_contact,
a.profile_url AS account_profile_url
FROM partners p
LEFT JOIN accounts a ON a.id = p.account_id
ORDER BY p.created_at DESC`,
)
.all()
: emptyResult(),
ownerFilter
? db
@@ -960,97 +975,6 @@ export async function getDashboardData(ownerUserid?: string) {
};
}
export type TaskReportRow = {
id: string;
name: string;
task_type: string;
brand: string;
platform: string;
status: string;
due_at: string;
created_at: string;
quantity: number;
project_name: string | null;
claimed_count: number;
published_count: number;
completed_count: number;
exposure_total: number | null;
views_total: number | null;
d2_likes: number | null;
d2_comments: number | null;
d2_collects: number | null;
d5_likes: number | null;
d5_comments: number | null;
d5_collects: number | null;
d7_likes: number | null;
d7_comments: number | null;
d7_collects: number | null;
latest_likes: number | null;
latest_comments: number | null;
latest_collects: number | null;
latest_shares: number | null;
related_task_id: string | null;
related_task_name: string | null;
reverse_related_task_id: string | null;
reverse_related_task_name: string | null;
submitted_count: number;
};
export async function getTaskReportData(db: DatabaseClient, ownerUserid?: string) {
// undefined=全量;""=未绑定企微的空范围(不能绑定 '',否则会匹配 owner_userid='' 的任务)
if (ownerUserid !== undefined && String(ownerUserid).trim() === "") {
return [] as TaskReportRow[];
}
const ownerFilter = String(ownerUserid ?? "").trim();
const base = `SELECT
t.id,
t.name,
t.task_type,
t.brand,
t.platform,
t.status,
t.due_at,
t.created_at,
t.quantity,
t.related_task_id,
rt.name AS related_task_name,
(SELECT rrt.id FROM tasks rrt WHERE rrt.related_task_id = t.id LIMIT 1) AS reverse_related_task_id,
(SELECT rrt.name FROM tasks rrt WHERE rrt.related_task_id = t.id LIMIT 1) AS reverse_related_task_name,
p.name AS project_name,
COUNT(d.id) AS claimed_count,
SUM(CASE WHEN TRIM(COALESCE(d.publish_url, '')) != '' THEN 1 ELSE 0 END) AS published_count,
SUM(CASE WHEN d.status = 'complete' THEN 1 ELSE 0 END) AS completed_count,
SUM(CASE WHEN d.result_submitted_at IS NOT NULL THEN 1 ELSE 0 END) AS submitted_count,
SUM(d.exposure) AS exposure_total,
SUM(d.views) AS views_total,
SUM(d.d2_likes) AS d2_likes,
SUM(d.d2_comments) AS d2_comments,
SUM(d.d2_collects) AS d2_collects,
SUM(d.d5_likes) AS d5_likes,
SUM(d.d5_comments) AS d5_comments,
SUM(d.d5_collects) AS d5_collects,
SUM(d.d7_likes) AS d7_likes,
SUM(d.d7_comments) AS d7_comments,
SUM(d.d7_collects) AS d7_collects,
SUM(d.latest_likes) AS latest_likes,
SUM(d.latest_comments) AS latest_comments,
SUM(d.latest_collects) AS latest_collects,
SUM(d.latest_shares) AS latest_shares
FROM tasks t
LEFT JOIN distributions d ON d.task_id = t.id
LEFT JOIN projects p ON p.id = t.project_id
LEFT JOIN tasks rt ON rt.id = t.related_task_id`;
const statement = ownerFilter
? db
.prepare(
`${base} WHERE (t.owner_userid = ? OR p.owner_userid = ?) GROUP BY t.id ORDER BY t.created_at DESC`,
)
.bind(ownerFilter, ownerFilter)
: db.prepare(`${base} GROUP BY t.id ORDER BY t.created_at DESC`);
const result = await statement.all<TaskReportRow>();
return result.results;
}
export function uid(prefix: string) {
return `${prefix}-${crypto.randomUUID().slice(0, 8)}`;
}