@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
@@ -146,12 +146,133 @@ export const addDependenciesFn = createServerFn({ method: "POST" })
146
146
  return { created, failed, activities };
147
147
  });
148
148
 
149
+ export const updateDependencyFn = createServerFn({ method: "POST" })
150
+ .validator(
151
+ z.object({
152
+ depId: z.string(),
153
+ sourceId: z.string(),
154
+ targetId: z.string(),
155
+ type: edgeTypeSchema,
156
+ }),
157
+ )
158
+ .handler(async ({ data }) => {
159
+ const request = getRequest();
160
+ const session = await requireAuth(request);
161
+ const userId = session.user!.id!;
162
+
163
+ const dep = await prisma.storyDependency.findUnique({ where: { id: data.depId } });
164
+ if (!dep) throw new Error("依赖关系不存在");
165
+ // 新边两端必须是原边的重定向(允许方向反转),防止越权改到别的需求对
166
+ const sameSet =
167
+ (data.sourceId === dep.sourceId && data.targetId === dep.targetId) ||
168
+ (data.sourceId === dep.targetId && data.targetId === dep.sourceId);
169
+ if (!sameSet) throw new Error("依赖关系两端不匹配");
170
+ if (
171
+ data.sourceId === dep.sourceId &&
172
+ data.targetId === dep.targetId &&
173
+ data.type === dep.type
174
+ ) {
175
+ return { ok: true as const, activities: [] };
176
+ }
177
+
178
+ // 方向反转可能成环:写入 (S,T) 后成环 ⇔ 排除自身边后存在路径 T →…→ S
179
+ const edges = await prisma.storyDependency.findMany({
180
+ where: { id: { not: dep.id } },
181
+ select: { sourceId: true, targetId: true },
182
+ });
183
+ const adjacency = new Map<string, string[]>();
184
+ for (const e of edges) {
185
+ const list = adjacency.get(e.sourceId);
186
+ if (list) list.push(e.targetId);
187
+ else adjacency.set(e.sourceId, [e.targetId]);
188
+ }
189
+ const visited = new Set<string>();
190
+ const stack = [data.targetId];
191
+ let cyclic = false;
192
+ while (stack.length > 0) {
193
+ const current = stack.pop()!;
194
+ if (current === data.sourceId) {
195
+ cyclic = true;
196
+ break;
197
+ }
198
+ if (visited.has(current)) continue;
199
+ visited.add(current);
200
+ for (const next of adjacency.get(current) ?? []) {
201
+ if (!visited.has(next)) stack.push(next);
202
+ }
203
+ }
204
+ if (cyclic) return { ok: false as const, reason: "CYCLE" as const };
205
+
206
+ await prisma.storyDependency.update({
207
+ where: { id: dep.id },
208
+ data: { sourceId: data.sourceId, targetId: data.targetId, type: data.type },
209
+ });
210
+
211
+ // 两端各记一条「更新依赖关系」(newValue 为对方需求描述,与添加/移除一致)
212
+ const endpoints = await prisma.story.findMany({
213
+ where: { id: { in: [data.sourceId, data.targetId] } },
214
+ select: { id: true, identifier: true, name: true },
215
+ });
216
+ const labelById = new Map(
217
+ endpoints.map((s) => [s.id, `${s.identifier} ${s.name}`.trim()]),
218
+ );
219
+ const acts = await Promise.all([
220
+ prisma.storyActivity.create({
221
+ data: {
222
+ storyId: data.sourceId,
223
+ userId,
224
+ action: "dependency_updated",
225
+ newValue: labelById.get(data.targetId) ?? "",
226
+ },
227
+ select: { ...ACTIVITY_SELECT, storyId: true },
228
+ }),
229
+ prisma.storyActivity.create({
230
+ data: {
231
+ storyId: data.targetId,
232
+ userId,
233
+ action: "dependency_updated",
234
+ newValue: labelById.get(data.sourceId) ?? "",
235
+ },
236
+ select: { ...ACTIVITY_SELECT, storyId: true },
237
+ }),
238
+ ]);
239
+ return {
240
+ ok: true as const,
241
+ activities: acts.map((a) => ({ ...toActivityItem(a), storyId: a.storyId })),
242
+ };
243
+ });
244
+
149
245
  export const removeDependencyFn = createServerFn({ method: "POST" })
