超凡魔力

君子善思,善假于物,而不物于物。

0%

关于选择模型

常用的属性有:

选择模型除了使用下面的方式(方式一),也可以在加载的时候,手动的为每个record增加一个属性,checked属性(方式二)。如果同时使用了二者,那么二者将同时显示,显示位置也不同。方式一,可以全配置,可以使用插件进行管理,参见grid表格中的getSelectionModel、selection等。而自己手动设置的,需要自己处理点击事件,做相应的处理。

selModel: {
		selType: 'checkboxmodel',
		checkOnly:false,
		mode:
	},

更新:会议预约,部门展示代码优化

补充说明(2019年4月18日):其实这个是关于grid的选择模型的,另外一篇参见

写在前面

php-crud-api

这个是数据库的api文件,如果是跟前端配合的话,那么前端最起码需要做两方面的工作。

  • 写一个orm类,代理底层Ajax的跟Api.php沟通。具体参考一下,odata提供的js库。
  • Api简化了后端的工作,但是前端相关接口处理工作,应该独立成一个库文件,这样方便多端复用。

build.php

这个是打包工具,将src文件夹中的php文件打包成一个文件夹。其中,src/index.php中有一行,不要再格式化此行代码,估计打包的时候严格的替换掉。打包的作用有两个:

  • 将多个php文件合成一个文件。
  • 删除php文件夹中的一些不必要的内容。如命名空间、<?php 等符号。

思考题:

  • 此项目的源代码解释,非常的少。难道是为了迎合build.php的原因,减少打包删除注释的难度?又或是太简单了,不需要解释?
  • build的顺序,越是深层次的文件,越有限打包,这样能合理的控制,各个文件打包的顺序。
  • 此api,build之后,所有的文件都非懒加载,如果配合swoole、workman等框架,常驻内存,效果是否更好?

思考:如果不需要支持sqlite、post等数据库,那么打包是否精减,不再包含其他的数据库的函数,这样减少体积?

源码分析

index.php

所有的文件都在PhpCrudApi中,在最外层有个index.php文件,这个是配置文件,所有的设置都在这个里面。通过在这个文件中进行设置,然后再通过build.php进行打包,能测试,能测出数据库连接是否正确(非主要),否则生成一个tmp_api文件。

//自动加载,如果是打包成独立文件,则不需要这端代码。
spl_autoload_register(function ($class) {include str_replace('\\', '/', __DIR__ . "/$class.php");});

上面的代码,要进行反思、总结。如何像空间一样,不需要手动include文件。如果自己写框架,是不是要用上上面的这个函数。

主要流程:

配置config对象→ Request对象→Api对象 然后api->handle处理生成一个response。response进行输出。

所以接着研究config、Request、Api对象。

Config.php

配置对象,即index.php中的配置。主要功能,储存所有的配置。不要在这个对象里面进行设置,试过,在这个里面进行了数据设置好像也没有用。但是这个框架里面的默认设置都是在这个里面的。

在这个对象的构造函数中,步骤如下:

  • 优先获取到当前是何种数据库,getDefaultDriver,如果没有设置,默认则为mysql。
  • 根据上一步判断数据库类型的结果,获取默认的参数defaults,’driver’,’address’ => $this->getDefaultAddress($driver),’port’ => $this->getDefaultPort($driver),
  • 合并结果三者结果。类默认的参数,获取到的参数,用户在index.php中的参数,即array_merge($this->values, $defaults, $values)。大概的意思是,怕用户设置错误。测试过array_merge,最后的优先级大,即会覆盖调前面的同名键名。所以,这功能:如果在index.php中进行了设置,则用,没有的则用类默认行为。
    //测试例子
    $a=array('name'=>'zhangsan1');
    $b=array('name'=>'zhangsan2');
    $c=array('name'=>'zhangsan3');
    var_dump(array_merge($a,$b,$c));
  • 解析中间件设置。 parseMiddlewares。这个函数,可以看到php7中,可以指定返回类型了。功能:上一步得到的设置只,middlewares 通过逗号分隔,忽略单词两边的空格。array_map(‘trim’, explode(‘,’, $values[‘middlewares’]))。还要考虑这个配置项中如果有句号.问题。这个函数的功能,有待深入了解。暂略。
  • 比较生成的配置的差异。array_diff_key,如果有差异,则抛出异常。(防止什么?有啥作用)

这个类的其他方法

都是简单的返回该类的成员变量,或者进行一些加工,json_decode,array_map(‘trim’, explode(‘,’, $this->values[‘controllers’]));

Request.php

构造函数有很多参数,如果没有,则进入默认处理。即从$_SERVER数组中获取信息。

构造函数,步骤:

  • parseMethod。默认参数method,设置成员变量的method值,优先级:参数 > $_SERVER[‘REQUEST_METHOD’] > 默认 ‘GET’ 。能否抽象成一个函数?
  • parsePath。跟上一步差不多。设置成员变量path的值。优先级:参数 > $_SERVER[‘PATH_INFO’] > 默认 ‘/‘ 。另外,$this->pathSegments = explode(‘/‘, $path);
  • parseParams。解析参数。也一样。设置成员变量 的值。$query 优先级:参数query > $_SERVER[‘QUERY_STRING’] > 默认 ‘’ 。

    备注:test.php?id=1&b=c&a=1#jk id=1&b=c&a=1

$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
//输出:"id=1&b=c&a=1&a=2" => id[]=1&b[]=c&a[]=1&a[]=2
//调用了php中系统方法,parse_str 
parse_str($query, $this->params);
//作用:将形成 key => array() 形式,如果有多个,数组里面的内容则有多个。
  • parseHeaders。如果没有传参数,那么从$_SERVER中以HTTP_开头的获取。。最终,$this->headers = $headers;
  • parseBody。这个函数如下,下面这个曾经是我的遇到的一个问题。
private function parseBody(String $body = null) /*: void*/
    {
        if (!$body) {
            $body = file_get_contents('php://input');
        }
        $this->body = $this->decodeBody($body);
    }
  • 复制成员变量highPerformance的值。 这个变量的作用,大概是阻止从$_SERVER中解析。

在这个类中,看到了很多注释掉的返回值类型。/: ?object/ 为何?难道是因为太严格了,报错? 另外,其他的方法,都是获取、设置成员变量的值。但是有个方法,fromString。

Response.php

输出结果的方法。总代码也不多。重点函数如下:

public function __construct(int $status, $body)
    {
        $this->status = $status;
        $this->headers = array();
        $this->parseBody($body);//这个步骤中进行了json_encode
    }

 public function output()
    {
        http_response_code($this->getStatus());
        foreach ($this->headers as $key => $value) {
            header("$key: $value");//之前一直听说双引号效率不高。但是这个却用了。
            //我自己简单的测试了下,好像并没有太大的区别。
        }
        echo $this->getBody();
    }

由于是api接口,所以如果data有值,则指定了Header(‘Content-Type’, ‘application/json’)。

Api.php

最复杂的应该是这个类,它本身不复杂,但是引入了很多的其他文件。相当于入口引导。从index.php的使用方法上可以看到,这个类很重要的一个方法是handle,然后返回一个response对象。

这个函数,主要用了api同目录下的其他子文件夹下面的文件。

这个类主要有三个私有成员变量。$router;$responder;$debug; 其中debug从配置中获取,然后控制添加调试的头信息。

这个类主要有两个方法,一个构造方法,一个handle方法。

  • handle方法。从$response = $this->router->route($request);进行获取。如果异常,则try catch处理异常。异常的抛出的值,数据库抛出,ErrorCode中定义。
  • 构造方法。这个是重点掌握。

SimpleRouter.php

这个是响应的核心。在api中,也是通过实例化此对象,然后调用route方法,会自动生成reponse对象。这个里面有个重点:$obj->handle($request)。其实,所有的中间件,包括SimpleRouter形成了一个单向的链,然后按栈的顺序加载。

public function route(Request $request): Response
    {
        if ($this->registration) {
            $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
            $this->cache->set('PathTree', $data, $this->ttl);
        }
        $obj = $this;
        if (count($this->middlewares) > 0) {
            $obj = $this->middlewares[0];
        }
        return $obj->handle($request);//这个过程,在不出错的情况下,会依次调用所有的中间件,以及SimpleRouter 
    }

另外,load是用来将所有的中间件,加载成单链结构。

public function load(Middleware $middleware) /*: void*/
    {
        //从中间件队列前面,取一个元素作为$next。如果没有,则为当前路由。
        if (count($this->middlewares) > 0) {
            $next = $this->middlewares[0];
        } else {
            $next = $this;
        }
        $middleware->setNext($next);
        //将新增的中间件,放在首位。
        array_unshift($this->middlewares, $middleware);
    }

上面的方法,在中间件对象生成的时候,又父类的Middleware自动执行

