欢迎来到十九分快乐的博客

生死看淡,不服就干。

9. 社交模块 - 添加好友,展示好友

社交模块

处理用户与用户之间的关系

好友列表页面添加

客户端显示页面

  1. 用户中心点击好友列表同时进入好友列表主副页面,html/user.html,代码:
<!DOCTYPE html>
<html>
    <head>
        <title>用户中心</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <!-- 引入用户头像处理js文件 -->
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user" id="app">
            <div class="bg">
                <img src="../static/images/bg0.jpg">
            </div>
            <img class="back" @click="back" src="../static/images/user_back.png" alt="">
            <img class="setting" @click='to_settings' src="../static/images/setting.png" alt="">
            <div class="header">
                <div class="info">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <!-- 用户头像处理 -->
                        <div class="user_avatar">
                            <v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
                            <v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
                            <v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
                        </div>
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <p class="user_name">{{user_data.nickname}}</p>
                </div>
                <div class="wallet">
                    <div class="balance">
                        <p class="title"><img src="../static/images/money.png" alt="">钱包</p>
                        <p class="num">{{user_data.money_format}}</p>
                    </div>
                    <div class="balance">
                        <p class="title"><img src="../static/images/integral.png" alt="">果子</p>
                        <p class="num">{{user_data.credit_format}}</p>
                    </div>
                </div>
                <div class="invite">
                    <img class="invite_btn" src="../static/images/invite.png" alt="">
                </div>
            </div>
            <div class="menu">
                <div class="item">
                    <span class="title">我的主页</span>
                    <span class="value">查看</span>
                </div>
                <div class="item" @click='to_friend_list'>
                    <span class="title">好友列表</span>
                    <span class="value">查看</span>
                </div>
                <div class="item">
                    <span class="title">收益明细</span>
                    <span class="value">查看</span>
                </div>
                <div class="item">
                    <span class="title">实名认证</span>
                    <span class="value">未认证</span>
                </div>
                <div class="item">
                    <span class="title">问题反馈</span>
                    <span class="value">去反馈</span>
                </div>
                </ul>
        </div>
        </div>
    <script>
        apiready = function(){
            var game = new Game("../static/mp3/bg4.mp3");
            // 在 #app 标签下渲染一个按钮组件
            Vue.prototype.game = game;
            new Vue({
                el:"#app",
                data(){
                    return {
                        prev:{name:"",url:"",params:{}},
                        current:{name:"user",url:"user.html",params:{}},
                        user_data:{},
                    }
                },
                // 页面加载之前获取用户数据
                created(){
                    // 获取用户数据
                    this.get_user_data()
                    // 监听事件变化
                    this.listen()
                },

                methods:{
                    // 监听事件
                    listen(){
                        // 监听头像更新的通知
                        this.listen_update_avatar();
                        this.listen_update_nickname();
                    },

                    // 监听头像更新的通知
                    listen_update_avatar(){
                        api.addEventListener({
                            name: 'update_avatar_success'
                        }, (ret, err) => {
                            // 更新用户数据
                            this.get_user_data()
                        });
                    },

                    // 监听昵称更新的通知
                    listen_update_nickname(){
                        api.addEventListener({
                            name: 'update_nickname_success'
                        }, (ret, err) => {
                            // 更新用户数据
                            this.get_user_data()
                        });
                    },

                    // 通过token值获取用户数据
                    get_user_data(){
                        // 获取token
                        let token = this.game.getfs('access_token') || this.game.getdata('access_token')
                        // 根据token获取用户数据
                        this.user_data = this.game.get_user_by_token(token)
                        // this.game.print(this.user_data)
                        // 格式化数字变成金钱格式,原始数据不变
                        this.user_data.money_format = this.game.number_format(this.user_data.money)
                        this.user_data.credit_format = this.game.number_format(this.user_data.credit)
                    },

                    back(){
                        // 返回首页
                        this.game.closeWin();
                    },
                    // 点击设置按钮,跳转到系统设置页面
                    to_settings(){
                        this.game.openFrame('settings', 'settings.html')
                    },

                    // 点击好友列表,跳转带好友列表页面
                    to_friend_list(){
                        this.game.openFrame('friends', 'friends.html')
                        this.game.openFrame('friend_list', 'friend_list.html', null, {
                            x: 0,             // 左上角x轴坐标
                            y: 194,           // 左上角y轴坐标
                            w: 'auto',        // 当前帧页面的宽度, auto表示满屏
                            h: 'auto'         // 当前帧页面的高度, auto表示满屏
                        })
                    },


                }
            });
        }
    </script>
    </body>
</html>

修改static/js/main.js中封装的打开帧页面方法

// 创建帧页面
openFrame(name,url,redirect='from_right',rect, pageParam){
    let frame = {
        name: name,		// 帧页面的名称
        url: url,	// 帧页面打开的url地址
        bounces:false,        // 页面是否可以下拉拖动
        reload: true,         // 帧页面如果已经存在,是否重新刷新加载
        useWKWebView:true,    // 是否使用WKWebView来加载页面
        historyGestureEnabled:true,  // 是否可以通过手势来进行历史记录前进后退,只在useWKWebView参数为true时有效
        vScrollBarEnabled: false,	// 是否显示垂直滚动条
        hScrollBarEnabled: false,	// 是否显示水平滚动条

        animation:{
            type:"push",             //动画类型(详见动画类型常量)
            subType:redirect,    //动画子类型(详见动画子类型常量)
            duration:300             //动画过渡时间,默认300毫秒
        },
        rect: {               // 当前帧的宽高范围
            // 方式1,设置矩形大小宽高
            x: 0,             // 左上角x轴坐标
            y: 0,             // 左上角y轴坐标
            w: 'auto',        // 当前帧页面的宽度, auto表示满屏
            h: 'auto'         // 当前帧页面的高度, auto表示满屏
            // 方式2,设置矩形大小宽高
            // marginLeft:,    //相对父页面左外边距的距离,数字类型
            // marginTop:,     //相对父页面上外边距的距离,数字类型
            // marginBottom:,  //相对父页面下外边距的距离,数字类型
            // marginRight:    //相对父页面右外边距的距离,数字类型
        },
        pageParam: {}          // 要传递新建帧页面的参数,在新页面可通过 api.pageParam.name 获取
    }
    if(rect){
        frame.rect = rect
    }
    if (pageParam) {
        frame.pageParam = pageParam
    }
    // 打开帧页面
    api.openFrame(frame);
}
  1. 好友列表主窗口页面 html/friends.html,,代码:
<!DOCTYPE html>
<html>
    <head>
        <title>好友列表主窗口</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user setting" id="app">
            <div class="bg">
                <img src="../static/images/friends_bg.png">
            </div>
            <img class="back" @click="back" src="../static/images/user_back.png" alt="">
            <div class="add_friend_btn" @click="add_friend">
                <img src="../static/images/add_friends.png" alt="">
            </div>
            <div class="friends_list">
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            friends:[],
                        }
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        add_friend(){
                            // 添加好友

                        }
                    }
                });
            }
        </script>
    </body>
</html>
  1. 新添css样式 css/main.css,并把素材中的图片添加到static/image文件夹中 样式代码:
.add_friend_btn {
  position: absolute;
  top: 12rem;
  left: 3.6rem;
  width: 26rem;
  height: 6rem;
}
.add_friend_btn img{
  box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
}
  1. 好友列表副窗口页面friend_list.html,设置打开好友列表页面的同时,也打开好友列表数据页面代码。

friend_list.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>好友列表</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user setting" id="app">
            <div class="friends_list">
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior pick">摘</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior protect">护</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior pick">摘</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            friends:[],
                            page: 1,
                            prev:{name:"",url:"",params:{}},
                            current:{name:"friend_list",url:"friend_list.html",params:{}},
                        }
                    },
                    created(){
                        this.get_friends();
                    },
                    methods:{
                        get_friends(){

                        },
                        goto_home(){
                            // 退出当前页面
                            this.game.closeFrame();
                        },
                    }
                });
            }
        </script>
    </body>
</html>

  1. 新添css样式 css/main.css,样式代码:
