Browse Source

新的分享给

vian 5 years ago
parent
commit
203bc82f21

+ 6 - 0
config/gulpConfig.js

@@ -46,10 +46,13 @@ module.exports = {
         'public/web/tree_sheet/tree_sheet_helper.js',
         'public/web/sheet/sheet_data_helper.js',
         'public/web/sheet/sheet_common.js',
+        'lib/pinyinjs/pinyin_dict_firstletter.js',
+        'lib/pinyinjs/pinyinUtil.js',
         'public/web/common_ajax.js',
         'lib/JSExpressionEval_src/Date.js',
         'web/building_saas/glj/js/socket.io.slim.js',
         'public/web/socket/connection.js',
+        'web/common/components/share/index.js',
         'web/building_saas/pm/js/**/*.js',
         'lib/ztree/*.js',
         'lib/jquery-contextmenu/jquery.contextMenu.min.js'
@@ -68,6 +71,8 @@ module.exports = {
         'lib/jquery-contextmenu/!*.js',*/
         //'lib/lodash/lodash.js',
         // 'test/tmp_data/test_ration_calc/ration_calc_base.js',
+        'lib/pinyinjs/pinyin_dict_firstletter.js',
+        'lib/pinyinjs/pinyinUtil.js',
         'web/building_saas/main/js/models/main_consts.js',
         'public/web/common_util.js',
         'web/building_saas/glj/js/project_glj.js',
@@ -92,6 +97,7 @@ module.exports = {
        // 'lib/spreadjs/views/gc.spread.views.dataview.10.0.0.min.js',
        // "lib/spreadjs/views/common/gc.spread.common.10.0.0.min.js",
       //  'lib/spreadjs/views/plugins/gc.spread.views.gridlayout.10.0.0.min.js',
+        'web/common/components/share/index.js',
         'web/building_saas/main/js/models/project.js',
         'web/building_saas/main/js/models/bills.js',
         'web/building_saas/main/js/models/ration.js',

+ 372 - 0
lib/pinyinjs/pinyinUtil.js

@@ -0,0 +1,372 @@
+
+/**
+ * 汉字与拼音互转工具,根据导入的字典文件的不同支持不同
+ * 对于多音字目前只是将所有可能的组合输出,准确识别多音字需要完善的词库,而词库文件往往比字库还要大,所以不太适合web环境。
+ * @start 2016-09-26
+ * @last 2016-09-29
+ */
+;(function(global, factory) {
+	if (typeof module === "object" && typeof module.exports === "object") {
+		module.exports = factory(global);
+	} else {
+		factory(global);
+	}
+})(typeof window !== "undefined" ? window : this, function(window) {
+
+	var toneMap = 
+	{
+		"ā": "a1",
+		"á": "a2",
+		"ǎ": "a3",
+		"à": "a4",
+		"ō": "o1",
+		"ó": "o2",
+		"ǒ": "o3",
+		"ò": "o4",
+		"ē": "e1",
+		"é": "e2",
+		"ě": "e3",
+		"è": "e4",
+		"ī": "i1",
+		"í": "i2",
+		"ǐ": "i3",
+		"ì": "i4",
+		"ū": "u1",
+		"ú": "u2",
+		"ǔ": "u3",
+		"ù": "u4",
+		"ü": "v0",
+		"ǖ": "v1",
+		"ǘ": "v2",
+		"ǚ": "v3",
+		"ǜ": "v4",
+		"ń": "n2",
+		"ň": "n3",
+		"": "m2"
+	};
+
+	var dict = {}; // 存储所有字典数据
+	var pinyinUtil =
+	{
+		/**
+		 * 解析各种字典文件,所需的字典文件必须在本JS之前导入
+		 */
+		parseDict: function()
+		{
+			// 如果导入了 pinyin_dict_firstletter.js
+			if(window.pinyin_dict_firstletter)
+			{
+				dict.firstletter = pinyin_dict_firstletter;
+			}
+			// 如果导入了 pinyin_dict_notone.js
+			if(window.pinyin_dict_notone)
+			{
+				dict.notone = {};
+				dict.py2hz = pinyin_dict_notone; // 拼音转汉字
+				for(var i in pinyin_dict_notone)
+				{
+					var temp = pinyin_dict_notone[i];
+					for(var j=0, len=temp.length; j<len; j++)
+					{
+						if(!dict.notone[temp[j]]) dict.notone[temp[j]] = i; // 不考虑多音字
+					}
+				}
+			}
+			// 如果导入了 pinyin_dict_withtone.js
+			if(window.pinyin_dict_withtone)
+			{
+				dict.withtone = {}; // 汉字与拼音映射,多音字用空格分开,类似这种结构:{'大': 'da tai'}
+				var temp = pinyin_dict_withtone.split(',');
+				for(var i=0, len = temp.length; i<len; i++)
+				{
+					// 这段代码耗时28毫秒左右,对性能影响不大,所以一次性处理完毕
+					dict.withtone[String.fromCharCode(i + 19968)] = temp[i]; // 这里先不进行split(' '),因为一次性循环2万次split比较消耗性能
+				}
+
+				// 拼音 -> 汉字
+				if(window.pinyin_dict_notone)
+				{
+					// 对于拼音转汉字,我们优先使用pinyin_dict_notone字典文件
+					// 因为这个字典文件不包含生僻字,且已按照汉字使用频率排序
+					dict.py2hz = pinyin_dict_notone; // 拼音转汉字
+				}
+				else
+				{
+					// 将字典文件解析成拼音->汉字的结构
+					// 与先分割后逐个去掉声调相比,先一次性全部去掉声调然后再分割速度至少快了3倍,前者大约需要120毫秒,后者大约只需要30毫秒(Chrome下)
+					var notone = pinyinUtil.removeTone(pinyin_dict_withtone).split(',');
+					var py2hz = {}, py, hz;
+					for(var i=0, len = notone.length; i<len; i++)
+					{
+						hz = String.fromCharCode(i + 19968); // 汉字
+						py = notone[i].split(' '); // 去掉了声调的拼音数组
+						for(var j=0; j<py.length; j++)
+						{
+							py2hz[py[j]] = (py2hz[py[j]] || '') + hz;
+						}
+					}
+					dict.py2hz = py2hz;
+				}
+			}
+		},
+		/**
+		 * 根据汉字获取拼音,如果不是汉字直接返回原字符
+		 * @param chinese 要转换的汉字
+		 * @param splitter 分隔字符,默认用空格分隔
+		 * @param withtone 返回结果是否包含声调,默认是
+		 * @param polyphone 是否支持多音字,默认否
+		 */
+		getPinyin: function(chinese, splitter, withtone, polyphone)
+		{
+			if(!chinese || /^ +$/g.test(chinese)) return '';
+			splitter = splitter == undefined ? ' ' : splitter;
+			withtone = withtone == undefined ? true : withtone;
+			polyphone = polyphone == undefined ? false : polyphone;
+			var result = [];
+			if(dict.withtone) // 优先使用带声调的字典文件
+			{
+				var noChinese = '';
+				for (var i=0, len = chinese.length; i < len; i++)
+				{
+					var pinyin = dict.withtone[chinese[i]];
+					if(pinyin)
+					{
+						// 如果不需要多音字,默认返回第一个拼音,后面的直接忽略
+						// 所以这对数据字典有一定要求,常见字的拼音必须放在最前面
+						if(!polyphone) pinyin = pinyin.replace(/ .*$/g, '');
+						if(!withtone) pinyin = this.removeTone(pinyin); // 如果不需要声调
+						//空格,把noChinese作为一个词插入
+						noChinese && ( result.push( noChinese), noChinese = '' );
+						result.push( pinyin ); 
+					}
+					else if ( !chinese[i] || /^ +$/g.test(chinese[i]) ){
+						//空格,把noChinese作为一个词插入
+						noChinese && ( result.push( noChinese), noChinese = '' );
+					}
+					else{
+						noChinese += chinese[i];
+					}
+				}
+				if ( noChinese ){
+					result.push( noChinese);
+					noChinese = '';
+				}
+			}
+			else if(dict.notone) // 使用没有声调的字典文件
+			{
+				if(withtone) console.warn('pinyin_dict_notone 字典文件不支持声调!');
+				if(polyphone) console.warn('pinyin_dict_notone 字典文件不支持多音字!');
+				var noChinese = '';
+				for (var i=0, len = chinese.length; i < len; i++)
+				{
+					var temp = chinese.charAt(i),
+						pinyin = dict.notone[temp];
+					if ( pinyin ){ //插入拼音
+						//空格,把noChinese作为一个词插入
+						noChinese && ( result.push( noChinese), noChinese = '' );
+						result.push( pinyin );
+					}
+					else if ( !temp || /^ +$/g.test(temp) ){
+						//空格,插入之前的非中文字符
+						noChinese && ( result.push( noChinese), noChinese = '' );
+					}
+					else {
+						//非空格,关联到noChinese中
+						noChinese += temp;
+					}
+				}
+
+				if ( noChinese ){
+					result.push( noChinese );
+					noChinese = '';
+				}
+			}
+			else
+			{
+				throw '抱歉,未找到合适的拼音字典文件!';
+			}
+			if(!polyphone) return result.join(splitter);
+			else
+			{
+				if(window.pinyin_dict_polyphone) return parsePolyphone(chinese, result, splitter, withtone);
+				else return handlePolyphone(result, ' ', splitter);
+			}
+		},
+		/**
+		 * 获取汉字的拼音首字母
+		 * @param str 汉字字符串,如果遇到非汉字则原样返回
+		 * @param polyphone 是否支持多音字,默认false,如果为true,会返回所有可能的组合数组
+		 */
+		getFirstLetter: function(str, polyphone)
+		{
+			polyphone = polyphone == undefined ? false : polyphone;
+			if(!str || /^ +$/g.test(str)) return '';
+			if(dict.firstletter) // 使用首字母字典文件
+			{
+				var result = [];
+				for(var i=0; i<str.length; i++)
+				{
+					var unicode = str.charCodeAt(i);
+					var ch = str.charAt(i);
+					if(unicode >= 19968 && unicode <= 40869)
+					{
+						ch = dict.firstletter.all.charAt(unicode-19968);
+						if(polyphone) ch = dict.firstletter.polyphone[unicode] || ch;
+					}
+					result.push(ch);
+				}
+				if(!polyphone) return result.join(''); // 如果不用管多音字,直接将数组拼接成字符串
+				else return handlePolyphone(result, '', ''); // 处理多音字,此时的result类似于:['D', 'ZC', 'F']
+			}
+			else
+			{
+				var py = this.getPinyin(str, ' ', false, polyphone);
+				py = py instanceof Array ? py : [py];
+				var result = [];
+				for(var i=0; i<py.length; i++)
+				{
+					result.push(py[i].replace(/(^| )(\w)\w*/g, function(m,$1,$2){return $2.toUpperCase();}));
+				}
+				if(!polyphone) return result[0];
+				else return simpleUnique(result);
+			}
+		},
+		/**
+		 * 拼音转汉字,只支持单个汉字,返回所有匹配的汉字组合
+		 * @param pinyin 单个汉字的拼音,可以包含声调
+		 */
+		getHanzi: function(pinyin)
+		{
+			if(!dict.py2hz)
+			{
+				throw '抱歉,未找到合适的拼音字典文件!';
+			}
+			return dict.py2hz[this.removeTone(pinyin)] || '';
+		},
+		/**
+		 * 获取某个汉字的同音字,本方法暂时有问题,待完善
+		 * @param hz 单个汉字
+		 * @param sameTone 是否获取同音同声调的汉字,必须传进来的拼音带声调才支持,默认false
+		 */
+		getSameVoiceWord: function(hz, sameTone)
+		{
+			sameTone = sameTone || false
+			return this.getHanzi(this.getPinyin(hz, ' ', false))
+		},
+		/**
+		 * 去除拼音中的声调,比如将 xiǎo míng tóng xué 转换成 xiao ming tong xue
+		 * @param pinyin 需要转换的拼音
+		 */
+		removeTone: function(pinyin)
+		{
+			return pinyin.replace(/[āáǎàōóǒòēéěèīíǐìūúǔùüǖǘǚǜńň]/g, function(m){ return toneMap[m][0]; });
+		},
+		/**
+		 * 将数组拼音转换成真正的带标点的拼音
+		 * @param pinyinWithoutTone 类似 xu2e这样的带数字的拼音
+		 */
+		getTone: function(pinyinWithoutTone)
+		{
+			var newToneMap = {};
+			for(var i in toneMap) newToneMap[toneMap[i]] = i;
+			return (pinyinWithoutTone || '').replace(/[a-z]\d/g, function(m) {
+				return newToneMap[m] || m;
+			});
+		}
+	};
+
+
+	/**
+	 * 处理多音字,将类似['D', 'ZC', 'F']转换成['DZF', 'DCF']
+	 * 或者将 ['chang zhang', 'cheng'] 转换成 ['chang cheng', 'zhang cheng']
+	 */
+	function handlePolyphone(array, splitter, joinChar)
+	{
+		splitter = splitter || '';
+		var result = [''], temp = [];
+		for(var i=0; i<array.length; i++)
+		{
+			temp = [];
+			var t = array[i].split(splitter);
+			for(var j=0; j<t.length; j++)
+			{
+				for(var k=0; k<result.length; k++)
+					temp.push(result[k] + (result[k]?joinChar:'') + t[j]);
+			}
+			result = temp;
+		}
+		return simpleUnique(result);
+	}
+
+	/**
+	 * 根据词库找出多音字正确的读音
+	 * 这里只是非常简单的实现,效率和效果都有一些问题
+	 * 推荐使用第三方分词工具先对句子进行分词,然后再匹配多音字
+	 * @param chinese 需要转换的汉字
+	 * @param result 初步匹配出来的包含多个发音的拼音结果
+	 * @param splitter 返回结果拼接字符
+	 */
+	function parsePolyphone(chinese, result, splitter, withtone)
+	{
+		var poly = window.pinyin_dict_polyphone;
+		var max = 7; // 最多只考虑7个汉字的多音字词,虽然词库里面有10个字的,但是数量非常少,为了整体效率暂时忽略之
+		var temp = poly[chinese];
+		if(temp) // 如果直接找到了结果
+		{
+			temp = temp.split(' ');
+			for(var i=0; i<temp.length; i++)
+			{
+				result[i] = temp[i] || result[i];
+				if(!withtone) result[i] = pinyinUtil.removeTone(result[i]);
+			}
+			return result.join(splitter);
+		}
+		for(var i=0; i<chinese.length; i++)
+		{
+			temp = '';
+			for(var j=0; j<max && (i+j)<chinese.length; j++)
+			{
+				if(!/^[\u2E80-\u9FFF]+$/.test(chinese[i+j])) break; // 如果碰到非汉字直接停止本次查找
+				temp += chinese[i+j];
+				var res = poly[temp];
+				if(res) // 如果找到了多音字词语
+				{
+					res = res.split(' ');
+					for(var k=0; k<=j; k++)
+					{
+						if(res[k]) result[i+k] = withtone ? res[k] : pinyinUtil.removeTone(res[k]);
+					}
+					break;
+				}
+			}
+		}
+		// 最后这一步是为了防止出现词库里面也没有包含的多音字词语
+		for(var i=0; i<result.length; i++)
+		{
+			result[i] = result[i].replace(/ .*$/g, '');
+		}
+		return result.join(splitter);
+	}
+
+	// 简单数组去重
+	function simpleUnique(array)
+	{
+		var result = [];
+		var hash = {};
+		for(var i=0; i<array.length; i++)
+		{
+			var key = (typeof array[i]) + array[i];
+			if(!hash[key])
+			{
+				result.push(array[i]);
+				hash[key] = true;
+			}
+		}
+		return result;
+	}
+
+	pinyinUtil.parseDict();
+	pinyinUtil.dict = dict;
+	window.pinyinUtil = pinyinUtil;
+
+});

File diff suppressed because it is too large
+ 8 - 0
lib/pinyinjs/pinyin_dict_firstletter.js


+ 50 - 19
modules/pm/controllers/pm_controller.js

@@ -33,7 +33,7 @@ let sectionTreeDao = new SectionTreeDao();
 let consts = require('../../main/models/project_consts');
 import multiparty from 'multiparty';
 let rp = require('request-promise');
-
+const commonUtil = require('../../../public/common_util');
 //统一回调函数
 let callback = function(req, res, err, message, data){
     res.json({error: err, message: message, data: data});
@@ -593,15 +593,34 @@ module.exports = {
             callback(req, res, 1, err.message, null);
         }
     },
-    share: async function(req, res){
-        try{
+    getInitialShareData: async function (req, res) {
+        try {
+            const { count, projectID } = 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);
+            // 分享过的人
+            const sharedUsers = await pm_facade.getProjectShareList(projectID);
+            callback(req, res, 0, 'success', { recentUsers, contacts, sharedUsers });
+        } catch (err) {
+            console.log(err);
+            callback(req, res, 1, err.message, null);
+        }
+    },
+    share: async function (req, res) {
+        try {
             const data = JSON.parse(req.body.data);
-            const { type, shareData, projectID } = data;
+            const { type, shareData, projectID, count } = 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 = [];
+            // 是否只是单纯的更新分享选项,如果不是,需要重新获取最近分享和联系人
+            let isSimpleUpdate = true;
             if (type === 'create') {
+                isSimpleUpdate = false;
                 // 生成分享记录
                 const docs = shareData.map(item => ({
                     ID: uuidV1(),
@@ -617,41 +636,53 @@ module.exports = {
                 // 分享即互相添加为联系人
                 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}}));
+                    isSimpleUpdate = false;
+                    task.push(pm_facade.deleteShareList({ projectID, receiver: { $in: cancelReceivers } }));
                 }
                 // 更新分享选项
                 const updateData = shareData
                     .filter(item => !item.isCancel)
-                    .map(item => (
-                        {
+                    .map(item => {
+                        const update = { updateDate: shareDate };
+                        if (commonUtil.isDef(item.allowCopy)) {
+                            update.allowCopy = item.allowCopy;
+                        }
+                        if (commonUtil.isDef(item.allowCooperate)) {
+                            update.allowCooperate = item.allowCooperate;
+                        }
+                        return {
                             query: {
                                 projectID,
                                 receiver: item.userID
                             },
-                            update: {
-                                allowCopy: item.allowCopy,
-                                allowCooperate: item.allowCooperate,
-                                updateDate: shareDate
-                            }
-                        }
-                    ));
+                            update
+                        };
+                    });
                 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}}));
+                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);
+            const rstTask = [
+                pm_facade.getRecentShareList(userID, count),
+                userModelObj.getContacts(userID)
+            ]
+            let rst = null;
+            if (!isSimpleUpdate) {
+                const [recentUsers, contacts] = await Promise.all(rstTask);
+                rst = { recentUsers, contacts };
+            }
+            callback(req, res, 0, 'success', rst);
         }
