Przeglądaj źródła

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

TonyKang 4 lat temu
rodzic
commit
365ba64558

+ 1 - 1
app/const/tender_info.js

@@ -108,7 +108,7 @@ const defaultInfo = {
         thousandth: false,
         stage: {
             realComplete: false,
-            correct: false,
+            correct: true,
         }
     },
     chapter: [

+ 2 - 1
app/controller/ledger_controller.js

@@ -455,7 +455,8 @@ module.exports = app => {
                 const ledgerData = await ctx.service.ledger.getData(ctx.tender.id);
                 const posData = this.ctx.tender.data.measure_type === measureType.tz.value
                     ? await ctx.service.pos.getPosData({ tid: ctx.tender.id }) : [];
-                ctx.body = { err: 0, msg: '', data: { bills: ledgerData, pos: posData } };
+                const ledgerTags = await this.ctx.service.ledgerTag.getDatas(ctx.tender.id);
+                ctx.body = { err: 0, msg: '', data: { bills: ledgerData, pos: posData, tags: ledgerTags } };
             } catch (err) {
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: [] };

+ 11 - 0
app/controller/tender_controller.js

@@ -893,6 +893,17 @@ module.exports = app => {
                 ctx.body = this.ajaxErrorBody(error, '保存审批流程设置失败');
             }
         }
+
+        async billsTag(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const result = await ctx.service.ledgerTag.update(data);
+                ctx.body = {err: 0, msg: '', data: result};
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '书签数据错误');
+            }
+        }
     }
 
     return TenderController;

+ 18 - 0
app/public/css/main.css

@@ -1205,4 +1205,22 @@ overflow-y: auto;
 }
 .list-table tr:hover .att-file-btn{
   display: inline-block;
+}
+.context-menu-icon.context-menu-icon--fa.text-success::before {
+    color: #28a745;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-danger::before {
+    color: #dc3545;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-warning::before {
+    color: #da9500;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-info::before {
+    color: #17a2b8;!important;
+}
+.context-menu-icon.context-menu-icon--fa span{
+     color: #fff;!important;
+ }
+.context-menu-icon.context-menu-hover.context-menu-icon--fa.text-success span{
+    color: #fff;!important;
 }

+ 18 - 0
app/public/css/main_s.css

@@ -1205,4 +1205,22 @@ overflow-y: auto;
 }
 .list-table tr:hover .att-file-btn{
   display: inline-block;
+}
+.context-menu-icon.context-menu-icon--fa.text-success::before {
+    color: #28a745;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-danger::before {
+    color: #dc3545;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-warning::before {
+    color: #da9500;!important;
+}
+.context-menu-icon.context-menu-icon--fa.text-info::before {
+    color: #17a2b8;!important;
+}
+.context-menu-icon.context-menu-icon--fa.fa-tag span{
+    color: #2f2f2f;!important;
+}
+.context-menu-icon.context-menu-hover.context-menu-icon--fa.fa-tag span{
+    color: #fff;!important;
 }

+ 132 - 7
app/public/js/ledger.js

@@ -67,6 +67,17 @@ $(document).ready(function() {
     });
     const posSpread = SpreadJsObj.createNewSpread($('#pos-spread')[0]);
 
