前后端交互—使用自己的服务器开发项目

代码下载

完善服务器

对于上一篇博客开发的服务端项目,还需要增加文章列表查询文章删除文章更新三个接口。

文章列表查询接口

验证表单数据

在路径 schema/user.js 中增加 articles 验证表单数据,其中使用的 .allow('') 表示允许空字符串:

    articles: {
        query: {
            pagenum,
            pagesize,
            cate_id: joi.number().integer().min(1).allow('').optional(),
            state: joi.string().valid('已发布', '草稿').allow('').optional()
        }
    },
处理函数
  1. 文章查询这里是根据 cate_idstate 进行的多表条件查询,对相应参数进行有效判断而给出合适的 sql 查询语句。
  2. 数量查询通过 select count (*/字段) from 表名 来进行。
  3. 分页查询通过 select * from 表名 limit 数值1, 数值2 来进行,其中 数值1 表示跳过前面多少条数据,数值2 表示取多少条数据。
  4. 排序通过 order by 字段 asc/desc 来进行。
// 查询文章
const articles = (req, res) => {
    console.log('query: ', req.query);
    // 跳过的条数
    let num = (req.query.pagenum - 1)*req.query.pagesize;
    
    // 查询文章的sql
    let sql;
    // 查询文章数量的sql
    let numSql;

    // 文章查询是根据参数按条件查询
    if (typeof req.query.cate_id === 'number' && typeof req.query.state === 'string' && req.query.state.length > 0) {
        sql = `select  articles.*, article_cate.name cate_name, article_cate.alias cate_alias, article_cate.is_delete cate_is_delete from articles, article_cate where cate_id = ? and state = ? and articles.cate_id = article_cate.id order by pub_date desc limit ${num}, ${req.query.pagesize}`;
        numSql = `select count (*) total from articles, article_cate where cate_id = ? and state = ? and articles.cate_id = article_cate.id`
    } else if (typeof req.query.cate_id === 'number') {
        sql = `select  articles.*, article_cate.name cate_name, article_cate.alias cate_alias, article_cate.is_delete cate_is_delete from articles, article_cate where cate_id = ? and articles.cate_id = article_cate.id order by pub_date desc limit ${num}, ${req.query.pagesize}`;
        numSql = `select count (*) total from articles, article_cate where cate_id = ? and articles.cate_id = article_cate.id`
    } else if (typeof req.query.state === 'string' && req.query.state.length > 0) {
        sql = `select articles.*, article_cate.name cate_name, article_cate.alias cate_alias, article_cate.is_delete cate_is_delete from articles, article_cate where state = ? and articles.cate_id = article_cate.id order by pub_date desc limit ${num}, ${req.query.pagesize}`;
        numSql = `select count (*) total from articles, article_cate where state = ? and articles.cate_id = article_cate.id`
    } else {
        sql = `select articles.*, article_cate.name cate_name, article_cate.alias cate_alias, article_cate.is_delete cate_is_delete from articles, article_cate where articles.cate_id = article_cate.id order by pub_date desc limit ${num}, ${req.query.pagesize}`;
        numSql = `select count (*) total from articles, article_cate where articles.cate_id = article_cate.id`
    }
    console.log('sql: ', sql);
    console.log('numSql: ', numSql);

    let params = new Array();
    console.log('init params: ', params);
    if (typeof req.query.cate_id === 'number') {
        params.push(req.query.cate_id);
    }
    if (typeof req.query.state === 'string' && req.query.state.length > 0) {
        params.push(req.query.state);
    }
    console.log('param: ', params);

    // 查询文章
    db.query(sql, params, (err, result) => {
        if (err) {
            res.cc(err);
        } else {
            console.log('result: ', result);
            // 查询文章数量
            db.query(numSql, params, (e, r) => {
                if (err) {
                    res.cc(err);
                } else {
                    console.log('total result: ', r[0].total);
                    let data = {
                        data: result,
                        total: r[0].total
                    }
                    res.send({
                        status: 0,
                        data: data,
                        message: '文章列表获取成功!'
                    });
                }
            });
        }
    });
}
实现路由

在路径 router/article.js 中实现如下路由:

routor.post('/addArticle', uploads.single('cover_img'), expressJoi(schema.addArticle), handler.addArticle);

文章删除接口

