detail.vue 56 KB

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