feat(v2): 纠偏第三批——深色导航、三模块独立入口与轨迹复盘
覆盖 AC-53/54/55(BR-50)。审计 P1-2「新界面未接入、原落地页与导航未改」 由此关闭:导航改为原型 token 的深色壳,三个模块各有独立路由与入口。 界面: - navigation.css:导航改用原型 token(nav.bg #102B27 / active #244D41 / muted #B1C6BE),身份选择器在深色底上改用 dark 变体(否则白底白字); 工作区画布保持浅色 #f5f7f9——"深色导航+浅色工作区"是两件事。 保留可读性守卫锁定的两条规则(导航项 min-height:42px/font-size:14px、 身份选择器 min-height:42px),原有 7 项守卫全部继续通过。 - AgentNav:品牌改为「兴邦金租/智审工作台」,加「业务工作区」分组标签, 三个模块入口(AI 预审 / 历史经验加工 / 轨迹复盘)指向各自路由; 权限仍按角色裁剪,身份仍来自服务端。 - 新增 /prescreen-workbench(AI 预审)、/trajectory-center(轨迹复盘)两个模块路由; / 从旧聊天落地页改为业务工作区首页,呈现三个模块入口;原落地页内容保留在 /assistant,没有被删掉(AC-53 要求的是不再"只保留"旧布局)。 - TrajectoryPanel:消费既有只读轨迹端点,失败/跳过步骤照常列出并带错误码, 成本缺失显示"未记录(不填 0)",并展示服务端对轨迹范围的说明; 会议结果只读展示(录入属第四批)。 复用而非复制:三个模块路由都 import 既有的 PrescreenPanel / ImportBatchPanel / ExperienceCenter,没有在路由里再写一份实现。 守卫:新增 module-shell-ui(已登记 verify.py,共 13 项检查),机械核对 深色 token、三入口存在、已合并旧页仍为重定向、按角色裁剪、 ?project= 上下文与不硬编码项目 id、轨迹面板不渲染推理字段, 并做前后端路径一致性断言(界面调用的端点必须真的在后端声明过)。 深色底对比度按 WCAG 公式在守卫内**实算**(阈值 AA 4.5:1,实测最低 8.37:1), 不在文档里声称"应该够清楚"。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {BarChart3, Bot, History, Inbox, LayoutDashboard, Settings2} from "lucide-react";
|
||||
import {BarChart3, Bot, GitBranch, History, Inbox, LayoutDashboard, Settings2, ShieldCheck} from "lucide-react";
|
||||
import IdentitySelector from "./IdentitySelector";
|
||||
import "./navigation.css";
|
||||
|
||||
@@ -19,6 +19,12 @@ function identityHeaders(){
|
||||
return {...(token?{Authorization:`Bearer ${token}`}:{}) ,...(role?{"X-Xingbang-Demo-Role":role}:{})};
|
||||
}
|
||||
|
||||
/** 深色业务导航(BR-50 / AC-53)。
|
||||
*
|
||||
* 三个模块各有**独立入口**:AI 预审 / 历史经验加工 / 轨迹复盘。它们指向各自的路由,
|
||||
* 不是旧页里的页签——BR-50 明确"不得只在旧页下叠加面板"。
|
||||
* 权限仍按角色裁剪(只读访客与客户经理看不到配置治理),数据域由后端每次重验。
|
||||
*/
|
||||
export default function AgentNav({active,project,identity:provided}:{active:string;project?:string;identity?:any}){
|
||||
const [loaded,setLoaded]=useState<any>(null),[roleHint,setRoleHint]=useState("");
|
||||
useEffect(()=>{
|
||||
@@ -38,18 +44,21 @@ export default function AgentNav({active,project,identity:provided}:{active:stri
|
||||
const canConfigure=identity?.preview||role==="admin"||role==="reviewer";
|
||||
const canUsePool=role==="manager"||role==="reviewer";
|
||||
return <nav className="xb-agent-nav" aria-label="兴邦对公业务导航">
|
||||
<a href={projectLink("/xingbang-workbench",project)} className="xb-brand" title="兴邦金租项目工作台">
|
||||
<a href={projectLink("/",project)} className="xb-brand" title="兴邦金租智审工作台">
|
||||
<span className="xb-brand-mark" aria-hidden="true">兴</span>
|
||||
<span><b>兴邦金租</b><small>对公智能评审</small></span>
|
||||
<span><b>兴邦金租</b><small>智审工作台</small></span>
|
||||
</a>
|
||||
<a href="/" className={active==="assistant"?"selected":""}><Bot size={16}/>业务助手</a>
|
||||
<a href={projectLink("/xingbang-workbench",project)} className={active==="overview"?"selected":""}><LayoutDashboard size={16}/>项目工作台</a>
|
||||
{canUsePool&&<a href="/review-pool" className={active==="pool"?"selected":""}><Inbox size={16}/>项目评审池</a>}
|
||||
<span className="xb-nav-group">业务工作区</span>
|
||||
<a href={projectLink("/prescreen-workbench",project)} className={active==="prescreen"?"selected":""}><ShieldCheck size={16}/>AI 预审</a>
|
||||
<a href={projectLink("/experience-center",project)} className={active==="experience"?"selected":""}><History size={16}/>历史经验加工</a>
|
||||
<a href={projectLink("/trajectory-center",project)} className={active==="trajectory"?"selected":""}><GitBranch size={16}/>轨迹复盘</a>
|
||||
<a href={projectLink("/xingbang-workbench",project)} className={active==="overview"?"selected":""}><LayoutDashboard size={16}/>项目工作台</a>
|
||||
{canUsePool&&<a href={projectLink("/review-pool",project)} className={active==="pool"?"selected":""}><Inbox size={16}/>项目评审池</a>}
|
||||
<a href={projectLink("/operations-center",project)} className={active==="time"?"selected":""}><BarChart3 size={16}/>业务时效</a>
|
||||
{canConfigure&&<a href="/data-access" className={active==="risk"?"selected":""}><Settings2 size={16}/>配置治理</a>}
|
||||
<a href="/assistant" className={active==="assistant"?"selected":""}><Bot size={16}/>业务助手</a>
|
||||
<span className="xb-nav-spacer"/>
|
||||
{project&&<span className="xb-project-context">项目 · {project}</span>}
|
||||
<IdentitySelector identity={identity||{role:roleHint}} pending={!identity}/>
|
||||
<IdentitySelector identity={identity||{role:roleHint}} pending={!identity} dark/>
|
||||
</nav>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
import {useEffect, useState} from "react";
|
||||
import {AlertTriangle, Clock, FileText, RefreshCw} from "lucide-react";
|
||||
import {PRESCREEN} from "@/lib/prescreen";
|
||||
|
||||
/** 轨迹复盘(AC-54 的"可回放轨迹";T10 的只读回放语义在界面上落地)。
|
||||
*
|
||||
* 三条界面纪律,直接对应后端已有的规则:
|
||||
* 1. **只读**:这里没有任何写操作;回放不改运行、不改历史结果。
|
||||
* 2. **失败保留**:failed / skipped 步骤照常列出并带错误码,不因为"看起来不好"被过滤。
|
||||
* 3. **成本未知留空**:没有成本口径时显示"未记录",**不填 0**——0 会被读成"零成本"。
|
||||
*
|
||||
* 不展示模型内部推理:轨迹只记录可观察动作与版本,界面也不假装能看到思维链。
|
||||
* 会议结果的**录入**属第四批;本页只读展示已保存的记录。
|
||||
*/
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
queued: "排队中", running: "执行中", completed: "已完成",
|
||||
skipped: "已跳过", failed: "未完成", cancelled: "已取消",
|
||||
};
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
plan: "方案", skill: "Skill", tool: "工具", rule: "规则",
|
||||
judgment: "专项判断", synthesis: "综合",
|
||||
};
|
||||
|
||||
export default function TrajectoryPanel({project, request}: any) {
|
||||
const [runId, setRunId] = useState("");
|
||||
const [trajectory, setTrajectory] = useState<any>(null);
|
||||
const [outcomes, setOutcomes] = useState<any[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const runs = (project?.runs || []).filter((item: any) => item.id);
|
||||
const path = `/projects/${encodeURIComponent(project.id)}`;
|
||||
|
||||
async function load(id: string, cursor?: string) {
|
||||
setBusy(true); setError("");
|
||||
try {
|
||||
const page = await request(`${PRESCREEN}${path}/runs/${encodeURIComponent(id)}/trajectory`
|
||||
+ (cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""));
|
||||
setTrajectory((current: any) => cursor && current
|
||||
? {...page, steps: [...current.steps, ...page.steps]}
|
||||
: page);
|
||||
} catch (e: any) {setError(e.message);} finally {setBusy(false); setLoaded(true);}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!runs.length) {setTrajectory(null); setLoaded(true); return;}
|
||||
const first = runs[0].id;
|
||||
setRunId(first);
|
||||
load(first);
|
||||
request(`${PRESCREEN}${path}/review-outcomes`)
|
||||
.then((data: any) => setOutcomes(data.items || []))
|
||||
.catch(() => setOutcomes([]));
|
||||
}, [project.id]);
|
||||
|
||||
const steps = trajectory?.steps || [];
|
||||
return <section className="xb-panel xb-wide">
|
||||
<h2>运行轨迹回放</h2>
|
||||
<p>一次运行是一条轨迹,多次工具与模型调用是其中的步骤。这里只记录**可观察的动作与版本**,不包含模型内部推理;回放是只读的,不会改动这次运行。</p>
|
||||
|
||||
{!loaded && <p>正在读取轨迹…</p>}
|
||||
{loaded && !runs.length && <div className="ra-empty">
|
||||
<h2>该项目还没有运行</h2>
|
||||
<p>轨迹来自真实运行;没有运行时不显示示例步骤。</p>
|
||||
</div>}
|
||||
|
||||
{runs.length > 1 && <label className="ra-field"><span>查看哪一次运行</span>
|
||||
<select aria-label="选择运行" value={runId} disabled={busy}
|
||||
onChange={(e) => {setRunId(e.target.value); load(e.target.value);}}>
|
||||
{runs.map((item: any) => <option key={item.id} value={item.id}>
|
||||
{new Date(item.created_at).toLocaleString()} · {STATUS_LABEL[item.status] || item.status} · {item.run_kind}
|
||||
</option>)}
|
||||
</select></label>}
|
||||
|
||||
{error && <div role="alert" className="ra-alert error">{error}</div>}
|
||||
|
||||
{trajectory && <>
|
||||
<div className="xb-finding">
|
||||
<div>
|
||||
<b>运行 {trajectory.run_id} · {STATUS_LABEL[trajectory.status] || trajectory.status} · {steps.length} 个步骤</b>
|
||||
<p>输入快照 {trajectory.snapshot_id} · 只读回放{trajectory.read_only ? "(服务端已标记)" : ""}</p>
|
||||
<p><small>{trajectory.note}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{steps.map((step: any) => <div className="xb-finding" key={step.id}>
|
||||
<div>
|
||||
<b>#{step.sequence} · {KIND_LABEL[step.kind] || step.kind} · {STATUS_LABEL[step.status] || step.status}</b>
|
||||
<p>{step.component_ref
|
||||
? <>组件 <code>{step.component_ref.id}@{step.component_ref.version}</code></>
|
||||
: "(无组件引用)"}
|
||||
{step.error_code ? ` · 错误码 ${step.error_code}` : ""}
|
||||
</p>
|
||||
{step.reason_summary && <p className="xb-warning"><AlertTriangle size={13}/> {step.reason_summary}</p>}
|
||||
{(step.evidence_ids || []).length > 0 &&
|
||||
<p><FileText size={13} aria-hidden="true"/> 引用证据 {step.evidence_ids.length} 条:{(step.evidence_ids || []).slice(0, 4).join("、")}{(step.evidence_ids || []).length > 4 ? "…" : ""}</p>}
|
||||
<p><small>
|
||||
成本 {step.cost === null || step.cost === undefined ? "未记录(不填 0)" : `${step.cost} ${step.cost_currency || ""}`}
|
||||
{" · "}耗时 {step.latency_ms === null || step.latency_ms === undefined ? "未记录" : `${step.latency_ms} ms`}
|
||||
{step.started_at ? ` · 开始 ${new Date(step.started_at).toLocaleString()}` : ""}
|
||||
</small></p>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{trajectory.next_cursor && <div className="xb-actions">
|
||||
<button className="ra-button" disabled={busy} onClick={() => load(runId, trajectory.next_cursor)}>
|
||||
<RefreshCw size={15}/>加载更多步骤
|
||||
</button>
|
||||
</div>}
|
||||
</>}
|
||||
|
||||
<h3>会议结果对照(只读)</h3>
|
||||
<p>AI 轨迹与会议记录并列查看。找不到材料版本的结果单列,既不当否决、也不进通过率分母。<b>录入属第四批</b>,这里只读。</p>
|
||||
{!outcomes.length && <p className="ra-note">尚未保存会议结果。</p>}
|
||||
{outcomes.map((item: any) => <div className="xb-finding" key={item.id}>
|
||||
<div>
|
||||
<b>{item.raw_code} · {item.status === "matched" ? "已关联材料版本" : "未匹配(单列)"}</b>
|
||||
<p>{item.normalized ? `归一化结果 ${item.normalized}` : "口径未映射,保留原码"}
|
||||
{item.label === true ? " · 计入通过" : item.label === false ? " · 计入未通过" : " · 不计入分母"}</p>
|
||||
<p><small><Clock size={12} aria-hidden="true"/> {new Date(item.occurred_at).toLocaleString()} · 记录人 {item.recorded_by}
|
||||
{item.supersedes_id ? ` · 更正自 ${item.supersedes_id}` : ""}</small></p>
|
||||
</div>
|
||||
</div>)}
|
||||
</section>;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
.xb-agent-nav{position:sticky;top:0;z-index:100;display:flex;align-items:center;gap:8px;padding:10px 22px;background:#fffffff5;backdrop-filter:blur(18px);border-bottom:1px solid #dfe7e3;color:#667b6d;box-shadow:0 2px 14px #1e3d2e0f;font-family:Arial,"Microsoft YaHei",sans-serif}.xb-agent-nav a{display:flex;align-items:center;gap:7px;padding:8px 10px;min-height:36px;border-radius:7px;font-size:12px;font-weight:500}.xb-agent-nav a:hover{background:#f2f6f3;color:#216b48}.xb-agent-nav a.selected{background:#e4f1e9;color:#17633f;box-shadow:inset 0 0 0 1px #cce2d4}.xb-agent-nav .xb-brand{gap:8px;padding:2px 14px 2px 0;margin-right:2px;border-right:1px solid #e4eae5;border-radius:0}.xb-brand-mark{width:30px;height:30px;display:grid;place-items:center;border-radius:7px;background:#17634c;color:#fff;font-size:15px;font-weight:700;box-shadow:inset 0 -2px 0 #0002}.xb-brand b{display:block;color:#17634c;font-size:13px;line-height:1.1}.xb-brand small{display:block;margin-top:4px;color:#91a197;font-size:9px;line-height:1.1}.xb-nav-spacer{flex:1}.xb-project-context{font-size:10px;color:#58705f;background:#f2f7f3;border:1px solid #deebe1;border-radius:999px;padding:7px 11px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.xb-identity{display:flex;align-items:center;gap:7px;padding:7px 11px;border-radius:999px;background:#f2f6f3;border:1px solid #dce8e0;font-size:10px;color:#7d8c82;white-space:nowrap}.xb-identity i{width:7px;height:7px;border-radius:50%;background:#55a778;box-shadow:0 0 0 4px #55a77818}.xb-identity b{font-size:11px;color:#244a36}.xb-identity.role-reviewer{background:#eef2fb;border-color:#dce3f3}.xb-identity.role-reviewer i{background:#607bb8}.xb-identity.role-reviewer b{color:#344c82}.xb-identity.role-admin{background:#f8f2e8;border-color:#eadfc9}.xb-identity.role-admin i{background:#bc8541}.xb-identity.role-admin b{color:#76511f}.xb-overview{max-width:1480px;margin:auto;padding:32px}.xb-overview-heading{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:28px}.xb-overview-heading h1{font-size:26px;font-weight:600}.xb-overview-heading p{font-size:12px;color:#84948b;margin-top:9px}.xb-role-switch{display:flex;align-items:center;gap:8px;border:1px solid #cee0d5;background:#fff;padding:9px 12px;border-radius:12px;box-shadow:0 7px 24px #183b2b0b}.xb-role-switch>span{font-size:10px;color:#87978f}.xb-role-switch svg{color:#27805a}.xb-role-switch select{font-weight:700;color:#245c43;border:0;background:transparent;outline:none}.xb-role-switch.dark{border-color:#ffffff1f;background:#ffffff0d;box-shadow:none}.xb-role-switch.dark>span{color:#8daf9e}.xb-role-switch.dark svg{color:#55d49b}.xb-role-switch.dark select{color:#e1f5eb}.xb-role-switch.dark select option{color:#17251e}@media(max-width:760px){.xb-agent-nav{padding:8px 10px;gap:3px;flex-wrap:wrap}.xb-agent-nav a{font-size:10px;padding:6px}.xb-agent-nav .xb-brand{border:0;padding-right:5px}.xb-brand small,.xb-nav-spacer,.xb-identity>span{display:none}.xb-identity{margin-left:auto}.xb-project-context{order:10;width:100%;max-width:none}.xb-overview{padding:22px 15px}.xb-overview-heading{align-items:flex-start;flex-wrap:wrap}}
|
||||
/* 深色导航(BR-50 / AC-53):token 取自已确认原型 docs/v2/prototypes/prescreen-workbench.html
|
||||
* 与 DESIGN.md —— nav.bg #102B27、nav.active #244D41、nav.text #F3F7F5、nav.muted #B1C6BE。
|
||||
* 画布仍是浅色(canvas #F3F5F4,见 workspace.css):"深色导航+浅色工作区"是两件事,
|
||||
* 不要把整页刷黑。
|
||||
*/
|
||||
.xb-agent-nav{position:sticky;top:0;z-index:100;display:flex;align-items:center;gap:8px;padding:10px 22px;background:#102B27;border-bottom:1px solid #244D41;color:#B1C6BE;box-shadow:0 2px 14px #102b2726;font-family:Arial,"Microsoft YaHei",sans-serif}.xb-agent-nav a{display:flex;align-items:center;gap:7px;padding:8px 10px;min-height:36px;border-radius:7px;font-size:12px;font-weight:500;color:#DCE9E3}.xb-agent-nav a:hover{background:#1B3D36;color:#fff}.xb-agent-nav a.selected{background:#244D41;color:#F3F7F5;box-shadow:inset 0 0 0 1px #3f6d5d}.xb-agent-nav .xb-brand{gap:8px;padding:2px 14px 2px 0;margin-right:2px;border-right:1px solid #244D41;border-radius:0}.xb-agent-nav .xb-brand:hover{background:transparent}.xb-brand-mark{width:30px;height:30px;display:grid;place-items:center;border-radius:7px;background:#17634c;color:#fff;font-size:15px;font-weight:700;box-shadow:inset 0 -2px 0 #0002}.xb-brand b{display:block;color:#F3F7F5;font-size:13px;line-height:1.1}.xb-brand small{display:block;margin-top:4px;color:#B1C6BE;font-size:9px;line-height:1.1}.xb-nav-spacer{flex:1}.xb-nav-group{font-size:10px;color:#8FAEA1;padding:0 8px;white-space:nowrap}.xb-project-context{font-size:10px;color:#D6E5DE;background:#ffffff14;border:1px solid #ffffff26;border-radius:999px;padding:7px 11px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.xb-identity{display:flex;align-items:center;gap:7px;padding:7px 11px;border-radius:999px;background:#ffffff14;border:1px solid #ffffff26;font-size:10px;color:#B1C6BE;white-space:nowrap}.xb-identity i{width:7px;height:7px;border-radius:50%;background:#55d49b;box-shadow:0 0 0 4px #55d49b26}.xb-identity b{font-size:11px;color:#EAF4EF}.xb-identity.role-reviewer{background:#ffffff1a;border-color:#7f9bd6a6}.xb-identity.role-reviewer i{background:#8fa9e2}.xb-identity.role-reviewer b{color:#DCE6FA}.xb-identity.role-admin{background:#ffffff1a;border-color:#d9b478a6}.xb-identity.role-admin i{background:#dcae66}.xb-identity.role-admin b{color:#F5E8CF}.xb-overview{max-width:1480px;margin:auto;padding:32px}.xb-overview-heading{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:28px}.xb-overview-heading h1{font-size:26px;font-weight:600}.xb-overview-heading p{font-size:12px;color:#84948b;margin-top:9px}.xb-role-switch{display:flex;align-items:center;gap:8px;border:1px solid #cee0d5;background:#fff;padding:9px 12px;border-radius:12px;box-shadow:0 7px 24px #183b2b0b}.xb-role-switch>span{font-size:10px;color:#87978f}.xb-role-switch svg{color:#27805a}.xb-role-switch select{font-weight:700;color:#245c43;border:0;background:transparent;outline:none}.xb-role-switch.dark{border-color:#ffffff1f;background:#ffffff0d;box-shadow:none}.xb-role-switch.dark>span{color:#8daf9e}.xb-role-switch.dark svg{color:#55d49b}.xb-role-switch.dark select{color:#e1f5eb}.xb-role-switch.dark select option{color:#17251e}@media(max-width:760px){.xb-agent-nav{padding:8px 10px;gap:3px;flex-wrap:wrap}.xb-agent-nav a{font-size:10px;padding:6px}.xb-agent-nav .xb-brand{border:0;padding-right:5px}.xb-brand small,.xb-nav-spacer,.xb-identity>span,.xb-nav-group{display:none}.xb-identity{margin-left:auto}.xb-project-context{order:10;width:100%;max-width:none}.xb-overview{padding:22px 15px}.xb-overview-heading{align-items:flex-start;flex-wrap:wrap}}
|
||||
|
||||
/* Business UI readability: keep navigation and identity obvious at a glance. */
|
||||
.xb-agent-nav{min-height:64px;padding:11px 24px;gap:10px;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
.xb-agent-nav a{min-height:42px;padding:10px 13px;border-radius:9px;font-size:14px;font-weight:600;transition:background-color .16s,color .16s,box-shadow .16s}
|
||||
.xb-agent-nav a.selected{box-shadow:inset 0 0 0 1px #bdd8c8,0 3px 10px #1d563a12}
|
||||
.xb-agent-nav{min-height:64px;padding:11px 24px;gap:10px;font-family:"Microsoft YaHei","PingFang SC",sans-serif;background:#102B27}
|
||||
.xb-agent-nav a{min-height:42px;padding:10px 13px;border-radius:9px;font-size:14px;font-weight:600;color:#DCE9E3;transition:background-color .16s,color .16s,box-shadow .16s}
|
||||
.xb-agent-nav a.selected{background:#244D41;color:#FFFFFF;box-shadow:inset 0 0 0 1px #4b7d6b,0 3px 10px #00000033}
|
||||
.xb-brand-mark{width:36px;height:36px;font-size:17px}
|
||||
.xb-brand b{font-size:15px}.xb-brand small{font-size:11px}
|
||||
.xb-nav-group{font-size:12px}
|
||||
.xb-project-context{font-size:12px;padding:8px 13px}
|
||||
.xb-role-switch{min-height:42px;padding:8px 13px;border-color:#bcd5c6;box-shadow:0 4px 16px #183b2b12}
|
||||
.xb-role-switch>span{font-size:12px}.xb-role-switch select{font-size:14px;cursor:pointer}
|
||||
|
||||
Reference in New Issue
Block a user