public function __construct(Router $router, Responder $responder, array $properties)
    {
        $router->load($this);
        $this->responder = $responder;
        $this->properties = $properties;
    }

最终,中间件执行的顺序,类似栈结构。路由的hanlder,最后执行。

为了保存栈结构能顺利执行,所有的中间件在没有出错的情况下,都会执行,下语句

return $this->next->handle($request);

写在前面

php-crud-api

这个是数据库的api文件,如果是跟前端配合的话,那么前端最起码需要做两方便的的工作。

  • 写一个orm类,代理底层Ajax的跟Api.php沟通。具体参考一下,odata提供的js库。
  • Api简化了后端的工作,但是前端相关接口处理工作,应该独立成一个库文件,这样方便多端复用。

代码思想

  • 运用psr-4的命令空间,方便类的加载,已经打包合并
  • 工厂思想(待完善。)参见

build.php

这个是打包工具,将src文件夹中的php文件打包成一个文件夹。其中,src/index.php中有一行,不要再格式化此行代码,估计打包的时候严格的替换掉。打包的左右有两个:

  • 将多个php文件合成一个文件。
  • 删除php文件夹中的一些不必要的内容。如命名空间、<?php 等符号。

思考:如何不需要支持sqlite、post等数据库,那么打包是否精减,不再包含其他的数据库的函数,这样减少体积?

源码分析

index.php

所有的文件都在PhpCrudApi中,在最外层有个index.php文件,这个是配置文件,所有的设置都在这个里面。通过在这个文件中进行设置,然后再通过build.php进行打包,能测试,能测出数据库连接是否正确(非主要),否则生成一个tmp_api文件。

//自动加载,如果是打包成独立文件,则不需要这端代码。
spl_autoload_register(function ($class) {include str_replace('\\', '/', __DIR__ . "/$class.php");});

上面的代码,要进行反思、总结。如何像空间一样,不需要手动include文件。如果自己写框架,是不是要用上上面的这个函数。

配置config对象→ Request对象→Api对象 然后api->handle处理生成一个response。response进行输出。

所以接着研究config、Request、Api对象。

Config.php

配置对象,即index.php中的配置。主要功能,储存所有的配置。不要在这个对象里面进行设置,试过,在这个里面进行了数据设置好像也没有用。但是这个框架里面的默认设置都是在这个里面的。

在这个对象的构造函数中,步骤如下:

  • 优先获取到当前是何种数据库,getDefaultDriver,如果没有设置,默认则为mysql。
  • 根据上一步判断数据库类型的结果,获取默认的参数defaults,’driver’,’address’ => $this->getDefaultAddress($driver),’port’ => $this->getDefaultPort($driver),
  • 合并结果三者结果。类默认的参数,获取到的参数,用户在index.php中的参数,即array_merge($this->values, $defaults, $values)。大概的意思是,怕用户设置错误。测试过array_merge,最后的优先级大,即会覆盖调前面的同名键名。所以,这功能:如果在index.php中进行了设置,则用,没有的则用类默认行为。
    //测试例子
    $a=array('name'=>'zhangsan1');
    $b=array('name'=>'zhangsan2');
    $c=array('name'=>'zhangsan3');
    var_dump(array_merge($a,$b,$c));
  • 解析中间件设置。 parseMiddlewares。这个函数,可以看到php7中,可以指定返回类型了。功能:上一步得到的设置只,middlewares 通过逗号分隔,忽略单词两遍的空格。array_map(‘trim’, explode(‘,’, $values[‘middlewares’]))。还要考虑这个配置项中如果有句号.问题。这个函数的功能,有待深入了解。暂略。
  • 比较生成的配置的差异。array_diff_key,如果有差异,则抛出异常。(防止什么?有啥作用)

这个类的其他方法

都是简单的返回该类的成员变量,或者进行一些加工,json_decode,array_map(‘trim’, explode(‘,’, $this->values[‘controllers’]));

Request.php

构造函数有很多参数,如果没有,则进入默认处理。即从$_SERVER数组中获取信息。

构造函数,步骤:

  • parseMethod。默认参数method,设置成员变量的method值,优先级:参数 > $_SERVER[‘REQUEST_METHOD’] > 默认 ‘GET’ 。能否抽象成一个函数?
  • parsePath。跟上一步差不多。设置成员变量path的值。优先级:参数 > $_SERVER[‘PATH_INFO’] > 默认 ‘/‘ 。另外,$this->pathSegments = explode(‘/‘, $path);
  • parseParams。解析参数。也一样。设置成员变量 的值。$query 优先级:参数query > $_SERVER[‘QUERY_STRING’] > 默认 ‘’ 。

    备注:test.php?id=1&b=c&a=1#jk id=1&b=c&a=1

$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
//输出:"id=1&b=c&a=1&a=2" => id[]=1&b[]=c&a[]=1&a[]=2
//调用了php中系统方法,parse_str 
parse_str($query, $this->params);
//作用:将形成 key => array() 形式,如果有多个,数组里面的内容则有多个。
  • parseHeaders。如果没有传参数,那么从$_SERVER中以HTTP_开头的获取。。最终,$this->headers = $headers;
  • parseBody。这个函数如下,下面这个曾经是我的遇到的一个问题。
private function parseBody(String $body = null) /*: void*/
    {
        if (!$body) {
            $body = file_get_contents('php://input');
        }
        $this->body = $this->decodeBody($body);
    }
  • 复制成员变量highPerformance的值。 这个变量的作用,大概是阻止从$_SERVER中解析。

在这个类中,看到了很多注释掉的返回值类型。/: ?object/ 为何?难道是因为太严格了,报错? 另外,其他的方法,都是获取、设置成员变量的值。但是有个方法,fromString。

Response.php

输出结果的方法。总代码也不多。重点函数如下:

public function __construct(int $status, $body)
    {
        $this->status = $status;
        $this->headers = array();
        $this->parseBody($body);//这个步骤中进行了json_encode
    }

 public function output()
    {
        http_response_code($this->getStatus());
        foreach ($this->headers as $key => $value) {
            header("$key: $value");//之前一直听说双引号效率不高。但是这个却用了。
            //我自己简单的测试了下,好像并没有太大的区别。
        }
        echo $this->getBody();
    }

由于是api接口,所以如果data有值,则指定了Header(‘Content-Type’, ‘application/json’)。

Api.php

最复杂的应该是这个类,它本身不复杂,但是引入了很多的其他文件。相当于入口引导。从index.php的使用方法上可以看到,这个类很重要的一个方法是handle,然后返回一个response对象。

这个函数,主要用了api同目录下的其他子文件夹下面的文件。

这个类主要有三个私有成员变量。$router;$responder;$debug; 其中debug从配置中获取,然后控制添加调试的头信息。

这个类主要有两个方法,一个构造方法,一个handle方法。

  • handle方法。从$response = $this->router->route($request);进行获取。如果异常,则try catch处理异常。异常的抛出的值,数据库抛出,ErrorCode中定义。
  • 构造方法。这个是重点掌握。

拖动排序

Ext.data.NodeInterface 这个对象还树形treepanel是很重要的。store里面存放的都是这个。展开、Expaned或者折叠。

相关知识

  • 快速查找相关的工具,比如grid插件,通过这个关键字 grid.plugin
  • DragDrop源码,这个插件源码非常简单,里面有个重要的函数,onViewRender。其中有一行代码:
ownerGrid.relayEvents(view, ['beforedrop', 'drop']);

所以从Ext.mixin.Observable中查找到这个relayEvents函数,主要功能是用来传递事件。即将插件的触发的事件传递到grid表格。

问题分析

从后端来说,为了满足排序,必须额外使用一个字段辅助排序。但是增加这个字段order,给一个默认值,比如100,又不会增加额外的编程难度。(排序,并不是更换两个id)。总结:目前还是没有好的想法,最简单粗暴的处理方式就是,前端直接将一个分组内的排序,告诉后端,后端直接进行遍历,从0开始增加1。(字段默认也是从0开始)。但是这样的问题是,业务处理代码简单,但是数据库增加了不必要的操作。(暂定此方法。)

从前端来说,需要支持拖拽排序。从网上找到了Ext的这个插件的用法。但是细节还不清楚。参见文章,代码基本可用。

核心代码:

viewConfig: {
	plugins: {
		ptype: "gridviewdragdrop",
		dragText: "可用鼠标拖拽进行上下排序"
	}
},

实现Grid行的上下移动

