This commit is contained in:
chenos 2021-12-06 21:23:34 +08:00
parent c2ff7882bc
commit 732d31009e
1231 changed files with 7902 additions and 90237 deletions

View File

@ -1,3 +0,0 @@
node_modules/
.env
packages/app/.env

View File

@ -1,57 +0,0 @@
import React, { useContext } from 'react';
import type { IApiComponentProps} from 'dumi/theme';
import { context, useApiData, AnchorLink } from 'dumi/theme';
const LOCALE_TEXTS = {
'zh-CN': {
name: '属性名',
description: '描述',
type: '类型',
default: '默认值',
required: '(必选)',
},
'en-US': {
name: 'Name',
description: 'Description',
type: 'Type',
default: 'Default',
required: '(required)',
},
};
export default ({ identifier, export: expt }: IApiComponentProps) => {
const data = useApiData(identifier);
const { locale } = useContext(context);
const texts = /^zh|cn$/i.test(locale) ? LOCALE_TEXTS['zh-CN'] : LOCALE_TEXTS['en-US'];
return (
<>
{data && (
<table style={{ marginTop: 24 }}>
<thead>
<tr>
<th>{texts.name}</th>
<th>{texts.description}</th>
<th>{texts.type}</th>
<th>{texts.default}</th>
</tr>
</thead>
<tbody>
{data[expt].map(row => (
<tr key={row.identifier}>
<td>{row.identifier}</td>
<td>{row.description || '--'}</td>
<td>
<code>{row.type}</code>
</td>
<td>
<code>{row.default || (row.required && texts.required) || '--'}</code>
</td>
</tr>
))}
</tbody>
</table>
)}
</>
);
};

View File

@ -1,68 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-alert {
@s-border-right: 3px;
position: relative;
margin: 24px 0;
padding: 15px;
color: @c-text;
font-size: 14px;
line-height: 20px;
border-left: 0;
background: #ffffff;
// box-shadow: 0 6px 16px -2px rgba(0, 0, 0, 0.06);
border-radius: 1px;
&::after {
content: '';
position: absolute;
display: inline-block;
top: 0;
left: 0;
bottom: 0;
width: @s-border-right;
border-radius: 1px;
}
&:first-child {
margin-top: 0;
}
&:not([type]),
&[type='warning'] {
background-color: #fffbe6;
&::after {
background: #ffe58f;
}
}
&[type='info'] {
background: rgba(105, 185, 255, .15);
&::after {
background: #69b9ff;
}
.@{prefix}-alert-title {
color: #69b9ff;
}
}
&[type='success'] {
&::after {
background: #8cd225;
}
}
&[type='error'] {
&::after {
background: #ff4646;
}
}
.@{prefix}-alert-title {
margin-top: 0;
font-weight: 600;
color: #fab00e;
margin-bottom: .6em;
}
}

View File

@ -1,17 +0,0 @@
import React from 'react';
import './Alert.less';
import { Alert } from 'antd';
import micromark from 'micromark'
export default (props: any) => {
const { title, children, ...others } = props;
console.log({children});
return (
<div className="__dumi-default-alert" {...others}>
{title && (
<p className={'__dumi-default-alert-title'}>{title}</p>
)}
{typeof children === 'string' ? <div dangerouslySetInnerHTML={{__html: micromark(`\n ${children} \n`)}}/> : children}
</div>
)
};

View File

@ -1,31 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 7px;
color: #fff;
font-size: 13px;
line-height: 20px;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 6px 16px -2px rgba(0, 0, 0, 0.06);
border-radius: 1px;
vertical-align: top;
&:not([type]),
&[type='info'] {
background: #4569d4;
}
&[type='warning'] {
background: #ffc121;
}
&[type='success'] {
background: #8cd225;
}
&[type='error'] {
background: #ff4646;
}
}

View File

@ -1,4 +0,0 @@
import React from 'react';
import './Badge.less';
export default (props: any) => <span className="__dumi-default-badge" {...props} />;

View File

@ -1,47 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-example-wrapper {
padding-top: 16px;
height: calc(100vh - @s-nav-height - 2px);
@media @mobile {
padding-top: 16px;
}
&-toolbar {
display: flex;
color: @c-heading;
font-size: 16px;
font-weight: 500;
justify-content: space-between;
margin-bottom: 16px;
border-bottom: 1px solid @c-border;
button,
a {
display: inline-block;
margin: 6px 0 6px 12px;
width: 16px;
height: 16px;
font-weight: 400;
border: 0;
outline: none;
vertical-align: middle;
cursor: pointer;
&:first-child {
background-position-x: -144px;
}
&:nth-child(2) {
background-position-x: -126px;
}
}
}
iframe {
width: 100%;
min-height: 100%;
border: 0;
}
}

View File

@ -1,34 +0,0 @@
import React, { useRef, useEffect, useState } from 'react';
import './Example.less';
export default (props: { route: any }) => {
const elm = useRef<HTMLIFrameElement>();
const [height, setHeight] = useState(0);
useEffect(() => {
setHeight(elm.current.contentWindow.document.documentElement.scrollHeight);
}, [elm]);
return (
props.route.meta.examplePath && (
<div className="__dumi-default-example-wrapper">
<div className="__dumi-default-example-wrapper-toolbar">
{props.route.meta.description || props.route.meta.title}
<span>
<button
className="__dumi-default-icon"
onClick={() => elm.current.contentWindow.location.reload()}
/>
<a
target="_blank"
rel="noopener noreferrer"
href={props.route.meta.examplePath}
className="__dumi-default-icon"
/>
</span>
</div>
<iframe src={props.route.meta.examplePath} ref={elm} style={{ height }} title="dumi" />
</div>
)
);
};

View File

@ -1,365 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-previewer {
background-color: #fff;
border: 1px solid @c-border;
border-radius: 1px;
margin-bottom: 24px;
&[data-debug] {
margin-top: 32px;
border-color: #ffcb00;
&::before {
content: 'DEV ONLY';
float: left;
margin-left: -1px;
margin-top: -18px;
padding: 3px 6px;
font-size: 12px;
line-height: 1;
background-color: #ffcb00;
color: #735600;
text-shadow: 0.5px 0.5px 0 rgba(255, 255, 255, 0.5);
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
}
&[data-iframe] {
.@{prefix}-previewer-browser-nav {
padding: 2px 6px;
background-color: @c-border;
&::before {
@s-btn: 12px;
@s-btn-gap: 8px;
content: '';
display: inline-block;
width: @s-btn;
height: @s-btn;
border-radius: 50%;
background-color: #fd6458;
box-shadow: (@s-btn + @s-btn-gap) 0 0 #ffbf2b, (@s-btn + @s-btn-gap) * 2 0 0 #24cc3d;
}
}
.@{prefix}-previewer-demo > iframe {
border: 0;
width: 100%;
height: 300px;
}
}
+ .@{prefix}-previewer {
margin-top: 32px;
}
&-demo {
padding: 40px 24px;
}
&-target {
border-color: fade(@c-primary, 50%);
box-shadow: 0 0 0 5px fade(@c-primary, 5%);
}
&-desc {
> div:last-child {
padding: 1.2em 1em 1em;
color: @c-text;
border-top: 1px solid @c-border;
}
.markdown > p:first-child {
margin-top: 0;
}
.markdown > p:last-child {
margin-bottom: 0;
}
&[data-title] {
position: relative;
> a:first-child {
position: absolute;
top: 0;
left: 1em;
margin-left: -4px;
padding: 0 4px;
color: @c-heading;
font-size: inherit;
font-weight: 500;
background: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
transform: translateY(-50%);
pointer-events: auto;
cursor: pointer;
}
&:empty {
padding-top: 0;
// modify action area style when only has title field
+ .@{prefix}-previewer-actions {
height: 46px;
border-top-style: solid;
}
}
}
}
&-actions {
display: flex;
height: 40px;
padding: 0 1em;
align-items: center;
border-top: 1px dashed @c-border;
> a:not(:last-child),
> button:not(:last-child) {
margin-right: 8px;
}
> a {
display: flex;
}
button {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
padding: 0;
border: 0;
box-sizing: border-box;
cursor: pointer;
opacity: 0.6;
outline: none;
transition: opacity 0.2s, background 0.2s;
// expand click area
&::after {
content: '';
position: absolute;
top: -8px;
left: -8px;
right: -8px;
bottom: -8px;
}
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.9;
}
&:disabled {
opacity: 0.2;
cursor: not-allowed;
}
&[role='codesandbox'] {
background-position: -18px 0;
}
&[role='codepen'] {
background-position: -36px 0;
}
&[role='source'] {
background-position: -72px 0;
}
&[role='change-jsx'] {
background-position: -90px 0;
}
&[role='change-tsx'] {
background-position: -108px 0;
}
&[role='open-demo'] {
background-position: -126px 0;
}
&[role='motions'] {
background-position: -162px 0;
}
&[role='sketch-component'] {
background-position: -182px 0;
}
&[role='sketch-group'] {
background-position: -200px 0;
}
&[role='copy'][data-status='ready'] {
background-position: -54px 0;
}
&[role='copy'][data-status='copied'] {
pointer-events: none;
background-position: -54px -16px;
}
&[role='refresh'] {
background-position-x: -144px;
}
}
// split action buttons by a blank node
> span {
flex: 1;
display: inline-block;
}
}
&-source {
border-top: 1px dashed @c-border;
&-tab {
border-top: 1px dashed @c-border;
.@{prefix}-tabs-tab-btn {
position: relative;
padding-left: 32px;
&::before,
&::after {
content: '';
position: absolute;
margin-right: 4px;
display: inline-block;
}
&::before {
left: 16px;
top: 50%;
margin-top: -6px;
width: 10px;
height: 12px;
border: 1px solid @c-secondary;
}
&::after {
top: 50%;
left: 23px;
margin-top: -7px;
width: 4px;
height: 4px;
background: #fff;
border-bottom: 1px solid @c-secondary;
transform: rotate(45deg);
}
}
}
}
}
.@{prefix}-tabs {
overflow: hidden;
&.@{prefix}-tabs-top {
flex-direction: column;
.@{prefix}-tabs-ink-bar {
bottom: 0;
}
}
&-nav {
display: flex;
&-wrap {
display: flex;
white-space: nowrap;
overflow: hidden;
&&-ping-left {
box-shadow: 5px 0 5px -5px rgba(0, 0, 0, 0.1) inset;
}
&&-ping-right ~ * > .@{prefix}-tabs-nav-more {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
}
&-list {
position: relative;
display: flex;
transition: transform 0.2s;
}
&-more {
height: 100%;
cursor: pointer;
background: none;
border: 0;
transition: box-shadow 0.2s;
}
}
&-tab {
display: flex;
&-btn {
padding: 0 16px;
font-size: 14px;
line-height: 36px;
border: 0;
outline: none;
background: transparent;
box-sizing: border-box;
cursor: pointer;
&:hover {
color: @c-primary;
}
}
}
&-ink-bar {
position: absolute;
height: 2px;
background: @c-primary;
transition: left 0.2s, width 0.2s;
pointer-events: none;
}
&-dropdown {
position: absolute;
background: #fff;
border: 1px solid @c-border;
max-height: 200px;
overflow: auto;
> ul {
list-style: none;
margin: 0;
padding: 0;
> li {
padding: 4px 12px;
font-size: 14px;
cursor: pointer;
&:hover {
color: @c-primary;
}
&:not(:last-child) {
border-bottom: 1px dashed @c-border;
}
}
}
&-hidden {
display: none;
}
}
}

View File

@ -1,253 +0,0 @@
import React, { useState, useContext, useRef, useEffect } from 'react';
import Tabs, { TabPane } from 'rc-tabs';
// @ts-ignore
import { history } from 'dumi';
import type { IPreviewerComponentProps} from 'dumi/theme';
import {
context,
useCodeSandbox,
useRiddle,
useMotions,
useCopy,
useLocaleProps,
useDemoUrl,
useTSPlaygroundUrl,
Link,
AnchorLink,
usePrefersColor
} from 'dumi/theme';
import type { ICodeBlockProps } from './SourceCode';
import SourceCode from './SourceCode';
import './Previewer.less';
export interface IPreviewerProps extends IPreviewerComponentProps {
/**
* enable transform to change CSS containing block for demo
*/
transform?: boolean;
/**
* modify background for demo area
*/
background?: string;
/**
* collapse padding of demo area
*/
compact?: boolean;
/**
* configurations for action button
*/
hideActions?: ('CSB' | 'EXTERNAL' | 'RIDDLE')[];
/**
* show source code by default
*/
defaultShowCode?: boolean;
/**
* use iframe mode for this demo
*/
iframe?: true | number;
/**
* replace builtin demo url
*/
demoUrl?: string;
}
/**
* get source code type for file
* @param file file path
* @param source file source object
*/
function getSourceType(file: string, source: IPreviewerComponentProps['sources']['_']) {
// use file extension as source type first
let type = file.match(/\.(\w+)$/)?.[1];
if (!type) {
type = source.tsx ? 'tsx' : 'jsx';
}
return type as ICodeBlockProps['lang'];
}
const Previewer: React.FC<IPreviewerProps> = oProps => {
const demoRef = useRef();
const { locale } = useContext(context);
const props = useLocaleProps<IPreviewerProps>(locale, oProps);
const builtinDemoUrl = useDemoUrl(props.identifier);
const demoUrl = props.demoUrl || builtinDemoUrl;
const isActive = history?.location.hash === `#${props.identifier}`;
const isSingleFile = Object.keys(props.sources).length === 1;
const openCSB = useCodeSandbox(props.hideActions?.includes('CSB') ? null : props);
// const openRiddle = useRiddle(props.hideActions?.includes('RIDDLE') ? null : props);
const [execMotions, isMotionRunning] = useMotions(props.motions || [], demoRef.current);
const [copyCode, copyStatus] = useCopy();
const [currentFile, setCurrentFile] = useState('_');
const [sourceType, setSourceType] = useState(
getSourceType(currentFile, props.sources[currentFile]),
);
const [showSource, setShowSource] = useState(Boolean(props.defaultShowCode));
const [iframeKey, setIframeKey] = useState(Math.random());
const currentFileCode =
props.sources[currentFile][sourceType] || props.sources[currentFile].content;
const playgroundUrl = useTSPlaygroundUrl(locale, currentFileCode);
const iframeRef = useRef<HTMLIFrameElement>();
const [color] = usePrefersColor();
// re-render iframe if prefers color changed
useEffect(() => {
setIframeKey(Math.random());
}, [color]);
function handleFileChange(filename: string) {
setCurrentFile(filename);
setSourceType(getSourceType(filename, props.sources[filename]));
}
return (
<div
style={props.style}
className={[
props.className,
'__dumi-default-previewer',
isActive ? '__dumi-default-previewer-target' : '',
]
.filter(Boolean)
.join(' ')}
id={props.identifier}
data-debug={props.debug || undefined}
data-iframe={props.iframe || undefined}
>
{props.iframe && <div className="__dumi-default-previewer-browser-nav" />}
<div
ref={demoRef}
className="__dumi-default-previewer-demo"
style={{
transform: props.transform ? 'translate(0, 0)' : undefined,
padding: props.compact || (props.iframe && props.compact !== false) ? '0' : undefined,
background: props.background,
}}
>
{props.iframe ? (
<iframe
title="dumi-previewer"
style={{
// both compatible with unit or non-unit, such as 100, 100px, 100vh
height: String(props.iframe).replace(/(\d)$/, '$1px'),
}}
key={iframeKey}
src={demoUrl}
ref={iframeRef}
/>
) : (
props.children
)}
</div>
<div className="__dumi-default-previewer-desc" data-title={props.title}>
{props.title && <AnchorLink to={`#${props.identifier}`}>{props.title}</AnchorLink>}
{props.description && (
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{ __html: props.description }}
/>
)}
</div>
<div className="__dumi-default-previewer-actions">
{openCSB && (
<button
title="Open demo on CodeSandbox.io"
className="__dumi-default-icon"
role="codesandbox"
onClick={openCSB}
/>
)}
{/* {openRiddle && (
<button
title="Open demo on Riddle"
className="__dumi-default-icon"
role="riddle"
onClick={openRiddle}
/>
)} */}
{props.motions && (
<button
title="Execute motions"
className="__dumi-default-icon"
role="motions"
disabled={isMotionRunning}
onClick={() => execMotions()}
/>
)}
{props.iframe && (
<button
title="Reload demo iframe page"
className="__dumi-default-icon"
role="refresh"
onClick={() => setIframeKey(Math.random())}
/>
)}
{!props.hideActions?.includes('EXTERNAL') && (
<Link target="_blank" to={demoUrl}>
<button
title="Open demo in new tab"
className="__dumi-default-icon"
role="open-demo"
type="button"
/>
</Link>
)}
<span />
<button
title="Copy source code"
className="__dumi-default-icon"
role="copy"
data-status={copyStatus}
onClick={() => copyCode(currentFileCode)}
/>
{sourceType === 'tsx' && showSource && (
<Link target="_blank" to={playgroundUrl}>
<button
title="Get JSX via TypeScript Playground"
className="__dumi-default-icon"
role="change-tsx"
type="button"
/>
</Link>
)}
<button
title="Toggle source code panel"
className={`__dumi-default-icon${showSource ? ' __dumi-default-btn-expand' : ''}`}
role="source"
type="button"
onClick={() => setShowSource(!showSource)}
/>
</div>
{showSource && (
<div className="__dumi-default-previewer-source-wrapper">
{!isSingleFile && (
<Tabs
className="__dumi-default-previewer-source-tab"
prefixCls="__dumi-default-tabs"
moreIcon="···"
defaultActiveKey={currentFile}
onChange={handleFileChange}
>
{Object.keys(props.sources).map(filename => (
<TabPane
tab={
filename === '_'
? `index.${getSourceType(filename, props.sources[filename])}`
: filename
}
key={filename}
/>
))}
</Tabs>
)}
<div className="__dumi-default-previewer-source">
<SourceCode code={currentFileCode} lang={sourceType} showCopy={false} />
</div>
</div>
)}
</div>
);
};
export default Previewer;

View File

@ -1,5 +0,0 @@
import React, { Redirect } from 'umi';
export default (props: any) => {
return <Redirect {...props}/>
};

View File

@ -1,58 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-code-block {
position: relative;
background-color: @c-light-bg;
& + &,
& + table {
margin-top: 16px;
}
> pre[class*='language-'] {
margin: 0;
background: transparent;
.token-line:not(:last-child) .plain:empty {
display: inline-block;
min-height: 1em;
}
}
&-copy-btn {
position: absolute;
top: 1.1em;
right: 1em;
display: inline-block;
width: 16px;
height: 16px;
border: 0;
outline: none;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s, background 0.2s;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.9;
}
&[data-status='ready'] {
background-position: -54px 0;
}
&[data-status='copied'] {
opacity: 1;
pointer-events: none;
background-position: -54px -16px;
}
}
&:not(:hover) &-copy-btn {
visibility: hidden;
opacity: 0;
}
}

View File

@ -1,41 +0,0 @@
import React from 'react';
import type { Language } from 'prism-react-renderer';
import Highlight, { defaultProps } from 'prism-react-renderer';
import { useCopy } from 'dumi/theme';
import 'prismjs/themes/prism.css';
import './SourceCode.less';
export interface ICodeBlockProps {
code: string;
lang: Language;
showCopy?: boolean;
}
export default ({ code, lang, showCopy = true }: ICodeBlockProps) => {
const [copyCode, copyStatus] = useCopy();
return (
<div className="__dumi-default-code-block">
<Highlight {...defaultProps} code={code} language={lang} theme={undefined}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{showCopy && (
<button
className="__dumi-default-icon __dumi-default-code-block-copy-btn"
data-status={copyStatus}
onClick={() => copyCode(code)}
/>
)}
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
);
};

View File

@ -1,22 +0,0 @@
@import (reference) '~dumi-theme-default/src/style/variables.less';
.__dumi-site-tree {
padding: 16px;
border: 1px solid @c-border;
border-radius: 2px;
background-color: @c-light-bg;
small {
padding-left: 24px;
font-size: 14px;
color: @c-secondary;
&::before {
content: '# ';
}
}
.ant-tree-switcher {
background: transparent;
}
}

