diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index 566897d..6689fb8 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -1,5 +1,14 @@ # Development Log +## 2026-08-15 片段审核与发送中心分组全选 + +- 片段审核页在候选列表顶部新增吸顶式“全选当前列表”:默认全部筛选下可一次启用或取消全部候选,使用其他筛选时只操作当前显示片段;右侧实时显示“已启用 N / M 条”,单独调整后会显示半选状态。 +- 全选只修改页面中的审核选择,不会自动写库;用户仍需点击“保存修改”“生成切片”或“保存并同步发送中心”,继续复用原有批量保存与切片同步校验。 +- 发送中心每个“原始处理任务”组头新增“全选本任务”,只选择该任务、当前抖音或 B站平台下可见的内容;不同任务互不影响,单条取消后显示半选状态,并继续联动现有“已选 N 条”批量栏。 +- 切换平台仍会清空选择,隐藏、失效和另一平台记录不会被任务级全选带入;没有新增 API、数据库字段,也没有改变排期、调度器或真实投稿边界。 +- 手机宽度下同步整理任务组头与底部批量栏:账号选择和批量按钮改为两列响应式布局,不再出现文字竖排或横向滚动。 +- JavaScript 语法检查通过;新增片段审核渲染/脚本契约、任务分组渲染/选择状态及发送中心 Playwright 回归。专项测试 `11 passed`;本机真实数据只做前端勾选回归,未保存审核、未修改排期、未触发投稿。 + ## 2026-08-13 GitHub 主页任务详情与发送中心图片更新 - 中文、英文 GitHub 主页的“任务详情 / Task detail”和“发送中心 / Publishing center”预览已分别改为用户提供的对应实机 PNG 图片。 diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 70c746c..1b2618f 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -1,5 +1,13 @@ # Next Steps +## 2026-08-15 两处全选功能检查方法 + +1. 重启 NiuMa Studio 后打开片段审核页,按 `Ctrl + F5` 强制刷新;候选列表第一张卡片上方应显示“全选当前列表”和“已启用 N / M 条”。 +2. 点击“全选当前列表”应一次勾选当前筛选结果,再次点击应全部取消;手动取消其中一条时,全选框应显示半选状态。只有点击“保存修改”“生成切片”或“保存并同步发送中心”后才会写入数据库。 +3. 打开发送中心“内容准备”,每个“原始处理任务”标题右侧应显示“全选本任务”;点击后只勾选该任务在当前平台下的内容,页面底部“已选 N 条”数量同步变化。 +4. 再选择另一任务或取消其中一条,确认两个任务的全选状态各自独立;切换抖音/B站后选择应自动清空,不允许跨平台混合操作。 +5. 手机宽度下,任务组操作应分成清楚的两行,底部批量栏应以两列按钮展示且没有横向滚动。功能验收只需检查勾选状态,除非确实要保存或排期,否则不要点击保存、设置排期或真实发送。 + ## 2026-08-13 GitHub 主页任务详情与发送中心图片检查方法 1. 打开本次 PR 的 `README.md` 预览,滚动到“产品预览”,确认“任务详情”显示任务详情图、“发送中心”显示发送中心图,且两张画面完整、位置正确。 diff --git a/app/static/css/styles.css b/app/static/css/styles.css index 9244ac8..f00f846 100644 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -1348,6 +1348,37 @@ fieldset input.visually-hidden-file { gap: 10px; } +.clip-bulk-select { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + position: sticky; + top: 76px; + z-index: 4; + padding: 10px 12px; + border: 1px solid rgba(31, 111, 255, 0.18); + border-radius: 12px; + background: rgba(247, 250, 255, 0.94); + box-shadow: 0 8px 24px rgba(31, 77, 143, 0.08); + backdrop-filter: blur(14px); +} + +.clip-bulk-select-control { + min-height: 32px; + padding: 4px 8px; + border: 0; + background: transparent; + cursor: pointer; + font-weight: 750; +} + +.clip-bulk-select-count { + color: var(--muted); + font-size: 13px; + font-weight: 700; +} + .clip-card { gap: 11px; padding: 12px; @@ -5182,6 +5213,26 @@ td a, } .publish-task-group-identity small { color: var(--muted); } .publish-task-group-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; } +.publish-task-group-select { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 36px; + padding: 7px 10px; + border: 1px solid rgba(31, 111, 255, 0.18); + border-radius: 10px; + background: rgba(255, 255, 255, 0.76); + color: var(--blue); + cursor: pointer; + font-size: 13px; + font-weight: 750; + white-space: nowrap; +} +.publish-task-group-select input { width: auto; min-height: 0; margin: 0; } +.publish-task-group-select:has(input:checked) { + border-color: rgba(31, 111, 255, 0.34); + background: var(--blue-soft); +} .publish-task-group-body { display: grid; gap: 12px; } .publish-task-group-body[hidden] { display: none; } @@ -5633,7 +5684,15 @@ td a, .publish-content-grid .span-2, .publish-bilibili-fields.span-2, .publish-bilibili-fields .span-2 { grid-column: 1; } .publish-card-heading { display: grid; } .publish-task-group-header { align-items: flex-start; flex-direction: column; } - .publish-task-group-actions { width: 100%; justify-content: space-between; } + .publish-task-group-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + width: 100%; + gap: 8px; + } + .publish-task-group-select { justify-self: start; } + .publish-task-group-actions .text-button { align-self: center; white-space: nowrap; } + .publish-task-group-actions [data-task-group-toggle] { grid-column: 1 / -1; width: 100%; } .publish-task-group-identity p strong { max-width: 70vw; } .publish-card-heading .publish-card-heading-actions { justify-content: flex-start; } .publish-plan-row, .publish-execution-row { grid-template-columns: 28px minmax(0, 1fr); } @@ -5653,4 +5712,29 @@ td a, .publish-history-view-switch { width: 100%; } .publish-history-view-switch .secondary-button { flex: 1; } .publish-history-toolbar-actions .compact-filter { width: 100%; } + .publish-selection-bar { + left: 14px; + right: 14px; + bottom: 14px; + width: auto; + max-height: calc(100vh - 28px); + transform: none; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow-x: hidden; + overflow-y: auto; + padding: 12px; + } + .publish-selection-bar select { + grid-column: 1 / -1; + width: 100%; + min-width: 0; + } + .publish-selection-bar button { + width: 100%; + min-width: 0; + height: auto; + white-space: normal; + } + .publish-selection-bar [data-clear-selection] { grid-column: 1 / -1; } } diff --git a/app/static/js/app.js b/app/static/js/app.js index d3fbbb7..20ff1d4 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -747,6 +747,9 @@ const clipReviewForm = document.querySelector("#clip-review-form"); const saveClipsButton = document.querySelector("#save-clips-button"); const generateClipsButton = document.querySelector("#generate-clips-button"); const clipReviewMessage = document.querySelector("#clip-review-message"); +const clipSelectAll = document.querySelector("[data-clip-select-all]"); +const clipSelectAllLabel = document.querySelector("[data-clip-select-all-label]"); +const clipSelectCount = document.querySelector("[data-clip-select-count]"); const clipPreviewVideo = document.querySelector("#clip-preview-video"); const clipPreviewDock = document.querySelector("#clip-preview-dock"); const clipPreviewCaption = document.querySelector("#clip-preview-caption"); @@ -786,10 +789,30 @@ function getClipReviewCards() { return Array.from(document.querySelectorAll("[data-clip-card]")); } +function getClipEnableCheckboxes() { + return getClipReviewCards() + .map((card) => card.querySelector("[name='enabled']")) + .filter(Boolean); +} + +function updateClipSelectAllUi() { + const checkboxes = getClipEnableCheckboxes(); + const enabledCount = checkboxes.filter((checkbox) => checkbox.checked).length; + const allEnabled = checkboxes.length > 0 && enabledCount === checkboxes.length; + if (clipSelectAll) { + clipSelectAll.disabled = checkboxes.length === 0; + clipSelectAll.checked = allEnabled; + clipSelectAll.indeterminate = enabledCount > 0 && !allEnabled; + } + if (clipSelectAllLabel) clipSelectAllLabel.textContent = allEnabled ? "取消全选" : "全选当前列表"; + if (clipSelectCount) clipSelectCount.textContent = `已启用 ${enabledCount} / ${checkboxes.length} 条`; +} + function updateClipReviewActionState() { const hasCards = getClipReviewCards().length > 0; if (saveClipsButton) saveClipsButton.disabled = !hasCards; if (generateClipsButton) generateClipsButton.disabled = !hasCards; + updateClipSelectAllUi(); } function collectClipReviewPayload() { @@ -1173,6 +1196,26 @@ document.querySelectorAll("[data-clip-card] input[name='start_time'], [data-clip input.addEventListener("change", () => updateCardTimeDataset(input.closest("[data-clip-card]"))); }); +if (clipSelectAll) { + clipSelectAll.addEventListener("change", () => { + const shouldEnable = clipSelectAll.checked; + getClipEnableCheckboxes().forEach((checkbox) => { checkbox.checked = shouldEnable; }); + updateClipSelectAllUi(); + showClipReviewMessage( + shouldEnable + ? "已全选当前列表。确认无误后,请点击“保存修改”写入数据库。" + : "已取消当前列表的全部选择。确认无误后,请点击“保存修改”写入数据库。", + "info", + ); + }); +} + +clipReviewForm?.addEventListener("change", (event) => { + if (event.target.matches("[data-clip-card] input[name='enabled']")) updateClipSelectAllUi(); +}); + +updateClipSelectAllUi(); + if (saveClipsButton && clipReviewForm) { saveClipsButton.addEventListener("click", async () => { const taskId = clipReviewForm.dataset.taskId; diff --git a/app/static/js/publish-center.js b/app/static/js/publish-center.js index df159b5..ae17918 100644 --- a/app/static/js/publish-center.js +++ b/app/static/js/publish-center.js @@ -297,6 +297,7 @@ if (publishCenterRoot) { group.hidden = visibleCount === 0; if (visibleCount > 0) visibleGroups.push(group); if (visibleCount === 0) setTaskGroupExpanded(group, false); + syncTaskGroupSelectionUi(group); }); if (visibleGroups.length && !visibleGroups.some((group) => group.dataset.expanded === "true")) { setTaskGroupExpanded(visibleGroups[0], true); @@ -1020,6 +1021,26 @@ if (publishCenterRoot) { queueHistoryRefresh(true); } + function visibleTaskGroupRows(group) { + if (!group) return []; + return Array.from(group.querySelectorAll('[data-publish-row][data-section="content"]')).filter( + (row) => !row.hidden && row.dataset.platform === activePlatform, + ); + } + + function syncTaskGroupSelectionUi(group) { + const checkbox = group?.querySelector("[data-task-group-select]"); + if (!checkbox) return; + const rows = visibleTaskGroupRows(group); + const selectedCount = rows.filter((row) => selectedJobIds.has(row.dataset.jobId)).length; + const allSelected = rows.length > 0 && selectedCount === rows.length; + checkbox.disabled = rows.length === 0; + checkbox.checked = allSelected; + checkbox.indeterminate = selectedCount > 0 && !allSelected; + const label = group.querySelector("[data-task-group-select-label]"); + if (label) label.textContent = allSelected ? "取消全选" : "全选本任务"; + } + function updateSelectionUi() { Array.from(selectedJobIds).forEach((jobId) => { const row = document.querySelector(`[data-publish-row][data-job-id="${CSS.escape(jobId)}"]`); @@ -1032,6 +1053,7 @@ if (publishCenterRoot) { if (selectedCountNode) selectedCountNode.textContent = String(count); if (drawerCount) drawerCount.textContent = String(count); if (selectionBar) selectionBar.hidden = count === 0; + document.querySelectorAll("[data-publish-task-group]").forEach(syncTaskGroupSelectionUi); } function setActivePlatform(nextPlatform) { @@ -1507,6 +1529,16 @@ if (publishCenterRoot) { updateHistorySelectionUi(); return; } + const taskGroupCheckbox = event.target.closest("[data-task-group-select]"); + if (taskGroupCheckbox) { + const group = taskGroupCheckbox.closest("[data-publish-task-group]"); + visibleTaskGroupRows(group).forEach((row) => { + if (taskGroupCheckbox.checked) selectedJobIds.add(row.dataset.jobId); + else selectedJobIds.delete(row.dataset.jobId); + }); + updateSelectionUi(); + return; + } const checkbox = event.target.closest("[data-publish-select]"); if (checkbox) { const row = checkbox.closest("[data-publish-row]"); diff --git a/app/templates/clip_review.html b/app/templates/clip_review.html index 309c3be..066cf02 100644 --- a/app/templates/clip_review.html +++ b/app/templates/clip_review.html @@ -29,6 +29,13 @@

片段审核 · {{ task.title }}

{% if clips %} +
+ + 已启用 {{ clips|selectattr('enabled')|list|length }} / {{ clips|length }} 条 +
{% for clip in clips %}
{{ group.task_name }} 创建于 {{ group.task_created_at_display }} · 0 条待准备
+ 查看任务详情