ShiftSchedule.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. <template>
  2. <div class="shift-schedule-container">
  3. <ContentWrap>
  4. <el-table v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange"
  5. @sort-change="handleSortChange" class="shift-schedule-table" ref="tableRef" :row-class-name="getRowClassName">
  6. <el-table-column type="selection" width="55" fixed="left" :selectable="isSelectable" />
  7. <el-table-column label="日期" prop="date" width="170" align="center" fixed="left" sortable>
  8. <template #default="{ row }">
  9. <div class="date-cell">
  10. <el-date-picker v-model="row.date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期"
  11. :clearable="false" class="!w-120px" :disabled="!canEdit(row)" @change="handleDateChange(row)" />
  12. </div>
  13. </template>
  14. </el-table-column>
  15. <el-table-column label="星期" prop="week" width="80" align="center" />
  16. <!-- 检验员 -->
  17. <el-table-column label="检验员" prop="checkers" width="300" align="center">
  18. <template #default="{ row }">
  19. <div>
  20. <!-- 不可编辑状态:只显示检验员名称 -->
  21. <div v-if="!canEdit(row)" class="checker-display">
  22. {{ getCheckerNames(row) || '-' }}
  23. </div>
  24. <!-- 可编辑状态:使用Popover -->
  25. <el-popover v-else placement="bottom-start" :width="600" trigger="click"
  26. popper-class="checker-select-popover" @show="() => handlePopoverShow(row)">
  27. <template #reference>
  28. <div class="checker-trigger" :class="{ 'has-checkers': getCheckerNames(row) }" @click.stop>
  29. <span class="checker-text">{{ getCheckerNames(row) || '请选择检验员' }}</span>
  30. <Icon icon="ep:edit" class="edit-icon ml-5px" />
  31. </div>
  32. </template>
  33. <!-- Popover内容 -->
  34. <div class="popover-content" @click.stop>
  35. <CheckerSelect :ref="(el) => setCheckerSelectRef(el, row.id)"
  36. :dept-id="userStore.getUser.deptId?.toString() || props.relateDepartment" v-model="inspectors"
  37. :multiple="true" :has-data="true"
  38. @change="(checkers) => handleCheckerSelectChange(checkers, row)" />
  39. </div>
  40. </el-popover>
  41. </div>
  42. </template>
  43. </el-table-column>
  44. <el-table-column label="检验类型" prop="checkType" width="150" align="center">
  45. <template #default="{ row }">
  46. {{ row.checkType === '100' ? '定期检验' : '年度检查' }}
  47. </template>
  48. </el-table-column>
  49. <el-table-column label="使用单位" prop="unitName" width="150" align="center" show-overflow-tooltip sortable />
  50. <el-table-column label="管道使用地址" prop="pipeAddress" width="200" align="center" show-overflow-tooltip />
  51. <el-table-column label="联系人" prop="contact" width="100" align="center" />
  52. <el-table-column label="电话" prop="contactPhone" width="130" align="center" />
  53. <!-- 管道介质 -->
  54. <el-table-column label="管道介质" prop="pipeMedium" width="150" align="center">
  55. <template #default="{ row }">
  56. <div>
  57. <el-tag v-for="m in (row.pipeMedium || [])" :key="m" size="small" class="mr-5px mb-5px" type="primary">
  58. {{ m }}
  59. </el-tag>
  60. <span v-if="!row.pipeMedium || row.pipeMedium.length === 0">-</span>
  61. </div>
  62. </template>
  63. </el-table-column>
  64. <!-- 管线长度 -->
  65. <el-table-column label="管线长度 (m)" prop="pipeLengthTotal" width="120" align="center">
  66. <template #default="{ row }">
  67. <span class="font-mono font-semibold text-blue-800">
  68. {{ formatPipeLength(row.pipeLengthTotal) }}
  69. </span>
  70. </template>
  71. </el-table-column>
  72. <el-table-column label="区域街道" prop="equipStreetName" width="120" align="center">
  73. <template #default="{ row }">
  74. <span>
  75. {{ row.equipDistrictName + ' ' + row.equipStreetName || '-' }}
  76. </span>
  77. </template>
  78. </el-table-column>
  79. <el-table-column label="上次报告编号" prop="lastLegalPeriodicalInspectionReportNo" width="150" align="center" />
  80. <el-table-column label="监督报告编号" prop="lastMaintenanceReportNo" width="150" align="center" />
  81. <!-- 状态 -->
  82. <el-table-column label="状态" prop="status" width="100" align="center" fixed="right">
  83. <template #header>
  84. <el-tooltip placement="top">
  85. <template #content>
  86. <div class="status-legend">
  87. <div class="legend-item">
  88. <span class="status-dot" style="background-color: #409eff;"></span>
  89. <span>已排期</span>
  90. </div>
  91. <div class="legend-item">
  92. <span class="status-dot" style="background-color: #e6a23c;"></span>
  93. <span>待约检</span>
  94. </div>
  95. <div class="legend-item">
  96. <span class="status-dot" style="background-color: #67c23a;"></span>
  97. <span>已受理</span>
  98. </div>
  99. <div class="legend-item">
  100. <span class="status-dot" style="background-color: #f56c6c;"></span>
  101. <span>检测中</span>
  102. </div>
  103. </div>
  104. </template>
  105. <div class="flex items-center justify-center gap-6px cursor-help">
  106. <span>状态</span>
  107. <Icon icon="ep:question-filled" class="text-gray-400" :size="14" />
  108. </div>
  109. </el-tooltip>
  110. </template>
  111. <template #default="{ row }">
  112. <div class="inline-flex items-center gap-6px">
  113. <span class="inline-block w-10px h-10px rounded-full" :class="getStatusClass(row.status)"></span>
  114. <span>{{ getStatusText(row.status) }}</span>
  115. </div>
  116. </template>
  117. </el-table-column>
  118. <!-- 操作 -->
  119. <el-table-column label="操作" width="120" align="center" fixed="right">
  120. <template #default="{ row }">
  121. <el-tag v-if="row.isCopied && !row.submitted" type="warning" size="small">已复制</el-tag>
  122. <el-button v-else size="small" :type="row.modified ? 'success' : 'primary'"
  123. :disabled="row.status === 100 || !canEdit(row)" @click="handleCopy(row)">
  124. 复制
  125. </el-button>
  126. </template>
  127. </el-table-column>
  128. </el-table>
  129. </ContentWrap>
  130. </div>
  131. </template>
  132. <script setup lang="ts">
  133. import { ref, onMounted, defineProps, nextTick, computed } from 'vue'
  134. import { ElMessage } from 'element-plus'
  135. import { Icon } from '@/components/Icon'
  136. import { EquipPipeSchedulingApi } from "@/api/pressure2/pipescheduling";
  137. import dayjs from 'dayjs'
  138. import 'dayjs/locale/zh-cn'
  139. import CheckerSelect from '@/views/pressure2/components/CheckerSelect/index.vue'
  140. import type { CheckerItem } from '@/views/pressure2/components/CheckerSelect/index.vue'
  141. import { useUserStoreWithOut } from '@/store/modules/user'
  142. const props = defineProps<{
  143. relateDepartment: string
  144. startDate?: string
  145. endDate?: string
  146. checkType?: string
  147. unitName?: string
  148. status?: string
  149. }>()
  150. // ==================== 数据定义 ====================
  151. interface ScheduleRow {
  152. id?: string
  153. index?: number // 行索引
  154. sourceIndex?: number // 源行索引
  155. date: string
  156. week: string
  157. checkers?: string[] // 检验员ID数组(用于提交)
  158. teamList?: any[] // 团队列表(后端返回的完整数据)
  159. checkType?: string
  160. unitName?: string
  161. pipeAddress?: string // 管道使用地址
  162. contact?: string
  163. contactPhone?: string
  164. pipeMedium?: string[] // 管道介质
  165. pipeLengthTotal?: string // 管道长度
  166. equipDistrictName?: string // 设备所在区域名称
  167. equipStreetName?: string // 设备所在街道名称
  168. lastLegalPeriodicalInspectionReportNo?: string // 上次定检报告编号
  169. confirmOrderId?: string // 确认订单ID
  170. modified?: boolean // 是否被修改
  171. originalData?: any // 原始数据,用于比较
  172. lastYearReportNo?: string // 上次年检报告编号
  173. lastMaintenanceReportNo?: string // 监督检验报告编号
  174. status?: number // 状态, 100 已排期 200 待约检 300 已受理 400 检测中
  175. isCopied?: boolean // 是否为复制的行
  176. submitted?: boolean // 是否已提交
  177. }
  178. const loading = ref(false)
  179. const exportLoading = ref(false)
  180. const tableData = ref<ScheduleRow[]>([])
  181. const selectedRows = ref<ScheduleRow[]>([])
  182. const tableRef = ref()
  183. // 处理选择变化
  184. const handleSelectionChange = (val: ScheduleRow[]) => {
  185. selectedRows.value = val
  186. }
  187. // 只有已排期状态才能勾选
  188. const isSelectable = (row: ScheduleRow): boolean => {
  189. return row.status === 100
  190. }
  191. // 用户store
  192. const userStore = useUserStoreWithOut()
  193. // Popover相关状态
  194. const currentEditingRow = ref<ScheduleRow | null>(null)
  195. const checkerSelectRefs = ref<Record<string, any>>({})
  196. // 查询参数 - 从 props 计算
  197. const queryParams = computed(() => ({
  198. startDate: props.startDate || '',
  199. endDate: props.endDate || '',
  200. checkType: props.checkType || '',
  201. unitName: props.unitName || '',
  202. status: props.status || '',
  203. orderBy: sortOrderBy.value,
  204. orderType: sortOrderType.value,
  205. relateDepartment: props.relateDepartment || ''
  206. }))
  207. // 排序参数
  208. const sortOrderBy = ref('')
  209. const sortOrderType = ref('')
  210. // ==================== 方法 ====================
  211. // 获取检验员昵称列表(用于显示)
  212. const getCheckerNames = (row: ScheduleRow): string => {
  213. if (row.teamList && row.teamList.length > 0) {
  214. // 从 teamList 中获取所有检验员名称,用顿号分隔
  215. const allMembers: string[] = [];
  216. row.teamList.forEach(team => {
  217. // 添加组长
  218. if (team.leaders && team.leaders.length > 0) {
  219. team.leaders.forEach(leader => {
  220. if (leader.nickname) {
  221. allMembers.push(leader.nickname);
  222. } else if (leader.id) {
  223. allMembers.push(leader.id);
  224. }
  225. });
  226. }
  227. // 添加成员
  228. if (team.members && team.members.length > 0) {
  229. team.members.forEach(member => {
  230. if (member.nickname) {
  231. allMembers.push(member.nickname);
  232. } else if (member.id) {
  233. allMembers.push(member.id);
  234. }
  235. });
  236. }
  237. });
  238. return allMembers.join('、') || '';
  239. } else if (row.checkers && row.checkers.length > 0) {
  240. // 如果只有 checkers ID 列表,直接显示 ID
  241. return row.checkers.join('、');
  242. }
  243. return ''
  244. }
  245. // 设置 CheckerSelect 引用
  246. const setCheckerSelectRef = (el: any, rowId: string) => {
  247. if (el) {
  248. checkerSelectRefs.value[rowId] = el
  249. }
  250. }
  251. const inspectors = ref()
  252. // 处理 Popover 显示
  253. const handlePopoverShow = async (row: ScheduleRow) => {
  254. if (!canEdit(row)) {
  255. return
  256. }
  257. inspectors.value = row.inspectors
  258. currentEditingRow.value = row
  259. const checkerSelectRef = checkerSelectRefs.value[row.id]
  260. // 加载检验员列表
  261. const deptId = userStore.getUser.deptId?.toString() || props.relateDepartment
  262. await checkerSelectRef.getCheckerList(deptId)
  263. console.log(row.inspectors)
  264. }
  265. // 关闭Popover
  266. const closePopover = () => {
  267. currentEditingRow.value = null
  268. }
  269. // 处理检验员选择变化
  270. const handleCheckerSelectChange = (selectedCheckers: CheckerItem[], row: ScheduleRow) => {
  271. if (!row) {
  272. return
  273. }
  274. // 同步更新checkers数组(用于提交)- 只需要memberId
  275. row.checkers = selectedCheckers.map(c => c.memberId.toString())
  276. // 构建 teamList
  277. const teamMap = new Map<string, any>()
  278. selectedCheckers.forEach(item => {
  279. const groupTeamId = String(item.groupTeamId || '')
  280. if (!teamMap.has(groupTeamId)) {
  281. teamMap.set(groupTeamId, {
  282. groupTeamId: groupTeamId,
  283. leaders: [],
  284. members: []
  285. })
  286. }
  287. const team = teamMap.get(groupTeamId)
  288. if (item.isLeader) {
  289. team.leaders.push({
  290. id: item.memberId,
  291. nickname: item.member?.nickname || ''
  292. })
  293. } else {
  294. team.members.push({
  295. id: item.memberId,
  296. nickname: item.member?.nickname || ''
  297. })
  298. }
  299. })
  300. row.teamList = Array.from(teamMap.values())
  301. // 标记为已修改并自动保存
  302. row.modified = true
  303. autoSaveRow(row)
  304. }
  305. // 处理日期变更
  306. const handleDateChange = (row: ScheduleRow) => {
  307. if (!row.date) {
  308. return
  309. }
  310. // 更新星期
  311. row.week = calculateWeek(row.date)
  312. // 标记为已修改并自动保存
  313. row.modified = true
  314. autoSaveRow(row)
  315. }
  316. // 通过日期计算星期
  317. const calculateWeek = (dateStr: string): string => {
  318. const weekMap = ['日', '一', '二', '三', '四', '五', '六']
  319. const week = dayjs(dateStr).day()
  320. return weekMap[week]
  321. }
  322. // 获取状态文本
  323. const getStatusText = (status: number | string | null | undefined): string => {
  324. if (status === null || status === undefined) {
  325. return '未知'
  326. }
  327. const statusMap: Record<number, string> = {
  328. 100: '已排期',
  329. 200: '待约检',
  330. 300: '已受理',
  331. 400: '检测中'
  332. }
  333. return statusMap[Number(status)] || '未知'
  334. }
  335. // 格式化管线长度,去掉小数点后无效的0
  336. const formatPipeLength = (val: string | undefined): string => {
  337. if (!val) return '-'
  338. const num = parseFloat(val)
  339. if (isNaN(num)) return '-'
  340. return String(num)
  341. }
  342. // 获取状态样式类
  343. const getStatusClass = (status: number | string | null | undefined): string => {
  344. if (status === null || status === undefined) {
  345. return ''
  346. }
  347. const classMap: Record<number, string> = {
  348. 100: 'dot-scheduled',
  349. 200: 'dot-pending-appointment',
  350. 300: 'dot-accepted',
  351. 400: 'dot-testing'
  352. }
  353. return classMap[Number(status)] || ''
  354. }
  355. // 判断是否可以编辑
  356. const canEdit = (row: ScheduleRow): boolean => {
  357. // 受理前(状态为 100 已排期或 200 待约检)的行可以编辑
  358. if (row.status === 100 || row.status === 200) {
  359. return true
  360. }
  361. // 其他行不可编辑
  362. return false
  363. }
  364. // 获取行类名
  365. const getRowClassName = ({ row }: { row: ScheduleRow }): string => {
  366. if (row.isCopied && !row.submitted) {
  367. return 'copied-row'
  368. }
  369. if (row.modified) {
  370. return 'modified-row'
  371. }
  372. return ''
  373. }
  374. // 复制行
  375. const handleCopy = (row: ScheduleRow) => {
  376. // 限制只有有 confirmOrderId 的情况下才能复制
  377. if (!row.confirmOrderId) {
  378. ElMessage.warning('只有已确认的订单才能复制')
  379. return
  380. }
  381. // 计算下一天的日期
  382. let nextDate = ''
  383. let nextWeek = ''
  384. if (row.date) {
  385. nextDate = dayjs(row.date).add(1, 'day').format('YYYY-MM-DD')
  386. nextWeek = calculateWeek(nextDate)
  387. }
  388. // 创建新行,复制所有属性
  389. const newRow: ScheduleRow = {
  390. ...row,
  391. sourceIndex: row.index,
  392. date: nextDate, // 设置为下一天
  393. week: nextWeek, // 设置对应的星期
  394. isCopied: true, // 标记为复制的行
  395. submitted: false, // 标记为未提交
  396. modified: true, // 标记为已修改
  397. // 清空检验员相关数据
  398. checkers: [],
  399. teamList: [],
  400. inspectors: [],
  401. // 深拷贝其他数组类型字段
  402. pipeMedium: row.pipeMedium ? [...row.pipeMedium] : []
  403. }
  404. // 找到源行的索引
  405. const sourceIndex = tableData.value.findIndex(r => r.id === row.id)
  406. // 将新行插入到源行后面
  407. if (sourceIndex !== -1) {
  408. // 设置新行的索引为源行索引 + 1
  409. newRow.index = sourceIndex + 1
  410. tableData.value.splice(sourceIndex + 1, 0, newRow)
  411. // 更新后续行的索引
  412. for (let i = sourceIndex + 2; i < tableData.value.length; i++) {
  413. tableData.value[i].index = i
  414. }
  415. } else {
  416. // 如果找不到源行,将新行添加到末尾
  417. newRow.index = tableData.value.length
  418. tableData.value.push(newRow)
  419. }
  420. ElMessage.success('复制成功,日期已自动设为下一天,请修改检验员后提交')
  421. }
  422. const handleQuery = async () => {
  423. loading.value = true
  424. const res = await EquipPipeSchedulingApi.planSchedulingShiftSchedule(queryParams.value)
  425. // 处理后端返回的null值
  426. tableData.value = (res || []).map((item, index) => ({
  427. ...item,
  428. index,
  429. date: item.date ? dayjs(item.date).format('YYYY-MM-DD') : '',
  430. week: item.week || '',
  431. checkers: item.checkers || [],
  432. teamList: item.teamList || [],
  433. checkType: item.checkType || '',
  434. unitName: item.unitName || '',
  435. pipeAddress: item.pipeAddress || '',
  436. contact: item.contact || '',
  437. contactPhone: item.contactPhone || '',
  438. pipeMedium: item.pipeMedium || [],
  439. pipeLengthTotal: item.pipeLengthTotal || '',
  440. equipDistrictName: item.equipDistrictName || '',
  441. equipStreetName: item.equipStreetName || '',
  442. lastLegalPeriodicalInspectionReportNo: item.lastLegalPeriodicalInspectionReportNo || '',
  443. lastYearReportNo: item.lastYearReportNo || '',
  444. lastMaintenanceReportNo: item.lastMaintenanceReportNo || '',
  445. confirmOrderId: item.confirmOrderId || '',
  446. status: item.status,
  447. isCopied: false,
  448. submitted: true
  449. }))
  450. tableData.value.forEach((row, index) => {
  451. const inspectors = row.teamList?.flatMap(team => {
  452. const teamMembers = []
  453. // 处理组长
  454. if (team.leaders && team.leaders.length > 0) {
  455. team.leaders.forEach(leader => {
  456. if (leader) {
  457. teamMembers.push({
  458. memberId: leader.id,
  459. groupTeamId: team.groupTeamId,
  460. leaderId: leader.id,
  461. member: leader,
  462. isLeader: true
  463. })
  464. }
  465. })
  466. }
  467. // 处理组员
  468. if (team.members && team.members.length > 0) {
  469. team.members.forEach(member => {
  470. if (member) {
  471. teamMembers.push({
  472. memberId: member.id,
  473. groupTeamId: team.groupTeamId,
  474. leaderId: team.leaders?.[0]?.id || '',
  475. member: member,
  476. isLeader: false
  477. })
  478. }
  479. })
  480. }
  481. return teamMembers
  482. }) || []
  483. row.inspectors = inspectors
  484. console.log('inspectors', inspectors)
  485. })
  486. loading.value = false
  487. }
  488. // 自动保存单行:修改日期或检验员后实时保存
  489. // 前提:日期不重复 + 检验员不为空
  490. const autoSaveRow = async (row: ScheduleRow) => {
  491. // 校验:日期不能为空
  492. if (!row.date || row.date.trim() === '') {
  493. return
  494. }
  495. // 校验:检验员不能为空
  496. if (!row.checkers || row.checkers.length === 0) {
  497. return
  498. }
  499. // 校验:同 confirmOrderId 日期不能重复
  500. if (row.confirmOrderId) {
  501. const sameGroup = tableData.value.filter(r => r.confirmOrderId === row.confirmOrderId)
  502. const duplicate = sameGroup.some(item => item.date === row.date && item !== row)
  503. if (duplicate) {
  504. ElMessage.warning('同一个约检确认单下日期不能重复')
  505. return
  506. }
  507. }
  508. try {
  509. const teamList = (row.teamList || []).map(team => ({
  510. groupTeamId: team.groupTeamId,
  511. leaderId: team.leaders?.[0]?.id || '',
  512. memberIdList: team.members?.map(member => member.id) || []
  513. }))
  514. if (row.isCopied && !row.submitted) {
  515. // 复制行
  516. await EquipPipeSchedulingApi.setShiftSchedule([{
  517. isCopy: true,
  518. sourceId: row.id,
  519. date: row.date,
  520. teamList: teamList
  521. }])
  522. row.submitted = true
  523. row.modified = false
  524. } else {
  525. // 修改行
  526. await EquipPipeSchedulingApi.setShiftSchedule([{
  527. isCopy: false,
  528. id: row.id,
  529. date: row.date,
  530. teamList: teamList
  531. }])
  532. row.modified = false
  533. }
  534. ElMessage.success('保存成功')
  535. } catch (error) {
  536. console.error('自动保存失败:', error)
  537. ElMessage.error('保存失败')
  538. }
  539. }
  540. // 提交计划 - 提交勾选的行
  541. const handleSubmitSelected = async () => {
  542. if (selectedRows.value.length === 0) {
  543. ElMessage.warning('请先勾选需要提交的计划')
  544. return
  545. }
  546. const taskIdsToSubmit: any[] = []
  547. selectedRows.value.forEach(row => {
  548. taskIdsToSubmit.push(
  549. row.id
  550. )
  551. })
  552. try {
  553. await ElMessageBox.confirm(
  554. `是否确认提交选中的这${taskIdsToSubmit.length} 个计划?`,
  555. '提示',
  556. {
  557. confirmButtonText: '确定',
  558. cancelButtonText: '取消',
  559. type: 'warning'
  560. }
  561. )
  562. await EquipPipeSchedulingApi.planSchedulingConfirm({
  563. ids: taskIdsToSubmit
  564. })
  565. ElMessage.success('提交计划成功')
  566. // 提交成功后清空选中状态
  567. selectedRows.value = []
  568. // 重新加载数据
  569. handleQuery()
  570. } catch (error) {
  571. console.error('提交计划失败:', error)
  572. ElMessage.error('提交计划失败')
  573. }
  574. }
  575. // 查询
  576. // 当前行变化(单选)
  577. const handleCurrentChange = (currentRow: ScheduleRow | null) => {
  578. if (currentRow) {
  579. selectedRows.value = [currentRow]
  580. } else {
  581. selectedRows.value = []
  582. }
  583. }
  584. // 排序变化
  585. const handleSortChange = ({ column, prop, order }: { column: any; prop: string; order: string }) => {
  586. if (order) {
  587. sortOrderBy.value = prop
  588. sortOrderType.value = order === 'ascending' ? 'asc' : 'desc'
  589. } else {
  590. sortOrderBy.value = ''
  591. sortOrderType.value = ''
  592. }
  593. handleQuery()
  594. }
  595. // 导出排班表 - 导出和列表看到的数据一模一样
  596. const handleExport = async () => {
  597. try {
  598. exportLoading.value = true
  599. const data = await EquipPipeSchedulingApi.exportShiftSchedule(queryParams.value)
  600. // 使用 Blob 方式下载
  601. const blob = new Blob([data], { type: 'application/vnd.ms-excel' })
  602. const href = URL.createObjectURL(blob)
  603. const downA = document.createElement('a')
  604. downA.href = href
  605. downA.download = '排班表'
  606. downA.click()
  607. URL.revokeObjectURL(href)
  608. ElMessage.success('导出成功')
  609. } catch (error) {
  610. console.error('导出失败:', error)
  611. ElMessage.error('导出失败')
  612. } finally {
  613. exportLoading.value = false
  614. }
  615. }
  616. // 暴露方法给父组件
  617. defineExpose({
  618. handleQuery,
  619. handleSubmitSelected,
  620. handleExport
  621. })
  622. // ==================== 生命周期 ====================
  623. onMounted(async () => {
  624. await handleQuery()
  625. })
  626. </script>
  627. <style scoped lang="scss">
  628. .shift-schedule-container {
  629. margin-top: 15px;
  630. .shift-schedule-table {
  631. :deep(.el-table__header th) {
  632. background-color: #e5f0fb;
  633. color: #0a2c48;
  634. font-weight: 600;
  635. font-size: 13px;
  636. text-transform: uppercase;
  637. letter-spacing: 0.3px;
  638. }
  639. :deep(.el-table__header th:first-child) {
  640. font-weight: 600;
  641. color: #113355;
  642. border-left: 3px solid #3f78b3;
  643. }
  644. :deep(.el-table__row td) {
  645. padding: 10px 6px;
  646. font-size: 13px;
  647. background-color: #ffffff;
  648. }
  649. // 当前选中行高亮
  650. :deep(.el-table__row.current-row) {
  651. background-color: #ecf5ff !important;
  652. td {
  653. background-color: #ecf5ff !important;
  654. }
  655. }
  656. // 复制的行高亮显示
  657. :deep(.el-table__row.copied-row) {
  658. background-color: #fff3cd !important;
  659. td {
  660. background-color: #fff3cd !important;
  661. }
  662. }
  663. // 修改的行高亮显示
  664. :deep(.el-table__row.modified-row) {
  665. background-color: #e7f3ff !important;
  666. td {
  667. background-color: #e7f3ff !important;
  668. }
  669. }
  670. :deep(.el-table__row td:first-child) {
  671. font-weight: 600;
  672. color: #113355;
  673. border-left: 3px solid #3f78b3;
  674. }
  675. // 日期单元格基础样式
  676. :deep(.date-cell) {
  677. display: flex;
  678. align-items: center;
  679. justify-content: center;
  680. gap: 8px;
  681. .edit-icon {
  682. cursor: pointer;
  683. color: #409eff;
  684. font-size: 14px;
  685. transition: all 0.3s;
  686. &:hover {
  687. color: #66b1ff;
  688. transform: scale(1.2);
  689. }
  690. }
  691. }
  692. // 已修改的日期单元格样式
  693. :deep(.modified-cell.date-cell) {
  694. background-color: #fff3cd;
  695. padding: 4px 0;
  696. border-radius: 4px;
  697. box-shadow: 0 0 0 1px #ffc107 inset !important;
  698. }
  699. // 已修改的检验员单元格样式
  700. :deep(.modified-checker-cell) {
  701. .checker-trigger {
  702. background-color: #fff3cd !important;
  703. box-shadow: 0 0 0 1px #ffc107 inset !important;
  704. }
  705. }
  706. // 检验员显示样式
  707. .checker-display {
  708. padding: 4px 8px;
  709. min-height: 32px;
  710. display: flex;
  711. align-items: center;
  712. justify-content: center;
  713. color: #606266;
  714. font-size: 13px;
  715. }
  716. // 检验员触发器样式
  717. .checker-trigger {
  718. padding: 4px 8px;
  719. min-height: 32px;
  720. display: flex;
  721. align-items: center;
  722. justify-content: center;
  723. cursor: pointer;
  724. border-radius: 4px;
  725. transition: all 0.3s;
  726. color: #909399;
  727. &:hover {
  728. background-color: #f5f7fa;
  729. color: #409eff;
  730. }
  731. &.has-checkers {
  732. color: #409eff;
  733. font-weight: 500;
  734. }
  735. .checker-text {
  736. flex: 1;
  737. text-align: center;
  738. overflow: visible;
  739. text-overflow: clip;
  740. white-space: normal;
  741. word-break: break-word;
  742. line-height: 1.6;
  743. max-width: 280px;
  744. }
  745. .edit-icon {
  746. font-size: 14px;
  747. opacity: 0.6;
  748. flex-shrink: 0;
  749. margin-left: 4px;
  750. }
  751. }
  752. // Popover内容样式
  753. .popover-content {
  754. max-height: 400px;
  755. overflow-y: auto;
  756. padding: 8px;
  757. }
  758. :deep(.dialog-footer) {
  759. display: flex;
  760. justify-content: flex-end;
  761. gap: 10px;
  762. }
  763. }
  764. }
  765. // 状态图例样式(用于tooltip)
  766. .status-legend {
  767. .legend-item {
  768. display: flex;
  769. align-items: center;
  770. gap: 8px;
  771. padding: 4px 0;
  772. font-size: 13px;
  773. color: #fff;
  774. .status-dot {
  775. display: inline-block;
  776. width: 10px;
  777. height: 10px;
  778. border-radius: 50%;
  779. flex-shrink: 0;
  780. }
  781. }
  782. }
  783. // 状态颜色类
  784. .dot-scheduled {
  785. background: #409eff;
  786. }
  787. .dot-pending-appointment {
  788. background: #e6a23c;
  789. }
  790. .dot-accepted {
  791. background: #67c23a;
  792. }
  793. .dot-testing {
  794. background: #f56c6c;
  795. }
  796. </style>
  797. <style lang="scss">
  798. // Popover 全局样式(不使用scoped)
  799. .checker-select-popover {
  800. padding: 0 !important;
  801. max-height: 500px;
  802. overflow: hidden;
  803. .el-popover__title {
  804. padding: 12px 16px;
  805. margin: 0;
  806. border-bottom: 1px solid #EBEEF5;
  807. font-weight: 600;
  808. color: #303133;
  809. }
  810. // 确保 CheckerSelect 组件的样式正确
  811. .checker-select-container {
  812. .checker-list {
  813. border: none;
  814. max-height: 450px;
  815. overflow-y: auto;
  816. }
  817. }
  818. }
  819. </style>