View File

@ -1,60 +0,0 @@
import React, { useEffect, useState, ReactNode, ComponentProps } from 'react';
import { Tree } from 'antd';
import { TreeProps } from 'antd/es/tree';
import './Tree.less';
function getTreeFromList(nodes: ReactNode, prefix = '') {
const data: TreeProps['treeData'] = [];
[].concat(nodes).forEach((node, i) => {
const key = `${prefix ? `${prefix}-` : ''}${i}`;
switch (node.type) {
case 'ul':
const parent = data[data.length - 1]?.children || data;
const ulLeafs = getTreeFromList(node.props.children || [], key);
parent.push(...ulLeafs);
break;
case 'li':
const liLeafs = getTreeFromList(node.props.children, key);
data.push({
title: [].concat(node.props.children).filter(child => child.type !== 'ul'),
key,
children: liLeafs,
isLeaf: !liLeafs.length,
});
break;
default:
}
});
return data;
}
const useListToTree = (nodes: ReactNode) => {
const [tree, setTree] = useState(getTreeFromList(nodes));
useEffect(() => {
setTree(getTreeFromList(nodes));
}, [nodes]);
return tree;
};
export default (props: ComponentProps<'div'>) => {
const data = useListToTree(props.children);
return (
<Tree.DirectoryTree
className="__dumi-site-tree"
showLine={{ showLeafIcon: false }}
selectable={false}
treeData={[{ key: '0', title: props.title || '<root>', children: data }]}
defaultExpandAll
/>
);
};

View File

@ -1,48 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-locale-select {
position: relative;
display: inline-block;
border: 1px solid #dadadf;
border-radius: 14px;
transition: background 0.2s;
&:hover {
background-color: #fafafa;
}
&:not([data-locale-count='1']):not([data-locale-count='2'])::after {
content: '';
position: absolute;
top: 50%;
right: 10px;
margin-top: -3px;
width: 0;
height: 0;
border: 4px solid transparent;
border-top: 6px solid #7b7f8d;
pointer-events: none;
}
a,
span,
select {
padding: 0 24px 0 16px;
height: 28px;
text-align: center;
text-decoration: none;
line-height: 28px;
appearance: none;
border: 0;
font-size: 16px;
color: #7b7f8d;
background: transparent;
outline: none;
cursor: pointer;
}
a,
span {
padding-right: 16px;
}
}

View File

@ -1,53 +0,0 @@
import type { FC} from 'react';
import React, { useContext } from 'react';
// @ts-ignore
import { history } from 'dumi';
import { context, Link } from 'dumi/theme';
import './LocaleSelect.less';
const LocaleSelect: FC<{ location: any }> = ({ location }) => {
const {
base,
locale,
config: { locales },
} = useContext(context);
const firstDiffLocale = locales.find(({ name }) => name !== locale);
function getLocaleTogglePath(target: string) {
const baseWithoutLocale = base.replace(`/${locale}`, '');
const pathnameWithoutLocale = location.pathname.replace(base, baseWithoutLocale) || '/';
// append locale prefix to path if it is not the default locale
if (target !== locales[0].name) {
// compatiable with integrate route prefix /~docs
const routePrefix = `${baseWithoutLocale}/${target}`.replace(/\/\//, '/');
const pathnameWithoutBase = location.pathname.replace(
// to avoid stripped the first /
base.replace(/^\/$/, '//'),
'',
);
return `${routePrefix}${pathnameWithoutBase}`.replace(/\/$/, '');
}
return pathnameWithoutLocale;
}
return firstDiffLocale ? (
<div className="__dumi-default-locale-select" data-locale-count={locales.length}>
{locales.length > 2 ? (
<select value={locale} onChange={ev => history.push(getLocaleTogglePath(ev.target.value))}>
{locales.map(localeItem => (
<option value={localeItem.name} key={localeItem.name}>
{localeItem.label}
</option>
))}
</select>
) : (
<Link to={getLocaleTogglePath(firstDiffLocale.name)}>{firstDiffLocale.label}</Link>
)}
</div>
) : null;
};
export default LocaleSelect;

View File

@ -1,172 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-navbar {
position: fixed;
z-index: 101;
top: 0;
left: 0;
right: 0;
display: none;
align-items: center;
justify-content: space-between;
padding: 0 58px;
height: @s-nav-height;
white-space: nowrap;
background: #fff;
box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 0.05);
@media @mobile {
display: flex;
justify-content: center;
height: @s-mobile-nav-height;
}
&-toggle {
position: absolute;
top: 14px;
left: 16px;
display: none;
width: 22px;
height: 22px;
border: 0;
outline: none;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAAA4CAYAAAB5YT9uAAAAAXNSR0IArs4c6QAAASVJREFUeAHt3DuOwjAUBVAHscWZjaSkAIqU2QjskV80QwpsF3ArdGiw3hW3OGnQM2Iof6/dfr7+n71/LjAdx+HRsvm8SkNPAHBPJ5ABDiD2KgD3dAIZ4ABirwJwTyeQAQ4g9ioA93QC2fbZsSm/z7MDAQIECBAgQIAAAQIECBAgQIAAAQKvAsvV8mO8O8yn19jkXYHpMC7byXVdeS0/75b5XFvAwr1tE0kARxjbJYDbNpEEcISxXQK4bRNJAEcY2yWA2zaRZP0ePJRzpFEJAQIECBAgQIAAAQIECBAgQIAAgW8VWK/tj7Nb5eBTnvbjsp1c15WX4ncRQeB7lb8zyHrW29xo1F1iU8AxynoR4LpLbAo4RlkvAlx3iU0BxyjrRYDrLrHpDVSAEEPXScHTAAAAAElFTkSuQmCC')
no-repeat center / contain;
@media @mobile {
display: block;
}
}
&-logo {
display: inline-block;
height: 40px;
width: 180px;
color: #080e29;
font-weight: 500;
text-decoration: none;
font-size: 24px;
line-height: 40px;
&:not([data-plaintext]) {
padding-left: 56px;
background: url(@img-logo) no-repeat 0 / contain;
}
&:active,
&:hover {
color: #080e29;
}
@media @mobile {
height: 28px;
line-height: 28px;
&:not([data-plaintext]) {
padding-left: 36px;
}
}
}
nav {
> span {
position: relative;
margin-left: 40px;
display: inline-block;
color: @c-text;
height: @s-nav-height;
cursor: pointer;
font-size: 14px;
line-height: @s-nav-height;
text-decoration: none;
letter-spacing: 0;
> a {
color: #4d5164;
text-decoration: none;
&:hover,
&.active {
color: @c-primary;
}
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: -18px;
left: -18px;
}
&.active::after {
content: '';
position: absolute;
bottom: 0;
left: -2px;
right: -2px;
height: 2px;
background-color: @c-primary;
border-radius: 1px;
}
}
+ *:not(a) {
margin-left: 40px;
}
// second nav
> ul {
list-style: none;
position: absolute;
top: 100%;
left: 50%;
margin: 0;
min-width: 100px;
padding: 8px 18px;
line-height: 2;
background-color: #fff;
box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 0.08);
transform: translate(-50%);
transform-origin: top;
border-radius: 1px;
transition: all 0.2s;
a {
position: relative;
display: block;
color: @c-text;
text-decoration: none;
&:hover,
&.active {
color: @c-primary;
}
}
}
&:not(:hover) > ul {
visibility: hidden;
pointer-events: none;
transform: translate(-50%) scaleY(0.9);
opacity: 0;
}
}
.@{prefix}-search + .@{prefix}-locale-select {
margin-left: 40px;
}
@media @mobile {
> a,
> span,
> div {
display: none;
}
}
}
&[data-mode='site'] {
display: flex;
}
}

View File

@ -1,68 +0,0 @@
import type { FC, MouseEvent } from 'react';
import React, { useContext } from 'react';
import { context, Link, NavLink } from 'dumi/theme';
import LocaleSelect from './LocaleSelect';
import './Navbar.less';
interface INavbarProps {
location: any;
navPrefix?: React.ReactNode;
onMobileMenuClick: (ev: MouseEvent<HTMLButtonElement>) => void;
}
const Navbar: FC<INavbarProps> = ({ onMobileMenuClick, navPrefix, location }) => {
const {
base,
config: { mode, title, logo },
nav: navItems,
} = useContext(context);
return (
<div className="__dumi-default-navbar" data-mode={mode}>
{/* menu toogle button (only for mobile) */}
<button className="__dumi-default-navbar-toggle" onClick={onMobileMenuClick} />
{/* logo & title */}
<Link
className="__dumi-default-navbar-logo"
style={{
backgroundImage: logo && `url('${logo}')`,
}}
to={base}
data-plaintext={logo === false || undefined}
>
{/* {title} */}
</Link>
<nav>
{navPrefix}
{/* nav */}
{navItems.map(nav => {
const child = Boolean(nav.children?.length) && (
<ul>
{nav.children.map(item => (
<li key={item.path}>
<NavLink to={item.path}>{item.title}</NavLink>
</li>
))}
</ul>
);
return (
<span key={nav.title || nav.path}>
{nav.path ? (
<NavLink to={nav.path} key={nav.path}>
{nav.title}
</NavLink>
) : (
nav.title
)}
{child}
</span>
);
})}
<LocaleSelect location={location} />
</nav>
</div>
);
};
export default Navbar;

View File

@ -1,107 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-search {
margin-left: 20px;
position: relative;
display: inline-block;
&-input {
width: 200px;
height: 32px;
padding: 0 38px 0 14px;
color: @c-heading;
font-size: 14px;
border: 0;
outline: none;
transition: all 0.2s;
border-radius: 16px;
background: url('data:image/png;base64,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')
#f5f6f7 no-repeat right 14px center / 16px;
appearance: none;
}
> ul {
list-style: none;
position: absolute;
top: 100%;
left: 0;
z-index: 10;
margin: 8px 0 0;
min-width: 280px;
max-width: 500px;
padding: 6px 0;
background-color: #fff;
border: 1px solid @c-border;
border-radius: 1px;
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.05);
box-sizing: border-box;
&:empty {
display: none;
}
li {
font-size: 15px;
a {
display: block;
padding: 6px 20px;
color: @c-secondary;
text-decoration: none;
transition: background-color 0.3s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: @c-primary;
background-color: @c-light-bg;
}
}
span:first-child {
position: relative;
display: inline-block;
max-width: 100%;
padding-right: 26px;
vertical-align: -0.37em;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.8;
&::after {
content: '>';
position: absolute;
top: 50%;
right: 6px;
opacity: 0.6;
transform: translateY(-54%);
}
}
}
}
@media only screen and (max-width: 1024px) {
margin-right: -14px;
> input:not(:focus) {
width: 32px;
padding-right: 0;
box-shadow: none;
cursor: pointer;
background-position: right 8px center;
+ ul {
transition: 0.1s visibility;
visibility: hidden;
}
}
}
@media @mobile {
position: absolute;
top: 9px;
right: 24px;
display: block !important;
}
}

View File

@ -1,41 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSearch, AnchorLink } from 'dumi/theme';
import './SearchBar.less';
export default () => {
const [keywords, setKeywords] = useState<string>('');
const [items, setItems] = useState([]);
const input = useRef<HTMLInputElement>();
const result = useSearch(keywords);
useEffect(() => {
if (Array.isArray(result)) {
setItems(result);
} else if (typeof result === 'function') {
result(`.${input.current.className}`);
}
}, [result]);
return (
<div className="__dumi-default-search">
<input
className="__dumi-default-search-input"
type="search"
ref={input}
{...(Array.isArray(result)
? { value: keywords, onChange: ev => setKeywords(ev.target.value) }
: {})}
/>
<ul>
{items.map(meta => (
<li key={meta.path} onClick={() => setKeywords('')}>
<AnchorLink to={meta.path}>
{meta.parent?.title && <span>{meta.parent.title}</span>}
{meta.title}
</AnchorLink>
</li>
))}
</ul>
</div>
);
};

View File

@ -1,321 +0,0 @@
@import (reference) '../style/variables.less';
.@{prefix}-menu {
position: fixed;
z-index: 100;
top: 0;
left: 0;
bottom: 0;
width: @s-menu-width;
background-color: #f2f5fa;
box-sizing: border-box;
transition: left 0.3s;
&[data-hidden] {
display: none;
}
@media @mobile {
left: -@s-menu-mobile-width;
top: @s-mobile-nav-height;
display: block !important;
width: @s-menu-mobile-width;
background-color: #fff;
&[data-mobile-show] {
left: 0;
}
}
// shadow
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: block;
width: 20px;
background: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.03));
pointer-events: none;
// use border on mobile devices
@media @mobile {
width: 1px;
background: @c-border;
}
}
&-header {
position: relative;
padding-top: 40px;
text-align: center;
border-bottom: 1px solid @c-border;
@media @mobile {
display: none;
}
.@{prefix}-menu-logo {
display: inline-block;
width: 70%;
height: 65px;
background: url(@img-logo) no-repeat 0 / contain;
}
h1 {
margin: 10px 0 0;
color: @c-heading;
font-weight: 500;
line-height: 1.40625;
}
p {
margin: 0 0 5px;
color: lighten(@c-secondary, 10%);
// badges
> object[data^='https://img.shields.io'] {
max-height: 20px;
}
+ p {
margin-bottom: 10px;
}
}
}
&-doc-locale {
padding: 16px 0;
text-align: center;
border-bottom: 1px solid @c-border;
&:empty {
display: none;
}
}
&-inner {
width: 100%;
height: 100%;
overflow: auto;
overscroll-behavior: contain;
// common list styles
ul {
list-style: none;
margin: 0;
padding: 0;
font-size: 16px;
li {
color: @c-text;
a,
> span {
position: relative;
display: block;
padding-right: 24px;
color: @c-heading;
line-height: 2.4;
text-decoration: none;
outline: none;
transition: color 0.3s, background 0.3s;
span {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover,
&.active {
color: @c-primary;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: -10px;
margin-top: -2.5px;
display: inline-block;
width: 5px;
height: 5px;
background-color: @c-primary;
border-radius: 50%;
opacity: 0;
transition: transform 0.2s, opacity 0.2s;
transform: scale(0) translateX(-10px);
}
}
&.active a,
a.active {
&::before {
opacity: 1;
transform: scale(1) translateX(0);
}
}
// level larger, offset larger, font size smaller
ul {
font-size: 0.9em;
padding-left: 1em;
}
}
}
// 1-level list styles
> ul {
> li > a {
line-height: 2.875;
&:not([href]) {
padding-top: 24px;
line-height: 1;
font-weight: 500;
color: @c-heading !important;
background: transparent !important;
cursor: default;
}
}
> li:first-child > a:not([href]) {
padding-top: 0;
}
}
// n-level list styles
> ul ul {
a {
color: @c-secondary;
&.active {
color: @c-primary;
}
}
}
.@{prefix}-menu-mobile-area {
display: none;
padding-bottom: 16px;
margin-bottom: 16px;
text-align: center;
border-bottom: 1px solid @c-border;
@media @mobile {
display: block;
}
}
// mobile nav list
.@{prefix}-menu-nav-list {
padding: 16px 0;
> li,
> li > a {
padding-right: 0;
line-height: 2.4;
ul {
padding-left: 0;
a {
padding-right: 0;
font-size: 90%;
}
}
}
}
// menu list
.@{prefix}-menu-list {
padding: 8px 0;
margin-bottom: 40px;
> li > a {
@c-active-bg: #e8ecf4;
padding-left: 28px;
&.active {
background: linear-gradient(to left, #e8ecf4, rgba(232, 236, 244, 0));
}
~ ul {
margin-top: 8px;
margin-left: 28px;
}
@media @mobile {
padding-left: 16px;
~ ul {
margin-left: 16px;
}
}
}
}
}
&[data-mode='site'] {
&::after {
width: 1px;
background: @c-border;
}
.@{prefix}-menu-list {
padding: 0;
> li > a {
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
display: block;
width: 3px;
background-color: @c-primary;
visibility: hidden;
opacity: 0;
transition: all 0.3s;
border-radius: 1px;
}
&.active {
z-index: 1;
background: linear-gradient(to left, #f8faff, rgba(248, 250, 255, 0));
&::after {
opacity: 1;
visibility: visible;
}
}
}
}
@media @desktop {
top: @s-nav-height;
width: @s-site-menu-width;
padding-top: 50px;
background: transparent;
.@{prefix}-menu-nav,
.@{prefix}-menu-header {
display: none;
}
.@{prefix}-menu-list > li > a {
padding-left: 58px;
~ ul {
margin-left: 58px;
}
}
}
}
}

View File

@ -1,139 +0,0 @@
import type { FC} from 'react';
import React, { useContext } from 'react';
import { context, Link, NavLink } from 'dumi/theme';
import LocaleSelect from './LocaleSelect';
import SlugList from './SlugList';
import './SideMenu.less';
interface INavbarProps {
mobileMenuCollapsed: boolean;
location: any;
}
const SideMenu: FC<INavbarProps> = ({ mobileMenuCollapsed, location }) => {
const {
config: {
logo,
title,
description,
mode,
repository: { url: repoUrl },
},
menu,
nav: navItems,
base,
meta,
} = useContext(context);
const isHiddenMenus =
Boolean((meta.hero || meta.features || meta.gapless) && mode === 'site') ||
meta.sidemenu === false ||
undefined;
return (
<div
className="__dumi-default-menu"
data-mode={mode}
data-hidden={isHiddenMenus}
data-mobile-show={!mobileMenuCollapsed || undefined}
>
<div className="__dumi-default-menu-inner">
<div className="__dumi-default-menu-header">
<Link
to={base}
className="__dumi-default-menu-logo"
style={{
backgroundImage: logo && `url('${logo}')`,
}}
/>
<h1>{title}</h1>
<p>{description}</p>
{/* github star badge */}
{/github\.com/.test(repoUrl) && mode === 'doc' && (
<p>
<object
type="image/svg+xml"
data={`https://img.shields.io/github/stars${
repoUrl.match(/((\/[^\/]+){2})$/)[1]
}?style=social`}
/>
</p>
)}
</div>
{/* mobile nav list */}
{navItems.length ? (
<div className="__dumi-default-menu-mobile-area">
<ul className="__dumi-default-menu-nav-list">
{navItems.map(nav => {
const child = Boolean(nav.children?.length) && (
<ul>
{nav.children.map(item => (
<li key={item.path || item.title}>
<NavLink to={item.path}>{item.title}</NavLink>
</li>
))}
</ul>
);
return (
<li key={nav.path || nav.title}>
{nav.path ? <NavLink to={nav.path}>{nav.title}</NavLink> : nav.title}
{child}
</li>
);
})}
</ul>
{/* site mode locale select */}
<LocaleSelect location={location} />
</div>
) : (
<div className="__dumi-default-menu-doc-locale">
{/* doc mode locale select */}
<LocaleSelect location={location} />
</div>
)}
{/* menu list */}
<ul className="__dumi-default-menu-list">
{!isHiddenMenus &&
menu.map(item => {
// always use meta from routes to reduce menu data size
const hasSlugs = Boolean(meta.slugs?.length);
const hasChildren = item.children && Boolean(item.children.length);
const show1LevelSlugs =
meta.toc === 'menu' && !hasChildren && hasSlugs && item.path === location.pathname.replace(/([^^])\/$/, '$1');
return (
<li key={item.path || item.title}>
<NavLink to={item.path} exact={!(item.children && item.children.length)}>
{item.title}
</NavLink>
{/* group children */}
{Boolean(item.children && item.children.length) && (
<ul>
{item.children.map(child => (
<li key={child.path}>
<NavLink to={child.path} exact>
<span>{child.title}</span>
</NavLink>
{/* group children slugs */}
{Boolean(
meta.toc === 'menu' &&
typeof window !== 'undefined' &&
child.path === location.pathname &&
hasSlugs,
) && <SlugList slugs={meta.slugs} />}
</li>
))}
</ul>
)}
{/* group slugs */}
{show1LevelSlugs && <SlugList slugs={meta.slugs} />}
</li>
);
})}
</ul>
</div>
</div>
);
};
export default SideMenu;

View File

@ -1,22 +0,0 @@
@import (reference) '../style/variables.less';
ul[role='slug-list'] {
&:empty {
margin: 0 !important;
padding: 0 !important;
}
li {
> a.active {
color: darken(@c-primary, 2%);
}
&[data-depth='3'] {
padding-left: 24px;
}
&[data-depth='4'] {
padding-left: 48px;
}
}
}

View File

@ -1,20 +0,0 @@
import type { FC } from 'react';
import React from 'react';
import { AnchorLink } from 'dumi/theme';
import './SlugList.less';
const SlugsList: FC<{ slugs: any; className?: string }> = ({ slugs, ...props }) => (
<ul role="slug-list" {...props}>
{slugs
.filter(({ depth }) => depth > 1 && depth < 5)
.map(slug => (
<li key={slug.heading} title={slug.value} data-depth={slug.depth}>
<AnchorLink to={`#${slug.heading}`}>
<span>{slug.value}</span>
</AnchorLink>
</li>
))}
</ul>
);
export default SlugsList;

View File

@ -1,120 +0,0 @@
import React, { useContext, useState, useEffect } from 'react';
import type { IRouteComponentProps } from '@umijs/types';
import { context, Link } from 'dumi/theme';
import Navbar from './components/Navbar';
import SideMenu from './components/SideMenu';
import SlugList from './components/SlugList';
import SearchBar from './components/SearchBar';
import './style/layout.less';
const Hero = hero => (
<>
<div className="__dumi-default-layout-hero">
{hero.image && <img src={hero.image} />}
<h1>{hero.title}</h1>
<div dangerouslySetInnerHTML={{ __html: hero.desc }} />
{hero.actions &&
hero.actions.map(action => (
<Link to={action.link} key={action.text}>
<button type="button">{action.text}</button>
</Link>
))}
</div>
</>
);
const Features = features => (
<div className="__dumi-default-layout-features">
{features.map(feat => (
<dl key={feat.title} style={{ backgroundImage: feat.icon ? `url(${feat.icon})` : undefined }}>
{feat.link ? (
<Link to={feat.link}>
<dt>{feat.title}</dt>
</Link>
) : (
<dt>{feat.title}</dt>
)}
<dd dangerouslySetInnerHTML={{ __html: feat.desc }} />
</dl>
))}
</div>
);
const Layout: React.FC<IRouteComponentProps> = ({ children, location }) => {
const {
config: { mode, repository },
meta,
locale,
} = useContext(context);
const { url: repoUrl, branch, platform } = repository;
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true);
const isSiteMode = mode === 'site';
const showHero = isSiteMode && meta.hero;
const showFeatures = isSiteMode && meta.features;
const showSideMenu = meta.sidemenu !== false && !showHero && !showFeatures && !meta.gapless;
const showSlugs =
!showHero &&
!showFeatures &&
Boolean(meta.slugs?.length) &&
(meta.toc === 'content' || meta.toc === undefined) &&
!meta.gapless;
const isCN = /^zh|cn$/i.test(locale);
const updatedTimeIns = new Date(meta.updatedTime);
const updatedTime: any = `${updatedTimeIns.toLocaleDateString([], { hour12: false })} ${updatedTimeIns.toLocaleTimeString([], { hour12: false })}`;
const repoPlatform =
{ github: 'GitHub', gitlab: 'GitLab' }[
(repoUrl || '').match(/(github|gitlab)/)?.[1] || 'nothing'
] || platform;
return (
<div
className="__dumi-default-layout"
data-route={location.pathname}
data-show-sidemenu={String(showSideMenu)}
data-show-slugs={String(showSlugs)}
data-site-mode={isSiteMode}
data-gapless={String(!!meta.gapless)}
onClick={() => {
if (menuCollapsed) return;
setMenuCollapsed(true);
}}
>
<div className={'tip'} style={{ position: 'fixed', top: 64, left: 0, right: 0, textAlign: 'center', lineHeight: '36px', background: '#fffbe6', borderBottom: '1px solid rgba(60, 72, 88, 0.05)', zIndex: 100 }}>
{isCN ? '文档还在建设中,与实际代码存在差异' : 'The documentation is still under construction and differs somewhat from the actual code.'}
</div>
<Navbar
location={location}
navPrefix={<SearchBar />}
onMobileMenuClick={ev => {
setMenuCollapsed(val => !val);
ev.stopPropagation();
}}
/>
<SideMenu mobileMenuCollapsed={menuCollapsed} location={location} />
{showSlugs && <SlugList slugs={meta.slugs} className="__dumi-default-layout-toc" />}
{showHero && Hero(meta.hero)}
{showFeatures && Features(meta.features)}
<div className="__dumi-default-layout-content">
{children}
{!showHero && !showFeatures && meta.filePath && !meta.gapless && (
<div className="__dumi-default-layout-footer-meta">
{repoPlatform && (
<Link to={`${repoUrl}/edit/${branch}/${meta.filePath}`}>
{isCN ? `${repoPlatform} 上编辑此页` : `Edit this doc on ${repoPlatform}`}
</Link>
)}
{/* <span data-updated-text={isCN ? '最后更新时间:' : 'Last update: '}>{updatedTime}</span> */}
</div>
)}
{(showHero || showFeatures) && meta.footer && (
<div
className="__dumi-default-layout-footer"
dangerouslySetInnerHTML={{ __html: meta.footer }}
/>
)}
</div>
</div>
);
};
export default Layout;

