# yii2-ledap

## 安装

推荐使用 [composer](http://getcomposer.org/download/)来安装

输入:

```bash
composer require --prefer-dist ethercap/yii2-ledap "dev-master"
```

或在 `composer.json` 文件中加入如下内容:

```
"ethercap/yii2-ledap": "dev-master"
```

## 使用说明

### 简介

本代码主要用到如下框架：

* [Bootstrap4](https://getbootstrap.com/docs/4.3/getting-started/introduction/)
* [Vue 2.6](https://cn.vuejs.org/v2/guide/instance.html)
* [Vue-Bootstrap 2.0.4](https://bootstrap-vue.js.org/docs)
* [Ledap](https://github.com/ethercap/ledap)
* [yii2-api-base](https://github.com/ethercap/yii2-api-base)

### 起步

复制layout。在复制后，你可以编辑layout的内容。

```bash
cp vendor/ethercap/yii2-ledap/src/gii/layout.php xxx/views/layouts/main.php
```

### 代码自动生成

代码生成器与gii/crud的使用方法一致：

```bash
php yii gii/ledapCrud --controllerClass="\frontend\controllers\TestController" --modelClass="common\models\Test" --searchModelClass="\frontend\forms\TestSearch"
```

在运行后，你能够在浏览器中访问这些页面。生成的代码如下：

1. **xxController :** 处理http请求的yii2 Controller.
2. **views/xx/\*.php :** 模板文件，用来渲染html.
3. **views/xx/\*.api :** Api接口文件，用来渲染接口.
4. **xx/web/xx/\*.js :** 模板文件中对应的js文件，它会被在view中渲染出，并带有一个hash串。

生成的文件结构如下所示：

![代码文件列表](https://2137901372-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Llk_7DQsWPv43GT9ZZm%2F-Llo8Lgz9GtMP_g5wBLm%2F-LloBhiYAwYE-nKKFmOE%2FWX20190809-102539%402x.png?alt=media\&token=8cf6d972-c158-44d1-98f7-0a1bf16b2e15)

### 全局组件

#### 1. loading

```markup
<!-- 在.js中注册变量isLoading-->
<div class="page-loading-container" v-if="isLoading">
  <div class="page-loading">加载中…</div>
</div>
```

#### 2. toast

toast是对[vue-bootstrap的toast](https://bootstrap-vue.js.org/docs/components/toast)进行了封装，做了一些默认的配置及支持了html.

```javascript
// 弹出默认的toast
this.$toast("hello world");
// toast支持html,vm代表当前的vue component
this.$toast('<span class="text-danger" @click="vm.alert()">hello world</span>')
// toast配置,具体参数详见 https://bootstrap-vue.js.org/docs/components/toast
this.$toast("hello world", {
    variant: 'error/danger/info...',
    title: 'BootstrapVue Toast',
    autoHideDelay: 5000,    
});
//toast 与下面基本等价,下面不支持直接写html
 this.$bvToast.toast(message, options);
```

#### 3. alert

alert是对[vue-bootstrap的modal](https://bootstrap-vue.js.org/docs/components/modal)进行了封装，做了一些默认的配置及支持。

```javascript
//弹出alert
this.$alert("hello world");
//弹出alert,支持html，vm代表当前的vue component
this.$alert('<span class="text-danger" @click="vm.alert()">hello world</span>');
this.$alert("hello world", {
      variant: 'error/danger/info...',
      title: 'Confirmation',
      size: 'sm',
      buttonSize: 'sm',
      okVariant: 'success',
      headerClass: 'p-2 border-bottom-0',
      footerClass: 'p-2 border-top-0',
      centered: true
}).then(()=>{
    console.log("ok");
}).catch(()=>{
    console.log("error");
});
//alert与下面的调用基本等价
this.$bvModal.msgBoxOk(message, options);
```

#### 4. confirm

confirm是对[vue-bootstrap的modal](https://bootstrap-vue.js.org/docs/components/modal)进行了封装，做了一些默认的配置及支持。

```javascript
//弹出confirm
this.$confirm("hello world");
//弹出alert,支持html，vm代表当前的vue component
this.$confirm('<span class="text-danger" @click="vm.alert()">hello world</span>');
this.$confirm("hello world", {
      variant: 'error/danger/info...',
      title: 'Confirmation',
      size: 'sm',
      buttonSize: 'sm',
      okVariant: 'success',
      headerClass: 'p-2 border-bottom-0',
      footerClass: 'p-2 border-top-0',
      centered: true
}).then(()=>{
    console.log("ok");
}).catch(()=>{
    console.log("error");
});
//alert与下面的调用基本等价
this.$bvModal.msgBoxConfirm(message, options);
```

### 列表页

列表页的js有一个dataProvider来控制整个页面（注意：它与php的DataProvider并不是同一个东西）。

#### 1.输入时刷新数据

我们可以在form-item上加一个事件侦听，这样当我们输入时，下面的表格可以自动搜索并刷新。

```markup
<form-item :model="dp.searchModel" attr="name" @input="refresh('')"></form-item>
```

#### &#x20;2. 一些DataProvider的API

{% code title="index.js" %}

```javascript
// 生成DataProvider
this.dp = ledap.App.getWebDataProvider({
    httpOptions:{
        url:'/test/xxx',
        params:'ddd',
    }
    // 这个可以不填。填了之后，数据会以model.id进行判重，防止重复的数据展示
    primaryKey: 'id',
    // 可以不填，代表两个请求间的最小间隔，这在suggestion, http请求上极为有用。
    // 0 代表无间隔
    timeWait : 600,
});

// dataprovider同四部分组成。
// searchModel代表dp的搜索参数
console.log(this.dp.searchModel)
// 数据
console.log(this.dp.models);
// 列表页的排序器
console.log(this.dp.sort);
// 列表页的分页器
console.log(this.dp.pager);

// 修改参数并刷新
this.dp.searchModel.name = 'xxx';
this.dp.refresh();
// 也可以用如下的函数达到同样的效果
this.dp.changeParams({name:"xxx"});

// 修改页码
this.dp.changePage(page)
this.dp.nextPage();
this.dp.prePage();

// 我们有时候希望实现h5类似的上拉刷新和下拉刷新效果
this.dp.refresh("header");
this.dp.refersh("footer");

// 我们可能希望排序
this.dp.sort = "id, -name";
this.dp.refresh();
// 我们也可以直接通过函数达到同样的效果
this.dp.setSort("id,-name");

// 本地排序（不走http请求）
this.dp.sortModels("name", asc=true);

// 可以通过isLoading来判断当前的加载状态
this.dp.isLoading
// 我们也可以通过事件来做一些事情。
this.dp.on(ledap.WebDataProvider.EVENT_BEFOREGETDATA, function(){
});
this.dp.on(ledap.WebDataProvider.EVENT_AFTERGETDATA, function(){
});
```

{% endcode %}

#### &#x20;3. grid

grid是一个ledap组件，可以通过传入columns和dataProvider来生成表格

{% code title="index.js" %}

```javascript
columns : {
    'id',
    {
        'attribute' : 'id',
        'label' : 'ID',
        //如果设置了sort,label是可以点击来排序的。
        'userSort' : true, 
    },
    {
        'attribute' : 'name',
        'value' : function() {
            // vm是当前的vue Component
            // 在模板中我们能使用value,model, index, attribute 和 dataProvider 
            return '<a @click="vm.xxx(model)"></a>';
        },
        'format': 'html',
    }
}
```

{% endcode %}

如果你不满意默认的内容，你可以通过Vue Scoped slot来修改内容。组件会传输四个变量到scoped slot中：

* **model**. 当前的model, 代表grid的一行。
* **column**. 当前的colomn, 代表grid中的一列。
* **index**.  model在dp.models中的索引。dp.model\[index] == model。
* **value**. cloumn与model计算出来的结果。&#x20;

{% code title="index.php" %}

```markup
<grid class="table" :data-provider="dp" :columns="columns">
    <template v-slot:label="p">
        <th class="xxx">{{p.value}}{{p.model}}{{p.index}}{{p.column}}</th>
    </template>

    <template v-slot:default="p">
        <td class="xxx">{{p.value}}{{p.model}}{{p.index}}{{p.column}}</td>
    </template>
</grid>
```

{% endcode %}

你也可以使用dataProvider自己画view或使用list，而不是使用grid。

{% code title="index.php" %}

```markup
<input type="xxx" v-model="dp.searchModel.name" @input="dp.refresh('')" />
<div v-for="model in dp.models">
    <div class="">{{model.id}}</div>
    <div class="xxx">{{model.img}}</div>
</div>
<div class="pager">
    <div>total:{{dp.pager.currentPage}}|{{dp.pager.totalCount}}</div>
    <a @click="dp.prePage()">PrePage</a>
    <a @click="dp.nextPage()">NextPage</a>
</div>
```

{% endcode %}

### View\&Update\&Create

在页面中有type这个参数，可以被用来控制页面的显示。它可以是view, update或create。

#### 1.model

model与yii2的form Model 对应。基本用法如下：

{% code title="view\.js" %}

```javascript
//生成一个model
let model = ledap.App.getModel(data);

// 修改model的值
model.name = "xxx";
// 获取model某个属性的label
model.getAttributeLabel("name");
// 获取model某个属性的hit
model.getAttributeHint("name");
// 获取model某个属性的error
model.getErrors("name");
// 获取model某个属性的第一条错误
model.getFirstError("name");
// 获取model的所有错误
model.getErrors();

// 校验model的值.如果不正确，会返回false.
// 我们可以使用getErrors来展示错误
model.validate();

```

{% endcode %}

#### 2.detail

detail组件与grid组件类似。我们可以使用columns来展示一个detail，它的写法也与grid一致。

{% code title="view\.js" %}

```javascript
columns : {
    'id',
    {
        'attribute' : 'id',
        'label' : 'ID',
        //if use sort, we can click header to sort table
        'userSort' : true, 
    },
    {
        'attribute' : 'name',
        'value' : function() {
            // vm refer to current vue component
            // value,model, index, attribute and dataProvider can be use in the template
            return '<a @click="vm.xxx(model)">test</a>';
        },
        'format': 'html',
    }
}
```

{% endcode %}

与grid一样，我们也可以使用scoped slot来修改默认的页面。&#x20;

#### 3. form-item

当我们想要展示form时，form-item非常重要。它由四部分组成：

* label
* hint
* input
* error

we cant use like this:

{% code title="\_form.php" %}

```markup
<!-- 
    常规输入框
    validator: 什么时候去校验数据
    tag: 组件的tag，默认为div
-->
<form-item :model="model" attr="name" :validator="['input', 'blur', 'focus']" :tag="div">
</form-item>

<!-- 我们可以使有scoped slot来修改默认的页面-->
<form-item :model="model" attr="name">
    <template v-slot:label="p">
        <label> {{p.model.getAttributeLabel(p.attr)}}{{p.model.isRequired(p.attr) ? '*' : ''}}</label>
    </template>
    <template v-slot="p">
        <baseinput type="password" v-bind="p"></baseinput>
    </template>
    <template v-slot:error="p">
        <p v-show="p.showError">{{p.showError}}</p>
    </template>
</form-item>

<!-- 一些其它输入框 -->
<!-- 
    groupinput 需要DictValidator。你应该在yii2的model中添加如下的rule来使用:
    ['name', \ethercap\common\validators\DictValidator::className(), [0 => 'xxx', 1=> 'xxx', ...]],
    另外：dropdown也需要DictValidator.
    ajax select 你可以参见下一节。
-->
<form-item :model="model" attr="name">
    <template v-slot="p">
        <groupinput v-bind="p"></groupinput>
    </template>
</form-item>
<form-item :model="model" attr="name">
    <template v-slot="p">
        <dropdown v-bind="p"></dropdown>
    </template>
</form-item>
```

{% endcode %}

有时，我们想用其它第三方写的vue组件，所以，我在这个包里写了一个示例。我使用了 [vue2-datepicker](https://www.npmjs.com/package/vue2-datepicker)来示例.

{% code title="\_form.php" %}

```php
<?php
// DatePickerAsset中只是写了js的地址，我们也可以直接register来引入js
// $this->registerJs("http://xxxx.js", ['depends' => '\ethercap\ledap\assets\VueAsset'])
\ethercap\ledap\assets\DatePickerAsset::register($this);
?>

<form-item :model="model" attr="time">
    <template v-slot="p">
        <date-picker v-model="p.model[p.attr]" :value-type="'format'"></date-picker>
    </template>
</form-item>

```

{% endcode %}

另外，我们需要在Vue中注册这个组件：

{% code title="view\.js" %}

```javascript
Vue.component("date-picker", DatePicker.default);
```

{% endcode %}

### 其它组件

#### 1.select2 & SearchAction

有时，我们需要一个类似于select2的ajax suggestion。这个包也为你提供了：

* SearchAction: 一个 php Action 来处理搜索的请求.

```php
<?php
namespace frontend\controllers;
use Yii;
class xxController extends Controller
{
    // ....
    
    public function actions()
    {
        return [
            'search' => [
                'class' => \ethercap\ledap\actions\SearchAction::className(),
                'processQuery' => function($model) {
                     $query = xxx::find()->select(['id', 'name as text'])->asArray();
                     if($model->id) {
                         return $query->where(['id' => $model->id]);
                     }
                     if($model->keyword){
                         $query->andWhere(['like', 'name', $model->keyword]);                         
                     }
                     return $query;
                },
                // 接口返回的数据字段。默认如下，可以不写。
                $dataConfig => [
                    'id',
                    'text'
                ]
            ],
        ];
    } 

}
```

after all this config, we have an api "/xx/search" to search data from database.

在这些配置完成后，我们就拥有了一个api——"/xx/search"来从数据库中搜索数据。

* select2 component: 一个vue组件来发起http请求。

{% code title="\_form.php" %}

```markup
<form-item :model="model" attr="search">
    <template v-slot="p">
        <select2 v-bind="p" :data-provider="dp"></select2>
    </template>
</form-item>
```

{% endcode %}

{% code title="view\.js" %}

```javascript
{
    data:{
        dp: App.ledap.getWebDp({
            httpOptions:{
                "url" : '/xx/search'
            }
        });
    }
}
```

{% endcode %}

#### 2.upload & UploadAction

我们在php中引入js等依赖

```php
<?php
ethercap\ledap\assets\UploadAsset::register($this); 
```

在页面调用组件上传即可。

{% code title="\_form.js" %}

```javascript
<form-item class="form-group" :model="model" attr="attr">
    <template v-slot="p">
        <uploader post-action='http://xxx.com/upload' v-model="p.model[p.attr]"></uploader>
    </template>
</form-item>
```

{% endcode %}

后端可以通过很简单的方式来处理, 文件在$\_FILE中。

在数据库中，该数据对应的结构为:

```
$model->attr=json_encode([
    ['name'=>'xxxx', 'url' => 'xxx'],
    ['name'=>'xxxx', 'url' => 'xxx'],
]);
```

### 高阶应用

#### 自己实现vue组件/Ledap组件

在页面中，如果有一些元素总是重复，我们可以书写成组件方便复用，我们可以直接书写Vue组件，参见[vue组件](https://cn.vuejs.org/v2/guide/components-registration.html)。

ledap组件跟vue组件没有本质区别，但是引入了继承，下面我们来看一个例子：

我们一直使用formItem来做为form的包装器，现在我们想做一个formItem1, 但是把label和input的位置对调，我们可以这样做：

```javascript
//我们定义了一个叫form-item1的组件，它继承于form-item，除了template不同，其它的所有都与form-item一样（data, props, methods等）
ledap.App.getTheme().addComponent({
    name : 'form-item1',
    template:`<component :is="tag" class="form-group" :class="{'has-error':showError}">
             <div class="col-sm-9">
                 <slot :model="model" :attr="attr" :validate="validate" :inputListeners="inputListeners">
                     <input class="form-control" :name="attr" :value="model[attr]" :placeholder="model.getAttributeHint(attr)"  v-on="inputListeners" />
                 </slot>
                 <slot name="error" :model="model" :attr="attr" :showError="showError">
                     <p v-show="showError" class="help-block">{{showError}}</p>
                 </slot>
             </div>
             <slot name="label" :model="model" :attr="attr">
                 <label class="col-sm-3 control-label"> {{model.getAttributeLabel(attr)}}{{model.isRequired(attr) ? '*' :       ''}}</label>
             </slot>
     </component>`,
},'form-item');
```

#### 这样，我们可以在页面上看到效果了：

```markup
<!-- 注意，别忘记在js上注册ledap.App.getTheme().register(['form-item1'], Vue) -->
<form-item1 :model="model" :attr="xxx"> </form-item1>
```

#### 改用其它css框架

### Tips

1. 你需要了解Vue.
2. 尽量少用yii2 widget.
3. 所有你使用的组件都需要先注册。App.register(\['xxx'], Vue);
4. 你可以使用其它组件。
5. 你可以通过修改AppAsset来替换全局模板或http请求

这个包，前后端并不是完全分离的。如果你想要使用npm来实现完全分离，直接使用ledap前端框架即可。
