feat: ship frontend visual refresh

This commit is contained in:
Codex
2026-05-01 10:37:38 +08:00
parent 934a90b523
commit 642485e488
23 changed files with 2093 additions and 364 deletions
+1
View File
@@ -12,3 +12,4 @@ backend/storage/
backend/tmp_migration_check.db
project-docs/qa-artifacts/
.claude/worktrees/
.gstack/
+5 -6
View File
@@ -161,20 +161,19 @@ export default function AuditLogsPage() {
<section className="review-history-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Audit Logs</span>
<Title level={2}>查看系统关键操作,保留可回溯的审计记录。</Title>
<Text type="secondary">当前已覆盖登录、创建审查任务、下载审查报告等关键动作,后续可继续扩展更多后台模块事件。</Text>
<Title level={2}>追踪关键操作,快速定位责任链路。</Title>
<Text type="secondary">搜索和动作筛选放在同一工具栏,适合管理员排查登录、审查和下载记录。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Only</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
审计日志工作台
审计节奏
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放审计日志查看能力。</li>
<li>支持按动作筛选日志。</li>
<li>支持按操作人、资源类型、资源 ID 搜索。</li>
<li>便于后续扩展到更完整的后台管理模块。</li>
<li>先筛选,再查看详情 JSON。</li>
</ul>
<Alert showIcon type="info" message="权限边界:audit-logs 当前按管理侧主用设计,后续再评估更细粒度审计查看权限。" />
@@ -186,7 +185,7 @@ export default function AuditLogsPage() {
<div className="review-toolbar-copy">
<span className="landing-card-eyebrow">Audit Workspace</span>
<strong>快速定位关键操作记录</strong>
<span>先把当前已有的审计能力可视化,作为后续管理端扩展的基础页面。</span>
<span>搜索操作人、资源类型、资源 ID 或动作名称。</span>
</div>
<div className="review-toolbar-controls">
<Input
+4 -4
View File
@@ -1174,11 +1174,11 @@ export default function ChatPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">{reviewContext ? "Review Copilot" : "Legal Chat"}</span>
<Title level={2}>{reviewContext ? "围绕当前审查结果继续追问这份合同。" : "围绕合同问题,发起一轮登录态法律问答。"}</Title>
<Title level={2}>{reviewContext ? "接着这份审查结果继续问。" : "用登录态问答处理合同问题。"}</Title>
<Text type="secondary">
{reviewContext
? "已从审查详情页带入当前合同的审查结果,可以直接围绕风险项、条款摘要和处理建议继续追问。"
: "补齐 V1 会话历史最小闭环:输入问题、流式回答、展示引用来源,并支持回看已保存会话。"}
? "风险项、条款摘要和处理建议已经带入上下文,可以直接追问下一步。"
: "输入问题、查看流式回答和引用来源,历史会话会自动沉淀。"}
</Text>
<div className="review-page-highlight-list">
{chatHighlights.map((item) => (
@@ -1191,7 +1191,7 @@ export default function ChatPage() {
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">{reviewContext ? "Review Context" : "Usage Guide"}</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
{reviewContext ? "已注入审查上下文" : "问答说明"}
{reviewContext ? "上下文已就绪" : "问答节奏"}
</Title>
<ul className="review-page-tip-list">
{chatTips.map((item) => (
+58 -76
View File
@@ -149,14 +149,14 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
};
return (
<main className="page-shell review-detail-page">
<main className="page-shell review-detail-page contract-detail-page">
{contextHolder}
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<section className="review-detail-hero">
<div className="page-header-block">
<span className="landing-badge">Generated Contract</span>
<Title level={2}>{detail?.template_name ?? "合同详情"}</Title>
<Text type="secondary">查看已生成合同的完整信息、变量回显、真实预览,并基于历史结果继续处理。</Text>
<Text type="secondary">保留生成参数、预览内容和下载入口,方便从历史结果继续编辑或复核。</Text>
<div className="review-detail-hero-meta">
<div className="review-detail-meta-chip">
<span>合同类型</span>
@@ -180,9 +180,9 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<span className="landing-panel-eyebrow">Workspace Actions</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
历史结果工作区
下一步动作
</Title>
<Text type="secondary">可直接下载,也可把历史模板、变量、导出格式和导出选项回填到生成页继续处理。</Text>
<Text type="secondary">下载文件、回填生成参数,或把生成内容带入问答复核。</Text>
<Space wrap>
<Button onClick={() => router.push("/contracts")}>返回列表</Button>
<Button onClick={handleRestore}>再次生成 / 继续编辑</Button>
@@ -240,81 +240,63 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
)}
</Card>
<Card className="review-surface-card" loading={loading} title="变量回显">
<Table
rowKey="key"
pagination={false}
dataSource={variableRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
columns={[
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
{
title: "变量值",
dataIndex: "value",
key: "value",
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
},
{
title: "状态",
dataIndex: "isEmpty",
key: "isEmpty",
width: 120,
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
},
]}
/>
</Card>
<div className="contract-detail-two-column">
<Card className="review-surface-card" loading={loading} title="变量回显">
<Table
rowKey="key"
pagination={false}
dataSource={variableRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
columns={[
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
{
title: "变量值",
dataIndex: "value",
key: "value",
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
},
{
title: "状态",
dataIndex: "isEmpty",
key: "isEmpty",
width: 120,
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
},
]}
/>
</Card>
<Card className="review-surface-card" loading={loading} title="导出选项">
<Table
rowKey="key"
pagination={false}
dataSource={exportOptionRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无导出选项。" /> }}
columns={[
{ title: "选项名", dataIndex: "name", key: "name", width: 240 },
{ title: "选项值", dataIndex: "value", key: "value" },
]}
/>
</Card>
<Card className="review-surface-card" loading={loading} title="导出选项">
<Table
rowKey="key"
pagination={false}
dataSource={exportOptionRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无导出选项。" /> }}
columns={[
{ title: "选项名", dataIndex: "name", key: "name", width: 180 },
{ title: "选项值", dataIndex: "value", key: "value" },
]}
/>
</Card>
</div>
<Card className="review-surface-card" loading={loading} title="真实预览">
{preview ? (
<Card
size="small"
style={{
backgroundColor: "#fafafa",
border: "1px solid #d9d9d9",
whiteSpace: "pre-wrap",
fontFamily: "monospace",
fontSize: "12px",
}}
>
{preview.preview_content}
</Card>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无预览内容。" />
)}
</Card>
<div className="contract-content-grid">
<Card className="review-surface-card" loading={loading} title="真实预览">
{preview ? (
<div className="contract-preview-panel">{preview.preview_content}</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无预览内容。" />
)}
</Card>
<Card className="review-surface-card" loading={loading} title="生成内容">
{detail ? (
<Card
size="small"
style={{
backgroundColor: "#fafafa",
border: "1px solid #d9d9d9",
whiteSpace: "pre-wrap",
fontFamily: "monospace",
fontSize: "12px",
}}
>
{detail.generated_content}
</Card>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无生成内容。" />
)}
</Card>
<Card className="review-surface-card" loading={loading} title="生成内容">
{detail ? (
<div className="contract-preview-panel">{detail.generated_content}</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无生成内容。" />
)}
</Card>
</div>
</Space>
</main>
);
+17 -25
View File
@@ -585,26 +585,26 @@ export default function ContractsPage() {
const restoredOptionEntries = Object.entries(restoredOptions);
return (
<main className="page-shell review-page-grid">
<main className="page-shell review-page-grid contracts-page">
{contextHolder}
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Contract Generation</span>
<Title level={2}>智能合同生成,快速创建专业合同。</Title>
<Text type="secondary">选择模板、填写变量、预览确认、一键生成专业合同文档。</Text>
<Title level={2}>从模板到下载,一屏完成合同生成。</Title>
<Text type="secondary">先选模板,再填写变量;预览确认后生成文件,历史结果保留在下方。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Smart Workflow</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
四步生成流程
当前链路
</Title>
<ul className="review-page-tip-list">
<li>从预置模板库中选择合适的合同模板。</li>
<li>填写模板变量,支持必填验证和默认值。</li>
<li>实时预览生成结果,确认无误后生成。</li>
<li>支持多种格式导出:DOCX、PDF、HTML、TXT。</li>
</ul>
<div className="contract-progress-strip">
<span>选模板</span>
<span>填变量</span>
<span>生成文件</span>
</div>
<Text type="secondary">支持 DOCX、PDF、HTML、TXT 导出;从审查结果进入时会自动带入上下文。</Text>
</Card>
</section>
@@ -668,7 +668,10 @@ export default function ContractsPage() {
)}
{restoredOptionEntries.length > 0 && (
<Card size="small" title="已恢复的导出选项">
<div className="embedded-panel">
<div className="embedded-panel-header">
<strong>已恢复的导出选项</strong>
</div>
<Space wrap>
{restoredOptionEntries.map(([key, value]) => (
<Tag key={key} color="blue">
@@ -676,7 +679,7 @@ export default function ContractsPage() {
</Tag>
))}
</Space>
</Card>
</div>
)}
<Form form={form} layout="vertical">
@@ -735,20 +738,9 @@ export default function ContractsPage() {
<Text type="secondary">预览内容如下:</Text>
</div>
<Card
size="small"
style={{
backgroundColor: "#fafafa",
border: "1px solid #d9d9d9",
maxHeight: "400px",
overflow: "auto",
whiteSpace: "pre-wrap",
fontFamily: "monospace",
fontSize: "12px",
}}
>
<div className="contract-preview-panel">
{previewResult.preview_content}
</Card>
</div>
<div style={{ marginTop: 24 }}>
<Title level={5}>变量填写情况</Title>
+6 -8
View File
@@ -293,20 +293,18 @@ export default function FilesPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">File Management</span>
<Title level={2}>统一管理平台内所有文件资源。</Title>
<Text type="secondary">支持文件上传、下载、分类管理和删除操作。</Text>
<Title level={2}>集中管理合同、报告和知识文档资产。</Title>
<Text type="secondary">上传区和资产列表并排呈现,文件详情、下载和删除都从列表完成。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
文件管理流程
资产流转
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放文件资产管理能力。</li>
<li>支持上传各类文件(合同、文档、图片等)。</li>
<li>可按文件类型筛选查看。</li>
<li>提供文件下载和删除功能。</li>
<li>文件存储支持多存储桶配置。</li>
<li>上传前先选类型与存储桶。</li>
<li>列表支持按文件类型筛选。</li>
<li>高亮来自其他页面跳转的目标文件。</li>
</ul>
<Alert showIcon type="info" message="权限边界:member 通过业务链路使用文件结果,admin 负责资产管理。" />
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -306,19 +306,18 @@ export default function KnowledgeBasesPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Knowledge Base</span>
<Title level={2}>集中维护知识库与文档索引状态。</Title>
<Text type="secondary">先补齐 V1 最小闭环:知识库列表、创建知识库、上传文档,以及文档处理状态与失败原因展示。</Text>
<Title level={2}>管理知识库、文档与索引状态。</Title>
<Text type="secondary">左侧选择知识库,右侧上传和追踪文档处理结果,失败原因直接暴露。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
知识库维护流程
管理节奏
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放知识库管理入口。</li>
<li>先创建知识库,再上传文档。</li>
<li>当前支持 txt、docx、pdf 等文档的文本提取与预览。</li>
<li>后续继续补强更深的切片、召回与索引能力。</li>
<li>处理失败时优先看生命周期说明。</li>
<li>索引完成后可进入问答继续使用。</li>
</ul>
<Alert showIcon type="info" message="权限边界:member 使用审查、Chat、合同生成;admin 额外负责知识库治理。" />
+27 -3
View File
@@ -178,6 +178,27 @@ function LoginPageContent() {
</span>
))}
</div>
<div className="auth-access-visual" aria-label="登录后的安全工作台预览">
<div className="auth-access-terminal">
<span>Access Matrix</span>
<strong>审查工作台已就绪</strong>
</div>
<div className="auth-access-track">
<span />
<span />
<span />
</div>
<div className="auth-access-metrics">
<div>
<span>Session</span>
<strong>Active</strong>
</div>
<div>
<span>Role</span>
<strong>Tenant</strong>
</div>
</div>
</div>
</div>
<Card className="review-side-card auth-side-card" bordered={false}>
@@ -211,9 +232,12 @@ function LoginPageContent() {
</div>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Alert showIcon type="success" message="推荐新用户直接注册账号;注册成功后会自动登录并进入工作台。" />
<Alert showIcon type="info" message="默认管理员仅用于本地初始化和联调,不建议作为常规使用方式。" />
<Alert showIcon type="info" message={redirectHint} description="表单支持直接按 Enter 提交。" />
<div className="auth-access-strip">
<span>注册后自动进入</span>
<span>Enter 快速提交</span>
<span>{redirectTarget === "/" ? "默认到工作台" : "返回目标页"}</span>
</div>
{hasStoredSession ? <Alert showIcon type="info" message={loginHint} /> : null}
{registerSuccessTip ? <Alert showIcon type="success" message={registerSuccessTip} /> : null}
<Tabs
+101 -51
View File
@@ -76,6 +76,14 @@ const riskLabelMap: Record<string, string> = {
low: "低风险",
};
const publicShowcaseCards = [
{ label: "Clause Scanner", value: "82 条", tone: "cyan" },
{ label: "Risk Radar", value: "12 项", tone: "coral" },
{ label: "Legal Sources", value: "36 条", tone: "amber" },
];
const commandFlowItems = ["文件解析", "风险识别", "条款建议", "报告交付"];
function formatDateTime(value: string) {
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
@@ -271,16 +279,30 @@ export default function HomePage() {
const adminWorkbenchLinks = useMemo(() => getAdminWorkbenchLinks(currentUser?.role), [currentUser?.role]);
const workspaceInsights = useMemo(() => buildWorkspaceInsights(reviews), [reviews]);
const gettingStartedLinks = useMemo(() => buildGettingStartedLinks(currentUser?.role), [currentUser?.role]);
const dashboardHighlights = useMemo(
() => [
...priorityTasks.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.meta })),
...workspaceInsights.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.description })),
].slice(0, 6),
[priorityTasks, workspaceInsights],
);
const workflowLinks = useMemo(
() => [
...gettingStartedLinks.map((item) => ({ ...item, eyebrow: "Next Step" })),
...quickLinks.map((item) => ({ href: item.href, label: item.label, description: item.description, eyebrow: item.eyebrow })),
].slice(0, 6),
[gettingStartedLinks, quickLinks],
);
if (!currentUser) {
return (
<main className="landing-page">
{contextHolder}
<section className="landing-hero-shell">
<div className="landing-hero">
<section className="landing-hero-shell landing-hero-shell-public">
<div className="landing-hero landing-hero-public">
<span className="landing-badge">AI Contract Platform</span>
<h1>面向真实业务流转的合同工作平台。</h1>
<p>现在系统已接入正式数据库,登录、合同审查、审查记录、结果详情与报告下载主链路均已打通。</p>
<h1>让合同审查像刷仪表盘一样清爽、快速、有判断力。</h1>
<p>围绕上传、识别、建议、报告导出重组体验,把 AI 审查结果变成可行动的法务工作流。</p>
<div className="landing-actions">
<Link className="landing-primary-action" href="/login?tab=register">
立即注册新账号
@@ -289,6 +311,41 @@ export default function HomePage() {
进入登录
</Link>
</div>
<div className="landing-trust-row">
<span>合同初审</span>
<span>风险雷达</span>
<span>依据引用</span>
<span>报告交付</span>
</div>
</div>
<div className="landing-visual-scene" aria-label="AI 合同审查可视化预览">
<div className="landing-visual-topline">
<span>Live Review Console</span>
<strong>V1 Pilot</strong>
</div>
<div className="landing-contract-stack">
<div className="landing-contract-paper">
<span className="paper-kicker">保密协议 · NDA</span>
<strong>自动定位高风险条款</strong>
<div className="paper-line paper-line-wide" />
<div className="paper-line" />
<div className="paper-line paper-line-hot" />
<div className="paper-line paper-line-short" />
</div>
<div className="landing-risk-pulse">
<span>High Risk</span>
<strong>03</strong>
</div>
</div>
<div className="landing-showcase-grid">
{publicShowcaseCards.map((item) => (
<div className={`landing-showcase-card landing-showcase-card-${item.tone}`} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</div>
</div>
</section>
</main>
@@ -301,8 +358,8 @@ export default function HomePage() {
<section className="landing-hero-shell">
<div className="landing-hero">
<span className="landing-badge">Contract Workspace</span>
<h1>你好,{currentUser.display_name}。这里是你的合同业务工作台。</h1>
<p>首页现在承担业务 cockpit 角色:展示当前租户审查概览、风险分布、待处理状态与核心操作入口,而不再只是简单 landing 页。</p>
<h1>你好,{currentUser.display_name}。今天先处理最重要的合同任务。</h1>
<p>审查概览、风险分布和核心入口集中在这里,首页负责帮你判断下一步。</p>
<div className="landing-actions">
<Link className="landing-primary-action" href="/review">
立即发起审查
@@ -311,6 +368,34 @@ export default function HomePage() {
进入合同生成
</Link>
</div>
<div className="landing-command-visual" aria-label="合同审查智能流程">
<div className="landing-command-header">
<span>AI Review Pipeline</span>
<strong>{stats.completedCount}/{stats.totalReviews || 0}</strong>
</div>
<div className="landing-command-flow">
{commandFlowItems.map((item, index) => (
<div className="landing-command-step" key={item}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{item}</strong>
</div>
))}
</div>
<div className="landing-command-alerts">
<div>
<span>高风险</span>
<strong>{stats.highRisk}</strong>
</div>
<div>
<span>处理中</span>
<strong>{stats.pendingCount}</strong>
</div>
<div>
<span>异常</span>
<strong>{stats.failedCount}</strong>
</div>
</div>
</div>
<div className="landing-stats">
<div className="landing-stat-card">
<strong>{stats.totalReviews}</strong>
@@ -381,35 +466,15 @@ export default function HomePage() {
<section className="landing-section">
<div className="landing-section-header">
<h2>优先处理摘要</h2>
<p>优先展示高风险、异常和处理中任务,让首页更像可执行的驾驶舱。</p>
<p>运营摘要与高风险、异常和处理中任务直接前置,减少二次判断。</p>
</div>
{priorityTasks.length === 0 ? (
{dashboardHighlights.length === 0 ? (
<Empty description="当前没有需要优先处理的任务。" />
) : (
<div className="landing-grid">
{priorityTasks.map((item) => (
<Link className="landing-card" href={item.href} key={`${item.label}-${item.id}`}>
<span className="landing-card-eyebrow">{item.label}</span>
<strong>{item.title}</strong>
<span>{item.meta}</span>
</Link>
))}
</div>
)}
</section>
<section className="landing-section">
<div className="landing-section-header">
<h2>运营摘要</h2>
<p>把当前最值得立刻动作的任务直接前置,减少再判断成本。</p>
</div>
{workspaceInsights.length === 0 ? (
<Empty description="当前没有需要额外关注的运营摘要。" />
) : (
<div className="landing-grid">
{workspaceInsights.map((item) => (
<Link className="landing-card" href={item.href} key={`${item.label}-${item.href}`}>
<span className="landing-card-eyebrow">{item.label}</span>
{dashboardHighlights.map((item) => (
<Link className="landing-card" href={item.href} key={`${item.eyebrow}-${item.href}`}>
<span className="landing-card-eyebrow">{item.eyebrow}</span>
<strong>{item.title}</strong>
<span>{item.description}</span>
</Link>
@@ -420,13 +485,13 @@ export default function HomePage() {
<section className="landing-section">
<div className="landing-section-header">
<h2>起步建议</h2>
<p>针对当前角色给出最合理的下一步,让新注册用户进入系统后不会迷路。</p>
<h2>核心入口</h2>
<p>把新用户起步和高频功能合并,避免首页变成导航墙。</p>
</div>
<div className="landing-grid">
{gettingStartedLinks.map((item) => (
<Link className="landing-card" href={item.href} key={item.href}>
<span className="landing-card-eyebrow">Getting Started</span>
{workflowLinks.map((item) => (
<Link className="landing-card" href={item.href} key={`${item.eyebrow}-${item.href}`}>
<span className="landing-card-eyebrow">{item.eyebrow}</span>
<strong>{item.label}</strong>
<span>{item.description}</span>
</Link>
@@ -482,21 +547,6 @@ export default function HomePage() {
)}
</section>
<section className="landing-section">
<div className="landing-section-header">
<h2>快捷入口</h2>
<p>保持首页的业务工作台属性,而不是只放导航链接。</p>
</div>
<div className="landing-grid">
{quickLinks.map((link) => (
<Link className="landing-card" href={link.href} key={link.href}>
<span className="landing-card-eyebrow">{link.eyebrow}</span>
<strong>{link.label}</strong>
<span>{link.description}</span>
</Link>
))}
</div>
</section>
</main>
);
}
+20 -9
View File
@@ -95,19 +95,30 @@ export default function PilotReadinessPage() {
}
return (
<main className="page-shell">
<main className="page-shell pilot-readiness-page">
<Space direction="vertical" size={20} style={{ width: "100%" }}>
<section className="page-header-block">
<Space direction="vertical" size={10}>
<Tag color="blue">Phase 3 Pilot Delivery</Tag>
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Pilot Delivery</span>
<Title level={2}>试点交付准备</Title>
<Paragraph type="secondary">
阶段 3 验收门禁集中在这里:环境状态、默认账号、演示链路和自动化验收结果需要能被管理员快速复查。
阶段 3 验收门禁集中在这里:环境、账号、演示链路和自动化验收项集中在一个工作台,失败项直接暴露。
</Paragraph>
</Space>
<Button icon={<ReloadOutlined />} onClick={loadReadiness} loading={loading}>
刷新状态
</Button>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Readiness Control</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
交付门禁
</Title>
<ul className="review-page-tip-list">
<li>先看必需项失败数。</li>
<li>再检查默认账号与演示链路。</li>
<li>最后用验收项确认试点路径。</li>
</ul>
<Button icon={<ReloadOutlined />} onClick={loadReadiness} loading={loading}>
刷新状态
</Button>
</Card>
</section>
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
+6 -6
View File
@@ -73,24 +73,24 @@ export default function ProfilePage() {
};
return (
<main className="page-shell review-page-grid">
<main className="page-shell review-page-grid profile-page">
{contextHolder}
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Profile Settings</span>
<Title level={2}>维护当前账号的显示名称与登录密码。</Title>
<Text type="secondary">补齐 V1 个人设置页,保持范围只覆盖当前登录用户的基础资料维护。</Text>
<Title level={2}>管理当前账号的名称与密码。</Title>
<Text type="secondary">这里只处理当前登录账号,资料范围更清晰,操作也更轻。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">My Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
个人设置
账号边界
</Title>
<ul className="review-page-tip-list">
<li>支持修改当前显示名称。</li>
<li>显示名称立即同步到顶栏。</li>
<li>密码留空时保持不变。</li>
<li>仅作用于当前登录账号。</li>
<li>不会影响租户内其他成员。</li>
</ul>
</Card>
</section>
+12 -27
View File
@@ -19,12 +19,6 @@ type ReviewFormValues = {
const reviewHighlights = ["支持 PDF / DOCX / TXT 上传", "可叠加补充文本说明", "提交后自动进入结果详情页"];
const reviewTips = [
"上传文件后会先抽取文本,再进入风险识别链路。",
"如合同有补充说明,可直接粘贴在右侧文本框中一起审查。",
"详情页会展示风险总览、条款摘要与可下载报告。",
];
export default function ReviewPage() {
const router = useRouter();
const [form] = Form.useForm<ReviewFormValues>();
@@ -101,8 +95,8 @@ export default function ReviewPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Start Review</span>
<Title level={2}>提交一份合同,快速进入审查工作流。</Title>
<Text type="secondary">支持文件上传与文本补充双入口,让合同提交从一开始就具备正式产品工作台的使用体验。</Text>
<Title level={2}>上传合同,直接进入风险审查。</Title>
<Text type="secondary">文件上传和文本粘贴合并成一个提交流,先完成合同输入,再设置审查视角。</Text>
<div className="review-page-highlight-list">
{reviewHighlights.map((item) => (
<span className="review-page-highlight-chip" key={item}>
@@ -110,14 +104,22 @@ export default function ReviewPage() {
</span>
))}
</div>
<div className="review-hero-visual" aria-label="合同审查流程预览">
{submissionSteps.map((item, index) => (
<div className="review-hero-visual-step" key={item}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{item}</strong>
</div>
))}
</div>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Submission Guide</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
审查前提示
提交节奏
</Title>
<ul className="review-page-tip-list">
{reviewTips.map((item) => (
{submissionSteps.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
@@ -160,16 +162,6 @@ export default function ReviewPage() {
</Space>
</Card>
<Card className="review-surface-card" title="提交流程">
<div className="review-submission-step-list">
{submissionSteps.map((item, index) => (
<div className="review-submission-step" key={item}>
<span className="review-submission-step-index">0{index + 1}</span>
<strong>{item}</strong>
</div>
))}
</div>
</Card>
</Space>
</Col>
@@ -221,13 +213,6 @@ export default function ReviewPage() {
</Button>
</Card>
<Card className="review-surface-card" title="本次提交说明">
<ul className="review-page-tip-list review-page-tip-list-compact">
{reviewTips.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</Card>
</Space>
</Col>
</Row>
+82 -82
View File
@@ -482,94 +482,94 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str
))}
</section>
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-hero">
<div>
<span className="landing-card-eyebrow">Review Copilot</span>
<strong>继续用合同审查助手跟进这份合同</strong>
<p>会把当前审查结果、风险等级、审查建议和文本摘要一起带入问答页,直接围绕这份合同继续追问。</p>
</div>
<div className="chat-review-meta-grid">
<div className="chat-review-meta-card">
<span>当前任务</span>
<strong>{detail?.title ?? "-"}</strong>
<div className="review-detail-action-dock">
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-hero">
<div>
<span className="landing-card-eyebrow">Review Copilot</span>
<strong>继续用审查助手处理这份合同</strong>
<p>自动带入风险等级、审查建议和文本摘要,直接进入下一轮追问。</p>
</div>
<div className="chat-review-meta-card">
<span>总体风险</span>
<strong>{overallRisk}</strong>
<div className="chat-review-meta-grid">
<div className="chat-review-meta-card">
<span>当前任务</span>
<strong>{detail?.title ?? "-"}</strong>
</div>
<div className="chat-review-meta-card">
<span>总体风险</span>
<strong>{overallRisk}</strong>
</div>
<div className="chat-review-meta-card">
<span>问答模式</span>
<strong>审查上下文</strong>
</div>
<div className="chat-review-meta-card">
<span>建议动作</span>
<strong>先处理高风险</strong>
</div>
</div>
<div className="chat-review-meta-card">
<span>问答模式</span>
<strong>审查上下文已预置</strong>
</div>
<div className="chat-review-meta-card">
<span>建议动作</span>
<strong>继续追问条款风险</strong>
</div>
</div>
<Space wrap>
{reviewChatPayloadReady ? (
<Link className="landing-primary-action" href={reviewChatHref} onClick={handleOpenReviewChat}>
进入审查助手问答
<Space wrap>
{reviewChatPayloadReady ? (
<Link className="landing-primary-action" href={reviewChatHref} onClick={handleOpenReviewChat}>
进入审查助手问答
</Link>
) : (
<Button type="primary" loading>
正在准备审查上下文
</Button>
)}
<Link className="landing-secondary-action" href="/chat">
打开通用问答页
</Link>
) : (
<Button type="primary" loading>
正在准备审查上下文
</Button>
)}
<Link className="landing-secondary-action" href="/chat">
打开通用问答页
</Link>
</Space>
</div>
</Card>
</Space>
</div>
</Card>
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-layout">
<div className="chat-review-section-header">
<span className="landing-card-eyebrow">Quick Actions</span>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-layout">
<div className="chat-review-section-header">
<span className="landing-card-eyebrow">QUICK ACTIONS</span>
<strong>围绕当前审查结果,直接继续推进</strong>
<Text type="secondary">{`当前总体风险为${overallRisk},可直接解释风险、生成修改清单或准备谈判策略。`}</Text>
</div>
<Space wrap>
{quickActionItems.map((item, index) => (
<Link className={index === 0 ? "landing-primary-action" : "landing-secondary-action"} href={item.href} key={item.label}>
{item.label}
</Link>
))}
</Space>
<div className="review-detail-info-note" style={{ marginTop: 16 }}>
当前追问主线:优先围绕总体风险、重点风险项与条款摘要,继续把问答结果回流为修改动作和谈判准备。
</div>
</div>
</Card>
<Text type="secondary">{`总体风险:${overallRisk}。优先解释风险、生成修改清单或准备谈判策略。`}</Text>
</div>
<Space wrap>
{quickActionItems.map((item, index) => (
<Link className={index === 0 ? "landing-primary-action" : "landing-secondary-action"} href={item.href} key={item.label}>
{item.label}
</Link>
))}
</Space>
</div>
</Card>
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-layout">
<div className="chat-review-section-header">
<span className="landing-card-eyebrow">Workbench</span>
<strong>处理工作台入口</strong>
<Text type="secondary">围绕当前审查任务,继续在报告、文件、知识库与审查列表之间快速切换。</Text>
</div>
<Space wrap>
{report ? (
<Button type="primary" loading={downloadingReport} onClick={() => void handleDownloadReport()}>
下载审查报告
</Button>
) : null}
{detail?.can_retry ? (
<Button loading={retryingReview} onClick={() => void handleRetryReview()}>
重新发起审查
</Button>
) : null}
{reviewWorkbenchItems.map((item) => (
<Link className={item.primary ? "landing-primary-action" : "landing-secondary-action"} href={item.href} key={item.label}>
{item.label}
</Link>
))}
</Space>
</div>
</Card>
<Card className="review-surface-card" bordered={false}>
<div className="chat-review-layout">
<div className="chat-review-section-header">
<span className="landing-card-eyebrow">Workbench</span>
<strong>报告、文件与列表入口</strong>
</div>
<Space wrap>
{report ? (
<Button type="primary" loading={downloadingReport} onClick={() => void handleDownloadReport()}>
下载审查报告
</Button>
) : null}
{detail?.can_retry ? (
<Button loading={retryingReview} onClick={() => void handleRetryReview()}>
重新发起审查
</Button>
) : null}
{reviewWorkbenchItems.map((item) => (
<Link className={item.primary ? "landing-primary-action" : "landing-secondary-action"} href={item.href} key={item.label}>
{item.label}
</Link>
))}
</Space>
</div>
</Card>
</Space>
</div>
<Row gutter={[16, 16]}>
<Col xs={24} lg={8}>
+3 -3
View File
@@ -411,8 +411,8 @@ export default function ReviewsPage() {
<section className="review-history-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Review History</span>
<Title level={2}>把审查记录沉淀成可继续处理的任务工作台。</Title>
<Text type="secondary">列表页已按需求继续收口为可交付工作台:支持筛选、进入详情、下载报告、查看文件资产与继续处理。</Text>
<Title level={2}>审查记录不再只是列表,而是任务工作台。</Title>
<Text type="secondary">从这里筛选任务、进入详情、下载报告,或继续处理未完成合同。</Text>
<div className="review-history-latest">最近一次任务创建时间:{latestTaskLabel}</div>
<div className="review-history-filter-summary">当前列表展示:{filterSummary}</div>
</div>
@@ -420,7 +420,7 @@ export default function ReviewsPage() {
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">History Insight</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
审查记录工作台
处理优先级
</Title>
<ul className="review-page-tip-list">
{historyTips.map((item) => (
+3 -4
View File
@@ -279,16 +279,15 @@ export default function RulesPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Rule Management</span>
<Title level={2}>集中维护合同审查规则,并直接驱动审查结果。</Title>
<Text type="secondary">先补齐 V1 最小闭环:规则列表、新增、编辑、启用停用,并接入现有关键词审查链路。</Text>
<Title level={2}>维护审查规则,直接影响风险识别。</Title>
<Text type="secondary">左侧编辑规则,右侧查看启停状态和命中依据,规则变更更容易复核。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
规则维护流程
规则节奏
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放规则治理能力。</li>
<li>先配置关键词规则,再发起合同审查。</li>
<li>支持按合同类型区分规则。</li>
<li>停用规则后不再参与后续审查匹配。</li>
+15 -15
View File
@@ -360,8 +360,8 @@ export default function TemplatesPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Template Management</span>
<Title level={2}>集中维护合同模板,先补齐可用的管理闭环。</Title>
<Text type="secondary">当前支持模板列表、创建、编辑、删除,以及变量 JSON 配置的最小 V1 管理流程。</Text>
<Title level={2}>维护可复用模板和变量字段。</Title>
<Text type="secondary">模板正文、变量配置和启停状态放在同一个工作台,方便生成链路直接消费。</Text>
<Space style={{ marginTop: 16 }} wrap>
<Button onClick={() => void handleSeedDefaults()} loading={seeding}>
一键初始化默认模板
@@ -372,13 +372,12 @@ export default function TemplatesPage() {
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
模板维护流程
模板节奏
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放模板治理能力。</li>
<li>先录入标准模板正文。</li>
<li>变量区先以 JSON 数组维护占位字段。</li>
<li>启用状态可控制模板是否对后续流程开放。</li>
<li>再维护变量名与显示名。</li>
<li>启用状态决定生成页是否可用。</li>
</ul>
<Alert showIcon type="info" message="权限边界:member 使用合同生成链路,admin 负责模板管理。" />
</Card>
@@ -399,15 +398,13 @@ export default function TemplatesPage() {
<Form.Item label="模板正文" name="content" rules={[{ required: true, message: "请输入模板正文" }]}>
<Input.TextArea rows={8} placeholder="请输入合同模板正文" />
</Form.Item>
<Card
size="small"
title="变量配置"
extra={
<div className="embedded-panel">
<div className="embedded-panel-header">
<strong>变量配置</strong>
<Button size="small" onClick={addVariable} disabled={hasEmptyVariable(watchedVariables)}>
添加变量
</Button>
}
>
</div>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
{(watchedVariables ?? [{}]).map((_, index) => (
<Space key={index} size="middle" style={{ display: "flex" }} align="start">
@@ -423,10 +420,13 @@ export default function TemplatesPage() {
</Space>
))}
</Space>
</Card>
<Card size="small" title="变量 JSON 预览">
</div>
<div className="embedded-panel">
<div className="embedded-panel-header">
<strong>变量 JSON 预览</strong>
</div>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>{variablePreview}</pre>
</Card>
</div>
<Form.Item label="启用状态" name="enabled" valuePropName="checked" style={{ marginTop: 16 }}>
<Switch checkedChildren="启用" unCheckedChildren="停用" />
</Form.Item>
+3 -4
View File
@@ -105,16 +105,15 @@ export default function TenantSettingsPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">Tenant Settings</span>
<Title level={2}>查看当前租户基础信息、套餐与容量限制。</Title>
<Text type="secondary">先补齐 V1 最小闭环:管理员可维护租户基础信息,并查看当前套餐和容量上限。</Text>
<Title level={2}>维护租户信息,核对套餐容量。</Title>
<Text type="secondary">基础信息可编辑,套餐和限制作为右侧只读核对区展示。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
租户设置工作台
租户边界
</Title>
<ul className="review-page-tip-list">
<li>当前页面为管理员主用,普通成员不开放租户设置更新能力。</li>
<li>支持维护租户名称、编码与状态。</li>
<li>展示当前套餐与容量限制。</li>
<li>范围保持在当前租户,不扩展到租户管理列表。</li>
+3 -4
View File
@@ -237,16 +237,15 @@ export default function UsersPage() {
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">User Management</span>
<Title level={2}>集中管理租户内账号、角色与启停状态。</Title>
<Text type="secondary">先补齐 V1 最小闭环:用户列表、新增、编辑、角色调整和启停管理。</Text>
<Title level={2}>管理成员账号、角色和启停状态。</Title>
<Text type="secondary">账号表单和成员列表并排处理,适合管理员快速开通、调整和停用。</Text>
</div>
<Card className="review-side-card" bordered={false}>
<span className="landing-panel-eyebrow">Admin Workspace</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
用户管理流程
成员治理
</Title>
<ul className="review-page-tip-list">
<li>管理员可在当前租户内维护账号。</li>
<li>登录账号统一按邮箱格式管理,和注册页口径保持一致。</li>
<li>支持调整角色与账号状态。</li>
<li>编辑用户时可选填新密码,不填则保持原密码。</li>
+90 -25
View File
@@ -1,35 +1,53 @@
"use client";
import {
AuditOutlined,
BookOutlined,
CommentOutlined,
DashboardOutlined,
FileDoneOutlined,
FileProtectOutlined,
FolderOpenOutlined,
HistoryOutlined,
SafetyCertificateOutlined,
SettingOutlined,
SolutionOutlined,
TeamOutlined,
ThunderboltOutlined,
UserOutlined,
DownOutlined,
} from "@ant-design/icons";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { Button } from "antd";
import { useEffect, useRef } from "react";
import { Button, Dropdown, type MenuProps } from "antd";
import { clearSession, useAuthStore } from "@/stores/auth.store";
const publicNavItems = [
{ href: "/", label: "首页" },
{ href: "/login?tab=register", label: "注册" },
{ href: "/login", label: "登录" },
{ href: "/", label: "首页", icon: <DashboardOutlined /> },
{ href: "/login?tab=register", label: "注册", icon: <UserOutlined /> },
{ href: "/login", label: "登录", icon: <SafetyCertificateOutlined /> },
];
const privateNavItems = [
{ href: "/", label: "首页" },
{ href: "/review", label: "发起审查" },
{ href: "/reviews", label: "审查记录" },
{ href: "/chat", label: "法律问答" },
{ href: "/contracts", label: "合同生成" },
{ href: "/profile", label: "个人设置" },
{ href: "/", label: "首页", icon: <DashboardOutlined /> },
{ href: "/review", label: "发起审查", icon: <FileProtectOutlined /> },
{ href: "/reviews", label: "审查记录", icon: <HistoryOutlined /> },
{ href: "/chat", label: "法律问答", icon: <CommentOutlined /> },
{ href: "/contracts", label: "合同生成", icon: <FileDoneOutlined /> },
{ href: "/profile", label: "个人设置", icon: <UserOutlined /> },
];
const adminNavItems = [
{ href: "/rules", label: "规则管理" },
{ href: "/templates", label: "模板管理" },
{ href: "/knowledge-bases", label: "知识库管理" },
{ href: "/files", label: "文件管理" },
{ href: "/tenant-settings", label: "租户设置" },
{ href: "/users", label: "用户管理" },
{ href: "/audit-logs", label: "审计日志" },
{ href: "/pilot-readiness", label: "试点交付" },
{ href: "/rules", label: "规则管理", icon: <SolutionOutlined /> },
{ href: "/templates", label: "模板管理", icon: <BookOutlined /> },
{ href: "/knowledge-bases", label: "知识库管理", icon: <FolderOpenOutlined /> },
{ href: "/files", label: "文件管理", icon: <FileDoneOutlined /> },
{ href: "/tenant-settings", label: "租户设置", icon: <SettingOutlined /> },
{ href: "/users", label: "用户管理", icon: <TeamOutlined /> },
{ href: "/audit-logs", label: "审计日志", icon: <AuditOutlined /> },
{ href: "/pilot-readiness", label: "试点交付", icon: <ThunderboltOutlined /> },
];
function isActive(pathname: string, href: string) {
@@ -43,18 +61,37 @@ function isActive(pathname: string, href: string) {
export function AppTopbar() {
const pathname = usePathname();
const router = useRouter();
const navRef = useRef<HTMLElement | null>(null);
const currentUser = useAuthStore((state) => state.currentUser);
const navItems = currentUser
? currentUser.role === "admin"
? [...privateNavItems, ...adminNavItems]
: privateNavItems
: publicNavItems;
const navItems = currentUser ? privateNavItems : publicNavItems;
const showAdminMenu = currentUser?.role === "admin";
const adminMenuActive = adminNavItems.some((item) => isActive(pathname, item.href));
const adminMenuItems: MenuProps["items"] = adminNavItems.map((item) => ({
key: item.href,
label: (
<Link className="app-admin-menu-item" href={item.href}>
<span className="app-nav-icon" aria-hidden="true">
{item.icon}
</span>
{item.label}
</Link>
),
}));
const handleLogout = () => {
clearSession();
router.push("/login");
};
useEffect(() => {
const frameId = window.requestAnimationFrame(() => {
const activeItem = navRef.current?.querySelector(".app-nav-link-active");
activeItem?.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" });
});
return () => window.cancelAnimationFrame(frameId);
}, [currentUser?.role, pathname]);
return (
<header className="app-topbar">
<div className="app-topbar-inner">
@@ -67,16 +104,44 @@ export function AppTopbar() {
</Link>
<div className="app-topbar-right">
<nav className="app-nav" aria-label="Primary">
<nav className="app-nav" aria-label="Primary" ref={navRef}>
{navItems.map((item) => (
<Link
className={isActive(pathname, item.href) ? "app-nav-link app-nav-link-active" : "app-nav-link"}
href={item.href}
key={item.href}
>
<span className="app-nav-icon" aria-hidden="true">
{item.icon}
</span>
{item.label}
</Link>
))}
{showAdminMenu ? (
<Link
className={isActive(pathname, "/pilot-readiness") ? "app-nav-link app-nav-link-active" : "app-nav-link"}
href="/pilot-readiness"
>
<span className="app-nav-icon" aria-hidden="true">
<ThunderboltOutlined />
</span>
试点交付
</Link>
) : null}
{showAdminMenu ? (
<Dropdown menu={{ items: adminMenuItems }} placement="bottomRight" trigger={["click"]}>
<button
className={adminMenuActive ? "app-nav-link app-nav-admin-trigger app-nav-link-active" : "app-nav-link app-nav-admin-trigger"}
type="button"
>
<span className="app-nav-icon" aria-hidden="true">
<SettingOutlined />
</span>
管理
<DownOutlined className="app-nav-admin-caret" />
</button>
</Dropdown>
) : null}
</nav>
{currentUser ? (
@@ -89,7 +154,7 @@ export function AppTopbar() {
退出
</Button>
</div>
) : (
) : pathname === "/login" ? null : (
<Link className="app-login-entry" href="/login">
进入登录
</Link>
@@ -15,11 +15,11 @@
| 阶段 3:试点交付准备 | PASS | `project-docs/阶段3-试点交付准备执行记录-2026-05-01.md` |
| 阶段 4:试点运行监控与安全加固 | PASS | `project-docs/阶段4-试点运行监控与安全加固执行记录-2026-05-01.md` |
| 阶段 5:生产运行、安全、监控、备份与依赖治理 | PASS | `project-docs/阶段5-生产运行安全监控备份与依赖治理执行记录-2026-05-01.md` |
| 阶段 6:生产运营自动化与发布门禁固化 | PASS_WITH_WORKTREE_WARN | `project-docs/阶段6-生产运营自动化与发布门禁固化执行记录-2026-05-01.md` |
| 阶段 6:生产运营自动化与发布门禁固化 | PASS_PENDING_STRICT_GATE | `project-docs/阶段6-生产运营自动化与发布门禁固化执行记录-2026-05-01.md` |
阶段 1 已完成源码闭环与生产入口闭环,生产无人 E2E `18/18` 通过。阶段 2 已完成 Review 重试、Knowledge Base 文档重试、Review -> Chat -> Contract 上下文联动、首页交付工作台和阶段 2 自动化无人闭环,本地与生产 E2E 均 `7/7` 通过。阶段 3 已完成系统 readiness、试点交付准备页、默认账号说明、演示样本数据、依赖健康门禁和阶段 3 自动化无人闭环,本地与生产 E2E 均 `5/5` 通过。
当前后续重点已从“生产运行治理”推进到“生产运营自动化与发布门禁固化”:阶段 6 已完成前端视觉改动梳理、发布门禁聚合脚本、默认发布门禁实跑和严格门禁探针。默认发布门禁 required 项全部 PASS,当前唯一 WARN 是工作区保留未提交前端视觉刷新。
当前后续重点已从“生产运行治理”推进到“生产运营自动化与发布门禁固化”:阶段 6 已完成前端视觉改动梳理、发布门禁聚合脚本、默认发布门禁实跑、严格门禁探针和前端视觉刷新验收。下一步是提交视觉刷新并复跑严格发布门禁。
---
@@ -219,4 +219,4 @@ AI合同平台当前已经不是从零建设阶段。登录、合同审查、Rev
## 六、当前一句话结论
AI合同平台阶段 1、阶段 2、阶段 3、阶段 4、阶段 5 均已完成闭环交付验证,阶段 6 首批发布门禁自动化也已形成闭环。当前系统具备主业务闭环、试点交付准备、生产健康巡检、备份恢复演练、依赖治理、安全基线、生产关键 E2E 回归和统一发布门禁证据;正式发布前需先处理当前未提交前端视觉刷新并复跑严格门禁。
AI合同平台阶段 1、阶段 2、阶段 3、阶段 4、阶段 5 均已完成闭环交付验证,阶段 6 首批发布门禁自动化也已形成闭环。当前系统具备主业务闭环、试点交付准备、生产健康巡检、备份恢复演练、依赖治理、安全基线、生产关键 E2E 回归、前端视觉刷新验收和统一发布门禁证据;提交视觉刷新后需复跑严格门禁。
@@ -0,0 +1,102 @@
# 阶段 6:前端视觉刷新验收记录(2026-05-01)
> 目标:对已完成的前端视觉刷新进行独立验收,确认它不破坏主链路、管理入口、试点交付入口和移动端基本可用性,然后再纳入发布门禁。
---
## 一、验收结论
| 项目 | 状态 | 备注 |
|---|---|---|
| 前端 lint | PASS | `npm run lint` 通过 |
| 前端 production build | PASS | `npm run build` 通过,18 个 app route 生成完成 |
| 本地视觉版阶段 1/2/3 E2E | PASS | 使用 `http://127.0.0.1:3014` 前端和试点后端代理,阶段 1/2/3 全部通过 |
| 桌面/移动视觉 smoke | PASS | 10 个页面 x 2 个视口,截图和 console error 检查通过 |
| 验收锚点兼容修补 | PASS | 恢复 `优先处理摘要`、`运营摘要`、`试点交付准备`、`阶段 3 验收门禁`、`QUICK ACTIONS` 等稳定验收文案 |
| 顶部导航兼容修补 | PASS | admin 顶栏保留直接 `/pilot-readiness` 链接,同时保留管理下拉菜单 |
当前判定:前端视觉刷新可以进入提交和严格发布门禁。
---
## 二、本次视觉刷新范围
| 区域 | 说明 |
|---|---|
| 全局主题 | 新增视觉主题、卡片、导航、页面布局、移动端约束等样式 |
| 顶部导航 | 增加图标化导航、admin 管理下拉、试点交付直接入口 |
| 首页 | 公共首页视觉场景、登录后工作台、优先摘要、核心入口和管理员入口重组 |
| 登录页 | 增加安全工作台预览和更紧凑的登录提示 |
| 发起审查 | 提交流程视觉前置,减少重复说明卡片 |
| 审查详情 | Review Copilot、Quick Actions、报告/文件入口集中为行动区 |
| 合同生成/详情 | 预览、变量、导出选项和生成内容布局优化 |
| 管理页 | 知识库、模板、文件、用户、规则、租户设置、审计日志等页面统一视觉口径 |
| 试点交付 | Readiness 门禁页面改为阶段 6 统一视觉风格 |
---
## 三、验证记录
静态和构建:
```text
npm run lint
npm run build
```
本地视觉版服务:
```text
http://127.0.0.1:3014
BACKEND_BASE_URL=http://192.168.3.90:8001
```
阶段 1/2/3 E2E 产物:
```text
project-docs/qa-artifacts/phase6-visual-refresh/2026-05-01T10-24-37/closed-loop-regression-dev-3014/2026-05-01T10-33-12/
```
视觉 smoke 产物:
```text
project-docs/qa-artifacts/phase6-visual-refresh/2026-05-01T10-24-37/visual-smoke-2026-05-01T10-34-32/
```
视觉 smoke 覆盖页面:
1. `/`
2. `/login`
3. `/review`
4. `/reviews`
5. `/contracts`
6. `/knowledge-bases`
7. `/templates`
8. `/pilot-readiness`
9. `/users`
覆盖视口:
1. desktop:`1440 x 1000`
2. mobile:`390 x 844`
---
## 四、发现与修补
| 问题 | 处理 |
|---|---|
| 首页视觉刷新后移除了旧 E2E 依赖的 `优先处理摘要` 和 `运营摘要` 文案 | 保留新布局,同时恢复稳定验收锚点 |
| 审查详情 Quick Actions 文案大小写与旧 E2E 不一致 | 恢复 `QUICK ACTIONS` 和 `围绕当前审查结果` |
| 试点交付页面标题更换后旧 E2E 无法识别 | 恢复 `试点交付准备` 和 `阶段 3 验收门禁` |
| admin 试点入口被收进管理下拉后旧 E2E 找不到直接链接 | 顶栏保留 `/pilot-readiness` 直接链接 |
| 旧 `3004` 本地服务占用端口且连接本地旧 API 状态 | 改用独立 `3014` 验收服务,代理到试点后端 |
---
## 五、下一步
1. 提交前端视觉刷新和 `.gstack/` 忽略规则。
2. 推送到 `192.168.3.90` 仓库。
3. 复跑严格发布门禁:`run-production-release-gate.ps1 -RequireCleanWorktree`。
4. 若严格门禁 PASS,则阶段 6 从 `PASS_WITH_WORKTREE_WARN` 更新为 `PASS`。
@@ -13,7 +13,8 @@
| 发布门禁聚合脚本 | READY | 已新增 `scripts/run-production-release-gate.ps1` |
| 发布门禁实跑 | WARN | 默认门禁已跑通,required 项全部 PASS,dirty worktree 为 advisory WARN |
| 严格门禁探针 | PASS | `-RequireCleanWorktree` 已按预期把未提交视觉刷新升级为 FAIL |
| 阶段 6 首批闭环 | PASS_WITH_WORKTREE_WARN | 发布门禁自动化已闭环,正式发布前仍需处理当前前端视觉刷新 |
| 前端视觉刷新验收 | PASS | lint、build、阶段 1/2/3 E2E、桌面/移动视觉 smoke 均通过 |
| 阶段 6 首批闭环 | PASS_PENDING_STRICT_GATE | 发布门禁自动化已闭环,前端视觉刷新已验收,下一步提交后复跑严格门禁 |
---
@@ -26,7 +27,7 @@
1. 本轮阶段 6 运维自动化提交不纳入这些前端视觉文件。
2. 发布门禁默认模式会记录 dirty worktree 为 WARN。
3. 正式发布前必须使用 `-RequireCleanWorktree`,把 dirty worktree 升级为 FAIL。
4. 前端视觉刷新后续单独做浏览器验收和提交。
4. 前端视觉刷新已完成独立验收,下一步提交后复跑严格发布门禁。
清单文档:
@@ -34,6 +35,12 @@
project-docs/阶段6-前端视觉改动梳理-2026-05-01.md
```
验收记录:
```text
project-docs/阶段6-前端视觉刷新验收记录-2026-05-01.md
```
---
## 三、发布门禁脚本
@@ -116,4 +123,4 @@ project-docs/qa-artifacts/phase6-release-gate/2026-05-01T09-29-45/
2. 发布门禁脚本可复跑,并生成统一汇总报告。
3. 默认门禁 required 项全部 PASS。
4. 严格门禁可以阻断未提交工作区。
5. 正式发布前下一步应先处理或提交当前前端视觉刷新。
5. 前端视觉刷新已通过验收,下一步应提交视觉刷新并复跑 `-RequireCleanWorktree` 严格门禁。