View File

@ -1,328 +0,0 @@
@import './markdown.less';
@import './variables.less';
@s-toc-width: 350px;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol;
font-variant: tabular-nums;
font-feature-settings: 'tnum';
-webkit-font-smoothing: antialiased;
}
.@{prefix}-layout {
box-sizing: border-box;
min-height: 100vh;
padding: 16px (@s-content-margin + @s-toc-width) 50px @s-menu-width + @s-content-margin;
@media @mobile {
padding-top: 98px !important;
padding-left: 16px !important;
padding-right: 16px !important;
.tip {
top: 50px !important;
}
}
&[data-gapless='true'] {
padding-top: @s-nav-height !important;
padding-right: 0 !important;
padding-left: 0 !important;
padding-bottom: 0;
@media @mobile {
padding-top: @s-mobile-nav-height !important;
}
}
&[data-show-sidemenu='false'] {
padding-left: @s-content-margin;
}
&[data-show-slugs='false'] {
padding-right: @s-content-margin;
}
&[data-site-mode='true'] {
padding-top: @s-nav-height + 50px;
&[data-show-sidemenu='true'] {
padding-left: @s-site-menu-width + 50px;
}
&[data-show-slugs='true'] {
padding-right: @s-content-margin + @s-toc-width + 14;
}
.@{prefix}-layout-content > .markdown:first-child > *:first-child {
margin-top: 0;
}
.@{prefix}-layout-toc {
top: 114px;
max-height: calc(100vh - 154px);
}
}
&-hero {
margin: -50px -58px 0;
padding: 100px 0;
text-align: center;
background-color: #f5f6f8;
@media @mobile {
margin: -16px -16px 0;
padding: 48px 0;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 1rem;
}
h1 {
margin: 0 0 16px;
font-size: 48px;
font-weight: 600;
line-height: 56px;
color: #080e29;
+ div {
margin: 16px 0 32px;
opacity: 0.78;
.markdown {
font-size: 16px;
}
}
}
button {
margin-right: 16px;
padding: 0 32px;
height: 44px;
color: @c-primary;
font-size: 16px;
background: transparent;
border: 1px solid @c-primary;
border-radius: 22px;
box-sizing: border-box;
cursor: pointer;
outline: none;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.9;
}
}
a:last-child button {
margin-right: 0;
color: #fff;
background: @c-primary;
}
}
&-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 96px;
grid-row-gap: 56px;
padding: 72px 0;
> dl {
flex: 1;
margin: 0;
text-align: center;
background: no-repeat center top / auto 48px;
&[style*='background-image'] {
padding-top: 64px;
}
dt {
margin-bottom: 12px;
font-size: 20px;
line-height: 1;
color: @c-heading;
}
a {
transition-duration: none;
}
a dt {
color: @c-link;
transition: opacity 0.2s;
&:hover {
opacity: 0.7;
text-decoration: underline;
}
&:active {
opacity: 0.9;
}
}
dd {
margin: 0;
.markdown {
color: @c-secondary;
font-size: 14px;
line-height: 22px;
> p:first-child {
margin-top: 0;
}
> p:last-child {
margin-bottom: 0;
}
}
}
}
@media @mobile {
display: block;
padding: 40px 0;
> dl {
text-align: left;
background-position: left top;
&[style*='background-image'] {
padding: 0 0 0 60px;
}
+ dl {
margin-top: 32px;
}
}
}
}
&-features,
&-features + &-content,
&-hero + &-content {
margin-left: auto;
margin-right: auto;
max-width: 960px;
}
&-hero + &-content {
margin-top: 60px;
}
&-toc {
list-style: none;
position: fixed;
z-index: 10;
top: 50px;
right: 0;
width: @s-toc-width;
max-height: calc(90vh - 80px);
margin: 0;
padding: 0 24px 0 0;
background-color: #fff;
box-shadow: 0 0 16px 16px #fff;
box-sizing: content-box;
overflow: auto;
@media @mobile {
display: none;
}
li {
position: relative;
margin: 0;
padding: 4px 0 4px 6px;
text-indent: 24px;
font-size: 13px;
// line-height: 1.40625;
line-height: 1.60625;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
a {
color: @c-text;
text-decoration: none;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
display: inline-block;
width: 2px;
background: @c-border;
}
&:hover {
color: lighten(@c-primary, 5%);
}
&:active {
color: lighten(@c-primary, 3%);
}
&.active {
color: @c-primary;
&::before {
background: @c-primary;
}
}
}
}
}
&-footer-meta {
margin-top: 40px;
padding-top: 24px;
display: flex;
color: @c-secondary;
font-size: 14px;
justify-content: space-between;
border-top: 1px solid @c-border;
@media only screen and (max-width: 960px) {
display: block;
}
> a {
margin-bottom: 4px;
display: block;
color: @c-primary;
text-decoration: none;
}
> span:last-child {
&::before {
content: attr(data-updated-text);
color: @c-primary;
}
}
}
}
.__dumi-default-layout-footer {
margin: 72px 0 -32px;
padding-top: 24px;
border-top: 1px solid @c-border;
text-align: center;
.markdown {
color: #b0b1ba;
}
}

View File

@ -1,196 +0,0 @@
@import (reference) './variables.less';
.markdown {
color: @c-text;
font-size: 15px;
line-height: 1.60625;
&:not(:first-child):empty {
min-height: 32px;
}
// titles
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 42px 0 18px;
color: @c-heading;
font-weight: 500;
line-height: 1.40625;
// anchor link
&:hover > a[aria-hidden] {
float: left;
margin-top: 0.06em;
margin-left: -20px;
width: 20px;
padding-right: 4px;
line-height: 1;
box-sizing: border-box;
@media @mobile {
width: 14px;
margin-left: -14px;
}
&::after {
content: '#';
display: inline-block;
vertical-align: middle;
font-size: 20px;
}
span {
display: none;
}
}
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
margin-top: 16px;
}
}
h1 {
margin-top: 48px;
margin-bottom: 32px;
font-size: 32px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 20px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
}
// paragraph
p {
margin: 16px 0;
}
// inline code
*:not(pre) code {
padding: 2px 5px;
color: #d56161;
background: darken(@c-light-bg, 1%);
}
code {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
}
// code block
pre {
font-size: 14px;
background: darken(@c-light-bg, 1%);
&:not([class^='language-']) {
padding: 1em;
}
}
// horizontal line
hr {
margin: 16px 0;
border: 0;
border-top: 1px solid @c-border;
}
// blockquote
blockquote {
margin: 16px 0;
padding: 0 24px;
color: fadeout(@c-text, 30%);
border-left: 4px solid @c-border;
overflow: hidden;
}
// list
ul,
ol {
margin: 8px 0 8px 32px;
padding: 0;
li {
margin-bottom: 4px;
}
}
// table
table {
width: 100%;
border-collapse: collapse;
border: 1px solid @c-border;
th,
td {
padding: 10px 24px;
border: 1px solid @c-border;
}
th {
font-weight: 600;
background: @c-light-bg;
}
td:first-child {
font-weight: 500;
}
a {
svg {
display: none;
}
}
}
// links
a {
color: @c-link;
text-decoration: none;
transition: opacity 0.2s;
outline: none;
&:hover {
opacity: 0.7;
text-decoration: underline;
}
&:active {
opacity: 0.9;
}
}
// images
img {
max-width: 100%;
}
}
.@{prefix} {
&-external-link-icon {
vertical-align: -0.155em;
margin-left: 2px;
}
}

View File

@ -1,26 +0,0 @@
/* 颜色表 */
@c-primary: #4569d4;
@c-heading: #454d64;
@c-text: #454d64;
@c-secondary: #717484;
@c-link: @c-primary;
@c-border: #ebedf1;
@c-light-bg: #f9fafb;
/* 尺寸表 */
@s-nav-height: 64px;
@s-mobile-nav-height: 50px;
@s-menu-width: 260px;
@s-site-menu-width: 350px;
@s-menu-mobile-width: 80vw;
@s-content-margin: 58px;
@img-logo: 'data:image/png;base64,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';
@prefix: __dumi-default;
@mobile: ~'only screen and (max-width: 767px)';
@desktop: ~'only screen and (min-width: 768px)';
@icons: 'data:image/png;base64,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';
.@{prefix}-icon {
background: url(@icons) no-repeat ~'0 0/230px auto';
}

View File