.friends_list .avatar{
  width: 6.39rem;
  height: 6.39rem;
  position: relative;
}
.friends_list .avatar_bf{
  position: absolute;
  z-index: 1;
  margin: auto;
  width: 4.56rem;
  height: 4.56rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.friends_list .user_avatar{
  position: absolute;
  z-index: 1;
  width: 4.56rem;
  height: 4.56rem;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 1rem;
}
.friends_list .avatar_border{
  position: absolute;
  z-index: 1;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 6.1rem;
  height: 6.1rem;
}
.friends_list{
  position: absolute;
  top: 0rem;
  left: 3.6rem;
}
.friends_list .item{
  position: relative;
  background-color: rgba(196,81,9,0.1);
  border-radius: 4px;
  height: 7rem;
  width: 25.8rem;
  margin-bottom: 1rem;
  box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
}
.friends_list .item .avatar{
  position: absolute;
  left: 1rem;
  top: 0;
  bottom: 0;
  margin: auto;
}
.friends_list .item .info{
  position: absolute;
  left: 8rem;
  top: 2rem;
  color: #fff;
  width: 10rem;
}
.friends_list .item .behavior{
  position: absolute;
  left: 16rem;
  font-size: 1.5rem;
  text-align: center;
  line-height: 4rem;
  height: 4rem;
  width: 4rem;
  color: #fff;
  top: 0;
  bottom: 0;
  margin: auto;
  box-shadow: 2px 2px 5px #333333;
}
.friends_list .item .pick{
  background: #336633;
  border-radius: 50%;
}
.friends_list .item .protect{
  background: #990000;
  border-top-right-radius: 5px;
  border-top-left-radius: 5px;
  border-bottom-left-radius: 30px;
  border-bottom-right-radius: 30px;
}
.friends_list .item .goto{
  position: absolute;
  left: 23rem;
  top: 0;
  bottom: 0;
  margin: auto;
  width: 0.96rem;
  height: 1.8rem;
}
  1. 在页面退出的时候,好友页面的主窗口和列表页都要同时退出。frients.html,代码:
<!DOCTYPE html>
<html>
    <head>
        <title>好友列表主窗口</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user setting" id="app">
            <div class="bg">
                <img src="../static/images/friends_bg.png">
            </div>
            <img class="back" @click="back" src="../static/images/user_back.png" alt="">
            <div class="add_friend_btn" @click="add_friend">
                <img src="../static/images/add_friends.png" alt="">
            </div>
            <div class="friends_list">
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            friends:[],
                            prev:{name:"",url:"",params:{}},
                            current:{name:"friends",url:"friends.html",params:{}},
                        }
                    },
                    methods:{
                        back(){
                            this.game.closeFrame("friend_list");
                            this.game.closeFrame();
                        },
                        add_friend(){
                            // 添加好友

                        }
                    }
                });
            }
        </script>
    </body>
</html>
  1. 给好友列表的数据页面,添加拉下拉刷新效果,html/friend_list.html,代码:
<!DOCTYPE html>
<html>
    <head>
        <title>好友列表</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user setting" id="app">
            <div class="friends_list">
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior pick">摘</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior protect">护</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="behavior pick">摘</div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
                <div class="item">
                    <div class="avatar">
                        <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                        <img class="user_avatar" src="../static/images/avatar.png" alt="">
                        <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                    </div>
                    <div class="info">
                        <p class="username">长昵称都很好</p>
                        <p class="fruit">果子:9,999.00</p>
                    </div>
                    <div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            friends:[],
                            page: 1,
                            prev:{name:"",url:"",params:{}},
                            current:{name:"friend_list",url:"friend_list.html",params:{}},
                        }
                    },
                    created(){
                        this.get_friends();
                    },
                    methods:{
                        get_friends(){
                            // 下拉刷新好友列表
                            api.setRefreshHeaderInfo({
                                loadingImg: 'widget://image/refresh.png',
                                bgColor: null,
                                textColor: '#fff',
                                textDown: '下拉刷新...',
                                textUp: '松开刷新...'
                            }, (ret, err)=>{
                                // 在这里从服务器加载数据,加载完成后调用api.refreshHeaderLoadDone()方法恢复组件到默认状态
                                setTimeout(()=>{
                                    api.refreshHeaderLoadDone();
                                },1500);
                            });
                        },
                        goto_home(){
                            // 退出当前页面
                            this.game.closeFrame();
                        },
                    }
                });
            }
        </script>
    </body>
</html>

添加好友显示页面

  1. 点击添加好友,跳转到条件好友页面 html/friends.html,代码:
<!DOCTYPE html>
<html>
    <head>
        <title>好友列表主窗口</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app user setting" id="app">
            <div class="bg">
                <img src="../static/images/friends_bg.png">
            </div>
            <img class="back" @click="back" src="../static/images/user_back.png" alt="">
            <div class="add_friend_btn" @click="add_friend">
                <img src="../static/images/add_friends.png" alt="">
            </div>
            <div class="friends_list">
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            friends:[],
                            prev:{name:"",url:"",params:{}},
                            current:{name:"friends",url:"friends.html",params:{}},
                        }
                    },
                    methods:{
                        back(){
                            this.game.closeFrame("friend_list");
                            this.game.closeFrame();
                        },
                        add_friend(){
                            // 添加好友
                            this.game.openFrame("add_friend","add_friend.html",null,null,{
                                type: "push", //动画类型(详见动画类型常量)
                                subType: "from_top", //动画子类型(详见动画子类型常量)
                                duration: 300 //动画过渡时间,默认300毫秒
                            });
                        },
                    }
                });
            }
        </script>
    </body>
</html>

  1. 添加好友页面 html/add_friend.html,,页面代码:
<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item">
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <img class="user_avatar" src="../static/images/avatar.png" alt="">
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">长昵称都很好</p>
                            <p class="time">刚刚搜索</p>
                        </div>
                        <div class="status">添加</div>
                    </div>
                    <div class="item">
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <img class="user_avatar" src="../static/images/avatar.png" alt="">
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">长昵称都很好</p>
                            <p class="time">3小时前</p>
                        </div>
                        <div class="status" @click="change_status">等待通过</div>
                    </div>
                    <div class="item">
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <img class="user_avatar" src="../static/images/avatar.png" alt="">
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">长昵称都很好</p>
                            <p class="time">1天前</p>
                        </div>
                        <div class="status">已通过</div>
                    </div>
                    <div class="item">
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <img class="user_avatar" src="../static/images/avatar.png" alt="">
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">长昵称都很好</p>
                            <p class="time">7天前</p>
                        </div>
                        <div class="status">已超时</div>
                    </div>
                    <div class="item">
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <img class="user_avatar" src="../static/images/avatar.png" alt="">
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">长昵称都很好</p>
                            <p class="time">7天前</p>
                        </div>
                        <div class="status">已拒绝</div>
                    </div>
                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"",
                        }
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        add_friend_commit(){
                            // 提交搜索信息

                        },
                        change_status(){
                            // 状态修改
                        }
                    }
                });
            }
        </script>
    </body>
</html>

  1. 添加页面样式 css/main.css,代码:
.add_friend input::-webkit-input-placeholder,
.add_friend textarea::-webkit-input-placeholder{
  color: #fff;
}
.add_friend .box{
  top: 4rem;
  height: 55.56rem;
  background: url("../images/long_bg1.png") no-repeat 0 0;
  background-size: 100%;
}
.add_friend .nickname{
  margin: 4rem 4.6rem 2rem;
  width: 19rem;
  height: 4rem;
  line-height: 4rem;
  background-color: #cc9966;
  outline: none;
  border: 1px solid #330000;
  text-align: center;
  font-size: 1rem;
  color: #ffffcc;
}

.add_friend .friends_list{
  position: absolute;
  top: 15rem;
  left: 3.6rem;
}
.add_friend .friends_list .item{
  position: relative;
  margin-left: 1rem;
  background-color: rgba(196,81,9,0.1);
  border-radius: 4px;
  height: 4rem;
  width: 19rem;
  margin-bottom: 1rem;
  box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
}
.add_friend .friends_list .avatar{
  width: 3.84rem;
  height: 3.84rem;
  position: absolute;
  left: 1rem;
}

