@allbluecn/web-app 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/package.json +230 -30
  2. package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
  3. package/src/components/iteration/burndown-chart.tsx +63 -0
  4. package/src/components/iteration/constants.ts +28 -0
  5. package/src/components/iteration/create-iteration-dialog.tsx +140 -0
  6. package/src/components/iteration/iteration-card.tsx +60 -0
  7. package/src/components/iteration/iteration-editor.tsx +116 -0
  8. package/src/components/iteration/iteration-progress-bar.tsx +44 -0
  9. package/src/components/iteration/iteration-status-badge.tsx +33 -0
  10. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
  11. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  12. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  13. package/src/components/project/create-project-dialog.tsx +44 -57
  14. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  15. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  16. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  17. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  18. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  19. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  20. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  21. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  22. package/src/components/story/__tests__/constants.test.ts +255 -0
  23. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  24. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  25. package/src/components/story/constants.ts +33 -1
  26. package/src/components/story/create-story-dialog.tsx +30 -10
  27. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  28. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  29. package/src/components/story/detail/activity-timeline.tsx +11 -4
  30. package/src/components/story/detail/story-detail-body.tsx +89 -38
  31. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +118 -9
  32. package/src/components/story/detail/story-detail-toolbar/children.tsx +7 -4
  33. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +67 -7
  34. package/src/components/story/detail/story-detail-toolbar.tsx +6 -1
  35. package/src/components/story/detail/story-properties.tsx +21 -4
  36. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  37. package/src/components/story/display/group-resolver.tsx +2 -2
  38. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  39. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  40. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  41. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  42. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  43. package/src/components/story/inline-editors/index.ts +2 -1
  44. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  45. package/src/components/story/inline-editors/story-select-dialog.tsx +22 -59
  46. package/src/components/story/peek-panel.tsx +121 -21
  47. package/src/components/story/relations/link-manage-dialog.tsx +2 -1
  48. package/src/components/story/relations/story-ref-row.tsx +17 -27
  49. package/src/components/story/story-panel-header.tsx +25 -19
  50. package/src/components/story/story-toolbar.tsx +3 -3
  51. package/src/components/story/transfer-story-popover.tsx +2 -2
  52. package/src/components/story/types.ts +1 -0
  53. package/src/components/story/views/create-view-dialog.tsx +3 -3
  54. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  55. package/src/components/story/views/story-list/group.tsx +1 -1
  56. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  57. package/src/components/story/views/story-list/row.tsx +1 -1
  58. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  59. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  60. package/src/lib/changelog/sdk-versions.ts +20 -20
  61. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  62. package/src/lib/iteration/status.ts +28 -0
  63. package/src/lib/project-display-id.ts +14 -5
  64. package/src/lib/project-module-meta.ts +77 -0
  65. package/src/lib/utils.d.ts +1 -1
  66. package/src/providers/index.tsx +6 -0
  67. package/src/routeTree.gen.ts +54 -0
  68. package/src/routes/__root.tsx +0 -8
  69. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  70. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  71. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  72. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  73. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  74. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  75. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  76. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  77. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  78. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +4 -4
  79. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  80. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  81. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  82. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  83. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  84. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  85. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  86. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  87. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  88. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  89. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  90. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  91. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  92. package/src/server/serverFns/iteration/index.ts +22 -0
  93. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  94. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  95. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  96. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  97. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  98. package/src/server/serverFns/iteration/schemas.ts +58 -0
  99. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  100. package/src/server/serverFns/project/module-overview.ts +109 -0
  101. package/src/server/serverFns/project.ts +2 -2
  102. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  103. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  104. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +106 -2
  105. package/src/server/serverFns/story/index.ts +3 -0
  106. package/src/server/serverFns/story/story-assoc.ts +36 -4
  107. package/src/server/serverFns/story/story-attachments.ts +20 -2
  108. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  109. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  110. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  111. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  112. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  113. package/src/server/serverFns/story/story-crud.ts +32 -2
  114. package/src/server/serverFns/story/story-dependencies.ts +123 -2
  115. package/src/server/serverFns/story/story-links.ts +30 -6
  116. package/src/server/serverFns/story/story-transfer.ts +5 -4
  117. package/src/server/serverFns/story/types.ts +1 -0
  118. package/src/types/project.ts +5 -5
  119. package/src/components/project/project-animated-icon.tsx +0 -73
  120. package/src/components/shared/icon-packs/index.ts +0 -1
  121. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  122. package/src/components/shared/icon-packs/prd.ts +0 -1
  123. package/src/components/shared/icon-packs/project.ts +0 -1
  124. package/src/components/shared/icon-picker.tsx +0 -1
  125. package/src/lib/project-mock.ts +0 -156