文章删除的实现跟文章类别删除类似,具体实现见代码……

文章更新接口

文章更新的实现跟文章发布和用户信息跟新类似,具体实现见代码……

安装VSCode插件辅助开发

由于使用VSCode访问开发的项目页面使用的是 file 协议,file 协议在开发当中会有很多的问题,通常需要 http 协议来访问开发的 html 文件,Live ServerExpress这两个插件都可以做到。

Live Server

  1. 在插件市场,搜索 Live Server 并安装
  2. 在页面上鼠标右键,选择 Open With Live Server 即可快速使用 http 协议访问页面,点击下方 Port 按钮可以关闭

Express

由于 Live Server 在页面保存之后会自动刷新整个页面,在不需要自动刷新的时候就不是很方便了,这时 Express 就会比较合适。

  1. 在插件市场,搜索 Express 并安装
  2. 打开命令面板(F1,或者Windows和Linux上的Ctrl+Shift+P,或者OSX上的Shift+CMD+P)
  3. 输入或选择 “Express: Host current workspace and open in browser”

配置 ajaxPrefilter

在目录中新建 baseApi.js 文件,编写如下代码:

// 在每个请求之前被发送和$.ajax()处理它们前处理,设置自定义Ajax选项或修改现有选项。
let host = 'http://127.0.0.1/';
$.ajaxPrefilter(function(options) {
    console.log('---ajaxPrefilter----');
    
    // 统一设置请求头
    if (options.url.indexOf('my/') === 0) {
        options.headers = {
            Authorization: localStorage.getItem('token')
        }
    }

    // 在发起真正的 Ajax 请求之前,统一拼接请求的根路径
    options.url = host + options.url;
    console.log('url: ', options.url);
    console.log('type: ', options.type);
    console.log('headers: ', options.headers);
    console.log('params: ', options.data);

    // 统一挂载 complete 回调 
    options.complete = function(res) {
        console.log('complete: ', res.responseJSON);
        // 处理身份验证失败
        if (res.responseJSON.status === 1 && res.responseJSON.message === '身份验证失败!') {
            // 清除 token 跳到登录页面
            localStorage.removeItem('token');
            location.href = 'project/login.html';
        }
    }
});

登录注册

使用 layui 绘制页面,具体实现见代码……

表单的验证

导入 layui 的 js 文件,为需要验证的表单项添加 lay-verify 属性,同时指定具体的校验规则即可。

通过 layui.form.verify() 函数自定义校验规则:

    // 表单验证
    layui.form.verify({
        username: [
            /^[\w]{2,20}$/,
            '用户名必须为2~20位字母和数字!'
        ],
        password: [
            /^[\S]{6,16}$/,
            '密码必须为6~16位非空格字符!'
        ],
        repassword: function(value) {
            if ($('.signin [name=password]').val() !== value) {
                return '两次输入的密码不一致!';
            }
        }
    });

实现

登录注册切换:

    // 去注册、去登录
    $('.to-signin a').click(function(){
        $('.login').hide();
        $('.signin').show();
    });
    $('.to-login a').click(function(){
        $('.signin').hide();
        $('.login').show();
    });

登录注册实现:

    let layer = layui.layer;
    // 登录
    $('.login form').on('submit', function(e) {
        // 阻止默认行为
        e.preventDefault();

        let params = {
            // 属性选择器
            username: $('.login [name=username]').val(),
            password: $('.login [name=password]').val()
        }
        $.post('api/login', params, function(data) {
            // 登录成功
            if (data.status === 0) {
                // 缓存token
                localStorage.setItem('token', data.token);
                // 跳首页
                location.href = 'index.html'
            }
            layer.msg(data.message);
        });
    });
    
    // 注册
    $('.signin form').submit(function(e) {
        // 阻止默认行为
        e.preventDefault();
        $('.to-login a').click();

        let params = {
            username: $('.signin [name=username]').val(),
            password: $('.signin [name=password]').val()
        }
        $.post('api/signin',params, function(data) {
            // 注册成功
            if (data.status === 0) {
                layer.msg('注册成功,请登录!');
                // 切换到登录
                $('.to-login a').click();
                // 填充用户名
                $('.login [name=username]').val(params.username);
                // 获得焦点
                $('.login [name=password]').focus();
            } else {
                layer.msg(data.message);
            }
        });
    });

