diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 96534d0..c848569 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,52 +1,47 @@ -@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600;9..144,700&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; :root { - --bg: #f7f4ee; - --bg-elevated: #fbf9f4; - --ink: #1a1814; - --ink-soft: #4a4640; - --ink-muted: #8a847a; - --line: #e6e0d4; - --line-soft: #efeae0; - --accent: #2563eb; - --accent-soft: #dbeafe; + /* Surfaces — zinc neutrals */ + --bg: #fafafa; + --bg-card: #ffffff; + --bg-subtle: #f4f4f5; + --bg-hover: #f4f4f5; + + /* Text */ + --ink: #18181b; + --ink-soft: #52525b; + --ink-muted: #a1a1aa; + + /* Borders */ + --line: #e4e4e7; + --line-soft: #f4f4f5; + + /* Brand — calm blue */ + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-soft: #eff6ff; + --accent-ring: rgba(59, 130, 246, 0.15); + + /* Shadow */ + --shadow-sm: 0 1px 2px rgba(24, 24, 27, 0.04); + --shadow-md: 0 4px 12px -2px rgba(24, 24, 27, 0.06), 0 2px 4px -1px rgba(24, 24, 27, 0.04); } html, body { background: var(--bg); color: var(--ink); - font-family: 'DM Sans', system-ui, sans-serif; - font-feature-settings: 'ss01', 'ss02', 'cv11'; + font-family: 'Inter', system-ui, sans-serif; + font-feature-settings: 'cv11', 'ss01'; -webkit-font-smoothing: antialiased; -} - -.font-display { - font-family: 'Fraunces', Georgia, serif; - font-feature-settings: 'ss01'; + font-size: 14px; } .font-mono { font-family: 'JetBrains Mono', monospace; } -.grain::before { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - opacity: 0.03; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); -} -@keyframes fade-up { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - -.animate-fade-up { - animation: fade-up 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; -} diff --git a/apps/web/app/products/[id]/page.tsx b/apps/web/app/products/[id]/page.tsx index 7114ee1..14388ee 100644 --- a/apps/web/app/products/[id]/page.tsx +++ b/apps/web/app/products/[id]/page.tsx @@ -11,9 +11,9 @@ import { RequirementForm } from '@/components/product/RequirementForm'; import { ProductForm } from '@/components/product/ProductForm'; const TABS = [ - { key: 'requirements' as const, label: '需求池', index: '01' }, - { key: 'projects' as const, label: '项目', index: '02' }, - { key: 'versions' as const, label: '版本', index: '03' }, + { key: 'requirements' as const, label: '需求池' }, + { key: 'projects' as const, label: '项目' }, + { key: 'versions' as const, label: '版本' }, ]; export default function ProductDetailPage() { @@ -61,7 +61,7 @@ export default function ProductDetailPage() { }; const handleDeleteProduct = async () => { - if (confirm('确定要删除此产品吗?此操作不可撤销。')) { + if (confirm('确定删除该产品?此操作不可撤销。')) { await deleteProduct(productId); router.push('/products'); } @@ -69,133 +69,128 @@ export default function ProductDetailPage() { if (!currentProduct) { return ( -
-

Loading...

+
+ 加载中…
); } return ( -
-
-
- +
+
+
+ + / +

+ {currentProduct.name} +

+
+
+ + +
+
-
-
-

- ── Product -

-

- {currentProduct.name} -

- {currentProduct.description && ( -

- {currentProduct.description} -

- )} -
-
- - -
-
- - {editingProduct && ( -
-

── 编辑产品

- { - await updateProduct(productId, data); - setEditingProduct(false); - await fetchProduct(productId); - }} - onCancel={() => setEditingProduct(false)} - /> -
+
+ {currentProduct.description && ( +

{currentProduct.description}

)} - -
+
{TABS.map((tab) => ( ))}
+
- {activeTab === 'requirements' && ( -
- {(showReqForm || editingReq) && ( -
-

- ── {editingReq ? '编辑需求' : '新建需求'} -

- { - setShowReqForm(false); - setEditingReq(null); - }} - /> -
- )} - setEditingReq(req)} - onStatusChange={(id, status) => updateStatus(productId, id, status)} - onDelete={(id) => deleteRequirement(productId, id)} - onCreate={() => setShowReqForm(true)} - /> -
- )} +
+
+ {editingProduct && ( +
+

编辑产品

+ { + await updateProduct(productId, data); + setEditingProduct(false); + await fetchProduct(productId); + }} + onCancel={() => setEditingProduct(false)} + /> +
+ )} - {activeTab === 'projects' && ( -
-

项目管理

-

即将到来 · Coming Soon

-
- )} + {activeTab === 'requirements' && ( + <> + {(showReqForm || editingReq) && ( +
+

+ {editingReq ? '编辑需求' : '新建需求'} +

+ { + setShowReqForm(false); + setEditingReq(null); + }} + /> +
+ )} + setEditingReq(req)} + onStatusChange={(id, status) => updateStatus(productId, id, status)} + onDelete={(id) => deleteRequirement(productId, id)} + onCreate={() => setShowReqForm(true)} + /> + + )} - {activeTab === 'versions' && ( -
-

版本管理

-

即将到来 · Coming Soon

-
- )} + {activeTab === 'projects' && ( +
+ 项目管理功能开发中 +
+ )} + + {activeTab === 'versions' && ( +
+ 版本管理功能开发中 +
+ )} +
); diff --git a/apps/web/app/products/page.tsx b/apps/web/app/products/page.tsx index 30e492e..03baf31 100644 --- a/apps/web/app/products/page.tsx +++ b/apps/web/app/products/page.tsx @@ -27,85 +27,67 @@ export default function ProductsPage() { }; return ( -
-
+
+
+
+

产品

+ + {products.length} + +
+ +
-
-
-
-

- ── 02 / Products -

-

- 产品目录 -

-

- 组织内所有产品的索引。每个产品聚合需求池、发布版本与归属项目。 -

-
- -
-
- +
+
+
+
+ setSearch(e.target.value)} - placeholder="搜索产品" - className="w-56 rounded-md border border-[var(--line)] bg-[var(--bg-elevated)] py-2 pl-9 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--ink)] focus:outline-none" + placeholder="搜索产品名称" + className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] pl-8 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]" />
-
-
-
-

