project-gantt.vue 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052
  1. <template>
  2. <div class="project-gantt" :class="{ 'is-fullscreen': isFullscreen }">
  3. <div class="view-switch">
  4. <div class="view-switch-left">
  5. <el-button
  6. size="mini"
  7. :type="viewMode === 'gantt' ? 'primary' : 'default'"
  8. @click="handleViewSwitch('gantt')"
  9. >
  10. 甘特图
  11. </el-button>
  12. <el-button
  13. size="mini"
  14. :type="viewMode === 'calendar' ? 'primary' : 'default'"
  15. @click="handleViewSwitch('calendar')"
  16. >
  17. 工作日历
  18. </el-button>
  19. <slot name="toolbar"></slot>
  20. <gantt-filter-bar
  21. v-show="viewMode === 'gantt'"
  22. class="gantt-filter-inline"
  23. :query="ganttQuery"
  24. :plan-code-label="
  25. resourceLabelMode === 'order'
  26. ? '\u751f\u4ea7\u8ba2\u5355\u53f7'
  27. : '\u8ba1\u5212\u7f16\u53f7'
  28. "
  29. @update:query="ganttQuery = $event"
  30. @apply="applyGanttFilters"
  31. @reset="resetGanttFilters"
  32. />
  33. <gantt-filter-bar
  34. v-show="viewMode === 'calendar'"
  35. class="gantt-filter-inline"
  36. :query="calendarQuery"
  37. :plan-code-label="
  38. resourceLabelMode === 'order'
  39. ? '\u751f\u4ea7\u8ba2\u5355\u53f7'
  40. : '\u8ba1\u5212\u7f16\u53f7'
  41. "
  42. @update:query="calendarQuery = $event"
  43. @apply="applyCalendarFilters"
  44. @reset="resetCalendarFilters"
  45. />
  46. </div>
  47. <el-button
  48. size="mini"
  49. :icon="isFullscreen ? 'el-icon-close' : 'el-icon-full-screen'"
  50. @click="toggleFullscreen"
  51. >
  52. {{ isFullscreen ? '退出全屏' : '全屏显示' }}
  53. </el-button>
  54. </div>
  55. <div class="project-gantt__body">
  56. <resource-panel
  57. :is-fullscreen="isFullscreen"
  58. :resource-tabs="resourceTabs"
  59. :active-tab="activeTab"
  60. :keyword="keyword"
  61. :filtered-resource-list="filteredResourceList"
  62. :selected-resource="selectedResource"
  63. @tab-change="handleResourceTabChange"
  64. @update:keyword="keyword = $event"
  65. @reset="resetResourceFilter"
  66. @select="handleResourceSelect"
  67. />
  68. <div v-show="viewMode === 'gantt'" class="gantt-view">
  69. <gantt-filter-bar
  70. v-show="!isFullscreen"
  71. :query="ganttQuery"
  72. :plan-code-label="
  73. resourceLabelMode === 'order'
  74. ? '\u751f\u4ea7\u8ba2\u5355\u53f7'
  75. : '\u8ba1\u5212\u7f16\u53f7'
  76. "
  77. @update:query="ganttQuery = $event"
  78. @apply="applyGanttFilters"
  79. @reset="resetGanttFilters"
  80. />
  81. <div ref="gantt_scrollbar" class="gantt-box">
  82. <div ref="gantt" class="gantt-container"></div>
  83. </div>
  84. </div>
  85. <div v-show="viewMode === 'calendar'" class="calendar-view">
  86. <gantt-filter-bar
  87. v-show="!isFullscreen"
  88. :query="calendarQuery"
  89. :plan-code-label="
  90. resourceLabelMode === 'order'
  91. ? '\u751f\u4ea7\u8ba2\u5355\u53f7'
  92. : '\u8ba1\u5212\u7f16\u53f7'
  93. "
  94. @update:query="calendarQuery = $event"
  95. @apply="applyCalendarFilters"
  96. @reset="resetCalendarFilters"
  97. />
  98. <calendar-table
  99. :calendar-month-groups="calendarMonthGroups"
  100. :calendar-days="calendarDays"
  101. :calendar-display-rows="calendarDisplayRows"
  102. :group-label="calendarGroupLabel"
  103. @show-tooltip="showCalendarTooltip"
  104. @move-tooltip="moveHoverTooltip"
  105. @hide-tooltip="hideHoverTooltip"
  106. />
  107. </div>
  108. </div>
  109. <div
  110. ref="hoverTooltip"
  111. v-show="hoverTooltip.visible"
  112. class="gantt-hover-tooltip"
  113. :style="hoverTooltip.style"
  114. v-html="hoverTooltip.content"
  115. ></div>
  116. </div>
  117. </template>
  118. <script>
  119. import { Gantt } from './gantt';
  120. import CalendarTable from './components/CalendarTable.vue';
  121. import GanttFilterBar from './components/GanttFilterBar.vue';
  122. import ResourcePanel from './components/ResourcePanel.vue';
  123. import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
  124. import {
  125. DAY_MS,
  126. PLAN_COLOR_PALETTE,
  127. RESOURCE_TABS,
  128. WEEK_MAP
  129. } from './project-gantt.constants';
  130. import {
  131. buildCalendarTooltipHtml,
  132. buildTooltipHtml,
  133. createHoverTooltipState,
  134. createSearchQuery,
  135. endOfMonth,
  136. escapeHtml,
  137. formatDateKey,
  138. parseDate,
  139. resolvePlanColor,
  140. resolveTaskColor,
  141. startOfDay,
  142. startOfMonth
  143. } from './project-gantt.utils';
  144. let gantt = new Gantt();
  145. export default {
  146. name: 'gantt',
  147. components: {
  148. CalendarTable,
  149. GanttFilterBar,
  150. ResourcePanel
  151. },
  152. props: {
  153. ganttTasks: {
  154. type: Array,
  155. default: () => {
  156. return [];
  157. }
  158. },
  159. planList: {
  160. type: Array,
  161. default: () => {
  162. return [];
  163. }
  164. },
  165. calendarSourceData: {
  166. type: Array,
  167. default: () => {
  168. return [];
  169. }
  170. },
  171. defaultTab: {
  172. type: String,
  173. default: 'team'
  174. },
  175. resourceTabsConfig: {
  176. type: Array,
  177. default: () => RESOURCE_TABS
  178. },
  179. resourceLabelMode: {
  180. type: String,
  181. default: 'plan'
  182. },
  183. dateRange: {
  184. type: Array,
  185. default: () => {
  186. return null;
  187. }
  188. }
  189. },
  190. watch: {
  191. defaultTab(val) {
  192. if (val && val !== this.activeTab) {
  193. this.activeTab = val;
  194. }
  195. },
  196. ganttTasks: {
  197. handler(val) {
  198. this.initData();
  199. },
  200. deep: true
  201. },
  202. activeTab() {
  203. this.selectedResource = '';
  204. this.initData();
  205. },
  206. selectedResource() {
  207. this.initData();
  208. },
  209. viewMode() {
  210. this.syncGanttVisibility();
  211. },
  212. dateRange: {
  213. handler(val) {
  214. if (val && val.length === 2) {
  215. this.initData();
  216. }
  217. },
  218. deep: true
  219. }
  220. },
  221. data() {
  222. return {
  223. ganttData: [],
  224. activeTab: this.defaultTab,
  225. selectedResource: '',
  226. keyword: '',
  227. viewMode: 'gantt',
  228. ganttQuery: createSearchQuery(),
  229. ganttFilters: createSearchQuery(),
  230. calendarQuery: createSearchQuery(),
  231. calendarFilters: createSearchQuery(),
  232. calendarExpand: true,
  233. hoverTooltip: createHoverTooltipState(),
  234. isFullscreen: false
  235. };
  236. },
  237. computed: {
  238. resourceTabs() {
  239. return this.resourceTabsConfig && this.resourceTabsConfig.length
  240. ? this.resourceTabsConfig
  241. : RESOURCE_TABS;
  242. },
  243. resourceList() {
  244. const source = this.ganttTasks
  245. .map((item) => this.getActiveResourceName(item))
  246. .filter(Boolean);
  247. const result = [];
  248. const cache = new Set();
  249. source.forEach((name, index) => {
  250. const label = (name || '').trim();
  251. if (!label || cache.has(label)) {
  252. return;
  253. }
  254. cache.add(label);
  255. result.push({
  256. key: `${this.activeTab}-${index}-${label}`,
  257. label
  258. });
  259. });
  260. return result;
  261. },
  262. filteredResourceList() {
  263. const keyword = this.keyword.trim().toLowerCase();
  264. if (!keyword) {
  265. return this.resourceList;
  266. }
  267. return this.resourceList.filter((item) =>
  268. item.label.toLowerCase().includes(keyword)
  269. );
  270. },
  271. filteredGanttTasks() {
  272. return this.buildFilteredGanttTasks();
  273. },
  274. planMetaByCode() {
  275. return this.planList.reduce((result, item) => {
  276. const key = item.code || item.planCode || item.id;
  277. if (!key || result[key]) {
  278. return result;
  279. }
  280. result[key] = item;
  281. return result;
  282. }, {});
  283. },
  284. orderPlanColorMap() {
  285. if (this.resourceLabelMode !== 'order') {
  286. return {};
  287. }
  288. const result = {};
  289. const planKeys = [];
  290. const assignColor = (primaryKey, aliases = []) => {
  291. if (!primaryKey) {
  292. return;
  293. }
  294. const existingKey = [primaryKey, ...aliases].find(
  295. (key) => key && result[key]
  296. );
  297. const color =
  298. (existingKey && result[existingKey]) ||
  299. PLAN_COLOR_PALETTE[planKeys.length % PLAN_COLOR_PALETTE.length];
  300. if (!existingKey) {
  301. planKeys.push(primaryKey);
  302. }
  303. [primaryKey, ...aliases].forEach((key) => {
  304. if (key) {
  305. result[key] = color;
  306. }
  307. });
  308. };
  309. this.ganttTasks.forEach((item) => {
  310. assignColor(item.planId || item.planCode, [
  311. item.planId,
  312. item.planCode
  313. ]);
  314. });
  315. return result;
  316. },
  317. calendarTaskSource() {
  318. if (
  319. Array.isArray(this.calendarSourceData) &&
  320. this.calendarSourceData.length
  321. ) {
  322. return this.calendarSourceData
  323. .map((item, index) => {
  324. const resourceList = Array.isArray(item.resourceList)
  325. ? item.resourceList
  326. : [];
  327. const resourceName = item.resourceName || item.name || '';
  328. const resourceCode = item.resourceCode || item.code || '';
  329. const dayEntryMap = {};
  330. const periodDates = [];
  331. const searchEntries = [];
  332. resourceList.forEach((resourceItem, resourceIndex) => {
  333. searchEntries.push({
  334. planCode: resourceItem.planCode || '',
  335. productionOrderNumber:
  336. resourceItem.productionOrderNumber ||
  337. resourceItem.apsWorkOrderCode ||
  338. resourceItem.workOrderCode ||
  339. '',
  340. productName: resourceItem.productName || '',
  341. routingName:
  342. resourceItem.routingName ||
  343. resourceItem.produceRoutingName ||
  344. '',
  345. productCode: resourceItem.productCode || '',
  346. specification: resourceItem.specification || '',
  347. modeType: resourceItem.modeType || ''
  348. });
  349. const periods = Array.isArray(resourceItem.planTimePeriod)
  350. ? resourceItem.planTimePeriod
  351. : [];
  352. periods.forEach((period, periodIndex) => {
  353. const dayKey = String(period.today || '').trim();
  354. if (!dayKey) {
  355. return;
  356. }
  357. const parsedDate = parseDate(dayKey);
  358. if (parsedDate) {
  359. periodDates.push(parsedDate);
  360. }
  361. if (!dayEntryMap[dayKey]) {
  362. dayEntryMap[dayKey] = [];
  363. }
  364. dayEntryMap[dayKey].push({
  365. id: `${item.resourceId || 'resource'}-${
  366. item.taskId || index
  367. }-${resourceItem.planId || resourceIndex}-${periodIndex}`,
  368. planId: resourceItem.planId || '',
  369. planCode:
  370. resourceItem.planCode || resourceItem.productCode || '',
  371. productionOrderNumber:
  372. resourceItem.productionOrderNumber ||
  373. resourceItem.apsWorkOrderCode ||
  374. resourceItem.workOrderCode ||
  375. '',
  376. displayCode:
  377. resourceItem.productionOrderNumber ||
  378. resourceItem.apsWorkOrderCode ||
  379. resourceItem.workOrderCode ||
  380. resourceItem.planCode ||
  381. resourceItem.productCode ||
  382. '',
  383. qty: this.getCalendarEntryQuantity(
  384. item,
  385. resourceItem,
  386. period
  387. ),
  388. productName: resourceItem.productName || '',
  389. routingName:
  390. resourceItem.routingName ||
  391. resourceItem.produceRoutingName ||
  392. '',
  393. productCode: resourceItem.productCode || '',
  394. specification: resourceItem.specification || '',
  395. batchNo: resourceItem.batchNo || '',
  396. modeType: resourceItem.modeType || '',
  397. resourceName,
  398. resourceCode
  399. });
  400. });
  401. });
  402. if (!periodDates.length) {
  403. return null;
  404. }
  405. periodDates.sort((a, b) => a.getTime() - b.getTime());
  406. return {
  407. id: `${item.resourceId || 'resource'}-${item.taskId || index}`,
  408. code: item.taskId || item.sourceTaskId || '',
  409. groupLabel: this.getActiveCalendarGroupName(item),
  410. productionLineName:
  411. this.getCalendarProductionLineName(item, resourceList) ||
  412. this.getPlanProductionLineName(resourceList[0]?.planCode) ||
  413. '',
  414. factoryName: '',
  415. routingName:
  416. resourceList[0]?.routingName ||
  417. resourceList[0]?.produceRoutingName ||
  418. '',
  419. taskLabel: this.getCalendarTaskLabel(item),
  420. startDate: periodDates[0],
  421. endDate: periodDates[periodDates.length - 1],
  422. searchEntries,
  423. dayEntryMap
  424. };
  425. })
  426. .filter(Boolean)
  427. .sort((a, b) => {
  428. if (a.groupLabel !== b.groupLabel) {
  429. return a.groupLabel.localeCompare(b.groupLabel, 'zh-CN');
  430. }
  431. return a.startDate.getTime() - b.startDate.getTime();
  432. });
  433. }
  434. return this.ganttTasks
  435. .filter((item) => item.start_date && item.end_date)
  436. .map((item, index) => {
  437. const planMeta = this.planMetaByCode[item.code] || {};
  438. const startDate = parseDate(item.start_date);
  439. const endDate = parseDate(item.end_date);
  440. if (!startDate || !endDate) {
  441. return null;
  442. }
  443. return {
  444. id: item.id || `${item.code || 'task'}-${index}`,
  445. code: item.code || '',
  446. groupLabel:
  447. this.getActiveResourceName(item) ||
  448. item.teamName ||
  449. planMeta.teamName ||
  450. '\u672a\u5206\u7ec4',
  451. productionLineName:
  452. item.productionLineName || planMeta.productionLineName || '',
  453. factoryName: planMeta.factoryName || '',
  454. routingName:
  455. planMeta.produceRoutingName ||
  456. planMeta.routingName ||
  457. planMeta.routeName ||
  458. '',
  459. taskLabel:
  460. item.name || item.text || item.code || `任务${index + 1}`,
  461. barLabel:
  462. [item.title, item.name || item.text || item.code]
  463. .filter(Boolean)
  464. .join(' / ') || `\u4efb\u52a1${index + 1}`,
  465. color: item.color || resolveTaskColor(item.type),
  466. startDate,
  467. endDate
  468. };
  469. })
  470. .filter(Boolean)
  471. .sort((a, b) => {
  472. if (a.groupLabel !== b.groupLabel) {
  473. return a.groupLabel.localeCompare(b.groupLabel, 'zh-CN');
  474. }
  475. return a.startDate.getTime() - b.startDate.getTime();
  476. });
  477. },
  478. calendarFilteredTasks() {
  479. const hasFilters =
  480. this.calendarFilters.routingName ||
  481. this.calendarFilters.productName ||
  482. this.calendarFilters.planCode;
  483. const resourceFilteredTasks = this.calendarTaskSource.filter((item) =>
  484. this.matchesCalendarResourceFilter(item)
  485. );
  486. if (!hasFilters) {
  487. return resourceFilteredTasks;
  488. }
  489. return resourceFilteredTasks
  490. .map((item) => {
  491. const sourceMap = item.dayEntryMap || {};
  492. const filteredDayEntryMap = Object.keys(sourceMap).reduce(
  493. (result, dayKey) => {
  494. const matchedEntries = (sourceMap[dayKey] || []).filter(
  495. (entry) => this.matchesCalendarFilters(entry)
  496. );
  497. if (matchedEntries.length) {
  498. result[dayKey] = matchedEntries;
  499. }
  500. return result;
  501. },
  502. {}
  503. );
  504. const matchedDayKeys = Object.keys(filteredDayEntryMap);
  505. if (!matchedDayKeys.length) {
  506. return null;
  507. }
  508. const matchedDates = matchedDayKeys
  509. .map((dayKey) => parseDate(dayKey))
  510. .filter(Boolean)
  511. .sort((a, b) => a.getTime() - b.getTime());
  512. return {
  513. ...item,
  514. dayEntryMap: filteredDayEntryMap,
  515. startDate: matchedDates[0] || item.startDate,
  516. endDate: matchedDates[matchedDates.length - 1] || item.endDate,
  517. searchEntries: Object.values(filteredDayEntryMap).flat()
  518. };
  519. })
  520. .filter(Boolean);
  521. },
  522. calendarDays() {
  523. if (!this.calendarFilteredTasks.length) {
  524. return [];
  525. }
  526. let minDate, maxDate;
  527. if (this.dateRange && this.dateRange.length === 2) {
  528. minDate = startOfDay(parseDate(this.dateRange[0]));
  529. maxDate = startOfDay(parseDate(this.dateRange[1]));
  530. } else {
  531. const firstTask = this.calendarFilteredTasks[0];
  532. minDate = startOfMonth(firstTask.startDate);
  533. maxDate = endOfMonth(firstTask.endDate);
  534. this.calendarFilteredTasks.forEach((item) => {
  535. const start = startOfMonth(item.startDate);
  536. const end = endOfMonth(item.endDate);
  537. if (start.getTime() < minDate.getTime()) {
  538. minDate = start;
  539. }
  540. if (end.getTime() > maxDate.getTime()) {
  541. maxDate = end;
  542. }
  543. });
  544. }
  545. const days = [];
  546. let current = new Date(minDate);
  547. while (current.getTime() <= maxDate.getTime()) {
  548. days.push({
  549. key: formatDateKey(current),
  550. label: current.getDate(),
  551. date: new Date(current),
  552. isWeekend: current.getDay() === 0 || current.getDay() === 6,
  553. isToday:
  554. startOfDay(current).getTime() === startOfDay(new Date()).getTime()
  555. });
  556. current = new Date(current.getTime() + DAY_MS);
  557. }
  558. return days;
  559. },
  560. calendarMonthGroups() {
  561. const result = [];
  562. this.calendarDays.forEach((day) => {
  563. const key = `${day.date.getFullYear()}-${day.date.getMonth() + 1}`;
  564. const current = result[result.length - 1];
  565. if (current && current.key === key) {
  566. current.span += 1;
  567. } else {
  568. result.push({
  569. key,
  570. label: `${day.date.getFullYear()}\u5e74${
  571. day.date.getMonth() + 1
  572. }\u6708`,
  573. span: 1
  574. });
  575. }
  576. });
  577. return result;
  578. },
  579. calendarDisplayRows() {
  580. if (!this.calendarDays.length) {
  581. return [];
  582. }
  583. const rowMap = new Map();
  584. this.calendarFilteredTasks.forEach((item) => {
  585. const groupKey = item.groupLabel || '\u672a\u5206\u7ec4';
  586. if (!rowMap.has(groupKey)) {
  587. rowMap.set(groupKey, []);
  588. }
  589. rowMap.get(groupKey).push({
  590. id: item.id,
  591. taskLabel: item.taskLabel,
  592. dayCells: this.calendarDays.map((day, index) => {
  593. const entries = item.dayEntryMap
  594. ? item.dayEntryMap[day.key] || []
  595. : [];
  596. return {
  597. key: `${item.id}-${day.key}`,
  598. entries,
  599. isActive: entries.length > 0,
  600. isWeekend: day.isWeekend,
  601. isToday: day.isToday
  602. };
  603. })
  604. });
  605. });
  606. const result = [];
  607. rowMap.forEach((rows, groupLabel) => {
  608. const visibleRows = this.calendarExpand ? rows : rows.slice(0, 1);
  609. visibleRows.forEach((row, index) => {
  610. result.push({
  611. ...row,
  612. groupLabel,
  613. showGroup: index === 0,
  614. rowspan: visibleRows.length
  615. });
  616. });
  617. });
  618. return result;
  619. },
  620. calendarGroupLabel() {
  621. const tab = this.resourceTabs.find(
  622. (item) => item.key === this.activeTab
  623. );
  624. return tab?.label ? `${tab.label}名称` : '资源名称';
  625. }
  626. },
  627. mounted() {
  628. this.initGantt(); // gantt初始化配置
  629. this.initData(); // 数据format以及gantt reload
  630. this.syncGanttVisibility();
  631. this.bindGanttHoverTooltip();
  632. // 监听全屏状态变化
  633. document.addEventListener(
  634. 'fullscreenchange',
  635. this.handleFullscreenChange
  636. );
  637. document.addEventListener(
  638. 'webkitfullscreenchange',
  639. this.handleFullscreenChange
  640. );
  641. document.addEventListener(
  642. 'mozfullscreenchange',
  643. this.handleFullscreenChange
  644. );
  645. document.addEventListener(
  646. 'MSFullscreenChange',
  647. this.handleFullscreenChange
  648. );
  649. },
  650. methods: {
  651. handleResourceTabChange(tabKey) {
  652. if (this.activeTab === tabKey) {
  653. return;
  654. }
  655. this.activeTab = tabKey;
  656. this.$emit('tab-change', tabKey);
  657. },
  658. handleResourceSelect(item) {
  659. this.selectedResource =
  660. this.selectedResource === item.label ? '' : item.label;
  661. },
  662. getCalendarTaskLabel(item = {}) {
  663. return String(item.taskName || '').trim();
  664. },
  665. getCalendarEntryQuantity(item = {}, resourceItem = {}, period = {}) {
  666. const planMeta =
  667. this.planMetaByCode[resourceItem.planCode] ||
  668. this.planMetaByCode[resourceItem.planId] ||
  669. this.planMetaByCode[period.planCode] ||
  670. this.planMetaByCode[period.planId] ||
  671. {};
  672. const quantity =
  673. period.todayRequiredFormingNum ??
  674. period.todayQuantity ??
  675. period.dayQuantity ??
  676. resourceItem.productNum ??
  677. resourceItem.formingNum ??
  678. resourceItem.requiredFormingNum ??
  679. resourceItem.requiredNum ??
  680. resourceItem.planNum ??
  681. resourceItem.quantity ??
  682. period.productNum ??
  683. period.formingNum ??
  684. period.requiredFormingNum ??
  685. period.requiredNum ??
  686. period.planNum ??
  687. period.quantity ??
  688. '';
  689. if (quantity === '' || quantity === null || quantity === undefined) {
  690. return '';
  691. }
  692. const unit =
  693. item.measuringUnit ||
  694. item.unit ||
  695. item.measuringUnitName ||
  696. item.unitName ||
  697. item.unitMeasurement ||
  698. item.productUnit ||
  699. resourceItem.measuringUnit ||
  700. resourceItem.unit ||
  701. resourceItem.measuringUnitName ||
  702. resourceItem.unitName ||
  703. resourceItem.unitMeasurement ||
  704. resourceItem.productUnit ||
  705. planMeta.measuringUnit ||
  706. planMeta.unit ||
  707. planMeta.measuringUnitName ||
  708. planMeta.unitName ||
  709. planMeta.unitMeasurement ||
  710. planMeta.productUnit ||
  711. period.measuringUnit ||
  712. period.unit ||
  713. period.measuringUnitName ||
  714. period.unitName ||
  715. period.unitMeasurement ||
  716. period.productUnit ||
  717. '';
  718. const text = String(quantity);
  719. if (!unit || text.includes(String(unit))) {
  720. return text;
  721. }
  722. return `${text}${unit}`;
  723. },
  724. resetResourceFilter() {
  725. this.keyword = '';
  726. this.selectedResource = '';
  727. },
  728. matchesResourceFilter(item) {
  729. if (!this.selectedResource) {
  730. return true;
  731. }
  732. const currentLabel = this.getActiveResourceName(item);
  733. return String(currentLabel || '').trim() === this.selectedResource;
  734. },
  735. resolveGanttPlanColor(planKey) {
  736. if (
  737. this.resourceLabelMode === 'order' &&
  738. this.orderPlanColorMap[planKey]
  739. ) {
  740. return this.orderPlanColorMap[planKey];
  741. }
  742. return resolvePlanColor(planKey);
  743. },
  744. matchesCalendarResourceFilter(item) {
  745. if (!this.selectedResource) {
  746. return true;
  747. }
  748. const currentLabel =
  749. this.activeTab === 'line'
  750. ? item.productionLineName
  751. : item.groupLabel || item.teamName || '';
  752. return String(currentLabel || '').trim() === this.selectedResource;
  753. },
  754. getActiveCalendarGroupName(item) {
  755. if (this.activeTab === 'person') {
  756. return this.getCalendarPersonGroupName(item);
  757. }
  758. return this.getCalendarTeamGroupName(item);
  759. },
  760. getCalendarTeamGroupName(item) {
  761. if (this.resourceLabelMode === 'order') {
  762. return (
  763. item.workstationName ||
  764. item.workstation ||
  765. item.stationName ||
  766. item.workStationName ||
  767. item.factoryWorkstationName ||
  768. item.resourceName ||
  769. item.name ||
  770. item.teamName ||
  771. '\u672a\u5206\u7ec4'
  772. );
  773. }
  774. return item.name || item.teamName || '\u672a\u5206\u7ec4';
  775. },
  776. getCalendarPersonGroupName(item) {
  777. if (this.resourceLabelMode === 'order') {
  778. return this.getTaskPersonName(item) || '\u672a\u5206\u7ec4';
  779. }
  780. return this.getTaskPersonName(item) || '\u672a\u5206\u7ec4';
  781. },
  782. getCalendarProductionLineName(item, resourceList = []) {
  783. if (this.resourceLabelMode === 'order') {
  784. return (
  785. item.equipmentName ||
  786. item.deviceName ||
  787. item.assetName ||
  788. item.machineName ||
  789. resourceList[0]?.equipmentName ||
  790. resourceList[0]?.deviceName ||
  791. resourceList[0]?.assetName ||
  792. resourceList[0]?.machineName ||
  793. item.productionLineName ||
  794. ''
  795. );
  796. }
  797. return item.productionLineName || '';
  798. },
  799. getPlanProductionLineName(planCode) {
  800. if (!planCode) {
  801. return '';
  802. }
  803. const planMeta = this.planMetaByCode[planCode] || {};
  804. return String(planMeta.productionLineName || '').trim();
  805. },
  806. getTaskTeamName(item) {
  807. return String(
  808. item.teamName || item.laneLabel || item.name || ''
  809. ).trim();
  810. },
  811. getTaskWorkstationName(item) {
  812. return String(
  813. item.workstationName ||
  814. item.workstation ||
  815. item.stationName ||
  816. item.workStationName ||
  817. item.factoryWorkstationName ||
  818. item.resourceName ||
  819. item.laneLabel ||
  820. item.name ||
  821. ''
  822. ).trim();
  823. },
  824. getTaskEquipmentName(item) {
  825. return String(
  826. item.equipmentName ||
  827. item.deviceName ||
  828. item.deviceCode ||
  829. item.assetName ||
  830. item.machineName ||
  831. item.productionLineName ||
  832. ''
  833. ).trim();
  834. },
  835. getTaskPersonName(item) {
  836. const directValue = String(
  837. item.executionObjectType === 'person'
  838. ? item.executionObjectName || ''
  839. : ''
  840. ).trim();
  841. const name =
  842. directValue ||
  843. item.assigneeUserName ||
  844. item.assigneeUser?.[0]?.userName ||
  845. item.assigneeUser?.[0]?.name ||
  846. item.executionUserName ||
  847. item.executionAssigneeName ||
  848. item.assigneeName ||
  849. item.operatorName ||
  850. item.userName ||
  851. item.executionTeamLeader ||
  852. item.leaderUserName ||
  853. '';
  854. return String(name || '')
  855. .replace(/^\u4eba\u5458[:\uff1a]/, '')
  856. .trim();
  857. },
  858. getActiveResourceName(item) {
  859. if (this.activeTab === 'line') {
  860. return this.getLineResourceName(item);
  861. }
  862. if (this.activeTab === 'person') {
  863. return this.getPersonResourceName(item);
  864. }
  865. return this.getTeamResourceName(item);
  866. },
  867. getTeamResourceName(item) {
  868. if (this.resourceLabelMode === 'order') {
  869. return (
  870. this.getTaskWorkstationName(item) || this.getTaskTeamName(item)
  871. );
  872. }
  873. return this.getTaskTeamName(item);
  874. },
  875. getPersonResourceName(item) {
  876. return this.getTaskPersonName(item);
  877. },
  878. getLineResourceName(item) {
  879. if (this.resourceLabelMode === 'order') {
  880. return (
  881. this.getTaskEquipmentName(item) ||
  882. this.getTaskProductionLineName(item)
  883. );
  884. }
  885. return this.getTaskProductionLineName(item);
  886. },
  887. getTaskProductionLineName(item) {
  888. const directValue = String(item.productionLineName || '').trim();
  889. const teamValue = this.getTaskTeamName(item);
  890. const planLineName =
  891. this.getPlanProductionLineName(item.planCode || item.code) || '';
  892. if (planLineName) {
  893. return planLineName;
  894. }
  895. if (directValue && directValue !== teamValue) {
  896. return directValue;
  897. }
  898. return '';
  899. },
  900. bindGanttHoverTooltip() {
  901. if (!this.$refs.gantt || this._ganttHoverBound) {
  902. return;
  903. }
  904. this._ganttHoverBound = true;
  905. this.$refs.gantt.addEventListener(
  906. 'mousemove',
  907. this.handleGanttMouseMove
  908. );
  909. this.$refs.gantt.addEventListener('mouseleave', this.hideHoverTooltip);
  910. },
  911. handleGanttMouseMove(event) {
  912. if (this.viewMode !== 'gantt') {
  913. this.hideHoverTooltip();
  914. return;
  915. }
  916. const taskNode =
  917. event.target &&
  918. event.target.closest &&
  919. event.target.closest('.gantt_task_line[data-task-id]');
  920. if (!taskNode) {
  921. this.hideHoverTooltip();
  922. return;
  923. }
  924. const taskId = taskNode.getAttribute('data-task-id');
  925. const task = this.ganttData.find(
  926. (item) => String(item.id) === String(taskId)
  927. );
  928. if (!task) {
  929. this.hideHoverTooltip();
  930. return;
  931. }
  932. this.setHoverTooltip(
  933. buildTooltipHtml(
  934. task,
  935. this.planMetaByCode,
  936. this.activeTab,
  937. this.resourceLabelMode
  938. ),
  939. event
  940. );
  941. },
  942. hideHoverTooltip() {
  943. this.removeNativeGanttTooltips();
  944. if (!this.hoverTooltip.visible) {
  945. return;
  946. }
  947. this.hoverTooltip = {
  948. ...this.hoverTooltip,
  949. visible: false
  950. };
  951. },
  952. removeNativeGanttTooltips() {
  953. document.querySelectorAll('.gantt_tooltip').forEach((item) => {
  954. item.parentNode && item.parentNode.removeChild(item);
  955. });
  956. },
  957. getHoverTooltipBaseStyle(event) {
  958. const viewportWidth =
  959. window.innerWidth || document.documentElement.clientWidth || 1200;
  960. const viewportHeight =
  961. window.innerHeight || document.documentElement.clientHeight || 800;
  962. const margin = 12;
  963. const maxWidth = Math.max(260, Math.min(420, viewportWidth - 24));
  964. const maxHeight = Math.max(180, viewportHeight - 24);
  965. return {
  966. left: `${Math.min(
  967. event.clientX + 14,
  968. viewportWidth - maxWidth - margin
  969. )}px`,
  970. top: `${Math.min(
  971. event.clientY + 14,
  972. viewportHeight - 180 - margin
  973. )}px`,
  974. maxWidth: `${maxWidth}px`,
  975. maxHeight: `${maxHeight}px`
  976. };
  977. },
  978. setHoverTooltip(content, event) {
  979. this.removeNativeGanttTooltips();
  980. this.hoverTooltip = {
  981. visible: true,
  982. content,
  983. style: this.getHoverTooltipBaseStyle(event)
  984. };
  985. this.$nextTick(() => {
  986. this.adjustHoverTooltipPosition(event);
  987. });
  988. },
  989. adjustHoverTooltipPosition(event) {
  990. const tooltip = this.$refs.hoverTooltip;
  991. if (!tooltip || !event) {
  992. return;
  993. }
  994. const viewportWidth =
  995. window.innerWidth || document.documentElement.clientWidth || 1200;
  996. const viewportHeight =
  997. window.innerHeight || document.documentElement.clientHeight || 800;
  998. const margin = 12;
  999. const offset = 14;
  1000. const rect = tooltip.getBoundingClientRect();
  1001. const width = rect.width || 360;
  1002. const height = rect.height || 220;
  1003. let left = event.clientX + offset;
  1004. let top = event.clientY + offset;
  1005. if (left + width + margin > viewportWidth) {
  1006. left = event.clientX - width - offset;
  1007. }
  1008. if (top + height + margin > viewportHeight) {
  1009. top = event.clientY - height - offset;
  1010. }
  1011. left = Math.max(margin, Math.min(left, viewportWidth - width - margin));
  1012. top = Math.max(margin, Math.min(top, viewportHeight - height - margin));
  1013. this.hoverTooltip = {
  1014. ...this.hoverTooltip,
  1015. style: {
  1016. ...this.hoverTooltip.style,
  1017. left: `${left}px`,
  1018. top: `${top}px`
  1019. }
  1020. };
  1021. },
  1022. syncGanttVisibility() {
  1023. this.$nextTick(() => {
  1024. const ganttRoot =
  1025. (this.$refs.gantt && this.$refs.gantt.firstElementChild) ||
  1026. gantt.$root ||
  1027. null;
  1028. const shouldShow = this.viewMode === 'gantt';
  1029. if (ganttRoot && ganttRoot.style) {
  1030. ganttRoot.style.display = shouldShow ? '' : 'none';
  1031. }
  1032. if (this.$refs.gantt_scrollbar && this.$refs.gantt_scrollbar.style) {
  1033. this.$refs.gantt_scrollbar.style.display = shouldShow ? '' : 'none';
  1034. }
  1035. if (!shouldShow) {
  1036. this.hideHoverTooltip();
  1037. }
  1038. if (shouldShow) {
  1039. gantt.render();
  1040. }
  1041. });
  1042. },
  1043. collectUniqueOptions(list, keys) {
  1044. const values = list
  1045. .map((item) => {
  1046. const matchedKey = keys.find((key) => item[key]);
  1047. return matchedKey ? item[matchedKey] : '';
  1048. })
  1049. .filter(Boolean);
  1050. return [...new Set(values)];
  1051. },
  1052. applyCalendarFilters() {
  1053. this.calendarFilters = {
  1054. ...this.calendarQuery
  1055. };
  1056. },
  1057. applyGanttFilters() {
  1058. this.ganttFilters = {
  1059. ...this.ganttQuery
  1060. };
  1061. this.hideHoverTooltip();
  1062. this.initData();
  1063. },
  1064. resetGanttFilters() {
  1065. this.ganttQuery = createSearchQuery();
  1066. this.ganttFilters = createSearchQuery();
  1067. this.hideHoverTooltip();
  1068. this.initData();
  1069. },
  1070. resetCalendarFilters() {
  1071. this.calendarQuery = createSearchQuery();
  1072. this.calendarFilters = createSearchQuery();
  1073. this.calendarExpand = true;
  1074. },
  1075. joinSearchValues(values = []) {
  1076. return values
  1077. .filter((item) => item !== '' && item !== null && item !== undefined)
  1078. .map((item) => String(item))
  1079. .join(' ');
  1080. },
  1081. normalizePlanSearchEntry(entry) {
  1082. const planKey = entry.planCode || entry.code || entry.id;
  1083. const planMeta = this.planMetaByCode[planKey] || {};
  1084. const isOrderScheduling = this.resourceLabelMode === 'order';
  1085. const visibleTaskText = [
  1086. entry.taskName,
  1087. entry.title,
  1088. entry.name,
  1089. entry.text,
  1090. entry.executor
  1091. ];
  1092. return {
  1093. routingName: this.joinSearchValues([
  1094. entry.routingName ||
  1095. entry.produceRoutingName ||
  1096. planMeta.routingName ||
  1097. planMeta.produceRoutingName ||
  1098. planMeta.routeName,
  1099. ...visibleTaskText
  1100. ]),
  1101. productName: this.joinSearchValues([
  1102. entry.productName,
  1103. planMeta.productName,
  1104. isOrderScheduling ? entry.name : '',
  1105. isOrderScheduling ? entry.text : ''
  1106. ]),
  1107. planCode: this.joinSearchValues([
  1108. entry.productionOrderNumber ||
  1109. entry.apsWorkOrderCode ||
  1110. entry.workOrderCode ||
  1111. entry.productionOrderCode,
  1112. entry.displayCode,
  1113. entry.planCode,
  1114. entry.orderCode,
  1115. entry.code,
  1116. planMeta.productionOrderNumber,
  1117. planMeta.apsWorkOrderCode,
  1118. planMeta.workOrderCode,
  1119. planMeta.code,
  1120. planMeta.planCode,
  1121. ...visibleTaskText
  1122. ])
  1123. };
  1124. },
  1125. matchesPlanSearchFilters(entry, filters) {
  1126. const normalizedEntry = this.normalizePlanSearchEntry(entry || {});
  1127. if (
  1128. filters.routingName &&
  1129. !String(normalizedEntry.routingName || '')
  1130. .toLowerCase()
  1131. .includes(filters.routingName.toLowerCase())
  1132. ) {
  1133. return false;
  1134. }
  1135. if (
  1136. filters.productName &&
  1137. !String(normalizedEntry.productName || '')
  1138. .toLowerCase()
  1139. .includes(filters.productName.toLowerCase())
  1140. ) {
  1141. return false;
  1142. }
  1143. if (
  1144. filters.planCode &&
  1145. !String(normalizedEntry.planCode || '')
  1146. .toLowerCase()
  1147. .includes(filters.planCode.toLowerCase())
  1148. ) {
  1149. return false;
  1150. }
  1151. return true;
  1152. },
  1153. matchesGanttFilters(item) {
  1154. return this.matchesPlanSearchFilters(item, this.ganttFilters);
  1155. },
  1156. matchesCalendarFilters(entry) {
  1157. return this.matchesPlanSearchFilters(entry, this.calendarFilters);
  1158. },
  1159. buildFilteredGanttTasks() {
  1160. const resourceMatchedTasks = this.ganttTasks.filter((item) =>
  1161. this.matchesResourceFilter(item)
  1162. );
  1163. const directMatchedTasks = resourceMatchedTasks.filter((item) =>
  1164. this.matchesGanttFilters(item)
  1165. );
  1166. const matchedIds = new Set(directMatchedTasks.map((item) => item.id));
  1167. const shouldExpandMatchedParent =
  1168. (!this.ganttFilters.planCode &&
  1169. (this.ganttFilters.routingName || this.ganttFilters.productName)) ||
  1170. (this.resourceLabelMode !== 'order' && this.ganttFilters.planCode);
  1171. const matchedSplitParentIds = shouldExpandMatchedParent
  1172. ? new Set(
  1173. directMatchedTasks
  1174. .filter((item) => item.render === 'split' && !item.parent)
  1175. .map((item) => item.id)
  1176. )
  1177. : new Set();
  1178. directMatchedTasks.forEach((item) => {
  1179. if (item.parent) {
  1180. matchedIds.add(item.parent);
  1181. }
  1182. });
  1183. resourceMatchedTasks.forEach((item) => {
  1184. if (matchedSplitParentIds.has(item.parent)) {
  1185. matchedIds.add(item.id);
  1186. }
  1187. });
  1188. return resourceMatchedTasks.filter((item) => matchedIds.has(item.id));
  1189. },
  1190. showCalendarTooltip(event, entries) {
  1191. this.removeNativeGanttTooltips();
  1192. this.setHoverTooltip(buildCalendarTooltipHtml(entries), event);
  1193. },
  1194. moveHoverTooltip(event) {
  1195. if (!this.hoverTooltip.visible) {
  1196. return;
  1197. }
  1198. this.adjustHoverTooltipPosition(event);
  1199. },
  1200. getChildRangeMap() {
  1201. return this.ganttTasks.reduce((result, item) => {
  1202. if (!item.parent || !item.start_date || !item.end_date) {
  1203. return result;
  1204. }
  1205. const start = parseDate(item.start_date);
  1206. const end = parseDate(item.end_date);
  1207. if (!start || !end) {
  1208. return result;
  1209. }
  1210. if (!result[item.parent]) {
  1211. result[item.parent] = {
  1212. start,
  1213. end
  1214. };
  1215. return result;
  1216. }
  1217. if (start.getTime() < result[item.parent].start.getTime()) {
  1218. result[item.parent].start = start;
  1219. }
  1220. if (end.getTime() > result[item.parent].end.getTime()) {
  1221. result[item.parent].end = end;
  1222. }
  1223. return result;
  1224. }, {});
  1225. },
  1226. handleViewSwitch(mode) {
  1227. if (this.viewMode === mode) {
  1228. return;
  1229. }
  1230. this.hideHoverTooltip();
  1231. this.removeNativeGanttTooltips();
  1232. this.viewMode = mode;
  1233. },
  1234. async toggleFullscreen() {
  1235. const element = this.$el;
  1236. if (!this.isFullscreen) {
  1237. // 进入全屏
  1238. try {
  1239. if (element.requestFullscreen) {
  1240. await element.requestFullscreen();
  1241. } else if (element.webkitRequestFullscreen) {
  1242. await element.webkitRequestFullscreen();
  1243. } else if (element.mozRequestFullScreen) {
  1244. await element.mozRequestFullScreen();
  1245. } else if (element.msRequestFullscreen) {
  1246. await element.msRequestFullscreen();
  1247. }
  1248. this.isFullscreen = true;
  1249. } catch (err) {
  1250. console.error('全屏请求失败:', err);
  1251. this.$message.warning('浏览器不支持全屏功能');
  1252. }
  1253. } else {
  1254. // 退出全屏
  1255. try {
  1256. if (document.exitFullscreen) {
  1257. await document.exitFullscreen();
  1258. } else if (document.webkitExitFullscreen) {
  1259. await document.webkitExitFullscreen();
  1260. } else if (document.mozCancelFullScreen) {
  1261. await document.mozCancelFullScreen();
  1262. } else if (document.msExitFullscreen) {
  1263. await document.msExitFullscreen();
  1264. }
  1265. this.isFullscreen = false;
  1266. } catch (err) {
  1267. console.error('退出全屏失败:', err);
  1268. }
  1269. }
  1270. this.$nextTick(() => {
  1271. if (this.viewMode === 'gantt') {
  1272. gantt.render();
  1273. }
  1274. });
  1275. },
  1276. handleFullscreenChange() {
  1277. // 监听全屏状态变化(用户按ESC键退出全屏)
  1278. const isCurrentlyFullscreen = !!(
  1279. document.fullscreenElement ||
  1280. document.webkitFullscreenElement ||
  1281. document.mozFullScreenElement ||
  1282. document.msFullscreenElement
  1283. );
  1284. if (this.isFullscreen && !isCurrentlyFullscreen) {
  1285. this.isFullscreen = false;
  1286. this.$nextTick(() => {
  1287. if (this.viewMode === 'gantt') {
  1288. gantt.render();
  1289. }
  1290. });
  1291. }
  1292. },
  1293. handleEscKey(event) {
  1294. if (event.key === 'Escape' && this.isFullscreen) {
  1295. this.toggleFullscreen();
  1296. }
  1297. },
  1298. initGantt() {
  1299. let _this = this;
  1300. gantt.config.readonly = true;
  1301. gantt.config.select_task = false;
  1302. gantt.config.autofit = false;
  1303. gantt.config.xml_date = '%Y-%m-%d %H:%i:%s';
  1304. gantt.config.show_grid = false;
  1305. gantt.config.open_split_tasks = true;
  1306. let daysStyle = function (date) {
  1307. let todayDate = new Date().setHours(0, 0, 0, 0);
  1308. let paramsDate = new Date(date).setHours(0, 0, 0, 0);
  1309. if (todayDate === paramsDate) {
  1310. return 'today';
  1311. }
  1312. if (date.getDay() === 0 || date.getDay() === 6) {
  1313. return 'weekend';
  1314. }
  1315. return '';
  1316. };
  1317. gantt.config.scales = [
  1318. { unit: 'month', step: 1, format: '%Y/%M', css: () => 'month' },
  1319. {
  1320. unit: 'day',
  1321. step: 1,
  1322. format(date) {
  1323. return `
  1324. <div class="gantt-day-scale">
  1325. <div class="gantt-day-scale__date">${date.getDate()}日</div>
  1326. <div class="gantt-day-scale__week">星期${
  1327. WEEK_MAP[date.getDay()]
  1328. }</div>
  1329. </div>
  1330. `;
  1331. },
  1332. css: daysStyle
  1333. }
  1334. ];
  1335. // 这里在时间轴表格内的cell上添加class
  1336. gantt.templates.timeline_cell_class = function (item, date) {
  1337. if (date.getDay() == 0 || date.getDay() == 6) {
  1338. return 'weekend';
  1339. }
  1340. };
  1341. // 条形图里的文字
  1342. gantt.templates.task_text = function (start, end, task) {
  1343. const label = task.name || task.text || task.code || '';
  1344. return `<span gantt_task_content-id="${
  1345. task.id
  1346. }" class="gantt-task-text">${escapeHtml(label)}</span>`;
  1347. };
  1348. // 条形图自定义class
  1349. gantt.templates.task_class = function (start, end, task) {
  1350. return ['task-222', task.customClass].filter(Boolean).join(' ');
  1351. };
  1352. gantt.config.scale_height = 50;
  1353. gantt.config.task_height = 22;
  1354. gantt.config.row_height = 27;
  1355. gantt.config.min_column_width = 40;
  1356. gantt.config.show_task_cells = false;
  1357. gantt.plugins({
  1358. marker: true
  1359. });
  1360. let today = new Date();
  1361. gantt.addMarker({
  1362. start_date: today,
  1363. css: 'today',
  1364. text: ''
  1365. });
  1366. gantt.plugins({
  1367. tooltip: true
  1368. });
  1369. // 自定义tooltip内容
  1370. gantt.templates.tooltip_text = function (start, end, task) {
  1371. return buildTooltipHtml(
  1372. task,
  1373. _this.planMetaByCode,
  1374. _this.activeTab,
  1375. _this.resourceLabelMode
  1376. );
  1377. };
  1378. gantt.attachEvent('onTaskClick', function (id, e) {
  1379. let box = document.querySelector('.gantt_tooltip');
  1380. if (box) {
  1381. box.parentNode.removeChild(box);
  1382. }
  1383. let classList = String(e?.target?.className || '').split(' ');
  1384. // 这里自定义了展开/收起树形的点击事件
  1385. if (classList.includes('gantt_open')) {
  1386. gantt.open(id); // 灞曞紑
  1387. return true;
  1388. }
  1389. if (classList.includes('gantt_close')) {
  1390. gantt.close(id); // 鏀惰捣
  1391. return true;
  1392. }
  1393. // 这里是其他的点击事件
  1394. let task = _this.ganttData.find((item) => {
  1395. return item.id == id;
  1396. });
  1397. if (!task) {
  1398. return true;
  1399. }
  1400. if (task.taskType === 'stage') {
  1401. // toDo()
  1402. return true;
  1403. }
  1404. return true;
  1405. });
  1406. gantt.i18n.setLocale('cn');
  1407. // 初始化gantt
  1408. gantt.init(this.$refs.gantt);
  1409. // 绑定数据
  1410. gantt.parse({
  1411. data: [], // 时间轴上横条的数据
  1412. link: [] // 连接线的数据
  1413. });
  1414. },
  1415. reload() {
  1416. gantt.clearAll();
  1417. if (this.dateRange && this.dateRange.length === 2) {
  1418. gantt.config.start_date = new Date(this.dateRange[0]);
  1419. gantt.config.end_date = new Date(this.dateRange[1]);
  1420. gantt.config.fit_tasks = false;
  1421. } else {
  1422. gantt.config.start_date = null;
  1423. gantt.config.end_date = null;
  1424. gantt.config.fit_tasks = true;
  1425. }
  1426. this.ganttData = JSON.parse(JSON.stringify(this.ganttData));
  1427. gantt.parse({
  1428. data: this.ganttData,
  1429. link: []
  1430. });
  1431. gantt.render();
  1432. },
  1433. getGanttData() {
  1434. // toDo
  1435. // formatGanttData()
  1436. this.ganttData = this.ganttTasks.map((item) =>
  1437. this.applyGanttFullDayRange(item)
  1438. );
  1439. this.reload();
  1440. },
  1441. formatGanttRenderDate(date) {
  1442. const pad = (value) => String(value).padStart(2, '0');
  1443. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
  1444. date.getDate()
  1445. )} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
  1446. date.getSeconds()
  1447. )}`;
  1448. },
  1449. applyGanttFullDayRange(task) {
  1450. const start = parseDate(task.start_date);
  1451. const end = parseDate(task.end_date);
  1452. if (!start || !end) {
  1453. return task;
  1454. }
  1455. const renderStart = startOfDay(start);
  1456. let renderEnd = new Date(startOfDay(end).getTime() + DAY_MS);
  1457. if (renderEnd.getTime() <= renderStart.getTime()) {
  1458. renderEnd = new Date(renderStart.getTime() + DAY_MS);
  1459. }
  1460. return {
  1461. ...task,
  1462. rawStartDate: task.rawStartDate || task.start_date,
  1463. rawEndDate: task.rawEndDate || task.end_date,
  1464. start_date: this.formatGanttRenderDate(renderStart),
  1465. end_date: this.formatGanttRenderDate(renderEnd)
  1466. };
  1467. },
  1468. initData() {
  1469. const sourceTasks = this.filteredGanttTasks;
  1470. const parentTaskMap = sourceTasks.reduce((result, item) => {
  1471. if (item.render === 'split' && !item.parent) {
  1472. result[item.id] = item;
  1473. }
  1474. return result;
  1475. }, {});
  1476. this.ganttData = sourceTasks
  1477. .filter((item) => !(item.render === 'split' && !item.parent))
  1478. .map((current) => {
  1479. const hasHiddenParent =
  1480. current.parent && parentTaskMap[current.parent];
  1481. const mergedCurrent = hasHiddenParent
  1482. ? Object.assign({}, parentTaskMap[current.parent], current, {
  1483. parent: undefined
  1484. })
  1485. : current;
  1486. let newObj = {};
  1487. if (current.parent || hasHiddenParent) {
  1488. const planKey =
  1489. mergedCurrent.planId ||
  1490. mergedCurrent.planCode ||
  1491. mergedCurrent.code ||
  1492. mergedCurrent.id;
  1493. newObj = Object.assign({}, mergedCurrent, {
  1494. color: this.resolveGanttPlanColor(planKey),
  1495. title: mergedCurrent.title || '任务',
  1496. customClass: 'task-child-plan'
  1497. });
  1498. return this.applyGanttFullDayRange(newObj);
  1499. }
  1500. if (mergedCurrent.type == 1) {
  1501. newObj = Object.assign({}, mergedCurrent, {
  1502. color: '#169fe6',
  1503. title: '计划',
  1504. customClass: 'task-parent-blue'
  1505. });
  1506. } else if (mergedCurrent.type == 2) {
  1507. newObj = Object.assign({}, mergedCurrent, {
  1508. color: '#ffa21a',
  1509. title: '工序',
  1510. customClass: 'task-child-plan'
  1511. });
  1512. } else if (mergedCurrent.type == 3) {
  1513. newObj = Object.assign({}, mergedCurrent, {
  1514. color: '#62ddd4',
  1515. title: '工位',
  1516. customClass: 'task-station-green'
  1517. });
  1518. } else if (mergedCurrent.type == 4) {
  1519. //计划节点
  1520. newObj = Object.assign({}, mergedCurrent, {
  1521. color: '#d1a6ff',
  1522. title: '计划节点',
  1523. customClass: 'task-node-purple'
  1524. });
  1525. } else if (mergedCurrent.type == 5) {
  1526. //任务
  1527. newObj = Object.assign({}, mergedCurrent, {
  1528. color: '#ffa21a',
  1529. title: '任务',
  1530. customClass: 'task-child-plan'
  1531. });
  1532. } else {
  1533. //默认色
  1534. newObj = Object.assign({}, mergedCurrent, {
  1535. color: '#169fe6',
  1536. title: '计划',
  1537. customClass: 'task-parent-blue'
  1538. });
  1539. }
  1540. return this.applyGanttFullDayRange(newObj);
  1541. });
  1542. this.reload();
  1543. }
  1544. },
  1545. activated() {
  1546. this.syncGanttVisibility();
  1547. },
  1548. beforeDestroy() {
  1549. if (this.$refs.gantt) {
  1550. this.$refs.gantt.removeEventListener(
  1551. 'mousemove',
  1552. this.handleGanttMouseMove
  1553. );
  1554. this.$refs.gantt.removeEventListener(
  1555. 'mouseleave',
  1556. this.hideHoverTooltip
  1557. );
  1558. }
  1559. document.removeEventListener('keydown', this.handleEscKey);
  1560. // 移除全屏状态监听
  1561. document.removeEventListener(
  1562. 'fullscreenchange',
  1563. this.handleFullscreenChange
  1564. );
  1565. document.removeEventListener(
  1566. 'webkitfullscreenchange',
  1567. this.handleFullscreenChange
  1568. );
  1569. document.removeEventListener(
  1570. 'mozfullscreenchange',
  1571. this.handleFullscreenChange
  1572. );
  1573. document.removeEventListener(
  1574. 'MSFullscreenChange',
  1575. this.handleFullscreenChange
  1576. );
  1577. this.removeNativeGanttTooltips();
  1578. }
  1579. };
  1580. </script>
  1581. <style lang="scss" scoped>
  1582. .project-gantt {
  1583. position: relative;
  1584. display: flex;
  1585. flex-direction: column;
  1586. width: 100%;
  1587. height: 100%;
  1588. min-height: 260px;
  1589. gap: 6px;
  1590. padding: 6px;
  1591. box-sizing: border-box;
  1592. border: 1px solid #d8e3ef;
  1593. border-radius: 10px;
  1594. background: linear-gradient(
  1595. 135deg,
  1596. rgba(31, 140, 235, 0.06) 0%,
  1597. transparent 32%
  1598. ),
  1599. linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
  1600. box-shadow: 0 10px 26px rgba(26, 39, 55, 0.08);
  1601. overflow: hidden;
  1602. &.is-fullscreen {
  1603. position: fixed;
  1604. top: 0;
  1605. left: 0;
  1606. right: 0;
  1607. bottom: 0;
  1608. width: 100vw;
  1609. height: 100vh;
  1610. z-index: 9999;
  1611. background: #eef4fa;
  1612. padding: 10px;
  1613. box-sizing: border-box;
  1614. }
  1615. }
  1616. .project-gantt::before {
  1617. position: absolute;
  1618. top: 0;
  1619. right: 0;
  1620. left: 0;
  1621. height: 2px;
  1622. background: linear-gradient(90deg, #2489e8 0%, #14b8a6 60%, #7aa7ff 100%);
  1623. content: '';
  1624. pointer-events: none;
  1625. }
  1626. .view-switch {
  1627. display: flex;
  1628. align-items: center;
  1629. justify-content: space-between;
  1630. gap: 6px;
  1631. min-height: 34px;
  1632. padding: 3px 6px;
  1633. box-sizing: border-box;
  1634. border: 1px solid #dfe9f4;
  1635. border-radius: 8px;
  1636. background: rgba(255, 255, 255, 0.94);
  1637. box-shadow: 0 3px 12px rgba(31, 45, 61, 0.05);
  1638. }
  1639. .view-switch-left {
  1640. display: flex;
  1641. align-items: center;
  1642. gap: 6px;
  1643. flex: 1 1 auto;
  1644. min-width: 0;
  1645. overflow-x: auto;
  1646. flex-wrap: nowrap;
  1647. padding-bottom: 1px;
  1648. }
  1649. .view-switch-left::-webkit-scrollbar {
  1650. height: 4px;
  1651. }
  1652. .view-switch-left::-webkit-scrollbar-thumb {
  1653. border-radius: 999px;
  1654. background: #c9d4e2;
  1655. }
  1656. .view-switch-left ::v-deep .el-date-editor--daterange.el-input__inner {
  1657. width: 330px;
  1658. flex: 0 0 auto;
  1659. }
  1660. .gantt-view ::v-deep > .calendar-toolbar,
  1661. .calendar-view ::v-deep > .calendar-toolbar {
  1662. display: none;
  1663. }
  1664. .view-switch ::v-deep .el-button {
  1665. flex: 0 0 auto;
  1666. border-radius: 7px;
  1667. font-weight: 600;
  1668. transition: all 0.18s ease;
  1669. }
  1670. .view-switch ::v-deep .el-button:not(.el-button--primary):hover {
  1671. color: #1683d8;
  1672. border-color: #bfdcf5;
  1673. background: #f4f9ff;
  1674. }
  1675. .view-switch ::v-deep .el-button--primary {
  1676. border-color: #1f8ceb;
  1677. background: linear-gradient(180deg, #2a9af0 0%, #1684df 100%);
  1678. box-shadow: 0 3px 8px rgba(31, 140, 235, 0.2);
  1679. }
  1680. .calendar-view {
  1681. flex: 1;
  1682. min-width: 0;
  1683. min-height: 0;
  1684. display: flex;
  1685. flex-direction: column;
  1686. gap: 6px;
  1687. }
  1688. .gantt-view {
  1689. flex: 1;
  1690. min-width: 0;
  1691. min-height: 0;
  1692. display: flex;
  1693. flex-direction: column;
  1694. gap: 6px;
  1695. }
  1696. .project-gantt__body {
  1697. flex: 1;
  1698. min-height: 0;
  1699. display: flex;
  1700. align-items: stretch;
  1701. gap: 6px;
  1702. }
  1703. .gantt-box {
  1704. flex: 1;
  1705. min-width: 0;
  1706. width: calc(100%);
  1707. height: calc(100%);
  1708. background: #fff;
  1709. border: 1px solid #d8e3ef;
  1710. border-radius: 9px;
  1711. overflow: hidden;
  1712. box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.74);
  1713. }
  1714. .gantt-hover-tooltip {
  1715. position: fixed;
  1716. z-index: 9999;
  1717. pointer-events: none;
  1718. max-width: calc(100vw - 24px);
  1719. max-height: calc(100vh - 24px);
  1720. }
  1721. .gantt-container {
  1722. width: calc(100%);
  1723. height: calc(100%);
  1724. }
  1725. /* 周末高亮 */
  1726. :deep(.weekend) {
  1727. background: #f7f9fb;
  1728. color: #7a8794 !important;
  1729. }
  1730. /* 今日高亮 */
  1731. :deep(.gantt_layout_cell),
  1732. :deep(.gantt_task),
  1733. :deep(.gantt_data_area),
  1734. :deep(.gantt_task_bg),
  1735. :deep(.gantt_task_scale) {
  1736. background: #ffffff;
  1737. }
  1738. :deep(.gantt_task) {
  1739. border-left: none;
  1740. }
  1741. :deep(.gantt_ver_scroll),
  1742. :deep(.gantt_hor_scroll) {
  1743. background: #eef4fa;
  1744. }
  1745. :deep(.gantt_ver_scroll::-webkit-scrollbar),
  1746. :deep(.gantt_hor_scroll::-webkit-scrollbar),
  1747. :deep(.gantt_data_area::-webkit-scrollbar) {
  1748. width: 10px;
  1749. height: 10px;
  1750. }
  1751. :deep(.gantt_ver_scroll::-webkit-scrollbar-thumb),
  1752. :deep(.gantt_hor_scroll::-webkit-scrollbar-thumb),
  1753. :deep(.gantt_data_area::-webkit-scrollbar-thumb) {
  1754. border: 2px solid #f8fafc;
  1755. border-radius: 999px;
  1756. background: #bcc8d6;
  1757. }
  1758. :deep(.gantt_ver_scroll::-webkit-scrollbar-thumb:hover),
  1759. :deep(.gantt_hor_scroll::-webkit-scrollbar-thumb:hover),
  1760. :deep(.gantt_data_area::-webkit-scrollbar-thumb:hover) {
  1761. background: #aeb7c4;
  1762. }
  1763. :deep(.gantt_task_scale) {
  1764. border-bottom: 1px solid #d9e4ef;
  1765. .gantt_scale_line > .gantt_scale_cell {
  1766. color: #344054;
  1767. font-size: 11px;
  1768. border-right: 1px solid #e7edf5;
  1769. background: #fbfdff;
  1770. &.today {
  1771. background: #e8f4ff;
  1772. color: #0f75bd !important;
  1773. box-shadow: inset 0 -2px 0 #14b8a6;
  1774. }
  1775. }
  1776. }
  1777. :deep(.gantt_scale_line) {
  1778. border-bottom: 1px solid #d9e4ef;
  1779. }
  1780. :deep(.gantt_scale_line:first-child .gantt_scale_cell) {
  1781. height: 24px;
  1782. line-height: 24px;
  1783. font-size: 14px;
  1784. font-weight: 600;
  1785. color: #253244;
  1786. background: linear-gradient(180deg, #f9fcff 0%, #eef5fb 100%);
  1787. }
  1788. :deep(.gantt_scale_line:last-child .gantt_scale_cell) {
  1789. height: 26px;
  1790. padding: 0;
  1791. box-sizing: border-box;
  1792. }
  1793. :deep(.gantt-day-scale) {
  1794. width: 100%;
  1795. height: 100%;
  1796. display: flex;
  1797. flex-direction: column;
  1798. align-items: center;
  1799. justify-content: center;
  1800. text-align: center;
  1801. }
  1802. :deep(.gantt-day-scale__date) {
  1803. font-size: 11px;
  1804. line-height: 13px;
  1805. color: #606266;
  1806. }
  1807. :deep(.gantt-day-scale__week) {
  1808. margin-top: 0;
  1809. font-size: 12px;
  1810. line-height: 14px;
  1811. font-weight: 500;
  1812. color: #303133;
  1813. }
  1814. /* 去掉文件夹图标 */
  1815. :deep(.gantt_tree_icon) {
  1816. &.gantt_folder_closed,
  1817. &.gantt_file,
  1818. &.gantt_folder_open {
  1819. display: none;
  1820. }
  1821. }
  1822. /* 把展开收起图标替换成element-ui的icon */
  1823. /* 这里没有找到在js中配置图标的方法,为省事,使用css */
  1824. /* 扒了element-ui的icon样式表 */
  1825. :deep(.gantt_tree_icon.gantt_open) {
  1826. background-image: none;
  1827. font-family: element-icons !important;
  1828. speak: none;
  1829. font-style: normal;
  1830. font-weight: 400;
  1831. font-variant: normal;
  1832. text-transform: none;
  1833. vertical-align: baseline;
  1834. display: inline-block;
  1835. -webkit-font-smoothing: antialiased;
  1836. -moz-osx-font-smoothing: grayscale;
  1837. &::before {
  1838. content: '\e6df';
  1839. }
  1840. }
  1841. :deep(.gantt_tree_icon.gantt_close) {
  1842. background-image: none;
  1843. font-family: element-icons !important;
  1844. speak: none;
  1845. font-style: normal;
  1846. font-weight: 400;
  1847. font-variant: normal;
  1848. text-transform: none;
  1849. vertical-align: baseline;
  1850. display: inline-block;
  1851. -webkit-font-smoothing: antialiased;
  1852. -moz-osx-font-smoothing: grayscale;
  1853. &::before {
  1854. content: '\e6e1';
  1855. }
  1856. }
  1857. :deep(.gantt_cell) {
  1858. border-right: 1px solid #edf2f7;
  1859. }
  1860. :deep(.gantt_task_bg) {
  1861. border-right: 1px solid #edf2f7;
  1862. }
  1863. :deep(.gantt_task_bg .gantt_task_row),
  1864. :deep(.gantt_task_row) {
  1865. border-bottom: 1px solid #edf2f7;
  1866. }
  1867. :deep(.gantt_task_bg .gantt_task_row:nth-child(odd)) {
  1868. background: #fbfdff;
  1869. }
  1870. :deep(.gantt_task_bg .gantt_task_row:nth-child(even)) {
  1871. background: #ffffff;
  1872. }
  1873. :deep(.gantt_task_bg .gantt_task_row:hover) {
  1874. background: #f5fbff;
  1875. }
  1876. :deep(.task-222) {
  1877. height: 22px !important;
  1878. line-height: 22px !important;
  1879. border: none !important;
  1880. border-radius: 7px !important;
  1881. box-shadow: 0 3px 8px rgba(15, 23, 42, 0.16) !important;
  1882. filter: saturate(0.92) brightness(1);
  1883. overflow: hidden;
  1884. transition: box-shadow 0.18s ease, filter 0.18s ease;
  1885. }
  1886. :deep(.task-222:hover) {
  1887. filter: saturate(1) brightness(1.02);
  1888. box-shadow: 0 5px 14px rgba(15, 23, 42, 0.2) !important;
  1889. }
  1890. :deep(.gantt_task_line.task-parent-red),
  1891. :deep(.gantt_task_line.task-parent-red .gantt_task_progress) {
  1892. background: #e60012 !important;
  1893. }
  1894. :deep(.gantt_task_line.task-parent-blue),
  1895. :deep(.gantt_task_line.task-parent-blue .gantt_task_progress) {
  1896. background: linear-gradient(90deg, #1688dc 0%, #26a9ee 100%) !important;
  1897. }
  1898. :deep(.gantt_task_line.task-child-plan) {
  1899. border: none !important;
  1900. }
  1901. :deep(.gantt_task_line.task-station-green),
  1902. :deep(.gantt_task_line.task-station-green .gantt_task_progress) {
  1903. background: linear-gradient(90deg, #24a865 0%, #53c988 100%) !important;
  1904. }
  1905. :deep(.gantt_task_line.task-node-purple),
  1906. :deep(.gantt_task_line.task-node-purple .gantt_task_progress) {
  1907. background: linear-gradient(90deg, #8d62e5 0%, #b985ff 100%) !important;
  1908. }
  1909. :deep(.gantt_split_parent) {
  1910. opacity: 1;
  1911. }
  1912. :deep(.gantt_task_content) {
  1913. width: 100%;
  1914. color: #fff;
  1915. }
  1916. :deep(.gantt-task-text) {
  1917. display: block;
  1918. width: 100%;
  1919. overflow: hidden;
  1920. padding: 0 8px;
  1921. box-sizing: border-box;
  1922. color: #fff;
  1923. font-size: 12px;
  1924. font-weight: 600;
  1925. line-height: 22px;
  1926. white-space: nowrap;
  1927. text-overflow: ellipsis;
  1928. text-align: center;
  1929. text-shadow: 0 1px 1px rgba(0, 0, 0, 0.16);
  1930. }
  1931. :deep(.month) {
  1932. font-size: 18px;
  1933. }
  1934. </style>
  1935. <style lang="scss">
  1936. .gantt_tooltip {
  1937. display: none !important;
  1938. padding: 0 !important;
  1939. border: none !important;
  1940. background: transparent !important;
  1941. box-shadow: none !important;
  1942. }
  1943. .gantt-tooltip-card {
  1944. position: relative;
  1945. width: min(420px, calc(100vw - 24px));
  1946. min-width: min(300px, calc(100vw - 24px));
  1947. max-height: inherit;
  1948. padding: 12px 14px 14px;
  1949. box-sizing: border-box;
  1950. overflow-y: auto;
  1951. border: 1px solid rgba(198, 210, 224, 0.92);
  1952. border-radius: 8px;
  1953. background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
  1954. color: #1f2937;
  1955. box-shadow: 0 16px 38px rgba(15, 23, 42, 0.18),
  1956. 0 0 0 1px rgba(255, 255, 255, 0.72) inset;
  1957. backdrop-filter: blur(10px);
  1958. }
  1959. .gantt-tooltip-card::before {
  1960. content: '';
  1961. position: absolute;
  1962. top: 0;
  1963. right: 0;
  1964. left: 0;
  1965. height: 3px;
  1966. background: linear-gradient(90deg, #15b8a6 0%, #2587d8 100%);
  1967. }
  1968. .gantt-tooltip-card::-webkit-scrollbar {
  1969. width: 8px;
  1970. }
  1971. .gantt-tooltip-card::-webkit-scrollbar-thumb {
  1972. border: 2px solid #f8fbff;
  1973. border-radius: 999px;
  1974. background: #b8c7d8;
  1975. }
  1976. .gantt-tooltip-card__title {
  1977. margin-bottom: 10px;
  1978. padding-right: 4px;
  1979. padding-bottom: 8px;
  1980. border-bottom: 1px solid #e8eef6;
  1981. font-size: 13px;
  1982. font-weight: 700;
  1983. line-height: 20px;
  1984. color: #0f172a;
  1985. word-break: break-all;
  1986. }
  1987. .gantt-tooltip-card__row {
  1988. display: flex;
  1989. align-items: flex-start;
  1990. gap: 8px;
  1991. font-size: 12px;
  1992. line-height: 19px;
  1993. }
  1994. .gantt-tooltip-card__section + .gantt-tooltip-card__section {
  1995. margin-top: 10px;
  1996. padding-top: 10px;
  1997. border-top: 1px dashed #d9e4ef;
  1998. }
  1999. .gantt-tooltip-card__section-title {
  2000. display: inline-flex;
  2001. align-items: center;
  2002. height: 20px;
  2003. margin-bottom: 6px;
  2004. padding: 0 8px;
  2005. border-radius: 999px;
  2006. background: #edf8f6;
  2007. font-size: 12px;
  2008. font-weight: 600;
  2009. color: #147b72;
  2010. }
  2011. .gantt-tooltip-card__row + .gantt-tooltip-card__row {
  2012. margin-top: 5px;
  2013. }
  2014. .gantt-tooltip-card__label {
  2015. flex: 0 0 78px;
  2016. color: #64748b;
  2017. text-align: right;
  2018. white-space: nowrap;
  2019. }
  2020. .gantt-tooltip-card__value {
  2021. flex: 1;
  2022. min-width: 0;
  2023. color: #111827;
  2024. font-weight: 500;
  2025. white-space: normal;
  2026. word-break: break-all;
  2027. }
  2028. </style>