work-agent 0.1.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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,511 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ Upload,
7
+ Download,
8
+ Trash2,
9
+ FileIcon,
10
+ Search,
11
+ Share2,
12
+ Loader2,
13
+ HardDrive,
14
+ Image,
15
+ FileText,
16
+ FileCode,
17
+ FileArchive,
18
+ Film,
19
+ Music,
20
+ File,
21
+ Eye,
22
+ Copy,
23
+ Check,
24
+ X,
25
+ } from 'lucide-react';
26
+ import { Button } from '@/components/ui/button';
27
+ import { Card, CardContent } from '@/components/ui/card';
28
+ import { Input } from '@/components/ui/input';
29
+ import { Badge } from '@/components/ui/badge';
30
+ import {
31
+ Dialog,
32
+ DialogContent,
33
+ DialogDescription,
34
+ DialogHeader,
35
+ DialogTitle,
36
+ } from '@/components/ui/dialog';
37
+ import { cn } from '@/lib/utils';
38
+
39
+ interface FileMetadata {
40
+ size: number;
41
+ mimeType: string;
42
+ md5?: string;
43
+ storageKey: string;
44
+ storageType: string;
45
+ originalName?: string;
46
+ }
47
+
48
+ interface FileItem {
49
+ id: string;
50
+ title: string;
51
+ author: string;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ fileMetadata: FileMetadata | null;
55
+ isPublic: boolean;
56
+ content?: string;
57
+ }
58
+
59
+ function formatFileSize(bytes: number): string {
60
+ if (bytes === 0) return '0 B';
61
+ const k = 1024;
62
+ const sizes = ['B', 'KB', 'MB', 'GB'];
63
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
64
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
65
+ }
66
+
67
+ function formatDate(dateStr: string): string {
68
+ const date = new Date(dateStr);
69
+ return date.toLocaleDateString('zh-CN', {
70
+ year: 'numeric',
71
+ month: '2-digit',
72
+ day: '2-digit',
73
+ hour: '2-digit',
74
+ minute: '2-digit',
75
+ });
76
+ }
77
+
78
+ function getFileIcon(mimeType: string) {
79
+ if (mimeType.startsWith('image/')) return Image;
80
+ if (mimeType.startsWith('video/')) return Film;
81
+ if (mimeType.startsWith('audio/')) return Music;
82
+ if (mimeType.includes('pdf')) return FileText;
83
+ if (mimeType.includes('word') || mimeType.includes('document')) return FileText;
84
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return FileText;
85
+ if (mimeType.includes('zip') || mimeType.includes('tar') || mimeType.includes('gzip')) return FileArchive;
86
+ if (mimeType.includes('javascript') || mimeType.includes('json') || mimeType.includes('html') || mimeType.includes('css')) return FileCode;
87
+ return File;
88
+ }
89
+
90
+ function getMimeTypeCategory(mimeType: string): string {
91
+ if (mimeType.startsWith('image/')) return '图片';
92
+ if (mimeType.startsWith('video/')) return '视频';
93
+ if (mimeType.startsWith('audio/')) return '音频';
94
+ if (mimeType.includes('pdf')) return 'PDF';
95
+ if (mimeType.includes('word') || mimeType.includes('document')) return '文档';
96
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '表格';
97
+ if (mimeType.includes('zip') || mimeType.includes('tar') || mimeType.includes('gzip')) return '压缩包';
98
+ if (mimeType.includes('javascript') || mimeType.includes('json') || mimeType.includes('html') || mimeType.includes('css')) return '代码';
99
+ if (mimeType.startsWith('text/')) return '文本';
100
+ return '文件';
101
+ }
102
+
103
+ function isTextFile(mimeType: string): boolean {
104
+ return mimeType.startsWith('text/') ||
105
+ mimeType === 'application/json' ||
106
+ mimeType === 'application/xml' ||
107
+ mimeType === 'application/javascript' ||
108
+ mimeType.includes('json') ||
109
+ mimeType.includes('xml') ||
110
+ mimeType.includes('javascript') ||
111
+ mimeType.includes('html') ||
112
+ mimeType.includes('css') ||
113
+ mimeType === 'application/octet-stream';
114
+ }
115
+
116
+ export default function MyFilesPage() {
117
+ const router = useRouter();
118
+ const [files, setFiles] = useState<FileItem[]>([]);
119
+ const [loading, setLoading] = useState(true);
120
+ const [uploading, setUploading] = useState(false);
121
+ const [userId, setUserId] = useState<string>('');
122
+ const [searchQuery, setSearchQuery] = useState('');
123
+ const [totalSize, setTotalSize] = useState('0 B');
124
+ const [total, setTotal] = useState(0);
125
+ const [dragOver, setDragOver] = useState(false);
126
+ const [previewFile, setPreviewFile] = useState<FileItem | null>(null);
127
+ const [previewContent, setPreviewContent] = useState<string>('');
128
+ const [previewLoading, setPreviewLoading] = useState(false);
129
+ const [shareCopied, setShareCopied] = useState(false);
130
+ const fileInputRef = useRef<HTMLInputElement>(null);
131
+
132
+ useEffect(() => {
133
+ const uid = localStorage.getItem('userId');
134
+ if (!uid) {
135
+ router.push('/login');
136
+ return;
137
+ }
138
+ setUserId(uid);
139
+ fetchFiles(uid);
140
+ }, [router]);
141
+
142
+ const fetchFiles = async (uid: string, search?: string) => {
143
+ try {
144
+ setLoading(true);
145
+ const params = new URLSearchParams({ userId: uid });
146
+ if (search) params.set('search', search);
147
+
148
+ const res = await fetch(`/api/my/files?${params}`);
149
+ if (res.ok) {
150
+ const data = await res.json();
151
+ setFiles(data.files || []);
152
+ setTotalSize(data.totalSizeFormatted || '0 B');
153
+ setTotal(data.total || 0);
154
+ }
155
+ } catch (error) {
156
+ console.error('Failed to fetch files:', error);
157
+ } finally {
158
+ setLoading(false);
159
+ }
160
+ };
161
+
162
+ const handleSearch = useCallback((query: string) => {
163
+ setSearchQuery(query);
164
+ if (userId) {
165
+ fetchFiles(userId, query);
166
+ }
167
+ }, [userId]);
168
+
169
+ const handleUpload = async (file: File) => {
170
+ if (!userId) return;
171
+
172
+ setUploading(true);
173
+ try {
174
+ const formData = new FormData();
175
+ formData.append('userId', userId);
176
+ formData.append('file', file);
177
+
178
+ const res = await fetch('/api/my/files', {
179
+ method: 'POST',
180
+ body: formData,
181
+ });
182
+
183
+ if (!res.ok) {
184
+ const error = await res.json();
185
+ alert(error.error || '上传失败');
186
+ return;
187
+ }
188
+
189
+ fetchFiles(userId, searchQuery);
190
+ } catch (error) {
191
+ console.error('Upload error:', error);
192
+ alert('上传失败');
193
+ } finally {
194
+ setUploading(false);
195
+ }
196
+ };
197
+
198
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
199
+ const file = e.target.files?.[0];
200
+ if (file) {
201
+ handleUpload(file);
202
+ }
203
+ e.target.value = '';
204
+ };
205
+
206
+ const handleDrop = (e: React.DragEvent) => {
207
+ e.preventDefault();
208
+ setDragOver(false);
209
+ const file = e.dataTransfer.files[0];
210
+ if (file) {
211
+ handleUpload(file);
212
+ }
213
+ };
214
+
215
+ const handleDragOver = (e: React.DragEvent) => {
216
+ e.preventDefault();
217
+ setDragOver(true);
218
+ };
219
+
220
+ const handleDragLeave = () => {
221
+ setDragOver(false);
222
+ };
223
+
224
+ const handleDownload = (file: FileItem) => {
225
+ const url = `/api/my/files/${file.id}?userId=${userId}&download=true`;
226
+ window.open(url, '_blank');
227
+ };
228
+
229
+ const handlePreview = async (file: FileItem) => {
230
+ setPreviewFile(file);
231
+ setPreviewContent('');
232
+
233
+ if (file.fileMetadata && isTextFile(file.fileMetadata.mimeType)) {
234
+ setPreviewLoading(true);
235
+ try {
236
+ const res = await fetch(`/api/my/files/${file.id}?userId=${userId}`);
237
+ if (res.ok) {
238
+ const text = await res.text();
239
+ setPreviewContent(text);
240
+ }
241
+ } catch (error) {
242
+ console.error('Failed to fetch file content:', error);
243
+ } finally {
244
+ setPreviewLoading(false);
245
+ }
246
+ }
247
+ };
248
+
249
+ const handleDelete = async (file: FileItem) => {
250
+ if (!confirm(`确定删除文件 "${file.title}" 吗?`)) return;
251
+
252
+ try {
253
+ const res = await fetch(`/api/my/files?userId=${userId}&fileId=${file.id}`, {
254
+ method: 'DELETE',
255
+ });
256
+
257
+ if (!res.ok) {
258
+ const error = await res.json();
259
+ alert(error.error || '删除失败');
260
+ return;
261
+ }
262
+
263
+ setFiles(prev => prev.filter(f => f.id !== file.id));
264
+ } catch (error) {
265
+ console.error('Delete error:', error);
266
+ alert('删除失败');
267
+ }
268
+ };
269
+
270
+ const handleTogglePublic = async (file: FileItem) => {
271
+ try {
272
+ const res = await fetch('/api/my/files', {
273
+ method: 'PATCH',
274
+ headers: { 'Content-Type': 'application/json' },
275
+ body: JSON.stringify({
276
+ userId,
277
+ fileId: file.id,
278
+ isPublic: !file.isPublic,
279
+ }),
280
+ });
281
+
282
+ if (!res.ok) {
283
+ const error = await res.json();
284
+ alert(error.error || '更新失败');
285
+ return;
286
+ }
287
+
288
+ setFiles(prev => prev.map(f =>
289
+ f.id === file.id ? { ...f, isPublic: !f.isPublic } : f
290
+ ));
291
+ } catch (error) {
292
+ console.error('Update error:', error);
293
+ alert('更新失败');
294
+ }
295
+ };
296
+
297
+ const handleCopyShareLink = async (file: FileItem) => {
298
+ const shareUrl = `${window.location.origin}/share/file/${file.id}`;
299
+ await navigator.clipboard.writeText(shareUrl);
300
+ setShareCopied(true);
301
+ setTimeout(() => setShareCopied(false), 2000);
302
+ };
303
+
304
+ return (
305
+ <div className="h-full overflow-auto bg-gray-50">
306
+ <div className="max-w-6xl mx-auto p-6">
307
+ <div className="flex items-center justify-between mb-6">
308
+ <div>
309
+ <h1 className="text-2xl font-bold text-gray-900">我的文件</h1>
310
+ <p className="text-sm text-gray-500 mt-1">
311
+ 共 {total} 个文件,总大小 {totalSize}
312
+ </p>
313
+ </div>
314
+ <div className="flex items-center gap-3">
315
+ <div className="relative">
316
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
317
+ <Input
318
+ placeholder="搜索文件..."
319
+ value={searchQuery}
320
+ onChange={(e) => handleSearch(e.target.value)}
321
+ className="pl-10 w-64"
322
+ />
323
+ </div>
324
+ <Button onClick={() => fileInputRef.current?.click()} disabled={uploading}>
325
+ {uploading ? (
326
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
327
+ ) : (
328
+ <Upload className="h-4 w-4 mr-2" />
329
+ )}
330
+ {uploading ? '上传中...' : '上传文件'}
331
+ </Button>
332
+ <input
333
+ ref={fileInputRef}
334
+ type="file"
335
+ className="hidden"
336
+ onChange={handleFileSelect}
337
+ />
338
+ </div>
339
+ </div>
340
+
341
+ <div
342
+ className={cn(
343
+ "border-2 border-dashed rounded-lg p-8 mb-6 text-center transition-colors",
344
+ dragOver ? "border-primary bg-primary/5" : "border-gray-200"
345
+ )}
346
+ onDrop={handleDrop}
347
+ onDragOver={handleDragOver}
348
+ onDragLeave={handleDragLeave}
349
+ >
350
+ <HardDrive className="h-12 w-12 mx-auto text-gray-400 mb-3" />
351
+ <p className="text-gray-600 mb-1">拖拽文件到此处上传</p>
352
+ <p className="text-sm text-gray-400">支持图片、文档、代码等,最大 50MB</p>
353
+ </div>
354
+
355
+ {loading ? (
356
+ <div className="flex items-center justify-center py-12">
357
+ <Loader2 className="h-8 w-8 animate-spin text-gray-400" />
358
+ </div>
359
+ ) : files.length === 0 ? (
360
+ <Card>
361
+ <CardContent className="py-12 text-center">
362
+ <FileIcon className="h-12 w-12 mx-auto text-gray-400 mb-3" />
363
+ <p className="text-gray-600">暂无文件</p>
364
+ <p className="text-sm text-gray-400 mt-1">点击上方按钮或拖拽文件上传</p>
365
+ </CardContent>
366
+ </Card>
367
+ ) : (
368
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
369
+ {files.map((file) => {
370
+ const Icon = file.fileMetadata ? getFileIcon(file.fileMetadata.mimeType) : File;
371
+ const category = file.fileMetadata ? getMimeTypeCategory(file.fileMetadata.mimeType) : '文件';
372
+
373
+ return (
374
+ <Card key={file.id} className="hover:shadow-md transition-shadow">
375
+ <CardContent className="p-4">
376
+ <div className="flex items-start gap-3">
377
+ <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
378
+ <Icon className="h-6 w-6 text-primary" />
379
+ </div>
380
+ <div className="flex-1 min-w-0">
381
+ <div className="flex items-center gap-2">
382
+ <p className="font-medium truncate" title={file.title}>
383
+ {file.title}
384
+ </p>
385
+ {file.isPublic && (
386
+ <Badge variant="secondary" className="text-xs">公开</Badge>
387
+ )}
388
+ </div>
389
+ <div className="flex items-center gap-2 mt-1 text-xs text-gray-500">
390
+ <Badge variant="outline" className="text-xs">{category}</Badge>
391
+ <span>{formatFileSize(file.fileMetadata?.size || 0)}</span>
392
+ </div>
393
+ <p className="text-xs text-gray-400 mt-1">
394
+ {formatDate(file.createdAt)}
395
+ </p>
396
+ </div>
397
+ </div>
398
+
399
+ <div className="flex items-center gap-2 mt-4 pt-3 border-t">
400
+ <Button
401
+ variant="ghost"
402
+ size="sm"
403
+ onClick={() => handlePreview(file)}
404
+ className="flex-1"
405
+ >
406
+ <Eye className="h-4 w-4 mr-1" />
407
+ 预览
408
+ </Button>
409
+ <Button
410
+ variant="ghost"
411
+ size="sm"
412
+ onClick={() => handleDownload(file)}
413
+ className="flex-1"
414
+ >
415
+ <Download className="h-4 w-4 mr-1" />
416
+ 下载
417
+ </Button>
418
+ <Button
419
+ variant="ghost"
420
+ size="sm"
421
+ onClick={() => handleTogglePublic(file)}
422
+ title={file.isPublic ? '设为私密' : '设为公开'}
423
+ >
424
+ <Share2 className={cn("h-4 w-4", file.isPublic && "text-primary")} />
425
+ </Button>
426
+ <Button
427
+ variant="ghost"
428
+ size="sm"
429
+ onClick={() => handleDelete(file)}
430
+ className="text-destructive hover:text-destructive"
431
+ >
432
+ <Trash2 className="h-4 w-4" />
433
+ </Button>
434
+ </div>
435
+ </CardContent>
436
+ </Card>
437
+ );
438
+ })}
439
+ </div>
440
+ )}
441
+ </div>
442
+
443
+ <Dialog open={!!previewFile} onOpenChange={() => { setPreviewFile(null); setPreviewContent(''); }}>
444
+ <DialogContent className="max-w-2xl">
445
+ <DialogHeader>
446
+ <DialogTitle className="flex items-center gap-2">
447
+ {previewFile?.fileMetadata && (
448
+ <>
449
+ {(() => {
450
+ const Icon = getFileIcon(previewFile.fileMetadata.mimeType);
451
+ return <Icon className="h-5 w-5" />;
452
+ })()}
453
+ </>
454
+ )}
455
+ {previewFile?.title}
456
+ </DialogTitle>
457
+ <DialogDescription>
458
+ {previewFile?.fileMetadata && (
459
+ <span>
460
+ {getMimeTypeCategory(previewFile.fileMetadata.mimeType)} · {formatFileSize(previewFile.fileMetadata.size)}
461
+ </span>
462
+ )}
463
+ </DialogDescription>
464
+ </DialogHeader>
465
+
466
+ {previewFile?.fileMetadata?.mimeType.startsWith('image/') && (
467
+ <div className="relative bg-gray-100 rounded-lg overflow-hidden">
468
+ <img
469
+ src={`/api/my/files/${previewFile.id}?userId=${userId}`}
470
+ alt={previewFile.title}
471
+ className="max-w-full max-h-96 mx-auto object-contain"
472
+ />
473
+ </div>
474
+ )}
475
+
476
+ {previewLoading && (
477
+ <div className="flex items-center justify-center py-8">
478
+ <Loader2 className="h-6 w-6 animate-spin text-gray-400" />
479
+ </div>
480
+ )}
481
+
482
+ {!previewLoading && previewContent && (
483
+ <div className="bg-gray-50 rounded-lg p-4 max-h-80 overflow-auto">
484
+ <pre className="text-sm whitespace-pre-wrap font-mono">{previewContent}</pre>
485
+ </div>
486
+ )}
487
+
488
+ <div className="flex items-center justify-between pt-4 border-t">
489
+ <div className="text-sm text-gray-500">
490
+ {previewFile?.isPublic ? (
491
+ <Badge variant="secondary">公开分享</Badge>
492
+ ) : (
493
+ <Badge variant="outline">私密</Badge>
494
+ )}
495
+ </div>
496
+ <div className="flex items-center gap-2">
497
+ <Button variant="outline" size="sm" onClick={() => previewFile && handleCopyShareLink(previewFile)}>
498
+ {shareCopied ? <Check className="h-4 w-4 mr-1" /> : <Copy className="h-4 w-4 mr-1" />}
499
+ {shareCopied ? '已复制' : '复制分享链接'}
500
+ </Button>
501
+ <Button size="sm" onClick={() => previewFile && handleDownload(previewFile)}>
502
+ <Download className="h-4 w-4 mr-1" />
503
+ 下载
504
+ </Button>
505
+ </div>
506
+ </div>
507
+ </DialogContent>
508
+ </Dialog>
509
+ </div>
510
+ );
511
+ }