- Showing -

-

- {String(filtered.length).padStart(2, '0')} -

- / {String(products.length).padStart(2, '0')} 个产品 + {showForm && ( +
+

新建产品

+ setShowForm(false)} /> +
+ )} + + {loading ? ( +
加载中…
+ ) : filtered.length === 0 ? ( +
+

+ {search ? '没有找到匹配的产品' : '尚未创建任何产品'} +

+

+ {search ? '尝试其他关键字' : '点击右上角"新建产品"开始'} +

+
+ ) : ( +
+ {filtered.map((product) => ( + router.push(`/products/${id}`)} + /> + ))} +
+ )}
- - {showForm && ( -
-

── 新建产品

- setShowForm(false)} /> -
- )} - - {loading ? ( -
-

Loading...

-
- ) : filtered.length === 0 ? ( -
-

- {search ? '没有匹配的产品' : '尚未创建任何产品'} -

-

- {search ? '试试其他关键字' : '点击右上角按钮,创建第一个产品'} -

-
- ) : ( -
- {filtered.map((product, idx) => ( - router.push(`/products/${id}`)} - /> - ))} -
- )}
); diff --git a/apps/web/components/layout/Sidebar.tsx b/apps/web/components/layout/Sidebar.tsx index 9374061..d70b18b 100644 --- a/apps/web/components/layout/Sidebar.tsx +++ b/apps/web/components/layout/Sidebar.tsx @@ -1,17 +1,24 @@ 'use client'; import { usePathname, useRouter } from 'next/navigation'; -import { Inbox, Package, FolderKanban, Tag, Users, Sparkles } from 'lucide-react'; +import { Inbox, Package, FolderKanban, Tag, Users, LayoutGrid, Search } from 'lucide-react'; -const NAV_ITEMS = [ - { label: '与我相关', path: '/workspace', icon: Inbox, index: '01' }, - { label: '产品', path: '/products', icon: Package, index: '02' }, - { label: '项目', path: '/projects', icon: FolderKanban, index: '03' }, - { label: '版本', path: '/versions', icon: Tag, index: '04' }, -]; - -const ADMIN_ITEMS = [ - { label: '成员管理', path: '/admin/members', icon: Users }, +const NAV_GROUPS = [ + { + label: '工作区', + items: [ + { label: '与我相关', path: '/workspace', icon: Inbox }, + { label: '产品', path: '/products', icon: Package }, + { label: '项目', path: '/projects', icon: FolderKanban }, + { label: '版本', path: '/versions', icon: Tag }, + ], + }, + { + label: '管理', + items: [ + { label: '成员', path: '/admin/members', icon: Users }, + ], + }, ]; export function Sidebar() { @@ -22,93 +29,62 @@ export function Sidebar() { pathname === path || (path !== '/' && pathname.startsWith(path)); return ( -