fix(deploy): 把 V2.1 设计稿挪进 电商AI平台/ · Docker 构建上下文是这里
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s

This commit is contained in:
UI 设计 2026-05-21 16:30:37 +08:00
parent e293aa43be
commit e7c0a14f75
22 changed files with 23431 additions and 2283 deletions

View File

@ -0,0 +1,900 @@
<!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">
<style>
#page-content { padding: 24px 28px 60px; }
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
.factory-hero {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; margin-bottom: 56px;
}
@media (max-width: 1400px) { .factory-hero { grid-template-columns: 1fr 1fr; } }
@media (max-width: 1000px) { .factory-hero { grid-template-columns: 1fr; } }
.factory-card {
position: relative;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 28px 30px;
overflow: hidden;
}
/* 卡片内 · 文上图下 单列(3 卡并排时保持视觉一致)*/
.factory-body {
display: flex; flex-direction: column;
gap: 18px;
height: 100%;
}
.factory-text { display: flex; flex-direction: column; min-width: 0; }
.factory-tag {
align-self: flex-start;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
padding: 2px 8px;
border: 1px solid var(--border-faint);
background: var(--background-lighter);
border-radius: var(--r-sm);
margin-bottom: 14px;
}
.factory-title {
font-size: 22px; font-weight: 600;
letter-spacing: -.018em; line-height: 1.25;
color: var(--accent-black);
}
.factory-desc {
margin-top: 8px;
font-size: 13.5px; color: var(--black-alpha-64); line-height: 1.55;
}
/* feature 列表 */
.factory-features {
list-style: none; padding: 0;
margin: 22px 0 0;
display: flex; flex-direction: column; gap: 11px;
}
.factory-features li {
display: flex; align-items: center; gap: 10px;
font-size: 13px; color: var(--black-alpha-72); font-weight: 500;
}
.factory-features .ff-ic {
width: 26px; height: 26px;
display: inline-flex; align-items: center; justify-content: center;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
flex-shrink: 0;
}
.factory-features .ff-ic svg { width: 14px; height: 14px; }
/* 平台 chip 行 */
.platform-row {
display: flex; flex-wrap: wrap; gap: 8px;
margin-top: 20px;
}
.platform-chip {
display: inline-flex; align-items: center; gap: 7px;
height: 30px; padding: 0 12px 0 8px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-pill);
transition: background var(--t-base);
cursor: pointer;
}
.platform-chip:hover { background: var(--black-alpha-4); }
.platform-chip .code {
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px;
background: var(--accent-black); color: var(--accent-white);
font-family: var(--font-mono); font-size: 8.5px; font-weight: 600;
border-radius: var(--r-pill); letter-spacing: .04em;
}
.platform-chip .nm {
font-size: 12px; color: var(--accent-black); font-weight: 500;
}
/* CTA 行:主按钮 + 价格 mono */
.factory-cta {
margin-top: auto; padding-top: 24px;
display: flex; align-items: center; gap: 14px;
}
.factory-cta .cost {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
}
/* 视觉占位 + feature 列表已隐藏 — CTA 卡片只保留标题 + 描述 + 按钮 */
.factory-visual { display: none; }
.factory-features { display: none; }
.model-visual { grid-template-columns: repeat(4, 1fr); }
.model-visual .main { aspect-ratio: 3 / 4; grid-column: span 1; }
.model-visual .stack { display: contents; }
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
.kit-visual { grid-template-columns: repeat(4, 1fr); }
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
.tri-visual { grid-template-columns: repeat(3, 1fr); }
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
/* ─── 任务中心 · section header ─── */
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
.section-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.section-h .sub-mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── 视图切换 (复用 projects.html · 图标 + 文字) ─── */
.view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border: 0; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }
.view-toggle button:last-child { border-right: 0; }
.view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }
.view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }
.view-toggle button svg { width: 13px; height: 13px; }
/* ─── 网格视图 · 卡片(原 history-grid) ─── */
.history-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 1280px) { .history-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 960px) { .history-grid { grid-template-columns: repeat(2, 1fr); } }
.history-grid[hidden] { display: none; }
.history-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 12px;
display: grid; grid-template-columns: 78px 1fr; gap: 14px;
align-items: center;
transition: background var(--t-base);
cursor: pointer;
position: relative;
}
.history-card:hover { background: var(--black-alpha-4); }
.history-card:hover .card-del-btn { opacity: 1; }
.history-card .placeholder { width: 78px; height: 78px; }
/* ─── 列表视图 · 表格 ─── */
#task-list-view[hidden] { display: none; }
.task-name-cell { display: flex; align-items: center; gap: 12px; }
.task-thumb { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--r-sm); }
.task-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
.task-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
table.t .task-list-prog { display: flex; align-items: center; gap: 8px; min-width: 120px; }
table.t .task-list-prog .bar { flex: 1; height: 4px; background: var(--black-alpha-7); border-radius: 2px; overflow: hidden; }
table.t .task-list-prog .bar span { display: block; height: 100%; background: var(--heat); border-radius: 2px; animation: hp-pulse 1.4s ease-in-out infinite; }
table.t .task-list-prog .pct { font-family: var(--font-mono); font-size: 10.5px; color: var(--heat); letter-spacing: .02em; white-space: nowrap; }
/* ─── 行末 ⋯ 删除气泡 (复用 projects.html) ─── */
.row-action { display: flex; gap: 4px; justify-content: flex-end; }
table.t tbody tr .row-more { opacity: 0; transition: opacity .15s; }
table.t tbody tr:hover .row-more { opacity: 1; }
.row-more {
position: relative; display: inline-flex;
cursor: pointer; align-items: center;
color: var(--black-alpha-56);
padding: 4px;
}
.row-more:hover { color: var(--accent-black); }
.row-more-tip {
position: absolute; top: calc(100% + 6px); right: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 4px 16px rgba(0,0,0,.08);
padding: 4px; min-width: 110px;
opacity: 0; pointer-events: none;
transform: translateY(-2px);
transition: opacity .15s, transform .15s;
z-index: 12;
}
.row-more-tip::before {
content: ''; position: absolute;
top: -8px; left: 0; right: 0; height: 8px;
}
.row-more:hover .row-more-tip,
.row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }
.row-more-tip .mi {
display: flex; align-items: center; gap: 6px;
width: 100%; padding: 6px 10px;
background: transparent; border: 0;
border-radius: var(--r-sm); cursor: pointer;
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; text-align: left;
transition: background var(--t-base), color var(--t-base);
}
.row-more-tip .mi:hover {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
}
.row-more-tip .mi svg { width: 13px; height: 13px; }
/* ─── 删除确认 modal · 复用 ─── */
.modal-bg.show { display: flex; }
.mono-acc { font-family: var(--font-mono); color: var(--heat); font-weight: 600; }
.history-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.history-name {
font-size: 13.5px; font-weight: 600; color: var(--accent-black);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-type {
font-size: 11.5px; color: var(--black-alpha-48);
font-family: var(--font-mono); letter-spacing: .02em;
}
.history-foot {
display: flex; align-items: center; justify-content: space-between;
margin-top: 4px; gap: 6px; min-width: 0;
}
.history-foot .mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-foot .pill { padding: 2px 8px; font-size: 10.5px; }
.history-foot .pill .dot { width: 5px; height: 5px; }
/* 进度条(生成中状态) */
.history-prog {
margin-top: 6px;
display: flex; align-items: center; gap: 8px;
}
.history-prog .bar {
flex: 1; height: 4px;
background: var(--black-alpha-7);
border-radius: 2px; overflow: hidden;
}
.history-prog .bar span {
display: block; height: 100%;
background: var(--heat); border-radius: 2px;
animation: hp-pulse 1.4s ease-in-out infinite;
}
@keyframes hp-pulse {
0%, 100% { opacity: 1; transform: scaleY(1); }
50% { opacity: .55; transform: scaleY(.7); }
}
.history-prog .pct {
font-family: var(--font-mono); font-size: 10px;
color: var(--heat); letter-spacing: .02em; white-space: nowrap;
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>图片生成</h1>
<div class="sub">
<span class="mono">// 一键生成</span>
<span>·</span>
<span>电商视觉素材,提升内容制作效率</span>
</div>
</div>
</div>
<!-- 双 Hero 卡片 -->
<div class="factory-hero">
<!-- 卡片 A · 模特上身图 -->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ MODEL · TRY-ON ]</span>
<div class="factory-title">模特上身图</div>
<div class="factory-desc">选择模特,AI 生成商品模特上身效果图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" 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>
</span>
支持多模特选择
</li>
<li>
<span class="ff-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="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一次生成 4 张
</li>
<li>
<span class="ff-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-4M3 17l9 4 9-4"/></svg>
</span>
支持多商品并行
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.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="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.30 / 张 ]</span>
</div>
</div>
<div class="factory-visual model-visual">
<div class="placeholder main"><span class="ph-frame">Ava · 9:16</span></div>
<div class="stack">
<div class="placeholder"><span class="ph-frame">变体 01</span></div>
<div class="placeholder"><span class="ph-frame">变体 02</span></div>
<div class="placeholder"><span class="ph-frame">变体 03</span></div>
</div>
</div>
</div>
</div>
<!-- 卡片 B · 平台套图 -->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ PLATFORM · KIT ]</span>
<div class="factory-title">平台套图</div>
<div class="factory-desc">选择平台模板,AI 生成电商平台套图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9M9 21V5M15 21v-8M21 21V11"/></svg>
</span>
覆盖主流电商平台
</li>
<li>
<span class="ff-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="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一键生成 4 张套图
</li>
<li>
<span class="ff-ic">
<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"/></svg>
</span>
智能排版设计
</li>
</ul>
<div class="platform-row">
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
</div>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="platform-cover.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="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.50 / 张 ]</span>
</div>
</div>
<div class="factory-visual kit-visual">
<div class="placeholder"><span class="ph-frame">套图 / TB</span></div>
<div class="placeholder"><span class="ph-frame">套图 / DY</span></div>
<div class="placeholder"><span class="ph-frame">套图 / XHS</span></div>
<div class="placeholder"><span class="ph-frame">套图 / PDD</span></div>
</div>
</div>
</div>
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
<div class="factory-title">图片优化</div>
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5"/></svg>
</span>
人物 · 商品 全支持
</li>
<li>
<span class="ff-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="3" width="6" height="18"/><rect x="9" y="3" width="6" height="18"/><rect x="15" y="3" width="6" height="18"/></svg>
</span>
正面 / 侧面 / 背面 一次输出
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
</span>
多镜头一致性保证
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
开始生成
<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="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.40 / 组 ]</span>
</div>
</div>
<div class="factory-visual tri-visual">
<div class="placeholder"><span class="ph-frame">正面</span></div>
<div class="placeholder"><span class="ph-frame">侧面</span></div>
<div class="placeholder"><span class="ph-frame">背面</span></div>
</div>
</div>
</div>
</div>
<!-- ============= 任务中心 · 参考 projects.html 布局 ============= -->
<div class="section-h">
<h2>任务中心</h2>
<span class="sub-mono">// <span id="tc-sub-total">0</span> 个 · <span id="tc-sub-gen">0</span> 生成中 · <span id="tc-sub-ok">0</span> 已完成 · <span id="tc-sub-err">0</span> 失败</span>
</div>
<!-- 状态 tabs (复用 .tabs) -->
<div class="tabs" id="tc-tabs">
<div class="tab active" data-filter="all">全部 <span class="count">0</span></div>
<div class="tab" data-filter="gen">生成中 <span class="count">0</span></div>
<div class="tab" data-filter="ok">已完成 <span class="count">0</span></div>
<div class="tab" data-filter="err">失败 <span class="count">0</span></div>
</div>
<!-- toolbar: search + 类型 chip + clear + view-toggle -->
<div class="toolbar">
<div class="search-inline">
<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>
<input class="input" id="tc-search" placeholder="搜索任务名">
</div>
<div class="chip-wrap" data-key="type">
<button class="chip" type="button"><span class="chip-label">任务类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<button class="clear-filters" id="tc-clear" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
<span class="spacer"></span>
<div class="view-toggle" id="tc-view-toggle">
<button type="button" data-view="grid">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="5" height="5"/><rect x="9" y="2" width="5" height="5"/><rect x="2" y="9" width="5" height="5"/><rect x="9" y="9" width="5" height="5"/></svg>
网格
</button>
<button type="button" class="active" data-view="list">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
列表
</button>
</div>
</div>
<div class="result-meta" id="tc-result-meta">// 显示 <span class="count">0</span> / 0 个任务</div>
<!-- ============= LIST VIEW (默认) ============= -->
<div id="task-list-view">
<table class="t">
<thead>
<tr>
<th style="width:32%">任务</th>
<th>类型</th>
<th style="width:140px">进度</th>
<th>状态</th>
<th style="width:120px">创建于</th>
<th style="width:48px"></th>
</tr>
</thead>
<tbody id="task-list-tbody"><!-- JS 从卡片同步生成 --></tbody>
</table>
</div>
<!-- ============= GRID VIEW ============= -->
<div class="history-grid" id="task-grid" hidden>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
<div class="history-body">
<div class="history-name">补水面膜 × Ava</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:30</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
<div class="history-body">
<div class="history-name">精华液 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:25</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
<div class="history-body">
<div class="history-name">防晒霜 × Luna</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:20</span>
<span class="pill info"><span class="dot"></span>生成中</span>
</div>
<div class="history-prog">
<div class="bar"><span style="width:65%"></span></div>
<span class="pct">65%</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
<div class="history-body">
<div class="history-name">面霜 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:15</span>
<span class="pill err"><span class="dot"></span>失败</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
<div class="history-body">
<div class="history-name">口红 × Mia</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 21:08</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
<div class="history-body">
<div class="history-name">眼霜 × 平台套图</div>
<div class="history-type">平台套图 · 8 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 18:42</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
<div class="history-body">
<div class="history-name">瑜伽裤 × Zoe</div>
<div class="history-type">模特上身图 · 12 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 11:20</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
<div class="history-body">
<div class="history-name">咖啡粉 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.17 · 16:50</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 删除确认 modal (复用 projects.html 风格) ===== -->
<div class="modal-bg" id="tc-del-bg">
<div class="modal" role="dialog">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
</div>
<div class="ti">确认删除任务<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="tc-del-body">即将删除任务记录。</div>
<div class="modal-f">
<button class="btn" type="button" id="tc-del-cancel">取消</button>
<button class="btn" type="button" id="tc-del-ok" style="background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
确认删除
</button>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script src="assets/new-product-drawer.js"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]
});
/* ============================================================
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
============================================================ */
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
const gridView = document.getElementById('task-grid');
const listView = document.getElementById('task-list-view');
const cards = [...taskGrid.querySelectorAll('.task-card')];
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
function goToWorkbench(type, name) {
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
const url = (type === 'model') ? 'model-photo.html' + q
: (type === 'platform') ? 'platform-cover.html' + q
: 'projects-new.html' + q;
location.href = url;
}
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
function rowFor(card) {
const name = card.dataset.name;
const type = card.dataset.type;
const status = card.dataset.status;
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
const pillClass = STATUS_PILL[status] || 'info';
const pillLabel = STATUS_LABEL[status] || status;
const tr = document.createElement('tr');
tr.dataset.taskRow = '1';
tr.dataset.name = name;
tr.dataset.type = type;
tr.dataset.status = status;
tr.addEventListener('click', () => goToWorkbench(type, name));
tr.innerHTML = `
<td>
<div class="task-name-cell">
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
<div>
<div class="task-name">${esc(name)}</div>
<div class="task-sub">${esc(subText)}</div>
</div>
</div>
</td>
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
<td>${status === 'gen'
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;"></span>')}</td>
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
<td class="muted-2">${esc(timeText)}</td>
<td>
<div class="row-action">
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
<div class="row-more-tip"><button class="mi mi-del-task" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>删除任务</button></div>
</span>
</div>
</td>
`;
// 阻止 row-more 区域冒泡触发整行点击
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
return tr;
}
cards.forEach(card => {
const tr = rowFor(card);
listTbody.appendChild(tr);
card._listRow = tr;
tr._card = card;
});
/* ---------- 2. 构建类型 chip 菜单 ---------- */
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
function syncTypeChip() {
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
if (state.type === 'all') {
label.textContent = '任务类型';
chip.classList.remove('active');
} else {
label.textContent = TYPE_LABEL[state.type] || state.type;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
}
/* ---------- 3. applyFilter ---------- */
function applyFilter() {
const q = state.search.toLowerCase();
let visible = 0;
cards.forEach(card => {
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
const okType = state.type === 'all' || card.dataset.type === state.type;
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okSearch;
card.style.display = show ? '' : 'none';
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
if (show) visible++;
});
// 计数
const counts = { all: cards.length, gen: 0, ok: 0, err: 0 };
cards.forEach(c => { if (counts[c.dataset.status] !== undefined) counts[c.dataset.status]++; });
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
const f = t.dataset.filter;
t.querySelector('.count').textContent = f === 'all' ? counts.all : counts[f];
});
document.getElementById('tc-sub-total').textContent = counts.all;
document.getElementById('tc-sub-gen').textContent = counts.gen;
document.getElementById('tc-sub-ok').textContent = counts.ok;
document.getElementById('tc-sub-err').textContent = counts.err;
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
}
/* ---------- 4. 事件绑定 ---------- */
// status tabs
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('#tc-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.filter = t.dataset.filter;
applyFilter();
});
});
// search
document.getElementById('tc-search').addEventListener('input', e => {
state.search = e.target.value.trim();
applyFilter();
});
// type chip
const typeWrap = document.querySelector('.chip-wrap[data-key="type"]');
typeWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = typeWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) typeWrap.classList.add('open');
});
typeMenu.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state.type = mi.dataset.value;
typeWrap.classList.remove('open');
syncTypeChip();
applyFilter();
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// clear filters
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
applyFilter();
Shell.toast('已清空筛选');
});
// view toggle
document.querySelectorAll('#tc-view-toggle button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#tc-view-toggle button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
state.view = b.dataset.view;
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
else { listView.hidden = true; gridView.hidden = false; }
});
});
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
const delBg = document.getElementById('tc-del-bg');
const delBody = document.getElementById('tc-del-body');
const delCancel = document.getElementById('tc-del-cancel');
const delOk = document.getElementById('tc-del-ok');
let _delTarget = null;
function openDelConfirm(target) {
_delTarget = target;
const name = target.dataset.name || '该任务';
delBody.innerHTML = '即将删除任务 <span class="mono-acc">' + esc(name) + '</span>。任务记录将清除,已入库的素材不受影响。';
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delTarget = null; }
delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
if (!_delTarget) return;
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
const row = card._listRow;
const name = card.dataset.name;
card.remove();
if (row) row.remove();
// 同步 cards 数组
const idx = cards.indexOf(card);
if (idx >= 0) cards.splice(idx, 1);
closeDelConfirm();
Shell.toast('已删除', name);
applyFilter();
});
// 绑定网格卡 删除按钮
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.task-card');
if (card) openDelConfirm(card);
});
});
// 绑定网格卡片点击 → 跳工作台
cards.forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', e => {
if (e.target.closest('.card-del-btn')) return;
goToWorkbench(card.dataset.type, card.dataset.name);
});
});
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
listTbody.addEventListener('click', e => {
const btn = e.target.closest('.mi-del-task');
if (!btn) return;
e.stopPropagation();
const tr = btn.closest('tr');
if (tr) openDelConfirm(tr);
});
/* ---------- 6. 初始化 ---------- */
applyFilter();
})();
</script>
</body>
</html>

