Menu

34. Vue 点赞组件

本节说明

  • 对应视频教程第 34 小节:A Vue Favorite Component

本节内容

本节我们将回复点赞的表单换成 Vue 点赞组件。首先我们来修改视图:
forum\resources\views\threads\reply.blade.php

.
.
<div class="panel-heading">
    <div class="level">
        <h5 class="flex">
            <a href="{{ route('profile',$reply->owner) }}"> {{ $reply->owner->name }}</a>
            回复于
            {{ $reply->created_at->diffForHumans() }}
        </h5>

        <div>
            <favorite :reply="{{ $reply }}"></favorite>
        </div>
    </div>
</div>
.
.

接下来新增Favorite.vue组件:
forum\resources\assets\js\components\Favorite.vue

<template>
    <button type="submit" class="btn btn-default">
        <span class="glyphicon glyphicon-heart"></span>
        <span v-text="favoritesCount"></span>
    </button>
</template>

<script>
    export default {
        data() {
           return {
               favoritesCount: 10
           }
        }
    }
</script>

现在Favorite.vue依然是不可用的,因为我们想要在Reply.vue组件中使用Favorite.vue,首先必须引入它:
forum\resources\assets\js\components\Reply.vue

<script>
    import Favorite from './Favorite.vue';

    export default {
        props: ['attributes'],

        components: { Favorite },

        data() {
            return {
              editing: false,
              body: this.attributes.body
            };
        },

        methods:{
            update() {
                axios.patch('/replies/' + this.attributes.id,{
                    body:this.body
                });

                this.editing = false;

                flash('Updated!');
            },

            destroy() {
                axios.delete('/replies/' + this.attributes.id);

                $(this.$el).fadeOut(300, () => {
                    flash('Your reply has been deleted!');
                });
            }
        }
    }
</script>

现在刷新页面即可看到点赞组件:
file
但是我们现在点击按钮是不会有任何反应的,因为我们没有为按钮定义动作。让我们来梳理一下接下来要做的事情:首先,我们要给按钮定义动作,点击按钮,点赞该回复,再次点击则取消点赞;接下来,我们要正确显示点赞的数量。
首先,给按钮定义动作:
forum\resources\assets\js\components\Favorite.vue

<template>
    <button type="submit" :class="classes" @click="toggle">
        <span class="glyphicon glyphicon-heart"></span>
        <span v-text="favoritesCount"></span>
    </button>
</template>

<script>
    export default {
        props:['reply'],

        data() {
           return {
               favoritesCount: this.reply.favoritesCount,
               isFavorited:false
           }
        },

        computed: {
            classes() {
                return ['btn',this.isFavorited ? 'btn-primary' : 'btn-default']
            }
        },

        methods: {
            toggle() {
                if (this.isFavorited){
                    axios.delete('/replies/' + this.reply.id + '/favorites')
                }else {
                    axios.post('/replies/' + this.reply.id + '/favorites');

                    this.isFavorited = true;

                    this.favoritesCount++;
                }
            }
        }
    }
</script>

其实在以上我们已经补充完整了两个步骤所需要进行的动作。我们使用favoritesCount来获取点赞的数量。那么这个属性是如何得到的呢?我们在之前的章节中将获取点赞数的方法封装成了Favoritable Trait,并且在 Trait 定义了一个 访问器getFavoritesCountAttribute,然后我们通过$reply->favorites_count来获取favorites_count属性。但是我们在 Vue 组件中无法使用这样的方式获取到favorites_count
file
所以我们改为使用 序列化 的方式,添加一个在数据库中没有对应字段的属性。要使用序列化,首先你需要为这个值定义一个 访问器(我们已经创建)。访问器创建成功后,只需添加该属性到该模型的 appends 属性中:
forum\app\Reply.php

class Reply extends Model
{
    use Favoritable,RecordsActivity;

    protected $guarded = [];
    protected $with = ['owner','favorites'];
    protected $appends = ['favoritesCount'];
    .
    .

我们刷新页面:
file
可以看到favoritesCount属性已经添加到模型属性组当中,并且在 Vue 组件的属性组中。我们已经将点赞的动作补充完整,如果我们现在点击按钮,会发现按钮变色,同时点赞数变成了 1:
file
接下来我们进行取消点赞的动作。首先我们先新建一个测试:
forum\tests\Feature\FavoritiesTest.php

.
.
/** @test */
public function au_authenticated_user_can_unfavorite_a_reply()
{
    $this->signIn();

    $reply = create('App\Reply');

    $this->post('replies/' . $reply->id . '/favorites');

    $this->assertCount(1,$reply->favorites);

    $this->delete('replies/' . $reply->id . '/favorites');

    $this->assertCount(0,$reply->favorites);
}
.
.

添加路由:
forum\routes\web.php

.
.
Route::post('/replies/{reply}/favorites','FavoritesController@store');
Route::delete('/replies/{reply}/favorites','FavoritesController@destroy');
.
.

添加destroy()方法:
forum\app\Http\Controllers\FavoritesController.php

