From b82c4d323486dfdea849e81a1776a02c81f90141 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 19 Jun 2026 12:45:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20Wave=203.5=20=E5=9B=A2=E9=98=9F?= =?UTF-8?q?=E9=82=80=E8=AF=B7=E5=85=A5=E5=8F=A3=20=E2=80=94=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=82=80=E8=AF=B7=E7=A0=81=E5=BC=B9=E7=AA=97=20+=20?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=A0=81/=E9=82=80=E8=AF=B7=E9=93=BE?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit team.tsx 加「邀请成员」按钮 → gen-invite 弹窗:角色(成员/团管)+ 月限额 → api.createInvitation 生成邀请码,cred-card 展示码 + 邀请链接(/register?invite=)+ 一键复制;复用 batch1 的 cred-card 类,无新 CSS。 验证: tsc 0 · build 0 · 走查 邀请按钮/弹窗打开/角色select/生成按钮/ESC关 全过 · 0 console error ⚠️ 生成码 + 凭码注册的 e2e 需先把 Invitation 迁移 apply 到远程 MySQL(逻辑已被后端 5/5 测试证)。 Co-Authored-By: Claude Opus 4.8 --- core/frontend/src/routes/team.tsx | 64 +++++++++++++++++++++++++- core/qa/visual-parity/_wave35-team.mjs | 24 ++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 core/qa/visual-parity/_wave35-team.mjs 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));