.add_friend .friends_list .avatar_bf{
  position: absolute;
  z-index: 1;
  margin: auto;
  width: 2.74rem;
  height: 2.74rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.add_friend .friends_list .user_avatar{
  position: absolute;
  z-index: 1;
  width: 2.74rem;
  height: 2.74rem;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 1rem;
}
.add_friend .friends_list .avatar_border{
  position: absolute;
  z-index: 1;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 3.66rem;
  height: 3.66rem;
}
.add_friend .friends_list .item .info{
  top: 0.6rem;
  left: 6rem;
}
.add_friend .friends_list .item .time{
  font-size: 0.6rem;
}
.add_friend .friends_list .item .status{
  position: absolute;
  left: 12rem;
  top: 1.2rem;
  width: 8rem;
  text-align: center;
  height: 2rem;
  color: #fff;
}
.add_friend .friends_list{
  height: 36rem;
  overflow-y: auto;
  overflow-x: hidden;
}

添加好友

服务端提供接口

模型创建

users/models.py,代码:

class UserApplyFriendHistory(BaseModel):
    """申请好友历史"""
    relration_chioce = (
        (1,"已申请"),
        (2,"已通过"),
        (3,"已超时"),
        (4,"已拒绝"),
        (5,"已取消"),
    )
    __tablename__ = "mf_user_apply_friend_history"
    apply_id = db.Column(db.Integer, comment="申请人ID")
    applied_id = db.Column(db.Integer, comment="被申请人ID")
    apply_user = db.relationship('User', primaryjoin='User.id == UserApplyFriendHistory.apply_id', foreign_keys='UserApplyFriendHistory.apply_id', backref=backref('apply_list', uselist=True), uselist=False)
    applied_user = db.relationship('User', primaryjoin='User.id == UserApplyFriendHistory.applied_id', foreign_keys='UserApplyFriendHistory.applied_id', backref=backref('applied_user_list',uselist=True), uselist=False)
    status = db.Column(db.Integer, default=1, comment="关系状态")

    def __repr__(self):
        return f"<UserApplyFriendHistory {self.apply_user.nickname}>"

    @property
    def get_status(self):
        """获取历史状态"""
        return self.relration_chioce[self.status-1]

获取好友申请添加记录

  1. 获取好友申请历史记录 users.api,代码:
# 获取当前用户的好友申请历史记录
@jwt_required()
@decorator.get_user_object
def get_apply_friend_history(user):
    '''
    获取当前用户的好友申请历史记录
    :param user: 装饰器通过token获取的用户模型对象
    :return: 列表
    '''
    # 获取好友申请记录列表
    friend_history_list = services.get_apply_friend_history_by_user_id(user.id)

    return {
        'errno' : code.CODE_OK,
        'errmsg' : message.ok,
        'friend_history_list': friend_history_list
    }

  1. 数据服务层 获取数据users.services,代码:
from sqlalchemy import or_, and_
from .models import db, User, UserApplyFriendHistory

# 获取当前用户的好友添加申请历史
def get_apply_friend_history_by_user_id(user_id):
    '''
    根据用户ID获取好友申请记录
    :param user_id: 用户ID
    :return: 历史记录列表
    '''
    # 获取当前对象申请与被申请的模型对象列表
    history_record_list = UserApplyFriendHistory.query.filter(
        or_(
            UserApplyFriendHistory.apply_id == user_id,
            and_(
                # 如果用户主动申请,并主动取消,则被申请人看不到申请记录
                UserApplyFriendHistory.applied_id == user_id,
                UserApplyFriendHistory.status < 5,
            )
        )
    ).order_by(UserApplyFriendHistory.created_time.desc()).limit(20).all()

    from .marshmallow import ApplyFriendHistorySchema
    # 实例化构造器
    afhs = ApplyFriendHistorySchema()
    # 序列化输出数据
    friend_history_list = afhs.dump(history_record_list, many=True)

    return friend_history_list

  1. 好友申请历史构造器 users.marshmallow,代码:
from marshmallow import Schema, fields, validate, validates, ValidationError, post_load, validates_schema, post_dump
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field

from .models import User, UserApplyFriendHistory

# 好友申请历史构造器
class ApplyFriendHistorySchema(SQLAlchemyAutoSchema):
    '''好友申请历史构造器'''
    # 序列化器嵌套
    apply_user = fields.Nested(UserSchema)
    applied_user = fields.Nested(UserSchema)
    class Meta:
        model = UserApplyFriendHistory
        include_fk = False  # 启用外键关系
        include_relationships = True  # 模型关系外部属性
        fields = ["id", "apply_user", "applied_user", "get_status", "created_time"]


  1. 路由 users.urls,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('refresh', api.refresh_token), # 刷新access_token值
    api_rpc('update_avatar', api.update_avatar), # 更新头像
    api_rpc('update_nickname', api.update_nickname), # 更新昵称
    api_rpc('update_mobile', api.update_mobile), # 更新手机号
    api_rpc('update_password', api.update_password), # 更新登录密码
    api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
]


客户端显示好友申请记录

html/add_friend.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item" v-for='user in user_list'>
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <div class="user_avatar">
                                <v-avatar v-if="user.avatar" :src="user.avatar" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else-if="user.nickname" :username="user.nickname" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else :username="user.id" :size="33" :rounded="true"></v-avatar>
                            </div>
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">{{user.nickname}}</p>
                        </div>
                        <div class="status" @click="apply_friend(user)">添加</div>
                    </div>
                    <div class="item" v-for='hfriend in friend_history_list'>
                        <!-- 自己申请好友 -->
                        <div v-if='hfriend.apply_user.id == user_info.id'>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.applied_user.avatar" :src="hfriend.applied_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.applied_user.nickname" :username="hfriend.applied_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.applied_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.applied_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status">{{hfriend.get_status[1]}}</div>
                        </div>
                        <!-- 用户被申请 -->
                        <div v-else>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.apply_user.avatar" :src="hfriend.apply_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.apply_user.nickname" :username="hfriend.apply_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.apply_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.apply_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status" v-if="hfriend.get_status[0]==1">等待审核</div>
                            <div class="status" v-if="hfriend.get_status[0]==2">已同意</div>
                            <div class="status" v-if="hfriend.get_status[0]==3">{{hfriend.get_status[1]}}</div>
                            <div class="status" v-if="hfriend.get_status[0]==4">{{hfriend.get_status[1]}}</div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"", // 搜索用户名
                            search_timer:null, // 搜索定时器标记符
                            user_list: [],     // 搜索结果用户列表
                            user_info: {}, // 当前用户信息
                            friend_history_list: [], // 好友申请历史列表
                        }
                    },
                    // 监听事件
                    watch:{
                        account(){
                            // 节流防抖
                            if(this.account.length >= 1){
                                // 清除定时器
                                clearTimeout(this.search_timer)
                                // 停止输入后,两秒钟发送请求
                                this.search_timer = setTimeout(() => {
                                    this.search_user_info();
                                },2000)
                            }
                        }
                    },
                    created(){
                        // this.token = this.game.getdata('access_token') || this.game.getfs('access_token')
                        // this.user_info = this.game.get_user_by_token(this.token)
                        this.get_user_info() // 获取当前用户信息
                        this.get_apply_friend_history() // 获取好友申请历史记录
                    },
                    // 过滤器
                    filters:{
                        // 时间过滤器
                        time_format(time){
                            // 计算时间距离,返回文本格式
                            his_time_obj = new Date(time)
                            now_time_obj = new Date()
                            duration = parseInt((now_time_obj - his_time_obj)/1000)
                            if(0 <= duration && duration < 60 * 5){
                                // 5分钟内
                                return '刚刚'
                            }
                            if(60 * 5 <= duration && duration < 60 * 60){
                                // 1小时内
                                return parseInt(duration/60) + '分前'
                            }
                            if(60 * 60 <= duration && duration < 60 * 60 * 24){
                                // 1天内
                                return parseInt(duration/60/60) + '小时前'
                            }
                            if(60 * 60 * 24 <= duration && duration < 60 * 60 * 24 * 30){
                                // 1月内
                                return parseInt(duration/60/60/24) + '天前'
                            }
                            // 判断月份和年份的时间距离
                            let year_duration = now_time_obj.getFullYear() - his_time_obj.getFullYear();
                            let month_duration = now_time_obj.getMonth() - his_time_obj.getMonth();
                            if( now_time_obj.getMonth() < his_time_obj.getMonth() ){
                                month_duration +=12;
                            }

                            if(year_duration > 1 && year_duration < 10){
                                return year_duration+"年前";
                            }

                            if(year_duration > 5){
                                return time.split("T")[0];
                            }

                            if( month_duration < 6){
                                return month_duration+"个月前";
                            }

                            if( month_duration < 12 ){
                                return "半年前";
                            }

                        },
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        // 获取当前用户信息
                        get_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.user_info = self.game.get_user_by_token(token)
                            })
                        },

                        // 获取好友申请历史记录
                        get_apply_friend_history(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.get_apply_friend_history',
                                    'params': {},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.friend_history_list = data.result.friend_history_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },
                        // 提交搜索信息,获取用户列表
                        search_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.search_user_info',
                                    'params': {'account': self.account},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.user_list = data.result.user_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 用户申请添加好友
                        apply_friend(user){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.apply_friend',
                                    'params': {'applied_user_id': user.id},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("成功发起好友申请!请等待...");
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        change_status(){
                            // 状态修改
                        }
                    }
                });
            }
        </script>
    </body>
</html>


搜索用户信息

服务端提供搜索用户的api接口

  1. 视图 users.api,代码:
@jwt_required()
@get_user_object
def search_user_info(user, account):
    """
    搜索用户信息
    :param user: 当前登录用户
    :param account: 搜索账号相关信息(昵称,手机,邮箱,账号)
    :return:
    """
    # 根据用户账户信息搜索用户列表,排除当前用户
    user_list = services.search_user_info(user, account)

    return {
        "errno": code.CODE_OK,
        "errmsg": message.ok,
        "user_list": user_list,
    }

  1. 数据服务层 users.services,代码:
def search_user_info(user, account):
    """
    根据用户搜索的账号条件搜索其他用户信息
    :param user: 当前用户模型
    :param account: 搜索账号相关信息(昵称,手机,邮箱,账号)
    :return: 用户列表
    """
    user_object_list = User.query.filter(or_(
        User.mobile == account,
        User.nickname.contains(account),
        User.email == account,
        User.name == account,
    )).filter(
        User.id!=user.id
    ).all()
    from .marshmallow import SearchUserInfoSchema
    us = SearchUserInfoSchema()
    us.current_user = user
    user_list = us.dump(user_object_list, many=True)
    return user_list

  1. 添加显示搜索用户信息序列化器,marshmallow,代码:
