本书未发布

88. 边栏资源推荐

未匹配的标注

简介

在本节里,我们完成话题列表和分类话题列表页右侧边栏的资源推荐功能。

需求分解

在本节里,我们需要完成推荐资源的前后台所有功能开发。

后台模块

  • 有权限的管理员用户才可以访问 CURD 页面;
  • 考虑到推荐资源不会很多,所以列表页不需要搜索和分页显示,一次列出所有记录;
  • 添加和编辑时,资源标题不能为空、必须唯一并且长度在 3-50 个字符之间,资源链接不能为空、必须是 URL 格式并且长度不超过 100 个字符;
  • 和其它控制器请求方式保持一致,创建表单提交是 POST 请求、编辑表单提交是 PUT 请求,删除操作是 DELETE 请求;
  • 点击删除用户时需要弹出确认提示,当管理员确认后再能删除;
  • 考虑到推荐资源更新频率比较低,为了减少数据库读写次数和服务器访问压力,在前台展示时会缓存数据,所以进行添加、编辑和删除操作后需要清空前台已有缓存。

前台模块

  • 在话题列表和分类话题列表页右侧边栏显示推荐资源列表;
  • 读取推荐资源列表时,优先使用已有缓存数据。

数据字典

字段名称 描述 字段类型 加索引缘由 其他
titie 标题 字符串(string) 不需要 'default => 0, 'limit' => 50
url 链接地址 字符串(string) 不需要 'default => 0, 'limit' => 200
  • default => xx—— 为字段添加默认值;
  • limit => xx—— 为字符串字段指定最大长度。

数据迁移

我们为资源推荐模型取名 Link, 首先我们创建数据库表迁移:

$ php think migrate:create CreateTableLink

在迁移文件里我们先为数据表插入几条默认的推荐资源,完整代码如下:

database/migrations/xxxxxxxxxxxxxx_create_table_link.php

<?php

use think\migration\Migrator;
use think\migration\db\Column;
use app\common\model\Link;

class CreateTableLink extends Migrator
{
    public function up()
    {
        $table = $this->table('link',array('engine'=>'InnoDB'));
        $table->addColumn('title', 'string', array('limit' => 50, 'default' => '', 'null' => false, 'comment' => '标题'))
            ->addColumn('url', 'string', array('default' => '', 'null' => true, 'comment' => '链接'))
            ->addColumn('create_time', 'integer', array('default' => 0, 'signed' => false, 'null' => true))
            ->addColumn('update_time', 'integer', array('default' => 0, 'signed' => false, 'null' => true))
            ->save();

        $current_time = time();
        $links = [
            [
                'title'        => 'ThinkPHP官网',
                'url' => 'http://www.thinkphp.cn/',
                'create_time' => $current_time,
                'update_time' => $current_time,
            ], [
                'title'        => '看云官网',
                'url' => 'https://www.kancloud.cn/explore',
                'create_time' => $current_time,
                'update_time' => $current_time,
            ], [
                'title'        => 'Laravel China社区',
                'url' => 'https://learnku.com/laravel',
                'create_time' => $current_time,
                'update_time' => $current_time,
            ],
        ];

        Link::insertAll($links);
    }

    public function down()
    {
        $this->dropTable('link');
    }
}

注意: 因为我们在迁移文件里使用 Link 模型来添加初始化数据,所以当我们完成 Link 数据库模型声明后再执行迁移命令生成数据库表。

验证器

接下来我们使用命令行工具创建推荐资源的验证器:

$ php think make:validate common/Link

验证器代码如下:

application/common/validate/Link.php

<?php

namespace app\common\validate;

use think\Validate;

class Link extends Validate
{
    protected $rule = [
        'title' => 'require|length:3,50|unique:link',
        'url' => 'require|max:100|url',
    ];

    protected $message = [
        'title.require' => '标题不能为空',
        'title.length' => '标题不能少于3个字符',
        'title.unique' => '当前标题已存在',
        'url.require' => '资源链接不能为空',
        'url.max' => '资源链接不能超过100个字符',
        'url.url' => '资源链接必须是URL',
    ];
}

观察者

因为推荐资源更新概率比较小,而话题列表访问频率比较大,所以我们在查询推荐资源时会缓存一段时间,但我们还需要创建一个观察者来监听推荐资源创建、更新或删除事件使当前缓存数据失效。

application/common/observer/Link.php

<?php

namespace app\common\observer;

use app\common\model\Link as LinkModel;

class Link
{
    public function afterWrite(LinkModel $topic)
    {
        LinkModel::clearCached();
    }

    public function afterDelete(LinkModel $topic)
    {
        LinkModel::clearCached();
    }
}

数据模型

使用命令行工具创建推荐资源的数据模型:

