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,358 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Search, Star, Download, ArrowLeft, BookOpen, Eye, X, Trash2, Loader2 } from 'lucide-react';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Input } from '@/components/ui/input';
10
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
11
+ import { ScrollArea } from '@/components/ui/scroll-area';
12
+ import { Alert, AlertDescription } from '@/components/ui/alert';
13
+
14
+ interface MarketKnowledge {
15
+ id: string;
16
+ title: string;
17
+ content: string;
18
+ category?: string;
19
+ tags?: string[] | string;
20
+ author?: string;
21
+ views: number;
22
+ rating: number;
23
+ ratingCount: number;
24
+ createdAt: string;
25
+ }
26
+
27
+ function parseTags(tags: string[] | string | undefined): string[] {
28
+ if (!tags) return [];
29
+ if (Array.isArray(tags)) return tags;
30
+ try {
31
+ const parsed = JSON.parse(tags);
32
+ return Array.isArray(parsed) ? parsed : [];
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ function SimpleMarkdown({ content }: { content: string }) {
39
+ const lines = content.split('\n');
40
+
41
+ return (
42
+ <div className="space-y-2">
43
+ {lines.map((line, i) => {
44
+ const trimmed = line.trim();
45
+
46
+ if (trimmed.startsWith('# ')) {
47
+ return <h1 key={i} className="text-2xl font-bold mt-4 mb-2">{trimmed.slice(2)}</h1>;
48
+ }
49
+ if (trimmed.startsWith('## ')) {
50
+ return <h2 key={i} className="text-xl font-bold mt-4 mb-2">{trimmed.slice(3)}</h2>;
51
+ }
52
+ if (trimmed.startsWith('### ')) {
53
+ return <h3 key={i} className="text-lg font-semibold mt-3 mb-1">{trimmed.slice(4)}</h3>;
54
+ }
55
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
56
+ return <li key={i} className="ml-4">{trimmed.slice(2)}</li>;
57
+ }
58
+ if (trimmed.match(/^\d+\.\s/)) {
59
+ return <li key={i} className="ml-4 list-decimal">{trimmed.replace(/^\d+\.\s/, '')}</li>;
60
+ }
61
+ if (trimmed.startsWith('```')) {
62
+ return null;
63
+ }
64
+ if (trimmed === '') {
65
+ return <br key={i} />;
66
+ }
67
+
68
+ let processed = trimmed
69
+ .replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1 rounded text-sm">$1</code>')
70
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
71
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>');
72
+
73
+ return (
74
+ <p
75
+ key={i}
76
+ className="text-sm text-gray-700"
77
+ dangerouslySetInnerHTML={{ __html: processed }}
78
+ />
79
+ );
80
+ })}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ export default function MarketKnowledgePage() {
86
+ const router = useRouter();
87
+ const [knowledge, setKnowledge] = useState<MarketKnowledge[]>([]);
88
+ const [loading, setLoading] = useState(true);
89
+ const [search, setSearch] = useState('');
90
+ const [selectedKnowledge, setSelectedKnowledge] = useState<MarketKnowledge | null>(null);
91
+ const [userId, setUserId] = useState<string>('');
92
+ const [userRole, setUserRole] = useState<'admin' | 'guest'>('guest');
93
+ const [deletingId, setDeletingId] = useState<string | null>(null);
94
+ const [alertMessage, setAlertMessage] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
95
+
96
+ useEffect(() => {
97
+ const uid = localStorage.getItem('userId');
98
+ if (!uid) {
99
+ router.push('/login');
100
+ return;
101
+ }
102
+ setUserId(uid);
103
+
104
+ const role = localStorage.getItem('role') as 'admin' | 'guest' | null;
105
+ setUserRole(role || 'guest');
106
+
107
+ fetchKnowledge();
108
+ }, [router]);
109
+
110
+ const fetchKnowledge = async () => {
111
+ try {
112
+ const res = await fetch(`/api/market/knowledge?search=${encodeURIComponent(search)}`);
113
+
114
+ if (!res.ok) {
115
+ const error = await res.json();
116
+
117
+ // 显示明确的错误信息
118
+ if (error.code === 'MARKET_UNAVAILABLE') {
119
+ setAlertMessage({ type: 'error', message: error.error || '远程市场服务不可用,请联系系统管理员' });
120
+ setKnowledge([]);
121
+ } else {
122
+ throw new Error(error.error || '获取市场知识失败');
123
+ }
124
+ return;
125
+ }
126
+
127
+ const data = await res.json();
128
+ setKnowledge(data.knowledge || []);
129
+ setAlertMessage(null); // 清除错误提示
130
+ } catch (error: any) {
131
+ console.error('Failed to fetch knowledge:', error);
132
+ setAlertMessage({ type: 'error', message: error.message || '获取市场知识失败' });
133
+ setKnowledge([]);
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ };
138
+
139
+ const handleSearch = (e: React.FormEvent) => {
140
+ e.preventDefault();
141
+ setLoading(true);
142
+ fetchKnowledge();
143
+ };
144
+
145
+ const handleView = (item: MarketKnowledge) => {
146
+ setSelectedKnowledge(item);
147
+ };
148
+
149
+ const handleDelete = async (id: string, title: string) => {
150
+ if (!userId || userRole !== 'admin') {
151
+ setAlertMessage({ type: 'error', message: '需要管理员权限' });
152
+ return;
153
+ }
154
+
155
+ if (!confirm(`确定要从市场中移除知识 "${title}" 吗?\n\n注意:这只会从市场中移除,不会删除您的本地知识。`)) {
156
+ return;
157
+ }
158
+
159
+ setDeletingId(id);
160
+ setAlertMessage(null);
161
+
162
+ try {
163
+ // 调用市场移除 API(不删除本地知识)
164
+ const res = await fetch(`/api/market/knowledge/${id}?userId=${userId}&role=${userRole}`, {
165
+ method: 'DELETE',
166
+ });
167
+
168
+ if (res.ok) {
169
+ setAlertMessage({ type: 'success', message: '已从市场中移除知识' });
170
+ setKnowledge(prev => prev.filter(k => k.id !== id));
171
+ if (selectedKnowledge?.id === id) {
172
+ setSelectedKnowledge(null);
173
+ }
174
+ } else {
175
+ const data = await res.json();
176
+ setAlertMessage({ type: 'error', message: data.error || '从市场中移除失败' });
177
+ }
178
+ } catch (error) {
179
+ console.error('Failed to remove from market:', error);
180
+ setAlertMessage({ type: 'error', message: '从市场中移除失败' });
181
+ } finally {
182
+ setDeletingId(null);
183
+ }
184
+ };
185
+
186
+ return (
187
+ <div className="min-h-screen bg-gray-50">
188
+ {/* Header */}
189
+ <header className="bg-white border-b">
190
+ <div className="max-w-6xl mx-auto px-4 py-4">
191
+ <div className="flex items-center gap-4">
192
+ <div className="flex items-center gap-2">
193
+ <BookOpen className="w-6 h-6 text-green-600" />
194
+ <h1 className="text-xl font-bold">知识市场</h1>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </header>
199
+
200
+ <main className="max-w-6xl mx-auto px-4 py-6">
201
+ {/* Alert Message */}
202
+ {alertMessage && (
203
+ <Alert
204
+ variant={alertMessage.type === 'error' ? 'destructive' : 'default'}
205
+ className="mb-4"
206
+ >
207
+ <AlertDescription>{alertMessage.message}</AlertDescription>
208
+ </Alert>
209
+ )}
210
+
211
+ {/* Search */}
212
+ <form onSubmit={handleSearch} className="mb-6">
213
+ <div className="flex gap-2">
214
+ <div className="relative flex-1">
215
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
216
+ <Input
217
+ type="text"
218
+ placeholder="搜索知识..."
219
+ value={search}
220
+ onChange={(e) => setSearch(e.target.value)}
221
+ className="pl-10"
222
+ />
223
+ </div>
224
+ <Button type="submit">搜索</Button>
225
+ </div>
226
+ </form>
227
+
228
+ {/* Stats */}
229
+ <div className="grid grid-cols-2 gap-4 mb-6">
230
+ <Card>
231
+ <CardContent className="p-4">
232
+ <div className="text-2xl font-bold">{knowledge.length}</div>
233
+ <div className="text-sm text-gray-500">市场知识</div>
234
+ </CardContent>
235
+ </Card>
236
+ <Card>
237
+ <CardContent className="p-4">
238
+ <div className="text-2xl font-bold">
239
+ {knowledge.reduce((sum, k) => sum + (Number(k.views) || 0), 0)}
240
+ </div>
241
+ <div className="text-sm text-gray-500">总浏览</div>
242
+ </CardContent>
243
+ </Card>
244
+ </div>
245
+
246
+ {/* Knowledge Grid */}
247
+ {loading ? (
248
+ <div className="text-center py-12 text-gray-500">加载中...</div>
249
+ ) : knowledge.length === 0 ? (
250
+ <div className="text-center py-12">
251
+ <BookOpen className="w-12 h-12 mx-auto text-gray-300 mb-4" />
252
+ <p className="text-gray-500">暂无知识</p>
253
+ <p className="text-sm text-gray-400 mt-1">
254
+ 可以在"我的知识"中创建并发布
255
+ </p>
256
+ </div>
257
+ ) : (
258
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
259
+ {knowledge.map((item) => (
260
+ <Card key={item.id} className="hover:shadow-md transition-shadow">
261
+ <CardHeader className="pb-3">
262
+ <CardTitle className="text-lg line-clamp-1">{item.title}</CardTitle>
263
+ <CardDescription className="text-xs">
264
+ by {item.author || '匿名'}
265
+ </CardDescription>
266
+ </CardHeader>
267
+ <CardContent>
268
+ <p className="text-sm text-gray-600 mb-3 line-clamp-3">
269
+ {item.content?.substring(0, 150)}...
270
+ </p>
271
+
272
+ {parseTags(item.tags).length > 0 && (
273
+ <div className="flex flex-wrap gap-1 mb-3">
274
+ {parseTags(item.tags).slice(0, 3).map((tag: string) => (
275
+ <Badge key={tag} variant="outline" className="text-xs">
276
+ {tag}
277
+ </Badge>
278
+ ))}
279
+ </div>
280
+ )}
281
+
282
+ <div className="flex items-center justify-between">
283
+ <div className="flex items-center gap-3 text-sm text-gray-500">
284
+ <span className="flex items-center gap-1">
285
+ <Eye className="w-4 h-4" />
286
+ {item.views}
287
+ </span>
288
+ <span className="flex items-center gap-1">
289
+ <Star className="w-4 h-4 text-yellow-500" />
290
+ {item.rating > 0 ? item.rating.toFixed(1) : '-'}
291
+ </span>
292
+ </div>
293
+ <div className="flex gap-2">
294
+ {userRole === 'admin' && (
295
+ <Button
296
+ variant="outline"
297
+ size="sm"
298
+ className="text-red-600 border-red-600 hover:bg-red-50"
299
+ onClick={() => handleDelete(item.id, item.title)}
300
+ disabled={deletingId === item.id}
301
+ title="删除知识"
302
+ >
303
+ {deletingId === item.id ? (
304
+ <Loader2 className="w-4 h-4 animate-spin" />
305
+ ) : (
306
+ <Trash2 className="w-4 h-4" />
307
+ )}
308
+ </Button>
309
+ )}
310
+ <Button
311
+ variant="outline"
312
+ size="sm"
313
+ onClick={() => handleView(item)}
314
+ >
315
+ 查看
316
+ </Button>
317
+ </div>
318
+ </div>
319
+ </CardContent>
320
+ </Card>
321
+ ))}
322
+ </div>
323
+ )}
324
+ </main>
325
+
326
+ {/* View Dialog */}
327
+ <Dialog open={!!selectedKnowledge} onOpenChange={(open) => !open && setSelectedKnowledge(null)}>
328
+ <DialogContent className="max-w-3xl max-h-[80vh]">
329
+ <DialogHeader>
330
+ <DialogTitle className="text-xl">{selectedKnowledge?.title}</DialogTitle>
331
+ <div className="flex items-center gap-4 text-sm text-gray-500 mt-2">
332
+ <span>by {selectedKnowledge?.author || '匿名'}</span>
333
+ <span className="flex items-center gap-1">
334
+ <Eye className="w-4 h-4" />
335
+ {selectedKnowledge?.views}
336
+ </span>
337
+ <span>{selectedKnowledge?.createdAt?.split('T')[0]}</span>
338
+ </div>
339
+ {parseTags(selectedKnowledge?.tags).length > 0 && (
340
+ <div className="flex flex-wrap gap-1 mt-2">
341
+ {parseTags(selectedKnowledge?.tags).map((tag: string) => (
342
+ <Badge key={tag} variant="outline">
343
+ {tag}
344
+ </Badge>
345
+ ))}
346
+ </div>
347
+ )}
348
+ </DialogHeader>
349
+ <ScrollArea className="h-[60vh] pr-4">
350
+ <div className="prose prose-sm max-w-none">
351
+ {selectedKnowledge && <SimpleMarkdown content={selectedKnowledge.content} />}
352
+ </div>
353
+ </ScrollArea>
354
+ </DialogContent>
355
+ </Dialog>
356
+ </div>
357
+ );
358
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { Package, BookOpen } from 'lucide-react';
4
+ import { SubNav, type NavItem } from '@/components/sub-nav';
5
+
6
+ const marketNavItems: NavItem[] = [
7
+ { href: '/market/knowledge', label: '知识', icon: BookOpen },
8
+ { href: '/market/skills', label: 'Skill', icon: Package },
9
+ ];
10
+
11
+ export default function MarketLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <div className="flex min-h-[calc(100vh-3rem)] bg-gray-50">
18
+ <div className="w-48 flex-shrink-0 border-r bg-white">
19
+ <div className="py-4 px-3 border-b">
20
+ <h2 className="text-sm font-semibold text-gray-900">市场</h2>
21
+ </div>
22
+ <SubNav items={marketNavItems} />
23
+ </div>
24
+ <main className="flex-1 overflow-auto">
25
+ {children}
26
+ </main>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ export default function MarketIndex() {
7
+ const router = useRouter();
8
+
9
+ useEffect(() => {
10
+ router.replace('/market/knowledge');
11
+ }, [router]);
12
+
13
+ return (
14
+ <div className="flex items-center justify-center h-full">
15
+ <div className="text-muted-foreground">加载中...</div>
16
+ </div>
17
+ );
18
+ }