42.话题订阅(三)

未匹配的标注

本节说明

  • 对应视频教程第 42 小节:Thread Subscriptions(Part 3)

本节内容

在前两节我们为订阅功能建立了测试,本节我们在前端页面显示订阅按钮,并且实现订阅功能。首先新建组件:
forum\resources\assets\js\components\SubscribeButton.vue

<template>
    <button class="btn btn-default" @click="subscribe">Subscribe</button>
</template>

<script>
    export default {
        methods:{
            subscribe(){
                axios.post(location.pathname + '/subscriptions');

                flash('Subscribed');
            }
        }
    }
</script>

当我们点击Subscribe按钮时,我们发送Ajax请求,订阅话题。我们还需要在话题组件中引入该组件:
forum\resources\assets\js\pages\Thread.vue

<script>
    import Replies from '../components/Replies';
    import SubscribeButton from '../components/SubscribeButton';

    export default {
        props: ['initialRepliesCount'],

        components: { Replies,SubscribeButton},

        data() {
            return {
                repliesCount:this.initialRepliesCount
            }
        }
    }
</script>

接下里显示订阅组件:
forum\resources\views\threads\show.blade.php

    .
    .
    <div class="col-md-4">
        <div class="panel panel-default">
            <div class="panel-body">
                <p>
                    This thread was published {{ $thread->created_at->diffForHumans() }} by
                    <a href="#">{{ $thread->creator->name }}</a>,and currently
                    has <span v-text="repliesCount"></span> {{ str_plural('comment',$thread->replies_count) }}
                </p>

                <p>
                    <subscribe-button></subscribe-button>
                </p>
            </div>
        </div>
    </div>
    .
    .

编译后测试订阅功能:
file
可以看到订阅功能已经实现,但是目前我们还有两个小地方需要修改:

  1. 点击多次按钮,多次订阅话题;
  2. 点击按钮后,应该显示已订阅而不是继续显示订阅按钮;

首先我们来修复第一个问题:
forum\database\migrations{timestamp}_create_thread_subscriptions_table.php

    .
    .
    public function up()
    {
        Schema::create('thread_subscriptions', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->unsignedInteger('thread_id');
            $table->timestamps();
            $table->unique(['user_id','thread_id']);

            $table->foreign('thread_id')
                ->references('id')
                ->on('threads')
                ->onDelete('cascade');
        });
    }
    .
    .

我们为thread_subscriptions表加上了唯一性索引,所以我们需要运行迁移:

$ php artisan migrate:rollback
$ php artisan migrate

现在我们清空表数据,再次尝试:
file
是的,现在我们已经在数据库层面加上了限制,如果你再次点击订阅按钮,是不会增加另一条数据的。接下来我们对第二个问题进行调整:
forum\resources\views\threads\show.blade.php

    .
    .
    <p>
        <subscribe-button :active="true"></subscribe-button>
    </p>
    .
    .

forum\resources\assets\js\components\SubscribeButton.vue

<template>
    <button :class="classes" @click="subscribe">Subscribe</button>
</template>

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

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

        methods:{
            subscribe(){
                axios.post(location.pathname + '/subscriptions');

                flash('Subscribed');
            }
        }
    }
</script>

我们为SubscribeButton组件绑定了active属性,并且在classes根据active的属性值为Subscribe按钮赋予不同的样式。当然我们这里的active属性默认赋予的是true,我们会在之后动态获取。我们先来看一下页面效果:
file
如果我们把active的值改为false
file
现在我们进行下一步,动态获取active的值。我们将通过下面的方式来获取:

$thread->isSubscribedTo 

我们首先新建测试:
forum\tests\Unit\ThreadTest.php

    .
    .
    /** @test */
    public function it_knows_if_the_authenticated_user_is_subscribed_to_it()
    {
        // Given we have a thread
        $thread = create('App\Thread');

        // And a user who is subscribed to the thread
        $this->signIn();

        $this->assertFalse($thread->isSubscribedTo);

        $thread->subscribe();

        $this->assertTrue($thread->isSubscribedTo);
    }
}

接着我们定义一个 访问器getIsSubscribedToAttribute,并且使用 序列化 的方式,添加一个在数据库中没有对应字段的属性。然后我们就可以通过$thread->isSubscribedTo的方式来获取isSubscribedTo属性。
forum\app\Thread.php

.
.
class Thread extends Model
{
    use RecordsActivity;

    protected $guarded = [];
    protected $with = ['creator','channel'];
    protected $appends = ['isSubscribedTo'];
    .
    .
    public function getIsSubscribedToAttribute()
    {
        return $this->subscriptions()
            ->where('user_id',auth()->id())
            ->exists();
    }
}

最后别忘了我们修改绑定属性的方式:
forum\resources\views\threads\show.blade.php

.
.
<subscribe-button :active="{{ json_encode($thread->isSubscribedTo)}}"></subscribe-button>
.
.

我们来试一下效果:
file
我们最终想要实现的效果当然不仅仅是这样:在我们点击了按钮之后,样式发生改变,表示已订阅;我们再次点击按钮,样式再次改变,表示我们取消订阅。完善我们的组件:
forum\resources\assets\js\components\SubscribeButton.vue

<template>
    <button :class="classes" @click="subscribe">Subscribe</button>
</template>

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

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

        methods:{
            subscribe(){
                axios[
                        (this.active ? 'delete' : 'post')
                    ](location.pathname + '/subscriptions');

                this.active = ! this.active;
            }
        }
    }
</script>

我们根据this.active的值来决定发送delete或者post请求:值为true,我们发送delete请求,取消订阅;值为false,我们发送post请求,进行订阅。但是我们现在还没有delete请求的处理逻辑,我们需要增加。首先依然是新建测试:
forum\tests\Feature\SubscribeToThreadsTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class SubscribeToThreadsTest extends TestCase
{
    use DatabaseMigrations;

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

        // Given we have a thread
        $thread = create('App\Thread');

        // And the user subscribes to the thread
        $this->post($thread->path() . '/subscriptions');

        // Then,each time a new reply is left...
        $thread->addReply([
           'user_id' => auth()->id(),
           'body' => 'Some reply here'
        ]);

        // A notification should be prepared for the user.
//        $this->assertCount(1,auth()->user()->notifications);
    }

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

        // Given we have a thread
        $thread = create('App\Thread');

        // And the user unsubscribes from the thread
        $this->delete($thread->path() . '/subscriptions');

        $this->assertCount(0,$thread->subscriptions);
    }
}

接着增加路由:
forum\routes\web.php

.
.
Route::post('/threads/{channel}/{thread}/subscriptions','ThreadSubscriptionsController@store')->middleware('auth');
Route::delete('/threads/{channel}/{thread}/subscriptions','ThreadSubscriptionsController@destroy')->middleware('auth');
.
.

然后增加destroy方法:
forum\app\Http\Controllers\ThreadSubscriptionsController.php

    .
    .
    public function destroy($channelId,Thread $thread)
    {
        $thread->unsubscribe();
    }
}

运行测试:
file
测试通过,我们再在页面进行验证:
file

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。