detail.vue 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. <template>
  2. <div class="project-task-detail-window" :class="{'task-detail-show': visible}" @click="$nextTick(()=>{visible=false})">
  3. <div class="task-detail-main" @click.stop="">
  4. <div class="detail-left">
  5. <div class="detail-title-box detail-icon">
  6. <Input v-model="detail.title"
  7. :disabled="!!loadData.title"
  8. type="textarea"
  9. class="detail-title-input"
  10. ref="titleInput"
  11. :rows="1"
  12. :autosize="{minRows:1,maxRows:5}"
  13. maxlength="60"
  14. @on-keydown="titleKeydown"
  15. @on-blur="handleTask('title')"/>
  16. <div v-if="detail.projectTitle && urlProjectid != detail.projectid" class="subtitle">
  17. {{$L('所属项目:')}}
  18. <span class="project-title" @click="openProject(detail.projectid)">{{detail.projectTitle}}</span>
  19. </div>
  20. <div class="subtitle">
  21. <span class="z-nick"><UserView :username="detail.createuser"/></span>
  22. {{$L('创建于:')}}
  23. <span>{{$A.formatDate("Y-m-d H:i:s", detail.indate)}}</span>
  24. </div>
  25. </div>
  26. <div class="detail-desc-box detail-icon">
  27. <div class="detail-h2"><strong class="active">{{$L('描述')}}</strong></div>
  28. <Input v-model="detail.desc"
  29. type="textarea"
  30. class="detail-desc-input"
  31. ref="descInput"
  32. :rows="2"
  33. :autosize="{minRows:2,maxRows:8}"
  34. maxlength="500"
  35. :placeholder="$L('添加详细描述...')"
  36. @on-keydown="descKeydown"
  37. @on-blur="handleTask('desc')"/>
  38. </div>
  39. <ul class="detail-text-box">
  40. <li v-if="detail.startdate > 0 && detail.enddate > 0" class="text-time detail-icon">
  41. <span>{{$L('计划时间:')}}</span>
  42. <em>{{$A.formatDate("Y-m-d H:i", detail.startdate)}} {{$L('至')}} {{$A.formatDate("Y-m-d H:i", detail.enddate)}}</em>
  43. <em v-if="detail.overdue" class="overdue">[{{$L('已超期')}}]</em>
  44. </li>
  45. <li class="text-username detail-icon">
  46. <span>{{$L('负责人:')}}</span>
  47. <em><UserView :username="detail.username"/></em>
  48. </li>
  49. <li v-if="followerLength(detail.follower) > 0" class="text-follower detail-icon">
  50. <span>{{$L('关注者:')}}</span>
  51. <em>
  52. <Tag v-for="(fname, findex) in detail.follower" :key="findex" closable @on-close="handleTask('unattention', {username:fname,uisynch:true})"><UserView :username="fname"/></Tag>
  53. </em>
  54. </li>
  55. <li class="text-level detail-icon">
  56. <span>{{$L('优先级:')}}</span>
  57. <em :class="`p${detail.level}`">{{levelFormt(detail.level)}}</em>
  58. </li>
  59. <li class="text-status detail-icon">
  60. <span>{{$L('任务状态:')}}</span>
  61. <em v-if="detail.complete" class="complete">{{$L('已完成')}}<span class="completedate">({{$A.formatDate("Y-m-d H:i", detail.completedate)}})</span></em>
  62. <em v-else class="unfinished">{{$L('未完成')}}</em>
  63. </li>
  64. </ul>
  65. <div class="detail-h2 detail-subtask-icon detail-icon">
  66. <strong class="active">{{$L('子任务')}}</strong>
  67. <div class="detail-button">
  68. <Button class="detail-button-batch" size="small" @click="subtaskBatchAdd">{{$L('批量添加子任务')}}</Button>
  69. <Button class="detail-button-btn" size="small" @click="handleTask('subtaskAdd')">{{$L('添加子任务')}}</Button>
  70. </div>
  71. </div>
  72. <div class="detail-subtask-box">
  73. <div v-if="detail.subtask.length == 0" class="detail-subtask-none" @click="handleTask('subtaskAdd')">{{$L('暂无子任务')}}</div>
  74. <div v-else>
  75. <Progress class="detail-subtask-progress" :percent="subtaskProgress" :stroke-width="5" status="active" />
  76. <draggable
  77. v-model="detail.subtask"
  78. draggable=".detail-subtask-item"
  79. handle=".detail-subtask-rmenu"
  80. :animation="150"
  81. @sort="handleTask('subtaskBlur')">
  82. <div v-for="(subitem, subindex) in detail.subtask" :key="subindex" :data-id="subitem.id" class="detail-subtask-item">
  83. <Checkbox v-model="subitem.status"
  84. true-value="complete"
  85. false-value="unfinished"
  86. @on-change="handleTask('subtaskBlur')"></Checkbox>
  87. <Input v-model="subitem.detail"
  88. type="textarea"
  89. class="detail-subtask-input"
  90. :readonly="subitem.status=='complete'"
  91. :ref="`subtaskInput_${subindex}`"
  92. :class="{'subtask-complete':subitem.status=='complete'}"
  93. :rows="1"
  94. :autosize="{minRows:1,maxRows:5}"
  95. maxlength="255"
  96. :placeholder="$L('子任务描述...')"
  97. @on-keydown="subtaskKeydown(subindex, $event)"
  98. @on-blur="handleTask('subtaskBlur')"/>
  99. <div class="detail-subtask-right" :style="subitem.showPoptip===true?{opacity:1}:{}">
  100. <Icon type="md-menu" class="detail-subtask-ricon detail-subtask-rmenu"/>
  101. <div v-if="subitem.detail==''" class="detail-subtask-ricon">
  102. <Icon type="md-trash" @click="handleTask('subtaskDelete', subindex)"/>
  103. </div>
  104. <Poptip
  105. v-else
  106. class="detail-subtask-ricon"
  107. transfer
  108. confirm
  109. :title="$L('你确定你要删除这个子任务吗?')"
  110. @on-ok="handleTask('subtaskDelete', subindex)"
  111. @on-popper-show="$set(subitem, 'showPoptip', true)"
  112. @on-popper-hide="$set(subitem, 'showPoptip', false)"><Icon type="md-trash" /></Poptip>
  113. </div>
  114. </div>
  115. </draggable>
  116. </div>
  117. </div>
  118. <div :style="`${detail.filenum>0?'':'display:none'}`">
  119. <div class="detail-h2 detail-file-box detail-icon">
  120. <strong class="active">{{$L('附件')}}</strong>
  121. <Button class="detail-button" size="small" @click="handleTask('fileupload')">{{$L('添加附件')}}</Button>
  122. </div>
  123. <project-task-files ref="upload" :taskid="taskid" :projectid="detail.projectid" :simple="true" @change="handleTask('filechange', $event)"></project-task-files>
  124. </div>
  125. <div class="detail-h2 detail-comment-box detail-icon"><strong class="link" :class="{active:logType=='评论'}" @click="logType='评论'">{{$L('评论')}}</strong><em></em><strong class="link" :class="{active:logType=='日志'}" @click="logType='日志'">{{$L('操作记录')}}</strong></div>
  126. <div class="detail-log-box">
  127. <project-task-logs ref="log" :logtype="logType" :projectid="detail.projectid" :taskid="taskid" :pagesize="5"></project-task-logs>
  128. </div>
  129. <div class="detail-footer-box">
  130. <Input class="comment-input" v-model="commentText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="commentKeydown" :placeholder="$L('输入评论,Enter发表评论,Shift+Enter换行')" />
  131. <Button :loading="!!loadData.comment" :disabled="!commentText" type="primary" @click="handleTask('comment')">评 论</Button>
  132. </div>
  133. </div>
  134. <div class="detail-right">
  135. <div class="cancel"><em @click="visible=false"></em></div>
  136. <Dropdown trigger="click" class="block" @on-click="handleTask">
  137. <Button :loading="!!loadData.unfinished || !!loadData.complete" :icon="detail.complete?'md-checkmark-circle-outline':'md-radio-button-off'" class="btn">{{$L('标记')}}{{$L(detail.complete?'未完成':'已完成')}}</Button>
  138. <DropdownMenu slot="list">
  139. <DropdownItem name="unfinished">{{$L('标记未完成')}}<Icon v-if="!detail.complete" type="md-checkmark" class="checkmark"/></DropdownItem>
  140. <DropdownItem name="complete">{{$L('标记已完成')}}<Icon v-if="detail.complete" type="md-checkmark" class="checkmark"/></DropdownItem>
  141. <DropdownItem name="archived2">{{$L('完成并归档')}}<Icon v-if="detail.complete && detail.archived" type="md-checkmark" class="checkmark"/></DropdownItem>
  142. </DropdownMenu>
  143. </Dropdown>
  144. <Dropdown trigger="click" class="block" @on-click="handleTask">
  145. <Button :loading="!!loadData.level" icon="md-funnel" class="btn">{{$L('优先级')}}</Button>
  146. <DropdownMenu slot="list">
  147. <DropdownItem v-for="level in [1,2,3,4]" :key="level" :name="`level-${level}`" :class="`p${level}`">{{levelFormt(level)}}<Icon v-if="detail.level==level" type="md-checkmark" class="checkmark"/></DropdownItem>
  148. </DropdownMenu>
  149. </Dropdown>
  150. <Poptip placement="bottom" class="block">
  151. <Button :loading="!!loadData.username" icon="md-person" class="btn">{{$L('负责人')}}</Button>
  152. <div slot="content">
  153. <div style="width:280px">
  154. {{$L('选择负责人')}}
  155. <UserInput :projectid="detail.projectid" :nousername="detail.username" :transfer="false" @change="handleTask('usernameb', $event)" :placeholder="$L('输入关键词搜索')" style="margin:5px 0 3px"></UserInput>
  156. </div>
  157. </div>
  158. </Poptip>
  159. <Poptip ref="timeRef" placement="bottom" class="block" @on-popper-show="handleTask('inittime')">
  160. <Button :loading="!!loadData.plannedtime || !!loadData.unplannedtime" icon="md-calendar" class="btn">{{$L('计划时间')}}</Button>
  161. <div slot="content">
  162. <div style="width:280px">
  163. {{$L('选择日期范围')}}
  164. <Date-picker
  165. v-model="timeValue"
  166. :options="timeOptions"
  167. :placeholder="$L('日期范围')"
  168. format="yyyy-MM-dd HH:mm"
  169. type="datetimerange"
  170. placement="bottom"
  171. @on-ok="handleTask('plannedtimeb')"
  172. @on-clear="handleTask('unplannedtimeb')"
  173. style="display:block;margin:5px 0 3px"></Date-picker>
  174. </div>
  175. </div>
  176. </Poptip>
  177. <Button icon="md-attach" class="btn" @click="handleTask('fileupload')">{{$L('添加附件')}}</Button>
  178. <Poptip ref="attentionRef" v-if="detail.username == myUsername" placement="bottom" class="block" @on-popper-show="() => {$set(detail, 'attentionLists', followerToStr(detail.follower))}">
  179. <Button :loading="!!loadData.attention" icon="md-at" class="btn">{{$L('关注人')}}</Button>
  180. <div slot="content">
  181. <div style="width:280px">
  182. {{$L('选择关注人')}}
  183. <UserInput :projectid="detail.projectid" :multiple="true" :transfer="false" v-model="detail.attentionLists" :placeholder="$L('输入关键词搜索')" style="margin:5px 0 3px"></UserInput>
  184. <Button :loading="!!loadData.attention" :disabled="!detail.attentionLists" class="btn" type="primary" style="text-align:center;width:72px;height:28px;font-size:13px" @click="handleTask('attention')">确 定</Button>
  185. </div>
  186. </div>
  187. </Poptip>
  188. <Button v-else-if="haveAttention(detail.follower)" :loading="!!loadData.unattention" icon="md-at" class="btn" @click="handleTask('unattention', {username:myUsername})">{{$L('取消关注')}}</Button>
  189. <Button v-else :loading="!!loadData.attention" icon="md-at" class="btn" @click="handleTask('attentiona')">{{$L('关注任务')}}</Button>
  190. <Button v-if="!detail.archived" :loading="!!loadData.archived" icon="md-filing" class="btn" @click="handleTask('archived')">{{$L('归档')}}</Button>
  191. <Button v-else :loading="!!loadData.unarchived" icon="md-filing" class="btn" @click="handleTask('unarchived')">{{$L('取消归档')}}</Button>
  192. <Button :loading="!!loadData.delete" icon="md-trash" class="btn" type="error" ghost @click="handleTask('deleteb')">{{$L('删除')}}</Button>
  193. </div>
  194. </div>
  195. </div>
  196. </template>
  197. <script>
  198. import ProjectTaskLogs from "../logs";
  199. import ProjectTaskFiles from "../files";
  200. import draggable from 'vuedraggable'
  201. import cloneDeep from "lodash/cloneDeep";
  202. export default {
  203. components: {ProjectTaskFiles, ProjectTaskLogs, draggable},
  204. data() {
  205. return {
  206. taskid: 0,
  207. detail: {},
  208. visible: false,
  209. urlProjectid: 0,
  210. bakData: {},
  211. loadData: {},
  212. commentText: '',
  213. logType: '评论',
  214. timeValue: [],
  215. timeOptions: {},
  216. myUsername: '',
  217. }
  218. },
  219. beforeCreate() {
  220. let doms = document.querySelectorAll('.project-task-detail-window');
  221. for (let i = 0; i < doms.length; ++i) {
  222. if (doms[i].parentNode != null) doms[i].parentNode.removeChild(doms[i]);
  223. }
  224. },
  225. created() {
  226. let lastSecond = (e) => {
  227. return new Date($A.formatDate("Y-m-d 23:59:29", Math.round(e / 1000)))
  228. };
  229. this.timeOptions = {
  230. shortcuts: [{
  231. text: this.$L('今天'),
  232. value() {
  233. return [new Date(), lastSecond(new Date().getTime())];
  234. }
  235. }, {
  236. text: this.$L('明天'),
  237. value() {
  238. let e = new Date();
  239. e.setDate(e.getDate() + 1);
  240. return [new Date(), lastSecond(e.getTime())];
  241. }
  242. }, {
  243. text: this.$L('本周'),
  244. value() {
  245. return [$A.getData('今天', true), lastSecond($A.getData('本周结束2', true))];
  246. }
  247. }, {
  248. text: this.$L('本月'),
  249. value() {
  250. return [$A.getData('今天', true), lastSecond($A.getData('本月结束', true))];
  251. }
  252. }, {
  253. text: this.$L('3天'),
  254. value() {
  255. let e = new Date();
  256. e.setDate(e.getDate() + 3);
  257. return [new Date(), lastSecond(e.getTime())];
  258. }
  259. }, {
  260. text: this.$L('5天'),
  261. value() {
  262. let e = new Date();
  263. e.setDate(e.getDate() + 5);
  264. return [new Date(), lastSecond(e.getTime())];
  265. }
  266. }, {
  267. text: this.$L('7天'),
  268. value() {
  269. let e = new Date();
  270. e.setDate(e.getDate() + 7);
  271. return [new Date(), lastSecond(e.getTime())];
  272. }
  273. }]
  274. };
  275. },
  276. mounted() {
  277. let match = (window.location.pathname + "").match(/\/project\/panel\/(\d+)$/i);
  278. this.urlProjectid = match ? match[1] : 0;
  279. //
  280. this.$nextTick(() => {
  281. let dom = this.$el;
  282. if (parseInt(this.taskid) === 0) {
  283. if (dom.parentNode != null) dom.parentNode.removeChild(dom);
  284. return;
  285. }
  286. //
  287. dom.addEventListener('transitionend', () => {
  288. if (dom !== null && dom.parentNode !== null && !this.visible) {
  289. dom.parentNode.removeChild(dom);
  290. }
  291. }, false);
  292. //
  293. setTimeout(() => {
  294. this.visible = true;
  295. }, 0)
  296. });
  297. this.bakData = cloneDeep(this.detail);
  298. this.myUsername = $A.getUserName();
  299. this.getTaskDetail();
  300. //
  301. $A.setOnUserInfoListener("components/project/task/detail", () => {
  302. this.myUsername = $A.getUserName();
  303. });
  304. $A.setOnTaskInfoListener('components/project/task/detail',(act, detail) => {
  305. if (detail.id != this.taskid) {
  306. return;
  307. }
  308. if (detail.__modifyUsername == this.myUsername) {
  309. return;
  310. }
  311. this.getTaskDetail();
  312. }, true);
  313. },
  314. watch: {
  315. taskid() {
  316. this.bakData = cloneDeep(this.detail);
  317. this.getTaskDetail();
  318. }
  319. },
  320. computed: {
  321. subtaskProgress() {
  322. const countLists = this.detail.subtask;
  323. if (countLists.length === 0) {
  324. return 0;
  325. }
  326. const completeLists = countLists.filter((item) => { return item.status == 'complete'});
  327. return parseFloat(((completeLists.length / countLists.length) * 100).toFixed(2));
  328. }
  329. },
  330. methods: {
  331. levelFormt(p) {
  332. switch (parseInt(p)) {
  333. case 1:
  334. return this.$L("重要且紧急") + " (P1)";
  335. case 2:
  336. return this.$L("重要不紧急") + " (P2)";
  337. case 3:
  338. return this.$L("紧急不重要") + " (P3)";
  339. case 4:
  340. return this.$L("不重要不紧急") + " (P4)";
  341. }
  342. },
  343. titleKeydown(e) {
  344. e = e || event;
  345. if (e.keyCode == 13) {
  346. e.preventDefault();
  347. e.target.blur();
  348. }
  349. },
  350. descKeydown(e) {
  351. e = e || event;
  352. if (e.keyCode == 13) {
  353. if (e.shiftKey) {
  354. return;
  355. }
  356. e.preventDefault();
  357. e.target.blur();
  358. }
  359. },
  360. commentKeydown(e) {
  361. e = e || event;
  362. if (e.keyCode == 13) {
  363. if (e.shiftKey) {
  364. return;
  365. }
  366. e.preventDefault();
  367. this.handleTask('comment');
  368. }
  369. },
  370. subtaskKeydown(subindex, e) {
  371. e = e || event;
  372. if (e.keyCode == 13) {
  373. if (e.shiftKey) {
  374. return;
  375. }
  376. e.preventDefault();
  377. this.handleTask('subtaskEnter', subindex);
  378. }
  379. },
  380. followerLength(follower) {
  381. if (follower instanceof Array) {
  382. return follower.length;
  383. } else {
  384. return 0;
  385. }
  386. },
  387. followerToStr(follower) {
  388. if (follower instanceof Array) {
  389. return follower.join(",");
  390. } else {
  391. return '';
  392. }
  393. },
  394. haveAttention(follower) {
  395. if (follower instanceof Array) {
  396. return follower.filter((uname) => { return uname == this.myUsername }).length > 0
  397. } else {
  398. return 0;
  399. }
  400. },
  401. getTaskDetail() {
  402. $A.apiAjax({
  403. url: 'project/task/lists',
  404. data: {
  405. taskid: this.taskid,
  406. archived: '全部'
  407. },
  408. error: () => {
  409. alert(this.$L('网络繁忙,请稍后再试!'));
  410. this.visible = false;
  411. },
  412. success: (res) => {
  413. if (res.ret === 1) {
  414. this.detail = res.data;
  415. this.bakData = cloneDeep(this.detail);
  416. this.$nextTick(() => {
  417. this.$refs.titleInput.resizeTextarea();
  418. this.$refs.descInput.resizeTextarea();
  419. this.detail.subtask.forEach((temp, index) => {
  420. this.$refs['subtaskInput_' + (index)][0].resizeTextarea();
  421. })
  422. });
  423. } else {
  424. this.$Modal.error({
  425. title: this.$L('温馨提示'),
  426. content: res.msg,
  427. onOk: () => {
  428. this.visible = false;
  429. }
  430. });
  431. }
  432. }
  433. });
  434. },
  435. subtaskBatchAdd() {
  436. this.inputValue = "";
  437. this.$Modal.confirm({
  438. width: 560,
  439. render: (h) => {
  440. return h('div', [
  441. h('div', {
  442. style: {
  443. fontSize: '16px',
  444. fontWeight: '500',
  445. marginBottom: '20px',
  446. }
  447. }, this.$L('批量添加子任务')),
  448. h('Input', {
  449. props: {
  450. type: 'textarea',
  451. rows: 4,
  452. autosize: {minRows: 4, maxRows: 30},
  453. value: this.inputValue,
  454. placeholder: this.$L('使用换行添加多个子任务')
  455. },
  456. on: {
  457. input: (val) => {
  458. this.inputValue = val;
  459. }
  460. }
  461. })
  462. ])
  463. },
  464. loading: true,
  465. onOk: () => {
  466. if (this.inputValue) {
  467. let tempArray = this.inputValue.split(/\n/);
  468. tempArray.forEach((detail) => {
  469. detail = detail.trim();
  470. detail && this.detail.subtask.push({
  471. id: $A.randomString(6),
  472. uname: $A.getUserName(),
  473. time: Math.round(new Date().getTime()/1000),
  474. status: 'unfinished',
  475. detail: detail
  476. });
  477. });
  478. this.handleTask('subtask', () => {
  479. this.$Modal.remove();
  480. });
  481. } else {
  482. this.$Modal.remove();
  483. }
  484. },
  485. });
  486. },
  487. handleTask(act, eve) {
  488. if (!!this.loadData[act]) {
  489. this.$Message.info(this.$L('请稍候...'));
  490. return;
  491. }
  492. //
  493. let ajaxData = {
  494. act: act,
  495. taskid: this.taskid,
  496. };
  497. let ajaxCallback = () => {};
  498. //
  499. switch (act) {
  500. case 'title':
  501. case 'desc':
  502. if (this.detail[act] == this.bakData[act]) {
  503. return;
  504. }
  505. if (act == 'title' && !this.detail[act]) {
  506. this.$set(this.detail, act, this.bakData[act]);
  507. return;
  508. }
  509. ajaxData.content = this.detail[act];
  510. ajaxCallback = (res) => {
  511. if (res !== 1) {
  512. this.$set(this.detail, act, this.bakData[act]);
  513. }
  514. };
  515. break;
  516. case 'subtaskAdd':
  517. if (!$A.isArray(this.detail.subtask)) {
  518. this.detail.subtask = [];
  519. }
  520. this.detail.subtask.push({
  521. id: $A.randomString(6),
  522. uname: $A.getUserName(),
  523. time: Math.round(new Date().getTime()/1000),
  524. status: 'unfinished',
  525. detail: ''
  526. });
  527. this.$nextTick(() => {
  528. this.$refs['subtaskInput_' + (this.detail.subtask.length - 1)][0].focus();
  529. });
  530. return;
  531. case 'subtaskDelete':
  532. this.detail.subtask.splice(eve, 1);
  533. this.handleTask('subtaskBlur');
  534. return;
  535. case 'subtaskEnter':
  536. if (!$A.isArray(this.detail.subtask)) {
  537. this.detail.subtask = [];
  538. }
  539. if (eve + 1 >= this.detail.subtask.length) {
  540. this.handleTask('subtaskAdd');
  541. return;
  542. }
  543. this.$refs['subtaskInput_' + (eve + 1)][0].focus();
  544. return;
  545. case 'subtaskBlur':
  546. this.handleTask('subtask');
  547. return;
  548. case 'subtask':
  549. let tempArray = cloneDeep(this.detail[act]);
  550. while (tempArray.length > 0 && tempArray[tempArray.length - 1].detail == '') {
  551. tempArray.splice(tempArray.length - 1, 1);
  552. }
  553. if ($A.jsonStringify(tempArray) === $A.jsonStringify(this.bakData[act])) {
  554. return;
  555. }
  556. ajaxData.content = tempArray;
  557. ajaxCallback = (res) => {
  558. if (res !== 1) {
  559. this.$set(this.detail, act, cloneDeep(this.bakData[act]));
  560. }
  561. typeof eve === "function" && eve(res);
  562. };
  563. break;
  564. case 'fileupload':
  565. this.$refs.upload.uploadHandleClick();
  566. return;
  567. case 'filechange':
  568. let filenum = $A.runNum(this.detail.filenum);
  569. switch (eve) {
  570. case 'up':
  571. this.$set(this.detail, 'filenum', filenum + 1);
  572. break;
  573. case 'error':
  574. case 'delete':
  575. this.$set(this.detail, 'filenum', filenum - 1);
  576. break;
  577. }
  578. if (eve == 'add' || eve == 'delete') {
  579. this.logType == '日志' && this.$refs.log.getLists(true, true);
  580. $A.triggerTaskInfoChange(ajaxData.taskid);
  581. }
  582. return;
  583. case 'complete':
  584. case 'unfinished':
  585. case 'archived':
  586. case 'unarchived':
  587. break;
  588. case 'archived2':
  589. ajaxData.act = 'complete';
  590. ajaxCallback = (res) => {
  591. if (res === 1 && !this.detail.archived) {
  592. this.handleTask('archived');
  593. return false;
  594. }
  595. };
  596. break;
  597. case 'level-1':
  598. case 'level-2':
  599. case 'level-3':
  600. case 'level-4':
  601. ajaxData.act = 'level';
  602. ajaxData.content = act.substring(6);
  603. break;
  604. case 'usernameb':
  605. if (!eve.username) {
  606. return;
  607. }
  608. this.$Modal.confirm({
  609. title: this.$L('修改负责人'),
  610. content: this.$L('你确定修改负责人设置为“%”吗?', (eve.nickname || eve.username)),
  611. onOk: () => {
  612. this.handleTask('username', eve);
  613. }
  614. });
  615. return;
  616. case 'username':
  617. if (!eve.username) {
  618. return;
  619. }
  620. ajaxData.content = eve.username;
  621. break;
  622. case 'inittime':
  623. if (this.detail.startdate > 0 && this.detail.enddate > 0) {
  624. this.timeValue = [$A.formatDate("Y-m-d H:i", this.detail.startdate), $A.formatDate("Y-m-d H:i", this.detail.enddate)]
  625. } else {
  626. this.timeValue = [];
  627. }
  628. return;
  629. case 'plannedtimeb':
  630. let temp = $A.date2string(this.timeValue, "Y-m-d H:i");
  631. this.$Modal.confirm({
  632. title: this.$L('修改计划时间'),
  633. content: this.$L('你确定将任务计划时间设置为“%”吗?', temp[0] + "~" + temp[1]),
  634. onOk: () => {
  635. this.handleTask('plannedtime');
  636. }
  637. });
  638. return;
  639. case 'plannedtime':
  640. this.timeValue = $A.date2string(this.timeValue, "Y-m-d H:i");
  641. ajaxData.content = this.timeValue[0] + "," + this.timeValue[1];
  642. this.$refs.timeRef.handleClose();
  643. break;
  644. case 'unplannedtimeb':
  645. this.$Modal.confirm({
  646. title: this.$L('取消计划时间'),
  647. content: this.$L('你确定将任务计划时间取消吗?'),
  648. onOk: () => {
  649. this.handleTask('unplannedtime');
  650. }
  651. });
  652. return;
  653. case 'unplannedtime':
  654. this.$refs.timeRef.handleClose();
  655. break;
  656. case 'attentiona':
  657. ajaxData.act = "attention";
  658. ajaxData.content = this.myUsername;
  659. break;
  660. case 'attention':
  661. if (!this.detail.attentionLists) {
  662. return;
  663. }
  664. ajaxData.content = this.detail.attentionLists;
  665. this.$refs.attentionRef.handleClose();
  666. break;
  667. case 'unattention':
  668. ajaxData.content = eve.username;
  669. if (eve.uisynch === true) {
  670. let bakFollower = cloneDeep(this.detail.follower);
  671. this.$set(this.detail, 'follower', this.detail.follower.filter((uname) => { return uname != eve }));
  672. ajaxCallback = (res) => {
  673. if (res !== 1) {
  674. this.$set(this.detail, 'follower', bakFollower);
  675. }
  676. };
  677. }
  678. break;
  679. case 'deleteb':
  680. this.$Modal.confirm({
  681. title: this.$L('删除提示'),
  682. content: this.$L('您确定要删除此任务吗?'),
  683. onOk: () => {
  684. this.handleTask('delete');
  685. },
  686. });
  687. return;
  688. case 'delete':
  689. ajaxCallback = (res) => {
  690. if (res === 1) {
  691. this.$Modal.info({
  692. title: this.$L('温馨提示'),
  693. content: this.$L('任务已删除,点击确定关闭窗口。'),
  694. onOk: () => {
  695. this.visible = false;
  696. }
  697. });
  698. return false;
  699. }
  700. };
  701. break;
  702. case 'comment':
  703. if (!this.commentText) {
  704. return;
  705. }
  706. ajaxData.content = this.commentText;
  707. ajaxCallback = (res) => {
  708. if (res === 1) {
  709. this.commentText = "";
  710. this.logType == '评论' && this.$refs.log.getLists(true, true);
  711. }
  712. };
  713. break;
  714. default: {
  715. return;
  716. }
  717. }
  718. //
  719. this.$set(this.loadData, ajaxData.act, true);
  720. let runTime = Math.round(new Date().getTime());
  721. $A.apiAjax({
  722. url: 'project/task/edit',
  723. method: 'post',
  724. data: ajaxData,
  725. complete: () => {
  726. this.$set(this.loadData, ajaxData.act, false);
  727. },
  728. error: () => {
  729. ajaxCallback(-1);
  730. alert(this.$L('网络繁忙,请稍后再试!'));
  731. },
  732. success: (res) => {
  733. runTime = Math.round(new Date().getTime()) - runTime;
  734. if (res.ret === 1) {
  735. let tempArray = cloneDeep(this.detail.subtask);
  736. this.detail = res.data;
  737. this.bakData = cloneDeep(this.detail);
  738. while (tempArray.length > 0 && tempArray[tempArray.length - 1].detail == '') {
  739. tempArray.splice(tempArray.length - 1, 1);
  740. this.detail.subtask.push({
  741. id: $A.randomString(6),
  742. uname: $A.getUserName(),
  743. time: Math.round(new Date().getTime()/1000),
  744. status: 'unfinished',
  745. detail: ''
  746. });
  747. }
  748. $A.triggerTaskInfoListener(ajaxData.act, res.data);
  749. $A.triggerTaskInfoChange(ajaxData.taskid);
  750. setTimeout(() => {
  751. if (ajaxCallback(1) !== false) {
  752. this.logType == '日志' && this.$refs.log.getLists(true, true);
  753. this.$Message.success(res.msg);
  754. }
  755. }, Math.max(0, 350 - runTime));
  756. } else {
  757. setTimeout(() => {
  758. ajaxCallback(0);
  759. this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
  760. }, Math.max(0, 350 - runTime));
  761. }
  762. }
  763. });
  764. },
  765. openProject(projectid) {
  766. try {
  767. this.visible = false;
  768. $A.app.$router.push({
  769. name: 'project-panel',
  770. params: {projectid: projectid, statistics: '', other: {}}
  771. });
  772. } catch (e) {
  773. this.visible = true;
  774. }
  775. }
  776. }
  777. }
  778. </script>
  779. <style lang="scss">
  780. .project-task-detail-window {
  781. .detail-title-box {
  782. .detail-title-input {
  783. textarea {
  784. margin: -7px 0 3px -2px;
  785. font-size: 20px;
  786. font-weight: 600;
  787. border: 2px solid #ffffff;
  788. padding: 2px;
  789. cursor: pointer;
  790. color: #172b4d;
  791. background: #ffffff;
  792. width: 100%;
  793. border-radius: 3px;
  794. resize: none;
  795. }
  796. }
  797. }
  798. .detail-desc-box {
  799. .detail-desc-input {
  800. margin: 10px 0 6px;
  801. textarea {
  802. border: 2px solid #F4F5F7;
  803. padding: 5px 8px;
  804. color: #172b4d;
  805. background: rgba(9, 30, 66, 0.04);
  806. resize: none;
  807. }
  808. }
  809. }
  810. .detail-subtask-input {
  811. flex: 1;
  812. border: 0;
  813. background: #ffffff;
  814. margin-left: 2px;
  815. border-bottom: 1px solid #f6f6f6;
  816. textarea {
  817. border: 0;
  818. box-shadow: none;
  819. outline: none;
  820. resize: none;
  821. min-height: auto;
  822. padding-left: 0;
  823. padding-right: 0;
  824. &:focus {
  825. color: #333333;
  826. }
  827. }
  828. &.subtask-complete {
  829. textarea {
  830. text-decoration: line-through;
  831. color: #999;
  832. }
  833. }
  834. }
  835. }
  836. </style>
  837. <style lang="scss" scoped>
  838. .project-task-detail-window {
  839. position: fixed;
  840. z-index: 1001;
  841. top: 0;
  842. left: 0;
  843. height: 100%;
  844. width: 100%;
  845. background-color: rgba(0, 0, 0, 0.5);
  846. transition: all .3s;
  847. opacity: 0;
  848. pointer-events: unset;
  849. display: flex;
  850. flex-direction: column;
  851. align-items: center;
  852. justify-content: center;
  853. &.task-detail-show {
  854. opacity: 1;
  855. }
  856. .task-detail-main {
  857. display: flex;
  858. flex-direction: row;
  859. width: 92%;
  860. max-width: 800px;
  861. max-height: 92%;
  862. background: #ffffff;
  863. overflow: visible;
  864. border-radius: 4px;
  865. padding: 10px 20px 2px;
  866. transform: translateZ(0);
  867. .detail-left {
  868. flex: 1;
  869. padding-left: 8px;
  870. padding-right: 20px;
  871. overflow: auto;
  872. .detail-h2 {
  873. color: #172b4d;
  874. font-size: 16px;
  875. display: flex;
  876. align-items: center;
  877. line-height: 26px;
  878. strong {
  879. font-size: 14px;
  880. font-weight: normal;
  881. &.link {
  882. cursor: pointer;
  883. }
  884. &.active {
  885. font-size: 16px;
  886. font-weight: bold;
  887. }
  888. }
  889. em {
  890. margin: 0 9px;
  891. width: 1px;
  892. height: 10px;
  893. background: #cccccc;
  894. }
  895. .detail-button {
  896. position: absolute;
  897. right: 12px;
  898. top: 50%;
  899. transform: translate(0, -50%);
  900. &:hover {
  901. .detail-button-batch {
  902. display: inline-block;
  903. }
  904. }
  905. .detail-button-btn,
  906. .detail-button-batch {
  907. font-size: 12px;
  908. opacity: 0.5;
  909. transition: all 0.3s;
  910. margin-left: 3px;
  911. &:hover {
  912. opacity: 1;
  913. }
  914. }
  915. .detail-button-batch {
  916. display: none;
  917. }
  918. }
  919. }
  920. .detail-icon {
  921. position: relative;
  922. padding-left: 26px;
  923. &:before {
  924. font-family: zenicon;
  925. font-size: 20px;
  926. color: #42526e;
  927. font-weight: 600;
  928. position: absolute;
  929. top: 0;
  930. left: 0;
  931. width: 26px;
  932. height: 26px;
  933. line-height: 26px;
  934. }
  935. }
  936. .detail-title-box {
  937. margin-top: 12px;
  938. margin-bottom: 12px;
  939. &:before {
  940. content: "\E740";
  941. }
  942. .subtitle {
  943. padding-top: 3px;
  944. font-size: 12px;
  945. color: #606266;
  946. .project-title {
  947. cursor: pointer;
  948. &:hover {
  949. color: #57a3f3;
  950. text-decoration: underline;
  951. }
  952. }
  953. }
  954. }
  955. .detail-desc-box {
  956. &:before {
  957. content: "\E75E";
  958. }
  959. }
  960. .detail-text-box {
  961. margin-bottom: 12px;
  962. li {
  963. color: #606266;
  964. font-size: 14px;
  965. line-height: 32px;
  966. word-break: break-all;
  967. display: flex;
  968. &:before {
  969. font-weight: normal;
  970. color: #606266;
  971. font-size: 14px;
  972. padding-left: 4px;
  973. line-height: 32px;
  974. }
  975. &.text-time {
  976. &:before {
  977. content: "\E706";
  978. }
  979. }
  980. &.text-username {
  981. &:before {
  982. content: "\E903";
  983. }
  984. }
  985. &.text-follower {
  986. &:before {
  987. content: "\E90D";
  988. }
  989. }
  990. &.text-level {
  991. &:before {
  992. content: "\E725";
  993. }
  994. }
  995. &.text-status {
  996. &:before {
  997. content: "\E6AF";
  998. }
  999. }
  1000. > span {
  1001. white-space: nowrap;
  1002. }
  1003. > em {
  1004. margin-left: 4px;
  1005. padding-top: 5px;
  1006. line-height: 22px;
  1007. &.p1 {
  1008. color: #ed3f14;
  1009. }
  1010. &.p2 {
  1011. color: #ff9900;
  1012. }
  1013. &.p3 {
  1014. color: #19be6b;
  1015. }
  1016. &.p4 {
  1017. color: #666666;
  1018. }
  1019. &.complete {
  1020. color: #666666;
  1021. .completedate {
  1022. font-size: 12px;
  1023. padding-left: 4px;
  1024. opacity: 0.6;
  1025. }
  1026. }
  1027. &.overdue {
  1028. color: #ff0000;
  1029. }
  1030. &.unfinished {
  1031. color: #19be6b;
  1032. }
  1033. }
  1034. }
  1035. }
  1036. .detail-file-box {
  1037. &:before {
  1038. content: "\E8B9";
  1039. font-size: 16px;
  1040. padding-left: 2px;
  1041. }
  1042. }
  1043. .detail-subtask-icon {
  1044. &:before {
  1045. content: "\E819";
  1046. font-size: 16px;
  1047. padding-left: 2px;
  1048. }
  1049. }
  1050. .detail-subtask-box {
  1051. padding: 12px;
  1052. margin-bottom: 4px;
  1053. .detail-subtask-progress {
  1054. margin: 2px 0 6px;
  1055. }
  1056. .detail-subtask-item {
  1057. display: flex;
  1058. flex-direction: row;
  1059. align-items: center;
  1060. margin: 0 2px 0 -6px;
  1061. padding-top: 4px;
  1062. padding-left: 8px;
  1063. position: relative;
  1064. background-color: #ffffff;
  1065. &:hover {
  1066. .detail-subtask-right {
  1067. opacity: 1;
  1068. }
  1069. }
  1070. .detail-subtask-right {
  1071. opacity: 0;
  1072. position: absolute;
  1073. top: 50%;
  1074. right: 0;
  1075. padding: 0 6px;
  1076. transform: translate(0, -50%);
  1077. background: #ffffff;
  1078. border-radius: 3px 0 0 3px;
  1079. transition: all 0.3s;
  1080. cursor: pointer;
  1081. box-shadow: -3px 0px 3px 0px rgba(45, 45, 45, 0.1);
  1082. .detail-subtask-ricon {
  1083. &:hover {
  1084. opacity: 1;
  1085. }
  1086. opacity: 0.9;
  1087. width: 18px;
  1088. height: 26px;
  1089. line-height: 26px;
  1090. font-size: 16px;
  1091. text-align: center;
  1092. }
  1093. }
  1094. }
  1095. .detail-subtask-none {
  1096. color: #666666;
  1097. cursor: pointer;
  1098. padding: 0 12px;
  1099. }
  1100. }
  1101. .detail-comment-box {
  1102. &:before {
  1103. content: "\E753";
  1104. }
  1105. }
  1106. .detail-footer-box {
  1107. border-top: 1px solid #e5e5e5;
  1108. display: flex;
  1109. flex-direction: row;
  1110. padding-top: 20px;
  1111. padding-bottom: 16px;
  1112. .comment-input {
  1113. margin-right: 12px;
  1114. }
  1115. }
  1116. }
  1117. .detail-right {
  1118. .cancel {
  1119. text-align: right;
  1120. width: auto;
  1121. height: 38px;
  1122. em {
  1123. display: inline-block;
  1124. width: 38px;
  1125. height: 38px;
  1126. cursor: pointer;
  1127. border-radius: 50%;
  1128. transform: scale(0.92);
  1129. &:after,
  1130. &:before {
  1131. position: absolute;
  1132. content: "";
  1133. top: 50%;
  1134. left: 50%;
  1135. width: 2px;
  1136. height: 20px;
  1137. background-color: #EE2321;
  1138. transform: translate(-50%, -50%) rotate(45deg) scale(0.6, 1);
  1139. transition: all .2s;
  1140. }
  1141. &:before {
  1142. position: absolute;
  1143. transform: translate(-50%, -50%) rotate(-45deg) scale(0.6, 1);
  1144. }
  1145. &:hover {
  1146. &:after,
  1147. &:before {
  1148. background-color: #ff0000;
  1149. transform: translate(-50%, -50%) rotate(135deg) scale(0.6, 1);
  1150. }
  1151. &:before {
  1152. background-color: #ff0000;
  1153. transform: translate(-50%, -50%) rotate(45deg) scale(0.6, 1);
  1154. }
  1155. }
  1156. }
  1157. }
  1158. .block {
  1159. display: block;
  1160. .p1 {
  1161. color: #ed3f14;
  1162. }
  1163. .p2 {
  1164. color: #ff9900;
  1165. }
  1166. .p3 {
  1167. color: #19be6b;
  1168. }
  1169. .p4 {
  1170. color: #666666;
  1171. }
  1172. .checkmark {
  1173. margin-left: 8px;
  1174. margin-right: -8px;
  1175. }
  1176. }
  1177. .btn {
  1178. display: block;
  1179. width: 118px;
  1180. text-align: left;
  1181. margin-top: 8px;
  1182. padding-left: 10px;
  1183. padding-right: 10px;
  1184. overflow: hidden;
  1185. white-space: nowrap;
  1186. text-overflow: ellipsis;
  1187. }
  1188. }
  1189. }
  1190. }
  1191. </style>