diff --git a/core/frontend/src/routes/team.tsx b/core/frontend/src/routes/team.tsx index 3393d6d..cfcd763 100644 --- a/core/frontend/src/routes/team.tsx +++ b/core/frontend/src/routes/team.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { CheckCircle2, CircleDollarSign, Copy, KeyRound, RefreshCw, UserPlus } from "lucide-react"; import { api } from "../api"; -import type { BillingSummary, Notification, Team, TeamMember, User } from "../types"; +import type { BillingSummary, Invitation, Notification, Team, TeamMember, User } from "../types"; import type { Page } from "./route-config"; import { money } from "./stage-config"; import { ConfirmModal, TeamModal } from "../components/overlays"; @@ -88,7 +88,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea onResetPassword: (id: string, password: string) => void | Promise; onRecharge: (amount: number, bonus: number) => void | Promise; }) { - const [modal, setModal] = useState<"" | "invite" | "limit" | "recharge">(""); + const [modal, setModal] = useState<"" | "invite" | "limit" | "recharge" | "gen-invite">(""); const [search, setSearch] = useState(""); // 团队成员 + 团队动态本页懒加载(不再走全局 bootstrap);成员/充值操作后 bump 刷新 @@ -141,6 +141,13 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea // 分享凭据弹窗(创建成功 / 重置成功后弹):captured creds(仅展示一次) const [share, setShare] = useState<{ mode: "create" | "reset"; name: string; username: string; password: string } | null>(null); + // 邀请码生成(凭码注册即加入本团队,不重复发额度) + const [invRole, setInvRole] = useState("member"); + const [invMonthly, setInvMonthly] = useState("2000"); + const [inviteResult, setInviteResult] = useState(null); + const [inviteBusy, setInviteBusy] = useState(false); + const [inviteErr, setInviteErr] = useState(""); + const rows: TeamMember[] = members.length ? members : [{ id: "owner", role: "owner", status: "active", monthly_credit_limit: "0", user } as TeamMember]; @@ -282,6 +289,9 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea 充值 + @@ -434,6 +444,56 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea {/* 设置月限额(团队级)· 预设 pill + 实时剩余 */} + } + close={() => setModal("")} + footer={inviteResult + ? + : } + > +
+ {!inviteResult ? ( + <> +
+ + +
+
+ + setInvMonthly(e.target.value)} /> +
+ {inviteErr &&
{inviteErr}
} + + ) : ( +
+
+
邀请码
{inviteResult.code}
+ +
+
+
邀请链接
{`${location.origin}${inviteResult.register_url}`}
+ +
+
// 角色 {invRole === "admin" ? "团队管理员" : "成员"} · 7 天有效 · 凭码注册即加入,不重复发额度
+
+ )} +
+
+ localStorage.setItem("airshelf_token", t), TOKEN); await seed.close(); +const p = await ctx.newPage(); const errs = []; +p.on("console", m => { if (m.type()==="error") errs.push(m.text()); }); +p.on("pageerror", e => errs.push("PAGEERR:"+e.message)); +await p.goto(BASE + "/team", { waitUntil: "load" }); await p.waitForTimeout(1500); +const r = { inviteBtn: await p.locator("#open-gen-invite").count() }; +if (r.inviteBtn) { + await p.click("#open-gen-invite"); await p.waitForTimeout(450); + r.modalShown = await p.locator(".modal-bg.show").count(); + r.roleSelect = await p.locator(".invite-gen-modal select").count(); + r.genBtnText = await p.locator(".modal-f .btn-primary").innerText().catch(()=>""); + await p.screenshot({ path: path.join(OUT, "team-gen-invite.png") }); + await p.keyboard.press("Escape"); await p.waitForTimeout(350); + r.closedByEsc = (await p.locator(".modal-bg").count()) === 0; +} +r.consoleErrors = errs.filter(e => !/Invitation|invitations|500|Failed to load resource/i.test(e)); +await b.close(); console.log(JSON.stringify(r, null, 2));