Ext.onReady(function() {
	var proStore = Ext.create("Ext.data.Store", {
		fields: ["code", "name", "num"],
		data: [
			["#001", "iPhone6", 100],
			["#002", "iPhone6 Plus", 80],
			["#003", "Mi Note4", 99],
			["#004", "Galaxy S6", 75],
			["#005", "Smartisan T2", 68]
		]
	});
	
	Ext.create("Ext.grid.Panel", {
		title: "可用鼠标拖拽行数据的Grid",
		width: 400,
		margin: 10,
		border: true,
		store: proStore,
		columns: [{
			text: "产品编号",
			dataIndex: "code",
			flex: 1
		}, {
			text: "产品名称",
			dataIndex: "name",
			flex: 1
		}, {
			text: "产品数量",
			dataIndex: "num",
			flex: 1
		}],
		viewConfig: {
			plugins: {
				ptype: "gridviewdragdrop",
				dragText: "可用鼠标拖拽进行上下排序"
			}
		},
		renderTo: Ext.getBody()
	});
});

实现多个Grid之间的相互移动

要让行数据可在多个Grid之间相互移动,只需在Grid引用Ext.grid.plugin.DragDrop插件时,为其指定同一个组名,表明它们属于同一组,可相互移动。

当然,前提是这些Grid使用的Model类应该一致,即表格字段应该一致。

viewConfig: {
	plugins: {
		ptype: "gridviewdragdrop",
		ddGroup: "DrapDropGroup"
	}
}

开始处理代码

版本说明:使用的是Ext5.1.0.107版本。使用6.5中的手册写法略有不同。

问题

  • 插件的事件无法监听到。解决办法,写到viewConfig的listeners中。