$ php think make:model common/Link

数据模型代码如下:

application/common/model/Link.php

<?php

namespace app\common\model;

use think\Model;
use think\facade\Cache;
use app\common\validate\Link as Validate;
use app\common\exception\ValidateException;

class Link extends Model
{
    // 缓存主键
    protected const CACHE_KEY = 'links';
    // 缓存有效时长(秒)
    protected const CACHE_SECONDS = 1440 * 60;

    protected static function init()
    {
        self::observe(\app\common\observer\Link::class);
    }

    /**
     * 查询出所有资源数据并缓存
     * @Author   zhanghong(Laifuzi)
     * @DateTime 2019-06-04
     * @return   array              [description]
     */
    public static function selectAll()
    {
        $links = Cache::store('redis')->get(self::CACHE_KEY);
        if(!empty($links)){
            // 当缓存有数据时直接返回缓存数据
            return $links;
        }

        $links = self::order('id', 'ASC')->select();
        // 当查询结果写入缓存
        Cache::store('redis')->set(self::CACHE_KEY, $links, self::CACHE_SECONDS);
        return $links;
    }

    /**
     * 清除缓存数据
     * @Author   zhanghong(Laifuzi)
     * @DateTime 2019-06-04
     * @return   [type]             [description]
     */
    public static function clearCached()
    {
        Cache::store('redis')->rm(self::CACHE_KEY);
    }

    /**
     * 后台模块搜索方法
     * @Author   zhanghong(Laifuzi)
     * @DateTime 2019-06-28
     * @param    array              $params    [description]
     * @param    integer            $page_rows [description]
     * @return   [type]                        [description]
     */
    public static function adminPaginate($params = [], $page_rows = 15)
    {
        $self = self::order('id', 'ASC');
        $map = [];
        foreach ($params as $name => $text) {
            $text = trim($text);
            switch ($name) {
                case 'keyword':
                    if(!empty($text)){
                        $like_text = '%'.$text.'%';
                        $self = $self->whereLike('title', $like_text);
                    }
                    break;
            }
        }
        return $self->paginate($page_rows, false, ['query' => $params]);
    }

    /**
     * 创建记录
     * @Author   zhanghong(Laifuzi)
     * @DateTime 2019-06-21
     * @param    array              $data 表单提交数据
     * @return   Topic                    [description]
     */
    public static function createItem($data)
    {
        $validate = new Validate;
        if(!$validate->batch(true)->check($data)){
            $e = new ValidateException('数据验证失败');
            $e->setData($validate->getError());
            throw $e;
        }

        try{
            $link = new self;
            $link->allowField(true)->save($data);
        }catch (\Exception $e){
            throw new \Exception('创建资源链接失败');
        }

        return $link;
    }

    /**
     * 更新记录
     * @Author   zhanghong(Laifuzi)
     * @DateTime 2019-06-21
     * @param    array              $data [description]
     * @return   [type]                   [description]
     */
    public function updateInfo($data)
    {
        $data['id'] = $this->id;

        $validate = new Validate;
        if(!$validate->batch(true)->check($data)){
            $e = new ValidateException('数据验证失败');
            $e->setData($validate->getError());
            throw $e;
        }

        $this->allowField(true)->save($data, ['id' => $this->id]);
        return $this;
    }
}

接下来,我们运行迁移命令完成数据表的创建:

$ php think migrate:run

后台管理

控制器

首先,我们在后台模块完成推荐资源管理:

$ php think make:controller admin/Link

在后台我们要完成推荐资源的 CURD 操作,所以完整代码如下:

application/admin/controller/Link.php

<?php

namespace app\admin\controller;

use think\Request;
use think\facade\Session;
use tpadmin\controller\Controller;
use app\common\model\Link as LinkModel;
use app\common\exception\ValidateException;

class Link extends Controller
{
    public function index(Request $request)
    {
        $param = $request->param();
        $paginate = LinkModel::adminPaginate($param);
        $this->assign('param', $param);
        $this->assign('paginate', $paginate);
        return $this->fetch('link/index');
    }

    public function create()
    {
        $this->assign('link', []);
        return $this->fetch('link/form');
    }

    public function save(Request $request)
    {
        if(!$request->isAjax()){
            $this->redirect('[admin.link.create]');
        }

        try{
            $data = $request->post();
            $link = LinkModel::createItem($data);
        }catch (ValidateException $e){
            return $this->error($e->getMessage(), '', ['errors' => $e->getData()]);
        }catch (\Exception $e){
            return $this->error($e->getMessage());
        }

        $message = '创建成功';
        Session::flash('success', $message);
        $this->success($message, url('[admin.link.index]'));
    }

