修复历史返款不可查看的BUG,修复问题认领对应逻辑问题
This commit is contained in:
parent
73ac3e8163
commit
7f7311a47c
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
if (pending) return pending
|
||||||
|
const request = (async () => {
|
||||||
try {
|
try {
|
||||||
const ownerQuery = linkToken ? `&owner_token=${encodeURIComponent(linkToken)}` : ''
|
const ownerQuery = linkToken ? `&owner_token=${encodeURIComponent(linkToken)}` : ''
|
||||||
const result = await api(`/api/answer/me/submissions/${record.id}?token=${encodeURIComponent(session.token)}${ownerQuery}`)
|
const result = await api(`/api/answer/me/submissions/${record.id}?token=${encodeURIComponent(session.token)}${ownerQuery}`)
|
||||||
setRecords(current => current.map(item => item.id === record.id ? { ...item, ...result.data, _imagesLoaded: true } : item))
|
const detailed = { ...result.data, _imagesLoadedFor: result.data.status }
|
||||||
} catch (err) { imageLoadIds.current.delete(loadKey); setError(err.message) }
|
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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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" : ""}`}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user