v0.6
This commit is contained in:
parent
c2ff7882bc
commit
732d31009e
@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
.env
|
||||
packages/app/.env
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import React from 'react';
|
||||
import './Badge.less';
|
||||
|
||||
export default (props: any) => <span className="__dumi-default-badge" {...props} />;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -1,5 +0,0 @@
|
||||
import React, { Redirect } from 'umi';
|
||||
|
||||
export default (props: any) => {
|
||||
return <Redirect {...props}/>
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
/>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
72
.env.example
72
.env.example
@ -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=
|
@ -8,3 +8,9 @@ website
|
||||
gh-pages
|
||||
weex
|
||||
build.ts
|
||||
packages/vue
|
||||
packages/element
|
||||
esm
|
||||
doc-site
|
||||
public
|
||||
package
|
38
.fatherrc.ts
38
.fatherrc.ts
@ -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,
|
||||
};
|
43
.github/workflows/vercel.yml
vendored
43
.github/workflows/vercel.yml
vendored
@ -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
|
20
.gitignore
vendored
20
.gitignore
vendored
@ -3,23 +3,11 @@ lib/
|
||||
esm/
|
||||
.env
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
!/yarn.lock
|
||||
!/package-lock.json
|
||||
yarn-error.log
|
||||
lerna-debug.log
|
||||
packages/database/package-lock.json
|
||||
packages/resourcer/package-lock.json
|
||||
|
||||
verdaccio
|
||||
uploads/
|
||||
.umi
|
||||
.umi-production
|
||||
/dist
|
||||
/.vscode
|
||||
/.idea
|
||||
|
||||
.vercel
|
||||
packages/database/src/__tests__/.db
|
||||
db.sqlite
|
||||
db.sqlite
|
||||
coverage
|
||||
.umi
|
||||
/uploads
|
||||
|
@ -6,3 +6,6 @@ package.json
|
||||
.umi
|
||||
.umi-production
|
||||
.umi-test
|
||||
packages/**/lib/**
|
||||
packages/**/esm/**
|
||||
packages/**/node_modules/**
|
69
.umirc.ts
69
.umirc.ts
@ -1,73 +1,10 @@
|
||||
import { defineConfig } from 'dumi';
|
||||
import { getUmiConfig } from './packages/utils/src/umiConfig';
|
||||
|
||||
process.env.MFSU_AD = 'none';
|
||||
|
||||
const umiConfig = getUmiConfig();
|
||||
|
||||
export default defineConfig({
|
||||
title: 'NocoBase',
|
||||
hash: true,
|
||||
define: {
|
||||
...umiConfig.define,
|
||||
},
|
||||
// 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: {},
|
||||
favicon: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
|
||||
logo: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
|
||||
outputPath: 'docs-dist',
|
||||
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
|
||||
});
|
||||
|
21
Dockerfile
21
Dockerfile
@ -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
21
LICENSE
@ -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
167
README.md
@ -1,112 +1,111 @@
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
# NocoBase 事件
|
||||
|
||||
![](https://nocobase.oss-cn-beijing.aliyuncs.com/bbcedd403d31cd1ccc4e9709581f5c2f.png)
|
||||
事件是被动的,由执行某个方法触发的一种行为。
|
||||
|
||||
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.
|
||||
Application 的安装、启动、停止触发的事件,挂在 app.on 下,有:
|
||||
|
||||
Homepage:
|
||||
https://www.nocobase.com/
|
||||
- init
|
||||
- start
|
||||
- stop
|
||||
- error(Koa 内置)
|
||||
|
||||
Online Demo:
|
||||
https://demo.nocobase.com/new
|
||||
不区分前后?
|
||||
|
||||
Contact Us:
|
||||
hello@nocobase.com
|
||||
全局的 resource action,也是 app.on 层面的事件
|
||||
|
||||
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
|
||||
- users.login
|
||||
|
||||
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
|
||||
配置 collection,db.on 层面的事件
|
||||
|
||||
Note
|
||||
----------
|
||||
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.
|
||||
If you are interested in NocoBase, please join us to discuss and develop it together.
|
||||
- afterDefineCollection
|
||||
- afterUpdateCollection
|
||||
- afterRemoveCollection
|
||||
- afterAddField
|
||||
- afterRemoveField
|
||||
|
||||
Architecture
|
||||
----------
|
||||
|
||||
![](https://docs.nocobase.com/static/NocoBase.c9542b1f.png)
|
||||
app.on
|
||||
|
||||
Requirements
|
||||
----------
|
||||
application 的安装、启动、停止等
|
||||
|
||||
Node:
|
||||
- init
|
||||
- start
|
||||
- stop
|
||||
- error(Koa 内置)
|
||||
|
||||
- Node.js 12.20+
|
||||
resource 的 actions
|
||||
|
||||
Database:
|
||||
- `<actionHookType>`
|
||||
- `<resourceName>.<actionHookType>`
|
||||
|
||||
- PostgreSQL 10.x+
|
||||
- Sqlite 3+
|
||||
db.on
|
||||
|
||||
Installation
|
||||
----------
|
||||
- afterDefineCollection
|
||||
- afterUpdateCollection
|
||||
- afterRemoveCollection
|
||||
- afterAddField
|
||||
- afterUpdateField
|
||||
- afterRemoveField
|
||||
- `<modelHookType>`
|
||||
- `<modelName>.<modelHookType>`
|
||||
|
||||
### Create a project with `create-nocobase-app`
|
||||
collection.on
|
||||
|
||||
#### Quickstart
|
||||
~~~shell
|
||||
yarn create nocobase-app my-nocobase-app --quickstart --lang=en-US
|
||||
# `--lang=en-US` Language settings support `--lang=en-US` and `--lang=zh-CN`
|
||||
~~~
|
||||
- afterAddField
|
||||
- afterUpdateField
|
||||
- afterRemoveField
|
||||
|
||||
#### 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
|
||||
|
116
README.zh-CN.md
116
README.zh-CN.md
@ -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
|
@ -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"
|
@ -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
|
@ -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
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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' }],
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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 },
|
||||
],
|
||||
};
|
||||
```
|
@ -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,
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
@ -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,
|
||||
},
|
||||
})
|
||||
```
|
@ -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,可以 update,remove 等等
|
||||
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,
|
||||
},
|
||||
})
|
||||
```
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
title: AdminLayout
|
||||
group:
|
||||
title: Route Components
|
||||
path: /components/route-components
|
||||
order: 1
|
||||
---
|
||||
|
||||
# AdminLayout
|
||||
|
||||
Coming soon...
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
title: AdminLayout
|
||||
group:
|
||||
title: 路由组件
|
||||
path: /zh-CN/components/route-components
|
||||
order: 1
|
||||
---
|
||||
|
||||
# AdminLayout
|
||||
|
||||
待补充...
|
@ -1,11 +0,0 @@
|
||||
---
|
||||
order: 2
|
||||
title: AuthLayout
|
||||
group:
|
||||
path: /components/route-components
|
||||
order: 1
|
||||
---
|
||||
|
||||
# AuthLayout
|
||||
|
||||
Coming soon...
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
sidemenu: false
|
||||
order: 1000
|
||||
hide: true
|
||||
---
|
||||
|
||||
<code src="./examples/demos/demo1.tsx"/>
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
sidemenu: false
|
||||
nav:
|
||||
title: Examples
|
||||
order: 99
|
||||
---
|
||||
|
||||
Coming soon...
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
sidemenu: false
|
||||
nav:
|
||||
title: 示例
|
||||
order: 99
|
||||
---
|
||||
|
||||
待补充...
|
@ -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.
|
@ -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 章节。
|
@ -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.
|
@ -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 章节。
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
hide: true
|
||||
---
|
||||
|
||||
# Deployment
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
hide: true
|
||||
---
|
||||
|
||||
# 部署
|
@ -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
|
@ -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
@ -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.
|
@ -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:路由分发
|
||||
|
||||
上面代码看似有些啰嗦,实际各部分的功能和作用并不一样,不适合过度封装。如果需要可以根据实际情况,再进一步封装。
|
@ -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()
|
@ -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()
|
||||
|
@ -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.
|
@ -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 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建,这种设计理念非常适合无代码平台。
|
@ -1,224 +0,0 @@
|
||||
# Talking about the design ideas of NocoBase
|
||||
|
||||
NocoBase 的内核只是一个无 GUI(Client)的 Server,Client 作为插件集成进来。Client 是变化的,不限于 React、Vue 等。虽然 Client 作为插件存在,但也是非常重要的一个分支,用于支持无代码平台。
|
||||
|
||||
在概念上有很多可能,但是 NocoBase 也需要基于特定的技术框架实现 Server 和 Client 内核。
|
||||
|
||||
- Server:Nodejs、Koa、Sequelize
|
||||
- Client:React、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 API(Application -> Action)
|
||||
- `客户端` 实现 Export Action Button(SchemaComponent -> Action)
|
||||
- `客户端` 为 Export Action Button 实现自己的 Export DesignableBar(SchemaComponent -> 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 设定一些常量,比如:
|
||||
|
||||
- highest:200
|
||||
- higher:300
|
||||
- high:400
|
||||
- normal:500(默认值)
|
||||
- low:600
|
||||
- lower:700
|
||||
- lowest:800
|
||||
|
||||
这个思路能够解决大部分优先级问题。不过,钻牛角尖来说,如果想把 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',
|
||||
});
|
||||
```
|
@ -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' });
|
||||
```
|
||||
|
||||
注:显式添加指的是代码配置上可见,隐式添加指的是自动生成,配置存在,但代码上但看不见。
|
@ -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
Loading…
Reference in New Issue
Block a user