import { useEffect, useState } from "react"; import { CircleDollarSign, KeyRound, UserPlus } from "lucide-react"; import { api } from "../api"; import type { BillingSummary, Notification, Team, TeamMember, User } from "../types"; import type { Page } from "./route-config"; import { money } from "./stage-config"; import { ConfirmModal, TeamModal } from "../components/overlays"; import { Pager } from "../components/pager"; const MEMBERS_PER_PAGE = 10; // 角色 → pill key/label(对齐 api-bridge roleUi) function roleUi(role: string): { key: "super" | "admin" | "member"; label: string } { if (role === "owner" || role === "super") return { key: "super", label: "超管" }; if (role === "admin") return { key: "admin", label: "团管" }; if (role === "viewer") return { key: "member", label: "访客" }; return { key: "member", label: "成员" }; } const PERM_ROWS: Array<{ cap: string; cells: [string, string, string]; last?: boolean }> = [ { cap: "邀请 / 移除成员", cells: ["✓", "✓", "—"] }, { cap: "设置成员额度", cells: ["✓", "✓", "—"] }, { cap: "团队充值", cells: ["✓", "—", "—"] }, { cap: "设置月限额", cells: ["✓", "—", "—"] }, { cap: "编辑别人项目", cells: ["✓", "✓", "—"] }, { cap: "团队共享资产库管理", cells: ["✓", "✓", "仅自传"] }, { cap: "查看团队消费明细", cells: ["✓", "✓", "仅自己"] }, { cap: "创建项目 / 用 AI 流程", cells: ["✓", "✓", "✓"], last: true } ]; export function TeamPage({ team, user, billing, navigate, onCreateMember: onCreateMemberRaw, onUpdateMember: onUpdateMemberRaw, onRemoveMember: onRemoveMemberRaw, onResetPassword, onRecharge: onRechargeRaw }: { team: Team; user: User; billing: BillingSummary | null; navigate: (page: Page) => void; onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; monthly_credit_limit?: number }) => void | Promise; onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise; onRemoveMember: (id: string) => void | Promise; onResetPassword: (id: string, password: string) => void | Promise; onRecharge: (amount: number, bonus: number) => void | Promise; }) { const [modal, setModal] = useState<"" | "invite" | "limit" | "recharge">(""); const [search, setSearch] = useState(""); // 团队成员 + 团队动态本页懒加载(不再走全局 bootstrap);成员/充值操作后 bump 刷新 const [reloadFlag, setReloadFlag] = useState(0); const [members, setMembers] = useState([]); const [notifications, setNotifications] = useState([]); const [notificationTotal, setNotificationTotal] = useState(0); useEffect(() => { let alive = true; api.teamMembers().then((m) => { if (alive) setMembers(m); }).catch(() => {}); api.allNotifications().then((n) => { if (alive) { setNotifications(n.results); setNotificationTotal(n.count); } }).catch(() => {}); return () => { alive = false; }; }, [reloadFlag]); const reloadTeam = () => setReloadFlag((n) => n + 1); // 包一层:操作完成后刷新本页成员列表(余额由全局 action→loadData 刷新);透传返回值(调用处据此判成功) const onCreateMember = async (p: Parameters[0]) => { const r = await onCreateMemberRaw(p); reloadTeam(); return r; }; const onUpdateMember = async (id: string, p: Parameters[1]) => { const r = await onUpdateMemberRaw(id, p); reloadTeam(); return r; }; const onRemoveMember = async (id: string) => { const r = await onRemoveMemberRaw(id); reloadTeam(); return r; }; const onRecharge = async (amount: number, bonus: number) => { const r = await onRechargeRaw(amount, bonus); reloadTeam(); return r; }; // 创建账户表单 const [cuUser, setCuUser] = useState(""); const [cuPass, setCuPass] = useState(""); const [cuName, setCuName] = useState(""); const [cuRole, setCuRole] = useState("member"); const [cuMonthly, setCuMonthly] = useState(""); // 编辑成员 const [editTarget, setEditTarget] = useState(null); const [edRole, setEdRole] = useState("member"); const [edMonthly, setEdMonthly] = useState(""); // 重置密码 const [resetTarget, setResetTarget] = useState(null); const [resetPwd, setResetPwd] = useState(""); // 移除成员 const [removeTarget, setRemoveTarget] = useState(null); // 团队充值 const [rechargeAmt, setRechargeAmt] = useState("500"); const rows: TeamMember[] = members.length ? members : [{ id: "owner", role: "owner", status: "active", monthly_credit_limit: "0", user } as TeamMember]; // 统计 · 对齐 api-bridge renderLiveTeamPayload const balance = Number(billing?.account.balance || 0); const used = Number(billing?.charged_total || 0); const limit = rows.reduce((sum, member) => sum + Math.max(0, Number(member.monthly_credit_limit || 0)), 0) || balance; const left = Math.max(0, limit - used); const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0; // 团队动态:取最近 6 条真实通知 const feedItems = [...notifications] .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, 6); const needle = search.trim().toLowerCase(); const list = rows.filter((member) => { const name = member.user.username || ""; const email = member.user.email || ""; return !needle || `${name} ${email}`.toLowerCase().includes(needle); }); // 成员分页:每页 10 人,搜索变化回第 1 页 const [memberPage, setMemberPage] = useState(1); useEffect(() => { setMemberPage(1); }, [search]); const memberTotalPages = Math.max(1, Math.ceil(list.length / MEMBERS_PER_PAGE)); const memberCurPage = Math.min(memberPage, memberTotalPages); const pagedMembers = list.slice((memberCurPage - 1) * MEMBERS_PER_PAGE, memberCurPage * MEMBERS_PER_PAGE); function openEdit(member: TeamMember) { setEditTarget(member); setEdRole(member.role === "owner" ? "admin" : member.role || "member"); setEdMonthly(Number(member.monthly_credit_limit || 0) > 0 ? String(Number(member.monthly_credit_limit)) : ""); } async function submitCreate() { if (!cuUser.trim() || cuPass.length < 8) return; const created = await onCreateMember({ username: cuUser.trim(), password: cuPass, name: cuName.trim() || undefined, role: cuRole, monthly_credit_limit: Number(cuMonthly) || 0 }); // 创建失败(action 报错返回 null)时保持弹窗与已填内容,只成功才关闭并清空 if (!created) return; setModal(""); setCuUser(""); setCuPass(""); setCuName(""); setCuRole("member"); setCuMonthly(""); } async function submitEdit() { if (!editTarget) return; await onUpdateMember(editTarget.id, { role: edRole, monthly_credit_limit: Number(edMonthly) || 0 }); setEditTarget(null); } async function submitReset() { if (!resetTarget || resetPwd.length < 8) return; await onResetPassword(resetTarget.id, resetPwd); setResetTarget(null); setResetPwd(""); } async function submitRemove() { if (!removeTarget) return; await onRemoveMember(removeTarget.id); setRemoveTarget(null); } async function submitRecharge() { const amt = Number(rechargeAmt) || 0; if (amt <= 0) return; await onRecharge(amt, 0); setModal(""); } return (

