docs: DatePicker design tab (#40745)

* docs: DatePicker design tab

* docs: add design demo

* docs: add anchor for design demo

* docs: init g6

* docs: behavior map

* test: fix test cov

* docs: behavior map comp

* docs: add map title

* docs: fix ssr

* docs: update demo

* docs: optimize copy ux

* docs: update demo

* chore: code clean
This commit is contained in:
MadCcc 2023-02-20 10:51:18 +08:00 committed by GitHub
parent 33275b6e80
commit 853283b7e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1508 additions and 619 deletions

View File

@ -0,0 +1,556 @@
import {
CheckOutlined,
LinkOutlined,
SnippetsOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import type { Project } from '@stackblitz/sdk';
import stackblitzSdk from '@stackblitz/sdk';
import { Alert, Badge, Space, Tooltip } from 'antd';
import classNames from 'classnames';
import LZString from 'lz-string';
import React, { useContext, useEffect, useRef, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import type { IPreviewerProps } from 'dumi';
import { FormattedMessage } from 'dumi';
import Prism from 'prismjs';
import JsonML from 'jsonml.js/lib/utils';
import toReactElement from 'jsonml-to-react-element';
import { ping } from '../../utils';
import ClientOnly from '../../common/ClientOnly';
import BrowserFrame from '../../common/BrowserFrame';
import EditButton from '../../common/EditButton';
import CodePenIcon from '../../common/CodePenIcon';
import CodePreview from '../../common/CodePreview';
import CodeSandboxIcon from '../../common/CodeSandboxIcon';
import RiddleIcon from '../../common/RiddleIcon';
import ExternalLinkIcon from '../../common/ExternalLinkIcon';
import type { SiteContextProps } from '../../slots/SiteContext';
import SiteContext from '../../slots/SiteContext';
import useLocation from '../../../hooks/useLocation';
const { ErrorBoundary } = Alert;
function toReactComponent(jsonML: any) {
return toReactElement(jsonML, [
[
(node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre',
(node: any, index: any) => {
// ref: https://github.com/benjycui/bisheng/blob/master/packages/bisheng/src/bisheng-plugin-highlight/lib/browser.js#L7
const attr = JsonML.getAttributes(node);
return React.createElement(
'pre',
{
key: index,
className: `language-${attr.lang}`,
},
React.createElement('code', {
dangerouslySetInnerHTML: { __html: attr.highlighted },
}),
);
},
],
]);
}
function compress(string: string): string {
return LZString.compressToBase64(string)
.replace(/\+/g, '-') // Convert '+' to '-'
.replace(/\//g, '_') // Convert '/' to '_'
.replace(/=+$/, ''); // Remove ending '='
}
const track = ({ type, demo }: { type: string; demo: string }) => {
if (!window.gtag) {
return;
}
window.gtag('event', 'demo', { event_category: type, event_label: demo });
};
let pingDeferrer: PromiseLike<boolean>;
function useShowRiddleButton() {
const [showRiddleButton, setShowRiddleButton] = useState(false);
useEffect(() => {
pingDeferrer ??= new Promise<boolean>((resolve) => {
ping((status) => {
if (status !== 'timeout' && status !== 'error') {
return resolve(true);
}
return resolve(false);
});
});
pingDeferrer.then(setShowRiddleButton);
}, []);
return showRiddleButton;
}
const CodePreviewer: React.FC<IPreviewerProps> = (props) => {
const {
asset,
expand,
iframe,
demoUrl,
children,
title,
description,
debug,
jsx,
style,
compact,
background,
filePath,
version,
} = props;
const location = useLocation();
const entryCode = asset.dependencies['index.tsx'].value;
const showRiddleButton = useShowRiddleButton();
const liveDemo = useRef<React.ReactNode>(null);
const anchorRef = useRef<HTMLAnchorElement>(null);
const codeSandboxIconRef = useRef<HTMLFormElement>(null);
const riddleIconRef = useRef<HTMLFormElement>(null);
const codepenIconRef = useRef<HTMLFormElement>(null);
const [codeExpand, setCodeExpand] = useState<boolean>(false);
const [copyTooltipOpen, setCopyTooltipOpen] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const [codeType, setCodeType] = useState<string>('tsx');
const { theme } = useContext<SiteContextProps>(SiteContext);
const { hash, pathname, search } = location;
const docsOnlineUrl = `https://ant.design${pathname}${search}#${asset.id}`;
const [showOnlineUrl, setShowOnlineUrl] = useState<boolean>(false);
const highlightedCodes = {
jsx: Prism.highlight(jsx, Prism.languages.javascript, 'jsx'),
tsx: Prism.highlight(entryCode, Prism.languages.javascript, 'jsx'),
};
const highlightedStyle = style ? Prism.highlight(style, Prism.languages.css, 'css') : '';
useEffect(() => {
const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses
setShowOnlineUrl(
process.env.NODE_ENV === 'development' || regexp.test(window.location.hostname),
);
}, []);
const handleCodeExpand = (demo: string) => {
setCodeExpand((prev) => !prev);
track({ type: 'expand', demo });
};
const handleCodeCopied = (demo: string) => {
setCopied(true);
track({ type: 'copy', demo });
};
const onCopyTooltipOpenChange = (open: boolean) => {
setCopyTooltipOpen(open);
if (open) {
setCopied(false);
}
};
useEffect(() => {
if (asset.id === hash.slice(1)) {
anchorRef.current?.click();
}
}, []);
useEffect(() => {
setCodeExpand(expand);
}, [expand]);
if (!liveDemo.current) {
liveDemo.current = iframe ? (
<BrowserFrame>
<iframe
src={demoUrl}
height={iframe === true ? undefined : iframe}
title="demo"
className="iframe-demo"
/>
</BrowserFrame>
) : (
children
);
}
const codeBoxClass = classNames('code-box', {
expand: codeExpand,
'code-box-debug': debug,
});
const localizedTitle = title;
const introChildren = <div dangerouslySetInnerHTML={{ __html: description }} />;
const highlightClass = classNames('highlight-wrapper', {
'highlight-wrapper-expand': codeExpand,
});
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
</head>
<body>
<div id="container" style="padding: 24px" />
<script>const mountNode = document.getElementById('container');</script>
</body>
</html>
`;
const tsconfig = `
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
}
}
`;
const suffix = codeType === 'tsx' ? 'tsx' : 'js';
const dependencies: Record<PropertyKey, string> = jsx.split('\n').reduce(
(acc, line) => {
const matches = line.match(/import .+? from '(.+)';$/);
if (matches && matches[1] && !line.includes('antd')) {
const paths = matches[1].split('/');
if (paths.length) {
const dep = paths[0].startsWith('@') ? `${paths[0]}/${paths[1]}` : paths[0];
acc[dep] = 'latest';
}
}
return acc;
},
{ antd: version },
);
dependencies['@ant-design/icons'] = 'latest';
if (suffix === 'tsx') {
dependencies['@types/react'] = '^18.0.0';
dependencies['@types/react-dom'] = '^18.0.0';
}
dependencies.react = '^18.0.0';
dependencies['react-dom'] = '^18.0.0';
const codepenPrefillConfig = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
html,
js: `const { createRoot } = ReactDOM;\n${jsx
.replace(/import\s+(?:React,\s+)?{(\s+[^}]*\s+)}\s+from\s+'react'/, `const { $1 } = React;`)
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'antd';/, 'const { $1 } = antd;')
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'@ant-design\/icons';/, 'const { $1 } = icons;')
.replace("import moment from 'moment';", '')
.replace("import React from 'react';", '')
.replace(/import\s+{\s+(.*)\s+}\s+from\s+'react-router';/, 'const { $1 } = ReactRouter;')
.replace(
/import\s+{\s+(.*)\s+}\s+from\s+'react-router-dom';/,
'const { $1 } = ReactRouterDOM;',
)
.replace(/([A-Za-z]*)\s+as\s+([A-Za-z]*)/, '$1:$2')
.replace(
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
editors: '001',
css: '',
js_external: [
'react@18/umd/react.development.js',
'react-dom@18/umd/react-dom.development.js',
'dayjs@1/dayjs.min.js',
`antd@${version}/dist/antd-with-locales.js`,
`@ant-design/icons/dist/index.umd.js`,
'react-router-dom/dist/umd/react-router-dom.production.min.js',
'react-router/dist/umd/react-router.production.min.js',
]
.map((url) => `https://unpkg.com/${url}`)
.join(';'),
js_pre_processor: 'typescript',
};
const riddlePrefillConfig = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
js: `${
/import React(\D*)from 'react';/.test(jsx) ? '' : `import React from 'react';\n`
}import { createRoot } from 'react-dom/client';\n${jsx.replace(
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
css: '',
json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2),
};
// Reorder source code
let parsedSourceCode = suffix === 'tsx' ? entryCode : jsx;
let importReactContent = "import React from 'react';";
const importReactReg = /import React(\D*)from 'react';/;
const matchImportReact = parsedSourceCode.match(importReactReg);
if (matchImportReact) {
[importReactContent] = matchImportReact;
parsedSourceCode = parsedSourceCode.replace(importReactReg, '').trim();
}
const demoJsContent = `
${importReactContent}
import './index.css';
${parsedSourceCode}
`.trim();
const indexCssContent = (style || '')
.trim()
.replace(new RegExp(`#${asset.id}\\s*`, 'g'), '')
.replace('</style>', '')
.replace('<style>', '');
const indexJsContent = `
import React from 'react';
import { createRoot } from 'react-dom/client';
import Demo from './demo';
createRoot(document.getElementById('container')).render(<Demo />);
`;
const codesandboxPackage = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
main: 'index.js',
dependencies: {
...dependencies,
react: '^18.0.0',
'react-dom': '^18.0.0',
'react-scripts': '^4.0.0',
},
devDependencies: {
typescript: '^4.0.5',
},
scripts: {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
},
browserslist: ['>0.2%', 'not dead'],
};
const codesanboxPrefillConfig = {
files: {
'package.json': { content: codesandboxPackage },
'index.css': { content: indexCssContent },
[`index.${suffix}`]: { content: indexJsContent },
[`demo.${suffix}`]: { content: demoJsContent },
'index.html': {
content: html,
},
},
};
const stackblitzPrefillConfig: Project = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
template: 'create-react-app',
dependencies,
description: '',
files: {
'index.css': indexCssContent,
[`index.${suffix}`]: indexJsContent,
[`demo.${suffix}`]: demoJsContent,
'index.html': html,
},
};
if (suffix === 'tsx') {
stackblitzPrefillConfig.files['tsconfig.json'] = tsconfig;
}
const backgroundGrey = theme.includes('dark') ? '#303030' : '#f0f2f5';
const codeBoxDemoStyle: React.CSSProperties = {
padding: iframe || compact ? 0 : undefined,
overflow: iframe || compact ? 'hidden' : undefined,
backgroundColor: background === 'grey' ? backgroundGrey : undefined,
};
const codeBox: React.ReactNode = (
<section className={codeBoxClass} id={asset.id}>
<section className="code-box-demo" style={codeBoxDemoStyle}>
<ErrorBoundary>
<React.StrictMode>{liveDemo.current}</React.StrictMode>
</ErrorBoundary>
{style ? <style dangerouslySetInnerHTML={{ __html: style }} /> : null}
</section>
<section className="code-box-meta markdown">
<div className="code-box-title">
<Tooltip title={debug ? <FormattedMessage id="app.demo.debug" /> : ''}>
<a href={`#${asset.id}`} ref={anchorRef}>
{localizedTitle}
</a>
</Tooltip>
<EditButton title={<FormattedMessage id="app.content.edit-demo" />} filename={filePath} />
</div>
<div className="code-box-description">{introChildren}</div>
<Space wrap size="middle" className="code-box-actions">
{showOnlineUrl && (
<Tooltip title={<FormattedMessage id="app.demo.online" />}>
<a
className="code-box-code-action"
target="_blank"
rel="noreferrer"
href={docsOnlineUrl}
>
<LinkOutlined className="code-box-online" />
</a>
</Tooltip>
)}
{showRiddleButton ? (
<form
className="code-box-code-action"
action="//riddle.alibaba-inc.com/riddles/define"
method="POST"
target="_blank"
ref={riddleIconRef}
onClick={() => {
track({ type: 'riddle', demo: asset.id });
riddleIconRef.current?.submit();
}}
>
<input type="hidden" name="data" value={JSON.stringify(riddlePrefillConfig)} />
<Tooltip title={<FormattedMessage id="app.demo.riddle" />}>
<RiddleIcon className="code-box-riddle" />
</Tooltip>
</form>
) : null}
<form
className="code-box-code-action"
action="https://codesandbox.io/api/v1/sandboxes/define"
method="POST"
target="_blank"
ref={codeSandboxIconRef}
onClick={() => {
track({ type: 'codesandbox', demo: asset.id });
codeSandboxIconRef.current?.submit();
}}
>
<input
type="hidden"
name="parameters"
value={compress(JSON.stringify(codesanboxPrefillConfig))}
/>
<Tooltip title={<FormattedMessage id="app.demo.codesandbox" />}>
<CodeSandboxIcon className="code-box-codesandbox" />
</Tooltip>
</form>
<form
className="code-box-code-action"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
ref={codepenIconRef}
onClick={() => {
track({ type: 'codepen', demo: asset.id });
codepenIconRef.current?.submit();
}}
>
<ClientOnly>
<input type="hidden" name="data" value={JSON.stringify(codepenPrefillConfig)} />
</ClientOnly>
<Tooltip title={<FormattedMessage id="app.demo.codepen" />}>
<CodePenIcon className="code-box-codepen" />
</Tooltip>
</form>
<Tooltip title={<FormattedMessage id="app.demo.stackblitz" />}>
<span
className="code-box-code-action"
onClick={() => {
track({ type: 'stackblitz', demo: asset.id });
stackblitzSdk.openProject(stackblitzPrefillConfig, {
openFile: [`demo.${suffix}`],
});
}}
>
<ThunderboltOutlined className="code-box-stackblitz" />
</span>
</Tooltip>
<CopyToClipboard text={entryCode} onCopy={() => handleCodeCopied(asset.id)}>
<Tooltip
open={copyTooltipOpen as boolean}
onOpenChange={onCopyTooltipOpenChange}
title={<FormattedMessage id={`app.demo.${copied ? 'copied' : 'copy'}`} />}
>
{React.createElement(copied && copyTooltipOpen ? CheckOutlined : SnippetsOutlined, {
className: 'code-box-code-copy code-box-code-action',
})}
</Tooltip>
</CopyToClipboard>
<Tooltip title={<FormattedMessage id="app.demo.separate" />}>
<a className="code-box-code-action" target="_blank" rel="noreferrer" href={demoUrl}>
<ExternalLinkIcon className="code-box-separate" />
</a>
</Tooltip>
<Tooltip
title={<FormattedMessage id={`app.demo.code.${codeExpand ? 'hide' : 'show'}`} />}
>
<div className="code-expand-icon code-box-code-action">
<img
alt="expand code"
src={
theme?.includes('dark')
? 'https://gw.alipayobjects.com/zos/antfincdn/btT3qDZn1U/wSAkBuJFbdxsosKKpqyq.svg'
: 'https://gw.alipayobjects.com/zos/antfincdn/Z5c7kzvi30/expand.svg'
}
className={codeExpand ? 'code-expand-icon-hide' : 'code-expand-icon-show'}
onClick={() => handleCodeExpand(asset.id)}
/>
<img
alt="expand code"
src={
theme?.includes('dark')
? 'https://gw.alipayobjects.com/zos/antfincdn/CjZPwcKUG3/OpROPHYqWmrMDBFMZtKF.svg'
: 'https://gw.alipayobjects.com/zos/antfincdn/4zAaozCvUH/unexpand.svg'
}
className={codeExpand ? 'code-expand-icon-show' : 'code-expand-icon-hide'}
onClick={() => handleCodeExpand(asset.id)}
/>
</div>
</Tooltip>
</Space>
</section>
<section className={highlightClass} key="code">
<CodePreview
codes={highlightedCodes}
toReactComponent={toReactComponent}
onCodeTypeChange={(type) => setCodeType(type)}
/>
{highlightedStyle ? (
<div key="style" className="highlight">
<pre>
<code className="css" dangerouslySetInnerHTML={{ __html: highlightedStyle }} />
</pre>
</div>
) : null}
</section>
</section>
);
if (version) {
return (
<Badge.Ribbon text={version} color={version.includes('<') ? 'red' : null}>
{codeBox}
</Badge.Ribbon>
);
}
return codeBox;
};
export default CodePreviewer;

