优化后台
This commit is contained in:
@@ -358,19 +358,13 @@ export function App() {
|
||||
}, [page, route.projectId, route.productId]);
|
||||
|
||||
// 平台后台 gating(身份就绪后):
|
||||
// - 平台超管打开站点根路径 / 登录页:直接进 /admin,不先落全能创作再点侧栏
|
||||
// - 无团队的超管:任何非 admin 路由都送进 /admin(否则普通外壳因 !team 永久卡 loading)
|
||||
// - 平台超管不进工作台:任何非 /admin 路由都送回后台
|
||||
// - 非超管访问 /admin/*:纠回工作台
|
||||
// 已有团队的超管从后台点「返回工作台」会落到 /omni-create,这里不拦。
|
||||
useLayoutEffect(() => {
|
||||
if (booting || !user?.is_platform_admin || route.admin !== undefined) return;
|
||||
const path = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
const defaultEntry = path === "/" || path === "/login" || path === "/dashboard";
|
||||
if (!team || defaultEntry) {
|
||||
navigateAdmin("", { replace: true });
|
||||
}
|
||||
navigateAdmin("", { replace: true });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [booting, user, team, route.admin]);
|
||||
}, [booting, user, route.admin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (booting || !user) return;
|
||||
@@ -396,12 +390,11 @@ export function App() {
|
||||
useLayoutEffect(() => {
|
||||
if (booting || !user || page !== "dashboard") return;
|
||||
if (route.admin !== undefined) return;
|
||||
const path = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
// 同一次渲染里超管入口已经改去 /admin,这里不能再改写成全能创作。
|
||||
if (user.is_platform_admin && (path === "/" || path === "/login" || path === "/dashboard" || !team)) return;
|
||||
// 超管由上面的后台 gating 接走,这里不能再改写成全能创作。
|
||||
if (user.is_platform_admin) return;
|
||||
navigate("omniCreate", { replace: true });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [booting, user, team, page, route.admin]);
|
||||
}, [booting, user, page, route.admin]);
|
||||
|
||||
// Load preferences + sessions when entering settings.
|
||||
// 进创作相关页时重拉模型目录,避免后台刚改能力/积分,前端还捧着首屏缓存
|
||||
@@ -901,14 +894,10 @@ export function App() {
|
||||
setTeam(payload.team);
|
||||
setRole(payload.role || "");
|
||||
setBooting(false);
|
||||
// 平台超管登录后直接进后台。有团队时后台补拉工作台数据,供「返回工作台」使用;
|
||||
// 无团队则不拉团队级接口(products/projects 会因无团队报错)。
|
||||
// 平台超管只进后台,不拉工作台数据。
|
||||
if (payload.user.is_platform_admin) {
|
||||
setAuthed(true);
|
||||
navigateAdmin("", { replace: true });
|
||||
if (payload.team) {
|
||||
loadDataWithRetry().catch((error) => console.error("[login] data hydrate failed:", error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
navigate("omniCreate", { replace: true });
|
||||
@@ -986,9 +975,7 @@ export function App() {
|
||||
<AdminApp
|
||||
section={route.admin}
|
||||
user={user}
|
||||
team={team}
|
||||
navigateAdmin={navigateAdmin}
|
||||
navigate={navigate}
|
||||
logout={logout}
|
||||
/>
|
||||
{sessionInvalidatedModal}
|
||||
|
||||
@@ -90,24 +90,6 @@
|
||||
border-top: 1px solid var(--st-line);
|
||||
background: rgba(28, 34, 43, 0.02);
|
||||
}
|
||||
.admin-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--st-muted);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.admin-back:hover { color: var(--st-text); background: var(--st-hover); }
|
||||
.admin-back svg { width: 16px; height: 16px; }
|
||||
|
||||
.admin-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -113,9 +113,15 @@
|
||||
|
||||
.models-page .ml-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 1280px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
.models-page .ml-card {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -281,7 +287,10 @@
|
||||
.models-page .ml-sel-bar { left: 50%; }
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); }
|
||||
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* 模特详情弹窗:参考视频项目角色详情结构 */
|
||||
|
||||
@@ -879,11 +879,27 @@
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.omni-case-visual img {
|
||||
transition: transform 260ms ease;
|
||||
}
|
||||
|
||||
/* 预览视频叠在封面上。播不起来时保持透明,封面继续可见。
|
||||
不要给 video 加 scale:Safari 里变换过的 video 会直接画成空白。 */
|
||||
.omni-case-visual video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.omni-case-visual video.is-playing {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.omni-case-card:hover .omni-case-visual img,
|
||||
.omni-case-card:hover .omni-case-visual video {
|
||||
.omni-case-card.active .omni-case-visual img {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowLeft, ArrowRight, LogOut } from "lucide-react";
|
||||
import { ArrowRight, LogOut } from "lucide-react";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import { CornerMarks, Decorations, ToastLike } from "../../components/app-shell";
|
||||
import { ConfirmModal } from "../../components/overlays";
|
||||
import type { Team, User } from "../../types";
|
||||
import type { NavigateFn } from "../route-config";
|
||||
import type { User } from "../../types";
|
||||
import { AdminLedgersPage, AdminQuotaPage } from "./admin-billing";
|
||||
import { AdminGovernancePage } from "./admin-governance";
|
||||
import { AdminInvitesPage } from "./admin-invites";
|
||||
@@ -70,13 +69,11 @@ const GROUP_HINT: Record<string, string> = {
|
||||
type AdminAppProps = {
|
||||
section: string;
|
||||
user: User;
|
||||
team: Team | null;
|
||||
navigateAdmin: (section: string, options?: { replace?: boolean }) => void;
|
||||
navigate: NavigateFn;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
export function AdminApp({ section, user, team, navigateAdmin, navigate, logout }: AdminAppProps) {
|
||||
export function AdminApp({ section, user, navigateAdmin, logout }: AdminAppProps) {
|
||||
const active = ADMIN_SECTIONS.find((s) => s.slug === section) || ADMIN_SECTIONS[0];
|
||||
const [toast, setToast] = useState<{ type: "success" | "error" | "info"; text: string } | null>(null);
|
||||
const [logoutOpen, setLogoutOpen] = useState(false);
|
||||
@@ -143,12 +140,6 @@ export function AdminApp({ section, user, team, navigateAdmin, navigate, logout
|
||||
})}
|
||||
</div>
|
||||
<div className="admin-foot">
|
||||
{team && (
|
||||
<button type="button" className="admin-back" onClick={() => navigate("dashboard")}>
|
||||
<ArrowLeft size={16} strokeWidth={1.8} />
|
||||
返回工作台
|
||||
</button>
|
||||
)}
|
||||
<div className="admin-user">
|
||||
<div className="av">{(user.username || "A").slice(0, 1).toUpperCase()}</div>
|
||||
<div className="admin-user-meta">
|
||||
|
||||
@@ -71,6 +71,37 @@ const IMAGE_PRESETS: PresetItem[] = [
|
||||
const MENTION_REF_LIMIT = 20;
|
||||
const ROLE_REF_LIMIT = 3;
|
||||
|
||||
function playPresetPreview(card: HTMLElement) {
|
||||
const video = card.querySelector<HTMLVideoElement>("video");
|
||||
if (!video || video.dataset.broken === "1") return;
|
||||
const reveal = () => {
|
||||
if (video.dataset.broken === "1" || video.videoWidth === 0) return;
|
||||
video.classList.add("is-playing");
|
||||
};
|
||||
const fail = () => {
|
||||
video.dataset.broken = "1";
|
||||
video.classList.remove("is-playing");
|
||||
video.pause();
|
||||
};
|
||||
video.addEventListener("error", fail, { once: true });
|
||||
video.addEventListener("playing", reveal, { once: true });
|
||||
void video.play().catch(fail);
|
||||
}
|
||||
|
||||
function stopPresetPreview(card: HTMLElement) {
|
||||
const video = card.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
if (video.dataset.broken !== "1" && Number.isFinite(video.duration)) {
|
||||
try {
|
||||
video.currentTime = 0;
|
||||
} catch {
|
||||
/* Safari 在还没解码时不允许回绕 */
|
||||
}
|
||||
}
|
||||
video.classList.remove("is-playing");
|
||||
}
|
||||
|
||||
const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [
|
||||
{ type: "asset", label: "素材", Icon: ImageIcon },
|
||||
{ type: "character", label: "角色", Icon: UserRound },
|
||||
@@ -671,39 +702,22 @@ export function OmniCreatePage({
|
||||
className={`omni-case-card${selectedCase?.name === card.name ? " active" : ""}`}
|
||||
data-mode={card.mode}
|
||||
key={card.name}
|
||||
onMouseEnter={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onMouseLeave={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onMouseEnter={(event) => playPresetPreview(event.currentTarget)}
|
||||
onMouseLeave={(event) => stopPresetPreview(event.currentTarget)}
|
||||
onFocus={(event) => playPresetPreview(event.currentTarget)}
|
||||
onBlur={(event) => stopPresetPreview(event.currentTarget)}
|
||||
onClick={() => {
|
||||
if (card.mode === "image") applyPreset(card);
|
||||
else setPreviewCase(card);
|
||||
}}
|
||||
>
|
||||
<span className="omni-case-visual">
|
||||
<img src={card.cover} alt="" />
|
||||
{card.previewVideo ? (
|
||||
<video muted loop playsInline preload="metadata" poster={card.cover} aria-label={`${card.title} 样片预览`}>
|
||||
<video muted loop playsInline preload="none" poster={card.cover} aria-hidden="true">
|
||||
<source src={card.previewVideo} type="video/mp4" />
|
||||
</video>
|
||||
) : (
|
||||
<img src={card.cover} alt="" />
|
||||
)}
|
||||
) : null}
|
||||
</span>
|
||||
<span className="omni-case-copy">
|
||||
<strong>{card.title}</strong>
|
||||
|
||||
Reference in New Issue
Block a user