zoukankan      html  css  js  c++  java
  • 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;
       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{
       6.39rem;
      height: 6.39rem;
      position: relative;
    }
    .friends_list .avatar_bf{
      position: absolute;
      z-index: 1;
      margin: auto;
       4.56rem;
      height: 4.56rem;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
    }
    .friends_list .user_avatar{
      position: absolute;
      z-index: 1;
       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;
       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;
       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;
       10rem;
    }
    .friends_list .item .behavior{
      position: absolute;
      left: 16rem;
      font-size: 1.5rem;
      text-align: center;
      line-height: 4rem;
      height: 4rem;
       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;
       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;
       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;
       19rem;
      margin-bottom: 1rem;
      box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
    }
    .add_friend .friends_list .avatar{
       3.84rem;
      height: 3.84rem;
      position: absolute;
      left: 1rem;
    }
    
    .add_friend .friends_list .avatar_bf{
      position: absolute;
      z-index: 1;
      margin: auto;
       2.74rem;
      height: 2.74rem;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
    }
    
    .add_friend .friends_list .user_avatar{
      position: absolute;
      z-index: 1;
       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;
       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;
       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')
    	}
    }
    
    
  • 相关阅读:
    自闭的D7
    D2
    Codeforces Round #531 (Div. 3)
    hello 2019 D
    牛客练习赛36B
    cf954H
    gym102007 E
    Gym 101972
    Gym 101810
    试题 历届试题 青蛙跳杯子(bfs)
  • 原文地址:https://www.cnblogs.com/jia-shu/p/14923281.html
Copyright © 2011-2022 走看看