按需为表单项添加校验规则:

<input type="password" name="repassword" required lay-verify="required|password|repassword" placeholder="请再次输入密码" autocomplete="off" class="layui-input">
…

主页

使用 layui 绘制页面,具体实现见代码……

使用iframe标签在内容主体区域显示网页内容

在页面主体的 div 中添加 iframe :

<div class="layui-body">
    <!-- 内容主体区域 -->
    <iframe name="fm" src="" frameborder="0"></iframe>
</div>

为 首页 链接添加 href 和 target 属性:

<a href="/home/dashboard.html" target="fm"><span class="iconfont icon- home"></span>首页</a>

为 首页 对应的导航 Item 项添加 layui-this 属性选中此 Item:

<li class="layui-nav-item layui-this">
   <a href="/home/dashboard.html" target="fm"><span class="iconfont icon-home"></span>首页</a> 
</li>

**禁止过渡动画,强制清除 链接的 CSS3 动画: **

a {
    /* 禁止过渡动画 */
    transition: none !important;
}

在目录中新建 index.js 文件,实现如下代码渲染用户头像:

// 用户信息数据
var userinfo = null;

// 渲染头像
function renderAvatar(user) {
    let name = user.nickname || user.username;
    $('#welcome').html(`欢迎&nbsp;&nbsp;${name}`);
    if (user.user_pic) {
        $('.layui-nav-img').attr('src', user.user_pic).show();
        $('.text-avatar').hide();
    } else {
        // 渲染大写首字母
        $('.text-avatar').html(name[0].toUpperCase()).show();
        $('.layui-nav-img').hide();
    }

    // 渲染完成后,保存数据
    userinfo = user;
}

获取用户的基本信息:

$(function(){
    // 用户信息
    getUserinfo();
});
// 请求用户信息数据
function getUserinfo() {
    $.get('my/userinfo', function(data) {
        if (data.status === 0) {
            renderAvatar(data.data);
        } else {
            layer.msg(data.message);
        }
    })
}

实现退出功能:

$(function(){
    // 退出
    $('#logout').click(function() {
        console.log('logout');
        layer.confirm('是否退出?', {
            icon: 3,
            title: '提示'
        }, function(index) {
            console.log(index);

            // 清除 token
            localStorage.removeItem('token');

            // 进入登录页
            location.href = 'login.html';

            // 关闭 询问框
            layer.close(index);
        });
    });
});

基本资料

使用 layui 绘制页面,具体实现见代码……

获取用户的基本信息

在目录中新建 userinfo.js 文件,实现如下代码填充表单数据:

    let form = layui.form;
    var layer = layui.layer;

    // 填充初始数据
    // 通过 window.parent 调用父框架 属性、方法 必须在http协议中打开
    form.val('userinfo', window.parent.userinfo);
}
中的子页面,如果想要调用父页面中的属性、方法,使用 window.parent 即可(但是必须http协议中打开)。

调用 layui.form.val() 可以快速为表单赋值。

实现表单的重置效果,阻止表单的默认重置行为,再重新为表单赋值即可:

   // 重置对表单的修改
   $('#reset-btn').click(function(e) {
       // 阻止默认行为
       e.preventDefault();

       // 用原始数据重新填充表单
       form.val('userinfo', window.parent.userinfo);
   });

发起请求更新用户的信息,阻止表单的默认提交行为,并发起数据请求:

   // 提交数据
   $('.layui-form').submit(function(e) {
       // 阻止默认行为
       e.preventDefault();

       let params = $(this).serialize();
       $.post('my/userinfo', params, function(data) {
           if (data['status'] === 0) {
               // 调用父框架的方法重新获取数据(缺点就是需要重新请求网络)
               // window.parent.getUserinfo();

               // 通过修改父框架的数据重新渲染(不需要请求网络,但是需要拼接数据)
               let userinfo = window.parent.userinfo
               // 属性选择器
               userinfo.nickname = $('.layui-form [name=nickname]').val();
               userinfo.email = $('.layui-form [name=email]').val();
               console.log('userinfo: ', userinfo);
               window.parent.renderAvatar(userinfo);
           } else {
               layer.msg(data['message']);
           }
       });
   });

重置密码