+    const billsTag = $.billsTag({
+        selector: '#bills-tag',
+        relaSpread: ledgerSpread,
+        afterLocated:  function () {
+            posOperationObj.loadCurPosData();
+        },
+        afterShow: function () {
+            ledgerSpread.refresh();
+            if (posSpread) posSpread.refresh();
+        },
+    });
     const errorList = $.cs_errorList({
         tabSelector: '#error-list-tab',
         selector: '#error-list',
@@ -963,17 +974,42 @@ $(document).ready(function() {
     sjsSettingObj.setFxTreeStyle(ledgerSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
     if (thousandth) sjsSettingObj.setTpThousandthFormat(ledgerSpreadSetting);
     ledgerTreeCol.initSpreadSetting(ledgerSpreadSetting);
+    // ledgerSpreadSetting.rowHeader = [
+    //     {
+    //         rowHeaderType: 'tag',
+    //         setting: {
+    //             getColor: function (index, data) {
+    //                 if (!data) return;
+    //                 if (index%10 === 0) return '#007bff';
+    //                 if (index%10 === 1) return '#28a745';
+    //                 if (index%10 === 2) return '#dc3545';
+    //                 if (index%10 === 3) return '#da9500';
+    //                 if (index%10 === 4) return '#17a2b8';
+    //             }
+    //         },
+    //     },
+    // ];
     ledgerSpreadSetting.rowHeader = [
         {
             rowHeaderType: 'tag',
             setting: {
                 getColor: function (index, data) {
                     if (!data) return;
-                    if (index%10 === 0) return '#007bff';
-                    if (index%10 === 1) return '#28a745';
-                    if (index%10 === 2) return '#dc3545';
-                    if (index%10 === 3) return '#da9500';
-                    if (index%10 === 4) return '#17a2b8';
+                    return billsTag.getBillsTagsColor(data.id);
+                },
+                getTagHtml: function (index, data) {
+                    if (!data) return;
+                    const getHtml = function (list) {
+                        const html = [];
+                        for (const l of list) {
+                            html.push('<div class="row">');
+                            html.push(`<div class="col-auto pr-1 ${l.tagClass}">`, '<i class="fa fa-tag"></i>', '</div>');
+                            html.push('<div class="col p-0">', '<p>', l.comment, '</p>', '</div>');
+                            html.push('</div>');
+                        }
+                        return html.join('');
+                    };
+                    return getHtml(billsTag.getBillsTagsInfo(data.id));
                 }
             },
         },
@@ -1135,6 +1171,23 @@ $(document).ready(function() {
             .keyup((e) => {if (e.keyCode === 13) item.batchInsert($input[0], root);})
             .on('input', function () {this.value = this.value.replace(/[^\d]/g, '');});
     };
+    // $.contextMenu.types.switch = function (item, opt, root) {
+    //     const self = this;
+    //     if ($.isFunction(item.icon)) {
+    //         item._icon = item.icon.call(this, this, $t, key, item);
+    //     } else {
+    //         if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') {
+    //             // to enable font awesome
+    //             item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon;
+    //         } else {
+    //             item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
+    //         }
+    //     }
+    //     const html = [];
+    //     html.push('<div>');
+    //     html.push('</div>');
+    //     const $obj = $('<div>' + item.name +)
+    // };
     // 右键菜单
     const billsContextMenuOptions = {
         selector: '#ledger-spread',
@@ -1523,6 +1576,71 @@ $(document).ready(function() {
         };
         billsContextMenuOptions.items.sprImport = '-----------';
     }
+    billsContextMenuOptions.items.tag = {
+        name: '书签',
+        items: {
+            tagPrimary: {
+                icon: 'fa-tag text-primary mt-2 mb-2',
+                name: '靛青',
+                callback: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                    postData(window.location.pathname + '/tag', {add: { color: '#007bff', lid: node.id }}, function (data) {
+                        if (data.add) data.add.node = node;
+                        SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), node);
+                    });
+                },
+            },
+            tagSuccess: {
+                icon: 'fa-tag text-success mt-2 mb-2',
+                name: '果绿',
+                callback: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                    postData(window.location.pathname + '/tag', {add: { color: '#28a745', lid: node.id }}, function (data) {
+                        if (data.add) data.add.node = node;
+                        billsTag.updateDatasAndShow(data);
+                        SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), node);
+                    });
+                },
+            },
+            tagDanger: {
+                icon: 'fa-tag text-danger mt-2 mb-2',
+                name: '朱砂',
+                callback: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                    postData(window.location.pathname + '/tag', {add: { color: '#dc3545', lid: node.id }}, function (data) {
+                        if (data.add) data.add.node = node;
+                        billsTag.updateDatasAndShow(data);
+                        SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), node);
+                    });
+                },
+            },
+            tagWarning: {
+                icon: 'fa-tag text-warning mt-2 mb-2',
+                name: '姜黄',
+                callback: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                    postData(window.location.pathname + '/tag', {add: { color: '#da9500', lid: node.id }}, function (data) {
+                        if (data.add) data.add.node = node;
+                        billsTag.updateDatasAndShow(data);
+                        SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), node);
+                    });
+                },
+            },
+            tagInfo: {
+                icon: 'fa-tag text-info mt-2 mb-2',
+                name: '天蓝',
+                callback: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                    postData(window.location.pathname + '/tag', {add: { color: '#17a2b8', lid: node.id }}, function (data) {
+                        if (data.add) data.add.node = node;
+                        billsTag.updateDatasAndShow(data);
+                        SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), node);
+                    });
+                },
+            }
+        },
+    };
+
     $.contextMenu(billsContextMenuOptions);
 
     const posSearch = $.posSearch({selector: '#pos-search', searchSpread: posSpread});
