diff --git a/examples/components/App.tsx b/examples/components/App.tsx index 8136f54d9..d7d468875 100644 --- a/examples/components/App.tsx +++ b/examples/components/App.tsx @@ -14,7 +14,7 @@ import { } from 'amis'; import {eachTree} from 'amis-core'; import 'amis-ui/lib/locale/en-US'; -import {withRouter} from 'react-router'; +import {withRouter} from 'react-router-dom'; // @ts-ignore import DocSearch from './DocSearch'; import Doc from './Doc'; @@ -142,6 +142,8 @@ class BackTop extends React.PureComponent { } } +// @ts-ignore +@withRouter export class App extends React.PureComponent<{ location: Location; }> { diff --git a/examples/components/SchemaRender.jsx b/examples/components/SchemaRender.jsx index 88a10a1cb..0a66fd121 100644 --- a/examples/components/SchemaRender.jsx +++ b/examples/components/SchemaRender.jsx @@ -3,7 +3,7 @@ import {render, toast, makeTranslator, LazyComponent, Drawer} from 'amis'; import axios from 'axios'; import Portal from 'react-overlays/Portal'; import {normalizeLink} from 'amis-core'; -import {withRouter} from 'react-router'; +import {withRouter} from 'react-router-dom'; import copy from 'copy-to-clipboard'; import {qsparse, parseQuery, attachmentAdpator} from 'amis-core'; import isPlainObject from 'lodash/isPlainObject'; @@ -31,391 +31,395 @@ export default function (schema, schemaProps, showCode, envOverrides) { }; } - return class extends React.Component { - static displayName = 'SchemaRenderer'; - iframeRef; - state = {open: false, schema: {}}; - originalTitle = document.title; - toggleCode = () => - this.setState({ - open: !this.state.open - }); - copyCode = () => { - copy(JSON.stringify(schema, null, 2)); - toast.success('页面配置JSON已复制到粘贴板'); - }; - close = () => - this.setState({ - open: false - }); - constructor(props) { - super(props); + return withRouter( + class extends React.Component { + static displayName = 'SchemaRenderer'; + iframeRef; + state = {open: false, schema: {}}; + originalTitle = document.title; + toggleCode = () => + this.setState({ + open: !this.state.open + }); + copyCode = () => { + copy(JSON.stringify(schema, null, 2)); + toast.success('页面配置JSON已复制到粘贴板'); + }; + close = () => + this.setState({ + open: false + }); + constructor(props) { + super(props); - const __ = makeTranslator(props.locale); - const {history} = props; - this.env = { - updateLocation: (location, replace) => { - history[replace ? 'replace' : 'push'](normalizeLink(location)); - }, - jumpTo: (to, action) => { - if (to === 'goBack') { - return history.location.goBack(); - } - to = normalizeLink(to); - if (action && action.actionType === 'url') { - action.blank === false - ? (window.location.href = to) - : window.open(to); - return; - } - if (action && to && action.target) { - window.open(to, action.target); - return; - } - if (/^https?:\/\//.test(to)) { - window.location.replace(to); - } else { - history.push(to); - } - }, - isCurrentUrl: to => { - const history = this.props.history; - const link = normalizeLink(to); - const location = history.location; - let pathname = link; - let search = ''; - const idx = link.indexOf('?'); - if (~idx) { - pathname = link.substring(0, idx); - search = link.substring(idx); - } - - if (search) { - if (pathname !== location.pathname || !location.search) { - return false; + const __ = makeTranslator(props.locale); + const {history} = props; + this.env = { + updateLocation: (location, replace) => { + history[replace ? 'replace' : 'push'](normalizeLink(location)); + }, + jumpTo: (to, action) => { + if (to === 'goBack') { + return history.location.goBack(); } - const currentQuery = parseQuery(location); - const query = qsparse(search.substring(1)); - - return Object.keys(query).every( - key => query[key] === currentQuery[key] - ); - } else if (pathname === location.pathname) { - return true; - } - - return false; - }, - fetcher: async api => { - let {url, method, data, responseType, config, headers} = api; - config = config || {}; - config.url = url; - responseType && (config.responseType = responseType); - - if (config.cancelExecutor) { - config.cancelToken = new axios.CancelToken(config.cancelExecutor); - } - - config.headers = headers || {}; - config.method = method; - config.data = data; - - if (method === 'get' && data) { - config.params = data; - } else if (data && data instanceof FormData) { - // 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['Content-Type'] = 'application/json'; - } - - // 支持返回各种报错信息 - config.validateStatus = function () { - return true; - }; - - let response = await axios(config); - response = await attachmentAdpator(response, __, api); - - if (response.status >= 400) { - if (response.data) { - // 主要用于 raw: 模式下,后端自己校验登录, - if ( - response.status === 401 && - response.data.location && - response.data.location.startsWith('http') - ) { - location.href = response.data.location.replace( - '{{redirect}}', - encodeURIComponent(location.href) - ); - return new Promise(() => {}); - } else if (response.data.msg) { - throw new Error(response.data.msg); - } else { - throw new Error(JSON.stringify(response.data, null, 2)); - } + to = normalizeLink(to); + if (action && action.actionType === 'url') { + action.blank === false + ? (window.location.href = to) + : window.open(to); + return; + } + if (action && to && action.target) { + window.open(to, action.target); + return; + } + if (/^https?:\/\//.test(to)) { + window.location.replace(to); } else { - throw new Error(`${response.status}`); + history.push(to); + } + }, + isCurrentUrl: to => { + const history = this.props.history; + const link = normalizeLink(to); + const location = history.location; + let pathname = link; + let search = ''; + const idx = link.indexOf('?'); + if (~idx) { + pathname = link.substring(0, idx); + search = link.substring(idx); } - } - return response; - }, - isCancel: value => axios.isCancel(value), - copy: (content, options) => { - copy(content, options); - toast.success('内容已复制到粘贴板'); - }, - blockRouting: fn => { - return history.block(fn); - }, - tracker(eventTrack) { - console.debug('eventTrack', eventTrack); - }, - loadTinymcePlugin: async tinymce => { - // 参考:https://www.tiny.cloud/docs/advanced/creating-a-plugin/ - /* + if (search) { + if (pathname !== location.pathname || !location.search) { + return false; + } + const currentQuery = parseQuery(location); + const query = qsparse(search.substring(1)); + + return Object.keys(query).every( + key => query[key] === currentQuery[key] + ); + } else if (pathname === location.pathname) { + return true; + } + + return false; + }, + fetcher: async api => { + let {url, method, data, responseType, config, headers} = api; + config = config || {}; + config.url = url; + responseType && (config.responseType = responseType); + + if (config.cancelExecutor) { + config.cancelToken = new axios.CancelToken(config.cancelExecutor); + } + + config.headers = headers || {}; + config.method = method; + config.data = data; + + if (method === 'get' && data) { + config.params = data; + } else if (data && data instanceof FormData) { + // 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['Content-Type'] = 'application/json'; + } + + // 支持返回各种报错信息 + config.validateStatus = function () { + return true; + }; + + let response = await axios(config); + response = await attachmentAdpator(response, __, api); + + if (response.status >= 400) { + if (response.data) { + // 主要用于 raw: 模式下,后端自己校验登录, + if ( + response.status === 401 && + response.data.location && + response.data.location.startsWith('http') + ) { + location.href = response.data.location.replace( + '{{redirect}}', + encodeURIComponent(location.href) + ); + return new Promise(() => {}); + } else if (response.data.msg) { + throw new Error(response.data.msg); + } else { + throw new Error(JSON.stringify(response.data, null, 2)); + } + } else { + throw new Error(`${response.status}`); + } + } + + return response; + }, + isCancel: value => axios.isCancel(value), + copy: (content, options) => { + copy(content, options); + toast.success('内容已复制到粘贴板'); + }, + blockRouting: fn => { + return history.block(fn); + }, + tracker(eventTrack) { + console.debug('eventTrack', eventTrack); + }, + loadTinymcePlugin: async tinymce => { + // 参考:https://www.tiny.cloud/docs/advanced/creating-a-plugin/ + /* Note: We have included the plugin in the same JavaScript file as the TinyMCE instance for display purposes only. Tiny recommends not maintaining the plugin with the TinyMCE instance and using the `external_plugins` option. */ - tinymce.PluginManager.add('example', function (editor, url) { - var openDialog = function () { - return editor.windowManager.open({ - title: 'Example plugin', - body: { - type: 'panel', - items: [ - { - type: 'input', - name: 'title', - label: 'Title' - } - ] - }, - buttons: [ - { - type: 'cancel', - text: 'Close' + tinymce.PluginManager.add('example', function (editor, url) { + var openDialog = function () { + return editor.windowManager.open({ + title: 'Example plugin', + body: { + type: 'panel', + items: [ + { + type: 'input', + name: 'title', + label: 'Title' + } + ] }, - { - type: 'submit', - text: 'Save', - primary: true + buttons: [ + { + type: 'cancel', + text: 'Close' + }, + { + type: 'submit', + text: 'Save', + primary: true + } + ], + onSubmit: function (api) { + var data = api.getData(); + /* Insert content when the window form is submitted */ + editor.insertContent('Title: ' + data.title); + api.close(); } - ], - onSubmit: function (api) { - var data = api.getData(); - /* Insert content when the window form is submitted */ - editor.insertContent('Title: ' + data.title); - api.close(); + }); + }; + /* Add a button that opens a window */ + editor.ui.registry.addButton('example', { + text: 'My button', + onAction: function () { + /* Open window */ + openDialog(); } }); - }; - /* Add a button that opens a window */ - editor.ui.registry.addButton('example', { - text: 'My button', - onAction: function () { - /* Open window */ - openDialog(); - } + /* Adds a menu item, which can then be included in any menu via the menu/menubar configuration */ + editor.ui.registry.addMenuItem('example', { + text: 'Example plugin', + onAction: function () { + /* Open window */ + openDialog(); + } + }); + /* Return the metadata for the help plugin */ + return { + getMetadata: function () { + return { + name: 'Example plugin', + url: 'http://exampleplugindocsurl.com' + }; + } + }; }); - /* Adds a menu item, which can then be included in any menu via the menu/menubar configuration */ - editor.ui.registry.addMenuItem('example', { - text: 'Example plugin', - onAction: function () { - /* Open window */ - openDialog(); - } - }); - /* Return the metadata for the help plugin */ - return { - getMetadata: function () { - return { - name: 'Example plugin', - url: 'http://exampleplugindocsurl.com' - }; - } - }; - }); - }, - // 是否开启测试 testid - // enableTestid: true, - ...envOverrides - }; - - 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) { - let host = `${window.location.protocol}//${window.location.host}`; - - // 如果在 gh-pages 里面 - if (/^\/amis/.test(window.location.pathname)) { - host += '/amis'; - } - - const schemaUrl = `${host}/schema.json`; - - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - schemas: [ - { - uri: schemaUrl, - fileMatch: ['*'] - } - ], - validate: true, - enableSchemaRequest: true, - allowComments: true - }); - } - - renderCode() { - return ( - - ); - } - - 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: { - ...(isPlainObject(schemaProps) ? schemaProps : {}), - location: this.props.location, - theme: this.props.theme, - locale: this.props.locale - } }, - '*' - ); + // 是否开启测试 testid + // enableTestid: true, + ...envOverrides + }; + + this.handleEditorMount = this.handleEditorMount.bind(this); + + this.iframeRef = React.createRef(); + this.watchIframeReady = this.watchIframeReady.bind(this); + window.addEventListener('message', this.watchIframeReady, false); } - } - componentWillUnmount() { - this.props.setAsideFolded && this.props.setAsideFolded(false); - window.removeEventListener('message', this.watchIframeReady, false); - document.title = this.originalTitle; - } + handleEditorMount(editor, monaco) { + let host = `${window.location.protocol}//${window.location.host}`; - componentDidMount() { - if (schema.title) { - document.title = schema.title; + // 如果在 gh-pages 里面 + if (/^\/amis/.test(window.location.pathname)) { + host += '/amis'; + } + + const schemaUrl = `${host}/schema.json`; + + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + schemas: [ + { + uri: schemaUrl, + fileMatch: ['*'] + } + ], + validate: true, + enableSchemaRequest: true, + allowComments: true + }); } - } - renderSchema() { - const {location, theme, locale} = this.props; - - if (viewMode === 'mobile') { + renderCode() { return ( - + ); } - return render( - schema, - { - ...(isPlainObject(schemaProps) ? schemaProps : {}), - context: { - // 上下文信息,无论那层可以获取到这个 - amisUser: { - id: 1, - name: 'AMIS User' - } - }, - location, - theme, - locale - }, - this.env - ); - } + watchIframeReady(event) { + // iframe 里面的 amis 初始化了就可以发数据 + if (event.data && event.data === 'amisReady') { + this.updateIframe(); + } + } - render() { - const ns = this.props.classPrefix; - const finalShowCode = this.props.showCode ?? showCode; - return ( - <> -
+ updateIframe() { + if (this.iframeRef && this.iframeRef.current) { + this.iframeRef.current.contentWindow.postMessage( + { + schema: schema, + props: { + ...(isPlainObject(schemaProps) ? schemaProps : {}), + 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); + document.title = this.originalTitle; + } + + componentDidMount() { + if (schema.title) { + document.title = schema.title; + } + } + + renderSchema() { + const {location, theme, locale} = this.props; + + if (viewMode === 'mobile') { + return ( + + ); + } + + return render( + schema, + { + ...(isPlainObject(schemaProps) ? schemaProps : {}), + context: { + // 上下文信息,无论那层可以获取到这个 + amisUser: { + id: 1, + name: 'AMIS User' + } + }, + location, + theme, + locale + }, + this.env + ); + } + + render() { + const ns = this.props.classPrefix; + const finalShowCode = this.props.showCode ?? showCode; + return ( + <> +
+ {finalShowCode !== false ? ( + + {this.state.open ? this.renderCode() : null} + + ) : null} + {this.renderSchema()} +
{finalShowCode !== false ? ( - + //
+ // 查看页面配置 + //
+ //
+ // 复制页面配置 + //
+ //
+ document.getElementById('Header-toolbar')} > - {this.state.open ? this.renderCode() : null} - - ) : null} - {this.renderSchema()} - - {finalShowCode !== false ? ( - //
- //
- // 查看页面配置 - //
- //
- // 复制页面配置 - //
- //
- document.getElementById('Header-toolbar')}> -
-
-
- - - - ) : null} - - ); + + ) : null} + + ); + } } - }; + ); } diff --git a/packages/amis-editor-core/package.json b/packages/amis-editor-core/package.json index d0ad3dd71..3d4d3aca1 100644 --- a/packages/amis-editor-core/package.json +++ b/packages/amis-editor-core/package.json @@ -95,8 +95,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-overlays": "5.1.1", - "react-router": "5.2.0", - "react-router-dom": "5.2.0", + "react-router": "5.2.1", + "react-router-dom": "5.3.0", "rimraf": "^3.0.2", "rollup": "^2.73.0", "rollup-plugin-auto-external": "^2.0.0", diff --git a/packages/amis-editor/package.json b/packages/amis-editor/package.json index 56058e917..7ba42d1f7 100644 --- a/packages/amis-editor/package.json +++ b/packages/amis-editor/package.json @@ -81,8 +81,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-overlays": "5.1.1", - "react-router": "5.2.0", - "react-router-dom": "5.2.0", + "react-router": "5.2.1", + "react-router-dom": "5.3.0", "rimraf": "^3.0.2", "rollup": "^2.73.0", "rollup-plugin-auto-external": "^2.0.0", diff --git a/packages/amis/src/renderers/OfficeViewer.tsx b/packages/amis/src/renderers/OfficeViewer.tsx index 98cd763ef..004674789 100644 --- a/packages/amis/src/renderers/OfficeViewer.tsx +++ b/packages/amis/src/renderers/OfficeViewer.tsx @@ -166,12 +166,20 @@ export default class OfficeViewer extends React.Component< async fetchWord() { const {env, src, data, translate: __} = this.props; - const finalSrc = src + let finalSrc; + const resolveSrc = src ? resolveVariableAndFilter(src, data, '| raw') : undefined; - if (typeof finalSrc === 'string') { + if (typeof resolveSrc === 'string') { + finalSrc = resolveSrc; this.fileName = finalSrc.split('/').pop(); + } else if ( + typeof resolveSrc === 'object' && + typeof resolveSrc.value === 'string' + ) { + finalSrc = resolveSrc.value; + this.fileName = resolveSrc.name || finalSrc.split('/').pop(); } if (!finalSrc) { @@ -198,7 +206,6 @@ export default class OfficeViewer extends React.Component< this.rootElement.current.innerHTML = __('loadingFailed') + ' url:' + finalSrc; } - } finally { this.setState({ loading: false }); @@ -268,6 +275,9 @@ export default class OfficeViewer extends React.Component< } this.office = office; + this.setState({ + loading: false + }); }); } @@ -275,6 +285,10 @@ export default class OfficeViewer extends React.Component< * 渲染本地文件,用于预览 input-file */ renderFormFile() { + this.setState({ + loading: true + }); + const {wordOptions, name, data, display} = this.props; const file = data[name]; if (file instanceof File) { @@ -291,6 +305,9 @@ export default class OfficeViewer extends React.Component< this.rootElement.current.innerHTML = ''; } this.office = office; + this.setState({ + loading: false + }); }); }; reader.readAsArrayBuffer(file); diff --git a/packages/amis/src/renderers/PdfViewer.tsx b/packages/amis/src/renderers/PdfViewer.tsx index d5089528a..c3e24fe49 100644 --- a/packages/amis/src/renderers/PdfViewer.tsx +++ b/packages/amis/src/renderers/PdfViewer.tsx @@ -74,9 +74,15 @@ export default class PdfViewer extends React.Component< componentDidUpdate(prevProps: PdfViewerProps) { const props = this.props; - if (isApiOutdated(prevProps.src, props.src, prevProps.data, props.data)) { + if ( + isApiOutdated(prevProps.src, props.src, prevProps.data, props.data) || + resolveVariableAndFilter(props.src, props.data, '| raw') !== + resolveVariableAndFilter(prevProps.src, prevProps.data, '| raw') + ) { this.abortLoad(); - this.fetchPdf(); + setTimeout(() => { + this.fetchPdf(); + }, 0); } if (getVariable(props.data, props.name)) { @@ -123,9 +129,19 @@ export default class PdfViewer extends React.Component< @autobind async fetchPdf() { const {env, src, data, translate: __} = this.props; - const finalSrc = src - ? resolveVariableAndFilter(src, data, '| raw') - : undefined; + let finalSrc; + + if (src) { + const resolveSrc = resolveVariableAndFilter(src, data, '| raw'); + if (typeof resolveSrc === 'string') { + finalSrc = resolveSrc; + } else if ( + typeof resolveSrc === 'object' && + typeof resolveSrc.value === 'string' + ) { + finalSrc = resolveSrc.value; + } + } if (!finalSrc) { console.warn('file src is empty'); @@ -134,7 +150,8 @@ export default class PdfViewer extends React.Component< this.setState({ inited: true, - loading: true + loading: true, + error: false }); try {