使用 layui 绘制页面,具体实现见代码……

发起请求实现重置密码的功能:

    $('.layui-form').submit(function(e) {
        // 阻止默认行为
        e.preventDefault();

        $.post('my/updatePassword', {
            oldPassword: $('[name=oldPassword]').val(),
            newPassword: $('[name=password]').val()
        }, function(data) {
            if (data.status === 0) {
                $(this)[0].reset();
                layui.layer.msg('修改密码成功!');
            } else {
                console.log('data: ', data);
                layui.layer.msg(data.message);
            }
        });
    });

使用 reset() 方法实现重置表单功能:

    $('#reset-btn').on('click', function() {
        $('.layui-form')[0].reset();
    })

更换头像

使用 layui 绘制页面,具体实现见代码……

cropper 实现图片裁剪预览

在 中导入 cropper.css 样式表,在 的结束标签之前,按顺序导入如下的 js 脚本:

<head>
    <link rel="stylesheet" href="../assets/lib/cropper/cropper.css">
</head>

<body>
    <script src="../assets/lib/jquery/jquery-3.7.1.min.js"></script>
    <script src="../assets/lib/cropper/Cropper.js"></script>
    <script src="../assets/lib/cropper/jquery-cropper.js"></script>
</body>

定义裁剪区与预览区的 html 结构:

            <!-- 第一行的图片裁剪和预览区域 --> <div class="row1">
            <!-- 图片裁剪区域 -->
            <div class="cropper-box">
                <!-- 这个 img 标签很重要,将来会把它初始化为裁剪区域 -->
                <img id="image" src="../assets/images/sample.jpg" />
            </div>

            <!-- 图片的预览区域 --> 
            <div class="preview-box">
            <div>
                <!-- 宽高为 100px 的预览区域 -->
                <div class="img-preview w100"></div> <p class="size">100 x 100</p>
                </div> 

                <div>
                    <!-- 宽高为 50px 的预览区域 -->
                    <div class="img-preview w50"></div> <p class="size">50 x 50</p>
                    </div>
                </div>
            </div>

实现基本裁剪效果:

    // 获取裁剪区域的 DOM 元素 
    var $image = $('#image');
    // 配置选项
    const options = {
        // 纵横比
        aspectRatio: 1,
        // 指定预览区域
        preview: '.img-preview'
    };
    // 创建裁剪区域 
    $image.attr('src', window.parent.userinfo.user_pic).cropper(options);

更换裁剪的图片,首先拿到用户选择的文件;然后根据选择的文件,创建一个对应的 URL 地址;最后销毁 旧的裁剪区域,重新设置图片路径 ,创建新的裁剪区域:

            let file = filelist[0];
            console.log('file: ', file);

            let url = URL.createObjectURL(file);
            console.log('url: ', url);

            $image.cropper('destroy') // 销毁旧的裁剪区域
            .attr('src', url) // 重新设置图片路径
            .cropper(options); // 重新初始化裁剪区域

读取文件,可以使用 URL.createObjectURL(file),还可以使用 FileReader 对象:

            // 或者使用 FileReader 读取图片
            let reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = function() {
                console.log('result: ', reader.result);

                $image.cropper('destroy') // 销毁旧的裁剪区域
                .attr('src', reader.result) // 重新设置图片路径
                .cropper(options); // 重新初始化裁剪区域
            };

将裁剪后的图片,输出为 base64 格式的字符串:

        var dataURL = $image.cropper('getCroppedCanvas', { // 创建一个 Canvas 画布
            width: 100,
            height: 100
        }).toDataURL('image/png'); // 将 Canvas 画布上的内容,转化为 base64 格式的字符串

将裁剪后的图片,输出为二进制数据对象:

        $image.cropper('getCroppedCanvas', { // 创建一个 Canvas 画布
            width: 400,
            height: 280
        }).toBlob(function(blob) {
            console.log('image: ', blob);
        });

将裁剪后的头像上传到服务器,更新父页面中的数据并渲染头像:

        $.post('my/updateAvatar', {
            user_pic: dataURL
        }, function(data) {
            if (data.status === 0) {
                let userinfo = window.parent.userinfo;
                userinfo.user_pic = dataURL;
                window.parent.renderAvatar(userinfo);
                layui.layer.msg('修改头像成功!');
            } else {
                layui.layer.msg(data.message);
            }
        });