@@ -15,16 +15,25 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
+ import { pinyin } from "pinyin-pro";
18
19
  import { BUILTIN_INDEPENDENT_DISPLAY_ID } from "./builtin-project";
19
20
 
20
21
  /**
21
- * 从项目名提取 displayId:提取字母数字,转大写,取前10位。
22
- * 中文/符号被忽略;纯中文项目名返回空字符串(需用户手动填)。
22
+ * 从项目名提取 displayId:字母数字原样保留,汉字转拼音首字母,
23
+ * 转大写,取前10位。示例:"产品需求PRD" → "CPXQPRD"。
23
24
  */
24
25
  export function extractDisplayId(name: string): string {
25
- const matches = name.match(/[A-Za-z0-9]/g)
26
- if (!matches || matches.length === 0) return ''
27
- return matches.join('').toUpperCase().slice(0, 10)
26
+ const chars: string[] = [];
27
+ for (const ch of name) {
28
+ if (/[A-Za-z0-9]/.test(ch)) {
29
+ chars.push(ch);
30
+ } else if (/[\u4e00-\u9fff]/.test(ch)) {
31
+ const initial = pinyin(ch, { pattern: "first", toneType: "none" });
32
+ if (initial && /^[a-z]$/i.test(initial)) chars.push(initial);
33
+ }
34
+ }
35
+ if (chars.length === 0) return "";
36
+ return chars.join("").toUpperCase().slice(0, 10);
28
37
  }
29
38
 
