完善前端UI
This commit is contained in:
+13
-30
@@ -21,7 +21,7 @@ import type {
|
||||
} from "./types";
|
||||
import { publicModelDisplayName } from "./model-display";
|
||||
import { generationErrorText } from "./generation-error";
|
||||
import { AccountMenu, CornerMarks, Decorations, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell";
|
||||
import { AccountMenu, CornerMarks, Decorations, ModeTabs, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell";
|
||||
import { SystemLoading } from "./components/loading";
|
||||
import {
|
||||
AccountPage,
|
||||
@@ -1132,23 +1132,7 @@ export function App() {
|
||||
<main>
|
||||
<Decorations />
|
||||
<header className="topbar">
|
||||
<div className="yz-mods" role="tablist" aria-label="创作模块">
|
||||
<button
|
||||
className={`yz-mod${topModule === "workbench" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => navigate("dashboard")}
|
||||
>工作台</button>
|
||||
<button
|
||||
className={`yz-mod${topModule === "image" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => navigate("assetFactory")}
|
||||
>图片创作</button>
|
||||
<button
|
||||
className={`yz-mod${topModule === "video" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => navigate("projects")}
|
||||
>视频创作</button>
|
||||
</div>
|
||||
<ModeTabs active={topModule} navigate={navigate} />
|
||||
<div className="right">
|
||||
<button className="top-search" type="button" onClick={() => openCommandPalette()} aria-label="搜索">
|
||||
<IconKitSvg name="search" />
|
||||
@@ -1178,23 +1162,22 @@ export function App() {
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="content" id="page-content">
|
||||
<div className="content" id="page-content" key={page}>
|
||||
<CornerMarks />
|
||||
{notice && <ToastLike notice={notice} />}
|
||||
{pipelinePage || renderPage()}
|
||||
</div>
|
||||
</main>
|
||||
{accountAnchor && (
|
||||
<AccountMenu
|
||||
anchorRect={accountAnchor}
|
||||
onClose={() => setAccountAnchor(null)}
|
||||
navigate={navigate}
|
||||
logout={logout}
|
||||
user={currentUser}
|
||||
team={currentTeam}
|
||||
canManageBilling={isOwner}
|
||||
/>
|
||||
)}
|
||||
<AccountMenu
|
||||
open={accountAnchor !== null}
|
||||
anchorRect={accountAnchor}
|
||||
onClose={() => setAccountAnchor(null)}
|
||||
navigate={navigate}
|
||||
logout={logout}
|
||||
user={currentUser}
|
||||
team={currentTeam}
|
||||
canManageBilling={isOwner}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+304
-108
@@ -1,117 +1,243 @@
|
||||
/* 账户页 · 对照影擎页头(返回方钮 + 大标题 + 灰摘要)。仅 scope 在 .account-page,不改共享 .btn/.pill/.input */
|
||||
/* 账单库 · 影擎没有独立页,按商品库/团队中心的页头、深色指标卡、data-panel 风格排。不改共享 .btn/.pill/.input */
|
||||
.account-page {
|
||||
--klein: #002fa7;
|
||||
--klein-hover: #002680;
|
||||
--st-muted: #6f747c;
|
||||
--st-text: #17181a;
|
||||
--st-black: #101012;
|
||||
--st-line: rgba(34, 42, 54, 0.10);
|
||||
--st-wash: rgba(34, 42, 54, 0.045);
|
||||
--st-shadow: 0 8px 20px rgba(20, 27, 38, 0.09);
|
||||
max-width: 1140px;
|
||||
padding-bottom: 44px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
box-sizing: border-box;
|
||||
margin: -24px -28px -60px;
|
||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||
|
||||
.page-head {
|
||||
.ac-head {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
.ac-head h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
font-weight: 600;
|
||||
letter-spacing: -.02em;
|
||||
color: var(--accent-black);
|
||||
}
|
||||
.ac-head p {
|
||||
margin: 0;
|
||||
color: var(--st-muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
.page-head h1 { font-size: 28px; }
|
||||
.page-head .sub { margin-top: 6px; font-size: 15px; color: var(--st-muted); }
|
||||
|
||||
.ac-back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: 0 0 36px;
|
||||
margin-top: 4px;
|
||||
.ac-top {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(28, 34, 43, 0.12);
|
||||
background: #fff;
|
||||
color: var(--st-text);
|
||||
cursor: pointer;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(430px, 0.9fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 28px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.ac-back:hover { background: rgba(34, 42, 54, 0.05); }
|
||||
.ac-back:focus-visible { outline: none; box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(0, 47, 167, 0.28); }
|
||||
.ac-back svg { width: 18px; height: 18px; display: block; }
|
||||
@media (max-width: 1120px) { .ac-top { grid-template-columns: 1fr; } }
|
||||
|
||||
.top-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(480px, .85fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
|
||||
@media (max-width: 1120px) { .top-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.balance-banner {
|
||||
background: #202124;
|
||||
color: #fff;
|
||||
padding: 26px 28px;
|
||||
position: relative;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
.ac-hero {
|
||||
min-height: 270px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
min-width: 0;
|
||||
min-height: 246px;
|
||||
justify-content: space-between;
|
||||
padding: 28px;
|
||||
border-radius: 14px;
|
||||
color: #fff;
|
||||
background: #202124;
|
||||
box-shadow: 0 14px 30px rgba(16, 16, 18, 0.18);
|
||||
}
|
||||
.balance-banner::before, .balance-banner::after,
|
||||
.balance-banner > .corner-tr, .balance-banner > .corner-bl {
|
||||
content: ''; position: absolute; width: 14px; height: 14px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain; pointer-events: none;
|
||||
.ac-hero > span,
|
||||
.ac-hero-stats span,
|
||||
.ac-progress-meta {
|
||||
color: rgba(255, 255, 255, 0.58);
|
||||
font-size: 12px;
|
||||
}
|
||||
.ac-hero > strong {
|
||||
display: block;
|
||||
margin: 8px 0 6px;
|
||||
font-size: 38px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.ac-hero > em {
|
||||
font-style: normal;
|
||||
color: rgba(255, 255, 255, 0.46);
|
||||
font-size: 12px;
|
||||
}
|
||||
.ac-hero-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18px;
|
||||
margin: 22px 0 18px;
|
||||
}
|
||||
.ac-hero-stats strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ac-progress {
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 99px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.ac-progress > span {
|
||||
display: block;
|
||||
min-width: 7px;
|
||||
height: 100%;
|
||||
background: var(--klein);
|
||||
}
|
||||
.ac-progress-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.balance-banner::before { top: -7px; left: -7px; }
|
||||
.balance-banner::after { bottom: -7px; right: -7px; }
|
||||
.balance-banner > .corner-tr { top: -7px; right: -7px; }
|
||||
.balance-banner > .corner-bl { bottom: -7px; left: -7px; }
|
||||
|
||||
.balance-hero { display: flex; flex-direction: column; gap: 4px; }
|
||||
.balance-hero .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.balance-hero .v { font-size: 38px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
||||
.balance-hero .meta { font-size: 11.5px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; }
|
||||
.ac-recharge {
|
||||
padding: 22px;
|
||||
border: 1px solid var(--st-line);
|
||||
border-radius: 14px;
|
||||
background: var(--st-wash);
|
||||
box-shadow: var(--st-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ac-recharge h2 {
|
||||
margin: 0 0 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--st-text);
|
||||
}
|
||||
.ac-recharge > p {
|
||||
margin: 0 0 18px;
|
||||
color: var(--st-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.ac-recharge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.ac-option {
|
||||
min-height: 66px;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(34, 42, 54, 0.10);
|
||||
border-radius: 10px;
|
||||
background: rgba(34, 42, 54, 0.035);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.ac-option:hover { background: rgba(34, 42, 54, 0.06); }
|
||||
.ac-option.active {
|
||||
border-color: var(--klein);
|
||||
background: rgba(0, 47, 167, 0.08);
|
||||
}
|
||||
.ac-option strong,
|
||||
.ac-option span { display: block; }
|
||||
.ac-option strong {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--st-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ac-option span {
|
||||
margin-top: 4px;
|
||||
color: #16a34a;
|
||||
font-size: 10px;
|
||||
}
|
||||
.ac-option span.plain { color: var(--st-muted); }
|
||||
.ac-option .ribbon {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 7px;
|
||||
font-size: 9px;
|
||||
padding: 1px 5px;
|
||||
background: var(--klein);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ac-pay-title {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--st-text);
|
||||
}
|
||||
.ac-recharge .input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
padding: 0 13px;
|
||||
border: 1px solid rgba(34, 42, 54, 0.11);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
.ac-pay-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.ac-pay {
|
||||
height: 42px;
|
||||
border: 1px solid rgba(28, 34, 43, 0.12);
|
||||
border-radius: 11px;
|
||||
background: #fff;
|
||||
color: var(--st-text);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ac-pay:hover { background: rgba(34, 42, 54, 0.05); }
|
||||
.ac-pay .pay-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 18px;
|
||||
}
|
||||
.ac-pay .pay-logo img { width: 100%; height: 100%; display: block; object-fit: cover; }
|
||||
.ac-tip {
|
||||
margin-top: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--st-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.ac-tip strong { color: var(--st-text); font-weight: 600; }
|
||||
@media (max-width: 720px) { .ac-recharge-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
|
||||
.balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 16px 0 0; border-top: 1px solid rgba(255,255,255,.1); }
|
||||
.balance-sub .col { min-width: 0; }
|
||||
.balance-sub .lbl { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,.5); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.balance-sub .v { font-size: 18px; font-weight: 600; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
||||
.balance-sub .meta { font-size: 10.5px; color: rgba(255,255,255,.42); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.pane {
|
||||
background: var(--st-wash);
|
||||
border: 1px solid var(--st-line);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--st-shadow);
|
||||
}
|
||||
.pane h3 { font-size: 18px; font-weight: 600; margin: 0 0 6px; color: var(--st-text); }
|
||||
.pane .desc { font-size: 12px; color: var(--st-muted); margin-bottom: 14px; }
|
||||
|
||||
.balance-foot { margin-top: auto; padding-top: 2px; }
|
||||
.balance-meter { height: 5px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); overflow: hidden; }
|
||||
.balance-meter > span { display: block; height: 100%; width: 5.4%; background: var(--klein); border-radius: inherit; }
|
||||
.balance-foot-meta { display: flex; justify-content: space-between; gap: 14px; margin-top: 8px; color: rgba(255,255,255,.46); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||||
|
||||
.pane { background: #fff; border: 1px solid var(--st-line); border-radius: 14px; padding: 20px; margin-bottom: 16px; box-shadow: var(--st-shadow); }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; color: var(--st-text); }
|
||||
.pane .desc { font-size: 11.5px; color: var(--st-muted); margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.topup-pane { display: flex; flex-direction: column; padding: 20px 22px; margin-bottom: 0; min-height: 246px; }
|
||||
.topup-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; }
|
||||
.topup-head h3 { margin-bottom: 5px; }
|
||||
.topup-head .desc { margin-bottom: 0; }
|
||||
.topup-selected { font-family: var(--font-mono); font-size: 11px; color: var(--st-muted); white-space: nowrap; padding-top: 2px; }
|
||||
|
||||
.recharge-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
||||
.recharge-card { min-height: 76px; border: 1px solid var(--st-line); border-radius: 10px; padding: 10px 8px; text-align: center; cursor: pointer; background: #fff; position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: background var(--t-base), border-color var(--t-base), box-shadow var(--t-base); }
|
||||
.recharge-card:hover { background: var(--st-wash); border-color: rgba(28, 34, 43, 0.18); }
|
||||
.recharge-card:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(0, 47, 167, 0.28); }
|
||||
.recharge-card.selected { border-color: rgba(0, 47, 167, 0.28); background: rgba(0, 47, 167, 0.07); box-shadow: inset 0 0 0 1px rgba(0, 47, 167, 0.18); }
|
||||
.recharge-card.selected::after { content: '✓'; position: absolute; top: 6px; right: 7px; width: 15px; height: 15px; border-radius: 50%; display: grid; place-items: center; background: var(--klein); color: #fff; font-size: 10px; line-height: 1; }
|
||||
.recharge-card .amt { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; color: var(--st-text); }
|
||||
.recharge-card .gift { font-size: 10px; color: var(--st-muted); margin-top: 4px; font-family: var(--font-mono); white-space: nowrap; }
|
||||
.recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }
|
||||
.recharge-card .ribbon { position: absolute; top: 6px; left: 7px; font-family: var(--font-mono); font-size: 9px; padding: 1px 5px; background: var(--klein); color: #fff; letter-spacing: .03em; font-weight: 600; border-radius: var(--r-sm); }
|
||||
.pay-row { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; padding-top: 0; border-top: 0; }
|
||||
.pay-title { font-size: 12px; font-weight: 600; color: var(--st-text); line-height: 1.2; }
|
||||
.pay-row .input { width: 100%; box-sizing: border-box; }
|
||||
.pay-btn-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.pay-method-btn { height: 38px; border-radius: 11px; display: inline-flex; justify-content: center; align-items: center; gap: 8px; }
|
||||
.pay-logo { width: 18px; height: 18px; border-radius: 6px; display: inline-block; flex: 0 0 18px; overflow: hidden; }
|
||||
.pay-logo img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: inherit; }
|
||||
@media (max-width: 720px) { .recharge-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
|
||||
.billing-tabs { display: flex; align-items: center; gap: 8px; border-bottom: 0; margin: 8px 0 18px; padding: 0; overflow-x: auto; scrollbar-width: none; }
|
||||
.billing-tabs { display: flex; align-items: center; gap: 8px; border-bottom: 0; margin: 0 0 18px; padding: 0; overflow-x: auto; scrollbar-width: none; }
|
||||
.billing-tabs::-webkit-scrollbar { display: none; }
|
||||
.ac-tab {
|
||||
display: inline-flex;
|
||||
@@ -143,33 +269,103 @@
|
||||
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }
|
||||
@media (max-width: 980px) { .overview-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.trend-pane { padding: 18px 20px 14px; display: flex; flex-direction: column; }
|
||||
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
|
||||
.trend-head h3 { margin-bottom: 0; }
|
||||
.trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--st-muted); letter-spacing: .02em; }
|
||||
.trend-pane { padding: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.trend-head {
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid rgba(34, 42, 54, 0.08);
|
||||
}
|
||||
.trend-head h3 { margin-bottom: 0; font-size: 18px; }
|
||||
.trend-head .sub { font-size: 12px; color: var(--st-muted); }
|
||||
.trend-head .spacer { flex: 1; }
|
||||
.ac-seg {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: var(--st-muted);
|
||||
background: rgba(34, 42, 54, 0.055);
|
||||
border-radius: 9px;
|
||||
color: #50555d;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ac-seg:hover { background: rgba(34, 42, 54, 0.10); color: var(--st-text); }
|
||||
.ac-seg.active { color: #fff; background: var(--st-black); }
|
||||
|
||||
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; min-height: 170px; flex: 1; padding: 6px 4px 2px; position: relative; }
|
||||
.trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }
|
||||
.trend-chart .bar { background: rgba(34, 42, 54, 0.06); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; height: 100%; }
|
||||
.trend-chart .bar > span { position: absolute; left: 0; bottom: 0; display: block; width: 100%; flex: none; min-height: 0; background: var(--klein); border-radius: 2px 2px 0 0; }
|
||||
.trend-chart .bar:hover > span { background: var(--st-black); }
|
||||
.trend-chart .bar.peak > span { background: var(--st-black); }
|
||||
.trend-chart .x-axis { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; font-family: var(--font-mono); font-size: 9.5px; color: var(--st-muted); text-align: center; letter-spacing: .02em; }
|
||||
.trend-foot { display: flex; gap: 14px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--st-line); font-size: 12px; }
|
||||
.ac-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 18px 20px 2px;
|
||||
}
|
||||
.ac-summary div {
|
||||
padding: 14px;
|
||||
border-radius: 11px;
|
||||
background: rgba(0, 47, 167, 0.055);
|
||||
}
|
||||
.ac-summary span,
|
||||
.ac-summary strong { display: block; }
|
||||
.ac-summary span { color: var(--st-muted); font-size: 11px; }
|
||||
.ac-summary strong { margin-top: 7px; font-size: 16px; font-weight: 600; color: var(--st-text); }
|
||||
|
||||
.trend-chart {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 30px 28px 22px;
|
||||
min-height: 0;
|
||||
}
|
||||
.trend-chart::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 42px;
|
||||
left: 28px;
|
||||
height: 1px;
|
||||
background: rgba(34, 42, 54, 0.12);
|
||||
}
|
||||
.ac-bar-group {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: var(--st-muted);
|
||||
font-size: 11px;
|
||||
min-width: 0;
|
||||
}
|
||||
.ac-bar {
|
||||
width: min(30px, 70%);
|
||||
min-height: 10px;
|
||||
border-radius: 7px 7px 3px 3px;
|
||||
background: var(--klein);
|
||||
opacity: 0.78;
|
||||
}
|
||||
.ac-bar-value {
|
||||
color: #343a45;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ac-bar-group span { overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
||||
.trend-chart-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--st-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.trend-foot { display: flex; gap: 14px; margin: 0 20px 16px; padding-top: 10px; border-top: 1px solid var(--st-line); font-size: 12px; }
|
||||
.trend-foot .item { display: flex; align-items: baseline; gap: 6px; }
|
||||
.trend-foot .item .k { color: var(--st-muted); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||||
.trend-foot .item .v { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--st-text); }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AlertCircle, Boxes, Check, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
||||
import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
||||
import { IconKitSvg } from "./IconKitSvg";
|
||||
import { useBodyScrollLock } from "./overlays";
|
||||
import { useBodyScrollLock, useOverlayTransition } from "./overlays";
|
||||
import type { Product, Project, Team, User } from "../types";
|
||||
import type { Notice, Page } from "../routes/route-config";
|
||||
|
||||
@@ -18,8 +18,8 @@ const SHELL_COMMANDS: Command[] = [
|
||||
{ id: "free-create", group: "导航", label: "自由创作", sub: "AI 视频生成 · 全能参考 / 首尾帧", page: "freeCreate", icon: "film", key: "F" },
|
||||
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
|
||||
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
|
||||
{ id: "account", group: "导航", label: "消费", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||
{ id: "settings", group: "导航", label: "设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||
{ id: "account", group: "导航", label: "账单库", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||
{ id: "settings", group: "导航", label: "系统设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
|
||||
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
|
||||
{ id: "new-project", group: "常用动作", label: "新建视频项目", sub: "选择商品并进入脚本配置", page: "projectWizard", icon: "clapperboard" },
|
||||
@@ -30,6 +30,7 @@ const SHELL_COMMANDS: Command[] = [
|
||||
|
||||
function CommandPalette({ open, onClose, navigate, canManageBilling = true }: { open: boolean; onClose: () => void; navigate: Navigate; canManageBilling?: boolean }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const { mounted, show } = useOverlayTransition(open, onClose, 220);
|
||||
useBodyScrollLock(open);
|
||||
useEffect(() => { if (open) setQuery(""); }, [open]);
|
||||
const items = useMemo(() => {
|
||||
@@ -40,13 +41,13 @@ function CommandPalette({ open, onClose, navigate, canManageBilling = true }: {
|
||||
.filter((cmd) => !q || [cmd.label, cmd.sub, cmd.group, cmd.id].join(" ").toLowerCase().includes(q));
|
||||
}, [query, canManageBilling]);
|
||||
const run = (cmd: Command) => { onClose(); navigate(cmd.page); };
|
||||
if (!open) return null;
|
||||
if (!mounted) return null;
|
||||
let lastGroup = "";
|
||||
return createPortal(
|
||||
<div
|
||||
id="shell-command-bg"
|
||||
className="shell-command-bg show"
|
||||
aria-hidden="false"
|
||||
className={`shell-command-bg${show ? " show" : ""}`}
|
||||
aria-hidden={!show}
|
||||
onClick={(event) => { if (event.target === event.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="shell-command" role="dialog" aria-modal="true" aria-label="命令面板">
|
||||
@@ -108,11 +109,12 @@ const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [
|
||||
{ act: "settings", icon: "settings", label: "个人设置" },
|
||||
{ act: "messages", icon: "bell", label: "消息中心" },
|
||||
{ act: "team", icon: "users", label: "团队管理" },
|
||||
{ act: "account", icon: "creditCard", label: "消费与余额" }
|
||||
{ act: "account", icon: "creditCard", label: "账单库" }
|
||||
];
|
||||
|
||||
export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
||||
anchorRect: DOMRect;
|
||||
export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
||||
open: boolean;
|
||||
anchorRect: DOMRect | null;
|
||||
onClose: () => void;
|
||||
navigate: Navigate;
|
||||
logout: () => void;
|
||||
@@ -120,24 +122,32 @@ export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team,
|
||||
team: Team | null;
|
||||
canManageBilling?: boolean;
|
||||
}) {
|
||||
const { mounted, show } = useOverlayTransition(open, undefined, 180);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>({ left: anchorRect.left, top: anchorRect.bottom + 8 });
|
||||
const lastRect = useRef<DOMRect | null>(anchorRect);
|
||||
if (anchorRect) lastRect.current = anchorRect;
|
||||
const rect = lastRect.current;
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>(() => (
|
||||
rect ? { left: rect.left, top: rect.bottom + 8 } : { left: 0, top: 0 }
|
||||
));
|
||||
|
||||
// 量出真实菜单尺寸后再定位(右对齐锚点、上下翻转防溢出),与 V1 toggleAccountMenu 一致
|
||||
useLayoutEffect(() => {
|
||||
if (!rect) return;
|
||||
const menu = menuRef.current;
|
||||
const width = menu?.offsetWidth || 232;
|
||||
const height = menu?.offsetHeight || 260;
|
||||
let left = anchorRect.right - width;
|
||||
if (left < 12) left = anchorRect.left;
|
||||
let left = rect.right - width;
|
||||
if (left < 12) left = rect.left;
|
||||
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
|
||||
let top = anchorRect.bottom + 8;
|
||||
if (top + height > window.innerHeight - 12) top = Math.max(12, anchorRect.top - height - 8);
|
||||
let top = rect.bottom + 8;
|
||||
if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 8);
|
||||
setPos({ left, top });
|
||||
}, [anchorRect]);
|
||||
}, [rect]);
|
||||
|
||||
// 点菜单外部 / Esc → 收起(锚点本身由触发器的 toggle 处理,避免点锚点立刻又被关掉)
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (menuRef.current?.contains(target)) return;
|
||||
@@ -151,15 +161,16 @@ export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team,
|
||||
document.removeEventListener("mousedown", onDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [onClose]);
|
||||
}, [open, onClose]);
|
||||
|
||||
const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase();
|
||||
const go = (page: Page) => { onClose(); navigate(page); };
|
||||
if (!mounted || !rect) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="shell-account-menu show"
|
||||
className={`shell-account-menu${show ? " show" : ""}`}
|
||||
id="shell-account-menu"
|
||||
role="menu"
|
||||
aria-label="账户菜单"
|
||||
@@ -199,19 +210,31 @@ const NAV: NavDef[] = [
|
||||
{ id: "free-create", page: "freeCreate", label: "自由创作", icon: "film" },
|
||||
{ id: "library", page: "library", label: "成品库", icon: "library" },
|
||||
{ id: "team", page: "team", label: "团队", icon: "users" },
|
||||
{ id: "account", page: "account", label: "消费", icon: "creditCard" },
|
||||
{ id: "account", page: "account", label: "账单库", icon: "creditCard" },
|
||||
{ id: "trash", page: "trash", label: "垃圾桶", icon: "trash" },
|
||||
{ id: "settings", page: "settings", label: "设置", icon: "settings" }
|
||||
{ id: "settings", page: "settings", label: "系统设置", icon: "settings" }
|
||||
];
|
||||
const RESOURCE_NAV: Array<{ id: string; page: Page; label: string }> = [
|
||||
{ id: "products", page: "products", label: "商品库" },
|
||||
{ id: "models", page: "models", label: "模特库" },
|
||||
{ id: "library", page: "library", label: "成品库" },
|
||||
// 成品库先隐藏,后续再加回资源中心
|
||||
{ id: "account", page: "account", label: "消费" },
|
||||
{ id: "account", page: "account", label: "账单库" },
|
||||
{ id: "trash", page: "trash", label: "垃圾桶" },
|
||||
];
|
||||
|
||||
type MajorNav = "resource" | "team" | "settings";
|
||||
const MAJOR_META: Record<MajorNav, { label: string; icon: "boxes" | "users-round" | "settings" }> = {
|
||||
resource: { label: "资源中心", icon: "boxes" },
|
||||
team: { label: "团队中心", icon: "users-round" },
|
||||
settings: { label: "系统设置", icon: "settings" },
|
||||
};
|
||||
|
||||
function LiquidIcon({ name }: { name: "boxes" | "users-round" | "settings" }) {
|
||||
if (name === "boxes") return <Boxes size={20} strokeWidth={1.9} />;
|
||||
if (name === "users-round") return <UsersRound size={20} strokeWidth={1.9} />;
|
||||
return <Settings size={20} strokeWidth={1.9} />;
|
||||
}
|
||||
|
||||
export type TopModule = "workbench" | "image" | "video";
|
||||
const OPEN_PALETTE_EVENT = "airshelf:open-palette";
|
||||
|
||||
@@ -233,6 +256,102 @@ export function topModuleForPage(page: Page): TopModule | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MODE_TABS: { id: TopModule; label: string; page: Page }[] = [
|
||||
{ id: "workbench", label: "工作台", page: "dashboard" },
|
||||
{ id: "image", label: "图片创作", page: "assetFactory" },
|
||||
{ id: "video", label: "视频创作", page: "projects" },
|
||||
];
|
||||
|
||||
// 顶栏三按钮滑动黑胶囊 · 对照影擎 .mode-tabs / moveModeIndicator 原样接入:
|
||||
// clip-path 只露出三个按钮形状,胶囊在缝里看不见;位移走 transform,不在 React state 里改尺寸。
|
||||
export function ModeTabs({ active, navigate }: { active: TopModule | null; navigate: Navigate }) {
|
||||
const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const clipRectsRef = useRef<Array<SVGRectElement | null>>([]);
|
||||
const indicatorRef = useRef<HTMLSpanElement>(null);
|
||||
const firstPaint = useRef(true);
|
||||
|
||||
const moveModeIndicator = (tab: HTMLButtonElement, animate = true) => {
|
||||
btnRefs.current.forEach((item, index) => {
|
||||
const rect = clipRectsRef.current[index];
|
||||
if (!item || !rect) return;
|
||||
rect.setAttribute("x", String(item.offsetLeft));
|
||||
rect.setAttribute("y", "0");
|
||||
rect.setAttribute("width", String(item.offsetWidth));
|
||||
rect.setAttribute("height", String(item.offsetHeight));
|
||||
});
|
||||
const modeIndicator = indicatorRef.current;
|
||||
if (!modeIndicator) return;
|
||||
if (!animate) modeIndicator.style.transition = "none";
|
||||
modeIndicator.style.width = `${tab.offsetWidth}px`;
|
||||
modeIndicator.style.transform = `translateX(${tab.offsetLeft}px)`;
|
||||
if (!animate) {
|
||||
void modeIndicator.offsetWidth;
|
||||
modeIndicator.style.transition = "";
|
||||
}
|
||||
};
|
||||
|
||||
const activeButton = () => {
|
||||
const idx = active ? MODE_TABS.findIndex((tab) => tab.id === active) : -1;
|
||||
return idx >= 0 ? btnRefs.current[idx] : null;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const tab = activeButton();
|
||||
if (tab) moveModeIndicator(tab, !firstPaint.current);
|
||||
firstPaint.current = false;
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
const snap = () => {
|
||||
const tab = activeButton();
|
||||
if (tab) moveModeIndicator(tab, false);
|
||||
};
|
||||
window.addEventListener("resize", snap);
|
||||
void document.fonts?.ready.then(snap);
|
||||
return () => window.removeEventListener("resize", snap);
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`yz-mods${active ? "" : " no-active"}`}
|
||||
role="tablist"
|
||||
aria-label="创作模块"
|
||||
>
|
||||
<svg className="yz-mod-clip-defs" width="0" height="0" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<clipPath id="yzModeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||
<rect ref={(el) => { clipRectsRef.current[0] = el; }} rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
||||
<rect ref={(el) => { clipRectsRef.current[1] = el; }} rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
||||
<rect ref={(el) => { clipRectsRef.current[2] = el; }} rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="yz-mod-indicator-mask" aria-hidden="true">
|
||||
<span
|
||||
ref={indicatorRef}
|
||||
className="yz-mod-indicator"
|
||||
style={{ width: 104, transform: "translateX(0px)" }}
|
||||
/>
|
||||
</span>
|
||||
{MODE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
ref={(el) => { btnRefs.current[index] = el; }}
|
||||
className={`yz-mod${active === tab.id ? " active" : ""}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
onClick={() => {
|
||||
const btn = btnRefs.current[index];
|
||||
if (btn) moveModeIndicator(btn, true);
|
||||
navigate(tab.page);
|
||||
}}
|
||||
>{tab.label}</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
|
||||
dashboard: "dashboard",
|
||||
products: "products",
|
||||
@@ -252,6 +371,7 @@ const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
|
||||
library: "library",
|
||||
team: "team",
|
||||
account: "account",
|
||||
trash: "trash",
|
||||
settings: "settings",
|
||||
settingsNotify: "settings"
|
||||
};
|
||||
@@ -276,7 +396,7 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
const navItems = NAV.filter((item) => canManageBilling || (item.page !== "team" && item.page !== "account"));
|
||||
const resourceNavItems = RESOURCE_NAV.filter((item) => navItems.some((nav) => nav.page === item.page));
|
||||
const topModule = topModuleForPage(page);
|
||||
const activeMajor: "resource" | "team" | "settings" | null =
|
||||
const activeMajor: MajorNav | null =
|
||||
activeNav === "team" ? "team"
|
||||
: activeNav === "settings" ? "settings"
|
||||
: topModule ? null
|
||||
@@ -305,12 +425,72 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [mobileNavOpen]);
|
||||
// 二级类目展开/收起:进资源中心时展开;切到顶栏模块(工作台/图片/视频)时收起,避免左边再出现选中。
|
||||
const [resourceOpen, setResourceOpen] = useState(activeMajor === "resource");
|
||||
// 二级类目:跟当前大类走,同一帧收起,避免液态胶囊先飞到「展开后的位置」再追回来。
|
||||
const [resourcePinned, setResourcePinned] = useState(true);
|
||||
useEffect(() => {
|
||||
if (activeMajor === "resource") setResourceOpen(true);
|
||||
else if (topModule) setResourceOpen(false);
|
||||
}, [activeMajor, topModule]);
|
||||
if (activeMajor === "resource") setResourcePinned(true);
|
||||
}, [activeMajor]);
|
||||
const resourceOpen = activeMajor === "resource" && resourcePinned;
|
||||
|
||||
const resourceItemRef = useRef<HTMLButtonElement>(null);
|
||||
const teamItemRef = useRef<HTMLButtonElement>(null);
|
||||
const settingsItemRef = useRef<HTMLButtonElement>(null);
|
||||
const submenuRef = useRef<HTMLDivElement>(null);
|
||||
const prevMajorRef = useRef<MajorNav | null>(activeMajor);
|
||||
const [liquid, setLiquid] = useState({ y: 0, hidden: true, pulse: false });
|
||||
const skipPulse = useRef(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const item = activeMajor === "resource" ? resourceItemRef.current
|
||||
: activeMajor === "team" ? teamItemRef.current
|
||||
: activeMajor === "settings" ? settingsItemRef.current
|
||||
: null;
|
||||
const leavingResource = prevMajorRef.current === "resource" && activeMajor !== "resource";
|
||||
prevMajorRef.current = activeMajor;
|
||||
|
||||
const targetY = (el: HTMLElement, subtractSubmenu: boolean) => {
|
||||
let top = el.offsetTop;
|
||||
if (subtractSubmenu && submenuRef.current) {
|
||||
const style = window.getComputedStyle(submenuRef.current);
|
||||
const space = submenuRef.current.offsetHeight
|
||||
+ (Number.parseFloat(style.marginTop) || 0)
|
||||
+ (Number.parseFloat(style.marginBottom) || 0);
|
||||
if (space > 1) top -= space;
|
||||
}
|
||||
return top - Math.max(0, (76 - el.offsetHeight) / 2);
|
||||
};
|
||||
|
||||
if (!item) {
|
||||
setLiquid((prev) => ({ ...prev, hidden: true, pulse: false }));
|
||||
skipPulse.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const animate = !skipPulse.current;
|
||||
skipPulse.current = false;
|
||||
setLiquid({
|
||||
y: targetY(item, leavingResource),
|
||||
hidden: false,
|
||||
pulse: animate,
|
||||
});
|
||||
|
||||
const onResize = () => {
|
||||
const current = activeMajor === "resource" ? resourceItemRef.current
|
||||
: activeMajor === "team" ? teamItemRef.current
|
||||
: activeMajor === "settings" ? settingsItemRef.current
|
||||
: null;
|
||||
if (!current) return;
|
||||
setLiquid((prev) => ({ ...prev, y: targetY(current, false), pulse: false }));
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [activeMajor, canManageBilling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!liquid.pulse) return;
|
||||
const timer = window.setTimeout(() => setLiquid((prev) => ({ ...prev, pulse: false })), 520);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [liquid.pulse]);
|
||||
|
||||
// 命令面板:Ctrl/Cmd K,以及顶栏搜索按钮
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
@@ -354,64 +534,90 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--collapse"><IconKitSvg name="chevronLeft" size={18} strokeWidth={1.8} /></span>
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--expand"><IconKitSvg name="chevronRight" size={18} strokeWidth={1.8} /></span>
|
||||
</button>
|
||||
<div className="yz-side-main">
|
||||
<button
|
||||
className={`yz-resource-head${activeMajor === "resource" ? " active" : ""}`}
|
||||
type="button"
|
||||
aria-expanded={resourceOpen}
|
||||
onClick={() => {
|
||||
// 在资源中心内点击=折叠/展开;在其它大类点击=切回资源中心并展开
|
||||
if (activeMajor !== "resource") {
|
||||
setResourceOpen(true);
|
||||
navigate("products");
|
||||
return;
|
||||
}
|
||||
setResourceOpen((value) => !value);
|
||||
}}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<Boxes size={16} strokeWidth={2} />
|
||||
<span>资源中心</span>
|
||||
<span className="chev"><IconKitSvg name="chevronRight" /></span>
|
||||
</button>
|
||||
<div className={`yz-resource-sub${resourceOpen ? " open" : " collapsed"}`}>
|
||||
{resourceNavItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`/${item.id}`}
|
||||
className={activeNav === item.page ? "active" : ""}
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate(item.page); }}
|
||||
<div className="nav-panel">
|
||||
<div className="nav-list">
|
||||
<div
|
||||
className={`liquid-indicator${liquid.hidden ? " is-hidden" : ""}${activeMajor && MAJOR_META[activeMajor].label.length >= 4 ? " wide-label" : ""}${liquid.pulse ? " pulse" : ""}`}
|
||||
aria-hidden="true"
|
||||
style={{ transform: `translateY(${liquid.y}px)` }}
|
||||
>
|
||||
<svg className="liquid-shape" viewBox="0 0 132 76" preserveAspectRatio="none">
|
||||
<path fill="#101012" d="M0 11H103C120 11 130 22 130 38C130 54 120 65 103 65H0Z" />
|
||||
</svg>
|
||||
{activeMajor && (
|
||||
<div className="indicator-content">
|
||||
<span className="indicator-icon-slot"><LiquidIcon name={MAJOR_META[activeMajor].icon} /></span>
|
||||
<span>{MAJOR_META[activeMajor].label}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
ref={resourceItemRef}
|
||||
className={`nav-item${activeMajor === "resource" ? " active" : ""}`}
|
||||
type="button"
|
||||
aria-expanded={resourceOpen}
|
||||
aria-controls="resourceSubmenu"
|
||||
onClick={() => {
|
||||
if (activeMajor !== "resource") {
|
||||
setResourcePinned(true);
|
||||
navigate("products");
|
||||
return;
|
||||
}
|
||||
setResourcePinned((value) => !value);
|
||||
}}
|
||||
>
|
||||
<span className="nav-inner">
|
||||
<Boxes size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">资源中心</span>
|
||||
<ChevronDown className="nav-chevron" size={17} strokeWidth={1.8} />
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
ref={submenuRef}
|
||||
id="resourceSubmenu"
|
||||
className={`nav-submenu${resourceOpen ? " expanded" : ""}`}
|
||||
aria-label="资源中心二级导航"
|
||||
aria-hidden={!resourceOpen}
|
||||
>
|
||||
{resourceNavItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`nav-subitem${activeNav === item.page ? " active" : ""}`}
|
||||
onClick={() => { setMobileNavOpen(false); navigate(item.page); }}
|
||||
>
|
||||
{item.label}
|
||||
{badges[item.id] !== undefined && <span className="pill-mini">{badges[item.id]}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{canManageBilling && (
|
||||
<button
|
||||
ref={teamItemRef}
|
||||
className={`nav-item${activeMajor === "team" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => { setMobileNavOpen(false); navigate("team"); }}
|
||||
>
|
||||
<span className="dot" />
|
||||
<span>{item.label}</span>
|
||||
{badges[item.id] !== undefined && <span className="pill-mini">{badges[item.id]}</span>}
|
||||
</a>
|
||||
))}
|
||||
<span className="nav-inner">
|
||||
<UsersRound size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">团队中心</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
ref={settingsItemRef}
|
||||
className={`nav-item${activeMajor === "settings" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => { setMobileNavOpen(false); navigate("settings"); }}
|
||||
>
|
||||
<span className="nav-inner">
|
||||
<Settings size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">系统设置</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className={`yz-side-link${activeMajor === "team" ? " active" : ""}`}
|
||||
href="/team"
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate("team"); }}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<UsersRound size={18} strokeWidth={2} />
|
||||
<span>团队中心</span>
|
||||
</a>
|
||||
<a
|
||||
className={`yz-side-link${activeMajor === "settings" ? " active" : ""}`}
|
||||
href="/settings"
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate("settings"); }}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<Settings size={18} strokeWidth={2} />
|
||||
<span>设置</span>
|
||||
</a>
|
||||
</div>
|
||||
{user.is_platform_admin && onOpenAdmin && (
|
||||
<>
|
||||
<div className="nav-section">平台</div>
|
||||
{user.is_platform_admin && onOpenAdmin && (
|
||||
<nav>
|
||||
<a
|
||||
href="/admin"
|
||||
@@ -423,8 +629,8 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
<span>平台后台</span>
|
||||
</a>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} navigate={navigate} canManageBilling={canManageBilling} />
|
||||
</>
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
/* 比影擎 164px 略宽:我们子项带数量徽章,164 会挤 */
|
||||
--sidebar-width: 192px;
|
||||
|
||||
/* ===== Backgrounds (冷灰 · YZ 影擎) ===== */
|
||||
--background-base: #f7f8fa;
|
||||
--background-lighter: #fbfbfc;
|
||||
@@ -217,7 +220,7 @@ img, svg, video { display: block; max-width: 100%; }
|
||||
/* ─── App shell ─── */
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 164px 1fr;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
min-height: 100vh;
|
||||
transition: grid-template-columns var(--t-base);
|
||||
}
|
||||
@@ -225,22 +228,26 @@ body.sidebar-collapsed .app { grid-template-columns: 96px 1fr; }
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
aside.sidebar {
|
||||
padding: 0 16px 22px;
|
||||
border-right: 1px solid var(--border-faint);
|
||||
background: #f5f6f8;
|
||||
--klein: #002fa7;
|
||||
padding: 0;
|
||||
border-right: 1px solid rgba(27, 32, 40, 0.08);
|
||||
background: rgba(28, 34, 43, 0.035);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: padding var(--t-base);
|
||||
}
|
||||
.sidebar-head {
|
||||
position: relative;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 -16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 116px;
|
||||
min-height: 116px;
|
||||
@@ -252,19 +259,20 @@ aside.sidebar {
|
||||
.brand { display: flex; align-items: center; justify-content: center; min-width: 0; color: var(--accent-black); }
|
||||
.brand-clip {
|
||||
display: block;
|
||||
width: 132px;
|
||||
height: 40px;
|
||||
width: 112px;
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
transition: width var(--t-base), height var(--t-base);
|
||||
}
|
||||
.brand-logo {
|
||||
display: block;
|
||||
width: 132px;
|
||||
height: 40px;
|
||||
width: 112px;
|
||||
height: 90px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
object-position: center;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
.brand-mark, .flame { width: 22px; height: 22px; color: var(--accent-black); }
|
||||
.brand-mark svg, .flame svg { width: 100%; height: 100%; }
|
||||
@@ -272,7 +280,7 @@ aside.sidebar {
|
||||
.sidebar-toggle {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 136px;
|
||||
left: calc(var(--sidebar-width) - 28px);
|
||||
z-index: 70;
|
||||
width: 28px;
|
||||
height: 100vh;
|
||||
@@ -395,180 +403,226 @@ nav a.disabled:hover { background: transparent; color: var(--black-alpha-32); }
|
||||
.user .av img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.user .em { font-size: 13px; color: var(--accent-black); }
|
||||
|
||||
/* ─── YZ 左侧导航预览(资源中心胶囊 + 子库) ─── */
|
||||
/* ─── 影擎侧栏 · 从 UI 源码原样接入(液态胶囊 + nav-item) ─── */
|
||||
.sidebar-toggle,
|
||||
.sidebar .search-box,
|
||||
.nav-section {
|
||||
display: none;
|
||||
}
|
||||
.yz-side-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.yz-resource-head {
|
||||
height: 48px;
|
||||
margin-left: -16px;
|
||||
margin-right: 10px;
|
||||
border: 0;
|
||||
border-radius: 0 24px 24px 0;
|
||||
background: transparent;
|
||||
color: #17191d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px 0 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.yz-resource-head > span { white-space: nowrap; flex: 0 0 auto; }
|
||||
.yz-resource-head.active {
|
||||
background: var(--nav-active);
|
||||
color: var(--accent-white);
|
||||
box-shadow: 0 6px 18px rgba(17, 19, 24, .14);
|
||||
}
|
||||
.yz-resource-head .major-dot,
|
||||
.yz-side-link .major-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.yz-resource-head .major-dot { display: none; }
|
||||
.yz-side-link .major-dot { display: none; }
|
||||
.yz-resource-head > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
color: #17191d;
|
||||
}
|
||||
.yz-resource-head.active > svg { color: rgba(255, 255, 255, .95); }
|
||||
.yz-resource-head .chev {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
opacity: .72;
|
||||
}
|
||||
.yz-resource-head.active .chev { opacity: .82; }
|
||||
.yz-resource-head .chev svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transform: rotate(90deg);
|
||||
transition: transform .18s ease;
|
||||
}
|
||||
.yz-resource-head[aria-expanded="false"] .chev svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.yz-resource-sub {
|
||||
aside.sidebar .nav-panel {
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
padding-left: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
/* 条目变多后不能再卡死 320px,否则「消费/垃圾桶」会被裁掉只剩一截字 */
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
margin-top: 2px;
|
||||
height: calc(100vh - 116px);
|
||||
min-height: 0;
|
||||
padding-top: 14px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.yz-resource-sub::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 6px;
|
||||
width: 1px;
|
||||
background: #d7dbe3;
|
||||
aside.sidebar .nav-list {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.yz-resource-sub a {
|
||||
min-height: 34px;
|
||||
border-radius: 11px;
|
||||
padding: 0 8px 0 10px;
|
||||
color: #888888;
|
||||
aside.sidebar .nav-item {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: clamp(70px, 7.6vh, 84px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background .15s, color .15s;
|
||||
padding: 0 10px 0 38px;
|
||||
color: #272a2f;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: color 180ms ease, transform 180ms ease;
|
||||
}
|
||||
.yz-resource-sub a .dot {
|
||||
aside.sidebar .nav-item .nav-inner {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 28px max-content 17px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
transition: opacity 140ms ease;
|
||||
}
|
||||
aside.sidebar .nav-item .nav-chevron {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
margin-left: 1px;
|
||||
color: rgba(39, 42, 47, 0.45);
|
||||
transform: translateX(-7px);
|
||||
transition: color 180ms ease, transform 220ms ease;
|
||||
}
|
||||
aside.sidebar .nav-item[aria-expanded="true"] .nav-chevron {
|
||||
color: var(--klein);
|
||||
transform: translateX(-7px) rotate(180deg);
|
||||
}
|
||||
aside.sidebar .nav-item:hover:not(.active) {
|
||||
color: var(--klein);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
aside.sidebar .nav-item.active .nav-inner {
|
||||
opacity: 0;
|
||||
}
|
||||
aside.sidebar .nav-item svg {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
aside.sidebar .nav-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
aside.sidebar .nav-submenu {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-height: 0;
|
||||
margin: 0 14px 0 28px;
|
||||
padding: 0 0 0 18px;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid rgba(0, 47, 167, 0.16);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
margin 280ms ease,
|
||||
padding 280ms ease,
|
||||
opacity 180ms ease,
|
||||
transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
aside.sidebar .nav-submenu.expanded {
|
||||
/* 比影擎 184px 略高:我们多「账单库 / 垃圾桶」两行 */
|
||||
max-height: 260px;
|
||||
margin: -8px 14px 10px 28px;
|
||||
padding: 8px 0 8px 18px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
aside.sidebar .nav-subitem {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
color: rgba(39, 42, 47, 0.62);
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 180ms ease, background 180ms ease, transform 180ms ease;
|
||||
}
|
||||
aside.sidebar .nav-subitem::before {
|
||||
content: "";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: #c8c8c8;
|
||||
background: currentColor;
|
||||
opacity: 0.45;
|
||||
}
|
||||
.yz-resource-sub a:hover {
|
||||
background: #eceef2;
|
||||
color: #5e6571;
|
||||
aside.sidebar .nav-subitem:hover {
|
||||
color: var(--klein);
|
||||
background: rgba(0, 47, 167, 0.055);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.yz-resource-sub a.active {
|
||||
background: #e9eefb;
|
||||
color: #2148bf;
|
||||
font-weight: 600;
|
||||
aside.sidebar .nav-subitem.active {
|
||||
color: var(--klein);
|
||||
background: rgba(0, 47, 167, 0.09);
|
||||
}
|
||||
.yz-resource-sub a.active .dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #3158d4;
|
||||
aside.sidebar .nav-subitem.active::before {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 4px rgba(0, 47, 167, 0.10);
|
||||
}
|
||||
.yz-resource-sub a .pill-mini {
|
||||
aside.sidebar .nav-subitem .pill-mini {
|
||||
margin-left: auto;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d4dcf4;
|
||||
padding: 0 7px;
|
||||
line-height: 18px;
|
||||
background: #fff;
|
||||
color: var(--klein);
|
||||
font-size: 11px;
|
||||
color: #4a5ca0;
|
||||
background: #f4f7ff;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.yz-resource-sub.collapsed {
|
||||
display: none;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
aside.sidebar .liquid-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 132px;
|
||||
height: 76px;
|
||||
pointer-events: none;
|
||||
transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
.yz-side-link {
|
||||
min-height: 48px;
|
||||
margin-left: -16px;
|
||||
margin-right: 10px;
|
||||
border-radius: 0 24px 24px 0;
|
||||
aside.sidebar .liquid-indicator.is-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
aside.sidebar .liquid-shape {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.14));
|
||||
transform-origin: left center;
|
||||
}
|
||||
aside.sidebar .liquid-indicator.pulse .liquid-shape {
|
||||
animation: liquid-pulse 520ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@keyframes liquid-pulse {
|
||||
0%, 100% { transform: scaleX(1); }
|
||||
42% { transform: scaleX(1.012); }
|
||||
72% { transform: scaleX(0.998); }
|
||||
}
|
||||
aside.sidebar .indicator-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 33px;
|
||||
height: 76px;
|
||||
width: 94px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 10px 0 14px;
|
||||
color: #17191d;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
gap: 9px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: background .22s ease, color .22s ease, box-shadow .22s ease;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
transition: opacity 140ms ease, transform 280ms ease;
|
||||
}
|
||||
.yz-side-link:hover { background: #eceef2; }
|
||||
.yz-side-link > svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: #4f5562;
|
||||
transition: color .22s ease;
|
||||
aside.sidebar .liquid-indicator.wide-label .indicator-content {
|
||||
left: 26px;
|
||||
gap: 7px;
|
||||
}
|
||||
.yz-side-link.active {
|
||||
color: #ffffff;
|
||||
background: var(--nav-active);
|
||||
box-shadow: 0 6px 18px rgba(17, 19, 24, .14);
|
||||
aside.sidebar .liquid-indicator.pulse .indicator-content {
|
||||
animation: label-pulse 520ms ease;
|
||||
}
|
||||
.yz-side-link.active > svg {
|
||||
color: rgba(255, 255, 255, .96);
|
||||
@keyframes label-pulse {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
44% { transform: translateX(2px); }
|
||||
}
|
||||
aside.sidebar .indicator-icon-slot {
|
||||
width: 24px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
aside.sidebar .indicator-icon-slot svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 1.9;
|
||||
}
|
||||
aside.sidebar .nav-panel > nav {
|
||||
padding: 8px 16px 20px;
|
||||
}
|
||||
@media (max-height: 780px) {
|
||||
aside.sidebar .nav-panel { padding-top: 8px; }
|
||||
aside.sidebar .nav-item { height: 68px; }
|
||||
}
|
||||
.aside-foot {
|
||||
margin-top: auto;
|
||||
@@ -602,8 +656,8 @@ nav a.disabled:hover { background: transparent; color: var(--black-alpha-32); }
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed aside.sidebar { padding: 0 12px 22px; }
|
||||
body.sidebar-collapsed .sidebar-head { gap: 6px; margin: 0 -12px; padding: 0 12px; height: 116px; min-height: 116px; }
|
||||
body.sidebar-collapsed aside.sidebar { padding: 0; }
|
||||
body.sidebar-collapsed .sidebar-head { gap: 6px; margin: 0; padding: 0; height: 116px; min-height: 116px; }
|
||||
body.sidebar-collapsed .brand-clip {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
@@ -648,20 +702,51 @@ body.sidebar-collapsed .user {
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
body.sidebar-collapsed .yz-side-main,
|
||||
body.sidebar-collapsed .nav-panel,
|
||||
body.sidebar-collapsed .user .em,
|
||||
body.sidebar-collapsed .user::after { display: none; }
|
||||
|
||||
/* ─── Main + grid background ─── */
|
||||
main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
main { position: relative; background: #fff; min-width: 0; }
|
||||
.grid-bg {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60'><circle cx='60' cy='60' r='0.9' fill='%23d8d8d8'/></svg>"),
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><g stroke='%23ebebeb' stroke-width='1' fill='none' stroke-dasharray='1.5 4'><path d='M240 0 L240 240'/><path d='M0 240 L240 240'/></g></svg>");
|
||||
background-size: 60px 60px, 240px 240px;
|
||||
mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
|
||||
linear-gradient(rgba(0, 47, 167, 0.16) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 47, 167, 0.16) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
background-position: -1px -1px;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.58) 2%,
|
||||
rgba(0, 0, 0, 0.3) 16%,
|
||||
rgba(0, 0, 0, 0.12) 30%,
|
||||
rgba(0, 0, 0, 0.03) 40%,
|
||||
transparent 46%,
|
||||
transparent 54%,
|
||||
rgba(0, 0, 0, 0.03) 60%,
|
||||
rgba(0, 0, 0, 0.12) 70%,
|
||||
rgba(0, 0, 0, 0.3) 84%,
|
||||
rgba(0, 0, 0, 0.58) 98%,
|
||||
#000 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.58) 2%,
|
||||
rgba(0, 0, 0, 0.3) 16%,
|
||||
rgba(0, 0, 0, 0.12) 30%,
|
||||
rgba(0, 0, 0, 0.03) 40%,
|
||||
transparent 46%,
|
||||
transparent 54%,
|
||||
rgba(0, 0, 0, 0.03) 60%,
|
||||
rgba(0, 0, 0, 0.12) 70%,
|
||||
rgba(0, 0, 0, 0.3) 84%,
|
||||
rgba(0, 0, 0, 0.58) 98%,
|
||||
#000 100%
|
||||
);
|
||||
}
|
||||
.scatter {
|
||||
position: absolute;
|
||||
@@ -694,37 +779,81 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
.crumbs .sep { color: var(--black-alpha-32); }
|
||||
.crumbs .here { color: var(--accent-black); font-weight: 500; }
|
||||
.crumbs a:hover { color: var(--accent-black); }
|
||||
.yz-mods {
|
||||
.topbar .yz-mods {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
width: max-content;
|
||||
max-width: none;
|
||||
gap: 16px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.yz-mod {
|
||||
.topbar .yz-mods.no-active .yz-mod-indicator-mask { opacity: 0; }
|
||||
.yz-mod-clip-defs {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.yz-mod-indicator-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
clip-path: url(#yzModeButtonMask);
|
||||
-webkit-clip-path: url(#yzModeButtonMask);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.yz-mod-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 104px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: #101012;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.16);
|
||||
pointer-events: none;
|
||||
transform-origin: left center;
|
||||
transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1), width 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
will-change: transform, width;
|
||||
}
|
||||
.topbar .yz-mods .yz-mod {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
min-width: 104px;
|
||||
height: 44px;
|
||||
padding: 0 22px;
|
||||
gap: 0;
|
||||
border: 1px solid #d9dde3;
|
||||
border-radius: 22px;
|
||||
background: rgba(31, 39, 51, 0.025);
|
||||
color: #2b2e33;
|
||||
background: rgba(31, 39, 51, 0.025);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
flex: 0 0 auto;
|
||||
transition: background 180ms ease, color 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
.yz-mod:hover { background: rgba(31, 39, 51, 0.06); color: #2b2e33; }
|
||||
.yz-mod.active {
|
||||
background: #101012;
|
||||
.topbar .yz-mods .yz-mod:hover:not(.active) { background: rgba(31, 39, 51, 0.06); color: #2b2e33; }
|
||||
.topbar .yz-mods .yz-mod.active {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.topbar .yz-mods .yz-mod.active:hover { background: transparent; color: #fff; }
|
||||
.top-search {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -739,9 +868,9 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--t-base), background var(--t-base);
|
||||
transition: border-color var(--t-base), background var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.top-search:hover { border-color: #e0e0e0; background: #f0f0f0; }
|
||||
.top-search:hover { border-color: #e0e0e0; background: #f0f0f0; transform: translateY(-1px); }
|
||||
.top-search svg { width: var(--icon-m); height: var(--icon-m); flex-shrink: 0; }
|
||||
.top-search span:not(.kbd) { flex: 1; text-align: left; }
|
||||
.top-search .kbd {
|
||||
@@ -763,9 +892,9 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
font-size: 13px;
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.balance-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
|
||||
.balance-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); transform: translateY(-1px); }
|
||||
.balance-chip strong { color: var(--accent-black); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.balance-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--black-alpha-48); }
|
||||
.icon-btn {
|
||||
@@ -777,9 +906,9 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.icon-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--border-loud); }
|
||||
.icon-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--border-loud); transform: translateY(-1px); }
|
||||
.icon-btn svg { width: var(--icon-m); height: var(--icon-m); }
|
||||
.icon-btn .dot-noti {
|
||||
position: absolute; top: 8px; right: 9px;
|
||||
@@ -853,6 +982,11 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: calc(100vh - 116px);
|
||||
animation: yz-page-enter 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@keyframes yz-page-enter {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.content > .corner-mark { display: none; }
|
||||
@@ -907,7 +1041,7 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
transition: background var(--t-base), border-color var(--t-base), transform var(--t-fast);
|
||||
}
|
||||
.btn:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
|
||||
.btn:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); transform: translateY(-1px); }
|
||||
.btn:active { background: var(--black-alpha-7); transform: scale(.99); }
|
||||
.btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--background-base), 0 0 0 4px var(--heat-40); }
|
||||
.btn:disabled, .btn.disabled {
|
||||
@@ -936,6 +1070,7 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
background: var(--heat-hover);
|
||||
border-color: var(--heat-hover);
|
||||
box-shadow: var(--shadow-cta-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.btn-primary:active {
|
||||
background: var(--heat);
|
||||
@@ -1129,9 +1264,9 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); color: var(--accent-black); }
|
||||
.chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); color: var(--accent-black); transform: translateY(-1px); }
|
||||
.chip.active { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
|
||||
.chip svg { width: 12px; height: 12px; }
|
||||
/* 清空筛选:幽灵态(无边框/透明底),仅在有筛选时出现,点击重置全部筛选 */
|
||||
@@ -2050,15 +2185,23 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1800;
|
||||
display: none;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 12vh 16px 24px;
|
||||
background: var(--black-alpha-48);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s ease, visibility .2s ease;
|
||||
}
|
||||
.shell-command-bg.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.shell-command-bg.show { display: flex; }
|
||||
.shell-command {
|
||||
width: min(640px, 100%);
|
||||
max-height: min(680px, 76vh);
|
||||
@@ -2069,6 +2212,13 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
overflow: hidden;
|
||||
transform: translateY(-10px) scale(.98);
|
||||
opacity: 0;
|
||||
transition: transform .22s cubic-bezier(0.22, 1, 0.36, 1), opacity .22s ease;
|
||||
}
|
||||
.shell-command-bg.show .shell-command {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
.shell-command-h {
|
||||
display: flex;
|
||||
@@ -2140,6 +2290,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
.shell-command-item:hover,
|
||||
.shell-command-item.active {
|
||||
@@ -2221,14 +2372,24 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
position: fixed;
|
||||
z-index: 1700;
|
||||
min-width: 232px;
|
||||
display: none;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: translateY(-6px);
|
||||
transition: opacity .18s ease, transform .18s cubic-bezier(0.22, 1, 0.36, 1), visibility .18s ease;
|
||||
}
|
||||
.shell-account-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: none;
|
||||
}
|
||||
.shell-account-menu.show { display: block; }
|
||||
.shell-account-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2279,6 +2440,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.shell-account-menu button svg {
|
||||
width: 14px;
|
||||
@@ -2302,26 +2464,28 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
background: rgba(21, 20, 15, .42);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: none; align-items: center; justify-content: center;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s ease, visibility .2s ease;
|
||||
}
|
||||
.modal-bg.show { display: flex; opacity: 1; }
|
||||
.modal-bg.show { opacity: 1; visibility: visible; pointer-events: auto; }
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
max-width: 480px; width: 90%;
|
||||
position: relative;
|
||||
transform: scale(.96);
|
||||
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform: translateY(8px) scale(.98);
|
||||
transition: transform .22s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal-bg.show .modal { transform: scale(1); }
|
||||
.modal-bg.show .modal { transform: none; }
|
||||
.modal::before, .modal::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -2428,10 +2592,14 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
.drawer-bg {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .32);
|
||||
display: none;
|
||||
display: block;
|
||||
z-index: 90;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s ease, visibility .2s ease;
|
||||
}
|
||||
.drawer-bg.show { display: block; }
|
||||
.drawer-bg.show { opacity: 1; visibility: visible; pointer-events: auto; }
|
||||
.drawer {
|
||||
position: fixed; right: 0; top: 0; bottom: 0;
|
||||
width: 540px; max-width: 100vw;
|
||||
@@ -2492,14 +2660,14 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
aside.sidebar {
|
||||
display: block;
|
||||
position: fixed; top: 0; left: 0; bottom: 0;
|
||||
width: 164px;
|
||||
width: var(--sidebar-width);
|
||||
z-index: 1200;
|
||||
transform: translateX(-100%);
|
||||
transition: transform .28s cubic-bezier(.32, .72, 0, 1);
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, .18);
|
||||
}
|
||||
/* 抽屉里收窄态不适用,强制展开宽度,避免 body.sidebar-collapsed 把抽屉压成 96px */
|
||||
body.sidebar-collapsed aside.sidebar { width: 164px; }
|
||||
body.sidebar-collapsed aside.sidebar { width: var(--sidebar-width); }
|
||||
aside.sidebar.mobile-open { transform: translateX(0); }
|
||||
.sidebar-toggle { display: none; }
|
||||
/* 抽屉内汉堡键无意义,藏掉,留出顶部空间给页面自己的汉堡 */
|
||||
@@ -2691,8 +2859,8 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
.recent-meta .name { font-weight: 600; font-size: 14px; color: var(--accent-black); }
|
||||
.recent-meta .sub { font-size: 12px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }
|
||||
.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; display: flex; align-items: flex-start; gap: 11px; cursor: pointer; text-align: left; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.shortcut:hover { background: var(--black-alpha-4); }
|
||||
.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; display: flex; align-items: flex-start; gap: 11px; cursor: pointer; text-align: left; transition: background var(--t-base), border-color var(--t-base), transform var(--t-base); }
|
||||
.shortcut:hover { background: var(--black-alpha-4); transform: translateY(-2px); }
|
||||
.shortcut .ic { width: 30px; height: 30px; background: var(--heat-12); color: var(--heat); display: grid; place-items: center; border: 1px solid var(--heat-20); border-radius: var(--r-sm); flex-shrink: 0; }
|
||||
.shortcut .t { font-size: 13px; font-weight: 600; }
|
||||
.shortcut .d { font-size: 12px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .01em; }
|
||||
@@ -2713,3 +2881,19 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
transition: color var(--t-base);
|
||||
}
|
||||
.dash-funds-mono:hover { color: var(--heat); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.yz-mod-indicator,
|
||||
.yz-mod-indicator-mask,
|
||||
.modal-bg,
|
||||
.modal,
|
||||
.drawer,
|
||||
.drawer-bg,
|
||||
.shell-command-bg,
|
||||
.shell-command,
|
||||
.shell-account-menu,
|
||||
.content {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@
|
||||
|
||||
.library-page .lib-sel-bar {
|
||||
position: fixed;
|
||||
left: calc(164px + (100vw - 164px) / 2);
|
||||
left: calc(var(--sidebar-width) + (100vw - var(--sidebar-width)) / 2);
|
||||
bottom: 26px;
|
||||
z-index: 100;
|
||||
min-width: 410px;
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
|
||||
.models-page .ml-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.models-page .ml-card {
|
||||
position: relative;
|
||||
@@ -142,7 +142,7 @@
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.models-page .ml-meta { padding: 15px 16px 17px; }
|
||||
.models-page .ml-meta { padding: 12px 13px 14px; }
|
||||
.models-page .ml-meta h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 15px;
|
||||
@@ -152,21 +152,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.models-page .ml-meta p { margin: 0; color: var(--ml-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.models-page .ml-triview {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(34, 42, 54, 0.06);
|
||||
}
|
||||
.models-page .ml-triview.has-mock-media {
|
||||
background-image: var(--mock-media-url);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border: 0;
|
||||
}
|
||||
.models-page .ml-triview .ph-frame { font-size: 11px; }
|
||||
.models-page .ml-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -232,7 +217,7 @@
|
||||
|
||||
.models-page .ml-sel-bar {
|
||||
position: fixed;
|
||||
left: calc(164px + (100vw - 164px) / 2);
|
||||
left: calc(var(--sidebar-width) + (100vw - var(--sidebar-width)) / 2);
|
||||
bottom: 26px;
|
||||
z-index: 100;
|
||||
min-width: 410px;
|
||||
@@ -269,6 +254,9 @@
|
||||
.models-page .ml-sel-confirm { color: #fff; background: #002fa7; }
|
||||
.models-page .ml-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.models-page .ml-sel-bar { left: 50%; }
|
||||
|
||||
@@ -470,7 +470,7 @@
|
||||
/* 影擎吸底选择条 · 管理模式即出现 */
|
||||
.products-page .pl-sel-bar {
|
||||
position: fixed;
|
||||
left: calc(164px + (100vw - 164px) / 2);
|
||||
left: calc(var(--sidebar-width) + (100vw - var(--sidebar-width)) / 2);
|
||||
bottom: 26px;
|
||||
z-index: 100;
|
||||
min-width: 410px;
|
||||
|
||||
@@ -71,7 +71,13 @@
|
||||
color: var(--vc-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.projects-page .vc-list-h { margin: 32px 0 17px; }
|
||||
.projects-page .vc-list-h {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 22px;
|
||||
margin: 32px 0 17px;
|
||||
}
|
||||
.projects-page .vc-list-h h2 { font-size: 21px; }
|
||||
.projects-page .vc-list-h p { font-size: 13px; }
|
||||
|
||||
@@ -456,7 +462,7 @@
|
||||
|
||||
.projects-page .vc-sel-bar {
|
||||
position: fixed;
|
||||
left: calc(164px + (100vw - 164px) / 2);
|
||||
left: calc(var(--sidebar-width) + (100vw - var(--sidebar-width)) / 2);
|
||||
bottom: 26px;
|
||||
z-index: 100;
|
||||
min-width: 410px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ArrowLeft, CreditCard, X } from "lucide-react";
|
||||
import { CreditCard, X } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api } from "../api";
|
||||
import type { BillingSummary, BillingTrend, Ledger, Project, Team, TeamMember } from "../types";
|
||||
@@ -63,11 +63,11 @@ const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: num
|
||||
];
|
||||
const MIN_RECHARGE_AMOUNT = 50;
|
||||
|
||||
const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [
|
||||
{ k: "视频片段(Seedance)", color: "var(--klein)", bucket: "video" },
|
||||
{ k: "故事板(image-2)", color: "var(--accent-forest)", bucket: "storyboard" },
|
||||
{ k: "基础资产", color: "var(--black-alpha-56)", bucket: "base" },
|
||||
{ k: "脚本 LLM", color: "var(--black-alpha-32)", bucket: "script" }
|
||||
const STAGES: Array<{ k: string; short: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [
|
||||
{ k: "视频片段(Seedance)", short: "视频生成", color: "var(--klein)", bucket: "video" },
|
||||
{ k: "故事板(image-2)", short: "故事板", color: "var(--accent-forest)", bucket: "storyboard" },
|
||||
{ k: "基础资产", short: "基础资产", color: "var(--black-alpha-56)", bucket: "base" },
|
||||
{ k: "脚本 LLM", short: "脚本生成", color: "var(--black-alpha-32)", bucket: "script" }
|
||||
];
|
||||
|
||||
// 充值扫码弹窗 · 复用设计系统 .modal-bg/.modal 外壳(QR 占位 + 金额 + 赠送 + 5 分钟有效 + 双按钮)
|
||||
@@ -116,7 +116,7 @@ function TopupModal({ open, channel, amount, bonus, rate, close, onDone }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountPage({ billing, projects, team, navigate, onRecharge, onNotify }: {
|
||||
export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
billing: BillingSummary | null;
|
||||
projects: Project[];
|
||||
// team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14)
|
||||
@@ -244,6 +244,11 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
|
||||
const total14 = Number(activeTrend?.total_14d ?? used);
|
||||
const avgValue = daily.length ? total14 / daily.length : 0;
|
||||
const stageTotal = STAGES.reduce((sum, s) => sum + Number(trend?.by_stage[s.bucket] || 0), 0) || used;
|
||||
const topStage = STAGES.reduce((best, s) => {
|
||||
const amt = Number(trend?.by_stage[s.bucket] || 0);
|
||||
return amt > best.amt ? { label: s.short, amt } : best;
|
||||
}, { label: "暂无消耗", amt: 0 });
|
||||
const topPct = stageTotal > 0 ? Math.round((topStage.amt / stageTotal) * 100) : 0;
|
||||
const projectSpend = (id: string) => Number(trend?.by_project[id] || 0);
|
||||
|
||||
// ── 项目筛选(状态;真实数据无 owner/per-range,故只做状态过滤)──
|
||||
@@ -282,101 +287,87 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
|
||||
|
||||
return (
|
||||
<section className="account-page">
|
||||
<div className="page-head">
|
||||
<button className="ac-back" type="button" onClick={() => navigate("dashboard")} aria-label="返回">
|
||||
<ArrowLeft size={18} strokeWidth={1.75} />
|
||||
</button>
|
||||
<header className="ac-head">
|
||||
<div>
|
||||
<h1>账户</h1>
|
||||
<div className="sub">{money(balance)} 余额 · {money(used)} 本月已用 · 充值与账单流水</div>
|
||||
<h1>账单库</h1>
|
||||
<p>余额、充值与流水</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="top-grid">
|
||||
<div className="balance-banner">
|
||||
<span className="corner-tr" aria-hidden="true"></span><span className="corner-bl" aria-hidden="true"></span>
|
||||
<div className="balance-hero">
|
||||
<div className="lbl">团队余额</div>
|
||||
<div className="v">{money(balance)}</div>
|
||||
<div className="meta">充值累加 · 不重置</div>
|
||||
<div className="ac-top">
|
||||
<div className="ac-hero">
|
||||
<div>
|
||||
<span>团队余额</span>
|
||||
<strong>{money(balance)}</strong>
|
||||
<em>充值累加 · 不重置</em>
|
||||
</div>
|
||||
<div className="balance-sub">
|
||||
<div className="col">
|
||||
<div className="lbl">本月限额</div>
|
||||
<div className="v">{money(limit)}</div>
|
||||
<div className="meta">按自然月重置</div>
|
||||
<div className="ac-hero-stats">
|
||||
<div>
|
||||
<span>本月限额</span>
|
||||
<strong>{money(limit)}</strong>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="lbl">当月已用</div>
|
||||
<div className="v">{money(used)}</div>
|
||||
<div className="meta">占比 {pct.toFixed(1)}% · {pct >= 80 ? "注意" : "健康"}</div>
|
||||
<div>
|
||||
<span>当月已用</span>
|
||||
<strong>{money(used)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="balance-foot">
|
||||
<div className="balance-meter" aria-label={`本月额度使用率 ${pct.toFixed(1)}%`}><span style={{ width: `${pct}%` }} /></div>
|
||||
<div className="balance-foot-meta">
|
||||
<div>
|
||||
<div className="ac-progress" aria-label={`本月额度使用率 ${pct.toFixed(1)}%`}>
|
||||
<span style={{ width: `${Math.max(pct, pct > 0 ? 2 : 0)}%` }} />
|
||||
</div>
|
||||
<div className="ac-progress-meta">
|
||||
<span>团队月剩余 {money(left)}</span>
|
||||
<span>使用率 {pct.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pane topup-pane">
|
||||
<div className="topup-head">
|
||||
<div>
|
||||
<h3>快速充值</h3>
|
||||
<div className="desc">充值后立刻到账,可开发票 · 仅超管可操作</div>
|
||||
</div>
|
||||
<div className="topup-selected">已选 ¥{effectiveAmount}(到账 {Math.round(effectiveAmount * pointsRate + effectiveBonus)} 积分){effectiveBonus > 0 ? ` · 含 ${effectiveBonus} 积分赠送` : ""}</div>
|
||||
</div>
|
||||
<div className="recharge-row">
|
||||
<div className="ac-recharge">
|
||||
<h2>快速充值</h2>
|
||||
<p>充值后立刻到账,可开发票 · 仅超管可操作 · 已选 ¥{effectiveAmount}(到账 {Math.round(effectiveAmount * pointsRate + effectiveBonus)} 积分){effectiveBonus > 0 ? ` · 含 ${effectiveBonus} 积分赠送` : ""}</p>
|
||||
<div className="ac-recharge-grid">
|
||||
{RECHARGE.map((item) => (
|
||||
<div
|
||||
<button
|
||||
key={item.amt}
|
||||
className={`recharge-card ${recharge === item.amt && !customAmt ? "selected" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
className={`ac-option${recharge === item.amt && !customAmt ? " active" : ""}`}
|
||||
aria-pressed={recharge === item.amt && !customAmt}
|
||||
onClick={() => { setRecharge(item.amt); setCustomAmt(""); }}
|
||||
onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setRecharge(item.amt); setCustomAmt(""); } }}
|
||||
>
|
||||
{item.ribbon && <span className="ribbon">{item.ribbon}</span>}
|
||||
<div className="amt">¥{item.amt}</div>
|
||||
<div className={`gift ${item.bonus ? "bonus" : ""}`}>{item.gift}</div>
|
||||
</div>
|
||||
<strong>¥{item.amt}</strong>
|
||||
<span className={item.bonus ? "" : "plain"}>{item.gift}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="pay-row">
|
||||
<div className="pay-title">自定义金额</div>
|
||||
<input
|
||||
className={`input${!customAmountIsValid ? " is-invalid" : ""}`}
|
||||
placeholder="最低 ¥50,可输入任意金额"
|
||||
type="number"
|
||||
min={MIN_RECHARGE_AMOUNT}
|
||||
step="1"
|
||||
inputMode="numeric"
|
||||
value={customAmt}
|
||||
aria-invalid={!customAmountIsValid}
|
||||
aria-describedby="custom-recharge-error"
|
||||
onChange={(event) => setCustomIntegerAmount(event.target.value)}
|
||||
/>
|
||||
{!customAmountIsValid && <div className="field-hint is-error" id="custom-recharge-error" role="alert">充值金额不能低于 {MIN_RECHARGE_AMOUNT} 元</div>}
|
||||
<div className="pay-btn-row">
|
||||
<button className="btn pay-method-btn pay-wechat" type="button" aria-label="微信支付" onClick={() => openTopup("wechat")}>
|
||||
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-wechat.png" alt="" /></span>
|
||||
微信支付
|
||||
</button>
|
||||
<button className="btn pay-method-btn pay-alipay" type="button" aria-label="支付宝" onClick={() => openTopup("alipay")}>
|
||||
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-alipay.png" alt="" /></span>
|
||||
支付宝
|
||||
</button>
|
||||
</div>
|
||||
<div className="ac-pay-title">自定义金额</div>
|
||||
<input
|
||||
className={`input${!customAmountIsValid ? " is-invalid" : ""}`}
|
||||
placeholder="最低 ¥50,可输入任意金额"
|
||||
type="number"
|
||||
min={MIN_RECHARGE_AMOUNT}
|
||||
step="1"
|
||||
inputMode="numeric"
|
||||
value={customAmt}
|
||||
aria-invalid={!customAmountIsValid}
|
||||
aria-describedby="custom-recharge-error"
|
||||
onChange={(event) => setCustomIntegerAmount(event.target.value)}
|
||||
/>
|
||||
{!customAmountIsValid && <div className="field-hint is-error" id="custom-recharge-error" role="alert">充值金额不能低于 {MIN_RECHARGE_AMOUNT} 元</div>}
|
||||
<div className="ac-pay-row">
|
||||
<button className="ac-pay" type="button" aria-label="微信支付" onClick={() => openTopup("wechat")}>
|
||||
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-wechat.png" alt="" /></span>
|
||||
微信支付
|
||||
</button>
|
||||
<button className="ac-pay" type="button" aria-label="支付宝" onClick={() => openTopup("alipay")}>
|
||||
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-alipay.png" alt="" /></span>
|
||||
支付宝
|
||||
</button>
|
||||
</div>
|
||||
<div className="tip" style={{ marginTop: 14 }}>
|
||||
<strong>计费说明</strong>
|
||||
1 元 = 10 积分。脚本/文案 10 积分/次;图片生成 20 积分/张;配音 10 积分/500 字;
|
||||
视频按时长与分辨率计量、按实际用量结算(如 15 秒 720P 竖屏约 224 积分),多退少不补超;
|
||||
拼接导出免费。生成失败、超时均不扣费,自动退回预留积分。
|
||||
<div className="ac-tip">
|
||||
<strong>计费说明 </strong>
|
||||
1 元 = 10 积分。脚本/文案 10 积分/次;图片生成 20 积分/张;配音 10 积分/500 字;
|
||||
视频按时长与分辨率计量。生成失败、超时均不扣费。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,24 +390,34 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
|
||||
<button key={r} className={`ac-seg${range === r ? " active" : ""}`} type="button" onClick={() => setRange(r)}>{RANGE_META[r].chip}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="ac-summary">
|
||||
<div>
|
||||
<span>本月已用</span>
|
||||
<strong>{money(used)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>本月剩余</span>
|
||||
<strong>{money(left)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>主要消耗</span>
|
||||
<strong>{topStage.amt > 0 ? `${topStage.label} ${topPct}%` : "暂无消耗"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="trend-chart">
|
||||
<div className="bars">
|
||||
{daily.map((d) => {
|
||||
const amt = Number(d.amount);
|
||||
const h = peak > 0 ? Math.max(amt > 0 ? 4 : 0, (amt / peak) * 100) : 0;
|
||||
const isPeak = peak > 0 && amt === peak;
|
||||
return (
|
||||
<div className={`bar${isPeak ? " peak" : ""}`} key={d.date} title={`${d.label} · ${money(amt)}`}>
|
||||
<span style={{ height: `${h}%` }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="x-axis">
|
||||
{daily.map((d, i) => (
|
||||
<span key={d.date}>{i % 2 === 0 ? d.label : ""}</span>
|
||||
))}
|
||||
</div>
|
||||
{daily.length === 0 ? (
|
||||
<div className="trend-chart-empty">暂无消费记录</div>
|
||||
) : daily.map((d) => {
|
||||
const amt = Number(d.amount);
|
||||
const h = peak > 0 ? Math.max(amt > 0 ? 8 : 4, (amt / peak) * 100) : 8;
|
||||
return (
|
||||
<div className="ac-bar-group" key={d.date} title={`${d.label} · ${money(amt)}`}>
|
||||
<strong className="ac-bar-value">{amt > 0 ? Math.round(amt) : ""}</strong>
|
||||
<div className="ac-bar" style={{ height: `${h}%` }} />
|
||||
<span>{d.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="trend-foot">
|
||||
<div className="item"><span className="k">{meta.totalLabel}</span><span className="v">{money(total14)}</span></div>
|
||||
|
||||
@@ -2566,7 +2566,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="dm-form-cta">
|
||||
<div className="dm-cost">
|
||||
<span>预估扣费 <span className="v">≈ 20 积分/张</span></span>
|
||||
<span>余额以消费页为准</span>
|
||||
<span>余额以账单库为准</span>
|
||||
</div>
|
||||
<button className="dm-gen" type="button" onClick={toRealTool}>
|
||||
<Sparkles size={15} />
|
||||
|
||||
@@ -428,12 +428,6 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
|
||||
<div className="ml-meta">
|
||||
<h3 title={m.name}>{m.name}</h3>
|
||||
<p>{m.description?.trim() || (m.source === "upload" ? "真人上传的人物参考" : "AI 生成的人物参考")}</p>
|
||||
<div
|
||||
className={`placeholder ml-triview${m.triview ? " has-mock-media" : ""}`}
|
||||
style={m.triview ? mediaStyle(m.triview) : undefined}
|
||||
>
|
||||
{!m.triview && <span className="ph-frame">待补三视图</span>}
|
||||
</div>
|
||||
<div className="ml-line">
|
||||
{m.is_official ? (
|
||||
<>
|
||||
|
||||
@@ -571,12 +571,6 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
|
||||
<h1>视频创作</h1>
|
||||
<p>从商品或参考视频出发,选择适合当前任务的生产方式</p>
|
||||
</div>
|
||||
<div className="vc-actions">
|
||||
<button className={`vc-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : setEditMode(true))}>
|
||||
<Settings2 />
|
||||
<span>{editMode ? "完成" : "管理项目"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="vc-sec">
|
||||
@@ -622,8 +616,14 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
|
||||
</section>
|
||||
|
||||
<div className="vc-sec-h vc-list-h">
|
||||
<h2>视频项目</h2>
|
||||
<p>统一查看各类视频任务的状态与生成结果</p>
|
||||
<div>
|
||||
<h2>视频项目</h2>
|
||||
<p>统一查看各类视频任务的状态与生成结果</p>
|
||||
</div>
|
||||
<button className={`vc-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : setEditMode(true))}>
|
||||
<Settings2 />
|
||||
<span>{editMode ? "完成" : "管理项目"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="vc-toolbar">
|
||||
|
||||
@@ -75,7 +75,7 @@ export const mainNav: NavItem[] = [
|
||||
{ page: "projects", label: "视频创作", icon: FolderKanban },
|
||||
{ page: "pipeline", label: "生产管线", icon: WandSparkles },
|
||||
{ page: "library", label: "成品库", icon: Library },
|
||||
{ page: "account", label: "账户", icon: Wallet }
|
||||
{ page: "account", label: "账单库", icon: Wallet }
|
||||
];
|
||||
|
||||
export const supplementNav: NavItem[] = [
|
||||
@@ -96,7 +96,7 @@ export const routeLabels: Record<Page, string> = {
|
||||
projectWizard: "新建视频项目",
|
||||
pipeline: "生产管线",
|
||||
library: "成品库",
|
||||
account: "账户",
|
||||
account: "账单库",
|
||||
team: "团队",
|
||||
messages: "消息",
|
||||
assetFactory: "图片工具",
|
||||
|
||||
@@ -92,7 +92,7 @@ img, svg, video { display: block; max-width: 100%; }
|
||||
.divider { height: 1px; background: var(--border); margin: 14px 0; }
|
||||
|
||||
/* ─── App shell ─── */
|
||||
.app { display: grid; grid-template-columns: 164px 1fr; min-height: 100vh; }
|
||||
.app { display: grid; grid-template-columns: var(--sidebar-width, 192px) 1fr; min-height: 100vh; }
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
aside.sidebar {
|
||||
@@ -177,15 +177,46 @@ nav a.disabled:hover { background: transparent; color: var(--ink-4); }
|
||||
.user .em { font-size: 13px; }
|
||||
|
||||
/* ─── Main + grid background ─── */
|
||||
main { position: relative; overflow: hidden; background: var(--bg); }
|
||||
main { position: relative; overflow: hidden; background: #fff; }
|
||||
.grid-bg {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60'><circle cx='60' cy='60' r='0.9' fill='%23CFCABB'/></svg>"),
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><g stroke='%23E2DED2' stroke-width='1' fill='none' stroke-dasharray='1.5 4'><path d='M240 0 L240 240'/><path d='M0 240 L240 240'/></g></svg>");
|
||||
background-size: 60px 60px, 240px 240px;
|
||||
mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
|
||||
linear-gradient(rgba(0, 47, 167, 0.16) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 47, 167, 0.16) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
background-position: -1px -1px;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.58) 2%,
|
||||
rgba(0, 0, 0, 0.3) 16%,
|
||||
rgba(0, 0, 0, 0.12) 30%,
|
||||
rgba(0, 0, 0, 0.03) 40%,
|
||||
transparent 46%,
|
||||
transparent 54%,
|
||||
rgba(0, 0, 0, 0.03) 60%,
|
||||
rgba(0, 0, 0, 0.12) 70%,
|
||||
rgba(0, 0, 0, 0.3) 84%,
|
||||
rgba(0, 0, 0, 0.58) 98%,
|
||||
#000 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.58) 2%,
|
||||
rgba(0, 0, 0, 0.3) 16%,
|
||||
rgba(0, 0, 0, 0.12) 30%,
|
||||
rgba(0, 0, 0, 0.03) 40%,
|
||||
transparent 46%,
|
||||
transparent 54%,
|
||||
rgba(0, 0, 0, 0.03) 60%,
|
||||
rgba(0, 0, 0, 0.12) 70%,
|
||||
rgba(0, 0, 0, 0.3) 84%,
|
||||
rgba(0, 0, 0, 0.58) 98%,
|
||||
#000 100%
|
||||
);
|
||||
}
|
||||
.scatter { position: absolute; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.05; color: var(--ink-4); white-space: pre; pointer-events: none; opacity: .8; letter-spacing: .04em; }
|
||||
.tag-corner { position: absolute; color: var(--ink-3); font-family: 'JetBrains Mono', monospace; font-size: 12px; letter-spacing: .06em; pointer-events: none; opacity: .85; z-index: 1; }
|
||||
@@ -490,21 +521,23 @@ table.t tbody tr:hover { background: var(--bg-soft); }
|
||||
.modal-bg {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .42);
|
||||
display: none; align-items: center; justify-content: center;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s ease, visibility .2s ease;
|
||||
}
|
||||
.modal-bg.show { display: flex; opacity: 1; }
|
||||
.modal-bg.show { opacity: 1; visibility: visible; pointer-events: auto; }
|
||||
.modal {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
max-width: 480px; width: 90%;
|
||||
position: relative;
|
||||
transform: scale(.96);
|
||||
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform: translateY(8px) scale(.98);
|
||||
transition: transform .22s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.modal-bg.show .modal { transform: scale(1); }
|
||||
.modal-bg.show .modal { transform: none; }
|
||||
.modal::before, .modal::after { content: '+'; position: absolute; color: var(--ink-3); font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1; }
|
||||
.modal::before { top: -8px; left: -8px; }
|
||||
.modal::after { bottom: -8px; right: -8px; }
|
||||
@@ -524,10 +557,14 @@ table.t tbody tr:hover { background: var(--bg-soft); }
|
||||
.drawer-bg {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .32);
|
||||
display: none;
|
||||
display: block;
|
||||
z-index: 90;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s ease, visibility .2s ease;
|
||||
}
|
||||
.drawer-bg.show { display: block; }
|
||||
.drawer-bg.show { opacity: 1; visibility: visible; pointer-events: auto; }
|
||||
.drawer {
|
||||
position: fixed; right: 0; top: 0; bottom: 0;
|
||||
width: 540px; max-width: 100vw;
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
--st-line: rgba(34, 42, 54, 0.10);
|
||||
--st-wash: rgba(34, 42, 54, 0.045);
|
||||
--st-shadow: 0 8px 20px rgba(20, 27, 38, 0.09);
|
||||
max-width: 1140px;
|
||||
padding-bottom: 44px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
box-sizing: border-box;
|
||||
margin: -24px -28px -60px;
|
||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||
|
||||
.page-head { margin-bottom: 22px; }
|
||||
.page-head h1 { font-size: 28px; }
|
||||
.page-head .sub { margin-top: 6px; font-size: 15px; color: var(--st-muted); }
|
||||
.page-head { margin-bottom: 34px; }
|
||||
.page-head h1 { font-size: 32px; line-height: 1.2; letter-spacing: -.02em; }
|
||||
.page-head .sub { margin-top: 10px; font-size: 15px; color: var(--st-muted); }
|
||||
|
||||
.tc-ghost,
|
||||
.tc-primary,
|
||||
@@ -364,6 +367,7 @@
|
||||
.list-pager { padding: 12px 20px 16px; border-top: 1px solid rgba(34, 42, 54, 0.07); }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
margin: -28px -24px -48px;
|
||||
.tc-hero { grid-template-columns: minmax(230px, 1fr) repeat(2, minmax(125px, 0.7fr)); }
|
||||
.tc-grid { grid-template-columns: 1fr; }
|
||||
.tc-row { grid-template-columns: minmax(140px, 1.2fr) 64px minmax(80px, 0.7fr) minmax(80px, 0.7fr) 92px; }
|
||||
|
||||
Reference in New Issue
Block a user