feat(core/frontend): wire P0 team mgmt / recharge / notifications
- App.tsx: load notifications, expose team/recharge/notification handlers, real unread bell count (was hardcoded 12) - team.tsx: create/edit/reset-password/remove member + team recharge modals - account.tsx: recharge via wechat/alipay buttons + custom amount - messages.tsx: real notification inbox, mark-read on select, mark-all-read - verified: tsc --noEmit clean; e2e create->update->reset->recharge->mark-read->delete all green Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d41e487f08
commit
25bf3293df
@@ -1,9 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { CircleDollarSign, UserPlus } from "lucide-react";
|
||||
import { CircleDollarSign, KeyRound, UserPlus } from "lucide-react";
|
||||
import type { BillingSummary, Team, TeamMember, User } from "../types";
|
||||
import type { Page } from "./route-config";
|
||||
import { money } from "./stage-config";
|
||||
import { TeamModal } from "../components/overlays";
|
||||
import { ConfirmModal, TeamModal } from "../components/overlays";
|
||||
|
||||
// 角色 → pill key/label(对齐 api-bridge roleUi)
|
||||
function roleUi(role: string): { key: "super" | "admin" | "member"; label: string } {
|
||||
@@ -24,16 +24,43 @@ const PERM_ROWS: Array<{ cap: string; cells: [string, string, string]; last?: bo
|
||||
{ cap: "创建项目 / 用 AI 流程", cells: ["✓", "✓", "✓"], last: true }
|
||||
];
|
||||
|
||||
export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
export function TeamPage({ team, user, members, billing, navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: {
|
||||
team: Team;
|
||||
user: User;
|
||||
members: TeamMember[];
|
||||
billing: BillingSummary | null;
|
||||
navigate: (page: Page) => void;
|
||||
onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
|
||||
onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
|
||||
onRemoveMember: (id: string) => void | Promise<unknown>;
|
||||
onResetPassword: (id: string, password: string) => void | Promise<unknown>;
|
||||
onRecharge: (amount: number, bonus: number) => void | Promise<unknown>;
|
||||
}) {
|
||||
const [modal, setModal] = useState<"" | "invite" | "limit">("");
|
||||
const [modal, setModal] = useState<"" | "invite" | "limit" | "recharge">("");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// 创建账户表单
|
||||
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<TeamMember | null>(null);
|
||||
const [edRole, setEdRole] = useState("member");
|
||||
const [edMonthly, setEdMonthly] = useState("");
|
||||
|
||||
// 重置密码
|
||||
const [resetTarget, setResetTarget] = useState<TeamMember | null>(null);
|
||||
const [resetPwd, setResetPwd] = useState("");
|
||||
|
||||
// 移除成员
|
||||
const [removeTarget, setRemoveTarget] = useState<TeamMember | null>(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];
|
||||
@@ -52,6 +79,55 @@ export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
return !needle || `${name} ${email}`.toLowerCase().includes(needle);
|
||||
});
|
||||
|
||||
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;
|
||||
await onCreateMember({
|
||||
username: cuUser.trim(),
|
||||
password: cuPass,
|
||||
name: cuName.trim() || undefined,
|
||||
role: cuRole,
|
||||
monthly_credit_limit: Number(cuMonthly) || 0
|
||||
});
|
||||
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 (
|
||||
<section className="team-page">
|
||||
<div className="page-head">
|
||||
@@ -80,7 +156,7 @@ export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
<div className="meta">// 团队 ID: {team.id} · {rows.length} 名成员</div>
|
||||
</div>
|
||||
<div className="banner-actions">
|
||||
<button className="btn btn-sm" type="button" onClick={() => navigate("account")}>
|
||||
<button className="btn btn-sm" type="button" onClick={() => setModal("recharge")}>
|
||||
<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>
|
||||
@@ -119,7 +195,7 @@ export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
<div className="h">
|
||||
<h3>团队动态</h3>
|
||||
<span className="ct">// 真实动态接口待接入</span>
|
||||
<a className="more" id="open-feed-all" role="button" tabIndex={0}>全部 →</a>
|
||||
<a className="more" id="open-feed-all" role="button" tabIndex={0} onClick={() => navigate("messages")}>全部 →</a>
|
||||
</div>
|
||||
<div className="feed-list">
|
||||
<div className="feed-item">
|
||||
@@ -171,9 +247,9 @@ export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
<td><div className="acts">{isOwner
|
||||
? <span style={{ fontFamily: "var(--font-mono)", fontSize: "10.5px", color: "var(--black-alpha-32)", alignSelf: "center" }}>不可编辑</span>
|
||||
: <>
|
||||
<button className="icon-btn-sm" type="button" title="编辑"><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="重置密码"><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="移出"><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>
|
||||
<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 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>
|
||||
);
|
||||
@@ -207,8 +283,83 @@ export function TeamPage({ team, user, members, billing, navigate }: {
|
||||
</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>
|
||||
<TeamModal open={modal === "invite"} title="创建账户" subtitle="// 直接生成账号 · 分享给成员登录" icon={<UserPlus size={16} />} close={() => setModal("")}><div className="field"><label className="field-label">用户名</label><input className="input" defaultValue="zhang.yunying" /></div><div className="field"><label className="field-label">登录密码</label><input className="input mono" defaultValue="AirShelf2026" /></div></TeamModal>
|
||||
{/* 设置月限额(团队级)· 注:后端暂无团队级限额端点,真实限额请在「编辑成员」逐人设置 */}
|
||||
<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>
|
||||
|
||||
{/* 创建账户 */}
|
||||
<TeamModal
|
||||
open={modal === "invite"}
|
||||
title="创建账户"
|
||||
subtitle="// 直接生成账号 · 分享给成员登录"
|
||||
icon={<UserPlus size={16} />}
|
||||
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>
|
||||
<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>
|
||||
|
||||
{/* 团队充值 */}
|
||||
<TeamModal
|
||||
open={modal === "recharge"}
|
||||
title="团队充值"
|
||||
subtitle="// 充值后立即到账 · 仅超管可操作"
|
||||
icon={<CircleDollarSign size={16} />}
|
||||
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>
|
||||
</TeamModal>
|
||||
|
||||
{/* 编辑成员 */}
|
||||
<TeamModal
|
||||
open={!!editTarget}
|
||||
title="编辑成员"
|
||||
subtitle={editTarget ? `// ${editTarget.user.username || editTarget.user.email}` : ""}
|
||||
icon={<UserPlus size={16} />}
|
||||
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>
|
||||
<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}` : ""}
|
||||
icon={<KeyRound size={16} />}
|
||||
close={() => setResetTarget(null)}
|
||||
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>
|
||||
</TeamModal>
|
||||
|
||||
{/* 移除成员确认 */}
|
||||
<ConfirmModal
|
||||
open={!!removeTarget}
|
||||
title="移除成员"
|
||||
detail={removeTarget ? `确认将「${removeTarget.user.username || removeTarget.user.email}」移出团队?移除后该成员将失去登录与访问权限。` : ""}
|
||||
confirmText="移除"
|
||||
onCancel={() => setRemoveTarget(null)}
|
||||
onConfirm={submitRemove}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user