6. 用户管理

6.1. 添加数据入口

在首页 app.tsx 中添加用户管理的Servlet,举例:users.tier

this.anReact = new AnReactExt(this.anClient, this.errorCtx)
                      .extendPorts({
                              menu: "menu.serv",
                              userstier: "users.tier",
                              gpatier: "gpa.tier",
                              mykidstier: "mykids.tier"
                      });

在后端创建 Servlet 类,举例:UsersTier.java

@WebServlet(description = "Semantic tier: users", urlPatterns = { "/users.tier" })
public class UsersTier extends ServPort<UserstReq> {

private static final long serialVersionUID = 1L;

protected static DATranscxt st;

public static String mtabl = "a_users";

static {
        try {
                st = new DATranscxt(null);
        } catch (SemanticException | SQLException | SAXException | IOException e) {
                e.printStackTrace();
        }
}

public UsersTier() {
        super(Port.userstier);
}

......

注意

新框架的数据处理都放到后端了,前端只需把参数传给后端,后端处理后把结果返给前端

6.2. 定义查询组件

在用户管理组件中定义查询组件,需要继承 CrudCompW 类,举例:

class UsersQuery extends CrudCompW<Comprops & {pageInf: PageInf, onQuery: (conds: PageInf) => void}> {
conds = [
        { name: 'userName', field: 'userName', type: 'text', val: undefined, label: L('Student'),
          grid: {sm: 3, md: 2} } as AnlistColAttrs<any, any>,
        { name: 'orgId',    field: 'orgId', type: 'cbb',  val: undefined, label: L('Class'),
          sk: Protocol.sk.cbbOrg, nv: {n: 'text', v: 'value'}, grid: {sm: 3, md: 2} } as ComboCondType,
        // { name: 'roleId',   field: 'roleId', type: 'cbb',  val: undefined, label: L('Role'),
        //   sk: Protocol.sk.cbbRole, nv: {n: 'text', v: 'value'}, grid: {md: 2, sm: 3} },
];
_images/query.png

注意

conds 里面配置的就是查询条件,其中field表示字段名,其中label查询框内文本, type表示字段类型(cbb表示下拉框,如果配置了sk,会自动填充下拉框数据)

6.3. 定义查询方法

① 在前端组件中定义查询列表的方法 toSearch(),举例:

toSearch(_condts: PageInf): void {

        if (!this.tier) {
                console.warn("really happens?")
                return;
        }

        let that = this;

        this.tier.records( this.q,
                (_cols, _rows) => {
                        that.state.selected.ids.clear();
                        that.setState( {buttons: {add: true, edit: false, del: false}} );
                } );
}

② 在前端 UsersTier 类中定义查询列表方法 records(),举例:

records(conds: PageInf, onLoad: OnLoadOk<Tierec>) {
        if (!this.client) return;

        let client = this.client;
        let that = this;

        let req = client.userReq(this.uri, this.port,
                                new UserstReq( this.uri, conds )
                                .A(UserstReq.A.records) );

        client.commit(req,
                (resp) => {
                        let {cols, rows} = AnsonResp.rs2arr(resp.Body().Rs());
                        that.rows = rows;
                        conds.total = resp.Body()?.Rs()?.total || 0;
                        onLoad(cols, rows as Tierec[]);
                },
                this.errCtx);
}

③ 在后端 UsersTier.java 中定义查询列表方法 records(),举例:

protected AnsonResp records(UserstReq jreq, IUser usr)
                throws SemanticException, TransException, SQLException {
        Query q = st.select(mtabl, "u")
                        .page(jreq.page)
                        .col("userId").col("userName").col("orgName").col("roleName")
                        .l("a_orgs", "o", "o.orgId = u.orgId")
                        .l("a_roles", "r", "r.roleId = u.roleId");

        if (!LangExt.isEmpty(jreq.userName))
                q.whereLike("userName", jreq.userName);

        if (!LangExt.isEmpty(jreq.userId))
                q.whereEq("userId", jreq.userId);

        if (!LangExt.isEmpty(jreq.roleId))
                q.whereEq("u.roleId", jreq.roleId);

        if (!LangExt.isEmpty(jreq.orgId))
                q.whereEq("u.orgId", jreq.orgId);

        AnResultset rs = (AnResultset) q
                .rs(st.instancontxt(Connects.uri2conn(jreq.uri()), usr))
                .rs(0);

        return new AnsonResp().rs(rs);
}

注意

前端查询框内的值,后端可以直接调用 jreq.xxx 获取(xxx一般为前端conds里配置的name)

6.4. 关联前后端

前后端能关联的必须条件:前端定义的 UserstReq.A 与后端定义的 UserstReq.A 要对应起来!

前端定义的UserstReq.A
     static A = {
             records: 'records',
             rec: 'rec',
             update: 'u',
             insert: 'c',
             del: 'd',
     }
后端定义的UserstReq.A
     public static class A {
             /** Ask for loading records */
             public static final String records = "records";
             /** Ask for loading a rec */
             public static final String rec = "rec";

             public static final String update = "u";
             public static final String avatar = "u/avatar";
             public static final String insert = "c";
             public static final String del = "d";
     }

注意

通用方法的名称约定如下,其余方法名可自定义:

