@blocklet/pages-kit-block-studio 0.4.127 → 0.4.128

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.
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  // import BlockStudio from '@blocklet/pages-kit-block-studio/frontend';
6
6
  // export default BlockStudio;
7
7
  import { createAuthServiceSessionContext } from '@arcblock/did-connect/lib/Session';
8
+ import Empty from '@arcblock/ux/lib/Empty';
8
9
  import { LocaleProvider, useLocaleContext } from '@arcblock/ux/lib/Locale/context';
9
10
  import Toast, { ToastProvider } from '@arcblock/ux/lib/Toast';
10
11
  import { createAxios } from '@blocklet/js-sdk';
@@ -20,15 +21,19 @@ import { parsePropertyValue } from '@blocklet/pages-kit/utils/property';
20
21
  import { Dashboard } from '@blocklet/studio-ui';
21
22
  import { BlockletStudio } from '@blocklet/ui-react';
22
23
  import AddIcon from '@mui/icons-material/Add';
23
- import { Alert, Box, Button, CircularProgress, Dialog, DialogContent, DialogTitle, Drawer, List, ListItem, ListItemButton, Stack, StyledEngineProvider, TextField, ThemeProvider, Tooltip, Typography, backdropClasses, circularProgressClasses, styled, } from '@mui/material';
24
- import { useDebounceFn, useReactive } from 'ahooks';
24
+ import LaptopMacIcon from '@mui/icons-material/LaptopMac';
25
+ import PhoneAndroidIcon from '@mui/icons-material/PhoneAndroid';
26
+ import { Alert, Box, Button, CircularProgress, Dialog, DialogContent, DialogTitle, List, ListItem, ListItemButton, Skeleton, Stack, StyledEngineProvider, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, backdropClasses, circularProgressClasses, styled, } from '@mui/material';
27
+ import { useDebounceFn, useLocalStorageState, useReactive } from 'ahooks';
25
28
  import cloneDeep from 'lodash/cloneDeep';
26
29
  import get from 'lodash/get';
27
30
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
28
- import { Suspense, useCallback, useContext, useEffect, useMemo } from 'react';
31
+ import { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react';
29
32
  import { DndProvider } from 'react-dnd';
30
33
  import { HTML5Backend } from 'react-dnd-html5-backend';
31
34
  import { Navigate, useLocation, useNavigate } from 'react-router-dom';
35
+ import SplitPane, { Pane } from 'split-pane-react';
36
+ import 'split-pane-react/esm/themes/default.css';
32
37
  import { joinURL } from 'ufo';
33
38
  // eslint-disable-next-line import/no-extraneous-dependencies
34
39
  import { useStaticData } from 'vite-plugin-react-pages/client';
@@ -45,17 +50,18 @@ function useSessionContext() {
45
50
  const info = useContext(SessionContext);
46
51
  return info;
47
52
  }
48
- const LEFT_DRAWER_WIDTH = 200;
53
+ const LEFT_DRAWER_WIDTH = 300;
49
54
  const RIGHT_DRAWER_WIDTH = 300;
50
55
  const defaultLocale = 'en';
51
- const ComparisonPreviewDialog = ({ open, title, leftTitle, leftContent, rightTitle, rightContent, description = '确认后将更新配置。', loading, onConfirm, onClose, }) => {
56
+ const ComparisonPreviewDialog = ({ open, title, leftTitle, leftContent, rightTitle, rightContent, description, loading, onConfirm, onClose, }) => {
57
+ const { t } = useLocaleContext();
52
58
  const handleConfirm = async () => {
53
59
  try {
54
60
  await onConfirm();
55
61
  }
56
62
  catch (error) {
57
63
  console.error('执行操作失败:', error);
58
- Toast.error('执行操作失败');
64
+ Toast.error(t('themeTranslations.operationFailed'));
59
65
  }
60
66
  };
61
67
  return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "md", fullWidth: true, children: [_jsx(DialogTitle, { children: title }), _jsx(DialogContent, { children: _jsxs(Box, { children: [_jsxs(Box, { sx: { display: 'flex', flexDirection: 'row', mb: 2 }, children: [_jsxs(Box, { sx: { flex: 1, mr: 1 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { mb: 1 }, children: leftTitle }), _jsx(TextField, { multiline: true, fullWidth: true, rows: 10, InputProps: {
@@ -72,13 +78,10 @@ const ComparisonPreviewDialog = ({ open, title, leftTitle, leftContent, rightTit
72
78
  fontFamily: 'monospace',
73
79
  fontSize: '0.875rem',
74
80
  },
75
- } })] })] }), _jsx(Typography, { variant: "body2", sx: { mt: 2, color: 'text.secondary' }, children: description })] }) }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', p: 2 }, children: [_jsx(Button, { variant: "outlined", onClick: onClose, sx: { mr: 1 }, children: "\u53D6\u6D88" }), _jsx(Button, { variant: "contained", onClick: handleConfirm, disabled: loading, children: loading ? _jsx(CircularProgress, { size: 24 }) : '确认更新' })] })] }));
81
+ } })] })] }), _jsx(Typography, { variant: "body2", sx: { mt: 2, color: 'text.secondary' }, children: description })] }) }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', p: 2 }, children: [_jsx(Button, { variant: "outlined", onClick: onClose, sx: { mr: 1 }, children: t('themeTranslations.cancel') }), _jsx(Button, { variant: "contained", onClick: handleConfirm, disabled: loading, children: loading ? _jsx(CircularProgress, { size: 24 }) : t('themeTranslations.confirmUpdate') })] })] }));
76
82
  };