    .
    .
    public function destroy(Reply $reply)
    {
        $reply->unfavorite();
    }
}

添加unfavorite()方法:
forum\app\Favoritable.php

.
.
public function favorite()
{
    $attributes = ['user_id' => auth()->id()];

    if (!$this->favorites()->where($attributes)->exists()) {
        return $this->favorites()->create($attributes);
    }

}

public function unfavorite()
{
    $attributes = ['user_id' => auth()->id()];

    $this->favorites()->where($attributes)->delete();
}
.
.

现在我们可以运行测试:
file
测试未通过,原因是什么呢?如果你对之前的内容记得很清楚的话,那么你应该记得我们是通过Reply模型的$with属性组来获取favorites
forum\app\Reply.php

.
.
class Reply extends Model
{
    use Favoritable,RecordsActivity;

    protected $guarded = [];
    protected $with = ['owner','favorites'];
    protected $appends = ['favoritesCount'];
    .
    .

而我们使用$reply->favorites来获取点赞数,这会让我们预加载favorites属性,所以我们的第二个断言未通过:

.
.
$this->assertCount(0,$reply->favorites);
.
.

我们需要使用fresh()函数:

.
.
$this->assertCount(0,$reply->fresh()->favorites);
.
.

再次测试,测试通过:
file
现在我们可以进行一些重构:

.
.
/** @test */
public function au_authenticated_user_can_unfavorite_a_reply()
{
    $this->signIn();

    $reply = create('App\Reply');

    $reply->favorite();

    $this->delete('replies/' . $reply->id . '/favorites');

    $this->assertCount(0,$reply->favorites);
}
.
.

再次测试:
file
到目前为止,我们取消点赞的动作已经完成,接下来我们只需要完善我们的组件即可:
forum\resources\assets\js\components\Favorite.vue

<script>
    export default {
        props:['reply'],

        data() {
           return {
               favoritesCount: this.reply.favoritesCount,
               isFavorited:false
           }
        },

        computed: {
            classes() {
                return ['btn',this.isFavorited ? 'btn-primary' : 'btn-default']
            }
        },

        methods: {
            toggle() {
                if (this.isFavorited){
                    axios.delete('/replies/' + this.reply.id + '/favorites');

                    this.isFavorited = false;
                    this.favoritesCount--;
                }else {
                    axios.post('/replies/' + this.reply.id + '/favorites');

                    this.isFavorited = true;
                    this.favoritesCount++;
                }
            }
        }
    }
</script>

现在我们刷新页面,已经可以完整地进行点赞和取消点赞行为了。但是,我们默认了isFavorited属性是false

isFavorited:false

我们该如何正确获取是否已经进行过点赞行为呢?答案是 序列化 。要使用序列化,首先我们要定义一个 访问器
forum\app\Favoritable.php

    .
    .
    public function getIsFavoritedAttribute()
    {
        return $this->isFavorited();
    }

    public function getFavoritesCountAttribute()
    {
        return $this->favorites->count();
    }
}

接着将isFavorited添加到模型的$with属性组:
forum\app\Reply.php

.
.
protected $appends = ['favoritesCount','isFavorited'];
.
.

最后我们在组件中使用:
forum\resources\assets\js\components\Favorite.vue

.
.
data() {
   return {
       favoritesCount: this.reply.favoritesCount,
       isFavorited:this.reply.isFavorited
   }
},
.
.

现在你可以刷新页面进行测试。你知道接下来我们要做什么吗?重构。最后让我们来重构组件,使其更具可读性:

<template>
    <button type="submit" :class="classes" @click="toggle">
        <span class="glyphicon glyphicon-heart"></span>
        <span v-text="count"></span>
    </button>
</template>

<script>
    export default {
        props:['reply'],

        data() {
           return {
               count: this.reply.favoritesCount,
               active:this.reply.isFavorited
           }
        },

        computed: {
            classes() {
                return ['btn',
                    this.active ? 'btn-primary' : 'btn-default'
                ];
            },

            endpoint() {
                return '/replies/' + this.reply.id + '/favorites';
            }
        },

        methods: {
            toggle() {
                    this.active ? this.destroy() : this.create();
                },

            create() {
                axios.post(this.endpoint);

                this.active = true;
                this.count++;
            },

            destroy() {
                axios.delete(this.endpoint);

                this.active = false;
                this.count--;
            }
        }
    }
</script>

再次刷新页面进行测试。最后的最后,运行一下全部测试:
file
Perfect!

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 1
发起讨论