    public function edit($id)
    {
        $link = LinkModel::find($id);

        $message = null;
        if(empty($link)){
            $message = '编辑资源不存在';
        }

        if(!empty($message)){
            Session::flash('alert', $message);
            $this->redirect('[admin.link.index]');
        }

        $this->assign('link', $link);
        return $this->fetch('link/form');
    }

    public function update(Request $request, $id)
    {
        if(!$request->isAjax()){
            $this->redirect(url('[admin.link.edit]', ['id' => $id]));
        }

        $link = LinkModel::find($id);

        if(empty($link)){
            $this->error('编辑资源不存在', '[admin.link.index]');
        }

        try{
            $data = $request->post();
            $link->updateInfo($data);
        }catch (ValidateException $e){
            $this->error($e->getMessage(), '', ['errors' => $e->getData()]);
        }catch (\Exception $e){
            $this->error($e->getMessage());
        }

        $message = '更新成功';
        Session::flash('success', $message);
        $this->success($message, url('[admin.link.index]'));
    }

    public function delete($id)
    {
        $link = LinkModel::find($id);

        if(empty($link)){
            $this->error('删除资源不存在', '[admin.link.index]');
        }

        $link->delete();

        $message = '删除成功';
        Session::flash('success', $message);
        $this->success($message, '[admin.link.index]');
    }
}

路由

在配置文件里定义控制方法访问路由规则:

route/admin_content.php

<?php

Route::group([
    'name' => 'admin',
    'middleware' => ['tpadmin.admin', 'tpadmin.admin.role'],
], function () {
    .
    .
    .
    // 资源管理
    Route::post('link', 'Link@save')->name('admin.link.save');
    Route::get('link/create', 'Link@create')->name('admin.link.create');
    Route::get('link/<id>/edit', 'Link@edit')->name('admin.link.edit');
    Route::put('link/<id>', 'Link@update')->name('admin.link.update');
    Route::delete('link/<id>', 'Link@delete')->name('admin.link.delete');
    Route::get('link', 'Link@index')->name('admin.link.index');
})->prefix('\\app\\admin\\controller\\');

视图模板

  1. 资源列表页:

application/admin/view/link/index.html

{extend name="layout:base" /}
{block name="main_content"}
<div class="row maintop">
    <div class="col-xs-10 col-sm-5">
        <form name="form_search" class="form-search" method="get">
            <div class="input-group">
                <span class="input-group-addon">
                    <i class="ace-icon fa fa-check"></i>
                </span>
                <input type="text" name="keyword" id="keyword" class="form-control" value="{$param.keyword|default=''}" placeholder="请输入关键词" />
                <span class="input-group-btn">
                    <button type="submit" class="btn btn-purple btn-sm">
                        <span class="ace-icon fa fa-search icon-on-right bigger-110"></span>
                        搜索
                    </button>
                </span>
            </div>
        </form>
    </div>
    <div class="col-xs-4 col-sm-5">
        <div class="input-group-btn">
            <a href="{:url('[admin.link.index]')}">
                <button type="button" class="btn btn-sm  btn-purple">
                    <span class="ace-icon fa fa-globe icon-on-right bigger-110"></span>
                    显示全部
                </button>
            </a>
        </div>
    </div>
    <div class="col-xs-4 col-sm-2">
        {if auth_check('link/create', $current_adminer->id) }
            <a href="{:url('[admin.link.create]')}">
                <button class="btn btn-sm btn-primary">
                    <i class="ace-icon fa fa-plus bigger-110"></i>添加资源
                </button>
            </a>
        {/if}
    </div>
</div>

<div class="row">
    <div class="col-xs-12">
        <table class="table table-striped table-bordered table-hover" id="dynamic-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>标题</th>
                    <th>URL</th>
                    <th>创建时间</th>
                    <th class="center">操作</th>
                </tr>
            </thead>
            <tbody>
                {foreach $paginate->all() as $key => $link }
                <tr>
                    <td>{$link->id}</td>
                    <td>{$link->title}</td>
                    <td>
                        <a href="{$link->url}" target="_blank">
                            {$link->url}
                        </a>
                    </td>
                    <td>{$link->create_time}</td>
                    <td class="center">
                        {if auth_check('link/edit', $current_adminer->id) }
                            <a class="green" href="{:url('[admin.link.edit]', ['id' => $link.id])}">
                                <i class="ace-icon fa fa-pencil bigger-130"></i>编辑
                            </a>
                        {/if}
                        {if auth_check('link/delete', $current_adminer->id) }
                            <a class="red" herf="javascript:void(0);" onclick="alert_del(this);" data-del-href="{:url('[admin.link.delete]', ['id' => $link.id])}" data-del-id='{$link.id}'>
                                <i class="ace-icon fa fa-trash-o bigger-130"></i>删除
                            </a>
                        {/if}
                    </td>
                </tr>
                {/foreach}
            </tbody>
        </table>
        <div>
            <?php echo($paginate->render()); ?>
        </div>
    </div>
