projectView.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. <template>
  2. <div class="app-container">
  3. <el-form :model="queryParams" ref="queryForm" size="mini" :inline="true">
  4. <el-form-item prop="projectId">
  5. <el-cascader
  6. :options="projectTree"
  7. @change="selectProjectChange"
  8. :props="{ expandTrigger: 'hover',value:'id',label:'name', checkStrictly: true }"
  9. collapse-tags
  10. placeholder="选择分类/项目"
  11. :show-all-levels="false" clearable></el-cascader>
  12. </el-form-item>
  13. <el-form-item prop="executors">
  14. <dept-user-tree ref="dut" :userList="userList" :multiple="true"
  15. @selected="selectExecutors"></dept-user-tree>
  16. </el-form-item>
  17. <el-form-item label="日期" prop="startDate">
  18. <el-date-picker
  19. v-model="queryParams.startDate"
  20. type="date"
  21. value-format="yyyy-MM-dd"
  22. @change="getList"
  23. placeholder="开始日期"
  24. :clearable="false"
  25. style="width: 135px">
  26. </el-date-picker>
  27. </el-form-item>
  28. <el-form-item label="至" prop="endDate">
  29. <el-date-picker
  30. v-model="queryParams.endDate"
  31. type="date"
  32. value-format="yyyy-MM-dd"
  33. @change="getList"
  34. placeholder="结束日期"
  35. :clearable="false"
  36. style="width: 135px">
  37. </el-date-picker>
  38. </el-form-item>
  39. <el-form-item prop="statusGroup">
  40. <el-checkbox-group v-model="queryParams.statusGroup" size="mini" @change="getList">
  41. <el-checkbox-button
  42. v-for="dict in dict.type.task_status"
  43. :key="dict.value"
  44. :label="dict.value"
  45. >{{ dict.label }}
  46. </el-checkbox-button>
  47. </el-checkbox-group>
  48. </el-form-item>
  49. <el-form-item prop="priority">
  50. <el-select
  51. v-model="queryParams.priority"
  52. @change="getList"
  53. clearable
  54. placeholder="优先级"
  55. style="width: 85px">
  56. <el-option
  57. v-for="dict in dict.type.task_priority"
  58. :key="dict.value"
  59. :label="dict.label"
  60. :value="dict.value">
  61. <el-tag :color="priorityColorMap[dict.value]" effect="dark" :hit="false">
  62. {{ dict.label }}
  63. </el-tag>
  64. </el-option>
  65. </el-select>
  66. </el-form-item>
  67. </el-form>
  68. <el-table
  69. class="view-table"
  70. :data="tableData"
  71. v-el-table-infinite-scroll="loadData"
  72. :infinite-scroll-disabled="disabled"
  73. size="mini"
  74. @cell-mouse-enter="cellMouseEnter"
  75. @cell-mouse-leave="cellMouseLeave"
  76. :cell-style="cellStyle"
  77. @row-click="rowClick"
  78. height="calc(100vh - 110px)"
  79. row-key="id"
  80. lazy
  81. :indent="10"
  82. :load="loadChildren"
  83. :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
  84. border>
  85. <el-table-column fixed prop="id" header-align="center" label="编号"/>
  86. <el-table-column fixed prop="taskName" label="任务名称" align="center" class="task-name-cell" width="180"/>
  87. <el-table-column fixed prop="executorName" label="主负责人" align="center" width="90">
  88. <template slot-scope="scope">
  89. <div style="line-height: 15px">{{ scope.row.executorName }}</div>
  90. <el-tag size="mini" :type="statusMap[scope.row.status].type">
  91. {{ statusMap[scope.row.status].name + ' ' + scope.row.progressValue + '%' }}
  92. </el-tag>
  93. </template>
  94. </el-table-column>
  95. <el-table-column fixed prop="" label="优先级" align="center" width="60">
  96. <template slot-scope="scope">
  97. <el-tag :color="priorityColorMap[scope.row.priority]" effect="dark" :hit="false">
  98. {{ getTaskPriority(scope.row.priority) }}
  99. </el-tag>
  100. </template>
  101. </el-table-column>
  102. <el-table-column align="center" v-for="header in tableHeaders" :key="header.day" :prop="header.day" width="42">
  103. <template slot="header">
  104. <div style="width:40px;text-align: center">
  105. <div style="font-size: 10px;text-align: center">{{ header.day }}</div>
  106. <div v-if="header.week=='周六'" class="view-cell" style="color: #1c84c6">{{ header.week }}</div>
  107. <div v-else-if="header.week=='周日'" class="view-cell" style="color: #1c84c6">{{ header.week }}</div>
  108. <div v-else-if="header.week=='今日'" class="view-cell" style="color: #00E704">{{ header.week }}</div>
  109. <div v-else class="view-cell">{{ header.week }}</div>
  110. </div>
  111. </template>
  112. <template slot-scope="scope">
  113. <span v-if="scope.row[header.day].comment" class="comment-badge"></span>
  114. <el-popover
  115. v-if="scope.row[header.day].value!=''"
  116. placement="top"
  117. width="700"
  118. trigger="click">
  119. <el-table :data="feedbacks" border size="mini">
  120. <el-table-column width="75" label="反馈状态" align="center">
  121. <template slot-scope="temp">
  122. <div>{{ getFeedbackTypeName(temp.row.feedbackType) }}</div>
  123. </template>
  124. </el-table-column>
  125. <el-table-column width="60" property="userName" label="反馈人" align="center"/>
  126. <el-table-column width="55" label="完成度" align="center">
  127. <template slot-scope="temp">
  128. <div>{{ !temp.row.value ? '' : temp.row.value + '%' }}</div>
  129. </template>
  130. </el-table-column>
  131. <el-table-column width="60" prop="hours" label="工时(h)" align="center"/>
  132. <el-table-column width="110" property="createTime" label="反馈时间" align="center">
  133. <template slot-scope="temp">
  134. <div>{{ parseTime(temp.row.createTime) }}</div>
  135. </template>
  136. </el-table-column>
  137. <el-table-column label="反馈备注" align="center">
  138. <template slot-scope="temp">
  139. <div>
  140. <span>{{ temp.row.description }}</span>
  141. <span v-for="(file,index) in temp.row.fileList">
  142. <a :href="file.url" style="color: darkgreen">
  143. <span style="margin-left: 5px">{{ file.name }}</span>
  144. </a>
  145. </span>
  146. </div>
  147. </template>
  148. </el-table-column>
  149. <el-table-column label="当前达成进度" width="100px" align="center">
  150. <template slot-scope="temp">
  151. <el-popover
  152. v-if="temp.row.descriptionDetail&&temp.row.descriptionDetail.length>0"
  153. placement="top"
  154. width="600"
  155. trigger="hover">
  156. <div v-html="temp.row.descriptionDetail"></div>
  157. <el-button slot="reference" type="text">详情</el-button>
  158. </el-popover>
  159. </template>
  160. </el-table-column>
  161. </el-table>
  162. <div slot="reference" style="line-height:40px;font-size: 10px;cursor: pointer">
  163. {{ scope.row[header.day].value }}
  164. </div>
  165. </el-popover>
  166. </template>
  167. </el-table-column>
  168. </el-table>
  169. <!-- 添加评论对话框 -->
  170. <el-dialog title="添加评论" :visible.sync="open" width="680px" class="feed-dialog" append-to-body @close="cancel">
  171. <el-form ref="form" :model="form" :rules="rules" size="mini" label-width="100px">
  172. <el-form-item label="任务名称">
  173. <div>{{ form.taskName }}</div>
  174. </el-form-item>
  175. <el-row :gutter="20">
  176. <el-col :span="12">
  177. <el-form-item label="所属项目">
  178. <div>{{ form.projectName }}</div>
  179. </el-form-item>
  180. </el-col>
  181. <el-col :span="12">
  182. <el-form-item label="反馈时间">
  183. <div>{{ form.feedbackDate }}</div>
  184. </el-form-item>
  185. </el-col>
  186. </el-row>
  187. <el-form-item label="附件" prop="fileUrl">
  188. <file-upload ref="fu" @getFileUrl="getFileUrl" @removeFile="removeFile"></file-upload>
  189. </el-form-item>
  190. <el-form-item label="描述" prop="description">
  191. <el-input v-model="form.description" type="textarea" autosize/>
  192. </el-form-item>
  193. <el-form-item label="描述详情" prop="descriptionDetail">
  194. <rich-text-editor v-model="form.descriptionDetail"></rich-text-editor>
  195. </el-form-item>
  196. </el-form>
  197. <div slot="footer" class="dialog-footer">
  198. <el-button type="primary" @click="submitForm" size="mini">确 定</el-button>
  199. <el-button @click="cancel" size="mini">取 消</el-button>
  200. </div>
  201. </el-dialog>
  202. <!-- 任务详情对话框 -->
  203. <el-dialog :title="detailForm.id+'、'+detailForm.taskName" :visible.sync="openDetail" width="900px"
  204. class="feed-dialog" :close-on-click-modal="true" append-to-body>
  205. <task-detail :detailForm="detailForm"></task-detail>
  206. </el-dialog>
  207. <!-- 审核任务对话框 -->
  208. <el-dialog :title="detailForm.id+'、'+detailForm.taskName" :visible.sync="auditOpen" width="900px" class="add-dialog"
  209. append-to-body @close="auditCancel">
  210. <task-detail :detail-form="detailForm"></task-detail>
  211. <el-form ref="auditForm" :model="auditForm" :rules="auditRules" size="mini" label-width="100px">
  212. <el-row>
  213. <el-col :span="12">
  214. <el-form-item label="审核意见" prop="auditResult">
  215. <el-radio-group v-model="auditForm.auditResult">
  216. <el-radio label="1">确认完成</el-radio>
  217. <el-radio label="0">驳回</el-radio>
  218. <el-radio label="9">评论</el-radio>
  219. </el-radio-group>
  220. </el-form-item>
  221. </el-col>
  222. <el-col :span="12">
  223. <el-form-item v-if="auditForm.auditResult==='0'" label="进度(%)" prop="value">
  224. <el-input-number v-model="auditForm.value" :min="1" :max="99"></el-input-number>
  225. </el-form-item>
  226. </el-col>
  227. </el-row>
  228. <el-form-item label="审核备注" prop="auditOpinion">
  229. <el-input v-model="auditForm.auditOpinion" type="textarea" :maxlength="1000"/>
  230. </el-form-item>
  231. </el-form>
  232. <div slot="footer" class="dialog-footer">
  233. <el-button type="primary" size="mini" @click="submitAudit">确 定</el-button>
  234. <el-button size="mini" @click="auditCancel">取 消</el-button>
  235. </div>
  236. </el-dialog>
  237. </div>
  238. </template>
  239. <script>
  240. import {
  241. getTask,
  242. listView,
  243. addTaskFeedback,
  244. getFeedbackList,
  245. confirmComment,
  246. listProjectView,
  247. childrenTask, auditTask
  248. } from "@/api/task/task";
  249. import {getProjectList, getProjectTree} from "@/api/task/project";
  250. import DateUtil from "@/utils/date"
  251. import TaskDetail from "./components/taskDetail"
  252. import FileUpload from "@/components/FileUpload"
  253. import DeptUserTree from "@/components/DeptUserTree"
  254. import RichTextEditor from "@/components/RichTextEditor"
  255. import {getDeptUserTree} from "@/api/system/user";
  256. import task from "@/views/mixins/task";
  257. import elTableInfiniteScroll from 'el-table-infinite-scroll'
  258. import {mapGetters} from "vuex";
  259. export default {
  260. components: {TaskDetail, FileUpload, DeptUserTree, RichTextEditor},
  261. dicts: ['task_status', 'task_priority'],
  262. directives: {
  263. 'el-table-infinite-scroll': elTableInfiniteScroll
  264. },
  265. mixins: [task],
  266. computed: {
  267. ...mapGetters(['userId'])
  268. },
  269. data() {
  270. return {
  271. queryParams: {
  272. projectId: undefined,
  273. startDate: undefined,
  274. endDate: undefined,
  275. statusGroup: ['0', '1', '2', '3'],
  276. priority: undefined
  277. },
  278. projectTree: [],
  279. userList: [],
  280. tableHeaders: [],
  281. totalData: [],
  282. disabled: false,
  283. tableData: [],
  284. page: 0,
  285. total: 0,
  286. clickCount: 0,
  287. timer: null,
  288. open: false,
  289. form: {},
  290. feedbacks: [],
  291. rules: {
  292. description: [
  293. {required: true, message: "描述不能为空", trigger: "blur"},
  294. {min: 3, max: 500, message: '长度在 3 到 500 个字符', trigger: 'blur'}
  295. ]
  296. },
  297. openDetail: false,
  298. detailForm: {},
  299. auditOpen: false,
  300. auditForm: {},
  301. auditRules: {
  302. auditResult: [
  303. {required: true, message: "审核意见不能为空", trigger: "change"}
  304. ],
  305. value: [
  306. {required: true, message: "进度不能为空", trigger: "blur"}
  307. ]
  308. }
  309. }
  310. },
  311. watch: {
  312. totalData: {
  313. handler(newVal, oldVal) {
  314. this.loadData()
  315. },
  316. deep: true,
  317. immediate: true
  318. }
  319. },
  320. created() {
  321. this.initData();
  322. },
  323. methods: {
  324. initData() {
  325. this.$set(this.queryParams, 'startDate', DateUtil.beforeDay(null, 15))
  326. this.$set(this.queryParams, 'endDate', DateUtil.afterDay(null, 15))
  327. this.getList()
  328. getProjectTree({status: '0'}).then(res => {
  329. this.projectTree = res.data
  330. })
  331. getDeptUserTree('1').then(res => {
  332. this.userList = res.data
  333. })
  334. },
  335. //项目选择事件
  336. selectProjectChange(val) {
  337. this.queryParams.projectId = val[val.length - 1]
  338. this.getList()
  339. },
  340. selectExecutors(val) {
  341. this.queryParams.executors = val
  342. this.getList()
  343. },
  344. getList() {
  345. if (DateUtil.unix(this.queryParams.startDate) > DateUtil.unix(this.queryParams.endDate)) {
  346. this.$message.warning("开始时间不能超过结束时间")
  347. return
  348. }
  349. let dayDiff = DateUtil.dayDiff(this.queryParams.startDate, this.queryParams.endDate);
  350. if (dayDiff > 366) {
  351. this.$message.warning("时间跨度不可超过一年")
  352. return
  353. }
  354. listProjectView(this.queryParams).then(res => {
  355. this.tableData = []
  356. this.page = 0
  357. this.disabled = false
  358. this.tableHeaders = res.data.headers
  359. this.totalData = res.data.data || []
  360. this.total = this.totalData.length > 0 ? Number.parseInt(this.totalData.length / 15) + 1 : 0
  361. })
  362. },
  363. loadData() {
  364. if (this.disabled) return;
  365. if (this.totalData.length === 0) return;
  366. if (this.page < this.total) {
  367. let temp = this.totalData.slice(this.page * 15, (this.page + 1) * 15)
  368. this.tableData = this.tableData.concat(temp);
  369. }
  370. this.page++;
  371. if (this.page === this.total) {
  372. this.disabled = true;
  373. }
  374. },
  375. cellStyle({row, column, rowIndex, columnIndex}) {
  376. let style = {
  377. fontSize: '12px',
  378. padding: '2px 0'
  379. }
  380. if (row[column.property]) {
  381. style.background = this.cellColorMap[row[column.property].color]
  382. }
  383. return style
  384. },
  385. cellClick(row, column, cell, event) {
  386. this.feedbacks = []
  387. if (!row[column.property].value || row[column.property].value === '') {
  388. return;
  389. }
  390. let date = row[column.property].date
  391. if (DateUtil.unix(date) > DateUtil.unix()) {
  392. return;
  393. }
  394. getFeedbackList(row.id, date).then(res => {
  395. this.feedbacks = res.data.map(item => {
  396. item['executor'] = row.executor
  397. return item
  398. })
  399. })
  400. },
  401. cellDbClick(row, column, cell, event) {
  402. if (row.status === '4') {
  403. this.$message.warning("任务已完成,不可评论")
  404. return;
  405. }
  406. if (row.status === '5') {
  407. this.$message.warning("任务已终止,不可评论")
  408. return;
  409. }
  410. let feedbackDate = DateUtil.getFeedBackDate(this.queryParams.startDate, this.queryParams.endDate, column.property)
  411. if (DateUtil.unix(feedbackDate) > DateUtil.unix()) {
  412. this.$message.warning("评论时间不能超过:" + DateUtil.day())
  413. return;
  414. }
  415. this.form = {
  416. id: undefined,
  417. taskName: row.taskName,
  418. taskId: row.id,
  419. projectName: row.projectName,
  420. feedbackDate: feedbackDate,
  421. feedbackType: '4',
  422. fileUrl: undefined,
  423. description: undefined
  424. };
  425. this.open = true
  426. },
  427. rowClick(row, column, event) {
  428. if (column.property === "taskName") {
  429. if (row.status === '6' && row.auditUserId === this.userId) {
  430. getTask(row.id).then(res => {
  431. if (res.data.ancestorsTask && res.data.ancestorsTask.length === 0) {
  432. this.detailForm = res.data;
  433. this.auditForm = {
  434. taskId: row.id,
  435. auditResult: undefined,
  436. value: undefined,
  437. auditOpinion: undefined
  438. }
  439. this.auditOpen = true;
  440. } else {
  441. this.detailForm = res.data
  442. this.openDetail = true
  443. }
  444. })
  445. } else {
  446. getTask(row.id).then(res => {
  447. this.detailForm = res.data
  448. this.openDetail = true
  449. })
  450. }
  451. }
  452. this.clickCount++;
  453. // 判断点击次数,如果是首次点击,则启动延时器
  454. if (this.clickCount === 1) {
  455. this.timer = setTimeout(() => {
  456. // 执行单击操作
  457. this.cellClick(row, column);
  458. this.clickCount = 0
  459. }, 300); // 设置延时时间,单位为毫秒
  460. } else {
  461. // 如果点击次数大于1,则说明是双击操作,清除延时器,并执行双击操作
  462. clearTimeout(this.timer);
  463. this.cellDbClick(row, column);
  464. this.clickCount = 0
  465. }
  466. },
  467. feedbackTypeChange(val) {
  468. if (val === '2') {
  469. this.form.value = 100
  470. } else {
  471. this.form.value = undefined
  472. }
  473. },
  474. // 取消按钮
  475. cancel() {
  476. this.$refs.fu.clear()
  477. this.reset();
  478. this.open = false;
  479. },
  480. // 表单重置
  481. reset() {
  482. this.form = {
  483. id: undefined,
  484. taskName: undefined,
  485. taskId: undefined,
  486. projectName: undefined,
  487. feedbackDate: undefined,
  488. feedbackType: undefined,
  489. value: undefined,
  490. hours: undefined,
  491. fileUrl: undefined,
  492. description: undefined
  493. };
  494. this.resetForm("form");
  495. },
  496. getFileUrl(val) {
  497. this.form['files'] = val
  498. },
  499. removeFile(val) {
  500. this.form.files = val
  501. },
  502. /** 提交按钮 */
  503. submitForm() {
  504. this.$refs["form"].validate(valid => {
  505. if (valid) {
  506. if (this.form.files) {
  507. let files = this.form.files.map(item => item.name);
  508. this.form.fileUrl = JSON.stringify(files)
  509. }
  510. let descriptionDetail = _.cloneDeep(this.form.descriptionDetail)
  511. if (!this.checkRichText(descriptionDetail)) {
  512. this.form.descriptionDetail = null
  513. }
  514. addTaskFeedback(this.form).then(res => {
  515. this.$message.success("评论成功");
  516. this.open = false;
  517. this.getList();
  518. });
  519. }
  520. })
  521. },
  522. getFeedbackTypeName(type) {
  523. if (type === '1') {
  524. return '进度反馈'
  525. } else if (type === '2') {
  526. return '完成'
  527. } else if (type === '3') {
  528. return '终止'
  529. } else if (type === '4') {
  530. return '评论'
  531. }
  532. return '审批'
  533. },
  534. /** 确认收到评论 */
  535. confirmComment(row) {
  536. confirmComment({feedbackId: row.id}).then(res => {
  537. if (res.code === '2000') {
  538. this.getList()
  539. }
  540. })
  541. },
  542. loadChildren(tree, treeNode, resolve) {
  543. let params = {
  544. projectId: this.queryParams.projectId,
  545. startDate: this.queryParams.startDate,
  546. endDate: this.queryParams.endDate,
  547. status: this.queryParams.status,
  548. priority: undefined,
  549. parentTaskId: tree.id
  550. }
  551. childrenTask(params).then(res => {
  552. resolve(res.data)
  553. })
  554. },
  555. /** 审核任务提交按钮 */
  556. submitAudit() {
  557. this.$refs["auditForm"].validate(valid => {
  558. if (valid) {
  559. if (this.auditForm.auditResult != '9') {
  560. auditTask(this.auditForm).then(res => {
  561. this.$message.success("操作成功");
  562. this.getList()
  563. this.auditCancel()
  564. })
  565. } else {
  566. let data = {
  567. taskId: this.auditForm.taskId,
  568. feedbackDate: DateUtil.day(),
  569. feedbackType: '4',
  570. description: this.auditForm.auditOpinion
  571. }
  572. addTaskFeedback(data).then(res => {
  573. this.$message.success("评论成功");
  574. this.auditCancel()
  575. });
  576. }
  577. }
  578. });
  579. },
  580. // 审核取消按钮
  581. auditCancel() {
  582. this.detailForm = {};
  583. this.auditForm = {
  584. taskId: undefined,
  585. auditResult: undefined,
  586. value: undefined,
  587. auditOpinion: undefined
  588. }
  589. this.resetForm("auditForm");
  590. this.auditOpen = false;
  591. },
  592. }
  593. }
  594. </script>
  595. <style scoped lang="scss">
  596. ::v-deep.el-table .cell {
  597. padding: 0px;
  598. }
  599. ::v-deep.el-table th.el-table__cell > .cell {
  600. padding: 0px
  601. }
  602. .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item {
  603. margin-bottom: 10px;
  604. }
  605. .view-cell {
  606. width: 40px;
  607. font-size: 10px;
  608. text-align: center
  609. }
  610. .comment-badge {
  611. height: 10px;
  612. width: 10px;
  613. border-radius: 5px;
  614. background-color: #409eff;
  615. position: absolute;
  616. top: 1px;
  617. right: 1px;
  618. font-size: 8px;
  619. color: white;
  620. vertical-align: text-top;
  621. }
  622. .feed-dialog ::v-deep .el-dialog__body {
  623. padding: 0 20px 10px 20px;
  624. }
  625. .add-dialog ::v-deep .el-dialog__body {
  626. padding: 0 20px 10px 20px;
  627. }
  628. .el-tag--dark {
  629. border-color: white;
  630. }
  631. </style>