@@ -2088,6 +2206,11 @@ $(document).ready(function() {
         treeOperationObj.loadExprToInput(ledgerSpread.getActiveSheet());
 
         checkList.loadHisCheckData();
+
+        for (const t of data.tags) {
+            t.node = ledgerTree.datas.find(x => {return x.id === t.lid});
+        }
+        billsTag.loadDatas(data.tags);
     }, null, true);
 
     $.divResizer({
@@ -2302,6 +2425,8 @@ $(document).ready(function() {
                     });
                 }
                 searchLedger.spread.refresh();
+            } else if (tab.attr('content') === '#bills-tag') {
+                // do something
             } else if (tab.attr('content') === '#error-list') {
                 errorList.spread.refresh();
             } else if (tab.attr('content') === '#check-list') {
@@ -2985,8 +3110,8 @@ $(document).ready(function() {
     //     $('#account_list').html(account_html);
     // });
 
-    let timer = null
-    let oldSearchVal = null
+    let timer = null;
+    let oldSearchVal = null;
 
     $('#gr-search').bind('input propertychange', function(e) {
         oldSearchVal = e.target.value

+ 180 - 0
app/public/js/shares/cs_tools.js

@@ -41,6 +41,17 @@ const showSideTools = function (show) {
     }
 };
 
+const showSelectTab = function(select, spread, afterShow) {
+    const tab = $(select), tabPanel = $(tab.attr('content'));
+    $('a', '#side-menu').removeClass('active');
+    tab.addClass('active');
+    $('.tab-content .tab-pane').removeClass('active');
+    tabPanel.addClass('active');
+    showSideTools(true);
+    spread && spread.refresh();
+    if (afterShow) afterShow();
+};
+
 (function($){
     /**
      * 错误列表
@@ -580,4 +591,173 @@ const showSideTools = function (show) {
         });
         return {spread: resultSpread};
     };
+
+    $.billsTag = function (setting) {
+        if (!setting.selector || !setting.relaSpread) return;
+        if (!setting.tagType) setting.tagType = [
+            {tagClass: 'text-primary', color: '#007bff'},
+            {tagClass: 'text-success', color: '#28a745'},
+            {tagClass: 'text-danger', color: '#dc3545'},
+            {tagClass: 'text-warning', color: '#da9500'},
+            {tagClass: 'text-info', color: '#17a2b8'},
+        ];
+        const obj = $(setting.selector);
+        const html = [], pageLength = 15;
+        let billsTags = [], classIndexes = [], billsIndexes = {};
+        html.push('<div class="sjs-bar d-flex justify-content-between">');
+        // 下拉过滤
+        html.push('<div class="dropdown mr-2">');
+        html.push('<a class="btn btn-sm btn-outline-secondary" id="dmb-bills-tag" data-toggle="dropdown" title="优先显示" aria-expanded="false"><i class="fa fa-list-ol" id="bills-tag-filter"></i></a>');
+        html.push('<div class="dropdown-menu" aria-labelledby="dmb-bills-tag" style="min-width: 60px; position: absolute; transform: translate3d(0px, 22px, 0px); top: 0px; left: 0px; will-change: transform;" x-placement="bottom-start">');
+        html.push('<a class="dropdown-item" href="javascript: void(0);" tagType="all" ><i class="fa fa-list-ol"></i></a>');
+        for (const t of setting.tagType) {
+            html.push(`<a class="dropdown-item ${t.tagClass}" href="javascript: void(0);" tagType="${t.tagClass}" ><i class="fa fa-tag"></i></a>`);
+            t.tags = [];
+            classIndexes.push(t);
+        }
+        html.push('</div>', '</div>');
+        // 搜索框
+        html.push('<div class="input-group input-group-sm">');
+        html.push('<input type="text" class="form-control" placeholder="可查找 项目节编号 / 清单编号 /名称">');
+        html.push('<div class="input-group-append">', '<button class="btn btn-outline-secondary" type="button" id="bills-tag-search">搜索</button>', '</div>');
+        html.push('</div>');
+        html.push('</div>');
+        // 书签列表
+        html.push('<div class="sjs-sh" style="overflow: auto;" id="bills-tag-list"></div>');
+        obj.html(html.join(''));
+
+        const clearViewTags = function () {
+            const viewTags = $('[name=billsTag]', obj);
+            if (viewTags && viewTags.length > 0) viewTags.remove();
+            billsTags.forEach(x => {x.display = false});
+        };
+
+        const getTagDisplayHtml = function(tag) {
+            const tagClass = classIndexes.find(x => {return x.color === tag.color});
+            const tagHtml = [];
+            tagHtml.push('<div class="card-header p-2"><div class="dropdown">');
+            tagHtml.push(`<a class="pull-left mr-2" href="" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-tag ${tagClass.tagClass}" title="修改书签颜色"></i></a>`);
+            tagHtml.push('</div>');
+            tag.node && tagHtml.push((tag.node.code || '') + (tag.node.b_code || ''), ' / ', tag.node.name || '');
+            if (tag.share) {
+                tagHtml.push('<i class="fa fa-users pull-right text-warning" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="所有参与台帐审批管理的用户都可以看到这条书签"></i>')
+            }
+            tagHtml.push('<div class="pull-right edit-tag-btn">');
+            const lid = tag.node ? tag.node.ledger_id : -1;
+            tagHtml.push(`<a class="mr-1" name="bills-tag-locate" href="javascript: void(0);" lid="${lid}"><i class="fa fa-crosshairs"></i> 定位</a>`);
+            if (tag.uid === userID) tagHtml.push('<a href="javascript: void(0);"><i class="fa fa-edit"></i> 编辑</a>');
+            tagHtml.push('</div>');
+            tagHtml.push('<div class="card-body p-2">', '<p class="card-text">', tag.comment, '</p>', '</div>');
+            return tagHtml.join('');
+        };
+
+        const reviewTag = function (tag, isTop = false) {
+            const obj = $('#bills-tag-' + tag.id);
+            if (obj && obj.length > 0) {
+                obj.html(getTagDisplayHtml(tag));
+            } else {
+                const objHtml = [];
+                objHtml.push(`<div class="card border-primary my-2 tag-item" id="bills-tag-${tag.id}">`);
+                objHtml.push(getTagDisplayHtml(tag));
+                objHtml.push('</div>');
+                if (isTop) {
+                    $('#bills-tag-list').prepend(objHtml.join(''));
+                } else {
+                    $('#bills-tag-list').append(objHtml.join(''));
+                }
+            }
+            tag.display = true;
+        };
+
+        const loadViewTags = function (tags) {
+            let showCount = 0;
+            for (const t of tags) {
+                if (showCount >= pageLength) continue;
+                if (t.display) continue;
+                reviewTag(t);
+                showCount++;
+            }
+        };
+
+        const _addToBillsIndex = function(data, isTop = false) {
+            let bi = billsIndexes[data.lid];
+            if (!bi) {
+                bi = [];
+                billsIndexes[data.lid] = bi;
+            }
+            isTop ? bi.unshift(data) : bi.push(data);
+        };
+
+        const loadDatas = function (datas) {
+            billsTags = [];
+            billsIndexes = {};
+            for (const d of datas) {
+                billsTags.push(d);
+                _addToBillsIndex(d);
+            }
+            for (const ci of classIndexes) {
+                ci.tags = billsTags.filter(x => {return x.color === ci.color});
+            }
+            clearViewTags();
+            loadViewTags(billsTags);
+        };
+
+        const updateDatas = function (data) {
+            const refresh = {};
+            if (data.add) {
+                billsTags.push(data.add);
+                const tagClass = classIndexes.find(x => {return x.color === data.add.color});
+                if (tagClass) tagClass.tags.push(data.add);
+                _addToBillsIndex(data.add, true);
+                refresh.add = data.add;
+            }
+            if (data.del) {
+                const delTag = billsTags.find(x => {return x.id === data.del.id});
+                billsTags.splice(billsTags.indexOf(delTag), 1);
+                const tagClass = classIndexes.find(x => {x.color = delTag.color});
+                if (tagClass) tagClass.splice(tagClass.indexOf(delTag), -1);
+                refresh.del = data.del
+            }
+            if (data.update) {
+                const updateTag = billsTags.find(x => {return x.id === data.update.id});
+                for (const prop in data.update) {
+                    updateTag[prop] = data.update[prop];
+                }
+                refresh.update = updateTag;
+            }
+            return refresh;
+        };
+
+        const updateDatasAndShow = function (data) {
+            const refresh = updateDatas(data);
+            if (refresh.add) {
+                reviewTag(refresh.add, true);
+            }
+        };
+
+        const show = function () {
+            showSelectTab(setting.selector, null, setting.afterShow);
+        };
+
+        const getBillsTagsColor = function (id) {
+            const billsTags = billsIndexes[id] || [];
+            return billsTags.length > 0 ? billsTags.map(x => {return x.color}) : undefined;
+        };
+
+        const getBillsTagsInfo = function (id) {
+            const billsTags = billsIndexes[id] || [];
+            return billsTags.length > 0 ? billsTags.map(x => {
+                const tagClass = classIndexes.find(tc => {return tc.color === x.color});
+                return {color: x.color, comment: x.comment, tagClass: tagClass.tagClass};
+            }) : undefined;
+        };
+
+        $('body').on('click', '[name=bills-tag-locate]', function () {
+            const lid = parseInt(this.getAttribute('lid'));
+            SpreadJsObj.locateTreeNode(setting.relaSpread.getActiveSheet(), lid);
+            setting.afterLocated && setting.afterLocated();
+        });
+
+        return { loadDatas, updateDatasAndShow, show, getBillsTagsColor, getBillsTagsInfo }
+    }
 })(jQuery);

+ 217 - 3
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -375,6 +375,7 @@ const SpreadJsObj = {
      * @param setting
      */
     initSheet: function (sheet, setting) {
+        const self = this;
         this.beginMassOperation(sheet);
         this._loadCacheSetting(sheet, setting);
         setting.pos = sheet.getParent().pos;
@@ -389,6 +390,10 @@ const SpreadJsObj = {
         this._initSheetHeader(sheet);
         sheet.setRowCount(sheet.zh_setting.emptyRows);
         sheet.extendCellType = {};
+        sheet.extendRowHeader = {};
+        sheet.zh_setting.rowHeader && sheet.zh_setting.rowHeader.forEach(function (col, j) {
+            self._defineRowHeader(sheet, j, col);
+        });
         sheet.borderLine = new spreadNS.LineBorder('#cccccc', spreadNS.LineStyle.thin);
         sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).locked(setting.readOnly).setBorder(sheet.borderLine, {all: true});
         if (setting.selectedBackColor) {
@@ -573,6 +578,20 @@ const SpreadJsObj = {
             sheet.AcitveRefresh = true;
         }
     },
+    _defineRowHeader: function (sheet, col, colSetting) {
+        if (colSetting.rowHeaderType === 'tag') {
+            if (!sheet.extendRowHeader.tag) {
+                sheet.extendRowHeader.tag = this.RowHeader.getTagRowHeader(colSetting.setting);
+            }
+            sheet.getRange(-1, col, -1, 1, spreadNS.SheetArea.rowHeader).cellType(sheet.extendRowHeader.tag);
+        }
+        if (colSetting.rowHeaderType === 'circle') {
+            if (!sheet.extendRowHeader.multiTag) {
+                sheet.extendRowHeader.multiTag = this.RowHeader.getCircleTagRowHeader(colSetting.setting);
+            }
+            sheet.getRange(-1, col, -1, 1, spreadNS.SheetArea.rowHeader).cellType(sheet.extendRowHeader.multiTag);
+        }
+    },
     _defineColCellType: function (sheet, col, colSetting) {
         sheet.AcitveComboRefresh = false;
         if(colSetting.cellType === 'ellipsis') {
@@ -819,13 +838,30 @@ const SpreadJsObj = {
     },
     reLoadNodesData: function (sheet, nodes) {
         this.beginMassOperation(sheet);
-        nodes = nodes instanceof Array ? nodes : [nodes];
+        nodes = nodes instanceof Array ? nodes : [nodes];rectangle
+        const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
         for (const node of nodes) {
-            const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
             this._loadRowData(sheet, node, sortData.indexOf(node));
         }
         this.endMassOperation(sheet);
     },
+    repaintNodesRowHeader: function (sheet, nodes) {
+        nodes = nodes instanceof Array ? nodes : [nodes];
+        const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
+        const rowIndex = [];
+        for (const node of nodes) {
+            rowIndex.push(sortData.indexOf(node));
+        }
+        this.repaintRowsRowHeader(sheet, rowIndex);
+    },
+    repaintRowsRowHeader: function (sheet, rows) {
+        for (const r of rows) {
+            const cellRect = sheet.getCellRect(r, 0);
+            cellRect.width = cellRect.x;
+            cellRect.x = 0;
+            sheet.repaint(cellRect);
+        }
+    },
     /**
      * 根据data加载sheet数据,合并了一般数据和树结构数据的加载
      * @param {GC.Spread.Sheets.Worksheet} sheet
@@ -981,6 +1017,19 @@ const SpreadJsObj = {
             }
         }
     },
+    getRowObject: function (sheet, row) {
+        if (!sheet) {
+            return null;
+        } else if (sheet.zh_dataType) {
+            if (sheet.zh_dataType === this.DataType.Tree) {
+                return sheet.zh_tree.nodes[row];
+            } else if (sheet.zh_dataType === this.DataType.Data) {
+                return sheet.zh_data[row];
+            } else {
+                return null;
+            }
+        }
+    },
     /**
      * 刷新列显示
      * @param sheet
@@ -1206,6 +1255,7 @@ const SpreadJsObj = {
                     canvas.clearRect(x, y, w, h);
                 }
 
+
                 const tree = options.sheet.zh_tree;
                 // 使用TreeCellType前,需定义sheet.tree
                 if (tree) {
@@ -1960,7 +2010,7 @@ const SpreadJsObj = {
         getHtmlCellType: function () {
             const HTMLCellType = function (){};
             HTMLCellType.prototype = new spreadNS.CellTypes.Text;
-            const proto = ImageCellType.prototype;
+            const proto = HTMLCellType.prototype;
             proto.paint = function (ctx, value, x, y, w, h, style, context) {
                 let DOMURL = window.URL || window.webkitURL || window;
                 let cell = context.sheet.getCell(context.row, context.col);
@@ -2185,6 +2235,170 @@ const SpreadJsObj = {
         }
     },
 
+    RowHeader: {
+        getTagRowHeader: function (setting) {
+            const indent = 16, maxHintWidth = 200, borderIndent = 10;
+            const drawTag = function (canvas, x, y, fillColor) {
+                canvas.save();
+                // 设置偏移量
+                canvas.translate(0.5, 0.5);
+                canvas.beginPath();
+                canvas.moveTo(x, y);
+                canvas.lineTo(x, y+5);
+                canvas.lineTo(x+6, y+11);
+                canvas.lineTo(x+11, y+6);
+                canvas.lineTo(x+5, y);
+                canvas.lineTo(x, y);
+                canvas.stroke();
+                canvas.fillStyle = fillColor instanceof Array ? fillColor[0] : fillColor;
+                canvas.fill();
+
+                canvas.beginPath();
+                canvas.arc(x+2, y+2, 1, 0, Math.PI*2, true);
+                canvas.closePath();
+                canvas.fillStyle = 'white';
+                canvas.fill();
+
+                if (fillColor instanceof Array && fillColor.length > 1) {
+                    canvas.fillStyle = '#444444';
+                    canvas.font="9px 微软雅黑";
+                    canvas.fillText(fillColor.length, x+13, y+14);
+                }
+
+                canvas.restore();
+            };
+
+            let TagCellType = function (){};
+            TagCellType.prototype = new spreadNS.CellTypes.RowHeader();
+            const proto = TagCellType.prototype;
+            proto.getTagColor = setting.getColor;
+            proto.getTagHtml = setting.getTagHtml;
+            proto.basePaint = proto.paint;
+            proto.paint = function (canvas, value, x, y, w, h, style, options) {
+                spreadNS.CellTypes.RowHeader.prototype.paint.apply(this, [canvas, '', x, y, w , h, style, options]);
+                spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w - indent, h, style, options]);
+                const node = SpreadJsObj.getRowObject(options.sheet, options.row);
+                const color = this.getTagColor(options.row, node);
+                const centerX = x + w - indent + 4, centerY = y + h/2;
+                color && drawTag(canvas, centerX - 6, centerY - 6, color);
+
+            };
+            /**
+             * 获取点击信息
+             * @param {Number} x
+             * @param {Number} y
+             * @param {Object} cellStyle
+             * @param {Object} cellRect
+             * @param {Object} context
+             * @returns {{x: *, y: *, row: *, col: *|boolean|*[]|number|{}|UE.dom.dtd.col, cellStyle: *, cellRect: *, sheet: *|StyleSheet, sheetArea: *}}
+             */
+            proto.getHitInfo = function (x, y, cellStyle, cellRect, context) {
+                return {
+                    x: x,
+                    y: y,
+                    row: context.row,
+                    col: context.col,
+                    cellStyle: cellStyle,
+                    cellRect: cellRect,
+                    sheet: context.sheet,
+                    sheetArea: context.sheetArea,
+                    ctx: context.sheet.getParent().xs,
+                };
+            };
+            /**
+             * 鼠标进入单元格事件 - 显示悬浮提示
+             * @param {Object} hitinfo - 见getHitInfo返回值
+             */
+            proto.processMouseEnter = function (hitinfo) {
+                const node = SpreadJsObj.getRowObject(hitinfo.sheet, hitinfo.row);
+                let html = this.getTagHtml(hitinfo.row, node);
+                const pos = SpreadJsObj.getObjPos(hitinfo.sheet.getParent().qo);
+                if (pos && html) {
+                    if (!this._tagTipElement) {
+                        let div = $('#autoTip')[0];
+                        if (!div) {
+                            div = document.createElement("div");
+                            $(div).css("position", "absolute")
+                                .css("border", "1px #C0C0C0 solid")
+                                .css("box-shadow", "1px 2px 5px rgba(0,0,0,0.4)")
+                                .css("font", "9pt Arial")
+                                .css("background", "white")
+                                .css("padding", 5)
+                                .css("width", maxHintWidth)
+                                .css("z-index", 999)
+                                .attr("id", 'autoTagTip')
+                                .css("top", pos.y + hitinfo.y + indent)
+                                .css("left", pos.x + hitinfo.x + indent)
+                                .html(html);
+                            document.body.insertBefore(div, null);
+                        }
+                        this._tagTipElement = div;
+                        $(div).show("fast");
+                    }
+                }
+            };
+            /**
+             * 鼠标移出单元格事件 - 隐藏悬浮提示
+             * @param {Object} hitinfo - 见getHitInfo返回值
+             */
+            proto.processMouseLeave = function (hitinfo) {
+                if (this._tagTipElement) {
+                    $(this._tagTipElement).hide().remove();
+                    this._tagTipElement = null;
+                }
+            };
+            return new TagCellType();
+        },
+        getCircleTagRowHeader: function (setting) {
+            const drawCircle = function (canvas, x, y, r, fillColor) {
+                canvas.save();
+                canvas.beginPath();
+                canvas.arc(x, y, r, 0, Math.PI*2, true);
+                canvas.closePath();
+                canvas.fillStyle = fillColor;
+                canvas.fill();
+                canvas.restore();
+            };
+            const drawCircle2 = function (canvas, x, y, r, lineColor, fillColor) {
+                canvas.save();
+                canvas.beginPath();
+                canvas.arc(x, y, r, 0, Math.PI*2, true);
+                canvas.closePath();
+                canvas.fillStyle = lineColor;
+                canvas.fill();
+                canvas.beginPath();
+                canvas.arc(x, y, r-2, 0, Math.PI*2, true);
+                canvas.closePath();
+                canvas.fillStyle = fillColor;
+                canvas.fill();
+                canvas.restore();
+            };
+
+            let CircleTagCellType = function (){};
+            CircleTagCellType.prototype = new spreadNS.CellTypes.RowHeader();
+            const proto = CircleTagCellType.prototype;
+            proto.indent = setting.indent || 16;
+            proto.getTagColor = setting.getColor;
+            proto.basePaint = proto.paint;
+            proto.paint = function (canvas, value, x, y, w, h, style, options) {
+                spreadNS.CellTypes.RowHeader.prototype.paint.apply(this, [canvas, '', x, y, w , h, style, options]);
+                spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w - this.indent, h, style, options]);
+                const node = SpreadJsObj.getRowObject(options.sheet, options.row);
+                let sel = options.sheet.getSelections();
+                sel = sel ? sel[0] : null;
+                const actualStyle = options.sheet.getActualStyle(options.row, options.col, options.sheetArea, false);
+                if (options.row === 1) console.log(actualStyle);
+                const backColor = style.backColor || (sel && options.row >= sel.row && options.row < sel.row + sel.rowCount ? '#dddfe1' : '#e9ecef');
+                let color = this.getTagColor(options.row, node);
+                color = color instanceof Array ? color : [color];
+                for (let i = color.length - 1; i >= 0; i--) {
+                    drawCircle2(canvas, x + w - this.indent + 5 + i*5 , y + h/2, 6, backColor, color[i]);
+                }
+            };
+            return new CircleTagCellType();
+        },
+    },
+
     Formatter: {
         baseNumberFormatter: function () {
             const formatter = function () {};

+ 4 - 0
app/router.js

@@ -435,4 +435,8 @@ module.exports = app => {
     app.get('/tender/:id/schedule/ledger', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.ledger');
     app.post('/tender/:id/schedule/ledger/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.loadLedgerData');
     app.post('/tender/:id/schedule/ledger/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.saveLedger');
+
+    // 书签
+    app.post('/tender/:id/ledger/tag', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.billsTag');
+    app.post('/tender/:id/ledger/measure/stage/:order/tag', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'tenderController.billsTag');
 };

+ 99 - 0
app/service/ledger_tag.js

@@ -0,0 +1,99 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const validField = ['lid', 'share', 'color', 'comment'];
+
+module.exports = app => {
+
+    class StageBillsDgn extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'ledger_tag';
+        }
+
+        /**
+         * 获取台账、期所有标段
+         * @param {Number} tid - 标段id
+         * @param {Number} sid - 期id(-1时查询台账分解全部标签)
+         * @returns {Promise<void>}
+         */
+        async getDatas(tid, sid = -1) {
+            return await this.db.select(this.tableName, {
+                where: {tid: tid, sid: -1},
+                columns: ['id', 'uid', 'lid', 'share', 'color', 'comment'],
+                orders: [['create_time', 'desc']],
+            });
+        }
+
+        /**
+         * 过滤无效字段,容错
+         * @param data
+         * @private
+         */
+        _filterInvalidField(data) {
+            for (const prop in data) {
+                if (validField.indexOf(prop) === -1) {
+                    delete data[prop];
+                }
+            }
+        }
+
+        async _addTag(data) {
+            this._filterInvalidField(data);
+            data.create_time = new Date();
+            data.modify_time = data.create_time;
+            data.uid = this.ctx.session.sessionUser.accountId;
+            data.tid = this.ctx.tender.id;
+            data.pid = this.ctx.tender.data.project_id;
+            if (this.ctx.stage) {
+                data.sid = this.ctx.stage.id;
+                data.sorder = this.ctx.stage.order;
+            }
+            const result = await this.db.insert(this.tableName, data);
+            data.id = result.insertId;
+            return data;
+        }
+
+        async _delTag(id) {
+            const tag = this.getDataById(id);
+            if (tag.uid !== this.ctx.session.sessionUser.accountId) throw '您无权删除该数据';
+
+            await this.deleteById(id);
+            return id;
+        }
+
+        async _updateTag(data) {
+            const tag = this.getDataById(data.id);
+            if (tag.uid !== this.ctx.session.sessionUser.accountId) throw '您无权删除该数据';
+
+            this._filterInvalidField(data);
+            data.modify_time = new Date();
+            data.id = tag.id;
+            const result = await this.db.udpate(this.tableName, data);
+            if (result.affectedRows === 1) return data;
+        }
+
+        async update(data) {
+            const result = {};
+            if (data.add) result.add = await this._addTag(data.add);
+            if (data.del) result.del = await this._delTag(data.del);
+            if (data.update) result.update = await this._updateTag(data.update);
+            return result;
+        }
+    }
+
+    return StageBillsDgn;
+};

+ 6 - 0
app/view/ledger/explode.ejs

@@ -150,6 +150,9 @@
                         <div id="deal-bills-spread" class="sjs-sh-4">
                         </div>
                     </div>
+                    <div id="bills-tag" class="tab-pane">
+
+                    </div>
                     <div id="error-list" class="tab-pane">
                     </div>
                     <div id="check-list" class="tab-pane">
@@ -173,6 +176,9 @@
                     <a class="nav-link" content="#deal-bills" href="javascript: void(0);">签约清单</a>
                 </li>
                 <li class="nav-item">
+                    <a class="nav-link" content="#bills-tag" href="javascript: void(0);">书签</a>
+                </li>
+                <li class="nav-item">
                     <a class="nav-link" content="#error-list" id="error-list-tab" href="javascript: void(0);" style="display: none;">错误列表</a>
                 </li>
                 <li class="nav-item">