View File

@ -0,0 +1,102 @@
import type { FC } from 'react';
import React, { useRef } from 'react';
import type { IPreviewerProps } from 'dumi';
import { createStyles, css } from 'antd-style';
import { CheckOutlined, SketchOutlined } from '@ant-design/icons';
import { nodeToGroup } from 'html2sketch';
import copy from 'copy-to-clipboard';
import { App } from 'antd';
const useStyle = createStyles(({ token }) => ({
wrapper: css`
border: 1px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadius}px;
padding: 20px 24px 40px;
position: relative;
margin-bottom: ${token.marginLG}px;
`,
title: css`
font-size: ${token.fontSizeLG}px;
font-weight: ${token.fontWeightStrong};
color: ${token.colorTextHeading};
&:hover {
color: ${token.colorTextHeading};
}
`,
description: css`
margin-top: ${token.margin}px;
`,
demo: css`
margin-top: ${token.marginLG}px;
display: flex;
justify-content: center;
`,
copy: css`
position: absolute;
inset-inline-end: 20px;
inset-block-start: 20px;
cursor: pointer;
`,
copyTip: css`
color: ${token.colorTextTertiary};
`,
copiedTip: css`
.anticon {
color: ${token.colorSuccess};
}
`,
tip: css`
color: ${token.colorTextTertiary};
margin-top: 40px;
`,
}));
const DesignPreviewer: FC<IPreviewerProps> = ({ children, title, description, tip, asset }) => {
const { styles } = useStyle();
const demoRef = useRef<HTMLDivElement>(null);
const [copied, setCopied] = React.useState<boolean>(false);
const { message } = App.useApp();
const handleCopy = async () => {
try {
const group = await nodeToGroup(demoRef.current);
copy(JSON.stringify(group.toSketchJSON()));
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 5000);
} catch (e) {
console.error(e);
message.error('复制失败');
}
};
return (
<div className={styles.wrapper} id={asset.id}>
<a className={styles.title} href={`#${asset.id}`}>
{title}
</a>
<div className={styles.description} dangerouslySetInnerHTML={{ __html: description }} />
<div className={styles.copy}>
{copied ? (
<div className={styles.copiedTip}>
<CheckOutlined />
<span style={{ marginLeft: 8 }}>使 Kitchen </span>
</div>
) : (
<div onClick={handleCopy} className={styles.copyTip}>
<SketchOutlined />
<span style={{ marginLeft: 8 }}> Sketch JSON</span>
</div>
)}
</div>
<div className={styles.demo} ref={demoRef}>
{children}
</div>
<div className={styles.tip}>{tip}</div>
</div>
);
};
export default DesignPreviewer;

