feat(core): notification inbox infinite scroll + command palette fix (+ pending WIP)
消息中心:全量渲染 → 真·后端分页滚动加载 - backend(ops/views): NotificationPagination(10/页,page_size 可覆盖)+ 响应回 type_counts(按收件人绝对计数,不受分页/搜索影响) - frontend(messages): 自管分页,滚到底加载下一批;tab/搜索走服务端并重置到第1页; 代号作废在途旧请求防切换卡空白;乐观标已读;「已加载 X / Y」分母用当前筛选总数 - api/App/types: listNotifications 支持 page/page_size/search;allNotifications 携带 type_counts 命令面板(侧边栏搜索):修复点开后 UI 错位 - app-shell: 遮罩 className 漏了基类 shell-command-bg(只有 .show)致无定位塌到左下; 补回基类 + header 类名对齐 .shell-command-h - messages-page.css: 工作台收进视口高度,收件箱在面板内滚动 本次提交一并带入此前若干未提交 WIP(account/ai-tools/library/pipeline/products/settings + accounts/ai/assets/billing/projects 后端),按用户要求整体推 dev。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,13 +39,13 @@ function CommandPalette({ open, onClose, navigate }: { open: boolean; onClose: (
|
||||
return createPortal(
|
||||
<div
|
||||
id="shell-command-bg"
|
||||
className="show"
|
||||
className="shell-command-bg show"
|
||||
aria-hidden="false"
|
||||
onClick={(event) => { if (event.target === event.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="shell-command" role="dialog" aria-modal="true" aria-label="命令面板">
|
||||
<div className="shell-command-head">
|
||||
<IconKitSvg name="search" />
|
||||
<div className="shell-command-h">
|
||||
<span className="ic"><IconKitSvg name="search" /></span>
|
||||
<input
|
||||
id="shell-command-input"
|
||||
autoFocus
|
||||
|
||||
@@ -1,6 +1,45 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Shield, X } from "lucide-react";
|
||||
|
||||
// 通用媒体预览灯箱:点击图片放大 / 点击视频弹窗播放(复用 .np-lightbox 样式)
|
||||
// 背景点击 / Esc / 关闭键都可关闭;点媒体本身不关闭。
|
||||
export function MediaLightbox({ open, src, kind, name, close }: {
|
||||
open: boolean;
|
||||
src: string;
|
||||
kind?: "image" | "video";
|
||||
name?: string;
|
||||
close: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") close(); };
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [open, close]);
|
||||
if (!open || !src) return null;
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<div className="np-lightbox show" onClick={close}>
|
||||
<button className="lb-x" type="button" aria-label="关闭" onClick={close}><X /></button>
|
||||
{kind === "video" ? (
|
||||
<video
|
||||
src={src}
|
||||
controls
|
||||
autoPlay
|
||||
playsInline
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{ maxWidth: "90vw", maxHeight: "88vh", borderRadius: "var(--r-md)", boxShadow: "0 20px 60px rgba(0,0,0,.5)", background: "#000", cursor: "default" }}
|
||||
/>
|
||||
) : (
|
||||
<img src={src} alt={name || "预览"} onClick={(event) => event.stopPropagation()} style={{ cursor: "default" }} />
|
||||
)}
|
||||
{name && <div className="lb-name">{name}</div>}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRow({ title, desc, action, toggle, checked }: { title: string; desc: string; action?: string; toggle?: boolean; checked?: boolean }) {
|
||||
return (
|
||||
<div className="setting-row">
|
||||
@@ -20,7 +59,8 @@ export function TeamModal({ open, title, subtitle, icon, close, children, footer
|
||||
footer?: ReactNode;
|
||||
}) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<div className="modal-bg show" onClick={close}>
|
||||
<div className="modal invite-modal" onClick={(event) => event.stopPropagation()}>
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
@@ -28,7 +68,8 @@ export function TeamModal({ open, title, subtitle, icon, close, children, footer
|
||||
<div className="modal-b">{children}</div>
|
||||
<div className="modal-f"><button className="btn" type="button" onClick={close}>取消</button>{footer || <button className="btn btn-primary" type="button" onClick={close}>保存</button>}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +82,8 @@ export function ConfirmModal({ open, title, detail, confirmText, onCancel, onCon
|
||||
onConfirm: () => void | Promise<unknown>;
|
||||
}) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<div className="modal-bg show" onClick={onCancel}>
|
||||
<div className="modal" onClick={(event) => event.stopPropagation()}>
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
@@ -49,20 +91,23 @@ export function ConfirmModal({ open, title, detail, confirmText, onCancel, onCon
|
||||
<div className="modal-b">{detail}</div>
|
||||
<div className="modal-f"><button className="btn" type="button" onClick={onCancel}>取消</button><button className="btn btn-primary" type="button" onClick={() => void onConfirm()}>{confirmText}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function Drawer({ title, open, close, children }: { title: string; open: boolean; close: () => void; children: ReactNode }) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<>
|
||||
<div className="drawer-bg show" onClick={close} />
|
||||
<aside className="drawer show">
|
||||
<div className="drawer-h"><h3>{title}</h3><button className="x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button></div>
|
||||
<div className="drawer-b">{children}</div>
|
||||
</aside>
|
||||
</>
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user