AirShelf/v2/projects.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

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">
<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"></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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// 按 createdAt 升序 → 倒序 insert,最新的排最上
pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
pending.forEach(p => {
const href = `pipeline.html#stage-${p.stage || 1}`;
const status = p.status || 'wip';
const shots = p.shots || 5;
const durLabel = p.durationLabel || '0-15s';
const pill = p.pillText || '脚本生成中';
// List row
const tr = document.createElement('tr');
tr.dataset.status = status;
tr.dataset.name = p.name;
tr.dataset.extraId = p.id;
tr.setAttribute('onclick', `location.href='${href}'`);
tr.innerHTML = `
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">${esc(p.name)}</div><div class="proj-sub">${shots} 镜 · ${esc(durLabel)}</div></div>
</div>
</td>
<td>${esc(p.product)}</td>
<td><span class="muted">${esc(p.source)}</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="cur"></span><span></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">1/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>${esc(pill)}</span></td>
<td class="muted-2">刚刚</td>
<td>
<div class="row-action">
<a href="${href}" onclick="event.stopPropagation()" title="继续"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></a>
<span class="row-more" onclick="event.stopPropagation()"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg><div class="row-more-tip"><button class="mi mi-del-row" type="button" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>删除项目</button></div></span>
</div>
</td>
`;
tbody.insertBefore(tr, tbody.firstElementChild);
// Grid card
const card = document.createElement('div');
card.className = 'proj-card';
card.dataset.status = status;
card.dataset.name = p.name;
card.dataset.extraId = p.id;
card.setAttribute('onclick', `location.href='${href}'`);
card.innerHTML = `
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除项目" onclick="event.stopPropagation();" data-action="delete-project"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 刚刚</span></div>
<div class="card-body">
<div>
<div class="card-name">${esc(p.name)}</div>
<div class="card-sub" style="margin-top:4px;">${esc(p.product)} · ${shots} 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="cur"></span><span></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">1/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>${esc(pill)}</span>
<span class="card-time">刚刚</span>
</div>
</div>
`;
gridBody.insertBefore(card, gridBody.firstElementChild);
});
})();
// ============== 从 DOM 实算项目计数,同步副标题 + tab 角标 ==============
const allRows = document.querySelectorAll('#list-tbody tr');
const counts = { total: allRows.length, wip: 0, done: 0, fail: 0, archived: 0 };
allRows.forEach(r => { const s = r.dataset.status; if (counts[s] !== undefined) counts[s]++; });
document.getElementById('sub-total').textContent = counts.total;
document.getElementById('sub-wip').textContent = counts.wip;
document.getElementById('sub-done').textContent = counts.done;
document.getElementById('sub-fail').textContent = counts.fail;
document.querySelectorAll('#status-tabs .tab').forEach(t => {
const f = t.dataset.filter;
const n = f === 'all' ? counts.total : (counts[f] || 0);
t.querySelector('.count').textContent = n;
});
// ============== Multi-filter state ==============
const state = { filter: 'all', view: 'list', search: '', product: 'all', source: 'all', time: 'all' };
const CHIP_LABELS = { product: '商品品类', source: '脚本来源', time: '创建时间' };
// 抖音爆款品类 · TOP 8
const CATEGORIES = ['美妆个护', '服饰内衣', '食品饮料', '家居家电', '数码 3C', '个护清洁', '运动户外', '母婴亲子'];
// 商品名 → 品类(关键字推断,demo 数据覆盖)
function inferCategory(product) {
const p = product || '';
if (/面膜|防晒|精华|护肤|彩妆|口红|粉底/.test(p)) return '美妆个护';
if (/速食|冻干|咖啡|零食|饮料|酒|茶/.test(p)) return '食品饮料';
if (/耳机|手机|数码|充电|蓝牙|3C/.test(p)) return '数码 3C';
if (/瑜伽|健身|户外|露营|运动/.test(p)) return '运动户外';
if (/服装|内衣|裤|衣|鞋|包/.test(p)) return '服饰内衣';
if (/炸锅|家电|香薰|收纳|床|家居/.test(p)) return '家居家电';
if (/洗|牙膏|清洁/.test(p)) return '个护清洁';
if (/婴|童|奶粉|辅食|玩具/.test(p)) return '母婴亲子';
return '';
}
// 时间字符串 → 时间桶 (today / week / month / earlier)
function parseTimeBucket(txt) {
if (!txt) return 'earlier';
if (txt.includes('分钟前') || txt.includes('小时前')) return 'today';
if (txt.includes('昨天')) return 'week';
// "5 月 7 日" 等当月日期 → month; 其他月份 → earlier
const m = txt.match(/(\d+)\s*月/);
if (m) {
const month = +m[1];
const now = new Date();
const curMonth = now.getMonth() + 1;
if (month === curMonth) return 'month';
if (month === curMonth - 1) return 'earlier';
}
return 'earlier';
}
const TIME_LABEL = { today: '今天', week: '本周', month: '本月', earlier: '更早' };
// 给行/卡片打数据标签 (从已有的列文字推断)
function tagItem(el, productCellText, sourceCellText, timeCellText) {
const product = (productCellText || '').trim();
el.dataset.product = product;
el.dataset.category = inferCategory(product);
el.dataset.source = (sourceCellText || '').trim();
el.dataset.time = parseTimeBucket(timeCellText || '');
}
// List view: 行内列顺序是 项目/商品/脚本来源/进度/状态/更新于
document.querySelectorAll('#list-tbody tr').forEach(tr => {
const tds = tr.querySelectorAll('td');
if (tds.length < 6) return;
tagItem(tr, tds[1].innerText, tds[2].innerText, tds[5].innerText);
});
// Grid view: 从 card-sub (商品 · N 镜) 抽出商品名,从 card-time 抽时间,无脚本来源信息 → 沿用同名 list 项的 source
const listMap = {};
document.querySelectorAll('#list-tbody tr').forEach(tr => {
listMap[tr.dataset.name] = { source: tr.dataset.source, product: tr.dataset.product };
});
document.querySelectorAll('.proj-card').forEach(card => {
const sub = card.querySelector('.card-sub')?.innerText || '';
const product = sub.split('·')[0].trim();
const time = card.querySelector('.card-time')?.innerText || '';
const name = card.dataset.name;
const listed = listMap[name] || {};
card.dataset.product = listed.product || product;
card.dataset.category = inferCategory(card.dataset.product);
card.dataset.source = listed.source || '';
card.dataset.time = parseTimeBucket(time);
});
// 计算唯一值列表用于填充下拉
function uniqueValues(key) {
const set = new Set();
document.querySelectorAll('#list-tbody tr').forEach(tr => {
const v = tr.dataset[key];
if (v) set.add(v);
});
return [...set];
}
// 填充菜单
function buildMenu(key, options) {
const wrap = document.querySelector(`.chip-wrap[data-key="${key}"]`);
const menu = wrap.querySelector('.chip-menu');
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
const all = `<div class="mi selected" data-value="all">${checkSvg}<span>全部${CHIP_LABELS[key]}</span></div><div class="mi-sep"></div>`;
const items = options.map(o => `<div class="mi" data-value="${o.value}">${checkSvg}<span>${o.label}</span></div>`).join('');
menu.innerHTML = all + items;
}
buildMenu('product', CATEGORIES.map(v => ({ value: v, label: v })));
buildMenu('source', uniqueValues('source').filter(Boolean).map(v => ({ value: v, label: v })));
buildMenu('time', [
{ value: 'today', label: '今天' },
{ value: 'week', label: '本周' },
{ value: 'month', label: '本月' },
{ value: 'earlier', label: '更早' },
]);
const TOTAL = document.querySelectorAll('#list-tbody tr').length;
function applyFilter() {
const isList = state.view === 'list';
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>