index.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <div class="project-gstc-gantt">
  3. <GSTC v-if="loadFinish" ref="gstc" :config="config" @state="emitState"/>
  4. <Dropdown class="project-gstc-dropdown-eye" @on-click="tapView">
  5. <Icon class="project-gstc-dropdown-icon" type="md-eye" />
  6. <DropdownMenu slot="list">
  7. <DropdownItem name="now">{{$L('现在')}}</DropdownItem>
  8. <DropdownItem name="day" :class="{'dropdown-active':period=='day'}">{{$L('天视图')}}</DropdownItem>
  9. <DropdownItem name="week" :class="{'dropdown-active':period=='week'}">{{$L('周视图')}}</DropdownItem>
  10. <DropdownItem name="month" :class="{'dropdown-active':period=='month'}">{{$L('月视图')}}</DropdownItem>
  11. </DropdownMenu>
  12. </Dropdown>
  13. <Dropdown class="project-gstc-dropdown-filtr" @on-click="tapProject">
  14. <Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId>0}" type="md-funnel" />
  15. <DropdownMenu slot="list">
  16. <DropdownItem :name="0" :class="{'dropdown-active':filtrProjectId==0}">{{$L('全部')}}</DropdownItem>
  17. <DropdownItem v-for="(item, index) in projectLabel" :key="index" :name="item.id" :class="{'dropdown-active':filtrProjectId==item.id}">{{item.title}} ({{item.taskLists.length}})</DropdownItem>
  18. </DropdownMenu>
  19. </Dropdown>
  20. <div class="project-gstc-close" @click="$emit('on-close')"><Icon type="md-close" /></div>
  21. <div class="project-gstc-edit" :class="{info:editShowInfo, visible:editData.length > 0}">
  22. <div class="project-gstc-edit-info">
  23. <Table class="tableFill" size="small" max-height="600" :columns="editColumns" :data="editData"></Table>
  24. <div class="project-gstc-edit-btns">
  25. <Button :loading="editLoad > 0" size="small" type="text" @click="editSubmit(false)">{{$L('取消')}}</Button>
  26. <Button :loading="editLoad > 0" size="small" type="primary" @click="editSubmit(true)">{{$L('保存')}}</Button>
  27. <Icon type="md-arrow-dropright" class="zoom" @click="editShowInfo=false"/>
  28. </div>
  29. </div>
  30. <div class="project-gstc-edit-small">
  31. <div class="project-gstc-edit-text" @click="editShowInfo=true">{{$L('未保存计划时间')}}: {{editData.length}}</div>
  32. <Button :loading="editLoad > 0" size="small" type="text" @click="editSubmit(false)">{{$L('取消')}}</Button>
  33. <Button :loading="editLoad > 0" size="small" type="primary" @click="editSubmit(true)">{{$L('保存')}}</Button>
  34. </div>
  35. </div>
  36. </div>
  37. </template>
  38. <style lang="scss">
  39. .project-gstc-gantt {
  40. position: absolute;
  41. top: 15px;
  42. left: 15px;
  43. right: 15px;
  44. bottom: 15px;
  45. z-index: 1;
  46. transform: translateZ(0);
  47. background-color: #fdfdfd;
  48. border-radius: 3px;
  49. .gantt-schedule-timeline-calendar {
  50. background-color: transparent;
  51. padding: 12px;
  52. }
  53. .gantt-schedule-timeline-calendar__list-column-header-resizer-container {
  54. line-height: 90px;
  55. max-width: 200px;
  56. overflow: hidden;
  57. text-overflow: ellipsis;
  58. white-space: nowrap;
  59. }
  60. .project-gstc-dropdown-eye,
  61. .project-gstc-dropdown-filtr {
  62. position: absolute;
  63. top: 46px;
  64. left: 276px;
  65. .project-gstc-dropdown-icon {
  66. cursor: pointer;
  67. color: #999;
  68. font-size: 20px;
  69. &.filtr {
  70. color: #058ce4;
  71. }
  72. }
  73. }
  74. .project-gstc-dropdown-eye {
  75. left: 238px;
  76. }
  77. .project-gstc-close {
  78. position: absolute;
  79. top: 8px;
  80. left: 12px;
  81. cursor: pointer;
  82. &:hover {
  83. i {
  84. transform: scale(1) rotate(45deg);
  85. }
  86. }
  87. i {
  88. color: #666666;
  89. font-size: 28px;
  90. transform: scale(0.92);
  91. transition: all .2s;
  92. }
  93. }
  94. .project-gstc-edit {
  95. position: absolute;
  96. bottom: 6px;
  97. right: 6px;
  98. background: #ffffff;
  99. border-radius: 4px;
  100. opacity: 0;
  101. transform: translate(120%, 0);
  102. transition: all 0.2s;
  103. &.visible {
  104. opacity: 1;
  105. transform: translate(0, 0);
  106. }
  107. &.info {
  108. .project-gstc-edit-info {
  109. display: block;
  110. }
  111. .project-gstc-edit-small {
  112. display: none;
  113. }
  114. }
  115. .project-gstc-edit-info {
  116. display: none;
  117. border: 1px solid #e4e4e4;
  118. background: #ffffff;
  119. padding: 6px;
  120. border-radius: 4px;
  121. width: 500px;
  122. .project-gstc-edit-btns {
  123. margin: 12px 6px 4px;
  124. display: flex;
  125. align-items: center;
  126. justify-content: flex-end;
  127. .ivu-btn {
  128. margin-right: 8px;
  129. font-size: 13px;
  130. }
  131. .zoom {
  132. font-size: 20px;
  133. color: #444444;
  134. cursor: pointer;
  135. &:hover {
  136. color: #57a3f3;
  137. }
  138. }
  139. }
  140. }
  141. .project-gstc-edit-small {
  142. border: 1px solid #e4e4e4;
  143. background: #ffffff;
  144. padding: 6px 12px;
  145. display: flex;
  146. align-items: center;
  147. .project-gstc-edit-text {
  148. cursor: pointer;
  149. text-decoration: underline;
  150. color: #444444;
  151. margin-right: 8px;
  152. &:hover {
  153. color: #57a3f3;
  154. }
  155. }
  156. .ivu-btn {
  157. margin-left: 4px;
  158. font-size: 13px;
  159. }
  160. }
  161. }
  162. }
  163. .gantt-notice-box {
  164. .ivu-notice-desc-btn {
  165. margin-top: 6px;
  166. text-align: right;
  167. .ivu-btn {
  168. margin-left: 4px;
  169. font-size: 13px;
  170. }
  171. }
  172. }
  173. </style>
  174. <script>
  175. import GSTC from "./GSTC";
  176. import ItemMovement from "gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"
  177. import CalendarScroll from "gantt-schedule-timeline-calendar/dist/CalendarScroll.plugin.js"
  178. import WeekendHighlight from "gantt-schedule-timeline-calendar/dist/WeekendHighlight.plugin.js"
  179. /**
  180. * 甘特图
  181. */
  182. export default {
  183. name: 'ProjectGantt',
  184. components: { GSTC },
  185. props: {
  186. projectLabel: {
  187. default: []
  188. },
  189. },
  190. data () {
  191. return {
  192. loadFinish: false,
  193. period: 'day',
  194. rows: {},
  195. items: {},
  196. config: {},
  197. editColumns: [],
  198. editData: [],
  199. editShowInfo: false,
  200. editLoad: 0,
  201. filtrProjectId: 0,
  202. }
  203. },
  204. mounted() {
  205. this.editColumns = [
  206. {
  207. title: this.$L('任务名称'),
  208. key: 'label',
  209. minWidth: 150,
  210. ellipsis: true,
  211. }, {
  212. title: this.$L('原计划时间'),
  213. minWidth: 135,
  214. align: 'center',
  215. render: (h, params) => {
  216. if (params.row.notime === true) {
  217. return h('span', '-');
  218. }
  219. return h('div', {
  220. style: {},
  221. }, [
  222. h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.backTime.start / 1000))),
  223. h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.backTime.end / 1000)))
  224. ]);
  225. }
  226. }, {
  227. title: this.$L('新计划时间'),
  228. minWidth: 135,
  229. align: 'center',
  230. render: (h, params) => {
  231. return h('div', {
  232. style: {},
  233. }, [
  234. h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.newTime.start / 1000))),
  235. h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.newTime.end / 1000)))
  236. ]);
  237. }
  238. }
  239. ];
  240. //
  241. this.initData();
  242. this.loadFinish = true;
  243. //
  244. this.$watch("projectLabel",
  245. (projectLabel) => {
  246. this.initData();
  247. },
  248. {deep: true}
  249. );
  250. },
  251. methods: {
  252. initData() {
  253. let isUpdate = this.loadFinish == true;
  254. this.rows = {};
  255. this.items = {};
  256. let index = 0;
  257. this.projectLabel.forEach((item) => {
  258. if (this.filtrProjectId > 0) {
  259. if (item.id != this.filtrProjectId) {
  260. return;
  261. }
  262. }
  263. item.taskLists.forEach((taskData) => {
  264. index++;
  265. let notime = taskData.startdate == 0 || taskData.enddate == 0;
  266. let times = this.getTimeObj(taskData);
  267. let start = times.start;
  268. let end = times.end;
  269. //
  270. let color = '#058ce4';
  271. if (taskData.complete) {
  272. color = '#c1c1c1';
  273. } else {
  274. if (taskData.level === 1) {
  275. color = '#ff0000';
  276. }else if (taskData.level === 2) {
  277. color = '#BB9F35';
  278. }else if (taskData.level === 3) {
  279. color = '#449EDD';
  280. }else if (taskData.level === 4) {
  281. color = '#84A83B';
  282. }
  283. }
  284. //
  285. let label = `<div class="gantt-schedule-timeline-calendar-title${taskData.complete?' complete-title':''}">${this.html2Escape(taskData.title)}</div>`;
  286. if (taskData.overdue) {
  287. label = `<div class="gantt-schedule-timeline-calendar-overdue"><em>${this.$L('已超期')}</em></div>${label}`;
  288. }
  289. label = `${label}<div class="gantt-schedule-timeline-calendar-goto"></div>`;
  290. this.rows[index] = {
  291. id: index,
  292. label: label,
  293. taskId: taskData.id,
  294. complete: taskData.complete,
  295. overdue: taskData.overdue,
  296. };
  297. let tempTime = { start, end };
  298. let findData = this.editData.find((t) => { return t.id == taskData.id });
  299. if (findData) {
  300. findData.backTime = $A.cloneData(tempTime)
  301. tempTime = $A.cloneData(findData.newTime);
  302. }
  303. this.items[taskData.id] = {
  304. rowId: index,
  305. id: taskData.id,
  306. label: taskData.title,
  307. time: tempTime,
  308. notime: notime,
  309. style: { background: color },
  310. };
  311. });
  312. });
  313. //
  314. if (Object.keys(this.rows).length == 0 && this.filtrProjectId == 0) {
  315. this.$Modal.warning({
  316. title: this.$L("温馨提示"),
  317. content: this.$L('任务列表为空,请先添加任务。'),
  318. onOk: () => {
  319. this.$emit('on-close');
  320. },
  321. });
  322. }
  323. //
  324. if (isUpdate) {
  325. this.$refs.gstc.getState().update('config.list.rows', this.rows);
  326. this.$refs.gstc.getState().update('config.chart.items', this.items);
  327. return;
  328. }
  329. //
  330. this.config = {
  331. plugins: [ItemMovement({
  332. moveable: 'x',
  333. resizeable: true,
  334. collisionDetection: true
  335. }), CalendarScroll(), WeekendHighlight()],
  336. height: this.$el.clientHeight - 24,
  337. list: {
  338. toggle: {
  339. display: false
  340. },
  341. rows: this.rows,
  342. columns: {
  343. percent: 100,
  344. resizer: {
  345. inRealTime: true,
  346. dots: 0
  347. },
  348. data: {
  349. label: {
  350. id: "label",
  351. data: "label",
  352. width: 300,
  353. isHTML: true,
  354. header: {
  355. content: this.$L("任务名称")
  356. }
  357. }
  358. }
  359. }
  360. },
  361. chart: {
  362. time: {
  363. period: 'day',
  364. additionalSpaces: {
  365. hour: { before: 24, after: 24, period: 'hour' },
  366. day: { before: 1, after: 1, period: 'month' },
  367. week: { before: 1, after: 1, period: 'year' },
  368. month: { before: 6, after: 6, period: 'year' },
  369. year: { before: 12, after: 12, period: 'year' }
  370. }
  371. },
  372. items: this.items
  373. },
  374. actions: {
  375. "list-column-row": [this.actionRow],
  376. 'chart-timeline-items-row-item': [this.actionResizing]
  377. },
  378. locale: this.getLanguage() == 'en' ? {} : {
  379. name: "zh-cn",
  380. weekdays: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
  381. weekdaysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
  382. weekdaysMin: ["日", "一", "二", "三", "四", "五", "六"],
  383. months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
  384. monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
  385. ordinal: function(t, e) {
  386. switch (e) {
  387. case "W":
  388. return "".concat(t, "周");
  389. default:
  390. return "".concat(t, "日")
  391. }
  392. },
  393. weekStart: 1,
  394. yearStart: 4,
  395. formats: {
  396. LT: "HH:mm",
  397. LTS: "HH:mm:ss",
  398. L: "YYYY/MM/DD",
  399. LL: "YYYY年M月D日",
  400. LLL: "YYYY年M月D日Ah点mm分",
  401. LLLL: "YYYY年M月D日ddddAh点mm分",
  402. l: "YYYY/M/D",
  403. ll: "YYYY年M月D日",
  404. lll: "YYYY年M月D日 HH:mm",
  405. llll: "YYYY年M月D日dddd HH:mm"
  406. },
  407. relativeTime: {
  408. future: "%s内",
  409. past: "%s前",
  410. s: "几秒",
  411. m: "1 分钟",
  412. mm: "%d 分钟",
  413. h: "1 小时",
  414. hh: "%d 小时",
  415. d: "1 天",
  416. dd: "%d 天",
  417. M: "1 个月",
  418. MM: "%d 个月",
  419. y: "1 年",
  420. yy: "%d 年"
  421. },
  422. },
  423. };
  424. },
  425. actionRow(element, data) {
  426. let onClick = (event) => {
  427. //打开任务
  428. let id = this.rows[data.rowId].taskId;
  429. if (event.target.className == 'gantt-schedule-timeline-calendar-goto') {
  430. id && this.$refs.gstc.getGstc().api.scrollToTime(this.items[id].time.start)
  431. } else {
  432. id && this.taskDetail(id);
  433. }
  434. }
  435. element.addEventListener('click', onClick);
  436. return {
  437. update(element, newData) {
  438. data = newData;
  439. },
  440. destroy(element, data) {
  441. element.removeEventListener('click', onClick);
  442. }
  443. };
  444. },
  445. actionResizing(element, data) {
  446. let thas = this;
  447. return {
  448. update(element, newData) {
  449. data = newData;
  450. if (data.item.isResizing) {
  451. let original = thas.getRawTime(data.item.id);
  452. if (Math.abs(original.end - data.item.time.end) > 1000 || Math.abs(original.start - data.item.time.start) > 1000) {
  453. //修改时间(变化超过1秒钟)
  454. let backTime = $A.cloneData(original);
  455. let newTime = $A.cloneData(data.item.time);
  456. let findData = thas.editData.find((item) => { return item.id == data.item.id });
  457. if (findData) {
  458. findData.newTime = newTime;
  459. } else {
  460. thas.editData.push({
  461. id: data.item.id,
  462. label: data.item.label,
  463. notime: data.item.notime,
  464. backTime,
  465. newTime,
  466. })
  467. }
  468. }
  469. }
  470. }
  471. };
  472. },
  473. getRawTime(taskId) {
  474. let times = null;
  475. this.projectLabel.some((item) => {
  476. item.taskLists.some((taskData) => {
  477. if (taskData.id == taskId) {
  478. times = this.getTimeObj(taskData);
  479. return true;
  480. }
  481. });
  482. if (times) {
  483. return true;
  484. }
  485. });
  486. return times;
  487. },
  488. getTimeObj(taskData) {
  489. let start = taskData.startdate || taskData.indate;
  490. let end = taskData.enddate || (taskData.indate + 86400);
  491. if (end == start) {
  492. end = Math.round(new Date($A.formatDate('Y-m-d 23:59:59', end)).getTime()/1000);
  493. }
  494. end = Math.max(end, start + 60);
  495. start*= 1000;
  496. end*= 1000;
  497. return {start, end};
  498. },
  499. editSubmit(save) {
  500. let triggerTask = [];
  501. this.editData.forEach((item) => {
  502. if (!this.items[item.id]) {
  503. return;
  504. }
  505. if (save) {
  506. this.editLoad++;
  507. let timeStart = $A.formatDate('Y-m-d H:i', Math.round(item.newTime.start / 1000));
  508. let timeEnd = $A.formatDate('Y-m-d H:i', Math.round(item.newTime.end / 1000));
  509. let ajaxData = {
  510. act: 'plannedtime',
  511. taskid: item.id,
  512. content: timeStart + "," + timeEnd,
  513. };
  514. $A.apiAjax({
  515. url: 'project/task/edit',
  516. method: 'post',
  517. data: ajaxData,
  518. error: () => {
  519. this.items[item.id].time = item.backTime;
  520. this.$refs.gstc.updateTime(item.id, item.backTime);
  521. },
  522. success: (res) => {
  523. if (res.ret === 1) {
  524. triggerTask.push({
  525. status: 'await',
  526. act: ajaxData.act,
  527. taskid: ajaxData.taskid,
  528. data: res.data,
  529. })
  530. } else {
  531. this.items[item.id].time = item.backTime;
  532. this.$refs.gstc.updateTime(item.id, item.backTime);
  533. }
  534. },
  535. afterComplete: () => {
  536. this.editLoad--;
  537. if (this.editLoad <= 0) {
  538. triggerTask.forEach((info) => {
  539. if (info.status == 'await') {
  540. info.status = 'trigger';
  541. $A.triggerTaskInfoListener(info.act, info.data);
  542. $A.triggerTaskInfoChange(info.taskid);
  543. }
  544. });
  545. }
  546. },
  547. });
  548. } else {
  549. this.items[item.id].time = item.backTime;
  550. this.$refs.gstc.updateTime(item.id, item.backTime);
  551. }
  552. });
  553. this.editData = [];
  554. },
  555. emitState(GSTCState) {
  556. GSTCState.subscribe('config.plugin.ItemMovement.item', data => {
  557. if (!data) return;
  558. GSTCState.update(`config.chart.items.${data.id}.isResizing`, !(data.waiting || data.moving || data.resizing));
  559. });
  560. },
  561. tapView(e) {
  562. if ("now" === e) {
  563. var i = this.$refs.gstc.getGstc()
  564. return i.api.scrollToTime(i.api.time.date().valueOf())
  565. }
  566. this.period = e;
  567. this.$refs.gstc.setPeriod(e);
  568. },
  569. tapProject(e) {
  570. this.filtrProjectId = $A.runNum(e);
  571. this.initData();
  572. },
  573. html2Escape(sHtml) {
  574. return sHtml.replace(/[<>&"]/g, function (c) {
  575. return {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;'}[c];
  576. });
  577. }
  578. }
  579. }
  580. </script>