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,21 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Bell, Search } from "lucide-react";
|
||||
import type { Notification } from "../types";
|
||||
import type { Page } from "./route-config";
|
||||
import { routeLabels } from "./route-config";
|
||||
|
||||
export function MessagesPage({ navigate }: { navigate: (page: Page) => void }) {
|
||||
const messages = [
|
||||
{ id: "m1", type: "task", priority: "ok", title: "补水面膜 · 痛点种草 v3 成片已完成", brief: "7 镜 · 40 秒 · ¥18.40 已结算。", body: "视频生成全部完成。", target: "pipeline" as Page },
|
||||
{ id: "m2", type: "billing", priority: "warn", title: "团队余额低于预警线", brief: "当前余额低于 ¥100。", body: "建议先充值或降低任务量。", target: "account" as Page }
|
||||
];
|
||||
// 通知的 related_url(.html 风格)→ 应用内 Page
|
||||
function targetPage(n: Notification): Page {
|
||||
const url = n.related_url || "";
|
||||
if (url.includes("pipeline")) return "pipeline";
|
||||
if (url.includes("account")) return "account";
|
||||
if (url.includes("library")) return "library";
|
||||
if (url.includes("settings")) return "settingsNotify";
|
||||
if (url.includes("product")) return "products";
|
||||
return "dashboard";
|
||||
}
|
||||
|
||||
export function MessagesPage({ notifications, unreadCount, onMarkRead, onMarkAllRead, navigate }: {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
onMarkRead: (id: string) => void | Promise<unknown>;
|
||||
onMarkAllRead: () => void | Promise<unknown>;
|
||||
navigate: (page: Page) => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedId, setSelectedId] = useState(messages[0].id);
|
||||
const visible = messages.filter((item) => !query || `${item.title} ${item.brief}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const selected = messages.find((item) => item.id === selectedId) || visible[0] || messages[0];
|
||||
const [selectedId, setSelectedId] = useState<string>("");
|
||||
|
||||
const visible = notifications.filter(
|
||||
(item) => !query || `${item.title} ${item.brief}`.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
const selected = notifications.find((item) => item.id === selectedId) || visible[0] || notifications[0] || null;
|
||||
|
||||
// 默认选中第一条(不自动标已读;仅显式点选才标)
|
||||
useEffect(() => {
|
||||
if (!selectedId && notifications.length) setSelectedId(notifications[0].id);
|
||||
}, [selectedId, notifications]);
|
||||
|
||||
function selectItem(item: Notification) {
|
||||
setSelectedId(item.id);
|
||||
if (!item.is_read) void onMarkRead(item.id);
|
||||
}
|
||||
|
||||
const target = selected ? targetPage(selected) : "dashboard";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-head"><div><h1>消息中心</h1><div className="sub"><span className="mono">// {messages.length} 条总计</span> 任务提醒 · 团队协作 · 计费与系统公告</div></div><div className="actions"><button className="btn" type="button" onClick={() => navigate("settingsNotify")}>通知设置</button></div></div>
|
||||
<div className="msg-workbench"><section className="msg-panel msg-inbox"><div className="msg-panel-h"><span className="ti">收件箱</span><span className="mono">// 显示 {visible.length} 条</span></div><div className="msg-search"><Search size={14} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索项目、来源、内容" /></div><div className="msg-list">{visible.map((item) => <button className={`msg-item ${selected.id === item.id ? "active" : ""}`} type="button" key={item.id} onClick={() => setSelectedId(item.id)}><span className={`msg-type-ic ${item.type}`}><Bell size={13} /></span><span className="msg-item-main"><span className="msg-item-title">{item.title}</span><span className="msg-brief">{item.brief}</span></span></button>)}</div></section><section className="msg-panel msg-detail"><div className="msg-detail-body"><div className="msg-detail-top"><span className={`msg-type-ic ${selected.type}`}><Bell size={15} /></span><div className="msg-detail-title"><h2>{selected.title}</h2><div className="meta"><span>{selected.type}</span></div></div></div><p className="msg-body-text">{selected.body}</p><div className="msg-props"><span className="k">关联资源</span><span className="v">{routeLabels[selected.target]}</span></div></div><div className="msg-detail-f"><span className="spacer" /><button className="btn btn-primary" type="button" onClick={() => navigate(selected.target)}>进入{routeLabels[selected.target]}</button></div></section></div>
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>消息中心</h1>
|
||||
<div className="sub">
|
||||
<span className="mono">// {notifications.length} 条总计 · {unreadCount} 未读</span> 任务提醒 · 团队协作 · 计费与系统公告
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn" type="button" onClick={() => void onMarkAllRead()} disabled={unreadCount === 0}>全部已读</button>
|
||||
<button className="btn" type="button" onClick={() => navigate("settingsNotify")}>通知设置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="msg-workbench">
|
||||
<section className="msg-panel msg-inbox">
|
||||
<div className="msg-panel-h"><span className="ti">收件箱</span><span className="mono">// 显示 {visible.length} 条</span></div>
|
||||
<div className="msg-search"><Search size={14} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索项目、来源、内容" /></div>
|
||||
<div className="msg-list">
|
||||
{visible.length === 0 && (
|
||||
<div className="msg-empty" style={{ padding: "24px 16px", color: "var(--black-alpha-48)", fontSize: "12px", fontFamily: "var(--font-mono)" }}>// 暂无消息</div>
|
||||
)}
|
||||
{visible.map((item) => (
|
||||
<button className={`msg-item ${selected?.id === item.id ? "active" : ""} ${item.is_read ? "" : "unread"}`} type="button" key={item.id} onClick={() => selectItem(item)}>
|
||||
<span className={`msg-type-ic ${item.notification_type}`}><Bell size={13} /></span>
|
||||
<span className="msg-item-main"><span className="msg-item-title">{item.title}</span><span className="msg-brief">{item.brief}</span></span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="msg-panel msg-detail">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="msg-detail-body">
|
||||
<div className="msg-detail-top">
|
||||
<span className={`msg-type-ic ${selected.notification_type}`}><Bell size={15} /></span>
|
||||
<div className="msg-detail-title">
|
||||
<h2>{selected.title}</h2>
|
||||
<div className="meta"><span>{selected.source || selected.notification_type}</span>{selected.stage ? <span> · {selected.stage}</span> : null}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="msg-body-text">{selected.body || selected.brief}</p>
|
||||
<div className="msg-props"><span className="k">关联资源</span><span className="v">{routeLabels[target]}</span></div>
|
||||
</div>
|
||||
<div className="msg-detail-f"><span className="spacer" /><button className="btn btn-primary" type="button" onClick={() => navigate(target)}>进入{routeLabels[target]}</button></div>
|
||||
</>
|
||||
) : (
|
||||
<div className="msg-detail-body"><p className="msg-body-text">// 选择左侧一条消息查看详情</p></div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user