Преглед изворни кода

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

MaiXinRong пре 3 дана
родитељ
комит
b57220acd4

+ 1 - 0
app/const/page_show.js

@@ -68,6 +68,7 @@ const defaultSetting = {
     isCommonSetup: 1,
     isPrivateSetup: 1,
     onlyOrgAdminArchivePDF: 0,
+    projectScreenTenderVersion: 0,
 };
 
 

+ 3 - 1
app/const/project_screen.js

@@ -10,12 +10,14 @@ const templates = [
         name: '通用大屏一',
         description: '通用项目综合展示模板',
         previewClass: 'project-screen-preview-general1',
+        hidden: true,
     },
     {
         code: 'general3',
         name: '通用大屏三',
         description: '通用项目图表展示模板',
         previewClass: 'project-screen-preview-general3',
+        hidden: true,
     },
     {
         code: 'guangyuan',
@@ -28,6 +30,6 @@ const templates = [
 const getTemplate = code => templates.find(item => item.code === code) || null;
 
 module.exports = {
-    templates,
+    templates: templates.filter(item => !item.hidden),
     getTemplate,
 };

+ 1 - 1
app/controller/project_screen_controller.js

@@ -37,7 +37,7 @@ module.exports = app => {
                 renderData.finishedProjects = listOnly(finishedProjects);
                 renderData.plannedProjects = listOnly(plannedProjects);
                 renderData.buildingProjects = listOnly(buildingProjects);
-                renderData.overview = template.code === 'guangyuan' ? await ctx.service.projectScreen.getOverview(plannedProjects, buildingProjects, finishedProjects) : null;
+                renderData.overview = template.code === 'guangyuan' ? await ctx.service.projectScreen.getOverview(summaries) : null;
                 await this.layout('project_screen/index.ejs', renderData, template.code === 'guangyuan' ? 'project_screen/guangyuan_modal.ejs' : '');
             } catch (error) {
                 this.log(error);

+ 17 - 5
app/controller/setting_controller.js

@@ -60,12 +60,13 @@ module.exports = app => {
                 const projectId = ctx.session.sessionProject.id;
                 await this._checkMenu(projectId);
                 const screens = await ctx.service.projectScreen.getList(projectId);
-                await this.layout('setting/module.ejs', {
+                const renderData = {
                     screens,
                     templates: projectScreenConst.templates,
                     projectScreenEnabled: ctx.session.sessionProject.page_show.openProjectScreen === 1,
-                    jsFiles: this.app.jsFiles.common.concat(['/public/js/project_screen_setting.js']),
-                }, 'setting/module_modal.ejs');
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.setting.module),
+                };
+                await this.layout('setting/module.ejs', renderData, 'setting/module_modal.ejs');
             } catch (error) {
                 ctx.log(error);
                 ctx.redirect('/dashboard');
@@ -83,8 +84,7 @@ module.exports = app => {
                     case 'status': {
                         errorMessage = '保存项目大屏开关失败';
                         const enabled = parseInt(data.enabled) === 1 ? 1 : 0;
-                        ctx.session.sessionProject.page_show.openProjectScreen = enabled;
-                        await ctx.service.project.updatePageshow(projectId);
+                        await ctx.service.projectScreen.saveModuleStatus(projectId, enabled);
                         responseData.data = { enabled };
                         break;
                     }
@@ -117,6 +117,18 @@ module.exports = app => {
                         errorMessage = '保存项目大屏权限失败';
                         await ctx.service.projectAccount.saveProjectScreenPermissions(projectId, data.changes);
                         break;
+                    case 'tenderinit':
+                        errorMessage = '加载大屏展示范围失败';
+                        responseData.data = await ctx.service.projectScreenTender.initTenderSettings(projectId);
+                        break;
+                    case 'tenderload':
+                        errorMessage = '加载项目标段失败';
+                        responseData.data = await ctx.service.projectScreenTender.loadTenderSettings(projectId, data.spid);
+                        break;
+                    case 'tendersave':
+                        errorMessage = '保存大屏展示范围失败';
+                        responseData.data = await ctx.service.projectScreenTender.saveTenderSettings(projectId, data);
+                        break;
                     default:
                         throw '提交数据错误';
                 }

+ 0 - 36
app/lib/project_screen.js

@@ -1,36 +0,0 @@
-'use strict';
-
-module.exports = {
-    getProgressPaths(rows) {
-        const nodes = new Map(rows.map(row => [String(row.tree_id), row]));
-        const paths = new Map();
-        for (const row of rows) {
-            const path = [];
-            const visited = new Set();
-            let node = row;
-            while (node && !visited.has(String(node.tree_id))) {
-                path.unshift(node);
-                visited.add(String(node.tree_id));
-                node = nodes.get(String(node.tree_pid));
-            }
-            paths.set(row.id, path);
-        }
-        return paths;
-    },
-    sortProgressRows(rows, paths) {
-        return rows.slice().sort((a, b) => {
-            const pathA = paths.get(a.id);
-            const pathB = paths.get(b.id);
-            for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
-                if (pathA[i].id === pathB[i].id) continue;
-                return pathA[i].tree_order - pathB[i].tree_order || String(pathA[i].id).localeCompare(String(pathB[i].id));
-            }
-            return pathA.length - pathB.length;
-        });
-    },
-    formatProgressPath(path, abbreviated = false) {
-        const names = path.map(node => node.name);
-        if (abbreviated && names.length > 2) return [names[0], '...', names[names.length - 1]].join(' / ');
-        return names.join(' / ');
-    },
-};

+ 31 - 8
app/public/css/main.css

@@ -2549,13 +2549,16 @@ animation:shake 1s .2s ease both;}
 .project-screen-preview-content main:before, .project-screen-preview-content main:after { content: ""; position: absolute; border-radius: 50%; background: #c9d6ff; }
 .project-screen-preview-content main:before { width: 28%; aspect-ratio: 1 / 1; right: 12%; top: 12%; }
 .project-screen-preview-content main:after { width: 55%; aspect-ratio: 1 / 1; right: 18%; bottom: -42%; }
-.project-screen-preview-guangyuan .project-screen-preview-nav { background: #295f70; }
+.project-screen-page .project-screen-preview.project-screen-preview-guangyuan { width: 94%; max-width: none; min-width: 0; box-sizing: content-box; aspect-ratio: 3594 / 1908; display: block; background: #2e3238 url('/public/images/guangyuandaping.png?v=0939983fbcff') center / contain no-repeat; }
+.project-screen-page .project-screen-preview-guangyuan > div { display: none; }
 
 .project-screen-template-item { cursor: pointer; }
 .project-screen-template-item.added { cursor: not-allowed; opacity: .6; }
 .project-screen-template-preview { height: 120px; display: grid; place-items: center; background: #e9ecef; }
 .project-screen-template-preview > div { width: 82%; height: 85px; display: grid; grid-template-columns: 22% 1fr; border: 4px solid #ced4da; background: #4d7881; }
 .project-screen-template-preview > div:after { content: ""; margin: 7px; background: #f8f9fa; }
+#project-screen-select-modal .project-screen-template-preview.project-screen-preview-guangyuan { background: #2e3238 url('/public/images/guangyuandaping.png?v=0939983fbcff') center / contain no-repeat; }
+#project-screen-select-modal .project-screen-template-preview.project-screen-preview-guangyuan > div { display: none; }
 
 .project-screen-view-page { height: 100vh; padding: 18px; color: #dce9ff; background: #07152e; }
 .project-screen-view-head { height: 55px; display: flex; justify-content: space-between; align-items: flex-start; }
@@ -2801,6 +2804,16 @@ animation:shake 1s .2s ease both;}
 @media(max-width:1200px){.gy-header{flex-wrap:wrap}.gy-nav{order:3;width:100%;justify-content:center}.gy-grid{grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:auto}.gy-card{min-height:270px}.gy-summary,.gy-progress,.gy-investment,.gy-building{grid-column:span 2}.gy-planned,.gy-finished{height:300px}.gy-ratio{height:300px}.gy-progress{height:330px}.gy-screen:fullscreen .gy-grid{grid-template-rows:auto}}
 @media(max-width:650px){.gy-screen{padding:10px}.gy-brand strong{font-size:15px}.gy-mark{display:none}.gy-nav{gap:5px}.gy-nav button{padding:8px 12px}.gy-grid{display:flex;flex-direction:column}.gy-card{padding:12px}.gy-stats{grid-template-columns:repeat(2,1fr)}.gy-stat{padding:14px}.gy-stat strong{font-size:30px}.gy-fullscreen{padding:8px}.gy-title h2{font-size:15px}}
 
+.gy-preview-dialog{--gy-card:#343a40;color:#edf3fa}
+.gy-preview-dialog .modal-dialog{width:90vw;max-width:1600px;margin-left:auto;margin-right:auto}
+.gy-preview-dialog .modal-content{background:var(--gy-card);border-color:#627589;border-radius:6px}
+.gy-preview-dialog .modal-body{max-height:70vh}
+.gy-preview-dialog .modal-header{border-bottom-color:#48515a}
+.gy-preview-dialog .modal-title .small{color:#aab6c3}
+.gy-preview-dialog .gy-preview-dialog-table{border:1px solid #4a545e;background:var(--gy-card)}
+.gy-preview-dialog .close{color:inherit;text-shadow:none}
+.gy-preview-dialog .gy-preview-table .gy-ellipsis{white-space:normal;overflow-wrap:anywhere}
+
 /* Fill the available screen; short windows scroll inside the dark container. */
 /* Keep both header rows together, without collapsed-border gaps during scrolling. */
 .gy-preview-table{border-collapse:separate;border-spacing:0;table-layout:fixed}
@@ -2812,6 +2825,13 @@ animation:shake 1s .2s ease both;}
 .gy-preview-table th,.gy-preview-table td{border:0;border-right:1px solid #4a545e;border-bottom:1px solid #4a545e}
 .gy-preview-table tr>*:first-child{border-left:1px solid #4a545e}
 .gy-preview-table thead tr:first-child th{border-top:1px solid #4a545e}
+.gy-preview-table .gy-preview-name{position:relative;display:flex;align-items:center;gap:8px;min-width:0;line-height:20px}
+.gy-preview-table .gy-preview-name>.fa{flex:0 0 14px;width:14px;color:#829bb0;text-align:center}
+.gy-preview-table .gy-preview-name>.gy-ellipsis{min-width:0}
+.gy-preview-table .gy-preview-child{padding-left:22px}
+.gy-preview-table .gy-year td{background:#30495d;color:#bddfff;font-weight:600;padding:9px}
+.gy-preview-table .gy-year .fa{color:#80bfff}
+.gy-preview-table .gy-preview-folder-cell{position:sticky;left:0;z-index:1;box-shadow:2px 0 0 #586674}
 .gy-header,.gy-message{flex-shrink:0}
 @media(min-width:1201px){
     .gy-header{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr)}
@@ -2873,15 +2893,16 @@ animation:shake 1s .2s ease both;}
 .gy-detail-text{font-size:14px;line-height:2;background:#303b47;padding:16px;overflow:auto;overflow-wrap:anywhere}
 .gy-detail-text strong{display:block;color:#84c4ff;margin-bottom:12px}
 .gy-list-heading{background:#3a536a;padding:12px;text-align:center;font-size:13px}
-.gy-screen .gy-folder-row{display:flex;align-items:center;gap:6px;min-height:40px;padding:5px 12px 5px calc(12px + var(--gy-depth) * 16px);border-left:2px solid transparent;color:#9eafc0;line-height:1.4}
-.gy-screen .gy-folder-row:hover{background:#37424c}
-.gy-screen .gy-folder-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:18px;height:28px;padding:0;border:0;background:none;color:#9eafc0;font-size:13px}
-.gy-screen .gy-folder-icon{width:14px;text-align:center;color:#8297aa}
-.gy-screen .gy-project-row{display:flex;align-items:center;gap:6px;width:100%;min-height:40px;padding:6px 12px 6px calc(36px + var(--gy-depth) * 16px);border:0;border-left:2px solid transparent;text-align:left;background:transparent;color:#c5d6e7;line-height:1.4;font-size:14px!important}
+.gy-screen .gy-folder-row{display:flex;align-items:center;gap:8px;min-height:42px;padding:6px 12px 6px calc(12px + var(--gy-depth) * 22px);border-left:2px solid transparent;background:#2f4354;color:#c2d7e9;font-weight:600;line-height:1.4}
+.gy-screen .gy-folder-row.gy-folder-nested{background:#36414b;color:#cbd5df;font-weight:400}
+.gy-screen .gy-folder-row:hover{background:#384e60}
+.gy-screen .gy-folder-row.gy-folder-nested:hover{background:#404c57}
+.gy-screen .gy-folder-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:18px;height:28px;padding:0;border:0;background:none;color:#a8c5de;font-size:16px}
+.gy-screen .gy-folder-icon{flex:0 0 16px;width:16px;text-align:center;color:#80bfff}
+.gy-screen .gy-project-row{display:flex;align-items:center;width:100%;min-height:42px;padding:8px 12px 8px calc(36px + var(--gy-depth) * 22px);border:0;border-left:2px solid transparent;text-align:left;background:transparent;color:#c5d6e7;line-height:1.5;font-size:14px!important}
 .gy-screen .gy-folder-row,.gy-screen .gy-project-row{border-bottom:1px solid #48515a}
 .gy-screen .gy-project-row:hover{background:#374552}
-.gy-screen .gy-project-row.active{background:#354d63;border-left-color:#3698ff;color:#fff}
-.gy-screen .gy-project-marker{flex:0 0 14px;width:14px;text-align:center;color:#7893aa}
+.gy-screen .gy-project-row.active{background:#225e91;border-left-color:#73c6ff;color:#fff}
 .gy-screen .gy-project-row span:last-child,.gy-screen .gy-folder-row span{min-width:0;overflow-wrap:anywhere}
 .gy-screen .gy-folder-toggle:focus-visible,.gy-screen .gy-project-row:focus-visible{outline:2px solid #80bfff;outline-offset:-2px}
 .gy-detail-button{background:transparent;border:1px solid #56718b;border-radius:4px;color:#8ac8ff;padding:5px 10px;font-size:12px!important}
@@ -3943,6 +3964,8 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 .project-screen-page .project-screen-tender-fold:focus:not(:focus-visible) { outline: none; box-shadow: none; }
 .project-screen-page .project-screen-tender-tree-actions .btn { font-size: 12px; }
 .project-screen-page .project-screen-tender-heading { min-height: 28px; }
+.project-screen-page #project-screen-tender-total,
+.project-screen-page .project-screen-tender-description { color: #495057; font-size: 14px; line-height: 1.5; }
 .project-screen-page .project-screen-tender-heading .project-screen-tender-tree-actions { flex-shrink: 0; }
 .project-screen-page .project-screen-tender-intro { flex: 1; min-width: 0; padding-right: 12px; }
 .project-screen-page .project-screen-tender-intro strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

BIN
app/public/images/guangyuandaping.png


Разлика између датотеке није приказан због своје велике величине
+ 31 - 10
app/public/js/project_screen_guangyuan.js


+ 3 - 8
app/public/js/project_screen_setting.js

@@ -122,14 +122,9 @@ $(function() {
         const tab = $(this).attr('data-tab');
         $('.project-screen-tab').removeClass('active');
         $(this).addClass('active');
-        if (tab === 'permission') {
-            $('#project-screen-config').hide();
-            $('#project-screen-permission').show();
-            loadPermissions(1);
-        } else {
-            $('#project-screen-permission').hide();
-            $('#project-screen-config').show();
-        }
+        $('#project-screen-config, #project-screen-permission, #project-screen-tender').hide();
+        $(`#project-screen-${tab}`).show();
+        if (tab === 'permission') loadPermissions(1);
     });
 
     $('#project-screen-select').click(function() {

Разлика између датотеке није приказан због своје велике величине
+ 572 - 0
app/public/js/project_screen_tender_setting.js


+ 9 - 6
app/public/js/shares/show_level.js

@@ -21,10 +21,12 @@ $.cs_showLevel = function (setting) {
 
     const initShowLevel = function () {
         const obj = $(setting.selector);
+        const buttonId = setting.buttonId || 'dropdownMenuButton';
+        const menuId = setting.buttonId ? `${buttonId}-menu` : 'showLevelList';
         const html = [];
-        html.push('<button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">' +
+        html.push(`<button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="${buttonId}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">` +
             '<i class="fa fa-list-ol"></i> 显示层级</button>');
-        html.push('<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" id="showLevelList">');
+        html.push(`<div class="dropdown-menu" aria-labelledby="${buttonId}" id="${menuId}">`);
         for (const l of setting.levels) {
             if (l.type === 'sort') {
                 let count = typeof l.count === 'function' ? l.count() : l.count;
@@ -40,7 +42,7 @@ $.cs_showLevel = function (setting) {
         obj.html(html.join(''));
 
 
-        $('a[name=showLevel]').click(function () {
+        obj.find('a[name=showLevel]').click(function () {
             setTimeout(() => {
                 const tag = $(this).attr('tag');
                 showWaitingView();
@@ -52,9 +54,10 @@ $.cs_showLevel = function (setting) {
     };
 
     const refreshMenuVisible = function () {
+        const obj = $(setting.selector);
         const showMenu = function (tag, visible) {
-            if (visible) $(`[tag=${tag}]`).show();
-            if (!visible) $(`[tag=${tag}]`).hide();
+            if (visible) obj.find(`a[name=showLevel][tag="${tag}"]`).show();
+            if (!visible) obj.find(`a[name=showLevel][tag="${tag}"]`).hide();
         };
         for (const l of setting.levels) {
             if (l.type === 'sort') {
@@ -70,4 +73,4 @@ $.cs_showLevel = function (setting) {
     };
 
     return { initShowLevel, refreshMenuVisible } ;
-};
+};

+ 22 - 3
app/reports/rpt_component/jpc_flow_tab.js

@@ -1022,6 +1022,7 @@ JpcFlowTabSrv.prototype.createNew = function() {
             rst = rst.concat(tabRstLst[i]);
             tabRstLst[i] = null;
         }
+        me.checkCombineEvent(JV.RUN_TYPE_AFTER_COMBINE, [], [], rst, $CURRENT_RPT, dataObj, me, page);
         return rst;
     };
     JpcFlowTabResult.setPageFlow = function(rptTpl, dataObj, page, thisPageRst, $CURRENT_RPT) {
@@ -1178,6 +1179,11 @@ JpcFlowTabSrv.prototype.createNew = function() {
         // 另:要考虑紧密输出情况,需要重新拆分数据
         // const regSplit = new RegExp('\\|', 'g');
         const _splitValues = function(oCell) {
+            let chkFontName = '宋体';
+            let chkFontHeight = 12;
+            const tmpVal = (`${oCell.Value || ''}`).replace(/\|/g, '');
+            const area = oCell.area;
+
             // JpcFieldHelper.setValue(data_field, theRecIdx, value);
             const values = (typeof oCell.Value === 'string') ? oCell.Value.split('|') : [oCell.Value];
             let font = oCell[JV.PROP_FONT];
@@ -1186,12 +1192,24 @@ JpcFlowTabSrv.prototype.createNew = function() {
             }
             // // if (values.length > rst) rst = values.length; // 考虑到紧凑输出,不能那么快设置rst
             // const font = private_get_font(tab_field[JV.PROP_FONT]);
-            let chkFontName = '宋体';
-            let chkFontHeight = 12;
             if (font) {
                 chkFontName = font[JV.FONT_PROPS[0]];
                 chkFontHeight = parseFloat(font[JV.FONT_PROPS[JV.FONT_PROP_IDX_HEIGHT]]);
             }
+
+            while (chkFontHeight >= 6) {
+                const tmpChnW = fontWidthMap.getFontWidth(chkFontName, chkFontHeight, '宽');
+                const tmpOtherW = fontWidthMap.getFontWidth(chkFontName, chkFontHeight, '窄');
+                let lines = Math.floor((area.Bottom - area.Top) / (chkFontHeight + JV.OUTPUT_OFFSET[JV.OFFSET_IDX_BOTTOM] + JV.OUTPUT_OFFSET[JV.OFFSET_IDX_TOP] + 4));
+                lines = lines === 0 ? 1 : lines; // lines是该单元格可容纳的行数
+                const tmpValues = JpcCommonHelper.splitString(oCell.area, tmpVal, tmpChnW, tmpOtherW);
+                if (lines < tmpValues.length) {
+                    chkFontHeight -= 1;
+                } else {
+                    break;
+                }
+            }
+
             let hasSplitStr = false;
             const splitStrArr = [];
             let accAmt = 0;
@@ -1345,6 +1363,7 @@ JpcFlowTabSrv.prototype.createNew = function() {
                     // 要拆分下数据
                     _splitValues(rCell);
                     setupControl(rCell, controls, true);
+                    rCell[JV.PROP_CONTROL].ShrinkFirst = 'T';
                 }
             }
         }
@@ -1560,7 +1579,7 @@ JpcFlowTabSrv.prototype.createNew = function() {
         }
         me.checkCombineEvent(JV.RUN_TYPE_BEFORE_COMBINE, verticalCombinePos, horizonCombinePos, rst, $CURRENT_RPT, dataObj, me, page);
         me.combinePageCells(rst, verticalCombinePos, horizonCombinePos, controls, fonts);
-        me.checkCombineEvent(JV.RUN_TYPE_AFTER_COMBINE, verticalCombinePos, horizonCombinePos, rst, $CURRENT_RPT, dataObj, me, page);
+        // me.checkCombineEvent(JV.RUN_TYPE_AFTER_COMBINE, verticalCombinePos, horizonCombinePos, rst, $CURRENT_RPT, dataObj, me, page);
         return rst;
     };
     JpcFlowTabResult.checkCombineEvent = function($RUN_TYPE, $VER_COMB_ARRAY, $HOR_COMB_ARRAY, $CURRENT_CELL_ITEMS, $CURRENT_RPT, $CURRENT_DATA, $PARENT, $PAGE) {

+ 58 - 31
app/service/project_screen.js

@@ -1,7 +1,6 @@
 'use strict';
 
 const projectScreenConst = require('../const/project_screen');
-const projectScreenLib = require('../lib/project_screen');
 const Ledger = require('../lib/ledger');
 
 module.exports = app => {
@@ -137,12 +136,9 @@ module.exports = app => {
 
         async getStatusProjects(status, options = {}) {
             const ctx = this.ctx;
-            const user = ctx.session.sessionUser;
             const pid = ctx.session.sessionProject.id;
-            const nodes = await ctx.service.subProject.getSubProject(pid, user.accountId, user.is_admin);
+            const nodes = await ctx.service.projectScreenTender.getDisplayProjects(pid);
             const infos = await ctx.service.subProjInfo.getAllDataByCondition({ where: { project_id: pid, proj_status: status || ['拟建', '在建', '已建'] } });
-            const account = await ctx.service.projectAccount.getDataById(user.accountId);
-            const permission = account ? ctx.service.projectAccount.parseProjectScreenPermission(account.permission) : null;
             const calc = ctx.helper;
             // Preserve decimal precision during aggregation and unit conversion.
             // 元 fields have eight decimal places; conversion to 万元 needs twelve.
@@ -162,24 +158,8 @@ module.exports = app => {
             const budgetTotals = (await Promise.all([ctx.service.budgetGu, ctx.service.budgetGai, ctx.service.budgetYu]
                 .map(service => service.getSumTpByBudgetIds(budgetIds))))
                 .map(rows => new Map(rows.map(row => [String(row.bid), row.total_price])));
-            const allTenders = [];
-            if (selectedNodes.length && (user.is_admin === 1 || (permission && permission.tender && permission.tender.indexOf('2') !== -1))) {
-                allTenders.push(...await ctx.service.tender.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) } }));
-                allTenders.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
-            } else {
-                // Batch projects with identical permission-related page settings.
-                const permissionGroups = new Map();
-                for (const node of selectedNodes) {
-                    const sub = Object.assign({}, node, { page_show: ctx.service.subProject.getPageShow(node.page_show) });
-                    const key = JSON.stringify(['openChangeProject', 'openChangeApply', 'openChangePlan'].map(flag => !!sub.page_show[flag]));
-                    if (!permissionGroups.has(key)) permissionGroups.set(key, { id: [], page_show: sub.page_show });
-                    permissionGroups.get(key).id.push(node.id);
-                }
-                for (const sub of permissionGroups.values()) {
-                    allTenders.push(...await ctx.service.tender.getList('', permission, user.is_admin, '', sub));
-                }
-            }
-            await ctx.service.tenderCache.loadTenderCaches(allTenders, user.accountId);
+            const allTenders = await ctx.service.projectScreenTender.getDisplayTenders(pid, selectedNodes.map(node => node.id));
+            await ctx.service.tenderCache.loadTenderCaches(allTenders, '');
             const allChanges = !options.summary && status === '在建' && allTenders.length ? await ctx.service.change.getAllCheckedChanges(allTenders.map(t => t.id)) : [];
             const allProgress = !options.summary && selectedNodes.length ? await ctx.service.subProjProgress.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) }, orders: [['update_time', 'desc'], ['id', 'asc']] }) : [];
             for (const node of selectedNodes) {
@@ -226,19 +206,19 @@ module.exports = app => {
                 }
                 const parent = nodes.find(n => n.id === node.tree_pid);
                 const progressRows = allProgress.filter(row => row.spid === node.id);
-                const progressPaths = projectScreenLib.getProgressPaths(progressRows);
+                const progressPaths = this.getProgressPaths(progressRows);
                 const latest = progressRows[0];
                 const keys = ['edit', 'submit', 'reply'];
                 const latestEvent = latest && keys.map((key, index) => ({ date: latest[key + '_date'], department: latest[key + '_department'], step: ['编制', '报审', '批复'][index] }))
                     .filter(event => event.date).sort((a, b) => String(b.date).localeCompare(String(a.date)))[0];
-                const stages = projectScreenLib.sortProgressRows(progressRows, progressPaths).filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
+                const stages = this.sortProgressRows(progressRows, progressPaths).filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
                     id: row.id, name: row.name, code: row.code || '',
-                    path: projectScreenLib.formatProgressPath(progressPaths.get(row.id)),
-                    shortPath: projectScreenLib.formatProgressPath(progressPaths.get(row.id), true),
+                    path: this.formatProgressPath(progressPaths.get(row.id)),
+                    shortPath: this.formatProgressPath(progressPaths.get(row.id), true),
                     states: keys.map(key => row[key + '_progress'] === '已完成' ? 0 : row[key + '_progress'] === '进行中' ? 1 : 2),
                     dates: keys.map(key => row[key + '_date'] || ''), departments: keys.map(key => row[key + '_department'] || '')
                 }));
-                result.push({ id: node.id, name: node.name, status: info.proj_status, group: parent ? parent.name : '未分组',
+                result.push({ id: node.id, name: node.name, status: info.proj_status, group: parent ? parent.name : '',
                     folders: projectTree.getAllParents(node).filter(folder => folder.is_folder).map(folder => ({ id: folder.id, name: folder.name })),
                     intro: info.proj_intro || '', date: info.jg_date || '', department: info.jg_department || '',
                     measured: hasMeasured ? toWan(measured, '元') : null,
@@ -252,11 +232,11 @@ module.exports = app => {
             return result;
         }
 
-        async getOverview(planned, building, finished) {
+        async getOverview(projects) {
             const ctx = this.ctx;
             const calc = ctx.helper;
+            const [planned, building, finished] = ['拟建', '在建', '已建'].map(status => projects.filter(project => project.status === status));
             const lists = [planned, building, finished];
-            const projects = [].concat(...lists);
             const present = value => value !== null && value !== undefined && value !== '';
             const rank = (list, amount) => list.map(p => ({ id: p.id, name: p.name, value: amount(p) }))
                 .filter(p => present(p.value)).sort((a, b) => b.value - a.value).slice(0, 10);
@@ -266,7 +246,7 @@ module.exports = app => {
             const dateText = value => value instanceof Date ? [value.getFullYear(), String(value.getMonth() + 1).padStart(2, '0'), String(value.getDate()).padStart(2, '0')].join('-') : String(value || '').slice(0, 10);
             const trees = new Map(projects.map(p => [p.id, rows.filter(row => row.spid === p.id)]));
             const leafColumns = tree => {
-                const paths = projectScreenLib.getProgressPaths(tree);
+                const paths = this.getProgressPaths(tree);
                 return tree.filter(row => Number(row.tree_level) > 1 && Number(row.tree_is_leaf) === 1)
                     .sort((a, b) => a.tree_order - b.tree_order).map(row => {
                         const path = paths.get(row.id);
@@ -327,6 +307,53 @@ module.exports = app => {
                 })
             };
         }
+
+        getProgressPaths(rows) {
+            const nodes = new Map(rows.map(row => [String(row.tree_id), row]));
+            const paths = new Map();
+            for (const row of rows) {
+                const path = [];
+                const visited = new Set();
+                let node = row;
+                while (node && !visited.has(String(node.tree_id))) {
+                    path.unshift(node);
+                    visited.add(String(node.tree_id));
+                    node = nodes.get(String(node.tree_pid));
+                }
+                paths.set(row.id, path);
+            }
+            return paths;
+        }
+
+        sortProgressRows(rows, paths) {
+            return rows.slice().sort((a, b) => {
+                const pathA = paths.get(a.id);
+                const pathB = paths.get(b.id);
+                for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
+                    if (pathA[i].id === pathB[i].id) continue;
+                    return pathA[i].tree_order - pathB[i].tree_order || String(pathA[i].id).localeCompare(String(pathB[i].id));
+                }
+                return pathA.length - pathB.length;
+            });
+        }
+
+        formatProgressPath(path, abbreviated = false) {
+            const names = path.map(node => node.name);
+            if (abbreviated && names.length > 2) return [names[0], '...', names[names.length - 1]].join(' / ');
+            return names.join(' / ');
+        }
+
+        async saveModuleStatus(pid, enabled) {
+            const ctx = this.ctx;
+            const service = ctx.service.projectScreenTender;
+            const pageShow = await service.withTransaction(async transaction => {
+                const settings = service.parseSettings(await service.getProject(pid, transaction, true));
+                settings.pageShow.openProjectScreen = enabled;
+                await service.saveSettings(pid, settings.pageShow, transaction);
+                return settings.pageShow;
+            });
+            ctx.session.sessionProject.page_show = await ctx.service.projectAccount.getPageShow(JSON.stringify(pageShow));
+        }
     }
 
     return ProjectScreen;

+ 269 - 0
app/service/project_screen_tender.js

@@ -0,0 +1,269 @@
+'use strict';
+
+module.exports = app => {
+    class ProjectScreenTender extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'project_screen_tender';
+        }
+
+        async getProject(pid, transaction = null, lock = false) {
+            const db = transaction || this.db;
+            if (!lock) return await db.get(this.ctx.service.project.tableName, { id: pid });
+            const sql = 'SELECT id, page_show, common_json FROM ?? WHERE id = ? FOR UPDATE';
+            const sqlParam = [this.ctx.service.project.tableName, pid];
+            return await db.queryOne(sql, sqlParam);
+        }
+
+        async getProjects(pid, transaction = null) {
+            return await this.ctx.service.subProject.getAllDataByCondition({
+                where: { project_id: pid, is_delete: 0 },
+                orders: [['tree_level', 'asc'], ['tree_order', 'asc'], ['id', 'asc']],
+            }, transaction);
+        }
+
+        async getTenders(pid, spids, transaction = null) {
+            if (!spids.length) return [];
+            return await this.ctx.service.tender.getAllDataByCondition({
+                where: { project_id: pid, spid: spids },
+                orders: [['id', 'asc']],
+            }, transaction);
+        }
+
+        async getList(pid, spid = '', transaction = null) {
+            const where = { pid };
+            if (spid) where.spid = spid;
+            return await (transaction || this.db).select(this.tableName, { where });
+        }
+
+        async saveChanges(pid, changes, transaction) {
+            if (!changes.length) return;
+            await transaction.delete(this.tableName, { pid, tid: changes.map(item => item.tid) });
+            const excluded = changes.filter(item => !item.checked).map(item => ({ pid, spid: item.spid, tid: item.tid }));
+            await this.insertDatasSplit(transaction, this.tableName, excluded);
+        }
+
+        async saveSettings(pid, pageShow, transaction) {
+            return await transaction.update(this.ctx.service.project.tableName, {
+                page_show: JSON.stringify(pageShow),
+            }, { where: { id: pid } });
+        }
+
+        async withTransaction(action) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const result = await action(transaction);
+                await transaction.commit();
+                return result;
+            } catch (error) {
+                await transaction.rollback();
+                throw error;
+            }
+        }
+
+        async syncTenderProject(pid, spid, tid, transaction) {
+            return await transaction.update(this.tableName, { spid }, { where: { pid, tid } });
+        }
+
+        async getChangedTenders(pid, tids, transaction) {
+            if (!tids.length) return [];
+            const sql = 'SELECT id, spid FROM ?? WHERE project_id = ? AND id IN (?) ORDER BY id FOR UPDATE';
+            const sqlParam = [this.ctx.service.tender.tableName, pid, tids];
+            return await transaction.query(sql, sqlParam);
+        }
+
+        async saveScopeSettings(pid, pageShow, commonJson, transaction) {
+            return await transaction.update(this.ctx.service.project.tableName, {
+                page_show: JSON.stringify(pageShow),
+                common_json: JSON.stringify(commonJson),
+            }, { where: { id: pid } });
+        }
+
+        parseSettings(project) {
+            if (!project) throw '项目不存在';
+            const pageShow = project.page_show ? (typeof project.page_show === 'string' ? JSON.parse(project.page_show) : project.page_show) : {};
+            const commonJson = project.common_json ? (typeof project.common_json === 'string' ? JSON.parse(project.common_json) : project.common_json) : {};
+            const excludedProjectIds = Array.isArray(commonJson.projectScreenExcludedProjectIds) ? commonJson.projectScreenExcludedProjectIds :
+                Array.isArray(pageShow.projectScreenExcludedProjectIds) ? pageShow.projectScreenExcludedProjectIds : [];
+            return {
+                pageShow,
+                commonJson,
+                version: Number(pageShow.projectScreenTenderVersion) || 0,
+                excludedProjectIds: excludedProjectIds.filter(id => typeof id === 'string' && id && id.length <= 36),
+            };
+        }
+
+        async getTenderState(pid, transaction) {
+            const settings = this.parseSettings(await this.getProject(pid, transaction));
+            const projects = await this.getProjects(pid, transaction);
+            const spids = projects.filter(project => !project.is_folder).map(project => project.id);
+            const tenders = await this.getTenders(pid, spids, transaction);
+            const excluded = await this.getList(pid, '', transaction);
+            const validIds = new Set(tenders.map(tender => Number(tender.id)));
+            const excludedIds = Array.from(new Set(excluded.map(item => Number(item.tid)).filter(id => validIds.has(id))));
+            return { settings, projects, tenders, excludedIds };
+        }
+
+        async initTenderSettings(pid) {
+            return await this.withTransaction(async transaction => {
+                const state = await this.getTenderState(pid, transaction);
+                const counts = new Map();
+                const excludedCounts = new Map();
+                const excluded = new Set(state.excludedIds);
+                const excludedProjects = new Set(state.settings.excludedProjectIds);
+                state.tenders.forEach(tender => counts.set(tender.spid, (counts.get(tender.spid) || 0) + 1));
+                state.tenders.filter(tender => excluded.has(Number(tender.id))).forEach(tender => {
+                    excludedCounts.set(tender.spid, (excludedCounts.get(tender.spid) || 0) + 1);
+                });
+                return {
+                    version: state.settings.version,
+                    projects: state.projects.map(project => ({
+                        id: project.id, name: project.name, tree_pid: project.tree_pid,
+                        tree_order: project.tree_order, is_folder: project.is_folder,
+                        total: counts.get(project.id) || 0,
+                        excludedTotal: excludedCounts.get(project.id) || 0,
+                        checked: !excludedProjects.has(project.id),
+                    })),
+                    total: state.tenders.length, excludedIds: state.excludedIds,
+                };
+            });
+        }
+
+        async loadTenderSettings(pid, spid) {
+            if (typeof spid !== 'string' || !spid || spid.length > 36) throw '项目参数错误';
+            return await this.withTransaction(async transaction => {
+                const settings = this.parseSettings(await this.getProject(pid, transaction));
+                const projects = await this.getProjects(pid, transaction);
+                const project = projects.find(item => item.id === spid && !item.is_folder);
+                if (!project) throw '项目不存在或不属于当前总项目';
+                const tenders = await this.getTenders(pid, [spid], transaction);
+                const excluded = await this.getList(pid, spid, transaction);
+                const categories = await this.ctx.service.category.getAllCategory(project);
+                return {
+                    version: settings.version, project: { id: project.id, name: project.name },
+                    nodes: this.buildTenderTree(tenders, categories),
+                    excludedIds: excluded.map(item => Number(item.tid)),
+                };
+            });
+        }
+
+        buildTenderTree(tenders, categories) {
+            const levels = categories.filter(item => Number(item.level) > 0).sort((a, b) => a.level - b.level);
+            const root = { id: '', children: [], tree_level: -1 };
+            for (const tender of tenders) {
+                const values = typeof tender.category === 'string' ? JSON.parse(tender.category || '[]') : tender.category;
+                const selected = Array.isArray(values) ? values : [];
+                let parent = root;
+                let path = [];
+                for (let index = 0; index < levels.length; index++) {
+                    const item = levels[index];
+                    const value = selected.find(value => String(value.cid) === String(item.id));
+                    if (value) path.push({ item, valueId: value.value });
+                    else {
+                        if (index === 0) {
+                            path = selected.map(value => ({
+                                item: categories.find(item => String(item.id) === String(value.cid)), valueId: value.value,
+                            })).filter(value => value.item);
+                        }
+                        break;
+                    }
+                }
+                for (const entry of path) {
+                    const value = entry.item.value.find(value => String(value.id) === String(entry.valueId));
+                    if (!value) break;
+                    const id = `${parent.id}/category-${entry.item.id}-${value.id}`;
+                    let folder = parent.children.find(child => child.id === id);
+                    if (!folder) {
+                        folder = { id, parentId: parent.id, name: value.value, sort: value.sort,
+                            tree_level: parent.tree_level + 1, is_folder: true, children: [] };
+                        parent.children.push(folder);
+                    }
+                    parent = folder;
+                }
+                parent.children.push({ id: String(tender.id), tid: Number(tender.id), parentId: parent.id,
+                    name: tender.name, tree_level: parent.tree_level + 1, is_folder: false });
+            }
+            const result = [];
+            const visit = parent => {
+                parent.children.sort((a, b) => {
+                    if (!!a.is_folder !== !!b.is_folder) return a.is_folder ? -1 : 1;
+                    return a.is_folder ? Number(a.sort || 0) - Number(b.sort || 0) : a.name.localeCompare(b.name, 'zh-CN');
+                });
+                for (const node of parent.children) {
+                    const row = Object.assign({}, node);
+                    delete row.children;
+                    result.push(row);
+                    if (node.children) visit(node);
+                }
+            };
+            visit(root);
+            return result;
+        }
+
+        async saveTenderSettings(pid, data) {
+            const projectChanges = data.projectChanges === undefined ? [] : data.projectChanges;
+            if (!Number.isSafeInteger(data.version) || data.version < 0 || !Array.isArray(data.changes) || !Array.isArray(projectChanges)) throw '提交数据错误';
+            const projectIds = new Set();
+            for (const item of projectChanges) {
+                if (!item || typeof item.spid !== 'string' || !item.spid || item.spid.length > 36 || typeof item.checked !== 'boolean') throw '项目参数错误';
+                if (projectIds.has(item.spid)) throw '项目重复提交,请刷新后重试';
+                projectIds.add(item.spid);
+            }
+            const byId = new Map();
+            for (const item of data.changes) {
+                if (!item || !Number.isSafeInteger(item.tid) || item.tid <= 0 || typeof item.checked !== 'boolean' ||
+                    typeof item.spid !== 'string' || !item.spid || item.spid.length > 36) throw '标段参数错误';
+                if (byId.has(item.tid)) throw '标段重复提交,请刷新后重试';
+                byId.set(item.tid, item);
+            }
+            const changes = Array.from(byId.values());
+            const result = await this.withTransaction(async transaction => {
+                const settings = this.parseSettings(await this.getProject(pid, transaction, true));
+                if (settings.version !== data.version) throw '配置已被其他管理员修改,请刷新页面后重新设置';
+                const projects = await this.getProjects(pid, transaction);
+                const spids = new Set(projects.filter(project => !project.is_folder).map(project => project.id));
+                if (changes.concat(projectChanges).some(item => !spids.has(item.spid))) throw '项目不存在或不属于当前总项目';
+                const tenders = await this.getChangedTenders(pid, changes.map(item => item.tid), transaction);
+                const tenderById = new Map(tenders.map(tender => [Number(tender.id), tender]));
+                if (changes.some(item => !tenderById.has(item.tid) || tenderById.get(item.tid).spid !== item.spid)) throw '标段已变更或不属于当前项目,请刷新后重试';
+                await this.saveChanges(pid, changes, transaction);
+                const excludedProjects = new Set(settings.excludedProjectIds.filter(id => spids.has(id)));
+                projectChanges.forEach(item => {
+                    if (item.checked) excludedProjects.delete(item.spid);
+                    else excludedProjects.add(item.spid);
+                });
+                settings.commonJson.projectScreenExcludedProjectIds = Array.from(excludedProjects);
+                delete settings.pageShow.projectScreenExcludedProjectIds;
+                delete settings.pageShow.projectScreenTenderMode;
+                settings.pageShow.projectScreenTenderVersion = settings.version + 1;
+                await this.saveScopeSettings(pid, settings.pageShow, settings.commonJson, transaction);
+                return { version: settings.version + 1, pageShow: settings.pageShow };
+            });
+            this.ctx.session.sessionProject.page_show = await this.ctx.service.projectAccount.getPageShow(JSON.stringify(result.pageShow));
+            return { version: result.version };
+        }
+
+        async getDisplayTenders(pid, spids) {
+            if (!spids.length) return [];
+            return await this.withTransaction(async transaction => {
+                const settings = this.parseSettings(await this.getProject(pid, transaction));
+                const excludedProjects = new Set(settings.excludedProjectIds);
+                const excluded = new Set((await this.getList(pid, '', transaction)).map(item => Number(item.tid)));
+                const tenders = (await this.getTenders(pid, spids.filter(id => !excludedProjects.has(id)), transaction))
+                    .filter(tender => !excluded.has(Number(tender.id)));
+                return tenders.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
+            });
+        }
+
+        async getDisplayProjects(pid) {
+            return await this.withTransaction(async transaction => {
+                const settings = this.parseSettings(await this.getProject(pid, transaction));
+                const excluded = new Set(settings.excludedProjectIds);
+                const projects = await this.getProjects(pid, transaction);
+                return projects.filter(project => project.is_folder || !excluded.has(project.id));
+            });
+        }
+    }
+
+    return ProjectScreenTender;
+};

+ 2 - 0
app/service/sub_project.js

@@ -582,6 +582,8 @@ module.exports = app => {
                 const addTenderId = relaTenderId.filter(x => { return orgRelaTenderId.indexOf(x) < 0});
                 if (removeTenderId.length > 0) await conn.update(this.ctx.service.tender.tableName, { spid: '' }, { where: { id: removeTenderId }});
                 if (addTenderId.length > 0) await conn.update(this.ctx.service.tender.tableName, { spid: data.id }, { where: { id: addTenderId }});
+                if (removeTenderId.length > 0) await this.ctx.service.projectScreenTender.syncTenderProject(subProject.project_id, '', removeTenderId, conn);
+                if (addTenderId.length > 0) await this.ctx.service.projectScreenTender.syncTenderProject(subProject.project_id, data.id, addTenderId, conn);
                 await conn.commit();
                 return data;
             } catch (error) {

+ 4 - 0
app/service/tender.js

@@ -358,6 +358,9 @@ module.exports = app => {
                 }
                 rowData.spid = postData.spid || '';
                 const result = await conn.update(this.tableName, rowData);
+                if (tender.spid !== rowData.spid) {
+                    await this.ctx.service.projectScreenTender.syncTenderProject(tender.project_id, rowData.spid, id, conn);
+                }
                 await conn.commit();
                 return result.affectedRows > 0;
             } catch(err) {
@@ -756,6 +759,7 @@ module.exports = app => {
             const conn = await this.db.beginTransaction();
             try {
                 await conn.updateRows(this.tableName, updateTenders);
+                if (tids.length) await this.ctx.service.projectScreenTender.syncTenderProject(pid, spid, tids, conn);
                 await conn.update(this.ctx.service.subProject.tableName, { id: subProject.id, rela_tender: this._.uniq(orgRelaTenderId).join(',') });
                 await conn.update(this.ctx.service.budget.tableName, { id: subProject.budget_id, rela_tender: this._.uniq(orgRelaTenderId).join(',') });
                 await this._addSubProjPermission(conn, subProject, tids);

Разлика између датотеке није приказан због своје велике величине
+ 8 - 1
app/view/project_screen/guangyuan.ejs


+ 13 - 0
app/view/project_screen/guangyuan_modal.ejs

@@ -11,3 +11,16 @@
         </div>
     </div>
 </div>
+<div id="gy-preview-dialog" class="modal fade gy-preview-dialog" tabindex="-1" role="dialog" aria-labelledby="gy-preview-dialog-title" aria-hidden="true">
+    <div class="modal-dialog modal-xl modal-dialog-scrollable" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 id="gy-preview-dialog-title" class="modal-title">进度预览<span class="small font-weight-normal ml-3">阶段批复日期</span></h5>
+                <button class="close" type="button" data-dismiss="modal" aria-label="关闭进度预览"><span aria-hidden="true">&times;</span></button>
+            </div>
+            <div class="modal-body d-flex overflow-hidden p-3">
+                <div id="gy-preview-dialog-content" class="gy-scroll gy-preview-dialog-table rounded" tabindex="0" role="region" aria-label="全部项目进度表格"></div>
+            </div>
+        </div>
+    </div>
+</div>

+ 60 - 2
app/view/setting/module.ejs

@@ -38,13 +38,13 @@
                     <div class="col-9 h-100">
                         <div class="card h-100 project-screen-panel-card">
                             <div class="card-header d-flex align-items-stretch p-0 project-screen-card-title">
-                                <div class="px-3 py-2">
+                                <div class="px-3 py-2 d-flex align-items-center">
                                     <strong class="project-screen-detail-title">项目大屏</strong>
-                                    <small class="d-block text-muted mt-1">配置大屏页面和模块访问权限</small>
                                 </div>
                                 <nav class="nav ml-auto project-screen-module-tabs" role="tablist">
                                     <a href="javascript:void(0);" class="nav-item nav-link project-screen-tab active" data-tab="config">功能配置</a>
                                     <a href="javascript:void(0);" class="nav-item nav-link project-screen-tab" data-tab="permission">权限管理</a>
+                                    <a href="javascript:void(0);" class="nav-item nav-link project-screen-tab" data-tab="tender">展示范围</a>
                                 </nav>
                             </div>
 
@@ -115,6 +115,64 @@
                                         </div>
                                     </div>
                                 </div>
+                                <div id="project-screen-tender" class="tab-pane h-100" style="display:none;">
+                                    <div class="h-100 d-flex flex-column project-screen-tender-setting">
+                                        <div class="project-screen-tender-columns flex-fill">
+                                            <div class="project-screen-tender-projects d-flex flex-column border-right">
+                                                <div class="p-3 flex-shrink-0">
+                                                    <div class="d-flex align-items-center project-screen-tender-heading">
+                                                        <strong>项目</strong>
+                                                        <div id="project-screen-tender-project-level" class="dropdown ml-auto project-screen-tender-tree-actions"></div>
+                                                    </div>
+                                                    <input id="project-screen-tender-project-search" class="form-control form-control-sm mt-2 project-screen-tender-search" placeholder="搜索项目" aria-label="搜索项目">
+                                                </div>
+                                                <div id="project-screen-tender-project-tree" class="flex-fill overflow-auto px-3 pb-3" aria-label="项目树"></div>
+                                            </div>
+                                            <div class="project-screen-tender-detail d-flex flex-column">
+                                                <div class="p-3 flex-shrink-0">
+                                                    <div class="d-flex align-items-center project-screen-tender-heading">
+                                                        <div class="project-screen-tender-intro">
+                                                            <strong id="project-screen-tender-project-name">请选择项目</strong>
+                                                        </div>
+                                                        <div id="project-screen-tender-tender-level" class="dropdown ml-auto project-screen-tender-tree-actions"></div>
+                                                    </div>
+                                                    <div class="input-group input-group-sm mt-2 project-screen-tender-search">
+                                                        <input id="project-screen-tender-locate" class="form-control form-control-sm" placeholder="输入标段名称定位" aria-label="定位标段名称" disabled>
+                                                        <div class="input-group-append">
+                                                            <span id="project-screen-tender-search-result" class="input-group-text" aria-live="polite">0/0</span>
+                                                            <button id="project-screen-tender-search-prev" type="button" class="btn btn-outline-secondary" aria-label="上一个匹配标段" title="上一个" disabled><i class="fa fa-arrow-up" aria-hidden="true"></i></button>
+                                                            <button id="project-screen-tender-search-next" type="button" class="btn btn-outline-secondary" aria-label="下一个匹配标段" title="下一个" disabled><i class="fa fa-arrow-down" aria-hidden="true"></i></button>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <div id="project-screen-tender-tree-scroll" class="flex-fill overflow-auto px-3 pb-3">
+                                                    <table class="table mb-0 project-screen-tender-list project-screen-tender-tree-table">
+                                                        <thead class="project-screen-sticky-head">
+                                                            <tr>
+                                                                <th><div class="d-flex align-items-center"><span>标段名称</span><label for="project-screen-tender-all" class="ml-auto mb-0 text-nowrap project-screen-tender-select-all">勾选全部</label></div></th>
+                                                                <th class="text-center project-screen-tender-check-cell"><input id="project-screen-tender-all" type="checkbox" disabled></th>
+                                                            </tr>
+                                                        </thead>
+                                                        <tbody id="project-screen-tender-tree"></tbody>
+                                                    </table>
+                                                    <div id="project-screen-tender-message" class="text-muted text-center p-4">请选择项目</div>
+                                                    <div class="text-center"><button id="project-screen-tender-retry" type="button" class="btn btn-outline-primary btn-sm" style="display:none;">重新加载</button></div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <div class="px-3 py-2 border-top d-flex align-items-center flex-shrink-0">
+                                            <div class="project-screen-tender-description mr-3">勾选项目及标段用于大屏展示及统计,新增默认展示,所有大屏共用。</div>
+                                            <div class="ml-auto mr-3 flex-shrink-0 text-right">
+                                                <span id="project-screen-tender-total" aria-live="polite"></span>
+                                                <small id="project-screen-tender-save-status" class="text-muted ml-2"></small>
+                                            </div>
+                                            <div class="text-nowrap flex-shrink-0">
+                                                <button id="project-screen-tender-cancel" type="button" class="btn btn-outline-secondary btn-sm" disabled>取消修改</button>
+                                                <button id="project-screen-tender-save" type="button" class="btn btn-primary btn-sm ml-1" disabled>保存</button>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
                             </div>
                         </div>
                     </div>

+ 9 - 0
config/web.js

@@ -1760,6 +1760,15 @@ const JsFiles = {
                 ],
                 mergeFile: 'sp_setting_data_range',
             },
+            module: {
+                files: [],
+                mergeFiles: [
+                    '/public/js/shares/show_level.js',
+                    '/public/js/project_screen_setting.js',
+                    '/public/js/project_screen_tender_setting.js',
+                ],
+                mergeFile: 'setting_module',
+            },
         },
         profile: {
             cert: {

+ 11 - 0
sql/update.sql

@@ -129,6 +129,17 @@ MODIFY COLUMN `tz_qc_tp` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '决算-变
 ADD COLUMN `un_tz_qc_qty` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '未批复变更令数量' AFTER `tz_qc_tp`,
 ADD COLUMN `un_tz_qc_tp` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '未批复变更令金额' AFTER `un_tz_qc_qty`;
 
+-- 项目大屏:记录不展示的标段,与决策大屏的排除记录独立。
+CREATE TABLE `zh_project_screen_tender` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `pid` int(11) NOT NULL COMMENT '总项目 ID',
+  `spid` varchar(36) NOT NULL COMMENT '具体项目 ID',
+  `tid` int(11) NOT NULL COMMENT '不展示的标段 ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_project_screen_tender_pid_tid` (`pid`, `tid`),
+  KEY `idx_project_screen_tender_pid_spid` (`pid`, `spid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目大屏排除标段';
+
 ------------------------------------
 -- 表数据
 ------------------------------------