View File

@ -0,0 +1,578 @@
/* ============================================================
新建商品 · 共享 Drawer 模块
----------------------------------------------------------
在任意页面只需 <script src="assets/new-product-drawer.js"> 引入,
然后调用 NewProductDrawer.open({ onSave: fn }) 即可在当前页之上
弹出右侧 Drawer点击遮罩 / X / 取消 / ESC 关闭后,用户停在原页面
提供:
window.NewProductDrawer.open(opts?)
window.NewProductDrawer.close()
opts 字段:
onSave(product) 保存时回调,product = { id, name, cat, target,
points: string[], images: { id, dataUrl, name }[] }
============================================================ */
(function () {
'use strict';
if (window.NewProductDrawer) return; // idempotent
const DRAWER_ID = 'npd-drawer';
const DRAWER_BG_ID = 'npd-drawer-bg';
/* ---------- 注入样式(独立 namespace 以免与 products.html 冲突) ---------- */
const CSS = `
/* drawer base (相同尺寸/动画) */
#${DRAWER_BG_ID} {
position: fixed; inset: 0;
background: rgba(21, 20, 15, .32);
display: none; z-index: 1100;
}
#${DRAWER_BG_ID}.show { display: block; }
#${DRAWER_ID} {
position: fixed; right: 0; top: 0; bottom: 0;
width: 820px; max-width: 100vw;
background: var(--surface);
border-left: 1px solid var(--border-faint);
z-index: 1101;
transform: translateX(100%);
transition: transform .25s cubic-bezier(.32, .72, 0, 1);
display: flex; flex-direction: column;
box-shadow: -4px 0 24px rgba(21, 20, 15, .04);
}
#${DRAWER_ID}.show { transform: translateX(0); }
#${DRAWER_ID} .drawer-h {
padding: 20px 24px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center;
}
#${DRAWER_ID} .drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
#${DRAWER_ID} .drawer-h .x {
margin-left: auto; width: 32px; height: 32px;
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56); cursor: pointer;
background: transparent; border: 0;
transition: background var(--t-base);
}
#${DRAWER_ID} .drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
#${DRAWER_ID} .drawer-b { padding: 24px 28px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
#${DRAWER_ID} .drawer-f {
padding: 14px 24px;
border-top: 1px solid var(--border-faint);
display: flex; gap: 10px; align-items: center;
background: var(--surface);
}
#${DRAWER_ID} .drawer-f .btn-guide {
margin-right: auto;
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--black-alpha-56);
background: transparent; border: 0; cursor: pointer;
padding: 8px 10px; border-radius: var(--r-md);
font-family: inherit;
transition: background var(--t-base), color var(--t-base);
}
#${DRAWER_ID} .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
#${DRAWER_ID} .drawer-f .btn-guide svg { width: 14px; height: 14px; }
/* form-card */
#${DRAWER_ID} .form-h {
font-size: 15px; font-weight: 600; color: var(--accent-black);
margin-bottom: 18px; padding-bottom: 12px;
border-bottom: 1px solid var(--border-faint);
}
#${DRAWER_ID} .field { margin-bottom: 16px; }
#${DRAWER_ID} .field:last-child { margin-bottom: 0; }
#${DRAWER_ID} .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
#${DRAWER_ID} .field-label {
display: block; font-size: 13px; font-weight: 500;
color: var(--accent-black); margin-bottom: 6px;
}
#${DRAWER_ID} .field-label .req { color: var(--heat); margin-left: 2px; }
#${DRAWER_ID} .field-label .opt {
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
}
#${DRAWER_ID} .input,
#${DRAWER_ID} .select {
width: 100%; height: 38px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 14px;
font-size: 13.5px; color: var(--accent-black);
outline: none; font-family: inherit;
transition: border-color var(--t-base);
}
#${DRAWER_ID} .input:focus,
#${DRAWER_ID} .select:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
/* upload */
#${DRAWER_ID} .pf-upload-row {
display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 16px; align-items: stretch;
}
#${DRAWER_ID} .pf-upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 28px 20px;
background: var(--background-lighter);
cursor: pointer; text-align: center;
transition: border-color var(--t-base), background var(--t-base);
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 180px;
}
#${DRAWER_ID} .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
#${DRAWER_ID} .pf-upload-zone .uz-ic {
width: 44px; height: 44px;
margin: 0 auto 10px;
background: var(--surface);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
color: var(--heat);
display: grid; place-items: center;
}
#${DRAWER_ID} .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
#${DRAWER_ID} .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
#${DRAWER_ID} .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
#${DRAWER_ID} .pf-upload-zone .uz-d {
margin-top: 8px;
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
#${DRAWER_ID} .pf-example {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
}
#${DRAWER_ID} .pf-example .ex-h { font-size: 13px; font-weight: 600; color: var(--accent-black); }
#${DRAWER_ID} .pf-example .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb {
aspect-ratio: 1;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
display: grid; place-items: center;
color: var(--black-alpha-32);
}
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb::after {
content: ''; position: absolute; inset: 0;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
pointer-events: none;
}
#${DRAWER_ID} .pf-example .ex-d { font-size: 12px; color: var(--black-alpha-56); line-height: 1.5; }
#${DRAWER_ID} .pf-grid {
display: grid; grid-template-columns: repeat(5, 1fr);
gap: 8px; margin-top: 12px;
}
#${DRAWER_ID} .pf-grid:empty { display: none; }
#${DRAWER_ID} .pf-thumb {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative; overflow: hidden; cursor: pointer;
}
#${DRAWER_ID} .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
#${DRAWER_ID} .pf-thumb .pf-x {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px;
background: rgba(0,0,0,.7); color: var(--accent-white);
border: 0; border-radius: 50%; cursor: pointer;
display: grid; place-items: center;
opacity: 0; transition: opacity var(--t-base);
}
#${DRAWER_ID} .pf-thumb:hover .pf-x { opacity: 1; }
#${DRAWER_ID} .pf-thumb .pf-x svg { width: 11px; height: 11px; }
/* bullet-list */
#${DRAWER_ID} .bullet-list { list-style: none; padding: 0; margin: 0; }
#${DRAWER_ID} .bullet-list .bl-item,
#${DRAWER_ID} .bullet-list .bl-add {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13.5px;
}
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
#${DRAWER_ID} .bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 11px; color: var(--heat); font-weight: 700;
display: grid; place-items: center; flex-shrink: 0;
}
#${DRAWER_ID} .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
#${DRAWER_ID} .bullet-list .bl-input {
flex: 1; background: transparent; border: 0; outline: none;
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
}
#${DRAWER_ID} .bullet-list .bl-x {
width: 22px; height: 22px;
color: var(--black-alpha-48);
cursor: pointer; display: grid; place-items: center;
border-radius: var(--r-sm);
transition: color var(--t-base), background var(--t-base);
}
#${DRAWER_ID} .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
#${DRAWER_ID} .bullet-list .bl-x svg { width: 11px; height: 11px; }
@media (max-width: 900px) {
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
}
`;
const HTML = `
<div id="${DRAWER_BG_ID}"></div>
<aside id="${DRAWER_ID}" role="dialog" aria-label="新建商品" aria-hidden="true">
<div class="drawer-h">
<h3>新建商品</h3>
<button class="x" type="button" data-act="close" aria-label="关闭">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
<div class="drawer-b">
<div class="form-card">
<div class="form-h">基础信息</div>
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" data-f="name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="field-row">
<div>
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" data-f="cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
<input class="input" data-f="target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="field">
<label class="field-label">商品主图<span class="req">*</span></label>
<input type="file" data-f="file" accept="image/*" multiple hidden>
<div class="pf-upload-row">
<div class="pf-upload-zone" data-act="upload-zone">
<div class="uz-ic">
<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-4M17 8l-5-5-5 5M12 3v12"/></svg>
</div>
<div class="uz-t">点击上传或<strong>拖拽图片</strong></div>
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
</div>
<div class="pf-example">
<div class="ex-h">示例图</div>
<div class="ex-grid">
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
</div>
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
</div>
</div>
<div class="pf-grid" data-f="grid"></div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" data-f="bullets">
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder=" · "></li>
</ul>
</div>
</div>
</div>
<div class="drawer-f">
<button class="btn-guide" type="button" data-act="guide">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
使用指南
</button>
<button class="btn" type="button" data-act="cancel">取消</button>
<button class="btn btn-primary" type="button" data-act="save">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
创建商品
</button>
</div>
</aside>
`;
/* ---------- DOM refs (populated by ensureInjected) ---------- */
let injected = false;
let bg, drawer, $f, $grid, $bullets, $blInput;
let currentOpts = {};
const PF_MAX = 5;
let pfFiles = []; // { id, dataUrl, name }
const blXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
function esc(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]); }
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
function toast(msg, sub) {
if (typeof Shell !== 'undefined' && Shell && Shell.toast) Shell.toast(msg, sub);
}
function ensureInjected() {
if (injected) return;
// style
const styleEl = document.createElement('style');
styleEl.textContent = CSS;
document.head.appendChild(styleEl);
// html
const wrap = document.createElement('div');
wrap.innerHTML = HTML;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
$f = {
name: drawer.querySelector('[data-f="name"]'),
cat: drawer.querySelector('[data-f="cat"]'),
target: drawer.querySelector('[data-f="target"]'),
file: drawer.querySelector('[data-f="file"]'),
};
$grid = drawer.querySelector('[data-f="grid"]');
$bullets = drawer.querySelector('[data-f="bullets"]');
$blInput = $bullets.querySelector('.bl-add .bl-input');
bindEvents();
injected = true;
}
function bindEvents() {
// 关闭交互
bg.addEventListener('click', close);
drawer.addEventListener('click', e => {
const a = e.target.closest('[data-act]');
if (!a) return;
const act = a.dataset.act;
if (act === 'close') return close();
if (act === 'cancel') return close();
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
if (act === 'save') return save();
if (act === 'upload-zone') return openFilePicker();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
});
// 上传
$f.file.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; });
const zone = drawer.querySelector('[data-act="upload-zone"]');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
zone.addEventListener('drop', e => {
e.preventDefault(); zone.style.borderColor = '';
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
});
// 卖点 bullet-list
$blInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
});
}
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
function pfRender() {
$grid.innerHTML = pfFiles.map(u => `
<div class="pf-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${esc(u.name)}">
<button class="pf-x" type="button" title="删除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
`).join('');
$grid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
b.onclick = e => {
e.stopPropagation();
const id = b.closest('.pf-thumb').dataset.id;
const i = pfFiles.findIndex(f => f.id === id);
if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }
};
});
}
function addFiles(fileList) {
const room = PF_MAX - pfFiles.length;
if (room <= 0) { toast('已达上限', PF_MAX + ' / ' + PF_MAX + ' 张'); return; }
const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);
let done = 0;
incoming.forEach(f => {
const r = new FileReader();
r.onload = e => {
pfFiles.push({ id: uid(), dataUrl: e.target.result, name: f.name });
if (++done === incoming.length) {
pfRender();
toast('已上传', '+ ' + done + ' 张 · 共 ' + pfFiles.length + ' / ' + PF_MAX);
}
};
r.readAsDataURL(f);
});
}
function blRenumber() {
[...$bullets.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
}
function blAdd(text) {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = '<span class="num">0</span><span class="bl-text">' + esc(t) + '</span><span class="bl-x" title="删除">' + blXSvg + '</span>';
$bullets.querySelector('.bl-add').before(li);
li.querySelector('.bl-x').addEventListener('click', () => {
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); blRenumber(); }, 150);
});
blRenumber();
}
function getBullets() {
return [...$bullets.querySelectorAll('.bl-item .bl-text')].map(t => t.textContent.trim()).filter(Boolean);
}
/* ---------- API ---------- */
function resetForm() {
$f.name.value = '';
$f.cat.value = $f.cat.options[0].value;
$f.target.value = '';
pfFiles = [];
pfRender();
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
$blInput.value = '';
}
function lockBody() {
// 优先用 Shell 的引用计数实现(避免多 overlay 互相解锁)
if (typeof Shell !== 'undefined' && Shell && typeof Shell.lockScroll === 'function') {
Shell.lockScroll();
return;
}
// 兜底: Shell 未加载时本地锁
const docEl = document.documentElement;
const sbw = window.innerWidth - docEl.clientWidth;
drawer._lockSnap = {
bodyOverflow: document.body.style.overflow,
bodyPaddingRight: document.body.style.paddingRight,
htmlOverflow: docEl.style.overflow,
};
document.body.style.overflow = 'hidden';
docEl.style.overflow = 'hidden';
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
}
function unlockBody() {
if (typeof Shell !== 'undefined' && Shell && typeof Shell.unlockScroll === 'function') {
Shell.unlockScroll();
return;
}
const s = drawer._lockSnap;
if (s) {
document.body.style.overflow = s.bodyOverflow;
document.body.style.paddingRight = s.bodyPaddingRight;
document.documentElement.style.overflow = s.htmlOverflow;
drawer._lockSnap = null;
}
}
function open(opts) {
ensureInjected();
if (drawer.classList.contains('show')) return; // 已开则不重复锁
currentOpts = opts || {};
resetForm();
bg.classList.add('show');
drawer.classList.add('show');
drawer.setAttribute('aria-hidden', 'false');
lockBody();
setTimeout(() => $f.name.focus(), 280);
}
function close() {
if (!injected) return;
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
bg.classList.remove('show');
drawer.classList.remove('show');
drawer.setAttribute('aria-hidden', 'true');
unlockBody();
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
}
function save() {
const name = ($f.name.value || '').trim();
const cat = $f.cat.value;
const target = ($f.target.value || '').trim();
const points = getBullets();
const images = pfFiles.slice();
if (!name) {
toast('请填写商品名称');
$f.name.focus();
return;
}
if (images.length === 0) {
toast('请上传商品主图', '至少 1 张');
return;
}
if (points.length === 0) {
toast('请添加核心卖点', '至少 1 条');
$blInput.focus();
return;
}
const product = {
id: 'np-' + uid(),
name, cat, target,
points,
images,
imgs: images.length,
};
toast('商品已创建', '+ ' + name);
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
close();
}
window.NewProductDrawer = { open, close };
/* ---------- sessionStorage 自动打开钩子 ---------- */
// 任何页面只要在跳转前 sessionStorage.setItem('npd-auto-open','1') 即可,
// 落地页加载完模块后,会自动 open() 一次并清掉 flag。
// 用于:product-create.html 重定向后让落地页弹出 drawer,而不是用户重新点击。
function checkAutoOpen() {
try {
if (sessionStorage.getItem('npd-auto-open') === '1') {
sessionStorage.removeItem('npd-auto-open');
// 延后一拍,确保宿主页面自己的 init 已经跑完
setTimeout(() => open(), 50);
}
} catch (e) { /* sessionStorage 不可用就静默放弃 */ }
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAutoOpen);
} else {
checkAutoOpen();
}
})();

View File

