fix: 平台套图/新建商品 YYX 第二轮(清单4 未完成项)
平台套图: - 商品列表无真封面时回退 mock 图(与商品库一致显图) - 商品分类去掉前缀「//」 - 左侧商品栏可折叠(收起只剩商品图) - 结果搜索框加 × 清空 + 平台筛选悬浮弹窗(可清空) - 结果卡精简:去容器/引号/平台/小标题,提示词可折叠展开、张数灰色 - 删悬浮「更多」里重复的加入资产库入口 - 去掉加入资产库后的中央遮罩,只保留左上角已入库标签 - 重跑/单图再生成的新批次置底并自动滚动到底 新建商品抽屉: - 上传失败:黑色遮罩 + 感叹号 + 提示 + 重新上传胶囊 - 填了内容中途退出:二次确认(复用 ConfirmModal) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { ChangeEvent } from "react";
|
||||
import {
|
||||
@@ -13,11 +13,14 @@ import {
|
||||
LayoutGrid,
|
||||
List,
|
||||
MoreHorizontal,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Pencil,
|
||||
Plus,
|
||||
Quote,
|
||||
RefreshCw,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Users,
|
||||
@@ -32,6 +35,7 @@ import { MediaLightbox } from "../components/overlays";
|
||||
import { Pager } from "../components/pager";
|
||||
import type { Page } from "./route-config";
|
||||
import { statusPill } from "./stage-config";
|
||||
import { productMockCoverUrl } from "./products";
|
||||
import "../ai-tools-page.css";
|
||||
|
||||
// 工作台生成模式 → 中文标签(优先用它给任务卡命名:能区分模特上身图/平台套图/图片创作,
|
||||
@@ -640,8 +644,6 @@ export function ImageWorkbenchPage({
|
||||
const [plDraft, setPlDraft] = useState<string[]>([]);
|
||||
const [plQuery, setPlQuery] = useState("");
|
||||
const [plCat, setPlCat] = useState("");
|
||||
// gen-card 就地反馈(§4.18:采用反馈不走全局弹窗,改卡内 show-feedback 1.5s)
|
||||
const [feedbackKey, setFeedbackKey] = useState("");
|
||||
// 单图「更多」气泡当前展开的 key(点击切换,点空收起)
|
||||
const [openMore, setOpenMore] = useState("");
|
||||
// 批次列表:每次生成/重跑追加一条,各自展示;可多批并行 generating
|
||||
@@ -662,6 +664,8 @@ export function ImageWorkbenchPage({
|
||||
// 参考图:支持多张(可多选 / 多次追加),逐张可移除。提交时上传成 Asset 作生成参考。
|
||||
const [refImages, setRefImages] = useState<{ name: string; url: string; file: File }[]>([]);
|
||||
const refInputRef = useRef<HTMLInputElement | null>(null);
|
||||
// 生成后把结果面板滚到最新批次用的哨兵(PMC#14/#22)
|
||||
const resultsEndRef = useRef<HTMLDivElement | null>(null);
|
||||
// 生成结果图片放大预览
|
||||
const [preview, setPreview] = useState<{ src: string; name: string } | null>(null);
|
||||
// 模特/平台 工作台头部:搜索 + 时间排序 + 模特筛选(对左侧网格真实生效)
|
||||
@@ -669,6 +673,11 @@ export function ImageWorkbenchPage({
|
||||
const [gridSort, setGridSort] = useState<"recent" | "name">("recent");
|
||||
const [tbOpen, setTbOpen] = useState<"" | "time" | "model">("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
// YYX#17:左侧商品栏折叠(收起后只剩商品图)
|
||||
const [sideCollapsed, setSideCollapsed] = useState(false);
|
||||
// YYX#12:平台套图结果筛选弹窗 —— 按平台筛选已生成的图(空集 = 不筛)
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [platformFilter, setPlatformFilter] = useState<string[]>([]);
|
||||
// 模特网格默认只露 6 个;「全部模特 →」展开后露全部,再点收起(超过 6 个时第 7 个之后本来选不到)
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
// 左侧商品空间搜索(原 input 无 state/无过滤=死框,输入无效);按名称/分类过滤
|
||||
@@ -682,7 +691,8 @@ export function ImageWorkbenchPage({
|
||||
const productCoverUrl = (p: Product): string => {
|
||||
const sorted = [...(p.images || [])].sort((a, b) => a.sort_order - b.sort_order);
|
||||
const primary = p.images?.find((img) => img.is_primary) || sorted[0];
|
||||
return p.cover_preview_url || primary?.preview_url || "";
|
||||
// YYX#14:无真封面时回退到按商品名匹配的 mock 图,与商品库一致显图,不再露灰占位
|
||||
return p.cover_preview_url || primary?.preview_url || productMockCoverUrl(p.title);
|
||||
};
|
||||
function pickReference(event: ChangeEvent<HTMLInputElement>) {
|
||||
const files = Array.from(event.target.files || []);
|
||||
@@ -740,11 +750,6 @@ export function ImageWorkbenchPage({
|
||||
setPickedIds((prev) => (prev[0] === id ? [] : [id]));
|
||||
if (mode !== "cover") setPickedModelName((prev) => (pickedIds[0] === id ? "" : name || ""));
|
||||
}
|
||||
// 单图就地反馈(§4.18):显示 1.5s 后自动撤掉
|
||||
function flashFeedback(key: string) {
|
||||
setFeedbackKey(key);
|
||||
window.setTimeout(() => setFeedbackKey((k) => (k === key ? "" : k)), 1500);
|
||||
}
|
||||
// 点击空白收起单图「更多」气泡
|
||||
useEffect(() => {
|
||||
if (!openMore) return;
|
||||
@@ -752,6 +757,13 @@ export function ImageWorkbenchPage({
|
||||
document.addEventListener("click", close);
|
||||
return () => document.removeEventListener("click", close);
|
||||
}, [openMore]);
|
||||
// YYX#12:点空白收起平台筛选弹窗
|
||||
useEffect(() => {
|
||||
if (!filterOpen) return;
|
||||
const close = (event: MouseEvent) => { if (!(event.target as HTMLElement).closest(".iw-filter-wrap")) setFilterOpen(false); };
|
||||
document.addEventListener("click", close);
|
||||
return () => document.removeEventListener("click", close);
|
||||
}, [filterOpen]);
|
||||
|
||||
const ratioVar = ratio.replace(":", " / ");
|
||||
const candidateCount = Math.max(1, Number(count) || 4);
|
||||
@@ -939,6 +951,8 @@ export function ImageWorkbenchPage({
|
||||
refs: opts.refs?.map((r) => ({ name: r.name, url: r.url }))
|
||||
};
|
||||
setBatches((prev) => [...prev, newBatch]);
|
||||
// YYX#16:新批次(含重跑/单图再生成)一律追加到末尾,提交即把面板滚到底部新任务处
|
||||
window.setTimeout(() => resultsEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }), 80);
|
||||
try {
|
||||
// 先把参考图上传成 Asset,拿到 id 列表带给后端 → 生成时真正作多图参考(image_edit)。
|
||||
// 上传任一失败不阻断:跳过该张,有几张算几张。
|
||||
@@ -1017,8 +1031,11 @@ export function ImageWorkbenchPage({
|
||||
// 图片创作:把已选参考图带进这一批(startBatch 内上传并传给后端)
|
||||
refs: mode === "image" && refImages.length ? refImages : undefined
|
||||
});
|
||||
// 参考图已交给本批,清空输入栏待下次(批次头会保留这次用过的参考图)
|
||||
if (mode === "image") setRefImages([]);
|
||||
// 图片创作:提交后清空输入栏(参考图 + 提示词文字),像对话一样「发完即清」,等下次输入(PMC#15)。
|
||||
// 批次头会保留这次用过的提示词/参考图,信息不丢。模特/平台模式的提示词是按商品预填的模板,不清。
|
||||
if (mode === "image") { setRefImages([]); setPrompt(""); }
|
||||
// 生成后自动把面板滚到最新批次,不用用户自己往下拖找(PMC#14/#22)。等新批次渲染出来再滚。
|
||||
window.setTimeout(() => resultsEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }), 80);
|
||||
}
|
||||
|
||||
/* 重跑指定批次(行32①②):用该批次的参数新起一个批次追加到列表末尾,原批次保留。 */
|
||||
@@ -1088,17 +1105,15 @@ export function ImageWorkbenchPage({
|
||||
const withId = batch.results.filter((a) => a.id);
|
||||
if (!withId.length) return;
|
||||
const next = !withId.every((a) => a.in_library);
|
||||
if (next) flashFeedback(`${batchId}:${withId[0].id}`);
|
||||
applyLibraryState(batchId, withId.map((a) => a.id), next);
|
||||
}
|
||||
|
||||
/* 加入资产库 / 取消(单图级:只改被点的那一张,§4.18 就地反馈不弹全局窗) */
|
||||
/* 加入资产库 / 取消(单图级:只改被点的那一张) */
|
||||
function toggleAdoptImage(batchId: string, assetId: string) {
|
||||
if (!assetId) return;
|
||||
const asset = batches.find((b) => b.id === batchId)?.results.find((a) => a.id === assetId);
|
||||
if (!asset) return;
|
||||
const next = !asset.in_library;
|
||||
if (next) flashFeedback(`${batchId}:${assetId}`);
|
||||
applyLibraryState(batchId, [assetId], next);
|
||||
}
|
||||
|
||||
@@ -1169,8 +1184,12 @@ export function ImageWorkbenchPage({
|
||||
const q = gridQuery.trim().toLowerCase();
|
||||
list = list.filter((b) => (b.prompt || "").toLowerCase().includes(q));
|
||||
}
|
||||
// YYX#12:筛选弹窗按平台筛选已生成的图(选了哪些平台就只看哪些)
|
||||
if (mode === "cover" && platformFilter.length) {
|
||||
list = list.filter((b) => (b.platformIds || []).some((p) => platformFilter.includes(p)));
|
||||
}
|
||||
return list;
|
||||
}, [batches, productId, mode, gridQuery]);
|
||||
}, [batches, productId, mode, gridQuery, platformFilter]);
|
||||
const hasResults = productBatches.length > 0;
|
||||
|
||||
/* ── 单个批次的结果网格 · §4.18 gen-card 三件套 ──
|
||||
@@ -1196,9 +1215,8 @@ export function ImageWorkbenchPage({
|
||||
const failed = !generating && !asset; // 非生成中且该格无图 → 这张失败了
|
||||
const key = assetId || `ph-${index}`;
|
||||
const cellKey = `${batch.id}:${assetId || key}`;
|
||||
const showFeedback = feedbackKey === `${batch.id}:${assetId}` && !!assetId;
|
||||
return (
|
||||
<div className={`gen-image ${generating && !url ? "gen" : ""}${failed ? " failed" : ""}${inLib && url ? " adopted" : ""}${showFeedback ? " show-feedback" : ""}`} key={key}>
|
||||
<div className={`gen-image ${generating && !url ? "gen" : ""}${failed ? " failed" : ""}${inLib && url ? " adopted" : ""}`} key={key}>
|
||||
{url ? (
|
||||
<img className="gen-image-img" src={url} alt={`${meta.title} #${index + 1}`} loading="lazy" title="点击放大" style={{ cursor: "zoom-in" }} onClick={() => setPreview({ src: url, name: `${meta.title} #${index + 1}` })} />
|
||||
) : (
|
||||
@@ -1229,13 +1247,7 @@ export function ImageWorkbenchPage({
|
||||
已入库
|
||||
</span>
|
||||
)}
|
||||
{/* ② 中央就地反馈(采用瞬间亮 1.5s,§4.18 禁全局 toast) */}
|
||||
{url && (
|
||||
<div className="gen-image-feedback" aria-hidden={!showFeedback}>
|
||||
<Check size={20} />
|
||||
<span>已加入资产库</span>
|
||||
</div>
|
||||
)}
|
||||
{/* YYX#20:去掉「加入资产库」后的中央遮罩反馈,只保留左上角「已入库」标签 */}
|
||||
{/* ① 右上悬浮操作组:再生成 / 下载 / 更多 */}
|
||||
{url && (
|
||||
<div className="gen-image-actions">
|
||||
@@ -1249,11 +1261,8 @@ export function ImageWorkbenchPage({
|
||||
<button className="gen-img-btn" type="button" title="更多" onClick={() => setOpenMore((k) => (k === cellKey ? "" : cellKey))}>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
{/* YYX#19:删掉「更多」气泡里的加入资产库(与批次操作行重复),只保留删除 */}
|
||||
<div className={`gen-bubble${openMore === cellKey ? " open" : ""}`}>
|
||||
<button type="button" className="gen-bubble-item" onClick={() => { setOpenMore(""); toggleAdoptImage(batch.id, assetId); }}>
|
||||
<Bookmark size={13} />
|
||||
{inLib ? "取消加入资产库" : "加入资产库"}
|
||||
</button>
|
||||
<button type="button" className="gen-bubble-item danger" onClick={() => { setOpenMore(""); removeImageFromBatch(batch.id, assetId); }}>
|
||||
<Trash2 size={13} />
|
||||
删除
|
||||
@@ -1286,43 +1295,13 @@ export function ImageWorkbenchPage({
|
||||
const failed = batch.status === "failed";
|
||||
return (
|
||||
<Fragment key={batch.id}>
|
||||
{/* 行34:每批一个合并导航头——商品/模特(平台)+ 结果(数量·比例·状态)左对齐一块;
|
||||
原右上「1 批·比例」与「N×」徽标系重复信息,已剔除 */}
|
||||
<div className="iw-pv-h">
|
||||
<Quote className="quote-icon" />
|
||||
{/* YYX#13:参考即梦 —— 显示用户输入的提示词 / 选择的平台 / 生成的张数 */}
|
||||
<div className="pv-line">
|
||||
<span className="k">提示词</span>
|
||||
<span className="v pv-prompt">{batch.prompt || "—"}</span>
|
||||
</div>
|
||||
{mode === "cover" && (
|
||||
<div className="pv-line">
|
||||
<span className="k">平台</span>
|
||||
<span className="v">
|
||||
{batch.platformIds && batch.platformIds.length
|
||||
? PLATFORM_OPTIONS.filter((p) => batch.platformIds!.includes(p.id))
|
||||
.map((p) => p.name)
|
||||
.join("、")
|
||||
: "未选择"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{mode === "model" && batch.modelName && (
|
||||
<div className="pv-line">
|
||||
<span className="k">模特</span>
|
||||
<span className="v">{batch.modelName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="pv-line">
|
||||
<span className="k">张数</span>
|
||||
<span className="v">
|
||||
{batch.count} 张 · {batch.ratio} · {generating ? "生成中" : failed ? "失败" : "已完成"}
|
||||
{!generating && !failed && batch.results.some((a) => a.in_library)
|
||||
? ` · ${batch.results.filter((a) => a.in_library).length} 张已入库`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* YYX#18:结果卡头部精简 —— 去容器/引号/平台/小标题,只留可折叠提示词 + 灰色张数文案 */}
|
||||
<BatchHeader
|
||||
prompt={batch.prompt}
|
||||
count={batch.count}
|
||||
statusText={generating ? "生成中" : failed ? "失败" : "已完成"}
|
||||
adoptedCount={!generating && !failed ? batch.results.filter((a) => a.in_library).length : 0}
|
||||
/>
|
||||
<div className="gen-card gen-batch-card">
|
||||
{renderBatchGrid(batch)}
|
||||
{/* 行30/32:批次底部操作行,与上方卡片拉开间距(.gen-batch-actions) */}
|
||||
@@ -1538,6 +1517,8 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
))}
|
||||
<MediaLightbox open={!!preview} src={preview?.src || ""} kind="image" name={preview?.name} close={() => setPreview(null)} />
|
||||
{/* 生成后自动滚到这里(最新批次末尾),用户不必手动往下拖(PMC#14/#22) */}
|
||||
<div ref={resultsEndRef} aria-hidden="true" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="ic-empty">
|
||||
@@ -1623,13 +1604,17 @@ export function ImageWorkbenchPage({
|
||||
return (
|
||||
<div className="image-workbench iw-prod">
|
||||
<div className="iw-layout">
|
||||
{/* 最左 · 商品空间 */}
|
||||
<aside className="iw-prod-space">
|
||||
{/* 最左 · 商品空间(YYX#17:可折叠 → 收起后只剩商品图) */}
|
||||
<aside className={`iw-prod-space${sideCollapsed ? " collapsed" : ""}`}>
|
||||
<div className="iw-side-top">
|
||||
<button className="back-pill" type="button" onClick={onBack}>
|
||||
<ArrowLeft size={14} />
|
||||
返回
|
||||
</button>
|
||||
{/* YYX#17:折叠/展开左侧商品栏 */}
|
||||
<button className="iw-side-collapse" type="button" title={sideCollapsed ? "展开商品栏" : "收起商品栏"} aria-label={sideCollapsed ? "展开商品栏" : "收起商品栏"} onClick={() => setSideCollapsed((v) => !v)}>
|
||||
{sideCollapsed ? <PanelLeftOpen size={15} /> : <PanelLeftClose size={15} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="iw-ps-search">
|
||||
<Search size={13} />
|
||||
@@ -1676,7 +1661,8 @@ export function ImageWorkbenchPage({
|
||||
)}
|
||||
<div className="body">
|
||||
<div className="nm">{item.title}</div>
|
||||
<div className="sub">// {item.category || "未分类"}</div>
|
||||
{/* YYX#15:商品分类去掉前缀「//」 */}
|
||||
<div className="sub">{item.category || "未分类"}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -1703,12 +1689,50 @@ export function ImageWorkbenchPage({
|
||||
<span className="spacer" />
|
||||
<div className="tb-search-wrap">
|
||||
{searchOpen && (
|
||||
<input className="input" autoFocus placeholder={mode === "model" ? "搜索模特" : "搜索图片提示词"} value={gridQuery} onChange={(event) => setGridQuery(event.target.value)} style={{ height: 30, marginRight: 6, width: 140 }} />
|
||||
<div className="iw-search-box">
|
||||
<input className="input" autoFocus placeholder={mode === "model" ? "搜索模特" : "搜索图片提示词"} value={gridQuery} onChange={(event) => setGridQuery(event.target.value)} />
|
||||
{/* YYX#12:输入框内 × 清空已输入内容 */}
|
||||
{gridQuery && (
|
||||
<button className="iw-search-clear" type="button" aria-label="清空搜索" onClick={() => setGridQuery("")}>
|
||||
<X size={13} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button className={`search-btn${searchOpen ? " active" : ""}`} type="button" title={mode === "model" ? "搜索批次/模特" : "搜索"} onClick={() => { setSearchOpen((v) => !v); if (searchOpen) setGridQuery(""); }}>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/* YYX#12:平台套图 —— 搜索 icon 右侧加筛选按钮,点开悬浮弹窗按平台筛选结果图 */}
|
||||
{mode === "cover" && (
|
||||
<div className={`tb-menu-wrap chip-wrap iw-filter-wrap${filterOpen ? " open" : ""}`} data-filter="platform">
|
||||
<button className={`search-btn${platformFilter.length ? " active" : ""}`} type="button" title="按平台筛选" onClick={() => setFilterOpen((v) => !v)}>
|
||||
<SlidersHorizontal size={14} />
|
||||
{platformFilter.length > 0 && <span className="iw-filter-count">{platformFilter.length}</span>}
|
||||
</button>
|
||||
<div className="chip-menu align-right iw-filter-menu">
|
||||
<div className="iw-filter-h">
|
||||
<span className="mono">// 按平台筛选</span>
|
||||
{/* 筛选弹窗清空按钮 */}
|
||||
<button type="button" className="iw-filter-clear" disabled={!platformFilter.length} onClick={() => setPlatformFilter([])}>清空</button>
|
||||
</div>
|
||||
<div className="iw-filter-grid">
|
||||
{PLATFORM_OPTIONS.map((p) => (
|
||||
<button
|
||||
type="button"
|
||||
key={p.id}
|
||||
className={`iw-filter-opt${platformFilter.includes(p.id) ? " on" : ""}`}
|
||||
onClick={() => setPlatformFilter((prev) => (prev.includes(p.id) ? prev.filter((x) => x !== p.id) : [...prev, p.id]))}
|
||||
>
|
||||
<span className={`p-logo p-logo-${p.id}`}>{p.logo}</span>
|
||||
<span className="nm">{p.name}</span>
|
||||
<Check className="ck" size={12} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mode === "model" && (
|
||||
<>
|
||||
<div className={`tb-menu-wrap chip-wrap${tbOpen === "time" ? " open" : ""}`} data-filter="time">
|
||||
@@ -1955,6 +1979,8 @@ export function ImageWorkbenchPage({
|
||||
<MediaLightbox open={!!preview} src={preview?.src || ""} kind="image" name={preview?.name} close={() => setPreview(null)} />
|
||||
{/* P0③:平台套图按平台分组出 section;模特图沿用每批一个导航头。按商品隔离 → productBatches */}
|
||||
{mode === "cover" ? renderCoverGrouped(productBatches) : renderBatchCards(productBatches)}
|
||||
{/* YYX#16:新任务滚到底锚点 */}
|
||||
<div ref={resultsEndRef} aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -2023,6 +2049,30 @@ export function ImageWorkbenchPage({
|
||||
);
|
||||
}
|
||||
|
||||
/* YYX#18:结果卡精简头部 —— 提示词单行折叠(超出给「展开/收起」)+ 灰色张数文案 */
|
||||
function BatchHeader({ prompt, count, statusText, adoptedCount }: { prompt: string; count: number; statusText: string; adoptedCount: number }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [overflow, setOverflow] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (el) setOverflow(el.scrollWidth > el.clientWidth + 1); // 折叠态(单行)是否被截断
|
||||
}, [prompt]);
|
||||
return (
|
||||
<div className="iw-batch-head">
|
||||
<div className={`ibh-prompt${expanded ? " expanded" : ""}`} ref={ref}>{prompt || "—"}</div>
|
||||
{(overflow || expanded) && (
|
||||
<button type="button" className="ibh-toggle" onClick={() => setExpanded((v) => !v)}>
|
||||
{expanded ? "收起提示词" : "展开提示词"}
|
||||
</button>
|
||||
)}
|
||||
<div className="ibh-count">
|
||||
{count} 张 · {statusText}{adoptedCount > 0 ? ` · ${adoptedCount} 张已入库` : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* 底部 chat 输入栏 · 参数胶囊(基线 image-optimize .param + 下拉气泡) */
|
||||
function Pill({
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user