文章分类

使用 layui 绘制页面,具体实现见代码……

使用模板引擎渲染表格的数据,导入模板引擎并定义模板:

    <script src="../assets/lib/template/template-web.js"></script>
    <script type="text/html" id="tmp-table">
        {
  {each}}
            <tr>
                <td>{
  {$value.name}}</td>
                <td>{
  {$value.alias}}</td>
                <td>
                    <button type="button" class="layui-btn layui-btn-xs btn-edit" data-id={
  {$value.id}}>编辑</button>
                    <button type="button" class="layui-btn layui-btn-danger layui-btn-xs btn-delete" data-id={
  {$value.id}}>删除</button>
                </td>
            </tr>
        {
  {/each}}
    </script>

发起请求获取数据渲染列表:

    // 获取分类列表数据
    getListData();
    function getListData() {
        $.get('my/artcate', function(data) {
            if (data.status === 0) {
                let html = template('tmp-table', data.data);
                $('tbody').html(html);
            } else {
                layui.layer.msg(data.message);
            }
        });
    }

添加文章分类

在页面中定义如下的 script 标签,渲染添加文章分类的 form 表单结构:

    <script type="text/html" id="editCateDialog">
        <form id="editForm" class="layui-form" lay-filter="editForm">
            <!-- 隐藏域 -->
            <input type="hidden" name="id">
            <div class="layui-form-item">
                <label class="layui-form-label">分类名称</label>
                <div class="layui-input-block">
                    <input type="text" name="name" required  lay-verify="required|password" placeholder="请输入分类名称" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">分类别名</label>
                <div class="layui-input-block">
                    <input type="text" name="alias" required  lay-verify="required|password|newPassword" placeholder="请输入分类别名" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <div class="layui-input-block">
                    <button class="layui-btn" lay-submit>修改</button>
                </div>
            </div>
        </form>
    </script>

使用 layui.layer.open 实现弹出层效果,在按钮的点击事件中,通过 layer.open() 展示弹出层,并在 layerIndex 中保存弹出层的索引:

    // 添加按钮事件
    let layerIndex = null;
    $('#addCateBtn').click(function(e) {
        layerIndex = layui.layer.open({
            type: 1,
            area: ['500px', '250px'],
            title: '添加文章分类',
            content: $('#addCateDialog').html()
        }); 
    });

发起Ajax请求,实现添加文章分类的功能,并使用 layui.layer.close 关闭弹出层:

    // 事件委托绑定编辑表单提交事件
    $('body').on('submit', '#editForm', function(e) {
        e.preventDefault();
        $.post('my/updatecate', $(e.target).serialize(), function(data) {
            if (data.status === 0) {
                getListData();
                layui.layer.msg('文章分类添加成功!');
                layui.layer.close(layerIndex);
            } else {
                layui.layer.msg(data.message);
            }
        });
    });

编辑文章分类

修改文章分类的弹出层与添加文章分类类似,具体实现见代码……

通过 代理 的形式,为 btn-edit 按钮绑定点击事件,请求数据填充表单:

    // 事件委托绑定编辑按钮事件
    $('tbody').on('click', '.btn-edit', function(e) {
        let cateid = $(e.target).attr('data-id');
        $.ajax({
            method: 'GET',
            url: 'my/cates/' + cateid,
            success: function(data) {
                if (data.status === 0) {
                    layerIndex = layui.layer.open({
                        type: 1,
                        area: ['500px', '250px'],
                        title: '修改文章分类',
                        content: $('#editCateDialog').html()
                    });
                    layui.form.val('editForm', data.data[0]);
                } else {
                    layui.layer.msg(data.message);
                }
            }
        })
    });

更新文章分类的数据,通过代理的形式,为修改分类的表单绑定 submit 事件:

    // 事件委托绑定编辑表单提交事件
    $('body').on('submit', '#editForm', function(e) {
        e.preventDefault();
        $.post('my/updatecate', $(e.target).serialize(), function(data) {
            if (data.status === 0) {
                getListData();
                layui.layer.msg('文章分类添加成功!');
                layui.layer.close(layerIndex);
            } else {
                layui.layer.msg(data.message);
            }
        });
    });

删除文章分类

