UNPKG

mutants-appfx

Version:

appfx module

703 lines (618 loc) 17.9 kB
import React from 'react'; import ReactDom from 'react-dom'; import PropTypes from 'prop-types' import { api } from 'mutants-microfx'; import { action, observable, computed, runInAction, when, reaction, toJS } from 'mobx'; import { observer } from 'mobx-react'; import { Form, Row, Col, Input, Button, Icon, Modal, message, InputNumber, Table, } from 'antd'; import { Action, TemplateComponent, ViewBase, VoucherViewController, VoucherController, BizError, registerViewModel, registerController, registerDataProvider, injectViewModel, eventPath } from './src'; const ButtonGroup = Button.Group; // 说明:视图模型根据端的交互不同所以定义不同、并且需要表达式解决复杂度问题 // 数据模型就是简单的数据结构,业务主要控制数据 // ====================== viewmodel =============== class OperationItem extends ViewBase { @observable textExpr; @observable iconExpr; @observable disabledExpr; @observable visibleExpr; @observable onClick; @computed get text() { return this.store.execExpr(this.textExpr); } @computed get icon() { return this.store.execExpr(this.iconExpr); } @computed get disabled() { return this.store.execExpr(this.disabledExpr); } @computed get visible() { return this.store.execExpr(this.visibleExpr); } constructor(store, name, textExpr, iconExpr, disabledExpr = false, visibleExpr = true, onClick) { super(store, name); this.textExpr = store.parseExpr(textExpr); this.iconExpr = store.parseExpr(iconExpr); this.disabledExpr = store.parseExpr(disabledExpr); this.visibleExpr = store.parseExpr(visibleExpr); this.onClick = onClick; } static fromJS(store, object) { return new OperationItem(store, object.name, object.text, object.icon, object.disabled, object.visible, Action.fromJS(store, object.onClick)); } } class OperationSet extends ViewBase { @observable allitems; @computed get items() { return this.allitems.filter(it => it.visible); } constructor(store, name, items = []) { super(store, name); this.allitems = items; } static fromJS(store, { operations }) { return new OperationSet(store, operations.name, operations.items && operations.items.map(it => OperationItem.fromJS(store, it))); } } class ColumnItem extends ViewBase { @observable labelExpr; @observable valueExpr; @observable disabledExpr; @observable visibleExpr; @observable typeExpr; @computed get label() { return this.store.execExpr(this.labelExpr); } @computed get disabled() { return !!this.store.execExpr(this.disabledExpr); } @computed get visible() { return !!this.store.execExpr(this.visibleExpr); } @computed get valueKey() { return this.store.execExpr(this.valueExpr); } @computed get value() { return this.store.data && this.store.data.getValue(this.valueKey); } @computed get type() { return this.store.execExpr(this.typeExpr); } constructor(store, name, labelExpr, valueExpr, disabledExpr, visibleExpr, typeExpr) { super(store, name); this.labelExpr = store.parseExpr(labelExpr); this.valueExpr = store.parseExpr(valueExpr); this.disabledExpr = store.parseExpr(disabledExpr || false); this.visibleExpr = store.parseExpr(visibleExpr || true); this.typeExpr = store.parseExpr(typeExpr); } static fromJS(store, object) { return new ColumnItem(store, object.name, object.label, object.value, object.disabled, object.visible, object.type); } } class ColumnSet extends ViewBase { @observable allitems; @observable visibleExpr; @computed get visible() { return !!this.store.execExpr(this.visibleExpr); } @computed get items() { return this.allitems.filter(it => it.visible); } constructor(store, name, visibleExpr, items = []) { super(store, name); this.visibleExpr = store.parseExpr(visibleExpr || true); this.allitems = items; } static fromJS(store, obj) { return new ColumnSet(store, obj.name, obj.visible, (obj.items || []).map(it => ColumnItem.fromJS(store, it))); } } class VoucherModel extends ViewBase { @observable allitems; @observable onLoad; @observable dataSouceField; @observable table; @observable focusCells = []; @computed get dataSource() { return this.store.data[this.dataSouceField]; } @computed get groups() { return this.allitems.filter(it => it.visible); } constructor(store, name, items = [], onLoad, table, dataSouceField) { super(store, name); this.allitems = items; this.onLoad = onLoad; this.table = table; this.dataSouceField = dataSouceField; } @action appendTable() { this.store.data.pushValue(this.dataSouceField, {}); } @action handleChange(column, value, rowIndex) { this.dataSource[rowIndex][column.valueKey] = value; } @action focusout(i, fieldName) { this.focusCells.remove(i + '_' + fieldName); } // uicontroller的实现 @action async triggerFieldInput(fieldNames, row) { const column = this.table.find(it => fieldNames.indexOf(it.valueKey) > -1); if (!column) { return; } await new Promise((resolve) => { this.focusCells.push(this.dataSource.indexOf(row) + '_' + column.name); // 失去焦点(录入完毕)后再进行其他业务计算 when(() => this.focusCells.indexOf(this.dataSource.indexOf(row) + '_' + column.name) === -1, () => { resolve(true); }); }); } // uicontroller的实现 @action async triggerFeedback(content, type) { if (type === 'confirm') { return new Promise((resolve) => { Modal.confirm({ content, onOk: () => { resolve(true); }, onCancel: () => { resolve(false); }, }); }); } const method = message[type]; if (method) { method(content); return true; } } static fromJS(store, { voucher = {} }) { const table = voucher.table || {}; return new VoucherModel(store, voucher.name, (voucher.tabs || voucher.items || []).map(it => ColumnSet.fromJS(store, it)), // 单据加载时行为 Action.fromJS(store, voucher.onLoad || '$id?"load":"create"'), (table.columns || []).map(it => ColumnItem.fromJS(store, it)), table.dataSouceField); } } class VoucherListModel extends ViewBase { @observable table; @observable pageIndex = 0; @observable pageSize = 10; @computed get dataSource() { return this.store.list.items; } @action nextPage() { this.pageIndex++; } @action prePage() { if (this.pageIndex > 1) { this.pageIndex--; } } @action changePageSize(size) { this.pageSize = size; } constructor(store, table) { super(store, name); this.table = table; this.store.list.load(this.pageIndex, this.pageSize); } static fromJS(store, { columns = [] }) { return new VoucherListModel(store, (columns || []).map(it => ColumnItem.fromJS(store, it))); } } // ====================== view ======================== @injectViewModel('OperationSet') @observer class Toolbar extends TemplateComponent { handleClick = (it) => { this.context.onEvent(it, 'onClick'); } render() { const viewModel = this.props.viewModel; return (<ButtonGroup> {viewModel.items.map(it=><Button key={it.key} disabled={it.disabled} onClick={()=>this.handleClick(it)}> {it.icon?<Icon type={it.icon} />:null} {it.text} </Button>)} </ButtonGroup>) } } @Form.create() @observer class VoucherForm extends TemplateComponent { static propTypes = { form: PropTypes.object, } state = { expand: false, }; constructor(props, context) { super(); const viewModel = context.viewModel.items.find(it => it.type === 'VoucherModel'); context.onEvent(viewModel, 'onLoad'); reaction(() => viewModel.focusCells[viewModel.focusCells.length - 1], cell => { setTimeout(() => { if (this[cell]) { const node = ReactDom.findDOMNode(this[cell]).querySelector('input'); node.focus(); } }, 0); }) } getFields() { const count = this.state.expand ? 10 : 6; const { getFieldDecorator } = this.props.form; const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherModel'); const children = []; for (let k = 0; k < viewModel.groups.length; k++) { const group = viewModel.groups[k]; for (let i = 0; i < group.items.length; i++) { const it = group.items[i]; children.push( <Col span={8} key={it.key} style={{ display: i < count ? 'block' : 'none' }}> <Form.Item label={it.label}> {getFieldDecorator(it.name, { initialValue: it.value, rules: [{ // required: true, message: 'Input something!', }], })( <Input ref={ref=>this[it.name]=ref} disabled={it.disabled} placeholder="placeholder" /> )} </Form.Item> </Col> ); } } return children; } handleSubmit = (e) => { e.preventDefault(); this.props.form.validateFields((err, values) => { if (err) { message.error(err.message); return; } const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherModel'); this.context.onEvent(viewModel, 'onSubmit', values); }); } handleReset = () => { this.props.form.resetFields(); } toggle = () => { const { expand } = this.state; this.setState({ expand: !expand }); } renderInput = (text, i, it, viewModel) => { if (it.type === 'numberinput') { return <InputNumber autoFocus={viewModel.focusCells[i+'_'+it.name]} ref={ref=>this[i+'_'+it.name] = ref} value={text} onChange={(value)=>viewModel.handleChange(it, value,i)} onBlur={()=>viewModel.focusout(i,it.name)}/> } return <Input autoFocus={viewModel.focusCells[i+'_'+it.name]} ref={ref=>this[i+'_'+it.name] = ref} value={text} onChange={(e)=>viewModel.handleChange(it,e.target.value,i)} onBlur={()=>viewModel.focusout(i,it.name)}/> } render() { // <Button type="primary" htmlType="submit">Save</Button> // <Button style={{ marginLeft: 8 }} onClick={this.handleReset}> // Clear // </Button> const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherModel'); return (<Form onSubmit={this.handleSubmit}> <Row gutter={24}>{this.getFields()}</Row> <Row> <Col span={24} style={{ textAlign: 'right' }}> <a style={{ marginLeft: 8, fontSize: 12 }} onClick={this.toggle}> Collapse <Icon type={this.state.expand ? 'up' : 'down'} /> </a> </Col> </Row> <Row> <ButtonGroup> <Button size='small' onClick={()=>viewModel.appendTable()}>Add</Button> </ButtonGroup> </Row> <Row> <Table dataSource={viewModel.dataSource.slice().map(it=>(toJS(it)))} columns={viewModel.table.map(it=>({ title: it.label, dataIndex: it.valueKey, key: it.key, render: (text, record, i)=>this.renderInput(text,i,it,viewModel) }))} /> </Row> </Form>) } } class VoucherPage extends TemplateComponent { componentDidMount() { const viewModel = this.context.viewModel.getViewItem('VoucherModel'); this.context.onEvent(viewModel, 'voucher.edit.loaded', { a: new Date().getTime() }) } render() { return (<div> <Toolbar/> <VoucherForm/> </div>) } } class VoucherViewPage extends TemplateComponent { componentDidMount() { const viewModel = this.context.viewModel.getViewItem('VoucherModel'); this.context.onEvent(viewModel, 'voucher.readlonly.loaded', { b: 'xxxxxxx' }) } getFields() { const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherModel'); const children = []; for (let k = 0; k < viewModel.groups.length; k++) { const group = viewModel.groups[k]; for (let i = 0; i < group.items.length; i++) { const it = group.items[i]; children.push( <Col span={8} key={it.key} > <label>{it.label}:<span>{it.value}</span></label> </Col> ); } } return children; } render() { const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherModel'); return (<div> <Toolbar/> <Row gutter={24}>{this.getFields()}</Row> <Row> <Table dataSource={viewModel.dataSource.slice().map(it=>(toJS(it)))} columns={viewModel.table.map(it=>({ title: it.label, dataIndex: it.valueKey, key: it.key, }))} /> </Row> </div>) } } class VoucherListPage extends TemplateComponent { render() { const viewModel = this.context.viewModel.items.find(it => it.type === 'VoucherListModel'); return (<div> <Toolbar/> <Row> <Table dataSource={viewModel.dataSource.slice().map(it=>(toJS(it)))} columns={viewModel.table.map(it=>({ title: it.label, dataIndex: it.valueKey, key: it.key, }))} /> </Row> </div>) } } class VoucherDraftPage extends TemplateComponent { render() { return (<div> VoucherDraftPage </div>) } } class VoucherSelectorPage extends TemplateComponent { render() { return (<div> VoucherSelectorPage </div>) } } // 第一个是view,后面的都是viewmodel,view和vm是有关联关系 //registerViewModel('VoucherView', VoucherViewPage, VoucherModel, OperationSet); registerViewModel('Voucher', { index: VoucherPage, // addnew: 'Voucher', // edit: 'Voucher', readonly: { type:VoucherViewPage, mode: 'multi' }, }, VoucherModel, OperationSet); registerViewModel('VoucherList', VoucherListPage, VoucherListModel, OperationSet); registerViewModel('VoucherDraft', VoucherDraftPage, VoucherListModel); registerViewModel('VoucherSelector', VoucherSelectorPage, VoucherListModel); // 模块的不同模式可以配置多个显示模板 // ================ biz logic ====================== // 视图无关的业务逻辑,跨端可以复用 class RetailOrderController extends VoucherController { // 一、非行为产生的业务规则,比如金额等联动计算 @action async init() { reaction(() => this.data.detail.reduce((r, it) => r + it.price * it.count, 0), (total) => { this.data.total = total; }); reaction(() => this.data.detail.filter(it => !it.price), (details) => { this.checkPrice(details).then(); }); } // 二、基于行为的业务规则 // 新增 @action async create() { await super.create(); runInAction(() => { this.data.code = 'RE' + new Date().getTime() }) } @action async save() { // 行为校验规则 if (!this.data.name) { throw new BizError('名称不能为空'); } super.save(); } // 三、 演示一种业务计算过程需要UI交互的场景,比如保存时提示现存量不足需要用户参与控制交互 async checkPrice(details) { // 模拟价格策略 for (const it of details) { if (this.data.detail.indexOf(it) % 2 === 0) { it.price = 100; } } // 要是没有需要用户自己输入 for (const it of details) { await this.ui.input('price', it); } // 要是用户没有输入提示 for (const it of details) { if (!it.price) { runInAction(() => { this.data.detail.remove(it); }) await this.ui.feedback('已删除'); } } } @action async testSnapshot() { this.data.name = 'testSnapshot'; this.data.pushValue('detail', { price: 100, count: 20 }); this.data.pushValue('detail', { price: 88, count: 1 }); throw new BizError('回滚业务数据'); } @eventPath('voucher.edit.loaded') async dosomething({ a }) { await this.ui.feedback('voucher.edit.loaded -> action:' + a); } } // 表示层业务逻辑放到这里 class RetailOrderViewController extends VoucherViewController { @action viewAction1() { this.ui.feedback('这段逻辑发生在视图控制器中') } @eventPath('voucher.readlonly.loaded') async refresh({ b }) { await this.ui.feedback('voucher.readlonly.loaded -> action:' + b); } } registerController('RetailOrder', RetailOrderController, RetailOrderViewController); // =================== other ================== //测试注册dataProvider的代码 const dataProviderKey = { sysid: 'RE', mid: 'RE01' }; const dataProvider = { async getModule({ sysid, mid }) { const data = await api.get('module', { params: { sysid, mid } }); return data.data.data }, async getDataInfo({ sysid, mid }) { const data = await api.get('dataInfo', { params: { sysid, mid } }); return data.data.data; }, async getData({ sysid, mid }) { const data = await api.get('data', { params: { sysid, mid } }); return data.data.data; }, async getViewInfo({ sysid, mid, pid }) { const data = await api.get('viewInfo', { params: { sysid, mid, pid } }); return data.data.data; } } registerDataProvider(dataProviderKey, dataProvider) // 直接打开单据 if (window.location.pathname == '/') { window.location = '/RE/RE01'; }