fix(deploy): 把 V2.1 设计稿挪进 电商AI平台/ · Docker 构建上下文是这里
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
This commit is contained in:
parent
e293aa43be
commit
e7c0a14f75
900
电商AI平台/asset-factory.html
Normal file
900
电商AI平台/asset-factory.html
Normal 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
578
电商AI平台/assets/new-product-drawer.js
Normal file
578
电商AI平台/assets/new-product-drawer.js
Normal 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 => ({ '<':'<','>':'>','&':'&','"':'"' })[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();
|
||||
}
|
||||
})();
|
||||
@ -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);
|
||||
|
||||
@ -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 => ({ '<':'<','>':'>','&':'&' })[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 => ({ '<':'<','>':'>','&':'&','"':'"' })[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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
1759
电商AI平台/library.html
1759
电商AI平台/library.html
File diff suppressed because it is too large
Load Diff
1961
电商AI平台/model-photo.html
Normal file
1961
电商AI平台/model-photo.html
Normal file
File diff suppressed because it is too large
Load Diff
1605
电商AI平台/pipeline.html
1605
电商AI平台/pipeline.html
File diff suppressed because it is too large
Load Diff
1360
电商AI平台/platform-cover.html
Normal file
1360
电商AI平台/platform-cover.html
Normal file
File diff suppressed because it is too large
Load Diff
14
电商AI平台/product-create-upload.html
Normal file
14
电商AI平台/product-create-upload.html
Normal 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>
|
||||
413
电商AI平台/product-create-v2.html
Normal file
413
电商AI平台/product-create-v2.html
Normal 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
3883
电商AI平台/product-create.legacy.html
Normal file
3883
电商AI平台/product-create.legacy.html
Normal file
File diff suppressed because it is too large
Load Diff
1671
电商AI平台/product-detail.html
Normal file
1671
电商AI平台/product-detail.html
Normal file
File diff suppressed because it is too large
Load Diff
1082
电商AI平台/product-studio.html
Normal file
1082
电商AI平台/product-studio.html
Normal file
File diff suppressed because it is too large
Load Diff
1841
电商AI平台/products.html
1841
电商AI平台/products.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
|
||||
524
电商AI平台/settings.html
Normal file
524
电商AI平台/settings.html
Normal 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>
|
||||
1402
电商AI平台/studio-v2.html
Normal file
1402
电商AI平台/studio-v2.html
Normal file
File diff suppressed because it is too large
Load Diff
1634
电商AI平台/studio.html
Normal file
1634
电商AI平台/studio.html
Normal file
File diff suppressed because it is too large
Load Diff
518
电商AI平台/team.html
Normal file
518
电商AI平台/team.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user