@ -276,7 +276,7 @@ nav a.disabled:hover { background: transparent; color: var(--black-alpha-32); }
.user .em { font-size: 13px; color: var(--accent-black); }
/* ─── Main + grid background ─── */
main { position: relative; overflow: hidden; background: var(--background-base); }
main { position: relative; background: var(--background-base); min-width: 0; }
.grid-bg {
position: absolute; inset: 0; pointer-events: none;
background-image:
@ -308,7 +308,7 @@ main { position: relative; overflow: hidden; background: var(--background-base);
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
background: var(--background-base);
position: relative; z-index: 2;
position: sticky; top: 0; z-index: 50;
}
.crumbs { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--black-alpha-48); }
.crumbs .sep { color: var(--black-alpha-32); }
@ -348,14 +348,70 @@ main { position: relative; overflow: hidden; background: var(--background-base);
width: 7px; height: 7px; border-radius: 50%;
background: var(--heat); border: 1.5px solid var(--surface);
}
.icon-btn .count-noti {
position: absolute; top: -4px; right: -4px;
min-width: 16px; height: 16px; padding: 0 4px;
display: inline-flex; align-items: center; justify-content: center;
background: var(--accent-crimson); color: var(--accent-white);
border: 1.5px solid var(--surface);
border-radius: var(--r-pill);
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
letter-spacing: .02em; line-height: 1;
pointer-events: none;
}
/* ─── Topbar · 任务队列 chip ─── */
.queue-chip {
display: inline-flex; align-items: center; gap: 7px;
height: 36px;
padding: 0 8px 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
font-size: 13px;
color: var(--accent-black);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.queue-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
.queue-chip svg { width: 14px; height: 14px; 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;
background: var(--heat-12); color: var(--heat);
border: 1px solid var(--heat-20);
border-radius: var(--r-pill);
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
letter-spacing: .02em;
}
/* ─── Topbar · 头像 ─── */
.topbar-avatar {
width: 36px; height: 36px;
border-radius: var(--r-pill);
background: var(--accent-black);
color: var(--accent-white);
display: inline-flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 600;
cursor: pointer;
border: 1px solid var(--border-faint);
transition: transform var(--t-fast), box-shadow var(--t-base);
flex-shrink: 0;
overflow: hidden;
}
.topbar-avatar:hover {
transform: scale(1.04);
box-shadow: 0 0 0 3px var(--heat-12);
}
.topbar-avatar img {
width: 100%; height: 100%; object-fit: cover;
}
/* ─── Content ─── */
.content {
padding: 48px 56px 72px;
padding: 48px 28px 72px;
position: relative;
z-index: 1;
max-width: 1480px;
margin: 0 auto;
min-height: calc(100vh - 64px);
}
@ -633,6 +689,454 @@ main { position: relative; overflow: hidden; background: var(--background-base);
}
.toolbar .search-inline input { padding-left: 36px; }
/* ─── Chip dropdown · 共享组件 ─── */
.chip-wrap { position: relative; display: inline-flex; }
.chip-wrap .chip svg.caret { transition: transform .15s; }
.chip-wrap.open .chip svg.caret { transform: rotate(180deg); }
.chip-menu {
position: absolute; top: calc(100% + 4px); left: 0;
min-width: 200px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.08);
padding: 4px;
z-index: 50;
display: none;
max-height: 320px;
overflow-y: auto;
}
.chip-wrap.open .chip-menu { display: block; }
.chip-menu.align-right { left: auto; right: 0; }
.chip-menu .mi {
height: 32px; padding: 0 10px;
font-size: 13px; color: var(--accent-black);
display: flex; align-items: center; gap: 8px;
border-radius: var(--r-sm);
cursor: pointer;
white-space: nowrap;
}
.chip-menu .mi:hover { background: var(--black-alpha-4); }
.chip-menu .mi.selected { color: var(--heat); background: var(--heat-12); }
.chip-menu .mi .mi-check { width: 13px; height: 13px; color: var(--heat); opacity: 0; flex-shrink: 0; }
.chip-menu .mi.selected .mi-check { opacity: 1; }
.chip-menu .mi-sep { height: 1px; background: var(--border-faint); margin: 4px 6px; }
/* ─── Clear-filters btn · 共享组件 ─── */
.clear-filters {
height: 36px; padding: 0 12px;
background: transparent; border: 0; border-radius: var(--r-md);
color: var(--black-alpha-56); font-size: 13px; font-family: inherit;
display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.clear-filters:hover { background: var(--black-alpha-4); color: var(--heat); }
.clear-filters svg { width: 13px; height: 13px; }
.clear-filters[hidden] { display: none; }
/* ─── Result count · 共享组件 ─── */
.result-meta {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-bottom: 14px;
letter-spacing: .04em;
}
.result-meta .count { color: var(--heat); font-weight: 600; }
/* ─── 新建商品 Modal (Upload Form · 居中弹窗) ─── */
.new-product-modal {
max-width: 1080px !important;
width: 94%;
max-height: 90vh;
display: flex;
flex-direction: column;
}
/* 左图右文 双栏 · 等宽 */
.np-body-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
align-items: start;
}
.np-left, .np-right { min-width: 0; }
@media (max-width: 880px) {
.np-body-grid { grid-template-columns: 1fr; gap: 20px; }
}
.np-header {
padding: 18px 24px 16px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 14px;
flex-shrink: 0;
}
.np-header .np-title-ic {
width: 36px; height: 36px;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
display: grid; place-items: center;
flex-shrink: 0;
}
.np-header .np-title-ic svg { width: 17px; height: 17px; }
.np-header h2 {
font-size: 16px; font-weight: 600; color: var(--accent-black);
margin: 0;
}
.np-header .np-mode-pill {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--heat);
background: var(--heat-12);
border: 1px solid var(--heat-20);
border-radius: var(--r-sm);
padding: 2px 8px;
letter-spacing: .04em;
font-weight: 500;
}
.np-header .np-x {
margin-left: auto;
width: 32px; height: 32px;
display: grid; place-items: center;
background: transparent; border: 0;
border-radius: var(--r-md);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.np-header .np-x:hover { background: var(--crimson-bg); color: var(--accent-crimson); }
.np-header .np-x svg { width: 14px; height: 14px; }
.np-body {
flex: 1;
overflow-y: auto;
padding: 22px 24px;
}
/* AI CTA · 独立全宽 CTA 区域 · 放在"商品图册"上方 */
.np-ai-cta {
display: flex; align-items: center; gap: 10px;
width: 100%;
height: 40px;
padding: 0 14px;
margin-bottom: 14px;
background: transparent;
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 13px;
color: var(--black-alpha-72);
cursor: pointer;
text-decoration: none;
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.np-ai-cta:hover { background: var(--heat-8); border-color: var(--heat); color: var(--accent-black); }
.np-ai-cta .ai-icon { color: var(--heat); display: grid; place-items: center; flex-shrink: 0; }
.np-ai-cta .ai-icon svg { width: 14px; height: 14px; }
.np-ai-cta .ai-label { font-weight: 500; color: var(--accent-black); flex: 1; }
.np-ai-cta .ai-arrow { color: var(--black-alpha-48); transition: transform var(--t-fast), color var(--t-base); flex-shrink: 0; display: grid; place-items: center; }
.np-ai-cta .ai-arrow svg { width: 14px; height: 14px; }
.np-ai-cta:hover .ai-arrow { color: var(--heat); transform: translateX(2px); }
/* 主推态: 用户还没上传图 → 醒目橙色描边 */
.np-ai-cta.primary { border-color: var(--heat-40); background: var(--heat-8); }
.np-ai-cta.primary .ai-label { color: var(--heat); font-weight: 600; }
.np-ai-cta.primary .ai-arrow { color: var(--heat); }
.np-ai-cta.primary:hover { background: var(--heat-12); border-color: var(--heat); }
/* Footer (sticky inside modal) */
.np-footer {
border-top: 1px solid var(--border-faint);
padding: 14px 24px;
display: flex; align-items: center; gap: 12px;
flex-shrink: 0;
background: var(--background-lighter);
}
.np-footer .np-meta {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-right: auto;
}
.np-footer .np-meta .accent { color: var(--heat); font-weight: 600; }
/* Upload zone · 弹窗内尺寸略缩 */
.np-body .upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 22px 20px;
text-align: center;
background: var(--background-lighter);
color: var(--black-alpha-56);
font-size: 13px;
cursor: pointer;
display: flex; flex-direction: column; align-items: center; gap: 4px;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.np-body .upload-zone:hover { border-color: var(--heat); background: var(--heat-8); color: var(--heat); }
.np-body .upload-zone:hover .uz-ic { background: var(--heat); color: #fff; border-color: var(--heat); }
.np-body .upload-zone strong { color: var(--heat); font-weight: 600; }
.np-body .upload-zone .uz-ic {
width: 40px; height: 40px;
border-radius: var(--r-md);
background: var(--surface);
color: var(--heat);
border: 1px solid var(--heat-20);
display: grid; place-items: center;
margin-bottom: 8px;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.np-body .upload-zone .uz-ic svg { width: 18px; height: 18px; }
.np-body .upload-zone .uz-hint { display: block; margin-top: 2px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* 上传子区域标题 (图片案例 / 我的上传) */
.np-section-h {
display: flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 500;
color: var(--accent-black);
margin: 12px 0 4px;
}
.np-section-h .check-ic {
width: 14px; height: 14px;
border-radius: 50%;
background: var(--accent-forest);
color: #fff;
display: grid; place-items: center;
flex-shrink: 0;
}
.np-section-h .check-ic svg { width: 8px; height: 8px; }
.np-section-h .counter {
margin-left: auto;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
font-weight: 400;
}
.np-section-h .counter .num { color: var(--heat); font-weight: 600; }
.np-section-sub {
font-size: 11.5px;
color: var(--black-alpha-56);
margin-bottom: 8px;
line-height: 1.5;
}
/* 图片案例 grid (静态展示) */
.np-examples {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
}
.np-examples .ex {
aspect-ratio: 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
position: relative;
display: grid;
place-items: center;
font-size: 9.5px;
color: var(--black-alpha-48);
font-family: var(--font-mono);
letter-spacing: .04em;
text-align: center;
padding: 4px;
overflow: hidden;
}
.np-examples .ex-badge {
position: absolute;
bottom: 3px; right: 3px;
width: 15px; height: 15px;
background: var(--accent-forest);
color: #fff;
border-radius: 50%;
display: grid; place-items: center;
z-index: 2;
}
.np-examples .ex-badge svg { width: 9px; height: 9px; }
/* 我的上传 grid · 始终 5 列, 含填充和空插槽 */
.np-body .upload-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
}
.up-slot-empty {
aspect-ratio: 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
cursor: pointer;
display: grid;
place-items: center;
font-size: 9.5px;
color: var(--black-alpha-32);
font-family: var(--font-mono);
letter-spacing: .04em;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.up-slot-empty:hover {
border-color: var(--heat);
background: var(--heat-8);
color: var(--heat);
}
/* dropzone 满 5 张时禁用态 */
.np-body .upload-zone.full {
cursor: not-allowed;
opacity: .55;
pointer-events: none;
}
.np-body .upload-zone.full .uz-ic { background: var(--background-lighter); color: var(--black-alpha-32); border-color: var(--border-faint); }
.up-thumb {
aspect-ratio: 1;
border-radius: var(--r-md);
overflow: hidden;
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
cursor: zoom-in;
transition: transform var(--t-fast), border-color var(--t-base);
}
.up-thumb:hover { transform: scale(1.02); border-color: var(--heat); }
.up-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.up-thumb .slot-x {
position: absolute;
top: 3px; right: 3px;
width: 18px; height: 18px;
background: rgba(0, 0, 0, .7);
color: #fff;
border-radius: 50%;
display: none;
place-items: center;
cursor: pointer;
z-index: 3;
border: 0;
padding: 0;
transition: background var(--t-base), transform var(--t-fast);
}
.up-thumb:hover .slot-x { display: grid; }
.up-thumb .slot-x:hover { background: var(--accent-crimson); transform: scale(1.1); }
.up-thumb .slot-x svg { width: 9px; height: 9px; }
/* 5 列尺寸太小,文件名 overlay 仅在 lightbox 显示,缩略图上隐藏 */
.up-thumb .slot-name { display: none; }
/* Lightbox: 点击缩略图全屏预览 */
.np-lightbox {
position: fixed; inset: 0;
background: rgba(0, 0, 0, .9);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: zoom-out;
opacity: 0;
transition: opacity .2s;
}
.np-lightbox.show { display: flex; opacity: 1; }
.np-lightbox img {
max-width: 90vw;
max-height: 88vh;
border-radius: var(--r-md);
box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
}
.np-lightbox .lb-x {
position: fixed;
top: 24px; right: 24px;
width: 44px; height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, .12);
color: #fff;
border: 0;
cursor: pointer;
display: grid;
place-items: center;
transition: background var(--t-base);
}
.np-lightbox .lb-x:hover { background: rgba(255, 255, 255, .24); }
.np-lightbox .lb-x svg { width: 18px; height: 18px; }
.np-lightbox .lb-name {
position: fixed;
bottom: 24px; left: 50%;
transform: translateX(-50%);
font-family: var(--font-mono);
font-size: 12px;
color: rgba(255, 255, 255, .7);
letter-spacing: .04em;
}
/* Bullet list · 弹窗内 (复用样式) */
.np-body .bullet-list { list-style: none; padding: 0; }
.np-body .bullet-list li {
display: flex; gap: 10px; align-items: center;
padding: 10px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
margin-bottom: 6px;
font-size: 13px;
color: var(--accent-black);
transition: border-color var(--t-base), background var(--t-base);
}
.np-body .bullet-list li.bl-item:hover { border-color: var(--black-alpha-24); }
.np-body .bullet-list li.bl-add { background: var(--surface); border-style: dashed; }
.np-body .bullet-list li.bl-add:focus-within { border-color: var(--heat-40); }
.np-body .bullet-list .num {
width: 20px; height: 20px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 11px; color: var(--black-alpha-56);
display: grid; place-items: center;
flex-shrink: 0;
font-family: var(--font-mono);
font-weight: 600;
}
.np-body .bullet-list li.bl-add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
.np-body .bullet-list .bl-text { flex: 1; min-width: 0; }
.np-body .bullet-list .bl-input {
flex: 1; min-width: 0;
height: 24px; border: 0; padding: 0 4px;
background: transparent; font-size: 13px;
color: var(--accent-black); font-family: inherit;
outline: none;
}
.np-body .bullet-list .bl-input::placeholder { color: var(--black-alpha-48); }
.np-body .bullet-list .bl-x {
width: 24px; height: 24px;
display: grid; place-items: center;
color: var(--black-alpha-32);
border-radius: var(--r-sm);
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
}
.np-body .bullet-list li.bl-item:hover .bl-x { opacity: 1; }
.np-body .bullet-list .bl-x:hover { background: var(--crimson-bg); color: var(--accent-crimson); }
.np-body .bullet-list .bl-x svg { width: 11px; height: 11px; }
/* ─── Empty state · 共享组件 ─── */
.empty-state {
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
padding: 80px 40px;
text-align: center;
color: var(--black-alpha-56);
display: none;
}
.empty-state.show { display: block; }
.empty-state .ic-empty {
width: 48px; height: 48px;
margin: 0 auto 14px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
}
.empty-state h3 { font-size: 14px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.empty-state p { font-size: 12.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── Progress (5 段流水线 · V2.1 语义色 + 脉动) ─── */
.prog { display: flex; gap: 3px; }
.prog span {
@ -761,6 +1265,10 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
position: relative;
transform: scale(.96);
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-bg.show .modal { transform: scale(1); }
.modal::before, .modal::after {
@ -789,6 +1297,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
padding: 22px 24px 16px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 14px;
flex-shrink: 0;
}
.modal-h .ic-m {
width: 36px; height: 36px;
@ -804,7 +1313,16 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
display: block; font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); font-weight: 400; margin-top: 4px; letter-spacing: .04em;
}
.modal-b { padding: 20px 24px; font-size: 13.5px; color: var(--black-alpha-72); line-height: 1.75; }
.modal-b {
padding: 20px 24px;
font-size: 13.5px;
color: var(--black-alpha-72);
line-height: 1.75;
overflow-y: auto;
overscroll-behavior: contain;
flex: 1 1 auto;
min-height: 0;
}
.modal-b .mono-acc {
font-family: var(--font-mono); color: var(--heat);
background: var(--heat-12); padding: 2px 6px;
@ -814,8 +1332,47 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
padding: 16px 24px;
border-top: 1px solid var(--border-faint);
display: flex; justify-content: flex-end; gap: 10px;
flex-shrink: 0;
}
/* 统一卡片删除按钮
矩形 + 删除 icon, hover 卡片才显示
用法: 卡片内放 <button class="card-del-btn">[icon]</button>
外层卡片需 position: relative
触发: .product-card / .asset-card / .project-card / .task-card / .model-card hover
--------------------------------------------------------- */
.card-del-btn {
position: absolute;
top: 8px; right: 8px;
width: 32px; height: 32px;
background: rgba(255,255,255,.95);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56);
cursor: pointer;
opacity: 0;
transition: opacity .18s, color .18s, border-color .18s, background .18s;
box-shadow: 0 2px 6px rgba(0,0,0,.06);
z-index: 5;
padding: 0;
font-family: inherit;
}
.card-del-btn svg { width: 14px; height: 14px; }
.product-card:hover .card-del-btn,
.asset-card:hover .card-del-btn,
.project-card:hover .card-del-btn,
.task-card:hover .card-del-btn,
.model-card:hover .card-del-btn {
opacity: 1;
}
.card-del-btn:hover {
color: var(--accent-crimson);
border-color: var(--accent-crimson);
background: var(--surface);
}
.card-del-btn:active { transform: scale(.95); }
/* ─── Drawer ─── */
.drawer-bg {
position: fixed; inset: 0;
@ -850,7 +1407,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
transition: background var(--t-base);
}
.drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.drawer-b { padding: 24px; overflow-y: auto; flex: 1; }
.drawer-b { padding: 24px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
.drawer-f {
padding: 16px 24px;
border-top: 1px solid var(--border-faint);

View File

@ -13,33 +13,35 @@ const NAV = [
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>'
},
{
id: 'products', label: '商品库', href: 'products.html', badge: '12',
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>'
},
{
id: 'projects', label: '视频项目', href: 'projects.html',
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>'
},
{
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>'
},
{
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>'
},
{
id: 'team', label: '团队', href: 'team.html', badge: '5',
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>'
},
{
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>'
},
{
id: 'settings', label: '设置', href: '#',
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>'
}
];
const TEAM_NAV = {
label: '团队', 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>',
badge: 'V1.5'
};
window.Shell = {
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
const navHtml = NAV.map(n => `
@ -63,14 +65,6 @@ window.Shell = {
</div>
<div class="nav-section">主要</div>
<nav>${navHtml}</nav>
<div class="nav-section">协作</div>
<nav>
<a class="disabled" title="V1.5 上线,敬请期待">
${TEAM_NAV.icon}
<span>${TEAM_NAV.label}</span>
<span class="pill-mini">${TEAM_NAV.badge}</span>
</a>
</nav>
<div class="aside-foot">
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
<div class="av"></div>
@ -99,10 +93,18 @@ window.Shell = {
<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>
余额 <strong>${balance}</strong>
</span>
<button class="icon-btn" onclick="Shell.toast('通知中心', '3 条未读')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>
<span class="dot-noti"></span>
<button class="queue-chip" onclick="Shell.toast('任务队列', '3 个进行中')">
<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>
任务队列
<span class="count">3</span>
</button>
<button class="icon-btn" onclick="Shell.toast('通知中心', '12 条未读')">
<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>
<span class="count-noti">12</span>
</button>
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
<span></span>
</div>
${topActions}
</div>
</header>
@ -110,29 +112,10 @@ window.Shell = {
const decorations = `
<div class="grid-bg"></div>
<pre class="scatter" style="top:96px;left:280px"> · · +
· +XX+
+XXXX·
+X· </pre>
<pre class="scatter" style="top:340px;right:96px">+ · ·
XX· ·
·XXXX·+
·++· </pre>
<pre class="scatter" style="bottom:160px;left:42%"> · +
+·XX·
·X+ ·
· </pre>
<pre class="scatter" style="top:580px;left:60px"> +X·
·XX·
+·X·+</pre>
<span class="sq-mark" style="top:238px;left:478px"></span>
<span class="sq-mark" style="top:478px;left:1198px"></span>
<span class="sq-mark" style="bottom:300px;left:238px"></span>
<span class="sq-mark" style="top:718px;right:240px"></span>
<span class="tag-corner" style="top:158px;left:34px">[ 200 OK ]</span>
<span class="tag-corner" style="top:158px;right:34px">[ /v2 ]</span>
<span class="tag-corner" style="bottom:36px;left:34px">[ .MP4 · 9:16 ]</span>
<span class="tag-corner" style="bottom:36px;right:34px">[ STUDIO ]</span>
`;
const toastHtml = `
@ -142,6 +125,142 @@ window.Shell = {
</div>
`;
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
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>
</button>
<img id="np-lightbox-img" alt="">
<span class="lb-name" id="np-lightbox-name"></span>
</div>
`;
// [DEPRECATED · 弹窗已废弃,创建商品直接进 product-create.html]
const _deprecatedModalHtml = `
<div class="modal-bg" id="new-product-bg" onclick="if(event.target===this)Shell.closeModal('new-product-bg')">
<div class="modal new-product-modal">
<span class="corner-tr"></span>
<span class="corner-bl"></span>
<div class="np-header">
<div class="np-title-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-4M3 17l9 4 9-4"/></svg>
</div>
<h2>新建商品</h2>
<span class="np-mode-pill">[ UPLOAD MODE ]</span>
<button class="np-x" type="button" onclick="Shell.closeModal('new-product-bg')">
<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>
</button>
</div>
<div class="np-body">
<div class="np-body-grid">
<!-- 左栏: 图片 -->
<div class="np-left">
<!-- AI CTA: 独立全宽区域,放在 商品图册 上方 -->
<a class="np-ai-cta primary" id="np-ai-cta" title="不上传也能用 AI 生成图">
<span class="ai-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2zM19 14l.9 2.7 2.6.8-2.6.8L19 21l-.9-2.7-2.6-.8 2.6-.8L19 14zM5 14l.7 2.1L7.8 17l-2.1.7L5 20l-.7-2.3-2.1-.7 2.1-.7L5 14z"/></svg>
</span>
<span class="ai-label" id="np-ai-label">没有图? AI 帮我生成</span>
<span class="ai-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</span>
</a>
<div class="field" style="margin-bottom: 12px;">
<label class="field-label">商品图册<span class="req">*</span></label>
<input type="file" id="np-file" accept="image/*" multiple hidden>
<!-- 图片案例 (静态示例) -->
<div class="np-section-h">
<span class="check-ic"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 8 7 12 13 4"/></svg></span>
图片案例
</div>
<div class="np-section-sub">上传您的商品的多角度白底图和使用图</div>
<div class="np-examples">
<div class="ex"><span>白底主图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>多角度</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>细节图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>使用图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>包装图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
</div>
<!-- 我的上传 (动态 5 ) -->
<div class="np-section-h">
我的上传
<span class="counter">( <span class="num" id="np-upload-count">0</span> / 5 )</span>
</div>
<div class="upload-grid" id="np-grid"></div>
<!-- Dropzone (放在 grid 之后, 5 张时禁用) -->
<div class="upload-zone" id="np-zone" style="margin-top: 10px;">
<span class="uz-ic">
<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-4M17 8l-5-5-5 5M12 3v12"/></svg>
</span>
<span id="np-zone-text">点击或拖拽上传图片</span>
<span class="uz-hint">// 支持多选 · 最多 5 张 · JPG / PNG / WEBP</span>
</div>
</div>
</div>
<!-- 右栏: 文案 -->
<div class="np-right">
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="np-name" placeholder="例: 透真玻尿酸补水面膜">
</div>
<div class="field">
<label class="field-label">品类</label>
<select class="select" id="np-cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div class="field">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" id="np-bullets" data-bl>
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder=" · "></li>
</ul>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">目标人群</label>
<input class="input" id="np-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
</div>
</div>
<div class="np-footer">
<span class="np-meta">// 上传模式 · <span class="accent">不消耗 token</span> · 一次性创建</span>
<button class="btn" type="button" onclick="Shell.closeModal('new-product-bg')">取消</button>
<button class="btn btn-primary" type="button" id="np-create" disabled style="opacity:.5;cursor:not-allowed;">
<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="M4 12l5 5L20 6"/></svg>
创建商品
</button>
</div>
</div>
</div>
<!-- Lightbox · 缩略图全屏预览 -->
<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>
</button>
<img id="np-lightbox-img" alt="">
<span class="lb-name" id="np-lightbox-name"></span>
</div>
`;
// 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+")
const cornerSvg = `<path 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" fill="currentColor"/>`;
const cornerMarks = `
@ -163,6 +282,7 @@ window.Shell = {
src.remove();
}
document.body.insertAdjacentHTML('beforeend', toastHtml);
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
@ -172,6 +292,247 @@ window.Shell = {
});
},
// [DEPRECATED] 新建商品弹窗 · 已废弃,创建商品改为直跳 product-create.html
_bindNewProductModal_DEPRECATED() {
const modal = document.getElementById('new-product-bg');
if (!modal) return;
// 上传图册的内存状态 (供 collect 和 AI CTA 用)
const uploads = []; // { id, dataUrl, name, type, size }
// 收集表单状态(供 AI CTA 和创建按钮用)
const collect = () => {
const get = (id) => document.getElementById(id)?.value?.trim() || '';
return {
name: get('np-name'),
cat: get('np-cat'),
target: get('np-target'),
bullets: [...document.querySelectorAll('#np-bullets .bl-item .bl-text')].map(el => el.textContent),
uploadedCount: uploads.length,
uploads: uploads.map(u => ({ name: u.name, type: u.type })),
};
};
const MAX_UPLOADS = 5;
// 创建按钮: 商品名 + 至少 1 张图
const nameInput = document.getElementById('np-name');
const createBtn = document.getElementById('np-create');
const updateCreateBtn = () => {
const nameOk = (nameInput.value || '').trim().length > 0;
const uploadOk = uploads.length >= 1;
const ok = nameOk && uploadOk;
createBtn.disabled = !ok;
createBtn.style.opacity = ok ? '' : '.5';
createBtn.style.cursor = ok ? '' : 'not-allowed';
// 提示用户缺什么
if (!nameOk) createBtn.title = '请填写商品名称';
else if (!uploadOk) createBtn.title = '请至少上传 1 张商品图';
else createBtn.title = '';
};
nameInput.addEventListener('input', updateCreateBtn);
createBtn.addEventListener('click', () => {
if (createBtn.disabled) return;
const s = collect();
Shell.toast('商品已创建', `+ ${s.name}`);
Shell.closeModal('new-product-bg');
});
// 核心卖点: bullet-list
const list = document.getElementById('np-bullets');
const addInput = list.querySelector('.bl-add .bl-input');
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const renumber = () => {
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
};
const bindX = (x) => {
x.addEventListener('click', () => {
const li = x.closest('li');
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); renumber(); }, 150);
});
};
const addBullet = (text) => {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">0</span><span class="bl-text">${t.replace(/[<>&]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;' })[c])}</span><span class="bl-x" title="删除">${xSvg}</span>`;
list.querySelector('.bl-add').before(li);
bindX(li.querySelector('.bl-x'));
renumber();
};
addInput?.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
addBullet(addInput.value);
addInput.value = '';
}
});
// 上传组件: 批量上传(MAX 5) + 5 固定槽 + 预览/删除 + AI CTA 联动
const zone = document.getElementById('np-zone');
const zoneText = document.getElementById('np-zone-text');
const grid = document.getElementById('np-grid');
const fileInput = document.getElementById('np-file');
const aiCtaEl = document.getElementById('np-ai-cta');
const aiLabel = document.getElementById('np-ai-label');
const uploadCount = document.getElementById('np-upload-count');
const syncAiCta = () => {
if (uploads.length === 0) {
aiCtaEl.classList.add('primary');
aiLabel.textContent = '没有图? 让 AI 帮我生成';
} else {
aiCtaEl.classList.remove('primary');
aiLabel.textContent = '用 AI 加工 / 生成模特上身图';
}
};
const syncZone = () => {
const full = uploads.length >= MAX_UPLOADS;
zone.classList.toggle('full', full);
zoneText.textContent = full ? `已达上限 (${MAX_UPLOADS} / ${MAX_UPLOADS})` : '点击或拖拽上传图片';
};
const syncCounter = () => {
uploadCount.textContent = uploads.length;
};
const thumbXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const esc = (s) => s.replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]);
const renderGrid = () => {
const filledHtml = uploads.map(u => `
<div class="up-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${esc(u.name)}">
<button class="slot-x" type="button" title="删除">${thumbXSvg}</button>
</div>
`).join('');
const emptyCount = MAX_UPLOADS - uploads.length;
const emptyHtml = Array.from({ length: emptyCount }, () => `<div class="up-slot-empty" data-action="add">无预览</div>`).join('');
grid.innerHTML = filledHtml + emptyHtml;
// 已填充槽: 点击预览 + X 删除
grid.querySelectorAll('.up-thumb').forEach(thumb => {
const id = thumb.dataset.id;
thumb.querySelector('.slot-x').addEventListener('click', (e) => {
e.stopPropagation();
const idx = uploads.findIndex(u => u.id === id);
if (idx >= 0) {
const removed = uploads.splice(idx, 1)[0];
Shell.toast('已删除', removed.name);
refreshAll();
}
});
thumb.addEventListener('click', () => {
const u = uploads.find(x => x.id === id);
if (u) Shell._openLightbox(u.dataUrl, u.name);
});
});
// 空槽: 点击触发上传
grid.querySelectorAll('[data-action="add"]').forEach(slot => {
slot.addEventListener('click', () => fileInput.click());
});
};
const refreshAll = () => {
renderGrid();
syncAiCta();
syncZone();
syncCounter();
updateCreateBtn();
};
const addFiles = (fileList) => {
const remaining = MAX_UPLOADS - uploads.length;
if (remaining <= 0) {
Shell.toast('已达上传上限', `${MAX_UPLOADS} / ${MAX_UPLOADS}`);
return;
}
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
if (!incoming.length) return;
const accepted = incoming.slice(0, remaining);
const overflow = incoming.length - accepted.length;
let added = 0;
accepted.forEach(f => {
const reader = new FileReader();
reader.onload = (e) => {
uploads.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
dataUrl: e.target.result,
name: f.name,
type: f.type,
size: f.size,
});
added++;
if (added === accepted.length) {
refreshAll();
const msg = overflow > 0
? `+ ${added} 张 · 超出 ${overflow} 张已忽略`
: `+ ${added} 张 · 共 ${uploads.length} / ${MAX_UPLOADS}`;
Shell.toast('图片已上传', msg);
}
};
reader.readAsDataURL(f);
});
};
fileInput.addEventListener('change', (e) => {
addFiles(e.target.files);
e.target.value = '';
});
zone.addEventListener('click', () => {
if (uploads.length < MAX_UPLOADS) fileInput.click();
});
zone.addEventListener('dragover', e => {
e.preventDefault();
if (uploads.length < MAX_UPLOADS) zone.style.borderColor = 'var(--heat)';
});
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
zone.addEventListener('drop', e => {
e.preventDefault();
zone.style.borderColor = '';
if (e.dataTransfer?.files?.length) addFiles(e.dataTransfer.files);
});
refreshAll();
// AI CTA: 把已填表单 + 上传图册存到 sessionStorage,跳到 AI 工作台
const aiCta = document.getElementById('np-ai-cta');
aiCta.addEventListener('click', (e) => {
e.preventDefault();
const s = collect();
sessionStorage.setItem('pending-product', JSON.stringify(s));
Shell.toast('正在跳转 AI 工作台', `带入 ${s.uploadedCount} 张图`);
setTimeout(() => { location.href = 'product-create.html'; }, 350);
});
updateCreateBtn();
},
_openLightbox(src, name) {
const lb = document.getElementById('np-lightbox');
const img = document.getElementById('np-lightbox-img');
const nm = document.getElementById('np-lightbox-name');
if (!lb || !img || lb.classList.contains('show')) return;
img.src = src;
if (nm) nm.textContent = name || '';
lb.classList.add('show');
this.lockScroll();
},
_closeLightbox() {
const lb = document.getElementById('np-lightbox');
if (!lb || !lb.classList.contains('show')) return;
lb.classList.remove('show');
this.unlockScroll();
},
toast(text, mono) {
const t = document.getElementById('__toast');
const txt = document.getElementById('__toast-txt');
@ -182,14 +543,60 @@ window.Shell = {
this._tt = setTimeout(() => t.classList.remove('show'), 2400);
},
openModal(id) { document.getElementById(id)?.classList.add('show'); },
closeModal(id) { document.getElementById(id)?.classList.remove('show'); },
/* ─── Body scroll lock (引用计数 · 多 overlay 叠加安全) ─── */
_scrollLockCount: 0,
_scrollLockSnapshot: null,
lockScroll() {
if (++this._scrollLockCount > 1) return;
const docEl = document.documentElement;
const sbw = window.innerWidth - docEl.clientWidth;
this._scrollLockSnapshot = {
bodyOverflow: document.body.style.overflow,
bodyPaddingRight: document.body.style.paddingRight,
htmlOverflow: docEl.style.overflow,
};
document.body.style.overflow = 'hidden';
docEl.style.overflow = 'hidden';
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
},
unlockScroll() {
if (--this._scrollLockCount > 0) return;
this._scrollLockCount = 0;
const s = this._scrollLockSnapshot;
if (s) {
document.body.style.overflow = s.bodyOverflow;
document.body.style.paddingRight = s.bodyPaddingRight;
document.documentElement.style.overflow = s.htmlOverflow;
this._scrollLockSnapshot = null;
}
},
openModal(id) {
const el = document.getElementById(id);
if (!el || el.classList.contains('show')) return;
el.classList.add('show');
this.lockScroll();
},
closeModal(id) {
const el = document.getElementById(id);
if (!el || !el.classList.contains('show')) return;
el.classList.remove('show');
this.unlockScroll();
},
openDrawer(id) {
document.getElementById(id)?.classList.add('show');
document.getElementById(id + '-bg')?.classList.add('show');
const el = document.getElementById(id);
const bg = document.getElementById(id + '-bg');
if (!el || el.classList.contains('show')) return;
el.classList.add('show');
if (bg) bg.classList.add('show');
this.lockScroll();
},
closeDrawer(id) {
document.getElementById(id)?.classList.remove('show');
document.getElementById(id + '-bg')?.classList.remove('show');
const el = document.getElementById(id);
const bg = document.getElementById(id + '-bg');
if (!el || !el.classList.contains('show')) return;
el.classList.remove('show');
if (bg) bg.classList.remove('show');
this.unlockScroll();
}
};