class SearchUserInfoSchema(MA.SQLAlchemyAutoSchema):
    """搜索用户信息的构造器"""

    class Meta:
        model = User
        include_fk = False  # 启用外键关系
        include_relationships = True  # 模型关系外部属性
        # 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
        fields = ["id", "name", "nickname", "avatar", "mobile"]

    @post_dump
    def get_object(self, data, **kwargs):
        # todo 判断搜索用户和当前用户的关系
        # self.current_user
        data["mobile"] = data["mobile"][:3] +"****" + data["mobile"][-4:]
        return data

  1. 路由,users.urls,代码:
from application import path,api
from . import api as apiviews
urlpatterns = [

]

apipatterns = [
    api("mobile", apiviews.check_mobile),
    api("register",apiviews.register),
    api("login",apiviews.login),
    api("refresh",apiviews.refresh),
    api("avatar.update",apiviews.update_avatar),
    api("nickname.update",apiviews.update_nickname),
    api("password.update",apiviews.update_password),
    api("pay_password.update",apiviews.update_pay_password),
    api("apply_friend_history",apiviews.get_apply_friend_history),
    api("search_user_info", apiviews.search_user_info),
]

客户端展示搜索用户信息

html/add_friend.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item" v-for='user in user_list'>
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <div class="user_avatar">
                                <v-avatar v-if="user.avatar" :src="user.avatar" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else-if="user.nickname" :username="user.nickname" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else :username="user.id" :size="33" :rounded="true"></v-avatar>
                            </div>
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">{{user.nickname}}</p>
                        </div>
                        <div class="status" @click="change_status">添加</div>
                    </div>
                    <div class="item" v-for='hfriend in friend_history_list'>
                        <!-- 自己申请好友 -->
                        <div v-if='hfriend.apply_user.id == user_info.id'>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.applied_user.avatar" :src="hfriend.applied_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.applied_user.nickname" :username="hfriend.applied_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.applied_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.applied_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status">{{hfriend.get_status[1]}}</div>
                        </div>
                        <!-- 用户被申请 -->
                        <div v-else>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.apply_user.avatar" :src="hfriend.apply_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.apply_user.nickname" :username="hfriend.apply_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.apply_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.apply_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status" v-if="hfriend.get_status[0]==1">等待审核</div>
                            <div class="status" v-if="hfriend.get_status[0]==2">已同意</div>
                            <div class="status" v-if="hfriend.get_status[0]==3">{{hfriend.get_status[1]}}</div>
                            <div class="status" v-if="hfriend.get_status[0]==4">{{hfriend.get_status[1]}}</div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"", // 搜索用户名
                            search_timer:null, // 搜索定时器标记符
                            user_list: [],     // 搜索结果用户列表
                            token: "",
                            user_info: {}, // 当前用户信息
                            friend_history_list: [], // 好友申请历史列表
                        }
                    },
                    // 监听事件
                    watch:{
                        account(){
                            // 节流防抖
                            if(this.account.length >= 1){
                                // 清除定时器
                                clearTimeout(this.search_timer)
                                // 停止输入后,两秒钟发送请求
                                this.search_timer = setTimeout(() => {
                                    this.search_user_info();
                                },2000)
                            }
                        }
                    },
                    created(){
                        this.token = this.game.getdata('access_token') || this.game.getfs('access_token')
                        this.user_info = this.game.get_user_by_token(this.token)
                        this.get_apply_friend_history() // 获取好友申请历史记录
                    },
                    // 过滤器
                    filters:{
                        // 时间过滤器
                        time_format(time){
                            // 计算时间距离,返回文本格式
                            his_time_obj = new Date(time)
                            now_time_obj = new Date()
                            duration = parseInt((now_time_obj - his_time_obj)/1000)
                            if(0 <= duration && duration < 60 * 5){
                                // 5分钟内
                                return '刚刚'
                            }
                            if(60 * 5 <= duration && duration < 60 * 60){
                                // 1小时内
                                return parseInt(duration/60) + '分前'
                            }
                            if(60 * 60 <= duration && duration < 60 * 60 * 24){
                                // 1天内
                                return parseInt(duration/60/60) + '小时前'
                            }
                            if(60 * 60 * 24 <= duration && duration < 60 * 60 * 24 * 30){
                                // 1月内
                                return parseInt(duration/60/60/24) + '天前'
                            }
                            // 判断月份和年份的时间距离
                            let year_duration = now_time_obj.getFullYear() - his_time_obj.getFullYear();
                            let month_duration = now_time_obj.getMonth() - his_time_obj.getMonth();
                            if( now_time_obj.getMonth() < his_time_obj.getMonth() ){
                                month_duration +=12;
                            }

                            if(year_duration > 1 && year_duration < 10){
                                return year_duration+"年前";
                            }

                            if(year_duration > 5){
                                return time.split("T")[0];
                            }

                            if( month_duration < 6){
                                return month_duration+"个月前";
                            }

                            if( month_duration < 12 ){
                                return "半年前";
                            }

                        },
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        // 获取好友申请历史记录
                        get_apply_friend_history(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.get_apply_friend_history',
                                    'params': {},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.friend_history_list = data.result.friend_history_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },
                        // 提交搜索信息,获取用户列表
                        search_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.search_user_info',
                                    'params': {'account': self.account},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.user_list = data.result.user_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        change_status(){
                            // 状态修改
                        }
                    }
                });
            }
        </script>
    </body>
</html>


申请添加好友

客户端提供用户添加申请好友的操作菜单

搜索用户信息, 显示用户列表, 点击添加操作, 向后台请求申请添加好友操作html/add_friend.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item" v-for='user in user_list'>
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <div class="user_avatar">
                                <v-avatar v-if="user.avatar" :src="user.avatar" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else-if="user.nickname" :username="user.nickname" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else :username="user.id" :size="33" :rounded="true"></v-avatar>
                            </div>
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">{{user.nickname}}</p>
                        </div>
                        <div class="status" @click="apply_friend(user)">添加</div>
                    </div>
                    <div class="item" v-for='hfriend in friend_history_list'>
                        <!-- 自己申请好友 -->
                        <div v-if='hfriend.apply_user.id == user_info.id'>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.applied_user.avatar" :src="hfriend.applied_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.applied_user.nickname" :username="hfriend.applied_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.applied_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.applied_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status">{{hfriend.get_status[1]}}</div>
                        </div>
                        <!-- 用户被申请 -->
                        <div v-else>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.apply_user.avatar" :src="hfriend.apply_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.apply_user.nickname" :username="hfriend.apply_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.apply_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.apply_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status" v-if="hfriend.get_status[0]==1">等待审核</div>
                            <div class="status" v-if="hfriend.get_status[0]==2">已同意</div>
                            <div class="status" v-if="hfriend.get_status[0]==3">{{hfriend.get_status[1]}}</div>
                            <div class="status" v-if="hfriend.get_status[0]==4">{{hfriend.get_status[1]}}</div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"", // 搜索用户名
                            search_timer:null, // 搜索定时器标记符
                            user_list: [],     // 搜索结果用户列表
                            token: "",
                            user_info: {}, // 当前用户信息
                            friend_history_list: [], // 好友申请历史列表
                        }
                    },
                    // 监听事件
                    watch:{
                        account(){
                            // 节流防抖
                            if(this.account.length >= 1){
                                // 清除定时器
                                clearTimeout(this.search_timer)
                                // 停止输入后,两秒钟发送请求
                                this.search_timer = setTimeout(() => {
                                    this.search_user_info();
                                },2000)
                            }
                        }
                    },
                    created(){
                        this.token = this.game.getdata('access_token') || this.game.getfs('access_token')
                        this.user_info = this.game.get_user_by_token(this.token)
                        this.get_apply_friend_history() // 获取好友申请历史记录
                    },
                    // 过滤器
                    filters:{
                        // 时间过滤器
                        time_format(time){
                            // 计算时间距离,返回文本格式
                            his_time_obj = new Date(time)
                            now_time_obj = new Date()
                            duration = parseInt((now_time_obj - his_time_obj)/1000)
                            if(0 <= duration && duration < 60 * 5){
                                // 5分钟内
                                return '刚刚'
                            }
                            if(60 * 5 <= duration && duration < 60 * 60){
                                // 1小时内
                                return parseInt(duration/60) + '分前'
                            }
                            if(60 * 60 <= duration && duration < 60 * 60 * 24){
                                // 1天内
                                return parseInt(duration/60/60) + '小时前'
                            }
                            if(60 * 60 * 24 <= duration && duration < 60 * 60 * 24 * 30){
                                // 1月内
                                return parseInt(duration/60/60/24) + '天前'
                            }
                            // 判断月份和年份的时间距离
                            let year_duration = now_time_obj.getFullYear() - his_time_obj.getFullYear();
                            let month_duration = now_time_obj.getMonth() - his_time_obj.getMonth();
                            if( now_time_obj.getMonth() < his_time_obj.getMonth() ){
                                month_duration +=12;
                            }

                            if(year_duration > 1 && year_duration < 10){
                                return year_duration+"年前";
                            }

                            if(year_duration > 5){
                                return time.split("T")[0];
                            }

                            if( month_duration < 6){
                                return month_duration+"个月前";
                            }

                            if( month_duration < 12 ){
                                return "半年前";
                            }

                        },
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        // 获取好友申请历史记录
                        get_apply_friend_history(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.get_apply_friend_history',
                                    'params': {},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.friend_history_list = data.result.friend_history_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },
                        // 提交搜索信息,获取用户列表
                        search_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.search_user_info',
                                    'params': {'account': self.account},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.user_list = data.result.user_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 用户申请添加好友
                        apply_friend(user){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.apply_friend',
                                    'params': {'applied_user_id': user.id},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("成功发起好友申请!请等待...");
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        change_status(){
                            // 状态修改
                        }
                    }
                });
            }
        </script>
    </body>
