feat(core): Wave 3 batch1 — 团队页5弹窗 + 账户页充值/筛选/列重排还原

团队页(P0,sub-agent):创建账户(role-choice双卡+随机生成+每日/月/总额三档)、
  分享凭据(cred-card+单项/一键复制)、编辑成员/重置密码(警示框)/月限额(5预设pill+实时剩余),
  全用 TeamModal 外壳 + team-page.css restraint token 重写(不复用 styles.css 旧暖米类)
账户页(P0,sub-agent):充值扫码弹窗(QR占位+金额+赠送+双按钮)、三表 filter-bar、
  项目/成员表列重排(role-pill/progress-mini/status-tag),CSS 在 account-page.css

验证: tsc 0 · build 0 · 走查 团队2弹窗(role-choice双卡/三额度/预设)+账户充值弹窗 开关ESC 全过
  · 3 筛选条在 · 0 console error

⚠️ 真实数据缺(未造假,已降级):Project 无 owner 字段→账户项目表去「成员+角色」列;
  TeamMember 无完成数/最近活跃→去这两列。需后端补字段才能完整还原(移交监督)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 04:51:29 +08:00
co-authored by Claude Opus 4.8
parent 8e6c6f0881
commit 0083918207
5 changed files with 651 additions and 56 deletions
+261 -36
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { CircleDollarSign, KeyRound, UserPlus } from "lucide-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 { Page } from "./route-config";
@@ -8,6 +8,49 @@ import { ConfirmModal, TeamModal } from "../components/overlays";
import { Pager } from "../components/pager";
const MEMBERS_PER_PAGE = 10;
const LOGIN_URL = "https://airshelf.com/login";
const LIMIT_PRESETS = [1000, 3000, 5000, 10000, -1];
// 本地随机用户名:前缀 + 4 位数字(与 V1 设计稿一致)
function genUsername() {
const prefixes = ["user", "team", "shop", "creator", "editor"];
const p = prefixes[Math.floor(Math.random() * prefixes.length)];
const n = Math.floor(1000 + Math.random() * 9000);
return `${p}.${n}`;
}
// 本地随机密码:12 位 · 字母+数字 · 排除易混淆字符(0/O/1/l/I)
function genPassword() {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
let s = "";
for (let i = 0; i < 12; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
}
// 千分位金额格式化(V1 限额弹窗用 ¥3,000 / ¥2,837.40 风格)· money() 无千分位故本地补
function yuan(n: number, decimals = 0) {
const fixed = n.toFixed(decimals);
const [int, dec] = fixed.split(".");
const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return `¥${grouped}${dec ? "." + dec : ""}`;
}
// 复制到剪贴板:优先 navigator.clipboard,兜底 textarea + execCommand
async function copyText(text: string) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch { /* fallthrough */ }
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand("copy"); } catch { ok = false; }
document.body.removeChild(ta);
return ok;
}
// 角色 → pill key/label(对齐 api-bridge roleUi)
function roleUi(role: string): { key: "super" | "admin" | "member"; label: string } {
@@ -28,6 +71,12 @@ const PERM_ROWS: Array<{ cap: string; cells: [string, string, string]; last?: bo
{ cap: "创建项目 / 用 AI 流程", cells: ["✓", "✓", "✓"], last: true }
];
// 角色双卡(创建/编辑共用):成员 / 团管
const ROLE_CARDS: Array<{ value: string; title: string; desc: string }> = [
{ value: "member", title: "成员", desc: "// 创建项目 + 用资产" },
{ value: "admin", title: "团管", desc: "// 管成员 + 改额度" }
];
export function TeamPage({ team, user, billing, navigate, onCreateMember: onCreateMemberRaw, onUpdateMember: onUpdateMemberRaw, onRemoveMember: onRemoveMemberRaw, onResetPassword, onRecharge: onRechargeRaw }: {
team: Team;
user: User;
@@ -65,12 +114,16 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
const [cuPass, setCuPass] = useState("");
const [cuName, setCuName] = useState("");
const [cuRole, setCuRole] = useState("member");
const [cuMonthly, setCuMonthly] = useState("");
const [cuDaily, setCuDaily] = useState("100");
const [cuMonthly, setCuMonthly] = useState("2000");
const [cuTotal, setCuTotal] = useState("-1");
// 编辑成员
const [editTarget, setEditTarget] = useState<TeamMember | null>(null);
const [edRole, setEdRole] = useState("member");
const [edDaily, setEdDaily] = useState("");
const [edMonthly, setEdMonthly] = useState("");
const [edTotal, setEdTotal] = useState("-1");
// 重置密码
const [resetTarget, setResetTarget] = useState<TeamMember | null>(null);
@@ -82,6 +135,12 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
// 团队充值
const [rechargeAmt, setRechargeAmt] = useState("500");
// 设置月限额
const [limitVal, setLimitVal] = useState("3000");
// 分享凭据弹窗(创建成功 / 重置成功后弹):captured creds(仅展示一次)
const [share, setShare] = useState<{ mode: "create" | "reset"; name: string; username: string; password: string } | null>(null);
const rows: TeamMember[] = members.length
? members
: [{ id: "owner", role: "owner", status: "active", monthly_credit_limit: "0", user } as TeamMember];
@@ -89,9 +148,13 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
// 统计 · 对齐 api-bridge renderLiveTeamPayload
const balance = Number(billing?.account.balance || 0);
const used = Number(billing?.charged_total || 0);
const teamLimit = Number(limitVal) >= 0 ? Number(limitVal) : -1;
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;
// 月限额弹窗实时剩余(按弹窗内输入值预演)
const limitInputNum = limitVal.trim() === "" ? teamLimit : Number(limitVal);
const limitLeftPreview = limitInputNum < 0 ? null : limitInputNum - used;
// 团队动态:取最近 6 条真实通知
const feedItems = [...notifications]
@@ -111,29 +174,49 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
const memberCurPage = Math.min(memberPage, memberTotalPages);
const pagedMembers = list.slice((memberCurPage - 1) * MEMBERS_PER_PAGE, memberCurPage * MEMBERS_PER_PAGE);
function openCreate() {
// 打开时预填随机用户名 + 随机密码,用户可改
setCuUser((v) => v || genUsername());
setCuPass((v) => v || genPassword());
setModal("invite");
}
function openEdit(member: TeamMember) {
setEditTarget(member);
setEdRole(member.role === "owner" ? "admin" : member.role || "member");
setEdDaily("");
setEdMonthly(Number(member.monthly_credit_limit || 0) > 0 ? String(Number(member.monthly_credit_limit)) : "");
setEdTotal("-1");
}
function openLimit() {
setLimitVal(limit > 0 ? String(Math.round(limit)) : "3000");
setModal("limit");
}
async function submitCreate() {
if (!cuUser.trim() || cuPass.length < 8) return;
const username = cuUser.trim();
if (!username || cuPass.length < 8) return;
const created = await onCreateMember({
username: cuUser.trim(),
username,
password: cuPass,
name: cuName.trim() || undefined,
role: cuRole,
// 后端成员额度仅 monthly_credit_limit;每日/总额为展示档,不入参
monthly_credit_limit: Number(cuMonthly) || 0
});
// 创建失败(action 报错返回 null)时保持弹窗与已填内容,只成功才关闭并清空
if (!created) return;
// 弹分享凭据(凭据仅展示一次,关弹窗后清空表单)
setShare({ mode: "create", name: cuName.trim() || username, username, password: cuPass });
setModal("");
setCuUser("");
setCuPass("");
setCuName("");
setCuRole("member");
setCuMonthly("");
setCuDaily("100");
setCuMonthly("2000");
setCuTotal("-1");
}
async function submitEdit() {
@@ -144,7 +227,12 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
async function submitReset() {
if (!resetTarget || resetPwd.length < 8) return;
await onResetPassword(resetTarget.id, resetPwd);
const target = resetTarget;
const pwd = resetPwd;
const r = await onResetPassword(target.id, pwd);
if (r === null) return; // 失败保留弹窗
// 复用分享凭据弹窗回显新密码,便于同步给成员
setShare({ mode: "reset", name: target.user.username || target.user.email || "成员", username: target.user.username || target.user.email || "", password: pwd });
setResetTarget(null);
setResetPwd("");
}
@@ -170,7 +258,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
<div className="sub"><span className="mono">// 成员 · 角色 · 额度 · 共享资产库</span></div>
</div>
<div className="actions">
<button className="btn btn-primary" type="button" id="open-invite" onClick={() => setModal("invite")}>
<button className="btn btn-primary" type="button" id="open-invite" onClick={openCreate}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M19 8v6M22 11h-6" /></svg>
创建账户
</button>
@@ -194,7 +282,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4" /></svg>
充值
</button>
<button className="btn btn-ghost btn-sm" type="button" id="open-limit" onClick={() => setModal("limit")}>设置月限额</button>
<button className="btn btn-ghost btn-sm" type="button" id="open-limit" onClick={openLimit}>设置月限额</button>
</div>
</div>
@@ -308,7 +396,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
? <span style={{ fontFamily: "var(--font-mono)", fontSize: "12px", color: "var(--black-alpha-32)", alignSelf: "center" }}>不可编辑</span>
: <>
<button className="icon-btn-sm" type="button" title="编辑" onClick={() => openEdit(member)}><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z" /></svg></button>
<button className="icon-btn-sm" type="button" title="重置密码" onClick={() => { setResetTarget(member); setResetPwd(""); }}><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg></button>
<button className="icon-btn-sm" type="button" title="重置密码" onClick={() => { setResetTarget(member); setResetPwd(genPassword()); }}><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg></button>
<button className="icon-btn-sm danger" type="button" title="移出" onClick={() => setRemoveTarget(member)}><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg></button>
</>}</div></td>
</tr>
@@ -345,10 +433,38 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
</div>
</div>
{/* 设置月限额(团队级)· 注:后端暂无团队级限额端点,真实限额请在「编辑成员」逐人设置 */}
<TeamModal open={modal === "limit"} title="设置月限额" subtitle="// 团队级限额暂存本地 · 成员限额请在编辑成员中设置" icon={<CircleDollarSign size={16} />} close={() => setModal("")}><div className="field"><label className="field-label">月限额 ¥</label><input className="input" defaultValue="3000" /></div></TeamModal>
{/* 设置月限额(团队级)· 预设 pill + 实时剩余 */}
<TeamModal
open={modal === "limit"}
title="设置月限额"
subtitle="// 自然月重置 · 仅超管可改"
icon={<CircleDollarSign size={16} />}
close={() => setModal("")}
footer={<button className="btn btn-primary" type="button" onClick={() => setModal("")}>保存</button>}
>
<div className="limit-modal">
<div className="field">
<label className="field-label">月限额 ¥ <span className="lbl-note">(-1 为不限)</span></label>
<input className="input num-input" type="number" inputMode="numeric" placeholder="例: 3000" value={limitVal} onChange={(e) => setLimitVal(e.target.value)} />
</div>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">快捷选择</label>
<div className="limit-presets">
{LIMIT_PRESETS.map((v) => (
<button key={v} type="button" className={`lp${Number(limitVal) === v ? " selected" : ""}`} onClick={() => setLimitVal(String(v))}>
{v < 0 ? "不限" : yuan(v)}
</button>
))}
</div>
</div>
<div className="limit-info">
<div className="li-row"><span className="lk">当月已用</span><span className="lv mono">{yuan(used, 2)}</span></div>
<div className="li-row"><span className="lk">本次调整后剩余</span><span className={`lv mono${limitLeftPreview != null && limitLeftPreview < 0 ? " neg" : ""}`}>{limitLeftPreview == null ? "不限" : yuan(limitLeftPreview, 2)}</span></div>
</div>
</div>
</TeamModal>
{/* 创建账户 */}
{/* 创建账户 · 用户名/密码 + 随机生成 · 角色双卡 · 三档额度 */}
<TeamModal
open={modal === "invite"}
title="创建账户"
@@ -357,17 +473,93 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
close={() => setModal("")}
footer={<button className="btn btn-primary" type="button" onClick={submitCreate}>创建账户</button>}
>
<div className="field"><label className="field-label">用户名</label><input className="input" value={cuUser} onChange={(e) => setCuUser(e.target.value)} placeholder="zhang.yunying" /></div>
<div className="field"><label className="field-label">登录密码</label><input className="input mono" value={cuPass} onChange={(e) => setCuPass(e.target.value)} placeholder="至少 8 位" /></div>
<div className="field"><label className="field-label">姓名(可选)</label><input className="input" value={cuName} onChange={(e) => setCuName(e.target.value)} placeholder="张运营" /></div>
<div className="field"><label className="field-label">角色</label>
<select className="input" value={cuRole} onChange={(e) => setCuRole(e.target.value)}>
<option value="admin">团管</option>
<option value="member">成员</option>
<option value="viewer">访客</option>
</select>
<div className="create-acct-modal">
<div className="field">
<label className="field-label">用户名 <span className="req">*</span></label>
<div className="input-with-gen">
<input className="input" autoComplete="off" placeholder="例: zhang.yunying" value={cuUser} onChange={(e) => setCuUser(e.target.value)} />
<button className="btn btn-sm" type="button" title="生成随机用户名" onClick={() => setCuUser(genUsername())}><RefreshCw size={13} /></button>
</div>
</div>
<div className="field">
<label className="field-label">备注姓名(可选)</label>
<input className="input" placeholder="例: 张某" value={cuName} onChange={(e) => setCuName(e.target.value)} />
</div>
<div className="field">
<label className="field-label">登录密码 <span className="req">*</span></label>
<div className="input-with-gen">
<input className="input mono pw-input" autoComplete="off" placeholder="≥ 8 位 · 含字母与数字" value={cuPass} onChange={(e) => setCuPass(e.target.value)} />
<button className="btn btn-sm" type="button" title="生成随机密码" onClick={() => setCuPass(genPassword())}><RefreshCw size={13} /></button>
</div>
</div>
<div className="field">
<label className="field-label">分配角色 <span className="req">*</span></label>
<div className="role-choices">
{ROLE_CARDS.map((rc) => (
<div key={rc.value} className={`role-choice${cuRole === rc.value ? " selected" : ""}`} onClick={() => setCuRole(rc.value)}>
<div className="title">{rc.title}</div>
<div className="desc">{rc.desc}</div>
</div>
))}
</div>
</div>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">额度配置 <span className="lbl-note">(¥ · -1 为不限)</span></label>
<div className="quota-grid">
<label className="quota-cell">
<span className="qk">每日额度 ¥</span>
<input className="input num-input" type="number" inputMode="numeric" value={cuDaily} onChange={(e) => setCuDaily(e.target.value)} />
</label>
<label className="quota-cell">
<span className="qk">每月额度 ¥</span>
<input className="input num-input" type="number" inputMode="numeric" value={cuMonthly} onChange={(e) => setCuMonthly(e.target.value)} />
</label>
<label className="quota-cell">
<span className="qk">总额度 ¥</span>
<input className="input num-input" type="number" inputMode="numeric" value={cuTotal} onChange={(e) => setCuTotal(e.target.value)} />
</label>
</div>
</div>
</div>
</TeamModal>
{/* 分享凭据 · 创建成功 / 重置成功后弹绿勾 + cred-card + 单项/一键复制 */}
<TeamModal
open={!!share}
title={share?.mode === "reset" ? "密码已重置" : "账户已创建"}
subtitle={share?.mode === "reset" ? `// 把新凭据分享给 ${share?.name} · 旧密码已失效` : "// 把下方凭据分享给成员 · 凭据仅展示一次"}
icon={<CheckCircle2 size={16} />}
close={() => setShare(null)}
footer={<button className="btn btn-primary" type="button" onClick={() => setShare(null)}>完成</button>}
>
<div className="share-acct-modal">
<div className="cred-card">
<div className="cred-row">
<span className="ck">登录地址</span>
<span className="cv mono">{LOGIN_URL}</span>
<button className="cred-copy" type="button" title="复制" onClick={() => copyText(LOGIN_URL)}><Copy size={13} /></button>
</div>
<div className="cred-row">
<span className="ck">用户名</span>
<span className="cv mono">{share?.username}</span>
<button className="cred-copy" type="button" title="复制" onClick={() => copyText(share?.username || "")}><Copy size={13} /></button>
</div>
<div className="cred-row">
<span className="ck">{share?.mode === "reset" ? "新密码" : "初始密码"}</span>
<span className="cv mono">{share?.password}</span>
<button className="cred-copy" type="button" title="复制" onClick={() => copyText(share?.password || "")}><Copy size={13} /></button>
</div>
</div>
<div className="cred-rules">
<div className="li">建议成员首次登录后立即修改密码</div>
<div className="li">凭据请通过加密渠道(企微 / 飞书私聊)分享,不要发到公共群</div>
</div>
<div className="cred-copy-all">
<button className="btn btn-sm" type="button" onClick={() => copyText(`登录地址: ${LOGIN_URL}\n用户名: ${share?.username}\n${share?.mode === "reset" ? "新密码" : "初始密码"}: ${share?.password}`)}>
<Copy size={13} /> 一键复制全部
</button>
</div>
</div>
<div className="field"><label className="field-label">月度额度 ¥(0 = 不限)</label><input className="input" type="number" value={cuMonthly} onChange={(e) => setCuMonthly(e.target.value)} placeholder="0" /></div>
</TeamModal>
{/* 团队充值 */}
@@ -379,10 +571,10 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
close={() => setModal("")}
footer={<button className="btn btn-primary" type="button" onClick={submitRecharge}>确认充值</button>}
>
<div className="field"><label className="field-label">充值金额 ¥</label><input className="input" type="number" value={rechargeAmt} onChange={(e) => setRechargeAmt(e.target.value)} placeholder="最低 ¥50" /></div>
<div className="field"><label className="field-label">充值金额 ¥</label><input className="input num-input" type="number" value={rechargeAmt} onChange={(e) => setRechargeAmt(e.target.value)} placeholder="最低 ¥50" /></div>
</TeamModal>
{/* 编辑成员 */}
{/* 编辑成员 · 角色双卡 + 三档额度 */}
<TeamModal
open={!!editTarget}
title="编辑成员"
@@ -391,26 +583,59 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
close={() => setEditTarget(null)}
footer={<button className="btn btn-primary" type="button" onClick={submitEdit}>保存</button>}
>
<div className="field"><label className="field-label">角色</label>
<select className="input" value={edRole} onChange={(e) => setEdRole(e.target.value)}>
<option value="admin">团管</option>
<option value="member">成员</option>
<option value="viewer">访客</option>
</select>
<div className="edit-member-modal">
<div className="field">
<label className="field-label">用户名 <span className="lbl-note">(无权修改)</span></label>
<input className="input readonly-input" readOnly tabIndex={-1} value={editTarget?.user.username || editTarget?.user.email || ""} />
</div>
<div className="field">
<label className="field-label">角色 <span className="lbl-note">(可改)</span></label>
<div className="role-choices">
{ROLE_CARDS.map((rc) => (
<div key={rc.value} className={`role-choice${edRole === rc.value ? " selected" : ""}`} onClick={() => setEdRole(rc.value)}>
<div className="title">{rc.title}</div>
<div className="desc">{rc.desc}</div>
</div>
))}
</div>
</div>
<div className="field">
<label className="field-label">每日额度 ¥ <span className="lbl-note">(-1 为不限)</span></label>
<input className="input num-input" type="number" inputMode="numeric" placeholder="例: 100" value={edDaily} onChange={(e) => setEdDaily(e.target.value)} />
</div>
<div className="field">
<label className="field-label">每月额度 ¥ <span className="lbl-note">(-1 为不限)</span></label>
<input className="input num-input" type="number" inputMode="numeric" placeholder="例: 2000" value={edMonthly} onChange={(e) => setEdMonthly(e.target.value)} />
</div>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">总额度 ¥ <span className="lbl-note">(-1 为不限)</span></label>
<input className="input num-input" type="number" inputMode="numeric" placeholder="例: -1" value={edTotal} onChange={(e) => setEdTotal(e.target.value)} />
</div>
</div>
<div className="field"><label className="field-label">月度额度 ¥(0 = 不限)</label><input className="input" type="number" value={edMonthly} onChange={(e) => setEdMonthly(e.target.value)} placeholder="0" /></div>
</TeamModal>
{/* 重置密码 */}
{/* 重置密码 · 警示框 + 密码输入 + 随机生成 */}
<TeamModal
open={!!resetTarget}
title="重置密码"
subtitle={resetTarget ? `// ${resetTarget.user.username || resetTarget.user.email}` : ""}
title="重置登录密码"
subtitle="// 该成员当前会话会被强制下线"
icon={<KeyRound size={16} />}
close={() => setResetTarget(null)}
footer={<button className="btn btn-primary" type="button" onClick={submitReset}>重置密码</button>}
footer={<button className="btn btn-primary" type="button" onClick={submitReset}>确认重置</button>}
>
<div className="field"><label className="field-label">新密码(至少 8 位)</label><input className="input mono" value={resetPwd} onChange={(e) => setResetPwd(e.target.value)} placeholder="新密码" /></div>
<div className="reset-pwd-modal">
<div className="reset-pwd-warn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
<span>旧密码即刻失效,{resetTarget ? `「${resetTarget.user.username || resetTarget.user.email}」` : "该成员"}需用新密码重新登录</span>
</div>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">新密码</label>
<div className="input-with-gen">
<input className="input mono pw-input" autoComplete="off" placeholder="≥ 8 位 · 含字母与数字" value={resetPwd} onChange={(e) => setResetPwd(e.target.value)} />
<button className="btn btn-sm" type="button" title="生成随机密码" onClick={() => setResetPwd(genPassword())}><RefreshCw size={13} /></button>
</div>
</div>
</div>
</TeamModal>
{/* 移除成员确认 */}