feat: initialize V2 contract web shell
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
backend/.venv/
|
||||
node_modules/
|
||||
frontend/.next/
|
||||
frontend/.next-dev-log/
|
||||
frontend/node_modules/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_CONTRACT_API_BASE_URL=http://localhost:8100
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Contract Web
|
||||
|
||||
Next.js app for the V2 contract platform.
|
||||
|
||||
## Local Run
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run web:dev
|
||||
```
|
||||
|
||||
Default URL: `http://localhost:3000`
|
||||
|
||||
## Validate
|
||||
|
||||
```powershell
|
||||
npm run web:typecheck
|
||||
npm run web:build
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ["@yuqei/design-system"]
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@yuqei/contract-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yuqei/design-system": "0.1.0",
|
||||
"lucide-react": "1.21.0",
|
||||
"next": "16.2.9",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.0.0",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Settings, ShieldCheck, SlidersHorizontal } from "lucide-react";
|
||||
|
||||
const adminSections = [
|
||||
{
|
||||
title: "企业权限",
|
||||
description: "组织、角色、合同可见范围和操作边界。",
|
||||
icon: ShieldCheck
|
||||
},
|
||||
{
|
||||
title: "AI 参数",
|
||||
description: "模型连接、运行参数、提示词版本和审计策略。",
|
||||
icon: SlidersHorizontal
|
||||
},
|
||||
{
|
||||
title: "系统设置",
|
||||
description: "文件存储、通知、备份、健康检查和部署配置。",
|
||||
icon: Settings
|
||||
}
|
||||
];
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>管理区</h1>
|
||||
<p>管理员能力集中管理,业务用户保持简洁主流程。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid-two">
|
||||
{adminSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
return (
|
||||
<section className="panel" key={section.title}>
|
||||
<div className="panel-body list">
|
||||
<Icon size={22} />
|
||||
<h2 className="panel-title">{section.title}</h2>
|
||||
<p className="muted">{section.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import Link from "next/link";
|
||||
import { Archive, Download, FilePenLine, GitCompare, Send } from "lucide-react";
|
||||
import { contracts } from "@/lib/mock-data";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default async function ContractDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const contract = contracts.find((item) => item.id === id) ?? contracts[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>{contract.title}</h1>
|
||||
<p>{contract.code} · {contract.counterparty} · 更新时间 {contract.updatedAt}</p>
|
||||
</div>
|
||||
<Link className="button" href="/contracts">
|
||||
返回合同列表
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="next-action">
|
||||
<div>
|
||||
<strong>下一步操作:{contract.nextAction}</strong>
|
||||
<span className="muted">只把当前最应该做的动作放到首位,其余能力继续保留但不抢视线。</span>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<button className="button button-primary">
|
||||
<Send size={16} />
|
||||
提交审批
|
||||
</button>
|
||||
<button className="button">
|
||||
<FilePenLine size={16} />
|
||||
编辑
|
||||
</button>
|
||||
<button className="button icon-button" title="下载">
|
||||
<Download size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar" style={{ marginTop: 16 }}>
|
||||
<a className="tag tag-primary" href="#overview">概览</a>
|
||||
<a className="tag" href="#preview">正文预览</a>
|
||||
<a className="tag" href="#approval">审批</a>
|
||||
<a className="tag" href="#versions">版本</a>
|
||||
<a className="tag" href="#audit">审计</a>
|
||||
</div>
|
||||
|
||||
<div className="detail-grid">
|
||||
<main className="list">
|
||||
<section className="panel" id="overview">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">合同概览</h2>
|
||||
<span className={`tag ${contract.statusTone}`}>{contract.status}</span>
|
||||
</div>
|
||||
<div className="panel-body grid-two">
|
||||
<div>
|
||||
<div className="item-meta">合同金额</div>
|
||||
<strong>{contract.amount}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<div className="item-meta">业务负责人</div>
|
||||
<strong>{contract.owner}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<div className="item-meta">相对方</div>
|
||||
<strong>{contract.counterparty}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<div className="item-meta">下一步</div>
|
||||
<strong>{contract.nextAction}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel" id="preview">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">正文预览</h2>
|
||||
<button className="button">
|
||||
<GitCompare size={16} />
|
||||
版本比对
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<article className="document-preview">
|
||||
<h2>{contract.title}</h2>
|
||||
<p>甲方与乙方基于真实交易背景,经平等协商,形成本合同。双方应按约定履行交付、验收、付款、保密和违约责任。</p>
|
||||
<p>法律审查意见、审批意见、版本差异和履约证据会在侧栏集中折叠展示,正文区域保持干净可读。</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside className="list">
|
||||
<details className="panel" id="approval" open>
|
||||
<summary className="panel-header">
|
||||
<span className="panel-title">审批时间线</span>
|
||||
</summary>
|
||||
<div className="panel-body list">
|
||||
<div className="list-item">
|
||||
<strong>业务提交</strong>
|
||||
<span className="item-meta">已完成 · 今天 09:30</span>
|
||||
</div>
|
||||
<div className="list-item">
|
||||
<strong>法务审查</strong>
|
||||
<span className="item-meta">待处理 · 建议先确认付款条款</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details className="panel" id="versions">
|
||||
<summary className="panel-header">
|
||||
<span className="panel-title">版本与履约</span>
|
||||
</summary>
|
||||
<div className="panel-body list">
|
||||
<button className="button">
|
||||
<GitCompare size={16} />
|
||||
查看版本差异
|
||||
</button>
|
||||
<button className="button">
|
||||
<Archive size={16} />
|
||||
管理履约证据
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details className="panel" id="audit">
|
||||
<summary className="panel-header">
|
||||
<span className="panel-title">审计与技术信息</span>
|
||||
</summary>
|
||||
<div className="panel-body list">
|
||||
<div className="item-meta">创建、下载、审查、审批、归档等动作会统一进入审计时间线。</div>
|
||||
<div className="item-meta">技术元数据默认折叠,不打扰普通用户完成主流程。</div>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import Link from "next/link";
|
||||
import { Download, FilePlus2, Search } from "lucide-react";
|
||||
import { contracts } from "@/lib/mock-data";
|
||||
|
||||
export default function ContractsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>合同</h1>
|
||||
<p>按下一步动作组织合同,而不是把所有流程一次性压给用户。</p>
|
||||
</div>
|
||||
<Link className="button button-primary" href="/workspace">
|
||||
<FilePlus2 size={17} />
|
||||
快速起草
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<input className="search-input" placeholder="搜索合同名称、相对方、编号" />
|
||||
<button className="button button-soft">
|
||||
<Search size={16} />
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>合同</th>
|
||||
<th>相对方</th>
|
||||
<th>状态</th>
|
||||
<th>下一步</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{contracts.map((contract) => (
|
||||
<tr key={contract.id}>
|
||||
<td>
|
||||
<Link className="item-title" href={`/contracts/${contract.id}`}>
|
||||
{contract.title}
|
||||
</Link>
|
||||
<div className="item-meta">{contract.code}</div>
|
||||
</td>
|
||||
<td>{contract.counterparty}</td>
|
||||
<td>
|
||||
<span className={`tag ${contract.statusTone}`}>{contract.status}</span>
|
||||
</td>
|
||||
<td>{contract.nextAction}</td>
|
||||
<td>{contract.updatedAt}</td>
|
||||
<td>
|
||||
<button className="button icon-button" title="下载">
|
||||
<Download size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import Link from "next/link";
|
||||
import { Download, Eye, Filter } from "lucide-react";
|
||||
import { files } from "@/lib/mock-data";
|
||||
|
||||
export default function FilesPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>合同仓库</h1>
|
||||
<p>按文件、关联合同、类型和上传时间统一管理合同资产。</p>
|
||||
</div>
|
||||
<button className="button button-primary">
|
||||
<Download size={16} />
|
||||
批量下载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<input className="search-input" placeholder="搜索文件名、合同、上传人" />
|
||||
<button className="button">
|
||||
<Filter size={16} />
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件</th>
|
||||
<th>关联合同</th>
|
||||
<th>类型</th>
|
||||
<th>上传时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr key={file.id}>
|
||||
<td>
|
||||
<strong>{file.name}</strong>
|
||||
<div className="item-meta">{file.size}</div>
|
||||
</td>
|
||||
<td>
|
||||
<Link className="item-title" href={`/contracts/${file.contractId}`}>
|
||||
{file.contractTitle}
|
||||
</Link>
|
||||
</td>
|
||||
<td><span className="tag">{file.type}</span></td>
|
||||
<td>{file.uploadedAt}</td>
|
||||
<td>
|
||||
<div className="topbar-actions">
|
||||
<button className="button icon-button" title="预览">
|
||||
<Eye size={16} />
|
||||
</button>
|
||||
<button className="button icon-button" title="下载">
|
||||
<Download size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #172033;
|
||||
--muted: #667085;
|
||||
--line: #d9e0ea;
|
||||
--canvas: #f6f8fb;
|
||||
--surface: #ffffff;
|
||||
--primary: #2457d6;
|
||||
--primary-soft: #eaf0ff;
|
||||
--success: #16794c;
|
||||
--success-soft: #e7f6ef;
|
||||
--warning: #b35b00;
|
||||
--warning-soft: #fff3df;
|
||||
--danger: #bd2d2d;
|
||||
--danger-soft: #fdecec;
|
||||
--accent: #0b7a75;
|
||||
--accent-soft: #e4f5f3;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--ink);
|
||||
background: var(--canvas);
|
||||
font-family: Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
padding: 20px 16px;
|
||||
color: #f8fbff;
|
||||
background: #16233f;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 4px 8px 18px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 18px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
color: #bac6d9;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
margin: 18px 8px 8px;
|
||||
color: #91a1bc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 40px;
|
||||
padding: 0 10px;
|
||||
border-radius: 8px;
|
||||
color: #d8e2f2;
|
||||
}
|
||||
|
||||
.nav-link svg {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link-active {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
display: grid;
|
||||
grid-template-rows: 64px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
height: 64px;
|
||||
padding: 0 28px;
|
||||
background: rgba(246, 248, 251, 0.92);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: min(100%, 1440px);
|
||||
margin: 0 auto;
|
||||
padding: 24px 28px 40px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
color: #ffffff;
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.button-soft {
|
||||
color: var(--primary);
|
||||
border-color: transparent;
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.7fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.list-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
min-width: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.item-title svg {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: min(100%, 360px);
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
min-height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: #edf1f7;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
color: var(--success);
|
||||
background: var(--success-soft);
|
||||
}
|
||||
|
||||
.tag-warning {
|
||||
color: var(--warning);
|
||||
background: var(--warning-soft);
|
||||
}
|
||||
|
||||
.tag-primary {
|
||||
color: var(--primary);
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
.tag-danger {
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.flow-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.flow-step-title {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.flow-step p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.next-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #bfd0ff;
|
||||
border-radius: 8px;
|
||||
background: #f5f8ff;
|
||||
}
|
||||
|
||||
.next-action strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
min-height: 420px;
|
||||
padding: 28px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.document-preview h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table th {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
details.panel summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details.panel summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.split-layout,
|
||||
.detail-grid,
|
||||
.grid-two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metric-row,
|
||||
.flow-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content,
|
||||
.topbar {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-row,
|
||||
.flow-strip,
|
||||
.nav-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SystemHealthPanel } from "@/features/system/system-health-panel";
|
||||
|
||||
export default function HealthPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>系统健康</h1>
|
||||
<p>上线前检查前端、合同 API、AI 平台和后台任务状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
<SystemHealthPanel />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "钰麒合同 V2",
|
||||
description: "企业合同智能起草、审查、协同与归档平台"
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>
|
||||
<AppShell>{children}</AppShell>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WorkspacePage } from "@/features/workspace/workspace-page";
|
||||
|
||||
export default function Home() {
|
||||
return <WorkspacePage />;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Link from "next/link";
|
||||
import { Search } from "lucide-react";
|
||||
import { searchGroups } from "@/lib/mock-data";
|
||||
|
||||
export default function SearchPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>统一搜索</h1>
|
||||
<p>标题就是主入口,结果按合同、审查、文件、模板分组展示。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<input className="search-input" placeholder="搜索合同、条款、相对方、文件" />
|
||||
<button className="button button-primary">
|
||||
<Search size={16} />
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="list">
|
||||
{searchGroups.map((group) => (
|
||||
<section className="panel" key={group.title}>
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">{group.title}</h2>
|
||||
<span className="tag">{group.items.length} 条</span>
|
||||
</div>
|
||||
<div className="panel-body list">
|
||||
{group.items.map((item) => (
|
||||
<Link className="list-item" href={item.href} key={item.title}>
|
||||
<div className="list-item-header">
|
||||
<span className="item-title">{item.title}</span>
|
||||
<span className="item-meta">{item.updatedAt}</span>
|
||||
</div>
|
||||
<span className="item-meta">{item.summary}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WorkspacePage } from "@/features/workspace/workspace-page";
|
||||
|
||||
export default function Page() {
|
||||
return <WorkspacePage />;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Activity,
|
||||
Archive,
|
||||
FileSearch,
|
||||
Files,
|
||||
Home,
|
||||
LayoutDashboard,
|
||||
Search,
|
||||
Settings
|
||||
} from "lucide-react";
|
||||
|
||||
const mainNav = [
|
||||
{ href: "/workspace", label: "工作台", icon: Home },
|
||||
{ href: "/contracts", label: "合同", icon: FileSearch },
|
||||
{ href: "/search", label: "统一搜索", icon: Search },
|
||||
{ href: "/files", label: "合同仓库", icon: Files }
|
||||
];
|
||||
|
||||
const adminNav = [
|
||||
{ href: "/admin", label: "管理区", icon: Settings },
|
||||
{ href: "/health", label: "系统健康", icon: Activity }
|
||||
];
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<aside className="app-sidebar">
|
||||
<Link className="brand-block" href="/workspace">
|
||||
<span className="brand-title">钰麒合同</span>
|
||||
<span className="brand-subtitle">AI Contract Platform V2</span>
|
||||
</Link>
|
||||
|
||||
<div className="nav-group-title">主流程</div>
|
||||
<nav className="nav-group" aria-label="主流程导航">
|
||||
{mainNav.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link className={`nav-link ${active ? "nav-link-active" : ""}`} href={item.href} key={item.href}>
|
||||
<Icon size={18} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="nav-group-title">管理</div>
|
||||
<nav className="nav-group" aria-label="管理导航">
|
||||
{adminNav.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link className={`nav-link ${active ? "nav-link-active" : ""}`} href={item.href} key={item.href}>
|
||||
<Icon size={18} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main className="app-main">
|
||||
<header className="topbar">
|
||||
<div className="topbar-title">{resolveTitle(pathname)}</div>
|
||||
<div className="topbar-actions">
|
||||
<Link className="button button-soft" href="/workspace">
|
||||
<LayoutDashboard size={16} />
|
||||
待我处理
|
||||
</Link>
|
||||
<Link className="button button-primary" href="/contracts">
|
||||
<Archive size={16} />
|
||||
新建合同
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
<div className="content">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTitle(pathname: string): string {
|
||||
if (pathname.startsWith("/contracts/")) return "合同详情";
|
||||
if (pathname.startsWith("/contracts")) return "合同主流程";
|
||||
if (pathname.startsWith("/search")) return "统一搜索";
|
||||
if (pathname.startsWith("/files")) return "合同仓库";
|
||||
if (pathname.startsWith("/admin")) return "管理区";
|
||||
if (pathname.startsWith("/health")) return "系统健康";
|
||||
return "工作台";
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Activity, RefreshCcw } from "lucide-react";
|
||||
|
||||
type HealthState =
|
||||
| { status: "checking" }
|
||||
| { status: "ok"; service: string; version: string; environment: string }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
const apiBaseUrl = process.env.NEXT_PUBLIC_CONTRACT_API_BASE_URL ?? "http://localhost:8100";
|
||||
|
||||
export function SystemHealthPanel() {
|
||||
const [health, setHealth] = useState<HealthState>({ status: "checking" });
|
||||
|
||||
async function checkHealth() {
|
||||
setHealth({ status: "checking" });
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/health`, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json() as { service: string; version: string; environment: string };
|
||||
setHealth({ status: "ok", ...data });
|
||||
} catch (error) {
|
||||
setHealth({
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : "unknown error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void checkHealth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">合同 API</h2>
|
||||
<button className="button" onClick={() => void checkHealth()}>
|
||||
<RefreshCcw size={16} />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body list">
|
||||
<div className="list-item">
|
||||
<div className="list-item-header">
|
||||
<span className="item-title">
|
||||
<Activity size={16} /> {apiBaseUrl}
|
||||
</span>
|
||||
{health.status === "ok" && <span className="tag tag-success">正常</span>}
|
||||
{health.status === "checking" && <span className="tag tag-primary">检查中</span>}
|
||||
{health.status === "error" && <span className="tag tag-danger">未连接</span>}
|
||||
</div>
|
||||
{health.status === "ok" && (
|
||||
<span className="item-meta">
|
||||
{health.service} · {health.version} · {health.environment}
|
||||
</span>
|
||||
)}
|
||||
{health.status === "error" && (
|
||||
<span className="item-meta">
|
||||
{health.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Link from "next/link";
|
||||
import { FilePlus2, Gavel, Inbox, Search, UploadCloud } from "lucide-react";
|
||||
import { mainFlow } from "@yuqei/design-system";
|
||||
import { tasks } from "@/lib/mock-data";
|
||||
|
||||
export function WorkspacePage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<h1>工作台</h1>
|
||||
<p>只突出待我处理和快速发起,复杂能力收进对应页面和管理区。</p>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<Link className="button button-primary" href="/contracts">
|
||||
<FilePlus2 size={17} />
|
||||
起草合同
|
||||
</Link>
|
||||
<Link className="button" href="/search">
|
||||
<Search size={17} />
|
||||
搜索
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-row">
|
||||
<div className="metric">
|
||||
<div className="metric-value">6</div>
|
||||
<div className="metric-label">待我处理</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-value">12</div>
|
||||
<div className="metric-label">进行中合同</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-value">3</div>
|
||||
<div className="metric-label">本周到期</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-value">98%</div>
|
||||
<div className="metric-label">审计覆盖</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="split-layout">
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">待我处理</h2>
|
||||
<span className="tag tag-primary">优先</span>
|
||||
</div>
|
||||
<div className="panel-body list">
|
||||
{tasks.map((task) => (
|
||||
<Link className="list-item" href={task.href} key={task.id}>
|
||||
<div className="list-item-header">
|
||||
<span className="item-title">{task.title}</span>
|
||||
<span className={`tag ${task.tone}`}>{task.type}</span>
|
||||
</div>
|
||||
<span className="item-meta">{task.summary}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="list">
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">快速发起</h2>
|
||||
</div>
|
||||
<div className="panel-body list">
|
||||
<Link className="button button-primary" href="/contracts">
|
||||
<FilePlus2 size={17} />
|
||||
起草新合同
|
||||
</Link>
|
||||
<Link className="button" href="/search">
|
||||
<Gavel size={17} />
|
||||
发起法律审查
|
||||
</Link>
|
||||
<Link className="button" href="/files">
|
||||
<UploadCloud size={17} />
|
||||
上传合同文件
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2 className="panel-title">主链路</h2>
|
||||
<Inbox size={18} />
|
||||
</div>
|
||||
<div className="panel-body flow-strip">
|
||||
{mainFlow.map((step) => (
|
||||
<div className="flow-step" key={step.key}>
|
||||
<div className="flow-step-title">{step.title}</div>
|
||||
<p>{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
export const contracts = [
|
||||
{
|
||||
id: "contract-001",
|
||||
title: "技术服务合同",
|
||||
code: "HT-2026-0001",
|
||||
counterparty: "北京辰星科技有限公司",
|
||||
status: "审查中",
|
||||
statusTone: "tag-warning",
|
||||
nextAction: "法务确认付款和验收条款",
|
||||
updatedAt: "今天 10:18",
|
||||
amount: "¥ 280,000",
|
||||
owner: "张敏"
|
||||
},
|
||||
{
|
||||
id: "contract-002",
|
||||
title: "房屋租赁合同",
|
||||
code: "HT-2026-0002",
|
||||
counterparty: "内蒙古青禾置业",
|
||||
status: "待提交",
|
||||
statusTone: "tag-primary",
|
||||
nextAction: "补充租赁期限后提交",
|
||||
updatedAt: "昨天 18:42",
|
||||
amount: "¥ 96,000",
|
||||
owner: "王磊"
|
||||
},
|
||||
{
|
||||
id: "contract-003",
|
||||
title: "采购框架协议",
|
||||
code: "HT-2026-0003",
|
||||
counterparty: "上海景云供应链",
|
||||
status: "已归档",
|
||||
statusTone: "tag-success",
|
||||
nextAction: "跟踪履约节点",
|
||||
updatedAt: "2026-06-20",
|
||||
amount: "¥ 1,200,000",
|
||||
owner: "李娜"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const tasks = [
|
||||
{
|
||||
id: "task-001",
|
||||
title: "技术服务合同需要法务审查",
|
||||
type: "审查",
|
||||
tone: "tag-warning",
|
||||
summary: "付款节点、验收标准和违约责任需要形成处理意见。",
|
||||
href: "/contracts/contract-001"
|
||||
},
|
||||
{
|
||||
id: "task-002",
|
||||
title: "房屋租赁合同待补充期限",
|
||||
type: "起草",
|
||||
tone: "tag-primary",
|
||||
summary: "补齐租赁期限、保证金和交付条件后即可提交。",
|
||||
href: "/contracts/contract-002"
|
||||
},
|
||||
{
|
||||
id: "task-003",
|
||||
title: "采购框架协议履约节点到期",
|
||||
type: "履约",
|
||||
tone: "tag-success",
|
||||
summary: "本周需要上传首批交付验收证据。",
|
||||
href: "/contracts/contract-003"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const searchGroups = [
|
||||
{
|
||||
title: "合同",
|
||||
items: contracts.map((contract) => ({
|
||||
title: contract.title,
|
||||
summary: `${contract.counterparty} · ${contract.nextAction}`,
|
||||
updatedAt: contract.updatedAt,
|
||||
href: `/contracts/${contract.id}`
|
||||
}))
|
||||
},
|
||||
{
|
||||
title: "审查意见",
|
||||
items: [
|
||||
{
|
||||
title: "付款条款风险意见",
|
||||
summary: "建议明确验收通过后的付款期限和逾期责任。",
|
||||
updatedAt: "今天 09:50",
|
||||
href: "/contracts/contract-001"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "文件",
|
||||
items: [
|
||||
{
|
||||
title: "技术服务合同-审查版.docx",
|
||||
summary: "来自合同仓库,已关联技术服务合同。",
|
||||
updatedAt: "今天 10:21",
|
||||
href: "/files"
|
||||
}
|
||||
]
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const files = [
|
||||
{
|
||||
id: "file-001",
|
||||
name: "技术服务合同-审查版.docx",
|
||||
contractId: "contract-001",
|
||||
contractTitle: "技术服务合同",
|
||||
type: "审查版",
|
||||
size: "128 KB",
|
||||
uploadedAt: "今天 10:21"
|
||||
},
|
||||
{
|
||||
id: "file-002",
|
||||
name: "房屋租赁合同-草稿.pdf",
|
||||
contractId: "contract-002",
|
||||
contractTitle: "房屋租赁合同",
|
||||
type: "草稿",
|
||||
size: "364 KB",
|
||||
uploadedAt: "昨天 19:03"
|
||||
}
|
||||
] as const;
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Generated
+1058
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "yuqei-ai-contract-platform-v2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "npm@10.9.7",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*",
|
||||
"sdks/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "python -m pytest",
|
||||
"web:dev": "npm --workspace @yuqei/contract-web run dev",
|
||||
"web:build": "npm --workspace @yuqei/contract-web run build",
|
||||
"web:typecheck": "npm --workspace @yuqei/contract-web run typecheck"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "8.5.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Yuqei Design System
|
||||
|
||||
Shared V2 design tokens and workflow primitives.
|
||||
|
||||
This package starts intentionally small. It holds product language, layout constants, tokens, and workflow metadata before it grows into a component library.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@yuqei/design-system",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./tokens";
|
||||
export * from "./workflow";
|
||||
@@ -0,0 +1,33 @@
|
||||
export const brandTokens = {
|
||||
color: {
|
||||
ink: "#172033",
|
||||
muted: "#667085",
|
||||
line: "#d9e0ea",
|
||||
canvas: "#f6f8fb",
|
||||
surface: "#ffffff",
|
||||
primary: "#2457d6",
|
||||
primarySoft: "#eaf0ff",
|
||||
success: "#16794c",
|
||||
successSoft: "#e7f6ef",
|
||||
warning: "#b35b00",
|
||||
warningSoft: "#fff3df",
|
||||
danger: "#bd2d2d",
|
||||
dangerSoft: "#fdecec",
|
||||
accent: "#0b7a75",
|
||||
accentSoft: "#e4f5f3"
|
||||
},
|
||||
radius: {
|
||||
sm: "6px",
|
||||
md: "8px"
|
||||
},
|
||||
shadow: {
|
||||
panel: "0 10px 30px rgba(16, 24, 40, 0.08)"
|
||||
},
|
||||
layout: {
|
||||
sidebarWidth: "248px",
|
||||
topbarHeight: "64px",
|
||||
contentMaxWidth: "1440px"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type BrandTokens = typeof brandTokens;
|
||||
@@ -0,0 +1,24 @@
|
||||
export const mainFlow = [
|
||||
{
|
||||
key: "draft",
|
||||
title: "合同起草",
|
||||
description: "从模板或智能问答快速生成合同初稿。"
|
||||
},
|
||||
{
|
||||
key: "review",
|
||||
title: "法律审查",
|
||||
description: "审查风险、补齐依据、形成处理意见。"
|
||||
},
|
||||
{
|
||||
key: "approve",
|
||||
title: "协同审批",
|
||||
description: "按条件触发审批,不默认压迫所有用户。"
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
title: "归档履约",
|
||||
description: "沉淀文件、审计、版本和履约证据。"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export type MainFlowStep = (typeof mainFlow)[number];
|
||||
@@ -0,0 +1,118 @@
|
||||
# V2 前端基础工程与轻量化体验底座记录
|
||||
|
||||
日期:2026-06-22
|
||||
|
||||
## 1. 本轮目标
|
||||
|
||||
承接 T0-5,建立 V2 前端基础工程和设计系统底座,同时把企业主线的轻量化体验原则落到第一版页面结构里。
|
||||
|
||||
## 2. 已完成内容
|
||||
|
||||
### Workspace
|
||||
|
||||
已建立根 npm workspace:
|
||||
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `tsconfig.base.json`
|
||||
|
||||
根命令:
|
||||
|
||||
```powershell
|
||||
npm run web:dev
|
||||
npm run web:typecheck
|
||||
npm run web:build
|
||||
```
|
||||
|
||||
### Design System
|
||||
|
||||
目录:`packages/design-system`
|
||||
|
||||
已建立:
|
||||
|
||||
- 品牌颜色、布局、圆角、阴影 tokens。
|
||||
- 企业主流程 `mainFlow` 元数据。
|
||||
- package export 边界。
|
||||
|
||||
### Contract Web
|
||||
|
||||
目录:`apps/contract-web`
|
||||
|
||||
已建立:
|
||||
|
||||
- Next.js App Router 工程。
|
||||
- 全局布局和左侧主导航。
|
||||
- 工作台。
|
||||
- 合同列表。
|
||||
- 合同详情。
|
||||
- 统一搜索。
|
||||
- 合同仓库。
|
||||
- 管理区。
|
||||
- 系统健康页。
|
||||
|
||||
## 3. 体验原则落地
|
||||
|
||||
本轮页面遵循以下原则:
|
||||
|
||||
- 工作台只突出“待我处理”和“快速发起”。
|
||||
- 主流程导航只保留:工作台、合同、统一搜索、合同仓库。
|
||||
- 管理员能力集中到“管理区”。
|
||||
- 合同详情首屏突出“下一步操作”。
|
||||
- 审批、版本、履约、审计在合同详情侧栏折叠呈现。
|
||||
- 统一搜索结果标题就是主入口,减少重复“打开”按钮。
|
||||
- 合同仓库保留文件预览、关联合同和批量下载入口。
|
||||
|
||||
## 4. 验证结果
|
||||
|
||||
已通过:
|
||||
|
||||
```powershell
|
||||
npm audit --audit-level=moderate
|
||||
npm run web:typecheck
|
||||
npm run web:build
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- npm audit:0 vulnerabilities。
|
||||
- TypeScript:通过。
|
||||
- Next.js production build:通过。
|
||||
- 后端 pytest:6 passed。
|
||||
|
||||
页面验收:
|
||||
|
||||
- `/`
|
||||
- `/workspace`
|
||||
- `/contracts`
|
||||
- `/contracts/contract-001`
|
||||
- `/search`
|
||||
- `/files`
|
||||
- `/admin`
|
||||
- `/health`
|
||||
|
||||
截图输出:
|
||||
|
||||
```text
|
||||
output/playwright/v2-front-foundation/
|
||||
```
|
||||
|
||||
## 5. 当前边界
|
||||
|
||||
本轮仍使用 mock 数据,不包含:
|
||||
|
||||
- 登录认证。
|
||||
- 真实合同 API 对接。
|
||||
- 真实文件上传/下载。
|
||||
- 真实搜索查询。
|
||||
- 真实审批提交。
|
||||
- AI 问答和审查调用。
|
||||
|
||||
## 6. 下一步建议
|
||||
|
||||
继续进入 S2/S3 的最小闭环:
|
||||
|
||||
1. 建立 AI Platform MVP 的数据模型和 `/api/v1/legal/qa` 假实现。
|
||||
2. 建立 Python SDK MVP,使 `contract-api` 只能通过 SDK 调用 `ai-platform-api`。
|
||||
3. 建立 Contract API 的工作台和合同列表真实接口。
|
||||
4. 前端从 mock 数据切换到 Contract API。
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es2022"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user