View File

@ -7,7 +7,8 @@
<link rel="stylesheet" href="assets/restraint.css">
<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 auto auto auto; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }
.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; }
.recent-row .prog, .recent-row .pill, .recent-row .btn { justify-self: start; }
.recent-row:last-child { border-bottom: 0; }
.recent-row:hover { background: var(--background-lighter); }
.recent-row .thumb { width: 54px; height: 70px; border-radius: var(--r-md); }
@ -38,7 +39,7 @@
</div>
</div>
<div class="actions">
<a class="btn" href="products.html">
<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>
@ -53,7 +54,7 @@
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<a class="stat" href="projects.html">
<div class="lbl">总项目 <span class="badge">ALL</span></div>
<div class="v">12</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>
</a>
<a class="stat" href="projects.html">
@ -63,7 +64,7 @@
</a>
<a class="stat" href="projects.html">
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
<div class="v">8</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>
</a>
<a class="stat" href="account.html">
@ -78,7 +79,7 @@
<div>
<div class="section-h">
<h2>最近项目</h2>
<a class="more" href="projects.html">[ ALL · 12 ] <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 ] <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>
</div>
<div class="card-hard">
<a class="recent-row" href="pipeline.html#stage-3">
@ -140,7 +141,7 @@
<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><div class="t">商品库</div><div class="d">12 SKU</div></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>
@ -152,7 +153,7 @@
</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><div class="t">所有项目</div><div class="d">12</div></div>
<div><div class="t">所有项目</div><div class="d">8</div></div>
</a>
</div>
</div>
@ -167,7 +168,9 @@
</div>
</div>
<script src="assets/shell.js"></script>
<script src="assets/new-product-drawer.js"></script>
<script>Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 跳转中...</title>
<script>
// 已废弃 · 新建商品改为 products.html 上的居中弹窗(Shell.openNewProduct)
// 直接访问此 URL 时,跳回商品库并自动打开弹窗
sessionStorage.setItem('auto-open-new-product', '1');
location.replace('products.html');
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,413 @@
<!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">
<style>
/* ─── 主表单 ─── */
.form-grid {
display: grid; grid-template-columns: 1.05fr 1fr; gap: 24px;
margin-bottom: 24px;
}
.form-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 24px;
}
.form-card .card-h {
display: flex; align-items: center; gap: 10px;
margin-bottom: 16px;
}
.form-card .card-h h3 {
font-size: 14px; font-weight: 600; color: var(--accent-black);
}
.form-card .card-h .req-tag {
font-family: var(--font-mono); font-size: 10px;
padding: 2px 7px;
background: var(--crimson-bg); color: var(--accent-crimson);
border-radius: var(--r-sm); letter-spacing: .04em;
border: 1px solid var(--red-bd);
}
.form-card .card-h .opt-tag {
font-family: var(--font-mono); font-size: 10px;
padding: 2px 7px;
background: var(--background-lighter); color: var(--black-alpha-56);
border-radius: var(--r-sm); letter-spacing: .04em;
border: 1px solid var(--border-faint);
}
.form-card .card-sub {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); margin: -10px 0 14px; letter-spacing: .02em;
}
/* 原图槽位 */
.photo-grid {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
margin-top: 8px;
}
.photo-slot {
aspect-ratio: 1;
border-radius: var(--r-md);
border: 1px dashed var(--border-faint);
background: var(--background-lighter);
display: grid; place-items: center;
color: var(--black-alpha-32);
cursor: pointer;
overflow: hidden;
position: relative;
font-size: 10px; font-family: var(--font-mono); letter-spacing: .04em;
transition: all var(--t-base);
}
.photo-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
.photo-slot.filled {
border-style: solid;
background-size: cover; background-position: center;
cursor: default; color: transparent;
}
.photo-slot.filled:hover { border-color: var(--heat-40); }
.photo-slot .slot-label {
position: absolute; top: 5px; left: 5px;
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
padding: 2px 6px;
background: rgba(255,255,255,.92); color: var(--black-alpha-72);
border-radius: var(--r-sm); letter-spacing: .04em;
}
.photo-slot .slot-main {
position: absolute; top: 5px; right: 5px;
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
padding: 2px 6px; background: var(--heat); color: #fff;
border-radius: var(--r-sm); letter-spacing: .04em;
}
.photo-slot .slot-x {
position: absolute; top: 4px; right: 4px;
width: 18px; height: 18px;
border-radius: 999px;
background: rgba(21,20,15,.7); color: #fff;
display: none; place-items: center; cursor: pointer; border: 0;
}
.photo-slot.filled:hover .slot-x { display: grid; }
.photo-slot.filled:hover .slot-main { display: none; }
.photo-slot .slot-x svg { width: 9px; height: 9px; }
.photo-slot .plus { width: 22px; height: 22px; border: 1px solid currentColor; border-radius: var(--r-sm); display: grid; place-items: center; margin-bottom: 4px; }
.photo-slot .plus svg { width: 12px; height: 12px; }
.upload-tip {
display: flex; align-items: center; gap: 8px;
margin-top: 14px; padding: 10px 12px;
background: var(--heat-8); border: 1px dashed var(--heat-40);
border-radius: var(--r-md);
font-size: 12px; color: var(--accent-black); line-height: 1.5;
}
.upload-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
.upload-tip strong { color: var(--heat); font-weight: 600; }
/* AI 提示 banner(选填字段说明) */
.ai-tip {
margin-top: -6px; margin-bottom: 16px;
padding: 10px 12px;
background: var(--background-lighter);
border-radius: var(--r-md);
border: 1px dashed var(--border-faint);
display: flex; align-items: flex-start; gap: 8px;
font-size: 12px; color: var(--black-alpha-72); line-height: 1.55;
}
.ai-tip svg { width: 13px; height: 13px; color: var(--heat); flex-shrink: 0; margin-top: 2px; }
.ai-tip strong { color: var(--accent-black); font-weight: 600; }
/* 卖点 bullet 输入 */
.sell-list { list-style: none; margin: 0; padding: 0; }
.sell-list li {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13px;
}
.sell-list li.add { background: var(--surface); border-style: dashed; }
.sell-list .num {
width: 18px; height: 18px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 10.5px; font-family: var(--font-mono);
color: var(--black-alpha-56);
display: grid; place-items: center; flex-shrink: 0;
}
.sell-list li.add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
.sell-list .txt { flex: 1; min-width: 0; }
.sell-list .bl-input {
flex: 1; border: 0; background: transparent;
font-size: 13px; color: var(--accent-black); padding: 0;
font-family: inherit;
}
.sell-list .bl-input::placeholder { color: var(--black-alpha-48); }
.sell-list .bl-x {
width: 20px; height: 20px;
display: grid; place-items: center;
color: var(--black-alpha-48); cursor: pointer;
background: transparent; border: 0; opacity: 0;
transition: opacity var(--t-base);
}
.sell-list li:hover .bl-x { opacity: 1; }
.sell-list .bl-x:hover { color: var(--accent-crimson); }
.sell-list .bl-x svg { width: 11px; height: 11px; }
/* 底部操作行 */
.form-foot {
position: sticky; bottom: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 22px;
display: flex; align-items: center; gap: 14px;
margin-top: 8px;
}
.form-foot .req-info {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.form-foot .req-info .ok { color: var(--accent-forest); }
.form-foot .req-info .miss { color: var(--accent-crimson); }
.form-foot .actions {
margin-left: auto;
display: flex; gap: 10px;
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>新建商品</h1>
<div class="sub"><span class="mono">// 上传原图 + 填写基本信息</span> · 保存后可在工作台逐步丰富素材</div>
</div>
</div>
<!-- ============ 表单 ============ -->
<div class="form-grid">
<!-- 左:原图 -->
<div class="form-card">
<div class="card-h">
<h3>商品原图</h3>
<span class="req-tag">必填</span>
</div>
<div class="card-sub">// 1-5 张 · 这是后续所有 AI 生成的源材料</div>
<input type="file" id="photo-input" accept="image/*" multiple hidden>
<div class="photo-grid" id="photo-grid"></div>
<div class="upload-tip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>建议上传 <strong>正面 / 侧面 / 细节 / 包装</strong> 4 张,后续在工作台生成的<strong>白底三视图</strong>更准确。</span>
</div>
</div>
<!-- 右:基本信息 -->
<div class="form-card">
<div class="card-h">
<h3>基本信息</h3>
<span class="req-tag">必填</span>
</div>
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="p-name" placeholder="例: 透真玻尿酸补水面膜">
</div>
<div class="field">
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" id="p-cat">
<option value="">— 选择品类 —</option>
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">价格(元)</label>
<input class="input" id="p-price" type="number" placeholder="选填 · 仅用于素材生成参考">
</div>
</div>
</div>
<div class="form-card" style="margin-bottom: 24px;">
<div class="card-h">
<h3>卖点 & 人群</h3>
<span class="opt-tag">选填 · 推荐</span>
</div>
<div class="ai-tip">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg>
<span>填上这两项,后续 AI 生脚本(<strong>痛点种草 / 剧情带货</strong> 等模板)质量明显更高 —— 系统会用卖点构造钩子,用人群定语气。现在不填也可以,做视频项目时仍可补。</span>
</div>
<div class="field">
<label class="field-label">核心卖点</label>
<div class="field-hint" style="margin: 4px 0 8px;">3-5 条要点,回车添加</div>
<ul class="sell-list" id="sell-list">
<li class="add"><span class="num">+</span><input class="bl-input" id="sell-input" placeholder="例: 玻尿酸双效保湿,4 小时持久水润"></li>
</ul>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">目标人群</label>
<input class="input" id="p-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<!-- ============ 底部操作 ============ -->
<div class="form-foot">
<span class="req-info" id="req-info">// 必填检查:<span class="miss">商品名 / 品类 / ≥1 张图</span> 未完成</span>
<div class="actions">
<a class="btn" href="products.html">取消</a>
<button class="btn btn-primary" id="save-btn" disabled>
<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="M4 12l5 5L20 6"/></svg>
保存并进入工作台
</button>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'products',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '商品库', href: 'products.html' },
{ label: '新建' }
]
});
const MAX = 5;
const photos = []; // { id, dataUrl }
const SLOT_LABELS = ['主图', '细节 02', '细节 03', '细节 04', '细节 05'];
const $ = id => document.getElementById(id);
// 渲染槽位
function renderPhotos() {
const grid = $('photo-grid');
grid.innerHTML = '';
for (let i = 0; i < MAX; i++) {
const slot = document.createElement('div');
const p = photos[i];
if (p) {
slot.className = 'photo-slot filled';
slot.style.backgroundImage = `url("${p.dataUrl}")`;
slot.innerHTML = `
<span class="slot-label">${SLOT_LABELS[i]}</span>
${i === 0 ? '<span class="slot-main">MAIN</span>' : ''}
<button class="slot-x" type="button" data-i="${i}" aria-label="移除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
`;
} else if (i === photos.length) {
slot.className = 'photo-slot empty';
slot.innerHTML = `<div class="plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div><span>添加</span>`;
slot.addEventListener('click', () => $('photo-input').click());
} else {
slot.className = 'photo-slot';
slot.style.opacity = '.6';
}
grid.appendChild(slot);
}
// 绑定删除
grid.querySelectorAll('.slot-x').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const i = +b.dataset.i;
photos.splice(i, 1);
renderPhotos();
syncSave();
});
});
}
// 文件上传
$('photo-input').addEventListener('change', e => {
const files = [...e.target.files].filter(f => f.type.startsWith('image/'));
const remain = MAX - photos.length;
files.slice(0, remain).forEach(f => {
const reader = new FileReader();
reader.onload = ev => {
photos.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
dataUrl: ev.target.result,
});
renderPhotos();
syncSave();
};
reader.readAsDataURL(f);
});
e.target.value = '';
});
// 卖点
const sellList = $('sell-list');
const sellInput = $('sell-input');
sellInput.addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
e.preventDefault();
const t = sellInput.value.trim();
if (!t) return;
const li = document.createElement('li');
li.innerHTML = `<span class="num"></span><span class="txt"></span><button class="bl-x" type="button" aria-label="删除"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>`;
li.querySelector('.txt').textContent = t;
sellList.querySelector('.add').before(li);
sellInput.value = '';
renumberSell();
li.querySelector('.bl-x').addEventListener('click', () => {
li.remove();
renumberSell();
});
});
function renumberSell() {
sellList.querySelectorAll('li:not(.add) .num').forEach((n, i) => n.textContent = i + 1);
}
// 必填检查
function syncSave() {
const hasName = $('p-name').value.trim().length > 0;
const hasCat = $('p-cat').value.length > 0;
const hasPhoto = photos.length > 0;
const ok = hasName && hasCat && hasPhoto;
$('save-btn').disabled = !ok;
const missing = [];
if (!hasName) missing.push('商品名');
if (!hasCat) missing.push('品类');
if (!hasPhoto) missing.push('≥1 张图');
const info = $('req-info');
if (ok) {
info.innerHTML = '// 必填检查:<span class="ok">已全部完成 ✓</span> · 可进入工作台';
} else {
info.innerHTML = `// 必填检查:<span class="miss">${missing.join(' / ')}</span> 未完成`;
}
}
$('p-name').addEventListener('input', syncSave);
$('p-cat').addEventListener('change', syncSave);
// 保存 → 跳工作台
$('save-btn').addEventListener('click', () => {
if ($('save-btn').disabled) return;
Shell.toast('商品已建档', `进入工作台`);
setTimeout(() => {
// 真实场景下会带商品 ID;demo 直接跳
location.href = 'product-studio.html?from=new&onboard=1';
}, 500);
});
renderPhotos();
syncSave();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,47 +26,100 @@
/* ─── Grid view ─── */
.proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.proj-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; display: flex; flex-direction: column; }
.proj-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; display: flex; flex-direction: column; position: relative; }
.proj-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.proj-card:hover .card-del-btn { opacity: 1; }
.proj-card .card-thumb { aspect-ratio: 9/16; max-height: 280px; border-radius: var(--r-md) var(--r-md) 0 0; }
/* 编辑模式 checkbox */
.proj-card .card-check {
position: absolute; top: 10px; left: 10px;
width: 22px; height: 22px; border-radius: 50%;
background: var(--surface); border: 2px solid var(--black-alpha-32);
display: none; place-items: center;
color: var(--accent-white); z-index: 5;
pointer-events: none;
}
.proj-card .card-check svg { width: 11px; height: 11px; opacity: 0; }
body.edit-mode .proj-card .card-check { display: grid; }
body.edit-mode .proj-card.selected .card-check {
background: var(--heat); border-color: var(--heat);
}
body.edit-mode .proj-card.selected .card-check svg { opacity: 1; }
body.edit-mode .proj-card.selected { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }
body.edit-mode .proj-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }
/* 列表行末 ⋯ 删除气泡 */
.row-more {
position: relative; display: inline-flex;
cursor: pointer; align-items: center;
color: var(--black-alpha-56);
padding: 4px;
}
.row-more:hover { color: var(--accent-black); }
.row-more-tip {
position: absolute; top: calc(100% + 6px); right: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 4px 16px rgba(0,0,0,.08);
padding: 4px; min-width: 110px;
opacity: 0; pointer-events: none;
transform: translateY(-2px);
transition: opacity .15s, transform .15s;
z-index: 12;
}
.row-more-tip::before {
content: ''; position: absolute;
top: -8px; left: 0; right: 0; height: 8px;
}
.row-more:hover .row-more-tip,
.row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }
.row-more-tip .mi {
display: flex; align-items: center; gap: 6px;
width: 100%; padding: 6px 10px;
background: transparent; border: 0;
border-radius: var(--r-sm); cursor: pointer;
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; text-align: left;
transition: background var(--t-base), color var(--t-base);
}
.row-more-tip .mi:hover {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
}
.row-more-tip .mi svg { width: 13px; height: 13px; }
.btn.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
/* bulk-bar */
.bulk-bar {
position: fixed; bottom: 24px; left: 50%;
transform: translateX(-50%);
background: var(--accent-black); color: var(--accent-white);
border-radius: var(--r-md);
padding: 10px 14px 10px 18px;
display: none; align-items: center; gap: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,.18);
z-index: 100; font-size: 13px;
}
body.edit-mode .bulk-bar { display: inline-flex; }
.bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }
.bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }
.bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }
.bulk-bar button {
height: 30px; padding: 0 12px;
background: transparent; border: 1px solid rgba(255,255,255,.24);
border-radius: var(--r-sm); color: var(--accent-white);
font-size: 12.5px; font-family: inherit; cursor: pointer;
display: inline-flex; align-items: center; gap: 5px;
}
.bulk-bar button:hover { background: rgba(255,255,255,.08); }
.bulk-bar button.danger { background: var(--accent-crimson); border-color: var(--accent-crimson); }
.bulk-bar button svg { width: 12px; height: 12px; }
.bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }
.proj-card .card-body { padding: 14px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.proj-card .card-name { font-size: 13.5px; font-weight: 600; color: var(--accent-black); line-height: 1.4; }
.proj-card .card-sub { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
.proj-card .card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid var(--border-faint); margin-top: auto; }
.proj-card .card-time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── Empty state ─── */
.empty-state {
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
padding: 80px 40px;
text-align: center;
color: var(--black-alpha-56);
display: none;
}
.empty-state.show { display: block; }
.empty-state .ic-empty {
width: 48px; height: 48px;
margin: 0 auto 14px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
}
.empty-state h3 { font-size: 14px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.empty-state p { font-size: 12.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── Result count ─── */
.result-meta {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-bottom: 14px;
letter-spacing: .04em;
}
.result-meta .count { color: var(--heat); font-weight: 600; }
</style>
</head>
<body>
@ -75,9 +128,13 @@
<div class="page-head">
<div>
<h1>视频项目</h1>
<div class="sub"><span class="mono">// 12 个 · 3 进行中 · 8 完成 · 1 失败</span></div>
<div class="sub"><span class="mono">// <span id="sub-total">0</span> 个 · <span id="sub-wip">0</span> 进行中 · <span id="sub-done">0</span> 完成 · <span id="sub-fail">0</span> 失败</span></div>
</div>
<div class="actions">
<button class="btn" type="button" id="proj-manage-btn">
<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>
新建项目
@ -86,10 +143,11 @@
</div>
<div class="tabs" id="status-tabs">
<div class="tab active" data-filter="all">全部 <span class="count">12</span></div>
<div class="tab" data-filter="wip">进行中 <span class="count">3</span></div>
<div class="tab" data-filter="done">已完成 <span class="count">8</span></div>
<div class="tab" data-filter="fail">失败 <span class="count">1</span></div>
<div class="tab active" data-filter="all">全部 <span class="count">0</span></div>
<div class="tab" data-filter="wip">进行中 <span class="count">0</span></div>
<div class="tab" data-filter="done">已完成 <span class="count">0</span></div>
<div class="tab" data-filter="fail">失败 <span class="count">0</span></div>
<div class="tab" data-filter="archived">已归档 <span class="count">0</span></div>
</div>
<div class="toolbar">
@ -97,9 +155,22 @@
<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>
<input class="input" id="search-input" placeholder="搜索项目名称、商品">
</div>
<button class="chip" onclick="Shell.toast('商品筛选', '/filter/product')">商品 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" onclick="Shell.toast('脚本来源筛选', '/filter/source')">脚本来源 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" onclick="Shell.toast('时间筛选', '/filter/date')">创建时间 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-wrap" data-key="product">
<button class="chip" type="button"><span class="chip-label">商品品类</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="source">
<button class="chip" type="button"><span class="chip-label">脚本来源</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="time">
<button class="chip" type="button"><span class="chip-label">创建时间</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<button class="clear-filters" id="clear-filters" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
<span class="spacer"></span>
<div class="view-toggle">
<button id="view-grid" data-view="grid">
@ -150,7 +221,7 @@
<td>
<div class="row-action">
<a href="pipeline.html#stage-3" onclick="event.stopPropagation()" title="继续"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></a>
<a onclick="event.stopPropagation();Shell.toast('更多操作')"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg></a>
<span class="row-more" onclick="event.stopPropagation()"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg><div class="row-more-tip"><button class="mi mi-del-row" type="button" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>删除项目</button></div></span>
</div>
</td>
</tr>
@ -295,7 +366,9 @@
<div id="grid-view" style="display:none;">
<div class="proj-grid" id="grid-body">
<div class="proj-card" data-status="wip" data-name="补水面膜 痛点种草" onclick="location.href='pipeline.html#stage-3'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/6</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/6</span></div>
<div class="card-body">
<div>
<div class="card-name">补水面膜 · 痛点种草 · v3</div>
@ -313,7 +386,9 @@
</div>
<div class="proj-card" data-status="wip" data-name="速食牛肉面 加班治愈" onclick="location.href='pipeline.html#stage-2'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 2/4</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 2/4</span></div>
<div class="card-body">
<div>
<div class="card-name">速食牛肉面 · 加班治愈</div>
@ -331,7 +406,9 @@
</div>
<div class="proj-card" data-status="wip" data-name="透真防晒 通勤对比" onclick="location.href='pipeline.html#stage-4'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 4/6</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 4/6</span></div>
<div class="card-body">
<div>
<div class="card-name">透真防晒 · 通勤对比</div>
@ -349,7 +426,9 @@
</div>
<div class="proj-card" data-status="fail" data-name="咖啡冻干 早八剧情" onclick="location.href='pipeline.html#stage-3'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/5</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/5</span></div>
<div class="card-body">
<div>
<div class="card-name">咖啡冻干 · 早八剧情</div>
@ -367,7 +446,9 @@
</div>
<div class="proj-card" data-status="done" data-name="蓝牙耳机 开箱测评" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">蓝牙耳机 · 开箱测评</div>
@ -385,7 +466,9 @@
</div>
<div class="proj-card" data-status="done" data-name="瑜伽裤 通勤穿搭" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">瑜伽裤 · 通勤穿搭</div>
@ -403,7 +486,9 @@
</div>
<div class="proj-card" data-status="done" data-name="空气炸锅 小户型" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">空气炸锅 · 小户型</div>
@ -421,7 +506,9 @@
</div>
<div class="proj-card" data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">补水面膜 · 痛点种草 · v1</div>
@ -454,9 +541,210 @@
<script>
Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目' }] });
// ============== Tab + search filter + view toggle ==============
const state = { filter: 'all', view: 'list', search: '' };
const TOTAL = 8; // 实际渲染的样本数
// ============== 注入用户新建的项目 (来自 projects-new.html 向导, 写入 localStorage) ==============
// 必须在计数 / tagItem / buildMenu 之前执行, 让后续逻辑把它们当作普通项目处理
(function injectExtraProjects() {
let pending;
try {
pending = JSON.parse(localStorage.getItem('fs-extra-projects') || '[]');
} catch (e) { return; }
if (!Array.isArray(pending) || !pending.length) return;
const tbody = document.getElementById('list-tbody');
const gridBody = document.getElementById('grid-body');
if (!tbody || !gridBody) return;
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// 按 createdAt 升序 → 倒序 insert,最新的排最上
pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
pending.forEach(p => {
const href = `pipeline.html#stage-${p.stage || 1}`;
const status = p.status || 'wip';
const shots = p.shots || 5;
const durLabel = p.durationLabel || '0-15s';
const pill = p.pillText || '脚本生成中';
// List row
const tr = document.createElement('tr');
tr.dataset.status = status;
tr.dataset.name = p.name;
tr.dataset.extraId = p.id;
tr.setAttribute('onclick', `location.href='${href}'`);
tr.innerHTML = `
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">${esc(p.name)}</div><div class="proj-sub">${shots} 镜 · ${esc(durLabel)}</div></div>
</div>
</td>
<td>${esc(p.product)}</td>
<td><span class="muted">${esc(p.source)}</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="cur"></span><span></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">1/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>${esc(pill)}</span></td>
<td class="muted-2">刚刚</td>
<td>
<div class="row-action">
<a href="${href}" onclick="event.stopPropagation()" title="继续"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></a>
<span class="row-more" onclick="event.stopPropagation()"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg><div class="row-more-tip"><button class="mi mi-del-row" type="button" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>删除项目</button></div></span>
</div>
</td>
`;
tbody.insertBefore(tr, tbody.firstElementChild);
// Grid card
const card = document.createElement('div');
card.className = 'proj-card';
card.dataset.status = status;
card.dataset.name = p.name;
card.dataset.extraId = p.id;
card.setAttribute('onclick', `location.href='${href}'`);
card.innerHTML = `
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 刚刚</span></div>
<div class="card-body">
<div>
<div class="card-name">${esc(p.name)}</div>
<div class="card-sub" style="margin-top:4px;">${esc(p.product)} · ${shots} 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="cur"></span><span></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">1/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>${esc(pill)}</span>
<span class="card-time">刚刚</span>
</div>
</div>
`;
gridBody.insertBefore(card, gridBody.firstElementChild);
});
})();
// ============== 从 DOM 实算项目计数,同步副标题 + tab 角标 ==============
const allRows = document.querySelectorAll('#list-tbody tr');
const counts = { total: allRows.length, wip: 0, done: 0, fail: 0, archived: 0 };
allRows.forEach(r => { const s = r.dataset.status; if (counts[s] !== undefined) counts[s]++; });
document.getElementById('sub-total').textContent = counts.total;
document.getElementById('sub-wip').textContent = counts.wip;
document.getElementById('sub-done').textContent = counts.done;
document.getElementById('sub-fail').textContent = counts.fail;
document.querySelectorAll('#status-tabs .tab').forEach(t => {
const f = t.dataset.filter;
const n = f === 'all' ? counts.total : (counts[f] || 0);
t.querySelector('.count').textContent = n;
});
// ============== Multi-filter state ==============
const state = { filter: 'all', view: 'list', search: '', product: 'all', source: 'all', time: 'all' };
const CHIP_LABELS = { product: '商品品类', source: '脚本来源', time: '创建时间' };
// 抖音爆款品类 · TOP 8
const CATEGORIES = ['美妆个护', '服饰内衣', '食品饮料', '家居家电', '数码 3C', '个护清洁', '运动户外', '母婴亲子'];
// 商品名 → 品类(关键字推断,demo 数据覆盖)
function inferCategory(product) {
const p = product || '';
if (/面膜|防晒|精华|护肤|彩妆|口红|粉底/.test(p)) return '美妆个护';
if (/速食|冻干|咖啡|零食|饮料|酒|茶/.test(p)) return '食品饮料';
if (/耳机|手机|数码|充电|蓝牙|3C/.test(p)) return '数码 3C';
if (/瑜伽|健身|户外|露营|运动/.test(p)) return '运动户外';
if (/服装|内衣|裤|衣|鞋|包/.test(p)) return '服饰内衣';
if (/炸锅|家电|香薰|收纳|床|家居/.test(p)) return '家居家电';
if (/洗|牙膏|清洁/.test(p)) return '个护清洁';
if (/婴|童|奶粉|辅食|玩具/.test(p)) return '母婴亲子';
return '';
}
// 时间字符串 → 时间桶 (today / week / month / earlier)
function parseTimeBucket(txt) {
if (!txt) return 'earlier';
if (txt.includes('分钟前') || txt.includes('小时前')) return 'today';
if (txt.includes('昨天')) return 'week';
// "5 月 7 日" 等当月日期 → month; 其他月份 → earlier
const m = txt.match(/(\d+)\s*月/);
if (m) {
const month = +m[1];
const now = new Date();
const curMonth = now.getMonth() + 1;
if (month === curMonth) return 'month';
if (month === curMonth - 1) return 'earlier';
}
return 'earlier';
}
const TIME_LABEL = { today: '今天', week: '本周', month: '本月', earlier: '更早' };
// 给行/卡片打数据标签 (从已有的列文字推断)
function tagItem(el, productCellText, sourceCellText, timeCellText) {
const product = (productCellText || '').trim();
el.dataset.product = product;
el.dataset.category = inferCategory(product);
el.dataset.source = (sourceCellText || '').trim();
el.dataset.time = parseTimeBucket(timeCellText || '');
}
// List view: 行内列顺序是 项目/商品/脚本来源/进度/状态/更新于
document.querySelectorAll('#list-tbody tr').forEach(tr => {
const tds = tr.querySelectorAll('td');
if (tds.length < 6) return;
tagItem(tr, tds[1].innerText, tds[2].innerText, tds[5].innerText);
});
// Grid view: 从 card-sub (商品 · N 镜) 抽出商品名,从 card-time 抽时间,无脚本来源信息 → 沿用同名 list 项的 source
const listMap = {};
document.querySelectorAll('#list-tbody tr').forEach(tr => {
listMap[tr.dataset.name] = { source: tr.dataset.source, product: tr.dataset.product };
});
document.querySelectorAll('.proj-card').forEach(card => {
const sub = card.querySelector('.card-sub')?.innerText || '';
const product = sub.split('·')[0].trim();
const time = card.querySelector('.card-time')?.innerText || '';
const name = card.dataset.name;
const listed = listMap[name] || {};
card.dataset.product = listed.product || product;
card.dataset.category = inferCategory(card.dataset.product);
card.dataset.source = listed.source || '';
card.dataset.time = parseTimeBucket(time);
});
// 计算唯一值列表用于填充下拉
function uniqueValues(key) {
const set = new Set();
document.querySelectorAll('#list-tbody tr').forEach(tr => {
const v = tr.dataset[key];
if (v) set.add(v);
});
return [...set];
}
// 填充菜单
function buildMenu(key, options) {
const wrap = document.querySelector(`.chip-wrap[data-key="${key}"]`);
const menu = wrap.querySelector('.chip-menu');
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
const all = `<div class="mi selected" data-value="all">${checkSvg}<span>全部${CHIP_LABELS[key]}</span></div><div class="mi-sep"></div>`;
const items = options.map(o => `<div class="mi" data-value="${o.value}">${checkSvg}<span>${o.label}</span></div>`).join('');
menu.innerHTML = all + items;
}
buildMenu('product', CATEGORIES.map(v => ({ value: v, label: v })));
buildMenu('source', uniqueValues('source').filter(Boolean).map(v => ({ value: v, label: v })));
buildMenu('time', [
{ value: 'today', label: '今天' },
{ value: 'week', label: '本周' },
{ value: 'month', label: '本月' },
{ value: 'earlier', label: '更早' },
]);
const TOTAL = document.querySelectorAll('#list-tbody tr').length;
function applyFilter() {
const isList = state.view === 'list';
@ -469,13 +757,17 @@ function applyFilter() {
const status = el.dataset.status || '';
const name = (el.dataset.name || '').toLowerCase();
const matchFilter = state.filter === 'all' || status.split(' ').includes(state.filter);
const matchSearch = !state.search || name.includes(state.search.toLowerCase());
const show = matchFilter && matchSearch;
const matchSearch = !state.search
|| name.includes(state.search.toLowerCase())
|| (el.dataset.product || '').toLowerCase().includes(state.search.toLowerCase());
const matchProduct = state.product === 'all' || el.dataset.category === state.product;
const matchSource = state.source === 'all' || el.dataset.source === state.source;
const matchTime = state.time === 'all' || el.dataset.time === state.time;
const show = matchFilter && matchSearch && matchProduct && matchSource && matchTime;
el.style.display = show ? '' : 'none';
if (show) visible++;
});
// empty state
const empty = document.getElementById('empty');
if (visible === 0) {
empty.classList.add('show');
@ -485,10 +777,67 @@ function applyFilter() {
empty.classList.remove('show');
}
// result meta
document.getElementById('result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${TOTAL} 个项目`;
// 是否有任意筛选生效 → 显示"清空筛选"
const hasFilter = state.filter !== 'all' || state.search || state.product !== 'all' || state.source !== 'all' || state.time !== 'all';
document.getElementById('clear-filters').hidden = !hasFilter;
}
// 清空所有筛选
document.getElementById('clear-filters').addEventListener('click', () => {
state.filter = 'all'; state.search = ''; state.product = 'all'; state.source = 'all'; state.time = 'all';
// tab 回到"全部"
document.querySelectorAll('#status-tabs .tab').forEach(t => t.classList.toggle('active', t.dataset.filter === 'all'));
// 搜索框清空
document.getElementById('search-input').value = '';
// 三个 chip 同步
['product', 'source', 'time'].forEach(syncChipUI);
applyFilter();
Shell.toast('已清空筛选');
});
// chip label + active 状态同步
function syncChipUI(key) {
const wrap = document.querySelector(`.chip-wrap[data-key="${key}"]`);
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
const v = state[key];
if (v === 'all') {
label.textContent = CHIP_LABELS[key];
chip.classList.remove('active');
} else {
label.textContent = key === 'time' ? TIME_LABEL[v] : v;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === v));
}
// Chip wrap → 点击 chip 打开/关闭,点击菜单项设置 state
document.querySelectorAll('.chip-wrap').forEach(wrap => {
const key = wrap.dataset.key;
wrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = wrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) wrap.classList.add('open');
});
wrap.querySelectorAll('.mi').forEach(mi => {
mi.addEventListener('click', e => {
e.stopPropagation();
state[key] = mi.dataset.value;
wrap.classList.remove('open');
syncChipUI(key);
applyFilter();
});
});
});
// 点击页面其他地方关闭打开的菜单
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// Tab clicks
document.querySelectorAll('#status-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
@ -496,7 +845,6 @@ document.querySelectorAll('#status-tabs .tab').forEach(t => {
t.classList.add('active');
state.filter = t.dataset.filter;
applyFilter();
Shell.toast('筛选: ' + t.textContent.trim().split(' ')[0], 'filter=' + state.filter);
});
});
@ -507,7 +855,6 @@ document.querySelectorAll('.view-toggle button').forEach(b => {
b.classList.add('active');
state.view = b.dataset.view;
applyFilter();
Shell.toast('视图切换: ' + (state.view === 'list' ? '列表' : '网格'), 'view=' + state.view);
});
});
@ -518,6 +865,199 @@ document.getElementById('search-input').addEventListener('input', e => {
});
applyFilter();
// ============================================================
// 列表行 + 网格卡 删除按钮 + 管理项目 模式
// ============================================================
const PROJECT_NEVER_DELETE = []; // 未来可挂载"未完成不可删"逻辑
function getProjectRefs(_card) { return []; } // 项目无引用检查 (PRD 没说项目反向)
// 给所有 list 行末 td 注入 row-more (含删除气泡)
const moreHTML = '<span class="row-more" onclick="event.stopPropagation()"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg><div class="row-more-tip"><button class="mi mi-del-row" type="button" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>删除项目</button></div></span>';
document.querySelectorAll('#list-tbody tr').forEach(tr => {
const lastTd = tr.querySelector('td:last-child');
if (!lastTd) return;
if (!lastTd.querySelector('.row-more')) {
// 已经有 row-action 的合并, 否则直接放
if (lastTd.querySelector('.row-action')) {
lastTd.querySelector('.row-action').insertAdjacentHTML('beforeend', moreHTML);
} else {
lastTd.innerHTML = lastTd.innerHTML + moreHTML;
}
}
});
// 删除确认 modal
const delBg = document.getElementById('del-confirm-bg');
const delBody = document.getElementById('del-confirm-body');
const delCancel = document.getElementById('del-confirm-cancel');
const delOk = document.getElementById('del-confirm-ok');
let _delQueue = [];
function openDelConfirm(targets) {
_delQueue = targets;
if (targets.length === 1) {
const name = targets[0].dataset.name || '该项目';
delBody.innerHTML = '即将删除项目 <span class="mono-acc">' + name + '</span>,已生成的视频和中间素材将清理,被引用的共享资产不受影响。';
} else {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + targets.length + ' 个项目</span>,这些项目的视频和中间素材都将清理。';
}
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }
delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
const n = _delQueue.length;
// 同步 localStorage (注入的项目带 data-extra-id) + 把同名的另一视图元素一起删
const KEY = 'fs-extra-projects';
let extraList = [];
try { extraList = JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) {}
let extraDirty = false;
const toRemove = new Set();
_delQueue.forEach(el => {
toRemove.add(el);
// 配对: 同 data-name 的另一视图元素一起删
const name = el.dataset.name;
if (name) {
document.querySelectorAll(`[data-name="${CSS.escape(name)}"]`).forEach(other => {
if (other.matches('.proj-card, #list-tbody tr')) toRemove.add(other);
});
}
// 注入的项目 → 从 localStorage 移除
const eid = el.dataset.extraId;
if (eid) {
const idx = extraList.findIndex(p => p.id === eid);
if (idx >= 0) { extraList.splice(idx, 1); extraDirty = true; }
}
});
toRemove.forEach(el => el.remove());
if (extraDirty) {
try { localStorage.setItem(KEY, JSON.stringify(extraList)); } catch (e) {}
}
closeDelConfirm();
Shell.toast('已删除', n === 1 ? '项目已移除' : '已删除 ' + n + ' 个项目');
updateBulkBar();
});
// 网格卡 card-del-btn 绑定
document.querySelectorAll('.proj-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.proj-card');
if (card) openDelConfirm([card]);
});
});
// 列表行 row-more 删除项目按钮绑定
document.querySelectorAll('.mi-del-row').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const tr = btn.closest('tr');
if (tr) openDelConfirm([tr]);
});
});
// 管理项目模式
const projManageBtn = document.getElementById('proj-manage-btn');
const projManageLabel = projManageBtn.querySelector('.proj-manage-label');
const bulkBar = document.getElementById('bulk-bar');
const bulkCount = document.getElementById('bulk-count');
const bulkClear = document.getElementById('bulk-clear');
const bulkDel = document.getElementById('bulk-del');
const bulkExit = document.getElementById('bulk-exit');
function getSelectedProjects() {
return [
...document.querySelectorAll('.proj-card.selected'),
...document.querySelectorAll('#list-tbody tr.selected'),
];
}
function updateBulkBar() {
const sel = getSelectedProjects();
bulkCount.textContent = sel.length;
bulkDel.disabled = sel.length === 0;
bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';
}
function enterEditMode() {
document.body.classList.add('edit-mode');
projManageBtn.classList.add('active');
projManageLabel.textContent = '完成';
updateBulkBar();
}
function exitEditMode() {
document.body.classList.remove('edit-mode');
projManageBtn.classList.remove('active');
projManageLabel.textContent = '管理项目';
document.querySelectorAll('.proj-card.selected, #list-tbody tr.selected').forEach(c => c.classList.remove('selected'));
}
projManageBtn.addEventListener('click', () => {
if (document.body.classList.contains('edit-mode')) exitEditMode();
else enterEditMode();
});
bulkExit.addEventListener('click', exitEditMode);
bulkClear.addEventListener('click', () => {
document.querySelectorAll('.proj-card.selected, #list-tbody tr.selected').forEach(c => c.classList.remove('selected'));
updateBulkBar();
});
bulkDel.addEventListener('click', () => {
const sel = getSelectedProjects();
if (sel.length) openDelConfirm(sel);
});
// 编辑模式下,卡片/列表行 点击切换 selected
document.querySelectorAll('.proj-card').forEach(card => {
card.addEventListener('click', e => {
if (!document.body.classList.contains('edit-mode')) return;
e.stopImmediatePropagation(); e.preventDefault();
card.classList.toggle('selected');
updateBulkBar();
}, true);
});
document.querySelectorAll('#list-tbody tr').forEach(tr => {
tr.addEventListener('click', e => {
if (!document.body.classList.contains('edit-mode')) return;
e.stopImmediatePropagation(); e.preventDefault();
tr.classList.toggle('selected');
updateBulkBar();
}, true);
});
</script>
<!-- ===== 删除确认 modal ===== -->
<div class="modal-bg" id="del-confirm-bg">
<div class="modal" role="dialog">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
</div>
<div class="ti">确认删除项目<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="del-confirm-body">即将删除项目。</div>
<div class="modal-f">
<button class="btn" type="button" id="del-confirm-cancel">取消</button>
<button class="btn" type="button" id="del-confirm-ok" style="background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
确认删除
</button>
</div>
</div>
</div>
<!-- ===== bulk-bar ===== -->
<div class="bulk-bar" id="bulk-bar">
<span class="ct">已选 <b id="bulk-count">0</b></span>
<button class="clear-sel" type="button" id="bulk-clear">清空</button>
<span class="sep"></span>
<button class="danger" type="button" id="bulk-del">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>
删除所选
</button>
<button type="button" id="bulk-exit">完成</button>
</div>
</body>
</html>

