2
0

JsonRenderer.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. /**
  2. * JsonRenderer - 使用官方 @json-render/react API 的渲染器
  3. *
  4. * 功能:
  5. * - 使用官方 Renderer 组件渲染 json-render spec
  6. * - 支持单个 spec 和 specs 数组
  7. * - 支持流式渲染(SSE)
  8. * - 兼容旧格式的 ComponentSpec
  9. *
  10. * 使用方式:
  11. * ```tsx
  12. * // 渲染单个 spec(旧格式)
  13. * <JsonRenderer spec={{ type: 'card', title: 'Hello' }} />
  14. *
  15. * // 渲染多个 specs(旧格式)
  16. * <JsonRenderer specs={[spec1, spec2]} />
  17. *
  18. * // 渲染官方格式 spec
  19. * <JsonRenderer spec={officialSpec} />
  20. * ```
  21. */
  22. 'use client';
  23. import { useMemo } from 'react';
  24. import {
  25. Renderer,
  26. JSONUIProvider,
  27. type Spec,
  28. } from '@json-render/react';
  29. import type { UIElement } from '@json-render/core';
  30. import { registry } from '@/lib/registry';
  31. // ============ Types ============
  32. /**
  33. * 旧格式组件 spec 类型
  34. * 旧格式: { type: 'card', title: '...', children: [...] }
  35. */
  36. export interface LegacyComponentSpec {
  37. type: string;
  38. children?: LegacyComponentSpec[];
  39. [key: string]: unknown;
  40. }
  41. /**
  42. * JsonRenderer Props
  43. */
  44. export interface JsonRendererProps {
  45. /** 单个组件 spec(支持旧格式和官方格式) */
  46. spec?: LegacyComponentSpec | Spec | null;
  47. /** 多个组件 spec 数组 */
  48. specs?: LegacyComponentSpec[] | Spec[];
  49. /** 可选样式类 */
  50. className?: string;
  51. /** 事件处理函数 */
  52. onAction?: (actionName: string, params?: Record<string, unknown>) => void;
  53. /** 是否正在加载 */
  54. loading?: boolean;
  55. }
  56. /**
  57. * StreamingJsonRenderer Props - 用于 SSE 流式渲染
  58. */
  59. export interface StreamingJsonRendererProps extends JsonRendererProps {
  60. /** 流式数据 */
  61. specs?: LegacyComponentSpec[];
  62. }
  63. // ============ Helper Functions ============
  64. /**
  65. * 生成唯一 key
  66. */
  67. let keyCounter = 0;
  68. function generateKey(): string {
  69. return `el_${++keyCounter}`;
  70. }
  71. /**
  72. * 将旧的嵌套格式 ComponentSpec 转换为扁平化的官方 Spec 格式
  73. *
  74. * 旧格式: { type: 'card', title: '...', children: [...] }
  75. * 新格式: { root: 'card1', elements: { 'card1': { type: 'card', props: { title: '...' }, children: ['text1'] } } }
  76. */
  77. function convertLegacyToSpec(
  78. legacySpec: LegacyComponentSpec,
  79. elements: Record<string, UIElement> = {},
  80. parentKey?: string
  81. ): string {
  82. const { type, children, ...restProps } = legacySpec;
  83. const key = generateKey();
  84. // 转换 children(递归)
  85. let childKeys: string[] | undefined;
  86. if (children && Array.isArray(children) && children.length > 0) {
  87. childKeys = children.map((child) =>
  88. convertLegacyToSpec(child, elements, key)
  89. );
  90. }
  91. // 创建 UIElement
  92. const element: UIElement = {
  93. type,
  94. props: restProps,
  95. };
  96. if (childKeys && childKeys.length > 0) {
  97. element.children = childKeys;
  98. }
  99. elements[key] = element;
  100. return key;
  101. }
  102. /**
  103. * 检查是否是官方 Spec 格式
  104. */
  105. function isOfficialSpec(spec: unknown): spec is Spec {
  106. if (!spec || typeof spec !== 'object') return false;
  107. const s = spec as Record<string, unknown>;
  108. return typeof s.root === 'string' && typeof s.elements === 'object';
  109. }
  110. /**
  111. * 检查是否是旧格式 ComponentSpec
  112. */
  113. function isLegacySpec(spec: unknown): spec is LegacyComponentSpec {
  114. if (!spec || typeof spec !== 'object') return false;
  115. const s = spec as Record<string, unknown>;
  116. return typeof s.type === 'string' && !('root' in s) && !('elements' in s);
  117. }
  118. /**
  119. * 将任何格式的 spec 转换为官方 Spec 格式
  120. */
  121. function normalizeSpec(spec: LegacyComponentSpec | Spec | null | undefined): Spec | null {
  122. if (!spec) return null;
  123. // 已经是官方格式
  124. if (isOfficialSpec(spec)) {
  125. return spec;
  126. }
  127. // 旧格式,需要转换
  128. if (isLegacySpec(spec)) {
  129. const elements: Record<string, UIElement> = {};
  130. const root = convertLegacyToSpec(spec, elements);
  131. if (Object.keys(elements).length === 0) {
  132. return null;
  133. }
  134. return { root, elements };
  135. }
  136. return null;
  137. }
  138. /**
  139. * 将多个 spec 转换为容器 spec
  140. */
  141. function wrapSpecsInContainer(specs: (LegacyComponentSpec | Spec)[]): Spec | null {
  142. if (!specs || specs.length === 0) return null;
  143. // 过滤有效的 specs
  144. const validSpecs = specs.filter(Boolean);
  145. if (validSpecs.length === 0) return null;
  146. // 如果只有一个 spec,直接转换
  147. if (validSpecs.length === 1) {
  148. return normalizeSpec(validSpecs[0]);
  149. }
  150. // 多个 specs,包装在 stack 容器中
  151. const elements: Record<string, UIElement> = {};
  152. keyCounter = 0; // 重置计数器
  153. const childKeys = validSpecs.map((spec) => {
  154. if (isOfficialSpec(spec)) {
  155. // 官方格式,合并 elements
  156. Object.assign(elements, spec.elements);
  157. return spec.root;
  158. }
  159. // 旧格式,转换
  160. return convertLegacyToSpec(spec as LegacyComponentSpec, elements);
  161. });
  162. // 创建 stack 容器
  163. const stackKey = generateKey();
  164. elements[stackKey] = {
  165. type: 'stack',
  166. props: {
  167. direction: 'column',
  168. spacing: 2,
  169. },
  170. children: childKeys,
  171. };
  172. return { root: stackKey, elements };
  173. }
  174. // ============ Components ============
  175. /**
  176. * 基础 JsonRenderer 组件
  177. *
  178. * 使用官方 @json-render/react Renderer 渲染组件
  179. */
  180. export function JsonRenderer({
  181. spec,
  182. specs,
  183. className,
  184. onAction,
  185. loading,
  186. }: JsonRendererProps) {
  187. // 转换 spec 格式
  188. const finalSpec = useMemo(() => {
  189. if (specs && specs.length > 0) {
  190. return wrapSpecsInContainer(specs);
  191. }
  192. if (spec) {
  193. return normalizeSpec(spec);
  194. }
  195. return null;
  196. }, [spec, specs]);
  197. // 创建 action handlers
  198. const handlers = useMemo(() => {
  199. const baseHandlers: Record<string, (params?: Record<string, unknown>) => Promise<void> | void> = {
  200. sendMessage: async (params?: Record<string, unknown>) => {
  201. onAction?.('sendMessage', params);
  202. },
  203. selectNovel: async (params?: Record<string, unknown>) => {
  204. onAction?.('selectNovel', params);
  205. },
  206. copy: async (params?: Record<string, unknown>) => {
  207. onAction?.('copy', params);
  208. // 如果有 text 参数,直接复制到剪贴板
  209. if (params?.text && typeof params.text === 'string') {
  210. try {
  211. await navigator.clipboard.writeText(params.text);
  212. } catch (e) {
  213. console.error('Failed to copy to clipboard:', e);
  214. }
  215. }
  216. },
  217. };
  218. return baseHandlers;
  219. }, [onAction]);
  220. if (!finalSpec) {
  221. return null;
  222. }
  223. return (
  224. <div className={className}>
  225. <JSONUIProvider
  226. registry={registry}
  227. handlers={handlers}
  228. >
  229. <Renderer
  230. spec={finalSpec}
  231. registry={registry}
  232. loading={loading}
  233. />
  234. </JSONUIProvider>
  235. </div>
  236. );
  237. }
  238. /**
  239. * 流式 JsonRenderer 组件
  240. *
  241. * 用于 SSE 流式渲染,支持动态更新 specs
  242. */
  243. export function StreamingJsonRenderer({
  244. specs,
  245. className,
  246. onAction,
  247. loading,
  248. }: StreamingJsonRendererProps) {
  249. // 转换 specs 格式
  250. const finalSpec = useMemo(() => {
  251. if (!specs || specs.length === 0) return null;
  252. return wrapSpecsInContainer(specs);
  253. }, [specs]);
  254. // 创建 action handlers
  255. const handlers = useMemo(() => {
  256. const baseHandlers: Record<string, (params?: Record<string, unknown>) => Promise<void> | void> = {
  257. sendMessage: async (params?: Record<string, unknown>) => {
  258. onAction?.('sendMessage', params);
  259. },
  260. selectNovel: async (params?: Record<string, unknown>) => {
  261. onAction?.('selectNovel', params);
  262. },
  263. copy: async (params?: Record<string, unknown>) => {
  264. onAction?.('copy', params);
  265. if (params?.text && typeof params.text === 'string') {
  266. try {
  267. await navigator.clipboard.writeText(params.text);
  268. } catch (e) {
  269. console.error('Failed to copy to clipboard:', e);
  270. }
  271. }
  272. },
  273. };
  274. return baseHandlers;
  275. }, [onAction]);
  276. if (!finalSpec) {
  277. return null;
  278. }
  279. return (
  280. <div className={className}>
  281. <JSONUIProvider
  282. registry={registry}
  283. handlers={handlers}
  284. >
  285. <Renderer
  286. spec={finalSpec}
  287. registry={registry}
  288. loading={loading}
  289. />
  290. </JSONUIProvider>
  291. </div>
  292. );
  293. }
  294. // ============ Default Export ============
  295. /**
  296. * 默认导出 - 基础 JsonRenderer
  297. *
  298. * 支持单个 spec 或 specs 数组
  299. */
  300. export default JsonRenderer;
  301. // ============ Additional Exports ============
  302. // 导出类型
  303. export type { Spec, StateModel } from '@json-render/react';
  304. export type { UIElement } from '@json-render/core';
  305. // 兼容旧类型
  306. export type ComponentSpec = LegacyComponentSpec;