View File

@ -1,95 +0,0 @@
import React, { useEffect, useState } from 'react';
import JsonML from 'jsonml.js/lib/utils';
import toReactComponent from 'jsonml-to-react-element';
import Prism from 'prismjs';
import 'prismjs/components/prism-typescript';
import { useLocation, useIntl, type IPreviewerProps } from 'dumi';
import { ping } from '../../utils';
let pingDeferrer: PromiseLike<boolean>;
function useShowRiddleButton() {
const [showRiddleButton, setShowRiddleButton] = useState(false);
useEffect(() => {
pingDeferrer ??= new Promise<boolean>((resolve) => {
ping((status) => {
if (status !== 'timeout' && status !== 'error') {
return resolve(true);
}
return resolve(false);
});
});
pingDeferrer.then(setShowRiddleButton);
}, []);
return showRiddleButton;
}
/**
* HOC for convert dumi previewer props to bisheng previewer props
*/
export default function fromDumiProps<P extends object>(
WrappedComponent: React.ComponentType<P>,
): React.FC<IPreviewerProps> {
const hoc = function DumiPropsAntdPreviewer(props: IPreviewerProps) {
const showRiddleButton = useShowRiddleButton();
const location = useLocation();
const { asset, children, demoUrl, expand, description = '', ...meta } = props;
const intl = useIntl();
const entryCode = asset.dependencies['index.tsx'].value;
const transformedProps = {
meta: {
id: asset.id,
title: '',
filename: meta.filePath,
...meta,
},
content: description,
preview: () => children,
utils: {
toReactComponent(jsonML: any) {
return toReactComponent(jsonML, [
[
(node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre',
(node: any, index: any) => {
// ref: https://github.com/benjycui/bisheng/blob/master/packages/bisheng/src/bisheng-plugin-highlight/lib/browser.js#L7
const attr = JsonML.getAttributes(node);
return React.createElement(
'pre',
{
key: index,
className: `language-${attr.lang}`,
},
React.createElement('code', {
dangerouslySetInnerHTML: { __html: attr.highlighted },
}),
);
},
],
]);
},
},
intl: { locale: intl.locale },
showRiddleButton,
sourceCodes: {
jsx: meta.jsx,
tsx: entryCode,
},
highlightedCodes: {
jsx: Prism.highlight(meta.jsx, Prism.languages.javascript, 'jsx'),
tsx: Prism.highlight(entryCode, Prism.languages.typescript, 'tsx'),
},
style: meta.style,
location,
src: demoUrl,
expand,
highlightedStyle: meta.style ? Prism.highlight(meta.style, Prism.languages.css, 'css') : '',
} as P;
return <WrappedComponent {...transformedProps} />;
};
return hoc;
}

View File

@ -1,512 +1,18 @@
import {
CheckOutlined,
SnippetsOutlined,
ThunderboltOutlined,
LinkOutlined,
} from '@ant-design/icons';
import stackblitzSdk from '@stackblitz/sdk';
import type { Project } from '@stackblitz/sdk';
import { Alert, Badge, Tooltip, Space } from 'antd';
import classNames from 'classnames';
import LZString from 'lz-string';
import React, { useContext, useEffect, useRef, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import ReactDOM from 'react-dom';
import { FormattedMessage } from 'dumi';
import ClientOnly from '../../common/ClientOnly';
import BrowserFrame from '../../common/BrowserFrame';
import EditButton from '../../common/EditButton';
import CodePenIcon from '../../common/CodePenIcon';
import CodePreview from '../../common/CodePreview';
import CodeSandboxIcon from '../../common/CodeSandboxIcon';
import RiddleIcon from '../../common/RiddleIcon';
import ExternalLinkIcon from '../../common/ExternalLinkIcon';
import fromDumiProps from './fromDumiProps';
import type { SiteContextProps } from '../../slots/SiteContext';
import SiteContext from '../../slots/SiteContext';
import { version } from '../../../../package.json';
import type { FC } from 'react';
import React from 'react';
import type { IPreviewerProps } from 'dumi';
import { useTabMeta } from 'dumi';
import CodePreviewer from './CodePreviewer';
import DesignPreviewer from './DesignPreviewer';
const { ErrorBoundary } = Alert;
const Previewer: FC<IPreviewerProps> = ({ ...props }) => {
const tab = useTabMeta();
function compress(string: string): string {
return LZString.compressToBase64(string)
.replace(/\+/g, '-') // Convert '+' to '-'
.replace(/\//g, '_') // Convert '/' to '_'
.replace(/=+$/, ''); // Remove ending '='
}
const track = ({ type, demo }: { type: string; demo: string }) => {
if (!window.gtag) {
return;
if (tab?.frontmatter.title === 'Design') {
return <DesignPreviewer {...props} />;
}
window.gtag('event', 'demo', { event_category: type, event_label: demo });
return <CodePreviewer {...props} />;
};
interface DemoProps {
meta: any;
intl: any;
utils?: any;
src: string;
content: string;
highlightedCodes: Record<PropertyKey, string>;
style: string;
highlightedStyle: string;
expand: boolean;
sourceCodes: Record<'jsx' | 'tsx', string>;
location: Location;
showRiddleButton: boolean;
preview: (react: typeof React, reactDOM: typeof ReactDOM) => React.ReactNode;
}
const Demo: React.FC<DemoProps> = (props) => {
const {
location,
sourceCodes,
meta,
src,
utils,
content,
highlightedCodes,
style,
highlightedStyle,
expand,
intl: { locale },
showRiddleButton,
preview,
} = props;
const liveDemo = useRef<React.ReactNode>(null);
const anchorRef = useRef<HTMLAnchorElement>(null);
const codeSandboxIconRef = useRef<HTMLFormElement>(null);
const riddleIconRef = useRef<HTMLFormElement>(null);
const codepenIconRef = useRef<HTMLFormElement>(null);
const [codeExpand, setCodeExpand] = useState<boolean>(false);
const [copyTooltipOpen, setCopyTooltipOpen] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const [codeType, setCodeType] = useState<string>('tsx');
const { theme } = useContext<SiteContextProps>(SiteContext);
const { hash, pathname, search } = location;
const docsOnlineUrl = `https://ant.design${pathname}${search}#${meta.id}`;
const [showOnlineUrl, setShowOnlineUrl] = useState<boolean>(false);
useEffect(() => {
const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses
setShowOnlineUrl(
process.env.NODE_ENV === 'development' || regexp.test(window.location.hostname),
);
}, []);
const handleCodeExpand = (demo: string) => {
setCodeExpand((prev) => !prev);
track({ type: 'expand', demo });
};
const handleCodeCopied = (demo: string) => {
setCopied(true);
track({ type: 'copy', demo });
};
const onCopyTooltipOpenChange = (open: boolean) => {
setCopyTooltipOpen(open);
if (open) {
setCopied(false);
}
};
useEffect(() => {
if (meta.id === hash.slice(1)) {
anchorRef.current?.click();
}
}, []);
useEffect(() => {
setCodeExpand(expand);
}, [expand]);
if (!liveDemo.current) {
liveDemo.current = meta.iframe ? (
<BrowserFrame>
<iframe src={src} height={meta.iframe} title="demo" className="iframe-demo" />
</BrowserFrame>
) : (
preview(React, ReactDOM)
);
}
const codeBoxClass = classNames('code-box', {
expand: codeExpand,
'code-box-debug': meta.originDebug,
});
const localizedTitle = meta?.title[locale] || meta?.title;
const localizeIntro = content[locale] || content;
const introChildren = <div dangerouslySetInnerHTML={{ __html: localizeIntro }} />;
const highlightClass = classNames('highlight-wrapper', {
'highlight-wrapper-expand': codeExpand,
});
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
</head>
<body>
<div id="container" style="padding: 24px" />
<script>const mountNode = document.getElementById('container');</script>
</body>
</html>
`;
const tsconfig = `
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
}
}
`;
const suffix = codeType === 'tsx' ? 'tsx' : 'js';
const dependencies: Record<PropertyKey, string> = sourceCodes?.jsx.split('\n').reduce(
(acc, line) => {
const matches = line.match(/import .+? from '(.+)';$/);
if (matches && matches[1] && !line.includes('antd')) {
const paths = matches[1].split('/');
if (paths.length) {
const dep = paths[0].startsWith('@') ? `${paths[0]}/${paths[1]}` : paths[0];
acc[dep] = 'latest';
}
}
return acc;
},
{ antd: version },
);
dependencies['@ant-design/icons'] = 'latest';
if (suffix === 'tsx') {
dependencies['@types/react'] = '^18.0.0';
dependencies['@types/react-dom'] = '^18.0.0';
}
dependencies.react = '^18.0.0';
dependencies['react-dom'] = '^18.0.0';
const codepenPrefillConfig = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
html,
js: `const { createRoot } = ReactDOM;\n${sourceCodes?.jsx
.replace(/import\s+(?:React,\s+)?{(\s+[^}]*\s+)}\s+from\s+'react'/, `const { $1 } = React;`)
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'antd';/, 'const { $1 } = antd;')
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'@ant-design\/icons';/, 'const { $1 } = icons;')
.replace("import moment from 'moment';", '')
.replace("import React from 'react';", '')
.replace(/import\s+{\s+(.*)\s+}\s+from\s+'react-router';/, 'const { $1 } = ReactRouter;')
.replace(
/import\s+{\s+(.*)\s+}\s+from\s+'react-router-dom';/,
'const { $1 } = ReactRouterDOM;',
)
.replace(/([A-Za-z]*)\s+as\s+([A-Za-z]*)/, '$1:$2')
.replace(
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
editors: '001',
css: '',
js_external: [
'react@18/umd/react.development.js',
'react-dom@18/umd/react-dom.development.js',
'dayjs@1/dayjs.min.js',
`antd@${version}/dist/antd-with-locales.js`,
`@ant-design/icons/dist/index.umd.js`,
'react-router-dom/dist/umd/react-router-dom.production.min.js',
'react-router/dist/umd/react-router.production.min.js',
]
.map((url) => `https://unpkg.com/${url}`)
.join(';'),
js_pre_processor: 'typescript',
};
const riddlePrefillConfig = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
js: `${
/import React(\D*)from 'react';/.test(sourceCodes?.jsx) ? '' : `import React from 'react';\n`
}import { createRoot } from 'react-dom/client';\n${sourceCodes?.jsx.replace(
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
css: '',
json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2),
};
// Reorder source code
let parsedSourceCode = suffix === 'tsx' ? sourceCodes?.tsx : sourceCodes?.jsx;
let importReactContent = "import React from 'react';";
const importReactReg = /import React(\D*)from 'react';/;
const matchImportReact = parsedSourceCode.match(importReactReg);
if (matchImportReact) {
[importReactContent] = matchImportReact;
parsedSourceCode = parsedSourceCode.replace(importReactReg, '').trim();
}
const demoJsContent = `
${importReactContent}
import './index.css';
${parsedSourceCode}
`.trim();
const indexCssContent = (style || '')
.trim()
.replace(new RegExp(`#${meta.id}\\s*`, 'g'), '')
.replace('</style>', '')
.replace('<style>', '');
const indexJsContent = `
import React from 'react';
import { createRoot } from 'react-dom/client';
import Demo from './demo';
createRoot(document.getElementById('container')).render(<Demo />);
`;
const codesandboxPackage = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
main: 'index.js',
dependencies: {
...dependencies,
react: '^18.0.0',
'react-dom': '^18.0.0',
'react-scripts': '^4.0.0',
},
devDependencies: {
typescript: '^4.0.5',
},
scripts: {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
},
browserslist: ['>0.2%', 'not dead'],
};
const codesanboxPrefillConfig = {
files: {
'package.json': { content: codesandboxPackage },
'index.css': { content: indexCssContent },
[`index.${suffix}`]: { content: indexJsContent },
[`demo.${suffix}`]: { content: demoJsContent },
'index.html': {
content: html,
},
},
};
const stackblitzPrefillConfig: Project = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
template: 'create-react-app',
dependencies,
description: '',
files: {
'index.css': indexCssContent,
[`index.${suffix}`]: indexJsContent,
[`demo.${suffix}`]: demoJsContent,
'index.html': html,
},
};
if (suffix === 'tsx') {
stackblitzPrefillConfig.files['tsconfig.json'] = tsconfig;
}
const backgroundGrey = theme.includes('dark') ? '#303030' : '#f0f2f5';
const codeBoxDemoStyle: React.CSSProperties = {
padding: meta.iframe || meta.compact ? 0 : undefined,
overflow: meta.iframe || meta.compact ? 'hidden' : undefined,
backgroundColor: meta.background === 'grey' ? backgroundGrey : undefined,
};
const codeBox: React.ReactNode = (
<section className={codeBoxClass} id={meta.id}>
<section className="code-box-demo" style={codeBoxDemoStyle}>
<ErrorBoundary>
<React.StrictMode>{liveDemo.current}</React.StrictMode>
</ErrorBoundary>
{style ? <style dangerouslySetInnerHTML={{ __html: style }} /> : null}
</section>
<section className="code-box-meta markdown">
<div className="code-box-title">
<Tooltip title={meta.originDebug ? <FormattedMessage id="app.demo.debug" /> : ''}>
<a href={`#${meta.id}`} ref={anchorRef}>
{localizedTitle}
</a>
</Tooltip>
<EditButton
title={<FormattedMessage id="app.content.edit-demo" />}
filename={meta.filename}
/>
</div>
<div className="code-box-description">{introChildren}</div>
<Space wrap size="middle" className="code-box-actions">
{showOnlineUrl && (
<Tooltip title={<FormattedMessage id="app.demo.online" />}>
<a
className="code-box-code-action"
target="_blank"
rel="noreferrer"
href={docsOnlineUrl}
>
<LinkOutlined className="code-box-online" />
</a>
</Tooltip>
)}
{showRiddleButton ? (
<form
className="code-box-code-action"
action="//riddle.alibaba-inc.com/riddles/define"
method="POST"
target="_blank"
ref={riddleIconRef}
onClick={() => {
track({ type: 'riddle', demo: meta.id });
riddleIconRef.current?.submit();
}}
>
<input type="hidden" name="data" value={JSON.stringify(riddlePrefillConfig)} />
<Tooltip title={<FormattedMessage id="app.demo.riddle" />}>
<RiddleIcon className="code-box-riddle" />
</Tooltip>
</form>
) : null}
<form
className="code-box-code-action"
action="https://codesandbox.io/api/v1/sandboxes/define"
method="POST"
target="_blank"
ref={codeSandboxIconRef}
onClick={() => {
track({ type: 'codesandbox', demo: meta.id });
codeSandboxIconRef.current?.submit();
}}
>
<input
type="hidden"
name="parameters"
value={compress(JSON.stringify(codesanboxPrefillConfig))}
/>
<Tooltip title={<FormattedMessage id="app.demo.codesandbox" />}>
<CodeSandboxIcon className="code-box-codesandbox" />
</Tooltip>
</form>
<form
className="code-box-code-action"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
ref={codepenIconRef}
onClick={() => {
track({ type: 'codepen', demo: meta.id });
codepenIconRef.current?.submit();
}}
>
<ClientOnly>
<input type="hidden" name="data" value={JSON.stringify(codepenPrefillConfig)} />
</ClientOnly>
<Tooltip title={<FormattedMessage id="app.demo.codepen" />}>
<CodePenIcon className="code-box-codepen" />
</Tooltip>
</form>
<Tooltip title={<FormattedMessage id="app.demo.stackblitz" />}>
<span
className="code-box-code-action"
onClick={() => {
track({ type: 'stackblitz', demo: meta.id });
stackblitzSdk.openProject(stackblitzPrefillConfig, {
openFile: [`demo.${suffix}`],
});
}}
>
<ThunderboltOutlined className="code-box-stackblitz" />
</span>
</Tooltip>
<CopyToClipboard text={sourceCodes?.tsx} onCopy={() => handleCodeCopied(meta.id)}>
<Tooltip
open={copyTooltipOpen as boolean}
onOpenChange={onCopyTooltipOpenChange}
title={<FormattedMessage id={`app.demo.${copied ? 'copied' : 'copy'}`} />}
>
{React.createElement(copied && copyTooltipOpen ? CheckOutlined : SnippetsOutlined, {
className: 'code-box-code-copy code-box-code-action',
})}
</Tooltip>
</CopyToClipboard>
<Tooltip title={<FormattedMessage id="app.demo.separate" />}>
<a className="code-box-code-action" target="_blank" rel="noreferrer" href={src}>
<ExternalLinkIcon className="code-box-separate" />
</a>
</Tooltip>
<Tooltip
title={<FormattedMessage id={`app.demo.code.${codeExpand ? 'hide' : 'show'}`} />}
>
<div className="code-expand-icon code-box-code-action">
<img
alt="expand code"
src={
theme?.includes('dark')
? 'https://gw.alipayobjects.com/zos/antfincdn/btT3qDZn1U/wSAkBuJFbdxsosKKpqyq.svg'
: 'https://gw.alipayobjects.com/zos/antfincdn/Z5c7kzvi30/expand.svg'
}
className={codeExpand ? 'code-expand-icon-hide' : 'code-expand-icon-show'}
onClick={() => handleCodeExpand(meta.id)}
/>
<img
alt="expand code"
src={
theme?.includes('dark')
? 'https://gw.alipayobjects.com/zos/antfincdn/CjZPwcKUG3/OpROPHYqWmrMDBFMZtKF.svg'
: 'https://gw.alipayobjects.com/zos/antfincdn/4zAaozCvUH/unexpand.svg'
}
className={codeExpand ? 'code-expand-icon-show' : 'code-expand-icon-hide'}
onClick={() => handleCodeExpand(meta.id)}
/>
</div>
</Tooltip>
</Space>
</section>
<section className={highlightClass} key="code">
<CodePreview
codes={highlightedCodes}
toReactComponent={utils?.toReactComponent}
onCodeTypeChange={(type) => setCodeType(type)}
/>
{highlightedStyle ? (
<div key="style" className="highlight">
<pre>
<code className="css" dangerouslySetInnerHTML={{ __html: highlightedStyle }} />
</pre>
</div>
) : null}
</section>
</section>
);
if (meta.version) {
return (
<Badge.Ribbon text={meta.version} color={meta.version.includes('<') ? 'red' : null}>
{codeBox}
</Badge.Ribbon>
);
}
return codeBox;
};
export default fromDumiProps(Demo);
export default Previewer;