View File

@ -0,0 +1,524 @@
<!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">
<style>
/* ─── 设置布局:左 nav + 右 panel ─── */
.settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
.settings-nav { position: sticky; top: 16px; }
.settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); }
.settings-nav a:hover { background: var(--background-lighter); }
.settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.settings-nav a svg { width: 16px; height: 16px; stroke-width: 1.5; }
/* ─── pane ─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pane .pane-desc { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; margin-bottom: 18px; }
.pane.danger { border-color: rgba(180,30,30,.25); background: rgba(180,30,30,.03); }
.pane.danger h3 { color: var(--accent-crimson); }
/* ─── form row ─── */
.form-row { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-faint); align-items: center; }
.form-row:last-child { border-bottom: 0; }
.form-row .lbl { font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .lbl .req { color: var(--accent-crimson); margin-left: 2px; }
.form-row .lbl-sub { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.form-row .val { display: flex; align-items: center; gap: 10px; min-width: 0; }
.form-row .val .input, .form-row .val .select { width: 100%; max-width: 380px; }
.form-row .val .static { font-size: 13px; color: var(--accent-black); font-variant-numeric: tabular-nums; }
.form-row .val .static.mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .val .role-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; background: var(--heat-12); color: var(--heat); }
.form-row .val .role-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }
/* ─── 头像上传 ─── */
.avatar-edit { display: flex; align-items: center; gap: 16px; }
.avatar-edit .av-big { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 24px; font-weight: 600; color: var(--accent-black); }
.avatar-edit .av-actions { display: flex; gap: 8px; }
/* ─── toggle switch ─── */
.switch { position: relative; width: 36px; height: 20px; flex: 0 0 36px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider { position: absolute; inset: 0; background: var(--black-alpha-24); border-radius: 20px; cursor: pointer; transition: background var(--t-base); }
.switch .slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; background: var(--accent-white); border-radius: 50%; transition: transform var(--t-base); }
.switch input:checked + .slider { background: var(--heat); }
.switch input:checked + .slider::before { transform: translateX(16px); }
/* ─── 偏好选项卡 ─── */
.pref-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; max-width: 540px; }
.pref-choice { padding: 10px 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.pref-choice:hover { background: var(--background-lighter); }
.pref-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.pref-choice .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.pref-choice .d { font-size: 11px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.pref-choice.selected .t { color: var(--heat); }
/* ─── 时长档 ─── */
.duration-row { display: flex; gap: 8px; }
.dur-chip { padding: 6px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 13px; cursor: pointer; font-family: var(--font-mono); font-variant-numeric: tabular-nums; transition: all var(--t-base); background: var(--surface); }
.dur-chip:hover { background: var(--background-lighter); }
.dur-chip.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }
/* ─── 设备列表 ─── */
.device-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-faint); }
.device-row:last-child { border-bottom: 0; }
.device-row .ic { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--background-lighter); display: grid; place-items: center; color: var(--black-alpha-56); }
.device-row .ic svg { width: 18px; height: 18px; }
.device-row .nm { font-size: 13px; font-weight: 500; }
.device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }
.device-row .spacer { margin-left: auto; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>设置</h1>
<div class="sub"><span class="mono">// 个人信息 · 偏好 · 通知 · 安全</span></div>
</div>
<div class="actions">
<button class="btn" id="save-cancel" disabled style="opacity:.5; cursor:not-allowed;">取消</button>
<button class="btn btn-primary" id="save-btn" disabled style="opacity:.5; cursor:not-allowed;">
<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="M4 12l5 5L20 6"/></svg>
保存所有变更
</button>
</div>
</div>
<div class="settings-grid">
<!-- 左侧 nav -->
<aside class="settings-nav">
<div class="nav-h">个人</div>
<a href="#sec-profile" class="active" data-jump="sec-profile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
个人信息
</a>
<a href="#sec-security" data-jump="sec-security">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
安全
</a>
<a href="#sec-notify" data-jump="sec-notify">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 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>
通知
</a>
<div class="nav-h" style="margin-top: 16px;">偏好</div>
<a href="#sec-pref" data-jump="sec-pref">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 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>
创作默认
</a>
<a href="#sec-display" data-jump="sec-display">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
显示
</a>
<div class="nav-h" style="margin-top: 16px;">账号</div>
<a href="#sec-danger" data-jump="sec-danger" style="color: var(--accent-crimson);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
危险操作
</a>
</aside>
<!-- 右侧内容 -->
<main>
<!-- ─── 个人信息 ─── -->
<section class="pane" id="sec-profile">
<h3>个人信息</h3>
<div class="pane-desc">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>
<div class="form-row">
<div class="lbl">头像</div>
<div class="val">
<div class="avatar-edit">
<div class="av-big"></div>
<div class="av-actions">
<button class="btn btn-sm" onclick="Shell.toast('上传头像', '占位 · 选择本地图片')">上传新头像</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('恢复默认头像', '已重置')">恢复默认</button>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">显示名称<span class="req">*</span></div>
<div class="val"><input class="input" id="prof-name" value="小李" data-track></div>
</div>
<div class="form-row">
<div class="lbl">登录邮箱</div>
<div class="val">
<input class="input" id="prof-email" value="li@shop.com" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送验证邮件', 'li@shop.com')">验证</button>
</div>
</div>
<div class="form-row">
<div class="lbl">手机号</div>
<div class="val">
<input class="input" id="prof-phone" value="138****8000" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送短信验证码', '+86 138****8000')">更换</button>
</div>
</div>
<div class="form-row">
<div class="lbl">所属团队<div class="lbl-sub">// 一人一团队</div></div>
<div class="val">
<span class="static">小李的店</span>
<span class="role-tag"><span class="dot"></span>超管 · 创建者</span>
<a href="team.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">管理团队 →</a>
</div>
</div>
<div class="form-row">
<div class="lbl">用户 ID<div class="lbl-sub">// 不可改</div></div>
<div class="val"><span class="static mono">USR-2026-A8F2-001</span></div>
</div>
</section>
<!-- ─── 安全 ─── -->
<section class="pane" id="sec-security">
<h3>安全</h3>
<div class="pane-desc">// 登录密码、双因素、在用设备</div>
<div class="form-row">
<div class="lbl">登录密码</div>
<div class="val">
<span class="static mono">●●●●●●●●●●</span>
<span class="muted-2 mono" style="font-size: 11px; margin-left: auto;">上次修改 2026-04-12</span>
<button class="btn btn-sm" onclick="Shell.toast('修改密码', '/settings/password')" style="margin-left: 10px;">修改</button>
</div>
</div>
<div class="form-row">
<div class="lbl">两步验证<div class="lbl-sub">// 推荐开启</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-2fa"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信 + Authenticator</span>
</div>
</div>
<h3 style="margin-top: 24px;">在用设备</h3>
<div class="pane-desc">// 不在此列表上的设备登录会触发短信告警</div>
<div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">MacBook Pro · Chrome<span class="tag-cur">CURRENT</span></div>
<div class="meta">// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42</div>
</div>
<div class="spacer"></div>
<span class="muted-2 mono" style="font-size: 11px;">当前会话</span>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M11 18h2"/></svg></div>
<div>
<div class="nm">iPhone 15 · Safari</div>
<div class="meta">// 上海 · 2026-05-20 21:43</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'iPhone 15')">下线</button>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">Windows · Edge</div>
<div class="meta">// 杭州 · 2026-05-18 09:12</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'Windows Edge')">下线</button>
</div>
</div>
<div style="margin-top: 14px;">
<button class="btn" onclick="if(confirm('下线所有其他设备?')) Shell.toast('已下线其他设备', '2 个')">下线所有其他设备</button>
</div>
</section>
<!-- ─── 通知 ─── -->
<section class="pane" id="sec-notify">
<h3>通知</h3>
<div class="pane-desc">// 邮件、短信、站内提示开关</div>
<div class="form-row">
<div class="lbl">项目完成通知<div class="lbl-sub">// 视频导出后</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-export" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">任务失败告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-fail" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件</span>
</div>
</div>
<div class="form-row">
<div class="lbl">额度不足提醒<div class="lbl-sub">// 团队或个人剩余 < 20%</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-quota" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">异地登录告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-login" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">营销 / 产品更新</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-marketing"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">邮件</span>
</div>
</div>
</section>
<!-- ─── 创作默认 ─── -->
<section class="pane" id="sec-pref">
<h3>创作默认</h3>
<div class="pane-desc">// 新建项目时的预填值,可在向导中改</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认模板</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-template">
<div class="pref-choice selected" data-v="pain"><div class="t">痛点种草</div><div class="d">// 30s 默认档</div></div>
<div class="pref-choice" data-v="unbox"><div class="t">开箱测评</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="compare"><div class="t">对比展示</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="howto"><div class="t">教程演示</div><div class="d">// 60s 默认档</div></div>
<div class="pref-choice" data-v="drama"><div class="t">剧情带货</div><div class="d">// 60s 默认档</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认时长档</div>
<div class="val">
<div class="duration-row" id="pref-duration">
<span class="dur-chip" data-v="30">30s</span>
<span class="dur-chip" data-v="45">45s</span>
<span class="dur-chip selected" data-v="60">60s</span>
</div>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48); margin-left: 10px;">// 60s = 4 段 × 15s</span>
</div>
</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认字幕样式</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-subtitle">
<div class="pref-choice selected" data-v="big-variety"><div class="t">大字综艺</div><div class="d">// 抖音热门</div></div>
<div class="pref-choice" data-v="clean-ec"><div class="t">简洁电商</div><div class="d">// 信息清晰</div></div>
<div class="pref-choice" data-v="premium"><div class="t">高级排版</div><div class="d">// 居中衬线</div></div>
<div class="pref-choice" data-v="bullet"><div class="t">弹幕轻量</div><div class="d">// 滚动出现</div></div>
<div class="pref-choice" data-v="emphasis"><div class="t">强调爆款</div><div class="d">// 高对比</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认 BGM 库</div>
<div class="val">
<select class="select" id="pref-bgm" data-track>
<option value="kapian">抖音 Top10 卡点曲库</option>
<option value="emotion">情绪向 · 治愈/悬念</option>
<option value="urban">都市电子 · 通勤场景</option>
<option value="none">无 BGM</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">默认转场</div>
<div class="val">
<select class="select" id="pref-transition" data-track>
<option>无转场</option>
<option selected>淡入淡出 · 0.3s</option>
<option>滑动 · 0.3s</option>
<option>缩放 · 0.3s</option>
</select>
</div>
</div>
<div class="form-row">
<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>
<a href="account.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">升级 VIP →</a>
</div>
</div>
</section>
<!-- ─── 显示 ─── -->
<section class="pane" id="sec-display">
<h3>显示</h3>
<div class="pane-desc">// 界面外观与语言</div>
<div class="form-row">
<div class="lbl">外观</div>
<div class="val">
<select class="select" id="pref-theme" data-track>
<option selected>跟随系统</option>
<option>浅色</option>
<option disabled>深色(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">语言</div>
<div class="val">
<select class="select" id="pref-lang" data-track>
<option selected>简体中文</option>
<option disabled>English(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">表格密度</div>
<div class="val">
<select class="select" id="pref-density" data-track>
<option>紧凑</option>
<option selected>标准</option>
<option>宽松</option>
</select>
</div>
</div>
</section>
<!-- ─── 危险操作 ─── -->
<section class="pane danger" id="sec-danger">
<h3>危险操作</h3>
<div class="pane-desc">// 这些操作不可撤销,请确认后再执行</div>
<div class="form-row">
<div class="lbl">导出我的数据<div class="lbl-sub">// 项目 + 资产元数据</div></div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 准备时间约 24 小时,完成后邮件通知</span>
<button class="btn btn-sm" style="margin-left: auto;" onclick="Shell.toast('已申请导出', '约 24 小时后邮件发送')">申请导出</button>
</div>
</div>
<div class="form-row">
<div class="lbl">退出登录</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 仅退出当前设备,数据保留</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('确定退出登录?')) Shell.toast('已退出', '正在跳转登录页')">退出登录</button>
</div>
</div>
<div class="form-row">
<div class="lbl">注销账号</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 团队余额清零、所有项目作为孤儿归档</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('彻底注销当前账号? 此操作不可恢复')) Shell.toast('已提交注销申请', '24 小时内人工复核')">注销账号</button>
</div>
</div>
</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
</div>
</main>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'settings',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
});
/* ─── 侧边 nav 高亮 + 滚动联动 ─── */
const sections = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
navLinks.forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const id = a.dataset.jump;
const el = document.getElementById(id);
if (el) {
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const offset = 16;
if (contentEl) {
contentEl.scrollTo({ top: el.offsetTop - contentEl.offsetTop - offset, behavior: 'smooth' });
} else {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
});
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const observer = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) {
const id = en.target.id;
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
}
});
}, { root: contentEl || null, rootMargin: '-20% 0px -60% 0px', threshold: 0 });
sections.forEach(id => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
/* ─── 偏好 chip 选择 ─── */
function bindChoice(containerId, label) {
const ct = document.getElementById(containerId);
if (!ct) return;
ct.querySelectorAll('.pref-choice').forEach(c => {
c.addEventListener('click', () => {
ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty();
});
});
}
bindChoice('pref-template');
bindChoice('pref-subtitle');
document.querySelectorAll('#pref-duration .dur-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty();
});
});
/* ─── dirty state ─── */
let dirty = false;
function markDirty() {
if (dirty) return;
dirty = true;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = false;
b.style.opacity = '';
b.style.cursor = '';
});
}
function clearDirty() {
dirty = false;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = true;
b.style.opacity = '.5';
b.style.cursor = 'not-allowed';
});
}
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
el.addEventListener('change', markDirty);
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', markDirty);
});
document.getElementById('save-btn').addEventListener('click', () => {
if (!dirty) return;
Shell.toast('设置已保存', '所有变更已生效');
clearDirty();
});
document.getElementById('save-cancel').addEventListener('click', () => {
if (!dirty) return;
if (confirm('放弃未保存的变更?')) location.reload();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

1634
电商AI平台/studio.html Normal file

File diff suppressed because it is too large Load Diff

518
电商AI平台/team.html Normal file
View File

@ -0,0 +1,518 @@
<!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">
<style>
/* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */
.team-banner {
background: var(--accent-black);
color: var(--accent-white);
padding: 22px 28px 24px;
margin-bottom: 24px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
}
/* 4 个装订线小十字(2 个 pseudo + 2 个 span)*/
.team-banner::before, .team-banner::after,
.team-banner > .corner-tr, .team-banner > .corner-bl {
content: ''; position: absolute; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
background-size: contain; pointer-events: none;
}
.team-banner::before { top: -7px; left: -7px; }
.team-banner::after { bottom: -7px; right: -7px; }
.team-banner > .corner-tr { top: -7px; right: -7px; }
.team-banner > .corner-bl { bottom: -7px; left: -7px; }
/* 第 1 行:标题 + 主操作 */
.banner-head { display: flex; align-items: flex-start; gap: 20px; }
.banner-id { flex: 1; min-width: 0; }
.banner-id .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; }
.banner-id .nm .tag { font-size: 10.5px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; }
.banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.banner-actions { display: flex; gap: 8px; flex-shrink: 0; }
.banner-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
.banner-actions .btn:hover { background: var(--background-base); }
.banner-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
.banner-actions .btn-ghost:hover { background: rgba(255,255,255,.08); color: var(--accent-white); }
/* 分隔线 */
.banner-divider { height: 1px; background: rgba(255,255,255,.1); margin: 20px 0 18px; }
/* 第 2 行:4 列统计 */
.banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }
.banner-stats .stat { min-width: 0; }
.banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; }
.banner-stats .stat .v.warn { color: #FFB870; }
.banner-stats .stat .sub { font-size: 11px; color: rgba(255,255,255,.5); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── 主体两栏 ─── */
.team-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; align-items: start; }
.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: 14px; display: flex; align-items: center; gap: 8px; }
.pane h3 .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); font-weight: 400; }
.pane h3 .spacer { margin-left: auto; }
/* ─── 成员表 ─── */
.members-table .av { width: 32px; height: 32px; border-radius: 50%; background: var(--background-lighter); display: inline-grid; place-items: center; font-weight: 600; font-size: 13px; color: var(--accent-black); border: 1px solid var(--border-faint); }
.members-table .who { display: flex; align-items: center; gap: 10px; }
.members-table .nm { font-weight: 500; font-size: 13.5px; line-height: 1.2; }
.members-table .em { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); }
.members-table .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }
.members-table .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }
.members-table .role-super { background: var(--heat-12); color: var(--heat); }
.members-table .role-super .dot { background: var(--heat); }
.members-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
.members-table .role-admin .dot { background: #1E40AF; }
.members-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
.members-table .role-member .dot { background: var(--black-alpha-56); }
.members-table .quota-cell { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12px; }
.members-table .quota-cell .lbl { color: var(--black-alpha-48); }
.members-table .quota-cell .v { color: var(--accent-black); font-weight: 600; }
.members-table .used-bar { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; margin-top: 4px; }
.members-table .used-bar > span { display: block; height: 100%; background: var(--heat); }
.members-table .used-bar > span.ok { background: var(--accent-forest); }
.members-table .used-bar > span.warn { background: #B45309; }
.members-table .acts { display: flex; gap: 4px; justify-content: flex-end; }
.members-table .icon-btn-sm { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; color: var(--black-alpha-56); transition: all var(--t-base); }
.members-table .icon-btn-sm:hover { color: var(--heat); border-color: var(--heat-20); }
.members-table .icon-btn-sm svg { width: 14px; height: 14px; }
.members-table .icon-btn-sm.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
.members-table tr.pending td { opacity: .65; }
.members-table tr.pending .nm::after { content: '· 待激活'; font-size: 11px; color: var(--black-alpha-48); margin-left: 6px; font-weight: 400; font-family: var(--font-mono); }
/* ─── 角色权限矩阵 ─── */
.perm-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.perm-table th, .perm-table td { padding: 8px 4px; border-bottom: 1px solid var(--border-faint); }
.perm-table th { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; text-align: left; }
.perm-table th:not(:first-child), .perm-table td:not(:first-child) { text-align: center; }
.perm-table tbody td:first-child { color: var(--accent-black); }
.perm-table .yes { color: var(--accent-forest); font-weight: 600; }
.perm-table .no { color: var(--black-alpha-32); }
/* ─── 额度检查规则 ─── */
.quota-rules { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.8; }
.quota-rules .step { display: flex; gap: 10px; padding: 6px 0; align-items: flex-start; }
.quota-rules .num { width: 18px; height: 18px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; flex: 0 0 18px; margin-top: 1px; }
.quota-rules .v { color: var(--accent-black); font-weight: 500; }
.quota-rules .formula { font-family: var(--font-mono); font-size: 11px; color: var(--heat); background: var(--heat-12); padding: 1px 6px; }
/* ─── 邀请 modal ─── */
.invite-modal { width: min(480px, 92vw); }
.invite-modal .field { margin-bottom: 14px; }
.invite-modal label.field-label { display: block; font-size: 12px; color: var(--black-alpha-56); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.invite-modal label.field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.role-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.role-choice { padding: 12px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.role-choice:hover { background: var(--background-lighter); }
.role-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.role-choice .title { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.role-choice .desc { font-size: 11px; color: var(--black-alpha-56); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.quota-input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.quota-input-row .input { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>团队管理</h1>
<div class="sub"><span class="mono">// 成员 · 角色 · 额度 · 共享资产库</span></div>
</div>
<div class="actions">
<button class="btn" onclick="Shell.toast('团队设置', '/team/settings')">
<svg width="14" height="14" 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="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"/></svg>
团队设置
</button>
<button class="btn btn-primary" id="open-invite">
<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="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 8v6M22 11h-6"/></svg>
邀请成员
</button>
</div>
</div>
<!-- 团队信息 banner -->
<div class="team-banner">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="banner-head">
<div class="banner-id">
<div class="lbl">[ TEAM ]</div>
<div class="nm">小李的店 <span class="tag">企业</span></div>
<div class="meta">// 团队 ID: T-2026-A8F2 · 创建于 2026-04-12 · 5 名成员</div>
</div>
<div class="banner-actions">
<button class="btn btn-sm" onclick="location.href='account.html'">
<svg width="13" height="13" 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 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
充值
</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('编辑月限额', '/team/limit')">设置月限额</button>
</div>
</div>
<div class="banner-divider"></div>
<div class="banner-stats">
<div class="stat">
<div class="lbl">[ 充值余额 ]</div>
<div class="v">¥327.40</div>
<div class="sub">// 团队总池</div>
</div>
<div class="stat">
<div class="lbl">[ 月限额 ]</div>
<div class="v">¥3,000</div>
<div class="sub">// 自然月重置</div>
</div>
<div class="stat">
<div class="lbl">[ 当月已用 ]</div>
<div class="v">¥162.60</div>
<div class="sub">// 占月限 5.4%</div>
</div>
<div class="stat">
<div class="lbl">[ 当月剩余 ]</div>
<div class="v warn">¥2,837.40</div>
<div class="sub">// 还可生成约 280 个项目</div>
</div>
</div>
</div>
<div class="team-grid">
<!-- 左:成员表 -->
<div>
<div class="pane" style="padding: 0;">
<div style="display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-faint);">
<h3 style="margin: 0;">成员列表 <span class="ct">// 5 人 · 1 超管 / 1 团管 / 3 成员</span></h3>
<span class="spacer"></span>
<input class="input" id="member-search" placeholder="搜索姓名 / 手机号" style="height: 32px; font-size: 12px; width: 220px;">
</div>
<table class="t members-table" style="border: 0; border-radius: 0;">
<thead>
<tr>
<th>成员</th>
<th>角色</th>
<th>每日额度</th>
<th>月度额度</th>
<th style="width: 140px;">当月已用</th>
<th style="text-align: right; width: 88px;">操作</th>
</tr>
</thead>
<tbody id="members-tbody">
<!-- JS 注入 -->
</tbody>
</table>
</div>
</div>
<!-- 右:权限矩阵 + 额度规则 -->
<div>
<div class="pane">
<h3>角色权限</h3>
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// PRD §10.2 权限矩阵节选</div>
<table class="perm-table">
<thead>
<tr><th>能力</th><th>超管</th><th>团管</th><th>成员</th></tr>
</thead>
<tbody>
<tr><td>邀请 / 移除成员</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>设置成员额度</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队充值</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>设置月限额</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>编辑别人项目</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队共享资产库管理</td><td class="yes"></td><td class="yes"></td><td class="no">仅自传</td></tr>
<tr><td>查看团队消费明细</td><td class="yes"></td><td class="yes"></td><td class="no">仅自己</td></tr>
<tr style="border-bottom: 0;"><td>创建项目 / 用 AI 流程</td><td class="yes"></td><td class="yes"></td><td class="yes"></td></tr>
</tbody>
</table>
</div>
<div class="pane">
<h3>额度预检规则</h3>
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 任一不通过即拦截</div>
<div class="quota-rules">
<div class="step"><span class="num">1</span><span><span class="v">个人日剩余</span> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
<div class="step"><span class="num">2</span><span><span class="v">个人月剩余</span> ≥ 同上</span></div>
<div class="step"><span class="num">3</span><span><span class="v">团队月剩余</span> ≥ 同上</span></div>
<div class="step"><span class="num">4</span><span><span class="v">团队总余额</span> ≥ 同上</span></div>
</div>
</div>
<div class="pane" style="background: var(--heat-12); border-color: var(--heat-20);">
<h3 style="color: var(--heat);">
<svg width="14" height="14" 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="10"/><path d="M12 8v4M12 16h.01"/></svg>
失败不扣费
</h3>
<div style="font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7;">
所有生成任务<strong style="color: var(--accent-black);">仅在用户 <span class="mono" style="background: var(--surface); padding: 1px 5px; font-family: var(--font-mono); color: var(--heat); font-size: 11.5px;">[ 通过 ]</span> 时才扣费</strong>。失败 / 超时 / 重跑(旧版本作废)一律不扣。
</div>
</div>
</div>
</div>
<!-- 邀请成员 modal -->
<div class="modal-bg" id="invite-bg" onclick="if(event.target===this)Shell.closeModal('invite-bg')">
<div class="modal invite-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><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 8v6M22 11h-6"/></svg>
</div>
<div class="ti">邀请成员<span>// 短信邀请 · 接受后自动加入团队</span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">手机号 <span class="req">*</span></label>
<input class="input" id="inv-phone" placeholder="例: 138 0013 8000">
</div>
<div class="field">
<label class="field-label">备注姓名(可选)</label>
<input class="input" id="inv-name" placeholder="例: 张某">
</div>
<div class="field">
<label class="field-label">分配角色 <span class="req">*</span></label>
<div class="role-choices">
<div class="role-choice selected" data-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">默认额度</label>
<div class="quota-input-row">
<input class="input" id="inv-daily" placeholder="每日 (¥)" value="100">
<input class="input" id="inv-monthly" placeholder="每月 (¥)" value="2000">
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('invite-bg')">取消</button>
<button class="btn btn-primary" type="button" id="inv-send">
<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="m22 2-7 20-4-9-9-4z"/><path d="m22 2-11 11"/></svg>
发送邀请
</button>
</div>
</div>
</div>
<!-- 编辑成员 modal -->
<div class="modal-bg" id="edit-member-bg" onclick="if(event.target===this)Shell.closeModal('edit-member-bg')">
<div class="modal invite-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</div>
<div class="ti" id="edit-title">编辑成员<span id="edit-sub">// 调整角色 / 额度</span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">角色</label>
<div class="role-choices">
<div class="role-choice" data-edit-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-edit-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">额度</label>
<div class="quota-input-row">
<input class="input" id="edit-daily" placeholder="每日 (¥)">
<input class="input" id="edit-monthly" placeholder="每月 (¥)">
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" style="margin-right: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" id="edit-remove">移出团队</button>
<button class="btn" type="button" onclick="Shell.closeModal('edit-member-bg')">取消</button>
<button class="btn btn-primary" type="button" id="edit-save">保存</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'team',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '团队' }]
});
/* ─── 团队成员 mock + 渲染 ─── */
const ROLE_META = {
super: { cls: 'role-super', label: '超管' },
admin: { cls: 'role-admin', label: '团管' },
member: { cls: 'role-member', label: '成员' },
};
const MEMBERS = [
{ id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, pending: false, creator: true },
{ id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, pending: false },
{ id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, pending: false },
{ id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, pending: false },
{ id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, pending: true },
];
function fmtMoney(n) { return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
function usedClass(pct) { if (pct < 0.5) return 'ok'; if (pct < 0.85) return ''; return 'warn'; }
function renderMembers(filter = '') {
const tb = document.getElementById('members-tbody');
const list = MEMBERS.filter(m => {
if (!filter) return true;
const q = filter.toLowerCase();
return m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q);
});
tb.innerHTML = list.map(m => {
const r = ROLE_META[m.role];
const pct = m.monthly > 0 ? m.used / m.monthly : 0;
return `
<tr data-id="${m.id}"${m.pending ? ' class="pending"' : ''}>
<td>
<div class="who">
<div class="av">${m.av}</div>
<div>
<div class="nm">${m.name}${m.creator ? ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--black-alpha-48);">· 创建者</span>' : ''}</div>
<div class="em">${m.email}</div>
</div>
</div>
</td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.daily)}</span></span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.monthly)}</span></span></td>
<td>
<div class="quota-cell"><span class="v">${fmtMoney(m.used)}</span> <span class="lbl">/ ${(pct * 100).toFixed(0)}%</span></div>
<div class="used-bar"><span class="${usedClass(pct)}" style="width: ${Math.min(100, pct * 100).toFixed(1)}%"></span></div>
</td>
<td>
<div class="acts">
${m.creator ? '<span style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-32);align-self:center;">不可编辑</span>' : `
<button class="icon-btn-sm" data-act="edit" data-id="${m.id}" title="编辑">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</button>
<button class="icon-btn-sm danger" data-act="remove" data-id="${m.id}" title="移出">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
`}
</div>
</td>
</tr>
`;
}).join('');
tb.querySelectorAll('[data-act="edit"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
openEdit(btn.dataset.id);
});
});
tb.querySelectorAll('[data-act="remove"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const m = MEMBERS.find(x => x.id === btn.dataset.id);
if (!m) return;
if (confirm('确定将「' + m.name + '」移出团队?')) {
const i = MEMBERS.findIndex(x => x.id === m.id);
MEMBERS.splice(i, 1);
Shell.toast('已移除成员', m.name);
renderMembers(document.getElementById('member-search').value);
}
});
});
}
renderMembers();
document.getElementById('member-search').addEventListener('input', e => {
renderMembers(e.target.value);
});
/* ─── 邀请 modal ─── */
document.getElementById('open-invite').addEventListener('click', () => {
Shell.openModal('invite-bg');
});
document.querySelectorAll('#invite-bg .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#invite-bg .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
document.getElementById('inv-send').addEventListener('click', () => {
const phone = document.getElementById('inv-phone').value.trim();
if (!phone) { Shell.toast('请填手机号', '邀请失败'); return; }
const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';
const name = document.getElementById('inv-name').value.trim() || '待激活成员';
const daily = Number(document.getElementById('inv-daily').value) || 100;
const monthly = Number(document.getElementById('inv-monthly').value) || 2000;
MEMBERS.push({
id: 'u' + Date.now(),
av: name[0] || '新',
name, email: phone, role,
daily, monthly, used: 0, pending: true,
});
renderMembers();
Shell.closeModal('invite-bg');
Shell.toast('邀请已发送', phone);
document.getElementById('inv-phone').value = '';
document.getElementById('inv-name').value = '';
});
/* ─── 编辑 modal ─── */
let editingId = null;
function openEdit(id) {
const m = MEMBERS.find(x => x.id === id);
if (!m) return;
editingId = id;
document.getElementById('edit-title').innerHTML = '编辑「' + m.name + '」<span id="edit-sub">// ' + m.email + '</span>';
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
c.classList.toggle('selected', c.dataset.editRole === (m.role === 'super' ? 'admin' : m.role));
});
document.getElementById('edit-daily').value = m.daily;
document.getElementById('edit-monthly').value = m.monthly;
Shell.openModal('edit-member-bg');
}
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#edit-member-bg .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
document.getElementById('edit-save').addEventListener('click', () => {
const m = MEMBERS.find(x => x.id === editingId);
if (!m) return;
m.role = document.querySelector('#edit-member-bg .role-choice.selected')?.dataset.editRole || m.role;
m.daily = Number(document.getElementById('edit-daily').value) || m.daily;
m.monthly = Number(document.getElementById('edit-monthly').value) || m.monthly;
Shell.closeModal('edit-member-bg');
Shell.toast('已保存', m.name);
renderMembers(document.getElementById('member-search').value);
});
document.getElementById('edit-remove').addEventListener('click', () => {
const m = MEMBERS.find(x => x.id === editingId);
if (!m) return;
if (confirm('确定将「' + m.name + '」移出团队?')) {
const i = MEMBERS.findIndex(x => x.id === m.id);
MEMBERS.splice(i, 1);
Shell.closeModal('edit-member-bg');
Shell.toast('已移除', m.name);
renderMembers(document.getElementById('member-search').value);
}
});
</script>
</body>
</html>