index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. :disabled-minutes="disableMinute"
  188. />
  189. </el-form-item>
  190. <div class="text-[#ff4e4e] ml-8">重启任务的时间不能在今天之前</div>
  191. </el-form>
  192. </div>
  193. <template #footer>
  194. <div class="dialog-footer">
  195. <el-button @click="restartPopUpWindowVisable = false">取消</el-button>
  196. <el-button type="primary" @click="restartTaksTime">
  197. 确定
  198. </el-button>
  199. </div>
  200. </template>
  201. </el-dialog>
  202. <TaskModal :visible="taskModalVisible" :title="isEdit ? '编辑任务' : '新建任务'" :save-loading="taskLoading"
  203. :edit-form="taskForm" :show-log="isEdit" @close="closeTaskModal" @submit="submitForm" />
  204. <ImportModal :visible="importVisible" :save-loading="importLoading" @close="closeImportModal"
  205. @submit="importExcel" />
  206. <ExportModal :visible="exportVisible" :save-loading="exportLoading" @close="closeExportModal"
  207. @submit="exportExcel" />
  208. </div>
  209. </template>
  210. <script lang="ts" setup>
  211. import { computed, inject, onBeforeMount, onMounted, ref, } from 'vue';
  212. import { useRouter } from 'vue-router';
  213. import { useStore } from '@/store';
  214. 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';
  215. import { ElTable, dayjs } from 'element-plus';
  216. import TaskModal from '@/components/TaskModal/index.vue';
  217. import ImportModal from './ImportModal.vue';
  218. import ExportModal from './ExportModal.vue';
  219. import taskCalendar from './taskCalendar.vue';
  220. import { post, uploadFile } from '@/utils/request';
  221. import { getFromValue, confirmAction, downloadFile, createTaskFromType } from '@/utils/tools';
  222. import { tableShowOverflowTooltip } from '@/utils/globalVariables'
  223. import { pushMap } from './type';
  224. const router = useRouter()
  225. const { getFunctionList } = useStore()
  226. const globalPopup = inject<GlobalPopup>('globalPopup')
  227. const pagePermission = ref<any[]>();
  228. const taskModalVisible = ref(false);
  229. const taskForm = ref<any>();
  230. const taskModalVisibleKey = ref<number>(1)
  231. const isEdit = ref(false);
  232. const len = computed(() => {
  233. return tableRef.value?.getSelectionRows().length
  234. })
  235. const taskLoading = ref<saveLoadingType>("1");
  236. const restartPopUpWindowVisable = ref(false);
  237. const restartFrom = ref<any>({});
  238. const dateOfTheDay = ref<any>(dayjs().format('YYYY-MM-DD'))
  239. const isExistBusiness = sessionStorage.getItem("isExistBusiness");
  240. const businessLabel = isExistBusiness === "1" ? "商机" : "项目";
  241. const layoutSingleChoice = ref(TABLE_VIEW) // 表格视图
  242. // const layoutSingleChoice = ref(KANBAN_VIEW) // 日历视图
  243. const taskCalendarRef = ref<InstanceType<typeof taskCalendar> | null>()
  244. function viewsSwitching() {
  245. }
  246. function disableMinute() {
  247. // 只允许选择 00 和 30 分钟
  248. const allowed = [0, 30];
  249. const disabled = [];
  250. for (let i = 0; i < 60; i++) {
  251. if (!allowed.includes(i)) {
  252. disabled.push(i);
  253. }
  254. }
  255. // return disabled;
  256. return disabled;
  257. }
  258. function closeTaskModal() {
  259. taskModalVisible.value = false;
  260. taskForm.value = null;
  261. }
  262. function submitForm(data: any, isClose: boolean) {
  263. const { executorId, startDate, endDate, repeatEndDate } = data;
  264. let params = {
  265. ...data,
  266. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD HH:mm'),
  267. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD HH:mm'),
  268. repeatEndDate: repeatEndDate && dayjs(repeatEndDate).format('YYYY-MM-DD')
  269. }
  270. if (executorId) {
  271. params = {
  272. ...params,
  273. executorId: executorId.join(','),
  274. taskLogs: []
  275. }
  276. }
  277. delete params.taskLogs
  278. taskLoading.value = "2";
  279. let url = isEdit.value ? UPDATE_TASK : ADD_TASK
  280. let msg = isEdit.value ? "修改成功" : "新建成功"
  281. post(url, getFromValue(params)).then(() => {
  282. taskLoading.value = "3";
  283. taskModalVisible.value = isClose;
  284. globalPopup?.showSuccess(msg)
  285. search();
  286. }).catch(err => {
  287. taskLoading.value = "4"
  288. globalPopup?.showError(err.msg)
  289. })
  290. }
  291. const searchForm = ref<any>();
  292. const tableRef = ref<InstanceType<typeof ElTable>>();
  293. const loading = ref<boolean>(false);
  294. const totalCount = ref<number>(0);
  295. const tableData = ref<any[]>([])
  296. function newExportTasks() {
  297. btnLoading.value = true
  298. const { startDate, endDate } = searchForm.value;
  299. let params = {
  300. ...searchForm.value,
  301. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD 00:00:00'),
  302. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD 23:59:59')
  303. }
  304. post(EXPORT_DATA, {...getFromValue(params)}).then((res) => {
  305. globalPopup?.showSuccess("导出成功")
  306. downloadFile(res.data, "任务列表.xlsx");
  307. }).finally(() => {
  308. btnLoading.value = false
  309. })
  310. }
  311. function search() {
  312. loading.value = true;
  313. const { startDate, endDate } = searchForm.value;
  314. let params = {
  315. ...searchForm.value,
  316. startDate: startDate && dayjs(startDate).format('YYYY-MM-DD 00:00'),
  317. endDate: endDate && dayjs(endDate).format('YYYY-MM-DD 23:59')
  318. }
  319. post(PAGE_LIST, getFromValue(params)).then(({ data }) => {
  320. loading.value = false;
  321. const { total, record } = data;
  322. totalCount.value = total;
  323. tableData.value = record.map((item: any) => ({
  324. ...item,
  325. executorNames: item.taskExecutors?.join(',') ?? ''
  326. }));
  327. }).catch(err => {
  328. globalPopup?.showError(err.msg);
  329. loading.value = false;
  330. })
  331. }
  332. function reset() {
  333. searchForm.value = { ...defaultSearchForm };
  334. }
  335. function sizeChage(currentSize: number): void {
  336. searchForm.value = {
  337. ...searchForm.value,
  338. pageSize: currentSize,
  339. pageIndex: 1
  340. }
  341. search()
  342. }
  343. function currentChange(currentPage: number): void {
  344. searchForm.value = {
  345. ...searchForm.value,
  346. pageIndex: currentPage
  347. }
  348. search()
  349. }
  350. function createTasks() {
  351. taskModalVisible.value = true;
  352. // taskForm.value = null;
  353. taskForm.value = createTaskFromType(0);
  354. isEdit.value = false;
  355. }
  356. function deleteTasks() {
  357. confirmAction("确定删除所选内容吗?").then(() => {
  358. const taskIds = tableRef.value?.getSelectionRows()?.map((item: any) => item.id).join(",")
  359. post(DELETE_TASKS, {
  360. taskIds
  361. }).then(() => {
  362. search();
  363. globalPopup?.showSuccess("删除成功")
  364. }).catch(err => {
  365. globalPopup?.showError(err.msg)
  366. })
  367. });
  368. }
  369. const importVisible = ref(false);
  370. const importLoading = ref<saveLoadingType>("1");
  371. function openImportModal() {
  372. importVisible.value = true;
  373. }
  374. function closeImportModal() {
  375. importVisible.value = false;
  376. }
  377. function importExcel(data: any) {
  378. const formData = new FormData();
  379. formData.append("multipartFile", data);
  380. importLoading.value = "2";
  381. uploadFile(IMPORT_DATA, formData).then(_res => {
  382. globalPopup?.showSuccess("导入成功")
  383. importLoading.value = "3";
  384. if(layoutSingleChoice.value == TABLE_VIEW) {
  385. search();
  386. } else {
  387. taskCalendarRef.value && taskCalendarRef.value.getTaskCalendar()
  388. }
  389. }).catch(err => {
  390. globalPopup?.showError(err.msg)
  391. importLoading.value = "4";
  392. })
  393. }
  394. const exportVisible = ref(false);
  395. const exportLoading = ref<saveLoadingType>("1");
  396. const btnLoading = ref(false);
  397. function exportTasks() {
  398. const data: any[] = tableRef.value?.getSelectionRows()
  399. if (data.length === 0) {
  400. // TODO
  401. exportVisible.value = true;
  402. return
  403. }
  404. btnLoading.value = true;
  405. const taskIds = data.map((v: any) => v.id).join(",");
  406. post(EXPORT_DATA_BY_TASK_ID, {
  407. taskIds
  408. }).then(({ data }) => {
  409. downloadFile(data, "任务列表.xlsx");
  410. btnLoading.value = false;
  411. }).catch(err => {
  412. btnLoading.value = false;
  413. globalPopup?.showError(err.msg)
  414. })
  415. }
  416. function closeExportModal() {
  417. exportVisible.value = false;
  418. }
  419. function exportExcel(data: any) {
  420. exportLoading.value = "2";
  421. post(EXPORT_DATA, getFromValue(data)).then(({ data }) => {
  422. downloadFile(data, "任务列表.xlsx");
  423. exportLoading.value = "3";
  424. exportVisible.value = false;
  425. }).catch(err => {
  426. globalPopup?.showError(err.msg)
  427. })
  428. setTimeout(() => {
  429. }, 2000)
  430. }
  431. function editRow(row: any) {
  432. isEdit.value = true;
  433. taskModalVisible.value = true;
  434. let value = {
  435. ...row
  436. }
  437. if (value.executorId) {
  438. value.executorId = value.executorId.split(",")
  439. }
  440. taskForm.value = value;
  441. }
  442. function finishRow(item: any) {
  443. post(UPDATE_TASK_STATUS, {
  444. id: item.id,
  445. status: 2
  446. }).then(() => {
  447. search()
  448. globalPopup?.showSuccess("操作成功")
  449. }).catch(err => {
  450. globalPopup?.showError(err.msg)
  451. })
  452. }
  453. function restartTaksTime() {
  454. const { id, timesList } = restartFrom.value
  455. const date = dayjs().format("YYYY-MM-DD")
  456. if(timesList[0] < date) {
  457. globalPopup?.showInfo("开始时间不能在今天之前")
  458. return
  459. }
  460. post(UPDATE_TASK_STATUS, {
  461. id, status: 0,
  462. startDate: timesList[0],
  463. endDate: timesList[1]
  464. }).then(() => {
  465. search()
  466. globalPopup?.showSuccess("操作成功")
  467. restartPopUpWindowVisable.value = false
  468. }).catch(err => {
  469. globalPopup?.showError(err.msg)
  470. })
  471. }
  472. function restart(item: any) {
  473. const starDate = item.startDate ? item.startDate : dayjs().format("YYYY-MM-DD")
  474. const endDate = item.endDate ? item.endDate : dayjs().format("YYYY-MM-DD")
  475. restartFrom.value = {
  476. ...item,
  477. timesList: [starDate, endDate]
  478. }
  479. restartPopUpWindowVisable.value = true
  480. }
  481. function deleteRow(item: any) {
  482. confirmAction("确定删除吗?").then(() => {
  483. post(DELETE_TASKS, {
  484. taskIds: item.id
  485. }).then(() => {
  486. search();
  487. globalPopup?.showSuccess("删除成功")
  488. }).catch(err => {
  489. console.log("err", err);
  490. globalPopup?.showError(err.msg)
  491. })
  492. })
  493. }
  494. function goDetail(item: any, path: keyof pushMap, typeId: pushMap[keyof pushMap]) {
  495. router.push({
  496. path: `/${path}/detail`,
  497. query: {
  498. id: item[typeId]
  499. }
  500. })
  501. }
  502. onBeforeMount(() => {
  503. pagePermission.value = getFunctionList(MOD);
  504. searchForm.value = { ...defaultSearchForm };
  505. })
  506. onMounted(() => {
  507. search()
  508. })
  509. </script>
  510. <style lang="scss" scoped></style>