index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <template>
  2. <div class="h-full flex">
  3. <div class="p-5 w-80 pr-0" v-if="layoutSingleChoice == TABLE_VIEW">
  4. <div class="bg-white w-full h-full shadow-md rounded-md flex flex-col">
  5. <div class="flex-1 p-3 overflow-y-auto scroll-bar">
  6. <el-form :model="searchForm">
  7. <el-form-item label="任务名称:" label-width="7em" prop="taskName">
  8. <el-input v-model="searchForm.taskName" placeholder="请输入" />
  9. </el-form-item>
  10. <el-form-item label="优先级:" label-width="7em" prop="priority">
  11. <el-select v-model="searchForm.priority" placeholder="请选择">
  12. <el-option v-for="item in PRIORITY" :key="item.value" :value="item.value" :label="item.label" />
  13. </el-select>
  14. </el-form-item>
  15. <el-form-item label="客户名称:" label-width="7em" prop="customName">
  16. <el-input v-model="searchForm.customName" placeholder="请输入" />
  17. </el-form-item>
  18. <el-form-item label="联系人号码:" label-width="7em" prop="contactsName">
  19. <el-input v-model="searchForm.contactsName" placeholder="请输入" />
  20. </el-form-item>
  21. <el-form-item label="执行人:" label-width="7em" prop="executorName">
  22. <el-input v-model="searchForm.executorName" placeholder="请输入" />
  23. </el-form-item>
  24. <el-form-item :label="`${businessLabel}名称`" label-width="7em" prop="businessName">
  25. <el-input v-model="searchForm.businessName" placeholder="请输入" />
  26. </el-form-item>
  27. <el-form-item label="销售订单:" label-width="7em" prop="orderName">
  28. <el-input v-model="searchForm.orderName" placeholder="请输入" />
  29. </el-form-item>
  30. <el-form-item label="线索名称:" label-width="7em" prop="clueName">
  31. <el-input v-model="searchForm.clueName" placeholder="请输入" />
  32. </el-form-item>
  33. <el-form-item label="任务状态:" label-width="7em" prop="status">
  34. <el-select v-model="searchForm.status" placeholder="请选择">
  35. <el-option v-for="item in STATUS" :key="item.value" :value="item.value" :label="item.label" />
  36. </el-select>
  37. </el-form-item>
  38. <el-form-item label="开始时间:" label-width="7em" prop="startDate">
  39. <el-date-picker v-model="searchForm.startDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
  40. </el-form-item>
  41. <el-form-item label="截止时间:" label-width="7em" prop="endDate">
  42. <el-date-picker v-model="searchForm.endDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
  43. </el-form-item>
  44. </el-form>
  45. </div>
  46. <div class="w-full flex p-3 shadow-[0_-3px_5px_0px_rgba(0,0,0,0.2)]">
  47. <el-button size="large" class="w-full" @click="reset()">重置</el-Button>
  48. <el-button type="primary" size="large" class="w-full" @click="search()">搜索</el-Button>
  49. </div>
  50. </div>
  51. </div>
  52. <div class="flex-1 p-5 overflow-auto">
  53. <div class="bg-white w-full h-full p-3 shadow-md rounded-md flex flex-col">
  54. <div class="p-3">
  55. <div class="flex items-center w-full justify-between">
  56. <div>
  57. <el-radio-group v-model="layoutSingleChoice" @change="viewsSwitching">
  58. <el-radio-button label="表格视图" :value="TABLE_VIEW" />
  59. <el-radio-button label="日历视图" :value="KANBAN_VIEW" />
  60. </el-radio-group>
  61. </div>
  62. <div class="justify-end flex">
  63. <el-button type="primary" v-permission="['tasksAdd']" @click="createTasks()">创建任务</el-Button>
  64. <el-button type="primary" v-permission="['tasksDelete']" :disabled="len == 0" :loading="btnLoading" @click="deleteTasks()">批量删除</el-Button>
  65. <el-button type="primary" v-permission="['tasksImport']" @click="openImportModal()">导入</el-Button>
  66. <!-- <el-button type="primary" :loading="btnLoading" @click="exportTasks()">导出</el-Button> -->
  67. <el-button type="primary" v-permission="['tasksExport']" :loading="btnLoading" @click="newExportTasks()">导出</el-Button>
  68. </div>
  69. </div>
  70. </div>
  71. <template v-if="layoutSingleChoice == TABLE_VIEW">
  72. <div class="flex-1 overflow-y-auto">
  73. <el-table :data="tableData" :show-overflow-tooltip="tableShowOverflowTooltip" style="width: 100%;height: 100%;" ref="tableRef" v-loading="loading">
  74. <el-table-column type="selection" width="55" />
  75. <el-table-column prop="taskName" label="任务名称" header-align="center" align="center" show-overflow-tooltip
  76. width="200" />
  77. <el-table-column prop="priority" label="优先级" width="100" :sortable="true" header-align="center"
  78. align="center">
  79. <template #default="scope">
  80. {{ PRIORITY.find(item => item.value == scope.row.priority)?.label }}
  81. </template>
  82. </el-table-column>
  83. <el-table-column prop="status" label="状态" width="100" header-align="center" align="center">
  84. <template #default="scope">
  85. <el-text :type="STATUS[scope.row.status]?.type">
  86. {{ STATUS[scope.row.status]?.label }}
  87. </el-text>
  88. </template>
  89. </el-table-column>
  90. <el-table-column prop="executorNames" label="执行人" width="120" header-align="center" align="center">
  91. <template #default="scope">
  92. <template v-for="(item, index) in (scope.row.taskExecutors || [])">
  93. <TextTranslation translationTypes="userName" :translationValue="item">
  94. </TextTranslation>
  95. <span v-if="index < (scope.row.taskExecutors || []).length - 1">,</span>
  96. </template>
  97. </template>
  98. </el-table-column>
  99. <el-table-column prop="startDate" label="开始时间" width="200" :sortable="true" header-align="center"
  100. align="center" value-format="YYYY-MM-DD" />
  101. <el-table-column prop="endDate" label="截止时间" width="200" :sortable="true" header-align="center"
  102. align="center" value-format="YYYY-MM-DD">
  103. <template #default="scope">
  104. <div :class="`${scope.row.endDate && scope.row.endDate < dateOfTheDay ? 'text-[#F56C6C]' : '' }`">{{ scope.row.endDate }}</div>
  105. </template>
  106. </el-table-column>
  107. <el-table-column prop="customName" label="客户名称" header-align="center" align="center" width="120">
  108. <template #default="scope">
  109. <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'customer', 'customId')">
  110. {{ scope.row.customName }}
  111. </el-link>
  112. </template>
  113. </el-table-column>
  114. <el-table-column prop="businessName" :label="`${businessLabel}名称`" header-align="center" align="center" width="200">
  115. <template #default="scope">
  116. <el-link :underline="false" type="primary"
  117. @click="goDetail(scope.row, 'business', 'businessOpportunityId')">
  118. {{ scope.row.businessName }}
  119. </el-link>
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="orderName" label="销售订单" header-align="center" align="center" width="200">
  123. <template #default="scope">
  124. <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'order', 'orderId')">
  125. {{ scope.row.orderName }}
  126. </el-link>
  127. </template>
  128. </el-table-column>
  129. <el-table-column prop="clueName" label="线索名称" header-align="center" align="center" width="200">
  130. <template #default="scope">
  131. <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'thread', 'clueId')">
  132. {{ scope.row.clueName }}
  133. </el-link>
  134. </template>
  135. </el-table-column>
  136. <el-table-column prop="contactsName" label="联系人名称" header-align="center" align="center" width="120">
  137. <template #default="scope">
  138. <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'contacts', 'contactsId')">
  139. {{ scope.row.contactsName }}
  140. </el-link>
  141. </template>
  142. </el-table-column>
  143. <el-table-column prop="contactsTel" label="联系人号码" header-align="center" align="center" width="140" />
  144. <el-table-column fixed="right" label="操作" header-align="center" align="center" width="160" v-permission="['tasksEdit']">
  145. <template #default="scope">
  146. <el-button link type="primary" @click.prevent="editRow(scope.row)">
  147. 编辑
  148. </el-button>
  149. <el-button link type="primary" v-if="scope.row.status == '2'"
  150. @click.prevent="restart(scope.row)">
  151. 重启
  152. </el-button>
  153. <el-button link type="primary" v-else @click.prevent="finishRow(scope.row)">
  154. 完成
  155. </el-button>
  156. <el-button link type="danger" v-permission="['tasksDelete']" @click.prevent="deleteRow(scope.row)">
  157. 删除
  158. </el-button>
  159. </template>
  160. </el-table-column>
  161. </el-table>
  162. </div>
  163. <div class="ml-auto">
  164. <el-pagination layout="total, prev, pager, next, sizes" :total="totalCount"
  165. :current-page="searchForm.pageIndex" @size-change="sizeChage" @current-change="currentChange" />
  166. </div>
  167. </template>
  168. <template v-if="layoutSingleChoice == KANBAN_VIEW">
  169. <div class="w-full p-4 h-full overflow-auto scroll-bar">
  170. <taskCalendar ref="taskCalendarRef" />
  171. </div>
  172. </template>
  173. </div>
  174. </div>
  175. <el-dialog v-model="restartPopUpWindowVisable" title="重启任务" width="500">
  176. <div class="mt-8">
  177. <el-form :model="restartFrom">
  178. <el-form-item label="重启时间" label-width="7em" prop="showStartDelayData">
  179. <el-date-picker
  180. v-model="restartFrom.timesList"
  181. type="datetimerange"
  182. start-placeholder="开始时间"
  183. end-placeholder="结束时间"
  184. value-format="YYYY-MM-DD HH:mm"
  185. format="YYYY-MM-DD HH:mm"
  186. style="width: 300px"
  187. />
  188. </el-form-item>
  189. <div class="text-[#ff4e4e] ml-8">重启任务的时间不能在今天之前</div>
  190. </el-form>
  191. </div>
  192. <template #footer>
  193. <div class="dialog-footer">
  194. <el-button @click="restartPopUpWindowVisable = false">取消</el-button>
  195. <el-button type="primary" @click="restartTaksTime">
  196. 确定
  197. </el-button>
  198. </div>
  199. </template>
  200. </el-dialog>
  201. <TaskModal :visible="taskModalVisible" :title="isEdit ? '编辑任务' : '新建任务'" :save-loading="taskLoading"
  202. :edit-form="taskForm" :show-log="isEdit" @close="closeTaskModal" @submit="submitForm" />
  203. <ImportModal :visible="importVisible" :save-loading="importLoading" @close="closeImportModal"
  204. @submit="importExcel" />
  205. <ExportModal :visible="exportVisible" :save-loading="exportLoading" @close="closeExportModal"
  206. @submit="exportExcel" />
  207. </div>
  208. </template>
  209. <script lang="ts" setup>
  210. import { computed, inject, onBeforeMount, onMounted, ref, } from 'vue';
  211. import { useRouter } from 'vue-router';
  212. import { useStore } from '@/store';
  213. import { MOD, PRIORITY, STATUS, defaultSearchForm, PAGE_LIST, ADD_TASK, DELETE_TASKS, UPDATE_TASK, UPDATE_TASK_STATUS, IMPORT_DATA, EXPORT_DATA, EXPORT_DATA_BY_TASK_ID, TABLE_VIEW, KANBAN_VIEW } from './api';
  214. import { ElTable, dayjs } from 'element-plus';
  215. import TaskModal from '@/components/TaskModal/index.vue';
  216. import ImportModal from './ImportModal.vue';
  217. import ExportModal from './ExportModal.vue';
  218. import taskCalendar from './taskCalendar.vue';
  219. import { post, uploadFile } from '@/utils/request';
  220. import { getFromValue, confirmAction, downloadFile, createTaskFromType } from '@/utils/tools';
  221. import { tableShowOverflowTooltip } from '@/utils/globalVariables'
  222. import { pushMap } from './type';
  223. const router = useRouter()
  224. const { getFunctionList } = useStore()
  225. const globalPopup = inject<GlobalPopup>('globalPopup')
  226. const pagePermission = ref<any[]>();
  227. const taskModalVisible = ref(false);
  228. const taskForm = ref<any>();
  229. const taskModalVisibleKey = ref<number>(1)
  230. const isEdit = ref(false);
  231. const len = computed(() => {
  232. return tableRef.value?.getSelectionRows().length
  233. })
  234. const taskLoading = ref<saveLoadingType>("1");
  235. const restartPopUpWindowVisable = ref(false);
  236. const restartFrom = ref<any>({});
  237. const dateOfTheDay = ref<any>(dayjs().format('YYYY-MM-DD'))
  238. const isExistBusiness = sessionStorage.getItem("isExistBusiness");
  239. const businessLabel = isExistBusiness === "1" ? "商机" : "项目";
  240. const layoutSingleChoice = ref(TABLE_VIEW) // 表格视图
  241. // const layoutSingleChoice = ref(KANBAN_VIEW) // 日历视图
  242. const taskCalendarRef = ref<InstanceType<typeof taskCalendar> | null>()
  243. function viewsSwitching() {
  244. }
  245. function closeTaskModal() {
  246. taskModalVisible.value = false;
  247. taskForm.value = null;
  248. }
  249. function submitForm(data: any, isClose: boolean) {
  250. const { executorId, startDate, endDate, repeatEndDate } = data;
  251. let params = {
  252. ...data,
  253. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD HH:mm'),
  254. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD HH:mm'),
  255. repeatEndDate: repeatEndDate && dayjs(repeatEndDate).format('YYYY-MM-DD')
  256. }
  257. if (executorId) {
  258. params = {
  259. ...params,
  260. executorId: executorId.join(','),
  261. taskLogs: []
  262. }
  263. }
  264. taskLoading.value = "2";
  265. let url = isEdit.value ? UPDATE_TASK : ADD_TASK
  266. let msg = isEdit.value ? "修改成功" : "新建成功"
  267. post(url, getFromValue(params)).then(() => {
  268. taskLoading.value = "3";
  269. taskModalVisible.value = isClose;
  270. globalPopup?.showSuccess(msg)
  271. search();
  272. }).catch(err => {
  273. taskLoading.value = "4"
  274. globalPopup?.showError(err.msg)
  275. })
  276. }
  277. const searchForm = ref<any>();
  278. const tableRef = ref<InstanceType<typeof ElTable>>();
  279. const loading = ref<boolean>(false);
  280. const totalCount = ref<number>(0);
  281. const tableData = ref<any[]>([])
  282. function newExportTasks() {
  283. btnLoading.value = true
  284. const { startDate, endDate } = searchForm.value;
  285. let params = {
  286. ...searchForm.value,
  287. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD 00:00:00'),
  288. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD 23:59:59')
  289. }
  290. post(EXPORT_DATA, {...getFromValue(params)}).then((res) => {
  291. globalPopup?.showSuccess("导出成功")
  292. downloadFile(res.data, "任务列表.xlsx");
  293. }).finally(() => {
  294. btnLoading.value = false
  295. })
  296. }
  297. function search() {
  298. loading.value = true;
  299. const { startDate, endDate } = searchForm.value;
  300. let params = {
  301. ...searchForm.value,
  302. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD 00:00'),
  303. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD 23:59')
  304. }
  305. post(PAGE_LIST, getFromValue(params)).then(({ data }) => {
  306. loading.value = false;
  307. const { total, record } = data;
  308. totalCount.value = total;
  309. tableData.value = record.map((item: any) => ({
  310. ...item,
  311. executorNames: item.taskExecutors?.join(',') ?? ''
  312. }));
  313. }).catch(err => {
  314. globalPopup?.showError(err.msg);
  315. loading.value = false;
  316. })
  317. }
  318. function reset() {
  319. searchForm.value = { ...defaultSearchForm };
  320. }
  321. function sizeChage(currentSize: number): void {
  322. searchForm.value = {
  323. ...searchForm.value,
  324. pageSize: currentSize,
  325. pageIndex: 1
  326. }
  327. search()
  328. }
  329. function currentChange(currentPage: number): void {
  330. searchForm.value = {
  331. ...searchForm.value,
  332. pageIndex: currentPage
  333. }
  334. search()
  335. }
  336. function createTasks() {
  337. taskModalVisible.value = true;
  338. // taskForm.value = null;
  339. taskForm.value = createTaskFromType(0);
  340. isEdit.value = false;
  341. }
  342. function deleteTasks() {
  343. confirmAction("确定删除所选内容吗?").then(() => {
  344. const taskIds = tableRef.value?.getSelectionRows()?.map((item: any) => item.id).join(",")
  345. post(DELETE_TASKS, {
  346. taskIds
  347. }).then(() => {
  348. search();
  349. globalPopup?.showSuccess("删除成功")
  350. }).catch(err => {
  351. globalPopup?.showError(err.msg)
  352. })
  353. });
  354. }
  355. const importVisible = ref(false);
  356. const importLoading = ref<saveLoadingType>("1");
  357. function openImportModal() {
  358. importVisible.value = true;
  359. }
  360. function closeImportModal() {
  361. importVisible.value = false;
  362. }
  363. function importExcel(data: any) {
  364. const formData = new FormData();
  365. formData.append("multipartFile", data);
  366. importLoading.value = "2";
  367. uploadFile(IMPORT_DATA, formData).then(_res => {
  368. globalPopup?.showSuccess("导入成功")
  369. importLoading.value = "3";
  370. if(layoutSingleChoice.value == TABLE_VIEW) {
  371. search();
  372. } else {
  373. taskCalendarRef.value && taskCalendarRef.value.getTaskCalendar()
  374. }
  375. }).catch(err => {
  376. globalPopup?.showError(err.msg)
  377. importLoading.value = "4";
  378. })
  379. }
  380. const exportVisible = ref(false);
  381. const exportLoading = ref<saveLoadingType>("1");
  382. const btnLoading = ref(false);
  383. function exportTasks() {
  384. const data: any[] = tableRef.value?.getSelectionRows()
  385. if (data.length === 0) {
  386. // TODO
  387. exportVisible.value = true;
  388. return
  389. }
  390. btnLoading.value = true;
  391. const taskIds = data.map((v: any) => v.id).join(",");
  392. post(EXPORT_DATA_BY_TASK_ID, {
  393. taskIds
  394. }).then(({ data }) => {
  395. downloadFile(data, "任务列表.xlsx");
  396. btnLoading.value = false;
  397. }).catch(err => {
  398. btnLoading.value = false;
  399. globalPopup?.showError(err.msg)
  400. })
  401. }
  402. function closeExportModal() {
  403. exportVisible.value = false;
  404. }
  405. function exportExcel(data: any) {
  406. exportLoading.value = "2";
  407. post(EXPORT_DATA, getFromValue(data)).then(({ data }) => {
  408. downloadFile(data, "任务列表.xlsx");
  409. exportLoading.value = "3";
  410. exportVisible.value = false;
  411. }).catch(err => {
  412. globalPopup?.showError(err.msg)
  413. })
  414. setTimeout(() => {
  415. }, 2000)
  416. }
  417. function editRow(row: any) {
  418. isEdit.value = true;
  419. taskModalVisible.value = true;
  420. let value = {
  421. ...row
  422. }
  423. if (value.executorId) {
  424. value.executorId = value.executorId.split(",")
  425. }
  426. delete value.taskLogs;
  427. taskForm.value = value;
  428. }
  429. function finishRow(item: any) {
  430. post(UPDATE_TASK_STATUS, {
  431. id: item.id,
  432. status: 2
  433. }).then(() => {
  434. search()
  435. globalPopup?.showSuccess("操作成功")
  436. }).catch(err => {
  437. globalPopup?.showError(err.msg)
  438. })
  439. }
  440. function restartTaksTime() {
  441. const { id, timesList } = restartFrom.value
  442. const date = dayjs().format("YYYY-MM-DD")
  443. if(timesList[0] < date) {
  444. globalPopup?.showInfo("开始时间不能在今天之前")
  445. return
  446. }
  447. post(UPDATE_TASK_STATUS, {
  448. id, status: 0,
  449. startDate: timesList[0],
  450. endDate: timesList[1]
  451. }).then(() => {
  452. search()
  453. globalPopup?.showSuccess("操作成功")
  454. restartPopUpWindowVisable.value = false
  455. }).catch(err => {
  456. globalPopup?.showError(err.msg)
  457. })
  458. }
  459. function restart(item: any) {
  460. const starDate = item.startDate ? item.startDate : dayjs().format("YYYY-MM-DD")
  461. const endDate = item.endDate ? item.endDate : dayjs().format("YYYY-MM-DD")
  462. restartFrom.value = {
  463. ...item,
  464. timesList: [starDate, endDate]
  465. }
  466. restartPopUpWindowVisable.value = true
  467. }
  468. function deleteRow(item: any) {
  469. confirmAction("确定删除吗?").then(() => {
  470. post(DELETE_TASKS, {
  471. taskIds: item.id
  472. }).then(() => {
  473. search();
  474. globalPopup?.showSuccess("删除成功")
  475. }).catch(err => {
  476. console.log("err", err);
  477. globalPopup?.showError(err.msg)
  478. })
  479. })
  480. }
  481. function goDetail(item: any, path: keyof pushMap, typeId: pushMap[keyof pushMap]) {
  482. router.push({
  483. path: `/${path}/detail`,
  484. query: {
  485. id: item[typeId]
  486. }
  487. })
  488. }
  489. onBeforeMount(() => {
  490. pagePermission.value = getFunctionList(MOD);
  491. searchForm.value = { ...defaultSearchForm };
  492. })
  493. onMounted(() => {
  494. search()
  495. })
  496. </script>
  497. <style lang="scss" scoped></style>