Browse Source

Merge branch 'master' into 分摊功能

zhangweicheng 5 years ago
parent
commit
ca90abf11c

+ 4 - 0
modules/all_models/share_list.js

@@ -9,6 +9,10 @@ const Schema = mongoose.Schema;
 const shareSchema = new Schema({
     ID: String,
     projectID: Number,
+    isRead: {
+        type: Boolean,
+        default: false
+    },
     owner: String, // 项目拥有者ID
     receiver: String, // 接收者ID
     allowCopy: {

+ 32 - 0
modules/blank/routes/index.js

@@ -0,0 +1,32 @@
+const { BlankType } = require('../../../public/common_constants');
+
+function getCardHtml(info, btn) {
+    return `<div class="row">
+    <div class="col-lg-8 mx-auto mt-5">
+        <div class="card newuser-input">
+            <div class="card-body">
+                <h4 class="mt-2 mb-5 text-center">${info}</h4>
+                ${btn ? `<div class="form-group text-center"><a class="btn btn-primary" href="${btn.href}">${btn.title}</a></div>` : ''}
+            </div>
+        </div>
+    </div>
+</div>`;
+}
+
+module.exports = function (app) {
+    app.get('/blank', function (req, res) {
+        const { type } = req.query;
+        let html = '';
+        switch (+type) {
+            case BlankType.SHARE_CANCEL:
+                html = getCardHtml('分享设置已被修改,当前项目无权查看。', { href: '/pm', title: '返回项目管理' });
+                break;
+            case BlankType.NOT_FOUND:
+            default:
+                html = getCardHtml('很抱歉,您要访问的页面不存在。');
+                break;
+
+        }
+        res.render('common/components/blank/index.html', { html });
+    });
+}

+ 6 - 3
modules/main/routes/main_route.js

@@ -5,6 +5,7 @@
 
 import BaseController from "../../common/base/base_controller";
 const projectModel = require("../../pm/models/project_model");
+const pmFacade = require('../../pm/facade/pm_facade');
 import OptionsDao from '../../options/models/optionsModel';
 import optionSetting from '../../options/models/optionTypes';
 let config = require("../../../config/config.js");
@@ -12,8 +13,8 @@ module.exports =function (app) {
     const baseController = new BaseController();
     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, allowCooperate) {
+        const projectID = +req.query.project;
+        pm.checkProjectRight(req.session.sessionUser.id, projectID, async function (hasRight, projectData, isOpenShareProject, allowCooperate) {
             if (hasRight) {
                 //分享的项目,只读、协作(允许编辑)
                 let projectReadOnly = false,
@@ -29,6 +30,7 @@ module.exports =function (app) {
                 if(options){
                     options = await optionsDao.saveOptions(req.session.sessionUser.id, req.session.sessionCompilation._id, optionSetting);
                 }
+                const markReadProjectIDs = isOpenShareProject ? await pmFacade.markShareItemsRead(projectID, req.session.sessionUser.id) : [];
                 res.render('building_saas/main/html/main.html',
                     {
                         userAccount: req.session.userAccount,
@@ -40,7 +42,8 @@ module.exports =function (app) {
                         projectCooperate: projectCooperate,
                         LicenseKey:config.getLicenseKey(process.env.NODE_ENV),
                         options:JSON.stringify(options),
-                        overWriteUrl:req.session.sessionCompilation.overWriteUrl
+                        overWriteUrl:req.session.sessionCompilation.overWriteUrl,
+                        markReadProjectIDs: JSON.stringify(markReadProjectIDs)
                     });
             } else {
                 res.redirect('/pm');

+ 15 - 7
modules/pm/controllers/pm_controller.js

@@ -59,13 +59,15 @@ module.exports = {
              * userId(String): Session.userID
              */
             let shareInfo = null;
+            let isOpenShareProject = false;
             //判断是否是打开分享的项目,分享项目shareInfo不为null
             if(userId !== result.userID){
                 shareInfo = await pm_facade.getShareInfo(userId, result.ID);
+                isOpenShareProject = true;
             }
             if ((userId === result.userID || shareInfo) && result._doc.projType === projType.tender) {
                 const allowCooperate = (shareInfo || {}).allowCooperate;
-                callback(true, result, allowCooperate);
+                callback(true, result, isOpenShareProject, allowCooperate);
             } else {
                 callback(false);
             }
@@ -292,8 +294,8 @@ module.exports = {
     },
     // 项目管理首页
     index: async function(request, response) {
-        // TODO 上线后删除,处理旧的分享数据
-        await pm_facade.prepareShareList();
+        // 上线后删除,处理旧的分享数据
+        // await pm_facade.prepareShareList();
         // 获取编办信息
         let sessionCompilation = request.session.sessionCompilation;
         if (sessionCompilation === undefined ||sessionCompilation ===null) {
@@ -323,7 +325,9 @@ module.exports = {
         let overWriteUrl = fs.existsSync(absoluteUrl) && fs.statSync(absoluteUrl).isFile()? compilationData.overWriteUrl : null;
         //欢迎页显示控制
         let [isShow,context,showTime] = await pm_facade.getWelcomeInfo(sessionCompilation._id,request.session.sessionUser,request.session.compilationVersion.includes('免费'));
+        const unreadShareList = await pm_facade.getUnreadShareListByCompilation(request.session.sessionUser.id, sessionCompilation._id);
         let renderData = {
+            unreadShareList: JSON.stringify(unreadShareList),
             isFirst: isFirst,
             isShow:isShow,
             context:context,
@@ -637,7 +641,7 @@ module.exports = {
     share: async function (req, res) {
         try {
             const data = JSON.parse(req.body.data);
-            const { type, shareData, projectID, count } = data;
+            const { type, permissionType, 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);
@@ -700,15 +704,19 @@ module.exports = {
                 pm_facade.getRecentShareList(userID, count),
                 userModelObj.getContacts(userID)
             ]
-            let rst = null;
+            // 获取需要广播推送的单位工程
+            // shareData数组的形式是以前需求需要,现在的需求下,shareData数组必定只有一个元素
+            const emitTenders = await pm_facade.getShareInfoAfterChangePermission(permissionType, shareData[0].userID, projectID);
+            let rst = { emitTenders };
             if (!isSimpleUpdate) {
                 const [recentUsers, contacts] = await Promise.all(rstTask);
-                rst = { recentUsers, contacts };
+                Object.assign(rst, { recentUsers, contacts });
             }
             callback(req, res, 0, 'success', rst);
         }
         catch (err) {
-            callback(req, res, 1, err, null);
+            console.log(err);
+            callback(req, res, 1, String(err), null);
         }
     },
     receiveProjects: async function(req, res) {

+ 112 - 13
modules/pm/facade/pm_facade.js

@@ -17,8 +17,11 @@ module.exports={
     getShareInfoMap,
     getRecentShareList,
     getProjectShareList,
+    getUnreadShareListByCompilation,
     getShareTip,
     getShareState,
+    markShareItemsRead,
+    getShareInfoAfterChangePermission,
     moveProject:moveProject,
     accessToCopyProject,
     copyProject:copyProject,
@@ -95,7 +98,7 @@ import SectionTreeDao from '../../complementary_ration_lib/models/sectionTreeMod
 let sectionTreeDao = new SectionTreeDao();
 import CounterModel from "../../glj/models/counter_model";
 import moment from 'moment-timezone';
-const { fixedFlag } = require('../../../public/common_constants');
+const { fixedFlag, SharePermissionChangeType } = require('../../../public/common_constants');
 const notDeleted = [{deleteInfo: null}, {'deleteInfo.deleted': false}];
 let cipher = require('../../../public/cipher');
 const compilationModel = mongoose.model('compilation');
@@ -271,6 +274,23 @@ async function getProjectShareList(projectID, limit = null) {
     return users;
 }
 
+// 获取某用户某费用定额的未读的被分享记录
+async function getUnreadShareListByCompilation(userID, compilationID, isGetCount) {
+    const fields = isGetCount ? '-_id projectID ' : '-_id';
+    const list = await shareListModel.find({ receiver: userID, isRead: false }, fields);
+    const idList = list.reduce((acc, cur) => {
+        acc.push(cur.projectID);
+        return acc;
+    }, []);
+    const notDeleted = [{ deleteInfo: null }, { 'deleteInfo.deleted': false }];
+    const query = { ID: { $in: idList }, compilation: compilationID, $or: notDeleted };
+    if (isGetCount) {
+        return await projectModel.count(query)
+    }
+    const items = await projectModel.find(query, '-_id ID').lean();
+    return items.map(item => item.ID);
+}
+
 // 获取分享的提示(造价书分享按钮tooltip使用)
 async function getShareTip(projectID, limit) {
     const task = [
@@ -305,6 +325,96 @@ async function getShareState(projectID, receiver) {
     };
 }
 
+// 自身及父链上的分享数据(链距离最近的一个)
+async function getUpChainShareData(projectID, receiver) {
+    while (projectID != -1) {
+        const shareData = await shareListModel.findOne({ projectID, receiver }).lean();
+        if (shareData) {
+            return shareData;
+        }
+        const project = await projectModel.findOne({ ID: projectID }, '-_id ParentID').lean();
+        projectID = project ? project.ParentID : -1;
+    }
+    return null;
+}
+
+// 标记已读分享条目
+// 打开任意的单位工程,都会将其父分享条目标记和自身为已读 
+// eg:被分享了一个建设项目,和其下一个单位工程,打开该一个单位工程,建设项目标记为已读、该单位工程也标记为已读
+async function markShareItemsRead(projectID, userID) {
+    const upChainIDs = await getUpChainIDs(projectID);
+    const unreadList = await shareListModel.find({ projectID: { $in: upChainIDs }, receiver: userID, isRead: false }, '-_id ID projectID').lean();
+    const markReadProjectIDs = [];
+    const bulks = [];
+    unreadList.forEach(item => {
+        markReadProjectIDs.push(item.projectID);
+        bulks.push({
+            updateOne: {
+                filter: { ID: item.ID },
+                update: { isRead: true }
+            }
+        });
+    });
+    if (bulks.length) {
+        await shareListModel.bulkWrite(bulks);
+    }
+    return markReadProjectIDs;
+}
+
+/**
+ * 分享权限变更后,根据变更操作,获取相关项目的分享权限信息
+ * 不能简单根据某个项目的分享信息获得其权限,因为目前的分享可以重复分享(分享单位工程A、分享包含单位工程A的父项) 
+ * 最新的分享权限是由项目链上(所有子项、自身、父项...)最新的分享信息决定的
+ * @param {Number} permissionType - 分享权限变更类型
+ * @param {String} receiver - 用户ID
+ * @param {Number} projectID - 项目ID
+ */
+async function getShareInfoAfterChangePermission(permissionType, receiver, projectID) {
+    const project = await projectModel.findOne({ ID: projectID }, '-_id ID projType').lean();
+    if (!project) {
+        return [];
+    }
+    // UPDATE_COPY不触发推送给主页面的消息
+    const propChange = [SharePermissionChangeType.UPDATE_COOPERATE].includes(permissionType);
+    const cancelShare = permissionType === SharePermissionChangeType.CANCEL;
+    // 需要处理的单位工程
+    if (propChange && project.projType === projectType.tender) {
+        const shareData = await shareListModel.findOne({ projectID, receiver }, '-_id allowCooperate allowCopy updateDate').lean();
+        const tender = { ID: projectID };
+        if (shareData) {
+            Object.assign(tender, shareData);
+        }
+        return [tender];
+    }
+    if (propChange && project.projType !== projectType.tender) {
+        const projects = await getPosterityProjects([projectID], false, '-_id ID projType');
+        const tenders = projects.filter(p => p.projType === projectType.tender);
+        const shareData = await shareListModel.findOne({ projectID, receiver }, '-_id allowCooperate allowCopy updateDate').lean();
+        if (shareData) {
+            tenders.forEach(tender => Object.assign(tender, shareData));
+        }
+        return tenders
+    }
+    if (cancelShare && project.projType === projectType.tender) {
+        const shareData = await getUpChainShareData(projectID, receiver);
+        return shareData ? [] : [{ ID: projectID }];
+    }
+    if (cancelShare && project.projType !== projectType.tender) {
+        const projects = await getPosterityProjects([projectID], false, '-_id ID projType');
+        const tenders = [];
+        for (const p of projects) {
+            if (p.projType === projectType.tender) {
+                const shareData = await getUpChainShareData(p.ID, receiver);
+                if (!shareData) {
+                    tenders.push({ ID: p.ID });
+                }
+            }
+        }
+        return tenders;
+    }
+    return [];
+}
+
 //拷贝例题项目
 //@param {String}userID {Array}projIDs拷贝的例题项目ID(建设项目、文件夹)@return {Boolean}
 async function copyExample(userID, compilation, projIDs){
@@ -697,12 +807,7 @@ async function copyUnitPriceFile(newProjectID,rootProjectID,userID,originaluUnit
                 rg.ID = uuidV1();
               }
             }
-            if(m.rations){
-              for(let r of m.rations){
-                r.ID = uuidV1(); 
-              }
-            }
-
+            //运费和原价定额ID不能更新,不然定额工料机找不到对应的定额
             rList.push(m);
           }
         }else{
@@ -1830,12 +1935,6 @@ async function importUnitPriceFiles(mainData,projectIDMap,unitPriceFileIDMap,use
                 rg.ID = uuidV1();
               }
             }
-            if(o.rations){
-              for(let r of o.rations){
-                r.ID = uuidV1();
-              }
-            }
-
             nList.push(o)
           }
         }else{

+ 30 - 0
public/common_constants.js

@@ -112,11 +112,41 @@
         RATION: 'ration',
     };
 
+    // 缓存key
+    const StorageKey = {
+        // 主界面一旦出现这个缓存,马上提示
+        ONCE_MAIN_LOADED: 'onceMainLoaded'
+    };
+
+    // 分享权限变更的类型
+    const SharePermissionChangeType = {
+        UPDATE_COOPERATE: 1,
+        UPDATE_COPY: 2,
+        CANCEL: 3,
+        SHARE: 4,
+        READ: 5
+    };
+
+    // 页面目标
+    const PageTarget = {
+        PM: 1, // 项目管理
+        MAIN: 2 // 造价书
+    };
+
+    const BlankType = {
+        NOT_FOUND: 1,
+        SHARE_CANCEL: 2,
+    };
+
     return {
         fixedFlag,
         COMPLEMENTARY_LIB,
         COMPILATION,
         ValuationType,
         SourceType,
+        StorageKey,
+        SharePermissionChangeType,
+        PageTarget,
+        BlankType
     };
 })

+ 7 - 1
public/web/socket/connection.js

@@ -13,6 +13,7 @@ socketObject = {
         socket.on('connect', function () {
             if (from == 'pm') {
                 me.roomInfo = {
+                    compilationUser: `${userID}@${compilationData._id}`,
                     userID: userID
                 };
             } else if (from == 'unitPrice') {
@@ -22,7 +23,8 @@ socketObject = {
             } else {
                 me.roomInfo = {
                     feeRate: me.getFeeRateRoomID(),
-                    unitFile: me.getUnitFileRoomID()
+                    unitFile: me.getUnitFileRoomID(),
+                    userIDProjectID: `${userID}@${projectObj.project.ID()}`
                 };
                 if (payload && payload.projectReadOnly === false) {
                     me.roomInfo.projectID = `projectID${projectObj.project.ID()}`;
@@ -38,6 +40,9 @@ socketObject = {
                     socket.emit(m.message, m.data);
                 }
             }
+            if (typeof markReadProjectIDs !== 'undefined' && markReadProjectIDs.length) {
+                SHARE_TO.emitPermissionChange(commonConstants.SharePermissionChangeType.READ, userID, null, [], { markReadProjectIDs });
+            }
             console.log('连接成功');
         });
 
@@ -75,6 +80,7 @@ socketObject = {
         socket.on('handleAvatarList', function ({ editingUsers }) {
             projectInfoObj.handleAvatarList(editingUsers);
         });
+        SHARE_TO.permissionChangeListener();
 
 
         //=============================================================================================

+ 22 - 0
socket.js

@@ -6,6 +6,8 @@
  * @version
  */
 import socket from "socket.io";
+const moment = require('moment');
+const { PageTarget } = require('./public/common_constants');
 
 const socketIO = socket(5500);
 
@@ -100,6 +102,26 @@ socketIO.on('connection', function(socket) {
         }
     });
 
+    // 分享权限变更
+    socket.on('sharePermissionChange', async function ({ permissionType, userID, compilationID, projectID, emitTenders, payload }) {
+        const emitPayload = { projectID };
+        if (payload) {
+            if (payload.prop) {
+                payload.prop.updateDate = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
+            }
+            Object.assign(emitPayload, payload);
+        }
+        // 推送给项目管理页面
+        socket.broadcast.to(`${userID}@${compilationID}`).emit('sharePermissionChange', { permissionType, target: PageTarget.PM, payload: emitPayload });
+        // 推送给主页面
+        for (const tender of emitTenders) {
+            const emitPayload = {};
+            if (typeof tender.allowCooperate !== 'undefined') {
+                emitPayload.allowCooperate = tender.allowCooperate;
+            }
+            socket.broadcast.to(`${userID}@${tender.ID}`).emit('sharePermissionChange', { permissionType, target: PageTarget.MAIN, payload: emitPayload });
+        }
+    });
 
     socket.on('disconnect', function () {
         // 由于用户可以重复打开项目,因此不做唯一性处理,只删除一个数据,不删除所有的同用户数据

+ 3 - 0
web/building_saas/css/custom.css

@@ -411,6 +411,9 @@ input.text-right{
     padding-left: 0;
     padding-right: 0;
 }
+.hide-area {
+    display: none;
+}
 .middle-modal-width {
     max-width: 650px;
 }

+ 14 - 2
web/building_saas/js/global.js

@@ -85,13 +85,25 @@ $(function () {
  * @param {String|Number} value
  * @return {void}
  */
+let storageErrCount = 0;
 function setLocalCache(key, value) {
     const storage = window.localStorage;
     if (!storage || key === '' || value === '') {
         return;
     }
-
-    storage.setItem(key, value);
+    try {
+        storage.setItem(key, value);
+        storageErrCount = 0;
+    } catch (err) {
+        storageErrCount++;
+        if (storageErrCount > 1) {
+            alert(`字段“${key}”数据过大,存入本地缓存失败。`)
+            storageErrCount = 0;
+            return;
+        }
+        storage.clear();
+        setLocalCache(key, value);
+    }
 }
 
 /**

+ 2 - 9
web/building_saas/main/html/main.html

@@ -43,7 +43,8 @@
         let projectCooperate = JSON.parse('<%- projectCooperate %>');
         let projectOptins =  JSON.parse('<%- options %>');
         // const G_SHOW_BLOCK_LIB = true;
-       const G_SHOW_BLOCK_LIB = false;
+        const G_SHOW_BLOCK_LIB = false;
+        const markReadProjectIDs = JSON.parse('<%- markReadProjectIDs %>');
     </script>
 </head>
 
@@ -55,14 +56,6 @@
 </div>-->
 <img src="/web/dest/css/img/question.png" id="question_pic" style="display: none">
     <div class="header">
-         <div class="top-msg clearfix">
-            <div class="alert alert-warning alert-dismissible" role="alert" id="notify" style="display: none">
-                <button type="button" class="close" aria-label="Close" onclick="$('#notify').hide();">
-                  <span aria-hidden="true">&times;</span>
-                </button>
-                <strong id="message"></strong>-
-            </div>
-        </div>
         <%include ../../../common/html/header.html %>
     </div>
     <div class="main">

+ 5 - 0
web/building_saas/main/js/views/project_view.js

@@ -994,6 +994,11 @@ var projectObj = {
             else {
 
             }
+            const onceAlert = getLocalCache(commonConstants.StorageKey.ONCE_MAIN_LOADED);
+            if (onceAlert) {
+                alert(onceAlert);
+                removeLocalCache(commonConstants.StorageKey.ONCE_MAIN_LOADED);
+            }
         });
 
     },

+ 6 - 1
web/building_saas/pm/html/project-management.html

@@ -22,7 +22,7 @@
         var userID = '<%- userID %>';
         let isFirst = JSON.parse('<%- isFirst %>');
         let isShow = JSON.parse('<%- isShow %>');
-
+        let unreadShareList = JSON.parse('<%- unreadShareList %>');
     </script>
     <style type="text/css">
         .hidden-area{
@@ -85,6 +85,11 @@
                         </li>-->
                         <li class="nav-item" data-original-title="分享" data-placement="right" data-toggle="tooltip">
                             <a class="nav-link" href="#pm_share" id="tab_pm_share" data-toggle="tab"><i class="fa fa-share-alt"></i></a>
+                            <% if (JSON.parse(unreadShareList).length > 0) { %>
+                                <span class="badge badge-danger" id="unread-share-count"><%- JSON.parse(unreadShareList).length %></span>
+                            <% } else {%>
+                                <span class="badge badge-danger hide-area" id="unread-share-count"><%- JSON.parse(unreadShareList).length %></span>
+                            <% } %>
                         </li>
                         <!--<li class="nav-item" data-original-title="协同工作" data-placement="right" data-toggle="tooltip">
                             <a class="nav-link" href="javascript:void(0);"><i class="fa fa-users"></i></a>

+ 1 - 3
web/building_saas/pm/js/pm_newMain.js

@@ -826,9 +826,7 @@ const projTreeObj = {
             let newTab = window.open('about:blank');
             BeforeOpenProject(ID, {'fullFolder': GetFullFolder(parent)}, function () {
                 let mainUrl = `/main?project=${ID}`;
-                CommonAjax.get(mainUrl, [], function () {
-                    newTab.location.replace(mainUrl); //不能后退
-                });
+                newTab.location.replace(mainUrl); //不能后退
             });
         }, timeoutTime);
     },

+ 219 - 138
web/building_saas/pm/js/pm_share.js

@@ -11,12 +11,12 @@
 const pmShare = (function () {
     const spreadDom = $('#shareSpread');
     let shareSeleted = null;
-    let spreadObj = {workBook: null, sheet: null};
+    let spreadObj = { workBook: null, sheet: null };
     let preSelection = null;
     //项目分享类型,由别人分享给自己的,和自己分享给别人的
-    const shareType = {receive: 'receive', shareTo: 'shareTo'};
+    const shareType = { receive: 'receive', shareTo: 'shareTo' };
     //操作类型
-    const oprType = {copy: 'copy', cancel: 'cancel'};
+    const oprType = { copy: 'copy', cancel: 'cancel' };
     let tree = null,
         actualIDShareInfo = {};//项目真实树ID与项目分享信息映射
     const treeCol = 0;
@@ -30,26 +30,26 @@ const pmShare = (function () {
         }
     };
     const headers = [
-        {name: '工程列表', dataCode: 'name', width: 300, rateWidth: 0.55, vAlign: 'center', hAlign: 'left'},
-        {name: '来自', dataCode: 'from', width: 80, rateWidth: 0.15, vAlign: 'center', hAlign: 'left'},
-        {name: '分享时间', dataCode: 'shareDate', width: 140, rateWidth: 0.15, vAlign: 'center', hAlign: 'left'},
-        {name: '总造价', dataCode: 'totalCost', width: 100, vAlign: 'center', hAlign: 'right', formatter: '0.00'},
-        {name: '项目类别', dataCode: 'valuationType', width: 100, vAlign: 'center', hAlign: 'left'},
+        { name: '工程列表', dataCode: 'name', width: 300, rateWidth: 0.55, vAlign: 'center', hAlign: 'left' },
+        { name: '来自', dataCode: 'from', width: 80, rateWidth: 0.15, vAlign: 'center', hAlign: 'left' },
+        { name: '分享时间', dataCode: 'shareDate', width: 140, rateWidth: 0.15, vAlign: 'center', hAlign: 'left' },
+        { name: '总造价', dataCode: 'totalCost', width: 100, vAlign: 'center', hAlign: 'right', formatter: '0.00' },
+        { name: '项目类别', dataCode: 'valuationType', width: 100, vAlign: 'center', hAlign: 'left' },
     ];
     const spreadOpts = {
         workBook: {
-            tabStripVisible:  false,
+            tabStripVisible: false,
             allowContextMenu: false,
-            allowCopyPasteExcelStyle : false,
+            allowCopyPasteExcelStyle: false,
             allowExtendPasteRange: false,
-            allowUserDragDrop : false,
+            allowUserDragDrop: false,
             allowUserDragFill: false,
-            scrollbarMaxAlign : true
+            scrollbarMaxAlign: true
         },
         sheet: {
             isProtected: true,
-            frozenlineColor : '#ababab',
-            protectionOptions: {allowResizeRows: true, allowResizeColumns: true},
+            frozenlineColor: '#ababab',
+            protectionOptions: { allowResizeRows: true, allowResizeColumns: true },
             clipBoardOptions: GC.Spread.Sheets.ClipboardPasteOptions.values
         }
     };
@@ -60,11 +60,11 @@ const pmShare = (function () {
     };
     //设置选中行底色
     //@param
-    function setSelStyle(sel, backColor,sheet) {
+    function setSelStyle(sel, backColor, sheet) {
         sel.row = sel.row === -1 ? 0 : sel.row;
         renderSheetFunc(sheet, function () {
             let style = projTreeObj.getSelStyle(backColor);
-            for(let i = 0; i < sel.rowCount; i++){
+            for (let i = 0; i < sel.rowCount; i++) {
                 let row = i + sel.row;
                 sheet.setStyle(row, -1, style);
             }
@@ -77,21 +77,21 @@ const pmShare = (function () {
         tree.selected = node;
         shareSeleted = node;
         //恢复底色
-        if(oldSel){
+        if (oldSel) {
             setSelStyle(oldSel, projTreeObj.setting.style.defalutBackColor, spreadObj.sheet);
         }
         //设置选中行底色
-        if(newSel){
+        if (newSel) {
             setSelStyle(newSel, projTreeObj.setting.style.selectedColor, spreadObj.sheet);
         }
         preSelection = newSel;
     }
     //渲染时方法,停止渲染
     //@param {Object}sheet {Function}func @return {void}
-    function renderSheetFunc(sheet, func){
+    function renderSheetFunc(sheet, func) {
         sheet.suspendEvent();
         sheet.suspendPaint();
-        if(func){
+        if (func) {
             func();
         }
         sheet.resumeEvent();
@@ -99,11 +99,11 @@ const pmShare = (function () {
     }
     //设置表选项
     //@param {Object}workBook {Object}opts @return {void}
-    function setSpreadOptions (workBook, opts) {
-        for(let opt in opts.workBook){
+    function setSpreadOptions(workBook, opts) {
+        for (let opt in opts.workBook) {
             workBook.options[opt] = opts.workBook[opt];
         }
-        for(let opt in opts.sheet){
+        for (let opt in opts.sheet) {
             workBook.getActiveSheet().options[opt] = opts.sheet[opt];
         }
     }
@@ -114,9 +114,9 @@ const pmShare = (function () {
             sheet.setColumnCount(headers.length);
             sheet.setRowHeight(0, 40, GC.Spread.Sheets.SheetArea.colHeader);
             //let spreadWidth = getWorkBookWidth();
-            for(let i = 0, len = headers.length; i < len; i++){
+            for (let i = 0, len = headers.length; i < len; i++) {
                 sheet.setColumnWidth(i, headers[i].width, GC.Spread.Sheets.SheetArea.colHeader);
-                if(headers[i].formatter){
+                if (headers[i].formatter) {
                     sheet.setFormatter(-1, i, headers[i].formatter);
                 }
                 sheet.setValue(0, i, headers[i].name, GC.Spread.Sheets.SheetArea.colHeader);
@@ -129,18 +129,18 @@ const pmShare = (function () {
     //表监听事件
     //@param {Object}workBook @return {void}
     function bindEvent(workBook, events) {
-        if(Object.keys(events).length === 0){
+        if (Object.keys(events).length === 0) {
             return;
         }
         const Events = GC.Spread.Sheets.Events;
-        for(let event in events){
+        for (let event in events) {
             workBook.bind(Events[event], events[event]);
         }
     }
     //建表
     //@return {void}
-    function buildSheet(){
-        spreadObj.workBook = new GC.Spread.Sheets.Workbook(spreadDom[0], {sheetCount: 1});
+    function buildSheet() {
+        spreadObj.workBook = new GC.Spread.Sheets.Workbook(spreadDom[0], { sheetCount: 1 });
         sheetCommonObj.spreadDefaultStyle(spreadObj.workBook);
         spreadObj.sheet = spreadObj.workBook.getActiveSheet();
         setSpreadOptions(spreadObj.workBook, spreadOpts);
@@ -178,7 +178,7 @@ const pmShare = (function () {
     }
     //此项目是否可以拷贝
     //@param {String}userID {Object}project @return {Boolean}
-    function isAllowCopy(userID, project){
+    function isAllowCopy(userID, project) {
         let myShareInfo = getShareInfo(userID, project);
         if (!myShareInfo) {
             return false;
@@ -196,7 +196,7 @@ const pmShare = (function () {
     }
     //获取树节点
     //@param {Object}tree @return {Object}
-    function getTreeNodeCell(tree){
+    function getTreeNodeCell(tree) {
         let indent = 20;
         let levelIndent = -5;
         let halfBoxLength = 5;
@@ -276,23 +276,23 @@ const pmShare = (function () {
                 if (centerX < x + w) {
                     drawLine(ctx, centerX, centerY, Math.min(x1, x + w), centerY, 'gray');
                     let img;
-                    if(node.data.projType === projectType.folder){
+                    if (node.data.projType === projectType.folder) {
                         img = document.getElementById('folder_open_pic');
                         imgWidth = 15;
                     }
-                    else if(node.data.projType === projectType.project){
+                    else if (node.data.projType === projectType.project) {
                         img = document.getElementById('proj_pic');
                         imgWidth = 18;
                     }
-                    else if(node.data.projType === projectType.engineering){
+                    else if (node.data.projType === projectType.engineering) {
                         img = document.getElementById('eng_pic');
                         imgWidth = 14;
                     }
-                    else if(node.data.projType === projectType.tender){
+                    else if (node.data.projType === projectType.tender) {
                         img = document.getElementById('tender_pic');
                         imgWidth = 14;
                     }
-                    ctx.drawImage(img, centerX+indent/2+3, centerY - 7, imgWidth,imgHeight);
+                    ctx.drawImage(img, centerX + indent / 2 + 3, centerY - 7, imgWidth, imgHeight);
                 }
                 // Draw Vertical Line
                 if (centerX < x + w) {
@@ -322,12 +322,12 @@ const pmShare = (function () {
                 }
             };
             // Draw Text
-            arguments[2] = x + (node.depth() + 1) * indent +  node.depth() * levelIndent + imgWidth + 3;
+            arguments[2] = x + (node.depth() + 1) * indent + node.depth() * levelIndent + imgWidth + 3;
             arguments[4] = w - (node.depth() + 1) * indent - node.depth() * levelIndent - imgWidth - 3;
             GC.Spread.Sheets.CellTypes.Text.prototype.paint.apply(this, arguments);
         };
         TreeNodeCellType.prototype.getHitInfo = function (x, y, cellStyle, cellRect, context) {
-            let info = {x: x, y: y, row: context.row, col: context.col, cellStyle: cellStyle, cellRect: cellRect, sheetArea: context.sheetArea};
+            let info = { x: x, y: y, row: context.row, col: context.col, cellStyle: cellStyle, cellRect: cellRect, sheetArea: context.sheetArea };
             let node = tree.items[info.row];
             if (node && node.data.projType === projectType.tender) {
                 info.isReservedLocation = true;
@@ -343,13 +343,13 @@ const pmShare = (function () {
             let value = hitinfo.sheet.getValue(hitinfo.row, hitinfo.col);
             let acStyle = hitinfo.sheet.getActualStyle(hitinfo.row, hitinfo.col),
                 zoom = hitinfo.sheet.zoom();
-            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, {sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport});
+            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, { sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport });
             //(图标+名字)区域
-            function withingClickArea(){
-                return hitinfo.x > centerX + halfBoxLength && hitinfo.x < centerX + halfBoxLength + imgWidth + indent/2+3 + textLength;
+            function withingClickArea() {
+                return hitinfo.x > centerX + halfBoxLength && hitinfo.x < centerX + halfBoxLength + imgWidth + indent / 2 + 3 + textLength;
             }
             //点击单位工程
-            if(node.data.projType === projectType.tender){
+            if (node.data.projType === projectType.tender) {
                 /*let newTab = window.open('about:blank');
                 //打开项目的实际ID
                 BeforeOpenProject(node.data.actualTreeInfo.ID, {'fullFolder': GetFullFolder(node.parent)}, function () {
@@ -405,11 +405,11 @@ const pmShare = (function () {
             let acStyle = hitinfo.sheet.getActualStyle(hitinfo.row, hitinfo.col),
                 zoom = hitinfo.sheet.zoom();
             let node = tree.items[hitinfo.row];
-            let nodeIndent = node ? (node.depth() + 1) * indent +  node.depth() * levelIndent + imgWidth + 3 : 0;
-            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, {sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport});
+            let nodeIndent = node ? (node.depth() + 1) * indent + node.depth() * levelIndent + imgWidth + 3 : 0;
+            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, { sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport });
             let cellWidth = hitinfo.sheet.getCell(-1, hitinfo.col).width();
-            if(textLength > cellWidth - nodeIndent){
-                TREE_SHEET_HELPER.showTipsDiv(text,{pos: {}},hitinfo);
+            if (textLength > cellWidth - nodeIndent) {
+                TREE_SHEET_HELPER.showTipsDiv(text, { pos: {} }, hitinfo);
             }
         };
         TreeNodeCellType.prototype.processMouseLeave = function (hitinfo) {
@@ -449,9 +449,9 @@ const pmShare = (function () {
                 let text = options.sheet.getText(options.row, options.col);
                 let acStyle = options.sheet.getActualStyle(options.row, options.col),
                     zoom = options.sheet.zoom();
-                let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, {sheet: options.sheet, row: options.row, col: options.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport});
+                let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, { sheet: options.sheet, row: options.row, col: options.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport });
                 const imgIndent = 5;
-                let nowX  = Math.floor(x) + textLength + imgIndent,
+                let nowX = Math.floor(x) + textLength + imgIndent,
                     nowY = Math.floor((y + (y + h)) / 2) - 7;
                 if (node.data.allowCooperate) {
                     ctx.drawImage(editImg, nowX, nowY, editImgWidth, editImgHeight);
@@ -482,11 +482,11 @@ const pmShare = (function () {
             let value = hitinfo.sheet.getValue(hitinfo.row, hitinfo.col);
             let acStyle = hitinfo.sheet.getActualStyle(hitinfo.row, hitinfo.col),
                 zoom = hitinfo.sheet.zoom();
-            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, {sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport});
-            if(hitinfo.x - hitinfo.cellRect.x > 0 && hitinfo.x - hitinfo.cellRect.x < textLength){
+            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, { sheet: hitinfo.sheet, row: hitinfo.row, col: hitinfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport });
+            if (hitinfo.x - hitinfo.cellRect.x > 0 && hitinfo.x - hitinfo.cellRect.x < textLength) {
                 //由..分享,弹出分享者信息
-                if(dataCode === 'from'){
-                    if(node.data.shareType === shareType.receive){
+                if (dataCode === 'from') {
+                    if (node.data.shareType === shareType.receive) {
                         $('#userinfo').find('h4').text(node.data.userInfo.name);
                         $('#userinfo').find('h6').text(node.data.userInfo.company);
                         let mobileHtml = `<i class="fa fa-tablet"> ${node.data.userInfo.mobile ? node.data.userInfo.mobile : ''}</i>`;
@@ -510,38 +510,73 @@ const pmShare = (function () {
             let value = hitInfo.sheet.getValue(hitInfo.row, hitInfo.col);
             let acStyle = hitInfo.sheet.getActualStyle(hitInfo.row, hitInfo.col),
                 zoom = hitInfo.sheet.zoom();
-            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, {sheet: hitInfo.sheet, row: hitInfo.row, col: hitInfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport});
-            if (sheet && hitInfo.x - hitInfo.cellRect.x >  0 && hitInfo.x - hitInfo.cellRect.x < textLength) {
-                canvas.style.cursor='pointer';
+            let textLength = this.getAutoFitWidth(value, text, acStyle, zoom, { sheet: hitInfo.sheet, row: hitInfo.row, col: hitInfo.col, sheetArea: GC.Spread.Sheets.SheetArea.viewport });
+            if (sheet && hitInfo.x - hitInfo.cellRect.x > 0 && hitInfo.x - hitInfo.cellRect.x < textLength) {
+                canvas.style.cursor = 'pointer';
                 return true;
-            }else{
-                canvas.style.cursor='default';
+            } else {
+                canvas.style.cursor = 'default';
             }
             return false;
         };
         return new InteractionCell();
     }
 
+    function isUnread(unreadList, node) {
+        if (!unreadList) {
+            return false;
+        }
+        const actualID = node && node.data && node.data.actualTreeInfo && node.data.actualTreeInfo.ID || null;
+        if (!actualID) {
+            return false;
+        }
+        return !!unreadList.find(ID => ID === actualID);
+    }
     const foreColor = '#007bff';
-    const cancelForeColor = 'red';
+    const dangerColor = '#dc3545';
+    // 标记已读
+    // 打开任意的单位工程,都会将其父分享条目标记和自身为已读 
+    // eg:被分享了一个建设项目,和其下一个单位工程,打开该一个单位工程,建设项目标记为已读、该单位工程也标记为已读
+    function handleMarkRead(unreadList, markReadProjectIDs) {
+        const col = headers.findIndex(item => item.dataCode === 'shareDate');
+        const sheet = spreadObj.workBook.getActiveSheet();
+        const style = new GC.Spread.Sheets.Style();
+        projTreeObj.renderSheetFuc(sheet, function () {
+            markReadProjectIDs.forEach(projectID => {
+                SHARE_TO.removeUnread(projectID, unreadList);
+                tree.items.forEach(node => {
+                    if (node.data && node.data.actualTreeInfo && node.data.actualTreeInfo.ID === projectID) {
+                        const row = node.serialNo();
+                        sheet.setStyle(row, col, style);
+                    }
+                });
+            });
+        });
+    }
     //显示树结构数据
     //@param {Array}nodes {Array}headers @return {void}
-    function showTreeData(nodes, headers){
+    function showTreeData(nodes, headers) {
         let sheet = spreadObj.workBook.getActiveSheet();
-        let fuc = function(){
+        let fuc = function () {
             sheet.setRowCount(nodes.length);
-            for(let i = 0; i < nodes.length; i++){
+            for (let i = 0; i < nodes.length; i++) {
                 let treeNodeCell = getTreeNodeCell(tree);
                 sheet.getCell(i, treeCol).cellType(treeNodeCell);
-                for(let j = 0; j < headers.length; j++){
+                for (let j = 0; j < headers.length; j++) {
                     sheet.getRange(-1, j, -1, 1).hAlign(GC.Spread.Sheets.HorizontalAlign[headers[j]['hAlign']]);
                     sheet.getRange(-1, j, -1, 1).vAlign(GC.Spread.Sheets.VerticalAlign[headers[j]['vAlign']]);
                     let dataCode = headers[j].dataCode;
-                    if(dataCode === 'from'){
+                    if (dataCode === 'from') {
                         let style = new GC.Spread.Sheets.Style();
                         style.foreColor = foreColor;
                         sheet.setStyle(i, j, style);
                         sheet.getCell(i, j).cellType(getInteractionCell());
+                    } else if (dataCode === 'shareDate') {
+                        let style = new GC.Spread.Sheets.Style();
+                        if (isUnread(unreadShareList, nodes[i])) {
+                            style.foreColor = dangerColor;
+                        }
+                        sheet.setStyle(i, j, style);
                     }
                     sheet.setValue(i, j, nodes[i].data[dataCode] !== null && typeof nodes[i].data[dataCode] !== 'undefined' ? nodes[i].data[dataCode] : '');
                 }
@@ -549,6 +584,10 @@ const pmShare = (function () {
         };
         renderSheetFunc(sheet, fuc);
     }
+    // 获取实际树ID为某值的所有节点
+    function getNodesByActualID(ID, items) {
+        return items.filter(node => node.data && node.data.actualTreeInfo && node.data.actualTreeInfo.ID === ID);
+    }
     //同一棵树,可能存在相同数据显示多条的问题(传入的datas中不存在相同数据)
     //将真实树结构数据存在actualTreeInfo中,外部树结构数据用uuid重置。
     //@param {Array}datas
@@ -557,12 +596,12 @@ const pmShare = (function () {
         for (let data of datas) {
             //项目真实ID与项目分享信息映射,方便确定项目的权限
             if (!actualIDShareInfo[data.ID]) {
-                actualIDShareInfo[data.ID] = {ID: data.ID, ParentID: data.ParentID, NextSiblingID: data.NextSiblingID, shareInfo: data.shareInfo};
+                actualIDShareInfo[data.ID] = { ID: data.ID, ParentID: data.ParentID, NextSiblingID: data.NextSiblingID, shareInfo: data.shareInfo };
             }
             IDMapping[data.ID] = uuid.v1();
         }
         for (let data of datas) {
-            data.actualTreeInfo = {ID: data.ID, ParentID: data.ParentID, NextSiblingID: data.NextSiblingID};
+            data.actualTreeInfo = { ID: data.ID, ParentID: data.ParentID, NextSiblingID: data.NextSiblingID };
             data.ID = IDMapping[data.ID];
             data.NextSiblingID = IDMapping[data.NextSiblingID] ? IDMapping[data.NextSiblingID] : -1;
             data.ParentID = IDMapping[data.ParentID] ? IDMapping[data.ParentID] : -1;
@@ -584,7 +623,7 @@ const pmShare = (function () {
         for (let data of datas) {
             if (data.userInfo) {
                 //shareInfo中我的条目
-                let selfInfo = _.find(data.shareInfo, {userID: userID});
+                let selfInfo = _.find(data.shareInfo, { userID: userID });
                 data.shareDate = selfInfo ? selfInfo.shareDate : ''
                 data.from = data.userInfo.name;
                 data.to = '分享给 我';
@@ -605,9 +644,9 @@ const pmShare = (function () {
                 allDatas = allDatas.concat(data.children);
             }
         }
-        for(let proj of allDatas){
+        for (let proj of allDatas) {
             let summaryProj = summaryInfo[proj.ID];
-            if(summaryProj){
+            if (summaryProj) {
                 proj.totalCost = summaryProj.totalCost;
             }
         }
@@ -619,11 +658,11 @@ const pmShare = (function () {
         //建立ID索引
         for (let data of treeDatas) {
             //新建一个简单对象,防止污染treeDatas的数据
-            treeMapping[data.ID] = {ID: data.ID, prev: null, next: null};
+            treeMapping[data.ID] = { ID: data.ID, prev: null, next: null };
         }
         //绑定prev next
         for (let data of treeDatas) {
-            let me =  treeMapping[data.ID],
+            let me = treeMapping[data.ID],
                 next = treeMapping[data.NextSiblingID];
             if (next) {
                 me.next = next;
@@ -634,17 +673,17 @@ const pmShare = (function () {
         let result = _.find(treeDatas, function (data) {
             return !treeMapping[data.ID].prev
         });
-        return  result ? result.ID : -1;
+        return result ? result.ID : -1;
     }
     //获取可成树的数据
     //@param {Array}datas @return {Array}
-    function getTreeDatas(groupedDatas, ungroupedDatas){
+    function getTreeDatas(groupedDatas, ungroupedDatas) {
         //设置新的树结构数据
         for (let data of groupedDatas) {
             setTreeInfo([data].concat(data.children));
         }
         //未分类分段
-        let tenders = _.filter(ungroupedDatas, {projType: projectType.tender});
+        let tenders = _.filter(ungroupedDatas, { projType: projectType.tender });
         setTreeInfo(tenders);
         let rst = [];
         //整理树结构
@@ -652,7 +691,7 @@ const pmShare = (function () {
         //第一个根节点数据
         let firstID = getFirstID(groupedDatas);
         //新建未分类建设项目及单项工程
-        let ungroupedProj = {ID: uuid.v1(), ParentID: -1, NextSiblingID: firstID, name: '未分类建设项目', projType: projectType.project};
+        let ungroupedProj = { ID: uuid.v1(), ParentID: -1, NextSiblingID: firstID, name: '未分类建设项目', projType: projectType.project };
         /*if (groupedDatas.length > 0) {
             groupedDatas[groupedDatas.length - 1].NextSiblingID = ungroupedProj.ID;
         }*/
@@ -672,10 +711,10 @@ const pmShare = (function () {
     }
     //按照时间排序
     //@param {Array}datas @return {void}
-    function sortByDate(datas){
+    function sortByDate(datas) {
         datas.sort(function (a, b) {
-            let shareInfoA = _.find(a.shareInfo, {userID}),
-                shareInfoB = _.find(b.shareInfo, {userID});
+            let shareInfoA = _.find(a.shareInfo, { userID }),
+                shareInfoB = _.find(b.shareInfo, { userID });
             let aV = shareInfoA ? Date.parse(shareInfoA.shareDate) : 0,
                 bV = shareInfoB ? Date.parse(shareInfoB.shareDate) : 0;
             //时间越晚越靠前
@@ -698,14 +737,14 @@ const pmShare = (function () {
                 data.allowCooperate = isAllowCoop(userID, data);
             }
         }
-        
+
     }
     //建立树
     //@return void
-    function initShareTree(){
+    function initShareTree() {
         $.bootstrapLoading.start();
         //获取分享数据
-        CommonAjax.post('/pm/api/receiveProjects', {user_id: userID}, function (rstData) {
+        CommonAjax.post('/pm/api/receiveProjects', { user_id: userID }, function (rstData) {
             // 排序 --分享的文件按照时间先后顺序排序,分享文件下的子文件,按照原本树结构显示,不需要排序
             sortByDate(rstData.grouped);
             sortByDate(rstData.ungrouped);
@@ -720,7 +759,7 @@ const pmShare = (function () {
             tree.selected = tree.items[0];
             showTreeData(tree.items, headers);
             //初始选择
-            let initSel = spreadObj.sheet.getSelections()[0] ? spreadObj.sheet.getSelections()[0] : {row: 0, rowCount: 1};
+            let initSel = spreadObj.sheet.getSelections()[0] ? spreadObj.sheet.getSelections()[0] : { row: 0, rowCount: 1 };
             initSelection(initSel);
             autoFlashHeight();
             spreadObj.sheet.frozenColumnCount(4);
@@ -734,7 +773,7 @@ const pmShare = (function () {
             selector: '#shareSpread',
             build: function ($trigger, e) {
                 let target = SheetDataHelper.safeRightClickSelection($trigger, e, spreadObj.workBook);
-                initSelection({row: target.row, rowCount: 1}, preSelection ? preSelection : null, spreadObj.sheet);
+                initSelection({ row: target.row, rowCount: 1 }, preSelection ? preSelection : null, spreadObj.sheet);
                 return target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader;
             },
             items: {
@@ -768,11 +807,11 @@ const pmShare = (function () {
     }
     //初始化视图
     //@return void
-    function initView(){
-        if(tree){
+    function initView() {
+        if (tree) {
             tree = null;
         }
-        if(spreadObj.workBook){
+        if (spreadObj.workBook) {
             spreadObj.workBook.destroy();
             spreadObj.workBook = null;
         }
@@ -782,11 +821,11 @@ const pmShare = (function () {
     }
     //根据建设项目获取单项工程
     //@param {Number}projID @return {void}
-    function setEng(projID){
-        let engQuery = {$or: [{deleteInfo: null}, {'deleteInfo.deleted': false}], projType: projectType.engineering, userID: userID, ParentID: projID};
-        CommonAjax.post('/pm/api/getProjectsByQuery', {user_id: userID, query: engQuery, options: '-_id -property'}, function (rstData) {
+    function setEng(projID) {
+        let engQuery = { $or: [{ deleteInfo: null }, { 'deleteInfo.deleted': false }], projType: projectType.engineering, userID: userID, ParentID: projID };
+        CommonAjax.post('/pm/api/getProjectsByQuery', { user_id: userID, query: engQuery, options: '-_id -property' }, function (rstData) {
             $('#copyShare_selectEng').empty();
-            for(let eng of rstData){
+            for (let eng of rstData) {
                 let opt = $('<option>').val(eng.ID).text(eng.name);
                 $('#copyShare_selectEng').append(opt);
             }
@@ -794,15 +833,15 @@ const pmShare = (function () {
     }
     //从其他建设项目中复制中,建设项目的文件层次结构名称和顺序
     //@param {Array}treeData @return {Array}
-    function getFileHierarchyInfo(treeData){
-        let tree = idTree.createNew({id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1});
+    function getFileHierarchyInfo(treeData) {
+        let tree = idTree.createNew({ id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1 });
         tree.loadDatas(treeData);
         let items = tree.items;
         let rst = [];
-        function getFileHierarchyName(node){
+        function getFileHierarchyName(node) {
             let nodeName = node.data.name;
             let name = [];
-            while (node.parent){
+            while (node.parent) {
                 name.push(node.parent.data.name ? node.parent.data.name : '');
                 node = node.parent;
             }
@@ -810,22 +849,22 @@ const pmShare = (function () {
             name.push(nodeName);
             return name.join('\\');
         }
-        for(let node of items){
-            if(node.children.length === 0 ){//project
-                rst.push({ID: node.data.ID, fileHierarchyName: getFileHierarchyName(node)})
+        for (let node of items) {
+            if (node.children.length === 0) {//project
+                rst.push({ ID: node.data.ID, fileHierarchyName: getFileHierarchyName(node) })
             }
         }
         return rst;
     }
     //设置拷贝工程下拉选择
     //@return {void}
-    function setCopyModal(){
+    function setCopyModal() {
         //获取建设项目
-        let projQuery = {$or: [{deleteInfo: null}, {'deleteInfo.deleted': false}], projType: {$in: [projectType.project, projectType.folder]}, userID: userID};
-        CommonAjax.post('/pm/api/getProjectsByQuery', {user_id: userID, query: projQuery, options: '-_id -property'}, function (rstData) {
+        let projQuery = { $or: [{ deleteInfo: null }, { 'deleteInfo.deleted': false }], projType: { $in: [projectType.project, projectType.folder] }, userID: userID };
+        CommonAjax.post('/pm/api/getProjectsByQuery', { user_id: userID, query: projQuery, options: '-_id -property' }, function (rstData) {
             let fileHierarchyData = getFileHierarchyInfo(rstData);
             $('#copyShare_selectProj').empty();
-            for(let proj of fileHierarchyData){
+            for (let proj of fileHierarchyData) {
                 let opt = $('<option>').val(proj.ID).text(proj.fileHierarchyName);
                 $('#copyShare_selectProj').append(opt);
             }
@@ -901,21 +940,77 @@ const pmShare = (function () {
         }
         return `${orgName} (${userInfo.name}分享拷贝)`;
     }
+    //清除了该节点后,可能还有该节点的数据在树上(树允许有重复数据),需要更新分享信息
+    function updateAfterCancel(userID, projectID) {
+        for (let item of tree.items) {
+            if (item.data.actualTreeInfo && item.data.actualTreeInfo.ID === projectID) {
+                _.remove(item.data.shareInfo, function (data) {
+                    return data.userID === userID;
+                });
+            }
+        }
+    }
+    // 处理节点操作属性变更(是否可拷贝、是否可编辑)
+    function handlePropChange(projectID, prop) {
+        const actualShareData = actualIDShareInfo[projectID];
+        if (actualShareData) {
+            const shareItem = actualShareData.shareInfo.find(s => s.userID === userID);
+            if (shareItem) {
+                Object.assign(shareItem, prop);
+            }
+        }
+        const nodes = getNodesByActualID(projectID, tree.items);
+        nodes.forEach(node => {
+            const shareItem = node.data.shareInfo.find(s => s.userID === userID);
+            if (shareItem) {
+                Object.assign(shareItem, prop);
+            }
+        });
+        const treeData = tree.items.map(item => item.data);
+        setPermissionsInfo(treeData);
+        showTreeData(tree.items, headers);
+    }
+    function handleCancelShare(cancelProjID) {
+        const node = tree.items.find(item => item.data.actualTreeInfo && item.data.actualTreeInfo.ID === cancelProjID);
+        if (node) {
+            tree.removeNode(node);
+        }
+        //更新与清除节点数据相同,且未被清除缓存分享信息
+        updateAfterCancel(userID, cancelProjID);
+        //重新设置actualIDShareInfo,以正确更新权限(清除了分享信息后,可能会导致权限变化 eg:清除了新的分享,则存留的分享项目采用旧的)
+        actualIDShareInfo = {};
+        let treeDatas = [];
+        for (let item of tree.items) {
+            treeDatas.push(item.data);
+            let actualTreeInfo = item.data.actualTreeInfo;
+            if (actualTreeInfo && !actualIDShareInfo[actualTreeInfo.ID]) {
+                actualIDShareInfo[actualTreeInfo.ID] = {
+                    ID: actualTreeInfo.ID,
+                    ParentID: actualTreeInfo.ParentID,
+                    NextSiblingID: actualTreeInfo.NextSiblingID,
+                    shareInfo: item.data.shareInfo
+                };
+            }
+        }
+        //重新设置权限
+        setPermissionsInfo(treeDatas);
+        showTreeData(tree.items, headers);
+    }
     //事件监听器
     //@return void
-    function eventListener(){
+    function eventListener() {
         //tab
         $('#tab_pm_share').on('shown.bs.tab', function () {
             //侧滑隐藏
             $('.slide-sidebar').removeClass('open');
             $('.slide-sidebar').css('width', '0');
             projTreeObj.tree = null;
-            if(projTreeObj.workBook){
+            if (projTreeObj.workBook) {
                 projTreeObj.workBook.destroy();
                 projTreeObj.workBook = null;
             }
             gcTreeObj.tree = null;
-            if(gcTreeObj.workBook){
+            if (gcTreeObj.workBook) {
                 gcTreeObj.workBook.destroy();
                 gcTreeObj.workBook = null;
             }
@@ -955,48 +1050,25 @@ const pmShare = (function () {
         //确认拷贝
         $('#copyShare_confirm').click(function () {
             let selProj = $('#copyShare_selectProj').select().val();
-            if(!selProj){
+            if (!selProj) {
                 $('#copyShareProj-info').show();
                 return;
             }
             copyShareProject(tree.selected, parseInt(selProj));
         });
         //清除分享
-        //清除了该节点后,可能还有该节点的数据在树上(树允许有重复数据),需要更新分享信息
-        function updateAfterCancel(userID, projectID) {
-            for (let item of tree.items) {
-                if (item.data.actualTreeInfo && item.data.actualTreeInfo.ID === projectID) {
-                    _.remove(item.data.shareInfo, function (data) {
-                        return data.userID === userID;
-                    });
-                }
-            }
-        }
         $('#cancelShareConfirm').click(function () {
             $.bootstrapLoading.start();
-            let cancelProjID = tree.selected.data.actualTreeInfo.ID;
-            CommonAjax.post('/pm/api/share', {user_id: userID, type: oprType.cancel,  projectID: cancelProjID, shareData:[{userID: userID}]}, function (rstData) {
-                tree.removeNode(tree.selected);
-                //更新与清除节点数据相同,且为被清除缓存分享信息
-                updateAfterCancel(userID, cancelProjID);
-                //重新设置actualIDShareInfo,以正确更新权限(清除了分享信息后,可能会导致权限变化 eg:清除了新的分享,则存留的分享项目采用旧的)
-                actualIDShareInfo = {};
-                let treeDatas = [];
-                for (let item of tree.items) {
-                    treeDatas.push(item.data);
-                    let actualTreeInfo = item.data.actualTreeInfo;
-                    if (actualTreeInfo && !actualIDShareInfo[actualTreeInfo.ID]) {
-                        actualIDShareInfo[actualTreeInfo.ID] = {
-                            ID: actualTreeInfo.ID,
-                            ParentID: actualTreeInfo.ParentID,
-                            NextSiblingID: actualTreeInfo.NextSiblingID,
-                            shareInfo: item.data.shareInfo
-                        };
-                    }
-                }
-                //重新设置权限
-                setPermissionsInfo(treeDatas);
-                showTreeData(tree.items, headers);
+            const cancelProjID = tree.selected.data.actualTreeInfo.ID;
+            const permissionType = commonConstants.SharePermissionChangeType.CANCEL;
+            CommonAjax.post('/pm/api/share', { user_id: userID, type: oprType.cancel, permissionType, projectID: cancelProjID, shareData: [{ userID: userID }] }, function (rstData) {
+                handleCancelShare(cancelProjID);
+
+                // 推送已打开的项目,通知已取消分享
+                SHARE_TO.emitPermissionChange(permissionType, userID, cancelProjID, rstData.emitTenders);
+                // 清除已读
+                SHARE_TO.removeUnread(cancelProjID, unreadShareList);
+
                 $.bootstrapLoading.end();
             }, function () {
                 $.bootstrapLoading.end();
@@ -1004,7 +1076,16 @@ const pmShare = (function () {
         });
     }
 
-    return {spreadObj, headers, initView, eventListener}
+    return {
+        spreadObj,
+        headers,
+        initView,
+        eventListener,
+        initShareTree,
+        handlePropChange,
+        handleCancelShare,
+        handleMarkRead
+    }
 })();
 
 $(document).ready(function () {

+ 17 - 29
web/building_saas/report/js/rpt_main.js

@@ -1021,11 +1021,20 @@ let rptControlObj = {
             }
         }
     },
+    _hidePdfIniPage: function () {
+        if ($('#ini_PDF_cover').is(':visible')) {
+            $("#ini_PDF_Btn_Cancel").trigger("click");
+        }
+    },
     getPdfFontCallback: function(fontProperty) {
         let me = rptControlObj;
         rptTplObj.pdfFont['SmartSimsun'].push(fontProperty);
         if (rptTplObj.pdfFont['SmartSimsun'].length === 2) {
-            me.getPDFEx();
+            me._hidePdfIniPage();
+            setTimeout(function(){
+                me.getPDFEx();
+            }, 20);
+            // me.getPDFEx();
         }
     },
     getPDFPre: function () {
@@ -1064,10 +1073,7 @@ let rptControlObj = {
                 CommonAjax.postEx("report_api/getMultiReports", params, WAIT_TIME_EXPORT, true,
                     function(result){
                         $.bootstrapLoading.end();
-                        if ($('#ini_PDF_cover').is(':visible')) {
-                            $("#ini_PDF_Btn_Cancel").trigger("click");
-                            // $("#ini_PDF_cover").remove();
-                        }
+                        me._hidePdfIniPage();
                         let pageSize = rptControlObj.getCurrentPageSize();
                         for (let idx = 0; idx < result.length; idx++) {
                             let pageData = result[idx];
@@ -1076,18 +1082,12 @@ let rptControlObj = {
                     },
                     function(failRst){
                         $.bootstrapLoading.end();
-                        if ($('#ini_PDF_cover').is(':visible')) {
-                            $("#ini_PDF_Btn_Cancel").trigger("click");
-                            // $("#ini_PDF_cover").remove();
-                        }
+                        me._hidePdfIniPage();
                         console.log(failRst);
                     },
                     function(exceptionRst){
                         $.bootstrapLoading.end();
-                        if ($('#ini_PDF_cover').is(':visible')) {
-                            $("#ini_PDF_Btn_Cancel").trigger("click");
-                            // $("#ini_PDF_cover").remove();
-                        }
+                        me._hidePdfIniPage();
                         console.log(exceptionRst);
                     }
                 );
@@ -1112,10 +1112,7 @@ let rptControlObj = {
                         CommonAjax.postEx("report_api/getMultiReports", params, WAIT_TIME_EXPORT, true,
                             function(result){
                                 $.bootstrapLoading.end();
-                                if ($('#ini_PDF_cover').is(':visible')) {
-                                    $("#ini_PDF_Btn_Cancel").trigger("click");
-                                    // $("#ini_PDF_cover").remove();
-                                }
+                                me._hidePdfIniPage();
                                 let pageSize = rptControlObj.getCurrentPageSize();
                                 for (let idx = 0; idx < result.length; idx++) {
                                     let pageData = result[idx];
@@ -1124,18 +1121,12 @@ let rptControlObj = {
                             },
                             function(failRst){
                                 $.bootstrapLoading.end();
-                                if ($('#ini_PDF_cover').is(':visible')) {
-                                    $("#ini_PDF_Btn_Cancel").trigger("click");
-                                    // $("#ini_PDF_cover").remove();
-                                }
+                                me._hidePdfIniPage();
                                 console.log(failRst);
                             },
                             function(exceptionRst){
                                 $.bootstrapLoading.end();
-                                if ($('#ini_PDF_cover').is(':visible')) {
-                                    $("#ini_PDF_Btn_Cancel").trigger("click");
-                                    // $("#ini_PDF_cover").remove();
-                                }
+                                me._hidePdfIniPage();
                                 console.log(exceptionRst);
                             }
                         );
@@ -1144,10 +1135,7 @@ let rptControlObj = {
                         let pageSize = rptControlObj.getCurrentPageSize();
                         let pageData = zTreeOprObj.currentRptPageRst;
                         $.bootstrapLoading.end();
-                        if ($('#ini_PDF_cover').is(':visible')) {
-                            $("#ini_PDF_Btn_Cancel").trigger("click");
-                            // $("#ini_PDF_cover").remove();
-                        }
+                        me._hidePdfIniPage();
                         JpcJsPDFHelper.outputAsPdf(pageData, pageSize, rpt_names[0]);
                     }
                 }

+ 32 - 0
web/common/components/blank/index.html

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>大司空云计价</title>
+    <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/web/building_saas/css/main.css">
+    <link rel="shortcut icon" href="/web/building_saas/css/favicon.ico">
+    <link rel="icon" type="image/gif" href="/web/building_saas/css/animated_favicon1.gif">
+</head>
+
+<body>
+    <div class="header">
+        <nav class="navbar navbar-expand-lg p-0 d-flex">
+            <div class="header-logo">
+            </div>
+        </nav>
+    </div>
+    <div class="container mt-3">
+        <%- html %>
+    </div>
+    <!-- JS. -->
+    <script src="/lib/jquery/jquery-3.2.1.min.js"></script>
+    <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>
+</body>
+
+</html>

+ 168 - 4
web/common/components/share/index.js

@@ -1,3 +1,4 @@
+
 const SHARE_TO = (() => {
 
     const ShareType = {
@@ -6,6 +7,8 @@ const SHARE_TO = (() => {
         CANCEL: 'cancel',
     };
 
+    const { SharePermissionChangeType: PermissionType, PageTarget } = commonConstants;
+
     // 当前分享的项目ID
     let curProjectID;
 
@@ -224,20 +227,23 @@ const SHARE_TO = (() => {
             let shareData;
             let type = ShareType.UPDATE;
             if (shareType === ShareType.CREATE) {
-                const allowCopy = !!$('#allow-copy').attr('checked');
-                const allowCooperate = !!$('#allow-edit').attr('checked');
+                const allowCopy = $('#allow-copy').prop('checked');
+                const allowCooperate = $('#allow-edit').prop('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');
+                const allowCopy = $(`[data-user=${receiver}].allow-copy`).prop('checked');
+                const allowCooperate = $(`[data-user=${receiver}].allow-edit`).prop('checked');
                 shareData = [{ userID: receiver, allowCopy, allowCooperate }];
             } else {
                 shareData = [{ userID: receiver, isCancel: true }];
             }
+            // 获取权限变更的类型
+            const permissionType = getPermissionType(shareType, curSharedUsers, shareData[0]);
             const postData = {
                 user_id: userID,
                 type,
+                permissionType,
                 projectID: curProjectID,
                 count: rencentCount,
                 shareData
@@ -263,6 +269,18 @@ const SHARE_TO = (() => {
                 initSharedView(curSharedUsers);
                 refreshShareTip(curSharedUsers);
                 refreshTreeView();
+            } else {
+                const matchItem = curSharedUsers.find(item => item._id === receiver);
+                if (matchItem) {
+                    matchItem.allowCopy = shareData[0].allowCopy;
+                    matchItem.allowCooperate = shareData[0].allowCooperate;
+                }
+            }
+            if (permissionType !== null) {
+                const payload = [PermissionType.UPDATE_COOPERATE, PermissionType.UPDATE_COPY].includes(permissionType)
+                    ? { prop: { allowCopy: shareData[0].allowCopy, allowCooperate: shareData[0].allowCooperate } }
+                    : null;
+                emitPermissionChange(permissionType, receiver, curProjectID, rst.emitTenders, payload);
             }
         } catch (err) {
             console.log(err);
@@ -273,6 +291,147 @@ const SHARE_TO = (() => {
         }
     }
 
+    // 获取权限变更的类型
+    function getPermissionType(shareType, cache, curShareData) {
+        if (shareType === ShareType.CANCEL) {
+            return PermissionType.CANCEL;
+        }
+        if (shareType === ShareType.CREATE) {
+            return PermissionType.SHARE;
+        }
+        if (!cache) {
+            return null;
+        }
+        const match = cache.find(item => item._id === curShareData.userID);
+        if (!match) {
+            return null;
+        }
+        if (match.allowCooperate !== curShareData.allowCooperate) {
+            return PermissionType.UPDATE_COOPERATE;
+        }
+        if (match.allowCopy !== curShareData.allowCopy) {
+            return PermissionType.UPDATE_COPY;
+        }
+        return null;
+    }
+
+    // 权限变更消息推送
+    function emitPermissionChange(permissionType, userID, projectID, emitTenders, payload) {
+        const compilationID = typeof projectObj !== 'undefined' ? projectObj.project.projectInfo.compilation : compilationData._id;
+        socket.emit('sharePermissionChange', { permissionType, userID, compilationID, projectID, emitTenders, payload });
+    }
+
+    // 权限变更处理监听
+    function permissionChangeListener() {
+        socket.on('sharePermissionChange', ({ permissionType, target, payload }) => {
+            const type = `${permissionType}-${target}`;
+            switch (type) {
+                case `${PermissionType.READ}-${PageTarget.PM}`:
+                    handleShareItemRead(payload);
+                    break;
+                case `${PermissionType.UPDATE_COOPERATE}-${PageTarget.PM}`:
+                case `${PermissionType.UPDATE_COPY}-${PageTarget.PM}`:
+                    handlePMPropChange(payload);
+                    break;
+                case `${PermissionType.UPDATE_COOPERATE}-${PageTarget.MAIN}`:
+                    handleMainCooperateChange(payload);
+                    break;
+                case `${PermissionType.CANCEL}-${PageTarget.PM}`:
+                    handlePMCancelPermission(payload);
+                    break;
+                case `${PermissionType.CANCEL}-${PageTarget.MAIN}`:
+                    handleMainCancelPermission();
+                    break;
+                case `${PermissionType.SHARE}-${PageTarget.PM}`:
+                    handlePMSharePermission(payload);
+                    break;
+            }
+        });
+    }
+
+    // 项目管理页面分享权限变更后相关处理函数
+
+    // 已读分享项目
+    function handleShareItemRead({ markReadProjectIDs }) {
+        const isActive = $('#tab_pm_share').hasClass('active');
+        if (isActive) {
+            pmShare.handleMarkRead(unreadShareList, markReadProjectIDs);
+        } else {
+            markReadProjectIDs.forEach(projectID => {
+                removeUnread(projectID, unreadShareList);
+            });
+        }
+    }
+    // 分享条数属性变更(是否可拷贝、是否可编辑)
+    // @param {Object} prop - 属性变更对象 eg: { allowCopy: false }
+    function handlePMPropChange({ projectID, prop }) {
+        // 如果当前在项目管理的分享页面,需要刷新相关节点
+        const isActive = $('#tab_pm_share').hasClass('active');
+        if (isActive) {
+            pmShare.handlePropChange(projectID, prop);
+        }
+    }
+
+    // 已读某未读分享项目
+    function removeUnread(projectID, list) {
+        const idx = list.indexOf(projectID);
+        if (idx >= 0) {
+            list.splice(idx, 1);
+            refreshUnreadCount(list.length);
+        }
+    }
+
+    // 取消分享后项目管理页面接收到推送后到操作
+    function handlePMCancelPermission({ projectID }) {
+        removeUnread(projectID, unreadShareList);
+        // 如果当前停留在分享标签界面,清楚分享项目
+        const isActive = $('#tab_pm_share').hasClass('active');
+        if (isActive) {
+            pmShare.handleCancelShare(projectID);
+        }
+    }
+    // 通知信息点击事件
+    function notify() {
+        pmShare.initShareTree();
+        $("#notify").hide();
+    }
+    // 新增分享后项目管理页面接收到推送后的操作
+    function handlePMSharePermission({ projectID }) {
+        unreadShareList.push(projectID);
+        unreadShareList = [...new Set(unreadShareList)];
+        refreshUnreadCount(unreadShareList.length);
+        // 如果当前停留在分享标签界面,需要弹出提示
+        const isActive = $('#tab_pm_share').hasClass('active');
+        if (isActive) {
+            $("#message").html('您有新的分享项目,<a href="javascript:void(0);" id="load-data" onclick="SHARE_TO.notify()">点击刷新列表</a>');
+            $("#notify").show();
+        }
+    }
+
+    // 刷新未读分享标记
+    function refreshUnreadCount(count) {
+        if (count) {
+            $('#unread-share-count').text(count);
+            $('#unread-share-count').show();
+        } else {
+            $('#unread-share-count').hide();
+        }
+    }
+
+
+    
+    // 主页面的分享权限变更后相关处理函数
+    function handleMainCooperateChange({ allowCooperate }) {
+        if (projectObj.project.projectInfo.shareState && projectObj.project.projectInfo.shareState.allowCooperate !== allowCooperate) {
+            setLocalCache(commonConstants.StorageKey.ONCE_MAIN_LOADED, '分享设置已被修改,当前项目已自动刷新。');
+            window.location.reload();
+        }
+    }
+    function handleMainCancelPermission() {
+        // 定位到空白页
+        window.location.replace(`/blank?type=${commonConstants.BlankType.SHARE_CANCEL}`);
+    }
+
     // 刷新项目管理树视图
     // 如果是在项目管理页面,需要刷新树(分享图标可能需要清除)
     function refreshTreeView() {
@@ -424,6 +583,7 @@ const SHARE_TO = (() => {
         }
         $('#share-phone').on('keyup', function () {
             delayKeyup(function () {
+                console.log(curSharedUsers);
                 handleSearch();
             });
         });
@@ -437,6 +597,10 @@ const SHARE_TO = (() => {
     return {
         initModal,
         handleEventListener,
+        permissionChangeListener,
+        emitPermissionChange,
         getAvatarHTML,
+        notify,
+        removeUnread,
     }
 })();

+ 8 - 0
web/common/html/header.html

@@ -1,4 +1,12 @@
 <img id="f_btn" src="/web/dest/css/img/feeRate_btn.jpg" alt="" style="display: none" />
+<div class="top-msg clearfix">
+    <div class="alert alert-warning alert-dismissible" role="alert" id="notify" style="display: none">
+        <button type="button" class="close" aria-label="Close" onclick="$('#notify').hide();">
+            <span aria-hidden="true">&times;</span>
+        </button>
+        <strong id="message"></strong>-
+    </div>
+</div>
 <nav class="navbar navbar-expand-lg p-0 d-flex <%= versionName.includes('免费') ? 'free-version' : 'pro-version' %>">
     <% if(controller === 'boot' || controller === 'pm'){ %>
     <!--<a style="text-decoration: none" href="javascript:void(0);" class="header-logo">-->