</html>


服务端接收并处理好友申请

  1. 视图: users.api,代码:
from . import tasks  # 引入定时任务

# 用户申请添加好友
@jwt_required()
@decorator.get_user_object
def apply_friend(user, applied_user_id):
    '''
    申请添加好友
    :param user: 装饰器通过token获取的用户模型对象
    :param applied_user_id: 被申请添加的用户ID
    :return:
    '''

    # todo 1.判断两者身份关系

    # 2. 添加申请好友的记录
    history = services.add_apply_friend_history(user, applied_user_id)

    # 3.发送延时任务,7天后没人处理的申请,自动超时
    from datetime import datetime, timedelta
    # 延时时间
    eta = datetime.utcnow() + timedelta(seconds=5)
    # 发布异步定时任务
    tasks.change_history_timeout.apply_async((history.id,), eta=eta)

    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok
    }


  1. 数据服务层: users.services,代码:
# 添加申请好友的历史记录
def add_apply_friend_history(user, applied_user_id):
    '''
    添加申请好友的历史记录
    :param user: 当前登录用户模型对象
    :param applied_user_id: 被申请添加好友的用户ID
    :return:
    '''
    history = UserApplyFriendHistory(
        apply_id = user.id,
        applied_id = applied_user_id,
        status = 1
    )
    db.session.add(history)
    db.session.commit()

    return history

# 更改好友申请记录的状态
def change_history_status(history_id, status):
    '''
    更改好友申请记录的状态
    :param history_id: 历史记录ID
    :param status: 更改的状态数值
    :return:
    '''
    # 获取历史记录
    history = UserApplyFriendHistory.query.get(history_id)
    # 如果状态是待审核,则修改
    if history.status == 1:
        history.status = status
    db.session.commit()

    return history


  1. 添加定时异步任务, 规定时间自动超时。

users.tasks,代码:

from application import celery
from . import services
@celery.task(name="change_history_timeout",bind=True)
def change_history_timeout(self, history_id:int):
    try:
        with celery.app.app_context():
            # 设置超时的好友申请记录自动改变状态为 已超时
            services.change_history_status(history_id, 3)
    except Exception as exc:
        # 发生异常,每隔3秒尝试重新执行,一共5次
        self.retry(exc=exc, countdown=3, max_retries=5)

终端启动celery

celery -A manage.celery worker -l info

  1. 路由,users.urls,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('refresh', api.refresh_token), # 刷新access_token值
    api_rpc('update_avatar', api.update_avatar), # 更新头像
    api_rpc('update_nickname', api.update_nickname), # 更新昵称
    api_rpc('update_mobile', api.update_mobile), # 更新手机号
    api_rpc('update_password', api.update_password), # 更新登录密码
    api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
    api_rpc('search_user_info', api.search_user_info), # 搜索用户信息
    api_rpc('apply_friend', api.apply_friend), # 添加用户好友申请记录
]

好友申请状态审核

服务端提供状态更新接口

  1. 声明用户之间好友关系的数据模型并创建数据表。apps.users.models,代码:
class UserFriendShip(BaseModel):
    """用户的好友关系"""
    __tablename__ = "mf_user_friendship"
    apply_id = db.Column(db.Integer, comment="主动添加好友的用户ID")
    applied_id = db.Column(db.Integer, comment="被添加好友的用户的ID")
    apply_user = db.relationship('User', primaryjoin='User.id == UserFriendShip.apply_id', foreign_keys='UserFriendShip.apply_id', backref=backref('apply_friend_list', uselist=True), uselist=False)
    applied_user = db.relationship('User', primaryjoin='User.id == UserFriendShip.applied_id', foreign_keys='UserFriendShip.applied_id', backref=backref('applied_friend_list', uselist=True), uselist=False)

    def __repr__(self):
        return f"<UserFriendShip {self.apply_user.nickname} {self.applied_user.nickname}>"

  1. 编写视图:审核好友关系 apps.users.api,代码:
# 审核好友关系(同意/拒绝添加好友)
@jwt_required()
@decorator.get_user_object
def add_friend(user, apply_user_id, history_id, status):
    '''
    审核好友关系(同意/拒绝添加好友)
    :param user: 装饰器通过token获取的用户模型对象
    :param apply_user_id: 主动申请添加好友的用户
    :param history_id: 申请好友历史记录ID
    :param status: 同意/拒绝添加好友(true/false)
    :return:
    '''
    # 如果两个用户已经是好友关系了,则不能继续审核操作
    res = services.get_friendship(user.id, apply_user_id)
    if res:
        return {
            'errno': code.CODE_ADD_FRIEND_ERROR,
            'errmsg': message.add_friend_error
        }

    # 同意(2)或拒绝(4)添加好友状态
    status = 2 if status else 4
    # 1. 修改申请好友记录的状态
    history = services.change_history_status(history_id, status)
    # 2. 当用户同意申请以后,添加好友关系
    if status == 2:
        services.add_friend(history.apply_user, history.applied_user)

    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok
    }


  1. 数据服务层, services.user.services,, 代码:
# 查询2个用户是否存在好友关系
def get_friendship(apply_user_id, applied_user_id):
    '''
    判断2个用户是否存在好友关系
    :param apply_user_id: 被添加用户ID
    :param applied_user_id: 主动添加用户ID
    :return:
    '''

    instance = UserFriendShip.query.filter(
        or_(
            and_(UserFriendShip.apply_id == apply_user_id, UserFriendShip.applied_id == applied_user_id),
            and_(UserFriendShip.apply_id == applied_user_id, UserFriendShip.applied_id == apply_user_id),
        )
    ).first()

    return instance

# 添加用户好友关系记录
def add_friend(apply_user, applied_user):
    '''
    添加用户好友关系记录
    :param apply_user: 申请添加好友的用户
    :param applied_user: 被申请添加好友的用户
    :return:
    '''
    # 添加用户好友关系记录
    friendship = UserFriendShip(
        apply_user = apply_user,
        applied_user = applied_user,
    )
    db.session.add(friendship)
    db.session.commit()

    return friendship


  1. 路由 user/urls.py代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('refresh', api.refresh_token), # 刷新access_token值
    api_rpc('update_avatar', api.update_avatar), # 更新头像
    api_rpc('update_nickname', api.update_nickname), # 更新昵称
    api_rpc('update_mobile', api.update_mobile), # 更新手机号
    api_rpc('update_password', api.update_password), # 更新登录密码
    api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
    api_rpc('search_user_info', api.search_user_info), # 搜索用户信息
    api_rpc('apply_friend', api.apply_friend), # 添加用户好友申请记录
    api_rpc('add_friend', api.add_friend), # 添加用户好友关系记录
]


  1. 提示码与提示信息

application/utils/message.py,代码:

add_friend_error = '添加好友失败!'

application/utils/code.py,代码:

CODE_ADD_FRIEND_ERROR = 1010    # 添加好友失败

  1. 完成用户对好友关系的状态操作接口以后,因为我们已经创建了好友关系表,所以前面针对添加申请好友记录时候,判断用户之间的关系这块也可以完成了。apps.users.api,代码:
from .tasks import change_history_timeout

# 用户申请添加好友
@jwt_required()
@decorator.get_user_object
def apply_friend(user, applied_user_id):
    '''
    申请添加好友
    :param user: 装饰器通过token获取的用户模型对象
    :param applied_user_id: 被申请添加的用户ID
    :return:
    '''

    # 1.判断两者是否为好友关系,如果有,则不能继续添加好友
    res = services.get_friendship(user.id, applied_user_id)
    if res:
        return {
            'errno': code.CODE_ADD_FRIEND_ERROR,
            'errmsg': message.add_friend_error
        }

    # 2. 添加申请好友的记录
    history = services.add_apply_friend_history(user, applied_user_id)

    # 3.发送延时任务,7天后没人处理的申请,自动超时
    from datetime import datetime, timedelta
    # 延时时间
    eta = datetime.utcnow() + timedelta(seconds=5)
    # 发布异步定时任务
    tasks.change_history_timeout.apply_async((history.id,), eta=eta)

    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok
    }


# 完成了好友关系查询以后,todo就可以去掉了。

  1. 还有之前搜索用户时,也需要在获取用户时判断被搜索用户与当前用户的关系。apps.users.marshmallow,代码:
