feat: 新增项目中心——按项目组织任务/任务群/总群并一键推送

- 新表 projects / project_group_chats,partners 加 partner_type/wecom_userid,tasks 加 project_id(迁移 0010)
- 项目 CRUD、总群成员配置、任务群人工映射(群主→合作方)、任务挂项目
- 推送:总群成员逐人应用消息 + 任务群按群主企业群发,结果分项回显
This commit is contained in:
ABAPPLO
2026-08-31 16:35:17 +08:00
parent ed13262166
commit a6f5a5fd4a
11 changed files with 2557 additions and 15 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,17 @@ import {
getAccountFieldRefreshStatus,
startAccountFieldRefresh,
} from "../../../lib/account-field-refresh-service";
import {
deleteProject,
getProjectDetail,
listGroupOwnerPartners,
listProjects,
ProjectError,
saveGroupOwnerPartner,
saveProject,
setProjectGroupChats,
} from "../../../lib/project-service";
import { pushProjectTask } from "../../../lib/project-push-service";
type ActionBody = {
action?: string;
@@ -119,6 +130,7 @@ export async function POST(request: Request) {
dueAt,
platform,
contentFormat,
projectId: String(body.projectId ?? "").trim(),
},
env as unknown as FeishuBindings,
);
@@ -131,6 +143,7 @@ export async function POST(request: Request) {
instructions: String(body.instructions ?? "").trim(),
quantity: numberValue(body.quantity),
exampleImageKey: String(body.exampleImageKey ?? "").trim(),
projectId: String(body.projectId ?? "").trim(),
});
} else if (body.action === "claim") {
const partnerId = String(body.partnerId ?? "");
@@ -687,6 +700,59 @@ export async function POST(request: Request) {
const taskId = String(body.taskId ?? "").trim();
const pushes = await listGroupPushes(db, taskId || undefined);
return Response.json({ pushes });
} else if (body.action === "project_list") {
return Response.json({ projects: await listProjects(db) });
} else if (body.action === "project_detail") {
return Response.json(await getProjectDetail(db, String(body.projectId ?? "")));
} else if (body.action === "project_save") {
return Response.json({
project: await saveProject(db, {
id: body.id,
name: body.name,
brand: body.brand,
mainGroupMemberIds: body.mainGroupMemberIds,
note: body.note,
}),
});
} else if (body.action === "project_delete") {
await deleteProject(db, String(body.projectId ?? ""));
return Response.json({ ok: true });
} else if (body.action === "project_set_group_chats") {
const detail = await setProjectGroupChats(
db,
String(body.projectId ?? ""),
Array.isArray(body.items) ? body.items : [],
);
return Response.json(detail);
} else if (body.action === "partner_save") {
return Response.json({
partner: await saveGroupOwnerPartner(db, {
id: body.id,
name: body.name,
wecomUserid: body.wecomUserid,
}),
});
} else if (body.action === "partner_group_owners") {
return Response.json({ partners: await listGroupOwnerPartners(db) });
} else if (body.action === "project_push_task") {
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
const outcome = await pushProjectTask(
db,
{
taskId: body.taskId,
sendMainGroup: body.sendMainGroup,
sendGroupChats: body.sendGroupChats,
text: body.text,
},
wecomConfig,
);
const appFailed = outcome.appResults.filter((item) => !item.ok);
return Response.json({
...outcome,
appSent: outcome.appResults.length - appFailed.length,
appFailedCount: appFailed.length,
hint: "任务群走企业群发,群主需在企微客户端「群发助手」点击发送后,消息才会送达客户群",
});
} else if (body.action === "refresh_account_fields") {
const status = startAccountFieldRefresh(
db,
@@ -711,7 +777,8 @@ export async function POST(request: Request) {
status:
error instanceof FeishuSourceError ||
error instanceof DistributionReleaseError ||
error instanceof WecomClientError
error instanceof WecomClientError ||
error instanceof ProjectError
? error.status
: 500,
},

View File

@@ -5372,3 +5372,261 @@ label small {
animation-iteration-count: 1 !important;
}
}
/* ── 项目中心 ─────────────────────────────────────────── */
.project-hero-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.project-panel {
padding: 18px 20px;
}
.project-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.project-card-title {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.project-card-title h3 {
font-size: 15px;
}
.project-card-title small {
display: block;
margin-top: 3px;
color: #8a968f;
font-size: 11px;
}
.project-card-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.project-note {
margin: 10px 0 0;
color: #7d8a83;
font-size: 11px;
line-height: 1.6;
}
.share-link-button.danger {
color: #c0453e;
}
.project-detail {
margin-top: 14px;
padding-top: 14px;
border-top: 1px dashed #dfe6e1;
display: flex;
flex-direction: column;
gap: 16px;
}
.project-detail-grid {
display: grid;
grid-template-columns: minmax(240px, 0.9fr) 1.4fr;
gap: 16px;
}
.project-detail-block {
min-width: 0;
}
.project-detail-block-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.project-detail-block-head h4 {
font-size: 12px;
}
.project-detail-block-head small {
color: #97a29c;
font-size: 10px;
}
.project-member-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.project-member-list code {
padding: 3px 8px;
border-radius: 6px;
background: #eef2ef;
color: #556861;
font-size: 11px;
}
.project-empty-line {
color: #9aa39f;
font-size: 11px;
}
.project-chat-table,
.project-task-table {
font-size: 11px;
}
.project-chat-missing td {
color: #b0685f;
background: #fdf6f5;
}
.project-chat-missing td:first-child {
text-decoration: line-through;
}
.project-push-bar {
display: flex;
align-items: center;
gap: 10px;
}
.project-push-bar small {
color: #97a29c;
font-size: 11px;
}
.project-chat-hint {
margin: 0 0 10px;
color: #7d8a83;
font-size: 11px;
line-height: 1.7;
}
.project-chat-edit-row {
display: flex;
align-items: center;
gap: 8px;
}
.project-chat-edit-row .group-push-row {
flex: 1;
min-width: 0;
}
.project-chat-edit-row select {
max-width: 220px;
font-size: 11px;
}
.project-delete-warning {
margin: 0;
padding: 10px 12px;
border-radius: 9px;
background: #fdf6f5;
color: #a2564e;
font-size: 11px;
line-height: 1.7;
}
.project-push-targets {
display: flex;
flex-direction: column;
gap: 8px;
margin: 10px 0;
}
.project-push-target {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border: 1px solid #e2e8e4;
border-radius: 9px;
background: #fbfcfb;
}
.project-push-target b {
display: block;
font-size: 12px;
}
.project-push-target small {
display: block;
margin-top: 2px;
color: #8a968f;
font-size: 10px;
line-height: 1.6;
}
/* ── 合作方类型 ───────────────────────────────────────── */
.partner-type-tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.partner-type-tabs button {
padding: 5px 12px;
border: 1px solid #dfe6e1;
border-radius: 999px;
background: #fbfcfb;
color: #687773;
font-size: 11px;
cursor: pointer;
}
.partner-type-tabs button.active {
border-color: #205843;
background: #205843;
color: #fff;
}
.partner-type-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
border-radius: 999px;
font-size: 10px;
font-style: normal;
vertical-align: 1px;
}
.partner-type-badge.group-owner {
background: #e7f2ec;
color: #1f6b4a;
}
.partner-type-badge.external-koc {
background: #eef1f6;
color: #4c5f85;
}
.partner-owner-hint {
color: #9aa39f;
font-size: 10px;
}
@media (max-width: 900px) {
.project-detail-grid {
grid-template-columns: 1fr;
}
.project-hero-actions {
flex-direction: column;
}
}