diff --git a/backend/app/main.py b/backend/app/main.py index 8546208..c152ba2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1943,10 +1943,16 @@ def select_public_ask_questions(task_no: str, payload: dict, db: Session = Depen used_ids = {item.question_id for item in assignment.used_questions} if selected_ids & used_ids: raise HTTPException(status_code=400, detail="已认领的问题不能重复选择") - for item in assignment.items: - # The selection represents only the next batch. Questions in - # ``used_ids`` remain locked/read-only and are not resubmitted. - item.selected = item.question_id in selected_ids + for item in list(assignment.items): + # Keep historical submitted questions for batch details, retain only + # the questions confirmed for this batch, and immediately return all + # other provisional questions to the public pool. + if item.question_id in used_ids: + item.selected = False + elif item.question_id in selected_ids: + item.selected = True + else: + db.delete(item) assignment.status = "进行中" assignment.reservation_started_at = datetime.utcnow() assignment.updated_at = assignment.reservation_started_at diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index e691c06..b8020bc 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1138,6 +1138,57 @@ def test_public_ask_questions_are_unique_across_users_and_eventually_exhausted() assert exhausted.json()["detail"] == "本次任务已被领取完,请关注下一个任务!" +def test_unselected_ask_questions_are_released_immediately(monkeypatch): + monkeypatch.setattr(main_module, "generate_questions", fake_ask_questions) + with TestClient(app) as client: + assert client.post( + "/api/admin/auth/login", + json={"username": "operator", "password": "operator123456"}, + ).status_code == 200 + generated = client.post( + "/api/admin/ask-tasks/generate", + json={ + "shop_name": "未领取释放测试店铺", + "product_link": "TOKEN-RELEASE-UNSELECTED", + "product_name": "未领取释放测试商品", + "question_count": 8, + }, + ).json()["data"] + assert client.post(f"/api/admin/ask-tasks/{generated['id']}/publish").status_code == 200 + + first_session = client.post( + "/api/ask/auth/taobao-login", + json={"taobao_id": f"tb{uuid4().hex[:14]}"}, + ).json()["data"] + second_session = client.post( + "/api/ask/auth/taobao-login", + json={"taobao_id": f"tb{uuid4().hex[:14]}"}, + ).json()["data"] + first_url = f"/api/ask/tasks/{generated['task_id']}?token={first_session['token']}" + first_assignment = client.get(first_url).json()["data"] + first_ids = {item["id"] for item in first_assignment["questions"]} + assert len(first_ids) == 5 + + selected_ids = set(list(first_ids)[:3]) + confirmed = client.post( + f"/api/ask/tasks/{generated['task_id']}/selection", + json={"token": first_session["token"], "question_ids": list(selected_ids)}, + ) + assert confirmed.status_code == 200 + confirmed_ids = {item["id"] for item in confirmed.json()["data"]["questions"]} + assert confirmed_ids == selected_ids + + second_url = f"/api/ask/tasks/{generated['task_id']}?token={second_session['token']}" + second_assignment = client.get(second_url) + assert second_assignment.status_code == 200 + second_ids = {item["id"] for item in second_assignment.json()["data"]["questions"]} + assert len(second_ids) == 5 + assert second_ids == { + item["id"] + for item in generated["selected_questions"] + } - selected_ids + + def test_answer_submission_requires_owner_link_and_allows_at_most_two_images(): with TestClient(app) as client: missing_owner = client.post( diff --git a/frontend/src/admin.css b/frontend/src/admin.css index 7646963..9b31cdb 100644 --- a/frontend/src/admin.css +++ b/frontend/src/admin.css @@ -5485,48 +5485,97 @@ body img { .global-image-viewer { position: fixed; z-index: 9999; - inset: 0; + top: 84px; + right: 18px; + bottom: 18px; display: grid; - place-items: center; - padding: 32px; - background: rgba(4, 12, 28, .82); - cursor: zoom-out; + grid-template-rows: auto minmax(0, 1fr) auto; + width: min(480px, calc(100vw - 36px)); + border: 1px solid #cbd8e8; + border-radius: 10px; + background: #fff; + box-shadow: 0 18px 55px rgba(8, 24, 50, .28); overflow: hidden; } -.global-image-viewer img { +.global-image-viewer.left { + right: auto; + left: 18px; +} + +.global-image-viewer-header { + display: flex; + align-items: center; + gap: 8px; + min-height: 56px; + padding: 10px 12px 10px 15px; + border-bottom: 1px solid #dce5ef; + background: #fff; + cursor: grab; + touch-action: none; + user-select: none; +} + +.global-image-viewer.dragging-window .global-image-viewer-header { + cursor: grabbing; +} + +.global-image-viewer-header > div { + display: grid; + min-width: 0; + flex: 1; + gap: 3px; +} + +.global-image-viewer-header strong { + color: #102744; + font-size: 14px; +} + +.global-image-viewer-header span { + color: #7187a1; + font-size: 11px; +} + +.global-image-viewer-canvas { + display: grid; + place-items: center; + min-height: 0; + padding: 14px; + overflow: hidden; + background: #eaf0f7; + overscroll-behavior: contain; +} + +.global-image-viewer-canvas img { display: block; - max-width: min(94vw, 1600px); - max-height: 92vh; + max-width: 100%; + max-height: 100%; width: auto; height: auto; object-fit: contain; - border-radius: 10px; - box-shadow: 0 20px 80px rgba(0, 0, 0, .45); + border-radius: 6px; + box-shadow: 0 10px 32px rgba(8, 24, 50, .2); cursor: grab; touch-action: none; transform-origin: center center; user-select: none; } -.global-image-viewer img.dragging { +.global-image-viewer-canvas img.dragging { cursor: grabbing; } .global-image-viewer-tools { - position: fixed; - z-index: 2; - left: 50%; - bottom: 22px; display: flex; align-items: center; + justify-content: center; gap: 8px; - padding: 6px 8px; - border: 1px solid rgba(255, 255, 255, .2); - border-radius: 10px; - background: rgba(13, 28, 51, .88); + min-height: 50px; + padding: 8px 12px; + border-top: 1px solid rgba(255, 255, 255, .16); + background: #0d1c33; color: #fff; - transform: translateX(-50%); } .global-image-viewer-tools button { @@ -5546,6 +5595,26 @@ body img { background: rgba(255, 255, 255, .24); } +.global-image-viewer-tools button:disabled { + opacity: .35; + cursor: not-allowed; +} + +.global-image-viewer-tools button.gallery-nav { + min-width: 68px; + font-size: 11px; +} + +.global-image-viewer-tools span.gallery-count { + min-width: 36px; +} + +.global-image-viewer-tools i { + width: 1px; + height: 22px; + background: rgba(255, 255, 255, .22); +} + .global-image-viewer-tools button.reset { font-size: 12px; } @@ -5614,33 +5683,69 @@ body img { } .global-image-viewer-close { - position: fixed; - top: 18px; - right: 22px; - z-index: 1; - width: 40px; - height: 40px; - border: 1px solid rgba(255, 255, 255, .35); - border-radius: 50%; - background: rgba(255, 255, 255, .12); - color: #fff; - font-size: 28px; + display: grid; + place-items: center; + width: 34px; + height: 34px; + padding: 0; + border: 1px solid #d5e0ec; + border-radius: 7px; + background: #f4f7fa; + color: #536d8b; + font-size: 22px; line-height: 1; cursor: pointer; } -@media (max-width: 640px) { - .global-image-viewer { - padding: 16px; +.global-image-viewer-dock { + display: grid; + place-items: center; + width: 34px; + height: 34px; + padding: 0; + border: 1px solid #d5e0ec; + border-radius: 7px; + background: #f4f7fa; + color: #1764e8; + font-size: 18px; + cursor: pointer; +} + +@media (min-width: 1201px) { + body.image-preview-right .review-v2-backdrop { + padding-right: 516px; + justify-items: end; } - .global-image-viewer-close { - top: 12px; - right: 12px; + body.image-preview-left .review-v2-backdrop { + padding-left: 516px; + justify-items: start; + } +} + +@media (max-width: 1200px) { + .global-image-viewer, + .global-image-viewer.left { + top: auto; + right: 10px; + bottom: calc(10px + env(safe-area-inset-bottom)); + left: 10px; + width: auto; + height: min(48dvh, 480px); + } + + .global-image-viewer-dock { + display: none; } .global-image-viewer-tools { - bottom: 14px; + flex-wrap: wrap; + gap: 5px; + padding-inline: 8px; + } + + .global-image-viewer-tools i { + display: none; } } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 2b163e7..ade568a 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { + BookOpen, ClipboardList, Database, FileText, @@ -326,6 +327,13 @@ const BASE_ITEMS = [ label: "\u5ba1\u6838\u4e2d\u5fc3", group: "\u4efb\u52a1\u7ba1\u7406", }, + { + id: "guide", + icon: BookOpen, + label: "\u4f7f\u7528\u8bf4\u660e", + group: "\u4efb\u52a1\u7ba1\u7406", + href: "https://alidocs.dingtalk.com/i/nodes/amweZ92PV6w4kNGdcqEmwAMYJxEKBD6p?corpId=dinga78635fdab496f8735c2f4657eb6378f&doc_type=wiki_doc&editNotify=true&iframeQuery=utm_medium=im_card&utm_source=im&utm_medium=im_card&utm_scene=team_space&utm_source=im", + }, ]; const ADMIN_ITEMS = [ { @@ -394,6 +402,21 @@ function Sidebar({ user, active, setActive, reviewBadge }) { } function NavItem({ item, active, setActive }) { const Icon = item.icon; + if (item.href) { + return ( + + + {item.label} + + + ); + } return ( -
event.stopPropagation()}> +
+
+ 图片预览 + 拖动标题栏可移动 · 页面仍可滚动 +
+ + +
+
{ + event.preventDefault(); + changeScale(event.deltaY > 0 ? -0.15 : 0.15); + }} + > + 大图预览 +
+
+ + {currentIndex + 1}/{gallery.length} + +