@blocklet/ui-react 3.2.17 → 3.2.19
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.
- package/lib/Dashboard/app-shell/app-badge.d.ts +24 -0
- package/lib/Dashboard/app-shell/app-badge.js +48 -0
- package/lib/Dashboard/app-shell/app-header.d.ts +5 -0
- package/lib/Dashboard/app-shell/app-header.js +72 -0
- package/lib/Dashboard/app-shell/app-info-context.d.ts +42 -0
- package/lib/Dashboard/app-shell/app-info-context.js +83 -0
- package/lib/Dashboard/app-shell/badges/app-badge-default.d.ts +20 -0
- package/lib/Dashboard/app-shell/badges/app-badge-default.js +84 -0
- package/lib/Dashboard/app-shell/badges/app-badge-did.d.ts +5 -0
- package/lib/Dashboard/app-shell/badges/app-badge-did.js +16 -0
- package/lib/Dashboard/app-shell/badges/app-badge-state.d.ts +6 -0
- package/lib/Dashboard/app-shell/badges/app-badge-state.js +34 -0
- package/lib/Dashboard/app-shell/badges/app-badge-switch.d.ts +8 -0
- package/lib/Dashboard/app-shell/badges/app-badge-switch.js +66 -0
- package/lib/Dashboard/app-shell/badges/app-badge-version.d.ts +14 -0
- package/lib/Dashboard/app-shell/badges/app-badge-version.js +50 -0
- package/lib/Dashboard/app-shell/index.d.ts +4 -0
- package/lib/Dashboard/app-shell/index.js +9 -0
- package/lib/Dashboard/index.d.ts +12 -2
- package/lib/Dashboard/index.js +83 -63
- package/lib/Footer/internal-footer.js +11 -11
- package/lib/Footer/links.d.ts +5 -3
- package/lib/Footer/links.js +63 -61
- package/lib/utils.js +28 -28
- package/package.json +12 -6
- package/src/Dashboard/app-shell/app-badge.stories.tsx +64 -0
- package/src/Dashboard/app-shell/app-badge.tsx +94 -0
- package/src/Dashboard/app-shell/app-header.tsx +104 -0
- package/src/Dashboard/app-shell/app-info-context.tsx +182 -0
- package/src/Dashboard/app-shell/badges/app-badge-default.tsx +131 -0
- package/src/Dashboard/app-shell/badges/app-badge-did.tsx +28 -0
- package/src/Dashboard/app-shell/badges/app-badge-state.tsx +40 -0
- package/src/Dashboard/app-shell/badges/app-badge-switch.tsx +72 -0
- package/src/Dashboard/app-shell/badges/app-badge-version.tsx +60 -0
- package/src/Dashboard/app-shell/index.ts +5 -0
- package/src/Dashboard/index.jsx +17 -3
- package/src/Footer/internal-footer.jsx +1 -1
- package/src/Footer/links.jsx +11 -7
- package/src/utils.js +6 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Box, Stack, Typography } from '@mui/material';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ThemeProvider } from '@arcblock/ux/lib/Theme';
|
|
4
|
+
import { ThemeModeToggle } from '@arcblock/ux/lib/Config';
|
|
5
|
+
import AppBadge from './app-badge';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Blocklet-UI-React/Dashboard',
|
|
9
|
+
component: AppBadge,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Badges() {
|
|
13
|
+
const [checked, setChecked] = useState(true);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<ThemeProvider prefer="system">
|
|
17
|
+
<ThemeModeToggle />
|
|
18
|
+
<Box sx={{ p: 3, backgroundColor: 'background.default' }}>
|
|
19
|
+
<Stack spacing={2}>
|
|
20
|
+
<Typography variant="h6">Loading</Typography>
|
|
21
|
+
<Stack direction="row" spacing={2}>
|
|
22
|
+
<AppBadge label="Loading" value="Value" loading />
|
|
23
|
+
</Stack>
|
|
24
|
+
<Typography variant="h6">Default</Typography>
|
|
25
|
+
<Stack direction="row" spacing={2}>
|
|
26
|
+
<AppBadge label="Label" value="Value" />
|
|
27
|
+
<AppBadge icon="lucide:info" label="Label" value="Value" />
|
|
28
|
+
</Stack>
|
|
29
|
+
<Typography variant="h6">Number</Typography>
|
|
30
|
+
<Stack direction="row" spacing={2}>
|
|
31
|
+
<AppBadge variant="number" value={1234} />
|
|
32
|
+
<AppBadge variant="number" value={1234567} round={1} />
|
|
33
|
+
<AppBadge variant="number" label="Label" value={1234567890} round={2} />
|
|
34
|
+
</Stack>
|
|
35
|
+
<Typography variant="h6">Time</Typography>
|
|
36
|
+
<Stack direction="row" spacing={2}>
|
|
37
|
+
<AppBadge variant="time" value={new Date()} />
|
|
38
|
+
<AppBadge variant="time" label="Label" value={Date.now() - 3600000} />
|
|
39
|
+
</Stack>
|
|
40
|
+
<Typography variant="h6">State</Typography>
|
|
41
|
+
<Stack direction="row" spacing={2}>
|
|
42
|
+
<AppBadge variant="state" value="Running" color="success" />
|
|
43
|
+
<AppBadge variant="state" value="Stopped" color="error" />
|
|
44
|
+
<AppBadge variant="state" value="Warning" color="warning" />
|
|
45
|
+
</Stack>
|
|
46
|
+
<Typography variant="h6">Version</Typography>
|
|
47
|
+
<Stack direction="row" spacing={2}>
|
|
48
|
+
<AppBadge variant="version" value="1.0.0" color="info" />
|
|
49
|
+
<AppBadge variant="version" icon="lucide:package" value="2.3.4" color="primary" />
|
|
50
|
+
</Stack>
|
|
51
|
+
<Typography variant="h6">DID</Typography>
|
|
52
|
+
<Stack direction="row" spacing={2}>
|
|
53
|
+
<AppBadge variant="did" value="z1kFxaZ5hJXHhqJzXqJzXqJzXqJzXqJzXqJz" />
|
|
54
|
+
<AppBadge variant="did" value="0x1234567890123456789012345678901234567890" />
|
|
55
|
+
</Stack>
|
|
56
|
+
<Typography variant="h6">Switch</Typography>
|
|
57
|
+
<Stack direction="row" spacing={2}>
|
|
58
|
+
<AppBadge variant="switch" label="Toggle" value={checked} onChange={setChecked} />
|
|
59
|
+
</Stack>
|
|
60
|
+
</Stack>
|
|
61
|
+
</Box>
|
|
62
|
+
</ThemeProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
4
|
+
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
5
|
+
import 'dayjs/locale/zh';
|
|
6
|
+
import 'dayjs/locale/zh-tw';
|
|
7
|
+
import 'dayjs/locale/ja';
|
|
8
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
9
|
+
import { AppBadgeDefault, AppBadgeDefaultProps } from './badges/app-badge-default';
|
|
10
|
+
import { AppBadgeSwitch, AppBadgeSwitchProps } from './badges/app-badge-switch';
|
|
11
|
+
import { AppBadgeVersion, AppBadgeVersionProps } from './badges/app-badge-version';
|
|
12
|
+
import { AppBadgeState, AppBadgeStateProps } from './badges/app-badge-state';
|
|
13
|
+
import { AppBadgeDID, AppBadgeDIDProps } from './badges/app-badge-did';
|
|
14
|
+
|
|
15
|
+
dayjs.extend(relativeTime);
|
|
16
|
+
dayjs.extend(localizedFormat);
|
|
17
|
+
|
|
18
|
+
export const formatNumber = (num: unknown, round: number = 0): string => {
|
|
19
|
+
if (typeof num !== 'number') return '';
|
|
20
|
+
|
|
21
|
+
const absNum = Math.abs(num);
|
|
22
|
+
let rounded;
|
|
23
|
+
let unit = '';
|
|
24
|
+
|
|
25
|
+
if (absNum >= 1e12) {
|
|
26
|
+
unit = 'T';
|
|
27
|
+
rounded = `${(num / 1e12).toFixed(round)}`; // 万亿
|
|
28
|
+
} else if (absNum >= 1e9) {
|
|
29
|
+
unit = 'B';
|
|
30
|
+
rounded = `${(num / 1e9).toFixed(round)}`; // 十亿
|
|
31
|
+
} else if (absNum >= 1e6) {
|
|
32
|
+
unit = 'M';
|
|
33
|
+
rounded = `${(num / 1e6).toFixed(round)}`; // 百万
|
|
34
|
+
} else if (absNum >= 1e3) {
|
|
35
|
+
unit = 'K';
|
|
36
|
+
rounded = `${(num / 1e3).toFixed(round)}`; // 千
|
|
37
|
+
} else {
|
|
38
|
+
rounded = num.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 去掉尾部的零
|
|
42
|
+
if (round > 0) {
|
|
43
|
+
rounded = rounded.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${rounded}${unit}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const formatTime = (value?: string | number | Date, locale: string = 'en'): string => {
|
|
50
|
+
if (!value) return '';
|
|
51
|
+
|
|
52
|
+
return dayjs(value).locale(locale).fromNow();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const BadgeVariants: Record<string, React.ComponentType<any>> = {
|
|
56
|
+
switch: AppBadgeSwitch,
|
|
57
|
+
version: AppBadgeVersion,
|
|
58
|
+
state: AppBadgeState,
|
|
59
|
+
did: AppBadgeDID,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type AppBadgeProps =
|
|
63
|
+
| (AppBadgeDefaultProps & { variant?: 'default' })
|
|
64
|
+
| (AppBadgeDefaultProps & { variant: 'number' })
|
|
65
|
+
| (Omit<AppBadgeDefaultProps, 'value'> & { variant: 'time'; value?: string | number | Date })
|
|
66
|
+
| (AppBadgeSwitchProps & { variant: 'switch' })
|
|
67
|
+
| (AppBadgeVersionProps & { variant: 'version' })
|
|
68
|
+
| (AppBadgeStateProps & { variant: 'state' })
|
|
69
|
+
| (AppBadgeDIDProps & { variant: 'did' });
|
|
70
|
+
|
|
71
|
+
export default function AppBadge(props: AppBadgeProps) {
|
|
72
|
+
const { locale = 'en' } = useLocaleContext() || {};
|
|
73
|
+
|
|
74
|
+
if (props.variant === 'number') {
|
|
75
|
+
const { value, round = 0, ...rest } = props;
|
|
76
|
+
const val = formatNumber(Number(value), round);
|
|
77
|
+
|
|
78
|
+
return <AppBadgeDefault value={val} {...rest} />;
|
|
79
|
+
}
|
|
80
|
+
if (props.variant === 'time') {
|
|
81
|
+
const { value, ...rest } = props;
|
|
82
|
+
const val = formatTime(value, locale);
|
|
83
|
+
|
|
84
|
+
return <AppBadgeDefault value={val} {...rest} />;
|
|
85
|
+
}
|
|
86
|
+
if (props.variant) {
|
|
87
|
+
const Badge = BadgeVariants[props.variant];
|
|
88
|
+
if (Badge) {
|
|
89
|
+
return <Badge {...props} />;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return <AppBadgeDefault {...(props as AppBadgeDefaultProps)} />;
|
|
94
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isValidElement, useEffect } from 'react';
|
|
2
|
+
import { Box, Stack, Typography, useMediaQuery, SxProps, Theme } from '@mui/material';
|
|
3
|
+
import Tabs from '@arcblock/ux/lib/Tabs';
|
|
4
|
+
import AppBadge from './app-badge';
|
|
5
|
+
import { useAppInfo } from './app-info-context';
|
|
6
|
+
|
|
7
|
+
function useMobile(): boolean {
|
|
8
|
+
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
|
|
9
|
+
|
|
10
|
+
return isMobile;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface DividerProps {
|
|
14
|
+
sx?: SxProps<Theme>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Divider({ sx = {} }: DividerProps) {
|
|
18
|
+
const isMobile = useMobile();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Box sx={{ mx: isMobile ? -2 : -3, borderBottom: '1px solid', borderColor: 'divider', mt: 1.5, mb: 1, ...sx }} />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AppHeaderProps {
|
|
26
|
+
onTabChange?: (tab: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function AppHeader({ onTabChange = undefined }: AppHeaderProps) {
|
|
30
|
+
const isMobile = useMobile();
|
|
31
|
+
const {
|
|
32
|
+
inService,
|
|
33
|
+
navItem,
|
|
34
|
+
icon = undefined,
|
|
35
|
+
name = '',
|
|
36
|
+
description = undefined,
|
|
37
|
+
actions = undefined,
|
|
38
|
+
badges = [],
|
|
39
|
+
tabs = [],
|
|
40
|
+
currentTab = '',
|
|
41
|
+
updateAppInfo,
|
|
42
|
+
} = useAppInfo();
|
|
43
|
+
|
|
44
|
+
// 非 Service 应用,自动更新 name / description
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!inService) {
|
|
47
|
+
updateAppInfo({
|
|
48
|
+
name: navItem?.title || '',
|
|
49
|
+
description: navItem?.description || '',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}, [navItem?.title, navItem?.description, inService, updateAppInfo]);
|
|
53
|
+
|
|
54
|
+
if (!name) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box className="app-header" sx={{ mt: 3, mb: 3 }}>
|
|
60
|
+
{/* Basic Info */}
|
|
61
|
+
<Box
|
|
62
|
+
sx={{
|
|
63
|
+
display: 'flex',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
gap: 1,
|
|
66
|
+
}}>
|
|
67
|
+
{icon && <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{icon}</Box>}
|
|
68
|
+
<Stack sx={{ flexGrow: 1 }}>
|
|
69
|
+
<Typography variant="h1" sx={{ mb: 0.5 }}>
|
|
70
|
+
{name}
|
|
71
|
+
</Typography>
|
|
72
|
+
{description && (
|
|
73
|
+
<Typography
|
|
74
|
+
variant="body2"
|
|
75
|
+
color="text.secondary"
|
|
76
|
+
sx={{ lineHeight: 1.6, '& a': { color: 'primary.main' }, maxWidth: 980 }}>
|
|
77
|
+
{description}
|
|
78
|
+
</Typography>
|
|
79
|
+
)}
|
|
80
|
+
</Stack>
|
|
81
|
+
{!isMobile && actions && <Box sx={{ ml: 1 }}>{actions}</Box>}
|
|
82
|
+
</Box>
|
|
83
|
+
{/* Badges */}
|
|
84
|
+
{badges.length > 0 && (
|
|
85
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', mt: 2 }}>
|
|
86
|
+
{badges.map((config, index) =>
|
|
87
|
+
isValidElement(config) ? config : <AppBadge key={config.label || index} {...config} />
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
{isMobile && actions && <Box sx={{ mt: isMobile ? 2 : 0 }}>{actions}</Box>}
|
|
92
|
+
{/* Tabs */}
|
|
93
|
+
{tabs.length <= 1 && <Divider />}
|
|
94
|
+
{tabs.length > 1 && (
|
|
95
|
+
<>
|
|
96
|
+
<Tabs tabs={tabs} current={currentTab} onChange={onTabChange} scrollButtons="auto" sx={{ mt: 2.5 }} />
|
|
97
|
+
<Divider sx={{ mt: 0 }} />
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</Box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default AppHeader;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { createContext, useState, useContext, useMemo } from 'react';
|
|
2
|
+
import { useCreation, useMemoizedFn } from 'ahooks';
|
|
3
|
+
import { withoutTrailingSlash } from 'ufo';
|
|
4
|
+
import isPlainObject from 'lodash/isPlainObject';
|
|
5
|
+
import { WELLKNOWN_BLOCKLET_ADMIN_PATH } from '@abtnode/constant';
|
|
6
|
+
import { Locale } from '@arcblock/ux/lib/type';
|
|
7
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
|
+
import { AppBadgeProps } from './app-badge';
|
|
9
|
+
|
|
10
|
+
export type NavItem = {
|
|
11
|
+
id: string;
|
|
12
|
+
parent: string; // pid
|
|
13
|
+
title: string;
|
|
14
|
+
link: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface TabConfig {
|
|
19
|
+
value: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
render?: React.ComponentType | React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AppInfo {
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
name?: React.ReactNode;
|
|
27
|
+
description?: React.ReactNode;
|
|
28
|
+
badges?: Array<AppBadgeProps | React.ReactElement>;
|
|
29
|
+
actions?: React.ReactNode;
|
|
30
|
+
tabs?: TabConfig[];
|
|
31
|
+
currentTab?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AppInfoContextValue extends AppInfo {
|
|
35
|
+
inService: boolean;
|
|
36
|
+
currentTab: string;
|
|
37
|
+
TabComponent: React.ComponentType | React.ReactNode;
|
|
38
|
+
navItem?: NavItem;
|
|
39
|
+
updateAppInfo: (patch: Partial<AppInfo>) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const AppInfoContext = createContext<AppInfoContextValue>({
|
|
43
|
+
inService: false,
|
|
44
|
+
currentTab: '',
|
|
45
|
+
TabComponent: null,
|
|
46
|
+
navItem: undefined,
|
|
47
|
+
updateAppInfo: () => {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const getI18nVal = (
|
|
51
|
+
obj: Record<string, string | Record<Locale, string>>,
|
|
52
|
+
key: string,
|
|
53
|
+
locale: Locale = 'en'
|
|
54
|
+
) => {
|
|
55
|
+
const val = obj?.[key];
|
|
56
|
+
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
return isPlainObject(val) ? val[locale] || val.en : val;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 基于前缀匹配算法查找 NavItem
|
|
62
|
+
export const findNavItem = (items: NavItem[], targetLink: string = '', locale: Locale = 'en') => {
|
|
63
|
+
const targetParts = targetLink === '/' ? ['/'] : targetLink.split('/').filter(Boolean);
|
|
64
|
+
|
|
65
|
+
let result: NavItem | null = null;
|
|
66
|
+
let maxLen = 0;
|
|
67
|
+
|
|
68
|
+
items.forEach((item) => {
|
|
69
|
+
const currentLink = getI18nVal(item, 'link', locale) || '';
|
|
70
|
+
const currentParts = currentLink === '/' ? ['/'] : currentLink.split('/').filter(Boolean);
|
|
71
|
+
|
|
72
|
+
if (currentParts.length > targetParts.length) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let isMatch = true;
|
|
77
|
+
for (let i = 0; i < currentParts.length; i++) {
|
|
78
|
+
if (currentParts[i] !== targetParts[i]) {
|
|
79
|
+
isMatch = false;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isMatch && currentParts.length > maxLen) {
|
|
85
|
+
result = item;
|
|
86
|
+
maxLen = currentParts.length;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
interface AppInfoProviderProps {
|
|
94
|
+
path?: string;
|
|
95
|
+
currentTab?: string;
|
|
96
|
+
meta?: object;
|
|
97
|
+
children?: React.ReactNode;
|
|
98
|
+
}
|
|
99
|
+
export function AppInfoProvider({
|
|
100
|
+
path = window?.location?.pathname || '',
|
|
101
|
+
currentTab = '',
|
|
102
|
+
meta = undefined,
|
|
103
|
+
children = null,
|
|
104
|
+
}: AppInfoProviderProps) {
|
|
105
|
+
const { locale = 'en' } = useLocaleContext() || {};
|
|
106
|
+
const targetLink = withoutTrailingSlash(path);
|
|
107
|
+
const inService = targetLink.startsWith(WELLKNOWN_BLOCKLET_ADMIN_PATH);
|
|
108
|
+
// 便于测试
|
|
109
|
+
const blockletMeta = useCreation(() => Object.assign({}, window.blocklet, meta), [meta]);
|
|
110
|
+
|
|
111
|
+
// 匹配 Dashboard NavItem
|
|
112
|
+
const navItem = useCreation(() => {
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
const navigations = blockletMeta.navigation?.filter((v) =>
|
|
115
|
+
Array.isArray(v.section) ? v.section.includes('dashboard') : v.section === 'dashboard'
|
|
116
|
+
);
|
|
117
|
+
const items: NavItem[] = navigations?.flatMap((v: { items?: NavItem[] }) => v.items || []);
|
|
118
|
+
// 前缀匹配 navItem
|
|
119
|
+
const item = findNavItem(items, targetLink, locale);
|
|
120
|
+
|
|
121
|
+
if (!item) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const parentItem = navigations.find((v: NavItem) => v.id === item.parent);
|
|
126
|
+
let title = getI18nVal(item, 'title', locale) || '';
|
|
127
|
+
|
|
128
|
+
if (!inService) {
|
|
129
|
+
// 应用带上父级标题
|
|
130
|
+
title = `${getI18nVal(parentItem, 'title', locale) || ''} - ${title}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: item.id,
|
|
135
|
+
parent: item.parent,
|
|
136
|
+
title,
|
|
137
|
+
link: getI18nVal(item, 'link', locale) || '',
|
|
138
|
+
description: getI18nVal(item, 'description', locale) || '',
|
|
139
|
+
} as NavItem;
|
|
140
|
+
}, [path, inService, locale, blockletMeta]);
|
|
141
|
+
|
|
142
|
+
const [appInfo, setAppInfo] = useState<AppInfo>({});
|
|
143
|
+
|
|
144
|
+
const updateAppInfo = useMemoizedFn((patch: Partial<AppInfo>) => {
|
|
145
|
+
setAppInfo((prev) => ({ ...prev, ...patch }));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const value = useMemo(
|
|
149
|
+
() => {
|
|
150
|
+
const { tabs = [] } = appInfo;
|
|
151
|
+
const currentTabConfig = tabs.find((v) => v.value === currentTab);
|
|
152
|
+
|
|
153
|
+
let ctab = currentTab;
|
|
154
|
+
let TabComponent = null;
|
|
155
|
+
// fallback 到第一个 tab
|
|
156
|
+
if (!currentTabConfig && tabs.length > 0) {
|
|
157
|
+
ctab = tabs[0].value;
|
|
158
|
+
TabComponent = tabs[0].render || null;
|
|
159
|
+
} else {
|
|
160
|
+
TabComponent = currentTabConfig?.render || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...appInfo,
|
|
165
|
+
inService,
|
|
166
|
+
navItem,
|
|
167
|
+
currentTab: ctab,
|
|
168
|
+
TabComponent,
|
|
169
|
+
updateAppInfo,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
|
+
[appInfo, navItem, inService, currentTab, updateAppInfo]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return <AppInfoContext.Provider value={value}>{children}</AppInfoContext.Provider>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function useAppInfo(): AppInfoContextValue {
|
|
180
|
+
const context = useContext(AppInfoContext);
|
|
181
|
+
return context;
|
|
182
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Box, Skeleton, styled, Typography } from '@mui/material';
|
|
2
|
+
import { Icon } from '@iconify/react';
|
|
3
|
+
import { TBox } from '@arcblock/ux/lib/MuiWrap';
|
|
4
|
+
import { Link } from 'react-router-dom';
|
|
5
|
+
import { BoxProps } from '@mui/material/Box';
|
|
6
|
+
|
|
7
|
+
export const isExternal = (to: string = ''): boolean => {
|
|
8
|
+
return to.startsWith('http:') || to.startsWith('https:');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const isSameOrigin = (to: string): boolean => {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(to);
|
|
14
|
+
return url.origin === window.location.origin;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const StateIcon = styled('span')<{ color: string }>(({ theme, color }) => {
|
|
21
|
+
return {
|
|
22
|
+
position: 'relative',
|
|
23
|
+
display: 'inline-block',
|
|
24
|
+
width: theme.spacing(1),
|
|
25
|
+
height: theme.spacing(1),
|
|
26
|
+
borderRadius: '50%',
|
|
27
|
+
backgroundColor: color,
|
|
28
|
+
marginRight: theme.spacing(1),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
export const StyledBadge = styled(Box)(({ theme }) => ({
|
|
32
|
+
position: 'relative',
|
|
33
|
+
display: 'inline-flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
height: theme.spacing(3),
|
|
36
|
+
paddingLeft: theme.spacing(1.5),
|
|
37
|
+
paddingRight: theme.spacing(1.5),
|
|
38
|
+
border: '1px solid',
|
|
39
|
+
borderColor: theme.palette.divider,
|
|
40
|
+
borderRadius: (theme.shape.borderRadius as number) * 0.5,
|
|
41
|
+
fontSize: 12,
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
'& .app-badge-label': {
|
|
44
|
+
display: 'inline-flex',
|
|
45
|
+
gap: theme.spacing(1),
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
lineHeight: theme.spacing(3),
|
|
48
|
+
marginLeft: theme.spacing(-1.5),
|
|
49
|
+
paddingLeft: theme.spacing(1.5),
|
|
50
|
+
marginRight: theme.spacing(1),
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
backgroundColor: theme.palette.grey[100],
|
|
53
|
+
color: theme.palette.text.secondary,
|
|
54
|
+
'&::after': {
|
|
55
|
+
content: '""',
|
|
56
|
+
height: theme.spacing(3),
|
|
57
|
+
borderRight: '1px solid',
|
|
58
|
+
borderColor: theme.palette.divider,
|
|
59
|
+
backgroundColor: theme.palette.grey[100],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
'& .app-badge-value': {
|
|
63
|
+
color: theme.palette.text.primary,
|
|
64
|
+
fontSize: 12,
|
|
65
|
+
},
|
|
66
|
+
})) as typeof TBox;
|
|
67
|
+
|
|
68
|
+
export interface BadgeContainerProps extends Omit<BoxProps, 'to'> {
|
|
69
|
+
loading?: boolean;
|
|
70
|
+
to?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function BadgeContainer({ loading = false, children = null, to = '', ...rest }: BadgeContainerProps) {
|
|
74
|
+
const container = (
|
|
75
|
+
<StyledBadge className="app-badge" component={to ? 'a' : 'div'} href={to} {...rest}>
|
|
76
|
+
{children}
|
|
77
|
+
</StyledBadge>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (loading) {
|
|
81
|
+
return <Skeleton variant="rounded">{container}</Skeleton>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (to) {
|
|
85
|
+
if (isExternal(to)) {
|
|
86
|
+
const target = isSameOrigin(to) ? '_self' : '_blank';
|
|
87
|
+
|
|
88
|
+
// 外部链接跳转
|
|
89
|
+
return (
|
|
90
|
+
<StyledBadge className="app-badge" component="a" href={to} target={target} rel="noreferrer noopener" {...rest}>
|
|
91
|
+
{children}
|
|
92
|
+
</StyledBadge>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 内部路由跳转
|
|
97
|
+
return (
|
|
98
|
+
<StyledBadge className="app-badge" component={Link} to={to} {...rest}>
|
|
99
|
+
{children}
|
|
100
|
+
</StyledBadge>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 无跳转
|
|
105
|
+
return (
|
|
106
|
+
<StyledBadge className="app-badge" component="div" {...rest}>
|
|
107
|
+
{children}
|
|
108
|
+
</StyledBadge>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface AppBadgeDefaultProps extends BadgeContainerProps {
|
|
113
|
+
icon?: string;
|
|
114
|
+
label?: string;
|
|
115
|
+
value?: string | number;
|
|
116
|
+
round?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function AppBadgeDefault({ icon = '', label = '', value = '', loading = false, ...rest }: AppBadgeDefaultProps) {
|
|
120
|
+
return (
|
|
121
|
+
<BadgeContainer loading={loading} {...rest}>
|
|
122
|
+
{label && (
|
|
123
|
+
<Typography className="app-badge-label">
|
|
124
|
+
{icon && <Icon icon={icon} />}
|
|
125
|
+
{label}
|
|
126
|
+
</Typography>
|
|
127
|
+
)}
|
|
128
|
+
<Typography className="app-badge-value">{value}</Typography>
|
|
129
|
+
</BadgeContainer>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCreation } from 'ahooks';
|
|
2
|
+
import { Typography } from '@mui/material';
|
|
3
|
+
import { isEthereumDid } from '@arcblock/ux/lib/Util';
|
|
4
|
+
import DidAddress from '@arcblock/ux/lib/Address';
|
|
5
|
+
import { AppBadgeDefaultProps, BadgeContainer } from './app-badge-default';
|
|
6
|
+
|
|
7
|
+
export interface AppBadgeDIDProps extends Omit<AppBadgeDefaultProps, 'value'> {
|
|
8
|
+
value?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AppBadgeDID({ value = '', loading = false, ...rest }: AppBadgeDIDProps) {
|
|
12
|
+
const label = useCreation(() => {
|
|
13
|
+
const isEthDid = isEthereumDid(value);
|
|
14
|
+
|
|
15
|
+
return `DID:${isEthDid ? 'ETH' : 'ABT'}`;
|
|
16
|
+
}, [value]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<BadgeContainer loading={loading} {...rest}>
|
|
20
|
+
<Typography className="app-badge-label">{label}</Typography>
|
|
21
|
+
<Typography className="app-badge-value">
|
|
22
|
+
<DidAddress inline size={14} compact responsive={false}>
|
|
23
|
+
{value}
|
|
24
|
+
</DidAddress>
|
|
25
|
+
</Typography>
|
|
26
|
+
</BadgeContainer>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useTheme, alpha, Typography, styled } from '@mui/material';
|
|
2
|
+
import { AppBadgeDefaultProps, BadgeContainer } from './app-badge-default';
|
|
3
|
+
import { BadgeColor, getBgColor, getTextColor } from './app-badge-version';
|
|
4
|
+
|
|
5
|
+
const StateIcon = styled('span')<{ color: string }>(({ theme, color }) => {
|
|
6
|
+
return {
|
|
7
|
+
position: 'relative',
|
|
8
|
+
display: 'inline-block',
|
|
9
|
+
width: theme.spacing(1),
|
|
10
|
+
height: theme.spacing(1),
|
|
11
|
+
borderRadius: '50%',
|
|
12
|
+
backgroundColor: color,
|
|
13
|
+
marginRight: theme.spacing(1),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export interface AppBadgeStateProps extends AppBadgeDefaultProps {
|
|
18
|
+
color?: BadgeColor;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AppBadgeState({ value = '', color = 'primary', loading = false, ...rest }: AppBadgeStateProps) {
|
|
22
|
+
const theme = useTheme();
|
|
23
|
+
const txtcolor = getTextColor(theme, color);
|
|
24
|
+
const bgcolor = getBgColor(theme, color);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<BadgeContainer
|
|
28
|
+
loading={loading}
|
|
29
|
+
sx={{
|
|
30
|
+
borderColor: alpha(txtcolor, 0.2),
|
|
31
|
+
bgcolor,
|
|
32
|
+
}}
|
|
33
|
+
{...rest}>
|
|
34
|
+
<StateIcon color={getTextColor(theme, color)} />
|
|
35
|
+
<Typography className="app-badge-value" style={{ color: txtcolor }}>
|
|
36
|
+
{value}
|
|
37
|
+
</Typography>
|
|
38
|
+
</BadgeContainer>
|
|
39
|
+
);
|
|
40
|
+
}
|