统一 Airshelf 界面组件与图标
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s

This commit is contained in:
iye 2026-05-27 12:29:41 +08:00
parent ea335587d2
commit 086d92991e
47 changed files with 750 additions and 450 deletions

View File

@ -1,4 +1,4 @@
# 流·Studio · 电商 AI 平台 · Claude Code 工程约定
# Airshelf · 电商 AI 平台 · Claude Code 工程约定
> **本文件由 Claude Code 启动时自动加载。所有 AI 协作必须遵循以下规则。**
@ -6,7 +6,7 @@
## 项目简介
**流·Studio** · AI 短视频带货生成平台 · 5 阶段流水线(商品 → 故事板 → 镜头 → 生成 → 投放)
**Airshelf** · AI 短视频带货生成平台 · 5 阶段流水线(商品 → 故事板 → 镜头 → 生成 → 投放)
- **设计代号:** Restraint · V2.1 · Firecrawl-aligned
- **主要工作目录:** [电商AI平台/](电商AI平台/)
@ -53,7 +53,7 @@
- **单橙锚点** · 全场只有一个 accent · hover 用 alpha 不用换 hue
- **Mono 装饰必有** · `[ 200 OK ]` `// 05.14` `[ /v2 ]`(品牌签名)
- **主 CTA 唯一允许阴影** · 4 层橙色发光 · 其他场景禁阴影
- **Inter(英)+ Alibaba PuHuiTi(中)+ JetBrains Mono(装饰)** · 字符级 fallthrough
- **Inter(英/数字/装饰)+ Alibaba PuHuiTi(中)** · 字符级 fallthrough
- **字重仅 3 档** · 400 / 500 / 600 · 700 仅给 Ctrl K 徽标
---

View File

@ -2,8 +2,8 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建项目 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<title>新建项目 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
.wizard { display: grid; grid-template-columns: 200px minmax(0, 1fr) 300px; gap: 36px; align-items: start; max-width: 1400px; }

View File

