import React, { useEffect, useMemo, useState } from "react"; import { Activity, AlertTriangle, ArrowUpRight, Ban, Bell, CheckCircle2, CircleDollarSign, Clock3, Database, Download, FileKey2, FileText, Film, Gauge, HardDrive, KeyRound, ListChecks, LogIn, ListFilter, Network, Play, Plus, RefreshCw, Save, Search, ServerCog, Settings2, ShieldAlert, ShieldCheck, SlidersHorizontal, MessageSquare, Link2, Paperclip, Send, AtSign, ChevronLeft, ChevronRight, X, UserRoundCheck, Users, UserRoundCog, Webhook, Workflow, XCircle, Zap } from "lucide-react"; import { createApiClient, createSystemUser, acceptInvitation, cancelMfaSetup, changePassword, fetchAuthSessions, fetchAuthDevices, fetchSecurityEvents, fetchAdminQueue, fetchWorkerStatus, dispatchWorker, disableMfa, enableMfa, fetchJob, fetchInvitations, fetchMfaStatus, fetchSystemConfig, fetchSystemHealth, fetchSystemReadiness, createSystemBackup, probeModel, registerModel, retryJob, revokeAuthSession, revokeOtherAuthSessions, trustAuthDevice, untrustAuthDevice, cancelJob, runJob, updateModel, updateApiClient, rotateApiClientKey, updateJobPriority, operateRunner, saveSystemConfig, startMfaSetup, updateFeatureFlag, updateNotificationChannel, previewInvitation, registerInvitedUser, testNotification, fetchNotificationDeliveries, fetchStorageUsage, fetchSsoProviders, fetchIdentityCenter, fetchOrganizationCommercial, fetchOrganizationUsage, fetchOrganizationInvoices, exportOrganizationCommercial, exportOrganizationUsage, exportOrganizationUsageCsv, exportOrganizationInvoices, exportOrganizationInvoicesCsv, fetchSystemUser, fetchSystemUsers, resetSystemUserMfa, resetSystemUserPassword, revokeSystemUserSessions, updateOrganizationBilling, generateOrganizationInvoice, updateOrganizationInvoiceStatus, updateCostCenter, updateQuotaAllocation, updateSystemUser, updateSystemUserMemberships, batchQueueAction, updateIdentityPolicy, createIdentityProvider, updateIdentityProvider, probeIdentityProvider, createDirectorySync, updateDirectorySync, rotateDirectorySyncToken, ssoStartUrl, fetchWorkItems, fetchTasks, createTask, updateTask, fetchTaskDetail, createTaskComment, addTaskLink, removeTaskLink, fetchProjectActivity, fetchProductionGraph, fetchAuditEvents, fetchAuditEvent, exportAuditEvents, fetchNotifications, fetchNotificationPreferences, markNotificationRead, markAllNotificationsRead, updateNotificationPreference } from "../lib/api"; import { downloadJson } from "../lib/exporters"; function toneForStatus(status) { if (["ready", "active", "completed", "pass", "enforced"].includes(status)) return "ok"; if (["failed", "blocked", "error", "disabled"].includes(status)) return "warn"; return "neutral"; } function AdminHeader({ eyebrow, title, description, action }) { return (
{eyebrow}

{title}

{description}

{action}
); } function AdminKpi({ label, value, detail, icon: Icon, tone = "neutral" }) { return (
{label}{value}{detail}
); } function SectionBar({ title, detail, action }) { return

{title}

{detail && {detail}}
{action}
; } function ContextLink({ children, onClick }) { return ; } const workItemKindLabels = { task: "协作任务", review: "审片", job: "生成", asset: "资产", delivery: "交付", invitation: "邀请" }; function formatWorkItemTime(value) { const date = new Date(value || ""); if (Number.isNaN(date.getTime())) return "刚刚"; const elapsed = Math.max(0, Date.now() - date.getTime()); if (elapsed < 60 * 1000) return "刚刚"; if (elapsed < 60 * 60 * 1000) return `${Math.floor(elapsed / 60000)} 分钟前`; if (elapsed < 24 * 60 * 60 * 1000) return `${Math.floor(elapsed / 3600000)} 小时前`; return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); } function workItemTone(item) { if (item.priority === "high" || ["failed", "blocked", "changes_requested", "rejected", "needs-evidence"].includes(item.status)) return "warn"; if (item.status === "approved" || item.status === "completed") return "ok"; return "neutral"; } const notificationCategoryLabels = { job: "生成任务", review: "审片流程", delivery: "交付运营", task: "协作任务", access: "组织访问", billing: "用量与配额", system: "系统通知" }; function formatNotificationTime(value) { const date = new Date(value || ""); if (Number.isNaN(date.getTime())) return "刚刚"; const elapsed = Math.max(0, Date.now() - date.getTime()); if (elapsed < 60 * 1000) return "刚刚"; if (elapsed < 60 * 60 * 1000) return `${Math.floor(elapsed / 60000)} 分钟前`; if (elapsed < 24 * 60 * 60 * 1000) return `${Math.floor(elapsed / 3600000)} 小时前`; return date.toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }); } const taskStatusLabels = { open: "待处理", in_progress: "进行中", blocked: "已阻塞", done: "已完成", cancelled: "已取消" }; const taskPriorityLabels = { high: "高优先级", medium: "普通", low: "低优先级" }; function formatTaskDue(value, overdue = false) { if (!value) return "无截止时间"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "截止时间待确认"; return `${overdue ? "已逾期 · " : "截止 "}${date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })}`; } function notificationIcon(severity) { if (severity === "error") return AlertTriangle; if (severity === "success") return CheckCircle2; if (severity === "warning") return ShieldAlert; return Bell; } function notificationTone(severity) { if (severity === "error" || severity === "warning") return "warn"; if (severity === "success") return "ok"; return "neutral"; } export function NotificationBell({ contextOverrides = {}, onOpen }) { const [data, setData] = useState({ notifications: [], unreadCount: 0 }); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId].join("|"); async function load() { setLoading(true); try { setData(await fetchNotifications({ limit: 8 }, contextOverrides)); } catch { setData((current) => current); } finally { setLoading(false); } } useEffect(() => { load(); const timer = window.setInterval(load, 30000); return () => window.clearInterval(timer); }, [scopeKey]); async function readAndOpen(notification) { try { const result = await markNotificationRead(notification.id, true, contextOverrides); setData(result); } catch { // The destination page still remains usable if the notification was already read elsewhere. } setOpen(false); if (notification.targetTab) onOpen?.(notification.targetTab); } async function readAll(event) { event.stopPropagation(); try { setData(await markAllNotificationsRead(contextOverrides)); } catch { // Keep the popover open so the user can retry after the API recovers. } } return (
{open && <>
{loading &&
读取通知…
} {!loading && data.notifications.slice(0, 6).map((notification) => { const Icon = notificationIcon(notification.severity); return ; })} {!loading && !data.notifications.length &&
当前工作区没有通知。
}
} ); } export function NotificationCenterPage({ contextOverrides = {}, setActiveTab }) { const [data, setData] = useState({ notifications: [], unreadCount: 0 }); const [preferences, setPreferences] = useState([]); const [filter, setFilter] = useState("all"); const [loading, setLoading] = useState(true); const [preferenceBusy, setPreferenceBusy] = useState(""); const [notice, setNotice] = useState(""); const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId].join("|"); async function load() { setLoading(true); try { const [nextData, nextPreferences] = await Promise.all([ fetchNotifications({ limit: 200, unreadOnly: filter === "unread" }, contextOverrides), fetchNotificationPreferences(contextOverrides) ]); setData(nextData); setPreferences(nextPreferences.preferences || []); setNotice(""); } catch (error) { setNotice(error.message); } finally { setLoading(false); } } useEffect(() => { load(); }, [scopeKey, filter]); async function markRead(notification, read = true) { try { setData(await markNotificationRead(notification.id, read, contextOverrides)); } catch (error) { setNotice(error.message); } } async function markAllRead() { try { setData(await markAllNotificationsRead(contextOverrides)); setNotice("当前工作区通知已全部标记为已读"); } catch (error) { setNotice(error.message); } } async function togglePreference(preference) { setPreferenceBusy(preference.category); try { const result = await updateNotificationPreference(preference.category, !preference.enabled, contextOverrides); setPreferences(result.preferences || []); setNotice(`${preference.label}通知已${!preference.enabled ? "开启" : "关闭"}`); } catch (error) { setNotice(error.message); } finally { setPreferenceBusy(""); } } return (
} /> {notice &&
{notice}
}
} /> {loading ?
读取通知…
:
{data.notifications.map((notification) => { const Icon = notificationIcon(notification.severity); return
{notificationCategoryLabels[notification.category] || notification.category}{formatNotificationTime(notification.createdAt)}
{notification.title}

{notification.body}

{notification.eventKey}{notification.targetId && 关联 {notification.targetId}}
{notification.targetTab && }
; })}{!data.notifications.length &&
当前筛选下没有消息。
}
}
{preferences.map((preference) =>
{preference.label}{preference.description}
)}{!preferences.length &&
暂无可配置的通知类别。
}
); } export function TaskCenterPage({ platformContext, contextOverrides = {} }) { const current = platformContext?.context || {}; const permissions = current.permissions || []; const canManage = permissions.includes("task:manage"); const canComplete = permissions.includes("task:complete"); const members = [...(platformContext?.platform?.projectMembers || []), ...(platformContext?.platform?.workspaceMembers || [])] .filter((member) => member.status === "active") .filter((member, index, list) => list.findIndex((item) => item.user_id === member.user_id) === index) .map((member) => ({ id: member.user_id, displayName: member.display_name, email: member.email || "" })); const currentUser = current.currentUser || {}; const assigneeOptions = [{ id: currentUser.id, displayName: `${currentUser.displayName || currentUser.display_name || currentUser.email || "我"}(我)`, email: currentUser.email || "" }, ...members.filter((member) => member.id !== currentUser.id)] .filter((member, index, list) => member.id && list.findIndex((item) => item.id === member.id) === index); const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId].join("|"); const [statusFilter, setStatusFilter] = useState("all"); const [mineOnly, setMineOnly] = useState(false); const [data, setData] = useState({ tasks: [], summary: { total: 0, open: 0, blocked: 0, done: 0, overdue: 0 } }); const [activity, setActivity] = useState([]); const [catalog, setCatalog] = useState({ shots: [], assets: [] }); const [loading, setLoading] = useState(true); const [activityLoading, setActivityLoading] = useState(true); const [busyId, setBusyId] = useState(""); const [notice, setNotice] = useState(""); const [error, setError] = useState(""); const [selectedTaskId, setSelectedTaskId] = useState(""); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailTab, setDetailTab] = useState("discussion"); const [comment, setComment] = useState(""); const [replyTo, setReplyTo] = useState(""); const [mentionIds, setMentionIds] = useState([]); const [linkType, setLinkType] = useState("shot"); const [linkTargetId, setLinkTargetId] = useState(""); const [filePath, setFilePath] = useState(""); const [form, setForm] = useState({ title: "", description: "", kind: "production", priority: "medium", assigneeUserId: currentUser.id || "", targetTab: "creator-home", targetId: "", dueAt: "", linkShotId: "" }); useEffect(() => { if (currentUser.id && !form.assigneeUserId) setForm((currentForm) => ({ ...currentForm, assigneeUserId: currentUser.id })); }, [currentUser.id]); async function load() { setLoading(true); setActivityLoading(true); const [taskResult, activityResult, graphResult] = await Promise.allSettled([ fetchTasks({ status: statusFilter, assignedTo: mineOnly ? "me" : "", limit: 200 }, contextOverrides), fetchProjectActivity({ limit: 80 }, contextOverrides), fetchProductionGraph(contextOverrides) ]); if (taskResult.status === "fulfilled") { setData(taskResult.value); setError(""); } else setError(taskResult.reason.message); if (activityResult.status === "fulfilled") setActivity(activityResult.value.activities || []); if (graphResult.status === "fulfilled") setCatalog({ shots: graphResult.value.graph?.shots || [], assets: graphResult.value.graph?.assets || [] }); setLoading(false); setActivityLoading(false); } useEffect(() => { let active = true; Promise.allSettled([ fetchTasks({ status: statusFilter, assignedTo: mineOnly ? "me" : "", limit: 200 }, contextOverrides), fetchProjectActivity({ limit: 80 }, contextOverrides), fetchProductionGraph(contextOverrides) ]).then(([taskResult, activityResult, graphResult]) => { if (!active) return; if (taskResult.status === "fulfilled") { setData(taskResult.value); setError(""); } else setError(taskResult.reason.message); if (activityResult.status === "fulfilled") setActivity(activityResult.value.activities || []); if (graphResult.status === "fulfilled") setCatalog({ shots: graphResult.value.graph?.shots || [], assets: graphResult.value.graph?.assets || [] }); }).finally(() => { if (active) { setLoading(false); setActivityLoading(false); } }); return () => { active = false; }; }, [scopeKey, statusFilter, mineOnly]); async function openTask(taskId) { setSelectedTaskId(taskId); setDetailLoading(true); setDetailTab("discussion"); try { setDetail(await fetchTaskDetail(taskId, contextOverrides)); } catch (loadError) { setError(loadError.message); } finally { setDetailLoading(false); } } async function submitTask(event) { event.preventDefault(); setBusyId("create"); setNotice(""); try { const result = await createTask({ ...form, dueAt: form.dueAt ? new Date(`${form.dueAt}T23:59:59`).toISOString() : null }, contextOverrides); if (form.linkShotId) await addTaskLink(result.task.id, { linkType: "shot", targetId: form.linkShotId }, contextOverrides); setForm({ title: "", description: "", kind: "production", priority: "medium", assigneeUserId: currentUser.id || "", targetTab: "creator-home", targetId: "", dueAt: "", linkShotId: "" }); setNotice("协作任务已创建,负责人、关联镜头和审计记录已建立"); await load(); await openTask(result.task.id); } catch (createError) { setError(createError.message); } finally { setBusyId(""); } } async function changeTaskStatus(task, status) { setBusyId(task.id); setNotice(""); try { const result = await updateTask(task.id, { status }, contextOverrides); setData((currentData) => ({ ...currentData, tasks: currentData.tasks.map((item) => item.id === task.id ? result.task : item) })); if (selectedTaskId === task.id) setDetail((currentDetail) => currentDetail ? { ...currentDetail, task: result.task } : currentDetail); setNotice(status === "done" ? "任务已完成" : "任务状态已更新,并已进入项目活动流"); await load(); } catch (updateError) { setError(updateError.message); } finally { setBusyId(""); } } async function submitComment(event) { event.preventDefault(); if (!selectedTaskId || !comment.trim()) return; setBusyId("comment"); try { const result = await createTaskComment(selectedTaskId, { body: comment.trim(), parentCommentId: replyTo || null, mentionUserIds: mentionIds }, contextOverrides); setDetail(result); setComment(""); setReplyTo(""); setMentionIds([]); setNotice("讨论已记录,相关负责人和 @成员已收到站内通知"); const activityResult = await fetchProjectActivity({ limit: 80 }, contextOverrides); setActivity(activityResult.activities || []); setData((currentData) => ({ ...currentData, tasks: currentData.tasks.map((item) => item.id === selectedTaskId ? result.task : item) })); } catch (commentError) { setError(commentError.message); } finally { setBusyId(""); } } async function submitLink(event) { event.preventDefault(); if (!selectedTaskId) return; const targetId = linkType === "file" ? filePath.trim() : linkTargetId; if (!targetId) return; setBusyId("link"); try { const result = await addTaskLink(selectedTaskId, { linkType, targetId }, contextOverrides); setDetail(result); setLinkTargetId(""); setFilePath(""); setNotice("生产对象已关联到任务"); const activityResult = await fetchProjectActivity({ limit: 80 }, contextOverrides); setActivity(activityResult.activities || []); } catch (linkError) { setError(linkError.message); } finally { setBusyId(""); } } async function unlink(linkId) { setBusyId(linkId); try { setDetail(await removeTaskLink(selectedTaskId, linkId, contextOverrides)); setNotice("任务关联已移除"); } catch (linkError) { setError(linkError.message); } finally { setBusyId(""); } } const selectedTask = detail?.task || data.tasks.find((task) => task.id === selectedTaskId); const linkOptions = linkType === "shot" ? catalog.shots : catalog.assets; return (
刷新工作区} /> {notice &&
{notice}
} {error &&
{error}
}
} /> {loading ?
读取协作任务…
:
{(data.tasks || []).map((task) =>
openTask(task.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openTask(task.id); } }} role="button" tabIndex="0">
{taskPriorityLabels[task.priority] || task.priority}{task.kind}{formatTaskDue(task.dueAt, task.overdue)}
{task.title}

{task.description || "暂无任务说明"}

负责人:{task.assignee?.displayName || "未分派"}创建人:{task.createdBy?.displayName || "未知"} {task.commentCount || 0} {task.linkCount || 0}更新于 {formatNotificationTime(task.updatedAt)}
{canManage ? : task.assignee?.id === currentUser.id && canComplete && !["done", "cancelled"].includes(task.status) ? : }
)}{!data.tasks?.length &&
当前筛选下没有协作任务。
}
}
{canManage ?