150
246
  .validator(z.object({ depId: z.string() }))
151
247
  .handler(async ({ data }) => {
152
248
  const request = getRequest();
153
- await requireAuth(request);
154
- return prisma.storyDependency.delete({ where: { id: data.depId } });
249
+ const session = await requireAuth(request);
250
+ const userId = session.user!.id!;
251
+
252
+ const dep = await prisma.storyDependency.findUnique({ where: { id: data.depId } });
253
+ if (!dep) return null;
254
+
255
+ const [sourceStory, targetStory] = await Promise.all([
256
+ prisma.story.findUnique({ where: { id: dep.sourceId }, select: { identifier: true, name: true } }),
257
+ prisma.story.findUnique({ where: { id: dep.targetId }, select: { identifier: true, name: true } }),
258
+ ]);
259
+
260
+ await prisma.storyDependency.delete({ where: { id: data.depId } });
261
+
262
+ // 两端都记活动记录
263
+ const depLabel = targetStory?.identifier || targetStory?.name || dep.targetId;
264
+ const act1 = await prisma.storyActivity.create({
265
+ data: { storyId: dep.sourceId, userId, action: "dependency_removed", newValue: depLabel },
266
+ select: { ...ACTIVITY_SELECT, storyId: true },
267
+ });
268
+ const depLabel2 = sourceStory?.identifier || sourceStory?.name || dep.sourceId;
269
+ const act2 = await prisma.storyActivity.create({
270
+ data: { storyId: dep.targetId, userId, action: "dependency_removed", newValue: depLabel2 },
271
+ select: { ...ACTIVITY_SELECT, storyId: true },
272
+ });
273
+
274
+ // toActivityItem:Date → ISO 字符串,Seroval 可序列化;带 storyId 供客户端过滤当前需求的记录
275
+ return { dep, activities: [act1, act2].map((a) => ({ ...toActivityItem(a), storyId: a.storyId })) };
155
276
  });
156
277
 
157
278
  export const getDependenciesFn = createServerFn({ method: "GET" })
@@ -21,7 +21,6 @@ import { z } from "zod";
21
21
  import { prisma } from "@allbluecn/database";
22
22
  import { requireAuth } from "@/server/auth-helpers";
23
23
  import { ACTIVITY_SELECT, toActivityItem } from "./types";
24
-
25
24
  export const addLinkFn = createServerFn({ method: "POST" })
26
25
  .validator(z.object({ storyId: z.string(), url: z.string().url(), title: z.string().optional(), description: z.string().optional() }))
