@allbluecn/web-app 0.5.0 → 0.6.0

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 (170) hide show
  1. package/package.json +239 -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/knowledge/knowledge-toolbar.tsx +124 -1
  12. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  13. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  14. package/src/components/project/create-project-dialog.tsx +44 -57
  15. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  16. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  17. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  18. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  19. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  20. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  21. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  22. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  23. package/src/components/story/__tests__/constants.test.ts +255 -0
  24. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  25. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  26. package/src/components/story/constants.ts +43 -1
  27. package/src/components/story/create-story-dialog.tsx +42 -16
  28. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  29. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  30. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  31. package/src/components/story/detail/activity-timeline.tsx +26 -4
  32. package/src/components/story/detail/story-detail-body.tsx +90 -38
  33. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
  34. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  35. package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
  36. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
  37. package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
  38. package/src/components/story/detail/story-properties.tsx +21 -4
  39. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  40. package/src/components/story/display/group-resolver.tsx +2 -2
  41. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  42. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  43. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  44. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  45. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  46. package/src/components/story/inline-editors/index.ts +2 -1
  47. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  48. package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
  49. package/src/components/story/peek-panel.tsx +121 -21
  50. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  51. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  52. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  53. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  54. package/src/components/story/relations/file-preview.tsx +173 -0
  55. package/src/components/story/relations/format.ts +120 -0
  56. package/src/components/story/relations/general-groups.ts +32 -0
  57. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  58. package/src/components/story/relations/link-manage-dialog.tsx +192 -0
  59. package/src/components/story/relations/story-ref-row.tsx +17 -27
  60. package/src/components/story/story-panel-header.tsx +25 -19
  61. package/src/components/story/story-toolbar.tsx +3 -3
  62. package/src/components/story/transfer-story-popover.tsx +2 -2
  63. package/src/components/story/types.ts +14 -0
  64. package/src/components/story/views/create-view-dialog.tsx +3 -3
  65. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  66. package/src/components/story/views/story-list/group.tsx +1 -1
  67. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  68. package/src/components/story/views/story-list/row.tsx +1 -1
  69. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  70. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  71. package/src/lib/changelog/sdk-versions.ts +20 -20
  72. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  73. package/src/lib/iteration/status.ts +28 -0
  74. package/src/lib/project-display-id.ts +14 -5
  75. package/src/lib/project-module-meta.ts +77 -0
  76. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  77. package/src/lib/story/video-embed.ts +59 -0
  78. package/src/lib/utils.d.ts +1 -1
  79. package/src/plugins/.gen-manifest.json +13 -0
  80. package/src/plugins/modules.gen.ts +20 -0
  81. package/src/plugins/plugins.gen.ts +20 -0
  82. package/src/plugins/ui.gen.ts +8 -0
  83. package/src/providers/index.tsx +6 -0
  84. package/src/routeTree.gen.ts +96 -0
  85. package/src/routes/__root.tsx +0 -8
  86. package/src/routes/_nav/inspiration/route.tsx +8 -0
  87. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  88. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  89. package/src/routes/_nav/prd/index.tsx +9 -0
  90. package/src/routes/_nav/prd/route.tsx +8 -0
  91. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  92. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  93. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  94. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  95. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  96. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  97. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  98. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  99. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
  100. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  101. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  102. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  103. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  104. package/src/routes/_nav/tags/index.tsx +8 -0
  105. package/src/routes/_nav/tags/route.tsx +8 -0
  106. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  107. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  108. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  109. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  110. package/src/routes/api/attachments/$id/content.ts +60 -0
  111. package/src/routes/api/attachments/upload.ts +148 -0
  112. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  114. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  115. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  116. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  117. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  118. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  119. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  120. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  121. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  122. package/src/server/serverFns/iteration/index.ts +22 -0
  123. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  124. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  125. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  126. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  127. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  128. package/src/server/serverFns/iteration/schemas.ts +58 -0
  129. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  130. package/src/server/serverFns/knowledge.ts +50 -4
  131. package/src/server/serverFns/project/module-overview.ts +109 -0
  132. package/src/server/serverFns/project.ts +45 -2
  133. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  134. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  135. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  136. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  137. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
  138. package/src/server/serverFns/story/index.ts +9 -4
  139. package/src/server/serverFns/story/kb-general.ts +39 -0
  140. package/src/server/serverFns/story/kb-scope.ts +52 -0
  141. package/src/server/serverFns/story/link-preview.ts +115 -0
  142. package/src/server/serverFns/story/story-assoc.ts +372 -51
  143. package/src/server/serverFns/story/story-attachments.ts +32 -45
  144. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  145. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  146. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  147. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  148. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  149. package/src/server/serverFns/story/story-crud.ts +53 -6
  150. package/src/server/serverFns/story/story-dependencies.ts +158 -5
  151. package/src/server/serverFns/story/story-links.ts +43 -7
  152. package/src/server/serverFns/story/story-transfer.ts +5 -4
  153. package/src/server/serverFns/story/story-tree.ts +28 -1
  154. package/src/server/serverFns/story/types.ts +38 -0
  155. package/src/styles/globals.css +25 -0
  156. package/src/types/project.ts +5 -5
  157. package/vite.shared.ts +2 -0
  158. package/src/components/project/project-animated-icon.tsx +0 -73
  159. package/src/components/shared/icon-packs/index.ts +0 -1
  160. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  161. package/src/components/shared/icon-packs/prd.ts +0 -1
  162. package/src/components/shared/icon-packs/project.ts +0 -1
  163. package/src/components/shared/icon-picker.tsx +0 -1
  164. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  165. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  166. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  167. package/src/components/story/relations/attachments-panel.tsx +0 -179
  168. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  169. package/src/components/story/relations/links-panel.tsx +0 -240
  170. package/src/lib/project-mock.ts +0 -156