</div>
{/block}
  1. 资源创建或编辑表单页

application/admin/view/link/form.html

{extend name="layout:base" /}
{block name="main_content"}
<?php if(isset($link['id'])): ?>
<form class="form-horizontal adminform" id="model-form" method="post" action="{:url('[admin.link.edit]', ['id' => $link.id])}">
    <input type="hidden" name="_method" value="PUT">
<?php else: ?>
<form class="form-horizontal adminform" id="model-form" method="post" action="{:url('[admin.link.save]')}">
<?php endif; ?>
    <div class="form-group">
        <label class="col-sm-2 control-label no-padding-right" for="form-field-1">
            标题:<span class="red">*</span>
        </label>
        <div class="col-sm-10">
            <input type="text" name="title" placeholder="例:ThinkPHP官网" class="col-xs-10 col-sm-5" value="{$link.title|default=''}" required/>
        </div>
    </div>
    <div class="space-4"></div>

    <div class="form-group">
        <label class="col-sm-2 control-label no-padding-right" for="form-field-1">
            URL:<span class="red">*</span>
        </label>
        <div class="col-sm-10">
            <input type="text" name="url" placeholder="例:http://www.thinkphp.cn" class="col-xs-10 col-sm-5" value="{$link.url|default=''}" required/>
        </div>
    </div>
    <div class="space-4"></div>

    <div class="clearfix form-actions">
        <div class="col-md-offset-3 col-md-9">
            <button class="btn btn-info" type="submit">
                <i class="ace-icon fa fa-check bigger-110"></i>保存
            </button>

            &nbsp; &nbsp; &nbsp;
            <button class="btn" type="reset">
                <i class="ace-icon fa fa-undo bigger-110"></i>重置
            </button>
        </div>
    </div>
</form>
{/block}
{block name="page_js"}
<script src="/static/assets/plugins/jquery-validate/jquery.validate.min.js"></script>
<script type="text/javascript">
    jQuery(function($){
        validAndSubmitForm(
            "form#model-form",
            {
                "title":{
                    required: true,
                    minlength: 3,
                    maxlength: 50
                }, "url":{
                    required: true,
                    maxlength: 100,
                    url: true
                }
            },{
                "title":{
                    required: "标题不能为空",
                    minlength: "标题不能少于3个字符",
                    maxlength: "标题不能超过50个字符"
                }, "url":{
                    required: "资源链接不能为空",
                    maxlength: "资源链接不能超过100个字符",
                    url: "资源链接必须是URL"
                }
            }
        );
    });
</script>
{/block}

权限管理

admin 账号登录后台,添加以下路由规则并给 运营 角色组分配访问权限。

效果预览

operator 账号登录查看用户管理功能。

前台模块

因为我们需要在话题列表页和分类话题列表页都的右侧都要显示出『活跃用户』,所以我们在两个控制器里添加读取『推荐资源』数据并输入到视图模板。

  1. 话题列表页

application/index/controller/Topic.php

<?php
.
.
.
use app\common\model\Link as LinkModel;

class Topic extends Base
{
    .
    .
    .
    public function index(Request $request)
    {
        .
        .
        .
        $this->assign('links', LinkModel::selectAll());

        return $this->fetch('index');
    }
}
  1. 分类话题列表页

application/index/controller/Category.php

<?php
.
.
.
use app\common\model\Link as LinkModel;

class Category extends Base
{
    public function read(Request $request, $id)
    {
        .
        .
        .
        $this->assign('links', LinkModel::selectAll());

        return $this->fetch('topic/index');
    }
}

视图模板

上面我们在控制器方法里已经读取到了活跃用户数据,所以接下来我们在左边栏中把数据显示出来:

application/index/view/topic/_sidebar.html

.
.
.
{notempty name='links'}
    <div class="card mt-4">
        <div class="card-body pt-2">
            <div class="text-center mt-1 mb-0 text-muted">资源推荐</div>
            <hr class="mt-2 mb-3">
            {volist name='links' id='link'}
                <a class="media mt-1" href="{$link->url}" target="_blank">
                    <div class="media-body">
                        <span class="media-heading text-muted">{$link->title}</span>
                    </div>
                </a>
            {/volist}
        </div>
    </div>
{/notempty}

效果预览

现在我们已经完成了资源管理前后台模块的所有业务开发,现在在后台编辑一条推荐资源信息,刷新前台话题列表页可以看到右侧「推荐资源」也已经立即更新。

Git 版本控制

下面把代码纳入到版本管理:

$ git add -A
$ git commit -m "前端推荐资源"

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

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~