27
26
  .handler(async ({ data }) => {
@@ -56,6 +55,7 @@ export const updateLinkFn = createServerFn({ method: "POST" })
56
55
  .handler(async ({ data }) => {
57
56
  const request = getRequest();
58
57
  const session = await requireAuth(request);
58
+ const userId = session.user!.id!;
59
59
 
60
60
  const link = await prisma.storyLink.update({
61
61
  where: { id: data.linkId },
@@ -66,7 +66,17 @@ export const updateLinkFn = createServerFn({ method: "POST" })
66
66
  },
67
67
  });
68
68
 
69
- return link;
69
+ const activity = await prisma.storyActivity.create({
70
+ data: {
71
+ storyId: link.storyId,
72
+ userId,
73
+ action: "link_updated",
74
+ newValue: link.title || link.url,
75
+ },
76
+ select: ACTIVITY_SELECT,
77
+ });
78
+
79
+ return { ...link, activity: toActivityItem(activity) };
70
80
  });
71
81
 
72
82
  export const removeLinkFn = createServerFn({ method: "POST" })
@@ -74,12 +84,26 @@ export const removeLinkFn = createServerFn({ method: "POST" })
74
84
  .handler(async ({ data }) => {
75
85
  const request = getRequest();
76
86
  const session = await requireAuth(request);
87
+ const userId = session.user!.id!;
77
88
 
78
- const link = await prisma.storyLink.delete({
79
- where: { id: data.linkId },
80
- });
89
+ const link = await prisma.storyLink.findUnique({ where: { id: data.linkId } });
90
+ await prisma.storyLink.delete({ where: { id: data.linkId } });
91
+
92
+ let activity = null;
93
+ if (link) {
94
+ const created = await prisma.storyActivity.create({
95
+ data: {
96
+ storyId: link.storyId,
97
+ userId,
98
+ action: "link_removed",
99
+ newValue: link.title || link.url,
100
+ },
101
+ select: ACTIVITY_SELECT,
102
+ });
103
+ activity = toActivityItem(created);
104
+ }
81
105
 
82
- return link;
106
+ return { activity };
83
107
  });
84
108
 
85
109
  export const getLinksFn = createServerFn({ method: "GET" })
@@ -73,8 +73,8 @@ async function allocateStoryIdentifier(
73
73
  const isIndependent = project.displayId === BUILTIN_INDEPENDENT_DISPLAY_ID;
74
74
  if (isIndependent) {
75
75
  const counter = await tx.sequenceCounter.upsert({
76
- where: { scope: "global" },
77
- create: { scope: "global", seq: 1 },
76
+ where: { scope: projectId },
77
+ create: { scope: projectId, seq: 1 },
78
78
  update: { seq: { increment: 1 } },
79
79
  });
80
80
  return { sequenceId: counter.seq, identifier: `STORY-${counter.seq}` };
@@ -350,7 +350,7 @@ const listProjectStoriesSchema = z.object({
350
350
  cursor: z.string().optional(),
351
351
  excludeStoryId: z.string().optional(),
352
352
  excludeIds: z.array(z.string()).optional(),
353
- mode: z.enum(["parent", "child", "dependency"]).default("parent"),
353
+ mode: z.enum(["parent", "child", "dependency", "iteration"]).default("parent"),
354
354
  pageSize: z.number().int().min(1).max(50).default(20),
355
355
  });
356
356
 
@@ -376,7 +376,7 @@ export const listProjectStoriesFn = createServerFn({ method: "GET" })
376
376
  const stories = await prisma.story.findMany({
377
377
  where: {
378
378
  ...(data.projectId ? { projectId: data.projectId } : {}),
379
- ...(data.mode === "dependency" ? {} : { parentId: null }),
379
+ ...(data.mode === "dependency" || data.mode === "iteration" ? {} : { parentId: null }),
380
380
  archived: false,
381
381
  ...(excludeIds.size > 0
382
382
  ? { id: { notIn: Array.from(excludeIds) } }
@@ -400,6 +400,7 @@ export const listProjectStoriesFn = createServerFn({ method: "GET" })
400
400
  state: true,
401
401
  priority: true,
402
402
  projectId: true,
403
+ ...(data.mode === "iteration" ? { iteration: { select: { id: true, name: true } } } : {}),
403
404
  },
404
405
  });
405
406
 
@@ -75,6 +75,7 @@ export const STORY_LIST_SELECT_LIGHT = {
75
75
  labels: { select: { label: { select: { id: true, name: true, color: true } } } },
76
76
  createdAt: true,
77
77
  updatedAt: true,
78
+ iteration: { select: { id: true, name: true } },
78
79
  } satisfies Prisma.StoryDefaultArgs["select"];
79
80
 
80
81
  export type StoryListRow = Prisma.StoryGetPayload<{ select: typeof STORY_LIST_SELECT }>;
@@ -82,7 +82,7 @@ export type ProjectModuleKey =
82
82
  | "bug"
83
83
  | "acceptance";
84
84
 
85
- // 板块概览(卡片网格用,mock
85
+ // 板块概览(卡片网格用,count/lastUpdatedAt 来自 getModuleOverviewFn
86
86
  export interface ProjectModuleOverview {
87
87
  key: ProjectModuleKey;
88
88
  title: string;
@@ -92,12 +92,12 @@ export interface ProjectModuleOverview {
92
92
  lastUpdatedAt: Date | null;
93
93
  }
94
94
 
95
- // 板块列表项(mock,用于板块详情页)
95
+ // 板块列表项(requirement 板块来自 listModuleStoriesFn,其余板块暂无数据源)
96
96
  export interface ProjectModuleItem {
97
97
  id: string;
98
98
  title: string;
99
- status: string;
99
+ state: string;
100
100
  priority: string;
101
- assignee?: string;
102
- updatedAt: Date;
101
+ assignee?: string | null;
102
+ updatedAt: string;
103
103
  }
@@ -1,73 +0,0 @@
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 type { ComponentType } from "react";
19
- import { FolderKanbanIcon } from "@/components/animated-icons/folder-kanban";
20
- import { BriefcaseBusinessIcon } from "@/components/animated-icons/briefcase-business";
21
- import { RocketIcon } from "@/components/animated-icons/rocket";
22
- import { FlaskIcon } from "@/components/animated-icons/flask";
23
- import { TerminalIcon } from "@/components/animated-icons/terminal";
24
- import { ContrastIcon } from "@/components/animated-icons/contrast";
25
- import { MessageSquareIcon } from "@/components/animated-icons/message-square";
26
- import { GraduationCapIcon } from "@/components/animated-icons/graduation-cap";
27
- import { HeartPulseIcon } from "@/components/animated-icons/heart-pulse";
28
- import { CartIcon } from "@/components/animated-icons/cart";
29
- import { LayersIcon } from "@/components/animated-icons/layers";
30
- import { UsersIcon } from "@/components/animated-icons/users";
31
- import { EarthIcon } from "@/components/animated-icons/earth";
32
- import { SparklesIcon } from "@/components/animated-icons/sparkles";
33
- import { CompassIcon } from "@/components/animated-icons/compass";
34
- import { ShieldCheckIcon } from "@/components/animated-icons/shield-check";
35
- import { cn } from "@/lib/utils";
36
-
37
- type AnimatedIconComp = ComponentType<{ size?: number; className?: string }>;
38
-
39
- // 项目动画图标映射(value 与 PROJECT_ICONS 一一对应)
40
- // 仅用于项目详情页顶部展示,其他位置用 ProjectIconDisplay(静态)
41
- const ANIMATED_MAP: Record<string, AnimatedIconComp> = {
42
- "folder-kanban": FolderKanbanIcon,
43
- "briefcase-business": BriefcaseBusinessIcon,
44
- rocket: RocketIcon,
45
- flask: FlaskIcon,
46
- terminal: TerminalIcon,
47
- contrast: ContrastIcon,
48
- "message-square": MessageSquareIcon,
49
- "graduation-cap": GraduationCapIcon,
50
- "heart-pulse": HeartPulseIcon,
51
- cart: CartIcon,
52
- layers: LayersIcon,
53
- users: UsersIcon,
54
- earth: EarthIcon,
55
- sparkles: SparklesIcon,
56
- compass: CompassIcon,
57
- "shield-check": ShieldCheckIcon,
58
- };
59
-
60
- interface ProjectAnimatedIconProps {
61
- name?: string;
62
- size?: number;
63
- className?: string;
64
- }
65
-
66
- export function ProjectAnimatedIcon({
67
- name,
68
- size = 24,
69
- className,
70
- }: ProjectAnimatedIconProps) {
71
- const Icon = ANIMATED_MAP[name ?? ""] ?? FolderKanbanIcon;
72
- return <Icon size={size} className={cn(className)} />;
73
- }
@@ -1 +0,0 @@
1
- export * from '@allbluecn/ui/icon-packs'
@@ -1 +0,0 @@
1
- export * from '@allbluecn/ui/icon-packs/knowledge'
@@ -1 +0,0 @@
1
- export * from '@allbluecn/ui/icon-packs/prd'
@@ -1 +0,0 @@
1
- export * from '@allbluecn/ui/icon-packs/project'
@@ -1 +0,0 @@
1
- export * from '@allbluecn/ui/icon-picker'
@@ -1,156 +0,0 @@
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
- ProjectModuleItem,
30
- } from "@/types/project";
31
-
32
- // 6 个板块元数据(key 与路由 $module 对应)
33
- export const PROJECT_MODULE_META: Record<
34
- ProjectModuleKey,
35
- Omit<ProjectModuleOverview, "count" | "lastUpdatedAt" | "link">
36
- > = {
37
- requirement: {
38
- key: "requirement",
39
- title: "需求",
40
- description: "收集与跟踪产品需求池",
41
- icon: ClipboardList,
42
- },
43
- prd: {
44
- key: "prd",
45
- title: "PRD",
46
- description: "产品需求文档与原型画板",
47
- icon: Package2,
48
- },
49
- design: {
50
- key: "design",
51
- title: "设计",
52
- description: "UI 设计稿与画板资源",
53
- icon: Palette,
54
- },
55
- testcase: {
56
- key: "testcase",
57
- title: "测例",
58
- description: "测试用例与执行计划",
59
- icon: FlaskConical,
60
- },
61
- bug: {
62
- key: "bug",
63
- title: "BUG",
64
- description: "缺陷跟踪与修复进度",
65
- icon: Bug,
66
- },
67
- acceptance: {
68
- key: "acceptance",
69
- title: "验收",
70
- description: "交付验收与上线检查",
71
- icon: CircleCheckBig,
72
- },
73
- };
74
-
75
- export const PROJECT_MODULE_KEYS = Object.keys(
76
- PROJECT_MODULE_META,
77
- ) as ProjectModuleKey[];
78
-
79
- // 生成 mock 板块概览(根据 projectId 稳定生成数量)
80
- export function buildModuleOverview(projectId: string): ProjectModuleOverview[] {
81
- const seed = projectId.charCodeAt(0) + projectId.length;
82
- return PROJECT_MODULE_KEYS.map((key, idx) => {
83
- const meta = PROJECT_MODULE_META[key];
84
- const count = (seed + idx * 7) % 40;
85
- const daysAgo = (seed + idx * 3) % 30;
86
- const lastUpdatedAt = new Date(Date.now() - daysAgo * 86400000);
87
- return {
88
- ...meta,
89
- count,
90
- lastUpdatedAt,
91
- };
92
- });
93
- }
94
-
95
- // 板块状态与优先级(mock 文案池)
96
- const STATUSES = ["待处理", "进行中", "已完成", "已验收", "已关闭"];
97
- const PRIORITIES = ["高", "中", "高", "中", "低"];
98
- const ASSIGNEES = ["张明", "李华", "王芳", "赵磊", "陈静", "刘洋"];
99
- const TITLES: Record<ProjectModuleKey, string[]> = {
100
- requirement: [
101
- "用户登录优化",
102
- "支付流程改造",
103
- "首页性能提升",
104
- "消息中心重构",
105
- "数据看板新增",
106
- "权限体系完善",
107
- ],
108
- prd: [
109
- "登录注册 PRD v2",
110
- "支付系统 PRD",
111
- "首页改版 PRD",
112
- "消息中心 PRD",
113
- ],
114
- design: [
115
- "首页视觉稿",
116
- "支付页设计",
117
- "个人中心 UI",
118
- "引导页插画",
119
- "图标库更新",
120
- ],
121
- testcase: [
122
- "登录用例集",
123
- "支付回归用例",
124
- "首页兼容性用例",
125
- "性能压测用例",
126
- "权限用例",
127
- ],
128
- bug: [
129
- "iOS 16 输入框错位",
130
- "支付偶发失败",
131
- "列表滑动卡顿",
132
- "深色模式样式异常",
133
- ],
134
- acceptance: [
135
- "v2.1 版本验收",
136
- "支付模块验收",
137
- "性能优化验收",
138
- ],
139
- };
140
-
141
- // 根据 projectId + module 生成稳定的 mock 列表
142
- export function listModuleItems(
143
- projectId: string,
144
- module: ProjectModuleKey,
145
- ): ProjectModuleItem[] {
146
- const titles = TITLES[module];
147
- const seed = projectId.charCodeAt(0) + module.charCodeAt(0);
148
- return titles.map((title, idx) => ({
149
- id: `${projectId}-${module}-${idx}`,
150
- title,
151
- status: STATUSES[(seed + idx) % STATUSES.length],
152
- priority: PRIORITIES[(seed + idx) % PRIORITIES.length],
153
- assignee: ASSIGNEES[(seed + idx) % ASSIGNEES.length],
154
- updatedAt: new Date(Date.now() - ((seed + idx * 2) % 30) * 86400000),
155
- }));
156
- }