project_screen_tender_setting.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. /* global $, postData, toastr, window */
  2. 'use strict';
  3. $(function() {
  4. let initialized = false;
  5. let initializing = false;
  6. let loading = false;
  7. let saving = false;
  8. let stale = false;
  9. let version = 0;
  10. let projects = [];
  11. let selectedProject = '';
  12. let changes = {};
  13. let projectChanges = {};
  14. let cache = {};
  15. let requestId = 0;
  16. let matches = [];
  17. let matchIndex = -1;
  18. let searchTimer = null;
  19. const collapsedProjects = new Set();
  20. let collapsedTenders = new Set();
  21. const prefix = '#project-screen-tender-';
  22. const element = function(name) {
  23. return $(prefix + name);
  24. };
  25. const escapeHtml = function(value) {
  26. return $('<div>').text(value === null || value === undefined ? '' : String(value)).html();
  27. };
  28. const post = function(type, data, success, failure) {
  29. postData('/setting/module/projectscreen', $.extend({ type }, data), success, failure);
  30. };
  31. const currentData = function() {
  32. return cache[selectedProject];
  33. };
  34. const dirty = function() {
  35. return Object.keys(changes).length > 0 || Object.keys(projectChanges).length > 0;
  36. };
  37. const isChecked = function(node) {
  38. if (changes[node.tid]) return changes[node.tid].checked;
  39. return !currentData().excluded.has(node.tid);
  40. };
  41. const projectLevels = createShowLevel(true);
  42. const tenderLevels = createShowLevel(false);
  43. const updateControls = function() {
  44. const disabled = !initialized || initializing || loading || saving || stale;
  45. const tenderDisabled = !canUseTenderControls();
  46. const data = currentData();
  47. const leaves = data && !loading ? data.nodes.filter(node => !node.is_folder) : [];
  48. const checked = leaves.filter(isChecked).length;
  49. element('all').prop('disabled', tenderDisabled || !leaves.length)
  50. .prop('checked', leaves.length > 0 && checked === leaves.length)
  51. .prop('indeterminate', checked > 0 && checked < leaves.length);
  52. element('locate').prop('disabled', tenderDisabled || !leaves.length);
  53. element('search-prev').add(element('search-next')).prop('disabled', tenderDisabled || !matches.length);
  54. element('tree').find('input[data-tid]').each(function() {
  55. const tid = Number($(this).attr('data-tid'));
  56. this.checked = changes[tid] ? changes[tid].checked : !data.excluded.has(tid);
  57. this.disabled = tenderDisabled;
  58. });
  59. element('tree').find('[data-folder]').prop('disabled', tenderDisabled);
  60. const counts = getProjectCounts();
  61. let total = 0;
  62. let selected = 0;
  63. const projectById = new Map(projects.map(project => [project.id, project]));
  64. counts.forEach((count, id) => {
  65. total += count.total;
  66. if (isProjectChecked(projectById.get(id))) selected += count.selected;
  67. });
  68. updateProjectCounts(counts);
  69. updateProjectVisibility();
  70. const leavesProjects = projects.filter(project => !project.is_folder);
  71. const shownProjects = leavesProjects.filter(isProjectChecked).length;
  72. element('total').text(initialized ? `展示项目 ${shownProjects} / ${leavesProjects.length},展示标段 ${selected} / ${total}` : '');
  73. element('save').prop('disabled', disabled || !dirty());
  74. element('cancel').prop('disabled', saving || initializing || !dirty());
  75. projectLevels.refreshMenuVisible();
  76. tenderLevels.refreshMenuVisible();
  77. element('project-level').find('button').prop('disabled', !initialized || initializing || saving || stale || !getTreeLevelCount(true));
  78. element('tender-level').find('button').prop('disabled', tenderDisabled || !getTreeLevelCount(false));
  79. if (!stale) element('save-status').text(saving ? '正在保存…' : dirty() ? '有未保存的修改' : '');
  80. };
  81. const renderProjects = function(revealMatches = false) {
  82. const keyword = element('project-search').val().trim().toLocaleLowerCase();
  83. const byId = new Map(projects.map(project => [project.id, project]));
  84. const included = new Set();
  85. if (keyword) {
  86. projects.filter(project => project.name.toLocaleLowerCase().includes(keyword)).forEach(project => {
  87. const visited = new Set();
  88. let node = project;
  89. while (node && !visited.has(node.id)) {
  90. visited.add(node.id);
  91. included.add(node.id);
  92. if (revealMatches) collapsedProjects.delete(node.id);
  93. node = byId.get(node.tree_pid);
  94. }
  95. });
  96. }
  97. const children = new Map();
  98. projects.forEach(project => {
  99. const parent = byId.has(project.tree_pid) ? project.tree_pid : '';
  100. if (!children.has(parent)) children.set(parent, []);
  101. children.get(parent).push(project);
  102. });
  103. const html = ['<table class="table mb-0 project-screen-tender-list project-screen-tender-project-list"><thead class="project-screen-sticky-head"><tr><th>项目名称</th><th class="text-center project-screen-tender-project-total" title="已勾选标段 / 标段总数">标段范围</th><th class="text-center project-screen-tender-project-check-cell"><input id="project-screen-tender-project-all" type="checkbox" aria-label="展示全部项目" title="展示全部项目" disabled></th></tr></thead><tbody>'];
  104. const visited = new Set();
  105. let firstProject = '';
  106. const visit = function(parent, depth) {
  107. const nodes = (children.get(parent) || []).filter(node => !keyword || included.has(node.id)).sort((a, b) => a.tree_order - b.tree_order);
  108. nodes.forEach((node, index) => {
  109. if (visited.has(node.id) || (keyword && !included.has(node.id))) return;
  110. visited.add(node.id);
  111. const open = !collapsedProjects.has(node.id);
  112. const active = selectedProject === node.id ? ' project-screen-tender-project-active' : '';
  113. html.push(`<tr class="project-screen-tender-project-node${active}${node.is_folder ? ' project-screen-tender-directory project-screen-tender-project-directory' : ''}" data-project="${escapeHtml(node.id)}"><td style="--tree-depth:${depth}"><div class="project-screen-tender-project-label">`);
  114. if (node.is_folder) {
  115. if ((children.get(node.id) || []).length) {
  116. html.push(`<button type="button" class="fold-switch project-screen-tender-fold" data-project-folder="${escapeHtml(node.id)}" aria-expanded="${!!open}" aria-label="展开或收起${escapeHtml(node.name)}" title="${open ? '收起' : '展开'}"><i class="fa ${open ? 'fa-minus-square-o' : 'fa-plus-square-o'}" aria-hidden="true"></i></button>`);
  117. }
  118. html.push(`<i class="fa fa-folder-o" aria-hidden="true"></i><span class="project-screen-tender-project-folder-name" title="${escapeHtml(node.name)}">${escapeHtml(node.name)}</span>`);
  119. } else {
  120. if (!firstProject) firstProject = node.id;
  121. const branch = index === nodes.length - 1 ? '└' : '├';
  122. html.push(`<span class="text-muted project-screen-tender-project-branch" aria-hidden="true">${branch}</span><button type="button" class="btn btn-link btn-sm p-0 project-screen-tender-project-name" aria-pressed="${!!active}" title="${escapeHtml(node.name)}">${escapeHtml(node.name)}</button>`);
  123. }
  124. html.push(`</div></td><td class="text-center project-screen-tender-project-total">${node.is_folder ? '' : `<span class="project-screen-tender-project-count" data-project-count="${escapeHtml(node.id)}"></span>`}</td>`);
  125. html.push(`<td class="text-center project-screen-tender-project-check-cell"><input type="checkbox" ${node.is_folder ? 'data-project-group' : 'data-project-check'}="${escapeHtml(node.id)}" aria-label="展示${escapeHtml(node.name)}${node.is_folder ? '下的全部项目' : ''}" disabled></td></tr>`);
  126. if (open) visit(node.id, depth + 1);
  127. });
  128. };
  129. visit('', 0);
  130. html.push('</tbody></table>');
  131. element('project-tree').html(visited.size ? html.join('') : '<div class="text-muted p-3 text-center">没有匹配的项目</div>');
  132. updateProjectCounts(getProjectCounts());
  133. updateProjectVisibility();
  134. return firstProject;
  135. };
  136. const applyTenderVisibility = function() {
  137. const data = currentData();
  138. if (!data) return;
  139. const byId = new Map(data.nodes.map(node => [node.id, node]));
  140. element('tree').find('tr[data-node]').each(function() {
  141. const node = byId.get($(this).attr('data-node'));
  142. let parent = byId.get(node.parentId);
  143. let visible = true;
  144. while (parent) {
  145. if (collapsedTenders.has(parent.id)) visible = false;
  146. parent = byId.get(parent.parentId);
  147. }
  148. $(this).toggle(visible);
  149. });
  150. element('tree').find('[data-folder]').each(function() {
  151. const open = !collapsedTenders.has($(this).attr('data-folder'));
  152. $(this).attr('aria-expanded', String(open)).attr('title', open ? '收起' : '展开')
  153. .find('i').toggleClass('fa-minus-square-o', open).toggleClass('fa-plus-square-o', !open);
  154. });
  155. };
  156. const locate = function(index) {
  157. if (!matches.length || !canUseTenderControls()) return;
  158. matchIndex = (index + matches.length) % matches.length;
  159. element('tree').find('.project-screen-tender-current-match').removeClass('project-screen-tender-current-match');
  160. const target = $(matches[matchIndex]).addClass('project-screen-tender-current-match');
  161. const row = target.closest('tr');
  162. const byId = new Map(currentData().nodes.map(node => [node.id, node]));
  163. let node = byId.get(row.attr('data-node'));
  164. while (node && node.parentId) {
  165. collapsedTenders.delete(node.parentId);
  166. node = byId.get(node.parentId);
  167. }
  168. applyTenderVisibility();
  169. element('search-result').text(`${matchIndex + 1}/${matches.length}`);
  170. const scroll = element('tree-scroll');
  171. scroll.scrollTop(scroll.scrollTop() + target.offset().top - scroll.offset().top - 35);
  172. };
  173. const searchTenders = function() {
  174. const keyword = element('locate').val().trim().toLocaleLowerCase();
  175. element('tree').find('.project-screen-tender-name').each(function() {
  176. const name = $(this).attr('data-name');
  177. const lower = name.toLocaleLowerCase();
  178. const html = [];
  179. let start = 0;
  180. let found = keyword ? lower.indexOf(keyword) : -1;
  181. while (found >= 0) {
  182. html.push(escapeHtml(name.slice(start, found)));
  183. html.push(`<mark class="project-screen-tender-match">${escapeHtml(name.slice(found, found + keyword.length))}</mark>`);
  184. start = found + keyword.length;
  185. found = lower.indexOf(keyword, start);
  186. }
  187. html.push(escapeHtml(name.slice(start)));
  188. $(this).html(html.join(''));
  189. });
  190. matches = element('tree').find('.project-screen-tender-match').toArray();
  191. matchIndex = -1;
  192. element('search-result').text(`0/${matches.length}`);
  193. element('search-prev').add(element('search-next')).prop('disabled', !matches.length || !canUseTenderControls());
  194. if (matches.length && canUseTenderControls()) locate(0);
  195. };
  196. const renderTenders = function() {
  197. const data = currentData();
  198. const html = [];
  199. const lastChildren = new Map();
  200. if (data) {
  201. data.nodes.forEach(node => lastChildren.set(node.parentId, node.id));
  202. data.nodes.forEach(node => {
  203. html.push(`<tr${node.is_folder ? ' class="project-screen-tender-directory"' : ''} data-node="${escapeHtml(node.id)}"><td style="--tree-depth:${node.tree_level}">`);
  204. if (node.is_folder) {
  205. const name = escapeHtml(node.name);
  206. html.push(`<button type="button" class="fold-switch mr-1 project-screen-tender-fold" data-folder="${escapeHtml(node.id)}" aria-expanded="true" aria-label="展开或收起${name}" title="收起"><i class="fa fa-minus-square-o" aria-hidden="true"></i></button> <i class="fa fa-folder-o mr-1" aria-hidden="true"></i>${node.tree_level === 0 ? `<b>${name}</b>` : name}`);
  207. } else {
  208. const branch = lastChildren.get(node.parentId) === node.id ? '└' : '├';
  209. html.push(`<span class="text-muted mr-2">${branch}</span><span class="project-screen-tender-name text-primary" data-name="${escapeHtml(node.name)}">${escapeHtml(node.name)}</span>`);
  210. }
  211. html.push('</td><td class="text-center project-screen-tender-check-cell">');
  212. if (!node.is_folder) html.push(`<input type="checkbox" data-tid="${node.tid}" aria-label="展示${escapeHtml(node.name)}">`);
  213. html.push('</td></tr>');
  214. });
  215. }
  216. element('tree').html(html.join(''));
  217. element('message').text('该项目暂无标段').toggle(!data || !data.nodes.length);
  218. applyTenderVisibility();
  219. updateControls();
  220. searchTenders();
  221. };
  222. const markStale = function() {
  223. stale = true;
  224. element('save-status').text('配置已更新,请重新加载配置后设置');
  225. element('retry').text('重新加载配置').show();
  226. updateControls();
  227. };
  228. const loadProject = function(spid, reload) {
  229. if (saving) return;
  230. selectedProject = spid;
  231. collapsedTenders = cache[spid] ? cache[spid].collapsed : new Set();
  232. clearTimeout(searchTimer);
  233. matches = [];
  234. matchIndex = -1;
  235. element('locate').val('');
  236. element('search-result').text('0/0');
  237. element('retry').hide();
  238. const project = projects.find(project => project.id === spid);
  239. element('project-name').text(project ? project.name : '请选择项目').attr('title', project ? project.name : '');
  240. renderProjects();
  241. const id = ++requestId;
  242. if (cache[spid] && !reload) {
  243. loading = false;
  244. renderTenders();
  245. return;
  246. }
  247. loading = true;
  248. element('tree').empty();
  249. element('message').text('正在加载全部标段…').show();
  250. updateControls();
  251. post('tenderload', { spid }, function(result) {
  252. if (id !== requestId) return;
  253. loading = false;
  254. if (result.version !== version) {
  255. element('message').text('配置已发生变化,请重新加载').show();
  256. markStale();
  257. return;
  258. }
  259. cache[spid] = { nodes: result.nodes, excluded: new Set(result.excludedIds), collapsed: collapsedTenders };
  260. const leaves = result.nodes.filter(node => !node.is_folder);
  261. project.total = leaves.length;
  262. project.excludedTotal = leaves.filter(node => cache[spid].excluded.has(node.tid)).length;
  263. renderProjects();
  264. renderTenders();
  265. }, function() {
  266. if (id !== requestId) return;
  267. loading = false;
  268. element('message').text('标段加载失败,请重试').show();
  269. element('retry').text('重新加载').show();
  270. updateControls();
  271. });
  272. };
  273. const initialize = function() {
  274. if (initializing || saving) return;
  275. initializing = true;
  276. loading = false;
  277. clearTimeout(searchTimer);
  278. matches = [];
  279. matchIndex = -1;
  280. element('locate').val('');
  281. element('search-result').text('0/0');
  282. ++requestId;
  283. element('message').text('正在加载项目配置…').show();
  284. element('retry').hide();
  285. updateControls();
  286. post('tenderinit', {}, function(result) {
  287. initializing = false;
  288. initialized = true;
  289. stale = false;
  290. version = result.version;
  291. projects = result.projects;
  292. cache = {};
  293. changes = {};
  294. projectChanges = {};
  295. selectedProject = projects.some(project => project.id === selectedProject && !project.is_folder) ? selectedProject : '';
  296. if (!selectedProject) {
  297. collapsedProjects.clear();
  298. element('project-search').val('');
  299. }
  300. const firstProject = renderProjects();
  301. if (!selectedProject) selectedProject = firstProject;
  302. if (selectedProject) loadProject(selectedProject);
  303. else {
  304. loading = false;
  305. element('tree').empty();
  306. element('project-name').text('暂无项目').attr('title', '');
  307. element('message').text('当前总项目尚未添加项目').show();
  308. updateControls();
  309. }
  310. }, function() {
  311. initializing = false;
  312. element('message').text('配置加载失败,请重试').show();
  313. element('retry').text('重新加载配置').show();
  314. updateControls();
  315. });
  316. };
  317. const setChange = function(tid, checked) {
  318. const original = !currentData().excluded.has(tid);
  319. if (checked === original) delete changes[tid];
  320. else changes[tid] = { spid: selectedProject, tid, checked };
  321. };
  322. $('.project-screen-tab[data-tab="tender"]').click(function() {
  323. if (!initialized) initialize();
  324. });
  325. element('project-tree').on('click', '[data-project]', function(event) {
  326. if (saving || initializing || stale) return;
  327. if ($(event.target).is('input[type="checkbox"]')) return;
  328. const id = $(this).attr('data-project');
  329. const project = projects.find(project => project.id === id);
  330. if (project.is_folder) {
  331. if (!$(event.target).closest('[data-project-folder]').length) return;
  332. if (collapsedProjects.has(id)) collapsedProjects.delete(id);
  333. else collapsedProjects.add(id);
  334. renderProjects();
  335. } else loadProject(id);
  336. });
  337. element('project-search').on('input', function() { renderProjects(true); });
  338. element('tree').on('click', '[data-folder]', function() {
  339. if (!canUseTenderControls()) return;
  340. const id = $(this).attr('data-folder');
  341. if (collapsedTenders.has(id)) collapsedTenders.delete(id);
  342. else collapsedTenders.add(id);
  343. applyTenderVisibility();
  344. });
  345. element('tree').on('change', 'input[data-tid]', function() {
  346. if (!canUseTenderControls()) return updateControls();
  347. setChange(Number($(this).attr('data-tid')), this.checked);
  348. updateControls();
  349. });
  350. element('all').change(function() {
  351. if (!canUseTenderControls()) return updateControls();
  352. const checked = this.checked;
  353. currentData().nodes.filter(node => !node.is_folder).forEach(node => setChange(node.tid, checked));
  354. updateControls();
  355. });
  356. element('locate').on('input', function() {
  357. clearTimeout(searchTimer);
  358. searchTimer = setTimeout(searchTenders, 200);
  359. }).on('keydown', function(event) {
  360. if (event.keyCode === 13) {
  361. event.preventDefault();
  362. locate(matchIndex + (event.shiftKey ? -1 : 1));
  363. }
  364. });
  365. element('search-prev').click(function() { locate(matchIndex - 1); });
  366. element('search-next').click(function() { locate(matchIndex + 1); });
  367. element('retry').click(function() {
  368. if (!initialized || stale) {
  369. // eslint-disable-next-line no-alert
  370. if (dirty() && !window.confirm('重新加载会取消未保存的修改,是否继续?')) return;
  371. initialize();
  372. } else loadProject(selectedProject, true);
  373. });
  374. element('cancel').click(function() {
  375. changes = {};
  376. projectChanges = {};
  377. updateControls();
  378. });
  379. element('save').click(function() {
  380. if (!dirty() || saving || loading || stale) return;
  381. const submitted = Object.values(changes);
  382. const submittedProjects = Object.values(projectChanges);
  383. saving = true;
  384. updateControls();
  385. post('tendersave', { version, changes: submitted, projectChanges: submittedProjects }, function(result) {
  386. submitted.forEach(change => {
  387. const data = cache[change.spid];
  388. if (change.checked) data.excluded.delete(change.tid);
  389. else data.excluded.add(change.tid);
  390. });
  391. projects.forEach(project => {
  392. if (cache[project.id]) project.excludedTotal = cache[project.id].nodes.filter(node => !node.is_folder && cache[project.id].excluded.has(node.tid)).length;
  393. });
  394. submittedProjects.forEach(change => { projects.find(project => project.id === change.spid).checked = change.checked; });
  395. version = result.version;
  396. changes = {};
  397. projectChanges = {};
  398. saving = false;
  399. updateControls();
  400. toastr.success('项目大屏展示范围保存成功');
  401. }, function(message) {
  402. saving = false;
  403. if (typeof message === 'string' && message.includes('配置已被其他管理员修改')) markStale();
  404. updateControls();
  405. });
  406. });
  407. $(window).on('beforeunload.projectScreenTender', function(event) {
  408. if (!dirty()) return;
  409. event.preventDefault();
  410. event.originalEvent.returnValue = '展示范围尚未保存';
  411. return '展示范围尚未保存';
  412. });
  413. element('project-tree').on('change', 'input[data-project-check], input[data-project-group], #project-screen-tender-project-all', function() {
  414. if (!initialized || initializing || saving || stale) return;
  415. const id = $(this).attr('data-project-check');
  416. const group = $(this).attr('data-project-group');
  417. const targets = id ? projects.filter(project => project.id === id && !project.is_folder) :
  418. group ? getProjectGroups().get(group) || [] : projects.filter(project => !project.is_folder);
  419. targets.forEach(project => setProjectChange(project, this.checked));
  420. updateControls();
  421. });
  422. updateControls();
  423. function setTreeLevel(projectTree, tag) {
  424. const data = currentData();
  425. if (!initialized || initializing || saving || stale) return;
  426. if (!projectTree && !canUseTenderControls()) return;
  427. const level = tag === 'last' ? Infinity : Number(tag);
  428. if (!(level > 0)) return;
  429. const depths = getTreeDepths(projectTree);
  430. const collapsed = projectTree ? collapsedProjects : collapsedTenders;
  431. collapsed.clear();
  432. (projectTree ? projects : data.nodes).filter(node => node.is_folder && depths.get(node.id) >= level)
  433. .forEach(node => collapsed.add(node.id));
  434. if (projectTree) renderProjects();
  435. else applyTenderVisibility();
  436. }
  437. function getTreeDepths(projectTree) {
  438. if (!projectTree) {
  439. const data = currentData();
  440. return new Map((data ? data.nodes : []).map(node => [node.id, Number(node.tree_level) + 1]));
  441. }
  442. const byId = new Map(projects.map(project => [project.id, project]));
  443. const depths = new Map();
  444. projects.forEach(project => {
  445. const visited = new Set([project.id]);
  446. let parent = byId.get(project.tree_pid);
  447. let depth = 1;
  448. while (parent && !visited.has(parent.id)) {
  449. visited.add(parent.id);
  450. depth++;
  451. parent = byId.get(parent.tree_pid);
  452. }
  453. depths.set(project.id, depth);
  454. });
  455. return depths;
  456. }
  457. function getTreeLevelCount(projectTree) {
  458. let maxDepth = 0;
  459. getTreeDepths(projectTree).forEach(depth => { maxDepth = Math.max(maxDepth, depth); });
  460. return Math.max(0, maxDepth - 1);
  461. }
  462. function createShowLevel(projectTree) {
  463. const name = projectTree ? 'project-level' : 'tender-level';
  464. const menu = $.cs_showLevel({
  465. selector: prefix + name,
  466. buttonId: `project-screen-tender-${name}-toggle`,
  467. levels: [
  468. { type: 'sort', count: 5, visible_count() { return getTreeLevelCount(projectTree); } },
  469. { type: 'last', title: '最底层', visible() { return getTreeLevelCount(projectTree) > 0; } },
  470. ],
  471. showLevel(tag) { setTreeLevel(projectTree, tag); },
  472. });
  473. menu.initShowLevel();
  474. element(name).find('button').attr('aria-label', projectTree ? '项目显示层级' : '标段显示层级');
  475. return menu;
  476. }
  477. function getProjectCounts() {
  478. const counts = new Map();
  479. projects.filter(project => !project.is_folder).forEach(project => {
  480. const total = Number(project.total) || 0;
  481. counts.set(project.id, { total, selected: total - (Number(project.excludedTotal) || 0) });
  482. });
  483. Object.values(changes).forEach(change => {
  484. const count = counts.get(change.spid);
  485. if (count) count.selected += change.checked ? 1 : -1;
  486. });
  487. return counts;
  488. }
  489. function updateProjectCounts(counts) {
  490. element('project-tree').find('[data-project-count]').each(function() {
  491. const count = counts.get($(this).attr('data-project-count'));
  492. if (!count) return;
  493. const description = `已勾选 ${count.selected} 个,共 ${count.total} 个标段`;
  494. $(this).text(`${count.selected} / ${count.total}`).attr('title', description).attr('aria-label', description);
  495. });
  496. }
  497. function isProjectChecked(project) {
  498. if (!project) return false;
  499. return projectChanges[project.id] ? projectChanges[project.id].checked : project.checked !== false;
  500. }
  501. function setProjectChange(project, checked) {
  502. if (checked === (project.checked !== false)) delete projectChanges[project.id];
  503. else projectChanges[project.id] = { spid: project.id, checked };
  504. }
  505. function getProjectGroups() {
  506. const byId = new Map(projects.map(project => [project.id, project]));
  507. const groups = new Map(projects.filter(project => project.is_folder).map(project => [project.id, []]));
  508. projects.filter(project => !project.is_folder).forEach(project => {
  509. const visited = new Set([project.id]);
  510. let parent = byId.get(project.tree_pid);
  511. while (parent && !visited.has(parent.id)) {
  512. visited.add(parent.id);
  513. if (groups.has(parent.id)) groups.get(parent.id).push(project);
  514. parent = byId.get(parent.tree_pid);
  515. }
  516. });
  517. return groups;
  518. }
  519. function updateProjectVisibility() {
  520. const disabled = !initialized || initializing || saving || stale;
  521. const byId = new Map(projects.map(project => [project.id, project]));
  522. const groups = getProjectGroups();
  523. element('project-tree').find('input[data-project-check]').each(function() {
  524. this.checked = isProjectChecked(byId.get($(this).attr('data-project-check')));
  525. this.disabled = disabled;
  526. });
  527. element('project-tree').find('input[data-project-group], #project-screen-tender-project-all').each(function() {
  528. const group = $(this).attr('data-project-group');
  529. const targets = group ? groups.get(group) || [] : projects.filter(project => !project.is_folder);
  530. const checked = targets.filter(isProjectChecked).length;
  531. this.checked = targets.length > 0 && checked === targets.length;
  532. this.indeterminate = checked > 0 && checked < targets.length;
  533. this.disabled = disabled || !targets.length;
  534. });
  535. }
  536. function canUseTenderControls() {
  537. return initialized && !initializing && !loading && !saving && !stale && !!currentData() &&
  538. isProjectChecked(projects.find(project => project.id === selectedProject));
  539. }
  540. });