View File

@ -0,0 +1,334 @@
import type { FC } from 'react';
import React, { useEffect, useRef } from 'react';
import G6 from '@antv/g6';
import { createStyles, css } from 'antd-style';
import { useRouteMeta } from 'dumi';
G6.registerNode('behavior-start-node', {
draw: (cfg, group) => {
const textWidth = G6.Util.getTextSize(cfg!.label, 16)[0];
const size = [textWidth + 20 * 2, 48];
const keyShape = group!.addShape('rect', {
name: 'start-node',
attrs: {
width: size[0],
height: size[1],
y: -size[1] / 2,
radius: 8,
fill: '#fff',
},
});
group!.addShape('text', {
attrs: {
text: `${cfg!.label}`,
fill: 'rgba(0, 0, 0, 0.88)',
fontSize: 16,
fontWeight: 500,
x: 20,
textBaseline: 'middle',
},
name: 'start-node-text',
});
return keyShape;
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
];
},
});
G6.registerNode(
'behavior-sub-node',
{
draw: (cfg, group) => {
const textWidth = G6.Util.getTextSize(cfg!.label, 14)[0];
const padding = 16;
const size = [textWidth + 16 * 2 + (cfg!.targetType ? 12 : 0) + (cfg!.link ? 20 : 0), 40];
const keyShape = group!.addShape('rect', {
name: 'sub-node',
attrs: {
width: size[0],
height: size[1],
y: -size[1] / 2,
radius: 8,
fill: '#fff',
cursor: 'pointer',
},
});
group!.addShape('text', {
attrs: {
text: `${cfg!.label}`,
x: cfg!.targetType ? 12 + 16 : padding,
fill: 'rgba(0, 0, 0, 0.88)',
fontSize: 14,
textBaseline: 'middle',
cursor: 'pointer',
},
name: 'sub-node-text',
});
if (cfg!.targetType) {
group!.addShape('rect', {
name: 'sub-node-type',
attrs: {
width: 8,
height: 8,
radius: 4,
y: -4,
x: 12,
fill: cfg!.targetType === 'mvp' ? '#1677ff' : '#A0A0A0',
cursor: 'pointer',
},
});
}
if (cfg!.children) {
const { length } = cfg!.children as any;
group!.addShape('rect', {
name: 'sub-node-children-length',
attrs: {
width: 20,
height: 20,
radius: 10,
y: -10,
x: size[0] - 4,
fill: '#404040',
cursor: 'pointer',
},
});
group!.addShape('text', {
name: 'sub-node-children-length-text',
attrs: {
text: `${length}`,
x: size[0] + 6 - G6.Util.getTextSize(`${length}`, 12)[0] / 2,
textBaseline: 'middle',
fill: '#fff',
fontSize: 12,
cursor: 'pointer',
},
});
}
if (cfg!.link) {
group!.addShape('dom', {
attrs: {
width: 16,
height: 16,
x: size[0] - 12 - 16,
y: -8,
cursor: 'pointer',
// DOM's html
html: `
<div style="width: 16px; height: 16px;">
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero">
<g id="编组-30" transform="translate(288.000000, 354.000000)">
<g id="编组-7备份-7" transform="translate(522.000000, 79.000000)">
<g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#BFBFBF"></path>
<path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#BFBFBF"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
`,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'sub-node-link',
});
}
return keyShape;
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
];
},
options: {
stateStyles: {
hover: {
stroke: '#1677ff',
'sub-node-link': {
html: `
<div style="width: 16px; height: 16px;">
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero">
<g id="编组-30" transform="translate(288.000000, 354.000000)">
<g id="编组-7备份-7" transform="translate(522.000000, 79.000000)">
<g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#1677ff"></path>
<path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#1677ff"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
`,
},
},
},
},
},
'rect',
);
const dataTransform = (data: BehaviorMapItem) => {
const changeData = (d: any, level = 0) => {
const clonedData: any = {
...d,
};
switch (level) {
case 0:
clonedData.type = 'behavior-start-node';
break;
case 1:
clonedData.type = 'behavior-sub-node';
clonedData.collapsed = true;
break;
default:
clonedData.type = 'behavior-sub-node';
break;
}
if (d.children) {
clonedData.children = d.children.map((child: any) => changeData(child, level + 1));
}
return clonedData;
};
return changeData(data);
};
type BehaviorMapItem = {
id: string;
label: string;
targetType?: 'mvp' | 'extension';
children?: BehaviorMapItem[];
link?: string;
};
const useStyle = createStyles(() => ({
container: css`
width: 100%;
height: 600px;
background-color: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
position: relative;
`,
title: css`
position: absolute;
top: 20px;
left: 20px;
font-size: 16px;
`,
tips: css`
display: flex;
position: absolute;
bottom: 20px;
right: 20px;
`,
mvp: css`
margin-right: 20px;
display: flex;
align-items: center;
&::before {
content: '';
display: block;
width: 8px;
height: 8px;
background-color: #1677ff;
border-radius: 50%;
margin-right: 8px;
}
`,
extension: css`
display: flex;
align-items: center;
&::before {
content: '';
display: block;
width: 8px;
height: 8px;
background-color: #A0A0A0;
border-radius: 50%;
margin-right: 8px;
}
`,
}));
export type BehaviorMapProps = {
data: BehaviorMapItem;
};
const BehaviorMap: FC<BehaviorMapProps> = ({ data }) => {
const ref = useRef<HTMLDivElement>(null);
const { styles } = useStyle();
const meta = useRouteMeta();
useEffect(() => {
const graph = new G6.TreeGraph({
container: ref.current!,
width: ref.current!.scrollWidth,
height: ref.current!.scrollHeight,
renderer: 'svg',
modes: {
default: ['collapse-expand', 'drag-canvas'],
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
lineWidth: 1,
stroke: '#BFBFBF',
},
},
layout: {
type: 'mindmap',
direction: 'LR',
getHeight: () => 48,
getWidth: (node: any) => G6.Util.getTextSize(node.label, 16)[0] + 20 * 2,
getVGap: () => 10,
getHGap: () => 60,
getSide: (node: any) => node.data.direction,
},
});
graph.on('node:mouseenter', (e) => {
graph.setItemState(e.item!, 'hover', true);
});
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item!, 'hover', false);
});
graph.on('node:click', (e) => {
const { link } = e.item!.getModel();
if (link) {
window.location.hash = link as string;
}
});
graph.data(dataTransform(data));
graph.render();
graph.fitCenter();
}, []);
return (
<div ref={ref} className={styles.container}>
<div className={styles.title}>{`${meta.frontmatter.title} 行为模式地图`}</div>
<div className={styles.tips}>
<div className={styles.mvp}>MVP </div>
<div className={styles.extension}></div>
</div>
</div>
);
};
export default BehaviorMap;