# 搜索用户信息构造器
class SearchUserInfoSchema(SQLAlchemyAutoSchema):
    """搜索用户信息的构造器"""
    class Meta:
        model = User
        include_fk = False # 启用外键关系
        include_relationships = True # 模型关系外部属性
        # 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
        fields = ["id", "name", "nickname", "avatar", "mobile"]

    # 修改序列化输出字段
    @post_dump
    def post_dump(self, data, **kwargs):
        # todo 判断搜索用户和当前用户是否为好友关系
        # self.current_user 当前用户 - 构造器初始化传过来的
        from application.apps.users import services
        friendship = services.get_friendship(self.current_user.id, data['id'])
        # 有返回值是好友关系
        data['is_friend'] = friendship is not None
        data['mobile'] = data["mobile"][:3] +"****" + data["mobile"][-4:]
        return data
    
 # 完成了这块功能以后,就可以把todo去掉了。

客户端发送请求更改好友申请状态

在用户进入添加好友的页面中,用户当处于被申请添加好友时,显示"等待审核",当点击了"等待审核"以后弹出菜单,可以决定是同意添加好友还是拒绝添加。

html/add_friend.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item" v-for='user in user_list'>
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <div class="user_avatar">
                                <v-avatar v-if="user.avatar" :src="user.avatar" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else-if="user.nickname" :username="user.nickname" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else :username="user.id" :size="33" :rounded="true"></v-avatar>
                            </div>
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">{{user.nickname}}</p>
                        </div>
                        <div class="status" @click="apply_friend(user)">添加</div>
                    </div>
                    <div class="item" v-for='hfriend in friend_history_list'>
                        <!-- 自己申请好友 -->
                        <div v-if='hfriend.apply_user.id == user_info.id'>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.applied_user.avatar" :src="hfriend.applied_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.applied_user.nickname" :username="hfriend.applied_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.applied_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.applied_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status">{{hfriend.get_status[1]}}</div>
                        </div>
                        <!-- 用户被申请 -->
                        <div v-else>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.apply_user.avatar" :src="hfriend.apply_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.apply_user.nickname" :username="hfriend.apply_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.apply_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.apply_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status" v-if="hfriend.get_status[0]==1" @click="change_apply(hfriend)">等待审核</div>
                            <div class="status" v-if="hfriend.get_status[0]==2">已同意</div>
                            <div class="status" v-if="hfriend.get_status[0]==3">{{hfriend.get_status[1]}}</div>
                            <div class="status" v-if="hfriend.get_status[0]==4">{{hfriend.get_status[1]}}</div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"", // 搜索用户名
                            search_timer:null, // 搜索定时器标记符
                            user_list: [],     // 搜索结果用户列表
                            user_info: {}, // 当前用户信息
                            friend_history_list: [], // 好友申请历史列表
                        }
                    },
                    // 监听事件
                    watch:{
                        account(){
                            // 节流防抖
                            if(this.account.length >= 1){
                                // 清除定时器
                                clearTimeout(this.search_timer)
                                // 停止输入后,两秒钟发送请求
                                this.search_timer = setTimeout(() => {
                                    this.search_user_info();
                                },2000)
                            }
                        }
                    },
                    created(){
                        this.get_user_info() // 获取当前用户信息
                        this.get_apply_friend_history() // 获取好友申请历史记录
                    },
                    // 过滤器
                    filters:{
                        // 时间过滤器
                        time_format(time){
                            // 计算时间距离,返回文本格式
                            his_time_obj = new Date(time)
                            now_time_obj = new Date()
                            duration = parseInt((now_time_obj - his_time_obj)/1000)
                            if(0 <= duration && duration < 60 * 5){
                                // 5分钟内
                                return '刚刚'
                            }
                            if(60 * 5 <= duration && duration < 60 * 60){
                                // 1小时内
                                return parseInt(duration/60) + '分前'
                            }
                            if(60 * 60 <= duration && duration < 60 * 60 * 24){
                                // 1天内
                                return parseInt(duration/60/60) + '小时前'
                            }
                            if(60 * 60 * 24 <= duration && duration < 60 * 60 * 24 * 30){
                                // 1月内
                                return parseInt(duration/60/60/24) + '天前'
                            }
                            // 判断月份和年份的时间距离
                            let year_duration = now_time_obj.getFullYear() - his_time_obj.getFullYear();
                            let month_duration = now_time_obj.getMonth() - his_time_obj.getMonth();
                            if( now_time_obj.getMonth() < his_time_obj.getMonth() ){
                                month_duration +=12;
                            }

                            if(year_duration > 1 && year_duration < 10){
                                return year_duration+"年前";
                            }

                            if(year_duration > 5){
                                return time.split("T")[0];
                            }

                            if( month_duration < 6){
                                return month_duration+"个月前";
                            }

                            if( month_duration < 12 ){
                                return "半年前";
                            }

                        },
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        // 获取当前用户信息
                        get_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.user_info = self.game.get_user_by_token(token)
                            })
                        },

                        // 获取好友申请历史记录
                        get_apply_friend_history(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.get_apply_friend_history',
                                    'params': {},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.friend_history_list = data.result.friend_history_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },
                        // 提交搜索信息,获取用户列表
                        search_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.search_user_info',
                                    'params': {'account': self.account},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.user_list = data.result.user_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 用户申请添加好友
                        apply_friend(user){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.apply_friend',
                                    'params': {'applied_user_id': user.id},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("成功发起好友申请!请等待...");
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 审核好友申请
                        change_apply(history){
                            api.actionSheet({
                                title:  `是否同意来自${history.apply_user.nickname}的好友申请?`,
                                cancelTitle: '忽略',
                                destructiveTitle: '同意',
                                buttons: ['拒绝']
                            }, (ret, err)=>{
                                if(ret.buttonIndex < 3){
                                    this.change_apply_http(history, ret.buttonIndex)
                                }
                            });
                        },
                        // 发送审核好友申请结果到服务端
                        change_apply_http(history, buttonIndex){
                            let status = buttonIndex==1?true:false;
                            let self = this;
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.add_friend',
                                    'params': {
                                        "apply_user_id": history.apply_user.id,
                                        "history_id": history.id,
                                        "status": status,
                                    },
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("审核操作成功~");
                                            // 发起全局广播通知
                                            self.game.sendEvent('add_friend_success')
                                            // 更改页面好友状态
                                            if(status){
                                                history.get_status = [2,'已同意']
                                            }else {
                                                history.get_status = [4,'已拒绝']
                                            }
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        }


                    }
                });
            }
        </script>
    </body>
</html>


用户取消自己发起的好友申请。

服务端接口

  1. 视图: users.api,代码:
# 用户取消自己发起的好友申请
@jwt_required()
@decorator.get_user_object
def cancel_apply_friend(user, history_id):
    '''
    用户取消自己发起的好友申请
    :param user: 装饰器通过token获取的用户模型对象
    :param history_id: 好友申请历史记录ID
    :return:
    '''
    # 获取好友申请历史记录
    history = services.get_apply_friend_history_by_history_id(history_id)
    if history is None: # 如果没有返回值
        return {
            'errno': code.CODE_NO_SUCH_HISTORY,
            'errmsg': message.no_such_history
        }

    # 取消好友申请
    try:
        services.cancel_apply_friend(history)
    except Exception:
        # 当前记录已经被处理了,返回超时[1. 对方处理, 2. 系统处理..]
        return {
            'errno': code.CODE_TIMEOUT,
            'errmsg': message.timeout
        }

    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok
    }

  1. 数据服务层 users.services,代码:
# 根据历史记录ID获取好友申请记录
def get_apply_friend_history_by_history_id(history_id):
    '''
    根据历史记录ID获取好友申请记录
    :param history_id: 历史记录ID
    :return:
    '''
    try:
        history = UserApplyFriendHistory.query.get(history_id)
        return history
    except Exception:
        return None

# 取消好友申请记录
def cancel_apply_friend(history):
    '''
    取消好友申请记录
    :param history: 好友申请记录模型对象
    :return:
    '''
    # 1. 对方已经处理的情况下,无法取消,提示用户
    if history.status != 1:
        raise Exception

    # 2. 对方没有处理的情况下,直接取消,5表示取消状态
    history.status = 5
    db.session.commit()


  1. 添加路由 users.urls,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('refresh', api.refresh_token), # 刷新access_token值
    api_rpc('update_avatar', api.update_avatar), # 更新头像
    api_rpc('update_nickname', api.update_nickname), # 更新昵称
    api_rpc('update_mobile', api.update_mobile), # 更新手机号
    api_rpc('update_password', api.update_password), # 更新登录密码
    api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
    api_rpc('search_user_info', api.search_user_info), # 搜索用户信息
    api_rpc('apply_friend', api.apply_friend), # 添加用户好友申请记录
    api_rpc('add_friend', api.add_friend), # 添加用户好友关系记录
    api_rpc('cancel_apply_friend', api.cancel_apply_friend), # 用户取消自己申请的好友记录
]




  1. 提示码和提示信息