@ -27,7 +27,8 @@
--blue-soft: #E8EFFB;
--topbar-h: 57px;
--aside-w: 248px;
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--mono: "Inter", system-ui, -apple-system, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--sans: "Inter", system-ui, -apple-system, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
@ -68,8 +69,8 @@ aside.sidebar {
position: relative;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 6px 8px 14px; }
.flame { width: 22px; height: 22px; color: var(--orange); }
.flame svg { width: 100%; height: 100%; }
.brand-mark, .flame { width: 22px; height: 22px; color: var(--orange); }
.brand-mark svg, .flame svg { width: 100%; height: 100%; }
.brand-name { font-weight: 700; font-size: 18px; letter-spacing: -.012em; }
.brand-ver {
font-family: var(--mono); font-size: 9.5px; color: var(--ink-3);
@ -287,6 +288,7 @@ main.main { position: relative; overflow: hidden; background: var(--bg); min-wid
.btn.btn-ghost:hover { background: var(--bg-soft); color: var(--ink); }
.btn.btn-sm { padding: 5px 11px; font-size: 12px; }
.btn.btn-lg { padding: 10px 18px; font-size: 13.5px; }
.btn.btn-create svg { width: 16px; height: 16px; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn:disabled:hover { background: var(--card); }
@ -561,7 +563,7 @@ main.main { position: relative; overflow: hidden; background: var(--bg); min-wid
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; border: 1px solid var(--orange-soft);
}
.shortcut .ic svg { width: 15px; height: 15px; }
.shortcut .ic svg { width: 16px; height: 16px; }
.shortcut .t { font-size: 13px; font-weight: 600; }
.shortcut .d {
font-size: 11.5px; color: var(--ink-3);

View File

@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/Sidebar";
import GridBg from "@/components/GridBg";
@ -10,14 +10,8 @@ const inter = Inter({
display: "swap",
});
const mono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "流·Studio — AI 短视频生产平台",
title: "Airshelf — AI 短视频生产平台",
description:
"为抖音 / TikTok 商户打造的 AI 短视频生产流水线 · 脚本 → 基础资产 → 故事板 → 视频片段 → 拼接导出",
};
@ -28,7 +22,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" className={`${inter.variable} ${mono.variable}`}>
<html lang="zh-CN" className={inter.variable}>
<body>
<div className="app">
<Sidebar />

View File

@ -63,12 +63,12 @@ export default function WorkspacePage() {
</div>
</div>
<div className="actions">
<Link className="btn" href="/products">
<Icon name="plus" size={14} />
<Link className="btn btn-create" href="/products">
<Icon name="product-plus" size={16} />
</Link>
<Link className="btn btn-primary" href="/projects/new">
<Icon name="plus" size={14} />
<Link className="btn btn-primary btn-create" href="/projects/new">
<Icon name="clapperboard" size={16} />
</Link>
</div>
@ -132,43 +132,28 @@ export default function WorkspacePage() {
</div>
<div className="shortcuts">
<Link className="shortcut" href="/products">
<div className="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8}>
<path d="m3 7 9-4 9 4-9 4-9-4z" />
<path d="m3 12 9 4 9-4M3 17l9 4 9-4" />
</svg>
</div>
<div className="ic"><Icon name="package" /></div>
<div>
<div className="t"></div>
<div className="d">12 SKU</div>
</div>
</Link>
<Link className="shortcut" href="/library">
<div className="ic"><Icon name="bars" /></div>
<div className="ic"><Icon name="images" /></div>
<div>
<div className="t"></div>
<div className="d"> 8 · 14 · 8</div>
</div>
</Link>
<Link className="shortcut" href="/account">
<div className="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8}>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4" />
</svg>
</div>
<div className="ic"><Icon name="credit-card" /></div>
<div>
<div className="t"></div>
<div className="d">¥327.40</div>
</div>
</Link>
<Link className="shortcut" href="/projects">
<div className="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M7 4v16M16 4v16M3 9h18M3 15h18" />
</svg>
</div>
<div className="ic"><Icon name="clapperboard" /></div>
<div>
<div className="t"></div>
<div className="d">12 </div>

View File

@ -36,8 +36,8 @@ export default function ProductsPage() {
</div>
</div>
<div className="actions">
<button className="btn btn-primary">
<Icon name="plus" size={14} />
<button className="btn btn-primary btn-create">
<Icon name="product-plus" size={16} />
</button>
</div>

View File

@ -247,7 +247,7 @@ export default function NewProjectPage() {
>
<div className="num">
{state === "done"
? <Icon name="check" size={12} strokeWidth={3} />
? <Icon name="check" size={12} strokeWidth={1.5} />
: s.n}
</div>
<div>

View File

@ -121,8 +121,8 @@ export default function ProjectsPage() {
</div>
</div>
<div className="actions">
<Link className="btn btn-primary btn-lg" href="/projects/new">
<Icon name="plus" size={14} />
<Link className="btn btn-primary btn-lg btn-create" href="/projects/new">
<Icon name="clapperboard" size={16} />
</Link>
</div>

View File

@ -4,6 +4,13 @@ type IconName =
| "home"
| "play"
| "folder"
| "package"
| "boxes"
| "images"
| "credit-card"
| "clapperboard"
| "film"
| "video"
| "tile"
| "bars"
| "bars2"
@ -17,6 +24,8 @@ type IconName =
| "doc"
| "up"
| "plus"
| "product-plus"
| "airshelf"
| "flame"
| "check"
| "x"
@ -39,7 +48,52 @@ const PATHS: Record<IconName, React.ReactNode> = {
<path d="M5 10v10h14V10" />
</>
),
play: <polygon points="6 4 20 12 6 20" fill="currentColor" stroke="none" />,
play: <path d="m6 4 14 8-14 8Z" />,
package: (
<>
<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z" />
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" />
<path d="m7.5 4.3 9 5.1" />
</>
),
boxes: (
<>
<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z" />
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" />
<path d="m7.5 4.3 9 5.1" />
</>
),
images: (
<>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" />
</>
),
clapperboard: (
<>
<path d="m12.3 3.5 3 4" />
<path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z" />
<path d="m6.2 5.3 3.1 3.9" />
<path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
</>
),
film: (
<>
<path d="m12.3 3.5 3 4" />
<path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z" />
<path d="m6.2 5.3 3.1 3.9" />
<path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
</>
),
video: (
<>
<path d="m16 13 5.2 3.1a.5.5 0 0 0 .8-.4V8.3a.5.5 0 0 0-.8-.4L16 11" />
<rect x="2" y="6" width="14" height="12" rx="2" />
</>
),
folder: (
<>
<rect x="3" y="4" width="18" height="16" rx="2" />
@ -49,9 +103,9 @@ const PATHS: Record<IconName, React.ReactNode> = {
tile: <path d="M3 6h18M3 12h18M3 18h18" />,
bars: (
<>
<rect x="3" y="4" width="6" height="16" />
<rect x="11" y="4" width="4" height="16" />
<rect x="17" y="6" width="4" height="14" />
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" />
</>
),
bars2: <path d="M3 21V9M9 21V5M15 21v-8M21 21V11" />,
@ -90,10 +144,33 @@ const PATHS: Record<IconName, React.ReactNode> = {
),
up: <path d="M12 19V5M5 12l7-7 7 7" />,
plus: <path d="M12 5v14M5 12h14" />,
flame: <path fill="currentColor" stroke="none" d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z" />,
"product-plus": (
<>
<path d="M12 22V12" />
<path d="M16 17h6" />
<path d="M19 14v6" />
<path d="M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1" />
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="m7.5 4.3 9 5.1" />
</>
),
airshelf: (
<>
<path d="M4.5 19 12 4.5 19.5 19" />
<path d="M8 14h8" />
<path d="M7 17h10" />
<path d="M9.5 10.3c1.5-1.2 3.5-1.2 5 0" />
</>
),
flame: (
<>
<path d="M8.5 14.5c0-2 1.5-3.4 3.5-5.5.4 2.2 2.8 3.1 2.8 5.6a3.3 3.3 0 0 1-6.3-.1Z" />
<path d="M12 22c4 0 7-2.8 7-7 0-4.7-3.3-7.3-5-12-2 3.3-7 6-7 12a5 5 0 0 0 5 7Z" />
</>
),
check: <path d="M4 12l5 5L20 6" />,
x: <path d="M5 5l14 14M19 5L5 19" />,
"play-tri": <path fill="currentColor" stroke="none" d="M5 4l14 8-14 8z" />,
"play-tri": <path d="m5 4 14 8-14 8Z" />,
rotate: (
<>
<path d="M4 12a8 8 0 0 1 14-5.5L21 9" />
@ -104,9 +181,9 @@ const PATHS: Record<IconName, React.ReactNode> = {
),
more: (
<>
<circle cx="5" cy="12" r="1.5" fill="currentColor" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
<circle cx="19" cy="12" r="1.5" fill="currentColor" />
<circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none" />
<circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none" />
</>
),
wallet: (
@ -115,6 +192,12 @@ const PATHS: Record<IconName, React.ReactNode> = {
<path d="M3 10h18M16 14h2" />
</>
),
"credit-card": (
<>
<rect x="2" y="5" width="20" height="14" rx="2" />
<path d="M2 10h20" />
</>
),
coin: (
<>
<circle cx="12" cy="12" r="9" />

View File

@ -10,12 +10,12 @@ const NAV = [
{
id: "projects",
label: "项目",
icon: "folder" as const,
icon: "clapperboard" as const,
href: "/projects",
chev: true,
},
{ id: "products", label: "商品库", icon: "tile" as const, href: "/products" },
{ id: "library", label: "资产库", icon: "bars" as const, href: "/library" },
{ id: "products", label: "商品库", icon: "package" as const, href: "/products" },
{ id: "library", label: "资产库", icon: "images" as const, href: "/library" },
{ id: "usage", label: "用量", icon: "bars2" as const, href: "/usage" },
{ id: "api", label: "API Keys", icon: "key" as const, href: "/api-keys" },
{ id: "settings", label: "设置", icon: "cog" as const, href: "/settings" },
@ -32,10 +32,10 @@ export default function Sidebar() {
return (
<aside className="sidebar">
<div className="brand">
<div className="flame">
<Icon name="flame" size={22} />
<div className="brand-mark">
<Icon name="airshelf" size={22} />
</div>
<div className="brand-name">·Studio</div>
<div className="brand-name">Airshelf</div>
<div className="brand-ver">v1</div>
</div>

View File

@ -40,7 +40,7 @@ export default function Topbar({ crumbs, balance = "¥327.40" }: Props) {
<div className="top-r">
<span className="balance-chip">
<Icon name="coin" size={13} />
<Icon name="credit-card" size={13} />
<strong>{balance}</strong>
</span>
<button className="icon-btn" aria-label="通知">

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "liu-studio",
"name": "airshelf",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "liu-studio",
"name": "airshelf",
"version": "0.1.0",
"dependencies": {
"next": "^15.5.18",

View File

@ -1,5 +1,5 @@
{
"name": "liu-studio",
"name": "airshelf",
"version": "0.1.0",
"private": true,
"scripts": {

View File

@ -1,4 +1,4 @@
# 电商AI平台 · 流·Studio
# 电商AI平台 · Airshelf
AI 短视频带货生成平台 · 静态设计稿(V2.1 Restraint)。
@ -36,7 +36,7 @@ npx http-server . -p 8080
- 圆角:统一 8 px / pill 999 px / 微元素 4 px
- Icon:SVG line · stroke 1.5 · linecap round · `currentColor` 继承
- 签名元素:容器装订线(左右 1 px 边)+ 四角 22×21 px SVG 准星(圆弧内凹)
- 字体:Inter Tight + JetBrains Mono + Alibaba PuHuiTi
- 字体:Inter Tight + Inter + Alibaba PuHuiTi
- 主 CTA:橙底 + 4 层橙阴影(全场唯一允许阴影的组件)
## 目录结构

View File

@ -2,25 +2,26 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>消费 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>消费 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 顶部:左右布局(余额 banner + 快速充值)─── */
.top-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
@media (max-width: 980px) { .top-grid { 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: var(--accent-black);
color: var(--accent-white);
padding: 24px 28px;
padding: 26px 28px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
display: flex;
flex-direction: column;
gap: 22px;
gap: 24px;
min-width: 0;
min-height: 246px;
}
.balance-banner::before, .balance-banner::after,
.balance-banner > .corner-tr, .balance-banner > .corner-bl {
@ -40,75 +41,59 @@
.balance-hero .meta { font-size: 11.5px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; }
/* 子统计 · 月限额 / 已用 */
.balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 14px 0 0; border-top: 1px solid rgba(255,255,255,.1); }
.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: 700; 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; }
.balance-actions { display: flex; gap: 8px; margin-top: auto; }
.balance-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); flex: 1; }
.balance-actions .btn:hover { background: var(--background-base); }
.balance-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); flex: 1; }
.balance-actions .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
/* 导出菜单 */
.balance-actions .export-menu {
position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
min-width: 220px;
background: var(--surface); color: var(--accent-black);
border: 1px solid var(--border-faint); border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.16);
padding: 6px;
display: flex; flex-direction: column; gap: 2px;
}
.balance-actions .export-menu[hidden] { display: none; }
.balance-actions .export-menu button {
background: transparent; border: 0; padding: 8px 10px;
display: grid; grid-template-columns: 22px 1fr; gap: 8px;
align-items: center; cursor: pointer;
border-radius: var(--r-sm);
font-family: inherit; font-size: 13px; color: var(--accent-black);
text-align: left;
}
.balance-actions .export-menu button:hover { background: var(--background-lighter); }
.balance-actions .export-menu button .ic {
width: 22px; height: 22px; display: grid; place-items: center;
font-family: var(--font-mono); color: var(--heat); font-size: 14px; font-weight: 700;
background: var(--heat-12); border-radius: var(--r-sm);
}
.balance-actions .export-menu button .t { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.balance-actions .export-menu button .t .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 2px; }
.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(--heat); 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(右栏)─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.pane .desc { font-size: 11.5px; color: var(--black-alpha-48); margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
.topup-pane { display: flex; flex-direction: column; padding: 20px 22px; margin-bottom: 0; }
.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(--black-alpha-48); white-space: nowrap; padding-top: 2px; }
.recharge-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.recharge-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; text-align: center; cursor: pointer; background: var(--surface); position: relative; transition: background var(--t-base), border-color var(--t-base); }
.recharge-card:hover { background: var(--background-lighter); }
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); }
.recharge-card.selected::before, .recharge-card.selected::after { 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='%23FA5D19'%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; }
.recharge-card.selected::before { top: -7px; left: -7px; }
.recharge-card.selected::after { bottom: -7px; right: -7px; }
.recharge-card .amt { font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; }
.recharge-card .gift { font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); }
.recharge-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.recharge-card { min-height: 76px; border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 8px; text-align: center; cursor: pointer; background: var(--surface); 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), transform var(--t-fast); }
.recharge-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.recharge-card:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--heat-40); }
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); box-shadow: inset 0 0 0 1px var(--heat); }
.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(--heat); color: var(--accent-white); font-size: 10px; line-height: 1; }
.recharge-card .amt { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
.recharge-card .gift { font-size: 10px; color: var(--black-alpha-48); 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: -8px; right: 8px; font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--heat); color: var(--accent-white); letter-spacing: .04em; font-weight: 600; border-radius: var(--r-sm); }
.pay-row { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 14px; }
.recharge-card .ribbon { position: absolute; top: 6px; left: 7px; font-family: var(--font-mono); font-size: 9px; padding: 1px 5px; background: var(--heat); color: var(--accent-white); letter-spacing: .03em; font-weight: 600; border-radius: var(--r-sm); }
.pay-row { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.pay-title { font-size: 12px; font-weight: 600; color: var(--accent-black); line-height: 1.2; }
.pay-row .input { width: 100%; box-sizing: border-box; height: 38px; }
.pay-btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pay-btn-row .btn { width: 100%; }
.pay-btn-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.pay-method-btn { height: 38px; border-radius: var(--r-pill); display: inline-flex; justify-content: center; align-items: center; gap: 8px; background: var(--surface); color: var(--accent-black); border-color: var(--border-faint); font-weight: 500; }
.pay-method-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
.pay-method-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--black-alpha-16); }
.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)); }
}
/* ─── Tab strip ─── */
.billing-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 26px 0 18px; padding: 0 2px; }
.billing-tabs .tab { background: none; border: 0; padding: 10px 16px 11px; font-size: 13px; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; border-bottom: 2px solid transparent; position: relative; top: 1px; letter-spacing: .01em; transition: color var(--t-base); }
.billing-tabs .tab:hover { color: var(--accent-black); }
.billing-tabs .tab.active { color: var(--accent-black); font-weight: 600; border-bottom-color: var(--heat); }
.billing-tabs .tab .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-32); margin-left: 4px; letter-spacing: .02em; font-weight: 400; }
.billing-tabs .tab.active .mono { color: var(--heat); }
.billing-tabs { display: flex; align-items: flex-end; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 24px 0 18px; padding: 0 2px; overflow-x: auto; scrollbar-width: none; }
.billing-tabs::-webkit-scrollbar { display: none; }
.billing-tabs .tab { display: inline-flex; align-items: center; flex: 0 0 auto; gap: 6px; background: transparent; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r-md) var(--r-md) 0 0; margin-bottom: -1px; padding: 10px 14px; font-size: 13px; font-weight: 500; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; letter-spacing: 0; user-select: none; transition: color var(--t-base), background var(--t-base), border-color var(--t-base); }
.billing-tabs .tab:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.billing-tabs .tab:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--heat-40); }
.billing-tabs .tab.active { color: var(--accent-black); border-bottom-color: var(--heat); font-weight: 600; }
.billing-tabs .tab .count { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding: 1px 7px; background: var(--black-alpha-4); border-radius: var(--r-sm); letter-spacing: .04em; }
.billing-tabs .tab.active .count { background: var(--heat-12); color: var(--heat); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
@ -244,37 +229,42 @@
<div class="meta">// 占比 5.4% · 健康</div>
</div>
</div>
<div class="balance-actions">
<button class="btn btn-lg" onclick="openTopup()">充值</button>
<div class="export-wrap" style="flex:1; position:relative;">
<button class="btn btn-ghost btn-lg" id="export-trigger" type="button" style="width:100%;">
导出账单
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px; vertical-align:-1px;"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="export-menu" id="export-menu" hidden>
<button type="button" data-fmt="csv"><span class="ic"></span><span class="t">CSV<span class="d">// 全部明细 · Excel 兼容</span></span></button>
<button type="button" data-fmt="xlsx"><span class="ic"></span><span class="t">XLSX<span class="d">// 含格式 + 公式</span></span></button>
<button type="button" data-fmt="pdf"><span class="ic"></span><span class="t">PDF<span class="d">// 含发票抬头</span></span></button>
</div>
<div class="balance-foot">
<div class="balance-meter" aria-label="本月额度使用率 5.4%"><span></span></div>
<div class="balance-foot-meta">
<span>团队月剩余 ¥2,837.40</span>
<span>使用率 5.4%</span>
</div>
</div>
</div>
<!-- 右:快速充值 -->
<div class="pane topup-pane">
<div class="topup-head">
<div>
<h3>快速充值</h3>
<div class="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
</div>
<div class="topup-selected" id="topup-selected-label">已选 ¥500</div>
</div>
<div class="recharge-row">
<div class="recharge-card" data-amt="100"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
<div class="recharge-card selected" data-amt="500"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
<div class="recharge-card" data-amt="1000"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
<div class="recharge-card" data-amt="3000"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
<div class="recharge-card" data-amt="100" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
<div class="recharge-card selected" data-amt="500" role="button" tabindex="0" aria-pressed="true"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
<div class="recharge-card" data-amt="1000" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
<div class="recharge-card" data-amt="3000" role="button" tabindex="0" aria-pressed="false"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
</div>
<div class="pay-row">
<input class="input" id="custom-amt" placeholder="自定义金额(最低 ¥50)">
<div class="pay-title">自定义金额</div>
<input class="input" id="custom-amt" placeholder="最低 ¥50,可输入任意金额">
<div class="pay-btn-row">
<button class="btn btn-primary" onclick="openTopup('wechat')">微信支付</button>
<button class="btn" onclick="openTopup('alipay')">支付宝</button>
<button class="btn pay-method-btn pay-wechat" onclick="openTopup('wechat')" aria-label="微信支付">
<span class="pay-logo" aria-hidden="true"><img src="assets/pay-wechat.png" alt=""></span>
微信支付
</button>
<button class="btn pay-method-btn pay-alipay" onclick="openTopup('alipay')" aria-label="支付宝">
<span class="pay-logo" aria-hidden="true"><img src="assets/pay-alipay.png" alt=""></span>
支付宝
</button>
</div>
</div>
</div>
@ -282,10 +272,10 @@
<!-- Tab strip -->
<div class="billing-tabs" role="tablist">
<button class="tab active" data-tab="overview" role="tab">总览 <span class="mono">// 当月趋势 + 阶段分布</span></button>
<button class="tab" data-tab="by-project" role="tab">项目 <span class="mono">// 8 个</span></button>
<button class="tab" data-tab="by-member" role="tab">成员 <span class="mono">// 5 人</span></button>
<button class="tab" data-tab="bills" role="tab">账单流水 <span class="mono">// 30 </span></button>
<button class="tab active" type="button" data-tab="overview" role="tab" aria-selected="true" aria-controls="panel-overview">总览</button>
<button class="tab" type="button" data-tab="by-project" role="tab" aria-selected="false" aria-controls="panel-by-project">项目 <span class="count">8</span></button>
<button class="tab" type="button" data-tab="by-member" role="tab" aria-selected="false" aria-controls="panel-by-member">成员 <span class="count">5</span></button>
<button class="tab" type="button" data-tab="bills" role="tab" aria-selected="false" aria-controls="panel-bills">账单流水 <span class="count">30</span></button>
</div>
<!-- ===== Tab 1: 总览 ===== -->
@ -500,7 +490,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消费' }] });
@ -688,7 +679,7 @@ function renderMembers() {
const used = m.used * mult;
const pct = m.monthly > 0 ? (used / m.monthly * 100) : 0;
return `
<tr style="cursor:pointer;" onclick="location.href='team.html'">
<tr style="cursor:pointer;">
<td><span class="who"><span class="av">${m.av}</span><strong style="font-weight:500;">${m.name}</strong></span></td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td>${m.projectsDone}</td>
@ -803,19 +794,54 @@ document.getElementById('bills-f-reset').addEventListener('click', () => {
/* ─── Tab 切换 ─── */
document.querySelectorAll('.billing-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.billing-tabs .tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
const targetId = 'panel-' + tab.dataset.tab;
document.querySelectorAll('.billing-tabs .tab').forEach(t => {
const active = t === tab;
t.classList.toggle('active', active);
t.setAttribute('aria-selected', active ? 'true' : 'false');
});
document.querySelectorAll('.tab-panel').forEach(p => {
p.classList.toggle('active', p.id === targetId);
});
});
});
/* ─── 快速充值卡选择 ─── */
function setRechargeCard(card) {
document.querySelectorAll('.recharge-card').forEach(c => {
const active = c === card;
c.classList.toggle('selected', active);
c.setAttribute('aria-pressed', active ? 'true' : 'false');
});
const label = document.getElementById('topup-selected-label');
if (label && card) label.textContent = '已选 ¥' + Number(card.dataset.amt).toLocaleString('zh-CN');
}
document.querySelectorAll('.recharge-card').forEach(card => {
card.addEventListener('click', () => {
document.querySelectorAll('.recharge-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
setRechargeCard(card);
document.getElementById('custom-amt').value = '';
});
card.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setRechargeCard(card);
document.getElementById('custom-amt').value = '';
}
});
});
document.getElementById('custom-amt').addEventListener('input', e => {
const raw = e.target.value.trim();
const label = document.getElementById('topup-selected-label');
if (!raw) {
const selected = document.querySelector('.recharge-card.selected');
if (label && selected) label.textContent = '已选 ¥' + Number(selected.dataset.amt).toLocaleString('zh-CN');
return;
}
document.querySelectorAll('.recharge-card').forEach(c => {
c.classList.remove('selected');
c.setAttribute('aria-pressed', 'false');
});
if (label) label.textContent = Number(raw) >= 50 ? '自定义 ¥' + Number(raw).toLocaleString('zh-CN') : '最低 ¥50';
});
/* ─── 充值 modal ─── */
@ -845,29 +871,6 @@ function topupDone() {
Shell.toast('充值成功', '余额已更新 · 可开发票');
}
/* ─── 导出菜单 ─── */
(function bindExport() {
const trigger = document.getElementById('export-trigger');
const menu = document.getElementById('export-menu');
if (!trigger || !menu) return;
const FMT_LABEL = { csv: 'CSV', xlsx: 'XLSX', pdf: 'PDF' };
trigger.addEventListener('click', e => {
e.stopPropagation();
menu.hidden = !menu.hidden;
});
menu.querySelectorAll('button[data-fmt]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const fmt = b.dataset.fmt;
menu.hidden = true;
Shell.toast('正在生成 ' + FMT_LABEL[fmt], '完成后会发送到注册邮箱');
});
});
document.addEventListener('click', () => { if (!menu.hidden) menu.hidden = true; });
// Esc 关闭
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !menu.hidden) menu.hidden = true; });
})();
/* ─── 初始化 ─── */
renderTrend();
renderProjects();

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>图片生成 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>图片生成 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
#page-content { padding: 24px 28px 60px; }
@ -531,7 +531,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>
Shell.render({

View File

@ -0,0 +1,89 @@
(function () {
const PATHS = {
home: '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 10v10h14V10"/><path d="M9 20v-6h6v6"/>',
package: '<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/><path d="m7.5 4.3 9 5.1"/>',
boxes: '<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/><path d="m7.5 4.3 9 5.1"/>',
clapperboard: '<path d="m12.3 3.5 3 4"/><path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z"/><path d="m6.2 5.3 3.1 3.9"/><path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>',
film: '<path d="m12.3 3.5 3 4"/><path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z"/><path d="m6.2 5.3 3.1 3.9"/><path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>',
video: '<path d="m16 13 5.2 3.1a.5.5 0 0 0 .8-.4V8.3a.5.5 0 0 0-.8-.4L16 11"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
sparkles: '<path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3Z"/><path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15Z"/>',
images: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
library: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
wallet: '<rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/><path d="M16 14h2"/>',
creditCard: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/>',
settings: '<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.2a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.2A1.7 1.7 0 0 0 4.7 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1.1-1.5V3a2 2 0 1 1 4 0v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1h.1a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/>',
airshelf: '<path d="M4.5 19 12 4.5 19.5 19"/><path d="M8 14h8"/><path d="M7 17h10"/><path d="M9.5 10.3c1.5-1.2 3.5-1.2 5 0"/>',
flame: '<path d="M8.5 14.5c0-2 1.5-3.4 3.5-5.5.4 2.2 2.8 3.1 2.8 5.6a3.3 3.3 0 0 1-6.3-.1Z"/><path d="M12 22c4 0 7-2.8 7-7 0-4.7-3.3-7.3-5-12-2 3.3-7 6-7 12a5 5 0 0 0 5 7Z"/>',
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/>',
list: '<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
plus: '<path d="M12 5v14"/><path d="M5 12h14"/>',
productPlus: '<path d="M12 22V12"/><path d="M16 17h6"/><path d="M19 14v6"/><path d="M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="m7.5 4.3 9 5.1"/>',
arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
arrowLeft: '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
arrowUp: '<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>',
chevronDown: '<path d="m6 9 6 6 6-6"/>',
rotateCcw: '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
moreHorizontal: '<circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/>',
trash: '<path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m19 6-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>',
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z"/>',
play: '<path d="m6 4 14 8-14 8Z"/>',
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
alertCircle: '<circle cx="12" cy="12" r="9"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/>',
shieldCheck: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="m9 12 2 2 4-4"/>',
messageCircle: '<path d="M21 11.5a8.5 8.5 0 0 1-12.5 7.5L3 20l1-5.5A8.5 8.5 0 1 1 21 11.5Z"/>',
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
lock: '<rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
eye: '<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
image: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
grid: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>',
copy: '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/>',
userPlus: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6"/><path d="M22 11h-6"/>',
helpCircle: '<circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.5-2.5 2-2.5 4"/><path d="M12 17h.01"/>'
};
const ALIASES = {
dashboard: 'home',
products: 'package',
projects: 'clapperboard',
assetFactory: 'sparkles',
account: 'creditCard',
billing: 'creditCard',
team: 'users',
queue: 'list',
arrow: 'arrowRight',
back: 'arrowLeft',
close: 'x',
rerun: 'rotateCcw',
more: 'moreHorizontal',
danger: 'alertCircle'
};
function svg(name, opts) {
opts = opts || {};
const key = ALIASES[name] || name;
const body = PATHS[key] || PATHS.helpCircle;
const size = opts.size || 16;
const strokeWidth = opts.strokeWidth || 1.5;
const className = ['ui-icon', opts.className || ''].join(' ').trim();
const label = opts.label ? ` role="img" aria-label="${escapeAttr(opts.label)}"` : ' aria-hidden="true"';
return `<svg class="${className}" data-icon="${key}" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" focusable="false"${label}>${body}</svg>`;
}
function escapeAttr(value) {
return String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
window.IconKit = {
svg,
names: () => Object.keys(PATHS),
has: name => !!PATHS[ALIASES[name] || name]
};
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,5 +1,5 @@
/* ============================================================
·Studio · Restraint V2.1 · Firecrawl-aligned · 纯净版
Airshelf · Restraint V2.1 · Firecrawl-aligned · 纯净版
============================================================
严格遵循 DESIGN_SPEC_V2.md V2.1 · 不含任何 V1/V2 legacy alias
============================================================ */
@ -115,11 +115,11 @@
--r-pill: 999px;
/* ===== Font · 关键:--font-mono 必须含 PuHuiTi 中文 fallback ===== */
/* Inter / JetBrains Mono 不含 CJK 字形
/* Inter 不含 CJK 字形
字体链按字符级 fallthrough,中文字符会找下一个候选 必须包含 PuHuiTi */
--font-sans: 'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
--font-inter: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Geist Mono', ui-monospace, 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', monospace;
--font-mono: 'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
/* ===== Transition ===== */
--t-fast: 100ms ease;
@ -161,6 +161,52 @@ button { font: inherit; cursor: pointer; border: 0; background: none; color: inh
input, textarea, select { font: inherit; color: inherit; outline: none; }
img, svg, video { display: block; max-width: 100%; }
/* ─── Icon system · Lucide-line only ─── */
:root {
--icon-stroke: 1.5;
--icon-xs: 12px;
--icon-s: 14px;
--icon-m: 16px;
--icon-l: 20px;
--icon-xl: 24px;
}
.ui-icon {
width: var(--icon-m);
height: var(--icon-m);
flex: 0 0 auto;
display: block;
fill: none;
stroke: currentColor;
stroke-width: var(--icon-stroke);
stroke-linecap: round;
stroke-linejoin: round;
}
.ui-icon * { vector-effect: non-scaling-stroke; }
.ui-icon [stroke="none"] {
fill: currentColor;
stroke: none;
}
:where(nav a, .search-box, .balance-chip, .queue-chip, .icon-btn, .btn, .chip, .toolbar, .page-head, .toast, .modal, .drawer, .card-del-btn, .row-more, .gen-img-btn, .shortcut, .empty-state) svg:not(.corner):not(.quote-icon) {
flex: 0 0 auto;
fill: none;
stroke: currentColor;
stroke-width: var(--icon-stroke);
stroke-linecap: round;
stroke-linejoin: round;
}
:where(nav a, .search-box, .balance-chip, .queue-chip, .icon-btn, .btn, .chip, .toolbar, .page-head, .toast, .modal, .drawer, .card-del-btn, .row-more, .gen-img-btn, .shortcut, .empty-state) svg:not(.corner):not(.quote-icon) [fill="currentColor"] {
fill: none;
stroke: currentColor;
}
:where(nav a, .search-box, .balance-chip, .queue-chip, .icon-btn, .btn, .chip, .toolbar, .page-head, .toast, .modal, .drawer, .card-del-btn, .row-more, .gen-img-btn, .shortcut, .empty-state) svg:not(.corner):not(.quote-icon) [stroke="none"] {
fill: currentColor;
stroke: none;
}
.row-more svg circle {
fill: currentColor !important;
stroke: none !important;
}
.num, .tnum { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--font-mono); }
.muted { color: var(--black-alpha-56); }
@ -185,8 +231,8 @@ aside.sidebar {
overflow-y: auto;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 6px 8px 16px; }
.flame { width: 22px; height: 22px; color: var(--heat); }
.flame svg { width: 100%; height: 100%; }
.brand-mark, .flame { width: 22px; height: 22px; color: var(--heat); }
.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 search · Ctrl K Inter Bold 平铺 */
@ -203,7 +249,7 @@ aside.sidebar {
}
.search-box:hover { border-color: var(--black-alpha-24); }
.search-box:focus-within { border-color: var(--heat-40); box-shadow: inset 0 0 0 1px var(--heat-40); }
.search-box svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-56); }
.search-box svg { width: var(--icon-m); height: var(--icon-m); flex-shrink: 0; color: var(--black-alpha-56); }
.search-box input {
flex: 1; min-width: 0;
border: 0; background: transparent;
@ -242,7 +288,7 @@ nav a {
}
nav a:hover { background: var(--black-alpha-4); color: var(--accent-black); }
nav a.active { background: var(--heat-12); color: var(--heat); }
nav a svg { width: 14px; height: 14px; opacity: .85; }
nav a svg { width: var(--icon-m); height: var(--icon-m); opacity: .85; }
nav a.active svg { opacity: 1; }
nav a .pill-mini {
margin-left: auto;
@ -330,20 +376,20 @@ 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: 13px; height: 13px; color: var(--heat); }
.balance-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--heat); }
.icon-btn {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
border-radius: var(--r-pill);
color: var(--black-alpha-56);
cursor: pointer;
position: relative;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.icon-btn:hover { background: var(--black-alpha-4); color: var(--accent-black); border-color: var(--black-alpha-24); }
.icon-btn svg { width: 15px; height: 15px; }
.icon-btn svg { width: var(--icon-m); height: var(--icon-m); }
.icon-btn .dot-noti {
position: absolute; top: 8px; right: 9px;
width: 7px; height: 7px; border-radius: 50%;
@ -376,7 +422,7 @@ main { position: relative; background: var(--background-base); min-width: 0; }
}
.queue-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
.queue-chip[hidden] { display: none; }
.queue-chip svg { width: 14px; height: 14px; color: var(--black-alpha-56); }
.queue-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--black-alpha-56); }
.queue-chip .count {
display: inline-flex; align-items: center; justify-content: center;
height: 20px; min-width: 20px; padding: 0 6px;
@ -441,7 +487,8 @@ main { position: relative; background: var(--background-base); min-width: 0; }
padding: 0 20px;
font-size: 13.5px;
}
.page-head .actions > .btn svg { width: 14px; height: 14px; }
.page-head .actions > .btn svg { width: var(--icon-s); height: var(--icon-s); }
.page-head .actions > .btn.btn-create svg { width: var(--icon-m); height: var(--icon-m); }
.section-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.section-h h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--accent-black); }
@ -482,7 +529,7 @@ main { position: relative; background: var(--background-base); min-width: 0; }
background: var(--black-alpha-5);
border-color: var(--black-alpha-12);
}
.btn svg { width: 13px; height: 13px; }
.btn svg { width: var(--icon-s); height: var(--icon-s); }
.btn[hidden] { display: none; }
.btn-primary {
@ -1588,7 +1635,10 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
.product-card:has(.tri-missing-badge:hover),
.product-card:has(.tri-missing-badge:focus-within),
.asset-card:has(.tri-missing-badge:hover),
.asset-card:has(.tri-missing-badge:focus-within) {
.asset-card:has(.tri-missing-badge:focus-within),
.asset-card-2:has(.tri-missing-badge:hover),
.asset-card-2:has(.tri-missing-badge:focus-within) {
overflow: visible;
z-index: 5;
position: relative;
}

View File

@ -1,5 +1,5 @@
/* ============================================================
·Studio · Shell renderer V2.1
Airshelf · Shell renderer V2.1
渲染 sidebar / topbar / 网格背景装饰 / Toast / Modal helpers
每个页面调用 Shell.render({ active, crumbs, balance, topActions })
@ -7,38 +7,40 @@
- sidebar 搜索 K "Ctrl K" Inter Bold 平铺( kbd 边框)
============================================================ */
const ShellIcon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
const NAV = [
{
id: 'dashboard', label: '工作台', href: 'index.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/></svg>'
icon: ShellIcon('home')
},
{
id: 'products', label: '商品库', href: 'products.html', badge: '7',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>'
icon: ShellIcon('package')
},
{
id: 'projects', label: '视频项目', href: 'projects.html', badge: '8',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
icon: ShellIcon('clapperboard')
},
{
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
icon: ShellIcon('sparkles')
},
{
id: 'library', label: '资产库', href: 'library.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
icon: ShellIcon('images')
},
{
id: 'team', label: '团队', href: 'team.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
icon: ShellIcon('users')
},
{
id: 'account', label: '消费', href: 'account.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
icon: ShellIcon('creditCard')
},
{
id: 'settings', label: '设置', href: 'settings.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8 2 2 0 0 1 2.8-2.8 1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5 2 2 0 0 1 4 0 1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3 2 2 0 0 1 2.8 2.8 1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1Z"/></svg>'
icon: ShellIcon('settings')
}
];
@ -55,11 +57,11 @@ window.Shell = {
const sidebar = `
<aside class="sidebar">
<div class="brand">
<div class="flame"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z"/></svg></div>
<div><div class="name">·Studio</div></div>
<div class="brand-mark">${ShellIcon('airshelf', { size: 22 })}</div>
<div><div class="name">Airshelf</div></div>
</div>
<div class="search-box" onclick="document.getElementById('global-search').focus()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
${ShellIcon('search')}
<input id="global-search" placeholder="搜索"/>
<span class="kbd">Ctrl K</span>
</div>
@ -90,16 +92,16 @@ window.Shell = {
${crumbHtml}
<div class="right">
<span class="balance-chip" onclick="location.href='account.html'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
${ShellIcon('creditCard')}
余额 <strong>${balance}</strong>
</span>
<button class="queue-chip" onclick="Shell.toast('任务队列', '3 个进行中')" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
${ShellIcon('list')}
任务队列
<span class="count">3</span>
</button>
<button class="icon-btn" onclick="location.href='messages.html'" title="消息中心">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
${ShellIcon('bell')}
<span class="count-noti">12</span>
</button>
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
@ -120,7 +122,7 @@ window.Shell = {
const toastHtml = `
<div class="toast" id="__toast">
<div class="ic-t"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="ic-t">${ShellIcon('check')}</div>
<div class="txt" id="__toast-txt">操作成功<span class="mono">[ 200 OK ]</span></div>
</div>
`;
@ -129,7 +131,7 @@ window.Shell = {
const lightboxHtml = `
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
${ShellIcon('x')}
</button>
<img id="np-lightbox-img" alt="">
<span class="lb-name" id="np-lightbox-name"></span>

View File

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>流·Studio 设计系统 · V2.1 · Interactive</title>
<title>Airshelf 设计系统 · V2.1 · Interactive</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- 引入项目共享 CSS · token + 100+ 组件类(1592 行) -->
<link rel="stylesheet" href="assets/restraint.css">
<style>
@ -1172,7 +1172,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<!-- ============ SIDEBAR ============ -->
<aside class="ds-sidebar">
<h1>流·Studio<br>设计系统</h1>
<h1>Airshelf<br>设计系统</h1>
<div class="mono-sub">// V2.1 · 2026-05-22 · INTERACTIVE</div>
<ul class="ds-nav">
@ -1231,7 +1231,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<!-- HERO -->
<div class="hero">
<span class="badge">// V2.1 · INTERACTIVE · 2026-05-22</span>
<h1>流·Studio 设计系统</h1>
<h1>Airshelf 设计系统</h1>
<p>本页是 <code class="tk">design.md</code> 的可视化样板间。所有 token / 组件均来自 <code class="tk">assets/restraint.css</code> —— <strong style="color:var(--accent-black)">看到的就是页面用的</strong>。点击色块复制 token,hover / 点击组件看真实交互反馈。</p>
<div class="meta">
<span>[ Restraint · V2.1 ]</span>
@ -1388,7 +1388,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="section-head">
<span class="mono-tag">// §2.5 · TYPE · MIXED STRATEGY</span>
<h2>字体系统 · 中英协作</h2>
<p><code class="tk">Inter</code>(英文/数字)+ <code class="tk">Alibaba PuHuiTi</code>(中文)+ <code class="tk">JetBrains Mono</code>(装饰)。浏览器按字符级 fallthrough,<strong>不需要 JS,中英自然分工</strong></p>
<p><code class="tk">Inter</code>(英文/数字)+ <code class="tk">Alibaba PuHuiTi</code>(中文)+ <code class="tk">Inter</code>(装饰)。浏览器按字符级 fallthrough,<strong>不需要 JS,中英自然分工</strong></p>
</div>
<div class="subsection">
@ -1406,7 +1406,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
</div>
<div style="padding:24px 26px;border:1px solid var(--border-faint);border-radius:8px;background:var(--surface)">
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-48);letter-spacing:.06em;margin-bottom:14px">// 03 · 装饰 MONO</div>
<div style="font-family:var(--font-mono);font-size:22px;font-weight:500;color:var(--accent-black);margin-bottom:8px">JetBrains</div>
<div style="font-family:var(--font-mono);font-size:22px;font-weight:500;color:var(--accent-black);margin-bottom:8px">Inter</div>
<div style="font-family:var(--font-mono);font-size:12px;color:var(--black-alpha-64);line-height:1.6;letter-spacing:.02em">[ 200 OK ] · // 05.14 · /v2 — 仅装饰,不参与正文。</div>
</div>
</div>
@ -1422,19 +1422,19 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px">
<div style="padding:20px 22px;border:1px solid var(--border-faint);border-radius:8px;background:var(--surface)">
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-48);letter-spacing:.04em;margin-bottom:10px">// REGULAR · 400</div>
<div style="font-size:18px;font-weight:400;color:var(--accent-black)">流·Studio · Aa Bb 123</div>
<div style="font-size:18px;font-weight:400;color:var(--accent-black)">Airshelf · Aa Bb 123</div>
</div>
<div style="padding:20px 22px;border:1px solid var(--heat-20);border-radius:8px;background:var(--heat-8)">
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--heat);letter-spacing:.04em;margin-bottom:10px">// MEDIUM · 500 ★ 默认</div>
<div style="font-size:18px;font-weight:500;color:var(--accent-black)">流·Studio · Aa Bb 123</div>
<div style="font-size:18px;font-weight:500;color:var(--accent-black)">Airshelf · Aa Bb 123</div>
</div>
<div style="padding:20px 22px;border:1px solid var(--border-faint);border-radius:8px;background:var(--surface)">
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-48);letter-spacing:.04em;margin-bottom:10px">// SEMIBOLD · 600</div>
<div style="font-size:18px;font-weight:600;color:var(--accent-black)">流·Studio · Aa Bb 123</div>
<div style="font-size:18px;font-weight:600;color:var(--accent-black)">Airshelf · Aa Bb 123</div>
</div>
<div style="padding:20px 22px;border:1px solid var(--border-faint);border-radius:8px;background:var(--surface)">
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-48);letter-spacing:.04em;margin-bottom:10px">// BOLD · 700 · 限徽标</div>
<div style="font-size:18px;font-weight:700;color:var(--accent-black)">流·Studio · Aa Bb 123</div>
<div style="font-size:18px;font-weight:700;color:var(--accent-black)">Airshelf · Aa Bb 123</div>
</div>
</div>
<p class="mono" style="margin-top:14px">// Bold 700 仅用于 Ctrl K 这种纯英文徽标场景 · 正文严禁 700(中英字重错位会暴露)</p>
@ -1491,7 +1491,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="sample" style="font-family:'Inter',sans-serif;font-size:11.5px;font-weight:700;color:var(--black-alpha-56);letter-spacing:.02em">Ctrl K · ESC · Enter · ⇧+Tab</div>
</div>
<div class="type-row">
<div class="meta-col"><strong>Mono 标签</strong>11 / 400 / 0.04em · JetBrains</div>
<div class="meta-col"><strong>Mono 标签</strong>11 / 400 / 0.04em · Inter</div>
<div class="sample" style="font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;color:var(--black-alpha-48)">[ 200 OK ] [ .MP4 · 9:16 ] [ /v2 ]</div>
</div>
<div class="type-row">
@ -1605,8 +1605,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="subsection">
<h3>HTML 模板</h3>
<pre class="codebox">&lt;link rel="stylesheet" href="assets/restraint.css"&gt;
&lt;link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700
&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet"&gt;
&lt;link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
&lt;div class="app"&gt;
&lt;aside class="sidebar"&gt;&lt;!-- shell.js 注入 --&gt;&lt;/aside&gt;
@ -1971,7 +1970,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<input class="input" placeholder="搜索任意内容..."/>
<span class="k">Ctrl K</span>
</div>
<p class="mono" style="margin-top:12px">// 关键坑:左侧 icon 必须 z-index: 2(否则被 input 白底盖住)· Ctrl K 用纯文本不用 ⌘(JetBrains Mono 不带该字形)</p>
<p class="mono" style="margin-top:12px">// 关键坑:左侧 icon 必须 z-index: 2(否则被 input 白底盖住)· Ctrl K 用纯文本不用 ⌘(Inter 不带该字形)</p>
</div>
</section>
@ -2552,7 +2551,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<!-- ② Meta -->
<div class="gen-meta">
<span class="m-item">流·Studio v2</span>
<span class="m-item">Airshelf v2</span>
<span class="m-sep">|</span>
<span class="m-item">1:1</span>
<span class="m-sep">|</span>
@ -2628,7 +2627,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<span class="text">东方美人 · 古典园林背景 · 短视频 9:16</span>
</div>
<div class="gen-meta">
<span class="m-item">流·Studio v2</span>
<span class="m-item">Airshelf v2</span>
<span class="m-sep">|</span>
<span class="m-item">9:16</span>
<span class="m-sep">|</span>
@ -2696,7 +2695,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="section-head">
<span class="mono-tag">// §5 · MONO DECOR · 品牌签名</span>
<h2>Mono 装饰元素</h2>
<p>方括号标签 / 双斜杠注释 / 中点连接 —— 这些是流·Studio 独有的"调试视图感",<strong>Firecrawl 没有,绝对保留</strong></p>
<p>方括号标签 / 双斜杠注释 / 中点连接 —— 这些是Airshelf 独有的"调试视图感",<strong>Firecrawl 没有,绝对保留</strong></p>
</div>
<div class="subsection">
@ -2909,7 +2908,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="dont-list-item"><span class="x">×</span><span><strong style="color:var(--accent-black)">同一行混用直角和圆角</strong> · 用户原话:"不要有些是直角,胶囊又是圆角"</span></div>
<div class="dont-list-item"><span class="x">×</span><span><strong style="color:var(--accent-black)">页面 inline style 重写共享类</strong> · 要变体先改 restraint.css</span></div>
<div class="dont-list-item"><span class="x">×</span><span><strong style="color:var(--accent-black)">新建色值</strong> · 必须复用现有 token</span></div>
<div class="dont-list-item"><span class="x">×</span><span><strong style="color:var(--accent-black)">⌘ Unicode 字符</strong> · JetBrains Mono 不带该字形 · 用 "Ctrl K" 纯文本</span></div>
<div class="dont-list-item"><span class="x">×</span><span><strong style="color:var(--accent-black)">⌘ Unicode 字符</strong> · Inter 不带该字形 · 用 "Ctrl K" 纯文本</span></div>
</div>
</section>
@ -2935,7 +2934,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="ch"><span class="stage">// STAGE 02</span><span class="ti">写代码时</span></div>
<ul class="cl-list">
<li><span class="mk"></span>HTML 用 <code class="tk">app &gt; sidebar + main &gt; topbar + content</code> 骨架</li>
<li><span class="mk"></span>head 含 <code class="tk">assets/restraint.css</code> + Inter + JetBrains Mono</li>
<li><span class="mk"></span>head 含 <code class="tk">assets/restraint.css</code> + Inter</li>
<li><span class="mk"></span>body 末尾 <code class="tk">assets/shell.js</code></li>
<li><span class="mk"></span>标题区用 <code class="tk">.page-head &gt; h1 + .sub</code></li>
<li><span class="mk"></span>主操作按钮放 <code class="tk">.page-head &gt; .actions</code>(自动 40 px 高)</li>

