diff --git a/src/index.tsx b/src/index.tsx index 2d0888287cc3cc17163164deea923e76e03b1cd3..28dbfa5ec8109b4980df0703bd1bdf3ec0bca0e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -42,4 +42,8 @@ export { default as DetailStep } from './steps/detail' export { default as DetailGroupField } from './components/detail/group' export { default as DetailTextField } from './components/detail/text' -export { default as InterfaceHelper } from "./util/interface" \ No newline at end of file +export { default as HeaderStep } from './steps/header' + +export { default as InterfaceHelper } from "./util/interface" + +export { default as OperationHelper } from "./util/operation" \ No newline at end of file diff --git a/src/steps/header/index.tsx b/src/steps/header/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3bca8d06e8e05ab0509d9cc0fb09504efadd2901 --- /dev/null +++ b/src/steps/header/index.tsx @@ -0,0 +1,375 @@ +import React, { ReactNode } from 'react' +import Step, { StepConfig } from '../common' +import { ParamConfig } from '../../interface' +import StatementHelper, { StatementConfig } from '../../util/statement' +import marked from 'marked' +import ParamHelper from '../../util/param' +import EnumerationHelper, { EnumerationOptionsConfig } from '../../util/enumeration' +import { InterfaceHelper } from '../..' +import OperationHelper, { OperationConfig } from '../../util/operation' +import { DetailFieldConfigs } from '../../components/detail' +import DetailStep, { DetailConfig } from '../detail' +import { merge } from 'lodash' + +/** + * header页头配置文件格式定义 + * - breadcrumb: 面包屑内容定义 + * - title: 主标题 + * - subTitle: 副标题 + * - back: 返回按钮 + * -- enable: 启用 + * - mainContent: 主内容 + * - extraContent: 扩展内容 + */ +export interface HeaderConfig extends StepConfig { + type: 'header' + breadcrumb?: { + enable?: boolean + separator?: string + items?: breadcrumbConfig[] + }, + title?: StatementConfig, + subTitle?: StatementConfig + back?: { + enable?: boolean + }, + mainContent?: plainContentConfig | markdownContentConfig | htmlContentConfig | detailContentConfig | statisticContentConfig + extraContent?: statisticContentConfig | imageContentConfig +} + +/** + * 面包屑内容 + */ +export interface breadcrumbConfig { + label?: string + type?: 'normal' | 'bold' + action?: OperationConfig +} + +interface basicContentConfig { + enable?: boolean +} + +export interface plainContentConfig extends basicContentConfig { + type: 'plain' + content?: string + params?: { field: string, data: ParamConfig }[] +} +export interface markdownContentConfig extends basicContentConfig { + type: 'markdown' + content?: string + params?: { field: string, data: ParamConfig }[] +} +export interface htmlContentConfig extends basicContentConfig { + type: 'html' + content?: string + params?: { field: string, data: ParamConfig }[] +} +export interface detailContentConfig extends basicContentConfig { + type: 'detail' + fields?: DetailFieldConfigs[] + defaultValue?: ParamConfig +} +export interface statisticContentConfig extends basicContentConfig { + type: 'statistic' + statistics?: (valueStatisticConfig | enumerationStatisticConfig)[] +} + + +interface basicStatisticConfig { + label?: string + value?: ParamConfig +} + +export interface valueStatisticConfig extends basicStatisticConfig { + type: 'value' +} + +export interface enumerationStatisticConfig extends basicStatisticConfig { + type: 'enumeration' + options?: EnumerationOptionsConfig +} +export interface imageContentConfig extends basicContentConfig { + type: 'image' + image?: { + maxWidth?: string + maxHeight?: string + src?: string + } +} + +/** + * 面包屑元素config + * - label: 文案 + * - type: 展示形式 + * - onClick: 点击事件 + */ +export interface IBreadcurmbItemProps { + label: string + type: 'normal' | 'bold' + onClick: () => void +} + +/** + * 面包屑内容config + * - items: 面包屑内容 + * - separator: 分割符 + */ +export interface IBreadcurmbProps { + items: Array + separator: string +} + +/** + * 绘制统计内容config + * - label: 统计标题 + * - value: 统计数值 + */ +export interface IStatisticProps { + label: string + value: string | number +} + +/** + * 头部组件config + * - breadcrumb:面包屑内容 + * - title: 主标题 + * - subTitle: 副标题 + * - onBack: 返回按钮点击事件 + * - mainContent: 主内容 + * - extraContent: 扩展内容 + */ +export interface IHeaderProps { + breadcrumb?: ReactNode + title?: string + subTitle?: string + onBack?: () => void + mainContent?: ReactNode + extraContent?: ReactNode +} + +export default class HeaderStep extends Step { + interfaceHelper = new InterfaceHelper() + OperationHelper = OperationHelper + DetailStep = DetailStep + + stepPush = async () => { + // 表单初始化结束,展示表单界面。 + if (this.props.onMount) { + this.props.onMount() + } + if (this.props.onSubmit) { + this.props.onSubmit({}) + } + } + + /** + * 用于展示面包屑元素 + * @param props + * @returns + */ + renderBreadcurmbItemComponent = (props: IBreadcurmbItemProps) => { + return + 您当前使用的UI版本没有实现HeaderStep组件的renderBreadcurmbItemComponent方法。 + + } + + /** + * 用于展示面包屑 + * @param props + * @returns + */ + renderBreadcurmbComponent = (props: IBreadcurmbProps) => { + return + 您当前使用的UI版本没有实现HeaderStep组件的renderBreadcurmbComponent方法。 + + } + + /** + * 用于展示统计内容 + * @param props + * @returns + */ + renderStatisticComponent = (props: IStatisticProps) => { + return + 您当前使用的UI版本没有实现HeaderStep组件的renderStatisticComponent方法。 + + } + + renderComponent = (props: IHeaderProps) => { + return + 您当前使用的UI版本没有实现HeaderStep组件。 + + } + + handlePlainContent = (config: plainContentConfig, _position: string) => { + return StatementHelper({ statement: config.content || '', params: config.params || [] }, { data: this.props.data, step: this.props.step }) + } + + handleMarkdownContent = (config: markdownContentConfig, _position: string) => { + const content = StatementHelper({ statement: config.content || '', params: config.params || [] }, { data: this.props.data, step: this.props.step }) + return
+ } + + handleHTMLContent = (config: htmlContentConfig, _position: string) => { + const content = StatementHelper({ statement: config.content || '', params: config.params || [] }, { data: this.props.data, step: this.props.step }) + return
+ } + + handleDetailContent = (config: detailContentConfig, _position: string) => { + const defaultConfig: DetailConfig = { + type: 'detail', + hiddenBack: true + } + return ( + | null) => { e && e.stepPush() }} + data={this.props.data} + step={this.props.step} + onSubmit={async (data, unmountView) => {}} + onMount={async () => {}} + onUnmount={async (reload = false, data) => {}} + config={merge(config, defaultConfig)} + baseRoute={this.props.baseRoute} + checkPageAuth={this.props.checkPageAuth} + loadPageURL={this.props.loadPageURL} + loadPageFrameURL={this.props.loadPageFrameURL} + loadPageConfig={this.props.loadPageConfig} + loadDomain={this.props.loadDomain} + /> + ) + } + + handleStatisticContent = (config: statisticContentConfig, _position: string) => { + return (config.statistics || []).map((statistic, index) => { + const value = statistic.value ? ParamHelper(statistic.value, { data: this.props.data, step: this.props.step }) : undefined + switch (statistic.type) { + case 'value': + return this.renderStatisticComponent({ + label: statistic.label || '', + value + }) + case 'enumeration': + if (statistic.options) { + EnumerationHelper.options(statistic.options, (config, source) => this.interfaceHelper.request(config, source, { data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain })).then((options) => { + if (!this.state || JSON.stringify(this.state[`statistic_options_${_position}_${index}`]) !== JSON.stringify(options)) { + this.setState({ + [`statistic_options_${_position}_${index}`]: options + }) + } + }) + + const options: { value: any, label: any }[] = this.state && this.state[`statistic_options_${_position}_${index}`] + if (options) { + const option = options.find((option) => option.value === value) + if (option) { + return this.renderStatisticComponent({ + label: statistic.label || '', + value: option.label + }) + } + } + } + return this.renderStatisticComponent({ + label: statistic.label || '', + value + }) + default: + return null + } + }) + } + + handleImageContent = (config: imageContentConfig, _position: string) => { + if (config.image && config.image.src) { + return + } else { + return null + } + } + + render () { + const props: IHeaderProps = {} + + if (this.props.config.breadcrumb && this.props.config.breadcrumb.enable) { + const breadcrumbConfig = this.props.config.breadcrumb + props.breadcrumb = this.renderBreadcurmbComponent({ + items: (breadcrumbConfig.items || []).map((breadcrumbItem) => ( + + {(onClick) => ( + this.renderBreadcurmbItemComponent({ + label: breadcrumbItem.label || '', + type: breadcrumbItem.type || 'normal', + onClick + }) + )} + + )), + separator: breadcrumbConfig.separator || '>' + }) + } + + if (this.props.config.title) { + props.title = StatementHelper(this.props.config.title, { data: this.props.data, step: this.props.step }) + } + + if (this.props.config.subTitle) { + props.subTitle = StatementHelper(this.props.config.subTitle, { data: this.props.data, step: this.props.step }) + } + + if (this.props.config.back && this.props.config.back.enable) { + props.onBack = () => { this.props.onUnmount() } + } + + if (this.props.config.mainContent && this.props.config.mainContent.enable) { + const mainContent = this.props.config.mainContent + switch (mainContent.type) { + case 'plain': + props.mainContent = this.handlePlainContent(mainContent, 'main') + break; + case 'markdown': + props.mainContent = this.handleMarkdownContent(mainContent, 'main') + break; + case 'html': + props.mainContent = this.handleHTMLContent(mainContent, 'main') + break; + case 'detail': + props.mainContent = this.handleDetailContent(mainContent, 'main') + break; + case 'statistic': + props.mainContent = this.handleStatisticContent(mainContent, 'main') + default: + break; + } + } + + if (this.props.config.extraContent && this.props.config.extraContent.enable) { + const extraContent = this.props.config.extraContent + switch (extraContent.type) { + case 'statistic': + props.extraContent = this.handleStatisticContent(extraContent, 'extra') + break; + case 'image': + props.extraContent = this.handleImageContent(extraContent, 'extra') + break; + default: + break; + } + } + + return ( + + {this.renderComponent(props)} + + ) + } +} diff --git a/src/steps/index.tsx b/src/steps/index.tsx index 99e62033a37499d6683ca879545b279b385b9cc5..7b8ae7fddb19c36f937e5b8f6dbf2cbc4f4ada4c 100644 --- a/src/steps/index.tsx +++ b/src/steps/index.tsx @@ -1,11 +1,12 @@ import FetchStep, { FetchConfig } from './fetch' import FilterStep, { FilterConfig } from './filter' import FormStep, { FormConfig } from './form' +import HeaderStep, { HeaderConfig } from './header' import SkipStep, { SkipConfig } from './skip' import TableStep, { TableConfig } from './table' import DetailStep, { DetailConfig } from './detail' -export type StepConfigs = FetchConfig | FormConfig | SkipConfig | TableConfig | FilterConfig | DetailConfig +export type StepConfigs = FetchConfig | FormConfig | SkipConfig | TableConfig | FilterConfig | DetailConfig | HeaderConfig export default { fetch: FetchStep, @@ -13,5 +14,6 @@ export default { skip: SkipStep, table: TableStep, filter: FilterStep, - detail: DetailStep + detail: DetailStep, + header: HeaderStep } diff --git a/src/util/enumeration.ts b/src/util/enumeration.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b3c41d09c39b7e18f7678360c86e0a0251de4a7 --- /dev/null +++ b/src/util/enumeration.ts @@ -0,0 +1,77 @@ +import { InterfaceConfig } from "./interface"; +import { getValue } from "./value"; + +export type EnumerationOptionsConfig = ManualEnumerationOptionsConfig | InterfaceEnumerationOptionsConfig + +interface ManualEnumerationOptionsConfig { + from: 'manual' + data?: Array<{ + value: string | number | boolean + label: string + [extra: string]: any + }> +} + +interface InterfaceEnumerationOptionsConfig { + from: 'interface' + interface?: InterfaceConfig + format?: InterfaceEnumerationOptionsKVConfig | InterfaceEnumerationOptionsListConfig +} + +interface InterfaceEnumerationOptionsKVConfig { + type: 'kv' +} + +interface InterfaceEnumerationOptionsListConfig { + type: 'list' + keyField: string + labelField: string +} + +export default class EnumerationHelper { + static _instance: EnumerationHelper + + public async options (config: EnumerationOptionsConfig, interfaceRequire: (config: InterfaceConfig, source: any) => Promise) { + if (config) { + if (config.from === 'manual') { + if (config.data) { + return config.data.map((option) => { + return { + value: option.value, + label: option.label + } + }) + } + } else if (config.from === 'interface') { + if (config.interface) { + const data = await interfaceRequire(config.interface, {}) + if (config.format) { + if (config.format.type === 'kv') { + return Object.keys(data).map((key) => ({ + value: key, + label: data[key] + })) + } else if (config.format.type === 'list') { + return data.map((item: any) => { + if (config.format && config.format.type === 'list') { + return ({ + value: getValue(item, config.format.keyField), + label: getValue(item, config.format.labelField) + }) + } + }) + } + } + } + } + } + return [] + } + + static async options (config: EnumerationOptionsConfig, interfaceRequire: (config: InterfaceConfig, source: any) => Promise) { + if (!EnumerationHelper._instance) { + EnumerationHelper._instance = new EnumerationHelper() + } + return await EnumerationHelper._instance.options(config, interfaceRequire) + } +} \ No newline at end of file diff --git a/src/util/operation.tsx b/src/util/operation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..675f4d7de88fd8321300de8f0138941eeab4afc3 --- /dev/null +++ b/src/util/operation.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import queryString from 'query-string'; +import { set } from "lodash"; +import { ParamConfig } from "../interface"; +import { CCMSConfig, CCMSProps } from "../main"; +import { getParam } from "./value"; + +export type OperationConfig = CCMSOperationConfig + +export interface IOperationModal { + title: string + content: React.ReactNode + onClose: () => void +} + +/** CCMS内部操作 */ +interface _CCMSOperationConfig { + /** 操作类型:CCMS内部操作 */ + type: 'ccms' + + /** 目标资源 */ + page: any + + /** 参数 */ + data: { [key: string]: ParamConfig } +} + +/** CCMS模态窗口操作 */ +interface CCMSPopupOperationConfig extends _CCMSOperationConfig { + mode: 'popup' + label: string +} + +/** CCMS重定向操作 */ +interface CCMSRedirectOperationConfig extends _CCMSOperationConfig { + mode: 'redirect' +} + +/** CCMS新标签页操作 */ +interface CCMSWindowOperationConfig extends _CCMSOperationConfig { + mode: 'window' +} + +/** CCMS无界面操作 */ +interface CCMSInvisibleOperationConfig extends _CCMSOperationConfig { + mode: 'invisible' +} + +type CCMSOperationConfig = CCMSPopupOperationConfig | CCMSRedirectOperationConfig | CCMSWindowOperationConfig | CCMSInvisibleOperationConfig + +interface OperationHelperProps { + config?: OperationConfig, + datas: { record?: object, data: object[], step: number }, + checkPageAuth: (pageID: any) => Promise, + loadPageURL: (pageID: any) => Promise, + loadPageFrameURL: (pageID: any) => Promise, + loadPageConfig: (pageID: any) => Promise, + baseRoute: string, + loadDomain: (domain: string) => Promise + + children?: (handleOperation: () => void) => React.ReactNode + callback?: () => void +} + +interface OperationHelperState { + operationConfig: CCMSConfig | null + sourceData?: any +} + +export default class OperationHelper extends React.Component { + constructor (props: OperationHelperProps) { + super(props) + + this.state = { + operationConfig: null + } + } + + protected renderModal (props: IOperationModal) { + return + 您当前使用的UI版本没有实现OpertionHelper组件。 + + } + + protected renderCCMS (props: CCMSProps) { + return + 您当前使用的UI版本没有实现OpertionHelper组件。 + + } + + private handleCCMS (config: CCMSOperationConfig) { + const { + datas, + loadPageURL, + loadPageFrameURL, + loadPageConfig, + } = this.props + return async () => { + if (config.type === 'ccms') { + const sourceData = {} + for (const [field, param] of Object.entries(config.data || {})) { + set(sourceData, field, getParam(param, datas)) + } + if (config.mode === 'popup' || config.mode === 'invisible') { + const operationConfig = await loadPageConfig(config.page) + this.setState({ + operationConfig, + sourceData + }) + } else if (config.mode === 'redirect') { + const sourceURL = await loadPageURL(config.page) + const { url, query } = queryString.parseUrl(sourceURL, { arrayFormat: 'bracket' }) + window.location.href = queryString.stringifyUrl({ url, query: { ...query, ...sourceData } }, { arrayFormat: 'bracket' }) || '' + } else if (config.mode === 'window') { + const sourceURL = await loadPageFrameURL(config.page) + const { url, query } = queryString.parseUrl(sourceURL, { arrayFormat: 'bracket' }) + window.open(queryString.stringifyUrl({ url, query: { ...query, ...sourceData } }, { arrayFormat: 'bracket' }) || '') + } + } + } + } + + + render () { + if (this.props.config) { + return ( + + {this.props.children && this.props.children(this.handleCCMS(this.props.config))} + {this.state.operationConfig !== null && this.props.config.mode === 'popup' && ( + this.renderModal({ + title: this.props.config.label, + content: this.renderCCMS({ + config: this.state.operationConfig, + sourceData: this.state.sourceData, + baseRoute: this.props.baseRoute, + checkPageAuth: this.props.checkPageAuth, + loadPageURL: this.props.loadPageURL, + loadPageFrameURL: this.props.loadPageFrameURL, + loadPageConfig: this.props.loadPageConfig, + loadDomain: this.props.loadDomain, + callback: () => { + this.setState({ + operationConfig: null, + sourceData: null + }) + this.props.callback && this.props.callback() + } + }), + onClose: () => { + this.setState({ + operationConfig: null, + sourceData: null + }) + this.props.callback && this.props.callback() + } + }) + )} + {this.state.operationConfig !== null && this.props.config.mode === 'invisible' && ( + this.renderCCMS({ + config: this.state.operationConfig, + sourceData: this.state.sourceData, + baseRoute: this.props.baseRoute, + checkPageAuth: this.props.checkPageAuth, + loadPageURL: this.props.loadPageURL, + loadPageFrameURL: this.props.loadPageFrameURL, + loadPageConfig: this.props.loadPageConfig, + loadDomain: this.props.loadDomain, + callback: () => { + this.setState({ + operationConfig: null, + sourceData: null + }) + this.props.callback && this.props.callback() + } + }) + )} + + ) + } else { + return ( + + {this.props.children && this.props.children(() => {})} + + ) + } + } +} \ No newline at end of file diff --git a/src/util/statement.ts b/src/util/statement.ts index cdfa24b1cf74779b9480c3368126bf9a453fcd63..81ea1e5820eaebf01fc0b0877b0a04948cb4f65e 100644 --- a/src/util/statement.ts +++ b/src/util/statement.ts @@ -33,4 +33,4 @@ export default function StatementHelper(config: StatementConfig | undefined, dat return '' } } -} +} \ No newline at end of file