View File

@ -5,7 +5,7 @@ import {
parentSelectorLinter,
StyleProvider,
} from '@ant-design/cssinjs';
import { ConfigProvider, theme as antdTheme } from 'antd';
import { ConfigProvider, theme as antdTheme, App } from 'antd';
import type { DirectionType } from 'antd/es/config-provider';
import { createSearchParams, useOutlet, useSearchParams } from 'dumi';
import React, { startTransition, useCallback, useEffect, useMemo } from 'react';
@ -118,13 +118,15 @@ const GlobalLayout: React.FC = () => {
algorithm: getAlgorithm(theme),
}}
>
{outlet}
{!pathname.startsWith('/~demos') && (
<ThemeSwitch
value={theme}
onChange={(nextTheme) => updateSiteConfig({ theme: nextTheme })}
/>
)}
<App>
{outlet}
{!pathname.startsWith('/~demos') && (
<ThemeSwitch
value={theme}
onChange={(nextTheme) => updateSiteConfig({ theme: nextTheme })}
/>
)}
</App>
</ConfigProvider>
</SiteContext.Provider>
</StyleProvider>

View File

@ -4,7 +4,7 @@ import ContributorsList from '@qixian.cs/github-contributors-list';
import { Affix, Anchor, Avatar, Col, Skeleton, Space, Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import DayJS from 'dayjs';
import { FormattedMessage, useIntl, useRouteMeta } from 'dumi';
import { FormattedMessage, useIntl, useRouteMeta, useTabMeta } from 'dumi';
import type { ReactNode } from 'react';
import React, { useContext, useLayoutEffect, useMemo, useState } from 'react';
import useLocation from '../../../hooks/useLocation';
@ -107,6 +107,7 @@ type AnchorItem = {
const Content: React.FC<{ children: ReactNode }> = ({ children }) => {
const meta = useRouteMeta();
const tab = useTabMeta();
const { pathname, hash } = useLocation();
const { formatMessage } = useIntl();
const styles = useStyle();
@ -132,7 +133,7 @@ const Content: React.FC<{ children: ReactNode }> = ({ children }) => {
const anchorItems = useMemo(
() =>
meta.toc.reduce<AnchorItem[]>((result, item) => {
(tab?.toc || meta.toc).reduce<AnchorItem[]>((result, item) => {
if (item.depth === 2) {
result.push({ ...item });
} else if (item.depth === 3) {
@ -144,7 +145,7 @@ const Content: React.FC<{ children: ReactNode }> = ({ children }) => {
}
return result;
}, []),
[meta.toc],
[tab?.toc, meta.toc],
);
const isRTL = direction === 'rtl';
@ -219,6 +220,7 @@ const Content: React.FC<{ children: ReactNode }> = ({ children }) => {
</Space>
</Typography.Paragraph>
) : null}
{meta.frontmatter.description !== meta.texts[0]?.value && meta.frontmatter.description}
{children}
{meta.frontmatter.filename && (
<ContributorsList

View File

@ -0,0 +1,57 @@
import type { FC, ReactNode } from 'react';
import React from 'react';
import { CodeOutlined, SkinOutlined } from '@ant-design/icons';
import { Tabs } from 'antd';
import { useRouteMeta } from 'dumi';
import type { IContentTabsProps } from 'dumi/theme-default/slots/ContentTabs';
import type { TabsProps } from 'rc-tabs';
const titleMap: Record<string, string> = {
design: '设计',
};
const iconMap: Record<string, ReactNode> = {
design: <SkinOutlined />,
};
const ContentTabs: FC<IContentTabsProps> = ({ tabs, tabKey, onChange }) => {
const meta = useRouteMeta();
if (!meta.tabs) {
return null;
}
const items: TabsProps['items'] = [
{
label: (
<span>
<CodeOutlined />
</span>
),
key: 'development',
},
];
tabs?.forEach((tab) => {
items.push({
label: (
<span>
{iconMap[tab.key]}
{titleMap[tab.key]}
</span>
),
key: tab.key,
});
});
return (
<Tabs
items={items}
activeKey={tabKey || 'development'}
onChange={(key) => onChange(tabs.find((tab) => tab.key === key))}
style={{ margin: '32px 0 -16px' }}
/>
);
};
export default ContentTabs;

View File

@ -52,6 +52,7 @@ module.exports = {
'!components/*/__tests__/image.test.{ts,tsx}',
'!components/__tests__/node.test.tsx',
'!components/*/demo/*.tsx',
'!components/*/design/**',
],
transformIgnorePatterns,
globals: {

View File

@ -0,0 +1,112 @@
import React from 'react';
import BehaviorMap from '../../../.dumi/theme/common/BehaviorMap';
const BehaviorPattern = () => (
<BehaviorMap
data={{
id: '200000004',
label: '选择(输入)日期数据',
children: [
{
id: '500000061',
label: '选择时间点',
targetType: 'mvp',
children: [
{
id: '707000085',
label: '选择某天',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-date',
},
{
id: '707000086',
label: '选择某周',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-week',
},
{
id: '707000087',
label: '选择某月',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-month',
},
{
id: '707000088',
label: '选择某季度',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-quarter',
},
{
id: '707000089',
label: '选择某年',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-year',
},
{
id: '707000090',
label: '选择某时间',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-time',
},
],
},
{
id: '200000005',
label: '选择时间段',
targetType: 'mvp',
children: [
{
id: '7070000851',
label: '选择某天至某天',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-date-range',
},
{
id: '7070000861',
label: '选择某周至某周',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-week-range',
},
{
id: '7070000871',
label: '选择某月至某月',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-month-range',
},
{
id: '7070000881',
label: '选择某季度至某季度',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-quarter-range',
},
{
id: '7070000891',
label: '选择某年至某年',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-year-range',
},
{
id: '7070000901',
label: '选择某时间至某时间',
link: 'components-date-picker-index-tab-design-zh-cn-demo-pick-time-range',
},
],
},
{
id: '200000006',
label: '快捷选择日期数据',
targetType: 'extension',
children: [
{
id: '70700008912',
label: '快捷选择时间点',
link: 'components-date-picker-index-tab-design-zh-cn-demo-preset-time',
},
{
id: '70700009012',
label: '快捷选择时间段',
link: 'components-date-picker-index-tab-design-zh-cn-demo-preset-range',
},
],
},
{
id: '200000007',
label: '查看日期附属信息',
targetType: 'extension',
link: 'components-date-picker-index-tab-design-zh-cn-demo-date-extra-info',
},
],
}}
/>
);
export default BehaviorPattern;

View File

@ -0,0 +1,120 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
import type { Dayjs } from 'dayjs';
import { createStyles, css } from 'antd-style';
import classNames from 'classnames';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const useStyle = createStyles(({ token }) => ({
weekendCell: css`
color: #ff4d4f40;
.ant-picker-cell-in-view & {
color: #ff4d4f;
}
`,
detailedCell: css`
width: 40px;
height: 40px !important;
`,
detailedPicker: css`
.ant-picker-date-panel {
width: auto;
.ant-picker-content {
width: auto;
}
}
`,
extraInfo: css`
font-size: 12px;
line-height: 12px;
transform: scale(${10 / 12});
color: ${token.colorTextQuaternary};
.ant-picker-cell-in-view & {
color: ${token.colorTextSecondary};
}
.ant-picker-cell-selected & {
color: #fff;
}
`,
add: css`
color: #ff4d4f80;
.ant-picker-cell-in-view & {
color: #ff4d4f;
}
.ant-picker-cell-selected & {
color: #fff;
}
`,
minus: css`
color: #52C41A80;
.ant-picker-cell-in-view & {
color: #52C41A;
}
.ant-picker-cell-selected & {
color: #fff;
}
`,
}));
const seeds = Array(30)
.fill(1)
.map(() => Math.random());
const getSales = (date: Dayjs) => Math.floor(seeds[date.date() % 30] * 10000);
const getData = (date: Dayjs) => (Math.floor(seeds[date.date() % 30] * 10000) - 5000) / 5000;
const Demo: FC = () => {
const { styles } = useStyle();
const dateRender = (current: Dayjs) => (
<div
className={classNames(
'ant-picker-cell-inner',
[6, 0].includes(current.day()) && styles.weekendCell,
)}
>
{current.date()}
</div>
);
const saleDateRender = (current: Dayjs) => (
<div className={classNames('ant-picker-cell-inner', styles.detailedCell)}>
{current.date()}
<div className={styles.extraInfo}>{getSales(current)}</div>
</div>
);
const dataDateRender = (current: Dayjs) => {
const data = getData(current);
return (
<div className={classNames('ant-picker-cell-inner', styles.detailedCell)}>
{current.date()}
<div className={classNames(styles.extraInfo, data > 0 ? styles.add : styles.minus)}>
{data.toFixed(2)}%
</div>
</div>
);
};
return (
<div style={{ width: '100%' }}>
<div style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 32 }}></div>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 40 }}>
<PureDatePicker dateRender={dateRender} popupClassName={styles.detailedPicker} />
</div>
<div style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 32 }}></div>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 40 }}>
<PureDatePicker dateRender={saleDateRender} popupClassName={styles.detailedPicker} />
</div>
<div style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 32 }}></div>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 40 }}>
<PureDatePicker dateRender={dataDateRender} popupClassName={styles.detailedPicker} />
</div>
</div>
);
};
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker picker="month" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker picker="month" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker picker="quarter" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker picker="quarter" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker showTime />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker showTime />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker picker="week" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker picker="week" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const Demo: FC = () => <PureRangePicker picker="year" />;
export default Demo;

