index.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  1. <template>
  2. <!-- 搜索工作栏 -->
  3. <ContentWrap>
  4. <el-form
  5. class="-mb-15px"
  6. :model="queryParams"
  7. ref="queryFormRef"
  8. :inline="true"
  9. label-width="68px"
  10. >
  11. <el-form-item label="月份" prop="month">
  12. <el-date-picker
  13. v-model="month"
  14. type="month"
  15. value-format="YYYY-MM"
  16. placeholder="日期选择"
  17. class="!w-240px"
  18. @change="handleMonthChange"
  19. />
  20. </el-form-item>
  21. <el-form-item label="部门" prop="relateDepartment">
  22. <DeptSelect
  23. v-model="queryParams.relateDepartment"
  24. placeholder="请选择部门"
  25. clearable
  26. class="!w-240px"
  27. />
  28. </el-form-item>
  29. <el-form-item>
  30. <el-button @click="handleQuery">
  31. <Icon icon="ep:search" class="mr-5px" /> 搜索
  32. </el-button>
  33. <el-button @click="resetQuery">
  34. <Icon icon="ep:refresh" class="mr-5px" /> 重置
  35. </el-button>
  36. <el-button type="danger" @click="handleSubmitPlan">
  37. <Icon icon="ep:upload" class="mr-5px" /> 提交计划
  38. </el-button>
  39. </el-form-item>
  40. </el-form>
  41. </ContentWrap>
  42. <!-- 右键菜单 -->
  43. <el-popover
  44. v-model:visible="contextMenuVisible"
  45. :virtual-ref="virtualRef"
  46. virtual-triggering
  47. trigger="contextmenu"
  48. placement="bottom"
  49. popper-class="context-menu-popover"
  50. >
  51. <div class="context-menu">
  52. <div class="context-menu-item" @click="handleEditPlan">
  53. <el-icon><Edit /></el-icon>
  54. <span>修改计划</span>
  55. </div>
  56. <div class="context-menu-item" @click="handleCancelPlan">
  57. <el-icon><Delete /></el-icon>
  58. <span>取消计划</span>
  59. </div>
  60. </div>
  61. </el-popover>
  62. <!-- 计划详情弹窗 -->
  63. <PlanDetail
  64. v-model:visible="showDetailDialog"
  65. :task-data="currentTask"
  66. @success="handleDetailSuccess"
  67. />
  68. <!-- 日历视图 -->
  69. <ContentWrap>
  70. <div class="calendar-wrap mb-20px">
  71. <div class="calendar-header-wrap">
  72. <div class="flex items-center">
  73. <el-radio-group v-model="queryParams.type" class="mr-4" @change="handleQuery">
  74. <el-radio-button value="100">仅看我排的计划</el-radio-button>
  75. <el-radio-button value="200">查看部门全部排期</el-radio-button>
  76. </el-radio-group>
  77. </div>
  78. </div>
  79. <el-calendar v-model="currentDate" class="mt-20px">
  80. <template #header="{ date }">
  81. <div class="calendar-header">
  82. <el-button-group>
  83. <el-button size="small" @click="handlePrevMonth">
  84. <el-icon><ArrowLeft /></el-icon>
  85. </el-button>
  86. <el-button size="small" @click="handleNextMonth">
  87. <el-icon><ArrowRight /></el-icon>
  88. </el-button>
  89. </el-button-group>
  90. <span class="current-month">{{ date }}</span>
  91. <div>
  92. <el-alert
  93. v-if="selectedTasks.length > 0"
  94. type="info"
  95. :closable="false"
  96. show-icon
  97. class="small-alert"
  98. >
  99. <span>已选择 <strong>{{ selectedTasks.length }}</strong> 个计划,将只提交已选择的计划</span>
  100. </el-alert>
  101. <el-alert
  102. v-else
  103. type="warning"
  104. :closable="false"
  105. show-icon
  106. class="small-alert"
  107. >
  108. <span>提示:按 <kbd>Ctrl</kbd> + 点击计划卡片可多选,提交时只提交已选择的计划</span>
  109. </el-alert>
  110. </div>
  111. </div>
  112. </template>
  113. <template #date-cell="{ data }">
  114. <div
  115. class="calendar-cell"
  116. >
  117. <div class="cell-content">
  118. <div class="date-text" @click.stop="handleDateClick(data.day)">
  119. {{ data.day }}
  120. </div>
  121. <div class="task-container" v-if="taskList[data.day]?.taskItems?.length > 0">
  122. <div
  123. v-for="task in taskList[data.day].taskItems"
  124. :key="task.taskId"
  125. class="task-item"
  126. :class="{
  127. 'selected': selectedTasks.some(t => t.taskId === task.taskId),
  128. 'dragging': isDragging && selectedTasks.some(t => t.taskId === task.taskId),
  129. [`check-type-${task.checkType}`]: true
  130. }"
  131. draggable="true"
  132. @dragstart="handleDragStart($event, task)"
  133. @dragend="handleDragEnd"
  134. @click="handleTaskSelect(task, $event)"
  135. >
  136. <div class="task-content">
  137. <div class="task-header">
  138. <div class="company-info flex-1">
  139. <div class="company-name">{{ task.unitName }}</div>
  140. <div class="company-address" v-if="task.equipDistrictName || task.equipStreetName">
  141. <span v-if="task.equipDistrictName">{{ task.equipDistrictName }}</span>
  142. <span v-if="task.equipDistrictName && task.equipStreetName">-</span>
  143. <span v-if="task.equipStreetName">{{ task.equipStreetName }}</span>
  144. </div>
  145. <div class="text-[12px] text-[#015293]">{{ getStatusText(task) }}</div>
  146. </div>
  147. <el-button
  148. class="delete-btn"
  149. type="danger"
  150. link
  151. @click.stop="handleDeleteTask(task)"
  152. >
  153. <el-icon><Delete /></el-icon>
  154. </el-button>
  155. </div>
  156. <div class="task-numbers">
  157. <span class="check-type regular-check" v-if="task.checkType === '100'">
  158. <img v-if="task.type === EQUIP_TYPES.PIPELINE" src="@/assets/svgs/pressure/gd.svg" class="type-icon" />
  159. <img v-if="task.type === EQUIP_TYPES.CONTAINER" src="@/assets/svgs/pressure/rq.svg" class="type-icon" />
  160. <img v-if="task.type === EQUIP_TYPES.BOILER" src="@/assets/svgs/pressure/wl.svg" class="type-icon" />
  161. {{ EQUIP_TYPE_NAMES[task.type as keyof typeof EQUIP_TYPES] }}内检设备: {{ task.equipCount }}
  162. <span v-if="task.source === 200" class="source-tag">前台约检</span>
  163. </span>
  164. <span class="check-type year-check" v-if="task.checkType === '200'">
  165. <img v-if="task.type === EQUIP_TYPES.PIPELINE" src="@/assets/svgs/pressure/gd.svg" class="type-icon" />
  166. <img v-if="task.type === EQUIP_TYPES.CONTAINER" src="@/assets/svgs/pressure/rq.svg" class="type-icon" />
  167. <img v-if="task.type === EQUIP_TYPES.BOILER" src="@/assets/svgs/pressure/wl.svg" class="type-icon" />
  168. {{ EQUIP_TYPE_NAMES[task.type as keyof typeof EQUIP_TYPES] }}外检设备: {{ task.equipCount }}
  169. <span v-if="task.source === 200" class="source-tag">前台约检</span>
  170. </span>
  171. <span class="check-type expired-check" v-if="task.checkType === '300'">
  172. <img v-if="task.type === EQUIP_TYPES.PIPELINE" src="@/assets/svgs/pressure/gd.svg" class="type-icon" />
  173. <img v-if="task.type === EQUIP_TYPES.CONTAINER" src="@/assets/svgs/pressure/rq.svg" class="type-icon" />
  174. <img v-if="task.type === EQUIP_TYPES.BOILER" src="@/assets/svgs/pressure/wl.svg" class="type-icon" />
  175. {{ EQUIP_TYPE_NAMES[task.type as keyof typeof EQUIP_TYPES] }}耐压检验: {{ task.equipCount }}
  176. <span v-if="task.source === 200" class="source-tag">前台约检</span>
  177. </span>
  178. </div>
  179. </div>
  180. </div>
  181. </div>
  182. </div>
  183. <div class="date-info" v-if="taskList[data.day]?.thisDistance || taskList[data.day]?.unitDistance">
  184. <template v-if="taskList[data.day].thisDistance">
  185. <span class="distance-info">
  186. <el-icon><Location /></el-icon>
  187. 离本院距离: {{ taskList[data.day].thisDistance }}km
  188. </span>
  189. </template>
  190. <template v-if="taskList[data.day].unitDistance">
  191. <span class="distance-info">
  192. <el-icon><Place /></el-icon>
  193. 单位相隔最大距离: {{ taskList[data.day].unitDistance }}km
  194. </span>
  195. </template>
  196. </div>
  197. </div>
  198. </template>
  199. </el-calendar>
  200. </div>
  201. </ContentWrap>
  202. </template>
  203. <script setup lang="ts">
  204. import { ref, onMounted, onUnmounted, watch } from 'vue'
  205. import { ArrowLeft, ArrowRight, Delete, Document, Location, Place, Edit } from '@element-plus/icons-vue'
  206. import DeptSelect from "@/views/pressure2/equipboilerscheduling/components/DeptSelect.vue";
  207. import { ElMessage, ElMessageBox } from 'element-plus'
  208. import { EquipBoilerSchedulingApi, BoilerTaskItem, BoilerPlanSchedulingCalendarVO } from '@/api/pressure2/equipboilerscheduling'
  209. import dayjs from 'dayjs'
  210. import PlanDetail from './detail.vue'
  211. // 设备类型常量
  212. const EQUIP_TYPES = {
  213. CONTAINER: '100', // 容器
  214. PIPELINE: '200', // 管道
  215. BOILER: '300' // 锅炉
  216. } as const
  217. // 设备类型显示名称
  218. const EQUIP_TYPE_NAMES: Record<string, string> = {
  219. [EQUIP_TYPES.PIPELINE]: '管道',
  220. [EQUIP_TYPES.CONTAINER]: '容器',
  221. [EQUIP_TYPES.BOILER]: '锅炉'
  222. } as const
  223. const getStatusText = (taskItem): string => {
  224. const { status } = taskItem
  225. if (status === null || status === undefined) return '未知'
  226. const statusMap: Record<number, string> = {
  227. 100: '已排期',
  228. 200: '待约检',
  229. 300: '已受理',
  230. 400: '检测中',
  231. 500: '部分办结',
  232. 600: '已办结'
  233. }
  234. return statusMap[Number(status)] || '未知'
  235. }
  236. // 查询参数
  237. const queryParams = ref({
  238. planDate: [] as string[],
  239. relateDepartment: undefined as string | undefined,
  240. type: '100',
  241. })
  242. const month = ref('')
  243. const queryFormRef = ref(null) // 搜索的表单
  244. const currentDate = ref(new Date())
  245. const taskList = ref<Record<string, BoilerPlanSchedulingCalendarVO>>({})
  246. // 添加选中计划列表
  247. const selectedTasks = ref<BoilerTaskItem[]>([])
  248. // 拖拽相关的状态
  249. const draggedTask = ref<BoilerTaskItem | null>(null)
  250. const isDragging = ref(false)
  251. // 日期备注相关
  252. const showNoteDialog = ref(false)
  253. const currentEditDate = ref('')
  254. //const currentNote = ref('')
  255. // 右键菜单相关
  256. const contextMenuVisible = ref(false)
  257. const virtualRef = ref()
  258. const currentTask = ref<BoilerTaskItem | {}>({})
  259. // 详情弹窗相关
  260. const showDetailDialog = ref(false)
  261. // 添加任务ID数组
  262. const taskIds = ref<string[]>([])
  263. // 添加 watch 监听器
  264. watch(currentDate, (newDate) => {
  265. // 获取新的月份格式
  266. const newMonth = dayjs(newDate).format('YYYY-MM')
  267. // 如果月份不同,则触发月份更新
  268. if (newMonth !== month.value) {
  269. handleMonthChange(newMonth)
  270. handleQuery()
  271. }
  272. })
  273. const handlePrevMonth = () => {
  274. const date = new Date(currentDate.value)
  275. date.setMonth(date.getMonth() - 1)
  276. currentDate.value = date
  277. handleMonthChange(dayjs(date).format('YYYY-MM'))
  278. handleQuery()
  279. }
  280. const handleNextMonth = () => {
  281. const date = new Date(currentDate.value)
  282. date.setMonth(date.getMonth() + 1)
  283. currentDate.value = date
  284. handleMonthChange(dayjs(date).format('YYYY-MM'))
  285. handleQuery()
  286. }
  287. /** 处理月份变化 */
  288. const handleMonthChange = (val: string | null) => {
  289. if (!val) {
  290. queryParams.value.planDate = []
  291. month.value = ''
  292. return
  293. }
  294. month.value = val
  295. // 用dayjs把月份转换为开始和结束日期
  296. const startDate = dayjs(val).startOf('month').format('YYYY-MM-DD')
  297. const endDate = dayjs(val).endOf('month').format('YYYY-MM-DD')
  298. queryParams.value.planDate = [startDate, endDate]
  299. }
  300. /** 搜索按钮操作 */
  301. const handleQuery = async () => {
  302. try {
  303. // 如果有指定日期参数,则切换日历到该日期所在月份
  304. if (queryParams.value.planDate.length > 0) {
  305. const targetDate = dayjs(queryParams.value.planDate[0])
  306. const targetMonth = targetDate.format('YYYY-MM')
  307. // 设置日历当前日期为目标日期
  308. currentDate.value = targetDate.toDate()
  309. // 更新月份和日期范围
  310. if (targetMonth !== month.value) {
  311. handleMonthChange(targetMonth)
  312. }
  313. }
  314. // 调用后端API获取数据
  315. const response = await EquipBoilerSchedulingApi.planSchedulingCalendar(queryParams.value)
  316. // 处理返回的数据,转换为以日期为键的对象
  317. const tasksMap: Record<string, BoilerPlanSchedulingCalendarVO> = {}
  318. // 清空之前的任务ID数组
  319. taskIds.value = []
  320. response.forEach((dayData) => {
  321. const dateKey = dayData.planDate; // 使用 planDate 作为 key
  322. // 初始化当天的 tasksMap 条目 (如果不存在)
  323. if (!tasksMap[dateKey]) {
  324. tasksMap[dateKey] = {
  325. planDate: dateKey,
  326. taskItems: [],
  327. unitDistance: dayData.unitDistance,
  328. thisDistance: dayData.thisDistance
  329. };
  330. }
  331. // 处理锅炉排期
  332. if (dayData.taskItems && dayData.taskItems.length > 0) {
  333. dayData.taskItems.forEach(task => {
  334. //锅炉类型
  335. task.type = EQUIP_TYPES.BOILER
  336. // 收集任务ID
  337. taskIds.value.push(task.taskId)
  338. tasksMap[dateKey].taskItems.push(task);
  339. })
  340. }
  341. })
  342. taskList.value = tasksMap
  343. //console.log(taskList.value)
  344. } catch (error) {
  345. //console.error('获取计划数据失败:', error)
  346. ElMessage.error('获取计划数据失败')
  347. taskList.value = {}
  348. taskIds.value = []
  349. }
  350. }
  351. /** 重置按钮操作 */
  352. const resetQuery = () => {
  353. queryFormRef.value && queryFormRef.value.resetFields()
  354. const startDate = dayjs().startOf('month').format('YYYY-MM-DD')
  355. const endDate = dayjs().endOf('month').format('YYYY-MM-DD')
  356. queryParams.value = {
  357. type: '100',
  358. planDate: [startDate, endDate]
  359. } as any;
  360. handleQuery()
  361. }
  362. /** 处理计划点击 */
  363. const handleTaskSelect = (task: BoilerTaskItem, event?: MouseEvent) => {
  364. // 检查是否按下了 Ctrl/Command 键
  365. const isMultiSelect = event?.ctrlKey || event?.metaKey
  366. if (isMultiSelect) {
  367. // Ctrl/Command 多选
  368. const index = selectedTasks.value.findIndex(t => t.taskId === task.taskId)
  369. if (index === -1) {
  370. selectedTasks.value.push(task)
  371. } else {
  372. selectedTasks.value.splice(index, 1)
  373. }
  374. } else {
  375. // 如果是单击,且不是拖拽开始,则打开详情
  376. // 使用setTimeout来区分点击和拖拽开始
  377. setTimeout(() => {
  378. if (!isDragging.value) {
  379. selectedTasks.value = []
  380. currentTask.value = task
  381. virtualRef.value = {
  382. getBoundingClientRect() {
  383. const element = event?.target as HTMLElement
  384. const rect = element.getBoundingClientRect()
  385. return {
  386. x: rect.left,
  387. y: rect.bottom,
  388. width: 0,
  389. height: 0,
  390. top: rect.bottom,
  391. right: rect.left,
  392. bottom: rect.bottom,
  393. left: rect.left,
  394. }
  395. }
  396. }
  397. contextMenuVisible.value = true
  398. }
  399. }, 200)
  400. }
  401. }
  402. /** 处理开始拖拽 */
  403. const handleDragStart = (event: DragEvent, task: BoilerTaskItem) => {
  404. if (event.dataTransfer) {
  405. // 隐藏右键菜单
  406. contextMenuVisible.value = false
  407. event.dataTransfer.effectAllowed = 'move'
  408. // 如果拖动的计划不在选中列表中,则将其设为唯一选中
  409. if (!selectedTasks.value.find(t => t.taskId === task.taskId)) {
  410. selectedTasks.value = [task]
  411. }
  412. draggedTask.value = task
  413. isDragging.value = true
  414. // 只有多选时才显示自定义拖动效果
  415. if (selectedTasks.value.length > 1) {
  416. // 创建一个半透明的拖动效果
  417. const dragImage = document.createElement('div')
  418. dragImage.textContent = `${selectedTasks.value.length} 个计划`
  419. dragImage.style.padding = '4px 8px'
  420. dragImage.style.background = 'rgba(64, 158, 255, 0.1)'
  421. dragImage.style.border = '2px solid #409EFF'
  422. dragImage.style.borderRadius = '4px'
  423. dragImage.style.position = 'fixed'
  424. dragImage.style.top = '-1000px'
  425. document.body.appendChild(dragImage)
  426. event.dataTransfer.setDragImage(dragImage, 0, 0)
  427. // 延迟移除临时元素
  428. setTimeout(() => document.body.removeChild(dragImage), 0)
  429. }
  430. }
  431. }
  432. /** 处理拖拽结束 */
  433. const handleDragEnd = (event: DragEvent) => {
  434. draggedTask.value = null
  435. isDragging.value = false
  436. // 清空选中状态
  437. selectedTasks.value = []
  438. }
  439. /** 处理放置 */
  440. const handleDrop = async (event: DragEvent, newDate: string) => {
  441. event.preventDefault()
  442. if (!draggedTask.value || selectedTasks.value.length === 0) return
  443. // 标准化日期格式
  444. const standardNewDate = dayjs(newDate.trim()).format('YYYY-MM-DD')
  445. // 检查是否所有选中计划的日期都相同
  446. if (selectedTasks.value.every(task => {
  447. const standardTaskDate = dayjs(task.planDate.trim()).format('YYYY-MM-DD')
  448. return standardTaskDate === standardNewDate
  449. })) {
  450. selectedTasks.value = [] // 如果拖动到相同日期,也清空选中状态
  451. return
  452. }
  453. try {
  454. // 调用后端 API 批量更新计划日期
  455. await EquipBoilerSchedulingApi.planSchedulingUpdateCalendar({
  456. ids: selectedTasks.value.map(task => task.taskId),
  457. date: standardNewDate
  458. })
  459. // 重新读取数据
  460. handleQuery()
  461. // 清空选中状态
  462. selectedTasks.value = []
  463. ElMessage.success('计划日期更新成功')
  464. } catch (error) {
  465. console.error('更新计划日期失败:', error)
  466. ElMessage.error('更新计划日期失败')
  467. }
  468. }
  469. // 生命周期钩子
  470. onMounted(() => {
  471. // 默认当月
  472. handleMonthChange(dayjs().format('YYYY-MM'))
  473. handleQuery()
  474. // 添加拖放事件监听
  475. const handleTableDragOver = (e: DragEvent) => {
  476. if (e.target instanceof HTMLElement) {
  477. const td = e.target.closest('td')
  478. if (td && td.querySelector('.date-text')) {
  479. e.preventDefault()
  480. }
  481. }
  482. }
  483. const handleTableDrop = (e: DragEvent) => {
  484. if (e.target instanceof HTMLElement) {
  485. const td = e.target.closest('td')
  486. if (td) {
  487. const dateText = td.querySelector('.date-text')?.textContent
  488. if (dateText) {
  489. handleDrop(e, dateText)
  490. }
  491. }
  492. }
  493. }
  494. // 给表格添加事件委托
  495. const calendar = document.querySelector('.el-calendar-table')
  496. if (calendar) {
  497. calendar.addEventListener('dragover', handleTableDragOver)
  498. calendar.addEventListener('drop', handleTableDrop)
  499. }
  500. // 清理事件监听
  501. onUnmounted(() => {
  502. const calendar = document.querySelector('.el-calendar-table')
  503. if (calendar) {
  504. calendar.removeEventListener('dragover', handleTableDragOver)
  505. calendar.removeEventListener('drop', handleTableDrop)
  506. }
  507. })
  508. document.addEventListener('click', handleClickOutside)
  509. })
  510. onUnmounted(() => {
  511. document.removeEventListener('click', handleClickOutside)
  512. })
  513. // 处理单个计划删除
  514. const handleDeleteTask = (task: BoilerTaskItem) => {
  515. handleDeleteSelected([task])
  516. }
  517. // 处理删除选中计划
  518. const handleDeleteSelected = async (tasksToDelete?: BoilerTaskItem[]) => {
  519. const tasks = tasksToDelete || selectedTasks.value
  520. if (tasks.length === 0) return
  521. try {
  522. await ElMessageBox.confirm(
  523. `确认删除选中的计划吗?`,
  524. '提示',
  525. {
  526. confirmButtonText: '确定',
  527. cancelButtonText: '取消',
  528. type: 'warning'
  529. }
  530. )
  531. // 调用后端 API 删除计划
  532. await EquipBoilerSchedulingApi.planSchedulingBatchDelete(tasks.map(t => t.taskId))
  533. // 更新本地数据
  534. const updatedTaskList = { ...taskList.value }
  535. Object.entries(updatedTaskList).forEach(([date, dayData]) => {
  536. const updatedTasks = dayData.taskItems.filter(
  537. task => !tasks.find(t => t.taskId === task.taskId)
  538. )
  539. if (updatedTasks.length === 0) {
  540. delete updatedTaskList[date]
  541. } else {
  542. updatedTaskList[date] = {
  543. ...dayData,
  544. taskItems: updatedTasks
  545. }
  546. }
  547. })
  548. taskList.value = updatedTaskList
  549. selectedTasks.value = selectedTasks.value.filter(
  550. task => !tasks.find(t => t.taskId === task.taskId)
  551. )
  552. taskIds.value = taskIds.value.filter(
  553. task => !tasks.find(t => t.taskId === task)
  554. )
  555. ElMessage.success('删除成功')
  556. } catch (error) {
  557. console.error('删除计划失败:', error)
  558. }
  559. }
  560. // 处理日期点击
  561. const handleDateClick = (date: string) => {
  562. currentEditDate.value = date
  563. //currentNote.value = taskList.value[date]?.remark || ''
  564. showNoteDialog.value = true
  565. }
  566. // 修改计划
  567. const handleEditPlan = () => {
  568. if (currentTask.value) {
  569. showDetailDialog.value = true
  570. contextMenuVisible.value = false
  571. }
  572. }
  573. // 取消计划
  574. const handleCancelPlan = () => {
  575. if (currentTask.value) {
  576. handleDeleteSelected([currentTask.value])
  577. contextMenuVisible.value = false
  578. }
  579. }
  580. // 点击其他地方关闭菜单
  581. const handleClickOutside = (event: MouseEvent) => {
  582. if (contextMenuVisible.value) {
  583. contextMenuVisible.value = false
  584. }
  585. }
  586. // 处理详情保存成功
  587. const handleDetailSuccess = () => {
  588. handleQuery() // 重新加载数据
  589. }
  590. // 提交计划
  591. const handleSubmitPlan = async () => {
  592. console.log('提交计划的任务IDs:', taskIds.value)
  593. // 如果有选中的计划,只提交选中的计划
  594. const taskIdsToSubmit = selectedTasks.value.length > 0
  595. ? selectedTasks.value.map(t => t.taskId)
  596. : taskIds.value
  597. if (taskIdsToSubmit.length === 0) {
  598. ElMessage.warning('没有可提交的计划')
  599. return
  600. }
  601. try {
  602. await ElMessageBox.confirm(
  603. `是否确认提交${selectedTasks.value.length > 0 ? '选中的' : '这'} ${taskIdsToSubmit.length} 个计划?`,
  604. '提示',
  605. {
  606. confirmButtonText: '确定',
  607. cancelButtonText: '取消',
  608. type: 'warning'
  609. }
  610. )
  611. await EquipBoilerSchedulingApi.planSchedulingConfirm({
  612. ids: taskIdsToSubmit
  613. })
  614. ElMessage.success('提交计划成功')
  615. // 提交成功后清空选中状态
  616. selectedTasks.value = []
  617. // 重新加载数据
  618. handleQuery()
  619. } catch (error) {
  620. if (error !== 'cancel') {
  621. console.error('提交计划失败:', error)
  622. ElMessage.error('提交计划失败')
  623. }
  624. }
  625. }
  626. </script>
  627. <style lang="scss" scoped>
  628. .calendar-wrap {
  629. background-color: var(--el-bg-color);
  630. border-radius: 4px;
  631. .calendar-header-wrap {
  632. padding: 16px;
  633. border-bottom: 1px solid var(--el-border-color-light);
  634. }
  635. }
  636. .calendar-cell {
  637. height: 100%;
  638. min-height: 100%;
  639. padding: 4px;
  640. display: flex;
  641. flex-direction: column;
  642. .cell-content {
  643. flex: 1;
  644. display: flex;
  645. flex-direction: column;
  646. overflow-y: auto;
  647. min-height: 0;
  648. }
  649. .date-text {
  650. font-size: 12px;
  651. margin-bottom: 4px;
  652. display: flex;
  653. align-items: center;
  654. gap: 4px;
  655. cursor: pointer;
  656. width: fit-content;
  657. padding: 2px 4px;
  658. border-radius: 2px;
  659. font-weight: 500;
  660. &:hover {
  661. background-color: var(--el-fill-color-light);
  662. }
  663. }
  664. .date-note {
  665. display: flex;
  666. align-items: center;
  667. font-size: 12px;
  668. color: var(--el-text-color-secondary);
  669. padding: 2px 4px;
  670. margin-bottom: 4px;
  671. background-color: var(--el-fill-color-lighter);
  672. border-radius: 2px;
  673. .note-icon {
  674. font-size: 12px;
  675. color: var(--el-text-color-secondary);
  676. margin-right: 4px;
  677. }
  678. span {
  679. flex: 1;
  680. // overflow: hidden;
  681. // text-overflow: ellipsis;
  682. // white-space: nowrap;
  683. }
  684. }
  685. .task-container {
  686. flex: 1;
  687. overflow-y: visible;
  688. .task-item {
  689. margin-bottom: 4px;
  690. border: 2px solid transparent;
  691. border-radius: 4px;
  692. background-color: var(--el-bg-color);
  693. transition: all 0.2s;
  694. cursor: move;
  695. box-sizing: border-box;
  696. position: relative;
  697. display: flex;
  698. // 添加类型指示色块
  699. &::before {
  700. content: '';
  701. width: 4px;
  702. height: 100%;
  703. position: absolute;
  704. left: 0;
  705. top: 0;
  706. border-radius: 4px 0 0 4px;
  707. }
  708. // 根据检验类型添加色块颜色和背景色
  709. &.check-type-100 {
  710. background-color: rgba(64, 158, 255, 0.05);
  711. &::before {
  712. background-color: var(--el-color-primary);
  713. }
  714. }
  715. &.check-type-200 {
  716. background-color: rgba(103, 194, 58, 0.05);
  717. &::before {
  718. background-color: var(--el-color-success);
  719. }
  720. }
  721. &.check-type-300 {
  722. background-color: rgba(245, 108, 108, 0.05);
  723. &::before {
  724. background-color: var(--el-color-danger);
  725. }
  726. }
  727. &.selected {
  728. &::after {
  729. content: '';
  730. position: absolute;
  731. inset: -2px;
  732. border: 2px solid var(--el-color-primary);
  733. border-radius: 4px;
  734. pointer-events: none;
  735. z-index: 1;
  736. }
  737. background-color: rgba(64, 158, 255, 0.05);
  738. }
  739. &.dragging {
  740. opacity: 0.5;
  741. transform: scale(0.95);
  742. cursor: move;
  743. }
  744. &:hover {
  745. .delete-btn {
  746. opacity: 1;
  747. }
  748. }
  749. .task-content {
  750. flex: 1;
  751. min-width: 0;
  752. }
  753. .task-header {
  754. display: flex;
  755. align-items: center;
  756. padding: 4px 8px;
  757. border-bottom: 1px solid var(--el-border-color-lighter);
  758. background-color: var(--el-fill-color-light);
  759. border-radius: 4px 4px 0 0;
  760. }
  761. .company-info {
  762. flex: 1;
  763. display: flex;
  764. flex-direction: column;
  765. gap: 2px;
  766. .company-name {
  767. font-size: 12px;
  768. font-weight: 500;
  769. color: var(--el-text-color-primary);
  770. }
  771. .company-address {
  772. font-size: 11px;
  773. color: var(--el-text-color-secondary);
  774. }
  775. }
  776. .delete-btn {
  777. opacity: 0;
  778. transition: opacity 0.2s;
  779. padding: 2px;
  780. }
  781. .task-numbers {
  782. padding: 4px;
  783. display: flex;
  784. flex-direction: column;
  785. gap: 2px;
  786. .check-type {
  787. display: inline-flex;
  788. align-items: center;
  789. font-size: 11px;
  790. padding: 2px 8px;
  791. border-radius: 12px;
  792. color: var(--el-text-color-regular);
  793. width: fit-content;
  794. .type-icon {
  795. width: 16px;
  796. height: 16px;
  797. margin-right: 4px;
  798. vertical-align: middle;
  799. }
  800. .source-tag {
  801. margin-left: 4px;
  802. padding: 0 4px;
  803. background-color: var(--el-color-warning-light-9);
  804. color: var(--el-color-warning);
  805. border-radius: 2px;
  806. font-size: 10px;
  807. }
  808. &.regular-check {
  809. color: var(--el-color-primary);
  810. .type-icon {
  811. filter: invert(30%) sepia(95%) saturate(1139%) hue-rotate(187deg) brightness(93%) contrast(104%);
  812. }
  813. }
  814. &.year-check {
  815. color: #2da44e;
  816. .type-icon {
  817. filter: invert(45%) sepia(64%) saturate(501%) hue-rotate(89deg) brightness(93%) contrast(89%);
  818. }
  819. }
  820. &.expired-check {
  821. color: var(--el-color-danger);
  822. .type-icon {
  823. filter: invert(54%) sepia(71%) saturate(4037%) hue-rotate(332deg) brightness(98%) contrast(93%);
  824. }
  825. }
  826. }
  827. }
  828. }
  829. }
  830. .date-info {
  831. margin-top: auto;
  832. flex-shrink: 0;
  833. display: flex;
  834. flex-direction: column;
  835. gap: 2px;
  836. font-size: 11px;
  837. padding-top: 4px;
  838. .distance-info {
  839. display: flex;
  840. align-items: center;
  841. gap: 4px;
  842. color: var(--el-text-color-secondary);
  843. padding: 1px 4px;
  844. .el-icon {
  845. font-size: 12px;
  846. }
  847. }
  848. }
  849. }
  850. :deep(.el-calendar) {
  851. background: none;
  852. .el-calendar__header {
  853. padding: 0;
  854. border-bottom: 1px solid var(--el-border-color-lighter);
  855. }
  856. .el-calendar__body {
  857. padding: 0;
  858. }
  859. .el-calendar-day {
  860. height: auto !important;
  861. &:hover {
  862. cursor: default;
  863. background-color: transparent;
  864. }
  865. }
  866. .is-selected {
  867. background-color: transparent !important;
  868. }
  869. .el-calendar-table {
  870. width: 100%;
  871. table-layout: fixed;
  872. border-collapse: collapse;
  873. border-right: 1px solid var(--el-border-color-lighter);
  874. border-bottom: 1px solid var(--el-border-color-lighter);
  875. td {
  876. height: 100px !important;
  877. padding: 0 !important;
  878. vertical-align: top;
  879. }
  880. .el-calendar-day {
  881. height: 100% !important;
  882. padding: 0;
  883. }
  884. }
  885. .calendar-header {
  886. display: flex;
  887. align-items: center;
  888. gap: 20px;
  889. padding: 0 20px 20px;
  890. .current-month {
  891. font-size: 16px;
  892. font-weight: bold;
  893. }
  894. }
  895. }
  896. .task-numbers {
  897. display: flex;
  898. flex-wrap: wrap;
  899. gap: 4px;
  900. font-size: 12px;
  901. color: var(--el-text-color-secondary);
  902. > div {
  903. background-color: var(--el-fill-color-lighter);
  904. padding: 1px 4px;
  905. border-radius: 2px;
  906. }
  907. }
  908. .type-icon {
  909. width: 16px;
  910. height: 16px;
  911. margin-right: 4px;
  912. vertical-align: middle;
  913. }
  914. .context-menu {
  915. min-width: 120px;
  916. .context-menu-item {
  917. display: flex;
  918. align-items: center;
  919. gap: 8px;
  920. padding: 8px 16px;
  921. cursor: pointer;
  922. transition: background-color 0.2s;
  923. &:hover {
  924. background-color: var(--el-fill-color-light);
  925. }
  926. .el-icon {
  927. font-size: 16px;
  928. }
  929. }
  930. }
  931. :deep(.context-menu-popover) {
  932. padding: 4px 0;
  933. min-width: 120px;
  934. }
  935. .small-alert {
  936. padding: 2px;
  937. :deep(.el-alert__title) {
  938. font-size: 10px;
  939. }
  940. :deep(.el-alert__icon) {
  941. font-size: 18px;
  942. width: 18px;
  943. height: 18px;
  944. }
  945. }
  946. </style>