@blocklet/ui-react 3.2.16 → 3.2.18
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 +64 -0
- package/lib/Dashboard/app-shell/app-info-context.d.ts +38 -0
- package/lib/Dashboard/app-shell/app-info-context.js +69 -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 +11 -1
- package/lib/Dashboard/index.js +82 -62
- 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 +91 -0
- package/src/Dashboard/app-shell/app-info-context.tsx +157 -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 +16 -2
- 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,91 @@
|
|
|
1
|
+
import { isValidElement } 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
|
+
icon = undefined,
|
|
33
|
+
name = '',
|
|
34
|
+
description = undefined,
|
|
35
|
+
actions = undefined,
|
|
36
|
+
badges = [],
|
|
37
|
+
tabs = [],
|
|
38
|
+
currentTab = '',
|
|
39
|
+
} = useAppInfo();
|
|
40
|
+
|
|
41
|
+
if (!name) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box className="app-header" sx={{ mt: 3, mb: 3 }}>
|
|
47
|
+
{/* Basic Info */}
|
|
48
|
+
<Box
|
|
49
|
+
sx={{
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
gap: 1,
|
|
53
|
+
}}>
|
|
54
|
+
{icon && <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{icon}</Box>}
|
|
55
|
+
<Stack sx={{ flexGrow: 1 }}>
|
|
56
|
+
<Typography variant="h1" sx={{ mb: 0.5 }}>
|
|
57
|
+
{name}
|
|
58
|
+
</Typography>
|
|
59
|
+
{description && (
|
|
60
|
+
<Typography
|
|
61
|
+
variant="body2"
|
|
62
|
+
color="text.secondary"
|
|
63
|
+
sx={{ lineHeight: 1.6, '& a': { color: 'primary.main' }, maxWidth: 980 }}>
|
|
64
|
+
{description}
|
|
65
|
+
</Typography>
|
|
66
|
+
)}
|
|
67
|
+
</Stack>
|
|
68
|
+
{!isMobile && actions && <Box sx={{ ml: 1 }}>{actions}</Box>}
|
|
69
|
+
</Box>
|
|
70
|
+
{/* Badges */}
|
|
71
|
+
{badges.length > 0 && (
|
|
72
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', mt: 2 }}>
|
|
73
|
+
{badges.map((config, index) =>
|
|
74
|
+
isValidElement(config) ? config : <AppBadge key={config.label || index} {...config} />
|
|
75
|
+
)}
|
|
76
|
+
</Box>
|
|
77
|
+
)}
|
|
78
|
+
{isMobile && actions && <Box sx={{ mt: isMobile ? 2 : 0 }}>{actions}</Box>}
|
|
79
|
+
{/* Tabs */}
|
|
80
|
+
{tabs.length <= 1 && <Divider />}
|
|
81
|
+
{tabs.length > 1 && (
|
|
82
|
+
<>
|
|
83
|
+
<Tabs tabs={tabs} current={currentTab} onChange={onTabChange} scrollButtons="auto" sx={{ mt: 2.5 }} />
|
|
84
|
+
<Divider sx={{ mt: 0 }} />
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default AppHeader;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React, { createContext, useState, useContext, useMemo, useEffect } 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
|
+
currentTab: string;
|
|
36
|
+
TabComponent: React.ComponentType | React.ReactNode;
|
|
37
|
+
navItem?: NavItem;
|
|
38
|
+
updateAppInfo: (patch: Partial<AppInfo>) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AppInfoContext = createContext<AppInfoContextValue>({
|
|
42
|
+
currentTab: '',
|
|
43
|
+
TabComponent: null,
|
|
44
|
+
navItem: undefined,
|
|
45
|
+
updateAppInfo: () => {},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const getI18nVal = (obj: Record<string, string | Record<Locale, string>>, key: string, locale: Locale = 'en') => {
|
|
49
|
+
const val = obj?.[key];
|
|
50
|
+
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
return isPlainObject(val) ? val[locale] || val.en : val;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
interface AppInfoProviderProps {
|
|
56
|
+
path?: string;
|
|
57
|
+
currentTab?: string;
|
|
58
|
+
meta?: object;
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
}
|
|
61
|
+
export function AppInfoProvider({
|
|
62
|
+
path = window?.location?.pathname || '',
|
|
63
|
+
currentTab = '',
|
|
64
|
+
meta = undefined,
|
|
65
|
+
children = null,
|
|
66
|
+
}: AppInfoProviderProps) {
|
|
67
|
+
const { locale = 'en' } = useLocaleContext() || {};
|
|
68
|
+
const targetLink = withoutTrailingSlash(path);
|
|
69
|
+
const inService = targetLink.startsWith(WELLKNOWN_BLOCKLET_ADMIN_PATH);
|
|
70
|
+
// 便于测试
|
|
71
|
+
const blockletMeta = useCreation(() => Object.assign({}, window.blocklet, meta), [meta]);
|
|
72
|
+
|
|
73
|
+
// 匹配 Dashboard NavItem
|
|
74
|
+
const navItem = useCreation(() => {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
const navigations = blockletMeta.navigation?.filter((v) =>
|
|
77
|
+
Array.isArray(v.section) ? v.section.includes('dashboard') : v.section === 'dashboard'
|
|
78
|
+
);
|
|
79
|
+
const items: NavItem[] = navigations?.flatMap((v: { items?: NavItem[] }) => v.items || []);
|
|
80
|
+
|
|
81
|
+
// 根据路由查找 navItem
|
|
82
|
+
const item = items?.find((v) => {
|
|
83
|
+
const link = getI18nVal(v, 'link', locale) || '';
|
|
84
|
+
return link.startsWith(targetLink);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!item) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parentItem = navigations.find((v: NavItem) => v.id === item.parent);
|
|
92
|
+
let title = getI18nVal(item, 'title', locale) || '';
|
|
93
|
+
|
|
94
|
+
if (!inService) {
|
|
95
|
+
// 应用带上父级标题
|
|
96
|
+
title = `${getI18nVal(parentItem, 'title', locale) || ''} - ${title}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: item.id,
|
|
101
|
+
parent: item.parent,
|
|
102
|
+
title,
|
|
103
|
+
link: getI18nVal(item, 'link', locale) || '',
|
|
104
|
+
description: getI18nVal(item, 'description', locale) || '',
|
|
105
|
+
} as NavItem;
|
|
106
|
+
}, [path, inService, locale, blockletMeta]);
|
|
107
|
+
|
|
108
|
+
const [appInfo, setAppInfo] = useState<AppInfo>({});
|
|
109
|
+
|
|
110
|
+
const updateAppInfo = useMemoizedFn((patch: Partial<AppInfo>) => {
|
|
111
|
+
setAppInfo((prev) => ({ ...prev, ...patch }));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const value = useMemo(
|
|
115
|
+
() => {
|
|
116
|
+
const { tabs = [] } = appInfo;
|
|
117
|
+
const currentTabConfig = tabs.find((v) => v.value === currentTab);
|
|
118
|
+
|
|
119
|
+
let ctab = currentTab;
|
|
120
|
+
let TabComponent = null;
|
|
121
|
+
// fallback 到第一个 tab
|
|
122
|
+
if (!currentTabConfig && tabs.length > 0) {
|
|
123
|
+
ctab = tabs[0].value;
|
|
124
|
+
TabComponent = tabs[0].render || null;
|
|
125
|
+
} else {
|
|
126
|
+
TabComponent = currentTabConfig?.render || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...appInfo,
|
|
131
|
+
navItem,
|
|
132
|
+
currentTab: ctab,
|
|
133
|
+
TabComponent,
|
|
134
|
+
updateAppInfo,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
[appInfo, navItem, currentTab, updateAppInfo]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 非 Service 应用,动态更新 name / description
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!inService) {
|
|
144
|
+
updateAppInfo({
|
|
145
|
+
name: navItem?.title || '',
|
|
146
|
+
description: navItem?.description || '',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}, [navItem?.title, navItem?.description, inService, updateAppInfo]);
|
|
150
|
+
|
|
151
|
+
return <AppInfoContext.Provider value={value}>{children}</AppInfoContext.Provider>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function useAppInfo(): AppInfoContextValue {
|
|
155
|
+
const context = useContext(AppInfoContext);
|
|
156
|
+
return context;
|
|
157
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Box, Typography, useTheme } from '@mui/material';
|
|
2
|
+
import { useMemoizedFn } from 'ahooks';
|
|
3
|
+
import { AppBadgeDefaultProps, BadgeContainer } from './app-badge-default';
|
|
4
|
+
|
|
5
|
+
export interface AppBadgeSwitchProps extends Omit<AppBadgeDefaultProps, 'value' | 'onChange'> {
|
|
6
|
+
value?: unknown;
|
|
7
|
+
trueValue?: unknown;
|
|
8
|
+
falseValue?: unknown;
|
|
9
|
+
onChange?: (checked: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AppBadgeSwitch({
|
|
13
|
+
label = '',
|
|
14
|
+
value = undefined,
|
|
15
|
+
trueValue = true,
|
|
16
|
+
falseValue = false,
|
|
17
|
+
loading = false,
|
|
18
|
+
onChange = undefined,
|
|
19
|
+
...rest
|
|
20
|
+
}: AppBadgeSwitchProps) {
|
|
21
|
+
const theme = useTheme();
|
|
22
|
+
let checked = false;
|
|
23
|
+
const handleClick = useMemoizedFn(() => onChange?.(!checked));
|
|
24
|
+
|
|
25
|
+
if (value === trueValue) {
|
|
26
|
+
checked = true;
|
|
27
|
+
} else if (value === falseValue) {
|
|
28
|
+
checked = false;
|
|
29
|
+
} else {
|
|
30
|
+
checked = Boolean(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<BadgeContainer
|
|
35
|
+
loading={loading}
|
|
36
|
+
sx={{
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
backgroundColor: checked ? 'primary.main' : 'grey.100',
|
|
39
|
+
borderColor: checked ? 'primary.main' : 'divider',
|
|
40
|
+
transition: 'background-color 0.3s ease',
|
|
41
|
+
}}
|
|
42
|
+
onClick={handleClick}
|
|
43
|
+
{...rest}>
|
|
44
|
+
{/* 内容 */}
|
|
45
|
+
<Typography
|
|
46
|
+
sx={{
|
|
47
|
+
fontWeight: 400,
|
|
48
|
+
paddingLeft: checked ? 0 : theme.spacing(2.5),
|
|
49
|
+
paddingRight: checked ? theme.spacing(2.5) : 0,
|
|
50
|
+
color: checked ? 'common.white' : 'text.secondary',
|
|
51
|
+
transition: 'color 0.3s ease',
|
|
52
|
+
}}>
|
|
53
|
+
{label}
|
|
54
|
+
</Typography>
|
|
55
|
+
{/* 滑块 */}
|
|
56
|
+
<Box
|
|
57
|
+
sx={{
|
|
58
|
+
width: theme.spacing(2.5),
|
|
59
|
+
height: theme.spacing(2.5),
|
|
60
|
+
borderRadius: `${(theme.shape.borderRadius as number) * 0.5}px`,
|
|
61
|
+
backgroundColor: 'common.white',
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
top: '50%',
|
|
64
|
+
left: checked ? `calc(100% - ${theme.spacing(3)})` : theme.spacing(0.5),
|
|
65
|
+
transform: 'translateY(-50%)',
|
|
66
|
+
transition: 'left 0.3s ease',
|
|
67
|
+
boxShadow: 1,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</BadgeContainer>
|
|
71
|
+
);
|
|
72
|
+
}
|