通过代理的形式,为删除按钮绑定点击事件:

    // 事件委托绑定删除按钮事件
    $('tbody').on('click', '.btn-delete', function(e) {
        layer.confirm('是否删除?', {icon: 3, title:'提示'}, function(index){
            let cateid = $(e.target).attr('data-id');
            $.ajax({
                method: 'GET',
                url: 'my/deletecate/' + cateid,
                success: function(data) {
                    if (data.status === 0) {
                        layui.layer.msg('文章分类删除成功!');
                        getListData();
                        layer.close(index);
                    } else {
                        layui.layer.msg(data.message);
                    }
                }
            });
        });
    });

文章列表

使用 layui 绘制页面,具体实现见代码……

文章列表主体区域

通过 template.defaults.imports 定义过滤器:

    // 时间格式化
    template.defaults.imports.dateformatter = function(date) {
        let dt = new Date(date);
        let year = dt.getFullYear();
        let month = zeroTwo(dt.getMonth());
        let day = zeroTwo(dt.getDay());
        let hour = zeroTwo(dt.getHours());
        let minutes = zeroTwo(dt.getMinutes());
        let seconds = zeroTwo(dt.getSeconds());

        return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds
    }
    // 补零函数
    function zeroTwo(value) {
        return value > 9 ? value : '0' + value
    }

使用过滤器定义列表数据的 template 模板结构:

    <script type="text/html" id="tmp-table">
        {
  {each}}
            <tr>
                <td>{
  {$value.title}}</td>
                <td>{
  {$value.cate_name}}</td>
                <td>{
  {$value.pub_date | dateformatter}}</td>
                <td>{
  {$value.state}}</td>
                <td>
                    <button type="button" class="layui-btn layui-btn-xs btn-edit" data-index={
  {$index}}>编辑</button>
                    <button type="button" class="layui-btn layui-btn-danger layui-btn-xs btn-delete" data-id={
  {$value}} data-index={
  {$index}}>删除</button>
                </td>
            </tr>
        {
  {/each}}
    </script>

定义一个查询的参数对象,获取文章列表数据并使用 template 渲染表格:

    // 获取列表数据参数
    let params = {
        pagenum: 1,
        pagesize: 2,
        cate_id: '',
        state: ''
    };

    // 获取列表数据
    getListData();
    let dataList = null;
    function getListData() {
        $.ajax({
            method: 'GET',
            url: 'my/articles',
            data: params,
            success: function(data) {
                if (data.status === 0) {
                    dataList = data.data.data;
                    let html = template('tmp-table', dataList);
                    $('tbody').html(html);
                    renderPage(data.data.total);
                } else {
                    layui.layer.msg(data.message);
                }
            }
        });
    }

文章列表筛选区域

定义分类可选项的 template 模板结构:

    <script type="text/html" id="tmp-cate-list">
        <option value="">所有分类</option>
        {
  {each}}
            <option value={
  {$value.id}}>{
  {$value.name}}</option>
        {
  {/each}}
    </script>

发起请求获取并渲染文章分类的下拉选择框:

    // 获取分类列表数据
    getCateListData();
    function getCateListData() {
        $.get('my/artcate', function(data) {
            if (data.status === 0) {
                let html = template('tmp-cate-list', data.data);
                $('[name=cate_id]').html(html);
                // 通过 layui 重新渲染表单区域的UI结构
                layui.form.render();
            } else {
                layui.layer.msg(data.message);
            }
        });
    }

为筛选表单绑定 submit 事件,实现筛选的功能:

    $('.layui-form').on('submit', function(e) {
        e.preventDefault();
        params.cate_id = $('[name=cate_id]').val();
        params.state = $('[name=state]').val();
        params.pagenum = 1;
        getListData();
    });

文章列表分页区域

定义渲染分页的 renderPage 方法,调用 laypage.render() 方法来渲染分页的结构,并在 getListData 中调用 renderPage 方法:

    function renderPage(total) {
        //执行一个laypage实例
        layui.laypage.render({
            elem: 'articlePage' //注意,这是 ID,不用加 # 号
            ,count: total //数据总数,从服务端得到
            ,curr: params.pagenum // 起始页
            ,limit: params.pagesize // 每页显示的条数
            ,limits: [2, 3, 4, 5] // 每页条数的选择项
            ,layout: ['count', 'limit', 'prev', 'page', 'next', 'skip']
            , jump: function(obj, first) { // 当分页被切换时触发,函数返回两个参数:obj(当前分页的所有选项值)、first(是否首次,一般用于初始加载的判断)
                if (!first) {
                    params.pagenum = obj.curr;
                    params.pagesize = obj.limit;
                    getListData();
                }
            }
        });
    }

