审核页面图片改为预览,[200~1. 任务不领取直接释放不等待一分钟
This commit is contained in:
parent
3ab8b52593
commit
68b59d2eab
@ -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}
|
used_ids = {item.question_id for item in assignment.used_questions}
|
||||||
if selected_ids & used_ids:
|
if selected_ids & used_ids:
|
||||||
raise HTTPException(status_code=400, detail="已认领的问题不能重复选择")
|
raise HTTPException(status_code=400, detail="已认领的问题不能重复选择")
|
||||||
for item in assignment.items:
|
for item in list(assignment.items):
|
||||||
# The selection represents only the next batch. Questions in
|
# Keep historical submitted questions for batch details, retain only
|
||||||
# ``used_ids`` remain locked/read-only and are not resubmitted.
|
# the questions confirmed for this batch, and immediately return all
|
||||||
item.selected = item.question_id in selected_ids
|
# 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.status = "进行中"
|
||||||
assignment.reservation_started_at = datetime.utcnow()
|
assignment.reservation_started_at = datetime.utcnow()
|
||||||
assignment.updated_at = assignment.reservation_started_at
|
assignment.updated_at = assignment.reservation_started_at
|
||||||
|
|||||||
@ -1138,6 +1138,57 @@ def test_public_ask_questions_are_unique_across_users_and_eventually_exhausted()
|
|||||||
assert exhausted.json()["detail"] == "本次任务已被领取完,请关注下一个任务!"
|
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():
|
def test_answer_submission_requires_owner_link_and_allows_at_most_two_images():
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
missing_owner = client.post(
|
missing_owner = client.post(
|
||||||
|
|||||||
@ -5485,48 +5485,97 @@ body img {
|
|||||||
.global-image-viewer {
|
.global-image-viewer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
inset: 0;
|
top: 84px;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 18px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
padding: 32px;
|
width: min(480px, calc(100vw - 36px));
|
||||||
background: rgba(4, 12, 28, .82);
|
border: 1px solid #cbd8e8;
|
||||||
cursor: zoom-out;
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 18px 55px rgba(8, 24, 50, .28);
|
||||||
overflow: hidden;
|
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;
|
display: block;
|
||||||
max-width: min(94vw, 1600px);
|
max-width: 100%;
|
||||||
max-height: 92vh;
|
max-height: 100%;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 10px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 20px 80px rgba(0, 0, 0, .45);
|
box-shadow: 0 10px 32px rgba(8, 24, 50, .2);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-image-viewer img.dragging {
|
.global-image-viewer-canvas img.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-image-viewer-tools {
|
.global-image-viewer-tools {
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 22px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 8px;
|
min-height: 50px;
|
||||||
border: 1px solid rgba(255, 255, 255, .2);
|
padding: 8px 12px;
|
||||||
border-radius: 10px;
|
border-top: 1px solid rgba(255, 255, 255, .16);
|
||||||
background: rgba(13, 28, 51, .88);
|
background: #0d1c33;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-image-viewer-tools button {
|
.global-image-viewer-tools button {
|
||||||
@ -5546,6 +5595,26 @@ body img {
|
|||||||
background: rgba(255, 255, 255, .24);
|
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 {
|
.global-image-viewer-tools button.reset {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@ -5614,33 +5683,69 @@ body img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.global-image-viewer-close {
|
.global-image-viewer-close {
|
||||||
position: fixed;
|
display: grid;
|
||||||
top: 18px;
|
place-items: center;
|
||||||
right: 22px;
|
width: 34px;
|
||||||
z-index: 1;
|
height: 34px;
|
||||||
width: 40px;
|
padding: 0;
|
||||||
height: 40px;
|
border: 1px solid #d5e0ec;
|
||||||
border: 1px solid rgba(255, 255, 255, .35);
|
border-radius: 7px;
|
||||||
border-radius: 50%;
|
background: #f4f7fa;
|
||||||
background: rgba(255, 255, 255, .12);
|
color: #536d8b;
|
||||||
color: #fff;
|
font-size: 22px;
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
.global-image-viewer-dock {
|
||||||
.global-image-viewer {
|
display: grid;
|
||||||
padding: 16px;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-image-viewer-close {
|
@media (min-width: 1201px) {
|
||||||
top: 12px;
|
body.image-preview-right .review-v2-backdrop {
|
||||||
right: 12px;
|
padding-right: 516px;
|
||||||
|
justify-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
.global-image-viewer-tools {
|
||||||
bottom: 14px;
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-image-viewer-tools i {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import {
|
import {
|
||||||
|
BookOpen,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Database,
|
Database,
|
||||||
FileText,
|
FileText,
|
||||||
@ -326,6 +327,13 @@ const BASE_ITEMS = [
|
|||||||
label: "\u5ba1\u6838\u4e2d\u5fc3",
|
label: "\u5ba1\u6838\u4e2d\u5fc3",
|
||||||
group: "\u4efb\u52a1\u7ba1\u7406",
|
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 = [
|
const ADMIN_ITEMS = [
|
||||||
{
|
{
|
||||||
@ -394,6 +402,21 @@ function Sidebar({ user, active, setActive, reviewBadge }) {
|
|||||||
}
|
}
|
||||||
function NavItem({ item, active, setActive }) {
|
function NavItem({ item, active, setActive }) {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="nav-item"
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="在新标签页打开使用说明"
|
||||||
|
>
|
||||||
|
<Icon size={17} />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<em aria-hidden="true">↗</em>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`nav-item ${active === item.id ? "active" : ""}`}
|
className={`nav-item ${active === item.id ? "active" : ""}`}
|
||||||
@ -3936,21 +3959,41 @@ function CompletionReviewPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
function GlobalImageViewer() {
|
function GlobalImageViewer() {
|
||||||
const [src, setSrc] = useState("");
|
const [gallery, setGallery] = useState([]);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [dockSide, setDockSide] = useState("right");
|
||||||
|
const [floatingPosition, setFloatingPosition] = useState(null);
|
||||||
|
const [panelDragging, setPanelDragging] = useState(false);
|
||||||
|
const panelRef = useRef(null);
|
||||||
|
const panelDragStart = useRef(null);
|
||||||
const dragStart = useRef(null);
|
const dragStart = useRef(null);
|
||||||
|
const src = gallery[currentIndex]?.src || "";
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
const image = event.target?.closest?.("img");
|
const image = event.target?.closest?.("img");
|
||||||
if (!image || !image.src) return;
|
if (!image || !image.src || image.closest(".global-image-viewer")) return;
|
||||||
setSrc(image.src);
|
const scope = image.closest(".review-v2-modal") || document;
|
||||||
|
const imageNodes = Array.from(scope.querySelectorAll("img")).filter(
|
||||||
|
(node) => node.src && !node.closest(".global-image-viewer"),
|
||||||
|
);
|
||||||
|
const items = imageNodes.map((node) => ({
|
||||||
|
src: node.currentSrc || node.src,
|
||||||
|
alt: node.alt || "审核图片",
|
||||||
|
}));
|
||||||
|
setGallery(items);
|
||||||
|
setCurrentIndex(Math.max(0, imageNodes.indexOf(image)));
|
||||||
setScale(1);
|
setScale(1);
|
||||||
setPosition({ x: 0, y: 0 });
|
setPosition({ x: 0, y: 0 });
|
||||||
};
|
};
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
if (event.key === "Escape") setSrc("");
|
if (event.key === "Escape") {
|
||||||
|
setGallery([]);
|
||||||
|
setCurrentIndex(-1);
|
||||||
|
setFloatingPosition(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("click", handleClick);
|
document.addEventListener("click", handleClick);
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
@ -3959,6 +4002,31 @@ function GlobalImageViewer() {
|
|||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!src) return undefined;
|
||||||
|
const handleGalleryKeyDown = (event) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
setCurrentIndex((current) => Math.max(0, current - 1));
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowRight") {
|
||||||
|
setCurrentIndex((current) => Math.min(gallery.length - 1, current + 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleGalleryKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleGalleryKeyDown);
|
||||||
|
}, [src, gallery.length]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!src) return;
|
||||||
|
setScale(1);
|
||||||
|
setPosition({ x: 0, y: 0 });
|
||||||
|
}, [currentIndex, src]);
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.remove("image-preview-left", "image-preview-right");
|
||||||
|
if (!src) return undefined;
|
||||||
|
const className = `image-preview-${dockSide}`;
|
||||||
|
document.body.classList.add(className);
|
||||||
|
return () => document.body.classList.remove(className);
|
||||||
|
}, [src, dockSide]);
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
const changeScale = (delta) => {
|
const changeScale = (delta) => {
|
||||||
setScale((current) => {
|
setScale((current) => {
|
||||||
@ -3991,38 +4059,131 @@ function GlobalImageViewer() {
|
|||||||
dragStart.current = null;
|
dragStart.current = null;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
};
|
};
|
||||||
|
const closeViewer = () => {
|
||||||
|
setGallery([]);
|
||||||
|
setCurrentIndex(-1);
|
||||||
|
setFloatingPosition(null);
|
||||||
|
};
|
||||||
|
const showImage = (nextIndex) => {
|
||||||
|
if (nextIndex < 0 || nextIndex >= gallery.length) return;
|
||||||
|
setCurrentIndex(nextIndex);
|
||||||
|
};
|
||||||
|
const beginPanelDrag = (event) => {
|
||||||
|
if (event.button !== 0 || event.target.closest("button") || !panelRef.current) return;
|
||||||
|
const rect = panelRef.current.getBoundingClientRect();
|
||||||
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||||
|
panelDragStart.current = {
|
||||||
|
pointerX: event.clientX,
|
||||||
|
pointerY: event.clientY,
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
};
|
||||||
|
setPanelDragging(true);
|
||||||
|
};
|
||||||
|
const movePanelDrag = (event) => {
|
||||||
|
if (!panelDragStart.current) return;
|
||||||
|
const start = panelDragStart.current;
|
||||||
|
const margin = 8;
|
||||||
|
const left = Math.min(
|
||||||
|
window.innerWidth - start.width - margin,
|
||||||
|
Math.max(margin, start.left + event.clientX - start.pointerX),
|
||||||
|
);
|
||||||
|
const top = Math.min(
|
||||||
|
window.innerHeight - start.height - margin,
|
||||||
|
Math.max(margin, start.top + event.clientY - start.pointerY),
|
||||||
|
);
|
||||||
|
setFloatingPosition({ left, top, height: start.height });
|
||||||
|
};
|
||||||
|
const endPanelDrag = () => {
|
||||||
|
panelDragStart.current = null;
|
||||||
|
setPanelDragging(false);
|
||||||
|
};
|
||||||
|
const switchDockSide = () => {
|
||||||
|
setFloatingPosition(null);
|
||||||
|
setDockSide((current) => current === "right" ? "left" : "right");
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
|
<aside
|
||||||
|
ref={panelRef}
|
||||||
|
className={`global-image-viewer ${dockSide === "left" ? "left" : ""}${panelDragging ? " dragging-window" : ""}`}
|
||||||
|
role="complementary"
|
||||||
|
aria-label="图片预览"
|
||||||
|
style={floatingPosition ? {
|
||||||
|
left: `${floatingPosition.left}px`,
|
||||||
|
top: `${floatingPosition.top}px`,
|
||||||
|
right: "auto",
|
||||||
|
bottom: "auto",
|
||||||
|
height: `${floatingPosition.height}px`,
|
||||||
|
} : undefined}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="global-image-viewer-header"
|
||||||
|
onPointerDown={beginPanelDrag}
|
||||||
|
onPointerMove={movePanelDrag}
|
||||||
|
onPointerUp={endPanelDrag}
|
||||||
|
onPointerCancel={endPanelDrag}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>图片预览</strong>
|
||||||
|
<span>拖动标题栏可移动 · 页面仍可滚动</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="global-image-viewer-dock"
|
||||||
|
aria-label={dockSide === "right" ? "将预览移到左侧" : "将预览移到右侧"}
|
||||||
|
title={dockSide === "right" ? "移到左侧" : "移到右侧"}
|
||||||
|
onClick={switchDockSide}
|
||||||
|
>
|
||||||
|
{dockSide === "right" ? "←" : "→"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="global-image-viewer-close" aria-label="关闭预览" onClick={closeViewer}>×</button>
|
||||||
|
</header>
|
||||||
<div
|
<div
|
||||||
className="global-image-viewer"
|
className="global-image-viewer-canvas"
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="图片大图预览"
|
|
||||||
onClick={() => setSrc("")}
|
|
||||||
onWheel={(event) => {
|
onWheel={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
changeScale(event.deltaY > 0 ? -0.15 : 0.15);
|
changeScale(event.deltaY > 0 ? -0.15 : 0.15);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button type="button" className="global-image-viewer-close" aria-label="关闭大图" onClick={() => setSrc("")}>×</button>
|
|
||||||
<div className="global-image-viewer-tools" onClick={(event) => event.stopPropagation()}>
|
|
||||||
<button type="button" aria-label="缩小" onClick={() => changeScale(-0.25)}>−</button>
|
|
||||||
<span>{Math.round(scale * 100)}%</span>
|
|
||||||
<button type="button" aria-label="放大" onClick={() => changeScale(0.25)}>+</button>
|
|
||||||
<button type="button" className="reset" onClick={resetView}>重置</button>
|
|
||||||
</div>
|
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
alt="大图预览"
|
alt="大图预览"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className={dragging ? "dragging" : ""}
|
className={dragging ? "dragging" : ""}
|
||||||
style={{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }}
|
style={{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }}
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onPointerDown={beginDrag}
|
onPointerDown={beginDrag}
|
||||||
onPointerMove={moveDrag}
|
onPointerMove={moveDrag}
|
||||||
onPointerUp={endDrag}
|
onPointerUp={endDrag}
|
||||||
onPointerCancel={endDrag}
|
onPointerCancel={endDrag}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="global-image-viewer-tools">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-nav"
|
||||||
|
disabled={currentIndex <= 0}
|
||||||
|
onClick={() => showImage(currentIndex - 1)}
|
||||||
|
>
|
||||||
|
← 上一张
|
||||||
|
</button>
|
||||||
|
<span className="gallery-count">{currentIndex + 1}/{gallery.length}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-nav"
|
||||||
|
disabled={currentIndex >= gallery.length - 1}
|
||||||
|
onClick={() => showImage(currentIndex + 1)}
|
||||||
|
>
|
||||||
|
下一张 →
|
||||||
|
</button>
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
<button type="button" aria-label="缩小" onClick={() => changeScale(-0.25)}>−</button>
|
||||||
|
<span>{Math.round(scale * 100)}%</span>
|
||||||
|
<button type="button" aria-label="放大" onClick={() => changeScale(0.25)}>+</button>
|
||||||
|
<button type="button" className="reset" onClick={resetView}>重置</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -313,6 +313,7 @@ button {
|
|||||||
color: #c5d2e8;
|
color: #c5d2e8;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.nav-item:hover,
|
.nav-item:hover,
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user