修复历史返款不可查看的BUG,修复问题认领对应逻辑问题

This commit is contained in:
db_huang 2026-09-16 14:32:31 +08:00
parent 73ac3e8163
commit 7f7311a47c
6 changed files with 159 additions and 22 deletions

View File

@ -1480,6 +1480,11 @@ def _sync_ask_task_status(task: AskTask) -> None:
def _ask_task_data(task: AskTask, include_questions: bool = True) -> dict: def _ask_task_data(task: AskTask, include_questions: bool = True) -> dict:
questions = sorted(task.questions, key=lambda value: value.sequence_no) questions = sorted(task.questions, key=lambda value: value.sequence_no)
selected_questions = [q for q in questions if q.selected] selected_questions = [q for q in questions if q.selected]
question_claimants = {
item.question_id: assignment.user
for assignment in task.assignments
for item in assignment.items
}
# Keep every submission batch as an independent child row. Falling back # Keep every submission batch as an independent child row. Falling back
# to the assignment row preserves compatibility with legacy assignments # to the assignment row preserves compatibility with legacy assignments
# created before batch submissions were introduced. # created before batch submissions were introduced.
@ -1542,7 +1547,24 @@ def _ask_task_data(task: AskTask, include_questions: bool = True) -> dict:
# the ambiguous placeholder "--". Claimed child rows still expose # the ambiguous placeholder "--". Claimed child rows still expose
# the actual answer user's public ID. # the actual answer user's public ID.
"user_id": task.owner.display_name if task.owner else "运营后台", "phone": "", "questions": [], "user_id": task.owner.display_name if task.owner else "运营后台", "phone": "", "questions": [],
"selected_questions": ([{"id": q.id, "sequence_no": q.sequence_no, "question": q.question_text, "answer": "", "product_title": task.product_name, "source": q.source or "AI生成"} for q in selected_questions] if include_questions else []), "selected_questions": ([{
"id": q.id,
"sequence_no": q.sequence_no,
"question": q.question_text,
"answer": "",
"product_title": task.product_name,
"source": q.source or "AI生成",
"user_id": (
_public_user_id(question_claimants[q.id])
if q.id in question_claimants
else ""
),
"phone": (
_public_user_account(question_claimants[q.id])
if q.id in question_claimants
else ""
),
} for q in selected_questions] if include_questions else []),
# One user can contribute several submission batches. Keep the # One user can contribute several submission batches. Keep the
# parent detail summary as a unique-user list; the child rows below # parent detail summary as a unique-user list; the child rows below
# still expose every batch independently. # still expose every batch independently.
@ -1774,6 +1796,20 @@ def _start_ask_reservation(db: Session, task: AskTask, assignment: AskAssignment
@app.get("/api/ask/tasks/{task_no}") @app.get("/api/ask/tasks/{task_no}")
def public_ask_task(task_no: str, token: str, refresh: bool = False, db: Session = Depends(get_db)): def public_ask_task(task_no: str, token: str, refresh: bool = False, db: Session = Depends(get_db)):
user = _answer_user_from_token(token, db) user = _answer_user_from_token(token, db)
completed_task = db.scalar(select(AskTask).where(
(AskTask.public_token == task_no) | (AskTask.task_no == task_no),
AskTask.status == "已完成",
))
if completed_task:
completed_assignment = db.scalar(select(AskAssignment).where(
AskAssignment.task_id == completed_task.id,
AskAssignment.user_id == user.id,
))
if completed_assignment:
raise HTTPException(
status_code=409,
detail="本次提问任务已全部完成,可查看历史状态",
)
task = _published_ask_task(task_no, db, lock=True) task = _published_ask_task(task_no, db, lock=True)
current_assignment = db.scalar(select(AskAssignment).where(AskAssignment.task_id == task.id, AskAssignment.user_id == user.id)) current_assignment = db.scalar(select(AskAssignment).where(AskAssignment.task_id == task.id, AskAssignment.user_id == user.id))
current_expired = _ask_assignment_expired(current_assignment) current_expired = _ask_assignment_expired(current_assignment)

View File

@ -946,6 +946,12 @@ def test_approved_answer_appears_as_pending_refund_with_detail(monkeypatch):
assert refunded.status_code == 200 assert refunded.status_code == 200
assert refunded.json()["data"]["status_label"] == "已完成" assert refunded.json()["data"]["status_label"] == "已完成"
assert refunded.json()["data"]["refund_endpoint"] is None assert refunded.json()["data"]["refund_endpoint"] is None
user_detail = client.get(
f"/api/answer/me/submissions/{created['id']}?token={token}&owner_token={owner_token}"
)
assert user_detail.status_code == 200
assert user_detail.json()["data"]["status"] == "已完成"
assert len(user_detail.json()["data"]["refund_screenshots"]) == 1
board_after_refund = task_board_items(client, page_size=100) board_after_refund = task_board_items(client, page_size=100)
completed_payout = next( completed_payout = next(
item item
@ -1128,3 +1134,31 @@ def test_ask_batches_survive_reservation_timeout_and_parent_completes_only_after
assert completed_parent["status_label"] == "已完成" assert completed_parent["status_label"] == "已完成"
assert completed_parent["completed_count"] == 2 assert completed_parent["completed_count"] == 2
assert len(completed_parent["claimed_tasks"]) == 2 assert len(completed_parent["claimed_tasks"]) == 2
parent_detail = client.get(completed_parent["detail_endpoint"])
assert parent_detail.status_code == 200
assert {
question["user_id"]
for question in parent_detail.json()["data"]["selected_questions"]
} == {session["user_id"]}
completed_link = client.get(task_url)
assert completed_link.status_code == 409
assert completed_link.json()["detail"] == "本次提问任务已全部完成,可查看历史状态"
history = client.get(
f"/api/ask/me/assignments?token={session['token']}"
)
assert history.status_code == 200
completed_batches = [
item for item in history.json()["data"]
if item["task_id"] == generated["task_id"] and item["status"] == "已完成"
]
assert len(completed_batches) == 2
other_session = client.post(
"/api/ask/auth/taobao-login",
json={"taobao_id": f"tb{uuid4().hex[:14]}"},
).json()["data"]
unrelated_user = client.get(
f"/api/ask/tasks/{generated['task_id']}?token={other_session['token']}"
)
assert unrelated_user.status_code == 404

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Check, ClipboardCheck, Clock3, Copy, FileCheck2, History, ImagePlus, LogOut, MessageCircle, RefreshCw, ShieldCheck } from 'lucide-react' import { Check, ClipboardCheck, Clock3, Copy, Eye, FileCheck2, History, ImagePlus, LogOut, MessageCircle, ReceiptText, RefreshCw, ShieldCheck } from 'lucide-react'
const SESSION_KEY = 'answer_user_taobao_session' const SESSION_KEY = 'answer_user_taobao_session'
const STATUS_TEXT = { '待筛选': '截图审核中', '识别失败': '运营处理中', '待运营派发': '截图审核中', '已派发': '截图审核中', '待用户完成': '待完成回答', '待审核': '完成审核中', '已通过': '已完成', '已完成': '已返款', '未通过': '审核不通过', '已撤销': '任务已撤销' } const STATUS_TEXT = { '待筛选': '截图审核中', '识别失败': '运营处理中', '待运营派发': '截图审核中', '已派发': '截图审核中', '待用户完成': '待完成回答', '待审核': '完成审核中', '已通过': '已完成', '已完成': '已返款', '未通过': '审核不通过', '已撤销': '任务已撤销' }
@ -121,15 +121,16 @@ function FinalStage({ latest, reload, busy, reopen, startNew }) {
</> </>
} }
function HistoryPage({ records, close, onContinue }) { function HistoryPage({ records, close, onOpen, openingId }) {
const canContinue = status => ['待筛选', '识别失败', '待运营派发', '已派发', '待用户完成', '未通过'].includes(status) const canContinue = status => ['待筛选', '识别失败', '待运营派发', '已派发', '待用户完成', '未通过'].includes(status)
return <section className="answer-v2-history"><header><button onClick={close}>← 返回任务</button><h2>任务状态 / 上传历史({records.length})</h2></header>{records.length === 0 ? <div className="answer-v2-card">暂无历史记录</div> : records.map(record => <article className="answer-v2-card" key={record.id}><header><strong>{record.submission_no}</strong><span>{STATUS_TEXT[record.status] || record.status}</span></header><p>{formatTime(record.created_at)} · 识别问题 {record.questions.length} 条 · 截图 {record.invited_screenshots.length} 张</p><div>{record.invited_screenshots.slice(0, 4).map(image => { const src = image.url || image.data_url; return <img key={image.id || src} src={src}/> })}</div>{canContinue(record.status) && <button className="answer-v2-history-continue" onClick={() => onContinue(record)}>↗ 继续此任务</button>}</article>)}</section> const actionText = record => record.status === '已完成' ? '查看返款凭证' : canContinue(record.status) ? '继续此任务' : '查看任务详情'
return <section className="answer-v2-history"><header><button onClick={close}>← 返回任务</button><h2>任务状态 / 上传历史({records.length})</h2></header>{records.length === 0 ? <div className="answer-v2-card">暂无历史记录</div> : records.map(record => <article className="answer-v2-card" key={record.id}><header><strong>{record.submission_no}</strong><span>{STATUS_TEXT[record.status] || record.status}</span></header><p>{formatTime(record.created_at)} · 识别问题 {record.questions.length} 条 · 截图 {record.invited_screenshots.length} 张</p><div>{record.invited_screenshots.slice(0, 4).map(image => { const src = image.url || image.data_url; return <img key={image.id || src} src={src} alt="被邀请回答截图"/> })}</div><button className="answer-v2-history-continue" onClick={() => onOpen(record)} disabled={openingId === record.id}>{record.status === '已完成' ? <ReceiptText size={14}/> : <Eye size={14}/>} {openingId === record.id ? '正在读取…' : actionText(record)}</button></article>)}</section>
} }
export default function AnswerUserPortalV2({ api, linkToken = '' }) { export default function AnswerUserPortalV2({ api, linkToken = '' }) {
const [session, setSession] = useState(() => { try { return JSON.parse(localStorage.getItem(SESSION_KEY) || 'null') } catch { return null } }) const [session, setSession] = useState(() => { try { return JSON.parse(localStorage.getItem(SESSION_KEY) || 'null') } catch { return null } })
const [records, setRecords] = useState([]); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [loaded, setLoaded] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [retrying, setRetrying] = useState(false); const [newTaskMode, setNewTaskMode] = useState(false); const [activeRecordId, setActiveRecordId] = useState(null); const [optimisticStatuses, setOptimisticStatuses] = useState({}) const [records, setRecords] = useState([]); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [loaded, setLoaded] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [retrying, setRetrying] = useState(false); const [newTaskMode, setNewTaskMode] = useState(false); const [activeRecordId, setActiveRecordId] = useState(null); const [optimisticStatuses, setOptimisticStatuses] = useState({}); const [openingId, setOpeningId] = useState(null)
const imageLoadIds = useRef(new Set()) const imageLoadPromises = useRef(new Map())
const loadVersion = useRef(0) const loadVersion = useRef(0)
async function load(silent = false) { async function load(silent = false) {
if (!session) return if (!session) return
@ -182,13 +183,34 @@ export default function AnswerUserPortalV2({ api, linkToken = '' }) {
async function loadSubmissionImages(record) { async function loadSubmissionImages(record) {
if (!record?.id) return if (!record?.id) return
const loadKey = `${record.id}:${record.status}` const loadKey = `${record.id}:${record.status}`
if (imageLoadIds.current.has(loadKey)) return if (record._imagesLoadedFor === record.status) return record
imageLoadIds.current.add(loadKey) const pending = imageLoadPromises.current.get(loadKey)
try { if (pending) return pending
const ownerQuery = linkToken ? `&owner_token=${encodeURIComponent(linkToken)}` : '' const request = (async () => {
const result = await api(`/api/answer/me/submissions/${record.id}?token=${encodeURIComponent(session.token)}${ownerQuery}`) try {
setRecords(current => current.map(item => item.id === record.id ? { ...item, ...result.data, _imagesLoaded: true } : item)) const ownerQuery = linkToken ? `&owner_token=${encodeURIComponent(linkToken)}` : ''
} catch (err) { imageLoadIds.current.delete(loadKey); setError(err.message) } const result = await api(`/api/answer/me/submissions/${record.id}?token=${encodeURIComponent(session.token)}${ownerQuery}`)
const detailed = { ...result.data, _imagesLoadedFor: result.data.status }
setRecords(current => current.map(item => item.id === record.id ? { ...item, ...detailed } : item))
return detailed
} catch (err) {
setError(err.message)
return null
} finally {
imageLoadPromises.current.delete(loadKey)
}
})()
imageLoadPromises.current.set(loadKey, request)
return request
}
async function openHistoryRecord(record) {
if (openingId !== null) return
setOpeningId(record.id); setError('')
const detailed = await loadSubmissionImages(record)
setOpeningId(null)
if (!detailed) return
setActiveRecordId(record.id); setRetrying(record.status === '未通过'); setNewTaskMode(false); setHistoryOpen(false)
window.scrollTo({ top: 0, behavior: 'smooth' })
} }
const latestRecord = records.find(record => record.id === activeRecordId) || records[0] const latestRecord = records.find(record => record.id === activeRecordId) || records[0]
const latest = latestRecord && optimisticStatuses[latestRecord.id] ? { ...latestRecord, status: optimisticStatuses[latestRecord.id] } : latestRecord const latest = latestRecord && optimisticStatuses[latestRecord.id] ? { ...latestRecord, status: optimisticStatuses[latestRecord.id] } : latestRecord
@ -212,9 +234,9 @@ export default function AnswerUserPortalV2({ api, linkToken = '' }) {
}, [visibleLatest?.id, visibleLatest?.status, historyOpen, newTaskMode]) }, [visibleLatest?.id, visibleLatest?.status, historyOpen, newTaskMode])
useEffect(() => { useEffect(() => {
if (!historyOpen) return if (!historyOpen) return
records.slice(0, 20).forEach(record => loadSubmissionImages(record)) records.slice(0, 20).forEach(record => { loadSubmissionImages(record) })
}, [historyOpen, records.length]) }, [historyOpen, records.length])
if (!session) return <Login api={api} onLogin={setSession} linkToken={linkToken}/> if (!session) return <Login api={api} onLogin={setSession} linkToken={linkToken}/>
return <main className="answer-v2-shell"><section className="answer-v2-app"><Top session={session} latest={visibleLatest} logout={logout}/><div className="answer-v2-tabs"><button className={!historyOpen ? 'active' : ''} onClick={() => setHistoryOpen(false)}><ClipboardCheck size={15}/>任务认领</button><button className={historyOpen ? 'active' : ''} onClick={() => setHistoryOpen(true)}><History size={15}/>任务状态 <small>{visibleLatest ? STATUS_TEXT[visibleLatest.status] || visibleLatest.status : '待上传'}</small></button></div>{!historyOpen && <Progress status={visibleLatest?.status}/>}<div className="answer-v2-body">{error && <div className="answer-v2-error">{error}</div>}{!loaded ? <div className="answer-v2-card answer-v2-loading">正在读取任务状态…</div> : historyOpen ? <HistoryPage records={records} close={() => setHistoryOpen(false)} onContinue={record => { setActiveRecordId(record.id); setRetrying(record.status === '未通过'); setNewTaskMode(false); setHistoryOpen(false) }}/> : !visibleLatest ? <UploadStage api={api} session={session} linkToken={linkToken} onUploaded={async () => { setNewTaskMode(false); setActiveRecordId(null); await load() }} setError={setError}/> : ['待筛选', '识别失败', '待运营派发', '已派发'].includes(visibleLatest.status) ? <ReviewStage latest={visibleLatest} reload={load} busy={busy} startNew={() => { setError(''); setRetrying(false); setActiveRecordId(null); setHistoryOpen(false); setNewTaskMode(true); window.scrollTo({ top: 0, behavior: 'smooth' }) }}/> : visibleLatest.status === '待用户完成' || (visibleLatest.status === '未通过' && retrying) ? <AssignmentStage api={api} session={session} latest={visibleLatest} linkToken={linkToken} reload={async submitted => { if (submitted) { applySubmittedRecord(submitted); setRetrying(false); load(true); return } await load(); setRetrying(false) }} setError={setError}/> : <FinalStage latest={visibleLatest} reload={load} busy={busy} reopen={() => setRetrying(true)} startNew={() => { setError(''); setRetrying(false); setActiveRecordId(null); setHistoryOpen(false); setNewTaskMode(true); window.scrollTo({ top: 0, behavior: 'smooth' }) }}/>}</div><footer className="answer-v2-footer"><button onClick={() => setHistoryOpen(true)}>上传历史({records.length})</button>{visibleLatest && <button className="primary" onClick={() => load()}><RefreshCw size={14}/>刷新状态</button>}</footer></section></main> return <main className="answer-v2-shell"><section className="answer-v2-app"><Top session={session} latest={visibleLatest} logout={logout}/><div className="answer-v2-tabs"><button className={!historyOpen ? 'active' : ''} onClick={() => setHistoryOpen(false)}><ClipboardCheck size={15}/>任务认领</button><button className={historyOpen ? 'active' : ''} onClick={() => setHistoryOpen(true)}><History size={15}/>任务状态 <small>{visibleLatest ? STATUS_TEXT[visibleLatest.status] || visibleLatest.status : '待上传'}</small></button></div>{!historyOpen && <Progress status={visibleLatest?.status}/>}<div className="answer-v2-body">{error && <div className="answer-v2-error">{error}</div>}{!loaded ? <div className="answer-v2-card answer-v2-loading">正在读取任务状态…</div> : historyOpen ? <HistoryPage records={records} close={() => setHistoryOpen(false)} onOpen={openHistoryRecord} openingId={openingId}/> : !visibleLatest ? <UploadStage api={api} session={session} linkToken={linkToken} onUploaded={async () => { setNewTaskMode(false); setActiveRecordId(null); await load() }} setError={setError}/> : ['待筛选', '识别失败', '待运营派发', '已派发'].includes(visibleLatest.status) ? <ReviewStage latest={visibleLatest} reload={load} busy={busy} startNew={() => { setError(''); setRetrying(false); setActiveRecordId(null); setHistoryOpen(false); setNewTaskMode(true); window.scrollTo({ top: 0, behavior: 'smooth' }) }}/> : visibleLatest.status === '待用户完成' || (visibleLatest.status === '未通过' && retrying) ? <AssignmentStage api={api} session={session} latest={visibleLatest} linkToken={linkToken} reload={async submitted => { if (submitted) { applySubmittedRecord(submitted); setRetrying(false); load(true); return } await load(); setRetrying(false) }} setError={setError}/> : <FinalStage latest={visibleLatest} reload={load} busy={busy} reopen={() => setRetrying(true)} startNew={() => { setError(''); setRetrying(false); setActiveRecordId(null); setHistoryOpen(false); setNewTaskMode(true); window.scrollTo({ top: 0, behavior: 'smooth' }) }}/>}</div><footer className="answer-v2-footer"><button onClick={() => setHistoryOpen(true)}>上传历史({records.length})</button>{visibleLatest && <button className="primary" onClick={() => load()}><RefreshCw size={14}/>刷新状态</button>}</footer></section></main>
} }

View File

@ -14,6 +14,7 @@ import {
const SESSION_KEY = "ask_user_taobao_session"; const SESSION_KEY = "ask_user_taobao_session";
const EXHAUSTED_MESSAGE = "本次任务已被领取完,请关注下一个任务!"; const EXHAUSTED_MESSAGE = "本次任务已被领取完,请关注下一个任务!";
const COMPLETED_TASK_MESSAGE = "本次提问任务已全部完成,可查看历史状态";
const INVALID_TASK_MESSAGE = "提问任务链接无效、未发布或已结束"; const INVALID_TASK_MESSAGE = "提问任务链接无效、未发布或已结束";
const fileKey = (file) => `${file.name}-${file.size}-${file.lastModified}`; const fileKey = (file) => `${file.name}-${file.size}-${file.lastModified}`;
const accountName = (session) => const accountName = (session) =>
@ -192,6 +193,30 @@ function InvalidTask({ message, onReload }) {
); );
} }
function CompletedTask({ session, onOpenHistory, busy, error }) {
return (
<main className="ask-user-shell">
<section className="ask-user-app ask-invalid-app">
<header className="ask-user-topbar">
<span><ShieldCheck size={14} /> 淘宝问答运营官方承接通道</span>
<em>官方认证</em>
</header>
<section className="ask-invalid-card ask-completed-card">
<span className="ask-invalid-icon"><PackageCheck size={34} /></span>
<em>任务已完成</em>
<h1>本次提问任务已全部完成</h1>
<p>当前任务的所有问题均已完成,您可以前往历史状态查看自己提交过的任务和审核进度。</p>
<small>当前淘宝ID:{accountName(session)}</small>
{error && <div className="ask-user-error">{error}</div>}
<button type="button" onClick={onOpenHistory} disabled={busy}>
{busy ? "正在读取历史…" : "查看历史状态"}
</button>
</section>
</section>
</main>
);
}
function ExpiredAssignment({ session, onReload }) { function ExpiredAssignment({ session, onReload }) {
return ( return (
<main className="ask-user-shell"> <main className="ask-user-shell">
@ -762,14 +787,14 @@ function TaskStatusPage({ history, onView }) {
["待选择", "进行中", "待审核"].includes(item.status), ["待选择", "进行中", "待审核"].includes(item.status),
).length; ).length;
const completed = history.filter((item) => const completed = history.filter((item) =>
["已通过", "未通过"].includes(item.status), ["已通过", "已完成", "未通过"].includes(item.status),
).length; ).length;
const filtered = history.filter( const filtered = history.filter(
(item) => (item) =>
filter === "all" || filter === "all" ||
(filter === "progress" (filter === "progress"
? ["待选择", "进行中", "待审核"].includes(item.status) ? ["待选择", "进行中", "待审核"].includes(item.status)
: ["已通过", "未通过"].includes(item.status)), : ["已通过", "已完成", "未通过"].includes(item.status)),
); );
return ( return (
<div className="ask-status-page"> <div className="ask-status-page">
@ -801,7 +826,7 @@ function TaskStatusPage({ history, onView }) {
) : ( ) : (
filtered.map((item) => ( filtered.map((item) => (
<article <article
className={`ask-history-item ${item.status === "待审核" ? "pending" : item.status === "已通过" ? "done" : ""}`} className={`ask-history-item ${item.status === "待审核" ? "pending" : ["已通过", "已完成"].includes(item.status) ? "done" : ""}`}
key={item.assignment_id} key={item.assignment_id}
> >
<header> <header>
@ -829,8 +854,8 @@ function TaskStatusPage({ history, onView }) {
<span> <span>
{item.status === "待审核" {item.status === "待审核"
? "凭证已锁定,专员核验中" ? "凭证已锁定,专员核验中"
: item.status === "已通过" : ["已通过", "已完成"].includes(item.status)
? "审核已通过" ? item.status === "已完成" ? "任务已完成" : "审核已通过"
: item.status === "未通过" : item.status === "未通过"
? "需要重新提交" ? "需要重新提交"
: "等待选择问题"} : "等待选择问题"}
@ -863,6 +888,7 @@ export default function AskUserPortal({ api }) {
const [claimEdit, setClaimEdit] = useState(false); const [claimEdit, setClaimEdit] = useState(false);
const [exhausted, setExhausted] = useState(false); const [exhausted, setExhausted] = useState(false);
const [invalidTask, setInvalidTask] = useState(""); const [invalidTask, setInvalidTask] = useState("");
const [completedTask, setCompletedTask] = useState(false);
const [expiredAssignment, setExpiredAssignment] = useState(false); const [expiredAssignment, setExpiredAssignment] = useState(false);
const [selectionExpired, setSelectionExpired] = useState(false); const [selectionExpired, setSelectionExpired] = useState(false);
@ -877,6 +903,7 @@ export default function AskUserPortal({ api }) {
setAssignment(result.data); setAssignment(result.data);
setExhausted(false); setExhausted(false);
setInvalidTask(""); setInvalidTask("");
setCompletedTask(false);
setExpiredAssignment(false); setExpiredAssignment(false);
setSelectionExpired(false); setSelectionExpired(false);
// Keep the current view. In particular, polling a pending review must // Keep the current view. In particular, polling a pending review must
@ -901,6 +928,11 @@ export default function AskUserPortal({ api }) {
setExhausted(true); setExhausted(true);
setInvalidTask(""); setInvalidTask("");
setError(""); setError("");
} else if (requestError.message === COMPLETED_TASK_MESSAGE) {
setAssignment(null);
setCompletedTask(true);
setInvalidTask("");
setError("");
} else if (isInvalidTaskError(requestError.message)) { } else if (isInvalidTaskError(requestError.message)) {
setAssignment(null); setAssignment(null);
setInvalidTask(requestError.message); setInvalidTask(requestError.message);
@ -918,14 +950,18 @@ export default function AskUserPortal({ api }) {
} }
} }
async function loadHistory() { async function loadHistory() {
setBusy(true);
try { try {
const result = await api( const result = await api(
`/api/ask/me/assignments?token=${encodeURIComponent(session.token)}`, `/api/ask/me/assignments?token=${encodeURIComponent(session.token)}`,
); );
setHistory(result.data); setHistory(result.data);
setView("status"); setView("status");
setError("");
} catch (requestError) { } catch (requestError) {
setError(requestError.message); setError(requestError.message);
} finally {
setBusy(false);
} }
} }
useEffect(() => { useEffect(() => {
@ -944,12 +980,15 @@ export default function AskUserPortal({ api }) {
setAssignment(null); setAssignment(null);
setExhausted(false); setExhausted(false);
setInvalidTask(""); setInvalidTask("");
setCompletedTask(false);
setExpiredAssignment(false); setExpiredAssignment(false);
setSelectionExpired(false); setSelectionExpired(false);
}; };
if (!session) return <TaobaoLogin api={api} onLogin={setSession} />; if (!session) return <TaobaoLogin api={api} onLogin={setSession} />;
if (exhausted) if (exhausted)
return <TaskExhausted session={session} onLogout={logout} />; return <TaskExhausted session={session} onLogout={logout} />;
if (completedTask && view !== "status" && !assignment)
return <CompletedTask session={session} onOpenHistory={loadHistory} busy={busy} error={error} />;
if (invalidTask) if (invalidTask)
return <InvalidTask message={invalidTask} onReload={() => loadAssignment()} />; return <InvalidTask message={invalidTask} onReload={() => loadAssignment()} />;
if (expiredAssignment) if (expiredAssignment)

View File

@ -281,8 +281,8 @@ function TaskDetail({ task, close, onConfirmRefund, onCreateRefundUpload, onPoll
className={`task-detail-cell${index ? " row-border" : ""}`} className={`task-detail-cell${index ? " row-border" : ""}`}
style={{ gridColumn: 3, gridRow: index + 1 }} style={{ gridColumn: 3, gridRow: index + 1 }}
> >
{task.user_id} {question.user_id || "--"}
{task.phone && task.phone !== task.user_id && <small>{maskPhone(task.phone)}</small>} {question.phone && question.phone !== question.user_id && <small>{maskPhone(question.phone)}</small>}
</span> </span>
<span <span
className={`task-detail-cell${index ? " row-border" : ""}`} className={`task-detail-cell${index ? " row-border" : ""}`}

View File

@ -4939,6 +4939,12 @@
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 7px 16px rgba(40, 100, 232, 0.32); box-shadow: 0 7px 16px rgba(40, 100, 232, 0.32);
} }
.answer-v2-history-continue:disabled {
cursor: wait;
opacity: 0.7;
transform: none;
box-shadow: none;
}
.ask-task-dashboard { .ask-task-dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;