审核页面图片改为预览,[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}
|
||||
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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<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 (
|
||||
<button
|
||||
className={`nav-item ${active === item.id ? "active" : ""}`}
|
||||
@ -3936,21 +3959,41 @@ function CompletionReviewPage() {
|
||||
);
|
||||
}
|
||||
function GlobalImageViewer() {
|
||||
const [src, setSrc] = useState("");
|
||||
const [gallery, setGallery] = useState([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
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 src = gallery[currentIndex]?.src || "";
|
||||
useEffect(() => {
|
||||
const handleClick = (event) => {
|
||||
const image = event.target?.closest?.("img");
|
||||
if (!image || !image.src) return;
|
||||
setSrc(image.src);
|
||||
if (!image || !image.src || image.closest(".global-image-viewer")) return;
|
||||
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);
|
||||
setPosition({ x: 0, y: 0 });
|
||||
};
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Escape") setSrc("");
|
||||
if (event.key === "Escape") {
|
||||
setGallery([]);
|
||||
setCurrentIndex(-1);
|
||||
setFloatingPosition(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClick);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
@ -3959,6 +4002,31 @@ function GlobalImageViewer() {
|
||||
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;
|
||||
const changeScale = (delta) => {
|
||||
setScale((current) => {
|
||||
@ -3991,38 +4059,131 @@ function GlobalImageViewer() {
|
||||
dragStart.current = null;
|
||||
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 (
|
||||
<div
|
||||
className="global-image-viewer"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="图片大图预览"
|
||||
onClick={() => setSrc("")}
|
||||
onWheel={(event) => {
|
||||
event.preventDefault();
|
||||
changeScale(event.deltaY > 0 ? -0.15 : 0.15);
|
||||
}}
|
||||
<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}
|
||||
>
|
||||
<button type="button" className="global-image-viewer-close" aria-label="关闭大图" onClick={() => setSrc("")}>×</button>
|
||||
<div className="global-image-viewer-tools" onClick={(event) => event.stopPropagation()}>
|
||||
<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
|
||||
className="global-image-viewer-canvas"
|
||||
onWheel={(event) => {
|
||||
event.preventDefault();
|
||||
changeScale(event.deltaY > 0 ? -0.15 : 0.15);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt="大图预览"
|
||||
draggable={false}
|
||||
className={dragging ? "dragging" : ""}
|
||||
style={{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }}
|
||||
onPointerDown={beginDrag}
|
||||
onPointerMove={moveDrag}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
/>
|
||||
</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>
|
||||
<img
|
||||
src={src}
|
||||
alt="大图预览"
|
||||
draggable={false}
|
||||
className={dragging ? "dragging" : ""}
|
||||
style={{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onPointerDown={beginDrag}
|
||||
onPointerMove={moveDrag}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -313,6 +313,7 @@ button {
|
||||
color: #c5d2e8;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-item:hover,
|
||||
.nav-item.active {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user