浏览代码

最近分享与联系人

1.重构分享数据结构
2.最近分享
3.联系人
vian 5 年之前
父节点
当前提交
80f5c7696b

+ 1 - 1
modules/all_models/projects.js

@@ -26,7 +26,7 @@ const ProjectSchema = new Schema({
     "compilation": String,
     "deleteInfo": deleteSchema,
     'fullFolder': Array,
-    "shareInfo": [shareSchema],
+    //"shareInfo": [shareSchema],
     "property": {
         type: Schema.Types.Mixed,
         default: {}

+ 26 - 0
modules/all_models/share_list.js

@@ -0,0 +1,26 @@
+/*
+ * @Descripttion: 分享列表
+ * @Author: Zhong
+ * @Date: 2020-01-10 10:51:35
+ */
+
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+const shareSchema = new Schema({
+    ID: String,
+    projectID: Number,
+    owner: String, // 项目拥有者ID
+    receiver: String, // 接收者ID
+    allowCopy: {
+        type: Boolean,
+        default: false
+    },
+    allowCooperate: {
+        type: Boolean,
+        default: false
+    },
+    shareDate: String,
+    updateDate: String
+}, {versionKey: false});
+
+mongoose.model('share_list', shareSchema, 'share_list');

+ 5 - 0
modules/all_models/user.js

@@ -84,6 +84,11 @@ let schema = {
         type: [userdList],
         default: []
     },
+    // 联系人
+    contacts: {
+        type: Array,
+        default: []
+    },
     // 是否邮箱已通过验证
     isUserActive: Number,
     // 是否只允许短信登录

+ 2 - 2
modules/main/routes/main_route.js

@@ -14,14 +14,14 @@ module.exports =function (app) {
     app.get('/main', baseController.init, function(req, res) {
         let pm = require('../../pm/controllers/pm_controller');
 
-        pm.checkProjectRight(req.session.sessionUser.id, req.query.project, async function (hasRight, projectData, shareInfo) {
+        pm.checkProjectRight(req.session.sessionUser.id, req.query.project, async function (hasRight, projectData, allowCooperate) {
             if (hasRight) {
                 //分享的项目,只读、协作(允许编辑)
                 let projectReadOnly = false,
                     projectCooperate = false;
                 if(req.session.sessionUser.id !== projectData.userID){
                     projectData._doc.readOnly = true;
-                    projectCooperate = !!shareInfo.allowCooperate;
+                    projectCooperate = allowCooperate;
                     //允许协作的项目允许编辑,非只读
                     projectReadOnly = !projectCooperate;
                 }

+ 108 - 29
modules/pm/controllers/pm_controller.js

@@ -4,6 +4,8 @@
 import UnitPriceFileModel from "../../glj/models/unit_price_file_model";
 import moment from 'moment';
 import CompilationModel from "../../users/models/compilation_model";
+import UserModel from "../../users/models/user_model";
+const userModelObj = new UserModel();
 let mongoose = require('mongoose');
 let ProjectsData = require('../models/project_model').project;
 let labourCoe = require('../../main/facade/labour_coe_facade');
@@ -26,6 +28,7 @@ const stdBillsGuidanceLibModel = mongoose.model('std_billsGuidance_lib');
 const fs = require('fs');
 const _ = require('lodash');
 import SectionTreeDao from '../../complementary_ration_lib/models/sectionTreeModel';
+const uuidV1 = require('uuid/v1');
 let sectionTreeDao = new SectionTreeDao();
 let consts = require('../../main/models/project_consts');
 const rationLibModel = mongoose.model('std_ration_lib_map');
@@ -59,10 +62,11 @@ module.exports = {
             let shareInfo = null;
             //判断是否是打开分享的项目,分享项目shareInfo不为null
             if(userId !== result.userID){
-                shareInfo = await pm_facade.getShareInfo(userId, result);
+                shareInfo = await pm_facade.getShareInfo(userId, result.ID);
             }
             if ((userId === result.userID || shareInfo) && result._doc.projType === projType.tender) {
-                callback(true, result, shareInfo);
+                const allowCooperate = (shareInfo || {}).allowCooperate;
+                callback(true, result, allowCooperate);
             } else {
                 callback(false);
             }
@@ -296,6 +300,8 @@ module.exports = {
     },
     // 项目管理首页
     index: async function(request, response) {
+        // TODO 上线后删除,处理旧的分享数据
+        await pm_facade.prepareShareList();
         // 获取编办信息
         let sessionCompilation = request.session.sessionCompilation;
         if (sessionCompilation === undefined ||sessionCompilation ===null) {
@@ -564,24 +570,88 @@ module.exports = {
     },
     projectShareInfo: async function(req, res){
         try{
-            let data = JSON.parse(req.body.data);
-            let shareInfo = await projectModel.findOne({ID: data.projectID, $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}]}, '-_id shareInfo');
+            const data = JSON.parse(req.body.data);
+            const shareList = await pm_facade.getShareList({projectID: data.projectID});
+            const shareInfoMap = await pm_facade.getShareInfoMap(null, shareList);
+            const shareInfo = shareInfoMap[data.projectID] || [];
+            //let shareInfo = await projectModel.findOne({ID: data.projectID, $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}]}, '-_id shareInfo');
             callback(req, res, 0, 'success', shareInfo);
         }
         catch (err){
             callback(req, res, 1, err, null);
         }
     },
+    getRecentShareInfo: async function (req, res) {
+        try {
+            const { count } = JSON.parse(req.body.data);
+            const userID = req.session.sessionUser.id;
+            const recentUsers = await pm_facade.getRecentShareList(userID, count);
+            const contacts = await userModelObj.getContacts(userID);
+            callback(req, res, 0, 'success', { recentUsers, contacts });
+        } catch (err) {
+            console.log(err);
+            callback(req, res, 1, err.message, null);
+        }
+    },
     share: async function(req, res){
         try{
-            let data = JSON.parse(req.body.data);
-            let shareDate = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss'),
-                shareUserIDs = [];
-            for (let data of data.shareData) {
-                shareUserIDs.push(data.userID);
-                data.shareDate = shareDate;
+            const data = JSON.parse(req.body.data);
+            const { type, shareData, projectID } = data;
+            const userID = req.session.sessionUser.id;
+            const shareDate = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
+            shareData.forEach(item => item.shareDate = shareDate);
+            const task = [];
+            if (type === 'create') {
+                // 生成分享记录
+                const docs = shareData.map(item => ({
+                    ID: uuidV1(),
+                    projectID,
+                    owner: userID,
+                    receiver: item.userID,
+                    allowCooperate: item.allowCooperate,
+                    allowCopy: item.allowCopy,
+                    shareDate: item.shareDate,
+                    updateDate: item.shareDate
+                }));
+                task.push(pm_facade.addShareList(docs));
+                // 分享即互相添加为联系人
+                task.push(userModelObj.addContact(docs[0].owner, docs[0].receiver));
+            } else if (type === 'update') {
+                // 取消分享(项目管理界面可一次进行更新和取消)
+                const cancelReceivers = shareData
+                    .filter(item => item.isCancel)
+                    .map(item => item.userID);
+                if (cancelReceivers.length) {
+                    task.push(pm_facade.deleteShareList({projectID, receiver: {$in: cancelReceivers}}));
+                }
+                // 更新分享选项
+                const updateData = shareData
+                    .filter(item => !item.isCancel)
+                    .map(item => (
+                        {
+                            query: {
+                                projectID,
+                                receiver: item.userID
+                            },
+                            update: {
+                                allowCopy: item.allowCopy,
+                                allowCooperate: item.allowCooperate,
+                                updateDate: shareDate
+                            }
+                        }
+                    ));
+                if (updateData.length) {
+                    task.push(pm_facade.updateShareList(updateData))
+                }
+            } else { // 取消分享
+                const cancelReceivers = shareData.map(item => item.userID);
+                task.push(pm_facade.deleteShareList({projectID, receiver: {$in: cancelReceivers}}));
             }
-            //添加分享
+            await Promise.all(task);
+            const rstData = shareData.filter(item => !item.isCancel);
+            callback(req, res, 0, 'success', rstData);
+
+           /*  //添加分享
             if(data.type === 'create'){
                 //新增
                 for (let sData of data.shareData) {
@@ -589,35 +659,40 @@ module.exports = {
                 }
             } else if (data.type === 'update') {
                 await projectModel.update({ID: data.projectID, $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}]}, {$set: {shareInfo: data.shareData}});
-            }
-            //取消分享
-            else {
+            } else { //取消分享
                 await projectModel.update({ID: data.projectID, $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}]}, {$pull: {shareInfo: {userID: {$in: shareUserIDs}}}});
             }
-            callback(req, res, 0, 'success', data.shareData);
+            callback(req, res, 0, 'success', data.shareData); */
         }
         catch (err){
             callback(req, res, 1, err, null);
         }
     },
+    // TODO bug: 单位工程分享、建设项目也分享的情况下,显示有问题
     receiveProjects: async function(req, res) {
         try {
             let rst = {grouped: [], ungrouped: [], summaryInfo: null};
             let userID = req.session.sessionUser.id;
-            let receiveProjects = await projectModel.find({
-                $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}], compilation: req.session.sessionCompilation._id, 'shareInfo.userID': userID}, '-_id');
+            const shareList = await pm_facade.getShareList({ receiver: userID });
+            const receiveProjectIDs = shareList.map(item => item.projectID);
+            const compilation = req.session.sessionCompilation._id;
+            const notDeleted = [{deleteInfo: null}, {'deleteInfo.deleted': false}];
+            const receiveProjects = await projectModel.find({ID: {$in: receiveProjectIDs}, compilation, $or: notDeleted}, '-_id').lean();
+            /* let receiveProjects = await projectModel.find({
+                $or: [{deleteInfo: null}, {'deleteInfo.deleted': false}], compilation: req.session.sessionCompilation._id, 'shareInfo.userID': userID}, '-_id'); */
             //设置原项目用户信息
+            const shareInfoMap = await pm_facade.getShareInfoMap(null, shareList);
             if(receiveProjects.length > 0){
                 let orgUserIDs = [];
                 for(let proj of receiveProjects){
                     orgUserIDs.push(proj.userID);
                     if (proj.projType === projType.tender) {
                         //设置工程专业
-                        proj._doc.feeStandardName = proj.property.feeStandardName || '';
+                        proj.feeStandardName = proj.property.feeStandardName || '';
                         //设置计税方法
-                        proj._doc.taxType = proj.property.taxType;
+                        proj.taxType = proj.property.taxType;
                     }
-                    delete proj._doc.property;
+                    delete proj.property;
                 }
                 orgUserIDs = Array.from(new Set(orgUserIDs));
                 let userObjIDs = [];
@@ -629,28 +704,32 @@ module.exports = {
                 let consProjIDs = [],
                     ungroupedTenders = [];
                 for(let proj of receiveProjects){
+                    // 设置分享信息
+                    proj.shareInfo = shareInfoMap[proj.ID] || [];
                     if (proj.projType === projType.project) {
                         consProjIDs.push(proj.ID);
                     }
                     //获取分享项目子项
                     if (proj.projType !== projType.tender) {
-                        proj._doc.children = await pm_facade.getPosterityProjects([proj.ID]);
-                        for (let projC of proj._doc.children) {
+                        proj.children = await pm_facade.getPosterityProjects([proj.ID]);
+                        for (let projC of proj.children) {
+                            // 设置分享信息
+                            projC.shareInfo = shareInfoMap[projC.ID] || [];
                             if (projC.projType === projType.project) {
                                 consProjIDs.push(projC.ID);
                             } else if (projC.projType === projType.tender) {
                                 //设置工程专业
-                                projC._doc.feeStandardName = projC.property.feeStandardName || '';
+                                projC.feeStandardName = projC.property.feeStandardName || '';
                                 //设置计税方法
-                                projC._doc.taxType = projC.property.taxType;
+                                projC.taxType = projC.property.taxType;
                                 if (proj.projType === projType.engineering) {
-                                    ungroupedTenders.push(projC._doc);
+                                    ungroupedTenders.push(projC);
                                 }
                             }
-                            delete projC._doc.property;
+                            delete projC.property;
                         }
                     } else {//未分类的单位工程不进行汇总,只取价格信息
-                        ungroupedTenders.push(proj._doc);
+                        ungroupedTenders.push(proj);
                     }
                     //设置分组,单位工程及单项工程分到未分组那
                     if (proj.projType === projType.tender || proj.projType === projType.engineering) {
@@ -659,11 +738,11 @@ module.exports = {
                         rst.grouped.push(proj);
                     }
                     //设置项目类型为来自别人分享
-                    proj._doc.shareType = 'receive';
+                    proj.shareType = 'receive';
                     for(let userData of orgUsersInfo){
                         if(proj.userID == userData._id.toString()){
                             let userInfo = {name: userData.real_name, mobile: userData.mobile, company: userData.company, email: userData.email};
-                            proj._doc.userInfo = userInfo;
+                            proj.userInfo = userInfo;
                         }
                     }
                 }

+ 162 - 27
modules/pm/facade/pm_facade.js

@@ -9,6 +9,13 @@ const projectType = {
 };
 //先导出后require可以解决循环引用问题
 module.exports={
+    prepareShareList,
+    getShareList,
+    addShareList,
+    updateShareList,
+    deleteShareList,
+    getShareInfoMap,
+    getRecentShareList,
     moveProject:moveProject,
     copyProject:copyProject,
     copyExample: copyExample,
@@ -84,6 +91,7 @@ let contractorListModel = mongoose.model("contractor_list");
 let featureLibModel =  mongoose.model("std_project_feature_lib");
 let importLogsModel = mongoose.model("import_logs");
 const overHeightLibModel = mongoose.model('std_over_height_lib');
+const shareListModel = mongoose.model('share_list');
 let scMathUtil = require('../../../public/scMathUtil').getUtil();
 let counter = require('../../../public/counter/counter');
 import SectionTreeDao from '../../complementary_ration_lib/models/sectionTreeModel';
@@ -104,6 +112,7 @@ let labourCoeFacade = require('../../main/facade/labour_coe_facade');
 let calcProgramFacade = require('../../main/facade/calc_program_facade');
 let mainColLibModel = mongoose.model('std_main_col_lib');
 import EngineeringLibModel from "../../users/models/engineering_lib_model";
+import { mongo } from "mongoose";
 let installationFacade = require('../../main/facade/installation_facade');
 let cipher = require('../../../public/cipher');
 let qiniu = require("qiniu");
@@ -120,6 +129,138 @@ let qiniu_config = {
 };
 let mac = new qiniu.auth.digest.Mac(qiniu_config.AccessKey, qiniu_config.SecretKey);
 
+// 重构了分享的数据结构,原本存在projects表的shareInfo数组中
+// 为了更好地适应新功能及以后拓展功能,现将分享信息存放在单独的share_list表中
+// 为了适应旧数据,需要在share_list没有数据的时候,将原本的分享信息复制到share_list中,同时生成联系人
+// TODO: 上线后这个方法执行过后,应该删除此方法
+async function prepareShareList() {
+    const shareListCount = await shareListModel.count({});
+    console.log(shareListCount);
+    if (shareListCount) {
+        return;
+    }
+    logger.info('================================enterPrepareShareList================================================');
+    // 将项目的shareInfo数据复制到share_list表中
+    const projects = await projectModel.find({'shareInfo.0': {$exists: true}}, '-_id ID userID shareInfo').lean();
+    if (!projects.length) {
+        return;
+    }
+    const shareList = [];
+    const userMap = {}; // 给生成联系人做准备
+    projects.forEach(project => {
+        const ownerContacts = userMap[project.userID] || (userMap[project.userID] = []);
+        project.shareInfo.forEach(shareItem => {
+            if (!shareItem.shareDate) {
+                return;
+            }
+            const receiverContacts = userMap[shareItem.userID] || (userMap[shareItem.userID] = []);
+            receiverContacts.push(project.userID);
+            const newItem = {
+                ID: uuidV1(),
+                projectID: project.ID,
+                owner: project.userID,
+                receiver: shareItem.userID,
+                allowCopy: shareItem.allowCopy,
+                allowCooperate: shareItem.allowCooperate,
+                shareDate: shareItem.shareDate,
+                updateDate: shareItem.shareDate
+            };
+            ownerContacts.push(shareItem.userID);
+            shareList.push(newItem);
+        });
+    });
+    // 插入分享信息
+    const shareListPromise = shareListModel.insertMany(shareList);
+    const tasks = [shareListPromise];
+    // 更新用户信息
+    const userTasks = [];
+    Object.entries(userMap).forEach(([userID, contacts]) => {
+        contacts = [...new Set(contacts)]
+            .map(user => ({ userID: user }));
+        const task = {
+            updateOne: {
+                filter: {
+                    _id: mongoose.Types.ObjectId(userID)
+                },
+                update: {
+                    contacts
+                }
+            }
+        };
+        userTasks.push(task);
+    });
+    if (userTasks.length) {
+        tasks.push(userModel.bulkWrite(userTasks));
+    }
+    await Promise.all(tasks);
+}
+
+
+// 获取分享列表
+async function getShareList(query) {
+    return await shareListModel.find(query, '-_id').lean();
+}
+
+async function addShareList(docs) {
+    await shareListModel.insertMany(docs);
+}
+
+async function deleteShareList(query) {
+    await shareListModel.deleteMany(query);
+}
+
+async function updateShareList(updateData) {
+    const bulks = updateData.map(item => (
+        {
+            updateOne: {
+                filter: item.query,
+                update: item.update
+            }
+        }
+    ));
+    if (bulks.length) {
+        await shareListModel.bulkWrite(bulks);
+    }
+}
+
+// 根据项目ID获取项目shareInfo字段的数据映射(前端旧逻辑依赖项目本身的shareInfo字段)
+async function getShareInfoMap(projectIDs, shareList = null) {
+    if (!shareList) {
+        shareList = await getShareList({projectID: {$in: projectIDs}});
+    }
+    // 将相同项目的分享信息进行映射
+    const map = {};
+    shareList.forEach(item => {
+        const shareInfo = map[item.projectID] || (map[item.projectID] = []);
+        shareInfo.push({
+            userID: item.receiver,
+            shareDate: item.shareDate,
+            updateDate: item.updateDate,
+            allowCopy: item.allowCopy,
+            allowCooperate: item.allowCooperate
+        });
+    });
+    return map;
+}
+
+// 获取最近分享(只算分享出去的记录)
+async function getRecentShareList(userID, count) {
+    const shareList = await getShareList({owner: userID});
+    shareList.sort((a, b) => Date.parse(b.shareDate) - Date.parse(a.shareDate));
+    const set = new Set();
+    for(const item of shareList) {
+        if (set.size === count) {
+            break;
+        }
+        set.add(item.receiver);
+    }
+    const sortedIDList = [...set];
+    const userIDList = sortedIDList.map(userID => mongoose.Types.ObjectId(userID));
+    const users = await userModel.find({_id: {$in: userIDList}}, 'real_name mobile company').lean();
+    users.sort((a, b) => sortedIDList.indexOf(a._id.toString()) - sortedIDList.indexOf(b._id.toString()));
+    return users;
+}
+
 //拷贝例题项目
 //@param {String}userID {Array}projIDs拷贝的例题项目ID(建设项目、文件夹)@return {Boolean}
 async function copyExample(userID, compilation, projIDs){
@@ -127,7 +268,7 @@ async function copyExample(userID, compilation, projIDs){
         IDMapping = {},
         projMapping = {};
     //例题项目不可为单项工程、单位工程、只能是建设项目、文件夹,否则不自动新建例题项目(这里不报错,让用户没有感觉)
-    let parentExample = await projectModel.find({ID: {$in: projIDs}, $or: notDeleted});
+    let parentExample = await projectModel.find({ID: {$in: projIDs}, $or: notDeleted}).lean();
     if (parentExample.length === 0) {
         return false;
     }
@@ -168,7 +309,7 @@ async function copyExample(userID, compilation, projIDs){
         data.compilation = compilation;
         data.shareInfo = [];
         if (data.projType !== projectType.tender) {
-            let newData = _.cloneDeep(data._doc);
+            let newData = _.cloneDeep(data);
             delete newData._id;
           //  await projectModel.create(newData);
             parentBulks.push({insertOne: {document: newData}});
@@ -1066,13 +1207,24 @@ async function getFullPath(projectID) {
     }
 }
 
+// 获取项目链上,自身、父项、爷爷项...的ID
+async function getUpChainIDs(projectID) {
+    const rst = [];
+    while (projectID != -1) {
+        rst.push(projectID);
+        const project = await projectModel.findOne({ ID: projectID }, '-_id ParentID').lean();
+        projectID = project ? project.ParentID : -1;
+    }
+    return rst;
+}
+
 //获取projectIDs文件下所有子项目(不包括projectIDs本身)
 async function getPosterityProjects(projectIDs) {
     let rst = [];
     async function getProjects(parentIDs) {
         if (parentIDs.length > 0) {
             let newIDs = [];
-            let projs = await projectModel.find({ParentID: {$in: parentIDs}, $or: notDeleted}, '-_id');
+            let projs = await projectModel.find({ParentID: {$in: parentIDs}, $or: notDeleted}, '-_id').lean();
             for (let proj of projs) {
                 // 兼容旧的错误数据,可能ParentID和ID相同
                 if (parentIDs.includes(proj.ID)) {
@@ -1090,30 +1242,13 @@ async function getPosterityProjects(projectIDs) {
 
 //根据项目获得分享信息(分享层级不一定在项目级)(以最新为准)
 //@param {String}userId {Object}tender @return {Object} || {null}
-async function getShareInfo(userId, project) {
-    //与该用户相关的分享
-    let shareList = [];
-    while (project) {
-        for(let shareData of project.shareInfo){
-            if(shareData.userID === userId){
-                shareList.push(shareData);
-                break;
-            }
-        }
-        project = await projectModel.findOne({ID: project.ParentID}, '-_id shareInfo ID ParentID');
-    }
-    //获取最新分享
-    shareList.sort(function (a, b) {
-        let aV = Date.parse(a.shareDate),
-            bV = Date.parse(b.shareDate);
-        if (aV > bV) {
-            return -1;
-        } else if (aV < bV) {
-            return 1;
-        }
-        return 0;
-    });
-    return shareList[0] || null;
+async function getShareInfo(userID, projectID) {
+    // 接受到的分享项目
+    const upChainIDs = await getUpChainIDs(projectID);
+    const shareList = await getShareList({receiver: userID, projectID: {$in: upChainIDs}});
+    // 返回最新的分享
+    //return shareList.sort((a, b) => Date.parse(b.shareDate) - Date.parse(a.shareDate))[0];
+    return shareList.sort((a, b) => Date.parse(b.updateDate) - Date.parse(a.updateDate))[0];
 }
 
 //打开的单位工程是否是被分享的.

+ 8 - 1
modules/pm/models/project_model.js

@@ -69,13 +69,20 @@ ProjectsDAO.prototype.getUserProjects = async function (userId, compilation, cal
             }, {'userID': userId, 'compilation': compilation, 'deleteInfo.deleted': {'$in': [null, false]}}]
         }, '-_id', {lean: true});
         let projIDs= [];
+        const allIDs = [];
         for(let project of projects){
+            allIDs.push(project.ID);
             if(project.projType === projectType.project){
                 projIDs.push(project.ID);
             }
         }
+        // 设置分享信息 
+        const shareMap = await pmFacade.getShareInfoMap(allIDs);
+        projects.forEach(project => {
+            project.shareInfo = shareMap[project.ID] || [];
+        });
+        // 设置汇总字段
         let summaryInfo = await pmFacade.getSummaryInfo(projIDs);
-        //设置汇总字段
         for(let proj of projects){
             let summaryProj = summaryInfo[proj.ID];
             if(summaryProj){

+ 1 - 0
modules/pm/routes/pm_route.js

@@ -59,6 +59,7 @@ module.exports = function (app) {
     pmRouter.post('/delGC', pmController.delGC);
     //share
     pmRouter.post('/getProjectShareInfo', pmController.projectShareInfo);
+    pmRouter.post('/getRecentShareInfo', pmController.getRecentShareInfo);
     pmRouter.post('/share', pmController.share);
     pmRouter.post('/receiveProjects', pmController.receiveProjects);
     pmRouter.post('/changeFile', pmController.changeFile);

+ 31 - 0
modules/users/models/user_model.js

@@ -355,6 +355,37 @@ class UserModel extends BaseModel {
         }
         return free
     }
+    
+    /* 
+    * 添加联系人,互相添加
+    */
+    async addContact(userID, contactID) {
+        const data = [
+            { user: userID, contact: contactID },
+            { user: contactID, contact: userID }
+        ];
+        const task = [];
+        for (const { user, contact } of data) {
+            const hasThisContact = await this.model.count({_id: mongoose.Types.ObjectId(user), 'contacts.userID': contact});
+            // 没有则添加
+            if (!hasThisContact) {
+                task.push(this.model.update({_id: mongoose.Types.ObjectId(user)}, {$push: {contacts: {userID: contact}}}));
+            }
+        }
+        if (task.length) {
+            await Promise.all(task);
+        }
+    }
+
+    async getContacts(userID) {
+        const user = await this.model.findOne({_id: mongoose.Types.ObjectId(userID)}, 'contacts').lean();
+        if (!user) {
+            return [];
+        }
+        const userIDList = user.contacts.map(item => mongoose.Types.ObjectId(item.userID));
+        const users = await this.model.find({_id: {$in: userIDList}}, 'real_name mobile company');
+        return users;
+    }
 }
 
 export default UserModel;

文件差异内容过多而无法显示
+ 438 - 535
web/building_saas/css/main.css


+ 39 - 4
web/building_saas/pm/html/project-management.html

@@ -718,15 +718,50 @@
                 <div class="form-group">
                     <div class="input-group input-group-sm">
                         <input id="sharePhone" type="text" class="form-control" placeholder="输入 手机号 添加分享" autofocus="autofocus">
-                       <!-- <div class="input-group-append">
-                            <button class="btn btn-primary" type="button" id="button-addon2">添加分享</button>
-                        </div>-->
+                        <div class="input-group-append">
+                            <a class="btn btn-outline-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuBook">
+                              最新分享
+                            </a>
+                            <div id="shareSubMenu" class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuBook" style="width: 250px;">
+                              <div class="">
+                                  <ul class="nav nav-tabs nav-fill mx-1">
+                                      <li class="nav-item">
+                                          <a class="nav-link active" id="zuijin-tab" data-toggle="tab" href="#zuijin" role="tab" aria-controls="zuijin" aria-selected="true">最近分享</a>
+                                        </li>
+                                        <li class="nav-item">
+                                          <a class="nav-link" id="lianxi-tab" data-toggle="tab" href="#lianxi" role="tab" aria-controls="lianxi" aria-selected="false">联系人</a>
+                                        </li>
+                                  </ul>
+                                  <div class="tab-content" id="myTabContent">
+                                    <div class="tab-pane fade show active" id="zuijin" role="tabpanel" aria-labelledby="zuijin-tab">
+                                      <!--最近分享5个人-->
+                                      <ul class="book-list" id="recentList">
+                                      </ul>
+                                    </div>
+                                    <div class="tab-pane fade" id="lianxi" role="tabpanel" aria-labelledby="lianxi-tab">
+                                        <ul class="book-list" id="contactsList">
+                                      </ul>
+                                    </div>
+                                  </div>
+                              </div>
+                            </div>
+                      </div>
                     </div>
                 </div>
                 <table class="table table-sm" id="shareFindDiv">
                     <tbody style="display: block">
                     <tr><th style="width: 112px;">姓名</th><th style="width: 165px;">公司</th><th style="width: 136px;">手机</th><th style="width: 160px;">邮箱</th><th style="width: 90px;">允许拷贝</th><th style="width: 90px;">允许编辑</th><th style="width: 90px;">添加分享</th></tr>
-                    <tr><td id="user_name">张三</td><td id="user_company">XX公司</td><td id="user_mobile">12345678900</td><td id="user_email"></td><td><input id="allowCopy" type="checkbox"></td><td><input id="allowCooperate" type="checkbox"></td><td><a id="share-confirm" href="javascript:void(0)" class="btn btn-sm btn-primary">添加分享</a></td></tr>
+                    <tr>
+                        <td id="user_name"></td>
+                        <td id="user_company"></td>
+                        <td id="user_mobile"></td>
+                        <td id="user_email"></td>
+                        <td><input id="allowCopy" type="checkbox"></td>
+                        <td><input id="allowCooperate" type="checkbox"></td>
+                        <td>
+                            <a id="share-confirm" href="javascript:void(0)" class="btn btn-sm btn-primary">添加分享</a>
+                        </td>
+                    </tr>
                     </tbody>
                 </table>
                 <p id="share-info" class="text-danger">不存在手机号 15812777651 的用户</p>

+ 119 - 36
web/building_saas/pm/js/pm_newMain.js

@@ -52,7 +52,6 @@ let regions = [];
 function isDef(v) {
     return typeof v !== 'undefined' && v !== null;
 }
-
 let keyupTime = 0,
     delayTime = 500;
 function delayKeyup(callback) {
@@ -1698,7 +1697,90 @@ function setupRequiredWarn(compilation) {
         map[compilation.name] ? $warn.html(map[compilation.name]) : $warn.html('');
     }
 }
+
+// 显示最近分享信息
+function showRecentShareInfo() {
+    function getBookListHtml(users) {
+        return users.reduce((acc, user) => {
+            const mobile = user.mobile || '';
+            const realName = user.real_name || '';
+            const company = user.company || '';
+            // 手机最后一位
+            const lastMobileNumer = mobile.substring(mobile.length - 1);
+            // 显示名称为真实名称后两位
+            const nickName = realName.substring(realName.length - 2);
+            return acc +
+            `<li>
+                <span class="avatar bg-${lastMobileNumer}">${nickName}</span>
+                <div class="book-body">
+                    <h5 class="mt-0" title="${company}">${realName}</h5>
+                    <span class="mobile-info">${mobile}</span>
+                </div>
+            </li>`;
+        }, '');
+        
+    }
+    $.bootstrapLoading.start();
+    CommonAjax.post('/pm/api/getRecentShareInfo', {user_id: userID, count: 5}, (data) => {
+        const recentHtml = getBookListHtml(data.recentUsers);
+        // 联系人按拼英首字母降序排序
+        data.contacts.sort((a, b) => {
+            const realNameA = a.real_name || '';
+            const realNameB = b.real_name || '';
+            return realNameA.localeCompare(realNameB, 'zh-Hans-CN', {sensitivity: 'accent'})
+        });
+        const contactsHtml = getBookListHtml(data.contacts);
+        $('#recentList').html(recentHtml);
+        $('#contactsList').html(contactsHtml);
+        $('#myTabContent ul li').click(function () {
+            const mobile = $(this).find('.mobile-info')[0].textContent;
+            $('#sharePhone').val(mobile);
+            shareTender();
+            const $subMenu = $('#shareSubMenu');
+            const subMenu = $subMenu[0];
+            $(subMenu.parentElement).removeClass('show');
+            $subMenu.removeClass('show');
+        });
+        $.bootstrapLoading.end();
+    }, () => {
+        $.bootstrapLoading.end();
+    });
+}
+
 $(document).ready(function() {
+    // 最近分享、联系人列表相关
+    $('body').click(function (e) {
+        const body = $(this)[0];
+        const $subMenu = $('#shareSubMenu');
+        const subMenu = $subMenu[0];
+        const menuBook = $('#dropdownMenuBook')[0]
+        if (!$subMenu.is(':visible')) {
+            return;
+        }
+        let target = e.target;
+        while (target !== body) {
+            if ([subMenu, menuBook].includes(target)) {
+                return;
+            }
+            target = target.parentElement;
+        }
+        $(subMenu.parentElement).removeClass('show');
+        $subMenu.removeClass('show');
+    });
+    // 最近分享、联系人列表相关
+    $('#dropdownMenuBook').click(function () {
+        const $subMenu = $('#shareSubMenu');
+        const visible = $subMenu.is(':visible');
+        if (visible) {
+            $(this).parent().removeClass('show');
+            $subMenu.removeClass('show');
+        } else {
+            $(this).parent().addClass('show');
+            $subMenu.addClass('show');
+            showRecentShareInfo();
+        }
+    });
+
     // 单位工程超限提示后,弹出客服列表
     $('#hintBox_form').on('hide.bs.modal', function () {
         const text = $('#hintBox_caption').text();
@@ -3101,7 +3183,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
                 projID, {updateType: 'new', projectType: projectType.project});
             let updateEng = {updateType: 'new', updateData: {ID: engID, ParentID: projID, NextSiblingID: -1, name: engName, projType: projectType.engineering}};
             property.rootProjectID = projID;
-            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: engID, NextSiblingID: -1, shareInfo: [], name: tenderName, projType: projectType.tender, property: property}};
+            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: engID, NextSiblingID: -1, name: tenderName, projType: projectType.tender, property: property}};
             updateDatas = updateDatas.concat(updateProjs);
             updateDatas.push(updateEng);
             updateDatas.push(updateTender);
@@ -3109,6 +3191,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
                 let projData, engData, tenderData;
                 datas.forEach(function (data) {
                     if (data.updateType === 'new') {
+                        data.updateData.shareInfo = [];
                         setInitSummaryData(data.updateData);
                         if(data.updateData.projType === projectType.project){
                             projData = data.updateData;
@@ -3136,7 +3219,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
             let next = null;
             let updateEng = {updateType: 'new', updateData: {ID: engID, ParentID: tempProj.data.ID, NextSiblingID: -1, name: engName, projType: projectType.engineering}};
             property.rootProjectID = tempProj.data.ID;
-            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: engID, NextSiblingID: -1,  shareInfo: [], name: tenderName, projType: projectType.tender, property: property}};
+            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: engID, NextSiblingID: -1, name: tenderName, projType: projectType.tender, property: property}};
             if(selected && selected.data.projType === projectType.engineering && selected.parent === tempProj){
                 pre = selected;
                 next = selected.nextSibling;
@@ -3151,6 +3234,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
                 let engData, tenderData;
                 datas.forEach(function (data) {
                     if (data.updateType === 'new') {
+                        data.updateData.shareInfo = [];
                         setInitSummaryData(data.updateData);
                         if(data.updateData.projType === projectType.engineering){
                             engData = data.updateData;
@@ -3172,7 +3256,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
             let tenderID = IDs.lowID;
             let pre = tempEng.lastChild();
             property.rootProjectID = tempProj.data.ID;
-            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: tempEng.id(), NextSiblingID: -1,  shareInfo: [], name: tenderName, projType: projectType.tender, property: property}};
+            let updateTender = {updateType: 'new', updateData: {ID: tenderID, ParentID: tempEng.id(), NextSiblingID: -1, name: tenderName, projType: projectType.tender, property: property}};
             updateDatas.push(updateTender);
             if(pre){
                 updateDatas.push({updateType: 'update', updateData: {ID: pre.id(), NextSiblingID: tenderID}});
@@ -3180,6 +3264,7 @@ function AddTenderItems(selected, projName, engName, tenderName, property, callb
             UpdateProjectData(updateDatas, function (datas) {
                 datas.forEach(function (data) {
                     if(data.updateType === 'new') {
+                        data.updateData.shareInfo = [];
                         setInitSummaryData(data.updateData);
                         data.updateData.feeStandardName = data.updateData.property.feeStandardName || '';
                         projTreeObj.insert(data.updateData, tempEng, null);
@@ -3220,6 +3305,7 @@ function AddChildrenItem(selected, name, property, type, existCallback, sucCallb
             UpdateProjectData(updateData, function(datas){
                 datas.forEach(function (data) {
                     if (data.updateType === 'new') {
+                        data.updateData.shareInfo = [];
                         setInitSummaryData(data.updateData);
                         projTreeObj.insert(data.updateData, parent, null);
                     }
@@ -3258,6 +3344,7 @@ function AddSiblingsItem(selected, name, property, type, existCallback, sucCallb
             UpdateProjectData(updateData, function(datas){
                 datas.forEach(function (data) {
                     if (data.updateType === 'new') {
+                        data.updateData.shareInfo = [];
                         setInitSummaryData(data.updateData);
                         projTreeObj.insert(data.updateData, parent, next);
                     }
@@ -4606,8 +4693,8 @@ function shareTender(){
             coopInput.prop('checked', false);
             userInfo.show();
             //判断项目是否已经分享
-            CommonAjax.post('/pm/api/getProjectShareInfo', {user_id: userID, projectID: shareSeleted.data.ID}, function (rstData) {
-                for(let shareData of rstData.shareInfo){
+            CommonAjax.post('/pm/api/getProjectShareInfo', {user_id: userID, projectID: shareSeleted.data.ID}, function (shareInfo) {
+                for(let shareData of shareInfo){
                     if(shareData.userID === userData._id){
                         setDangerInfo(hintInfo, '已与该用户分享。', true);
                         return;
@@ -4649,7 +4736,7 @@ $('#share-confirm').click(function(){
                         <td style="width: 90px;"><input value="allowCopy" ${allowCopy ? 'checked' : ''} type="checkbox"></td>
                         <td style="width: 90px;"><input value="allowCooperate" ${allowCoop ? 'checked' : ''} type="checkbox"></td>
                         <td style="width: 90px;"><input value="cancelShare" type="checkbox"></td>
-                     </tr>`);
+                    </tr>`);
         let tbodyTotalHeight = $('#shareToInfo').height() + perHeight > 200 ? 200 : $('#shareToInfo').height() + perHeight;
         $('#shareToInfo').height(tbodyTotalHeight);
         $('#shareToInfo').append($tr);
@@ -4807,28 +4894,28 @@ function setShareToModal(selected){
         let infoArr = [];
         //居中style="width: 90px;text-align: center"
         let theadHtml = `<tr>
-                                          <th style="width: 112px;">姓名</th>
-                                          <th style="width: 165px;">公司</th>
-                                          <th style="width: 136px;">手机</th>
-                                          <th style="width: 136px;">邮箱</th>
-                                          <th style="width: 90px;">允许拷贝</th>
-                                          <th style="width: 90px;">允许编辑</th>
-                                          <th style="width: 90px;">取消分享</th>
-                               </tr>`;
+                            <th style="width: 112px;">姓名</th>
+                            <th style="width: 165px;">公司</th>
+                            <th style="width: 136px;">手机</th>
+                            <th style="width: 136px;">邮箱</th>
+                            <th style="width: 90px;">允许拷贝</th>
+                            <th style="width: 90px;">允许编辑</th>
+                            <th style="width: 90px;">取消分享</th>
+                        </tr>`;
         infoArr.push(theadHtml);
         for(let user of selected.data.shareInfo){
             if (!user.exist) {
                 continue;
             }
             let infoHtml = `<tr>
-                                          <td style="width: 112px;">${user.name}</td>
-                                          <td style="width: 165px;">${user.company}</td>
-                                          <td style="width: 136px;">${user.mobile}</td>
-                                          <td style="width: 160px;">${user.email}</td>
-                                          <td style="width: 90px;"><input value="allowCopy" ${user.allowCopy ? 'checked' : ''} type="checkbox"></td>
-                                          <td style="width: 90px;"><input value="allowCooperate" ${user.allowCooperate ? 'checked' : ''} type="checkbox"></td>
-                                          <td style="width: 90px;"><input value="cancelShare" type="checkbox"></td>
-                               </tr>`;
+                                <td style="width: 112px;">${user.name}</td>
+                                <td style="width: 165px;">${user.company}</td>
+                                <td style="width: 136px;">${user.mobile}</td>
+                                <td style="width: 160px;">${user.email}</td>
+                                <td style="width: 90px;"><input value="allowCopy" ${user.allowCopy ? 'checked' : ''} type="checkbox"></td>
+                                <td style="width: 90px;"><input value="allowCooperate" ${user.allowCooperate ? 'checked' : ''} type="checkbox"></td>
+                                <td style="width: 90px;"><input value="cancelShare" type="checkbox"></td>
+                            </tr>`;
             infoArr.push(infoHtml);
         }
         let tbodyTotalHeight = infoArr.length * perHeight + 5 > 200 ? 200 : infoArr.length * perHeight + 5;
@@ -4871,6 +4958,7 @@ function updateShareInfo(selected){
     }
     let newShareInfo = [],
         orgShareInfo = [];
+    const postData = [];
     //数据不同才提交
     for (let shareData of selected.data.shareInfo) {
         orgShareInfo.push({userID: shareData.userID, allowCopy: shareData.allowCopy, allowCooperate: shareData.allowCooperate})
@@ -4882,15 +4970,19 @@ function updateShareInfo(selected){
         let cancelShare = $(userTr).find('input:eq(2)').prop('checked');
         selected.data.shareInfo[i].allowCopy = allowCopy;
         selected.data.shareInfo[i].allowCooperate = allowCoop;
-        if(!cancelShare){
-            //newShareInfo.push(selected.data.shareInfo[i]);
-            newShareInfo.push({userID: selected.data.shareInfo[i].userID, allowCopy: allowCopy, allowCooperate: allowCoop});
+        let shareItem;
+        if(cancelShare){
+            shareItem = {userID: selected.data.shareInfo[i].userID, isCancel: true};
+        } else {
+            shareItem = {userID: selected.data.shareInfo[i].userID, allowCopy: allowCopy, allowCooperate: allowCoop};
+            newShareInfo.push(shareItem);
         }
+        postData.push(shareItem);
     }
     if (_.isEqual(newShareInfo, orgShareInfo)) {
         return;
     }
-    CommonAjax.post('/pm/api/share', {user_id: userID, type: 'update', projectID: selected.data.ID, shareData: newShareInfo}, function (shareData) {
+    CommonAjax.post('/pm/api/share', {user_id: userID, type: 'update', projectID: selected.data.ID, shareData: postData}, function (shareData) {
         selected.data.shareInfo = shareData;
         let sheet = projTreeObj.workBook.getSheet(0);
         projTreeObj.renderSheetFuc(sheet, function () {
@@ -4898,15 +4990,6 @@ function updateShareInfo(selected){
             sheet.repaint();
         });
     });
-    /*CommonAjax.post('/pm/api/updateProjects', {user_id: userID, updateData: [{updateType: 'update', updateData: {ID: selected.data.ID, shareInfo: newShareInfo}}]}, function () {
-        //todo 更新
-        selected.data.shareInfo = newShareInfo;
-        let sheet = projTreeObj.workBook.getSheet(0);
-        projTreeObj.renderSheetFuc(sheet, function () {
-            sheet.invalidateLayout();
-            sheet.repaint();
-        });
-    });*/
 }
 
 //刷新建设项目汇总金额信息

+ 3 - 8
web/building_saas/pm/js/pm_share.js

@@ -178,14 +178,8 @@ const pmShare = (function () {
         }
         //获取最新分享
         shareList.sort(function (a, b) {
-            let aV = Date.parse(a.shareDate),
-                bV = Date.parse(b.shareDate);
-            if (aV > bV) {
-                return -1;
-            } else if (aV < bV) {
-                return 1;
-            }
-            return 0;
+            //return Date.parse(b.shareDate) - Date.parse(a.shareDate)
+            return Date.parse(b.updateDate) - Date.parse(a.updateDate);
         });
         return shareList[0] || null;
 
@@ -771,6 +765,7 @@ const pmShare = (function () {
         $.bootstrapLoading.start();
         //获取分享数据
         CommonAjax.post('/pm/api/receiveProjects', {user_id: userID}, function (rstData) {
+            debugger;
             // 排序 --分享的文件按照时间先后顺序排序,分享文件下的子文件,按照原本树结构显示,不需要排序
             sortByDate(rstData.grouped);
             sortByDate(rstData.ungrouped);