提示码; application/utils/code.py,代码:

CODE_NO_SUCH_HISTORY = 1011     # 没有历史记录
CODE_TIMEOUT = 1012             # 超时

提示信息 ; application/utils/message,代码:

no_such_history = '没有这条历史记录'
timeout = '超时'

客户端发起取消好友申请操作

html/add_friend.html,代码:

<!DOCTYPE html>
<html>
    <head>
        <title>添加好友</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta charset="utf-8">
        <link rel="stylesheet" href="../static/css/main.css">
        <script src="../static/js/vue.js"></script>
        <script src="../static/js/axios.js"></script>
        <script src="../static/js/uuid.js"></script>
        <script src="../static/js/v-avatar-2.0.3.min.js"></script>
        <script src="../static/js/main.js"></script>
    </head>
    <body>
        <div class="app frame avatar update_nickname add_friend" id="app">
            <div class="box">
                <p class="title">添加好友</p>
                <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
                <div class="content">
                    <input class="nickname" type="text" v-model="account" placeholder="输入昵称/手机/邮箱/魔方账号....">
                </div>
                <div class="friends_list">
                    <div class="item" v-for='user in user_list'>
                        <div class="avatar">
                            <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                            <div class="user_avatar">
                                <v-avatar v-if="user.avatar" :src="user.avatar" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else-if="user.nickname" :username="user.nickname" :size="33" :rounded="true"></v-avatar>
                                <v-avatar v-else :username="user.id" :size="33" :rounded="true"></v-avatar>
                            </div>
                            <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                        </div>
                        <div class="info">
                            <p class="username">{{user.nickname}}</p>
                        </div>
                        <div class="status" @click="apply_friend(user)">添加</div>
                    </div>
                    <div class="item" v-for='hfriend in friend_history_list'>
                        <!-- 自己申请好友 -->
                        <div v-if='hfriend.apply_user.id == user_info.id'>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.applied_user.avatar" :src="hfriend.applied_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.applied_user.nickname" :username="hfriend.applied_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.applied_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.applied_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <!-- 用户取消自己发起的好友申请 -->
                            <div class="status" v-if='hfriend.get_status[0] == 1' @click='cancel_apply_friend(hfriend)'>{{hfriend.get_status[1]}}</div>
                            <div class="status" v-else>{{hfriend.get_status[1]}}</div>
                        </div>
                        <!-- 用户被申请 -->
                        <div v-else>
                            <div class="avatar">
                                <img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
                                <div class="user_avatar">
                                    <v-avatar v-if="hfriend.apply_user.avatar" :src="hfriend.apply_user.avatar" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else-if="hfriend.apply_user.nickname" :username="hfriend.apply_user.nickname" :size="33" :rounded="true"></v-avatar>
                                    <v-avatar v-else :username="hfriend.apply_user.id" :size="33" :rounded="true"></v-avatar>
                                </div>
                                <img class="avatar_border" src="../static/images/avatar_border.png" alt="">
                            </div>
                            <div class="info">
                                <p class="username">{{hfriend.apply_user.nickname}}</p>
                                <!-- 时间需要过滤显示 -->
                                <p class="time">{{hfriend.created_time|time_format}}</p>
                            </div>
                            <div class="status" v-if="hfriend.get_status[0]==1" @click="change_apply(hfriend)">等待审核</div>
                            <div class="status" v-if="hfriend.get_status[0]==2">已同意</div>
                            <div class="status" v-if="hfriend.get_status[0]==3">{{hfriend.get_status[1]}}</div>
                            <div class="status" v-if="hfriend.get_status[0]==4">{{hfriend.get_status[1]}}</div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <script>
            apiready = function(){
                var game = new Game("../static/mp3/bg1.mp3");
                // 在 #app 标签下渲染一个按钮组件
                Vue.prototype.game = game;
                new Vue({
                    el:"#app",
                    data(){
                        return {
                            account:"", // 搜索用户名
                            search_timer:null, // 搜索定时器标记符
                            user_list: [],     // 搜索结果用户列表
                            user_info: {}, // 当前用户信息
                            friend_history_list: [], // 好友申请历史列表
                        }
                    },
                    // 监听事件
                    watch:{
                        account(){
                            // 节流防抖
                            if(this.account.length >= 1){
                                // 清除定时器
                                clearTimeout(this.search_timer)
                                // 停止输入后,两秒钟发送请求
                                this.search_timer = setTimeout(() => {
                                    this.search_user_info();
                                },2000)
                            }
                        }
                    },
                    created(){
                        this.get_user_info() // 获取当前用户信息
                        this.get_apply_friend_history() // 获取好友申请历史记录
                    },
                    // 过滤器
                    filters:{
                        // 时间过滤器
                        time_format(time){
                            // 计算时间距离,返回文本格式
                            his_time_obj = new Date(time)
                            now_time_obj = new Date()
                            duration = parseInt((now_time_obj - his_time_obj)/1000)
                            if(0 <= duration && duration < 60 * 5){
                                // 5分钟内
                                return '刚刚'
                            }
                            if(60 * 5 <= duration && duration < 60 * 60){
                                // 1小时内
                                return parseInt(duration/60) + '分前'
                            }
                            if(60 * 60 <= duration && duration < 60 * 60 * 24){
                                // 1天内
                                return parseInt(duration/60/60) + '小时前'
                            }
                            if(60 * 60 * 24 <= duration && duration < 60 * 60 * 24 * 30){
                                // 1月内
                                return parseInt(duration/60/60/24) + '天前'
                            }
                            // 判断月份和年份的时间距离
                            let year_duration = now_time_obj.getFullYear() - his_time_obj.getFullYear();
                            let month_duration = now_time_obj.getMonth() - his_time_obj.getMonth();
                            if( now_time_obj.getMonth() < his_time_obj.getMonth() ){
                                month_duration +=12;
                            }

                            if(year_duration > 1 && year_duration < 10){
                                return year_duration+"年前";
                            }

                            if(year_duration > 5){
                                return time.split("T")[0];
                            }

                            if( month_duration < 6){
                                return month_duration+"个月前";
                            }

                            if( month_duration < 12 ){
                                return "半年前";
                            }

                        },
                    },
                    methods:{
                        back(){
                            this.game.closeFrame();
                        },
                        // 获取当前用户信息
                        get_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.user_info = self.game.get_user_by_token(token)
                            })
                        },

                        // 获取好友申请历史记录
                        get_apply_friend_history(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.get_apply_friend_history',
                                    'params': {},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.friend_history_list = data.result.friend_history_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },
                        // 提交搜索信息,获取用户列表
                        search_user_info(){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.search_user_info',
                                    'params': {'account': self.account},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.user_list = data.result.user_list
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 用户申请添加好友
                        apply_friend(user){
                            let self = this
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.apply_friend',
                                    'params': {'applied_user_id': user.id},
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("成功发起好友申请!请等待...");
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 审核好友申请
                        change_apply(history){
                            api.actionSheet({
                                title:  `是否同意来自${history.apply_user.nickname}的好友申请?`,
                                cancelTitle: '忽略',
                                destructiveTitle: '同意',
                                buttons: ['拒绝']
                            }, (ret, err)=>{
                                if(ret.buttonIndex < 3){
                                    this.change_apply_http(history, ret.buttonIndex)
                                }
                            });
                        },
                        // 发送审核好友申请结果到服务端
                        change_apply_http(history, buttonIndex){
                            let status = buttonIndex==1?true:false;
                            let self = this;
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.add_friend',
                                    'params': {
                                        "apply_user_id": history.apply_user.id,
                                        "history_id": history.id,
                                        "status": status,
                                    },
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("审核操作成功~");
                                            // 发起全局广播通知
                                            self.game.sendEvent('add_friend_success')
                                            // 更改页面好友状态
                                            if(status){
                                                history.get_status = [2,'已同意']
                                            }else {
                                                history.get_status = [4,'已拒绝']
                                            }
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },

                        // 用户取消好友申请
                        cancel_apply_friend(history){
                            api.actionSheet({
                                title:  `是否取消申请${history.applied_user.nickname}为好友?`,
                                cancelTitle: '忽略',
                                destructiveTitle: '取消',
                            }, (ret, err)=>{
                                // this.game.print(ret.buttonIndex)
                                if(ret.buttonIndex == 1){
                                    // 取消申请发送请求
                                    this.cancel_apply_friend_http(history)
                                }
                            });
                        },

                        // 用户取消自己好友申请请求到服务端
                        cancel_apply_friend_http(history){
                            let self = this;
                            self.game.check_user_login(self, () => {
                                let token = self.game.getdata('access_token') || self.game.getfs('access_token')
                                self.game.post(self, {
                                    'method': 'Users.cancel_apply_friend',
                                    'params': {
                                        "history_id": history.id,
                                    },
                                    'header': {
                                        'Authorization': 'jwt ' + token
                                    },
                                    success(response){
                                        let data = response.data;
                                        if(data.result && data.result.errno === 1000){
                                            self.game.tips("取消好友申请成功~");
                                            // 更改页面好友状态
                                            history.get_status = [5,'已取消']
                                        } else {
                                            self.game.tips(data.result.errmsg)
                                        }
                                    }
                                })
                            })
                        },


                    }
                });
            }
        </script>
    </body>