@@ -21,6 +21,7 @@ import type { StoryState, StoryPriority } from "@/components/story/constants";
21
21
  import type {
22
22
  UserBrief,
23
23
  ReactionGroup,
24
+ ActivityItem,
24
25
  } from "@/components/story/types";
25
26
 
26
27
  export const USER_BRIEF_SELECT = {
@@ -74,6 +75,7 @@ export const STORY_LIST_SELECT_LIGHT = {
74
75
  labels: { select: { label: { select: { id: true, name: true, color: true } } } },
75
76
  createdAt: true,
76
77
  updatedAt: true,
78
+ iteration: { select: { id: true, name: true } },
77
79
  } satisfies Prisma.StoryDefaultArgs["select"];
78
80
 
79
81
  export type StoryListRow = Prisma.StoryGetPayload<{ select: typeof STORY_LIST_SELECT }>;
@@ -140,6 +142,42 @@ export function castUserBrief(u: {
140
142
  };
141
143
  }
142
144
 
145
+ /** 活动记录 select(含 user brief),供 serverFn 返回新创建的活动给 UI 即时追加 */
146
+ export const ACTIVITY_SELECT = {
147
+ id: true,
148
+ action: true,
149
+ field: true,
150
+ oldValue: true,
151
+ newValue: true,
152
+ isAI: true,
153
+ createdAt: true,
154
+ user: { select: USER_BRIEF_SELECT },
155
+ } as const;
156
+
157
+ export interface ActivityRow {
158
+ id: string;
159
+ action: string;
160
+ field: string | null;
161
+ oldValue: string | null;
162
+ newValue: string | null;
163
+ isAI: boolean;
164
+ createdAt: Date;
165
+ user: { id: string; username: string; email: string; avatarConfig: unknown; bgShape: string | null; bgColor: string | null };
166
+ }
167
+
168
+ export function toActivityItem(a: ActivityRow): ActivityItem {
169
+ return {
170
+ id: a.id,
171
+ action: a.action,
172
+ field: a.field,
173
+ oldValue: a.oldValue,
174
+ newValue: a.newValue,
175
+ isAI: a.isAI,
176
+ createdAt: a.createdAt.toISOString(),
177
+ user: castUserBrief(a.user),
178
+ };
179
+ }
180
+
143
181
  export function aggregateReactions(
144
182
  reactions: { emoji: string; userId: string }[],
145
183
  currentUserId: string,
@@ -493,3 +493,28 @@ html.theme-transitioning *::after {
493
493
  height: 40px !important;
494
494
  border-radius: 20px !important;
495
495
  }
496
+
497
+ /* 关联附件预览网格:笔记本4列,大屏(≥1536px)5列;4列时预览高度较矮 */
498
+ .attachment-preview-grid {
499
+ grid-template-columns: repeat(4, minmax(0, 1fr));
500
+ --attachment-preview-h: 18rem;
501
+ --attachment-preview-h-sub: 9rem;
502
+ }
503
+ @media (min-width: 1536px) {
504
+ .attachment-preview-grid {
505
+ grid-template-columns: repeat(5, minmax(0, 1fr));
506
+ /* 5列(大屏)保持原高度(h-96 / h-48) */
507
+ --attachment-preview-h: 24rem;
508
+ --attachment-preview-h-sub: 12rem;
509
+ }
510
+ }
511
+
512
+ /* open-file-viewer 预览(FilePreview):插件不支持的命令按钮处于 disabled 态,直接隐藏 */
513
+ .ofv-preview-scope .ofv-toolbar button:disabled {
514
+ display: none;
515
+ }
516
+ /* open-file-viewer 预览(FilePreview):默认隐藏 text/code 顶部标题栏,由组件切换类名显隐;
517
+ 音频等类型的标题不隐藏 */
518
+ .ofv-preview-title-hidden .ofv-code-header {
519
+ display: none;
520
+ }
@@ -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
  }
package/vite.shared.ts CHANGED
@@ -160,6 +160,8 @@ export function createWebViteConfig(opts: WebViteConfigOptions) {
160
160
  '@allblue/devtools',
161
161
  '@allbluecn/plugins-core',
162
162
  '@allbluecn/tiptap',
163
+ '@open-file-viewer/core',
164
+ '@open-file-viewer/react',
163
165
  ],
164
166
  },