View File

@ -1,4 +1,4 @@
# 流·Studio 设计规范 · design.md
# Airshelf 设计规范 · design.md
> **唯一权威 source of truth** · 所有页面调整必须遵循本文件。
> **代号:** Restraint(克制)· V2.1 · Firecrawl-aligned
@ -108,7 +108,7 @@
| ---- | -------- | -------- |
| 正文 / UI · `--font-sans` | `'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif` | Inter Google Fonts + 普惠体 CDN |
| 强制纯英 · `--font-inter` | `'Inter', system-ui, sans-serif` | 用于 Ctrl K 等英文徽标 |
| Mono 装饰 · `--font-mono` | `'JetBrains Mono', 'Geist Mono', ui-monospace, 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', monospace` | JetBrains Google Fonts |
| 数字 / 装饰 · `--font-mono` | `'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif` | Inter Google Fonts |
**字符级 fallthrough 原理:** 浏览器对每个字符逐个查找,Inter 不含 CJK → 中文自动跳到普惠体。**不需要 JS,不会字重错位。**
@ -224,16 +224,17 @@
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
```
每个新页面 `<body>` 末尾:
```html
<script src="assets/icons.js" defer></script>
<script src="assets/shell.js" defer></script>
```
`shell.js` 自动注入 sidebar + topbar,**页面只写 `<main>` 内的 `.content` 内容**。
`icons.js` 提供统一 Lucide-line 图标注册表;`shell.js` 自动注入 sidebar + topbar,**页面只写 `<main>` 内的 `.content` 内容**。
### §3.2 标题区(必有)
@ -399,7 +400,7 @@
**带 Ctrl K 搜索框关键坑:**
1. 左侧 icon 必须 `z-index: 2`(否则被 input 白底盖住)
2. 用 "Ctrl K" 纯文本,**不要用 `⌘` 字符**(JetBrains Mono 不带该字形,显示成方框)
2. 用 "Ctrl K" 纯文本,**不要用 `⌘` 字符**(Inter 不带该字形,显示成方框)
3. 快捷键提示不要 kbd 边框,**纯灰色 mono 平铺**(克制)
4. 字体用 `var(--font-inter) / 700`(Inter Bold 紧凑感)
@ -525,7 +526,7 @@ Disabled: 底 `--black-alpha-5` + 边 `--black-alpha-12` + 半透明。
**通用规则:**
- 一律 **SVG inline**,禁止 `<img>` 引图标
- 库:Lucide 或 Phosphor Regular
- 库:统一使用本地 Lucide-line 注册表 `assets/icons.js``IconKit.svg(name, opts)`,禁止页面内临时手写另一套图标
- `stroke-width: 1.5` / `stroke-linecap: round` / `stroke-linejoin: round` / `fill: none`
- 颜色:`stroke="currentColor"` 继承
- **禁:** 彩色 emoji / filled icon
@ -878,7 +879,7 @@ Modal 内多选 picker:`bg --background-lighter / padding 10 / display flex colu
<!-- ② Meta chip 行(竖线分隔) -->
<div class="gen-meta">
<span class="m-item">流·Studio v2</span>
<span class="m-item">Airshelf v2</span>
<span class="m-sep">|</span>
<span class="m-item">1:1</span>
<span class="m-sep">|</span>
@ -970,7 +971,7 @@ img.addEventListener('click', () => {
## §5 · Mono 装饰元素(品牌签名 · 不能丢)
> 方括号标签 / 双斜杠注释 / 中点连接 —— 这些是流·Studio 独有的"调试视图感",Firecrawl 没有,绝对保留。
> 方括号标签 / 双斜杠注释 / 中点连接 —— 这些是Airshelf 独有的"调试视图感",Firecrawl 没有,绝对保留。
| 用法 | 示例 |
| ---- | ---- |
@ -1076,7 +1077,7 @@ img.addEventListener('click', () => {
- ❌ **荧光状态色** — 避免霓虹绿、电光蓝、霓虹粉
- ❌ **页面内 `<style>` 重写共享类**(`.btn` `.pill` `.input` `.modal` 等)— 改 restraint.css
- ❌ **新建色 token** — 必须复用现有 token
- ❌ **`⌘` Unicode 字符** — JetBrains Mono webfont 不带该字形,显示成方框 → 用 "Ctrl K" 纯文本或 SVG
- ❌ **`⌘` Unicode 字符** — Inter webfont 不带该字形,显示成方框 → 用 "Ctrl K" 纯文本或 SVG
- ❌ **多色 emoji icon / filled icon** — 一律 line icon
- ❌ **居中卡片浮在背景上的"贴纸感"** — 用边框 + 装订线,不用阴影
- ❌ **彩虹流光 / hover 旋转缩放** — 无意义微动效
@ -1094,8 +1095,8 @@ img.addEventListener('click', () => {
### 写代码时
- [ ] HTML 用 `<div class="app"><aside class="sidebar"></aside><main><div class="topbar"></div><div class="content">…</div></main></div>` 骨架
- [ ] head 含 `<link rel="stylesheet" href="assets/restraint.css">` + Inter + JetBrains Mono Google Fonts
- [ ] body 末尾 `<script src="assets/shell.js" defer></script>`
- [ ] head 含 `<link rel="stylesheet" href="assets/restraint.css">` + Inter Google Fonts
- [ ] body 末尾先引 `<script src="assets/icons.js" defer></script>`,再引 `<script src="assets/shell.js" defer></script>`
- [ ] 标题区用 `.page-head > h1 + .sub`
- [ ] 主操作按钮放 `.page-head > .actions`(自动 40 px 高)
- [ ] 子区块用 `.section-h > h2 + .more`
@ -1103,7 +1104,7 @@ img.addEventListener('click', () => {
- [ ] 状态徽标全 `.pill.info/.ok/.err/.neutral` · 要新变体先回 restraint.css 加
- [ ] 输入框全 `.input / .select / .textarea` · 字段用 `.field`
- [ ] 浮层全用现成 Modal / Drawer / Toast
- [ ] 图标 SVG line icon · stroke 1.5 / linecap round / `stroke="currentColor"`
- [ ] 图标统一走 `IconKit.svg()` · SVG line icon · stroke 1.5 / linecap round / `stroke="currentColor"`
- [ ] 时间戳 mono 注释 `// 05.22 · 周四`
- [ ] 强调单词加深(不变橙) `<b style="color:var(--accent-black)">3 个</b>`
- [ ] 数字加 `.num``tabular-nums`

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>图片创作 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>图片创作 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* viewport-fit · 工作台铺满 */
.app { height: 100vh; overflow: hidden; }
@ -760,7 +760,7 @@
<div class="param" data-param="model" tabindex="0" hidden>
<span class="lbl-mono">模型</span>
<span id="io-param-model-lbl">流·Studio v2</span>
<span id="io-param-model-lbl">Airshelf v2</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
<div class="io-param-menu" id="io-menu-model"></div>
</div>
@ -818,7 +818,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'asset-factory',
@ -845,8 +846,8 @@ Shell.render({
const PRICE_PER = 0.10;
const MODELS = [
{ id: 'studio-v2', label: '流·Studio v2', sub: '通用 · 速度优' },
{ id: 'studio-v2-pro', label: '流·Studio v2 Pro', sub: '细节 · 商用' },
{ id: 'studio-v2', label: 'Airshelf v2', sub: '通用 · 速度优' },
{ id: 'studio-v2-pro', label: 'Airshelf v2 Pro', sub: '细节 · 商用' },
{ id: 'realistic', label: '写实增强', sub: '商品 · 人像' },
{ id: 'anime', label: '国风动漫', sub: '二次元 · 海报' },
];
@ -1120,7 +1121,7 @@ Shell.render({
</button>
<div class="cell-more-wrap">
<button type="button" data-act="more" title="更多">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>
</button>
<div class="cell-more-menu">
<button type="button" data-act="save-lib"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>加入资产库</button>
@ -1165,7 +1166,7 @@ Shell.render({
</button>
<div class="msg-more-wrap">
<button type="button" class="icon" data-act="msg-more" data-id="${m.id}" title="更多">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>
</button>
<div class="msg-more-menu" role="menu">
<button type="button" data-act="msg-save-all" data-id="${m.id}">

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>工作台 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>工作台 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }
.recent-row { display: grid; grid-template-columns: 54px 1fr 110px 130px 60px; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }
@ -39,12 +39,12 @@
</div>
</div>
<div class="actions">
<a class="btn" href="javascript:void(0)" onclick="event.preventDefault(); window.NewProductDrawer && NewProductDrawer.open();">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
<a class="btn btn-create" href="javascript:void(0)" onclick="event.preventDefault(); window.NewProductDrawer && NewProductDrawer.open();">
<span data-iconkit="productPlus" data-icon-size="16"></span>
新建商品
</a>
<a class="btn btn-primary btn-lg" href="projects-new.html">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
<a class="btn btn-primary btn-lg btn-create" href="projects-new.html">
<span data-iconkit="clapperboard" data-icon-size="16"></span>
新建项目
</a>
</div>
@ -55,7 +55,7 @@
<a class="stat" href="projects.html">
<div class="lbl">总项目 <span class="badge">ALL</span></div>
<div class="v">8</div>
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 本月 +3</div>
<div class="delta up"><span data-iconkit="arrowUp" data-icon-size="14"></span> 本月 +3</div>
</a>
<a class="stat" href="projects.html?filter=wip">
<div class="lbl">进行中 <span class="badge">WIP</span></div>
@ -65,7 +65,7 @@
<a class="stat" href="projects.html?filter=done">
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
<div class="v">3</div>
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 较上月 +33%</div>
<div class="delta up"><span data-iconkit="arrowUp" data-icon-size="14"></span> 较上月 +33%</div>
</a>
<a class="stat" href="account.html">
<div class="lbl">余额 <span class="badge">¥</span></div>
@ -79,7 +79,7 @@
<div>
<div class="section-h">
<h2>最近项目</h2>
<a class="more" href="projects.html">[ ALL · 8 ] <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><path d="M5 12h14M12 5l7 7-7 7"/></svg></a>
<a class="more" href="projects.html">[ ALL · 8 ]</a>
</div>
<div class="card-hard">
<a class="recent-row" href="pipeline.html?product=%E9%80%8F%E7%9C%9F%E8%A1%A5%E6%B0%B4%E9%9D%A2%E8%86%9C#stage-3">
@ -140,19 +140,19 @@
<div class="section-h"><h2>快捷入口</h2><span class="more">[ /shortcuts ]</span></div>
<div class="shortcuts">
<a class="shortcut" href="products.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg></div>
<div class="ic" data-iconkit="package"></div>
<div><div class="t">商品库</div><div class="d">7 SKU</div></div>
</a>
<a class="shortcut" href="library.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg></div>
<div class="ic" data-iconkit="images"></div>
<div><div class="t">资产库</div><div class="d">人 8 · 景 14 · 片 8</div></div>
</a>
<a class="shortcut" href="account.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg></div>
<div class="ic" data-iconkit="creditCard"></div>
<div><div class="t">充值</div><div class="d">¥327.40</div></div>
</a>
<a class="shortcut" href="projects.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg></div>
<div class="ic" data-iconkit="clapperboard"></div>
<div><div class="t">所有项目</div><div class="d">8 个</div></div>
</a>
</div>
@ -169,8 +169,14 @@
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });</script>
<script>
document.querySelectorAll('[data-iconkit]').forEach(el => {
el.innerHTML = IconKit.svg(el.dataset.iconkit, { size: Number(el.dataset.iconSize || 16) });
});
Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });
</script>
</body>
</html>

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>资产库 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
<title>资产库 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.asset-grid.video-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
@ -939,7 +939,8 @@
<button type="button" id="bulk-exit">完成</button>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });
@ -2271,7 +2272,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
tags = [card.dataset.gender, card.dataset.age, card.dataset.role].filter(Boolean);
props = [
['性别', card.dataset.gender || '-'], ['种族', '东亚'], ['作品ID', _fmtAssetId(name, 'person')],
['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', '流·Studio'],
['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', 'Airshelf'],
['身高', '中等'], ['来源', source], ['文件大小', _fmtSize(name)],
['使用次数', used + ' 次'], ['授权', '商用'], ['发布时间', '2024-05-20'],
];
@ -2282,7 +2283,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
tags = [card.dataset.sceneType, source].filter(Boolean);
props = [
['场景类型', card.dataset.sceneType], ['来源', source], ['作品ID', _fmtAssetId(name, 'scene')],
['镜头', '通用'], ['光线', '自然光'], ['创作人', '流·Studio'],
['镜头', '通用'], ['光线', '自然光'], ['创作人', 'Airshelf'],
['用途', '本项目场景资产'], ['使用次数', used + ' 次'], ['文件大小', _fmtSize(name)],
];
} else if (card.dataset.product) {
@ -2292,7 +2293,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
tags = ['商品', source].filter(Boolean);
props = [
['关联商品', card.dataset.product], ['来源', source], ['作品ID', _fmtAssetId(name, 'product')],
['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', '流·Studio'],
['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', 'Airshelf'],
['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
} else {
@ -2302,7 +2303,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
tags = [source].filter(Boolean);
props = [
['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],
['创作人', '流·Studio'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
['创作人', 'Airshelf'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
}

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>登录 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>登录 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }
@ -107,8 +107,8 @@
<!-- 左:品牌 -->
<aside class="auth-brand">
<div class="logo">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L19 7 V17 L12 22 L5 17 V7 Z"/><path d="M12 2 V22"/><path d="M5 7 L19 17"/></svg></span>
<span>流·Studio</span>
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 19 12 4.5 19.5 19"/><path d="M8 14h8"/><path d="M7 17h10"/><path d="M9.5 10.3c1.5-1.2 3.5-1.2 5 0"/></svg></span>
<span>Airshelf</span>
</div>
<div class="tag">// SHORT-VIDEO COMMERCE PLATFORM</div>
@ -211,7 +211,7 @@
el.style.cssText = 'position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(20px);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:12px 18px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .2s,transform .2s;z-index:9999;font-family:inherit;max-width:360px;';
document.body.appendChild(el);
}
el.innerHTML = '<div style="font-size:13.5px;font-weight:600;color:#262626;">' + t + '</div>' + (sub ? '<div style="font-size:11.5px;color:rgba(0,0,0,.56);font-family:\'JetBrains Mono\',monospace;letter-spacing:.02em;">// ' + sub + '</div>' : '');
el.innerHTML = '<div style="font-size:13.5px;font-weight:600;color:#262626;">' + t + '</div>' + (sub ? '<div style="font-size:11.5px;color:rgba(0,0,0,.56);font-family:\'Inter\',system-ui,sans-serif;letter-spacing:.02em;">// ' + sub + '</div>' : '');
requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; });
clearTimeout(el._t);
el._t = setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(20px)'; }, 2800);

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>消息中心 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>消息中心 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#page { display: flex; flex-direction: column; height: 100%; min-height: 0; }
@ -349,7 +349,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({ active: '', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消息中心' }] });
@ -374,16 +375,17 @@ function _fmtFull(d) {
+ ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
const icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
const ICONS = {
taskOk: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.1V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>',
taskFail: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/></svg>',
taskRun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
team: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3 2.7-5 6-5s6 2 6 5"/><circle cx="17" cy="9" r="2.5"/><path d="M14 20c.5-2.4 2.4-4 5-4"/></svg>',
comment: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-8.5 8.5 8.5 8.5 0 0 1-4.5-1.3L3 20l1.3-5a8.5 8.5 0 0 1-1.3-4.5A8.4 8.4 0 0 1 11.5 2 8.5 8.5 0 0 1 21 11.5z"/></svg>',
system: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>',
billing: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>',
shield: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>',
pipeline: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>',
taskOk: icon('check'),
taskFail: icon('x'),
taskRun: icon('clock'),
team: icon('users'),
comment: icon('messageCircle'),
system: icon('info'),
billing: icon('wallet'),
shield: icon('shieldCheck'),
pipeline: icon('film'),
};
const MESSAGES = [
@ -524,7 +526,7 @@ const MESSAGES = [
id: 'm-011', type: 'system', icon: 'system',
title: '5/25 23:00 - 5/26 01:00 · 视频生成服务例行维护',
body: '本次维护涉及视频生成 + 三视图生成两项服务,期间提交的任务会自动延迟到维护结束后开始处理,已生成任务不受影响。',
ts: _ago(720), from: '流·Studio 运维', tag: '公告', tagKind: 'info',
ts: _ago(720), from: 'Airshelf 运维', tag: '公告', tagKind: 'info',
project: '系统', stage: '维护公告',
target: null,
actions: [

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>方案 A · 商品空间 · 模特上身图 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>方案 A · 商品空间 · 模特上身图 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
@ -622,7 +622,8 @@
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'asset-factory',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>方案 A · v2 · 模特上身图 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>方案 A · v2 · 模特上身图 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
@ -626,7 +626,8 @@
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'asset-factory',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>模特上身图 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>模特上身图 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* viewport-fit · 工作台铺满 (跟 demo-a 一致 · padding:0) */
.app { height: 100vh; overflow: hidden; }
@ -2816,7 +2816,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>
Shell.render({
@ -3309,7 +3310,7 @@ const ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" st
let _batchSeq = 0;
const CELL_RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>';
const CELL_DL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>';
const CELL_ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>';
const CELL_DEL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>';
const CELL_EDIT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z"/></svg>';
@ -3907,7 +3908,7 @@ _mcInputText?.addEventListener('input', () => {
const _MC_SVG = {
rerun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>',
dl: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>',
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>',
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>',
adopt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>',
del: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>',
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
@ -4524,7 +4525,7 @@ function openModelDetail(id) {
const tags = [m.vibe, m.style, m.age, m.hairLen, m.region].filter(Boolean);
const props = [
['性别', m.gender], ['种族', m.region], ['作品ID', assetId],
['年龄段', m.age], ['气质', m.vibe], ['创作人', '流·Studio'],
['年龄段', m.age], ['气质', m.vibe], ['创作人', 'Airshelf'],
['身高', m.height], ['体格', m.build], ['文件大小', fileSize],
['发型', m.hairLen + ' · ' + m.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],
];

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title id="page-title">流水线 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title id="page-title">流水线 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── Project header ─── */
.proj-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 22px; align-items: flex-start; }
@ -622,6 +622,24 @@
.asset-card-2:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.asset-card-2 .thumb-2 { aspect-ratio: 1; }
.asset-card-2 .body-2 { padding: 12px 14px; }
.asset-card-2 .body-2 .btn:disabled,
.asset-card-2 .body-2 .btn.disabled {
background: transparent;
border-color: transparent;
color: var(--black-alpha-32);
box-shadow: none;
cursor: not-allowed;
opacity: .72;
transform: none;
}
.asset-card-2 .body-2 .btn:disabled:hover,
.asset-card-2 .body-2 .btn.disabled:hover {
background: transparent;
border-color: transparent;
color: var(--black-alpha-32);
box-shadow: none;
transform: none;
}
.asset-card-2 .body-2 .btn-apply { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
.asset-card-2 .body-2 .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
@ -1179,6 +1197,39 @@
.sb-scene-thumb .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.sb-main-img { aspect-ratio: 16/9; min-height: 0; }
.sb-rerun-note {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
margin-bottom: 14px;
background: rgba(180,83,9,.08);
border: 1px solid rgba(180,83,9,.20);
border-radius: var(--r-md);
color: #7C3A05;
line-height: 1.55;
}
.sb-rerun-note .warn-ic {
width: 22px;
height: 22px;
border-radius: var(--r-sm);
background: rgba(180,83,9,.12);
color: #B45309;
display: grid;
place-items: center;
flex: 0 0 22px;
}
.sb-rerun-note .warn-ic svg {
width: 14px;
height: 14px;
}
.sb-rerun-note .note-copy {
min-width: 0;
font-size: 11.5px;
}
.sb-rerun-note strong { color: #B45309; }
.sb-rerun-note a { color: #B45309; text-decoration: underline; text-underline-offset: 2px; }
.sb-stage-actions { display: flex; gap: 8px; margin-top: 14px; margin-bottom: 12px; }
/* 故事板历史版本 */
@ -1759,11 +1810,12 @@
<div class="muted-2" style="font-size:12px; line-height:1.55; margin-bottom:10px;">
整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。
</div>
<div style="display:flex; gap:8px; align-items:flex-start; padding:9px 10px; margin-bottom:14px; background:rgba(180,83,9,.08); border:1px solid rgba(180,83,9,.20); border-radius:var(--r-sm);">
<svg style="flex-shrink:0;margin-top:1px;" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#B45309" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
<div style="font-size:11.5px; color:#7C3A05; line-height:1.55; min-width:0;">
<strong style="color:#B45309;">仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1" style="color:#B45309;text-decoration:underline;">Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。
<div style="font-family:var(--font-mono); font-size:10.5px; color:rgba(180,83,9,.7); letter-spacing:.02em; margin-top:3px;">// PRD §5.2 · image-2 单次输出限制</div>
<div class="sb-rerun-note">
<span class="warn-ic" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4M12 17h.01"/></svg>
</span>
<div class="note-copy">
<strong>仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1">Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。
</div>
</div>
@ -2423,7 +2475,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
/* ─── 商品名贯穿全流程(从 ?product= 读取,无参数时回退到 mock 默认值)─── */
const URL_PRODUCT_NAME = (function () {
@ -2451,7 +2504,7 @@ Shell.render({
});
/* 渲染贯穿商品名 / 项目名 */
document.getElementById('page-title').textContent = PROJECT_TITLE + ' · 流水线 · 流·Studio';
document.getElementById('page-title').textContent = PROJECT_TITLE + ' · 流水线 · Airshelf';
/* ─── 把 stage-pill anchor 注入 .topbar 中部(圆点全状态都靠 .sp-dot 实时同步)─── */
(function _injectStagePill() {
@ -3033,7 +3086,7 @@ const Stage2 = (function () {
const tags = [a.vibe, a.age, a.hairLen, a.region, a.skin].filter(Boolean);
const props = [
['性别', a.gender], ['种族', a.region], ['作品ID', _fmtAssetId(name, 'model')],
['年龄段', a.age], ['气质', a.vibe], ['创作人', '流·Studio'],
['年龄段', a.age], ['气质', a.vibe], ['创作人', 'Airshelf'],
['身高', a.height], ['体格', a.build], ['文件大小', _fmtFileSize(name)],
['发型', a.hairLen + ' · ' + a.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],
];
@ -3047,7 +3100,7 @@ const Stage2 = (function () {
} else {
const props = [
['类别', '场景 · ' + (isOwn ? '我的上传' : '预设')], ['标签', sub || '-'], ['作品ID', _fmtAssetId(name, 'scene')],
['来源', sourceLabel], ['用途', '本项目场景资产'], ['创作人', '流·Studio'],
['来源', sourceLabel], ['用途', '本项目场景资产'], ['创作人', 'Airshelf'],
['镜头', '通用'], ['光线', '自然光'], ['文件大小', _fmtFileSize(name)],
];
renderAssetDetail({
@ -3084,7 +3137,7 @@ const Stage2 = (function () {
const kindLabelMap = { character: '人物', scene: '场景', product: '商品' };
const baseProps = d.info.slice();
baseProps.push(['作品ID', _fmtAssetId(d.title, d.kind === 'character' ? 'model' : d.kind === 'scene' ? 'scene' : 'product')]);
baseProps.push(['创作人', '流·Studio']);
baseProps.push(['创作人', 'Airshelf']);
baseProps.push(['文件大小', _fmtFileSize(d.title)]);
baseProps.push(['发布时间', '2024-05-20']);
renderAssetDetail({
@ -3398,7 +3451,7 @@ const Stage2 = (function () {
const _MC_SVG = {
rerun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>',
dl: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>',
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>',
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>',
adopt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>',
del: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>',
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>平台套图 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>平台套图 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
@ -1420,7 +1420,8 @@
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>
Shell.render({
@ -1881,7 +1882,7 @@ const RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" st
const ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
const CELL_RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>';
const CELL_DL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>';
const CELL_ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>';
const CELL_DEL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>';
const CELL_EDIT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z"/></svg>';

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 跳转中...</title>
<title>新建商品 · Airshelf</title>
<script>
// 已废弃 · 新建商品改为 products.html 上的居中弹窗(Shell.openNewProduct)
// 直接访问此 URL 时,跳回商品库并自动打开弹窗

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>新建商品 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 主表单 ─── */
.form-grid {
@ -275,7 +275,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'products',

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<title>新建商品 · Airshelf</title>
<meta http-equiv="Cache-Control" content="no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

View File

@ -1,10 +1,10 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>新建商品 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ============================================================
V2.1 product-create · 全屏新建商品工作台
@ -2596,7 +2596,8 @@
</aside>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
// ============================================================
// 调用 Shell.render · 注入网站 sidebar (form mode 显示)

View File

@ -5,9 +5,9 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>商品详情 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>商品详情 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 顶部 标题 + 状态 ─── */
.pd-title {
@ -1349,7 +1349,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
// 从 URL ?product= 读出商品名,注入 crumb / h1 / 商品名字段
const _urlProductName = (function () {

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>商品工作台 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>商品工作台 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── Onboarding tip(首次进入) ─── */
.onboard-tip {
@ -771,7 +771,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'products',
@ -800,24 +801,26 @@ function dismissOnboard() {
}
// ============= 假数据 =============
const icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
const MODELS = [
{ id: 'm1', name: '林夕', meta: '女 · 25 · 都市白领', emoji: '◐' },
{ id: 'm2', name: '小苏', meta: '女 · 23 · 文艺学生', emoji: '◑' },
{ id: 'm3', name: '阿楠', meta: '女 · 28 · 同事型', emoji: '◓' },
{ id: 'm4', name: '小七', meta: '女 · 20 · 学生', emoji: '◒' },
{ id: 'm5', name: '王姐', meta: '女 · 38 · 居家', emoji: '◍' },
{ id: 'm6', name: '阿杰', meta: '男 · 30 · 都市', emoji: '◐' },
{ id: 'm7', name: '阿强', meta: '男 · 26 · 健身', emoji: '◑' },
{ id: 'm8', name: '+ 自定义', meta: '上传自有模特照', emoji: '+', custom: true },
{ id: 'm1', name: '林夕', meta: '女 · 25 · 都市白领', icon: 'users' },
{ id: 'm2', name: '小苏', meta: '女 · 23 · 文艺学生', icon: 'users' },
{ id: 'm3', name: '阿楠', meta: '女 · 28 · 同事型', icon: 'users' },
{ id: 'm4', name: '小七', meta: '女 · 20 · 学生', icon: 'users' },
{ id: 'm5', name: '王姐', meta: '女 · 38 · 居家', icon: 'users' },
{ id: 'm6', name: '阿杰', meta: '男 · 30 · 都市', icon: 'users' },
{ id: 'm7', name: '阿强', meta: '男 · 26 · 健身', icon: 'users' },
{ id: 'm8', name: '+ 自定义', meta: '上传自有模特照', icon: 'userPlus', custom: true },
];
const PLATFORMS = [
{ id: 'p1', name: '淘宝 / 天猫', meta: '1:1 · 800×800', emoji: '🅣' },
{ id: 'p2', name: '抖店', meta: '3:4 · 750×1000', emoji: '🅓' },
{ id: 'p3', name: '拼多多', meta: '1:1 · 800×800', emoji: '🅟' },
{ id: 'p4', name: '京东', meta: '1:1 · 800×800', emoji: '🅙' },
{ id: 'p5', name: '小红书', meta: '3:4 · 600×800', emoji: '🅡' },
{ id: 'p6', name: '1688', meta: '1:1 · 750×750', emoji: '🅑' },
{ id: 'p1', name: '淘宝 / 天猫', meta: '1:1 · 800×800', icon: 'image' },
{ id: 'p2', name: '抖店', meta: '3:4 · 750×1000', icon: 'image' },
{ id: 'p3', name: '拼多多', meta: '1:1 · 800×800', icon: 'image' },
{ id: 'p4', name: '京东', meta: '1:1 · 800×800', icon: 'image' },
{ id: 'p5', name: '小红书', meta: '3:4 · 600×800', icon: 'image' },
{ id: 'p6', name: '1688', meta: '1:1 · 750×750', icon: 'image' },
];
// 已生成资产 (mock)
@ -914,7 +917,7 @@ function openModelPicker() {
// 填充模特库
$('model-grid').innerHTML = MODELS.map(m => `
<div class="opt-card${m.custom ? ' add' : ''}" data-id="${m.id}" onclick="selectOption('model', '${m.id}', this)">
<div class="opt-thumb">${m.emoji}</div>
<div class="opt-thumb">${icon(m.icon, { size: 18 })}</div>
<div class="opt-name">${m.name}</div>
<div class="opt-meta">${m.meta}</div>
</div>
@ -926,7 +929,7 @@ function openModelPicker() {
function openPlatformPicker() {
$('platform-grid').innerHTML = PLATFORMS.map(p => `
<div class="opt-card" data-id="${p.id}" onclick="selectOption('platform', '${p.id}', this)">
<div class="opt-thumb">${p.emoji}</div>
<div class="opt-thumb">${icon(p.icon, { size: 18 })}</div>
<div class="opt-name">${p.name}</div>
<div class="opt-meta">${p.meta}</div>
</div>

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>商品库 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
<title>商品库 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 全局 viewport 高度链 (让右侧 toolbar/分页吸顶吸底) ─── */
/* 整页滚动 · 头部 H1+actions sticky 固定 · 其他随页面滚 */
@ -147,7 +147,7 @@
padding: 10px 12px;
border-top: 1px solid var(--border-faint);
font-size: 11.5px;
color: var(--black-alpha-56);
color: #6f6f6f;
background: var(--background-base);
}
.product-footer .stat {
@ -170,9 +170,10 @@
.product-footer .stat[data-type]:hover svg { color: var(--heat); }
.product-footer .stat[data-type]:hover b { color: var(--heat); }
.product-footer .stat svg {
width: 13px; height: 13px;
color: var(--black-alpha-48);
width: 14px; height: 14px;
color: currentColor;
flex-shrink: 0;
stroke-width: 1.25;
transition: color var(--t-base);
}
.product-footer .stat b {
@ -182,10 +183,11 @@
}
/* default ↔ hover 文本切换 */
.product-footer .stat .stat-hover { display: none; }
.product-footer .stat[data-type]:hover > svg { display: none; }
.product-footer .stat[data-type]:hover .stat-default { display: none; }
.product-footer .stat[data-type]:hover .stat-hover { display: inline; }
.product-footer .sep {
color: var(--black-alpha-24);
color: #b8b8b8;
font-family: var(--font-mono);
flex-shrink: 0;
}
@ -627,8 +629,8 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
<span class="btn-edit-label">管理商品</span>
</button>
<button class="btn btn-primary" type="button" id="open-new-product">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
<button class="btn btn-primary btn-create" type="button" id="open-new-product">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22V12"/><path d="M16 17h6"/><path d="M19 14v6"/><path d="M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="m7.5 4.3 9 5.1"/></svg>
新建商品
</button>
</div>
@ -1015,7 +1017,8 @@
<button type="button" id="bulk-exit">完成</button>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });
@ -1122,17 +1125,24 @@ cards.forEach(card => {
const label = m[1];
const num = m[2];
const isAsset = label === '素材';
const cta = isAsset ? '+ 去生成素材' : '+ 去生成视频';
const cta = isAsset ? '去生成素材' : '去生成视频';
const svg = stat.querySelector('svg');
// 重组结构: <svg> <span.stat-default>素材 <b>N</b></span> <span.stat-hover>+ 去生成素材</span>
const icon = window.IconKit
? window.IconKit.svg(isAsset ? 'images' : 'video', {
size: 14,
strokeWidth: 1.25,
className: 'product-stat-icon'
})
: (svg ? svg.outerHTML : '');
// 重组结构: <svg> <span.stat-default>素材 <b>N</b></span> <span.stat-hover>去生成素材</span>
stat.innerHTML = '';
if (svg) stat.appendChild(svg);
if (icon) stat.insertAdjacentHTML('beforeend', icon);
const dft = document.createElement('span');
dft.className = 'stat-default';
dft.innerHTML = `${label} <b>${num}</b>`;
const hv = document.createElement('span');
hv.className = 'stat-hover';
hv.textContent = cta;
hv.textContent = cta.replace(/^\s*\+\s*/, '');
stat.appendChild(dft);
stat.appendChild(hv);
stat.dataset.type = isAsset ? 'asset' : 'video';

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建项目 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>新建项目 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.wizard { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 36px; align-items: start; max-width: 1400px; }
@media (max-width: 1180px) { .wizard { grid-template-columns: 200px minmax(0, 1fr); } }
@ -88,10 +88,11 @@
.opt-card .badge { font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-48); display: inline-block; margin-top: 8px; letter-spacing: .04em; align-self: flex-start; }
.opt-card.selected .badge { color: var(--heat); border-color: var(--heat-20); }
.theme-pill { display: inline-flex; gap: 4px; align-items: center; height: 28px; padding: 0 12px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 12.5px; cursor: pointer; color: var(--black-alpha-56); transition: background var(--t-base), border-color var(--t-base); }
.theme-pill-row { display: flex; gap: 8px; flex-wrap: wrap; }
.theme-pill { display: inline-flex; gap: 6px; align-items: center; height: 36px; padding: 0 16px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer; color: var(--accent-black); transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.theme-pill:hover { background: var(--background-lighter); }
.theme-pill.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.theme-pill svg { width: 12px; height: 12px; }
.theme-pill.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat); font-weight: 600; }
.theme-pill svg { width: 13px; height: 13px; }
.reco-bubble { position: relative; margin-top: 10px; padding: 10px 14px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-md); display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--accent-black); }
.reco-bubble::before { content: ''; position: absolute; top: -5px; left: 28px; width: 9px; height: 9px; background: var(--heat-12); border-left: 1px solid var(--heat-20); border-top: 1px solid var(--heat-20); transform: rotate(45deg); }
@ -433,6 +434,10 @@
/* ── shared field styles ── */
.field { display: block; margin-bottom: 16px; }
.config-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: end; margin-bottom: 16px; }
.config-row .field { margin-bottom: 0; }
.duration-select { cursor: pointer; }
@media (max-width: 900px) { .config-row { grid-template-columns: 1fr; } }
.field-label { display: block; font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; margin-bottom: 6px; }
.field-label .req { color: var(--heat); margin-left: 2px; }
.field-hint { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 4px; }
@ -605,7 +610,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '新建项目' }] });</script>
@ -767,14 +773,15 @@
/* ---------- icons ---------- */
const icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
const ICONS = {
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>',
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>',
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>',
x: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l14 14M19 5L5 19"/></svg>',
bulb: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>',
arrow: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
wallet: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>',
check: icon('check'),
search: icon('search'),
plus: icon('plus'),
x: icon('x'),
bulb: icon('info'),
arrow: icon('arrowRight'),
wallet: icon('wallet'),
};
/* ---------- actions ---------- */
@ -1222,27 +1229,23 @@
<p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>
</div>
<div class="config-row">
<div class="field">
<label class="field-label">项目名称<span class="req">*</span></label>
<input class="input" value="${esc(state.projectName)}" oninput="_wiz.setName(this.value)">
</div>
<div class="field">
<label class="field-label">视频时长<span class="req">*</span></label>
<div class="opt-row cols-4">
${DURATIONS.map(d => `<div class="opt-card${state.duration === d.id ? ' selected' : ''}" onclick="_wiz.setDur('${d.id}')">
<h4>${esc(d.label)}</h4>
<div class="sub">${d.shots[0]}-${d.shots[1]} 镜</div>
<div class="note">${esc(d.tag)}</div>
<div class="metric">完播 <span class="val">${d.completion}%</span></div>
</div>`).join('')}
<select class="input duration-select" onchange="_wiz.setDur(this.value)">
<option value="" disabled ${state.duration ? '' : 'selected'}>选择时长</option>
${DURATIONS.map(d => `<option value="${esc(d.id)}" ${state.duration === d.id ? 'selected' : ''}>${esc(d.label)} · ${d.shots[0]}-${d.shots[1]} 镜</option>`).join('')}
</select>
</div>
<div class="field-hint">数据来源:抖音同品类 TOP 视频均值 · 实际镜头数由 LLM 决定</div>
</div>
<div class="field">
<label class="field-label">脚本风格</label>
<div class="opt-row">
<div class="opt-row cols-4">
${STYLES.map(s => `<div class="opt-card${state.scriptStyle === s.id ? ' selected' : ''}" onclick="_wiz.setStyle('${s.id}')">
<h4>${esc(s.name)}</h4>
<div class="note">${esc(s.note)}</div>
@ -1274,8 +1277,8 @@
${Object.keys(state.points).length > 0 ? `<div class="field" style="margin-bottom: 0;">
<label class="field-label">关键卖点(可勾选要重点突出的)</label>
<div style="display:flex; gap:6px; flex-wrap:wrap;">
${Object.entries(state.points).map(([k, v]) => `<span class="theme-pill${v ? ' active' : ''}" onclick="_wiz.togglePt('${esc(k).replace(/'/g, "\\'")}')">${v ? ICONS.check : ICONS.plus}<span>${esc(k)}</span></span>`).join('')}
<div class="theme-pill-row">
${Object.entries(state.points).map(([k, v]) => `<button class="theme-pill${v ? ' active' : ''}" type="button" aria-pressed="${v ? 'true' : 'false'}" onclick="_wiz.togglePt('${esc(k).replace(/'/g, "\\'")}')">${v ? ICONS.check : ''}<span>${esc(k)}</span></button>`).join('')}
</div>
</div>` : ''}
</div>`;

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>视频项目 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>视频项目 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── List view ─── */
.proj-name-cell { display: flex; align-items: center; gap: 12px; }
@ -135,8 +135,8 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
<span class="proj-manage-label">管理项目</span>
</button>
<a class="btn btn-primary btn-lg" href="projects-new.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
<a class="btn btn-primary btn-lg btn-create" href="projects-new.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m12.3 3.5 3 4"/><path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z"/><path d="m6.2 5.3 3.1 3.9"/><path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg>
新建项目
</a>
</div>
@ -571,7 +571,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目' }] });

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>注册团队 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>注册团队 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }
@ -101,8 +101,8 @@
<aside class="auth-brand">
<div class="logo">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L19 7 V17 L12 22 L5 17 V7 Z"/><path d="M12 2 V22"/><path d="M5 7 L19 17"/></svg></span>
<span>流·Studio</span>
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 19 12 4.5 19.5 19"/><path d="M8 14h8"/><path d="M7 17h10"/><path d="M9.5 10.3c1.5-1.2 3.5-1.2 5 0"/></svg></span>
<span>Airshelf</span>
</div>
<div class="tag">// SHORT-VIDEO COMMERCE PLATFORM</div>
@ -239,7 +239,7 @@
el.style.cssText = 'position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(20px);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:12px 18px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .2s,transform .2s;z-index:9999;font-family:inherit;max-width:380px;';
document.body.appendChild(el);
}
el.innerHTML = '<div style="font-size:13.5px;font-weight:600;color:#262626;">' + t + '</div>' + (sub ? '<div style="font-size:11.5px;color:rgba(0,0,0,.56);font-family:\'JetBrains Mono\',monospace;letter-spacing:.02em;">// ' + sub + '</div>' : '');
el.innerHTML = '<div style="font-size:13.5px;font-weight:600;color:#262626;">' + t + '</div>' + (sub ? '<div style="font-size:11.5px;color:rgba(0,0,0,.56);font-family:\'Inter\',system-ui,sans-serif;letter-spacing:.02em;">// ' + sub + '</div>' : '');
requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; });
clearTimeout(el._t);
el._t = setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(20px)'; }, 3000);

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>设置 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>设置 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 设置布局:左 nav + 右 panel ─── */
.settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
@ -363,7 +363,7 @@
<div class="lbl">导出水印<div class="lbl-sub">// VIP 可关闭</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-watermark" checked disabled><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">右下角 · 流·Studio</span>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">右下角 · Airshelf</span>
<a href="account.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">升级 VIP →</a>
</div>
</div>
@ -434,7 +434,7 @@
</section>
<div style="text-align: center; padding: 24px 0 8px; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;">
// 流·Studio · v2.1 · build 20260521
// Airshelf · v2.1 · build 20260521
</div>
</main>
</div>
@ -485,7 +485,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'settings',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>商品工作台 V2 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>商品工作台 V2 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
.content { max-width: none !important; padding: 0 !important; }
.content > .corner-mark { display: none; }
@ -544,7 +544,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'products',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>商品工作台 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>商品工作台 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 撑满 .content 的限制 ─── */
.content { max-width: none !important; padding: 0 !important; }
@ -771,7 +771,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'products',

View File

@ -2,9 +2,9 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>团队 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<title>团队 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */
/* 顶部行:banner(左)+ 团队动态(右),与下方 team-grid 同列宽对齐 */
@ -672,7 +672,7 @@
<div class="cred-card">
<div class="cred-row">
<span class="ck">登录地址</span>
<span class="cv mono" id="share-url">https://liu-studio.com/login</span>
<span class="cv mono" id="share-url">https://airshelf.com/login</span>
<button class="cred-copy" type="button" data-copy="share-url" title="复制">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
@ -757,7 +757,8 @@
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'team',
@ -1040,7 +1041,7 @@ document.getElementById('inv-send').addEventListener('click', () => {
id: 'u' + Date.now(),
av: name[0] || username[0]?.toUpperCase() || 'U',
name, username,
email: username + '@liu-studio.local',
email: username + '@airshelf.local',
role,
daily, monthly, totalQuota,
used: 0, usedToday: 0,