-        catch (err){
+        catch (err) {
             callback(req, res, 1, err, null);
         }
     },

+ 22 - 0
modules/pm/facade/pm_facade.js

@@ -16,6 +16,7 @@ module.exports={
     deleteShareList,
     getShareInfoMap,
     getRecentShareList,
+    getProjectShareList,
     moveProject:moveProject,
     accessToCopyProject,
     copyProject:copyProject,
@@ -242,6 +243,27 @@ async function getRecentShareList(userID, count) {
     return users;
 }
 
+// 获取某项目的分享记录
+async function getProjectShareList(projectID) {
+    const sharedList = await shareListModel.find({ projectID }).lean();
+    const userIDs = [];
+    const userMap = {};
+    sharedList.forEach(item => {
+        userIDs.push(item.receiver);
+        userMap[item.receiver] = item;
+    });
+    const userObjectIDs = userIDs.map(userID => mongoose.Types.ObjectId(userID));
+    const users = await userModel.find({_id: {$in: userObjectIDs}}, 'real_name mobile company').lean();
+    users.forEach(user => {
+        const matched = userMap[user._id];
+        if (matched) {
+            user.allowCopy = matched.allowCopy;
+            user.allowCooperate = matched.allowCooperate;
+        }
+    });
+    return users;
+}
+
 //拷贝例题项目
 //@param {String}userID {Array}projIDs拷贝的例题项目ID(建设项目、文件夹)@return {Boolean}
 async function copyExample(userID, compilation, projIDs){

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

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

+ 4 - 1
web/building_saas/main/html/main.html

@@ -1894,7 +1894,7 @@
             </div>
         </div>
     </div>
-
+    <%include ../../../common/components/share/index.html %>
     <img src="/web/dest/css/img/folder_open.png" id="folder_open_pic" style="display: none">
     <img src="/web/dest/css/img/folder_close.png" id="folder_close_pic" style="display: none">
     <img src="/web/dest/css/img/project.png" id="proj_pic" style="display: none">
@@ -1922,6 +1922,8 @@
     <script type="text/javascript" src="/lib/jspdf/jspdf.min.js"></script>
     <!-- inject:js -->
     <!--<script type="text/javascript" src="/test/tmp_data/test_ration_calc/ration_calc_base.js"></script>-->
+    <script src="/lib/pinyinjs/pinyin_dict_firstletter.js"></script>
+    <script src="/lib/pinyinjs/pinyinUtil.js"></script>
     <script type="text/javascript" src="/web/building_saas/main/js/models/main_consts.js"></script>
     <script type="text/javascript" src="/public/web/common_util.js"></script>
     <script type="text/javascript" src="/web/building_saas/glj/js/project_glj.js"></script>
@@ -1954,6 +1956,7 @@
     <!--<script src="/lib/popper/popper.min.js"></script>
     <script src="/lib/bootstrap/bootstrap.min.js"></script>-->
     <script src="/web/building_saas/js/global.js"></script>
+    <script src="/web/common/components/share/index.js"></script>
     <!--报表 zTree -->
 
     <!-- SpreadJs -->

+ 6 - 1
web/building_saas/main/js/views/project_info.js

@@ -20,7 +20,9 @@ var projectInfoObj = {
                 : ''}
                 ${projectCooperate ? 
                 '<span data-toggle="tooltip" data-placement="bottom" data-original-title="当前的工程状态为“协作”,可直接编辑分享人的原始数据。">(协作)</span>'
-                : ''}`;
+                : ''}
+                ${projectReadOnly || projectCooperate ? '' : '<span><a id="init-share" class="pl-2" href="javascript:;"><i class="fa fa-share-alt" aria-hidden="true"></i> 分享</a></span>'}
+                `;
             fullPath.push(newHtml);
 
         }
@@ -44,6 +46,9 @@ var projectInfoObj = {
             basicInfoView.orgDatas = data.property.basicInformation ? basicInfoView.toViewDatas(data.property.basicInformation) : [];
             projFeatureView.orgDatas = data.property.projectFeature ? projFeatureView.toViewDatas(data.property.projectFeature) : [];
             $('#fullpath').html(this.getFullPathHtml(data));
+            // 分享给
+            $('#init-share').click(() => SHARE_TO.initModal(projectObj.project.ID()));
+            SHARE_TO.handleEventListener();
             $("[data-toggle='tooltip']").tooltip();
             if (data.property.valuationType == 'bill'){
                 $("#tab_tender_price").css('display', 'none');

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

@@ -574,7 +574,7 @@
         </div>
     </div>
 </div>
-
+<%include ../../../common/components/share/index.html %>
 <!--弹出删除 单价/费率 文件-->
 <div class="modal fade" id="del-wj" data-backdrop="static">
     <div class="modal-dialog" role="document">
@@ -596,86 +596,6 @@
         </div>
     </div>
 </div>
-<!--弹出分享-->
-<div class="modal fade" id="share" data-backdrop="static">
-    <div class="modal-dialog modal-lg" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title">分享给...</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                    <span aria-hidden="true">&times;</span>
-                </button>
-            </div>
-            <div class="modal-body">
-                <!--添加分享-->
-                <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">
-                            <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"></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>
-                <!--已分享数据-->
-                <legend>已分享</legend>
-                <table class="table table-sm">
-                    <tbody id="shareToInfo">
-                    <tr><th>姓名</th><th>公司</th><th>手机</th><th>邮箱</th><th width="90">允许拷贝</th><th width="90">取消分享</th></tr>
-                    <tr><td>张三</td><td>XX公司</td><td>12345678900</td><td></td><td><input type="checkbox"></td><td><input type="checkbox"></td></tr>
-                    <tr><td>王五</td><td>XX公司</td><td>12345678900</td><td></td><td><input type="checkbox"></td><td><input type="checkbox"></td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="modal-footer">
-                <!--分享人可以取消-->
-                <button id="shareToConfirm" type="button" class="btn btn-primary" data-dismiss="modal">确定</button>
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
-            </div>
-        </div>
-    </div>
-</div>
 <!--弹出 新用户第一次进度条-->
 <div class="modal fade" id="progress" data-backdrop="static">
     <div class="modal-dialog" role="document">
@@ -753,10 +673,13 @@
 <script src="/public/web/tree_sheet/tree_sheet_helper.js"></script>
 <script src="/public/web/sheet/sheet_data_helper.js"></script>
 <script src="/public/web/sheet/sheet_common.js"></script>
+<script src="/lib/pinyinjs/pinyin_dict_firstletter.js"></script>
+<script src="/lib/pinyinjs/pinyinUtil.js"></script>
 <script type="text/javascript" src="/public/web/common_ajax.js"></script>
 <script src="/lib/JSExpressionEval_src/Date.js"></script>
 <script src="/web/building_saas/glj/js/socket.io.slim.js"></script>
 <script src="/public/web/socket/connection.js"></script>
+<script src="/web/common/components/share/index.js" ></script>
 <script src="/web/building_saas/pm/js/pm_ajax.js"></script>
 <script src="/web/building_saas/pm/js/pm_newMain.js"></script>
 <script src="/web/building_saas/pm/js/pm_gc.js"></script>

+ 6 - 360
web/building_saas/pm/js/pm_newMain.js

@@ -39,17 +39,6 @@ function isDef(v) {
     return typeof v !== 'undefined' && v !== null;
 }
 
-let keyupTime = 0,
-    delayTime = 500;
-function delayKeyup(callback) {
-    let nowTime = Date.now();
-    keyupTime = nowTime;
-    setTimeout(function () {
-        if (nowTime - keyupTime == 0) {
-            callback();
-        }
-    }, delayTime);
-}
 //操作状态
 const STATE = {
     addingTender: false,
@@ -321,15 +310,10 @@ const projTreeObj = {
                 return !selectedItem;
             },
             callback: function (key, opt) {
-                let selected = projTreeObj.tree.selected;
-                $('#sharePhone').val('');
-                $('#share-info').hide();
-                $('#shareFindDiv').hide();
-                setShareToModal(selected);
-                $('#share').modal('show');
-                setTimeout(function () {
-                    $('#sharePhone').focus();
-                }, 200);
+                const selected = projTreeObj.tree.selected;
+                if (selected && selected.data.ID) {
+                    SHARE_TO.initModal(selected.data.ID);
+                }
             }
         },
         manageFiles: {
@@ -634,7 +618,6 @@ const projTreeObj = {
             return;
         }
         me.tree.selected = node;
-        shareSeleted = node;
         me.preSelection = newSel;
     },
     onSelectionChanging: function (sender, args) {
@@ -1633,88 +1616,9 @@ const projTreeObj = {
 
 };
 
-// 显示最近分享信息
-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();
-        }
-    });
+    // 分享给的一些事件监听
+    SHARE_TO.handleEventListener();
     
     // 单位工程超限提示后,弹出客服列表
     $('#hintBox_form').on('hide.bs.modal', function () {
@@ -3763,127 +3667,6 @@ function refreshTenderFile(tenders, type, newName){
        }
    });
 }