@ -1,315 +0,0 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, queryByAttribute, fireEvent } from '@testing-library/react';
import type { MemoryHistory} from '@umijs/runtime';
import { createMemoryHistory, Router } from '@umijs/runtime';
import { context as Context } from 'dumi/theme';
import SourceCode from '../builtins/SourceCode';
import Alert from '../builtins/Alert';
import Badge from '../builtins/Badge';
import Previewer from '../builtins/Previewer';
import API from '../builtins/API';
import Layout from '../layout';
let history: MemoryHistory;
// mock history location which import from 'dumi'
jest.mock('dumi', () => ({
history: { location: { pathname: '/' } },
}));
describe('default theme', () => {
history = createMemoryHistory({ initialEntries: ['/', '/en-US'], initialIndex: 0 });
const baseCtx = {
title: 'test',
locale: 'zh-CN',
routes: [
{
path: '/',
title: '首页',
meta: {},
},
{
path: '/en-US',
title: 'Home',
meta: {},
},
],
config: {
locales: [
{ name: 'zh-CN', label: '中文' },
{ name: 'en-US', label: 'English' },
],
menus: {},
navs: {},
title: 'test',
logo: '/',
mode: 'site' as 'doc' | 'site',
repository: { branch: 'mater' },
},
meta: {},
menu: [
{
title: '分组',
children: [
{
title: 'English',
path: '/en',
},
],
},
],
nav: [
{
path: '/',
title: '首页',
children: [],
},
{
title: '生态',
children: [
{
path: 'https://d.umijs.org',
title: 'GitHub',
children: [],
},
],
},
],
base: '/',
};
const baseProps = {
history,
location: { ...history.location, query: {} },
match: { params: {}, isExact: true, path: '/', url: '/' },
route: { path: '/', routes: baseCtx.routes },
};
it('should render site home page', () => {
const wrapper = ({ children }) => (
<Context.Provider
value={{
...baseCtx,
meta: {
title: 'test',
hero: {
title: 'Hero',
desc: 'Hero Description',
actions: [{ text: '开始', link: '/' }],
},
features: [
{ title: 'Feat', desc: 'Feature' },
{ title: 'Feat2', link: '/' },
],
},
}}
>
{children}
</Context.Provider>
);
const { container, getAllByText, getByText } = render(
<Router history={history}>
<Layout {...baseProps}>
<h1>Home Page</h1>
</Layout>
</Router>,
{ wrapper },
);
// expect navbar be rendered
expect(getAllByText('首页')).not.toBeNull();
// expect content be rendered
expect(getByText('Home Page')).not.toBeNull();
// expect hero be rendered
expect(getByText('Hero')).not.toBeNull();
// expect features be rendered
expect(getByText('Feature')).not.toBeNull();
expect(getByText('Feat2')).not.toBeNull();
// trigger mobile menu display
queryByAttribute('class', container, '__dumi-default-navbar-toggle').click();
// expect sidemenu display for mobile
expect(queryByAttribute('data-mobile-show', container, 'true')).not.toBeNull();
});
it('should render documentation page', async () => {
const updatedTime = 1604026996000;
const wrapper = ({ children }) => (
<Context.Provider
value={{
...baseCtx,
meta: {
title: 'test',
slugs: [{ value: 'Slug A', heading: 'a', depth: 2 }],
updatedTime,
filePath: 'temp',
},
}}
>
{children}
</Context.Provider>
);
const { getByText, getAllByText } = render(
<Router history={history}>
<Layout {...baseProps}>
<h1>Doc</h1>
</Layout>
</Router>,
{ wrapper },
);
// expect slugs be rendered
expect(getByText('Slug A')).not.toBeNull();
// expect footer date show
expect(new Date(updatedTime).toLocaleString([], { hour12: false })).not.toBeNull();
// trigger locale change
getAllByText('English')[0].click();
// expect location change
expect(history.location.pathname).toEqual(baseCtx.routes[1].path);
});
it('should render builtin components correctly', () => {
const code = "console.log('Hello World!')";
const wrapper = ({ children }) => (
<Context.Provider
value={{
...baseCtx,
meta: {
title: 'test',
slugs: [{ value: 'Slug A', heading: 'a', depth: 2 }],
},
}}
>
{children}
</Context.Provider>
);
const { getByText, getByTitle, getAllByTitle, container } = render(
<Router history={history}>
<Layout {...baseProps}>
<>
<a href="" id="btn">
click
</a>
<SourceCode code={code} lang="javascript" />
<Alert type="info">Alert</Alert>
<Badge type="info">Badge</Badge>
<Previewer
title="demo-1"
identifier="demo-1"
sources={{
_: {
jsx: "export default () => 'JavaScript'",
tsx: "export default () => 'TypeScript'",
},
}}
dependencies={{}}
>
<>demo-1 Content</>
</Previewer>
<Previewer
title="demo-2"
identifier="demo-2"
sources={{
_: {
jsx: "export default () => 'Main'",
},
'Other.jsx': {
import: './Other.jsx',
content: "export default () => 'Other'",
},
}}
dependencies={{}}
>
<>demo-2 Content</>
</Previewer>
<Previewer
title="demo-3"
identifier="demo-3"
sources={{
_: {
jsx: "export default () => 'Main'",
},
'Other.jsx': {
import: './Other.jsx',
content: "export default () => 'Other'",
},
}}
dependencies={{}}
iframe={100}
>
<>demo-3 Content</>
</Previewer>
<API identifier="MultipleExports" export="Other" />
</>
</Layout>
</Router>,
{ wrapper },
);
// toggle side menu display
fireEvent(
container.querySelector('.__dumi-default-navbar-toggle'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
fireEvent(
container.querySelector('#btn'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
// expect SourceCode highlight
expect(getByText('console')).toHaveClass('token');
// expect Alert be rendered
expect(getByText('Alert')).toHaveAttribute('type', 'info');
// expect Badge be rendered
expect(getByText('Badge')).toHaveClass('__dumi-default-badge');
// expect Previewer be rendered
expect(getByText('demo-1')).not.toBeNull();
// trigger source code display for demo-1
getAllByTitle('Toggle source code panel')[0].click();
// expect show TypeScript code default
expect(getByText("'TypeScript'")).not.toBeNull();
// trigger source code display for demo-2
getAllByTitle('Toggle source code panel')[1].click();
// expect show code of main file
expect(getByText("'Main'")).not.toBeNull();
// trigger file change
getByText('Other.jsx').click();
// expect show code of main file
expect(getByText("'Other'")).not.toBeNull();
// expect render iframe demo
(container.querySelector('[data-iframe] button[role=refresh]') as HTMLElement).click();
expect(container.querySelector('[data-iframe]').innerHTML).not.toContain('demo-3 Content');
expect(container.querySelector('[data-iframe] iframe')).not.toBeNull();
expect((container.querySelector('[data-iframe] iframe') as HTMLElement).style.height).toEqual(
'100px',
);
// expect render API property
expect(getByText('other', { selector: 'table td' })).not.toBeNull();
});
});

View File

@ -1,14 +0,0 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.gradle]
indent_size = 4

View File

@ -1,72 +0,0 @@
########## DOCKER COMPOSE ENV ##########
ADMINER_PORT=8080
DB_MYSQL_PORT=3306
DB_POSTGRES_PORT=5432
VERDACCIO_PORT=4873
APP_PORT=13001
########## NOCOBASE ENV ##########
# DATABASE
DB_DIALECT=sqlite
DB_STORAGE=db.sqlite
# DB_DIALECT=postgres
# DB_HOST=localhost
# DB_PORT=5432
# DB_DATABASE=nocobase
# DB_USER=nocobase
# DB_PASSWORD=nocobase
# set to 'on' to enable log
DB_LOG_SQL=
# API & APP
NOCOBASE_ENV=
API_PORT=13002
# api base path endpoint for app(web)
API_BASE_PATH=/api/
# api server access point for app(web when build)
API_BASE_URL=
# api server access point will be proxied from app(umi dev)
PROXY_TARGET_URL=
# app(web) assets
APP_DIST=packages/app/dist
# ADMIN USER (Initialization only)
ADMIN_EMAIL=admin@nocobase.com
ADMIN_PASSWORD=admin123
# STORAGE (Initialization only)
# local or ali-oss
DEFAULT_STORAGE_TYPE=local
# LOCAL STORAGE
LOCAL_STORAGE_USE_STATIC_SERVER=true
LOCAL_STORAGE_BASE_URL=/uploads # same origin
# LOCAL_STORAGE_BASE_URL=http://your.cdn.domain/pathname # cdn or other origin driven
# ALI OSS STORAGE
ALI_OSS_STORAGE_BASE_URL=
ALI_OSS_REGION=oss-cn-beijing
ALI_OSS_ACCESS_KEY_ID=
ALI_OSS_ACCESS_KEY_SECRET=
ALI_OSS_BUCKET=
# AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_S3_BUCKET=
AWS_S3_STORAGE_BASE_URL=

View File

@ -8,3 +8,9 @@ website
gh-pages gh-pages
weex weex
build.ts build.ts
packages/vue
packages/element
esm
doc-site
public
package

View File

@ -1,38 +0,0 @@
import { readdirSync } from 'fs';
import { join } from 'path';
// utils must build before core
// runtime must build before renderer-react
const headPkgs = [
'utils',
'database',
'resourcer',
'actions',
'client',
'server',
];
const tailPkgs = [];
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
(pkg) => {
return !['father-build', 'app'].includes(pkg) && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
},
);
let pkgs = [];
if (process.argv.length > 2) {
pkgs = process.argv;
pkgs.shift();
pkgs.shift();
} else {
pkgs = [...headPkgs, ...otherPkgs, ...tailPkgs];
}
console.log(pkgs);
export default {
target: 'node',
cjs: { type: 'babel', lazy: true },
// disableTypeCheck: true,
pkgs,
};

View File

@ -1,43 +0,0 @@
name: vercel
on:
push:
branches:
- develop
jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2.1.2
with:
node-version: '12.x'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Build
env:
API_BASE_URL: ${{ secrets.API_ORIGIN }}${{ secrets.API_BASE_PATH }}
run: |
yarn install
yarn bootstrap
yarn build-docs
- name: Deploy
uses: amondnet/vercel-action@v19
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
github-token: ${{ secrets.GITHUB_TOKEN }} #Optional
vercel-args: '--prod' #Optional
vercel-org-id: ${{ secrets.ORG_ID}} #Required
vercel-project-id: ${{ secrets.PROJECT_ID}} #Required
working-directory: ./dist

18
.gitignore vendored
View File

@ -3,23 +3,11 @@ lib/
esm/ esm/
.env .env
.DS_Store .DS_Store
package-lock.json
yarn.lock
!/yarn.lock
!/package-lock.json
yarn-error.log yarn-error.log
lerna-debug.log lerna-debug.log
packages/database/package-lock.json
packages/resourcer/package-lock.json
verdaccio
uploads/
.umi
.umi-production
/dist
/.vscode /.vscode
/.idea /.idea
.vercel
packages/database/src/__tests__/.db
db.sqlite db.sqlite
coverage
.umi
/uploads

View File

@ -6,3 +6,6 @@ package.json
.umi .umi
.umi-production .umi-production
.umi-test .umi-test
packages/**/lib/**
packages/**/esm/**
packages/**/node_modules/**

View File

@ -1,73 +1,10 @@
import { defineConfig } from 'dumi'; import { defineConfig } from 'dumi';
import { getUmiConfig } from './packages/utils/src/umiConfig';
process.env.MFSU_AD = 'none';
const umiConfig = getUmiConfig();
export default defineConfig({ export default defineConfig({
title: 'NocoBase', title: 'NocoBase',
hash: true, favicon: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
define: { logo: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
...umiConfig.define, outputPath: 'docs-dist',
},
// only proxy when using `umi dev`
// if the assets are built, will not proxy
proxy: {
...umiConfig.proxy,
},
resolve: {
includes: ['docs', 'packages/client/src/schemas'],
},
styles: [
`
.__dumi-default-navbar-logo {
height: 100%;
width: 100px;
}
.__dumi-default-layout .__dumi-default-navbar {
box-shadow: 0 0 3px rgb(60 72 88 / 15%);
}
.__dumi-default-layout[data-site-mode='true'] {
padding-top: 150px !important;
}
@media only screen and (max-width: 767px) {
.__dumi-default-layout[data-site-mode="true"] {
padding-top: 128px !important;
}
}
@media only screen and (min-width: 768px) {
.__dumi-default-menu[data-mode='site'] {
top: 100px !important;
}
.__dumi-default-layout[data-site-mode='true'] .__dumi-default-layout-toc {
top: 150px !important;
}
}
video {
max-width: 800px;
width: 100%;
border-radius: 5px;
box-shadow: 0 8px 24px -2px rgb(0 0 0 / 5%);
}
`,
],
// mfsu: {},
// ssr: {},
// exportStatic: {},
mode: 'site', mode: 'site',
logo: 'https://www.nocobase.com/images/logo.png',
navs: {
'en-US': [
null,
{ title: 'GitHub', path: 'https://github.com/nocobase/nocobase' },
// { title: 'Changelog', path: 'https://github.com/nocobase/nocobase/releases' },
],
'zh-CN': [
null,
{ title: 'GitHub', path: 'https://github.com/nocobase/nocobase' },
// { title: '更新日志', path: 'https://github.com/nocobase/nocobase/releases' },
],
},
// more config: https://d.umijs.org/config // more config: https://d.umijs.org/config
}); });

View File

@ -1,21 +0,0 @@
FROM node:12.20.0-stretch
WORKDIR /app
# COPY . /app
RUN ls -a
RUN npm config set registry https://registry.npm.taobao.org
RUN yarn config set registry https://registry.npm.taobao.org
# RUN npm install
# RUN npm run bootstrap
# RUN npm run build
# # Install app dependencies
# ENV NPM_CONFIG_LOGLEVEL warn
# RUN yarn install
# # Show current folder structure in logs
RUN ls -a
# CMD [ "npm", "run", "serve" ]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 NocoBase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

167
README.md
View File

@ -1,112 +1,111 @@
English | [简体中文](./README.zh-CN.md) # NocoBase 事件
![](https://nocobase.oss-cn-beijing.aliyuncs.com/bbcedd403d31cd1ccc4e9709581f5c2f.png) 事件是被动的,由执行某个方法触发的一种行为。
What is NocoBase Application 的安装、启动、停止触发的事件,挂在 app.on 下,有:
----------
NocoBase is a scalability-first, open-source no-code development platform. No programming required, build your own collaboration platform, management system with NocoBase in minutes.
Homepage: - init
https://www.nocobase.com/ - start
- stop
- errorKoa 内置)
Online Demo: 不区分前后?
https://demo.nocobase.com/new
Contact Us: 全局的 resource action也是 app.on 层面的事件
hello@nocobase.com
When to use NocoBase - users.login
----------
- **SMEs and organizations build business platforms and management systems for themselves or for their industry**
- Want the price to be low enough or even free
- Can be flexibly customized without programming knowledge
- Need full control of source code and data
- Can freely distribute and sell as their own products
- **Service providers and outsourcing teams develop collaboration platforms and management systems for their clients**
- Want to keep development costs as low as possible
- Need the most user-friendly secondary development experience
- Must be deployed privately as a standalone product for the client
- Can be freely distributed and sold by the client
Why choose NocoBase 配置 collectiondb.on 层面的事件
----------
- **Open source and free**
- Unrestricted commercial use under the MIT license
- Full code ownership, private deployment, private and secure data
- Free to expand and develop for actual needs
- Good ecological support
- **Strong no-code capability**
- WYSIWYG visual configuration
- Separation of data structure configuration from interface configuration
- Rich combination of blocks and operations
- Role-based access control
- **Developer-friendly**
- Microkernel architecture, flexible and easy to extend, with a robust plug-in system
- Node.js-based, with popular frameworks and technologies, including Koa, Sequelize, React, Formily, Ant Design, etc.
- Progressive development, easy for getting-started, friendly to newcomers
- No binding, no strong dependencies, can be used in any combination or extensions, can be used in existing projects
Note - afterDefineCollection
---------- - afterUpdateCollection
NocoBase is still in early development and is for preview purposes only and is not suitable for use in a production environment. A relatively stable and well-documented public beta is expected to be released as early as the first quarter of 2022. - afterRemoveCollection
If you are interested in NocoBase, please join us to discuss and develop it together. - afterAddField
- afterRemoveField
Architecture
----------
![](https://docs.nocobase.com/static/NocoBase.c9542b1f.png) app.on
Requirements application 的安装、启动、停止等
----------
Node: - init
- start
- stop
- errorKoa 内置)
- Node.js 12.20+ resource 的 actions
Database: - `<actionHookType>`
- `<resourceName>.<actionHookType>`
- PostgreSQL 10.x+ db.on
- Sqlite 3+
Installation - afterDefineCollection
---------- - afterUpdateCollection
- afterRemoveCollection
- afterAddField
- afterUpdateField
- afterRemoveField
- `<modelHookType>`
- `<modelName>.<modelHookType>`
### Create a project with `create-nocobase-app` collection.on
#### Quickstart - afterAddField
~~~shell - afterUpdateField
yarn create nocobase-app my-nocobase-app --quickstart --lang=en-US - afterRemoveField
# `--lang=en-US` Language settings support `--lang=en-US` and `--lang=zh-CN`
~~~
#### Step by step
~~~shell
# 1. create project
yarn create nocobase-app my-nocobase-app
# 2. switch to the project directory
cd my-nocobase-app
# 3. create initialization data
yarn nocobase init --import-demo --lang=en-US
# `--import-demo` Importing demo data
# `--lang=en-US` Language settings support `--lang=en-US` and `--lang=zh-CN`
# 4. start project
yarn start
~~~
Open http://localhost:8000 in a web browser. 1. 初始化 app.constructor
2. 注册插件 app.plugin
3. 加载配置 app.load
4. 安装 app.init
5. 启动 app.start
6. 停止 app.stop
### Installing from Docker
```bash 生产环境需要:
docker run --name my-nocobase-app -p 8000:13002 -d nocobase/nocobase
docker logs my-nocobase-app
```
Open http://localhost:8000 in a web browser.
## Contributing DB_DIALECT=sqlite
DB_STORAGE=db.sqlite
# DB_DIALECT=mysql
# DB_HOST=localhost
# DB_PORT=13306
# DB_DATABASE=nocobase
# DB_USER=nocobase
# DB_PASSWORD=nocobase
DB_LOG_SQL=
## APP_LANG <Badge>server</Badge>
zh-CN 和 en-US初始化时使用
## NOCOBASE_ENV <Badge>server</Badge>
production 和 development
## API_PORT <Badge>server</Badge>
用于设定 API 端口
## API_BASE_PATH <Badge>server</Badge>
resourcer 的 api prefix默认值 /api/
## API_BASE_URL <Badge>client</Badge>
缺失时,使用 API_BASE_PATH 补齐
## ~~API_HOSTNAME <Badge>client</Badge>~~
可以去掉
## PROXY_TARGET <Badge>client</Badge>
本地开发时有用,默认为 http://localhost:${API_PORT},如果是远程的 API 时才需要修改
https://docs.nocobase.com/guide/contributing

View File

@ -1,116 +0,0 @@
[English](./README.md) | 简体中文
![](https://www.nocobase.com/images/demo/11.png)
NocoBase 是什么
----------
NocoBase 是一个极易扩展的开源无代码开发平台。
无需编程,使用 NocoBase 搭建自己的协作平台、管理系统,只需要几分钟时间。
官网https://cn.nocobase.com/
在线体验https://demo-cn.nocobase.com/new
哪些场景适合使用 NocoBase
----------
- 中小企业和组织为自己或者为所在行业搭建业务平台和管理系统
- 希望价格足够低,甚至免费
- 不懂编程也可以灵活定制
- 需要完全掌控源代码和数据
- 可以以自有产品的形态自由分发和销售
- 服务商和外包团队为客户开发协作平台和管理系统
- 希望尽可能降低开发成本
- 需要极致友好的二次开发体验
- 必须以独立产品的形态为客户私有部署
- 客户可以自由分发和销售
为什么选择 NocoBase
----------
- 开源免费
- 采用 MIT 许可协议,不限制商业使用
- 拥有全部代码,私有化部署,保障数据私有和安全
- 针对实际需求自由扩展开发
- 具备良好的生态支持
- 无代码能力强
- 所见即所得的可视化配置
- 数据结构配置与界面配置分离
- 丰富的区块和操作任意组合
- 基于角色的访问权限
- 对开发者友好
- 微内核,灵活易扩展,具备健全的插件体系
- 基于 Node.js使用主流框架和技术包括 Koa、Sequelize、React、Formily、Ant Design 等
- 渐进式开发,上手难度低,对新人友好
- 不绑架、不强依赖,可任意组合使用或扩展,可用于现有项目中
说明
----------
NocoBase 仍处于早期开发阶段,功能不完整,稳定性不高,仅用于预览,不适合在生产环境中使用。相对稳定以及包含完善文档的公开测试版预计最早将于 2022 年第一季度发布。
如果你希望加入我们一起开发 NocoBase或者探讨 NocoBase 未来发展,或者需要提供 NocoBase 使用上的帮助欢迎通过邮件联系我们hello@nocobase.com
或者添加我们的微信:
![](https://www.nocobase.com/images/wechat.png)
NocoBase 架构
----------
![](https://docs.nocobase.com/static/NocoBase.c9542b1f.png)
环境要求
----------
Node:
- Node.js 12.20+
Database:
- PostgreSQL 10.x+
- Sqlite 3+
安装 & 运行
----------
### 通过 `create-nocobase-app` 创建项目
#### 快速启动
~~~shell
yarn create nocobase-app my-nocobase-app --quickstart --lang=zh-CN
# `--lang=en-US` 语言设置,支持 `--lang=en-US``--lang=zh-CN`
~~~
#### 分步骤执行
~~~shell
# 1. 创建项目
yarn create nocobase-app my-nocobase-app
# 2. 切换到项目根目录
cd my-nocobase-app
# 3. 初始化数据
yarn nocobase init --import-demo --lang=zh-CN
# `--import-demo` 导入演示数据
# `--lang=en-US` 语言设置,支持 `--lang=en-US``--lang=zh-CN`
# 4. 启动项目
yarn start
~~~
使用浏览器打开 http://localhost:8000
### 使用 docker
```bash
# 启动 app
docker run --name my-nocobase-app -p 8000:13002 -d nocobase/nocobase
# 查看日志
docker logs my-nocobase-app
```
使用浏览器打开 http://localhost:8000
## 参与贡献
https://docs.nocobase.com/zh-CN/guide/contributing

View File

@ -1,60 +0,0 @@
version: "3"
networks:
nocobase:
driver: bridge
services:
adminer:
image: adminer
restart: always
networks:
- nocobase
ports:
- ${ADMINER_PORT}:8080
verdaccio:
image: verdaccio/verdaccio
networks:
- nocobase
environment:
VERDACCIO_PORT: ${VERDACCIO_PORT}
restart: always
ports:
- "${VERDACCIO_PORT}:${VERDACCIO_PORT}"
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
restart: always
ports:
- "${DB_MYSQL_PORT}:3306"
networks:
- nocobase
postgres:
image: postgres:10
restart: always
ports:
- "${DB_POSTGRES_PORT}:5432"
networks:
- nocobase
command: postgres -c wal_level=logical
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_PASSWORD: ${DB_PASSWORD}
nocobase:
build:
context: .
dockerfile: Dockerfile
networks:
- nocobase
command: [ "yarn", "start" ]
working_dir: /app
env_file: ./.env
volumes:
- ./:/app
expose:
- 8000
ports:
- "${APP_PORT}:8000"

View File

@ -1,105 +0,0 @@
# Changelog
## v0.5 - 2021/5 ~ 2021/9
Redesign client to achieve WYSIWYG configuration interface without code; refactor some core plugins; provide server API.
Changed
- @nocobase/client
- @nocobase/database
- @nocobase/plugin-collections
- @nocobase/plugin-permissions
- @nocobase/server
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
Added
- @nocobase/plugin-ui-schemas
- @nocobase/plugin-ui-router
- @nocobase/plugin-client
- @nocobase/plugin-notifications
- @nocobase/plugin-system-settings
- @nocobase/plugin-multi-apps
- create-nocobase-app
Deprecated
- @nocobase/plugin-pages
## v0.4 - 2021/3 ~ 2021/4
Provide no-code configuration UI.
Changed
- @nocobase/plugin-collections
- @nocobase/plugin-pages
- @nocobase/client
- @nocobase/plugin-permissions
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
- @nocobase/plugin-automations
Added
- @nocobase/plugin-china-region
- @nocobase/plugin-export
## v0.3 - 2020/10 ~ 2021/1
Split the app and api, refined the server and client kernels, and implemented some core plugins.
Changes
- @nocobase/database
- @nocobase/actions
Added
- @nocobase/server
- @nocobase/client
- @nocobase/plugin-collections
- @nocobase/plugin-pages
- @nocobase/plugin-permissions
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
- @nocobase/plugin-automations
Deprecated
- @nocobase/app
- @nocobase/api
## v0.2 - 2020/7 ~ 2020/9
Refactored database using sequelize. Split router into resourcer and actions.Initially built app and api for no-code platform.
Changed
- @nocobase/database
Added
- @nocobase/resourcer
- @nocobase/actions
- @nocobase/app
- @nocobase/api
Deprecated
- @nocobase/router
## v0.1 - 2020/6
Built the first version of configured database and router based on Bookshelf and Koa.
Added
- @nocobase/database
- @nocobase/router

View File

@ -1,111 +0,0 @@
# 更新日志
## v0.5 - 2021/5 ~ 2021/9
重新设计 client实现所见即所得的无代码配置界面重构部分核心插件提供 server API。
重构:
- @nocobase/client
- @nocobase/database
- @nocobase/plugin-collections
- @nocobase/plugin-permissions
更新:
- @nocobase/server
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
新增:
- @nocobase/plugin-ui-schemas
- @nocobase/plugin-ui-router
- @nocobase/plugin-client
- @nocobase/plugin-notifications
- @nocobase/plugin-system-settings
- @nocobase/plugin-multi-apps
- create-nocobase-app
废弃:
- @nocobase/plugin-pages
## v0.4 - 2021/3 ~ 2021/4
实现表单形式的无代码配置方案。
重构:
- @nocobase/plugin-collections
- @nocobase/plugin-pages
更新:
- @nocobase/client
- @nocobase/plugin-permissions
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
- @nocobase/plugin-automations
新增:
- @nocobase/plugin-china-region
- @nocobase/plugin-export
## v0.3 - 2020/10 ~ 2021/1
将 app 和 api 进行拆分,提炼了 server 和 client 内核,并实现了核心的一些插件。
更新:
- @nocobase/database
- @nocobase/actions
新增:
- @nocobase/server
- @nocobase/client
- @nocobase/plugin-collections
- @nocobase/plugin-pages
- @nocobase/plugin-permissions
- @nocobase/plugin-users
- @nocobase/plugin-file-manager
- @nocobase/plugin-action-logs
- @nocobase/plugin-automations
废弃:
- @nocobase/app
- @nocobase/api
## v0.2 - 2020/7 ~ 2020/9
使用 sequelize 重构 database将 router 拆分为 resourcer 和 actions并初步构建了无代码平台的 app 和 api。
重构:
- @nocobase/database
新增:
- @nocobase/resourcer
- @nocobase/actions
- @nocobase/app
- @nocobase/api
废弃:
- @nocobase/router
## v0.1 - 2020/6
基于 Bookshelf 和 Koa 构建了第一版配置化的 database 和 router。
新增:
- @nocobase/database
- @nocobase/router

View File

@ -1,59 +0,0 @@
---
group:
title: Collection Fields
path: /components/collection-fields
order: 3
---
# Attachment - 附件
## Interface
```ts
export const attachment: FieldOptions = {
name: 'attachment',
type: 'object',
group: 'media',
title: '附件',
isAssociation: true,
default: {
dataType: 'belongsToMany',
target: 'attachments',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Upload.Attachment',
'x-decorator': 'FormItem',
'x-designable-bar': 'Upload.DesignableBar',
},
},
initialize: (values: any) => {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
},
properties: {
...defaultProps,
'uiSchema.x-component-props.multiple': {
type: 'boolean',
'x-content': '允许上传多个文件',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
default: true,
},
},
};
```

View File

@ -1,59 +0,0 @@
---
group:
title: 字段组件
path: /zh-CN/components/collection-fields
order: 3
---
# Attachment - 附件
## Interface
```ts
export const attachment: FieldOptions = {
name: 'attachment',
type: 'object',
group: 'media',
title: '附件',
isAssociation: true,
default: {
dataType: 'belongsToMany',
target: 'attachments',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Upload.Attachment',
'x-decorator': 'FormItem',
'x-designable-bar': 'Upload.DesignableBar',
},
},
initialize: (values: any) => {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
},
properties: {
...defaultProps,
'uiSchema.x-component-props.multiple': {
type: 'boolean',
'x-content': '允许上传多个文件',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
default: true,
},
},
};
```

View File

@ -1,30 +0,0 @@
# CheckboxGroup - 复选框
## Interface
```ts
export const checkboxGroup: FieldOptions = {
name: 'checkboxGroup',
type: 'object',
group: 'choices',
order: 5,
title: '复选框',
default: {
interface: 'checkboxGroup',
dataType: 'json',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Checkbox.Group',
'x-decorator': 'FormItem',
'x-designable-bar': 'Checkbox.Group.DesignableBar',
},
},
properties: {
...defaultProps,
'uiSchema.enum': dataSource,
},
operations: multipleSelect.operations,
};
```

View File

@ -1,31 +0,0 @@
# Checkbox - 勾选
## Interface
```ts
export const checkbox: FieldOptions = {
name: 'checkbox',
type: 'object',
group: 'choices',
order: 1,
title: '勾选',
default: {
dataType: 'boolean',
// name,
uiSchema: {
type: 'boolean',
// title,
'x-component': 'Checkbox',
'x-decorator': 'FormItem',
'x-designable-bar': 'Checkbox.DesignableBar',
},
},
properties: {
...defaultProps,
},
operations: [
{ label: '是', value: '$isTruly', selected: true, noValue: true },
{ label: '否', value: '$isFalsy', noValue: true },
],
};
```

View File

@ -1,83 +0,0 @@
# ChinaRegion - 中国行政区
## Interface
```ts
export const chinaRegion: FieldOptions = {
name: 'chinaRegion',
type: 'object',
group: 'choices',
order: 7,
title: '中国行政区划',
isAssociation: true,
default: {
dataType: 'belongsToMany',
target: 'china_regions',
targetKey: 'code',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Cascader',
'x-component-props': {
changeOnSelectLast: false,
loadData: '{{ ChinaRegion.loadData() }}',
labelInValue: true,
maxLevel: 3,
fieldNames: {
label: 'name',
value: 'code',
children: 'children',
},
},
'x-reactions': [
'{{ ChinaRegion.useFieldValue }}',
'{{ useAsyncDataSource(ChinaRegion.loadDataSource()) }}',
],
'x-decorator': 'FormItem',
'x-designable-bar': 'Cascader.DesignableBar',
},
},
initialize: (values: any) => {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
},
properties: {
...defaultProps,
'uiSchema.x-component-props.maxLevel': {
type: 'number',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
title: '可选择的层级',
default: 3,
enum: [
{ value: 1, label: '省' },
{ value: 2, label: '市' },
{ value: 3, label: '区/县' },
{ value: 4, label: '乡镇/街道' },
{ value: 5, label: '村/居委会' },
],
},
'uiSchema.x-component-props.changeOnSelectLast': {
type: 'boolean',
'x-component': 'Checkbox',
'x-content': '必须选到最后一级',
'x-decorator': 'FormItem',
},
},
operations: [{ label: '等于', value: 'code.in' }],
};
```

View File

@ -1,33 +0,0 @@
# CreatedAt - 添加时间
## Interface
```ts
export const createdAt: FieldOptions = {
name: 'createdAt',
type: 'object',
group: 'systemInfo',
order: 1,
title: '添加时间',
sortable: true,
default: {
dataType: 'date',
field: 'created_at',
// name,
uiSchema: {
type: 'datetime',
title: '添加时间',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-designable-bar': 'DatePicker.DesignableBar',
},
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: datetime.operations,
};
```

View File

@ -1,37 +0,0 @@
# createdBy - 添加人
## Interface
```ts
export const createdBy: FieldOptions = {
name: 'createdBy',
type: 'object',
group: 'systemInfo',
order: 3,
title: '添加人',
isAssociation: true,
default: {
dataType: 'belongsTo',
target: 'users',
foreignKey: 'created_by_id',
// name,
uiSchema: {
type: 'object',
title: '添加人',
'x-component': 'Select.Drawer',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-decorator': 'FormItem',
'x-read-pretty': true,
'x-designable-bar': 'Select.Drawer.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,42 +0,0 @@
# Datetime - 日期
## Interface
```ts
export const datetime: FieldOptions = {
name: 'datetime',
type: 'object',
group: 'datetime',
order: 1,
title: '日期',
sortable: true,
default: {
dataType: 'date',
// name,
uiSchema: {
type: 'datetime',
// title,
'x-component': 'DatePicker',
'x-component-props': {
showTime: false,
},
'x-decorator': 'FormItem',
'x-designable-bar': 'DatePicker.DesignableBar',
},
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: [
{ label: '等于', value: '$dateOn', selected: true },
{ label: '不等于', value: '$dateNotOn' },
{ label: '早于', value: '$dateBefore' },
{ label: '晚于', value: '$dateAfter' },
{ label: '不早于', value: '$dateNotBefore' },
{ label: '不晚于', value: '$dateNotAfter' },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,30 +0,0 @@
# Email - 电子邮箱
## Interface
```ts
export const email: FieldOptions = {
name: 'email',
type: 'object',
group: 'basic',
order: 4,
title: '电子邮箱',
sortable: true,
default: {
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-validator': 'email',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
...defaultProps,
},
operations: string.operations,
};
```

View File

@ -1,27 +0,0 @@
# Icon - 图标
## Interface
```ts
export const icon: FieldOptions = {
name: 'icon',
type: 'object',
group: 'basic',
order: 8,
title: '图标',
default: {
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
'x-designable-bar': 'IconPicker.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,70 +0,0 @@
# LinkTo - 关联字段
## Interface
```ts
export const linkTo: FieldOptions = {
name: 'linkTo',
type: 'object',
group: 'relation',
order: 1,
title: '关联字段',
isAssociation: true,
default: {
dataType: 'belongsToMany',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Select.Drawer',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-designable-bar': 'Select.Drawer.DesignableBar',
},
},
initialize: (values: any) => {
if (values.dataType === 'belongsToMany') {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
}
},
properties: {
...defaultProps,
target: {
type: 'string',
title: '要关联的数据表',
required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
// 'uiSchema.x-component-props.fieldNames.label': {
// type: 'string',
// title: '要显示的标题字段',
// required: true,
// 'x-reactions': ['{{useAsyncDataSource(loadCollectionFields)}}'],
// 'x-decorator': 'FormItem',
// 'x-component': 'Select',
// },
'uiSchema.x-component-props.multiple': {
type: 'boolean',
'x-content': '允许关联多条记录',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
},
};
```

View File

@ -1,26 +0,0 @@
# Markdown
## Interface
```ts
export const markdown: FieldOptions = {
name: 'markdown',
type: 'object',
title: 'Markdown',
group: 'media',
default: {
dataType: 'text',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Markdown',
'x-decorator': 'FormItem',
'x-designable-bar': 'Markdown.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,49 +0,0 @@
# MultipleSelect - 下拉选择(多选)
## Interface
```ts
export const multipleSelect: FieldOptions = {
name: 'multipleSelect',
type: 'object',
group: 'choices',
order: 3,
title: '下拉选择(多选)',
default: {
dataType: 'json',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Select',
'x-component-props': {
mode: 'multiple',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'Select.DesignableBar',
enum: [],
},
},
properties: {
...defaultProps,
'uiSchema.enum': dataSource,
},
operations: [
{
label: '等于',
value: '$match',
selected: true,
schema: { 'x-component': 'Select' },
},
{
label: '不等于',
value: '$notMatch',
schema: { 'x-component': 'Select' },
},
{ label: '包含', value: '$anyOf', schema: { 'x-component': 'Select' } },
{ label: '不包含', value: '$noneOf', schema: { 'x-component': 'Select' } },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,58 +0,0 @@
# Number - 数字
## Interface
```ts
export const number: FieldOptions = {
name: 'number',
type: 'object',
group: 'basic',
order: 5,
title: '数字',
sortable: true,
default: {
dataType: 'float',
// name,
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '0',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'InputNumber.DesignableBar',
},
},
properties: {
...defaultProps,
'uiSchema.x-component-props.step': {
type: 'string',
title: '精度',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: '0',
enum: [
{ value: '0', label: '1' },
{ value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' },
],
},
},
operations: [
{ label: '等于', value: 'eq', selected: true },
{ label: '不等于', value: 'ne' },
{ label: '大于', value: 'gt' },
{ label: '大于等于', value: 'gte' },
{ label: '小于', value: 'lt' },
{ label: '小于等于', value: 'lte' },
// {label: '介于', value: 'between'},
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,27 +0,0 @@
# Password - 密码
## Interface
```ts
export const password: FieldOptions = {
name: 'password',
type: 'object',
group: 'basic',
order: 7,
title: '密码',
default: {
dataType: 'password',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Password',
'x-decorator': 'FormItem',
'x-designable-bar': 'Password.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,49 +0,0 @@
# Percent - 百分比
## Interface
```ts
export const percent: FieldOptions = {
name: 'percent',
type: 'object',
group: 'basic',
order: 6,
title: '百分比',
sortable: true,
default: {
dataType: 'float',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '0',
addonAfter: '%',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'InputNumber.DesignableBar',
},
},
properties: {
...defaultProps,
'uiSchema.x-component-props.step': {
type: 'string',
title: '精度',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: '0',
enum: [
{ value: '0', label: '1' },
{ value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' },
],
},
},
operations: number.operations,
};
```

View File

@ -1,30 +0,0 @@
# Phone - 手机号码
## Interface
```ts
export const phone: FieldOptions = {
name: 'phone',
type: 'object',
group: 'basic',
order: 3,
title: '手机号码',
sortable: true,
default: {
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-validator': 'phone',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
...defaultProps,
},
operations: string.operations,
};
```

View File

@ -1,29 +0,0 @@
# RadioGroup - 单选框
## Interface
```ts
export const radioGroup: FieldOptions = {
name: 'radioGroup',
type: 'object',
group: 'choices',
order: 4,
title: '单选框',
default: {
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
'x-designable-bar': 'Radio.DesignableBar',
},
},
properties: {
...defaultProps,
'uiSchema.enum': dataSource,
},
operations: select.operations,
};
```

View File

@ -1,56 +0,0 @@
# Select - 下拉选择(单选)
## Interface
```ts
export const select: FieldOptions = {
name: 'select',
type: 'object',
group: 'choices',
order: 2,
title: '下拉选择(单选)',
default: {
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Select',
'x-decorator': 'FormItem',
'x-designable-bar': 'Select.DesignableBar',
enum: [],
},
},
properties: {
...defaultProps,
'uiSchema.enum': dataSource,
},
operations: [
{
label: '等于',
value: 'eq',
selected: true,
schema: { 'x-component': 'Select' },
},
{ label: '不等于', value: 'ne', schema: { 'x-component': 'Select' } },
{
label: '包含',
value: 'in',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
{
label: '不包含',
value: 'notIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,37 +0,0 @@
# String - 单行文本
## Interface
```ts
export const string: FieldOptions = {
name: 'string',
type: 'object',
group: 'basic',
order: 1,
title: '单行文本',
sortable: true,
default: {
interface: 'string',
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
...defaultProps,
},
operations: [
{ label: '包含', value: '$includes', selected: true },
{ label: '不包含', value: '$notIncludes' },
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,45 +0,0 @@
# SubTable - 子表格
## Interface
```ts
export const subTable: FieldOptions = {
name: 'subTable',
type: 'object',
group: 'relation',
order: 2,
title: '子表格',
isAssociation: true,
disabled: true,
default: {
dataType: 'hasMany',
// name,
uiSchema: {
type: 'array',
// title,
'x-decorator': 'FormItem',
'x-component': 'Table',
'x-component-props': {},
'x-designable-bar': 'Table.DesignableBar',
enum: [],
},
},
initialize: (values: any) => {
if (!values.target) {
values.target = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
},
properties: {
...defaultProps,
children: {
type: 'array',
title: '子表格字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
},
},
};
```

View File

@ -1,27 +0,0 @@
# TextArea - 多行文本
## Interface
```ts
export const textarea: FieldOptions = {
name: 'textarea',
type: 'object',
group: 'basic',
order: 2,
title: '多行文本',
default: {
dataType: 'text',
// name,
uiSchema: {
type: 'string',
// title,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,55 +0,0 @@
# Time - 时间
## Interface
```ts
export const time: FieldOptions = {
name: 'time',
type: 'object',
group: 'datetime',
order: 2,
title: '时间',
sortable: true,
default: {
dataType: 'time',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'TimePicker',
'x-decorator': 'FormItem',
'x-designable-bar': 'TimePicker.DesignableBar',
},
},
properties: {
...defaultProps,
'uiSchema.x-component-props.format': {
type: 'string',
title: '时间格式',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'HH:mm:ss',
enum: [
{
label: '24小时制',
value: 'HH:mm:ss',
},
{
label: '12小时制',
value: 'hh:mm:ss a',
},
],
},
},
operations: [
{ label: '等于', value: 'eq', selected: true },
{ label: '不等于', value: 'neq' },
{ label: '大于', value: 'gt' },
{ label: '大于等于', value: 'gte' },
{ label: '小于', value: 'lt' },
{ label: '小于等于', value: 'lte' },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
```

View File

@ -1,33 +0,0 @@
# UpdatedAt - 最后更新时间
## Interface
```ts
export const updatedAt: FieldOptions = {
name: 'updatedAt',
type: 'object',
group: 'systemInfo',
order: 2,
title: '最后更新时间',
sortable: true,
default: {
dataType: 'date',
field: 'updated_at',
// name,
uiSchema: {
type: 'datetime',
title: '最后更新时间',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-designable-bar': 'DatePicker.DesignableBar',
},
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: datetime.operations,
};
```

View File

@ -1,37 +0,0 @@
# UpdatedBy - 最后修改人
## Interface
```ts
export const updatedBy: FieldOptions = {
name: 'updatedBy',
type: 'object',
group: 'systemInfo',
order: 4,
title: '最后修改人',
isAssociation: true,
default: {
dataType: 'belongsTo',
target: 'users',
foreignKey: 'updated_by_id',
// name,
uiSchema: {
type: 'object',
title: '最后修改人',
'x-component': 'Select.Drawer',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-decorator': 'FormItem',
'x-read-pretty': true,
'x-designable-bar': 'Select.Drawer.DesignableBar',
},
},
properties: {
...defaultProps,
},
};
```

View File

@ -1,101 +0,0 @@
---
title: Overview
order: 0
nav:
title: Components
order: 4
---
# Components
There are a total of three types of client components for NocoBase.
- Routing components created by createRouteSwitch, such as Layou, Page
- Field components created by createCollectionField, which are used to extend fields
- JSON Schema components created by createSchemaComponent, which can be anything, such as tables, forms, calendars, kanban, etc. Schema Component can be used in Route Component or Collection Field.
## Route Components
```js
function Hello() {
return <div>Hello World</div>
}
const RouteSwitch = createRouteSwitch({
components: {
Hello,
},
});
const routes = [
{ path: '/hello', component: 'Hello' },
];
<Router>
<RouteSwitch routes={routes}/>
</Router>
```
## Schema Components
```js
const Hello = () => {
return <div>Hello</div>;
}
const SchemaComponent = createSchemaComponent({
components: {
Hello,
},
});
<SchemaComponent
schema={{
type: 'void',
'x-component': 'Hello',
}}
/>
```
## Collection Fields
```ts
const string: FieldOptions = {
name: 'string',
type: 'object',
group: 'basic',
order: 1,
title: '单行文本',
sortable: true,
default: {
interface: 'string',
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
},
operations: [
{ label: '包含', value: '$includes', selected: true },
{ label: '不包含', value: '$notIncludes' },
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
};
const CollectionField = createCollectionField({
interface: {
string,
},
})
```

View File

@ -1,163 +0,0 @@
---
title: 概述
order: 0
nav:
title: 组件
order: 4
---
# 组件
NocoBase 的客户端组件总共有三类:
- 通过 createRouteSwitch 创建的路由组件,如 Layou、Page
- 通过 createCollectionField 创建的字段组件,用于扩展字段
- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等。
Schema Component 可用于 Route Component 或 Collection Field 中。
## 路由组件
```js
function Hello() {
return <div>Hello World</div>
}
const RouteSwitch = createRouteSwitch({
components: {
Hello,
},
});
const routes = [
{ path: '/hello', component: 'Hello' },
];
<Router>
<RouteSwitch routes={routes}/>
</Router>
```
## Schema 组件
```js
const Hello = () => {
return <div>Hello</div>;
}
const SchemaComponent = createSchemaComponent({
components: {
Hello,
},
});
<SchemaComponent
schema={{
type: 'void',
'x-component': 'Hello',
}}
/>
```
SchemaComponent 是通过 Schema 协议渲染的组件SchemaComponent 可能由多个原子组件组合而成。
```ts
interface ISchema {
type: string;
name?: string;
title?: any;
properties?: any;
['x-component']?: any;
['x-component-props']?: any;
['x-decorator']?: any;
['x-decorator-props']?: any;
['x-designable-bar']?: any;
['x-designable-bar-props']?: any;
}
```
Schema 组件的完整结构如下:
<pre lang="tsx">
<Decorator>
<DesignableBar />
<Component>
{...properties}
</Component>
</Decorator>
</pre>
DesignableBar 可以用于修改当前 SchemaComponent 的 Schema。可以以任意形态出现例如
<pre lang="tsx">
function DesignableBar() {
// 这里是随意写的,当前 schema可以 updateremove 等等
const { schema, update, remove } = useDesignableSchema();
return (
<Space>
<Dropdown overlay={
<Menu>
<Menu.Item>配置项1</Menu.Item>
<Menu.Item>配置项2</Menu.Item>
<Menu.Item>配置项3</Menu.Item>
</Menu>
}>
<a>配置<a/>
</DropDown>
<Dropdown>
<a>配置<a/>
</DropDown>
<Dropdown>
<a>配置<a/>
</DropDown>
</Space>
)
}
</pre>
如果 DesignableBar 只是修改当前层级的参数比较好处理,但是如果修改的是 properties 子节点里的参数,情况会变得比较复杂。
## 字段组件
```ts
const string: FieldOptions = {
name: 'string',
type: 'object',
group: 'basic',
order: 1,
title: '单行文本',
sortable: true,
default: {
interface: 'string',
dataType: 'string',
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-designable-bar': 'Input.DesignableBar',
},
},
properties: {
},
operations: [
{ label: '包含', value: '$includes', selected: true },
{ label: '不包含', value: '$notIncludes' },
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
{ label: '非空', value: '$notNull', noValue: true },
{ label: '为空', value: '$null', noValue: true },
],
designableBar: {
key1: {},
key2: {},
},
};
const CollectionField = createCollectionField({
interfaces: {
string,
},
})
```

View File

@ -1,12 +0,0 @@
---
order: 3
title: AdminLayout
group:
title: Route Components
path: /components/route-components
order: 1
---
# AdminLayout
Coming soon...

View File

@ -1,12 +0,0 @@
---
order: 3
title: AdminLayout
group:
title: 路由组件
path: /zh-CN/components/route-components
order: 1
---
# AdminLayout
待补充...

View File

@ -1,11 +0,0 @@
---
order: 2
title: AuthLayout
group:
path: /components/route-components
order: 1
---
# AuthLayout
Coming soon...

View File

@ -1,7 +0,0 @@
---
sidemenu: false
order: 1000
hide: true
---
<code src="./examples/demos/demo1.tsx"/>

View File

@ -1,68 +0,0 @@
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import React, { useMemo } from 'react';
import { HashRouter as Router } from 'react-router-dom';
import {
createRouteSwitch,
RouteRedirectProps,
AdminLayout,
AuthLayout,
RouteSchemaRenderer,
ConfigProvider,
ClientSDK,
} from '@nocobase/client';
import { UseRequestProvider } from 'ahooks';
import { extend } from 'umi-request';
const request = extend({
prefix: process.env.API_BASE_URL,
timeout: 30000,
});
request.use(async (ctx, next) => {
const { headers } = ctx.req.options as any;
const token = localStorage.getItem('NOCOBASE_TOKEN');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
headers['X-Hostname'] = window.location.hostname;
await next();
});
const client = new ClientSDK({
request,
});
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,
AuthLayout,
RouteSchemaRenderer,
},
});
const App = () => {
const { data, loading } = useRequest('routes:getAccessible', {
formatResult: (result) => result?.data,
});
if (loading) {
return <Spin size={'large'} className={'nb-spin-center'} />;
}
return (
<div>
<Router>
<RouteSwitch routes={data} />
</Router>
</div>
);
};
export default () => {
return (
<ConfigProvider client={client}>
<App />
</ConfigProvider>
);
};

View File

@ -1,8 +0,0 @@
---
sidemenu: false
nav:
title: Examples
order: 99
---
Coming soon...

View File

@ -1,8 +0,0 @@
---
sidemenu: false
nav:
title: 示例
order: 99
---
待补充...

View File

@ -1,315 +0,0 @@
---
order: 2
---
# Client Components
To allow more non-developers to participate, NocoBase provides a companion client - a visual interface without code. The client interface is very flexible and consists of different components, which are divided into three categories.
- Routing components created by createRouteSwitch, such as Layout, Page
- Field components created by createCollectionField, used to extend fields
- JSON Schema components created by createSchemaComponent, which can be anything, such as tables, forms, calendars, kanban, etc.
[For more on components, see the section on components](#)
## Component tree structure
The interface is a component tree composed of components with the following structure.
<pre lang="tsx">
<Layout>
<Page>
<Grid>
// Block, table
<Table>
<Table.DesignableBar/>
<Table.ActionBar>
<Action/>
<Action/>
</Table.ActionBar>
<Table.Content>
<Table.Column>
// Fields in table columns
<CollectionField />
</Table.Column>
<Table.Column>
<CollectionField />
</Table.Column>
</Table.Content>
</Table>
</Grid>
<AddNew />
</Page>
</Layout>
</pre>
Note: The above example is only for expressing the structure of the component tree and the relationship between components, the actual code does not.
Next, let's introduce the concept of each component in detail.
## Layout and pages
Pages are web pages that can be accessed by address. Different pages may have the same header, footer and navigation between them, and usually we put these common contents in the layout component. For example, the initialized NocoBase provides two layout components, as shown in
Figure
- AuthLayout: accessible without login, usually used to embed login, registration, forgot password, etc. pages.
- AdminLayout: requires login and manages all pages of the backend.
Layout and page components are registered through createRouteSwitch, more extensions are available here.
## Page content layout
For developers, the writing of page content is free, but to facilitate the layout of page content, two types of layout are provided.
### Simple top-down structure
<pre lang="tsx">
<Page>
<BlockItem />
<BlockItem />
<BlockItem />
</Page>
</pre>
Example
```js
// coming soon
```
### Drag and drop grid
<pre lang="tsx">
<Page>
<Grid>
<Grid.Row>
<Grid.Col>
<BlockItem />
</Grid.Col>
<Grid.Col>
<BlockItem />
</Grid.Col>
</Grid.Row>
<Grid.Row>
<Grid.Col>
<BlockItem />
</Grid.Col>
<Grid.Col>
<BlockItem />
</Grid.Col>
</Grid.Row>
</Grid>
</Page>
</pre>
Grid component defines the outer frame of the block based on rows (Grid.Row) and columns (Grid.Col). Examples are as follows:
```js
// coming soon
```
## AddNew
AddNew is the most important button for visual configuration of the page, more about [AddNew](#) here
## Block
Blocks are generally placed in pages and can be anything, including text, attachments, tables, forms, calendars, kanban boards, etc. A complete block consists of three parts.
- Content, the body of the block
- ActionBar, where you can place various action buttons to manipulate the block data (optional)
- DesignableBar, buttons for operating the block configuration (optional)
Take the table block as an example, the component structure is as follows:
<pre lang="tsx">
<Table>
<Table.DesignableBar />
<Table.ActionBar />
<Table.Content />
</Table>
</pre>
Example
```js
// coming soon
```
There are several types of blocks.
- Data types, used to display data from data tables, such as tables, calendars, kanban, forms, details, etc.
- Multimedia, for enriching page content, such as text paragraphs, attachments, etc. For now there is only a simple Markdown.
- Charts, for displaying data statistics.
- Templates, which can directly template certain finished products and apply them directly to the page.
Blocks can be extended at will, see the [createSchemaComponent](#) chapter for how to do so.
## ActionBar
An action bar is a collection of actions, typically used inside a block. The user issues an action command, the program makes a change, and responds with the result in the block's content area.
Example.
Form, the content area is a table, and the action area will place some action buttons, such as filter, add, delete, export, etc
```js
// coming soon
```
details, the content area is the details of the data, the operation area will be placed on the edit, export and other buttons
```js
// coming soon
```
The buttons of the action bar may be different for different blocks. The action bar buttons are also customizable, check the action chapter for details.
## Action
An action is an encapsulated piece of instruction that generally requires user participation.
For example.
- Delete data, which requires the user to select the data to be deleted, and then trigger the delete command
- Filtering data requires the user to fill in the filter items and then triggers the filter command
- Add data, the user needs to fill in the data and then submit it, triggering the add operation instruction
- View details, the user clicks the operation button, the pop-up window to view the details or the current window to open the details page to view
The simplest operation, you only need to bind a paragraph of instructions, simply specify a function, no need to pass parameters. The component structure is as follows.
<pre lang="tsx">
<Action useAction={useAction} />
</pre>
Most of the action instructions require user-supplied parameters, such as the add data action, which requires the user to fill in the data, which usually requires a pop-up form, and the user fills in the data and clicks submit to trigger the action instruction. The component structure is as follows.
<pre lang="tsx">
<Action useAction={useAction}>
{/* This is a popup form with a built-in submit button that triggers an action command when clicked, the specific code is omitted */}
<Action.Modal x-decorator={'Form'}></Action.Modal>
</Action>
</pre>
A complete action is roughly divided into two steps.
- Bind a directive to the Action
- If the directive requires user-supplied parameters, it needs to provide an interaction interface, which is currently built in as follows
- Action.Drawer: drawer
- Modal: dialog box
- Popover: bubble
Action is a very important concept in NocoBase, more details click here to see
## DesignableBar
All Schema Components can be bound to their own configuration toolbar (DesignableBar) for modifying the Schema of the current component.
**What is a Schema Component?**
A component written in JSON-like Schema format via the Schema protocol, e.g.
```js
{
type: 'void',
'x-Component': 'Hello',
'x-designable-bar': 'Hello.DesignableBar',
'x-dect': 'CardItem',
}
```
To give a few examples, e.g.
JSON Schema for form fields
```js
const schema = {
type: 'string',
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-designable-bar': 'Form.Field.DesignableBar',
};
```
The effect of the form item's configuration toolbar `Form.Field.DesignableBar`
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/1ffba32f9a5625760c3fe11e7eb19974.png" style="max-width: 350px;"/>
JSON Schema for the form
```js
const schema = {
type: 'array',
'x-component': 'Table',
'x-decorator': 'CardItem',
'x-designable-bar': 'Table.DesignableBar',
};
```
The effect of the table configuration toolbar `Table.DesignableBar`
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/dcd762a0444ef55a8515c53d706f7bc4.png" style="max-width: 250px;"/>
JSON Schema for the menu item.
```js
const schema = {
type: 'array',
'x-component': 'Menu.Item',
'x-designable-bar': 'Menu.Item.DesignableBar',
};
```
Effect of menu item configuration toolbar `Menu.Item.DesignableBar`
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/984ab6da6a8f72fe790bb9bd18b3eb35.png" style="max-width: 200px;"/>
For more details on the configuration toolbar click here
## CollectionField
The configuration parameters of field components can be very many, and the same field component can be used in different data blocks. In order to reduce code duplication, NocoBase assigns the configuration of field components to the data table for unified management. One configuration, many uses. The field component is directly referenced in the data block, and then extended if there are other different parameters.
<pre lang="tsx">
<Table>
// 原生态的写法
<Table.Column title={'姓名'}>
<Input {...others} name="name" readPretty={true}/>
</Table.Column>
// 简化之后的字段引用
<Table.Column>
<CollectionField name="name"/>
</Table.Column>
</Table>
<Form>
// 如果在表格里也用到,再写一遍
<FormItem title={'姓名'}>
<Input {...others} name="name"/>
</FormItem>
// 字段引用,只需要提供 name 即可
<FormItem>
<CollectionField name="name"/>
</FormItem>
</Form>
</pre>
The field component has three display states.
- fillable - editable
- unfillable - disabled
- Read mode - read-pretty
As an example for a single line of text (Input).
```js
// example (three display states for Input)
// example to be added
```
**Why do fields have multiple display states? **
- In a form, the field is normally filled in (editable), but if it is for viewing only, the field is set to disabled or read-pretty.
- In forms, fields are generally read-pretty, but if you need to edit them quickly within the form, you can dynamically activate a field as editable.
The field component can be extended in any way, see the createCollectionField section for how to do so.

View File

@ -1,324 +0,0 @@
---
order: 2
---
# 客户端组件
为了让更多非开发人员也能参与进来NocoBase 提供了配套的客户端 —— 无代码的可视化界面。客户端界面非常灵活,由不同组件构成,分为了三类:
- 通过 createRouteSwitch 创建的路由组件,如 Layout、Page
- 通过 createCollectionField 创建的字段组件,用于扩展字段
- 通过 createSchemaComponent 创建的 JSON Schema 组件,可以是任意东西,比如表格、表单、日历、看板等
[更多组件内容,查看组件章节](#)
## 组件树结构
界面是由组件构成的组件树,结构如下:
<pre lang="tsx">
// 布局
<Layout>
// 页面
<Page>
// 栅格
<Grid>
// 区块,以表格为例
<Table>
// 配置工具栏
<Table.DesignableBar/>
// 操作栏
<Table.ActionBar>
// 操作
<Action/>
<Action/>
</Table.ActionBar>
// 内容区
<Table.Content>
<Table.Column>
// 表格列的字段
<CollectionField />
</Table.Column>
<Table.Column>
<CollectionField />
</Table.Column>
</Table.Content>
</Table>
</Grid>
<AddNew />
</Page>
</Layout>
</pre>
注:以上例子只为表达组件树的结构和组件之间的关系,实际代码并不如此。
接下来,我们来详细的介绍各部分的概念。
## 布局和页面
页面是可以通过地址访问的网页,不同页面之间可能具有相同的页眉、页脚和导航,通常我们会把这些公共的内容放在布局组件里。例如,初始化的 NocoBase 提供了两个布局组件,如图所示:
- AuthLayout无需登录就能访问一般用于嵌入登录、注册、忘记密码等页面。
- AdminLayout需要登录管理后台的所有页面。
布局和页面组件通过 createRouteSwitch 注册,更多扩展内容点此查看。
## 页面内容排版
对开发者来说,页面内容的编写是自由的,不过为了方便对页面内容进行排版,提供了两种排版方式:
### 简易的上下结构
<pre lang="tsx">
<Page>
<BlockItem />
<BlockItem />
<BlockItem />
</Page>
</pre>
例子如下:
```js
// 示例
```
### 可拖拽的栅格
<pre lang="tsx">
<Page>
<Grid>
<Grid.Row>
<Grid.Col>
<BlockItem />
</Grid.Col>
<Grid.Col>
<BlockItem />
</Grid.Col>
</Grid.Row>
<Grid.Row>
<Grid.Col>
<BlockItem />
</Grid.Col>
<Grid.Col>
<BlockItem />
</Grid.Col>
</Grid.Row>
</Grid>
</Page>
</pre>
栅格组件 Grid 基于行Grid.Row和列Grid.Col来定义区块的外部框架。例子如下
```js
// 示例
```
## AddNew
AddNew 是页面可视化配置最重要的操作按钮,更多关于 AddNew 的内容点此查看
## 区块 - Block
区块一般放在页面里,可以是任意东西,包括文字、附件、表格、表单、日历、看板等等。一个完整的区块由三部分组成:
- 内容区 Content区块的主体
- 操作栏 ActionBar可以放置各种操作按钮用于操作区块数据可选
- 配置工具栏 DesignableBar操作区块配置的按钮可选
以表格区块为例,组件结构如下:
<pre lang="tsx">
<Table>
<Table.DesignableBar />
<Table.ActionBar />
<Table.Content />
</Table>
</pre>
具体形态:
```js
//示例(这里放上一个表示区块结构的示例)
```
区块有几种类型:
- 数据类型,用于展示数据表的数据,如表格、日历、看板、表单、详情等。
- 多媒体,用于丰富页面内容,如文本段、附件等。暂时只有一个简易的 Markdown。
- 图表,用于展示数据统计。
- 模板,可直接将某些成品模板化,直接应用到页面上。
区块可以任意扩展,如何扩展查看 createSchemaComponent 章节。
## 操作栏 - ActionBar
操作栏是一系列操作的集合,一般用于区块内部。用户发出操作指令,程序做出改变,并将结果响应在区块内容区。
例如:
表格,内容区是一个表格,操作区会放置一些操作按钮,如筛选、新增、删除、导出等
```js
// 示例(放一个简易的表格,把操作栏重点突出一下)
```
详情,内容区是详情数据,操作区会放置编辑、导出等按钮
```js
// 示例(放一个简易的详情,把操作栏重点突出一下)
```
不同的区块,操作栏的按钮可能不同。操作栏的按钮也是可以自定义的,具体内容查看操作章节。
## 操作 - Action
操作是封装的一段指令,一般需要用户参与。
例如:
- 删除数据,需要用户选中待删除数据,再触发删除指令
- 筛选数据,需要用户填写筛选项,再触发筛选指令
- 新增数据,需要用户填写数据之后提交,触发新增操作指令
- 查看详情,用户点击操作按钮,弹窗查看详情或当前窗口打开详情页查看
最简单的操作,只需要绑定一段指令即可,简单来说就是指定一段函数,无需传参。组件结构如下:
<pre lang="tsx">
<Action useAction={useAction} />
</pre>
大部分的操作指令需要用户提供参数,如新增数据操作,需要用户填写数据,填写数据一般需要弹出表单,用户填写完数据,点击提交,才触发操作指令。组件结构如下:
<pre lang="tsx">
<Action useAction={useAction}>
{/* 这是个弹窗表单,内置提交按钮,点击提交触发操作指令,具体代码省略 */}
<Action.Modal x-decorator={'Form'}></Action.Modal>
</Action>
</pre>
一个完整的操作大概分为两步:
- 为 Action 绑定一段指令
- 如果指令需要用户提供参数,需要提供交互界面,目前内置的有:
- Action.Drawer抽屉
- Action.Modal对话框
- Action.Popover气泡
操作是 NocoBase 里非常重要的一个概念,更多详情点此查看
## 配置工具栏 - DesignableBar
所有的 Schema Component 都可以绑定自己的配置工具栏DesignableBar用于修改当前组件的 Schema。
**什么是 Schema Component**
通过 Schema 协议编写的类 JSON Schema 格式的组件,如:
```js
{
type: 'void',
'x-Component': 'Hello',
'x-designable-bar': 'Hello.DesignableBar',
'x-dect': 'CardItem',
}
```
举几个例子,如:
表单字段的 JSON Schema
```js
const schema = {
type: 'string',
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-designable-bar': 'Form.Field.DesignableBar',
};
```
表单项的配置工具栏 `Form.Field.DesignableBar` 的效果
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/1ffba32f9a5625760c3fe11e7eb19974.png" style="max-width: 350px;"/>
表格的 JSON Schema
```js
const schema = {
type: 'array',
'x-component': 'Table',
'x-decorator': 'CardItem',
'x-designable-bar': 'Table.DesignableBar',
};
```
表格配置工具栏 `Table.DesignableBar` 的效果
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/dcd762a0444ef55a8515c53d706f7bc4.png" style="max-width: 250px;"/>
菜单项的 JSON Schema
```js
const schema = {
type: 'array',
'x-component': 'Menu.Item',
'x-designable-bar': 'Menu.Item.DesignableBar',
};
```
菜单项配置工具栏 `Menu.Item.DesignableBar` 的效果
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/984ab6da6a8f72fe790bb9bd18b3eb35.png" style="max-width: 200px;"/>
更多配置工具栏详情点此查看
## 字段组件 - CollectionField
字段组件的配置参数可能非常多在不同数据区块里也可能用到同一个字段组件为了减少代码重复NocoBase 里,将字段组件的配置交由数据表统一管理。一处配置,多处使用。数据区块里直接引用字段组件,如果有其他不同参数再另行扩展。
<pre lang="tsx">
<Table>
// 原生态的写法
<Table.Column title={'姓名'}>
<Input {...others} name="name" readPretty={true}/>
</Table.Column>
// 简化之后的字段引用
<Table.Column>
<CollectionField name="name"/>
</Table.Column>
</Table>
<Form>
// 如果在表格里也用到,再写一遍
<FormItem title={'姓名'}>
<Input {...others} name="name"/>
</FormItem>
// 字段引用,只需要提供 name 即可
<FormItem>
<CollectionField name="name"/>
</FormItem>
</Form>
</pre>
字段组件有三种显示状态:
- 可填写 - editable
- 不可填写 - disabled
- 阅读模式 - read-pretty
以单行文本Input为例
```js
// 示例Input 的三种显示状态)
// 示例待补充
```
**为什么字段有多种显示状态?**
- 在表单中一般情况字段为可填写状态editable但如果只供查看这时候就会把字段设置为 disabled 或 read-pretty。
- 在表格中一般情况字段为阅读模式read-pretty但如果需要在表格内快捷编辑又可以动态的将某个字段激活为 editable。
字段组件可以任意扩展,如何扩展查看 createCollectionField 章节。

View File

@ -1,82 +0,0 @@
---
order: 1
group:
title: Basic Concepts
path: /guide/basic
order: 4
---
# Collections & Fields
The data table of NocoBase consists of fields (columns) and records (rows). The concept of a data table is similar to the concept of a relational database data table, but the concept of fields is not the same.
## Fields
In NocoBase, the most common fields have component forms, such as: single-line text, multi-line text, and single-select boxes. These components have values, which can be filled in by the user, and are called valued components. The structure is as follows:
```ts
{
interface: 'textarea',
type: 'text',
name: 'description',
uiSchema: {
type: 'string',
title: 'Description',
'x-component': 'Input.TextArea',
'x-decorator': 'FormItem',
},
}
```
The above is a description of the field configuration.
- type indicates the field's storage type, which is text long text type
- uiSchema is the component parameter of the field
- uiSchema.type is the value type of the field's component
- uiSchema.x-component indicates the component type, which is a multi-line input box
- The fields bound to the component are set with an interface that indicates the type of the current field, the example describes the field as a multi-line text type
In addition to the common fields bound to components, there are also fields that do not need to be bound to components, such as token fields, which are not displayed on the interface. The structure of a field without a component is as follows.
```ts
{
type: 'string',
name: 'token',
}
```
**Why do fields distinguish between storage types and component types? **
1. Store types and component types are many-to-many relationships and do not lend themselves to merging.
The value of the same component may not be of the same type (storage type), e.g., the value of select may be string or integer, and the same storage type may be presented as different components, e.g., the component to which string is bound may be Input or Select.
2. A limited number of storage types and component types can be combined to create an infinite number of field types.
Single line text, email, URL, cell phone number are all the same storage type and component type, but the validation parameters are not the same, so you can create countless fields by simply adjusting the validate parameter.
## Field Types
| 名称 | Interface | Type | Component | 备注 |
| :------- | :-------- | :----- | :------------- | :---------------- |
| 单行文本 | string | string | Input | |
| 多行文本 | textarea | text | Input.TextArea | |
| Email | email | string | Input | validate: 'email' |
| Phone | phone | string | Input | validate: 'phone' |
## What can be done?
### Fast Modeling
Unlike professional modeling tools, NocoBase provides a more user-friendly approach to data table configuration.
- It can be written directly in code via app.collection(), mostly used to configure the underlying system tables.
- You can also configure data tables through the data table configuration portal of the no-code platform, which is mostly used to configure business tables.
### Create data blocks
The configured data table can be used to create corresponding blocks of data, e.g. to display the contents of a particular data table in a table format. The table allows you to select which fields are displayed as table columns.
More about blocks can be found in the Client-side Components chapter.
### HTTP API
Cross-platform operation of data tables (add, delete, configure, etc.) is also possible via HTTP API, see the REST API chapter for more details.

View File

@ -1,82 +0,0 @@
---
order: 1
group:
title: 基础概念
path: /zh-CN/guide/basic
order: 4
---
# 数据表和字段
NocoBase 的数据表由字段(列)和记录(行)组成。数据表的概念与关系型数据库的数据表概念相近,但是字段的概念并不相同。
## 字段
NocoBase 里最常见的字段具有组件形态单行文本、多行文本、单选框。这些组件都有数值value可交由用户填写称为有值组件。结构如下
```ts
{
interface: 'textarea',
type: 'text',
name: 'description',
uiSchema: {
type: 'string',
title: '描述',
'x-component': 'Input.TextArea',
'x-decorator': 'FormItem',
},
}
```
上述是一个描述字段的配置:
- type 表示字段的存储类型,为 text 长文本类型
- uiSchema 为字段的组件参数
- uiSchema.type 为字段组件的数值类型
- uiSchema.x-component 表示组件类型,为多行输入框
- 绑定了组件的字段,都要设置一个 interface表示当前字段的类型例子描述字段为多行文本类型
除了常见的绑定了组件的字段以外,还有一些无需绑定组件的字段,如 token 字段,这类组件并不会显示在界面上。无组件字段的结构如下:
```ts
{
type: 'string',
name: 'token',
}
```
**为什么字段要区分存储类型和组件类型?**
其一:存储类型和组件类型是多对多关系,并不适合合并处理。
同一组件的 value 的类型(存储类型)可能并不相同,比如 select 的 value 可能是 string 或者 integer。同一存储类型也可能以不同的组件呈现如 string 绑定的组件可能是 Input也可能是 Select。
其二:有限的存储类型和组件类型可以组合出无数种字段类型。
单行文本、电子邮件、网址、手机号这些字段的存储类型和组件类型虽然都相同,但是校验参数并不相同,只需要调整 validate 参数即可创建出无数种字段。
## 字段的类型
| 名称 | Interface | Type | Component | 备注 |
| :------- | :-------- | :----- | :------------- | :---------------- |
| 单行文本 | string | string | Input | |
| 多行文本 | textarea | text | Input.TextArea | |
| 邮箱 | email | string | Input | validate: 'email' |
| 手机号 | phone | string | Input | validate: 'phone' |
## 可以做什么?
### 快速建模
与专业的建模工具不同NocoBase 提供了一种更利于普通用户理解的数据表配置方法。
- 可以直接通过 app.collection() 直接写代码里,多用于配置底层系统表。
- 也可以通过无代码平台的数据表配置入口配置数据表,多用于配置业务表。
### 创建数据区块
配置好的数据表可用于创建对应的数据区块,如以表格的形式展示某个数据表的内容。表格里可以选择哪些字段作为表格列显示出来。
更多关于区块的内容可以查看客户端组件章节。
### HTTP API
跨平台也可以通过 HTTP API 的方式操作数据表(增删改查配置等),更多内容查看 SDK 章节。

View File

@ -1,183 +0,0 @@
---
order: 100
toc: menu
---
# Contributing
## Basic process
- Fork the source code to your own repository
- Modify source code
- Submit pull request
## Installation and start-up
```bash
# Replace the following git address with your own repo
git clone https://github.com/nocobase/nocobase.git
cd nocobase
cp .env.example .env
yarn install
yarn bootstrap
yarn build
yarn nocobase init --import-demo
yarn start
```
Open http://localhost:8000/ in your browser
<Alert title="Note">
Involved in core code development, the project starts with a documentation page, not the application's login page.
</Alert>
## Main scripts
### Startup and reboot
The above commands are only executed the first time, after that the project is restarted with
```bash
yarn start
```
### Reinstallation
If you want to clear and reinstall
```bash
### If you want to import demo data, you can add --import-demo
yarn nocobase init --import-demo
# After reinstallation, you also need to restart
yarn start
```
### Build
<Alert title="Note">
For integration testing or site-wide debugging, the following package changes need to be recompiled and repackaged.
- actions
- database
- resourcer
- server
- test
- utils
In addition to the compilation issues, there are many details of the project build that remain unresolved. If you have some good suggestions, you are welcome to discuss them on [GitHub Discussions](https://github.com/nocobase/nocobase/discussions).
</Alert>
```bash
# for all packages
yarn build
# for specific package
yarn build <package_name_1> <package_name_2>
# e.g.
# yarn build database
```
### Testing
<Alert title="Note">
After upgrading v0.5, some tests have not been fixed yet, and the ci tests are not available yet. The code tests are not perfect yet, more tests will be added and improved in phases...
</Alert>
```bash
# For all packages
yarn test
# For specific package
yarn test packages/<name>
```
### More scripts
View [scripts](https://github.com/nocobase/nocobase/blob/ff4d432c9fc3faa38cd65ab6d4dad250da02c2fd/package.json#L7) of package.json
## Document revision and translation
The documentation is in the [docs](https://github.com/nocobase/nocobase/tree/develop/docs) directory and follows Markdown syntax, defaults to English and ends with `.zh-CN.md` in Chinese, e.g.
```bash
|- /docs/
|- index.md # English document
|- index.zh-CN.md # Chinese document, when it is missing, the content of index.md is displayed
```
After modification, open http://localhost:8000/ in your browser to see the final effect.
## Back-end
Most of the changes on the back-end can be verified by the test command.
```bash
yarn test packages/<name>
```
Of course, if you are adding new content, you will need to write new tests. ``@nocobase/test`` provides ``mockDatabase`` and ``mockServer` for database and server testing, e.g.
```ts
import { mockServer, MockServer } from '@nocobase/test';
describe('mock server', () => {
let api: MockServer;
beforeEach(() => {
api = mockServer({
dataWrapping: false,
});
api.actions({
list: async (ctx, next) => {
ctx.body = [1, 2];
await next();
},
});
api.resource({
name: 'test',
});
});
afterEach(async () => {
return api.destroy();
});
it('agent.get', async () => {
const response = await api.agent().get('/test');
expect(response.body).toEqual([1, 2]);
});
it('agent.resource', async () => {
const response = await api.agent().resource('test').list();
expect(response.body).toEqual([1, 2]);
});
});
```
## Full-stack demo
http://localhost:8000/develop
To facilitate local debugging for developers, the full-stack demo is also an embedded demo, which can be opened full-screen by clicking on the new tab in the bottom left corner.
## Client components
<Alert title="Note">
The component library is still being organized...
</Alert>
Each component is independent, easy to debug and use. See the component list at http://localhost:8000/components
## Provide more examples
<Alert title="Note">
Examples are still being compiled...
</Alert>
Examples are available at http://localhost:8000/examples

View File

@ -1,183 +0,0 @@
---
order: 100
toc: menu
---
# 参与贡献
## 基本流程
- Fork 源码到自己的仓库
- 修改源码
- 提交 pull request
## 安装与启动
```bash
# 将以下 git 地址换成自己的 repo
git clone https://github.com/nocobase/nocobase.git
cd nocobase
cp .env.example .env
yarn install
yarn bootstrap
yarn build
yarn nocobase init --import-demo
yarn start
```
浏览器内打开 http://localhost:8000/
<Alert title="注意">
参与核心代码开发,项目启动打开的是文档页,并非应用的登录页。
</Alert>
## 主要的 Scripts
### 启动与重启
以上命令只在第一次执行,之后重启项目只需要
```bash
yarn start
```
### 重新安装
如果想清空重新安装
```bash
# 如果需要导入 demo 数据,可以加上 --import-demo
yarn nocobase init --import-demo
# 重装之后,也需要重启
yarn start
```
### 打包编译
<Alert title="注意">
集成测试或全站调试时,涉及以下包的修改需要重新编译打包:
- actions
- database
- resourcer
- server
- test
- utils
除了编译的问题,项目的构建还有诸多细节问题未解决。如果你有一些不错的建议,欢迎你前往 [GitHub Discussions](https://github.com/nocobase/nocobase/discussions) 讨论。
</Alert>
```bash
# for all packages
yarn build
# for specific package
yarn build <package_name_1> <package_name_2>
# e.g.
# yarn build database
```
### 测试
<Alert title="注意">
升级 v0.5 之后,有部分测试还未修复,测试的 ci 暂时也不能用。代码测试还不够完善,更多测试会阶段性的补充并完善...
</Alert>
```bash
# For all packages
yarn test
# For specific package
yarn test packages/<name>
```
### 更多命令
查看 package.json 的 [scripts](https://github.com/nocobase/nocobase/blob/ff4d432c9fc3faa38cd65ab6d4dad250da02c2fd/package.json#L7)
## 文档修改和翻译
文档在 [docs](https://github.com/nocobase/nocobase/tree/develop/docs) 目录下,遵循 Markdown 语法,默认为英文,中文以 `.zh-CN.md` 结尾,如:
```bash
|- /docs/
|- index.md # 英文文档
|- index.zh-CN.md 中文文档,缺失时,显示为 index.md 的内容
```
修改之后,浏览器内打开 http://localhost:8000/ 查看最终效果。
## 后端代码修改
后端的大部分修改可以通过 test 命令校验。
```bash
yarn test packages/<name>
```
当然,如果是新增的内容,需要编写新的测试。`@nocobase/test` 提供了 `mockDatabase``mockServer` 用于数据库和服务器的测试,如:
```ts
import { mockServer, MockServer } from '@nocobase/test';
describe('mock server', () => {
let api: MockServer;
beforeEach(() => {
api = mockServer({
dataWrapping: false,
});
api.actions({
list: async (ctx, next) => {
ctx.body = [1, 2];
await next();
},
});
api.resource({
name: 'test',
});
});
afterEach(async () => {
return api.destroy();
});
it('agent.get', async () => {
const response = await api.agent().get('/test');
expect(response.body).toEqual([1, 2]);
});
it('agent.resource', async () => {
const response = await api.agent().resource('test').list();
expect(response.body).toEqual([1, 2]);
});
});
```
## 全栈演示
http://localhost:8000/develop
为了方便开发者本地调试,全栈的演示也是内嵌的 Demo可以点击左下角新标签页内全屏打开。
## 客户端组件
<Alert title="注意">
组件库还在整理中...
</Alert>
各组件是独立的,方便调试和使用。组件列表查看 http://localhost:8000/components
## 提供更完整的示例
<Alert title="注意">
示例还在整理中...
</Alert>
示例查看 http://localhost:8000/examples

View File

@ -1,6 +0,0 @@
---
order: 3
hide: true
---
# Deployment

View File

@ -1,6 +0,0 @@
---
order: 3
hide: true
---
# 部署

View File

@ -1,42 +0,0 @@
---
nav:
title: Guide
order: 1
order: 1
title: Introducation
---
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform. No programming required, build your own collaboration platform, management system with NocoBase in minutes.
## When to use NocoBase
- **SMEs and organizations build business platforms and management systems for themselves or for their industry**
- Want the price to be low enough or even free
- Can be flexibly customized without programming knowledge
- Need full control of source code and data
- Can freely distribute and sell as their own products
- **Service providers and outsourcing teams develop collaboration platforms and management systems for their clients**
- Want to keep development costs as low as possible
- Need the most user-friendly secondary development experience
- Must be deployed privately as a standalone product for the client
- Can be freely distributed and sold by the client
## Why choose NocoBase
- **Open source and free**
- Unrestricted commercial use under the MIT license
- Full code ownership, private deployment, private and secure data
- Free to expand and develop for actual needs
- Good ecological support
- **Strong no-code capability**
- WYSIWYG visual configuration
- Separation of data structure configuration from interface configuration
- Rich combination of blocks and operations
- Role-based access control
- **Developer-friendly**
- Microkernel architecture, flexible and easy to extend, with a robust plug-in system
- Node.js-based, with popular frameworks and technologies, including Koa, Sequelize, React, Formily, Ant Design, etc.
- Progressive development, easy for getting-started, friendly to newcomers
- No binding, no strong dependencies, can be used in any combination or extensions, can be used in existing projects

View File

@ -1,42 +0,0 @@
---
nav:
title: 指南
order: 1
order: 1
title: 介绍
---
## NocoBase 是什么
NocoBase 是一个极易扩展的开源无代码开发平台。无需编程,使用 NocoBase 搭建自己的协作平台、管理系统,只需要几分钟时间。
## 哪些场景适合使用 NocoBase
- 中小企业和组织为自己或者为所在行业搭建业务平台和管理系统
- 希望价格足够低,甚至免费
- 不懂编程也可以灵活定制
- 需要完全掌控源代码和数据
- 可以以自有产品的形态自由分发和销售
- 服务商和外包团队为客户开发协作平台和管理系统
- 希望尽可能降低开发成本
- 需要极致友好的二次开发体验
- 必须以独立产品的形态为客户私有部署
- 客户可以自由分发和销售
## 为什么选择 NocoBase
- 开源免费
- 采用 MIT 许可协议,不限制商业使用
- 拥有全部代码,私有化部署,保障数据私有和安全
- 针对实际需求自由扩展开发
- 具备良好的生态支持
- 无代码能力强
- 所见即所得的可视化配置
- 数据结构配置与界面配置分离
- 丰富的区块和操作任意组合
- 基于角色的访问权限
- 对开发者友好
- 微内核,灵活易扩展,具备健全的插件体系
- 基于 Node.js使用主流框架和技术包括 Koa、Sequelize、React、Formily、Ant Design 等
- 渐进式开发,上手难度低,对新人友好
- 不绑架、不强依赖,可任意组合使用或扩展,可用于现有项目中

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,158 +0,0 @@
---
order: 2
---
# Client-side Kernel
To allow more non-developers to participate, NocoBase provides a companion client-side plugin -- a visual configuration interface with no code. The core of this part is @nocobase/client, which ideally can be used within any front-end build tool or framework, e.g.
- umijs
- create-react-app
- icejs
- vite
- snowpack
- nextjs
- Other
For the time being only support umijs (packaging compilation is still some problems), the future will gradually support the above-listed frameworks.
The main components of the client include.
## Request
- API Client
- Request Hook
```ts
const api = new APIClient({
request,
});
api.auth();
api.get();
api.post();
api.resource('collections').create();
api.resource('collections').findOne({});
api.resource('collections').findMany({});
api.resource('collections').relationship('fields').of(1).create();
```
The following details are TBD, special resources
```js
api.collections.create();
api.uiSchemas.create();
```
Request Hook
[https://www.npmjs.com/package/@ahooksjs/use-request](https://www.npmjs.com/package/@ahooksjs/use-request)
```js
const { data } = useRequest(() => api.resource('users').findMany());
```
## Routing
- createRouteSwitch
```js
const RouteSwitch = createRouteSwitch({
components: {},
});
<RouteSwitch routes={[]} />
```
## Schema component
- createSchemaComponent
```js
function Hello() {
return <div>Hello Word</div>
}
const SchemaComponent = createSchemaComponent({
scope,
components: {
Hello
},
});
const schema = {
type: 'void',
'x-component': 'Hello',
};
<SchemaComponent schema={schema} />
```
## How do you assemble it?
<pre lang="tsx">
import { I18nextProvider } from 'react-i18next';
import { createRouteSwitch, APIClient } from '@nocobase/client';
const apiClient = new APIClient();
const i18n = i18next.createInstance();
const Hello = () => {
return <div>Hello</div>;
}
const SchemaComponent = createSchemaComponent({
components: {
Hello,
},
});
const PageTemplate = () => {
const schema = {
type: 'void',
'x-component': 'Hello',
};
return (
<SchemaComponent schema={schema}/>
);
}
const RouteSwitch = createRouteSwitch({
components: {
PageTemplate,
},
});
const routes = [
{ path: '/hello', component: 'Hello' },
];
function AntdProvider(props) {
// The locale here can be handled dynamically depending on the i18next
return (
<ConfigProvider locale={locale}>{props.children}</ConfigProvider>
);
}
const App = () => {
return (
<APIClientProvider client={apiClient}>
<I18nextProvider i18n={i18n}>
<AntdProvider>
<Router>
<RouteSwitch routes={routes}/>
</Router>
</AntdProvider>
</I18nextProvider>
</APIClientProvider>
);
}
</pre>
- APIClientProvider: provides the APIClient
- I18nextProvider: internationalization
- AntdProvider: handles the internationalization of antd components, which needs to be placed in I18nextProvider
- Router: route driver
- RouteSwitch: route distribution
The above code may seem a bit verbose, but the actual function and role of each part is not the same, so it is not suitable for over-encapsulation. If needed, it can be further encapsulated according to the actual situation.

View File

@ -1,158 +0,0 @@
---
order: 2
---
# 客户端内核
为了让更多非开发人员也能参与进来NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。这部分的核心就是 @nocobase/client,理想状态可以用在任意前端构建工具或框架内,如:
- umijs
- create-react-app
- icejs
- vite
- snowpack
- nextjs
- 其他
暂时只支持 umijs打包编译还有些问题未来会逐步支持以上罗列的各个框架。
客户端主要的组成部分包括:
## 请求
- API Client
- Request Hook
```ts
const api = new APIClient({
request,
});
api.auth();
api.get();
api.post();
api.resource('collections').create();
api.resource('collections').findOne({});
api.resource('collections').findMany({});
api.resource('collections').relation('fields').of(1).create();
```
以下细节待定,特殊的资源
```js
api.collections.create();
api.uiSchemas.create();
```
Request Hook
[https://www.npmjs.com/package/@ahooksjs/use-request](https://www.npmjs.com/package/@ahooksjs/use-request)
```js
const { data } = useRequest(() => api.resource('users').findMany());
```
## 路由
- createRouteSwitch
```js
const RouteSwitch = createRouteSwitch({
components: {},
});
<RouteSwitch routes={[]} />
```
## Schema 组件
- createSchemaComponent
```js
function Hello() {
return <div>Hello Word</div>
}
const SchemaComponent = createSchemaComponent({
scope,
components: {
Hello
},
});
const schema = {
type: 'void',
'x-component': 'Hello',
};
<SchemaComponent schema={schema} />
```
## 怎么组装起来?
<pre lang="tsx">
import { I18nextProvider } from 'react-i18next';
import { createRouteSwitch, APIClient } from '@nocobase/client';
const apiClient = new APIClient();
const i18n = i18next.createInstance();
const Hello = () => {
return <div>Hello</div>;
}
const SchemaComponent = createSchemaComponent({
components: {
Hello,
},
});
const PageTemplate = () => {
const schema = {
type: 'void',
'x-component': 'Hello',
};
return (
<SchemaComponent schema={schema}/>
);
}
const RouteSwitch = createRouteSwitch({
components: {
PageTemplate,
},
});
const routes = [
{ path: '/hello', component: 'Hello' },
];
function AntdProvider(props) {
// 可以根据 i18next 的情况动态处理这里的 locale
return (
<ConfigProvider locale={locale}>{props.children}</ConfigProvider>
);
}
const App = () => {
return (
<APIClientProvider client={apiClient}>
<I18nextProvider i18n={i18n}>
<AntdProvider>
<Router>
<RouteSwitch routes=[routes]/>
</Router>
</AntdProvider>
</I18nextProvider>
</APIClientProvider>
);
}
</pre>
- APIClientProvider提供 APIClient
- I18nextProvider国际化
- AntdProvider处理 antd 组件的国际化,需要放在 I18nextProvider 里
- Router路由驱动
- RouteSwitch路由分发
上面代码看似有些啰嗦,实际各部分的功能和作用并不一样,不适合过度封装。如果需要可以根据实际情况,再进一步封装。

View File

@ -1,20 +0,0 @@
---
order: 3
---
# Installation and Startup Process
## app.init
- app.load()
- db.sync()
- db.emitAsync('init')
## app.start
- app.load()
- db.emitAsync('start')
- Load the collections configuration in the database
- Load the pm configuration in the database
- Start the activated plugin
- app.listen()

View File

@ -1,21 +0,0 @@
---
order: 3
---
# 项目安装和启动流程
## app.init
- app.load()
- db.sync()
- db.emitAsync('init')
## app.start
- app.load()
- db.emitAsync('start')
- 加载数据库里的 collections 配置
- 加载数据库里的 pm 配置
- 启动激活的插件
- app.listen()

View File

@ -1,13 +0,0 @@
---
order: 0
group:
title: Kernel Principle
path: /guide/kernel-principle
order: 6
---
# Microkernel Architecture
<img src="../../images/NocoBase.png" style="max-width: 800px; width: 100%;">
NocoBase adopts microkernel architecture, and various functions are extended in the form of plug-ins, so the microkernel architecture is also called plug-in architecture, which consists of two parts: kernel and plug-ins. The kernel provides the minimum functional WEB server and various plug-in interfaces; plug-ins are various independent modules divided by functions, which are pluggable through interface adaptation. The plug-in design reduces the coupling between modules and improves the reuse rate. With the continuous expansion of the plug-in library, common scenarios only need to combine plug-ins to complete the basic construction, and this design concept is ideal for codeless platforms.

View File

@ -1,13 +0,0 @@
---
order: 0
group:
title: 内核原理
path: /zh-CN/guide/kernel-principle
order: 6
---
# 微内核架构
<img src="../../images/NocoBase.png" style="max-width: 800px; width: 100%;">
NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建,这种设计理念非常适合无代码平台。

View File

@ -1,224 +0,0 @@
# Talking about the design ideas of NocoBase
NocoBase 的内核只是一个无 GUIClient的 ServerClient 作为插件集成进来。Client 是变化的,不限于 React、Vue 等。虽然 Client 作为插件存在,但也是非常重要的一个分支,用于支持无代码平台。
在概念上有很多可能,但是 NocoBase 也需要基于特定的技术框架实现 Server 和 Client 内核。
- ServerNodejs、Koa、Sequelize
- ClientReact、Formily、Ant Design
在实际应用中Server 可能只有一个,但 Client 可能有很多WEB、小程序、Android、IOS 都不一样。为了最大的兼容多端可能,客户端核心的 Schema Component 基于 Formily 实现,理论上支持适配 React/ReactNative/Vue2/Vue3 框架。
理想状态 Server 和 Client 都可以单独使用。只用 NocoBase 的 Server对接的其他 Client。只用 NocoBase 的 Client对接其他的 Server。不过 Client 是为 NocoBase 的 Server 服务,虽然可以适配非 NocoBase 的 Server但是会有些局限不是所有功能都能使用
Server 和 Client 是非常重要的两个分支插件也围绕着这两部分开展。NocoBase 提供了公共的接口用于对接各类插件,但是实现插件的核心在于内核提供了哪些 Extension API。
## 扩展 - Extensions
扩展是按「概念」划分的最小化的独立模块。
<Alert title="注意">
扩展的划分还比较清晰,但是有些扩展怎么更好的协调起来,有待商榷。
</Alert>
Database
- Collections
- Field Types
- Filter Operators
- Repositories
- Models
- Events/Hooks
Application
- Middlewares
- Resources也可以理解为提供 Services
- Actions
- Events/Hooks
- CLI
Client
- SchemaComponent
- CollectionField
- RouteSwitch
SchemaComponent只列三个特殊的存在扩展的组件
- Action
- AddNew
- DesignableBar
## 插件 - Plugins
插件是按「功能」划分的可插拔的独立模块。插件可能需要由多个扩展协调起来才能正常工作。
<Alert title="扩展和插件的区别">
两者虽然都称之为模块,但是存在差别。扩展是概念性的,比较抽象;插件是功能性的,比较具体。插件可能需要由多个扩展协调起来才能正常工作。
</Alert>
以完整的导出功能为例,需要实现的扩展包括:
- `服务端` 实现 Export Action APIApplication -> Action
- `客户端` 实现 Export Action ButtonSchemaComponent -> Action
- `客户端` 为 Export Action Button 实现自己的 Export DesignableBarSchemaComponent -> Action 和 DesignableBar
除此之外,还有两个需要考虑的细节:
- `客户端` Action Button 是放在区块的可操作区Block.ActionBar那这个 Export Button 可以放在哪些区块的 Block.ActionBar 里。在表格区块里导入的可能是表格,在详情区块里导出的可能是 PDF、图片、Word 文档。
- `服务端` Action 是有权限的,权限除了控制服务端的 Action API也需要控制 Action Button 的显示和隐藏。
再举个例子,例如文件管理器,相关扩展包括:
- `服务端` 提供 attachments 表,用于存上传的文件信息
- `服务端` 提供 storages 表,用于配置存储服务信息,如 oss、s3、minio、local 等
- `服务端` 提供 upload action api
- `客户端` 提供 attachment 的 field interface扩展的附件字段
- `客户端` 提供 file-manager 区块,用于管理上传的文件(特定的 SchemaComponent
以上两个例子都有服务端和客户端代码,有两个没有考虑清楚的细节:
- 前后端代码怎么安排比较合适?
- 客户端代码怎么动态载入和去除呢?
这就是下一个话题了 —— 插件管理器
## 插件管理器
插件可能需要由多个扩展协调起来才能正常工作,那如何让插件正常工作?
上述的两个例子
- 导出只需要把各扩展模块放到该放的地方(模块的加载)就能正常工作了。
- 文件管理器除了模块的加载,还需要初始化 attachments 和 storages 表,并且配置上 storage。
以上工作就是插件管理器负责的,协调各扩展模块的加载,让插件可以正常的运作起来。
插件管理器的职责:
- 负责插件的下载、安装、激活、禁用和移除
- 协调插件
- 各模块归位,包括按依赖顺序载入/移除模块
- 如果是客户端的代码,如何处理
- 什么时候做什么事情,包括:
- `nocobase init`
- `nocobase start`
- `nocobase pm:download <plugin-name> --enable`
- `nocobase pm:enable <plugin-name>`
- `nocobase pm:disable <plugin-name>`
- `nocobase pm:remove <plugin-name>`
## Events & Middlewares
在上面列举的几类扩展中Events 和 Middlewares 需要考虑优先级问题。以 Events 为例,假设有个 `tests.beforeCreate` 事件:
A 插件:
```ts
db.on('tests.beforeCreate', (model) => {
model.set('x', 'a1');
});
```
B 插件:
```ts
db.on('tests.beforeCreate', (model) => {
model.set('x', 'b1');
});
```
如果是按照 A、B 顺序加载model.x 的结果为 b1如果是按照 B、A 的顺序加载model.x 的结果为 a1。A、B 的加载顺序对结果会有影响,但这个影响并不是插件的依赖关系,所以并不能通过插件依赖来解决。较好的思路,可以给事件的 listener 加个 priority
A 插件:
```ts
db.on('tests.beforeCreate', (model) => {
model.set('x', 'a1');
}, {
priority: 100,
});
db.on('tests.beforeCreate', (model) => {
model.set('y', 'a2');
}, {
priority: 400,
});
```
B 插件:
```ts
db.on('tests.beforeCreate', (model) => {
model.set('x', 'b1');
}, {
priority: 200,
});
db.on('tests.beforeCreate', (model) => {
model.set('y', 'b2');
}, {
priority: 300,
});
```
有了 priority不管插件通过什么顺序加载最终的 model.x 的值都是 b1 model.y 的值都是 a2。进一步整理可以给 priority 设定一些常量,比如:
- highest200
- higher300
- high400
- normal500默认值
- low600
- lower700
- lowest800
这个思路能够解决大部分优先级问题。不过,钻牛角尖来说,如果想把 C 的事件放在 A、B 中间呢?如:
```ts
// A 插件
db.on('tests.beforeCreate', (model) => {
}, {
priority: 500,
});
// C 插件
db.on('tests.beforeCreate', (model) => {
}, {
priority: 500,
});
// B 插件
db.on('tests.beforeCreate', (model) => {
}, {
priority: 500,
});
```
上述例子是个非常极端的情况,一般不用考虑这么细,如果想要支持精确插入,可以考虑加个 name 参数,再配合 insertAfter 或 insertBefore 指定插入位置,如:
```ts
// A 插件
db.on('tests.beforeCreate', (model) => {
});
// B 插件
db.on('tests.beforeCreate', (model) => {
}, {
name: 'b1'
});
// C 插件
db.on('tests.beforeCreate', (model) => {
}, {
name: 'c1',
insertBefore: 'b1', // 虽然写在最后,但是要插入在 b1 前面,
// insertAfter: 'a1',
});
```

View File

@ -1,261 +0,0 @@
---
order: 999
---
# Reverse relationship fields
## 关系字段类型
目前内置的关系字段有:
- hasOne
- hasMany
- belongsTo
- belongsToMany
配置 Collection 的时候可以这样写:
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasOne', name: 'profile' },
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [
{ type: 'belongsTo', name: 'user' },
{ type: 'belongsToMany', name: 'tags' },
],
});
db.collection({
name: 'tags',
fields: [
{ type: 'belongsToMany', name: 'posts' },
],
});
```
以上配置关系字段时,只填写了 type 和 name 两个参数,但是实际上,完整的参数如下:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'posts',
target: 'posts', // 缺失时,取 name 当 target
foreignKey: 'userId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
},
{
type: 'hasOne',
name: 'profile',
target: 'profiles', // 缺失时,取 name 的复数形态
foreignKey: 'userId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
},
],
});
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'user',
target: 'users', // 缺失时,取 name 的复数形态
foreignKey: 'userId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
{
type: 'belongsToMany',
name: 'tags',
target: 'tags', // 缺失时,取 name
through: 'posts_tags', // 缺失时,取 SourceModel 的 name 和 TargetModel 的 name 首字母自然顺序拼接的字符串
foreignKey: 'postId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
otherKey: 'tagId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
],
});
db.collection({
name: 'tags',
fields: [
{
type: 'belongsToMany',
name: 'posts',
target: 'posts', // 缺失时,取 name
through: 'posts_tags', // 缺失时,取 SourceModel 的 name 和 TargetModel 的 name 首字母自然顺序拼接的字符串
foreignKey: 'tagId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
otherKey: 'postId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
],
});
```
## 反向关系字段
每个关系字段都存在一个对应的反向关系字段,如上面例子 `posts.tags``tags.posts` 是一对。例如:
posts 里的关系字段:
```ts
{
type: 'belongsToMany',
name: 'tags',
target: 'tags',
through: 'posts_tags',
foreignKey: 'postId',
sourceKey: 'id',
otherKey: 'tagId',
targetKey: 'id',
}
```
反向关系字段为(在 tags 里):
```ts
{
type: 'belongsToMany',
name: 'posts',
target: 'posts',
through: 'posts_tags',
foreignKey: 'tagId',
sourceKey: 'id',
otherKey: 'postId',
targetKey: 'id',
}
```
必须符合以下条件:
- type 都是 belongsToMany
- target 等于 sourcemodel.name
- through 相同
- foreignKey 等于 otherKey
- sourceKey 等于 targetKey
- otherKey 等于 foreignKey
- targetKey 等于 sourceKey
也就是说,除了 name 以外,其他的几个核心参数都要对应上。
### 判断条件
为了方便理解,设定了三个变量。
- field 表是某个关系字段的参数配置
- reverse 表示反向关系字段的参数配置
- field.model.name 表示 field 所在 model 的 name
#### hasOne
- reverse.type === 'belongsTo',
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
#### hasMany
- reverse.type === 'belongsTo',
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
#### belongsTo
- reverse.type === 'hasMany'
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
belongsTo 的情况较为特殊,默认按 hasMany 处理,但可能不是,还需要一个参数来判断(反向关系字段是否可以关联多条数据)。
#### belongsToMany
- reverse.type === 'belongsToMany'
- reverse.target === field.model.name
- reverse.through === field.through
- reverse.foreignKey === field.otherKey
- reverse.sourceKey === field.targetKey
- reverse.otherKey === field.foreignKey
- reverse.targetKey === field.sourceKey
### 缺失补齐逻辑
当反向关系字段缺失时,自动补齐。例如:
如果 posts 不存在,不做任何处理
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
```
如果 posts 里显式的声明了反向关系字段,不需要自动生成
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [
{ type: 'belongsTo', name: 'user' },
],
});
```
如果 posts 里缺失,自动生成
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [],
});
```
自动生成是隐式的,如果后续又显式的添加了,要解决合并问题
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
const collection = db.collection({
name: 'posts',
fields: [],
});
// 这里不是新增,而是替换
collection.addField({ type: 'belongsTo', name: 'user' });
```
注:显式添加指的是代码配置上可见,隐式添加指的是自动生成,配置存在,但代码上但看不见。

View File

@ -1,261 +0,0 @@
---
order: 999
---
# Reverse relationship fields
## 关系字段类型
目前内置的关系字段有:
- hasOne
- hasMany
- belongsTo
- belongsToMany
配置 Collection 的时候可以这样写:
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasOne', name: 'profile' },
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [
{ type: 'belongsTo', name: 'user' },
{ type: 'belongsToMany', name: 'tags' },
],
});
db.collection({
name: 'tags',
fields: [
{ type: 'belongsToMany', name: 'posts' },
],
});
```
以上配置关系字段时,只填写了 type 和 name 两个参数,但是实际上,完整的参数如下:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'posts',
target: 'posts', // 缺失时,取 name 当 target
foreignKey: 'userId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
},
{
type: 'hasOne',
name: 'profile',
target: 'profiles', // 缺失时,取 name 的复数形态
foreignKey: 'userId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
},
],
});
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'user',
target: 'users', // 缺失时,取 name 的复数形态
foreignKey: 'userId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
{
type: 'belongsToMany',
name: 'tags',
target: 'tags', // 缺失时,取 name
through: 'posts_tags', // 缺失时,取 SourceModel 的 name 和 TargetModel 的 name 首字母自然顺序拼接的字符串
foreignKey: 'postId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
otherKey: 'tagId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
],
});
db.collection({
name: 'tags',
fields: [
{
type: 'belongsToMany',
name: 'posts',
target: 'posts', // 缺失时,取 name
through: 'posts_tags', // 缺失时,取 SourceModel 的 name 和 TargetModel 的 name 首字母自然顺序拼接的字符串
foreignKey: 'tagId', // 缺失时,取 SourceModel 的 name 的单数形态 + Id
sourceKey: 'id', // 缺失时,取 SourceModel 的 primaryKeyAttribute
otherKey: 'postId', // 缺失时,取 TargetModel 的 name 的单数形态 + Id
targetKey: 'id', // 缺失时,取 TargetModel 的 primaryKeyAttribute
},
],
});
```
## 反向关系字段
每个关系字段都存在一个对应的反向关系字段,如上面例子 `posts.tags``tags.posts` 是一对。例如:
posts 里的关系字段:
```ts
{
type: 'belongsToMany',
name: 'tags',
target: 'tags',
through: 'posts_tags',
foreignKey: 'postId',
sourceKey: 'id',
otherKey: 'tagId',
targetKey: 'id',
}
```
反向关系字段为(在 tags 里):
```ts
{
type: 'belongsToMany',
name: 'posts',
target: 'posts',
through: 'posts_tags',
foreignKey: 'tagId',
sourceKey: 'id',
otherKey: 'postId',
targetKey: 'id',
}
```
必须符合以下条件:
- type 都是 belongsToMany
- target 等于 sourcemodel.name
- through 相同
- foreignKey 等于 otherKey
- sourceKey 等于 targetKey
- otherKey 等于 foreignKey
- targetKey 等于 sourceKey
也就是说,除了 name 以外,其他的几个核心参数都要对应上。
### 判断条件
为了方便理解,设定了三个变量。
- field 表是某个关系字段的参数配置
- reverse 表示反向关系字段的参数配置
- field.model.name 表示 field 所在 model 的 name
#### hasOne
- reverse.type === 'belongsTo',
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
#### hasMany
- reverse.type === 'belongsTo',
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
#### belongsTo
- reverse.type === 'hasMany'
- reverse.target === field.model.name
- reverse.foreignKey === field.foreignKey
- reverse.targetKey === field.sourceKey
belongsTo 的情况较为特殊,默认按 hasMany 处理,但可能不是,还需要一个参数来判断(反向关系字段是否可以关联多条数据)。
#### belongsToMany
- reverse.type === 'belongsToMany'
- reverse.target === field.model.name
- reverse.through === field.through
- reverse.foreignKey === field.otherKey
- reverse.sourceKey === field.targetKey
- reverse.otherKey === field.foreignKey
- reverse.targetKey === field.sourceKey
### 缺失补齐逻辑
当反向关系字段缺失时,自动补齐。例如:
如果 posts 不存在,不做任何处理
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
```
如果 posts 里显式的声明了反向关系字段,不需要自动生成
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [
{ type: 'belongsTo', name: 'user' },
],
});
```
如果 posts 里缺失,自动生成
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
db.collection({
name: 'posts',
fields: [],
});
```
自动生成是隐式的,如果后续又显式的添加了,要解决合并问题
```ts
db.collection({
name: 'users',
fields: [
{ type: 'hasMany', name: 'posts' },
],
});
const collection = db.collection({
name: 'posts',
fields: [],
});
// 这里不是新增,而是替换
collection.addField({ type: 'belongsTo', name: 'user' });
```
注:显式添加指的是代码配置上可见,隐式添加指的是自动生成,配置存在,但代码上但看不见。

Some files were not shown because too many files have changed in this diff Show More