30
39
  /**
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import {
19
+ ClipboardList,
20
+ Package2,
21
+ Palette,
22
+ FlaskConical,
23
+ Bug,
24
+ CircleCheckBig,
25
+ } from "lucide-react";
26
+ import type {
27
+ ProjectModuleKey,
28
+ ProjectModuleOverview,
29
+ } from "@/types/project";
30
+
31
+ // 6 个板块元数据(key 与路由 $module 对应)
32
+ export const PROJECT_MODULE_META: Record<
33
+ ProjectModuleKey,
34
+ Omit<ProjectModuleOverview, "count" | "lastUpdatedAt" | "link">
35
+ > = {
36
+ requirement: {
37
+ key: "requirement",
38
+ title: "需求",
39
+ description: "收集与跟踪产品需求池",
40
+ icon: ClipboardList,
41
+ },
42
+ prd: {
43
+ key: "prd",
44
+ title: "PRD",
45
+ description: "产品需求文档与原型画板",
46
+ icon: Package2,
47
+ },
48
+ design: {
49
+ key: "design",
50
+ title: "设计",
51
+ description: "UI 设计稿与画板资源",
52
+ icon: Palette,
53
+ },
54
+ testcase: {
55
+ key: "testcase",
56
+ title: "测例",
57
+ description: "测试用例与执行计划",
58
+ icon: FlaskConical,
59
+ },
60
+ bug: {
61
+ key: "bug",
62
+ title: "BUG",
63
+ description: "缺陷跟踪与修复进度",
64
+ icon: Bug,
65
+ },
66
+ acceptance: {
67
+ key: "acceptance",
68
+ title: "验收",
69
+ description: "交付验收与上线检查",
70
+ icon: CircleCheckBig,
71
+ },
72
+ };
73
+
74
+ export const PROJECT_MODULE_KEYS = Object.keys(
75
+ PROJECT_MODULE_META,
76
+ ) as ProjectModuleKey[];
77
+
@@ -1 +1 @@
1
- export { cn } from '@allbluecn/ui/utils';
1
+ export { cn } from '@/components/ui-shared/utils';
@@ -28,6 +28,12 @@ import type { Session } from "@/lib/auth/auth-client";
28
28
  import { exchangeRemoteTokenFn } from "@/server/serverFns/auth/session";
29
29
  import { toast } from "sonner";
30
30
  import { createServerI18n } from "@/lib/i18n/server";
31
+ import { registerIconPicker } from "@allbluecn/plugins-core/prd/icon-picker-bridge";
32
+ import { LucideIconPicker } from "@/components/shared/lucide-icon-picker";
33
+
34
+ // 宿主注入图标选择器实现,供 plugins-core 的 PRD 弹窗等使用(模块级单例)
35
+ registerIconPicker(LucideIconPicker);
36
+
31
37
 
32
38
  function DesktopDeepLinkListener() {
33
39
  useEffect(() => {
@@ -104,6 +104,8 @@ import { Route as NavWorkspacesWorkspaceIdSettingsRouteRouteImport } from './rou
104
104
  import { Route as ApiAttachmentsIdContentRouteImport } from './routes/api/attachments/$id/content'
105
105
  import { Route as DesignFilesProjectIdArtboardIdFilenameRouteImport } from './routes/design-files/$projectId/$artboardId/$filename'
106
106
  import { Route as NavKnowledgeKbIdDocDocIdRouteRouteImport } from './routes/_nav/knowledge/$kbId/doc/$docId/route'
107
+ import { Route as NavProjectsProjectIdIterationsIndexRouteImport } from './routes/_nav/projects/$projectId/iterations/index'
108
+ import { Route as NavProjectsProjectIdIterationsIterationIdRouteRouteImport } from './routes/_nav/projects/$projectId/iterations/$iterationId/route'
107
109
  import { Route as ApiSharePrdPrdIdEventsRouteImport } from './routes/api/share/prd/$prdId/events'
108
110
 
109
111
  const NavKnowledgeBookmarksIndexLazyRouteImport = createFileRoute(
@@ -836,6 +838,26 @@ const NavKnowledgeLawsEditIdLazyRoute =
836
838
  } as any).lazy(() =>
837
839
  import('./routes/_nav/knowledge/laws/edit/$id.lazy').then((d) => d.Route),
838
840
  )
841
+ const NavProjectsProjectIdIterationsIndexRoute =
842
+ NavProjectsProjectIdIterationsIndexRouteImport.update({
843
+ id: '/iterations/',
844
+ path: '/iterations/',
845
+ getParentRoute: () => NavProjectsProjectIdRouteRoute,
846
+ } as any).lazy(() =>
847
+ import('./routes/_nav/projects/$projectId/iterations/index.lazy').then(
848
+ (d) => d.Route,
849
+ ),
850
+ )
851
+ const NavProjectsProjectIdIterationsIterationIdRouteRoute =
852
+ NavProjectsProjectIdIterationsIterationIdRouteRouteImport.update({
853
+ id: '/iterations/$iterationId',
854
+ path: '/iterations/$iterationId',
855
+ getParentRoute: () => NavProjectsProjectIdRouteRoute,
856
+ } as any).lazy(() =>
857
+ import('./routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy').then(
858
+ (d) => d.Route,
859
+ ),
860
+ )
839
861
  const ApiSharePrdPrdIdEventsRoute = ApiSharePrdPrdIdEventsRouteImport.update({
840
862
  id: '/api/share/prd/$prdId/events',
841
863
  path: '/api/share/prd/$prdId/events',
@@ -944,9 +966,11 @@ export interface FileRoutesByFullPath {
944
966
  '/knowledge/summary/': typeof NavKnowledgeSummaryIndexLazyRoute
945
967
  '/projects/$projectId/': typeof NavProjectsProjectIdIndexLazyRoute
946
968
  '/knowledge/$kbId/doc/$docId': typeof NavKnowledgeKbIdDocDocIdRouteRoute
969
+ '/projects/$projectId/iterations/$iterationId': typeof NavProjectsProjectIdIterationsIterationIdRouteRoute
947
970
  '/api/share/prd/$prdId/events': typeof ApiSharePrdPrdIdEventsRoute
948
971
  '/knowledge/compliance/edit/$id': typeof NavKnowledgeComplianceEditIdLazyRoute
949
972
  '/knowledge/laws/edit/$id': typeof NavKnowledgeLawsEditIdLazyRoute
973
+ '/projects/$projectId/iterations/': typeof NavProjectsProjectIdIterationsIndexRoute
950
974
  }
951
975
  export interface FileRoutesByTo {
952
976
  '/': typeof PublicIndexRouteRoute
@@ -1036,9 +1060,11 @@ export interface FileRoutesByTo {
1036
1060
  '/knowledge/summary': typeof NavKnowledgeSummaryIndexLazyRoute
1037
1061
  '/projects/$projectId': typeof NavProjectsProjectIdIndexLazyRoute
1038
1062
  '/knowledge/$kbId/doc/$docId': typeof NavKnowledgeKbIdDocDocIdRouteRoute
1063
+ '/projects/$projectId/iterations/$iterationId': typeof NavProjectsProjectIdIterationsIterationIdRouteRoute
1039
1064
  '/api/share/prd/$prdId/events': typeof ApiSharePrdPrdIdEventsRoute
1040
1065
  '/knowledge/compliance/edit/$id': typeof NavKnowledgeComplianceEditIdLazyRoute
1041
1066
  '/knowledge/laws/edit/$id': typeof NavKnowledgeLawsEditIdLazyRoute
1067
+ '/projects/$projectId/iterations': typeof NavProjectsProjectIdIterationsIndexRoute
1042
1068
  }
1043
1069
  export interface FileRoutesById {
1044
1070
  __root__: typeof rootRouteImport
@@ -1146,9 +1172,11 @@ export interface FileRoutesById {
1146
1172
  '/_nav/knowledge/summary/': typeof NavKnowledgeSummaryIndexLazyRoute
1147
1173
  '/_nav/projects/$projectId/': typeof NavProjectsProjectIdIndexLazyRoute
1148
1174
  '/_nav/knowledge/$kbId/doc/$docId': typeof NavKnowledgeKbIdDocDocIdRouteRoute
1175
+ '/_nav/projects/$projectId/iterations/$iterationId': typeof NavProjectsProjectIdIterationsIterationIdRouteRoute
1149
1176
  '/api/share/prd/$prdId/events': typeof ApiSharePrdPrdIdEventsRoute
1150
1177
  '/_nav/knowledge/compliance/edit/$id': typeof NavKnowledgeComplianceEditIdLazyRoute
1151
1178
  '/_nav/knowledge/laws/edit/$id': typeof NavKnowledgeLawsEditIdLazyRoute
1179
+ '/_nav/projects/$projectId/iterations/': typeof NavProjectsProjectIdIterationsIndexRoute
1152
1180
  }
1153
1181
  export interface FileRouteTypes {
1154
1182
  fileRoutesByFullPath: FileRoutesByFullPath
@@ -1254,9 +1282,11 @@ export interface FileRouteTypes {
1254
1282
  | '/knowledge/summary/'
1255
1283
  | '/projects/$projectId/'
1256
1284
  | '/knowledge/$kbId/doc/$docId'
1285
+ | '/projects/$projectId/iterations/$iterationId'
1257
1286
  | '/api/share/prd/$prdId/events'
1258
1287
  | '/knowledge/compliance/edit/$id'
1259
1288
  | '/knowledge/laws/edit/$id'
1289
+ | '/projects/$projectId/iterations/'
1260
1290
  fileRoutesByTo: FileRoutesByTo
1261
1291
  to:
1262
1292
  | '/'
@@ -1346,9 +1376,11 @@ export interface FileRouteTypes {
1346
1376
  | '/knowledge/summary'
1347
1377
  | '/projects/$projectId'
1348
1378
  | '/knowledge/$kbId/doc/$docId'
1379
+ | '/projects/$projectId/iterations/$iterationId'
1349
1380
  | '/api/share/prd/$prdId/events'
1350
1381
  | '/knowledge/compliance/edit/$id'
1351
1382
  | '/knowledge/laws/edit/$id'
1383
+ | '/projects/$projectId/iterations'
1352
1384
  id:
1353
1385
  | '__root__'
1354
1386
  | '/_login'
@@ -1455,9 +1487,11 @@ export interface FileRouteTypes {
1455
1487
  | '/_nav/knowledge/summary/'
1456
1488
  | '/_nav/projects/$projectId/'
1457
1489
  | '/_nav/knowledge/$kbId/doc/$docId'
1490
+ | '/_nav/projects/$projectId/iterations/$iterationId'
1458
1491
  | '/api/share/prd/$prdId/events'
1459
1492
  | '/_nav/knowledge/compliance/edit/$id'
1460
1493
  | '/_nav/knowledge/laws/edit/$id'
1494
+ | '/_nav/projects/$projectId/iterations/'
1461
1495
  fileRoutesById: FileRoutesById
1462
1496
  }
1463
1497
  export interface RootRouteChildren {
@@ -2232,6 +2266,20 @@ declare module '@tanstack/react-router' {
2232
2266
  preLoaderRoute: typeof NavKnowledgeLawsEditIdLazyRouteImport
2233
2267
  parentRoute: typeof NavKnowledgeLawsRouteRoute
2234
2268
  }
2269
+ '/_nav/projects/$projectId/iterations/': {
2270
+ id: '/_nav/projects/$projectId/iterations/'
2271
+ path: '/iterations'
2272
+ fullPath: '/projects/$projectId/iterations/'
2273
+ preLoaderRoute: typeof NavProjectsProjectIdIterationsIndexRouteImport
2274
+ parentRoute: typeof NavProjectsProjectIdRouteRoute
2275
+ }
2276
+ '/_nav/projects/$projectId/iterations/$iterationId': {
2277
+ id: '/_nav/projects/$projectId/iterations/$iterationId'
2278
+ path: '/iterations/$iterationId'
2279
+ fullPath: '/projects/$projectId/iterations/$iterationId'
2280
+ preLoaderRoute: typeof NavProjectsProjectIdIterationsIterationIdRouteRouteImport
2281
+ parentRoute: typeof NavProjectsProjectIdRouteRoute
2282
+ }
2235
2283
  '/api/share/prd/$prdId/events': {
2236
2284
  id: '/api/share/prd/$prdId/events'
2237
2285
  path: '/api/share/prd/$prdId/events'
@@ -2465,6 +2513,8 @@ interface NavProjectsProjectIdRouteRouteChildren {
2465
2513
  NavProjectsProjectIdStoryIdRouteRoute: typeof NavProjectsProjectIdStoryIdRouteRoute
2466
2514
  NavProjectsProjectIdSettingsRouteRoute: typeof NavProjectsProjectIdSettingsRouteRoute
2467
2515
  NavProjectsProjectIdIndexLazyRoute: typeof NavProjectsProjectIdIndexLazyRoute
2516
+ NavProjectsProjectIdIterationsIterationIdRouteRoute: typeof NavProjectsProjectIdIterationsIterationIdRouteRoute
2517
+ NavProjectsProjectIdIterationsIndexRoute: typeof NavProjectsProjectIdIterationsIndexRoute
2468
2518
  }
2469
2519
 
2470
2520
  const NavProjectsProjectIdRouteRouteChildren: NavProjectsProjectIdRouteRouteChildren =
@@ -2474,6 +2524,10 @@ const NavProjectsProjectIdRouteRouteChildren: NavProjectsProjectIdRouteRouteChil
2474
2524
  NavProjectsProjectIdSettingsRouteRoute:
2475
2525
  NavProjectsProjectIdSettingsRouteRoute,
2476
2526
  NavProjectsProjectIdIndexLazyRoute: NavProjectsProjectIdIndexLazyRoute,
2527
+ NavProjectsProjectIdIterationsIterationIdRouteRoute:
2528
+ NavProjectsProjectIdIterationsIterationIdRouteRoute,
2529
+ NavProjectsProjectIdIterationsIndexRoute:
2530
+ NavProjectsProjectIdIterationsIndexRoute,
2477
2531
  }
2478
2532
 
2479
2533
  const NavProjectsProjectIdRouteRouteWithChildren =
@@ -26,8 +26,6 @@ import globalsCss from '@/styles/globals.css?url'
26
26
  import type { RouterContext } from '@/lib/router-context'
27
27
  import { Providers } from '@/providers'
28
28
  import { Toaster } from '@/components/ui/sonner'
29
- import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
30
- import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
31
29
  import { DevtoolsPanel } from '@allblue/devtools'
32
30
  import { getSessionFn } from '@/server/serverFns/auth/session'
33
31
  import { getLocaleFn } from '@/server/serverFns/locale'
@@ -82,12 +80,6 @@ function RootComponent() {
82
80
  <Outlet />
83
81
  <ShortcutsPanel />
84
82
  <Toaster position="top-center" />
85
- {import.meta.env.DEV && (
86
- <ReactQueryDevtools initialIsOpen={false} />
87
- )}
88
- {import.meta.env.DEV && (
89
- <TanStackRouterDevtools initialIsOpen={false} />
90
- )}
91
83
  {import.meta.env.DEV && <DevtoolsPanel />}
92
84
  </HotkeysProvider>
93
85
  </Providers>
@@ -23,7 +23,7 @@ import { Button } from '@/components/ui/button'
23
23
  import { Card } from '@/components/ui/card'
24
24
  import { AnimatedGridPattern } from '@/components/magicui/animated-grid-pattern'
25
25
  import { BookOpenTextIcon } from '@/components/animated-icons/book-open-text'
26
- import { KnowledgeIconDisplay } from '@/components/knowledge/knowledge-icon'
26
+ import { LucideIconByName } from '@/components/shared/lucide-icon-picker/lucide-icon-by-name'
27
27
  import { KnowledgeFormDialog } from '@/components/knowledge/dialog/knowledge-form-dialog'
28
28
 
29
29
  type KnowledgeList = ReturnType<typeof import('@/server/serverFns/knowledge')['listKnowledgeFn']> extends Promise<infer T> ? T : never
@@ -81,7 +81,7 @@ function KnowledgeListPage() {
81
81
  <div className="flex-1">
82
82
  <div className="flex items-start justify-between">
83
83
  <div className="flex size-10 items-center justify-center rounded-lg bg-primary/10">
84
- <KnowledgeIconDisplay name={item.icon} size={20} />
84
+ <LucideIconByName name={item.icon} size={20} />
85
85
  </div>
86
86
  </div>
87
87
  <h3 className="mt-4 font-medium text-lg">{item.name}</h3>
@@ -20,25 +20,44 @@ import { useTranslation } from "react-i18next";
20
20
  import { ArrowLeft, ChevronRight } from "lucide-react";
21
21
  import { Button } from "@/components/ui/button";
22
22
  import { Badge } from "@/components/ui/badge";
23
- import { ProjectIconDisplay } from "@/components/project/project-icon";
24
- import { PROJECT_MODULE_META } from "@/lib/project-mock";
23
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
24
+ import { PROJECT_MODULE_META } from "@/lib/project-module-meta";
25
25
  import type { ProjectModuleItem } from "@/types/project";
26
26
 
27
- const STATUS_VARIANT: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
28
- 已完成: "secondary",
29
- 已验收: "default",
30
- 已关闭: "outline",
31
- 进行中: "default",
32
- 待处理: "outline",
27
+ const STATE_VARIANT: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
28
+ done: "secondary",
29
+ cancelled: "outline",
30
+ in_progress: "default",
31
+ todo: "outline",
32
+ backlog: "outline",
33
33
  };
34
34
 
35
35
  const PRIORITY_VARIANT: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
36
- 高: "destructive",
37
- 中: "secondary",
38
- 低: "outline",
36
+ urgent: "destructive",
37
+ high: "destructive",
38
+ medium: "secondary",
39
+ low: "outline",
40
+ none: "outline",
39
41
  };
40
42
 
41
- function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string) => string }) {
43
+ const STATE_LABEL_KEYS: Record<string, string> = {
44
+ backlog: "state.backlog",
45
+ todo: "state.todo",
46
+ in_progress: "state.in_progress",
47
+ done: "state.done",
48
+ cancelled: "state.cancelled",
49
+ };
50
+
51
+ const PRIORITY_LABEL_KEYS: Record<string, string> = {
52
+ urgent: "priority.urgent",
53
+ high: "priority.high",
54
+ medium: "priority.medium",
55
+ low: "priority.low",
56
+ none: "priority.none",
57
+ };
58
+
59
+ function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string, opts?: Record<string, unknown>) => string }) {
60
+ const stateLabel = STATE_LABEL_KEYS[item.state] ?? item.state;
42
61
  return (
43
62
  <div className="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-accent/40">
44
63
  <div className="min-w-0 flex-1">
@@ -47,9 +66,9 @@ function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string) => s
47
66
  {t('module.assigneeLabel')}{item.assignee ?? t('module.unassigned')}
48
67
  </p>
49
68
  </div>
50
- <Badge variant={STATUS_VARIANT[item.status] ?? "outline"}>{item.status}</Badge>
69
+ <Badge variant={STATE_VARIANT[item.state] ?? "outline"}>{t(`story:${stateLabel}`)}</Badge>
51
70
  <Badge variant={PRIORITY_VARIANT[item.priority] ?? "outline"} className="hidden sm:inline-flex">
52
- {item.priority}
71
+ {t(`story:${PRIORITY_LABEL_KEYS[item.priority] ?? item.priority}`)}
53
72
  </Badge>
54
73
  <span className="hidden text-xs text-muted-foreground md:block">
55
74
  {new Intl.DateTimeFormat("zh-CN", { month: "short", day: "numeric" }).format(new Date(item.updatedAt))}
@@ -60,7 +79,7 @@ function ModuleRow({ item, t }: { item: ProjectModuleItem; t: (key: string) => s
60
79
  }
61
80
 
62
81
  function ProjectModulePage() {
63
- const { t } = useTranslation('projects');
82
+ const { t } = useTranslation(["projects", "story"]);
64
83
  const { project, items, moduleKey } = Route.useLoaderData();
65
84
  const meta = PROJECT_MODULE_META[moduleKey];
66
85
  const Icon = meta.icon;
@@ -86,25 +105,33 @@ function ProjectModulePage() {
86
105
  <p className="text-sm text-muted-foreground">{meta.description}</p>
87
106
  </div>
88
107
  <div className="ml-auto flex items-center gap-2">
89
- <ProjectIconDisplay name={project.icon} size={20} />
108
+ <LucideIconByName name={project.icon} size={20} />
90
109
  <span className="text-sm text-muted-foreground">{project.name}</span>
91
110
  </div>
92
111
  </div>
93
112
 
94
113
  <div className="mb-3 flex items-center justify-between">
95
114
  <p className="text-sm text-muted-foreground">
96
- {t('module.totalItems', { count: items.length })} · {t('module.mockDataLabel')}
115
+ {t('projects:module.totalItems', { count: items.length })}
97
116
  </p>
98
- <Badge variant="outline" className="text-muted-foreground">
99
- {t('module.mockBadge')}
100
- </Badge>
117
+ {moduleKey === "requirement" && (
118
+ <Badge variant="outline" className="text-muted-foreground">
119
+ {t('projects:module.liveBadge')}
120
+ </Badge>
121
+ )}
101
122
  </div>
102
123
 
103
- <div className="divide-y rounded-lg border bg-card">
104
- {items.map((item) => (
105
- <ModuleRow key={item.id} item={item} t={t} />
106
- ))}
107
- </div>
124
+ {items.length === 0 ? (
125
+ <div className="rounded-lg border bg-card px-4 py-10 text-center text-sm text-muted-foreground">
126
+ {t('projects:module.empty')}
127
+ </div>
128
+ ) : (
129
+ <div className="divide-y rounded-lg border bg-card">
130
+ {items.map((item) => (
131
+ <ModuleRow key={item.id} item={item} t={t} />
132
+ ))}
133
+ </div>
134
+ )}
108
135
  </div>
109
136
  </div>
110
137
  );
@@ -17,10 +17,10 @@
17
17
 
18
18
  import { createFileRoute, notFound } from "@tanstack/react-router";
19
19
  import { getProjectDetailFn } from "@/server/serverFns/project";
20
+ import { listModuleStoriesFn } from "@/server/serverFns/project/module-overview";
20
21
  import {
21
- listModuleItems,
22
22
  PROJECT_MODULE_KEYS,
23
- } from "@/lib/project-mock";
23
+ } from "@/lib/project-module-meta";
24
24
  import type { ProjectModuleKey } from "@/types/project";
25
25
 
26
26
  export const Route = createFileRoute("/_nav/projects/$projectId/$storyId")({
@@ -32,7 +32,10 @@ export const Route = createFileRoute("/_nav/projects/$projectId/$storyId")({
32
32
  const project = await getProjectDetailFn({
33
33
  data: { projectId: params.projectId },
34
34
  });
35
- const items = listModuleItems(params.projectId, moduleKey);
35
+ const items =
36
+ moduleKey === "requirement"
37
+ ? (await listModuleStoriesFn({ data: { projectId: params.projectId } })).items
38
+ : [];
36
39
  return { project, items, moduleKey };
37
40
  },
38
41
  });
@@ -17,17 +17,17 @@
17
17
 
18
18
  import { createLazyFileRoute, Link, useLoaderData } from "@tanstack/react-router";
19
19
  import { useTranslation } from "react-i18next";
20
- import { Settings, LayoutGrid, Activity as ActivityIcon } from "lucide-react";
20
+ import { Settings, LayoutGrid, Activity as ActivityIcon, CalendarRange } from "lucide-react";
21
21
  import { useTheme } from "next-themes";
22
22
  import { UserAvatar } from "@allbluecn/ui";
23
23
  import type { AvatarConfig, ShapeType } from "@allbluecn/shared";
24
24
  import { Button } from "@/components/ui/button";
25
25
  import { AvatarGroup } from "@/components/magicui/components/animate/avatar-group";
26
26
  import { GlyphMatrix } from "@/components/magicui/glyph-matrix";
27
- import { ProjectAnimatedIcon } from "@/components/project/project-animated-icon";
27
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
28
28
  import { ProjectModuleCard } from "@/components/project/project-module-card";
29
29
  import { ProjectActivityList } from "@/components/project/project-activity-list";
30
- import { buildModuleOverview } from "@/lib/project-mock";
30
+ import { PROJECT_MODULE_META, PROJECT_MODULE_KEYS } from "@/lib/project-module-meta";
31
31
  import { useMemo } from "react";
32
32
  import { formatInTimeZone } from "date-fns-tz";
33
33
  import { subDays } from "date-fns";
@@ -63,14 +63,20 @@ function buildContributionData(
63
63
 
64
64
  function ProjectDetailPage() {
65
65
  const { t } = useTranslation('projects');
66
- const { project, members, activities } = useLoaderData({
66
+ const { project, members, activities, moduleOverview } = useLoaderData({
67
67
  from: "/_nav/projects/$projectId",
68
68
  });
69
69
  const { resolvedTheme } = useTheme();
70
70
  const glyphColor = resolvedTheme === "dark" ? "#6b7280" : "#9ca3af";
71
71
  const timezone = useTimezone();
72
72
 
73
- const modules = buildModuleOverview(project.id);
73
+ const modules = PROJECT_MODULE_KEYS.map((key) => ({
74
+ ...PROJECT_MODULE_META[key],
75
+ count: moduleOverview[key]?.count ?? 0,
76
+ lastUpdatedAt: moduleOverview[key]?.lastUpdatedAt
77
+ ? new Date(moduleOverview[key]!.lastUpdatedAt!)
78
+ : null,
79
+ }));
74
80
  const contributionData = useMemo(
75
81
  () => buildContributionData(activities, timezone),
76
82
  [activities, timezone],
@@ -85,16 +91,27 @@ function ProjectDetailPage() {
85
91
  </div>
86
92
  <div className="relative z-10 mx-auto max-w-6xl px-6 py-6">
87
93
  <div className="relative mb-10 mt-2 flex flex-col items-center py-10 text-center">
88
- <Button variant="outline" size="sm" asChild className="absolute right-0 top-0">
89
- <Link
90
- to="/projects/$projectId/settings"
91
- params={{ projectId: project.id }}
92
- >
93
- <Settings className="size-4" />
94
- {t('detail.settings')}
95
- </Link>
96
- </Button>
97
- <ProjectAnimatedIcon name={project.icon} size={56} className="mb-4 text-primary" />
94
+ <div className="absolute right-0 top-0 flex gap-2">
95
+ <Button variant="outline" size="sm" asChild>
96
+ <Link
97
+ to="/projects/$projectId/iterations"
98
+ params={{ projectId: project.id }}
99
+ >
100
+ <CalendarRange className="size-4" />
101
+ {t('detail.iterations')}
102
+ </Link>
103
+ </Button>
104
+ <Button variant="outline" size="sm" asChild>
105
+ <Link
106
+ to="/projects/$projectId/settings"
107
+ params={{ projectId: project.id }}
108
+ >
109
+ <Settings className="size-4" />
110
+ {t('detail.settings')}
111
+ </Link>
112
+ </Button>
113
+ </div>
114
+ <LucideIconByName name={project.icon} size={56} className="mb-4 text-primary" />
98
115
  <h1 className="text-3xl font-semibold">{project.name}</h1>
99
116
  <p className="mt-2 max-w-2xl text-sm text-muted-foreground">
100
117
  {project.description || t('detail.noDescription')}