feat: 新增项目中心——按项目组织任务/任务群/总群并一键推送
- 新表 projects / project_group_chats,partners 加 partner_type/wecom_userid,tasks 加 project_id(迁移 0010) - 项目 CRUD、总群成员配置、任务群人工映射(群主→合作方)、任务挂项目 - 推送:总群成员逐人应用消息 + 任务群按群主企业群发,结果分项回显
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
},
|
||||
|
||||
258
app/globals.css
258
app/globals.css
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user