</html>


展示好友列表

服务端提供当前用户好友列表功能

  1. 接口视图,users.api,代码:
# 获取用户好友列表
@jwt_required()
@decorator.get_user_object
def get_friend_list(user):
    '''
    获取用户好友列表
    :param user: 装饰器通过token获取的用户模型对象
    :return:
    '''
    friend_list = services.get_friend_list(user)

    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok,
        'friend_list': friend_list
    }

  1. 因为用户好友列表往往需要进行昵称排序,所以我们需要准备一个文字转拼音模块。
# 安装文字转拼音模块
pip install xpinyin

基本用法:

from xpinyin import Pinyin
pinyin = Pinyin()  # 实例拼音转换对象
# 拼音转换
ret = pinyin.get_pinyin("汉语拼音转换", tone_marks='marks')
ret1 = pinyin.get_pinyin("汉语拼音转换", tone_marks='numbers')
print(ret) # hàn-yǔ-pīn-yīn-zhuǎn-huàn
print(ret1) # han4-yu3-pin1-yin1-zhuan3-huan4

在项目入口主文件进行初始化 application/__init__.py

from xpinyin import Pinyin

# 文字转拼音初始化
pinyin = Pinyin()

  1. 数据服务层: users.services,代码:
from application import pinyin

# 获取用户的好友列表
def get_friend_list(user):
    '''
    获取用户的好友列表
    :param user: 用户模型对象
    :return:
    '''
    # 用户申请好友或被申请为好友信息都查出来
    friends = UserFriendShip.query.filter(
        or_(
            UserFriendShip.apply_id == user.id,
            UserFriendShip.applied_id == user.id
        )
    ).all()

    from .marshmallow import FriendShipSchema
    # 实力化好友关系构造器,通过context进行参数传递在构造器内部调用
    fs = FriendShipSchema(context={'user':user})
    # 序列化输出
    friend_list = fs.dump(friends, many=True)
    # 输出数据根据名字拼音排序
    data = sorted(friend_list, key=lambda item: pinyin.get_pinyin(item['nickname'], tone_marks='number'), reverse=False)
    
    return data

  1. 好友关系构造器,users.marshmallow,代码:
# 好友关系构造器
class FriendShipSchema(SQLAlchemyAutoSchema):
    '''好友关系构造器'''
    # 构造器嵌套
    apply_user = fields.Nested(UserSchema)
    applied_user = fields.Nested(UserSchema)
    class Meta:
        model = UserFriendShip
        include_fk = False  # 启用外键关系
        include_relationships = True  # 模型关系外检属性
        # 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
        fields = ["id", "apply_user", "applied_user"]

    # 加工序列化输出数据
    @post_dump
    def post_dump(self, data, **kwargs):
        # 当前用户模型对象 - 构造器初始化传过来的
        current_user = self.context.get('user')
        # 判断当前用户是属于主动添加还是被动添加
        if data['apply_user']['id'] == current_user.id:
            '''当前好友关系属于用户主动添加的,则返回被添加的用户'''
            data = data['applied_user']
        else:
            # 被动添加
            data = data['apply_user']

        return data

  1. 路由,users.urls,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = []

# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
    api_rpc('check_mobile', api.check_mobile),
    api_rpc('register', api.register),
    api_rpc('login', api.login),
    api_rpc('refresh', api.refresh_token), # 刷新access_token值
    api_rpc('update_avatar', api.update_avatar), # 更新头像
    api_rpc('update_nickname', api.update_nickname), # 更新昵称
    api_rpc('update_mobile', api.update_mobile), # 更新手机号
    api_rpc('update_password', api.update_password), # 更新登录密码
    api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
    api_rpc('search_user_info', api.search_user_info), # 搜索用户信息
    api_rpc('apply_friend', api.apply_friend), # 添加用户好友申请记录
    api_rpc('add_friend', api.add_friend), # 添加用户好友关系记录
    api_rpc('cancel_apply_friend', api.cancel_apply_friend), # 用户取消自己申请的好友记录
    api_rpc('get_friend_list', api.get_friend_list), # 获取好友列表
]


客户端在用户进入好友列表页展示好友列表数据

  1. 进入页面展示好友列表数据 friend_list.html,代码:
<!DOCTYPE html>
<html>

<head>
	<title>好友列表</title>
	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<meta charset="utf-8">
	<link rel="stylesheet" href="../static/css/main.css">
	<script src="../static/js/vue.js"></script>
	<script src="../static/js/axios.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/v-avatar-2.0.3.min.js"></script>
	<script src="../static/js/main.js"></script>
</head>

<body>
	<div class="app user setting" id="app">
		<div class="friends_list">
			<div class="item" v-for='friend in friend_list'>
				<div class="avatar">
					<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
					<div class="user_avatar">
						<v-avatar v-if="friend.avatar" :src="friend.avatar" :size="55" :rounded="true"></v-avatar>
						<v-avatar v-else-if="friend.nickname" :username="friend.nickname" :size="55" :rounded="true"></v-avatar>
						<v-avatar v-else :username="friend.id" :size="55" :rounded="true"></v-avatar>
					</div>
					<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
				</div>
				<div class="info">
					<p class="username">{{friend.nickname}}</p>
					<p class="fruit">果子: {{game.credit_format(friend.credit)}}</p>
				</div>
				<div class="behavior pick">摘</div>
				<div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
			</div>
			<div class="item">
				<div class="avatar">
					<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
					<img class="user_avatar" src="../static/images/avatar.png" alt="">
					<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
				</div>
				<div class="info">
					<p class="username">长昵称都很好</p>
					<p class="fruit">果子:9,999.00</p>
				</div>
				<div class="behavior protect">护</div>
				<div class="goto"><img src="../static/images/arrow1.png" alt=""></div>
			</div>
		</div>
	</div>
	<script>
		apiready = function() {
			var game = new Game("../static/mp3/bg1.mp3");
			// 在 #app 标签下渲染一个按钮组件
			Vue.prototype.game = game;
			new Vue({
				el: "#app",
				data() {
					return {
						friend_list: [], // 好友列表
						page: 1,
						prev: {
							name: "",
							url: "",
							params: {}
						},
						current: {
							name: "friend_list",
							url: "friend_list.html",
							params: {}
						},
					}
				},
				created() {
					// 获取好友列表
					this.get_friend_list();
					// 刷新好友列表
					this.refresh_friend_list();
					// 监听事件
					this.listen()
				},
				methods: {
					// 监听事件
					listen() {
						// 监听其他页面进行的添加好友操作通知,获取最新好友列表
						this.listen_add_friend();
					},

					listen_add_friend() {
						api.addEventListener({
							name: 'add_friend_success'
						}, (ret, err) => {
							this.get_friend_list()
						});
					},

					// 获取好友列表
					get_friend_list() {
						let self = this
							// 检测token是否过期,刷新token值
						self.game.check_user_login(self, () => {
							let token = self.game.getdata('access_token') || self.game.getfs('access_token')
								// 向服务端发送请求,或好友列表
							self.game.post(self, {
								'method': 'Users.get_friend_list',
								'params': {},
								'header': {
									'Authorization': 'jwt ' + token
								},
								success(response) {
									let data = response.data
									if (data.result && data.result.errno === 1000) {
										self.game.print(data.result.friend_list)
											// 请求成功,获取好友列表
										self.friend_list = data.result.friend_list
									} else {
										self.game.tips(data.result.errmsg)
									}
								}
							})
						})
					},

					// 下拉刷新好友列表
					refresh_friend_list() {
						// 下拉刷新好友列表
						api.setRefreshHeaderInfo({
							loadingImg: 'widget://image/refresh.png',
							bgColor: null,
							textColor: '#fff',
							textDown: '下拉刷新...',
							textUp: '松开刷新...'
						}, (ret, err) => {
							// 在这里从服务器加载数据,刷新好友列表
							this.get_friend_list()
								// 加载完成后调用api.refreshHeaderLoadDone()方法恢复组件到默认状态
							setTimeout(() => {
								api.refreshHeaderLoadDone();
							}, 1500);
						});
					},

					goto_home() {
						// 退出当前页面
						this.game.closeFrame();
					},
				}
			});
		}
	</script>
</body>

</html>


  1. static/js/main.js 添加果子积分格式转换(英文格式数字 - 123,45.67)
class Game{
    // ...省略
    // 打印数据方法更改
	print(data, st = false) {
      // 打印数据
      data = JSON.stringify(data);
      if (st) {
          api.alert({
              "msg": data
          });
      } else {
          console.log(data);
      }
  	}
	// ...省略
	// 果子格式化
	credit_format(credit){
		return credit.toLocaleString('en-US')
	}
}

posted @ 2021-06-23 16:21  十九分快乐  阅读(435)  评论(0编辑  收藏  举报