165
167
  }
@@ -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,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 { Paperclip } from "lucide-react";
19
- import { useTranslation } from "react-i18next";
20
- import { Button } from "@/components/ui/button";
21
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
- import { AttachmentsPanel } from "../../relations/attachments-panel";
23
- import type { RelationsData } from "../../types";
24
-
25
- export function StoryDetailAttachments({
26
- storyId,
27
- data,
28
- onRefresh,
29
- }: {
30
- storyId: string;
31
- data: RelationsData;
32
- onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
33
- }) {
34
- const { t } = useTranslation("story");
35
- const count = data.attachments.length;
36
-
37
- return (
38
- <Popover>
39
- <PopoverTrigger asChild>
40
- <Button
41
- variant="ghost"
42
- size="sm"
43
- className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
44
- title={t("relations.uploadAttachment")}
45
- >
46
- <Paperclip className="size-3.5 text-muted-foreground" />
47
- {t("relations.attachment")}
48
- {count > 0 && (
49
- <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
50
- {count}
51
- </span>
52
- )}
53
- </Button>
54
- </PopoverTrigger>
55
- <PopoverContent align="start" className="w-[380px] p-4">
56
- <div className="flex items-center justify-between mb-3">
57
- <span className="text-sm font-semibold">{t("relations.attachment")}</span>
58
- {count > 0 && <span className="text-xs text-muted-foreground">{t("relations.itemCount", { count })}</span>}
59
- </div>
60
- <AttachmentsPanel
61
- storyId={storyId}
62
- data={data.attachments}
63
- onUpdate={(newAttachments) =>
64
- onRefresh((prev) => ({
65
- ...prev,
66
- attachments: newAttachments,
67
- }))
68
- }
69
- />
70
- </PopoverContent>
71
- </Popover>
72
- );
73
- }
@@ -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 { BookOpenText } from "lucide-react";
19
- import { useTranslation } from "react-i18next";
20
- import { Button } from "@/components/ui/button";
21
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
- import { KnowledgePanel } from "../../relations/knowledge-panel";
23
- import type { RelationsData } from "../../types";
24
-
25
- export function StoryDetailKnowledge({
26
- storyId,
27
- data,
28
- onRefresh,
29
- }: {
30
- storyId: string;
31
- data: RelationsData;
32
- onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
33
- }) {
34
- const { t } = useTranslation("story");
35
- const count = data.knowledge.prds.length + data.knowledge.docs.length;
36
-
37
- return (
38
- <Popover>
39
- <PopoverTrigger asChild>
40
- <Button
41
- variant="ghost"
42
- size="sm"
43
- className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
44
- title={t("relations.knowledgeTitle")}
45
- >
46
- <BookOpenText className="size-3.5 text-muted-foreground" />
47
- {t("relations.knowledge")}
48
- {count > 0 && (
49
- <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
50
- {count}
51
- </span>
52
- )}
53
- </Button>
54
- </PopoverTrigger>
55
- <PopoverContent align="start" className="w-[380px] p-4">
56
- <div className="flex items-center justify-between mb-3">
57
- <span className="text-sm font-semibold">{t("relations.knowledge")}</span>
58
- {count > 0 && <span className="text-xs text-muted-foreground">{t("relations.itemCount", { count })}</span>}
59
- </div>
60
- <KnowledgePanel
61
- storyId={storyId}
62
- data={data.knowledge}
63
- onUpdate={(updates) =>
64
- onRefresh((prev) => ({
65
- ...prev,
66
- knowledge: { ...prev.knowledge, ...updates },
67
- }))
68
- }
69
- />
70
- </PopoverContent>
71
- </Popover>
72
- );
73
- }
@@ -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 { Link2 } from "lucide-react";
19
- import { useTranslation } from "react-i18next";
20
- import { Button } from "@/components/ui/button";
21
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
- import { LinksPanel } from "../../relations/links-panel";
23
- import type { RelationsData } from "../../types";
24
-
25
- export function StoryDetailLinks({
26
- storyId,
27
- data,
28
- onRefresh,
29
- }: {
30
- storyId: string;
31
- data: RelationsData;
32
- onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
33
- }) {
34
- const { t } = useTranslation("story");
35
- const count = data.links.length;
36
-
37
- return (
38
- <Popover>
39
- <PopoverTrigger asChild>
40
- <Button
41
- variant="ghost"
42
- size="sm"
43
- className="h-7 gap-1.5 px-2.5 text-xs font-normal hover:bg-muted rounded-md"
44
- title={t("relations.linksTitle")}
45
- >
46
- <Link2 className="size-3.5 text-muted-foreground" />
47
- {t("relations.links")}
48
- {count > 0 && (
49
- <span className="ml-0.5 inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
50
- {count}
51
- </span>
52
- )}
53
- </Button>
54
- </PopoverTrigger>
55
- <PopoverContent align="start" className="w-[380px] p-4">
56
- <div className="flex items-center justify-between mb-3">
57
- <span className="text-sm font-semibold">{t("relations.links")}</span>
58
- {count > 0 && <span className="text-xs text-muted-foreground">{t("relations.itemCount", { count })}</span>}
59
- </div>
60
- <LinksPanel
61
- storyId={storyId}
62
- data={data.links}
63
- onUpdate={(newLinks) =>
64
- onRefresh((prev) => ({
65
- ...prev,
66
- links: newLinks,
67
- }))
68
- }
69
- />
70
- </PopoverContent>
71
- </Popover>
72
- );
73
- }
@@ -1,179 +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 * as React from "react";
19
- import { Plus, Upload, FileText, Trash2 } from "lucide-react";
20
- import { useTranslation } from "react-i18next";
21
- import { Button } from "@/components/ui/button";
22
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
23
- import { useServerFn } from "@tanstack/react-start";
24
- import { getAttachmentsFn, removeAttachmentFn } from "@/server/serverFns/story";
25
- import { toast } from "sonner";
26
- import { type StoryAttachmentItem } from "../types";
27
-
28
- export interface AttachmentsPanelProps {
29
- storyId: string;
30
- data: StoryAttachmentItem[];
31
- onUpdate: (newAttachments: StoryAttachmentItem[]) => void;
32
- }
33
-
34
- export function AttachmentsPanel({ storyId, data, onUpdate }: AttachmentsPanelProps) {
35
- const { t } = useTranslation("story");
36
- const [open, setOpen] = React.useState(false);
37
- const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
38
- const [uploading, setUploading] = React.useState(false);
39
-
40
- const removeAttachment = useServerFn(removeAttachmentFn);
41
-
42
- const handleUpload = async () => {
43
- if (!selectedFile) {
44
- toast.error(t("attachments.selectFile"));
45
- return;
46
- }
47
-
48
- setUploading(true);
49
- try {
50
- // 使用原生 fetch 调用 serverFn 端点上传(支持 FormData)
51
- const formData = new FormData();
52
- formData.append("storyId", storyId);
53
- formData.append("file", selectedFile);
54
-
55
- // 注意:serverFn 不支持 FormData,这里使用 Mock 实现
56
- // 实际应用中应调用专用上传 API
57
- const mockUrl = `/mock-storage/${Date.now()}-${selectedFile.name}`;
58
- const newAttachment: StoryAttachmentItem = {
59
- id: `mock-${Date.now()}`,
60
- fileName: selectedFile.name,
61
- fileSize: selectedFile.size,
62
- mimeType: selectedFile.type || "application/octet-stream",
63
- storageUrl: mockUrl,
64
- };
65
-
66
- // 乐观更新
67
- onUpdate([newAttachment, ...data]);
68
- toast.success(t("attachments.uploaded"));
69
- setOpen(false);
70
- setSelectedFile(null);
71
- } catch (error: any) {
72
- console.error(t("attachments.uploadFailed"), error);
73
- toast.error(error.message || t("attachments.uploadFailed"));
74
- } finally {
75
- setUploading(false);
76
- }
77
- };
78
-
79
- const handleRemove = async (attachmentId: string) => {
80
- try {
81
- // Mock 附件用本地移除
82
- if (attachmentId.startsWith("mock-")) {
83
- onUpdate(data.filter((att) => att.id !== attachmentId));
84
- toast.success(t("attachments.removed"));
85
- return;
86
- }
87
-
88
- await removeAttachment({ data: { attachmentId } });
89
- toast.success(t("attachments.removed"));
90
- onUpdate(data.filter((att) => att.id !== attachmentId));
91
- } catch (error: any) {
92
- console.error(t("attachments.removeFailed"), error);
93
- toast.error(error.message || t("attachments.removeFailed"));
94
- }
95
- };
96
-
97
- const formatFileSize = (bytes: number) => {
98
- if (bytes === 0) return "0 B";
99
- const k = 1024;
100
- const sizes = ["B", "KB", "MB", "GB"];
101
- const i = Math.floor(Math.log(bytes) / Math.log(k));
102
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
103
- };
104
-
105
- return (
106
- <div className="space-y-4">
107
- <div className="flex items-center justify-between">
108
- <div className="text-sm font-medium">{t("attachments.title")}</div>
109
- <Dialog open={open} onOpenChange={setOpen}>
110
- <DialogTrigger asChild>
111
- <Button type="button" variant="outline" size="sm">
112
- <Plus className="h-4 w-4 mr-2" />
113
- {t("attachments.add")}
114
- </Button>
115
- </DialogTrigger>
116
- <DialogContent>
117
- <DialogHeader>
118
- <DialogTitle>{t("attachments.uploadTitle")}</DialogTitle>
119
- </DialogHeader>
120
- <div className="space-y-4">
121
- <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-md cursor-pointer hover:bg-muted transition-colors">
122
- <Upload className="h-8 w-8 mb-2 text-muted-foreground" />
123
- <span className="text-sm text-muted-foreground">
124
- {selectedFile ? selectedFile.name : t("attachments.clickToSelect")}
125
- </span>
126
- <input
127
- type="file"
128
- className="hidden"
129
- onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
130
- />
131
- </label>
132
- <div className="flex justify-end gap-2">
133
- <Button type="button" variant="outline" onClick={() => setOpen(false)}>
134
- {t("attachments.cancel")}
135
- </Button>
136
- <Button type="button" onClick={handleUpload} disabled={uploading || !selectedFile}>
137
- {uploading ? t("attachments.uploading") : t("attachments.upload")}
138
- </Button>
139
- </div>
140
- </div>
141
- </DialogContent>
142
- </Dialog>
143
- </div>
144
-
145
- {data.length > 0 ? (
146
- <div className="space-y-2">
147
- {data.map((att) => (
148
- <div
149
- key={att.id}
150
- className="flex items-center justify-between p-3 bg-muted rounded-md"
151
- >
152
- <div className="flex items-center gap-2 flex-1 min-w-0">
153
- <FileText className="h-4 w-4 text-muted-foreground shrink-0" />
154
- <div className="flex-1 min-w-0">
155
- <div className="text-sm font-medium truncate">{att.fileName}</div>
156
- <div className="text-xs text-muted-foreground">
157
- {formatFileSize(att.fileSize)} · {att.mimeType}
158
- </div>
159
- </div>
160
- </div>
161
- <Button
162
- type="button"
163
- variant="ghost"
164
- size="icon"
165
- onClick={() => handleRemove(att.id)}
166
- >
167
- <Trash2 className="h-4 w-4" />
168
- </Button>
169
- </div>
170
- ))}
171
- </div>
172
- ) : (
173
- <div className="text-sm text-muted-foreground text-center py-4">
174
- {t("attachments.empty")}
175
- </div>
176
- )}
177
- </div>
178
- );
179
- }