View File

@ -0,0 +1,9 @@
import type { FC } from 'react';
import React from 'react';
import { DatePicker } from 'antd';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const Demo: FC = () => <PureDatePicker picker="year" />;
export default Demo;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { DatePicker } from 'antd';
import dayjs from 'dayjs';
const { _InternalRangePanelDoNotUseOrYouWillBeFired: PureRangePicker } = DatePicker;
const App: React.FC = () => (
<PureRangePicker
presets={[
{ label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()] },
{ label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()] },
{ label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()] },
{ label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()] },
]}
/>
);
export default App;

View File

@ -0,0 +1,17 @@
import React from 'react';
import { DatePicker } from 'antd';
import dayjs from 'dayjs';
const { _InternalPanelDoNotUseOrYouWillBeFired: PureDatePicker } = DatePicker;
const App: React.FC = () => (
<PureDatePicker
presets={[
{ label: 'Yesterday', value: dayjs().add(-1, 'd') },
{ label: 'Last Week', value: dayjs().add(-7, 'd') },
{ label: 'Last Month', value: dayjs().add(-1, 'month') },
]}
/>
);
export default App;

View File

@ -0,0 +1,39 @@
## 组件定义
DatePicker 的本质是选择(输入)日期型数据。
<code src="./design/behavior-pattern.tsx" inline></code>
## 基础使用
<code src="./design/demo/pick-date.tsx" description="用于具体日期的选择。用户仅需要输入非常具体的日期信息时使用。">选择某天</code>
<code src="./design/demo/pick-week.tsx" description="用于周的选择。用户仅需输入年份 + 周信息时使用。">选择某周</code>
<code src="./design/demo/pick-month.tsx" description="用于月份的选择。用户仅需输入年份 + 月份信息时使用。">选择某月</code>
<code src="./design/demo/pick-quarter.tsx" description="用于季度的选择。用户仅需输入年份 + 季度信息时使用。">选择某季度</code>
<code src="./design/demo/pick-year.tsx" description="用于年的选择。用户仅需输入年份时使用。">选择某年</code>
<code src="./design/demo/pick-time.tsx" description="用于具体时刻的选择。用户需输入年份+月份+日期+时间信息时使用。">选择某时刻</code>
<code src="./design/demo/pick-date-range.tsx" description="用于具体日期范围的选择。">选择某天至某天</code>
<code src="./design/demo/pick-week-range.tsx" description="用于周范围的选择。">选择某周至某周</code>
<code src="./design/demo/pick-month-range.tsx" description="用于月范围的选择。">选择某月至某月</code>
<code src="./design/demo/pick-quarter-range.tsx" description="用于季度范围的选择。">选择某季度至某季度</code>
<code src="./design/demo/pick-year-range.tsx" description="用于年范围的选择。">选择某年至某年</code>
<code src="./design/demo/pick-time-range.tsx" description="用于具体时刻范围的选择。">选择某时刻至某时刻</code>
## 交互变体
<code src="./design/demo/preset-time.tsx" description="通过面板左侧区域提供的预置项,帮助用户快速完成时间点的选择。" tip="根据希克定律建议快捷选项的个数不超过8个。">快捷选择时间点</code>
<code src="./design/demo/preset-range.tsx" description="通过面板左侧区域提供的预置项,帮助用户快速完成时间段的选择。" tip="根据希克定律建议快捷选项的个数不超过8个。">快捷选择时间段</code>
<code src="./design/demo/date-extra-info.tsx" description="通过定义日期单元格内容及样式,为用户展示更多业务场景相关信息作为选择参考。">查看日期附属信息</code>

