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

@@ -37,14 +37,18 @@ export function TaskCustomerChatModal({
const [selectedChatId, setSelectedChatId] = useState(task.customerChatId);
const [bindResult, setBindResult] = useState<BindResult | null>(null);
const loadChats = useCallback(async () => {
const loadChats = useCallback(async (options?: { sync?: boolean }) => {
setLoading(true);
setError("");
try {
const response = await fetch("/api/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "task_customer_chats", taskId: task.id }),
body: JSON.stringify({
action: "task_customer_chats",
taskId: task.id,
sync: options?.sync === true,
}),
});
const parsed = await readApiResponse<{
chats?: CustomerChat[];
@@ -129,7 +133,7 @@ export function TaskCustomerChatModal({
<div>
<h2>绑定客户群</h2>
<p>
列出任务负责人名下的企业微信客户群,绑定后生成进群活码,认领人在领取页扫码进群。
列出全部成员名下的企业微信客户群(含各群主的外部群),绑定后生成进群活码,认领人在领取页扫码进群。
</p>
</div>
<button type="button" onClick={onClose} aria-label="关闭">×</button>
@@ -142,10 +146,10 @@ export function TaskCustomerChatModal({
<button
type="button"
className="ghost-button"
onClick={() => void loadChats()}
onClick={() => void loadChats({ sync: true })}
disabled={loading || saving}
>
{loading ? "拉取中…" : "刷新列表"}
{loading ? "同步中…" : "同步最新群列表"}
</button>
</div>
{error && <p className="form-error">{error}</p>}
@@ -158,11 +162,11 @@ export function TaskCustomerChatModal({
)}
<div className="wecom-picker-list">
{loading ? (
<div className="wecom-picker-empty">正在拉取负责人名下客户群…</div>
<div className="wecom-picker-empty">正在拉取客户群列表…</div>
) : chats.length === 0 ? (
<div className="wecom-picker-empty">
未拉取到客户群——请任务负责人先在企微客户端创建客户群(内外部成员都可拉入),
再点击「刷新列表」。
未拉取到客户群——请群主先在企微客户端创建客户群(内外部成员都可拉入,且群主需在应用可见范围内),
再点击「同步最新群列表」。
</div>
) : (
chats.map((chat) => {
@@ -174,7 +178,9 @@ export function TaskCustomerChatModal({
</span>
<span className="wecom-picker-meta">
<span className="wecom-picker-name">{chat.name || chat.chatId}</span>
<span className="wecom-picker-sub">{chat.memberCount} 人</span>
<span className="wecom-picker-sub">
群主 {chat.ownerUserId || "—"} · {chat.memberCount} 人
</span>
<code className="wecom-picker-id">{chat.chatId}</code>
</span>
<input