-//添加分享 pm share共用
-const shareType = {create: 'create', cancel: 'cancel'};
-let shareSeleted = null;
-let shareUserID = null;
-let canShare = false;
-$('#sharePhone').bind('keypress', function (e) {
-    if(e.keyCode === 13){
-        $(this).blur();
-    }
-});
-function shareTender(){
-    canShare = false;
-    let phone = $('#sharePhone').val();
-    const hintInfo = $('#share-info');
-    const userInfo = $('#shareFindDiv');
-    const addUser = $('#addShareUser');
-    const copyInput = $('#allowCopy');
-    const coopInput = $('#allowCooperate');
-    const shareConfirm = $('#share-confirm');
-    shareConfirm.addClass('disabled');
-    //可分享才可添加用户
-    addUser.addClass('disabled');
-    if (hintInfo.is(':visible')) {
-        hintInfo.hide();
-    }
-    if(!phone || phone.trim() === ''){
-        setDangerInfo(hintInfo, '请输入手机号码。', true);
-        if(userInfo.is(':visible')){
-            userInfo.hide();
-        }
-        return;
-    }
-    //根据手机号获取用户
-    CommonAjax.post('/user/getUserByMobile', {mobile: phone}, function (userData) {
-        if(!userData){
-            setDangerInfo(hintInfo, '账号不存在。', true);
-            if(userInfo.is(':visible')){
-                userInfo.hide();
-            }
-            return;
-        }
-        else{
-            if(userData._id === userID){
-                setDangerInfo(hintInfo, '不可分享给自己。', true);
-                return;
-            }
-            shareUserID = userData._id;
-            setDangerInfo(hintInfo, '', false);
-            $('#user_name').text(userData.real_name ? userData.real_name : '');
-            $('#user_company').text(userData.company ? userData.company : '');
-            $('#user_mobile').text(userData.mobile ? userData.mobile : '');
-            $('#user_email').text(userData.email ? userData.email : '');
-            //默认可拷贝
-            copyInput.prop('checked', true);
-            //默认不可协作
-            coopInput.prop('checked', false);
-            userInfo.show();
-            //判断项目是否已经分享
-            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;
-                    }
-                }
-                addUser.removeClass('disabled');
-                shareConfirm.removeClass('disabled');
-                canShare = true;
-            });
-        }
-    });
-}
-$('#sharePhone').on('keyup',function () {
-    delayKeyup(function () {
-        shareTender();
-    });
-});
-
-//确认分享
-$('#share-confirm').click(function(){
-    const hintInfo = $('#share-info');
-    $.bootstrapLoading.start();
-    const perHeight = 30;
-    let allowCopy = $('#allowCopy').prop('checked'),
-        allowCoop = $('#allowCooperate').prop('checked');
-        userName = $('#user_name').text() || '',
-        userCompany = $('#user_company').text() || '',
-        userMobile = $('#user_mobile').text() || '',
-        userEmail = $('#user_email').text() || '';
-    let shareData = [{userID: shareUserID, allowCopy: allowCopy, allowCooperate: allowCoop}];
-    //分享
-    CommonAjax.post('/pm/api/share', {user_id: userID, type: shareType.create,  projectID: shareSeleted.data.ID, shareData: shareData}, function (rstData) {
-        //更新已分享table
-        let $tr = $(`<tr>
-                        <td style="width: 112px">${userName}</td>
-                        <td style="width: 165px">${userCompany}</td>
-                        <td style="width: 136px">${userMobile}</td>
-                        <td style="width: 136px">${userEmail}</td>
-                        <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>`);
-        let tbodyTotalHeight = $('#shareToInfo').height() + perHeight > 200 ? 200 : $('#shareToInfo').height() + perHeight;
-        $('#shareToInfo').height(tbodyTotalHeight);
-        $('#shareToInfo').append($tr);
-        //更新缓存
-        shareSeleted.data.shareInfo = shareSeleted.data.shareInfo.concat(shareData);
-        let sheet = projTreeObj.workBook.getSheet(0);
-        projTreeObj.renderSheetFuc(sheet, function () {
-            sheet.invalidateLayout();
-            sheet.repaint();
-        });
-        bindCancelShareCheck(shareSeleted);
-        $.bootstrapLoading.end();
-        $('#shareFindDiv').hide();
-        $('#share-confirm').addClass('disabled');
-    }, function () {
-        $.bootstrapLoading.end();
-        $('#share-confirm').removeClass('disabled');
-    });
-});
 
 //批量替换文件,切换建设项目
 $('#otherProject').change(function(){
@@ -3904,17 +3687,6 @@ $('#otherProject').change(function(){
     }
 });
 
-
-//分享给...界面确认
-$('#shareToConfirm').click(function () {
-    let selected = projTreeObj.tree.selected;
-    if (!selected) {
-        return;
-    }
-    updateShareInfo(selected);
-});
-
-
 $("#confirm-import").click(function() {
     //$.bootstrapLoading.start();
     if (STATE.importing) {
@@ -4052,132 +3824,6 @@ $('#import').on('show.bs.modal', function(){
     $('.custom-file-label').text(`请选择建设项目文件`);
 });
 
-//设置分享给界面数据
-//@param {Object}selected @return {void}
-function setShareToModal(selected){
-    const perHeight = 30; //每条分享给数据的高度
-    $('#shareToInfo').empty();
-    if(!selected){
-        return;
-    }
-    let userIDs = [];
-    for(let user of selected.data.shareInfo){
-        userIDs.push(user.userID);
-    }
-    CommonAjax.post('/user/getUsers', {userIDs: userIDs}, function (rstData) {
-        for(let userInfo of rstData){
-            for(let user of selected.data.shareInfo){
-                if(user.userID === userInfo._id){
-                    user.exist = true;
-                    user.name = userInfo.real_name;
-                    user.company = userInfo.company;
-                    user.mobile = userInfo.mobile;
-                    user.email = userInfo.email;
-                }
-            }
-        }
-        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>`;
-        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>`;
-            infoArr.push(infoHtml);
-        }
-        let tbodyTotalHeight = infoArr.length * perHeight + 5 > 200 ? 200 : infoArr.length * perHeight + 5;
-        $('#shareToInfo').height(tbodyTotalHeight);
-        let infoHtml = infoArr.join('');
-        $('#shareToInfo').html(infoHtml);
-        bindCancelShareCheck(selected);
-    });
-}
-
-//取消分享的勾选操作
-//@param {Object}selected(选中项目节点) @return {void}
-function bindCancelShareCheck(selected) {
-    $('#shareToInfo input[value="cancelShare"]').unbind('click');
-    $('#shareToInfo input[value="cancelShare"]').click(function () {
-        let index = $(this).parent().parent().index() - 1,
-            $coo = $(this).parent().prev().children(),
-            $copy = $(this).parent().prev().prev().children();
-        let isCancel = $(this).prop('checked');
-        if (isCancel) {//取消分享,取消拷贝、协作勾选
-            $coo.prop('checked', false);
-            $copy.prop('checked', false);
-        } else {//取消取消分享,恢复原本的拷贝、协作勾选状态
-            let shareData = selected.data.shareInfo[index];
-            if (shareData) {
-                $coo.prop('checked', shareData.allowCooperate);
-                $copy.prop('checked', shareData.allowCopy);
-            }
-        }
-    });
-}
-//更新项目分享信息
-//@param {Object}selected
-function updateShareInfo(selected){
-    if(!selected){
-        return;
-    }
-    let usersTr = $('#shareToInfo').find('tr');
-    if (usersTr && usersTr.length > 0) {
-        usersTr = usersTr.slice(1);
-    }
-    let newShareInfo = [],
-        orgShareInfo = [];
-    const postData = [];
-    //数据不同才提交
-    for (let shareData of selected.data.shareInfo) {
-        orgShareInfo.push({userID: shareData.userID, allowCopy: shareData.allowCopy, allowCooperate: shareData.allowCooperate})
-    }
-    for(let i = 0; i < usersTr.length; i++){
-        let userTr = usersTr[i];
-        let allowCopy = $(userTr).find('input:eq(0)').prop('checked');
-        let allowCoop = $(userTr).find('input:eq(1)').prop('checked');
-        let cancelShare = $(userTr).find('input:eq(2)').prop('checked');
-        selected.data.shareInfo[i].allowCopy = allowCopy;
-        selected.data.shareInfo[i].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: postData}, function (shareData) {
-        selected.data.shareInfo = shareData;
-        let sheet = projTreeObj.workBook.getSheet(0);
-        projTreeObj.renderSheetFuc(sheet, function () {
-            sheet.invalidateLayout();
-            sheet.repaint();
-        });
-    });
-}
-
 //刷新建设项目汇总金额信息
 function refreshProjSummary(project, summaryInfo) {
     let refreshNodes = [];

+ 1 - 68
web/building_saas/pm/js/pm_share.js

@@ -10,6 +10,7 @@
 
 const pmShare = (function () {
     const spreadDom = $('#shareSpread');
+    let shareSeleted = null;
     let spreadObj = {workBook: null, sheet: null};
     let preSelection = null;
     //项目分享类型,由别人分享给自己的,和自己分享给别人的
@@ -520,75 +521,7 @@ const pmShare = (function () {
         };
         return new InteractionCell();
     }
-    //设置分享给界面数据
-    //@param {Object}selected @return {void}
-    function setShareToModal(selected){
-        $('#shareToInfo').empty();
-        if(!selected){
-            return;
-        }
-        let userIDs = [];
-        for(let user of selected.data.shareInfo){
-            userIDs.push(user.userID);
-        }
-        CommonAjax.post('/user/getUsers', {userIDs: userIDs}, function (rstData) {
-            for(let userInfo of rstData){
-                for(let user of selected.data.shareInfo){
-                    if(user.userID === userInfo._id){
-                        user.name = userInfo.real_name;
-                        user.company = userInfo.company;
-                        user.mobile = userInfo.mobile;
-                        user.email = userInfo.email;
-                    }
-                }
-            }
-            let infoArr = [];
-            for(let user of selected.data.shareInfo){
-                let infoHtml = `<tr>
-                                          <td style="width: 106px;">${user.name}</td>
-                                          <td style="width: 146px;">${user.company}</td>
-                                          <td style="width: 146px;">${user.mobile}</td>
-                                          <td style="width: 156px;">${user.email}</td>
-                                          <td style="width: 70px;text-align: center"><input value="allowCopy" ${user.allowCopy ? 'checked' : ''} type="checkbox"></td>
-                                          <td style="width: 70px;text-align: center"><input value="cancelShare" type="checkbox"></td>
-                               </tr>`;
-                infoArr.push(infoHtml);
-            }
-            let infoHtml = infoArr.join('');
-            $('#shareToInfo').html(infoHtml);
-        });
-    }
-    //更新项目分享信息
-    //@param {Object}selected
-    function updateShareInfo(selected){
-        if(!selected){
-            return;
-        }
-        let usersTr = $('#shareToInfo').find('tr');
-        let newShareInfo = [];
-        for(let i = 0; i < usersTr.length; i++){
-            let userTr = usersTr[i];
-            let allowCopy = $(userTr).find('input:first').prop('checked');
-            let cancelShare = $(userTr).find('input:last').prop('checked');
-            selected.data.shareInfo[i].allowCopy = allowCopy;
-            if(!cancelShare){
-                newShareInfo.push(selected.data.shareInfo[i]);
-            }
-        }
-        CommonAjax.post('/pm/api/updateProjects', {user_id: userID, updateData: [{updateType: 'update', updateData: {ID: selected.data.ID, shareInfo: newShareInfo}}]}, function () {
-            selected.data.shareInfo = newShareInfo;
-            if(newShareInfo.length === 0){
-                renderSheetFunc(spreadObj.sheet, function () {
-                    let rIdx = selected.serialNo();
-                    tree.removeNode(selected);
-                    spreadObj.sheet.deleteRows(rIdx, 1);
-                    spreadObj.sheet.setRowCount(tree.items);
-                    initSelection({row: spreadObj.sheet.getActiveRowIndex(), rowCount: 1},null);
 
-                });
-            }
-        });
-    }
     const foreColor = '#007bff';
     const cancelForeColor = 'red';
     //显示树结构数据

+ 54 - 0
web/common/components/share/index.html

@@ -0,0 +1,54 @@
+<div class="modal fade" id="share" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">分享给...</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="row justify-content-center">
+                    <div class="form-group col-6 ">
+                        <div class="input-group input-group-sm">
+                            <input id="share-phone" type="text" class="form-control" placeholder="输入 手机号 添加分享" autofocus="autofocus">
+                            <div class="btn-group btn-group-sm ml-1">
+                                <a class="btn btn-primary dropdown-toggle" href="#" role="button" id="contacts-dropdown">
+                                    联系人
+                                </a>
+                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="contacts-dropdown" style="width: 250px;" id="contacts-menue">
+                                    <div class="">
+                                        <ul class="book-list" id="contacts">
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="col-3">
+                        <legend>最近分享</legend>
+                        <!--最近分享-->
+                        <ul class="book-list" id="recent-share">
+                        </ul>
+                    </div>
+                    <div class="col-9">
+                <!-- 搜索提示 -->
+                <p class="text-danger" id="share-hint"></p>    
+                <!--搜索结果-->
+                <ul class="book-list my-3" style="height:auto" id="share-search-result">
+                </ul>
+                <!--已分享列表-->
+                <legend>已分享</legend>
+                <ul class="book-list" style="max-height: 250px" id="shared-list">
+                    </ul>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 412 - 0
web/common/components/share/index.js

@@ -0,0 +1,412 @@
+const SHARE_TO = (() => {
+
+    const ShareType = {
+        CREATE: 'create',
+        UPDATE: 'update',
+        CANCEL: 'cancel',
+    };
+
+    // 当前分享的项目ID
+    let curProjectID;
+
+    // 当前项目的已分享列表
+    let curSharedUsers = [];
+
+    // 清除缓存
+    function clearCache() {
+        curProjectID = null;
+        curSharedUsers = [];
+    }
+
+    // 最近联系人显示数量
+    const rencentCount = 5;
+
+    // 获取初始数据:1.最近分享人 2.联系人 3.已分享人
+    async function getInitalData(projectID) {
+        return await ajaxPost('/pm/api/getInitialShareData', { user_id: userID, count: rencentCount, projectID }, false);
+
+    }
+
+    // 获取头像视图html
+    function getAvatarHTML(mobile, realName) {
+        // 手机最后一位
+        const lastMobileNumer = mobile.substr(-1);
+        // 显示名称为真实名称后两位
+        const nickName = realName.substr(-2);
+        return `<span class="avatar bg-${lastMobileNumer}">${nickName}</span>`;
+    }
+
+    /**
+     * 获取用户列表视图html:最近分享和联系人用
+     * @param {Array} users - 排序过的用户数据
+     * @param {Boolean} showAlphabet - 是否显示字母表分类
+     * @return {HTMLString}
+     *   */
+    function getUserHTML(users, showAlphabet) {
+        let curLetter = '';
+        return users.reduce((html, user) => {
+            const mobile = user.mobile || '';
+            const realName = user.real_name || '';
+            const company = user.company || '';
+            if (showAlphabet) {
+                // 名字首个字符对应拼音
+                const letter = pinyinUtil.getFirstLetter(realName).substr(0, 1);
+                if (letter !== curLetter) {
+                    curLetter = letter;
+                    html += `<li class="letter">${letter}</li>`;
+                }
+            }
+            const avatarHtml = getAvatarHTML(mobile, realName);
+            return html +
+                `<li>
+                    ${avatarHtml}
+                    <div class="book-body">
+                        <h5 class="mt-0" title="${company}">${realName}</h5>
+                        <span>${mobile}</span>
+                    </div>
+                </li>`;
+
+        }, '');
+    }
+
+    // 初始化最近分享视图
+    function initRecentView(recentUsers) {
+        const recentShareHTML = getUserHTML(recentUsers, false);
+        $('#recent-share').html(recentShareHTML);
+        // 点击最近分享列表自动添加该用户添加到搜索框中
+        $('#recent-share li').click(function () {
+            const mobile = $(this).find('div span')[0].textContent;
+            $('#share-phone').val(mobile);
+            handleSearch()
+        })
+    }
+    // 初始化联系人视图
+    function initContactsView(contacts) {
+        // 联系人按拼英首字母降序排序
+        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 = getUserHTML(contacts, true);
+        $('#contacts').html(contactsHTML);
+        // 点击联系人自动添加该联系人添加到搜索框中
+        $('#contacts li:not(.letter)').click(function () {
+            const mobile = $(this).find('div span')[0].textContent;
+            $('#share-phone').val(mobile);
+            $('#contacts-menue').removeClass('show');
+            handleSearch()
+        });
+    }
+
+    // 初始化已分享视图
+    function initSharedView(sharedUsers) {
+        const html = sharedUsers.reduce((html, user, index) => {
+            const mobile = user.mobile || '';
+            const realName = user.real_name || '';
+            const company = user.company || '';
+            const avatarHTML = getAvatarHTML(mobile, realName);
+            const copyLabelFor = `allowCopy${index}`;
+            const editLabelFor = `allowEdit${index}`;
+            return html +
+                `<li class="card mb-1">
+                    <div class="card-body p-1 row m-0">
+                        ${avatarHTML}
+                        <div class="book-body col-auto pl-0">
+                            <h5 class="mt-0">${realName}</h5>
+                            ${mobile}
+                        </div>
+                        <div class="col-5">${company}</div>
+                        <div class="col ml-auto p-0">
+                            <div class="d-flex justify-content-end">
+                                <div>
+                                    <div class="custom-control custom-checkbox">
+                                        <input type="checkbox" class="custom-control-input allow-copy" id="${copyLabelFor}" data-user="${user._id}" ${user.allowCopy ? 'checked' : ''}>
+                                        <label class="custom-control-label" for="${copyLabelFor}">允许拷贝</label>
+                                    </div>
+                                    <div class="custom-control custom-checkbox">
+                                        <input type="checkbox" class="custom-control-input allow-edit" id="${editLabelFor}" data-user="${user._id}" ${user.allowCooperate ? 'checked' : ''}>
+                                        <label class="custom-control-label" for="${editLabelFor}">允许编辑</label>
+                                    </div>
+                                </div>
+                                <div class="ml-3 d-flex align-items-center">
+                                    <button class="btn btn-sm btn-outline-danger cancel-share" data-user="${user._id}">取消分享</button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </li>`;
+        }, '');
+        $('#shared-list').html(html);
+        // 编辑允许拷贝
+        $('#shared-list .allow-copy').click(function () {
+            handleCheckBoxClick.call(this);
+            handleShareAction.call(this, ShareType.UPDATE);
+        });
+        // 编辑允许编辑
+        $('#shared-list .allow-edit').click(function () {
+            handleCheckBoxClick.call(this);
+            handleShareAction.call(this, ShareType.UPDATE);
+        });
+        // 取消分享
+        $('#shared-list .cancel-share').click(function () {
+            handleShareAction.call(this, ShareType.CANCEL);
+        });
+    }
+
+    // 初始化搜索结果视图
+    function initSearchResultView(user) {
+        if (!user) {
+            $('#share-search-result').html('')
+        } else {
+            const mobile = user.mobile || '';
+            const realName = user.real_name || '';
+            const company = user.company || '';
+            const avatarHTML = getAvatarHTML(mobile, realName);
+            const html =
+                `<li class="card mb-1">
+                        <div class="card-body p-1 row m-0">
+                            ${avatarHTML}
+                            <div class="book-body col-auto pl-0">
+                                <h5 class="mt-0">${realName}</h5>
+                                ${mobile}
+                            </div>
+                            <div class="col-5">${company}</div>
+                            <div class="col ml-auto p-0">
+                                    <div class="d-flex justify-content-end">
+                                        <div>
+                                            <div class="custom-control custom-checkbox">
+                                                <input type="checkbox" class="custom-control-input" id="allow-copy" checked="">
+                                                <label class="custom-control-label" for="allow-copy">允许拷贝</label>
+                                            </div>
+                                            <div class="custom-control custom-checkbox">
+                                                <input type="checkbox" class="custom-control-input" id="allow-edit" checked="">
+                                                <label class="custom-control-label" for="allow-edit">允许编辑</label>
+                                            </div>
+                                        </div>
+                                        <div class="ml-3 d-flex align-items-center"><button class="btn btn-sm btn-primary" id="share-to" data-user="${user._id}">分享给Ta</button></div>
+                                    </div>
+                                </div>
+                        </div>
+                    </li>`;
+            $('#share-search-result').html(html);
+
+            // 允许拷贝
+            $('#allow-copy').click(function () {
+                handleCheckBoxClick.call(this);
+            });
+            // 允许编辑
+            $('#allow-edit').click(function () {
+                handleCheckBoxClick.call(this);
+            });
+            // 分享给事件
+            $('#share-to').click(function () {
+                handleShareAction.call(this, ShareType.CREATE, user);
+            });
+        }
+    }
+
+    // 复选框框的状态随点击更改,不处理的话input的checked并不会自动处理
+    function handleCheckBoxClick() {
+        const curChecked = !$(this).attr('checked');
+        if (curChecked) {
+            $(this).attr('checked', 'checked');
+        } else {
+            $(this).removeAttr('checked');
+        }
+    }
+
+    // 添加分享、编辑分享、取消分享的动作
+    async function handleShareAction(shareType, user) {
+        try {
+            $.bootstrapLoading.start();
+            const receiver = $(this).data('user');
+            let shareData;
+            let type = ShareType.UPDATE;
+            if (shareType === ShareType.CREATE) {
+                const allowCopy = !!$('#allow-copy').attr('checked');
+                const allowCooperate = !!$('#allow-edit').attr('checked');
+                shareData = [{ userID: receiver, allowCopy, allowCooperate }];
+                type = ShareType.CREATE; // 上传的服务器的type,删除跟更新是一样的
+            } else if (shareType === ShareType.UPDATE) {
+                const allowCopy = !!$(`[data-user=${receiver}].allow-copy`).attr('checked');
+                const allowCooperate = !!$(`[data-user=${receiver}].allow-edit`).attr('checked');
+                shareData = [{ userID: receiver, allowCopy, allowCooperate }];
+            } else {
+                shareData = [{ userID: receiver, isCancel: true }];
+            }
+            const postData = {
+                user_id: userID,
+                type,
+                projectID: curProjectID,
+                count: rencentCount,
+                shareData
+            };
+            const rst = await ajaxPost('/pm/api/share', postData);
+            // 请求成功后刷新视图
+            if (shareType === ShareType.CREATE || shareType === ShareType.CANCEL) {
+                if (shareType === ShareType.CREATE) {
+                    user.allowCopy = shareData[0].allowCopy;
+                    user.allowCooperate = shareData[0].allowCooperate;
+                    curSharedUsers.unshift(user);
+                    $('#share-phone').val('');
+                    initSearchResultView();
+                } else {
+                    curSharedUsers = curSharedUsers.filter(user => user._id !== receiver);
+                }
+                if (Array.isArray(rst.recentUsers)) {
+                    initRecentView(rst.recentUsers);
+                }
+                if (Array.isArray(rst.contacts)) {
+                    initContactsView(rst.contacts)
+                }
+                initSharedView(curSharedUsers);
+                refreshTreeView();
+            }
+        } catch (err) {
+            console.log(err);
+            alert(`${String(err)} 请重试。`);
+            initSharedView(curSharedUsers);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 刷新项目管理树视图
+    // 如果是在项目管理页面,需要刷新树(分享图标可能需要清除)
+    function refreshTreeView() {
+        if (typeof projTreeObj !== 'undefined' && projTreeObj.tree.selected) {
+            projTreeObj.tree.selected.data.shareInfo = curSharedUsers;
+            const sheet = projTreeObj.workBook.getSheet(0);
+            projTreeObj.renderSheetFuc(sheet, function () {
+                sheet.invalidateLayout();
+                sheet.repaint();
+            });
+        }
+    }
+
+    // 初始化分享给的页面
+    async function initModal(projectID) {
+        try {
+            curProjectID = projectID;
+            $.bootstrapLoading.start();
+            // 恢复
+            $('#share-phone').val('');
+            initSearchResultView();
+            $('#share-hint').text('');
+            const { sharedUsers, recentUsers, contacts } = await getInitalData(projectID);
+            curSharedUsers = sharedUsers;
+            initSharedView(sharedUsers);
+            initRecentView(recentUsers);
+            initContactsView(contacts);
+            $.bootstrapLoading.end();
+            setTimeout(() => $('#sharePhone').focus(), 200);
+            $('#share').modal('show');
+        } catch (err) {
+            console.log(err);
+            alert(err);
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 退出分享给页面
+    function exitModal() {
+        clearCache();
+    }
+
+    // 分享给
+    async function handleSearch() {
+        let phone = $('#share-phone').val();
+        phone = phone && phone.trim() || '';
+        //$('#share-hint').text('');
+        initSearchResultView();
+        if (!phone) {
+            $('#share-hint').text('请输入手机号码。');
+            return;
+        }
+        // 根据手机号获取用户
+        const user = await ajaxPost('/user/getUserByMobile', { mobile: phone });
+        if (!user) {
+            $('#share-hint').text('账号不存在。');
+            return;
+        }
+        if (user._id === userID) {
+            $('#share-hint').text('不可分享给自己。');
+            return;
+        }
+        const matched = curSharedUsers.find(item => item._id === user._id);
+        if (matched) {
+            $('#share-hint').text('已与该用户分享。');
+            return;
+        }
+        $('#share-hint').text('');
+        initSearchResultView(user);
+    }
+
+    // 一些事件的监听
+    function handleEventListener() {
+        // 界面消失
+        $('#share').on('hide.bs.modal', function () {
+            exitModal();
+        });
+
+        // 联系人下拉
+        $('#contacts-dropdown').click(function () {
+            const $subMenu = $('#contacts-menue');
+            const visible = $subMenu.is(':visible');
+            if (visible) {
+                $subMenu.removeClass('show');
+            } else {
+                $subMenu.addClass('show');
+            }
+        });
+
+        // 点击body时,联系人菜单的处理
+        $('body').click(function (e) {
+            const body = $(this)[0];
+            const $contactsMenu = $('#contacts-menue');
+            const contactsMenu = $contactsMenu[0];
+            const dropdownButton = $('#contacts-dropdown')[0]
+            if (!$contactsMenu.is(':visible')) {
+                return;
+            }
+            let target = e.target;
+            while (target !== body) {
+                if ([contactsMenu, dropdownButton].includes(target)) {
+                    return;
+                }
+                target = target.parentElement;
+            }
+            $(contactsMenu.parentElement).removeClass('show');
+            $contactsMenu.removeClass('show');
+        });
+
+        // 输入手机号查找要分享给的用户
+        let keyupTime = 0;
+        let delayTime = 500;
+        function delayKeyup(callback) {
+            const nowTime = Date.now();
+            keyupTime = nowTime;
+            setTimeout(function () {
+                if (nowTime - keyupTime == 0) {
+                    callback();
+                }
+            }, delayTime);
+        }
+        $('#share-phone').on('keyup', function () {
+            delayKeyup(function () {
+                handleSearch();
+            });
+        });
+        $('#sharePhone').on('keypress', function (e) {
+            if (e.keyCode === 13) {
+                $(this).blur();
+            }
+        });
+    }
+
+    return {
+        initModal,
+        handleEventListener,
+    }
+})();