官网文档及示例支持切换到移动端下显示

This commit is contained in:
wuduoyi 2020-11-19 13:47:56 +08:00 committed by liaoxuezhi
parent 7dabf46bc0
commit 771557224e
11 changed files with 357 additions and 49 deletions

View File

@ -8,9 +8,9 @@ amis 有两种使用方法:
- [JS SDK](#SDK)
- [React](#react)
React 可以完整使用 amis 的所有功能,方便扩展。
React 版本可以完整使用 amis 的所有功能,方便扩展。
SDK 适合对前端或 React 不了解的开发者,它不依赖 npm 及 webpack直接引入代码就能使用,但需要注意这种方式难以支持 [自定义组件](./custom),只能使用 amis 内置的组件。
SDK 版本适合对前端或 React 不了解的开发者,它不依赖 npm 及 webpack可以像 Vue/jQuery 那样外链代码就能使用,但需要注意这种方式难以支持 [自定义组件](./custom),只能使用 amis 内置的组件。
## SDK

View File

@ -66,6 +66,18 @@ const locales = [
}
];
const viewModes = [
{
label: '桌面端',
value: 'pc'
},
{
label: '移动端',
value: 'mobile'
}
];
function getPath(path) {
return path
? path[0] === '/'
@ -108,6 +120,7 @@ class BackTop extends React.PureComponent {
@withRouter
export class App extends React.PureComponent {
state = {
viewMode: localStorage.getItem('viewMode') || 'pc',
offScreen: false,
headerVisible: true,
themeIndex: 0,
@ -226,11 +239,12 @@ export class App extends React.PureComponent {
onChange={locale => {
this.setState({locale: locale.value});
localStorage.setItem('locale', locale.value);
window.location.reload();
}}
/>
</div>
<div className="hidden-xs p-t pull-right">
<div className="hidden-xs p-t pull-right m-l-sm">
<Select
clearable={false}
theme={this.state.theme.value}
@ -248,6 +262,20 @@ export class App extends React.PureComponent {
}}
/>
</div>
<div className="hidden-xs p-t pull-right">
<Select
clearable={false}
theme={this.state.theme.value}
value={this.state.viewMode || 'pc'}
options={viewModes}
onChange={viewMode => {
this.setState({viewMode: viewMode.value});
localStorage.setItem('viewMode', viewMode.value);
window.location.reload();
}}
/>
</div>
</div>
<div className={`${theme.ns}Layout-searchBar hidden-xs hidden-sm`}>
@ -368,6 +396,7 @@ export class App extends React.PureComponent {
setNavigations: this.setNavigations,
theme: theme.value,
classPrefix: theme.ns,
viewMode: this.state.viewMode,
locale: this.state.locale,
offScreen: this.state.offScreen,
ContextPath

View File

@ -1045,6 +1045,7 @@ export default class Doc extends React.PureComponent {
theme: this.props.theme,
classPrefix: this.props.classPrefix,
locale: this.props.locale,
viewMode: this.props.viewMode,
offScreen: this.props.offScreen,
ContextPath: this.props.ContextPath,
prevDoc: this.state.prevDoc,

View File

@ -560,6 +560,7 @@ export default class Example extends React.PureComponent {
theme: this.props.theme,
classPrefix: this.props.classPrefix,
locale: this.props.locale,
viewMode: this.props.viewMode,
offScreen: this.props.offScreen
})}
</>

View File

@ -24,17 +24,19 @@ class CodePreview extends React.Component {
}
render() {
const {
container,
height,
setAsideFolded,
setHeaderVisible,
...rest
} = this.props;
const {container, setAsideFolded, setHeaderVisible, ...rest} = this.props;
let height = this.props.height;
const PlayGround = this.state.PlayGround;
// 不要放在 .markdown-body 下面,因为样式会干扰,复写又麻烦,所以通过 Overlay 渲染到同级
if (this.props.viewMode === 'mobile') {
// 移动端下高度不能太低
if (height < 500) {
height = 500;
}
}
return (
<div>
<span style={{display: 'block', height: height}} ref="span" />

View File

@ -61,15 +61,15 @@ export default class PlayGround extends React.Component {
startX = 0;
oldContents = '';
frameTemplate;
iframeRef;
static defaultProps = {
useIFrame: false,
vertical: false
};
constructor(props) {
super(props);
this.iframeRef = React.createRef();
const {router} = props;
const schema = this.buildSchema(props.code || DEFAULT_CONTENT, props);
@ -139,12 +139,27 @@ export default class PlayGround extends React.Component {
}
};
const links = [].slice
.call(document.head.querySelectorAll('link,style'))
.map(item => item.outerHTML);
this.frameTemplate = `<!DOCTYPE html><html><head>${links.join(
''
)}</head><body><div></div></body></html>`;
this.watchIframeReady = this.watchIframeReady.bind(this);
window.addEventListener('message', this.watchIframeReady, false);
}
watchIframeReady(event) {
// iframe amis
if (event.data && event.data === 'amisReady') {
this.updateIframe();
}
}
updateIframe() {
if (this.iframeRef && this.iframeRef.current) {
this.iframeRef.current.contentWindow.postMessage(
{
schema: this.state.schema,
props: {theme: this.props.theme, locale: this.props.locale}
},
'*'
);
}
}
componentWillReceiveProps(nextprops) {
@ -168,6 +183,7 @@ export default class PlayGround extends React.Component {
componentWillUnmount() {
this.props.setAsideFolded && this.props.setAsideFolded(false);
window.removeEventListener('message', this.watchIframeReady, false);
}
buildSchema(schemaContent, props = this.props) {
@ -224,33 +240,37 @@ export default class PlayGround extends React.Component {
affixFooter: false
};
if (!this.props.useIFrame) {
return render(schema, props, this.env);
if (this.props.viewMode === 'mobile') {
return (
<iframe
width="375"
height="100%"
frameBorder={0}
className="mobile-frame"
ref={this.iframeRef}
// @ts-ignore
src={__uri('../index.html#mobileView')}
></iframe>
);
}
return (
<Frame
width="100%"
height="100%"
frameBorder={0}
initialContent={this.frameTemplate}
>
{render(schema, props, this.env)}
</Frame>
);
return render(schema, props, this.env);
}
handleChange(value) {
this.setState({
schemaCode: value
});
try {
const schema = JSON.parse(value);
this.setState({
schema
});
this.setState(
{
schema
},
() => {
this.updateIframe();
}
);
} catch (e) {
//ignore
}

View File

@ -15,6 +15,9 @@ function loadEditor() {
resolve(component.default))
);
}
const viewMode = localStorage.getItem('viewMode') || 'pc';
export default function (schema) {
if (!schema['$schema']) {
schema = {
@ -25,7 +28,8 @@ export default function (schema) {
return withRouter(
class extends React.Component {
static displayName = 'SchemaRenderer';
state = {open: false};
iframeRef;
state = {open: false, schema: {}};
toggleCode = () =>
this.setState({
open: !this.state.open
@ -40,6 +44,7 @@ export default function (schema) {
});
constructor(props) {
super(props);
const {router} = props;
this.env = {
updateLocation: (location, replace) => {
@ -92,6 +97,10 @@ export default function (schema) {
};
this.handleEditorMount = this.handleEditorMount.bind(this);
this.iframeRef = React.createRef();
this.watchIframeReady = this.watchIframeReady.bind(this);
window.addEventListener('message', this.watchIframeReady, false);
}
handleEditorMount(editor, monaco) {
@ -130,9 +139,51 @@ export default function (schema) {
);
}
watchIframeReady(event) {
// iframe amis
if (event.data && event.data === 'amisReady') {
this.updateIframe();
}
}
updateIframe() {
if (this.iframeRef && this.iframeRef.current) {
this.iframeRef.current.contentWindow.postMessage(
{
schema: schema,
props: {
location: this.props.location,
theme: this.props.theme,
locale: this.props.locale
}
},
'*'
);
}
}
componentWillUnmount() {
this.props.setAsideFolded && this.props.setAsideFolded(false);
window.removeEventListener('message', this.watchIframeReady, false);
}
renderSchema() {
const {router, location, theme, locale} = this.props;
if (viewMode === 'mobile') {
return (
<iframe
width="375"
height="100%"
frameBorder={0}
className="mobile-frame"
ref={this.iframeRef}
// @ts-ignore
src={__uri('../index.html#mobileView')}
></iframe>
);
}
return render(
schema,
{

View File

@ -44,21 +44,53 @@
<div id="root" class="app-wrapper"></div>
<script src="./mod.js"></script>
<script type="text/javascript">
var _hmt = _hmt || [];
if (location.hash === '#mobileView') {
// TODO: 本来应该使用 mobile.html 的,但 gh-pages 编译时 themes 下的 scss hash 不一致导致出错,所以先和 index.html 放一起
const themes = [
{
label: '默认主题',
value: 'default'
},
{
label: '百度云舍',
value: 'cxd'
},
{
label: 'Dark',
value: 'dark'
}
];
const theme = themes[localStorage.getItem('themeIndex') || 0];
// mobile 下先禁掉所有外链避免影响
document.querySelectorAll('link').forEach(item => {
item.disabled = true;
});
document.querySelector(`link[title=${theme.value}]`).disabled = false;
if (theme.value === 'dark') {
document.querySelector('body').classList.add('dark');
}
// 百度统计
(function () {
var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?1f80f2c9dbe21dc3af239cf9eee90f1f';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
amis.require(['./mobile.tsx'], function (app) {
app.bootstrap(document.getElementById('root'), {});
});
} else {
var _hmt = _hmt || [];
/* @require ./index.jsx 标记为同步依赖,提前加载 */
amis.require(['./index.jsx'], function (app) {
var initialState = {};
app.bootstrap(document.getElementById('root'), initialState);
});
// 百度统计
(function () {
var hm = document.createElement('script');
hm.src =
'https://hm.baidu.com/hm.js?1f80f2c9dbe21dc3af239cf9eee90f1f';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
/* @require ./index.jsx 标记为同步依赖,提前加载 */
amis.require(['./index.jsx'], function (app) {
var initialState = {};
app.bootstrap(document.getElementById('root'), initialState);
});
}
</script>
</body>
</html>

75
examples/mobile.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>MobileView</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link type="image/x-icon" rel="shortcut icon" href="./static/favicon.png" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<link rel="stylesheet" href="font-awesome/css/font-awesome.css" />
<link rel="stylesheet" href="animate.css/animate.css" />
<link rel="stylesheet" title="default" href="../scss/themes/default.scss" />
<link
rel="stylesheet"
title="cxd"
disabled
href="../scss/themes/cxd.scss"
/>
<link
rel="stylesheet"
title="dark"
disabled
href="../scss/themes/dark.scss"
/>
<style>
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root" class="app-wrapper"></div>
<script src="./mod.js"></script>
<script type="text/javascript">
const themes = [
{
label: '默认主题',
ns: 'a-',
value: 'default'
},
{
label: '百度云舍',
ns: 'cxd-',
value: 'cxd'
},
{
label: 'Dark',
ns: 'dark-',
value: 'dark'
}
];
const theme = themes[localStorage.getItem('themeIndex') || 0];
if (theme.value !== 'default') {
document.querySelectorAll('link[title]').forEach(item => {
item.disabled = true;
});
document.querySelector(`link[title=${theme.value}]`).disabled = false;
if (theme.value === 'dark') {
document.querySelector('body').classList.add('dark');
}
}
amis.require(['./mobile.tsx'], function (app) {
var initialState = {};
app.bootstrap(document.getElementById('root'), initialState);
});
</script>
</body>
</html>

90
examples/mobile.tsx Normal file
View File

@ -0,0 +1,90 @@
/**
* @file postMessage
*/
import './polyfills/index';
import React from 'react';
import {render} from 'react-dom';
import axios from 'axios';
import copy from 'copy-to-clipboard';
import {toast} from '../src/components/Toast';
import '../src/locale/en';
import {render as renderAmis} from '../src/index';
class AMISComponent extends React.Component {
state = {
schema: null,
props: {}
};
constructor(props) {
super(props);
window.addEventListener('message', event => {
const data = event.data;
if (data && data.schema) {
this.setState({schema: data.schema, props: data.props});
}
});
window.parent.postMessage('amisReady', '*');
}
render() {
return this.state.schema ? (
<div>
{renderAmis(this.state.schema, this.state.props, {
fetcher: ({
url, // 接口地址
method, // 请求方法 get、post、put、delete
data, // 请求数据
responseType,
config, // 其他配置
headers // 请求头
}: any) => {
config = config || {};
config.withCredentials = true;
responseType && (config.responseType = responseType);
if (config.cancelExecutor) {
config.cancelToken = new (axios as any).CancelToken(
config.cancelExecutor
);
}
config.headers = headers || {};
if (method !== 'post' && method !== 'put' && method !== 'patch') {
if (data) {
config.params = data;
}
return (axios as any)[method](url, config);
} else if (data && data instanceof FormData) {
config.headers = config.headers || {};
config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data);
config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/json';
}
return (axios as any)[method](url, data, config);
},
isCancel: (value: any) => (axios as any).isCancel(value),
copy: content => {
copy(content);
toast.success('内容已复制到粘贴板');
}
})}
</div>
) : null;
}
}
export function bootstrap(mountTo, initalState) {
render(<AMISComponent />, mountTo);
}

View File

@ -928,3 +928,10 @@ a {
max-height: 70vh;
overflow: auto;
}
.mobile-frame {
border: 5px solid #565656;
display: block;
margin: 0 auto;
border-radius: 30px;
}