1064 lines
54 KiB
HTML
1064 lines
54 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>视频项目 · 流·Studio</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
|
<style>
|
|
/* ─── List view ─── */
|
|
.proj-name-cell { display: flex; align-items: center; gap: 12px; }
|
|
.proj-thumb { width: 40px; height: 52px; flex-shrink: 0; border-radius: var(--r-md); }
|
|
.proj-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
|
|
.proj-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
|
|
.row-action { display: flex; gap: 4px; visibility: hidden; }
|
|
table.t tbody tr:hover .row-action { visibility: visible; }
|
|
.row-action a { width: 28px; height: 28px; display: grid; place-items: center; color: var(--black-alpha-56); border-radius: var(--r-md); }
|
|
.row-action a:hover { background: var(--surface); color: var(--heat); border: 1px solid var(--border-faint); }
|
|
|
|
/* ─── View toggle ─── */
|
|
.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-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; }
|
|
|
|
/* ─── 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; 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; }
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="page">
|
|
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>视频项目</h1>
|
|
<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>
|
|
新建项目
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs" id="status-tabs">
|
|
<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">
|
|
<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="search-input" placeholder="搜索项目名称、商品">
|
|
</div>
|
|
<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">
|
|
<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 id="view-list" 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="result-meta">// 显示 <span class="count">12</span> / 12 个项目</div>
|
|
|
|
<!-- ============= LIST VIEW ============= -->
|
|
<div id="list-view">
|
|
<table class="t">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:32%">项目</th>
|
|
<th>商品</th>
|
|
<th>脚本来源</th>
|
|
<th style="width:200px">进度</th>
|
|
<th>状态</th>
|
|
<th style="width:120px">更新于</th>
|
|
<th style="width:60px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="list-tbody">
|
|
<tr data-status="wip" data-name="补水面膜 痛点种草" onclick="location.href='pipeline.html#stage-3'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">补水面膜 · 痛点种草 · v3</div><div class="proj-sub">6 镜 · 0-15s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>透真补水面膜</td>
|
|
<td><span class="muted">AI 全生</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">3/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill info"><span class="dot"></span>故事板 待确认</span></td>
|
|
<td class="muted-2">12 分钟前</td>
|
|
<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>
|
|
<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>
|
|
<tr data-status="wip" data-name="速食牛肉面 加班治愈" onclick="location.href='pipeline.html#stage-2'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">速食牛肉面 · 加班治愈</div><div class="proj-sub">4 镜 · 0-12s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>滋啦速食 · 6 桶装</td>
|
|
<td><span class="muted">一句话主题</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">2/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill info"><span class="dot"></span>资产生成中</span></td>
|
|
<td class="muted-2">37 分钟前</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="wip" data-name="透真防晒 通勤对比" onclick="location.href='pipeline.html#stage-4'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">透真防晒 · 通勤对比</div><div class="proj-sub">6 镜 · 0-18s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>透真清透防晒霜</td>
|
|
<td><span class="muted">AI 全生</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">4/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill info"><span class="dot"></span>视频生成 4/6</span></td>
|
|
<td class="muted-2">2 小时前</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="fail" data-name="咖啡冻干 早八剧情" onclick="location.href='pipeline.html#stage-3'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">咖啡冻干 · 早八剧情</div><div class="proj-sub">5 镜 · 0-15s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>三顿半同款冻干</td>
|
|
<td><span class="muted">一句话主题</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">3/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill err"><span class="dot"></span>故事板生成失败</span></td>
|
|
<td class="muted-2">昨天 18:42</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="done" data-name="蓝牙耳机 开箱测评" onclick="location.href='pipeline.html#stage-5'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">蓝牙耳机 · 开箱测评</div><div class="proj-sub">5 镜 · 0-15s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>南卡 Lite Pro</td>
|
|
<td><span class="muted">自带脚本</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">5/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
|
|
<td class="muted-2">5 月 7 日</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="done" data-name="瑜伽裤 通勤穿搭" onclick="location.href='pipeline.html#stage-5'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">瑜伽裤 · 通勤穿搭</div><div class="proj-sub">5 镜 · 0-15s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>露露同款瑜伽裤</td>
|
|
<td><span class="muted">AI 全生</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">5/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
|
|
<td class="muted-2">5 月 6 日</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="done" data-name="空气炸锅 小户型" onclick="location.href='pipeline.html#stage-5'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">空气炸锅 · 小户型</div><div class="proj-sub">4 镜 · 0-12s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>小熊 4L 空气炸锅</td>
|
|
<td><span class="muted">一句话主题</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">5/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
|
|
<td class="muted-2">5 月 4 日</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
|
|
<td>
|
|
<div class="proj-name-cell">
|
|
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
|
|
<div><div class="proj-name">补水面膜 · 痛点种草 · v1</div><div class="proj-sub">6 镜 · 0-15s</div></div>
|
|
</div>
|
|
</td>
|
|
<td>透真补水面膜</td>
|
|
<td><span class="muted">AI 全生</span></td>
|
|
<td>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:11px;">5/5</span>
|
|
</div>
|
|
</td>
|
|
<td><span class="pill neutral"><span class="dot"></span>已归档</span></td>
|
|
<td class="muted-2">4 月 28 日</td>
|
|
<td></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- ============= GRID VIEW ============= -->
|
|
<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'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">透真补水面膜 · 6 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">3/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
|
|
<span class="card-time">12 分钟前</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="wip" data-name="速食牛肉面 加班治愈" onclick="location.href='pipeline.html#stage-2'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">滋啦速食 · 4 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">2/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill info"><span class="dot"></span>资产生成中</span>
|
|
<span class="card-time">37 分钟前</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="wip" data-name="透真防晒 通勤对比" onclick="location.href='pipeline.html#stage-4'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">透真清透防晒霜 · 6 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">4/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill info"><span class="dot"></span>视频 4/6</span>
|
|
<span class="card-time">2 小时前</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="fail" data-name="咖啡冻干 早八剧情" onclick="location.href='pipeline.html#stage-3'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">三顿半同款 · 5 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">3/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill err"><span class="dot"></span>故事板失败</span>
|
|
<span class="card-time">昨天 18:42</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="done" data-name="蓝牙耳机 开箱测评" onclick="location.href='pipeline.html#stage-5'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">南卡 Lite Pro · 5 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">5/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill ok"><span class="dot"></span>已完成</span>
|
|
<span class="card-time">5 月 7 日</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="done" data-name="瑜伽裤 通勤穿搭" onclick="location.href='pipeline.html#stage-5'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">露露同款 · 5 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">5/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill ok"><span class="dot"></span>已完成</span>
|
|
<span class="card-time">5 月 6 日</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="done" data-name="空气炸锅 小户型" onclick="location.href='pipeline.html#stage-5'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">小熊 4L · 4 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">5/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill ok"><span class="dot"></span>已完成</span>
|
|
<span class="card-time">5 月 4 日</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="proj-card" data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
|
|
<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>
|
|
<div class="card-sub" style="margin-top:4px;">透真补水面膜 · 6 镜</div>
|
|
</div>
|
|
<div class="hstack">
|
|
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
|
<span class="muted-2 mono" style="font-size:10.5px;">5/5</span>
|
|
</div>
|
|
<div class="card-foot">
|
|
<span class="pill neutral"><span class="dot"></span>已归档</span>
|
|
<span class="card-time">4 月 28 日</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div class="empty-state" id="empty">
|
|
<div class="ic-empty">
|
|
<svg width="20" height="20" 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>
|
|
</div>
|
|
<h3>没有匹配的项目</h3>
|
|
<p>// 试试切换 tab 或修改搜索词</p>
|
|
</div>
|
|
|
|
</div>
|
|
<script src="assets/shell.js?v=202605211643"></script>
|
|
<script>
|
|
Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目' }] });
|
|
|
|
// ============== 注入用户新建的项目 (来自 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';
|
|
document.getElementById('list-view').style.display = isList ? '' : 'none';
|
|
document.getElementById('grid-view').style.display = isList ? 'none' : '';
|
|
|
|
const items = document.querySelectorAll(isList ? '#list-tbody tr' : '.proj-card');
|
|
let visible = 0;
|
|
items.forEach(el => {
|
|
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())
|
|
|| (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++;
|
|
});
|
|
|
|
const empty = document.getElementById('empty');
|
|
if (visible === 0) {
|
|
empty.classList.add('show');
|
|
document.getElementById('list-view').style.display = 'none';
|
|
document.getElementById('grid-view').style.display = 'none';
|
|
} else {
|
|
empty.classList.remove('show');
|
|
}
|
|
|
|
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', () => {
|
|
document.querySelectorAll('#status-tabs .tab').forEach(x => x.classList.remove('active'));
|
|
t.classList.add('active');
|
|
state.filter = t.dataset.filter;
|
|
applyFilter();
|
|
});
|
|
});
|
|
|
|
// View toggle
|
|
document.querySelectorAll('.view-toggle button').forEach(b => {
|
|
b.addEventListener('click', () => {
|
|
document.querySelectorAll('.view-toggle button').forEach(x => x.classList.remove('active'));
|
|
b.classList.add('active');
|
|
state.view = b.dataset.view;
|
|
applyFilter();
|
|
});
|
|
});
|
|
|
|
// Search
|
|
document.getElementById('search-input').addEventListener('input', e => {
|
|
state.search = e.target.value.trim();
|
|
applyFilter();
|
|
});
|
|
|
|
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>
|