mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 12:39:49 +08:00
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:
parent
33275b6e80
commit
853283b7e4
556
.dumi/theme/builtins/Previewer/CodePreviewer.tsx
Normal file
556
.dumi/theme/builtins/Previewer/CodePreviewer.tsx
Normal 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;
|
102
.dumi/theme/builtins/Previewer/DesignPreviewer.tsx
Normal file
102
.dumi/theme/builtins/Previewer/DesignPreviewer.tsx
Normal 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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
334
.dumi/theme/common/BehaviorMap/index.tsx
Normal file
334
.dumi/theme/common/BehaviorMap/index.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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
|
||||
|
57
.dumi/theme/slots/ContentTabs/index.tsx
Normal file
57
.dumi/theme/slots/ContentTabs/index.tsx
Normal 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;
|
1
.jest.js
1
.jest.js
@ -52,6 +52,7 @@ module.exports = {
|
||||
'!components/*/__tests__/image.test.{ts,tsx}',
|
||||
'!components/__tests__/node.test.tsx',
|
||||
'!components/*/demo/*.tsx',
|
||||
'!components/*/design/**',
|
||||
],
|
||||
transformIgnorePatterns,
|
||||
globals: {
|
||||
|
112
components/date-picker/design/behavior-pattern.tsx
Normal file
112
components/date-picker/design/behavior-pattern.tsx
Normal 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;
|
120
components/date-picker/design/demo/date-extra-info.tsx
Normal file
120
components/date-picker/design/demo/date-extra-info.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-date-range.tsx
Normal file
9
components/date-picker/design/demo/pick-date-range.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-date.tsx
Normal file
9
components/date-picker/design/demo/pick-date.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-month-range.tsx
Normal file
9
components/date-picker/design/demo/pick-month-range.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-month.tsx
Normal file
9
components/date-picker/design/demo/pick-month.tsx
Normal 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;
|
@ -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;
|
9
components/date-picker/design/demo/pick-quarter.tsx
Normal file
9
components/date-picker/design/demo/pick-quarter.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-time-range.tsx
Normal file
9
components/date-picker/design/demo/pick-time-range.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-time.tsx
Normal file
9
components/date-picker/design/demo/pick-time.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-week-range.tsx
Normal file
9
components/date-picker/design/demo/pick-week-range.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-week.tsx
Normal file
9
components/date-picker/design/demo/pick-week.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-year-range.tsx
Normal file
9
components/date-picker/design/demo/pick-year-range.tsx
Normal 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;
|
9
components/date-picker/design/demo/pick-year.tsx
Normal file
9
components/date-picker/design/demo/pick-year.tsx
Normal 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;
|
18
components/date-picker/design/demo/preset-range.tsx
Normal file
18
components/date-picker/design/demo/preset-range.tsx
Normal 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;
|
17
components/date-picker/design/demo/preset-time.tsx
Normal file
17
components/date-picker/design/demo/preset-time.tsx
Normal 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;
|
39
components/date-picker/index.$tab-design.zh-CN.md
Normal file
39
components/date-picker/index.$tab-design.zh-CN.md
Normal 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>
|
@ -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.
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
输入或选择日期的控件。
|
||||
|
||||
## 何时使用
|
||||
|
||||
当用户需要输入一个日期,可以点击标准输入框,弹出日期面板进行选择。
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user