团队管理

// 成员 · 角色 · 额度 · 共享资产库
{/* 顶部行:团队 banner(左)+ 团队动态(右) */}
[ TEAM ]
{team.name} 企业
// 团队 ID: {team.id} · {rows.length} 名成员
[ 充值余额 ]
{money(balance)}
// 团队总池
[ 月限额 ]
{money(limit)}
// 自然月重置
[ 当月已用 ]
{money(used)}
// 占月限 {pct.toFixed(1)}%
[ 当月剩余 ]
{money(left)}
// 还可生成约 {Math.max(0, Math.round(left / 10))} 个项目
{/* 团队动态(banner 右栏)· 接真实 ops/notifications 事件流 */}

团队动态

// 最近 {Math.min(feedItems.length, 6)} 条 · 共 {notificationTotal ?? notifications.length} navigate("messages")}>全部 →
{feedItems.length === 0 ? (
{(team.name || "T").slice(0, 1).toUpperCase()}
{team.name}已同步{rows.length} 名成员
// 暂无团队动态
) : ( feedItems.map((n) => (
{(n.owner_label || n.source || team.name || "·").slice(0, 1).toUpperCase()}
{n.owner_label || n.source || "系统"} {n.title} {n.project_name && {n.project_name}}
{n.created_at ? new Date(n.created_at).toLocaleString("zh-CN") : n.brief}
)) )}
{/* /.team-top */}
{/* 左:成员表 */}

成员列表 // {list.length} / {rows.length} 人 · 真实团队表

setSearch(event.target.value)} />
{pagedMembers.map((member) => { const rawName = (member.user.username || "").trim(); const email = (member.user.email || "").trim(); // 用户名是邮箱时取 @ 前作为显示名,完整邮箱作副行,避免名字与邮箱重复显示 const displayName = rawName && !rawName.includes("@") ? rawName : (email ? email.split("@")[0] : (rawName || "成员")); const showEmail = !!email && email.toLowerCase() !== displayName.toLowerCase(); const role = roleUi(member.role); const monthly = Number(member.monthly_credit_limit || 0); const memberUsed = Number(member.month_charged || 0); // 月度不限时按团队月限额/余额作分母给参考进度;有消费即显可见细条,避免「用了钱进度条却空白」 const quotaDenom = monthly > 0 ? monthly : limit; const memberPct = quotaDenom > 0 ? Math.min(100, (memberUsed / quotaDenom) * 100) : 0; const barWidth = memberUsed > 0 ? Math.max(memberPct, 3) : 0; const barClass = memberPct >= 80 ? "warn" : "ok"; const isOwner = member.role === "owner"; return ( ); })}
成员 角色 每日额度 月度额度 当月已用 操作
{displayName.slice(0, 1).toUpperCase()}{displayName}{showEmail && {email}} {role.label} 不限 {monthly > 0 ? money(monthly) : "不限"}
{money(memberUsed)} / {monthly > 0 ? `${memberPct.toFixed(0)}%` : "不限"}
{isOwner ? 不可编辑 : <> }
{/* 右:权限矩阵 */}

角色权限

// PRD §10.2 权限矩阵节选
{PERM_ROWS.map((row) => ( {row.cells.map((cell, index) => ( ))} ))}
能力超管团管成员
{row.cap}{cell}
{/* 设置月限额(团队级)· 注:后端暂无团队级限额端点,真实限额请在「编辑成员」逐人设置 */} } close={() => setModal("")}>
{/* 创建账户 */} } close={() => setModal("")} footer={} >
setCuUser(e.target.value)} placeholder="zhang.yunying" />
setCuPass(e.target.value)} placeholder="至少 8 位" />
setCuName(e.target.value)} placeholder="张运营" />
setCuMonthly(e.target.value)} placeholder="0" />
{/* 团队充值 */} } close={() => setModal("")} footer={} >
setRechargeAmt(e.target.value)} placeholder="最低 ¥50" />
{/* 编辑成员 */} } close={() => setEditTarget(null)} footer={} >
setEdMonthly(e.target.value)} placeholder="0" />
{/* 重置密码 */} } close={() => setResetTarget(null)} footer={} >
setResetPwd(e.target.value)} placeholder="新密码" />
{/* 移除成员确认 */} setRemoveTarget(null)} onConfirm={submitRemove} />
); }