diff --git a/core/frontend/nginx.conf b/core/frontend/nginx.conf index 4863094..9bb0431 100644 --- a/core/frontend/nginx.conf +++ b/core/frontend/nginx.conf @@ -19,7 +19,10 @@ server { root /usr/share/nginx/html; index index.html; - client_max_body_size 50m; + # 上传视频提炼允许 200MB(见 video_digest.MAX_UPLOAD_BYTES)。multipart 还有表单开销, + # 给到 220m。以前 50m 会在进 Django 前直接 413,手机拍的参考片很容易超。 + client_max_body_size 220m; + client_body_timeout 300s; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; @@ -31,7 +34,11 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $fwd_proto; + # web 容器只有 128Mi,不要把整段视频缓存在 nginx 里再转发。 + proxy_request_buffering off; proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 60s; } # Django 自带后台(已从 /admin/ 挪到 /django-admin/);/admin/* 留给前端 SPA 平台后台,走底部 try_files 回落 index.html diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 1eed76a..3c2ea4a 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -21,7 +21,7 @@ import type { } from "./types"; import { publicModelDisplayName } from "./model-display"; import { generationErrorText } from "./generation-error"; -import { CornerMarks, Decorations, Sidebar, ToastLike } from "./components/app-shell"; +import { CornerMarks, Decorations, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell"; import { SystemLoading } from "./components/loading"; import { AccountPage, @@ -43,35 +43,13 @@ import { TeamPage } from "./routes"; import type { AuthMode, NavigateOptions, Notice, Page, ResolvedRoute } from "./routes/route-config"; -import { isOwnerOnlyPage, pathForPage, resolveRoute, routeLabels } from "./routes/route-config"; +import { isOwnerOnlyPage, pathForPage, resolveRoute } from "./routes/route-config"; import { AdminApp } from "./routes/admin/admin-app"; import { TrashPage } from "./routes/trash"; import { ModelsPage } from "./routes/models"; import { money } from "./routes/stage-config"; import type { ProductBatchResult } from "./routes/products"; -const crumbLabels: Partial> = { - dashboard: "工作台", - products: "商品库", - productDetail: "商品详情", - productCreateUpload: "商品库", - projects: "视频项目", - projectWizard: "新建视频项目", - pipeline: "生产管线", - library: "资产库", - account: "消费", - team: "团队", - messages: "消息中心", - assetFactory: "图片生成", - imageOptimize: "图片创作", - modelPhoto: "模特上身图", - modelPhotoDemoA: "模特图方案 A", - modelPhotoDemoB: "模特图方案 B", - platformCover: "平台套图", - settings: "设置", - settingsNotify: "设置" -}; - /* 图片生成工作台·跨刷新持久化:把在跑的任务 id + 已出结果按 mode 存本地, 刷新后可恢复"生成中"占位并继续轮询(worker 在后台出图,任务永不丢)。 */ const imgwbKey = (mode?: string) => `airshelf:imgwb:${mode || "image"}`; @@ -1004,12 +982,13 @@ export function App() { case "settingsNotify": return setNotice({ type: "success", text })} onLogout={logout} />; default: - return action(() => api.createProduct(payload), "")} />; + return action(() => api.createProduct(payload), "")} />; } } const avatarChar = (currentTeam.name || currentUser.username || "A").slice(0, 1).toUpperCase(); - const here = crumbLabels[page] || routeLabels[page] || "工作台"; + const topModule = topModuleForPage(page); + const searchKbd = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘K" : "Ctrl K"; // 生产管线 · 全屏 bespoke 布局(自带顶栏 + 步进器),脱离常规 shell // 这里只认「当前激活项目的完整详情」(含 stages/timeline/script_versions/metadata 等嵌套字段)。 @@ -1153,18 +1132,29 @@ export function App() {
-
- {page === "dashboard" ? ( - 工作台 - ) : ( - <> - { event.preventDefault(); navigate("dashboard"); }}>工作台 - / - {here} - - )} +
+ + +
+ navigate("account")}> 余额 {money(billing?.account.balance)} diff --git a/core/frontend/src/ai-tools-page.css b/core/frontend/src/ai-tools-page.css index c3edc10..7329344 100644 --- a/core/frontend/src/ai-tools-page.css +++ b/core/frontend/src/ai-tools-page.css @@ -10,95 +10,308 @@ A · 图片生成入口(.asset-factory) ============================================================ */ -/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片创作 · 等比)─── */ -.asset-factory .factory-hero { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - margin-bottom: 56px; -} -@media (max-width: 1400px) { - .asset-factory .factory-hero { grid-template-columns: 1fr 1fr; } -} -@media (max-width: 1000px) { - .asset-factory .factory-hero { grid-template-columns: 1fr; } -} - -.asset-factory .factory-card { - position: relative; - background: var(--surface); - border: 1px solid var(--border-faint); - border-radius: var(--r-md); - padding: 28px 30px; - overflow: hidden; -} - -.asset-factory .factory-body { - display: flex; - flex-direction: column; - gap: 18px; - height: 100%; - /* 盖掉旧全局 .factory-body(styles.css) 漏进来的 align-items:center —— 否则内容被居中、宽卡里偏离左边老远 */ - align-items: stretch; -} -.asset-factory .factory-text { display: flex; flex-direction: column; min-width: 0; } - -.asset-factory .factory-tag { - align-self: flex-start; - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--black-alpha-48); - letter-spacing: .06em; - padding: 2px 8px; - border: 1px solid var(--border-faint); - background: var(--background-lighter); - border-radius: var(--r-sm); - margin-bottom: 14px; -} -.asset-factory .factory-title { - font-size: 22px; +/* ─── YZ 图片创作入口 · 页头 + 三张入口卡 + 最近创作网格 ─── */ +.asset-factory .af-hero-head { margin: 4px 0 28px; } +.asset-factory .af-hero-head h1 { + font-size: 32px; font-weight: 600; - letter-spacing: -.018em; - line-height: 1.25; + letter-spacing: -.03em; + line-height: 1.2; color: var(--accent-black); } -.asset-factory .factory-desc { - margin-top: 8px; - font-size: 13.5px; - color: var(--black-alpha-64); - line-height: 1.55; +.asset-factory .af-hero-head p { + margin-top: 10px; + font-size: 14px; + color: var(--black-alpha-48); } -/* CTA 行:主按钮 + 价格 mono */ -.asset-factory .factory-cta { - margin-top: auto; - padding-top: 24px; +.asset-factory .af-hero { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + margin-bottom: 48px; +} +.asset-factory .af-hero-card { + min-width: 0; + aspect-ratio: 3 / 2; + position: relative; + display: flex; + flex-direction: column; + padding: 0; + border: 0; + border-radius: 12px; + background: var(--surface); + box-shadow: var(--shadow-floating), inset 0 0 0 1px var(--border-faint); + overflow: hidden; + text-align: left; + cursor: pointer; + font: inherit; + color: inherit; + transition: transform var(--t-fast), box-shadow var(--t-base); +} +.asset-factory .af-hero-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px rgba(17, 19, 24, .08), inset 0 0 0 1px var(--border-faint); +} +.asset-factory .af-hero-visual { + flex: 1.65 1 0; + min-height: 0; + width: 100%; + background: var(--black-alpha-4); + overflow: hidden; +} +.asset-factory .af-hero-visual img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.asset-factory .af-hero-card.gradient .af-hero-visual { + background: linear-gradient(145deg, var(--accent-amethyst) 0%, #c4b5fd 42%, var(--heat) 100%); +} +.asset-factory .af-hero-copy { + flex: 1 1 0; + min-height: 0; + position: relative; + padding: 16px 40px 16px 18px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 6px; +} +.asset-factory .af-hero-copy .t { + font-size: 16px; + font-weight: 600; + letter-spacing: -.01em; + color: var(--accent-black); +} +.asset-factory .af-hero-copy .d { + font-size: 13px; + line-height: 1.5; + color: var(--black-alpha-48); +} +.asset-factory .af-hero-arrow { + position: absolute; + top: 14px; + right: 14px; + width: 22px; + height: 22px; + color: var(--accent-bluetron); + display: grid; + place-items: center; + pointer-events: none; +} +.asset-factory .af-hero-arrow svg { width: 16px; height: 16px; } + +.asset-factory .af-recent { margin-top: 8px; } +.asset-factory .af-recent-head { margin-bottom: 18px; } +.asset-factory .af-recent-head h2 { + font-size: 20px; + font-weight: 600; + letter-spacing: -.02em; + color: var(--accent-black); +} +.asset-factory .af-recent-head p { + margin-top: 6px; + font-size: 13px; + color: var(--black-alpha-48); +} + +.asset-factory .af-toolbar { display: flex; align-items: center; - gap: 14px; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 18px; } -.asset-factory .factory-cta .cost { - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--black-alpha-48); - letter-spacing: .04em; +.asset-factory .af-pills { + display: flex; + flex-direction: row; + gap: 8px; } +.asset-factory .af-pill { + height: 36px; + padding: 0 16px; + border: 0; + border-radius: var(--r-pill); + background: transparent; + color: var(--black-alpha-56); + font: inherit; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} +.asset-factory .af-pill:hover { background: var(--black-alpha-4); color: var(--accent-black); } +.asset-factory .af-pill.active { + background: var(--nav-active); + color: var(--accent-white); +} +.asset-factory .af-toolbar-right { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.asset-factory .af-search { + position: relative; + width: 200px; +} +.asset-factory .af-search svg { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--black-alpha-40); + pointer-events: none; +} +.asset-factory .af-search input { + width: 100%; + height: 36px; + padding: 0 12px 0 34px; + border: 1px solid var(--border-faint); + border-radius: var(--r-md); + background: var(--surface); + font: inherit; + font-size: 13px; + color: var(--accent-black); +} +.asset-factory .af-search input::placeholder { color: var(--black-alpha-40); } +.asset-factory .af-search input:focus { + outline: none; + border-color: var(--heat-40); + box-shadow: inset 0 0 0 1px var(--heat-40); +} +.asset-factory .af-view { + display: inline-flex; + height: 36px; + padding: 3px; + border: 1px solid var(--border-faint); + border-radius: var(--r-md); + background: var(--surface); + gap: 2px; +} +.asset-factory .af-view button { + width: 30px; + height: 28px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--black-alpha-40); + display: grid; + place-items: center; + cursor: pointer; + padding: 0; +} +.asset-factory .af-view button.active { + background: var(--black-alpha-7); + color: var(--accent-black); +} +.asset-factory .af-view button:hover { color: var(--accent-black); } -/* ─── 任务中心 · section header(基线 .section-h 带 mono sub)─── */ -.asset-factory .section-h { margin-top: 24px; } -.asset-factory .section-h .sub-mono { - font-family: var(--font-mono); +.asset-factory .af-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} +.asset-factory .af-card { + background: var(--surface); + border-radius: 12px; + overflow: hidden; + cursor: pointer; + box-shadow: var(--shadow-floating); + outline: none; +} +.asset-factory .af-card:hover { box-shadow: 0 12px 32px rgba(17, 19, 24, .08); } +.asset-factory .af-card-visual { + position: relative; + aspect-ratio: 4 / 3; + background: var(--black-alpha-4); + overflow: hidden; +} +.asset-factory .af-card-visual img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.asset-factory .af-card-prog { + position: absolute; + left: 10px; + bottom: 10px; + height: 24px; + padding: 0 10px; + border-radius: var(--r-pill); + background: rgba(17, 19, 24, .72); + color: var(--accent-white); + font-size: 12px; + font-weight: 500; + display: inline-flex; + align-items: center; +} +.asset-factory .af-card-body { padding: 14px 14px 16px; } +.asset-factory .af-card-name { + font-size: 14px; + font-weight: 600; + color: var(--accent-black); + line-height: 1.35; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.asset-factory .af-card-meta { + margin-top: 6px; font-size: 12px; color: var(--black-alpha-48); - letter-spacing: .02em; +} +.asset-factory .af-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 12px; +} +.asset-factory .af-tag { + height: 22px; + padding: 0 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + display: inline-flex; + align-items: center; + white-space: nowrap; +} +.asset-factory .af-tag.ok { + background: var(--forest-bg); + color: var(--accent-forest); +} +.asset-factory .af-tag.info { + background: var(--heat-12); + color: var(--accent-bluetron); +} +.asset-factory .af-tag.err { + background: var(--crimson-bg); + color: var(--accent-crimson); +} +.asset-factory .af-tag.type { + background: transparent; + color: var(--black-alpha-56); + box-shadow: inset 0 0 0 1px var(--border-muted); +} +.asset-factory .af-tag.type.image { + color: var(--accent-bluetron); + box-shadow: inset 0 0 0 1px rgba(42, 109, 251, .28); } -.asset-factory .result-meta { - font-family: var(--font-mono); - font-size: 12px; - color: var(--black-alpha-48); - letter-spacing: .02em; - margin: 2px 0 12px; +@media (max-width: 1280px) { + .asset-factory .af-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} +@media (max-width: 1100px) { + .asset-factory .af-hero { grid-template-columns: 1fr; } + .asset-factory .af-grid { grid-template-columns: 1fr 1fr; } +} +@media (max-width: 720px) { + .asset-factory .af-grid { grid-template-columns: 1fr; } } /* ─── 列表视图 · 表格容器(基线 #task-list-view)─── */ @@ -149,6 +362,8 @@ 50% { opacity: .55; transform: scaleY(.7); } } +.asset-factory .list-pager { margin-top: 20px; } + .asset-factory .task-empty { padding: 60px 20px; text-align: center; @@ -259,18 +474,12 @@ border-radius: var(--r-sm); font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer; - box-shadow: - inset 0 -2px 4px rgba(250, 93, 25, 0.20), - 0 1px 1px rgba(250, 93, 25, 0.12), - 0 2px 4px rgba(250, 93, 25, 0.10); + box-shadow: var(--shadow-cta); transition: filter var(--t-base), transform var(--t-fast), box-shadow var(--t-base); } .image-workbench .iw-list-h .new-prod:hover { filter: brightness(.96); - box-shadow: - inset 0 -2px 4px rgba(250, 93, 25, 0.20), - 0 1px 1px rgba(250, 93, 25, 0.16), - 0 4px 8px rgba(250, 93, 25, 0.20); + box-shadow: var(--shadow-cta-hover); } .image-workbench .iw-list-h .new-prod:active { transform: scale(.98); } .image-workbench .iw-list-h .new-prod svg { width: 12px; height: 12px; } diff --git a/core/frontend/src/api.ts b/core/frontend/src/api.ts index 5783247..eb0eba2 100644 --- a/core/frontend/src/api.ts +++ b/core/frontend/src/api.ts @@ -139,6 +139,10 @@ async function request(path: string, options: RequestInit = {}): Promise { if (response.status === 403 && message === "permission denied") { message = "当前账号权限不足"; } + // nginx / 网关在请求进 Django 前拦大文件,响应体是 HTML「413 Request Entity Too Large」。 + if (response.status === 413) { + message = "文件太大,服务器没接收。视频请压到 200MB 以内再传"; + } throw new ApiError(response.status, message); } if (response.status === 204) return undefined as T; diff --git a/core/frontend/src/components/IconKitSvg.tsx b/core/frontend/src/components/IconKitSvg.tsx index eed387f..d422381 100644 --- a/core/frontend/src/components/IconKitSvg.tsx +++ b/core/frontend/src/components/IconKitSvg.tsx @@ -28,7 +28,11 @@ const iconPaths: Record = { gauge: '', sliders: '', logOut: '', - film: '' + film: '', + wand: '', + scan: '', + swap: '', + arrowRight: '' }; const iconAliases: Record = { diff --git a/core/frontend/src/components/app-shell.tsx b/core/frontend/src/components/app-shell.tsx index 17f8fc7..ef024e0 100644 --- a/core/frontend/src/components/app-shell.tsx +++ b/core/frontend/src/components/app-shell.tsx @@ -205,6 +205,35 @@ const NAV: NavDef[] = [ { id: "trash", page: "trash", label: "垃圾桶", icon: "trash" }, { 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: "trash", page: "trash", label: "垃圾桶" }, +]; + +export type TopModule = "workbench" | "image" | "video"; +const OPEN_PALETTE_EVENT = "airshelf:open-palette"; + +export function openCommandPalette() { + window.dispatchEvent(new Event(OPEN_PALETTE_EVENT)); +} + +export function topModuleForPage(page: Page): TopModule | null { + if (page === "dashboard") return "workbench"; + if ( + page === "assetFactory" + || page === "imageOptimize" + || page === "modelPhoto" + || page === "modelPhotoDemoA" + || page === "modelPhotoDemoB" + || page === "platformCover" + ) return "image"; + if (page === "projects" || page === "projectWizard" || page === "pipeline" || page === "freeCreate") return "video"; + return null; +} const PAGE_TO_NAV: Partial> = { dashboard: "dashboard", @@ -251,6 +280,14 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p const activeNav = PAGE_TO_NAV[page]; // 子账号:导航里去掉「团队」「消费」两项(命令面板、账户菜单同步过滤)。 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 = + activeNav === "team" ? "team" + : activeNav === "settings" ? "settings" + : topModule ? null + : resourceNavItems.some((item) => item.page === activeNav) ? "resource" + : null; // 徽标用后端真实总数(分页 count),不用已加载的页内条数 const badges: Partial> = { products: productTotal ?? products.length, projects: projectTotal ?? projects.length }; // YYX#row22:有未读生成任务才显示胶囊(0 不显示) @@ -275,6 +312,12 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [mobileNavOpen]); + // 二级类目展开/收起:进资源中心时展开;切到顶栏模块(工作台/图片/视频)时收起,避免左边再出现选中。 + const [resourceOpen, setResourceOpen] = useState(activeMajor === "resource"); + useEffect(() => { + if (activeMajor === "resource") setResourceOpen(true); + else if (topModule) setResourceOpen(false); + }, [activeMajor, topModule]); // 账户下拉菜单:点侧栏 .user 用其 DOMRect 当锚点展开,再次点击收起(toggle)。 const [accountAnchor, setAccountAnchor] = useState(null); @@ -290,9 +333,8 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p window.location.href = "/login"; }; - // 命令面板:Ctrl/Cmd K 开关,点搜索框打开 + // 命令面板:Ctrl/Cmd K,以及顶栏搜索按钮 const [paletteOpen, setPaletteOpen] = useState(false); - const openCommandPalette = () => setPaletteOpen(true); useEffect(() => { const onKey = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { @@ -300,8 +342,13 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p setPaletteOpen((value) => !value); } }; + const onOpen = () => setPaletteOpen(true); window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + window.addEventListener(OPEN_PALETTE_EVENT, onOpen); + return () => { + window.removeEventListener("keydown", onKey); + window.removeEventListener(OPEN_PALETTE_EVENT, onOpen); + }; }, []); return ( @@ -328,28 +375,61 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p -
{ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openCommandPalette(); } }}> - - - Ctrl K +
+ + + { event.preventDefault(); setMobileNavOpen(false); navigate("team"); }} + > + + { event.preventDefault(); setMobileNavOpen(false); navigate("settings"); }} + > +
-
主要
- {user.is_platform_admin && onOpenAdmin && ( <>
平台
@@ -399,16 +479,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p } export function Decorations() { - // 与设计稿 shell.js 一致:grid-bg 底纹 + 4 个 sq-mark(写死坐标,不压内容) - return ( - <> -
- - - - - - ); + return
; } export function CornerMarks() { diff --git a/core/frontend/src/dashboard-page.css b/core/frontend/src/dashboard-page.css new file mode 100644 index 0000000..799b96a --- /dev/null +++ b/core/frontend/src/dashboard-page.css @@ -0,0 +1,353 @@ +/* 工作台 · YZ 影擎对照布局。仅 scope 在 .dashboard-page,不改共享 .btn/.pill */ +.dashboard-page { + max-width: 1080px; +} + +.dashboard-page .dash-hero { + margin: 8px 0 36px; +} +.dashboard-page .dash-hero h1 { + font-size: 32px; + font-weight: 600; + letter-spacing: -.03em; + line-height: 1.2; + color: var(--accent-black); +} +.dashboard-page .dash-hero-sub { + margin-top: 10px; + font-size: 14px; + color: var(--black-alpha-48); +} + +.dashboard-page .dash-create h2 { + font-size: 18px; + font-weight: 600; + letter-spacing: -.01em; + color: var(--accent-black); + margin-bottom: 18px; +} +.dashboard-page .dash-create-group + .dash-create-group { + margin-top: 22px; +} +.dashboard-page .dash-create-label { + font-size: 13px; + color: var(--black-alpha-48); + margin-bottom: 10px; +} +.dashboard-page .dash-create-row { + display: flex; + flex-direction: row; + gap: 14px; +} +.dashboard-page .dash-create-card { + flex: 1; + min-width: 0; + height: 96px; + padding: 0 22px; + border: 0; + border-radius: 16px; + display: flex; + align-items: center; + gap: 14px; + text-align: left; + cursor: pointer; + font: inherit; + transition: transform var(--t-fast), box-shadow var(--t-base), filter var(--t-base); +} +.dashboard-page .dash-create-card:hover { + transform: translateY(-1px); +} +.dashboard-page .dash-create-ic { + width: 36px; + height: 36px; + flex-shrink: 0; + display: grid; + place-items: center; +} +.dashboard-page .dash-create-ic svg, +.dashboard-page .dash-create-arrow svg { + width: 22px; + height: 22px; +} +.dashboard-page .dash-create-copy { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.dashboard-page .dash-create-copy .t { + font-size: 16px; + font-weight: 600; + letter-spacing: -.01em; +} +.dashboard-page .dash-create-copy .d { + font-size: 13px; + line-height: 1.4; + opacity: .82; +} +.dashboard-page .dash-create-arrow { + flex-shrink: 0; + display: grid; + place-items: center; + opacity: .88; +} + +.dashboard-page .dash-create-card.blue-a { + background: linear-gradient(90deg, #1f4fd4 0%, #3f78f6 100%); + color: var(--accent-white); + box-shadow: 0 10px 28px rgba(47, 93, 232, .28); +} +.dashboard-page .dash-create-card.blue-b { + background: linear-gradient(90deg, #1636b8 0%, #2b5ee6 100%); + color: var(--accent-white); + box-shadow: 0 10px 28px rgba(22, 54, 184, .28); +} +.dashboard-page .dash-create-card.blue-a:hover, +.dashboard-page .dash-create-card.blue-b:hover { + filter: brightness(1.04); +} +.dashboard-page .dash-create-card.light { + background: var(--surface); + color: var(--accent-black); + box-shadow: var(--shadow-floating); + border: 1px solid var(--border-faint); +} +.dashboard-page .dash-create-card.light .d { + color: var(--black-alpha-48); + opacity: 1; +} +.dashboard-page .dash-create-card.light:hover { + background: var(--background-lighter); +} + +.dashboard-page .dash-overview { + margin-top: 48px; +} +.dashboard-page .dash-overview-h { + display: flex; + align-items: center; + gap: 14px; + font-size: 13px; + color: var(--black-alpha-48); + margin-bottom: 14px; +} +.dashboard-page .dash-overview-h::before, +.dashboard-page .dash-overview-h::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border-faint); +} +.dashboard-page .dash-overview-bar { + display: flex; + align-items: center; + min-height: 88px; + padding: 18px 8px; + border-radius: 999px; + background: var(--black-alpha-4); +} +.dashboard-page .dash-overview-cell { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + border: 0; + background: transparent; + cursor: pointer; + font: inherit; + color: var(--accent-black); +} +.dashboard-page .dash-overview-cell .lbl { + font-size: 13px; + color: var(--black-alpha-48); +} +.dashboard-page .dash-overview-cell .v { + font-size: 22px; + font-weight: 600; + letter-spacing: -.02em; + font-variant-numeric: tabular-nums; +} +.dashboard-page .dash-overview-cell:hover .v { + color: var(--nav-active); +} + +.dashboard-page .dash-mine { + margin-top: 36px; +} +.dashboard-page .dash-mine-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} +.dashboard-page .dash-mine-head h2 { + font-size: 20px; + font-weight: 600; + letter-spacing: -.02em; + color: var(--accent-black); +} +.dashboard-page .dash-mine-all { + display: inline-flex; + align-items: center; + gap: 2px; + border: 0; + background: transparent; + color: var(--black-alpha-48); + font: inherit; + font-size: 13px; + cursor: pointer; +} +.dashboard-page .dash-mine-all:hover { color: var(--accent-black); } +.dashboard-page .dash-mine-all svg { width: 14px; height: 14px; } + +.dashboard-page .dash-mine-pills { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} +.dashboard-page .dash-mine-pill { + height: 32px; + padding: 0 14px; + border: 0; + border-radius: var(--r-pill); + background: var(--black-alpha-4); + color: var(--accent-black); + font: inherit; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} +.dashboard-page .dash-mine-pill.active { + background: #e8eefc; + color: #2a4fd7; +} + +.dashboard-page .dash-mine-list { + display: flex; + flex-direction: column; + gap: 12px; +} +.dashboard-page .dash-mine-empty { + padding: 28px 16px; + text-align: center; + color: var(--black-alpha-48); + font-size: 13px; +} +.dashboard-page .dash-proj-card { + display: flex; + flex-direction: row; + align-items: center; + gap: 18px; + min-height: 96px; + padding: 16px 18px 16px 20px; + border: 0; + border-radius: 14px; + background: var(--black-alpha-4); + position: relative; + cursor: pointer; + overflow: hidden; +} +.dashboard-page .dash-proj-card::before { + content: ""; + position: absolute; + left: 0; + top: 14px; + bottom: 14px; + width: 3px; + border-radius: 0 2px 2px 0; + background: #3d63ff; +} +.dashboard-page .dash-proj-card:hover { + background: var(--black-alpha-7); +} +.dashboard-page .dash-proj-thumb { + width: 64px; + height: 64px; + border-radius: 10px; + flex-shrink: 0; +} +.dashboard-page .dash-proj-main { + flex: 1; + min-width: 0; +} +.dashboard-page .dash-proj-title { + font-size: 15px; + font-weight: 600; + color: var(--accent-black); + line-height: 1.35; +} +.dashboard-page .dash-proj-sub { + margin-top: 6px; + font-size: 13px; + color: var(--black-alpha-48); + line-height: 1.4; +} +.dashboard-page .dash-proj-stage { + width: 168px; + flex-shrink: 0; +} +.dashboard-page .dash-proj-stage-lbl { + font-size: 12px; + color: var(--black-alpha-48); + margin-bottom: 4px; +} +.dashboard-page .dash-proj-stage-name { + font-size: 14px; + font-weight: 600; + color: var(--accent-black); + margin-bottom: 10px; +} +.dashboard-page .dash-proj-prog { + display: flex; + gap: 4px; +} +.dashboard-page .dash-proj-prog span { + flex: 1; + height: 6px; + border-radius: 3px; + background: var(--black-alpha-8); +} +.dashboard-page .dash-proj-prog span.done { background: var(--accent-forest); } +.dashboard-page .dash-proj-prog span.cur { background: var(--accent-honey); } +.dashboard-page .dash-proj-prog span.fail { background: var(--accent-crimson); } +.dashboard-page .dash-proj-cta { + height: 40px; + min-width: 88px; + padding: 0 22px; + border: 0; + border-radius: 999px; + background: var(--nav-active); + color: var(--accent-white); + font: inherit; + font-size: 14px; + font-weight: 500; + cursor: pointer; + flex-shrink: 0; +} +.dashboard-page .dash-proj-cta:hover { opacity: .9; } + +@media (max-width: 820px) { + .dashboard-page .dash-create-row { + flex-direction: column; + } + .dashboard-page .dash-overview-bar { + border-radius: 16px; + flex-wrap: wrap; + min-height: 0; + } + .dashboard-page .dash-overview-cell { + flex: 1 1 40%; + padding: 8px 0; + } + .dashboard-page .dash-proj-card { + flex-wrap: wrap; + } + .dashboard-page .dash-proj-stage { + width: 100%; + } +} diff --git a/core/frontend/src/design-restraint.css b/core/frontend/src/design-restraint.css index 9f0fa04..3dbf08a 100644 --- a/core/frontend/src/design-restraint.css +++ b/core/frontend/src/design-restraint.css @@ -49,25 +49,26 @@ * { box-sizing: border-box; margin: 0; padding: 0; } :root { - /* ===== Backgrounds (冷灰) ===== */ - --background-base: #f9f9f9; - --background-lighter: #fbfbfb; + /* ===== Backgrounds (冷灰 · YZ 影擎) ===== */ + --background-base: #f7f8fa; + --background-lighter: #fbfbfc; --surface: #ffffff; --surface-raised: #ffffff; /* ===== Borders (冷灰 3 档,语义优先) ===== */ - --border-faint: #ededed; - --border-muted: #e8e8e8; - --border-loud: #e6e6e6; + --border-faint: #eceef2; + --border-muted: #e6e8ee; + --border-loud: #e2e5eb; /* ===== Accent multi-color (5 色信号) ===== */ - --accent-black: #262626; + --accent-black: #17191d; --accent-white: #ffffff; --accent-amethyst: #9061ff; --accent-bluetron: #2a6dfb; --accent-crimson: #eb3424; --accent-forest: #42c366; --accent-honey: #ecb730; + --nav-active: #111318; /* status 配套底/边 */ --forest-bg: rgba(66, 195, 102, .08); @@ -77,15 +78,16 @@ --honey-bg: rgba(236, 183, 48, .08); --honey-bd: rgba(236, 183, 48, .20); - /* ===== Heat · 单 hue + 8 档 alpha ===== */ - --heat: #fa5d19; - --heat-90: rgba(250, 93, 25, .90); - --heat-40: rgba(250, 93, 25, .40); - --heat-20: rgba(250, 93, 25, .20); - --heat-16: rgba(250, 93, 25, .16); - --heat-12: rgba(250, 93, 25, .12); - --heat-8: rgba(250, 93, 25, .08); - --heat-4: rgba(250, 93, 25, .04); + /* ===== Heat · 单 hue + 8 档 alpha · 丁香蓝(对齐 YZ 主 CTA) ===== */ + --heat: #A5B4FC; + --heat-hover: #8ea0f8; + --heat-90: rgba(165, 180, 252, .90); + --heat-40: rgba(165, 180, 252, .40); + --heat-20: rgba(165, 180, 252, .20); + --heat-16: rgba(165, 180, 252, .16); + --heat-12: rgba(165, 180, 252, .12); + --heat-8: rgba(165, 180, 252, .08); + --heat-4: rgba(165, 180, 252, .04); /* ===== Black-alpha 阶梯 (20 档) ===== */ --black-alpha-1: rgba(0, 0, 0, .01); @@ -128,19 +130,14 @@ /* ===== Shadows ===== */ --shadow-cta: - inset 0 -4px 8px rgba(250, 93, 25, .20), - 0 1px 1px rgba(250, 93, 25, .12), - 0 2px 4px rgba(250, 93, 25, .10), - 0 .5px .5px rgba(250, 93, 25, .16); + 0 1px 2px rgba(129, 140, 248, .16), + 0 6px 16px rgba(165, 180, 252, .28); --shadow-cta-hover: - inset 0 -4px 8px rgba(250, 93, 25, .20), - 0 1px 1px rgba(250, 93, 25, .16), - 0 4px 8px rgba(250, 93, 25, .20), - 0 .5px .5px rgba(250, 93, 25, .16); + 0 2px 4px rgba(129, 140, 248, .20), + 0 10px 22px rgba(165, 180, 252, .34); --shadow-cta-active: - inset 0 -4px 8px rgba(250, 93, 25, .28), - 0 1px 2px rgba(250, 93, 25, .16); - --shadow-floating: 0 4px 20px rgba(21, 20, 15, .06); + 0 1px 2px rgba(129, 140, 248, .18); + --shadow-floating: 0 8px 28px rgba(17, 19, 24, .06); } ::selection { background: var(--heat-20); color: var(--heat); } @@ -235,6 +232,8 @@ aside.sidebar { top: 0; height: 100vh; overflow-y: auto; + display: flex; + flex-direction: column; transition: padding var(--t-base); } .sidebar-head { @@ -251,7 +250,7 @@ aside.sidebar { transition: width var(--t-base); } .brand-logo { display: block; width: 142px; max-width: none; height: auto; margin: -8px 0 -6px -8px; object-fit: contain; } -.brand-mark, .flame { width: 22px; height: 22px; color: var(--heat); } +.brand-mark, .flame { width: 22px; height: 22px; color: var(--accent-black); } .brand-mark svg, .flame svg { width: 100%; height: 100%; } .brand .name { font-weight: 600; font-size: 18px; letter-spacing: -.012em; color: var(--accent-black); } .sidebar-toggle { @@ -380,6 +379,203 @@ 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 左侧导航预览(资源中心胶囊 + 子库) ─── */ +.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: 54px; + margin-left: -16px; + margin-right: 22px; + border: 0; + border-radius: 0 28px 28px 0; + background: transparent; + color: #2d3442; + display: flex; + align-items: center; + gap: 10px; + padding: 0 16px 0 18px; + font-size: 14px; + font-weight: 600; + font-family: inherit; + cursor: pointer; +} +.yz-resource-head.active { + background: var(--nav-active); + color: var(--accent-white); +} +.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: 15px; + height: 15px; + color: #2d3442; +} +.yz-resource-head.active > svg { color: rgba(255, 255, 255, .95); } +.yz-resource-head .chev { + margin-left: auto; + display: inline-flex; + 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 { + 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; +} +.yz-resource-sub::before { + content: ""; + position: absolute; + left: 0; + top: 4px; + bottom: 6px; + width: 1px; + background: #d7dbe3; +} +.yz-resource-sub a { + min-height: 38px; + border-radius: 11px; + padding: 0 12px; + color: #7f838b; + display: flex; + align-items: center; + gap: 9px; + font-size: 15px; + font-weight: 500; + transition: background .15s, color .15s; +} +.yz-resource-sub a .dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: #c8c8c8; +} +.yz-resource-sub a:hover { + background: #f1f3f8; + color: #5e6571; +} +.yz-resource-sub a.active { + background: #e9eefb; + color: #2148bf; + font-weight: 600; +} +.yz-resource-sub a.active .dot { + width: 7px; + height: 7px; + background: #3158d4; +} +.yz-resource-sub a .pill-mini { + margin-left: auto; + border-radius: 999px; + border: 1px solid #d4dcf4; + padding: 0 7px; + line-height: 18px; + font-size: 11px; + color: #4a5ca0; + background: #f4f7ff; +} +.yz-resource-sub.collapsed { + display: none; + max-height: 0; + opacity: 0; + margin-top: 0; + pointer-events: none; +} +.yz-side-link { + min-height: 54px; + margin-left: -16px; + margin-right: 22px; + border-radius: 0 28px 28px 0; + display: flex; + align-items: center; + gap: 10px; + padding: 0 16px 0 18px; + color: #2d3442; + font-size: 15px; + font-weight: 600; + transition: background .22s ease, color .22s ease, box-shadow .22s ease; +} +.yz-side-link:hover { background: #f2f4f8; } +.yz-side-link > svg { + width: 20px; + height: 20px; + color: #4f5562; + transition: color .22s ease; +} +.yz-side-link.active { + color: #ffffff; + background: var(--nav-active); + box-shadow: 0 6px 18px rgba(17, 19, 24, .14); +} +.yz-side-link.active > svg { + color: rgba(255, 255, 255, .96); +} +.aside-foot { + margin-top: auto; + padding-top: 14px; + border-top: 0; +} +.user { + border-radius: 999px; + background: #f3f4f7; + justify-content: space-between; + padding: 8px 14px 8px 8px; +} +.user .av { + width: 36px; + height: 36px; + border-radius: 50%; + background: #17191d; + font-size: 14px; +} +.user .em { + flex: 1; + margin-left: 8px; + font-size: 14px; + color: #2f3340; + line-height: 1.2; +} +.user::after { + content: "›"; + color: #575f6f; + font-size: 20px; + line-height: 1; +} + body.sidebar-collapsed aside.sidebar { padding: 22px 12px; } body.sidebar-collapsed .sidebar-head { gap: 6px; padding: 2px 0 16px; } body.sidebar-collapsed .brand-clip { @@ -420,16 +616,18 @@ body.sidebar-collapsed .user { justify-content: center; padding: 8px 0; } +body.sidebar-collapsed .yz-side-main, +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; } .grid-bg { position: absolute; inset: 0; pointer-events: none; background-image: - url("data:image/svg+xml;utf8,"), url("data:image/svg+xml;utf8,"), url("data:image/svg+xml;utf8,"); - background-size: 240px 240px, 60px 60px, 240px 240px; + 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%); } @@ -450,20 +648,77 @@ main { position: relative; background: var(--background-base); min-width: 0; } /* ─── Topbar ─── */ .topbar { - display: flex; align-items: center; gap: 12px; - padding: 14px 28px; + display: flex; align-items: center; gap: 16px; + padding: 16px 28px; border-bottom: 1px solid var(--border-faint); - background: var(--background-base); + background: var(--surface); position: sticky; top: 0; z-index: 50; + min-height: 72px; } .crumbs { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--black-alpha-48); } .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 { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + gap: 8px; + min-height: 44px; +} +.yz-mod { + height: 40px; + padding: 0 20px; + border: 0; + border-radius: var(--r-pill); + background: var(--black-alpha-4); + color: var(--black-alpha-56); + font: inherit; + font-size: 14px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; + transition: background var(--t-base), color var(--t-base); +} +.yz-mod:hover { background: var(--black-alpha-8); color: var(--accent-black); } +.yz-mod.active { + background: var(--nav-active); + color: var(--accent-white); + font-weight: 600; +} +.top-search { + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + min-width: 196px; + padding: 0 12px; + border: 1px solid var(--border-faint); + border-radius: var(--r-pill); + background: var(--background-lighter); + color: var(--black-alpha-48); + font: inherit; + font-size: 13px; + cursor: pointer; + transition: border-color var(--t-base), background var(--t-base); +} +.top-search:hover { border-color: var(--black-alpha-24); background: var(--surface); } +.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 { + margin-left: auto; + font-family: var(--font-inter); + font-weight: 700; + font-size: 12px; + color: var(--black-alpha-40); + letter-spacing: .02em; +} .topbar .right { margin-left: auto; display: flex; align-items: center; gap: 10px; } .balance-chip { display: inline-flex; align-items: center; gap: 7px; - height: 36px; + height: 40px; padding: 0 14px 0 12px; background: var(--surface); border: 1px solid var(--border-faint); @@ -475,9 +730,9 @@ main { position: relative; background: var(--background-base); min-width: 0; } } .balance-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); } .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(--heat); } +.balance-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--black-alpha-48); } .icon-btn { - width: 36px; height: 36px; + width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: var(--surface); border: 1px solid var(--border-faint); @@ -492,7 +747,7 @@ main { position: relative; background: var(--background-base); min-width: 0; } .icon-btn .dot-noti { position: absolute; top: 8px; right: 9px; width: 7px; height: 7px; border-radius: 50%; - background: var(--heat); border: 1.5px solid var(--surface); + background: var(--accent-crimson); border: 1.5px solid var(--surface); } .icon-btn .count-noti { position: absolute; top: -4px; right: -4px; @@ -534,7 +789,7 @@ main { position: relative; background: var(--background-base); min-width: 0; } /* ─── Topbar · 头像 ─── */ .topbar-avatar { - width: 36px; height: 36px; + width: 40px; height: 40px; border-radius: var(--r-pill); background: var(--accent-black); color: var(--accent-white); @@ -641,8 +896,8 @@ main { position: relative; background: var(--background-base); min-width: 0; } box-shadow: var(--shadow-cta); } .btn-primary:hover { - background: var(--heat); - border-color: var(--heat); + background: var(--heat-hover); + border-color: var(--heat-hover); box-shadow: var(--shadow-cta-hover); } .btn-primary:active { diff --git a/core/frontend/src/main.tsx b/core/frontend/src/main.tsx index 90da611..c166410 100644 --- a/core/frontend/src/main.tsx +++ b/core/frontend/src/main.tsx @@ -2,6 +2,7 @@ import { createRoot } from "react-dom/client"; import { App } from "./App"; import "./styles.css"; import "./design-restraint.css"; +import "./dashboard-page.css"; import "./account-page.css"; import "./product-detail-page.css"; import "./team-page.css"; diff --git a/core/frontend/src/projects-page.css b/core/frontend/src/projects-page.css index f60c0a0..953aff6 100644 --- a/core/frontend/src/projects-page.css +++ b/core/frontend/src/projects-page.css @@ -1,7 +1,358 @@ /* 视频项目页 · 从 public/exact/projects.html 内联