mutants-appfx
Version:
appfx module
703 lines (618 loc) • 17.9 kB
JavaScript
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 {
textExpr;
iconExpr;
disabledExpr;
visibleExpr;
onClick;
get text() {
return this.store.execExpr(this.textExpr);
}
get icon() {
return this.store.execExpr(this.iconExpr);
}
get disabled() {
return this.store.execExpr(this.disabledExpr);
}
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 {
allitems;
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 {
labelExpr;
valueExpr;
disabledExpr;
visibleExpr;
typeExpr;
get label() {
return this.store.execExpr(this.labelExpr);
}
get disabled() {
return !!this.store.execExpr(this.disabledExpr);
}
get visible() {
return !!this.store.execExpr(this.visibleExpr);
}
get valueKey() {
return this.store.execExpr(this.valueExpr);
}
get value() {
return this.store.data && this.store.data.getValue(this.valueKey);
}
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 {
allitems;
visibleExpr;
get visible() {
return !!this.store.execExpr(this.visibleExpr);
}
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 {
allitems;
onLoad;
dataSouceField;
table;
focusCells = [];
get dataSource() {
return this.store.data[this.dataSouceField];
}
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;
}
appendTable() {
this.store.data.pushValue(this.dataSouceField, {});
}
handleChange(column, value, rowIndex) {
this.dataSource[rowIndex][column.valueKey] = value;
}
focusout(i, fieldName) {
this.focusCells.remove(i + '_' + fieldName);
}
// uicontroller的实现
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的实现
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 {
table;
pageIndex = 0;
pageSize = 10;
get dataSource() {
return this.store.list.items;
}
nextPage() {
this.pageIndex++;
}
prePage() {
if (this.pageIndex > 1) {
this.pageIndex--;
}
}
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 ========================
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>)
}
}
.create()
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 {
// 一、非行为产生的业务规则,比如金额等联动计算
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();
});
}
// 二、基于行为的业务规则
// 新增
async create() {
await super.create();
runInAction(() => {
this.data.code = 'RE' + new Date().getTime()
})
}
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('已删除');
}
}
}
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('回滚业务数据');
}
async dosomething({
a
}) {
await this.ui.feedback('voucher.edit.loaded -> action:' + a);
}
}
// 表示层业务逻辑放到这里
class RetailOrderViewController extends VoucherViewController {
viewAction1() {
this.ui.feedback('这段逻辑发生在视图控制器中')
}
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';
}