2
0

json-render-registry.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. /**
  2. * json-render 注册表
  3. *
  4. * 创建组件注册表,将 Schema 定义与 React 组件绑定
  5. * 用于 Renderer 的自动解析和渲染
  6. */
  7. 'use client';
  8. import { useState } from 'react';
  9. // ============ React 组件实现 ============
  10. // Card 组件
  11. const Card = ({ title, children, className, renderChildren, emit }: any) => (
  12. <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 shadow-sm p-4 ${className || ''}`}>
  13. {title && <h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{title}</h3>}
  14. {renderChildren ? renderChildren(children, emit) : null}
  15. </div>
  16. );
  17. // Stack 组件
  18. const Stack = ({ direction = 'column', spacing = 2, align = 'start', children, className, renderChildren, emit }: any) => {
  19. const directionClass = direction === 'row' ? 'flex-row' : 'flex-col';
  20. const spacingClass = spacing > 0 ? `gap-${spacing}` : '';
  21. const alignClass = align === 'center' ? 'items-center' : align === 'end' ? 'items-end' : align === 'stretch' ? 'items-stretch' : 'items-start';
  22. return (
  23. <div className={`flex ${directionClass} ${spacingClass} ${alignClass} ${className || ''}`}>
  24. {renderChildren ? renderChildren(children, emit) : null}
  25. </div>
  26. );
  27. };
  28. // Heading 组件
  29. const Heading = ({ level = 'h2', text, className }: any) => {
  30. const Tag = level;
  31. const sizeClasses: Record<string, string> = {
  32. h1: 'text-3xl font-bold',
  33. h2: 'text-2xl font-semibold',
  34. h3: 'text-xl font-semibold',
  35. h4: 'text-lg font-medium',
  36. h5: 'text-base font-medium',
  37. h6: 'text-sm font-medium',
  38. };
  39. const sizeClass = sizeClasses[level] || sizeClasses.h2;
  40. return <Tag className={`${sizeClass} text-gray-900 dark:text-white ${className || ''}`}>{text}</Tag>;
  41. };
  42. // Text 组件
  43. const Text = ({ content, variant = 'body', className }: any) => {
  44. const variantClasses: Record<string, string> = {
  45. body: 'text-gray-700 dark:text-gray-300',
  46. muted: 'text-gray-500 dark:text-gray-400',
  47. code: 'font-mono text-sm bg-gray-100 dark:bg-gray-900 px-1 py-0.5 rounded',
  48. };
  49. const variantClass = variantClasses[variant] || variantClasses.body;
  50. return <p className={`${variantClass} ${className || ''}`}>{content}</p>;
  51. };
  52. // Button 组件 - 支持交互事件
  53. const Button = ({ label, variant = 'default', onClick, action, actionPayload, disabled = false, className, emit }: any) => {
  54. const variantClasses: Record<string, string> = {
  55. default: 'bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-200',
  56. primary: 'bg-blue-600 hover:bg-blue-700 text-white',
  57. secondary: 'bg-purple-600 hover:bg-purple-700 text-white',
  58. ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
  59. danger: 'bg-red-600 hover:bg-red-700 text-white',
  60. };
  61. const variantClass = variantClasses[variant] || variantClasses.default;
  62. const handleClick = () => {
  63. if (disabled) return;
  64. // 优先使用 action 事件(emit)
  65. if (action && emit) {
  66. emit(action, actionPayload);
  67. return;
  68. }
  69. // 回退到 onClick 代码执行
  70. if (onClick) {
  71. try {
  72. // 安全执行 onClick 代码
  73. const fn = new Function('event', onClick);
  74. fn(new Event('click'));
  75. } catch (e) {
  76. console.error('Error executing onClick:', e);
  77. }
  78. }
  79. };
  80. return (
  81. <button
  82. onClick={handleClick}
  83. disabled={disabled}
  84. className={`${variantClass} px-4 py-2 rounded-md font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className || ''}`}
  85. >
  86. {label}
  87. </button>
  88. );
  89. };
  90. // Input 组件
  91. const Input = ({ placeholder, value, onChange, disabled = false, className, type = 'text' }: any) => {
  92. const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  93. if (onChange) {
  94. try {
  95. const fn = new Function('event', onChange);
  96. fn(e);
  97. } catch (err) {
  98. console.error('Error executing onChange:', err);
  99. }
  100. }
  101. };
  102. return (
  103. <input
  104. type={type}
  105. placeholder={placeholder}
  106. defaultValue={value}
  107. onChange={handleChange}
  108. disabled={disabled}
  109. className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 ${className || ''}`}
  110. />
  111. );
  112. };
  113. // Badge 组件
  114. const Badge = ({ text, variant = 'default', className }: any) => {
  115. const variantClasses: Record<string, string> = {
  116. default: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
  117. success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200',
  118. warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200',
  119. error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200',
  120. info: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200',
  121. };
  122. const variantClass = variantClasses[variant] || variantClasses.default;
  123. return (
  124. <span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${variantClass} ${className || ''}`}>
  125. {text}
  126. </span>
  127. );
  128. };
  129. // Separator 组件
  130. const Separator = ({ orientation = 'horizontal', className }: any) => {
  131. const orientationClass = orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full';
  132. return <div className={`bg-gray-200 dark:bg-gray-700 ${orientationClass} ${className || ''}`} />;
  133. };
  134. // ============ MCP 专用组件实现 ============
  135. // TranslationResult 组件 - 翻译结果展示(支持 emit 事件)
  136. const TranslationResult = ({ translated, termsUsed, className, emit }: any) => {
  137. const [copied, setCopied] = useState(false);
  138. const handleCopy = () => {
  139. // 使用 emit 触发复制事件(如果可用),否则直接调用 clipboard API
  140. if (emit) {
  141. emit('copy', { text: translated });
  142. } else {
  143. navigator.clipboard.writeText(translated);
  144. }
  145. setCopied(true);
  146. setTimeout(() => setCopied(false), 2000);
  147. };
  148. return (
  149. <div className={`translation-result bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${className || ''}`}>
  150. {/* 头部 */}
  151. <div className="bg-gradient-to-r from-blue-500 to-purple-500 px-4 py-3 flex items-center gap-2">
  152. <span className="text-xl">🌐</span>
  153. <h4 className="font-semibold text-white">翻译结果</h4>
  154. </div>
  155. {/* 内容区域 */}
  156. <div className="p-4">
  157. {/* 译文 */}
  158. <div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg p-3 border border-blue-100 dark:border-blue-800/50 mb-3">
  159. <div className="flex items-center gap-2 mb-2">
  160. <span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wide">译文</span>
  161. </div>
  162. <p className="text-gray-900 dark:text-gray-100 font-medium leading-relaxed">{translated}</p>
  163. </div>
  164. {/* 使用的术语 */}
  165. {termsUsed && termsUsed.length > 0 && (
  166. <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3">
  167. <div className="flex items-center gap-2 mb-2">
  168. <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">使用术语</span>
  169. </div>
  170. <div className="flex flex-wrap gap-2">
  171. {termsUsed.map((term: string, idx: number) => (
  172. <span key={idx} className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200">
  173. {term}
  174. </span>
  175. ))}
  176. </div>
  177. </div>
  178. )}
  179. </div>
  180. {/* 底部操作栏 */}
  181. <div className="px-4 py-3 bg-gray-50 dark:bg-gray-900/30 border-t dark:border-gray-700 flex gap-2">
  182. <button
  183. onClick={handleCopy}
  184. className="flex-1 flex items-center justify-center gap-1 px-3 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
  185. >
  186. <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  187. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
  188. </svg>
  189. <span>{copied ? '已复制!' : '复制译文'}</span>
  190. </button>
  191. </div>
  192. </div>
  193. );
  194. };
  195. // NovelList 组件 - 支持点击交互
  196. const NovelList = ({ novels, className, emit }: any) => (
  197. <div className={`novel-list space-y-3 ${className || ''}`}>
  198. {novels.map((novel: any) => (
  199. <div
  200. key={novel.id}
  201. onClick={() => emit && emit('selectNovel', { id: novel.id, title: novel.title, ...novel })}
  202. className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all cursor-pointer"
  203. >
  204. <h4 className="font-semibold text-lg text-gray-900 dark:text-white mb-1">{novel.title}</h4>
  205. {novel.author && <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">By {novel.author}</p>}
  206. {novel.description && <p className="text-sm text-gray-600 dark:text-gray-300 mb-2 line-clamp-2">{novel.description}</p>}
  207. <div className="flex items-center gap-2">
  208. {novel.chapterCount && <Badge text={`${novel.chapterCount} chapters`} variant="info" />}
  209. {novel.tags?.map((tag: string) => (
  210. <Badge key={tag} text={tag} variant="default" />
  211. ))}
  212. </div>
  213. </div>
  214. ))}
  215. </div>
  216. );
  217. // ChapterReader 组件
  218. const ChapterReader = ({ novelTitle, chapterTitle, content, chapterNumber, totalChapters, className }: any) => (
  219. <div className={`chapter-reader bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
  220. <div className="border-b dark:border-gray-700 pb-4 mb-4">
  221. <p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{novelTitle}</p>
  222. <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{chapterTitle}</h2>
  223. {chapterNumber && totalChapters && (
  224. <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
  225. Chapter {chapterNumber} of {totalChapters}
  226. </p>
  227. )}
  228. </div>
  229. <div className="prose dark:prose-invert max-w-none">
  230. <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">{content}</p>
  231. </div>
  232. </div>
  233. );
  234. // McpToolCall 组件
  235. const McpToolCall = ({ tool, status, args, result, error, className }: any) => {
  236. const statusConfig: Record<string, { icon: string; color: string; bg: string }> = {
  237. pending: { icon: '⏳', color: 'text-yellow-600', bg: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-300 dark:border-yellow-700' },
  238. running: { icon: '🔄', color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700' },
  239. success: { icon: '✅', color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-700' },
  240. error: { icon: '❌', color: 'text-red-600', bg: 'bg-red-50 dark:bg-red-900/20 border-red-300 dark:border-red-700' },
  241. };
  242. const config = statusConfig[status] || statusConfig.success;
  243. return (
  244. <div className={`mcp-tool-call border-l-4 p-3 rounded-r ${config.bg} ${className || ''}`}>
  245. <div className="flex items-center gap-2 mb-2">
  246. <span className="text-lg">{config.icon}</span>
  247. <span className={`font-mono text-sm font-medium ${config.color}`}>{tool}</span>
  248. </div>
  249. {args && (
  250. <div className="mt-2">
  251. <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Arguments:</p>
  252. <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
  253. {JSON.stringify(args, null, 2)}
  254. </pre>
  255. </div>
  256. )}
  257. {result && status === 'success' && (
  258. <div className="mt-2">
  259. <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Result:</p>
  260. <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
  261. {JSON.stringify(result, null, 2)}
  262. </pre>
  263. </div>
  264. )}
  265. {error && status === 'error' && (
  266. <div className="mt-2">
  267. <p className="text-xs text-red-600 dark:text-red-400 mb-1">Error:</p>
  268. <p className="text-sm text-red-700 dark:text-red-300">{error}</p>
  269. </div>
  270. )}
  271. </div>
  272. );
  273. };
  274. // LoginPanel 组件
  275. const LoginPanel = ({ server, email, className }: any) => (
  276. <div className={`login-panel bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
  277. <div className="text-center mb-6">
  278. <div className="text-4xl mb-2">🔐</div>
  279. <h3 className="text-xl font-semibold text-gray-900 dark:text-white">{server}</h3>
  280. <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to continue</p>
  281. </div>
  282. <form className="space-y-4">
  283. <div>
  284. <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
  285. <Input type="email" placeholder="your@email.com" defaultValue={email} />
  286. </div>
  287. <div>
  288. <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
  289. <Input type="password" placeholder="••••••••" />
  290. </div>
  291. <Button label="Sign In" variant="primary" className="w-full" />
  292. </form>
  293. </div>
  294. );
  295. // CodeBlock 组件
  296. const CodeBlock = ({ code, language = 'text', inline = false, className }: any) => {
  297. if (inline) {
  298. return (
  299. <code className={`bg-gray-100 dark:bg-gray-900 px-1.5 py-0.5 rounded text-sm font-mono text-gray-800 dark:text-gray-200 ${className || ''}`}>
  300. {code}
  301. </code>
  302. );
  303. }
  304. return (
  305. <div className={`code-block bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto ${className || ''}`}>
  306. <div className="flex items-center justify-between mb-2">
  307. <span className="text-xs text-gray-400 uppercase">{language}</span>
  308. <button
  309. onClick={() => navigator.clipboard.writeText(code)}
  310. className="text-xs text-gray-400 hover:text-white transition-colors"
  311. >
  312. Copy
  313. </button>
  314. </div>
  315. <pre className="text-sm text-gray-100 font-mono">
  316. <code>{code}</code>
  317. </pre>
  318. </div>
  319. );
  320. };
  321. // DataTable 组件
  322. const DataTable = ({ columns, rows, className }: any) => (
  323. <div className={`data-table overflow-x-auto ${className || ''}`}>
  324. <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
  325. <thead className="bg-gray-50 dark:bg-gray-800">
  326. <tr>
  327. {columns.map((col: any) => (
  328. <th key={col.key} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
  329. {col.label}
  330. </th>
  331. ))}
  332. </tr>
  333. </thead>
  334. <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
  335. {rows.map((row: any, idx: number) => (
  336. <tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
  337. {columns.map((col: any) => (
  338. <td key={col.key} className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
  339. {String(row[col.key] ?? '')}
  340. </td>
  341. ))}
  342. </tr>
  343. ))}
  344. </tbody>
  345. </table>
  346. </div>
  347. );
  348. // NovelDetail 组件 - 小说详情卡片
  349. const NovelDetail = ({ novel, className, emit }: any) => {
  350. if (!novel) return null;
  351. return (
  352. <div className={`novel-detail bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${className || ''}`}>
  353. {/* 头部 - 渐变背景 */}
  354. <div className="bg-gradient-to-r from-purple-500 to-pink-500 px-4 py-4">
  355. <h3 className="font-bold text-white text-xl">{novel.title || '未知标题'}</h3>
  356. {novel.isVip && (
  357. <span className="inline-block mt-1 px-2 py-0.5 bg-yellow-400 text-yellow-900 text-xs rounded-full font-medium">
  358. 👑 VIP
  359. </span>
  360. )}
  361. </div>
  362. {/* 内容区域 */}
  363. <div className="p-4 space-y-4">
  364. {/* 基本信息 */}
  365. <div>
  366. <h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-1">
  367. <span>📖</span> 基本信息
  368. </h4>
  369. <div className="space-y-1 text-sm text-gray-700 dark:text-gray-300">
  370. <p><span className="text-gray-500">作者:</span>{novel.author || '-'}</p>
  371. <p><span className="text-gray-500">类型:</span>{novel.category || '-'}</p>
  372. <p><span className="text-gray-500">简介:</span>{novel.description || '暂无简介'}</p>
  373. </div>
  374. </div>
  375. {/* 统计数据 */}
  376. <div>
  377. <h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-1">
  378. <span>📊</span> 统计数据
  379. </h4>
  380. <div className="grid grid-cols-2 gap-2">
  381. <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
  382. <span className="text-xs text-gray-500">状态</span>
  383. <p className="font-medium text-sm">{novel.status || '-'}</p>
  384. </div>
  385. <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
  386. <span className="text-xs text-gray-500">章节数</span>
  387. <p className="font-medium text-sm">{novel.chapterCount ?? '-'}章</p>
  388. </div>
  389. <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
  390. <span className="text-xs text-gray-500">总字数</span>
  391. <p className="font-medium text-sm">{novel.wordCount ?? '-'}字</p>
  392. </div>
  393. <div className="bg-gray-50 dark:bg-gray-900/50 p-2 rounded-lg">
  394. <span className="text-xs text-gray-500">阅读量</span>
  395. <p className="font-medium text-sm">{novel.viewCount ?? '-'}次</p>
  396. </div>
  397. </div>
  398. </div>
  399. </div>
  400. </div>
  401. );
  402. };
  403. // SuggestionButtons 组件 - 建议操作按钮组
  404. const SuggestionButtons = ({ suggestions, className, emit }: any) => {
  405. if (!suggestions || !Array.isArray(suggestions) || suggestions.length === 0) return null;
  406. return (
  407. <div className={`suggestion-buttons flex flex-wrap gap-2 ${className || ''}`}>
  408. {suggestions.map((suggestion: any, index: number) => {
  409. const handleClick = () => {
  410. if (emit && suggestion.message) {
  411. emit('sendMessage', { message: suggestion.message });
  412. }
  413. };
  414. return (
  415. <button
  416. key={index}
  417. onClick={handleClick}
  418. className="px-4 py-2 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors text-sm font-medium cursor-pointer"
  419. >
  420. {suggestion.icon && <span className="mr-1">{suggestion.icon}</span>}
  421. {suggestion.label || suggestion.message}
  422. </button>
  423. );
  424. })}
  425. </div>
  426. );
  427. };
  428. // ============ 创建注册表 ============
  429. // 简化的组件注册表 - 直接映射组件名称到 React 组件
  430. export const jsonRenderRegistry: Record<string, React.ComponentType<any>> = {
  431. // 基础组件
  432. card: Card,
  433. stack: Stack,
  434. heading: Heading,
  435. text: Text,
  436. button: Button,
  437. input: Input,
  438. badge: Badge,
  439. separator: Separator,
  440. // MCP 专用组件
  441. 'translation-result': TranslationResult,
  442. 'novel-list': NovelList,
  443. 'novel-detail': NovelDetail,
  444. 'chapter-reader': ChapterReader,
  445. 'mcp-tool-call': McpToolCall,
  446. 'login-panel': LoginPanel,
  447. 'code-block': CodeBlock,
  448. 'data-table': DataTable,
  449. 'suggestion-buttons': SuggestionButtons,
  450. };
  451. // ============ 辅助函数 ============
  452. /**
  453. * 递归渲染子组件
  454. * 用于 Card 和 Stack 组件中渲染 children 数组
  455. * @param children - 子组件数组
  456. * @param emit - ActionContext emit 函数(用于组件交互)
  457. */
  458. export function renderChildren(children: any, emit?: (eventName: string, payload?: any) => void): React.ReactNode {
  459. if (!children) return null;
  460. if (!Array.isArray(children)) return null;
  461. return children.map((child: any, idx: number) => {
  462. if (!child || typeof child !== 'object') return null;
  463. if (!child.type) return <span key={idx}>{String(child)}</span>;
  464. const componentType = child.type;
  465. const Component = (jsonRenderRegistry as any)[componentType];
  466. if (Component) {
  467. // 合并 props:原始 child props + emit 函数
  468. const props: any = { ...child };
  469. if (emit) {
  470. props.emit = emit;
  471. }
  472. // 如果组件需要渲染子组件,传入 renderChildren 函数(并传递 emit)
  473. if (componentType === 'card' || componentType === 'stack') {
  474. props.renderChildren = (grandchildren: any) => renderChildren(grandchildren, emit);
  475. }
  476. return <Component key={idx} {...props} />;
  477. }
  478. return (
  479. <div key={idx} className="p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded border border-yellow-200 dark:border-yellow-800">
  480. <p className="text-sm text-yellow-700 dark:text-yellow-300">Unknown component: {componentType}</p>
  481. </div>
  482. );
  483. });
  484. }
  485. // 导出类型
  486. export type { ComponentSpec } from './json-render-catalog';