77
83
  function Layout({ loadState, loadedData }) {
78
84
  const state = useReactive({
79
- injectBlocks: [],
80
- selectingParam: null,
81
- componentSelectOpen: false,
82
85
  createResourceOpen: false,
83
86
  createBlockOpen: false,
84
87
  metadata: {
@@ -106,8 +109,16 @@ function Layout({ loadState, loadedData }) {
106
109
  init: false,
107
110
  allComponents: [],
108
111
  propertiesValue: {},
112
+ draggingSplitPane: false,
113
+ simulatorType: 'pc',
114
+ iframeLoaded: false,
109
115
  });
110
- const { locale } = useLocaleContext();
116
+ // 添加renderComponentTrigger状态,用于触发组件重新渲染
117
+ const [renderComponentTrigger, setRenderComponentTrigger] = useState(0);
118
+ const [hSizes, setHSizes] = useLocalStorageState('BlockStudioHorizontalSizes', {
119
+ defaultValue: [LEFT_DRAWER_WIDTH, 'auto', RIGHT_DRAWER_WIDTH],
120
+ });
121
+ const { t, locale } = useLocaleContext();
111
122
  const { session } = useSessionContext();
112
123
  const location = useLocation();
113
124
  const navigate = useNavigate();
@@ -142,14 +153,6 @@ function Layout({ loadState, loadedData }) {
142
153
  const response = await api.get('/api/blocks/components');
143
154
  return response.data || [];
144
155
  };
145
- useEffect(() => {
146
- if (currentPage && state.injectBlocks.length === 0) {
147
- state.injectBlocks.push({
148
- ...currentPage,
149
- level: 0,
150
- });
151
- }
152
- }, [currentPage]);
153
156
  // Add new effect to fetch metadata when page changes
154
157
  useEffect(() => {
155
158
  const fetchMetadata = async () => {
@@ -235,39 +238,402 @@ function Layout({ loadState, loadedData }) {
235
238
  description: '',
236
239
  };
237
240
  };
238
- let mergedPropertiesValues = Object.fromEntries(Object.values(state.metadata.properties ?? {}).map(({ data }) => {
239
- return [
240
- data.id,
241
- {
242
- value: state.propertiesValue[data.id]?.value ??
243
- parsePropertyValue(data, data.locales?.[locale]?.defaultValue ?? data.locales?.[defaultLocale]?.defaultValue, {
244
- locale,
245
- defaultLocale,
246
- }),
247
- },
248
- ];
249
- }));
250
- const getRenderContent = useCallback(() => {
251
- if (loadState.type === '404') {
252
- return null;
241
+ let mergedPropertiesValues = useMemo(() => {
242
+ return Object.fromEntries(Object.values(state.metadata.properties ?? {}).map(({ data }) => {
243
+ return [
244
+ data.id,
245
+ {
246
+ value: cloneDeep(state.propertiesValue[data.id]?.value ??
247
+ parsePropertyValue(data, data.locales?.[locale]?.defaultValue ?? data.locales?.[defaultLocale]?.defaultValue, {
248
+ locale,
249
+ defaultLocale,
250
+ })),
251
+ },
252
+ ];
253
+ }));
254
+ }, [JSON.stringify(state.metadata.properties), JSON.stringify(state.propertiesValue)]);
255
+ // 添加防抖函数,延迟渲染
256
+ const { run: triggerRerender } = useDebounceFn(() => {
257
+ // 发送消息到 iframe
258
+ try {
259
+ const iframe = document.querySelector('#preview-iframe');
260
+ if (iframe && iframe.contentWindow) {
261
+ // 创建精简版数据,只包含必要信息,并避免循环引用
262
+ const safeData = {
263
+ type: 'STATE_UPDATE',
264
+ data: {
265
+ componentId: state.metadata.id,
266
+ // 不传递整个 components 对象,只传递当前组件需要的最小数据集
267
+ currentComponent: {
268
+ id: state.metadata.id,
269
+ // 使用 JSON 序列化和反序列化清除循环引用和函数
270
+ mergedPropertiesValues: cloneDeep({
271
+ ...mergedPropertiesValues,
272
+ updatedAt: Date.now(),
273
+ } || {}),
274
+ },
275
+ locale: locale,
276
+ defaultLocale: defaultLocale,
277
+ },
278
+ };
279
+ iframe.contentWindow.postMessage(safeData, '*');
280
+ }
281
+ }
282
+ catch (error) {
283
+ console.error('Failed to send message to iframe:', error);
284
+ }
285
+ }, { wait: 50 });
286
+ // 当属性变化时触发重新渲染
287
+ useEffect(() => {
288
+ triggerRerender();
289
+ }, [JSON.stringify(mergedPropertiesValues), JSON.stringify(mergedAllBlocks), state.metadata.id]);
290
+ const DraggingSplitPlaceholder = useMemo(() => {
291
+ return (_jsx(Box, { p: 1.5, width: "100%", height: "100%", children: _jsx(Skeleton, { variant: "rectangular", height: "100%", sx: { borderRadius: 1 } }) }));
292
+ }, []);
293
+ // 修改 iframe URL 构建逻辑,添加初始状态参数
294
+ const iframeUrl = useMemo(() => {
295
+ const url = new URL(window.location.href);
296
+ url.searchParams.set('simulator', '1');
297
+ // 添加初始状态参数
298
+ url.searchParams.set('componentId', state.metadata.id || '');
299
+ url.searchParams.set('locale', locale || defaultLocale || 'en');
300
+ return url.toString();
301
+ }, [window.location.href, state.metadata.id, locale, defaultLocale]);
302
+ // 获取当前URL参数判断是否是在iframe内
303
+ const isInsideIframe = useMemo(() => {
304
+ const url = new URL(window.location.href);
305
+ return url.searchParams.get('simulator') === '1';
306
+ }, [window.location.href]);
307
+ // 添加消息事件监听
308
+ useEffect(() => {
309
+ const handleMessage = (event) => {
310
+ if (event.data && event.data.type === 'STATE_UPDATE') {
311
+ // 接收并应用父窗口发来的状态更新
312
+ const { componentId, currentComponent, locale } = event.data.data;
313
+ // 安全地更新组件ID
314
+ if (componentId && componentId !== state.metadata.id) {
315
+ state.metadata.id = componentId;
316
+ }
317
+ // 只更新当前组件的属性值,而不是整个组件列表
318
+ if (currentComponent && currentComponent.mergedPropertiesValues) {
319
+ state.propertiesValue = cloneDeep(currentComponent.mergedPropertiesValues);
320
+ }
321
+ setRenderComponentTrigger((prev) => prev + 1);
322
+ }
323
+ };
324
+ window.addEventListener('message', handleMessage);
325
+ return () => window.removeEventListener('message', handleMessage);
326
+ }, []);
327
+ const leftPanelContent = useMemo(() => {
328
+ if (state.draggingSplitPane) {
329
+ return DraggingSplitPlaceholder;
253
330
  }
254
- if (['load-error', 'loading'].includes(loadState.type)) {
255
- return (_jsx(Box, { width: "100%", height: "100%", display: "flex", justifyContent: "center", alignItems: "center", children: loadState.type === 'load-error' ? (_jsx(Alert, { severity: "error", variant: "filled", children: "Failed to load component code" })) : (_jsx(CircularProgress, {})) }));
331
+ return (_jsxs(Stack, { height: "100%", children: [_jsxs(Stack, { gap: 1, direction: "row", alignItems: "center", sx: { py: 2, pr: 1, pl: 0.5, flex: 1 }, children: [_jsx(TextField, { placeholder: t('themeTranslations.search'), sx: { minWidth: 60, flex: 1 }, onChange: (e) => {
332
+ state.searchValue = e.target.value;
333
+ } }), _jsx(Button, { variant: "contained", sx: { minWidth: 40 }, onClick: () => {
334
+ state.createBlockOpen = true;
335
+ }, children: _jsx(AddIcon, { fontSize: "small" }) })] }), routes?.length > 0 ? (_jsx(List, { sx: { pr: 1, overflowY: 'auto', height: 'calc(100% - 60px)' }, children: routes
336
+ .map((route) => {
337
+ const routeName = route;
338
+ const staticDataInRoute = staticData[route]?.main;
339
+ if (state.searchValue && !routeName?.toLowerCase().includes(state.searchValue?.toLowerCase())) {
340
+ return null;
341
+ }
342
+ if (!state.allComponents?.find(({ blockName }) => `/${blockName}` === routeName)) {
343
+ return null;
344
+ }
345
+ return (_jsx(ListItem, { disablePadding: true, children: _jsx(ListItemButton, { selected: currentPage.pageId === route, onClick: () => {
346
+ navigate(route);
347
+ state.iframeLoaded = false;
348
+ }, sx: {
349
+ borderRadius: 1,
350
+ mb: 1,
351
+ width: '100%',
352
+ textOverflow: 'ellipsis',
353
+ whiteSpace: 'nowrap',
354
+ overflowX: 'hidden',
355
+ transition: 'all 0.3s ease',
356
+ '&.Mui-selected': {
357
+ backgroundColor: 'primary.main',
358
+ color: 'white',
359
+ '&:hover': {
360
+ backgroundColor: 'primary.main',
361
+ },
362
+ },
363
+ fontSize: '14px',
364
+ }, children: _jsx(Tooltip, { title: staticDataInRoute.blockName || routeName, children: _jsx("div", { style: {
365
+ width: '100%',
366
+ overflow: 'hidden',
367
+ textOverflow: 'ellipsis',
368
+ }, children: staticDataInRoute.blockName || routeName }) }) }) }, route));
369
+ })
370
+ .filter(Boolean) })) : (_jsx(Box, { display: "flex", justifyContent: "center", alignItems: "center", height: "100%", children: _jsx(Empty, { children: _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('themeTranslations.noRoutesFound') }) }) }))] }));
371
+ }, [routes, staticData, state.allComponents, state.searchValue, state.draggingSplitPane, locale]);
372
+ // 修改 middlePanelContent - iframe 内部监听消息
373
+ const middlePanelContent = useMemo(() => {
374
+ // 如果在iframe内部,添加消息接收逻辑
375
+ if (isInsideIframe) {
376
+ if (loadState.type === '404' || loadState.type === 'loading') {
377
+ return null;
378
+ }
379
+ if (loadState.type === 'load-error') {
380
+ return (_jsx(Box, { width: "100%", height: "100%", display: "flex", justifyContent: "center", alignItems: "center", children: _jsx(Alert, { severity: "error", variant: "filled", children: t('themeTranslations.failedLoadCode') }) }));
381
+ }
382
+ // 从 URL 获取初始组件 ID 和语言
383
+ const url = new URL(window.location.href);
384
+ const initialComponentId = url.searchParams.get('componentId') || state.metadata.id;
385
+ const initialLocale = url.searchParams.get('locale') || locale || 'en';
386
+ return (_jsx(Box, { className: "custom-component-root", sx: { height: '100%', width: '100%', overflow: 'auto' }, children: _jsx(ThemeProvider, { theme: pagesTheme, children: _jsx(Suspense, { fallback: _jsx(CircularProgress, {}), children: _jsx(CustomComponentRenderer, { locale: initialLocale, componentId: initialComponentId || state.metadata.id, dev: { mode: 'draft', components: mergedAllBlocks, defaultLocale }, properties: mergedPropertiesValues }, `custom-${renderComponentTrigger}`) }) }) }));
256
387
  }
257
- // const pageData = loadedData[loadState.routePath];
258
- // const Component = pageData.main.default;
259
- // return <Component />;
260
- return [
261
- _jsx(CustomComponentRenderer, { locale: locale, componentId: state.metadata.id, dev: { mode: 'draft', components: mergedAllBlocks, defaultLocale }, properties: mergedPropertiesValues }, "custom"),
262
- // <Component key="original" />,
263
- ];
264
- }, [loadedData, loadState, locale, state.injectBlocks, state.metadata, mergedAllBlocks, mergedPropertiesValues]);
388
+ // 外部容器,包含设备切换按钮和iframe
389
+ return (_jsxs(Box, { sx: { height: '100%', display: 'flex', flexDirection: 'column' }, children: [_jsx(Box, { sx: { p: 1, display: 'flex', justifyContent: 'center', borderBottom: '1px solid #e0e0e0' }, children: _jsxs(ToggleButtonGroup, { size: "small", exclusive: true, color: "primary", value: state.simulatorType, onChange: (_, value) => (state.simulatorType = value ?? state.simulatorType), sx: {
390
+ '& > button': {
391
+ p: 0.75,
392
+ minWidth: 50,
393
+ },
394
+ }, children: [_jsx(ToggleButton, { value: "pc", children: _jsx(LaptopMacIcon, { fontSize: "small" }) }), _jsx(ToggleButton, { value: "mobile", children: _jsx(PhoneAndroidIcon, { fontSize: "small" }) })] }) }), _jsx(Box, { sx: {
395
+ height: '100%',
396
+ width: '100%',
397
+ display: 'flex',
398
+ justifyContent: 'center',
399
+ flex: 1,
400
+ overflowY: 'auto',
401
+ position: 'relative', // 为绝对定位的loading提供参考
402
+ p: 1.5,
403
+ alignItems: 'center',
404
+ }, children: _jsxs(Box, { sx: {
405
+ position: 'relative',
406
+ height: '100%',
407
+ width: state.simulatorType === 'mobile' ? '375px' : '100%',
408
+ maxWidth: '100%',
409
+ display: 'flex',
410
+ flexDirection: 'column',
411
+ }, children: [state.simulatorType === 'pc' && (_jsx(Box, { sx: {
412
+ height: '30px',
413
+ backgroundColor: '#f5f5f5',
414
+ borderTopLeftRadius: '6px',
415
+ borderTopRightRadius: '6px',
416
+ borderTop: '1px solid #e0e0e0',
417
+ borderLeft: '1px solid #e0e0e0',
418
+ borderRight: '1px solid #e0e0e0',
419
+ display: 'flex',
420
+ alignItems: 'center',
421
+ px: 1.5,
422
+ }, children: _jsxs(Box, { sx: { display: 'flex', gap: 0.7, alignItems: 'center' }, children: [_jsx(Box, { sx: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ff5f57' } }), _jsx(Box, { sx: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#febc2e' } }), _jsx(Box, { sx: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#28c840' } })] }) })), state.simulatorType === 'mobile' && (_jsxs(Box, { sx: {
423
+ height: '30px',
424
+ backgroundColor: '#111',
425
+ borderTopLeftRadius: '20px',
426
+ borderTopRightRadius: '20px',
427
+ display: 'flex',
428
+ justifyContent: 'center',
429
+ alignItems: 'center',
430
+ boxShadow: '0 0 0 12px #111',
431
+ mt: 2,
432
+ }, children: [_jsx(Box, { sx: {
433
+ width: '60px',
434
+ height: '5px',
435
+ backgroundColor: '#333',
436
+ borderRadius: '3px',
437
+ } }), _jsx(Box, { sx: {
438
+ width: '40%',
439
+ height: '5px',
440
+ backgroundColor: '#333',
441
+ borderRadius: '3px',
442
+ position: 'absolute',
443
+ bottom: 20,
444
+ left: '50%',
445
+ transform: 'translateX(-50%)',
446
+ } })] })), !state.iframeLoaded && !isInsideIframe && (_jsx(Box, { sx: {
447
+ position: 'absolute',
448
+ top: 0,
449
+ left: 0,
450
+ right: 0,
451
+ bottom: 0,
452
+ display: 'flex',
453
+ justifyContent: 'center',
454
+ alignItems: 'center',
455
+ zIndex: 2,
456
+ }, children: _jsx(CircularProgress, {}) })), _jsxs(Box, { sx: {
457
+ border: 'none',
458
+ height: state.simulatorType === 'mobile' ? 'calc(100% - 60px)' : 'calc(100% - 30px)',
459
+ display: 'flex',
460
+ width: '100%',
461
+ backgroundColor: '#fff',
462
+ ...(state.simulatorType === 'mobile'
463
+ ? {
464
+ borderRadius: '0 0 20px 20px',
465
+ boxShadow: '0 0 0 12px #111',
466
+ }
467
+ : {
468
+ borderBottomLeftRadius: '6px',
469
+ borderBottomRightRadius: '6px',
470
+ border: '1px solid #e0e0e0',
471
+ }),
472
+ }, children: [state.draggingSplitPane && DraggingSplitPlaceholder, _jsx(Box, { id: "preview-iframe", component: "iframe", src: iframeUrl, sx: {
473
+ flex: 1,
474
+ border: 'none',
475
+ display: state.draggingSplitPane ? 'none' : 'flex',
476
+ }, onLoad: () => {
477
+ state.iframeLoaded = true;
478
+ }, title: "Component Preview" })] })] }) })] }, "middle-panel"));
479
+ }, [
480
+ loadedData,
481
+ loadState,
482
+ iframeUrl,
483
+ state.simulatorType,
484
+ state.draggingSplitPane,
485
+ renderComponentTrigger,
486
+ isInsideIframe,
487
+ state.iframeLoaded,
488
+ locale,
489
+ ]);
490
+ const rightPanelContent = useMemo(() => {
491
+ if (state.draggingSplitPane) {
492
+ return DraggingSplitPlaceholder;
493
+ }
494
+ return (_jsxs(List, { sx: { height: '100%', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(ListItem, { children: _jsx(Box, { sx: { width: '100%' }, children: _jsx(BasicInfo, { config: state.metadata }) }) }), _jsx(ListItem, { children: _jsxs(Box, { sx: { width: '100%' }, children: [_jsx(PropertiesConfig, { config: state.metadata, currentLocale: locale, defaultLocale: defaultLocale, allComponents: mergedAllBlocks, onUpdateConfig: (updater) => {
495
+ updater(state.metadata);
496
+ }, useI18nEditor: false }), _jsxs(Stack, { direction: "column", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { variant: "contained", size: "small", color: "primary", onClick: async () => {
497
+ try {
498
+ const { dataPath } = getStaticData() || {};
499
+ if (!dataPath) {
500
+ Toast.error(t('themeTranslations.componentPathNotFound'));
501
+ return;
502
+ }
503
+ Toast.info(t('themeTranslations.analyzingInterface'));
504
+ const response = await api.post('/api/blocks/interface-to-properties', {
505
+ componentPath: dataPath,
506
+ });
507
+ if (response.data.success) {
508
+ const { currentProperties, newProperties } = response.data;
509
+ state.previewDialog = {
510
+ open: true,
511
+ title: t('themeTranslations.interfacePropertiesPreview'),
512
+ leftTitle: t('themeTranslations.currentProperties'),
513
+ leftContent: JSON.stringify(currentProperties, null, 2),
514
+ rightTitle: t('themeTranslations.newProperties'),
515
+ rightContent: JSON.stringify(newProperties, null, 2),
516
+ description: t('themeTranslations.confirmUpdateMetadata'),
517
+ loading: false,
518
+ onConfirm: async () => {
519
+ state.previewDialog.loading = true;
520
+ try {
521
+ const updateResponse = await api.post('/api/blocks/interface-to-properties', {
522
+ componentPath: dataPath,
523
+ write: true,
524
+ });
525
+ if (updateResponse.data.success) {
526
+ Toast.success(t('themeTranslations.metadataSuccess'));
527
+ // 更新当前的metadata状态
528
+ state.metadata = {
529
+ ...updateResponse.data.metadata,
530
+ renderer: state.metadata.renderer,
531
+ };
532
+ state.previewDialog.open = false;
533
+ }
534
+ else {
535
+ throw new Error(updateResponse.data.error || t('themeTranslations.generationFailed'));
536
+ }
537
+ }
538
+ finally {
539
+ state.previewDialog.loading = false;
540
+ }
541
+ },
542
+ };
543
+ }
544
+ else {
545
+ Toast.error(response.data.error || t('themeTranslations.previewFailed'));
546
+ }
547
+ }
548
+ catch (error) {
549
+ console.error(error);
550
+ Toast.error(t('themeTranslations.previewFailed'));
551
+ }
552
+ }, children: t('themeTranslations.interfaceToProperties') }), _jsx(Button, { variant: "outlined", size: "small", color: "primary", onClick: async () => {
553
+ try {
554
+ const { dataPath } = getStaticData() || {};
555
+ if (!dataPath) {
556
+ Toast.error(t('themeTranslations.componentPathNotFound'));
557
+ return;
558
+ }
559
+ Toast.info(t('themeTranslations.generatingInterface'));
560
+ const response = await api.post('/api/blocks/properties-to-interface', {
561
+ componentPath: dataPath,
562
+ });
563
+ if (response.data.success) {
564
+ const { currentInterface, newInterface } = response.data;
565
+ state.previewDialog = {
566
+ open: true,
567
+ title: t('themeTranslations.propertiesInterfacePreview'),
568
+ leftTitle: t('themeTranslations.currentInterface'),
569
+ leftContent: currentInterface,
570
+ rightTitle: t('themeTranslations.newInterface'),
571
+ rightContent: newInterface,
572
+ description: t('themeTranslations.confirmUpdateInterface'),
573
+ loading: false,
574
+ onConfirm: async () => {
575
+ state.previewDialog.loading = true;
576
+ try {
577
+ const updateResponse = await api.post('/api/blocks/properties-to-interface', {
578
+ componentPath: dataPath,
579
+ write: true,
580
+ });
581
+ if (updateResponse.data.success) {
582
+ Toast.success(t('themeTranslations.interfaceSuccess'));
583
+ state.previewDialog.open = false;
584
+ }
585
+ else {
586
+ throw new Error(updateResponse.data.error || t('themeTranslations.generationFailed'));
587
+ }
588
+ }
589
+ finally {
590
+ state.previewDialog.loading = false;
591
+ }
592
+ },
593
+ };
594
+ }
595
+ else {
596
+ Toast.error(response.data.error || t('themeTranslations.previewGenerationFailed'));
597
+ }
598
+ }
599
+ catch (error) {
600
+ console.error(error);
601
+ Toast.error(t('themeTranslations.interfacePreviewFailed'));
602
+ }
603
+ }, children: t('themeTranslations.propertiesToInterface') })] })] }) }), state.metadata.id && (_jsx(ListItem, { children: _jsx(Box, { sx: { width: '100%' }, children: _jsx(ParametersConfig, { config: state.metadata, allComponents: mergedAllBlocks, defaultLocale: defaultLocale, locale: locale, propertiesValue: mergedPropertiesValues, onChange: ({ key, value, id, path, ...rest }) => {
604
+ const realPath = [...path, 'data'];
605
+ const property = get(state.metadata, realPath);
606
+ // ensure property exist
607
+ if (!property) {
608
+ Toast.warning(`${t('themeTranslations.propertyNotFound')} ${realPath.join('.')}`);
609
+ return;
610
+ }
611
+ const realValue = parsePropertyValue(property, value.value, {
612
+ locale,
613
+ defaultLocale,
614
+ });
615
+ state.propertiesValue[id] = {
616
+ value: realValue,
617
+ };
618
+ } }) }) }))] }));
619
+ }, [
620
+ JSON.stringify(state.metadata || {}),
621
+ locale,
622
+ defaultLocale,
623
+ JSON.stringify(mergedAllBlocks || []),
624
+ JSON.stringify(mergedPropertiesValues || {}),
625
+ getStaticData,
626
+ state.draggingSplitPane,
627
+ ]);
265
628
  // add auto redirect to first route
266
629
  if (loadState.type === '404' &&
267
630
  !routes.includes(location.pathname) &&
268
631
  !location.search.includes('no-redirect=true')) {
269
632
  return _jsx(Navigate, { to: `${firstRoute ?? '/'}`, replace: true });
270
633
  }
634
+ if (isInsideIframe) {
635
+ return middlePanelContent;
636
+ }
271
637
  return (_jsx(DndProvider, { backend: HTML5Backend, children: _jsxs(StyledDashboard, { HeaderProps: {
272
638
  // @ts-ignore
273
639
  homeLink: joinURL(basename),
@@ -275,7 +641,7 @@ function Layout({ loadState, loadedData }) {
275
641
  return [
276
642
  _jsx(Button, { onClick: async () => {
277
643
  if (!session?.user?.did) {
278
- Toast.warning('请先连接钱包');
644
+ Toast.warning(t('themeTranslations.connectWallet'));
279
645
  await session.login();
280
646
  setTimeout(() => {
281
647
  state.createResourceOpen = true;
@@ -284,208 +650,35 @@ function Layout({ loadState, loadedData }) {
284
650
  else {
285
651
  state.createResourceOpen = true;
286
652
  }
287
- }, children: "Create Resource" }, "logout"),
653
+ }, children: t('themeTranslations.createResource') }, "logout"),
288
654
  ...addons,
289
655
  ];
290
656
  },
291
- }, MenusDrawerProps: { sx: { [`.${backdropClasses.root}`]: { top: 64 } } }, children: [_jsxs(Drawer, { variant: "permanent", sx: {
292
- width: LEFT_DRAWER_WIDTH,
293
- flexShrink: 0,
294
- zIndex: 1000,
295
- '& .MuiDrawer-paper': {
296
- width: LEFT_DRAWER_WIDTH,
297
- boxSizing: 'border-box',
298
- position: 'relative',
299
- height: '100%',
657
+ }, MenusDrawerProps: { sx: { [`.${backdropClasses.root}`]: { top: 64 } } }, children: [_jsxs(StyledSplitPane, { split: "vertical", sizes: hSizes, onChange: setHSizes, sashRender: SashRender, onDragStart: () => (state.draggingSplitPane = true), onDragEnd: () => (state.draggingSplitPane = false), sx: {
658
+ '&.react-split--dragging': {
659
+ '.react-split__pane': {
660
+ '*': {
661
+ userSelect: 'none',
662
+ },
663
+ },
300
664
  },
301
- }, children: [_jsxs(Stack, { gap: 1, direction: "row", alignItems: "center", sx: { py: 2, pr: 1, pl: 0.5 }, children: [_jsx(TextField, { placeholder: "Search Blocks...", sx: { minWidth: 120 }, onChange: (e) => {
302
- state.searchValue = e.target.value;
303
- } }), _jsx(Button, { variant: "contained", sx: { minWidth: 'auto' }, onClick: () => {
304
- state.createBlockOpen = true;
305
- }, children: _jsx(AddIcon, { fontSize: "small" }) })] }), _jsx(List, { sx: { pr: 1, overflowY: 'auto' }, children: routes
306
- .map((route) => {
307
- const routeName = route;
308
- const staticDataInRoute = staticData[route]?.main;
309
- if (state.searchValue && !routeName?.toLowerCase().includes(state.searchValue?.toLowerCase())) {
310
- return null;
311
- }
312
- if (!state.allComponents?.find(({ blockName }) => `/${blockName}` === routeName)) {
313
- return null;
314
- }
315
- return (_jsx(ListItem, { disablePadding: true, children: _jsx(ListItemButton, { selected: currentPage.pageId === route, onClick: () => {
316
- navigate(route);
317
- }, sx: {
318
- borderRadius: 1,
319
- mb: 1,
320
- width: '100%',
321
- textOverflow: 'ellipsis',
322
- whiteSpace: 'nowrap',
323
- overflowX: 'hidden',
324
- transition: 'all 0.3s ease',
325
- '&.Mui-selected': {
326
- backgroundColor: 'primary.main',
327
- color: 'white',
328
- '&:hover': {
329
- backgroundColor: 'primary.main',
330
- },
331
- },
332
- fontSize: '14px',
333
- }, children: _jsx(Tooltip, { title: staticDataInRoute.blockName || routeName, children: _jsx("div", { style: {
334
- width: '100%',
335
- overflow: 'hidden',
336
- textOverflow: 'ellipsis',
337
- }, children: staticDataInRoute.blockName || routeName }) }) }) }, route));
338
- })
339
- .filter(Boolean) })] }), _jsx(Box, { sx: { flex: 1, overflowX: 'hidden', overflowY: getStaticData()?.isHtml ? 'hidden' : 'auto' }, children: _jsx(ThemeProvider, { theme: pagesTheme, children: _jsx(Suspense, { children: getRenderContent() }) }) }), _jsx(Drawer, { variant: "permanent", anchor: "right", sx: {
340
- width: RIGHT_DRAWER_WIDTH,
341
- flexShrink: 0,
342
- zIndex: 1000,
343
- '& .MuiDrawer-paper': {
344
- width: RIGHT_DRAWER_WIDTH,
345
- boxSizing: 'border-box',
346
- position: 'relative',
347
- height: '100%',
348
- },
349
- }, children: _jsxs(List, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(ListItem, { children: _jsx(Box, { sx: { width: '100%' }, children: _jsx(BasicInfo, { config: state.metadata }) }) }), _jsx(ListItem, { children: _jsxs(Box, { sx: { width: '100%' }, children: [_jsx(PropertiesConfig, { config: state.metadata, currentLocale: locale, defaultLocale: defaultLocale, allComponents: mergedAllBlocks, onUpdateConfig: (updater) => {
350
- updater(state.metadata);
351
- }, useI18nEditor: false }), _jsxs(Stack, { direction: "column", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { variant: "contained", size: "small", color: "primary", onClick: async () => {
352
- try {
353
- const { dataPath } = getStaticData() || {};
354
- if (!dataPath) {
355
- Toast.error('无法找到组件路径');
356
- return;
357
- }
358
- Toast.info('正在分析组件接口...');
359
- const response = await api.post('/api/blocks/interface-to-properties', {
360
- componentPath: dataPath,
361
- });
362
- if (response.data.success) {
363
- const { currentProperties, newProperties } = response.data;
364
- state.previewDialog = {
365
- open: true,
366
- title: 'Interface → Properties 预览',
367
- leftTitle: '当前 Properties',
368
- leftContent: JSON.stringify(currentProperties, null, 2),
369
- rightTitle: '新 Properties',
370
- rightContent: JSON.stringify(newProperties, null, 2),
371
- description: '确认后将更新metadata文件。这将保留现有配置值,但可能更改属性结构。',
372
- loading: false,
373
- onConfirm: async () => {
374
- state.previewDialog.loading = true;
375
- try {
376
- const updateResponse = await api.post('/api/blocks/interface-to-properties', {
377
- componentPath: dataPath,
378
- write: true,
379
- });
380
- if (updateResponse.data.success) {
381
- Toast.success('Metadata生成成功!');
382
- // 更新当前的metadata状态
383
- state.metadata = {
384
- ...updateResponse.data.metadata,
385
- renderer: state.metadata.renderer,
386
- };
387
- state.previewDialog.open = false;
388
- }
389
- else {
390
- throw new Error(updateResponse.data.error || '生成失败');
391
- }
392
- }
393
- finally {
394
- state.previewDialog.loading = false;
395
- }
396
- },
397
- };
398
- }
399
- else {
400
- Toast.error(response.data.error || '预览失败');
401
- }
402
- }
403
- catch (error) {
404
- console.error('生成预览失败:', error);
405
- Toast.error('生成预览失败');
406
- }
407
- }, children: "Interface \u2192 Properties" }), _jsx(Button, { variant: "outlined", size: "small", color: "primary", onClick: async () => {
408
- try {
409
- const { dataPath } = getStaticData() || {};
410
- if (!dataPath) {
411
- Toast.error('无法找到组件路径');
412
- return;
413
- }
414
- Toast.info('正在生成TypeScript接口预览...');
415
- const response = await api.post('/api/blocks/properties-to-interface', {
416
- componentPath: dataPath,
417
- });
418
- if (response.data.success) {
419
- const { currentInterface, newInterface } = response.data;
420
- state.previewDialog = {
421
- open: true,
422
- title: 'Properties → Interface 预览',
423
- leftTitle: '当前接口',
424
- leftContent: currentInterface,
425
- rightTitle: '新接口',
426
- rightContent: newInterface,
427
- description: '确认后将更新TypeScript接口。这将覆盖当前的接口定义。',
428
- loading: false,
429
- onConfirm: async () => {
430
- state.previewDialog.loading = true;
431
- try {
432
- const updateResponse = await api.post('/api/blocks/properties-to-interface', {
433
- componentPath: dataPath,
434
- write: true,
435
- });
436
- if (updateResponse.data.success) {
437
- Toast.success('TypeScript接口生成成功!');
438
- state.previewDialog.open = false;
439
- }
440
- else {
441
- throw new Error(updateResponse.data.error || '生成失败');
442
- }
443
- }
444
- finally {
445
- state.previewDialog.loading = false;
446
- }
447
- },
448
- };
449
- }
450
- else {
451
- Toast.error(response.data.error || '预览失败');
452
- }
453
- }
454
- catch (error) {
455
- console.error('生成接口预览失败:', error);
456
- Toast.error('生成接口预览失败');
457
- }
458
- }, children: "Properties \u2192 Interface" })] })] }) }), state.metadata.id && (_jsx(ListItem, { children: _jsx(Box, { sx: { width: '100%' }, children: _jsx(ParametersConfig, { config: state.metadata, allComponents: mergedAllBlocks, defaultLocale: defaultLocale, locale: locale, propertiesValue: mergedPropertiesValues, onChange: ({ key, value, id, path, ...rest }) => {
459
- const realPath = [...path, 'data'];
460
- const property = get(state.metadata, realPath);
461
- // ensure property exist
462
- if (!property) {
463
- Toast.warning(`无法找到属性,请检查在 @metadata.json 中,是否存在该属性: ${realPath.join('.')}`);
464
- return;
465
- }
466
- const realValue = parsePropertyValue(property, value.value, {
467
- locale,
468
- defaultLocale,
469
- });
470
- state.propertiesValue[id] = {
471
- value: realValue,
472
- };
473
- } }) }) }))] }) }), _jsx(CreateResource, { open: state.createResourceOpen, onClose: () => {
665
+ flex: 1,
666
+ }, children: [_jsx(Pane, { minSize: 100, maxSize: 400, children: leftPanelContent }), _jsx(Pane, { minSize: 400, children: middlePanelContent }), _jsx(Pane, { minSize: 100, maxSize: '30%', children: rightPanelContent })] }), _jsx(CreateResource, { open: state.createResourceOpen, onClose: () => {
474
667
  state.createResourceOpen = false;
475
- } }), _jsxs(Dialog, { open: state.createBlockOpen, onClose: onCloseCreateBlock, children: [_jsx(DialogTitle, { children: "Create New Block" }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 2, sx: { pt: 1, minWidth: 300 }, children: [_jsx(TextField, { autoFocus: true, required: true, label: "Name", fullWidth: true, value: state.newBlockParams.name, onChange: (e) => {
668
+ } }), _jsxs(Dialog, { open: state.createBlockOpen, onClose: onCloseCreateBlock, children: [_jsx(DialogTitle, { children: t('themeTranslations.createNewBlock') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 2, sx: { pt: 1, minWidth: 300 }, children: [_jsx(TextField, { autoFocus: true, required: true, label: t('themeTranslations.name'), fullWidth: true, value: state.newBlockParams.name, onChange: (e) => {
476
669
  state.newBlockParams.name = e.target.value.replace(/[^a-zA-Z0-9-]/g, '');
477
- } }), _jsx(TextField, { label: "Description", fullWidth: true, multiline: true, rows: 3, value: state.newBlockParams.description, onChange: (e) => {
670
+ } }), _jsx(TextField, { label: t('themeTranslations.description'), fullWidth: true, multiline: true, rows: 3, value: state.newBlockParams.description, onChange: (e) => {
478
671
  state.newBlockParams.description = e.target.value;
479
672
  } }), _jsx(Button, { variant: "contained", fullWidth: true, onClick: async () => {
480
673
  if (!state.newBlockParams.name) {
481
- Toast.warning('Block name is required');
674
+ Toast.warning(t('themeTranslations.blockNameRequired'));
482
675
  return;
483
676
  }
484
677
  if (routes.some((route) => {
485
678
  const staticDataInRoute = staticData[route]?.main;
486
679
  return staticDataInRoute?.blockName?.toLowerCase() === state.newBlockParams.name.toLowerCase();
487
680
  })) {
488
- Toast.warning('Block name already exists, please change it');
681
+ Toast.warning(t('themeTranslations.blockNameExists'));
489
682
  return;
490
683
  }
491
684
  try {
@@ -498,12 +691,29 @@ function Layout({ loadState, loadedData }) {
498
691
  }
499
692
  catch (error) {
500
693
  console.error('Failed to create block:', error);
501
- Toast.error('Failed to create block');
694
+ Toast.error(t('themeTranslations.failedCreateBlock'));
502
695
  }
503
- }, children: "Create" })] }) })] }), _jsx(ComparisonPreviewDialog, { open: state.previewDialog.open, title: state.previewDialog.title, leftTitle: state.previewDialog.leftTitle, leftContent: state.previewDialog.leftContent, rightTitle: state.previewDialog.rightTitle, rightContent: state.previewDialog.rightContent, description: state.previewDialog.description, loading: state.previewDialog.loading, onConfirm: state.previewDialog.onConfirm, onClose: () => {
696
+ }, children: t('themeTranslations.create') })] }) })] }), _jsx(ComparisonPreviewDialog, { open: state.previewDialog.open, title: state.previewDialog.title, leftTitle: state.previewDialog.leftTitle, leftContent: state.previewDialog.leftContent, rightTitle: state.previewDialog.rightTitle, rightContent: state.previewDialog.rightContent, description: state.previewDialog.description, loading: state.previewDialog.loading, onConfirm: state.previewDialog.onConfirm, onClose: () => {
504
697
  state.previewDialog.open = false;
505
698
  } })] }) }));
506
699
  }
700
+ // Add SplitPane styling
701
+ const StyledSplitPane = styled(SplitPane) `
702
+ .react-split__sash {
703
+ z-index: 1000; // resolve the bug of uploader zIndex
704
+ }
705
+ `;
706
+ function SashRender() {
707
+ return _jsx(DragHandle, {});
708
+ }
709
+ const DragHandle = styled('div') `
710
+ height: 100%;
711
+ background-color: #f0f0f0;
712
+
713
+ &:hover {
714
+ background-color: #e3e3e3;
715
+ }
716
+ `;
507
717
  const StyledDashboard = styled(Dashboard) `
508
718
  .dashboard-content {
509
719
  display: flex;
@@ -557,3 +767,85 @@ function CreateResource({ open, onClose }) {
557
767
  // 默认选中的资源
558
768
  resources: {} }) }));
559
769
  }
770
+ // 添加 themeTranslations 到 translations 对象
771
+ // 这里我们在运行时扩展 translations 对象,而不是修改源文件
772
+ if (!translations.en.themeTranslations) {
773
+ translations.en.themeTranslations = {
774
+ search: 'Search Blocks...',
775
+ createNewBlock: 'Create New Block',
776
+ name: 'Name',
777
+ description: 'Description',
778
+ create: 'Create',
779
+ blockNameRequired: 'Block name is required',
780
+ blockNameExists: 'Block name already exists, please change it',
781
+ failedCreateBlock: 'Failed to create block',
782
+ createResource: 'Create Resource',
783
+ failedLoadCode: 'Failed to load component code',
784
+ interfaceToProperties: 'Interface → Properties',
785
+ propertiesToInterface: 'Properties → Interface',
786
+ componentPathNotFound: 'Component path not found',
787
+ analyzingInterface: 'Analyzing component interface...',
788
+ interfacePropertiesPreview: 'Interface → Properties Preview',
789
+ currentProperties: 'Current Properties',
790
+ newProperties: 'New Properties',
791
+ confirmUpdateMetadata: 'Confirm to update metadata file. This will preserve existing configuration values, but may change property structure.',
792
+ metadataSuccess: 'Metadata generated successfully!',
793
+ generationFailed: 'Generation failed',
794
+ previewFailed: 'Failed to generate preview',
795
+ generatingInterface: 'Generating TypeScript interface preview...',
796
+ propertiesInterfacePreview: 'Properties → Interface Preview',
797
+ currentInterface: 'Current Interface',
798
+ newInterface: 'New Interface',
799
+ confirmUpdateInterface: 'Confirm to update TypeScript interface. This will overwrite the current interface definition.',
800
+ interfaceSuccess: 'TypeScript interface generated successfully!',
801
+ previewGenerationFailed: 'Preview failed',
802
+ interfacePreviewFailed: 'Failed to generate interface preview',
803
+ propertyNotFound: 'Property not found, please check if it exists in @metadata.json:',
804
+ connectWallet: 'Please connect wallet first',
805
+ cancel: 'Cancel',
806
+ confirmUpdate: 'Confirm Update',
807
+ configUpdateConfirmation: 'Configuration will be updated after confirmation.',
808
+ operationFailed: 'Operation failed',
809
+ noRoutesFound: 'No Components Found',
810
+ };
811
+ }
812
+ if (!translations.zh.themeTranslations) {
813
+ translations.zh.themeTranslations = {
814
+ search: '搜索组件...',
815
+ createNewBlock: '创建新组件',
816
+ name: '名称',
817
+ description: '描述',
818
+ create: '创建',
819
+ blockNameRequired: '组件名称必填',
820
+ blockNameExists: '组件名称已存在,请更改',
821
+ failedCreateBlock: '创建组件失败',
822
+ createResource: '创建资源',
823
+ failedLoadCode: '加载组件代码失败',
824
+ interfaceToProperties: '接口 → 属性',
825
+ propertiesToInterface: '属性 → 接口',
826
+ componentPathNotFound: '无法找到组件路径',
827
+ analyzingInterface: '正在分析组件接口...',
828
+ interfacePropertiesPreview: '接口 → 属性预览',
829
+ currentProperties: '当前属性',
830
+ newProperties: '新属性',
831
+ confirmUpdateMetadata: '确认后将更新metadata文件。这将保留现有配置值,但可能更改属性结构。',
832
+ metadataSuccess: 'Metadata生成成功!',
833
+ generationFailed: '生成失败',
834
+ previewFailed: '生成预览失败',
835
+ generatingInterface: '正在生成TypeScript接口预览...',
836
+ propertiesInterfacePreview: '属性 → 接口预览',
837
+ currentInterface: '当前接口',
838
+ newInterface: '新接口',
839
+ confirmUpdateInterface: '确认后将更新TypeScript接口。这将覆盖当前的接口定义。',
840
+ interfaceSuccess: 'TypeScript接口生成成功!',
841
+ previewGenerationFailed: '预览失败',
842
+ interfacePreviewFailed: '生成接口预览失败',
843
+ propertyNotFound: '无法找到属性,请检查在 @metadata.json 中,是否存在该属性:',
844
+ connectWallet: '请先连接钱包',
845
+ cancel: '取消',
846
+ confirmUpdate: '确认更新',
847
+ configUpdateConfirmation: '确认后将更新配置。',
848
+ operationFailed: '执行操作失败',
849
+ noRoutesFound: '未找到组件',
850
+ };
851
+ }