View File

@ -2,14 +2,13 @@
category: Components
group: Data Entry
title: DatePicker
description: To select or input a date.
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xXA9TJ8BTioAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3OpRQKcygo8AAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
---
To select or input a date.
## When To Use
By clicking the input box, you can select a date from a popup calendar.

View File

@ -19,7 +19,10 @@ const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
/* istanbul ignore next */
const PurePanel = genPurePanel(DatePicker, 'picker');
(DatePicker as any)._InternalPanelDoNotUseOrYouWillBeFired = PurePanel;
const PureRangePanel = genPurePanel(DatePicker.RangePicker, 'picker');
(DatePicker as any)._InternalRangePanelDoNotUseOrYouWillBeFired = PureRangePanel;
export default DatePicker as typeof DatePicker & {
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
_InternalRangePanelDoNotUseOrYouWillBeFired: typeof PureRangePanel;
};

View File

@ -3,14 +3,13 @@ category: Components
group: 数据录入
title: DatePicker
subtitle: 日期选择框
description: 输入或选择日期的控件。
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xXA9TJ8BTioAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3OpRQKcygo8AAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
---
输入或选择日期的控件。
## 何时使用
当用户需要输入一个日期,可以点击标准输入框,弹出日期面板进行选择。

View File

@ -2,6 +2,7 @@
category: Components
group: General
title: Icon
description: Semantic vector graphics.
toc: false
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*PdAYS7anRpoAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xEDOTJx2DEkAAAAAAAAAAAAADrJ8AQ/original
@ -9,7 +10,9 @@ demo:
cols: 2
---
Semantic vector graphics. Before use icons, you need to install `@ant-design/icons` package:
## How to use
Before use icons, you need to install `@ant-design/icons` package:
```bash
npm install --save @ant-design/icons

View File

@ -1,6 +1,7 @@
---
category: Components
subtitle: 图标
description: 语义化的矢量图形。
group: 通用
title: Icon
toc: false
@ -10,7 +11,9 @@ demo:
cols: 2
---
语义化的矢量图形。使用图标组件,你需要安装 `@ant-design/icons` 图标组件包:
## 使用方法
使用图标组件,你需要安装 `@ant-design/icons` 图标组件包:
```bash
npm install --save @ant-design/icons

View File

@ -157,6 +157,7 @@
},
"devDependencies": {
"@ant-design/tools": "^17.0.0",
"@antv/g6": "^4.8.5",
"@babel/eslint-plugin": "^7.19.1",
"@dnd-kit/core": "^6.0.7",
"@dnd-kit/sortable": "^7.0.2",
@ -195,6 +196,7 @@
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"antd-img-crop": "^4.2.8",
"antd-style": "^2.0.2",
"antd-token-previewer": "^1.1.0-21",
"chalk": "^4.0.0",
"cheerio": "1.0.0-rc.12",
@ -220,6 +222,7 @@
"fs-extra": "^11.0.0",
"gh-pages": "^5.0.0",
"glob": "^8.0.1",
"html2sketch": "^1.0.0",
"http-server": "^14.0.0",
"husky": "^8.0.1",
"identity-obj-proxy": "^3.0.0",