删除文章

通过代理的形式,为删除按钮绑定点击事件处理函数:

    // 事件委托绑定删除按钮事件
    $('tbody').on('click', '.btn-delete', function(e) {
        layer.confirm('是否删除?', {icon: 3, title:'提示'}, function(index){
            let articleid = $(e.target).attr('data-id');
            $.ajax({
                method: 'GET',
                url: 'my/deletearticle/' + articleid,
                success: function(data) {
                    if (data.status === 0) {
                        layui.layer.msg('文章删除成功!');
                        let i = $(e.target).attr('data-index');
                        console.log('index: ', i);
                        if (i === '0' && params.pagenum > 1) {
                            params.pagenum = params.pagenum - 1;
                        }
                        console.log('params: ', params);
                        getListData();
                        layer.close(index);
                    } else {
                        layui.layer.msg(data.message);
                    }
                }
            });
        });
    });

发布文章

使用 layui 绘制页面,具体实现见代码……

渲染文章类别对应的下拉选择框结构与文章列表页类似,具体实现见代码……

富文本编辑器

添加如下的 layui 表单行:

          <div class="layui-form-item">
            <!-- 左侧的 label -->
            <label class="layui-form-label">文章内容</label>
            <!-- 为富文本编辑器外部的容器设置高度 -->
            <div class="layui-input-block" style="height: 400px;">
              <!-- 重要:将来这个 textarea 会被初始化为富文本编辑器 -->
              <textarea name="content"></textarea>
            </div>
          </div>

导入富文本必须的 script 脚本:

    <script src="../assets/lib/tinymce/tinymce.min.js"></script>
    <script src="../assets/lib/tinymce/tinymce_setup.js"></script>

调用 initEditor() 方法,初始化富文本编辑器:

    // 初始化富文本编辑器
    initEditor()

图片封面

图片封面的实现与更换头像类似,具体实现见代码……

发布文章的实现

定义文章的发布状态,为存为草稿按钮,绑定点击事件处理函数::

    let state = '已发布';
    $('#btnSave2').on('click', function() {
        state = '草稿';
    });

为表单绑定 submit 提交事件,将裁剪后的封面追加到FormData对象中,发起Ajax请求实现发布文章的功能:

    $('.layui-form').on('submit', function(e) {
        e.preventDefault();
        let fm = new FormData(this);
        fm.append('state', state);
        // 遍历 FormData
        fm.forEach((v, k) => {
            console.log(k, '-', v);
        });

        $image.cropper('getCroppedCanvas', { // 创建一个 Canvas 画布
            width: 400,
            height: 280
        }).toBlob(function(blob) {
            fm.append('cover_img', blob);
            $.ajax({
                method: 'POST',
                url: 'my/addArticle',
                data: fm,
                // 注意:如果向服务器提交的是 FormData 格式的数据,必须添加以下两个配置项
                // 不修改 Content-Type 属性,使用 FormData 默认的 Content-Type 值 
                contentType: false,
                // 不对 FormData 中的数据进行 url 编码,而是将 FormData 数据原样发送到服务器 
                processData: false,
                success: function(data) {
                    if (data.status === 0) {
                        layui.layer.msg('文章发表成功!');
                        // 发布文章成功后,跳转到文章列表页面
                        location.href = '/project/article/list.html';
                    } else {
                        layui.layer.msg(data.message);
                    }
                }
            });
        });
    });

(补充)编辑文章

编辑文章与发布文章十分相似,主要区别为一下两点:

  1. 需要使用初始数据填充文章编辑表单
  2. 提交表单的接口与参数不同

基于上述分析的两点,使用发布文章的页面做下细微改动来实现文章编辑功能。

在 iframe 框架父页面的 index.js 中添加 editArticle 属性,绑定发布文章按钮将 editArticle 的值设置为null:

    // 编辑的文章数据
    var editArticle = null;

    $('#publish').click(function() {
        editArticle = null;
    });