//备注:me指代的是grid。另外写在grid的listeners也没有反应。
me.on('beforedrop',function(node, data, overModel, dropPosition, dropHandlers) {
  • 找到排序后的数据

只是简单的跟了一个位置before、after。通过store.getData获取到数据。

  • 理解事件传递的值,解决了判断,该节点能不能拖拽到节点

当前拖动的记录:data.records[0]
目标的记录:overModel
相对位置关系:dropPosition

  • 父节点,默认只展开一级。代码写了,却没有效果。

  • 显示优化问题

如果重新加载数据,则数据全部重新刷新了。原来展开的,折叠住了。如果不刷新吧,那么拖动末个节点,显示异常。

  • 展开节点问题

必须使用Ext.data.NodeInterface提供的expand、collapse等方法。而不能通过record.set方法来实现。注意跟checked的区别。

/************
	2019/4/2
	只展开顶级节点
************/
expandTopRecord:function(){
	var me=this;
	var root = me.store.getRoot();
	Ext.Array.each(root.childNodes,function(node){
		//注意不能使用//node.set('expanded',false);
		node.collapse();
	});
},
//官方手册
Ext.grid.plugin.DragDrop
drop ( node, data, overModel, dropPosition, eOpts ) 

// 官方例子 build/examples/kitchensink/?classic#dd-grid-to-grid
onDropGrid1: function (node, data, dropRec, dropPosition) {
        this.onDrop(dropRec, data.records[0], dropPosition, 'Drag from right to left');
},
  • 范围

简单的处理逻辑

//me.getdog();
var view=me.getView();
view.on('beforedrop',function(node, data, overModel, dropPosition, dropHandlers) {
	// Defer the handling
	//dropHandlers.wait = true;
	
	//1、判断当前节点,
	//一级节点,则进行折叠
	//否则,停止拖动前进行判断,如果合适,则允许放入。
	
	
	/*Ext.MessageBox.confirm('Drop', 'Are you sure', function(btn){
		if (btn === 'yes') {
			dropHandlers.processDrop();
		} else {
			dropHandlers.cancelDrop();
		}
	});*/
});
view.on('drop',function(node, data, overModel, dropPosition, eOpts) {
	//获取排序后的数据console.log(me.store.getData());
	//根据当前节点ParentID,过滤出该组的数据。
	//然该组数据发送致后台
});

正式代码


var view=me.getView();
//注意,使用这种方式进行监听
view.on('beforedrop',function(node, data, overModel, dropPosition, dropHandlers,eOpts) {
	// Defer the handling
	dropHandlers.wait = true;
	var curRd=data.records[0];
	//判断当前节点,是否可移动,同一组内可移动。
	if(curRd.get('ParentID')!=overModel.get('ParentID')){
		dropHandlers.cancelDrop();
	}else{
		dropHandlers.processDrop();
	}
});
view.on('drop',function(node, data, overModel, dropPosition, eOpts) {
	//获取排序后的数据console.log(me.store.getData());
	var curRd=data.records[0];
	var pid=curRd.get('ParentID');
	var Rds=me.store.getData().items;
	//根据当前节点ParentID,过滤出该组的数据。
	var fRds=Ext.Array.filter(Rds,x=>x.get('ParentID')==pid);
	var ids=Ext.Array.map(fRds,x=>x.get('GroupID'));
	//然该组数据发送致后台
	Ext.Ajax.request({
		url: '/admin/device/Terminal/orderterminalgroup/', 
		params: {'ids': Ext.encode(ids)}, 
		method: 'POST', 
		success: function(response, option)
		{
			//记录当前折叠状态
			me.updateExpandIds();
			//更新树木,避免显示乱问题
			me.store.reload();
			//但是如果更新了,节点展开的就关闭了
		},
		failure: function(){}
	});
});

//node.set('expanded',false); 除了数据加载阶段可用
//其他阶段不可用,应使用expand函数等

/************
	2019/4/2
	记录展开的id。此方法不是递归的,所以呢,只能处理简一层。
************/
updateExpandIds:function(){
	var me=this;
	var root = me.store.getRoot();
	me.expandIds=[];
	Ext.Array.each(root.childNodes,function(node){
		if(node.data.expanded===true){
			me.expandIds.push(node.data.GroupID);
		}
	});
},

//在store.load时,更新expand状态
Ext.Array.each(records,function(rd){
	if(Ext.Array.indexOf(me.expandIds,rd.get('GroupID'))!==-1){
		rd.set({expanded:true});//此时可用,因为树还没有重新渲染
	}
});

会议预约代码改进

改进说明:原来的代码,可能想到了点击会正常的触发事件,而点击checkbox时,没有效果。所以呢,想在checkchange进行触发事件,所以只是简单触发了一次itemclick时件。上述代码问题比较多,其一,无论是点击普通的地方还是点击checkbox,其实都是触发了itemclick(理解错了,因为手动设置record的check属性,所以呢,相当于checkOnly)。其二,默认的模式可能是:

//说明,无法通过更改checkOnly来更改属性,
//因为这个会导致树前面增加了一个checkbox。
//显示效果不满足。
selModel: {
    type: 'checkboxmodel',
    checkOnly: true  //只有点击checkbox才会触发改变。
},

只有点击checkbox才会触发改变。导致了点击普通的地方跟点击checkbox的效果不一致,而导致其以为checkbox没有作用,进而继续监听了itemclick事件。其三,就算是没有左右,也不应该手动的触发itemclick事件,而是将itemclick的代码,封装成一个函数,在两个事件中分别调用。而手动触发,会导致事件的循环触发。即A触发了B,而B继而又触发了A。

综上,代码进行了改进。

改进前:

me.on('checkchange',function( node, checked, eOpts){

            me.fireEvent("itemclick",null,node);
        });

me.on("itemclick",function( store, record, item, index, e, eOpts ){
    record.set('checked',record.get("checked") == false);
    me.SetChecked(record,record.get("checked"));
    var groupIdList = [];
    Ext.each(me.getChecked(),function(checkRd){
        var GroupID = checkRd.get("department_id");
        if(undefined != GroupID){
            groupIdList.push(GroupID);
        }
    });
    ShineMessageHub.fireEvent("usergridgroupidlistevent",groupIdList,me);
});

改进后:

//下面的代码会造成循环,性能不好,而且存在点击问题。
//me.on('checkchange',function( node, checked, eOpts){
//          me.fireEvent("itemclick",null,node);
//        });

me.on("itemclick",function( store, record, item, index, e, eOpts ){
    //增加下面一处判断,这样,无论点击checkbox或者 普通的点击,表现一致
    if(e.target.getAttribute('role')!=='button'){
        record.set('checked',record.get("checked") == false);
    }
    me.SetChecked(record,record.get("checked"));
    var groupIdList = [];
    Ext.each(me.getChecked(),function(checkRd){
        var GroupID = checkRd.get("department_id");
        if(undefined != GroupID){
            groupIdList.push(GroupID);
        }
    });
    ShineMessageHub.fireEvent("usergridgroupidlistevent",groupIdList,me);
});

vue 学习笔记

router-link中传值的三种方式

  • 环境搭建

vue2+webpack4+scss

以下代码中,如果传递参数

1为对应的学生的id?

<li v-for="student in students" :key="student.id" @click="queryScore(1)">
    <img src="../../assets/1.png" alt="">
    <p>{{student.name}}</p>
</li>

使用图标展示数据

引入库文件

npm install echarts --save

vuejs中使用echart图表
饼形图实例

动态组件

  • 简单用法

动态组件,使用以下标签进行绑定,其中which_to_show指定绑定的组件的名称。然后在其他处动态的改变这个值,即能达到切换效果。

<component v-bind:is="which_to_show"></component>

动态组件

  • 动态组件,监听子组件传递的消息

跟普通的组件传递消息一致。

父组件监听消息

<component v-bind:is="dayRecordShow" v-on:dayforward="dayRecordShow='DayList'"></component>

子组件通过按钮,触发消息

<mt-button type="primary" size="large" @click="$emit('dayforward')">查看详情</mt-button>

v-for使用

v-for动态选择,然后根据选中的id动态设置选择的样式class。实现了动态选择选项的功能。

//模板
<div class="stuclass">
	<ul>
	  <li 
	    v-for="classInfo in classList" :key='classInfo.cid'
	    :class="classInfo.cid==selClass?'active':'deactive'"
	    @click="selClass=classInfo.cid"
	  >
	    {{classInfo.className}}
	  </li>
	</ul>
</div>
//数据
 data() {
    return {
      selClass:1,
      classList:[
        {cid:1,className:'一班'},
        {cid:2,className:'二班'},
        {cid:3,className:'三班'},
        {cid:4,className:'四班'},
        {cid:5,className:'五班'},
        {cid:6,className:'六班'},
        {cid:7,className:'七班'}
      ],
      chart: null,
    };
  },

MintUI日期选择控件使用

参照官方的例子,确实比较简单。简单的使用例子如下datetime-picker

注意,不要将此组件,放到触发的组件容器中,否则,无限弹出。

//引入日期时间选择器。 下面进行全局注册
import { DatetimePicker } from 'mint-ui';
Vue.component(DatetimePicker.name, DatetimePicker);

//

增加字体

将font-awesome下载后,放到/src/static/font-awesome文件夹下。(css、font两个文件夹,其他的不要。)

//在main.js中直接引用以下样式
import './static/font-awesome/css/font-awesome.min.css';

在代码中,即可使用。如:

<i class="fa fa-angle-left" aria-hidden="true"></i>

MintUI封装picker的例子

Vue + Mint-ui 封装滚轮选择器

全局引入第三方库

在引入underscore的库时候,遇到了这样的一个需求,需要全局引入一个库。但是在main.js中按如下代码方式引入,结果发现并不行。

参考:
Vue 中如何引入第三方 JS 库

canvas清空画布的方法

canvas清空画布方法

  • 最简单的方法:由于canvas每当高度或宽度被重设时,画布内容就会被清空,因此可以用以下方法清空
function clearCanvas()
{  
    var c=document.getElementById("myCanvas");  
    var cxt=c.getContext("2d");  
    c.height=c.height;  
} 
  • 使用clearRect方法
function clearCanvas()  
{  
    var cxt=document.getElementById("myCanvas").getContext("2d");
    cxt.clearRect(0,0,c.width,c.height);  
}  

引用mint-ui中的swipe

引入到文件中,看不到任何效果,查看dom节点,发现有结构。所以,是因为div没有默认的高度,所以呢,设置高度即可显示。如果是图片,则会为img增加widh=100%。

div单行显示

如果想让div单行显示,其实只要设置行高跟div的高度一致,然后多余的文字使用overflow:hidden即可。

常用的圣杯布局、双飞布局

圣杯布局小结文章讲解比较详细。而且这个人的博客还是很不错的。

keep-alive属性

这个属性,用在动态组件上面,这样动态组件不用每次都销毁重建。(下面方式进行监听)

destroyed(){
    console.log('我销毁了啦');
},

mint-ui中的切换卡片按钮

通过观察dom节点,实际上改变的是该节点的可见性。(mint-tab-container-item display:none)
也就是说,所有节点其实都已经渲染上了,但是只是没有显示而已。这个跟keep-alive动态组件还不一样。动态组建,实际上整体已经脱离了文档流,估计状态都还保存在内存中。再次需要的时候,再次放回去。原理不一样。

为什么说,动态组件,切换时dom节点会脱离文档流?因为使用了定期任务setInterval,在定时任务执行时,切换了,结果找不到了dom节点。所以,如果有定时任务,延时任务,而且还需要对dom节点操作,请不要这么干,直接使用v-show好啦。这个东西,就不会啦。2019/2/28

mint-ui中的Indicator

其功能其实跟Ext.Msg.Wait的功能很像,作用是,防止用户多次点提交按钮。所以,用法也相似。2019/2/28

Indicator.open('审核中...');
//请求成功或者失败,都应该关闭
setTimeout(function(){
        Indicator.close();//关闭全局的mask
        me.$router.go(-1);
},1000);

感悟

做一件事的方法,有很多种,善于思考,总能找到一种适合的解决办法。

(2019年2月28日)今天做考勤路径跳转,在做之前,已经想过了一种解决方法,就是,如果当前页面是考勤页面,默认的全局返回按钮隐藏起来,然后设置该页面特定的按钮,然后跟该组件能紧密的联合起来,能获取到相关的参数,控制特定的返回过程。

但是,中午看手册,路由地址变化,如果地址不变,只是参数发生了变化,那么,组件不会刷新。正好利用此特性,在原因的路由上面,加了请求参数。

watch: {
    '$route' (to, from) {
        var show = this.$route.params.show;
        var dayRds = ['DaySummary', 'DayList', 'DayDetail'];
        var monthRds = ['MonthSummary', 'MonthList', 'MonthDetail'];
        var index = dayRds.indexOf(show);
        if (index > -1) {
            this.selected = 'dayRecord';
            this.dayRecordShow = dayRds[index];
            return;
        }
        index = monthRds.indexOf(show);
        if (index > -1) {
            this.selected = 'monthRecord';
            this.monthRecordShow = monthRds[index];
        }
    }
},

slot

  • 如果引入的组件,含有插槽,在插槽里面的内容,其能享受到该组件的所有域。
<mt-popup
      v-model="popupVisible"
      popup-transition="popup-fade"
>
      <div class="photo-container" @click="popupVisible=false">
      这个里面的所有域,跟外面的相同。
        <div class="photo-img"         
          v-for="(photo,index) in photos" 
          :key="index"
        >
          <img src="../../assets/1.png" alt="">
        </div>
      </div>
</mt-popup>

使用sass文件格式

使用sass语言的时候,由于层层嵌套的原因,如果在浏览器中发现想要的样式,没有列处理,查看拼写是否错误,或者嵌套的错误。无法识别等。

使用overflow清除浮动

  • div内部有float元素,该div可以使用float来清除浮动。
  • 另外一种方法是,该div增加一个子div,子div style=”clear:both”

事件绑定

遇到一个如下的问题,需要循环photos里面的元素,并根据该元素绑定一个事件,但是如果photos为空的话,那么会报如下错误:

Uncaught TypeError: Cannot read property '$emit' of undefined

大概的意思是:为遍历的元素增加了触发事件,结果该遍历的条目为空。然后事件绑定就报错了。暂无解法,只能限制用户不能将photos数组清空。

<!-- 点击轮播图,查看图片详情 -->
    <mt-popup
      v-model="popupVisible"
      position="right"
      :modal="false"
    >
      <div class="photo-container">
        <div class="photo-img"         
          v-for="(photo,index) in photos" 
          :key="index"
        >
          <div class="tools" @click="deletePic(photo.src)">
            <i class="iconfont icon-delete"></i>
          </div>
          <img src="../../assets/1.png" alt="">
        </div>
        <div class="btn">
          <mt-button type="primary" size="large" @click.native="popupVisible=false">确定</mt-button>
        </div>
      </div>
    </mt-popup>
    //删除方法

    deletePic(src){
      console.log('您要删除图片'+src);
      var index=-1;
      var ps=this.photos;
      //查找到对应删除的条目
      for(var i=0;i<ps.length;i++){
        if(src===ps[i].src){
          index=i;
          break;
        }
      }
      if(index>-1){//找到
        this.photos.splice(index,1);
        //发送请求,将图片从服务器上删除。
        //todo
      }
      
    },

另外,发现Mint UI中Toast的z-index更小。

Toast('必须要保留一张图片。');

临时解决办法:

//条件判断,要不要显示
<div class="tools" @click="deletePic(photo.src)" v-if="photos.length>1">
            <i class="iconfont icon-delete"></i>
          </div>

vue中使用手势库问题

可用的库 vue-touch 还有touchjs。反正这个库不是很好用。
对于前一种库,使用如下:

//默认的生成div标签,但是问题是,滑动,swiperight,标签多了一个touch-action: none;样式,
//该样式阻止了正常上下的滑动。
//style="touch-action: pan-y!important;" 该样式能恢复,但是非常麻烦。
<v-touch v-on:swiperight="sright"></v-touch>

vue中引入图片

如果直接在html代码中引入代码,框架打包的时候,会自动的处理图片的连接。但是如果在代码中,引入的话,需要自己来控制。核心代码如下:

require("../assets/test.png");

应用举例:

//1、首先给图片地址绑定变量
<template>
    <img :src="imgUrl">
</template>

//2、在script中设置变量
<script>
    //方法1.直接将图片引入为模块
    require imgUrl from "../assets/test.png"
    //方法2.将imgUrl放在数据里
    data(){
        return {
            imgUrl:require("../assets/test.png")
        }
    }
</script>

页面跳转“保活”功能

有的时候,页面跳转,返回回来时,页面要求还是上次已加载的样式。我个人称其为“保活”。在Ext中,如果采用card布局,很容易就实现了“保活”。在Vue里面,根据情况,分为三种:

  • if判断:如果页面路由没有发生跳转,此页面就能实现报活。(主要利用v-if或者v-show动态的显示不同的页面)
  • 参数:如果页面跳转了,但是只是路由的参数发生了变化,根据Vue官方路由中的介绍,页面不会发生重新渲染。
  • 子路由:做一个空的组件,做容器。路由出口,让其保活。
  • 动态组件。暂未思考。
    子路由应用具体的如下:
    父容器:
<template>
  <!-- 父容器,让其保活,keep-alive -->
  <div class="scorehome">

    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  </div>
</template>

<script>
export default {
  name: "ScoreHome",
};
</script>

子路由设置:

{
    path: '/score/',
    name: 'ScoreHome',
    meta: { title: '成绩查询' },
    component: resolve => require(['../components/score/ScoreHome'], resolve),
    children: [
      {
        path: 'index',
        name: 'Score',
        meta: { title: '成绩查询' },
        component: resolve => require(['../components/score/Score'], resolve),//懒加载
      },
      {
        path: 'queryscore',
        name: 'QueryScore',
        meta: { title: '成绩查询' },
        component: resolve => require(['../components/score/QueryScore'], resolve),//懒加载
      }
    ]
  },

这样即实现了保活功能。

但是,现实需求是复杂的,页面从1→2→3或者3→2→1这样方向传递。其中2需要保活,3不需要保活。怎么做呢?

根据上面的三种方式,后两种方式差不多。每次进入3的时候,让其主动刷新呢?那么就需要导航守位或者监控导航的变化。或者还可以利用消息传递机制。强制让一个组件重新刷新,这个?

临时解决办法

临时监控路由变化。

watch:{
  $route(to,from){
    var me = this;
    //如果判断是当前路由,则进行初始化,并重新加载数据。
    //注意,这个方法,首次并不会执行,只有再次过来才会执行。
    if(to.path=="/score/queryscore"){
      this.user_name= null;
      this.exam_name=null; //考试名称
      this.exam_type= null; //考试类型
      me.studentObj = to.query.studentObj;
      me.parseExam();
    }
  }
},

导航守位

beforeRouteEnter这个函数里面好像访问不到this,这个函数里面必须要调用next(),否则加载不到页面。它比watch里面监控到的$route变化发生更早。

关于未修改,不提示直接返回

这个是Ext后台的功能,用户希望,没有修改时,可以直接返回,不要进行提醒。思路,监控用户有无输入比较麻烦。则判断,用户点击组件的次数,如果只点了一次(这一次是发生在点返回按钮时),那么认为没有做任何修改。弊端,有可能用户用键盘发生了变化,这个就不考虑了。核心代码如下:

//增加点击时,
listeners:{
        'afterrender':function(cmp, eOpts){
            var me=this;
            me.el.dom.onclick=function(){
                me.bodyCount++;
            };
        },
        'destroy ':function(cmp, eOpts){
            this.el.dom.onclick=null;
        }
    },
//在每次添加、修改时,重新初始化一次。因为这个组件是不死的。
ShineMessageHub.on("roomlistgridpaneladdevent",function(rd){
            me.bodyCount=0;
//在返回时,判断次数,<=1则进行默认处理。

serviceworker

第一次听说这个概念

//通过一下方式,进行控制。
chrome://serviceworker-internals/

手机端禁止缩放

代码如下,但是对于浏览器好像,并没有强制,但是对于微信,好像支持。


<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1,user-scalable=no">

网上找来的解释:

该meta标签的作用是让当前viewport的宽度等于设备的宽度,同时不允许用户手动缩放。 其中 maximum-scale为允许用户的最大缩放值,user-scalable为是否允许用户进行缩放,yes(默认)代表允许,no(0)代表不允许,两者结合使用可以阻止页面被放大(经测试,少一项都达不到效果)

vue中引入包的先后顺序

代码如下,错误的解释如下:

import Vue from 'vue'
//注意,这个App包先引入,所以无法使用后面引入的东西。
import App from './App'
import router from './router/index'
import Mint from 'mint-ui';
import 'mint-ui/lib/style.css'
import { Header } from 'mint-ui';
import { TabContainer, TabContainerItem } from 'mint-ui';
import { Navbar, TabItem } from 'mint-ui';
import axios from "axios"
import GlobalTools from './components/GlobalTools.js'//引用模块进来
//像下面的这个MessageBox就无法在App中直接使用。因为顺序问题。
import { MessageBox } from 'mint-ui';
import { InfiniteScroll } from 'mint-ui';
import { Loadmore } from 'mint-ui';
import { Actionsheet } from 'mint-ui';

重要的原因是:

import Mint from 'mint-ui';
//在注册到Vue中,每个实例都多了$indicator、$messagebox、$toast方法。
Vue.use(Mint);

监控返回事件

本来想做的功能是,监控返回时,判断,如果要退出了,则提示是否要退出。结果,下面的这个功能死活不起作用。(估计在调试模式下,websocket会返回的加载更改的代码)

window.addEventListener('popstate',function(){
      console.log('监控退出事件');
      var notShowBackList=['/','/HomePanel/','/MySelfPanel/','/SettingPanel/'];
      var find=notShowBackList.indexOf(me.$router.history.current.path);
      if(find>-1){
        MessageBox.confirm('确定要退出吗?').then(action => {
        }).catch(action=>{
          window.history.pushState('forward', null, '#/');
          window.history.forward(1);
        });
      }
    },false);

格式化时间代码优化

以前使用Ext的库函数惯了,后来自己使用vue框架的时候,很多库都没有了。然后自己写了格式化时间代码。又经再次优化,如下:

/************
 * 2019年2月22日
 * a 时间   sep 分隔符,格式化输出时间
 ************/
formatDate(date, sep) {
    if (!date) {
        return '';
    }
    //改进的代码
    return date.toISOString()
        .replace(/T/, ' ') // replace T with a space
	.replace(/\..+/, '')
        .slice(0, -3);
    //之前的代码
    return (
        date.getFullYear() +
        "-" +
        (date.getMonth() > 8 ? "" : "0") +
        (date.getMonth() + 1) +
        "-" +
        (date.getDate() > 9 ? "" : "0") +
        date.getDate() +
        " " +
        (date.getHours() > 9 ? "" : "0") +
        date.getHours() +
        ':' +
        (date.getMinutes() > 9 ? "" : "0") +
        date.getMinutes());

然后最终优化为:

/************
* 2019年2月22日
* date时间对象 ,格式化输出时间
************/
    //date.toISOString如果没有这个方法,那么认为不是时间格式。返回为空字符串。
    //比上面的异常判断更好。
    formatDate(date) {
      return date&&date.toISOString?date.toISOString()
              .replace(/T/, ' ')      // replace T with a space
              .replace(/\..+/, '')
              .slice(0,-3):'';
    },

然后发现,时区还不对,格始化的时间小了8小时。然后,又再次优化为:

formatDate(date) {
//运用了括号表达式,(1,2)返回最后一个值的值,1表达式,主要功能,纠正时间8小时误差。
      return date&&date.toISOString?(date=new Date(date.getTime()-date.getTimezoneOffset()*60*1000),date.toISOString()
              .replace(/T/, ' ')      // replace T with a space
              .replace(/\..+/, '')
              .slice(0,-3)):'';
    },

上面运用到了一个知识,Date本身是对象,所以呢,其是按址传递的。但是,date=new Date 其实相当于又拷贝了一个新对象。跟原对象已经没有关系了。所以呢,new之后,跟调用处的date,formatDate(date) ,已经脱离关系了。
至于如何验证Date传址,下面是个例子。

a= new Date();
console.log(a);
b=a;
b.setFullYear(2018);
console.log(a);

伪元素after之content

本来的想法是,after里面放“内容”之内的html,结果发现不行。

.selected::after {
      content: "\e63e";
      position: absolute;//能使用绝对定位
}

使用参数传递状态值

之前,从上个页面跳到页面来,上个页面直接将查询处理后结果传递过来,这样做,确实比较省事,但是问题:页面刷新,上个页面传递的数据就不存在了。

变通:将其作为参数params来传递,传递一个资源值。是不是更好一点?

vue中使用过滤器

使用过滤器,对要展示的数据进行转换,这是另外一个地方,可以对数据进行转换。扩展:

  • 后端处理。Mysql的时候处理。
  • 后端处理。php语言处理。
  • 数据加载后,处理。Ajax后,立马组织数据。
  • 计算属性,跟Ext中Store的convert属性作用差不多。
  • filter处理。这个跟Ext中renderer函数有点像,这个不会更改原数据。

vuex使用

vuex的出现,主要解决的问题是,多组件之间的通信问题。在未引入vuex之前,组件的通信:父组件→子组件,通过props向子组件传递,而子组件→父组件就必须通过事件触发$emit的方式进行传递。如果兄弟组件通信,需要利用父组件进行中转,如果是更复杂的组件之间通信,方式非常麻烦。为了解决这个问题,引入了store的概念。官方手册中,讲解了可以定义一个共享的store,然后其他的组件,直接使用store的state属性来初始化变量,直接更改state并共享,这种方式,非常不利于排查问题。所以建议,所有更改必须通过一个方法来触发,这样可以跟踪变量。

vuex中共有五个概念:

  • State:共享的数据。这个是所有需要通信的组件,共享此属性内的组件。(js对象变量传址方式传递值。但是,不要直接操纵变量来更改)
  • Getter:提供数据转换的方法。(类似Ext中store的fields的convert方法),共享此方法,过滤、转换等作用。
  • Mutaction方法。(突变)。主要用来更改状态。(不能使用延时的操作)。2019/3/11
  • Action。跟Mutaction最大的区别,允许有一步操作,操作结束并不直接更改State中的数据,而是commit去调用Mutaction更改变化,返回的是Promise。
  • Module。模块,用来组织多个组件共享的方式。分模块。

会议预约代码改进

前言

写程序的时候,要知道代码的前提条件是什么。比如,开根号,就应该想到,我可能用到的数可能为负数。所以,需要先验证。(至于验证在哪进行,是开根号函数定义还是调用,视情况而定。)所以这些,才能保证程序的健壮性。

常用策略

使用变量替代复杂的表达式

一个表达式、多个逻辑值求值或函数调用等反复引用,应该起一个有一定含义的名字,作为一个变量来使用。这样能简化代码。如检查是否是特权用户。使用super变量,真:特权。假:非特权。

$super=in_array($user->role_id,array(1,3));

相关的变量或者逻辑应该抽象成对象,封装在一起。

如初始化的变量

//下面整体应该保存到一个变量,就不用这么麻烦。
//var origin= rdData;//只要这一句就ok。
me.originalMeetingdate=me.meetingdate = rdData['meeting_date'];//保存初始值
me.originalStarttime=me.starttime = rdData['starttime'];    //保存初始值
me.originalStoptime=me.stoptime = rdData['stoptime'];       //保存初始值

代码展示

时间显示

{//第二块
    xtype:'menublockpanel',//仅显示
    items:[{
	xtype:'selecttextfield',
	bind:{
	    // value:"{meeting_date}  {starttime}-{stoptime}" //注意这种方式,如果三个字段为空的话,那么还是会有内容,空格+'-'号
	    //而下面是一种改进,如果meeting_date没有内容,直接显示的就为空的内容。这样就能显示emptyText:'请选择会议召开时间。',
	    //另外需关注:字符串能运算。当然下面的代码也能放到viewModel中,能使用公式来计算。但是没有此方法简单。
	    value:'{meeting_date?meeting_date+"  "+starttime+"-"+stoptime:""}'
	},
	submitValue:false,
	fieldLabel:Ext.String.format(DIC.labelString,5,'时间'),
	emptyText:'请选择会议召开时间。',
	setItemIndex:4,
	needRoom:true
    }]
}

增加输入变化时,change事件的响应。

在MenuBlockPanel中,集中的为子items设置默认的change属性。先判断setItemIndex是否为1,即先判断是否需要输入提醒。如果确定有,则进行相应的正确、错误提醒。这个在后期还能优化,如正则验证后,相应的提示。

listeners:{
    'focus':function(cmp, event, eOpts){
	var meetingordermenu=cmp.up('meetingordermenu');
	ShineMessageHub.fireEvent('meetingordermenuinputfocus',cmp,meetingordermenu,event, eOpts);
    },
    'change':function(cmp, newValue, oldValue, eOpts ){
	if(cmp.setItemIndex!==1){
	    return ;
	}
	var o;
	if(newValue){
	    o={icon:'pass'};
	}else{
	    o={icon:'warning'};
	}
	ShineMessageHub.fireEvent('meetingordersettingcontainersetviewmodelevent',o);
    }
}

表单提交前的数据验证

先进行空的判断,然后再进行valid的正则验证等。如果输入错误,则进行提示相应的内容。

话说,不死布局真得不好用。

不死布局,真的不好用,上一次操作的记录,如果不能完全清空。对下一次的操作影响真得很大。为了这个缺点,每次都要进行初始化工作。

//添加会议时,设置默认项
ShineMessageHub.on("meetingorderhomeaddevent",function(rdData){
    me.meetingdate = rdData['meeting_date'];
    me.starttime = rdData['starttime'];
    me.stoptime = rdData['stoptime'];
    me.conference_room_id = rdData['conference_room_id'];
    me.meeting_id = rdData['meeting_id'];
    me.displayStart=Ext.Date.format(new Date(),'Y-m-d');
    me.ajaxGetOccupy();
    me.idx=-1;//这个记录了上一次操作的点。结果这个东西,没有重置。
},me);

超级用户等,可以更改正在进行中的会议

更新需求。超级用户可以修改正在进行中的会议,可以修改任何内容。由于之前,创建的时候,时间如果过期了,就不可选。现在还要可选。像这样的需求,首先要明白,这相当于开了一个口子。这个口子开多大,要根据实际情况定。

如本例中,肯定要修改,会议当天的时间,而且如果换了会议室,这个口子就不要看了。所以还需要保存,会议室的id。

'storeloaddata':function(dataview,store){
    var me=this;
    console.log(me.starttime);
    var indexs=[me.getTimeListIndex(me.starttime),me.getTimeListIndex(me.stoptime)-1];
    var oindexs=[me.getTimeListIndex(me.originalStarttime),me.getTimeListIndex(me.originalStoptime)-1];
    store.each(function(rd){
	//编辑会议时,之前的时间可编辑
	//if(me.originalMeetingdate&&rd.get('meetingdate')==me.originalMeetingdate){
	//注意,下面的条件进行了增强,要求是修改前的会议室id,才允许此特例。
	if(me.originalMeetingdate&&me.originalRoomId==me.conference_room_id&&rd.get('meetingdate')==me.originalMeetingdate){
	    var lis=rd.get('timelist');
	    for(var i=oindexs[0];i<=oindexs[1];i++){
		if(lis[i].status==1){
		    lis[i].status=0;
		}
	    }
	    rd.set({timelist:lis});
	    rd.commit();
	}
	//上面的代码,相当于开了一个口子。在会议修改时,有originalMeetingdate,
	//然后针对这一天,如果时间过期了,强制更新为状态0,即时间可用。而且这个应该还跟会议室相关。
	//如果更换了会议室,上面的代码应该不执行的。
	
	if(rd.get('meetingdate')==me.meetingdate){
	    var lis=rd.get('timelist');
	    for(var i=indexs[0];i<=indexs[1];i++){
		if(lis[i].status!=0){
		    Ext.toast('当前选择时间段不可用,请重新选择。');
		    return ;
		}
	    }
	    me.setSelectStyle(rd,indexs);
	}
    });
},

需要将会议的初始的originalMeetingdate、originalRoomId等信息保存起来。

 //添加会议时,设置默认项
ShineMessageHub.on("meetingorderhomeaddevent",function(rdData){
    me.originalMeetingdate=me.meetingdate = rdData['meeting_date'];//保存初始值
    me.originalStarttime=me.starttime = rdData['starttime'];    //保存初始值
    me.originalStoptime=me.stoptime = rdData['stoptime'];       //保存初始值
    me.originalRoomId=me.conference_room_id = rdData['conference_room_id'];   //保存初始值
    me.meeting_id = rdData['meeting_id'];
    me.displayStart=Ext.Date.format(new Date(),'Y-m-d');
    me.ajaxGetOccupy();
    me.idx=-1;
},me);

现在时间,增加会议详情

//info.desc变量比较简单,没有空格分隔,
//其实不加引号也没有问题。但是如果有空格等,就多了引号,显示就不正常了。
 title="{info.desc}"
 //简单形式,要求变量没有空格等。则显示也没有问题。  title={info.desc}

完整的模板代码,如下

tpl:[
    '<div class="selecttimepanel-right-box">',
	'<div class="arrow left-arrow"></div>',
	'<tpl for=".">',
	'<div class="selecttimepanelchoseitemCls"><table>',
	    '<tr><th>{displaydate}</th></tr>',
	    '<tpl for="timelist">',
	    '<tr>',
		'<td dataref={index} {[values.selected?"class=selecttimepanel-selected":""]} {[values.status==1?"class=selecttimepanel-expired":""]} {[values.status==2?"class=selecttimepanel-occupy":""]} title="{info.desc}">',
		    '{info.employee_name}',
		'</td>',
	    '</tr>',
	    '</tpl>',
	'</table></div>',
	'</tpl>',
	'<div class="arrow right-arrow"></div>',
    '</div>',
],

在模板里面进行比较麻烦,直接在store加载数据的时候,进行处理。

//计算时间是否占用
if(status===0){
    var occupy=me.isOccupy(items,start,stop);
    if(occupy!==false){
	status=2;
	info=occupy;
	//将所有的计算,在此进行,组合成一个字段。这样在模板里面,就没有复杂的允许。
	//看起来也比较简洁。
	info.desc='名称:'+info.meeting_name+';时间:'+info.starttime+'至'+info.stoptime;
	console.log(info.desc);
    }
}

后台增加代码

//修改
$sql="select meeting.user_id,employee_name,meeting_name,starttime,stoptime from meeting ".
	'LEFT JOIN USER ON `user`.user_id = meeting.user_id '.
	"where starttime>'$startStamp' and starttime<'$stopStamp' and status in(0,1) $where ".
	"order by starttime asc";
//原始
$sql="select meeting_name,starttime,stoptime from meeting ".
	"where starttime>'$startStamp' and starttime<'$stopStamp' and status in(0,1) $where ".
	"order by starttime asc";

将登录的数据保存起来

其实这个问题,并不是问题。只是当初设计的时候,根本没有想到这个东西,会共用。反复的初始化同一个组件,没有这个必要。应该在6个模块中,共有一个标题头实例。这样,一个更新,其他的也就更新了。但是框架已经写好了。就懒得动了。

将Ajax获取到的数据保存起来

getLoginUser:function(){
var me = this;
Ext.Ajax.request({
		url: SHINEVMSHTTP +'/admin/default/index/checklogin',
    success: function(response, opts) {
			var obj = Ext.decode(response.responseText);
	console.log(obj);
	if(obj&&obj.user&&obj.user.employee_name){
	    me.employee_name=obj.user.employee_name;//在这一步,将数据保存起来。
	    //后面这个发消息,需要相应的组件存在才行。
	    ShineMessageHub.fireEvent("mainmeetingtitlebarsetviewmodelevent",{'userName':obj.user.employee_name});
	}
    },
    failure: function(response, opts) {
	console.log('网络异常 ' + response.status);
    }
	});
}

主页点击事件,进行了拦截

if(record.data.id==2 ||record.data.id==6){
	var o=record.data.id==2?{xtype:"mymeetinghome"}:{xtype:"meetingmarkhome"};
	//注意,在这一步,为新添加的组件增加一个vmData属性,如果这个属性存在
	//新生成的组件会将其放到viewModel中。
	o.vmData={'userName':ctBox.employee_name};
	ctBox.items.items[record.data.id].removeAll();
	ctBox.items.items[record.data.id].add(o);
	setTimeout(() => {
	    Ext.fly(itemId).setStyle({
		border:"0px"
	    });
	    ctBox.getLayout().setActiveItem(record.data.id);
	}, 200);
	
    }

组件对新添加的属性,进行响应。

constructor:function(config){//后执行
	var me = this;
        me.superclass.constructor.call(me, config);
	//注意constructor的最后,才能使用me.down('mainmeetingtitlebar')这个方法
	//否则会找不到组件的。
	//这个应该是常用的技俩。习惯将vmData放到ViewModel中。
        if(me.vmData){
            me.down('mainmeetingtitlebar').getViewModel().setData(me.vmData);
        } 
       
},

会议预约后台数据导出功能

//indexshineloginsuccessevent这个事件,会在登录成功后,或者页面刷新后,都会发消息。
//在这个事件中接收消息。确保,ShineMessageHub.roleId始终都能获取到值。
//接收登陆成功消息
ShineMessageHub.on("indexshineloginsuccessevent",function(obj){
	me.getLayout().setActiveItem(0);
	ShineMessageHub.roleId=obj['role_id'];
});

对比:

form.submit({
    success: function (form, action) {
        Ext.Msg.hide();
        console.log(action["result"]["user"]);
        ShineMessageHub.fireEvent("indexshineloginsuccessevent", action["result"]["user"]);
	//注意,这个事件中,不能保证稳定触发。所以,应该
	//监听indexshineloginsuccessevent这个消息。不论是登录、还是刷新页面,
	//都能正常的获取到值。下面的代码直接删掉。
	ShineMessageHub.roleId = action["result"]["user"]['role_id'];
    },
    failure: function (form, action) {
        var msg = action["result"]["msg"];
        Ext.Msg.hide();
        msgPanel.update('<font color="red">' + msg + '</font>');
        //重置验证码
        Ext.get("safecode").dom.src = me.getCodeSrc();

    }
});

取消代码优化

这个是突发奇想,没有试过。用在桌面版会议室选择跟模板选择的功能。原本的限制取消选择功能,是写在selectionchange(this, selected, eOpts )事件中的。但是发现写在beforedeselect中更好,检测eOpts,如果点击了非item节点,然后return flase,来阻止取消选择功能。(好吧,感觉也还有一点复杂。)

beforedeselect ( this, record, index, eOpts ) 

更改radio的颜色

下面这个属性,干不过内联样式。

.x-meetingordermenu{//这个类名,自己为组件增加的cls属性。缩小范围。
    span.x-form-radio{
        color:#F3AD03;
    }
}

就是下面这个东西会产生在html内的style标签。

//设置radio的颜色
var radios = el.query('span.x-form-radio-default', false);
Ext.each(radios, function (radio) {
    radio.setStyle({
        color: '#FFFFFF'
    });
});

html中显示的style

<span id="radiofield-1026-displayEl" data-ref="displayEl" 
role="presentation" class="x-form-field x-form-radio x-form-radio-default x-form-cb x-form-cb-default   " 
style="color: rgb(255, 255, 255);">
<input type="radio" id="radiofield-1026-inputEl" name="meeting_type" data-ref="inputEl" class="x-form-cb-input" autocomplete="off" hidefocus="true" aria-hidden="false" aria-disabled="false" aria-invalid="false" aria-describedby="radiofield-1026-ariaStatusEl" data-componentid="radiofield-1026" style="background-color: rgb(17, 96, 150); color: rgb(255, 255, 255);">
</span>

提交失败

数据提交失败,区分失败的原因。

failure: function (form, action) {
    //action的属性,来区分失败的提示语。
    var message = action.failureType == "connect" ? '联网失败,请检查网络连接状态。' : '提交失败,请检查输入的数据是否完整。';
    Ext.Msg.hide();
    var win = Ext.widget({
        xtype: 'commonmsgwin',
        vmData: {
            title: '操作提示',
            message: message,
            hiddenLeft: true
        },
        rightHandler: function () {
            win.destroy();
        }
    }).show();
}

选择时间的tbar标签的宽度样式

设置一个最小宽度。防止太小挤变形。

tbar: [{
        xtype: 'label',
        text: '查看会议室使用情况',
        flex: 1,
        minWidth: 230, //增加一个宽度,防止太小挤变形。
        style: {
            color: 'black',
            fontSize: '20px',
            lineHeight: '20px'
        }
    }]

优化toast效果

本方案采用两种方式同时进行优化,第一种,屏蔽用户频繁点击。如果距离上次点击的时间太近,则直接不响应。直到下次响应后,记录下响应的时间。(相当于锁机制)。另外一种机制,如果用户点击的时候,发现还有其他的相同的实例存在,干掉他们。然后自己再显示出来。

巧了,在框架里面也发现了跟我命令差不多的变量,lastTouchTime。这样的话,也能优化,自己的代码呢。

Ext.isTouchMode = function () {
    return (Ext.now() - Ext.lastTouchTime) < 500;
};
//
Ext.define("Ext.locale.zh_CN.window.Toast", {	
    override: "Ext.window.Toast",
    listeners:{
        'beforeshow':function(cmp, eOpts){
            var t=Ext.all('toast');
	    
            var now=new Date().getTime();
            if(Ext.lastToastTime&&(now-Ext.lastToastTime)<1000){
                return false;
            }
            Ext.lastToastTime = now;
            Ext.each(t,function(tst){
                if(tst!==cmp){
                    tst.destroy();
                }
            });
        } 
    },
});

另外,now的定义如下(为啥多个+号,就能将时间变成时间戳。这是什么骚操作?):

Ext.now = Date.now || (Date.now = function() {
        return +new Date();
});

另外,发现,这个组件好像也自动监测了,其他的toast组件。尤其是me.getToasts()函数。这个消息提示,其实做得真心的复杂,功能多。但是,展示出来的效果却不是我们想要的,只能更改。

removeFromAnchor: function () {
    var me = this,
        activeToasts, index;

    if (me.anchor) {
        activeToasts = me.getToasts();
        index = Ext.Array.indexOf(activeToasts, me);
        if (index !== -1) {
            Ext.Array.erase(activeToasts, index, 1);

            // Slide "down" all activeToasts "above" the hidden one 
            for (; index < activeToasts.length; index++) {
                activeToasts[index].slideBack();
            }
        }
    }
},

查找泄漏的变量

js的局部变量应该封装在局部,这样避免其他的地方引用此变量,造成意外错误,而且排查错误难。查找泄漏的变量,直接在控制台输入window,所有绑定到该变量上的其他的变量都是泄漏的变量或者全局变量。这是一种好方法,可以看到全局的变量的一些特征,如:ShineMessageHub监听的事件等。

以下为一个泄漏的局部变量。部分代码如下:

/************
     * 
     * 修改会议
     * 2019年1月17日
     * ***********/
    editMeeting:function(record){
        var me=this;
        var menu=me.down('meetingordermenu');
        me.setItemIndex=2;
	//这一块忘了声明了,结果rdData泄漏成全局变量
        rdData = Ext.clone(record['data']);
        console.log(rdData);

条件判断

逻辑运算具有短路效应,优先将容易返回值的放在前面。&& 将容易为false的放在前面。||将容易为真的放在前面。

php代码:

//为了保护访问,临时增加了一个password进行保护。
if (isset($params['password']) && $params['password'] == 'clearmeeting') {
    $now = time();
    //前面的表达式更容易返回值,所以放在前面。
    if ($now < strtotime(date('Y-m-01 06:00')) && $now > strtotime(date('Y-m-01 01:00'))) {
        $data = MeetingListUtils::clearMeeting($params);
        echo Zend_Json::encode($data);
    } else {
        echo Zend_Json::encode(array('code' = > 2, 'msg' = > "未到月初清理时间,当前时间".date('Y-m-d H:i:s')));
    }
}

store加载拦截

之所以要拦截,是因为就有axios拦截器,而store却没有,如果store加载请求的时候,并没有使用Ext.Ajax进行请求。按找传统html的设计,如果用户没有登录,可以通过重定向,让用户跳转到登录界面。但是ajax却不能让浏览器的页面跳转。所以,想在ajax里面让其跳转。

proxy: {
    type: 'ajax',
    // url: SHINEVMSHTTP + '/admin/meeting/list/mymeeting',
    url: SHINEVMSHTTP + "/admin/desktop/mymeetinglist/getmeetinginfo",
    reader: {
        type: 'json',
        rootProperty: 'result',
        totalProperty: 'count'
    },
    //注意,这个proxy是能响应事件的。
    listeners: {
        'beginprocessresponse': function () {
            console.log('猜猜我是谁?阴险');
            return false;//这个并不会组织解析
        }
	//另外还能响应如下事件。以下事件是在源代码Ext.define('Ext.data.ProxyStore', 中发现。
        //endprocessresponse
        //exception
        //metachange
    }
},

获取部门树形结构时,导致死循环

在获取树形结构时,当前id跟parent_id一致,导致了死循环。

/**********
*部门列表
**********/
public static function getDeptTree($params){
	$db = Zend_Registry::get('db');
	//在此处进行了限制。
	$sql = "select * from department where department_id !=department_parent_id";
	$res = $db->query($sql);
	$rows = $res->fetchAll();
	$data = self::getChild($rows,"null",$params);
	if(isset($params['format'])&&$params['format']==='object'){//转换对象json格式
		return $data[0];
	}
	return $data;
}

会议室选择时间设计

会议室选择时间,根据效果图来看,样式深度定制。外观第一印象,就像一个Grid表格。初选方案有两种:grid、dataview。

方案分析

第一种方案,grid,默认的数据都是按行来显示,而效果图则是竖向显示,每天的情况按列显示。不管是grid、dataview都跟数据离不开关系。如果是竖向的展示,那么grid
来显示,还需要将数据横竖方向进行转换,这个难度也大,加上grid表格的是深度定制的样式,选择grid来做,困难很大。
第二种方案,dataview样式可以轻松定制,这个是自定义样式并要处理大量数据的法宝之一。数据的显示的横竖向,都可以通过css样式来定义,而且html骨架都是可控的。基于此,更倾向于选择dataview。

阅读全文 »

提交表单设计

增加了提交前的验证函数,利用一个dict映射数组,保存提示消息保。并利用循环,将映射的数组的关系进行查找,如果有此值,则提示响应的内容。

需要改进的地方。能否省略这个dict数组,将其存放到组件的属性中,直接获取组件的提示消息?

猜想,估计能实行,利用getFields来进行如下操作。

var me=this;
var form =me.down('form').getForm();
var data=form.getFields();
//然后再遍历这个数组,如果有自定义的字段,则提示响应的消息。

完整的代码如下:

formSubmit:function(){
    var me=this;
    var form =me.down('form').getForm();
    var data=form.getValues();
    //对数据进行验证
    if(!me.formIsValid()){
        return;
    }
    Ext.Msg.wait("保存中,请稍后... ...","操作提示");
    form.submit({
        submitEmptyText:false,
        success: function(form, action) {
            Ext.Msg.hide();
            if(parseInt(action.result.code)>0){//数据验证失败
                var msg=action.result.msg;
                if(action.result.code==1){//一般错误提示
                    Ext.Msg.show({
                        title:"操作提示",
                        msg:msg,
                        icon:Ext.Msg.WARNING,
                        buttons:Ext.Msg.OK,
                        minWidth:300
                    });
                }
                if(action.result.code==2){//时间冲突
                    Ext.Msg.show({
                        title: "操作提示",
                        msg: msg,
                        minWidth: 300,
                        icon: Ext.Msg.QUESTION,
                        buttons: Ext.Msg.YESNO,
                        fn: function (idx) {
                            if ("yes" == idx) {
                                //显示错误列表
                                //me.showConflictMeetingGrid(action.result.data,action.result.interval_time);
                            }
                        }
                    });
                }
            }else{//数据验证成功
                Ext.Msg.show({
                    title:"操作提示",
                    msg:"保存成功!",
                    icon:Ext.Msg.INFO,
                    buttons:Ext.Msg.OK,
                    fn:function(){						
                       
                    }
                });
            }
        },
        failure: function(form, action) {
            Ext.Msg.hide();
            Ext.Msg.show({
                title:"操作提示",
                msg:"编辑失败,请检查输入的数据是否完整。",
                icon:Ext.Msg.ERROR,
                buttons:Ext.Msg.OK
            });
        }
    });
},
/***********
 * 2019年1月18日
 * 表单验证
 ***********/
formIsValid:function(){
    var me=this;
    var form =me.down('form').getForm();
    var data=form.getValues();
    var msg=[];
    var dict={
        'conference_room_id':'会议室',
        'meeting_type':'会议类型',
        'meeting_video':'会议属性',
        'meeting_logo':'桌牌设置',
        'starttime':'时间',
        'meeting_name':'主题',
        'speaker':'主讲人',
        'attend_desc':'参与者',
        'template_id':'模板'
    };
    for(var key in dict){
        if(!data[key]){
            msg.push(dict[key]);
        }
    }
    if(msg.length>0){
        Ext.toast('请完善以下字段:'+msg.join('、')+'!');
    }
    return msg.length===0;
},

题外话

本来想利用,input输入错误后,特殊的css样式来提示用户输入错误的,结果发现这样做,由于html的差别较大,实现起来较难。

//这个是input标签,输入错误的对比
x-form-field x-form-required-field x-form-text x-form-text-default  x-form-empty-field x-form-empty-field-default x-form-invalid-field x-form-invalid-field-default
x-form-field x-form-required-field x-form-text x-form-text-default 
//这个是外层包裹的div,输入错误的对比
x-form-text-wrap x-form-text-wrap-default x-form-text-wrap-invalid
x-form-text-wrap x-form-text-wrap-default
//上面的方式,只适用于input框。

快速测试

其实有的时候,想要验证一个功能是否正常,或者自己不确定的时候,最好能有一个地方能快速验证,代码的地方。

快速删除创建的垃圾数据

快速搭建的时候,虽然功能某一部分不完整。但是不影响,能快速的提交表单,进行测试。
将其中的一个字段设置为特征值,然后快速建立会议,然后删除。
然后将后续环节直接停掉。一步步验证这个环节没有问题,再打通。

SELECT * FROM meeting where meeting_name ='桌面会议预约测试';
DELETE from meeting where meeting_name ='桌面会议预约测试';