  • records 表示查询列表(带分页的,前端传page和size即可)

  • rec 查询单条记录

  • update 修改单条记录

  • insert 新增单条记录

  • del 删除单条或多条记录

6.5. 调用表格组件

直接调用封装好的表格组件 AnTablPager,举例:

<AnTablPager
        pk={tier.pkval.pk}
        className={classes.root}
        checkbox={tier.checkbox}
        selected={this.state.selected}
        columns={tier.columns()}
        rows={tier.rows}
        sizeOptions={[5, 8, 10]}
        pageInf={this.q}
        onPageChange={this.onPageInf}
        onSelectChange={this.onTableSelect}
/>
_images/table1.png

注意

rows为查询组件的返回结果,sizeOptions为每页行数,pageInf为分页信息

6.6. 定义表格的列

表格中的列需要在 UsersTier 中单独定义,举例:

_cols = [
        { label: L('check'), field: 'userId', checkbox: true },
        { label: L('Log Id'), field: 'userId' },
        { label: L('User Name'), field: 'userName' },

        { label: L('Organization'), field: 'orgName',
          sk: Protocol.sk.cbbOrg, nv: {n: 'text', v: 'value'} },
        { label: L('Role'), field: 'roleName',
          sk: Protocol.sk.cbbRole, nv: {n: 'text', v: 'value'} }
] as Array<AnlistColAttrs<JSX.Element, CompOpts>>;
_images/table1.png

注意

label为表头的名称,field为数据库字段

6.7. 定义form表单

UsersTier 配置好字段后,在详情页面调用 TRecordForm 组件,即可自动生成表单:

_fields = [
        { type: 'text', field: 'userId', label: L('Log ID'),
          validator: {len: 20, notNull: true} },
        { type: 'text', field: 'userName', label: L('User Name') },
        { type: 'password', field: 'pswd', label: L('Password'),
          validator: {notNull: true} },
        { type: 'cbb', field: 'roleId', label: L('Role'),
          grid: {md: 5},
          sk: Protocol.sk.cbbRole, nv: {n: 'text', v: 'value'},
          validator: {notNull: true} } as TierComboField<JSX.Element, CompOpts>,
        { type: 'cbb', field: 'orgId', label: L('Organization'),
          grid: {md: 5},
          sk: Protocol.sk.cbbOrg, nv: {n: 'text', v: 'value'},
          validator: {notNull: true} } as TierComboField<JSX.Element, CompOpts>,
] as AnlistColAttrs<JSX.Element, CompOpts>[];
_images/form.png

注意

type为text表示文本框,password为密码框,cbb为下拉框(配置了sk,会自动填充下拉数据)

6.8. 提交form表单

①先在 UsersTier 定义保存方法(新增和修改共用),举例:

saveRec(opts: { uri: string; crud: CRUD; pkval: string; }, onOk: OnCommitOk) {

        if (!this.client) return;
        let client = this.client;
        let that = this;

        let { uri, crud } = opts;

        if (crud === CRUD.u && !this.pkval)
                throw Error("Can't update with null ID.");

        this.rec.iv = null; // working - but why?

        let req = this.client.userReq(uri, this.port,
          new UserstReq( uri, { record:this.rec, relations: this.collectRelations(), pk: this.pkval.v})
          .A(crud === CRUD.c ? UserstReq.A.insert : UserstReq.A.update) );

        client.commit(req,
          (resp) => {
                let bd = resp.Body();
                if (crud === CRUD.c)
                        // NOTE:
                        // resulving auto-k is a typicall semantic processing, don't expose this to caller
                        that.pkval.v = bd.resulve(that.pkval.tabl, that.pkval.pk, that.rec);
                onOk(resp);
          },
          this.errCtx);
}

②后端在 UsersTier.java 类定义新增方法,举例:

protected AnsonResp ins(UserstReq jreq, IUser usr)
                throws SemanticException, TransException, SQLException {
        if (jreq.record == null)
                throw new SemanticException("Inserting a null record ...");

        ISemantext stx = st.instancontxt(Connects.uri2conn(jreq.uri()), usr);

        AnResultset rs = (AnResultset) st.select(mtabl, "u")
                        .col(Funcall.count("userId"), "c")
                        .whereEq("userId", jreq.record.get("userId"))
                        .rs(stx)
                        .rs(0);

        rs.beforeFirst().next();
        if (rs.getInt("c") > 0)
                throw new SemanticException("record id doesn't exist");

        SemanticObject res = (SemanticObject)
                        ((Insert) jreq.nvs(st.insert(mtabl, usr)))
                        .ins(stx);

        return new AnsonResp().data(res.props());
}

③前后端关联新增方法:

前端定义的UserstReq.A
     static A = {
             records: 'records',
             rec: 'rec',
             update: 'u',
             insert: 'c',
             del: 'd',
     }
后端定义的UserstReq.A
     public static class A {
             /** Ask for loading records */
             public static final String records = "records";
             /** Ask for loading a rec */
             public static final String rec = "rec";

             public static final String update = "u";
             public static final String avatar = "u/avatar";
             public static final String insert = "c";
             public static final String del = "d";
     }

参见

演示地址: http://192.168.0.85:4000/login.html