diff --git a/app/admin-app.tsx b/app/admin-app.tsx index 214bffc..5491764 100644 --- a/app/admin-app.tsx +++ b/app/admin-app.tsx @@ -202,7 +202,6 @@ type FeishuPreview = { type NavKey = | "overview" - | "projects" | "tasks" | "resources" | "partners" @@ -212,7 +211,6 @@ type NavKey = const NAV_ITEMS: Array<{ key: NavKey; label: string; mark: string }> = [ { key: "overview", label: "工作台", mark: "⌂" }, - { key: "projects", label: "项目中心", mark: "▦" }, { key: "tasks", label: "任务中心", mark: "▤" }, { key: "resources", label: "KOC资源", mark: "◎" }, { key: "partners", label: "合作方", mark: "Ⓒ" }, @@ -559,7 +557,6 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { const visibleNavItems = NAV_ITEMS.filter((item) => { if (item.key === "resources" && currentUser.role === "user") return false; if (item.key === "users" && !isManager) return false; - if (item.key === "projects" && !isManager) return false; if (item.key === "reports" && !isManager) return false; return true; }); @@ -668,7 +665,6 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { const navigate = (key: NavKey) => { if (key === "resources" && currentUser.role === "user") return; if (key === "users" && !isManager) return; - if (key === "projects" && !isManager) return; if (key === "reports" && !isManager) return; setActiveNav(key); setMenuOpen(false); @@ -1029,13 +1025,9 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { title: "分发工作台", subtitle: "用最少动作,跑完内容领取、发布与数据回收。", }, - projects: { - title: "项目中心", - subtitle: "按项目组织任务群与总群成员,一键把任务分发到项目各群。", - }, tasks: { title: "任务中心", - subtitle: "从飞书同步内容、开放领取,并按任务跟进每篇笔记的分发进度。", + subtitle: "按项目组织任务群与总群成员,从飞书同步内容、开放领取并跟进分发进度。", }, resources: { title: "KOC资源", @@ -1169,21 +1161,16 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { showResources={currentUser.role !== "user"} /> )} - {activeNav === "projects" && isManager && ( - setTaskModalOpen(true)} - /> - )} {activeNav === "tasks" && ( setTaskModalOpen(true)} onCopyShare={copyTaskShareLink} onPushToGroups={(task) => setGroupPushTask(task)} @@ -1461,7 +1448,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { ))} - 挂在项目下的任务可通过「项目中心」直接推送到项目总群与任务群。 + 挂在项目下的任务可在「任务中心」的项目分组中直接推送到项目总群与任务群。
@@ -2156,7 +2143,7 @@ function PartnersPage({

userid 在企业微信管理后台「通讯录」成员详情中查看。群主合作方是内部员工, - 用于在项目中心把任务群映射到负责人。 + 用于在任务中心的项目分组中把任务群映射到负责人。

@@ -2539,166 +2526,87 @@ type GroupOwnerOption = { wecom_userid: string; }; -function ProjectsPage({ +async function postAction(payload: Record, fallback: string) { + const response = await fetch("/api/action", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const parsed = await readApiResponse(response, fallback); + if (!response.ok) throw new Error(parsed.error || fallback); + return parsed; +} + +type ProjectEditorDraft = { + id: string; + name: string; + brand: string; + members: string; + note: string; +}; + +const EMPTY_PROJECT_DRAFT: ProjectEditorDraft = { id: "", name: "", brand: "", members: "", note: "" }; + +function ProjectSection({ + project, + tasks, + detailVersion, portalUrl, setToast, - onTasksChanged, - onCreateTask, + onProjectsChanged, + onEditProject, + onDeleteProject, + onOpenTaskDetail, + onCopyShare, + onPushToGroups, + onOneClickPush, }: { + project: ProjectSummary; + tasks: Task[]; + detailVersion: number; portalUrl: string; setToast: (message: string) => void; - onTasksChanged: () => Promise; - onCreateTask: () => void; + onProjectsChanged: () => Promise; + onEditProject: (project: ProjectSummary) => void; + onDeleteProject: (project: ProjectSummary) => void; + onOpenTaskDetail: (taskId: string) => void; + onCopyShare: (task: Task) => void; + onPushToGroups: (task: Task) => void; + onOneClickPush: (task: Task) => void; }) { - const [projects, setProjects] = useState([]); - const [loading, setLoading] = useState(true); - const [expandedId, setExpandedId] = useState(null); + const [expanded, setExpanded] = useState(false); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); - const [editorOpen, setEditorOpen] = useState(false); - const [editorSaving, setEditorSaving] = useState(false); - const [editorDraft, setEditorDraft] = useState({ - id: "", - name: "", - brand: "", - members: "", - note: "", - }); const [chatManaging, setChatManaging] = useState(false); const [chatSaving, setChatSaving] = useState(false); const [chatLoading, setChatLoading] = useState(false); const [chatGroups, setChatGroups] = useState([]); const [chatOwners, setChatOwners] = useState([]); const [chatDraft, setChatDraft] = useState>([]); - const [deleteTarget, setDeleteTarget] = useState(null); - const [deleteWorking, setDeleteWorking] = useState(false); const [pushProject, setPushProject] = useState(null); - const postAction = useCallback(async (payload: Record, fallback: string) => { - const response = await fetch("/api/action", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - const parsed = await readApiResponse(response, fallback); - if (!response.ok) throw new Error(parsed.error || fallback); - return parsed; - }, []); - - const loadProjects = useCallback(async () => { - try { - const parsed = await postAction<{ projects?: ProjectSummary[] }>( - { action: "project_list" }, - "读取项目列表失败", - ); - setProjects(parsed.projects ?? []); - } catch (reason) { - setToast(reason instanceof Error ? reason.message : "读取项目列表失败"); - } finally { - setLoading(false); - } - }, [postAction, setToast]); - - useEffect(() => { - const timer = window.setTimeout(() => { - void loadProjects(); - }, 0); - return () => window.clearTimeout(timer); - }, [loadProjects]); - - const refreshDetail = useCallback( - async (projectId: string) => { - try { - const parsed = await postAction( - { action: "project_detail", projectId }, - "读取项目详情失败", - ); - setDetail(parsed); - } catch (reason) { - setToast(reason instanceof Error ? reason.message : "读取项目详情失败"); - } - }, - [postAction, setToast], - ); - - const toggleDetail = async (project: ProjectSummary) => { - if (expandedId === project.id) { - setExpandedId(null); - setDetail(null); - return; - } - setExpandedId(project.id); - setDetail(null); + const loadDetail = useCallback(async () => { setDetailLoading(true); try { - await refreshDetail(project.id); + const parsed = await postAction( + { action: "project_detail", projectId: project.id }, + "读取项目详情失败", + ); + setDetail(parsed); + } catch (reason) { + setToast(reason instanceof Error ? reason.message : "读取项目详情失败"); } finally { setDetailLoading(false); } - }; + }, [project.id, setToast]); - const openEditor = (project?: ProjectSummary) => { - setEditorDraft({ - id: project?.id ?? "", - name: project?.name ?? "", - brand: project?.brand ?? "", - members: (project?.mainGroupMemberIds ?? []).join("\n"), - note: project?.note ?? "", - }); - setEditorOpen(true); - }; - - const submitEditor = async () => { - if (!editorDraft.name.trim()) { - setToast("请填写项目名称"); - return; - } - setEditorSaving(true); - try { - await postAction( - { - action: "project_save", - id: editorDraft.id, - name: editorDraft.name, - brand: editorDraft.brand, - mainGroupMemberIds: editorDraft.members, - note: editorDraft.note, - }, - "保存项目失败", - ); - setEditorOpen(false); - setToast(editorDraft.id ? "项目已更新" : "项目已创建"); - await loadProjects(); - if (editorDraft.id && expandedId === editorDraft.id) { - await refreshDetail(editorDraft.id); - } - } catch (reason) { - setToast(reason instanceof Error ? reason.message : "保存项目失败"); - } finally { - setEditorSaving(false); - } - }; - - const confirmDelete = async () => { - if (!deleteTarget) return; - setDeleteWorking(true); - try { - await postAction( - { action: "project_delete", projectId: deleteTarget.id }, - "删除项目失败", - ); - setToast(`项目「${deleteTarget.name}」已删除,任务已解除关联`); - setDeleteTarget(null); - setExpandedId(null); - setDetail(null); - await loadProjects(); - await onTasksChanged(); - } catch (reason) { - setToast(reason instanceof Error ? reason.message : "删除项目失败"); - } finally { - setDeleteWorking(false); - } - }; + useEffect(() => { + if (!expanded) return; + const timer = window.setTimeout(() => { + void loadDetail(); + }, 0); + return () => window.clearTimeout(timer); + }, [expanded, detailVersion, loadDetail]); const openChatManager = async () => { if (!detail) return; @@ -2758,7 +2666,7 @@ function ProjectsPage({ setDetail(parsed); setChatManaging(false); setToast("项目任务群已更新"); - await loadProjects(); + await onProjectsChanged(); } catch (reason) { setToast(reason instanceof Error ? reason.message : "保存任务群关联失败"); } finally { @@ -2767,233 +2675,111 @@ function ProjectsPage({ }; return ( -
-
-
-

项目 = 任务 + 任务群 + 总群

-

按项目组织分发链路

-

项目总群成员收到应用消息后,各自把任务群发到自己负责的客户群。

+
+
+
+
+ {(project.brand || project.name).slice(0, 1)} +
+
+

{project.name}

+ + {project.brand || "未填品牌"} · {project.taskCount} 个任务 · {project.groupChatCount} 个任务群 · 总群 {project.mainGroupMemberIds.length} 人 + +
-
- - +
+ + +
-
+
+ {project.note &&

{project.note}

} - {loading ? ( -
-
…
-

正在读取项目

-
- ) : projects.length === 0 ? ( -
-
▦
-

还没有项目

-

新建项目后,把任务挂到项目下,即可一次推送到项目总群与各任务群。

- -
+ {tasks.length === 0 ? ( +

项目下还没有任务,新建任务时选择所属项目即可挂入。

) : ( - projects.map((project) => ( -
-
-
-
- {(project.brand || project.name).slice(0, 1)} +
+ {tasks.map((task) => ( + onOpenTaskDetail(task.id)} + onCopyShare={onCopyShare} + onPushToGroups={onPushToGroups} + onOneClickPush={onOneClickPush} + /> + ))} +
+ )} + + {expanded && ( +
+ {detailLoading || !detail || detail.project.id !== project.id ? ( +
正在读取项目详情…
+ ) : ( + <> +
+
+
+

项目总群成员({detail.project.mainGroupMemberIds.length})

+ 推送任务时逐人发送应用消息 +
+ {detail.project.mainGroupMemberIds.length === 0 ? ( +

未配置,编辑项目可填写成员 userid

+ ) : ( +
    + {detail.project.mainGroupMemberIds.map((userid) => ( +
  • {userid}
  • + ))} +
+ )}
-
-

{project.name}

- - {project.brand || "未填品牌"} · {project.taskCount} 个任务 · {project.groupChatCount} 个任务群 · 总群 {project.mainGroupMemberIds.length} 人 - +
+
+

任务群({detail.groupChats.length})

+ +
+ {detail.groupChats.length === 0 ? ( +

未关联任务群,点击「管理关联」从客户群中选择

+ ) : ( + + + + + + {detail.groupChats.map((chat) => ( + + + + + + + ))} + +
群名称群主 userid人数对应合作方
{chat.missing ? `${chat.name || chat.chatId}(群不存在)` : chat.name || "(未命名群)"}{chat.ownerUserId || "—"}{chat.missing ? "—" : chat.memberCount}{chat.partnerId ? chat.partnerName || "(已删除)" : 未映射}
+ )}
-
- - - -
-
- {project.note &&

{project.note}

} - {expandedId === project.id && ( -
- {detailLoading || !detail || detail.project.id !== project.id ? ( -
正在读取项目详情…
+ {detail.tasks.length === 0 ? ( + 项目下没有可推送的任务 ) : ( - <> -
-
-
-

项目总群成员({detail.project.mainGroupMemberIds.length})

- 推送任务时逐人发送应用消息 -
- {detail.project.mainGroupMemberIds.length === 0 ? ( -

未配置,编辑项目可填写成员 userid

- ) : ( -
    - {detail.project.mainGroupMemberIds.map((userid) => ( -
  • {userid}
  • - ))} -
- )} -
-
-
-

任务群({detail.groupChats.length})

- -
- {detail.groupChats.length === 0 ? ( -

未关联任务群,点击「管理关联」从客户群中选择

- ) : ( - - - - - - {detail.groupChats.map((chat) => ( - - - - - - - ))} - -
群名称群主 userid人数对应合作方
{chat.missing ? `${chat.name || chat.chatId}(群不存在)` : chat.name || "(未命名群)"}{chat.ownerUserId || "—"}{chat.missing ? "—" : chat.memberCount}{chat.partnerId ? chat.partnerName || "(已删除)" : 未映射}
- )} -
-
-
-
-

项目任务({detail.tasks.length})

- 在「任务中心」创建任务时选择所属项目 -
- {detail.tasks.length === 0 ? ( -

项目下还没有任务

- ) : ( - - - - - - {detail.tasks.map((task) => ( - - - - - - - - ))} - -
任务名称数量截止状态
{task.name}{task.claimed_quantity}/{task.quantity}{formatDate(task.due_at)}{statusLabel(task.status)} - -
- )} -
-
- - {detail.tasks.length === 0 && 项目下没有可推送的任务} -
- + 可在推送弹层中选择项目下的具体任务 )}
- )} -
- )) - )} - - {editorOpen && ( -
setEditorOpen(false)}> -
event.stopPropagation()}> -
-
-

项目中心

-

{editorDraft.id ? "编辑项目" : "新建项目"}

-
- -
-
- - -
-