通过代理的形式,为文章列表页面中的编辑按钮绑定点击事件处理函数,取出文章数据,存到父页面的 editArticle 属性中,并跳转到发布文章页面:

    $('tbody').on('click', '.btn-edit', function(e) {
        let index = $(this).attr('data-index');
        let data = dataList[index];
        window.parent.editArticle = data;
        location.href = '/project/article/publish.html';
    });

如此在发布文章的 publish.js 中就可以通过判断 window.parent.editArticle 的值是否为 null 确定是编辑文章还是发布文章。

修改 publish.js 在编辑文章时,使用初始数据填充表单:

    // 填充初始数据
    // 通过 window.parent 调用父框架 属性、方法 必须在http协议中打开
    let article = window.parent.editArticle;
    if (article !== null) {
        layui.form.val('publish', window.parent.editArticle);
    }

修改 publish.js 在编辑文章时,使用文章封面创建封面裁剪区:

    // 创建裁剪区域 
    let path = null;
    if (window.parent.editArticle === null) {
        path = '../assets/images/sample2.jpg';
    } else {
        path = 'http://localhost' + window.parent.editArticle.cover_img;
    }
    $image.attr('src', path).cropper(options);

修改 publish.js 在编辑文章时,根据发表文章还是编辑文章使用不同的参数和接口:

    $('.layui-form').on('submit', function(e) {
        e.preventDefault();
        let fm = new FormData(this);
        fm.append('state', state);
        if (window.parent.editArticle !== null) {
            fm.append('id', window.parent.editArticle.id);
        }
        // 遍历 FormData
        fm.forEach((v, k) => {
            console.log(k, '-', v);
        });

        $image.cropper('getCroppedCanvas', { // 创建一个 Canvas 画布
            width: 400,
            height: 280
        }).toBlob(function(blob) {
            fm.append('cover_img', blob);
            $.ajax({
                method: 'POST',
                url: window.parent.editArticle === null ? 'my/addArticle' : 'my/updateArticle',
                data: fm,
                // 注意:如果向服务器提交的是 FormData 格式的数据,必须添加以下两个配置项
                // 不修改 Content-Type 属性,使用 FormData 默认的 Content-Type 值 
                contentType: false,
                // 不对 FormData 中的数据进行 url 编码,而是将 FormData 数据原样发送到服务器 
                processData: false,
                success: function(data) {
                    if (data.status === 0) {
                        layui.layer.msg('文章发表成功!');
                        // 发布文章成功后,跳转到文章列表页面
                        location.href = '/project/article/list.html';
                    } else {
                        layui.layer.msg(data.message);
                    }
                }
            });
        });
    });

最近更新

  1. TCP协议是安全的吗?

    2024-01-26 17:50:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-26 17:50:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-26 17:50:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-26 17:50:02       20 阅读

热门阅读

  1. qt学习:实战 http请求获取qq的价值

    2024-01-26 17:50:02       31 阅读
  2. AI模型压缩技术

    2024-01-26 17:50:02       34 阅读
  3. C语言求两数最大公约数(辗转相除法)

    2024-01-26 17:50:02       32 阅读
  4. 洛谷 P1032 字串变换

    2024-01-26 17:50:02       33 阅读
  5. Vue3 Element-plust表格导出excel文件

    2024-01-26 17:50:02       33 阅读
  6. 数据结构-二叉树

    2024-01-26 17:50:02       28 阅读
  7. 数据结构-队列

    2024-01-26 17:50:02       34 阅读
  8. C语言-算法-数论基础

    2024-01-26 17:50:02       40 阅读
  9. Spring Boot 访问数据库——JdbcTemplate

    2024-01-26 17:50:02       29 阅读
  10. Python中的面向对象编程

    2024-01-26 17:50:02       38 阅读
  11. 免费分享Deepl和google谷歌翻译api接口

    2024-01-26 17:50:02       36 阅读
  12. C++求逆元、分数取模

    2024-01-26 17:50:02       31 阅读
  13. 牛客周赛 Round 29(A B C D E)

    2024-01-26 17:50:02       34 阅读
  14. 【Digester解析XML文件的三种方式】

    2024-01-26 17:50:02       31 阅读