fix: 完成态判据统一为曝光+阅读齐+交互断点三处修复

- 创作者凭证提交(manual_metrics/submit_creator_metrics)的完成判据从
  d7_likes(全库无赋值方,永不触发)改为 exposure+views 齐,与采集侧口径
  对齐;管理端手动回填后同步 refreshTaskStatus 联动任务状态
- 回收队列头部改为「已发布 N / 总数」口径,未回填链接行不再计入
- 行内更新/补填笔记链接从原生 prompt 改为弹窗表单,失败保留已输入内容
- 顶栏「获取KOC领取链接」从导航跳转改为弹窗直接列出各任务领取链接,
  一键复制;弹窗内点击不冒泡误关
This commit is contained in:
ABAPPLO
2026-10-08 15:17:14 +08:00
parent c54649258b
commit c686003cfb
5 changed files with 275 additions and 14 deletions

View File

@@ -548,6 +548,11 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
const [ownerLinksTask, setOwnerLinksTask] = useState<Task | null>(null);
const [customerChatTask, setCustomerChatTask] = useState<Task | null>(null);
const [metricModal, setMetricModal] = useState<Distribution | null>(null);
const [publishUrlModal, setPublishUrlModal] = useState<{
distribution: Distribution;
value: string;
} | null>(null);
const [claimLinksOpen, setClaimLinksOpen] = useState(false);
const [taskForm, setTaskForm] = useState({
id: "",
platform: "小红书" as "小红书" | "抖音",
@@ -905,27 +910,29 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
const updateDistributionPublishUrl = async (
distribution: Distribution,
) => {
const publishInput = window.prompt(
distribution.publish_url
? "更新小红书笔记链接(支持长链、短链或完整分享文案)"
: "填写小红书笔记链接(支持长链、短链或完整分享文案)",
distribution.publish_url || "",
);
if (publishInput === null) return;
if (!publishInput.trim()) {
setPublishUrlModal({ distribution, value: distribution.publish_url || "" });
};
const submitPublishUrl = async (event: FormEvent) => {
event.preventDefault();
if (!publishUrlModal) return;
const publishInput = publishUrlModal.value.trim();
if (!publishInput) {
setToast("请填写笔记链接");
return;
}
await runAction(
const distribution = publishUrlModal.distribution;
const updated = await runAction(
{
action: "update_distribution_publish_url",
distributionId: distribution.id,
publishUrl: publishInput.trim(),
publishUrl: publishInput,
},
distribution.publish_url
? "笔记链接已更新,旧公开采集数据已清空"
: "笔记链接已补充,可开始数据采集",
);
if (updated) setPublishUrlModal(null);
};
const retryFailedMetrics = async (taskId: string, failedCount: number) => {
@@ -1233,7 +1240,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
</div>
<div className="top-actions">
<span className="sync-state"><i />飞书内容表已接入</span>
<button className="ghost-button" onClick={() => setActiveNav("tasks")}>
<button className="ghost-button" onClick={() => setClaimLinksOpen(true)}>
获取KOC领取链接
</button>
{isManager && (
@@ -1699,6 +1706,98 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
</div>
)}
{publishUrlModal && (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setPublishUrlModal(null)}>
<form className="modal-card compact" onSubmit={submitPublishUrl} onMouseDown={(event) => event.stopPropagation()}>
<div className="modal-heading">
<div>
<p className="eyebrow">发布链接</p>
<h2>{publishUrlModal.distribution.publish_url ? "更新笔记链接" : "填写笔记链接"}</h2>
</div>
<button type="button" onClick={() => setPublishUrlModal(null)} aria-label="关闭">×</button>
</div>
<div className="context-strip">
<strong>{publishUrlModal.distribution.account_nickname || "待识别账号"}</strong>
<span>{publishUrlModal.distribution.content_title}</span>
</div>
<div className="form-grid">
<label>
<span>笔记链接</span>
<textarea
rows={3}
value={publishUrlModal.value}
onChange={(event) =>
setPublishUrlModal({
...publishUrlModal,
value: event.target.value,
})
}
placeholder="支持长链、短链或完整分享文案"
required
/>
</label>
</div>
<div className="modal-actions">
<button type="button" className="ghost-button" onClick={() => setPublishUrlModal(null)}>取消</button>
<button className="primary-button" disabled={working}>
{publishUrlModal.distribution.publish_url ? "确认更新" : "确认填写"}
</button>
</div>
</form>
</div>
)}
{claimLinksOpen && (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setClaimLinksOpen(false)}>
<div
className="modal-card compact claim-links-modal"
onMouseDown={(event) => event.stopPropagation()}
>
<div className="modal-heading">
<div>
<p className="eyebrow">任务领取链接</p>
<h2>获取KOC领取链接</h2>
</div>
<button type="button" onClick={() => setClaimLinksOpen(false)} aria-label="关闭">×</button>
</div>
<div className="claim-links-list">
{data.tasks.map((task) => {
const url = task.share_token
? `${data.portal_url.replace(/\/$/, "")}/?task=${task.share_token}`
: "";
return (
<article key={task.id} className={url ? "" : "empty"}>
<div>
<strong>{task.name}</strong>
<span>
{task.brand}
{url ? ` · 已领取 ${task.dist_total ?? 0}/${task.quantity}` : " · 尚未生成领取链接"}
</span>
</div>
{url ? (
<button
type="button"
className="ghost-button"
onClick={() => {
void navigator.clipboard
.writeText(url)
.then(() => setToast("KOC领取链接已复制"))
.catch(() => setToast("复制失败,请稍后重试"));
}}
>
复制链接
</button>
) : (
<span className="muted">未生成</span>
)}
</article>
);
})}
</div>
</div>
</div>
)}
{groupPushTask && (
<GroupPushModal
task={groupPushTask}
@@ -6490,6 +6589,9 @@ function RecoveryPage({
const taskDistributions = distributions.filter(
(item) => item.task_id === selectedTask.id,
);
const publishedCount = taskDistributions.filter(
(item) => item.publish_url,
).length;
const recoveryEnabled = Number(selectedTask.recovery_enabled ?? 1) === 1;
const recoverySubmitted = taskDistributions.filter(
(item) => item.result_submitted_at,
@@ -6534,7 +6636,7 @@ function RecoveryPage({
<div className="panel-heading">
<div>
<h2>数据回收队列</h2>
<p>{taskDistributions.length} 篇已发布作品 · 展示最近一次采集结果</p>
<p>已发布 {publishedCount} / {taskDistributions.length} 篇 · 展示最近一次采集结果</p>
</div>
<div className="recovery-panel-actions">
<button

View File

@@ -24,6 +24,7 @@ import {
type CollectionMcpBindings,
} from "../../../lib/mcp-collection-client";
import { adminForbidden, isAdminRequest } from "../../../lib/admin-auth";
import { refreshTaskStatus } from "../../../lib/task-service";
import {
FeishuSourceError,
readFeishuSource,
@@ -663,12 +664,19 @@ export async function POST(request: Request) {
exposure = ?,
views = ?,
ocr_status = 'manual',
status = CASE WHEN d7_likes IS NOT NULL THEN 'complete' ELSE status END,
status = CASE WHEN exposure IS NOT NULL AND views IS NOT NULL THEN 'complete' ELSE status END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
)
.bind(exposure, views, distributionId)
.run();
const ownerTask = await db
.prepare("SELECT task_id FROM distributions WHERE id = ?")
.bind(distributionId)
.first<{ task_id: string }>();
if (ownerTask) {
await refreshTaskStatus(ownerTask.task_id, db).catch(() => undefined);
}
} else if (body.action === "bind_wecom_external_id") {
const forbidden = managerGate();
if (forbidden) return forbidden;

View File

@@ -1243,7 +1243,7 @@ async function handlePost(request: Request) {
exposure = ?,
views = ?,
ocr_status = 'manual',
status = CASE WHEN d7_likes IS NOT NULL THEN 'complete' ELSE status END,
status = CASE WHEN exposure IS NOT NULL AND views IS NOT NULL THEN 'complete' ELSE status END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
)

View File

@@ -1765,6 +1765,52 @@ a {
width: min(720px, 100%);
}
.modal-card.claim-links-modal {
width: min(560px, 100%);
}
.claim-links-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 52vh;
overflow-y: auto;
}
.claim-links-list article {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgb(20 40 34 / 0.08);
background: white;
}
.claim-links-list article.empty {
background: rgb(20 40 34 / 0.03);
}
.claim-links-list article > div {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.claim-links-list article strong {
font-size: 13px;
}
.claim-links-list article span {
font-size: 12px;
color: rgb(20 40 34 / 0.55);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-push-toolbar {
display: flex;
align-items: center;