index.uvue 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. <template>
  2. <uni-navbar-lite :showLeft=false title="工单"></uni-navbar-lite>
  3. <view class="list-page">
  4. <!-- 搜索栏 -->
  5. <view class="search-bar">
  6. <view class="search-box">
  7. <image class="search-icon" src="/static/images/workbench/list/1.png" mode="aspectFit"></image>
  8. <input class="search-input" type="text" placeholder="搜索工单编码、风机编号" v-model="keyword" @confirm="handleSearch" @blur="handleSearch" />
  9. <text v-if="keyword.length > 0" class="clear-icon" @click="clearSearch">✕</text>
  10. </view>
  11. </view>
  12. <view class="status-bar">
  13. <scroll-view class="scroll-view_H" direction="horizontal" show-scrollbar="false">
  14. <view class="scroll-view-item_H uni-bg-red">
  15. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'all'}" @click="switchStatus('all')">全部</text>
  16. </view>
  17. <!-- <view class="scroll-view-item_H uni-bg-green" v-if="checkPermi(['gxt:maintenance:order:edit','gxt:repairOrder:edit']) && !dealLoad">
  18. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'to_issue,accept_return'}" @click="switchStatus('to_issue,accept_return')">待下发</text>
  19. </view> -->
  20. <view class="scroll-view-item_H uni-bg-green" v-if="checkPermi(['gxt:maintenance:order:accept','gxt:repairOrder:accept']) && !dealLoad">
  21. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'assigned'}" @click="switchStatus('assigned')">待接单</text>
  22. </view>
  23. <view class="scroll-view-item_H" v-if="checkPermi(['gxt:maintenance:order:suspend','gxt:repairOrder:suspend']) && !dealLoad">
  24. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'to_finish'}" @click="switchStatus('to_finish')">待结单</text>
  25. </view>
  26. <view class="scroll-view-item_H" v-if="checkPermi(['gxt:maintenance:order:approve','gxt:repairOrder:approve']) && !dealLoad">
  27. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'to_approve'}" @click="switchStatus('to_approve')">待审批</text>
  28. </view>
  29. <view class="scroll-view-item_H" v-if="checkPermi(['gxt:maintenance:order:resume','gxt:repairOrder:resume']) && !dealLoad">
  30. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'suspended'}" @click="switchStatus('suspended')">已挂起</text>
  31. </view>
  32. <view class="scroll-view-item_H" v-if="checkPermi(['gxt:maintenance:order:autoResume','gxt:repairOrder:autoResume']) && !dealLoad">
  33. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'auto_suspend'}" @click="switchStatus('auto_suspend')">自动挂起</text>
  34. </view>
  35. <view class="scroll-view-item_H" v-if="checkPermi(['gxt:maintenance:order:restart','gxt:repairOrder:restart']) && !dealLoad">
  36. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'completed'}" @click="switchStatus('completed')">已完成</text>
  37. </view>
  38. <view class="scroll-view-item_H" v-if="dealLoad">
  39. <text class="status-txt" :class="{'stauts-sel': currentStatus === 'pending'}" @click="switchStatus('pending')">待处理</text>
  40. </view>
  41. </scroll-view>
  42. </view>
  43. <!-- 列表内容 -->
  44. <common-list
  45. :dataList="dataList"
  46. :loading="loading"
  47. :refreshing="refreshing"
  48. :hasMore="hasMore"
  49. @refresh="handleRefresh"
  50. @loadMore="loadMore"
  51. @itemClick="handleView"
  52. >
  53. <template #default="{ item, index }">
  54. <view class="list-item">
  55. <view class="item-container">
  56. <view class="item-header">
  57. <text class="item-title">{{ getWorkOrderProjectNo(item) }}-{{ getPcsStationName(item) }}{{ getPcsDeviceName(item) }}{{ getOrderType(item) }}</text>
  58. <text class="status-tag" :class="getStatusClass(item)" >{{ getWorkOrderStatus(item) }}</text>
  59. <!-- <view class="status-view" :class="getStatusClass(item)"><text class="status-tag" :class="getStatusClass(item)" >{{ getWorkOrderStatus(item) }}</text></view> -->
  60. </view>
  61. <view class="info-row">
  62. <view class="info-label">
  63. <text class="text-gray">{{ getDisplayTime(item) }}</text>
  64. </view>
  65. </view>
  66. <view class="btn-group">
  67. <view
  68. v-if="getOrderStatus(item) == 'assigned' && canHandleOrder(item,'accept')"
  69. class="btn-primary info-value"
  70. @click.stop="handleItemClick(item,'')"
  71. >
  72. <text class="btn-text">接单</text>
  73. </view>
  74. <view
  75. v-if="getOrderStatus(item) == 'to_approve' && canHandleOrder(item,'')"
  76. class="btn-primary info-value"
  77. @click.stop="handleItemClick(item,'')"
  78. >
  79. <text class="btn-text">审批</text>
  80. </view>
  81. <view
  82. v-if="getOrderStatus(item) == 'suspended' && canHandleOrder(item,'')"
  83. class="btn-primary info-value"
  84. @click.stop="handleItemClick(item,'')"
  85. >
  86. <text class="btn-text">恢复</text>
  87. </view>
  88. <view
  89. v-if="getOrderStatus(item) == 'auto_suspend' && canHandleOrder(item,'')"
  90. class="btn-primary info-value"
  91. @click.stop="handleItemClick(item,'')"
  92. >
  93. <text class="btn-text">恢复</text>
  94. </view>
  95. <view
  96. v-if="getOrderStatus(item) == 'return' && canHandleOrder(item,'acceptReturn')"
  97. class="btn-primary info-value"
  98. @click.stop="handleItemClick(item,'acceptReturn')"
  99. >
  100. <text class="btn-text">退回</text>
  101. </view>
  102. <view
  103. v-if="getOrderStatus(item) == 'assigned' && canHandleOrder(item,'acceptReturn')"
  104. class="btn-primary info-value"
  105. @click.stop="handleItemClick(item,'acceptReturn')"
  106. >
  107. <text class="btn-text">退回</text>
  108. </view>
  109. <view
  110. v-if="getOrderStatus(item) == 'to_finish' && canHandleOrder(item, 'return')"
  111. class="btn-primary info-value"
  112. @click.stop="handleItemClick(item, 'return')"
  113. >
  114. <text class="btn-text">退回</text>
  115. </view>
  116. <view
  117. v-if="getOrderStatus(item) == 'to_finish' && canHandleOrder(item, 'suspend')"
  118. class="btn-primary info-value"
  119. @click.stop="handleItemClick(item, 'suspend')"
  120. >
  121. <text class="btn-text">挂起</text>
  122. </view>
  123. <view
  124. v-if="getOrderStatus(item) == 'to_finish' && canHandleOrder(item, 'finalize')"
  125. class="btn-primary info-value"
  126. @click.stop="handleItemClick(item, 'finalize')"
  127. >
  128. <text class="btn-text">复启</text>
  129. </view>
  130. </view>
  131. </view>
  132. </view>
  133. </template>
  134. </common-list>
  135. <custom-tabbar :current="1" :hide0="tabbar[0]" :hide1="tabbar[1]" :hide2="tabbar[2]" :hide3="tabbar[3]"/>
  136. </view>
  137. </template>
  138. <script setup lang="uts">
  139. import { ref, onBeforeUnmount, onMounted, reactive } from 'vue'
  140. import type { acceptOrderInfo } from '../../types/order'
  141. import type { SysDictData, DictDataResponse } from '../../types/dict'
  142. import { getOrderList } from '../../api/order/list'
  143. import { getDictDataByType } from '../../api/dict/index'
  144. import {checkPermi, getUserInfo} from '../../utils/storage'
  145. const userId = ref<string>("")
  146. const roles = ref<string>('')
  147. // 列表数据
  148. const dataList = ref<acceptOrderInfo[]>([])
  149. let keyword = ref<string>("")
  150. const page = ref<number>(1)
  151. const pageSize: number = 10
  152. const hasMore = ref<boolean>(true)
  153. const loading = ref<boolean>(false)
  154. const refreshing = ref<boolean>(false)
  155. const total = ref<number>(0)
  156. let currentStatus = ref<string>('') // 添加状态管理
  157. const statusDictList = ref<SysDictData[]>([]) // 工单状态字典列表
  158. const tabbar = reactive([1,1,1,1])
  159. // 添加防重复请求的标志位(参考score/pending.uvue的实现)
  160. const isSearching = ref<boolean>(false)
  161. // 添加防重复刷新的标志
  162. const isRefreshing = ref<boolean>(false)
  163. // 添加刷新时间戳,用于防抖
  164. const lastRefreshTime = ref<number>(0)
  165. // 添加字典加载状态
  166. const dictLoaded = ref<boolean>(false)
  167. // 待处理工单加载
  168. const dealLoad = ref<boolean>(false)
  169. type StatusItem = {
  170. key: string,
  171. permis: string[],
  172. condition: () => boolean
  173. }
  174. const statusConfig: StatusItem[] = [
  175. { key: 'all', permis: ['gxt:order:all'], condition: () => !dealLoad.value },
  176. { key: 'assigned', permis: ['gxt:maintenance:order:accept','gxt:repairOrder:accept'], condition: () => !dealLoad.value },
  177. { key: 'to_finish', permis: ['gxt:maintenance:order:suspend','gxt:repairOrder:suspend'], condition: () => !dealLoad.value },
  178. { key: 'to_approve', permis: ['gxt:maintenance:order:approve','gxt:repairOrder:approve'], condition: () => !dealLoad.value },
  179. { key: 'suspended', permis: ['gxt:maintenance:order:resume','gxt:repairOrder:resume'], condition: () => !dealLoad.value },
  180. { key: 'completed', permis: ['gxt:maintenance:order:restart','gxt:repairOrder:restart'], condition: () => !dealLoad.value }
  181. ]
  182. const getOrderStatus = (item: any | null): string => {
  183. if (item == null) return ''
  184. const orderItem = item as acceptOrderInfo
  185. return orderItem.workOrderStatus ?? ''
  186. }
  187. // 获取工单状态字典列表
  188. const loadStatusDictList = async (): Promise<void> => {
  189. try {
  190. const result = await getDictDataByType('gxt_work_order_status')
  191. const resultObj = result as UTSJSONObject
  192. if (resultObj['code'] == 200) {
  193. const data = resultObj['data'] as any[]
  194. const dictData: SysDictData[] = []
  195. if (data.length > 0) {
  196. for (let i = 0; i < data.length; i++) {
  197. const item = data[i] as UTSJSONObject
  198. // 只提取需要的字段
  199. const dictItem: SysDictData = {
  200. dictValue: item['dictValue'] as string | null,
  201. dictLabel: item['dictLabel'] as string | null,
  202. dictCode: null,
  203. dictSort: null,
  204. dictType: null,
  205. cssClass: null,
  206. listClass: null,
  207. isDefault: null,
  208. status: null,
  209. default: null,
  210. createTime: null,
  211. remark: null
  212. }
  213. dictData.push(dictItem)
  214. }
  215. }
  216. statusDictList.value = dictData
  217. dictLoaded.value = true
  218. }
  219. } catch (e: any) {
  220. console.error('获取工单状态字典失败:', e.message)
  221. dictLoaded.value = true
  222. }
  223. }
  224. const getFirstVisibleStatus = (): string => {
  225. for (const item of statusConfig) {
  226. const hasPerm = item.permis.length === 0 || checkPermi(item.permis)
  227. const meetsCondition = item.condition()
  228. if (hasPerm && meetsCondition) {
  229. // if (hasPerm) {
  230. return item.key as string
  231. }
  232. }
  233. return 'all' // fallback,理论上不会走到这里
  234. }
  235. let loadCount = 0
  236. // 加载列表数据
  237. const loadData = async (isRefresh: boolean | null, disablePullDown: boolean | null): Promise<void> => {
  238. const id = ++loadCount
  239. console.log(`【loadData #${id}】开始, isRefresh=${isRefresh}, disablePullDown=${disablePullDown}`)
  240. // 防止重复请求的核心机制(参考score/pending.uvue的实现)
  241. if (loading.value) {
  242. // 确保刷新状态最终被重置,防止卡死
  243. if (isRefresh != true) {
  244. refreshing.value = false;
  245. }
  246. return
  247. }
  248. const shouldRefresh = isRefresh != null ? isRefresh : false
  249. const shouldDisablePullDown = disablePullDown != null ? disablePullDown : false
  250. loading.value = true
  251. if (shouldRefresh && !shouldDisablePullDown) {
  252. page.value = 1
  253. refreshing.value = true
  254. } else if (shouldRefresh && shouldDisablePullDown) {
  255. // 状态切换时,重置页码但不触发动画
  256. page.value = 1
  257. // 即使禁用下拉刷新,也要确保刷新状态最终被重置
  258. refreshing.value = false
  259. } else {
  260. // 对于加载更多操作,不需要显示下拉刷新状态
  261. refreshing.value = false;
  262. }
  263. try {
  264. // 处理默认值
  265. const searchKeyword = keyword.value.length > 0 ? keyword.value : null
  266. const result = await getOrderList(page.value, pageSize, searchKeyword, currentStatus.value)
  267. // 提取响应数据
  268. const resultObj = result as UTSJSONObject
  269. const code = resultObj['code'] as number
  270. const responseData = resultObj['rows'] as any[]
  271. const responseTotal = resultObj['total'] as number
  272. if (code == 200) {
  273. // 将 any[] 转换为 acceptOrderInfo[]
  274. const newData: acceptOrderInfo[] = []
  275. for (let i = 0; i < responseData.length; i++) {
  276. const item = responseData[i] as UTSJSONObject
  277. const orderItem: acceptOrderInfo = {
  278. orderType: item['orderType'] as Number,
  279. id: item['id'] as Number,
  280. teamLeaderId: item['teamLeaderId'] != null ? (item['teamLeaderId'] as Number) : 0,
  281. acceptUserId: item['acceptUserId'] != null ? (item['acceptUserId'] as Number) : 0,
  282. teamLeaderName: item['teamLeaderName'] as string | null,
  283. acceptUserName: item['acceptUserName'] as string | null,
  284. acceptTime: item['acceptTime'] as string | null,
  285. assignTime: item['assignTime'] as string | null,
  286. assignUserName: item['assignUserName'] as string | null,
  287. status: (item['status']==null)?0:item['status'] as Number,
  288. workOrderProjectNo: item['workOrderProjectNo'] as string | null,
  289. workOrderStatus: item['workOrderStatus'] as string | null,
  290. gxtCenterId: item['gxtCenterId'] as Number | 0,
  291. gxtCenter: item['gxtCenter'] as string | null,
  292. pcsStationId: item['pcsStationId'] as Number | 0,
  293. pcsStationName: item['pcsStationName'] as string | null,
  294. pcsDeviceId: item['pcsDeviceId'] as Number | 0,
  295. pcsDeviceName: item['pcsDeviceName'] as string | null,
  296. brand: item['brand'] as string | null,
  297. model: item['model'] as string | null,
  298. createTime: item['createTime'] as string | null,
  299. suspendReason: item['suspendReason'] as string | null,
  300. rejectionReason: item['rejectionReason'] as string | null,
  301. updateTime: item['updateTime'] as string | null, // 新增字段
  302. workEndTime: item['workEndTime'] as string | null // 新增字段
  303. }
  304. newData.push(orderItem)
  305. }
  306. // 不再在前端过滤,直接使用API返回的数据
  307. if (shouldRefresh) {
  308. dataList.value = newData
  309. } else {
  310. dataList.value = [...dataList.value, ...newData]
  311. }
  312. total.value = responseTotal
  313. hasMore.value = dataList.value.length < responseTotal
  314. // 通知首页更新待接单数量
  315. if (currentStatus.value === 'assigned') {
  316. uni.$emit('refreshAssignedCount')
  317. }
  318. } else {
  319. const msg = resultObj['msg'] as string | null
  320. uni.showToast({
  321. title: msg ?? '加载失败',
  322. icon: 'none'
  323. })
  324. }
  325. } catch (e: any) {
  326. uni.showToast({
  327. title: e.message ?? '加载失败',
  328. icon: 'none'
  329. })
  330. } finally {
  331. loading.value = false
  332. // 确保刷新状态能结束(参考score/pending.uvue的实现)
  333. if (shouldRefresh) {
  334. refreshing.value = false;
  335. // 使用setTimeout确保状态彻底重置
  336. setTimeout(() => {
  337. isRefreshing.value = false;
  338. }, 50);
  339. }
  340. }
  341. }
  342. // 辅助函数:从 any 类型提取属性
  343. const getOrderType = (item: any | null): string => {
  344. if (item == null) return ''
  345. const orderInfoItem = item as acceptOrderInfo
  346. return orderInfoItem.orderType == 1?"维修工单":"维保工单";
  347. }
  348. const getWorkOrderProjectNo = (item: any | null): string | null => {
  349. if (item == null) return ''
  350. const orderInfoItem = item as acceptOrderInfo
  351. return orderInfoItem.workOrderProjectNo
  352. }
  353. const getPcsStationName = (item: any | null): string | null=> {
  354. if (item == null) return ''
  355. const orderInfoItem = item as acceptOrderInfo
  356. return orderInfoItem.pcsStationName
  357. }
  358. const getPcsDeviceName = (item: any | null): string | null=> {
  359. if (item == null) return ''
  360. const orderInfoItem = item as acceptOrderInfo
  361. return orderInfoItem.pcsDeviceName
  362. }
  363. const getAssignTime = (item: any | null): string|null => {
  364. if (item == null) return null
  365. const orderInfoItem = item as acceptOrderInfo
  366. return orderInfoItem.assignTime
  367. }
  368. const getAcceptTime = (item: any | null): string|null => {
  369. if (item == null) return null
  370. const orderInfoItem = item as acceptOrderInfo
  371. return orderInfoItem.acceptTime
  372. }
  373. const getCreateTime = (item: any | null): string|null => {
  374. if (item == null) return null
  375. const orderInfoItem = item as acceptOrderInfo
  376. return orderInfoItem.createTime
  377. }
  378. // 根据状态显示不同的时间
  379. const getDisplayTime = (item: any | null): string|null => {
  380. if (item == null) return null
  381. const orderInfoItem = item as acceptOrderInfo
  382. // 如果是"待接单"状态,显示派单时间
  383. if (orderInfoItem.workOrderStatus == 'assigned') {
  384. return '下发时间:' + orderInfoItem.assignTime
  385. } else if(orderInfoItem.workOrderStatus == 'to_finish') {
  386. if(orderInfoItem.workEndTime != null) {
  387. return '结束时间:' + orderInfoItem.workEndTime
  388. }
  389. return '接单时间:' + orderInfoItem.acceptTime
  390. } else if(orderInfoItem.workOrderStatus == 'to_approve') {
  391. return '申请挂起时间:' + orderInfoItem.updateTime
  392. } else if(orderInfoItem.workOrderStatus == 'suspended') {
  393. return '审批通过时间:' + orderInfoItem.updateTime
  394. } else if(orderInfoItem.workOrderStatus == 'return' || orderInfoItem.workOrderStatus == 'accept_return') {
  395. return '退回时间:' + orderInfoItem.updateTime
  396. } else if(orderInfoItem.workOrderStatus == 'completed') {
  397. return '结单时间:' + orderInfoItem.updateTime
  398. } else if(orderInfoItem.workOrderStatus == 'archived') {
  399. return '归档时间:' + orderInfoItem.updateTime
  400. }
  401. // 默认显示创建时间
  402. return '创建时间:' + orderInfoItem.createTime
  403. }
  404. const getWorkOrderStatus = (item: any | null): string | null => {
  405. if (item == null) return ''
  406. const orderInfoItem = item as acceptOrderInfo
  407. const rawStatus = orderInfoItem.workOrderStatus
  408. if (rawStatus==null) return ''
  409. // 如果字典尚未加载,返回原始值
  410. if (!dictLoaded.value) {
  411. return rawStatus
  412. }
  413. // 查找字典中对应的标签
  414. const dictItem = statusDictList.value.find(dict => dict.dictValue == rawStatus)
  415. return dictItem!=null ? dictItem.dictLabel : rawStatus
  416. }
  417. const getStatusClass = (item: any | null): string => {
  418. if (item == null) return ''
  419. const orderInfoItem = item as acceptOrderInfo
  420. const rawStatus = orderInfoItem.workOrderStatus
  421. if (rawStatus==null) return ''
  422. // const status = rawStatus
  423. // 返回对应的状态类名
  424. return `status-${rawStatus}`
  425. }
  426. // 切换状态
  427. const switchStatus = (status: string): void => {
  428. // 添加防重复调用检查(参考score/pending.uvue的实现)
  429. if (loading.value) {
  430. return;
  431. }
  432. // 添加防重复请求检查
  433. if (isSearching.value) {
  434. return
  435. }
  436. isSearching.value = true
  437. currentStatus.value = status
  438. page.value = 1
  439. loadData(true, true) // 第二个参数为true表示禁用下拉刷新动画
  440. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  441. setTimeout(() => {
  442. isSearching.value = false
  443. }, 100)
  444. }
  445. // 下拉刷新
  446. const handleRefresh = async (): Promise<void> => {
  447. console.log("handleRefresh被触发")
  448. console.log("loading.value===",loading.value)
  449. console.log("isRefreshing.value===",isRefreshing.value)
  450. // 防抖处理,避免频繁触发(参考score/pending.uvue的实现)
  451. const now = Date.now();
  452. if (now - lastRefreshTime.value < 1000) {
  453. refreshing.value = false;
  454. return;
  455. }
  456. lastRefreshTime.value = now;
  457. // 添加防重复调用检查
  458. if (loading.value || isRefreshing.value) {
  459. // 如果已经在加载或正在刷新,直接重置刷新状态
  460. refreshing.value = false;
  461. return;
  462. }
  463. console.log("loading.value1===",loading.value)
  464. console.log("isRefreshing.value1===",isRefreshing.value)
  465. // 设置刷新标志
  466. isRefreshing.value = true;
  467. try {
  468. await loadData(true, false); // 使用默认的下拉刷新行为
  469. } catch (error) {
  470. console.error('刷新失败:', error);
  471. refreshing.value = false;
  472. isRefreshing.value = false;
  473. }
  474. // 确保在一定时间后重置刷新标志,防止意外情况
  475. setTimeout(() => {
  476. isRefreshing.value = false
  477. }, 100) // 延迟重置,确保状态完全更新
  478. }
  479. // 加载更多
  480. const loadMore = (): void => {
  481. if (!hasMore.value || loading.value) {
  482. return
  483. }
  484. page.value++
  485. loadData(false, false)
  486. }
  487. // 搜索
  488. const handleSearch = (): void => {
  489. // 添加防重复调用检查(参考score/pending.uvue的实现)
  490. if (loading.value) {
  491. return;
  492. }
  493. // 添加防重复请求检查
  494. if (isSearching.value) {
  495. return
  496. }
  497. isSearching.value = true
  498. page.value = 1
  499. loadData(true, true) // 状态切换时禁用下拉刷新动画
  500. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  501. setTimeout(() => {
  502. isSearching.value = false
  503. }, 100)
  504. }
  505. const handleSearchOnBlur = (): void => {
  506. handleSearch()
  507. }
  508. // 方法:判断当前工单是否显示操作按钮
  509. const canHandleOrder = (item: any | null, buttonType: string | ''): boolean => {
  510. if (item == null) return false
  511. let permit: string[] = []
  512. const orderItem = item as acceptOrderInfo
  513. if(orderItem.workOrderStatus == 'assigned' && buttonType != '' && buttonType == "acceptReturn" && orderItem.orderType == 1) {
  514. // 接单退回
  515. permit = ['gxt:repairOrder:acceptReturn']
  516. } else if(orderItem.workOrderStatus == 'assigned' && buttonType != '' && buttonType == "accept") {
  517. // 接单
  518. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:accept'] : ['gxt:repairOrder:accept']
  519. } else if(orderItem.workOrderStatus == 'to_finish') {
  520. if(buttonType != '' && buttonType == "suspend" && (orderItem.teamLeaderId == parseInt(userId.value) || roles.value.includes("管理员"))) {
  521. // 挂起
  522. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:suspend'] : ['gxt:repairOrder:suspend']
  523. } else if(buttonType != '' && buttonType == "return" && orderItem.orderType == 1 && (orderItem.teamLeaderId == parseInt(userId.value) || roles.value.includes("管理员"))) {
  524. // 退回
  525. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:return'] : ['gxt:repairOrder:return']
  526. } else if(buttonType != '' && buttonType == "finalize" && orderItem.orderType == 1 && (orderItem.teamLeaderId == parseInt(userId.value) || roles.value.includes("管理员"))) {
  527. // 复启
  528. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:finalize'] : ['gxt:repairOrder:finalize']
  529. }
  530. } else if(orderItem.workOrderStatus == 'to_approve') {
  531. // 审批
  532. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:approve'] : ['gxt:repairOrder:approve']
  533. } else if(orderItem.workOrderStatus == 'suspended' && (orderItem.teamLeaderId == parseInt(userId.value) || roles.value.includes("管理员"))) {
  534. // 恢复
  535. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:resume'] : ['gxt:repairOrder:resume']
  536. } else if(orderItem.workOrderStatus == 'return') {
  537. // 接单退回
  538. permit = ['gxt:repairOrder:acceptReturn']
  539. } else if(orderItem.workOrderStatus == 'auto_suspend') {
  540. // 自动挂起恢复
  541. permit = orderItem.orderType == 2 ? ['gxt:maintenance:order:autoResume'] : ['gxt:repairOrder:autoResume']
  542. }
  543. else {
  544. return false
  545. }
  546. // const orderType = (item as acceptOrderInfo).orderType
  547. return checkPermi(permit)
  548. }
  549. // 点击列表项
  550. const handleItemClick = (item: any | null, buttonType: string | ''): void => {
  551. if (item == null) return
  552. const orderItem = item as acceptOrderInfo
  553. // if(currentStatus.value === '' || currentStatus.value === 'completed' || currentStatus.value === 'all') {
  554. // // 传递orderType参数以便详情页决定调用哪个API
  555. // uni.navigateTo({
  556. // url: `/pages/order/detail/index?id=${orderItem.id}&orderType=${orderItem.orderType}`
  557. // })
  558. // } else
  559. if(orderItem.workOrderStatus == 'assigned') {
  560. if(buttonType != '' && buttonType == "acceptReturn") {
  561. // 跳转到退回页面
  562. uni.navigateTo({
  563. url: `/pages/order/detail/returnIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  564. })
  565. } else {
  566. // 跳转到接单页面
  567. uni.navigateTo({
  568. url: `/pages/order/detail/acceptIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  569. })
  570. }
  571. } else if(orderItem.workOrderStatus == 'to_finish') {
  572. if(buttonType != '' && buttonType == "suspend") {
  573. // 跳转到待结单页面
  574. uni.navigateTo({
  575. url: `/pages/order/detail/suspendIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  576. })
  577. } else if(buttonType != '' && buttonType == "return") {
  578. // 跳转到退回页面
  579. uni.navigateTo({
  580. url: `/pages/order/detail/returnIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  581. })
  582. } else if(buttonType != '' && buttonType == "finalize") {
  583. // 跳转到复启页面
  584. uni.navigateTo({
  585. url: `/pages/order/detail/resetIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  586. })
  587. }
  588. } else if(orderItem.workOrderStatus == 'to_approve') {
  589. // 跳转到待审批页面
  590. uni.navigateTo({
  591. url: `/pages/order/detail/approveIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  592. })
  593. } else if(orderItem.workOrderStatus == 'suspended' || orderItem.workOrderStatus == 'auto_suspend') {
  594. // 跳转到恢复页面
  595. uni.navigateTo({
  596. url: `/pages/order/detail/resumeIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  597. })
  598. } else if(orderItem.workOrderStatus == 'return') {
  599. // 跳转到退回页面
  600. uni.navigateTo({
  601. url: `/pages/order/detail/returnIndex?id=${orderItem.id}&orderType=${orderItem.orderType}`
  602. })
  603. } else {
  604. uni.navigateTo({
  605. url: `/pages/order/detail/index?id=${orderItem.id}&orderType=${orderItem.orderType}`
  606. })
  607. }
  608. }
  609. // 点击列表项
  610. const handleView = (item: any | null): void => {
  611. if (item == null) return
  612. const orderItem = item as acceptOrderInfo
  613. uni.navigateTo({
  614. url: `/pages/order/detail/index?id=${orderItem.id}&orderType=${orderItem.orderType}`
  615. })
  616. }
  617. // 查看工单详情
  618. const showWorkOrderDetail = (item: any | null): void => {
  619. if (item == null) return
  620. const orderItem = item as acceptOrderInfo
  621. uni.navigateTo({
  622. url: `/pages/workbench/detail/index?id=${orderItem.id}`
  623. })
  624. }
  625. // 清空搜索
  626. const clearSearch = (): void => {
  627. // 添加防重复调用检查(参考score/pending.uvue的实现)
  628. if (loading.value) {
  629. return;
  630. }
  631. // 添加防重复请求检查
  632. if (isSearching.value) {
  633. return
  634. }
  635. isSearching.value = true
  636. keyword.value = ""
  637. page.value = 1
  638. loadData(true, true) // 状态切换时禁用下拉刷新动画
  639. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  640. setTimeout(() => {
  641. isSearching.value = false
  642. }, 100)
  643. }
  644. // 初始化
  645. // onMounted(() => {
  646. onLoad((options: any) => {
  647. // 检查权限设置tabbar
  648. tabbar[2] = checkPermi(['gxt:app:worktime']) ? 1 : 0
  649. tabbar[3] = checkPermi(['gxt:app:score']) ? 1 : 0
  650. const userInfo = getUserInfo()
  651. if (userInfo != null) {
  652. const userIdStr = userInfo['userId'].toString()
  653. userId.value = userIdStr
  654. roles.value = userInfo['roleNames'].toString()
  655. }
  656. const params = options as UTSJSONObject
  657. const isDeal = params['isDeal'] as string | null
  658. if(isDeal != null) {
  659. dealLoad.value = true;
  660. }
  661. loadStatusDictList()
  662. // 自动选中第一个可显示的状态
  663. // currentStatus.value = getFirstVisibleStatus()
  664. currentStatus.value = 'all'
  665. loadData(true, true) // 首次加载时禁用下拉刷新动画
  666. // 监听首页切换状态事件
  667. uni.$on('switchOrderStatus', (status: string) => {
  668. switchStatus(status)
  669. })
  670. // 监听接单成功的事件,刷新列表
  671. uni.$on('refreshOrderList', () => {
  672. page.value = 1
  673. loadData(true, false)
  674. })
  675. })
  676. // 组件卸载前清理事件监听
  677. onBeforeUnmount(() => {
  678. refreshing.value = false
  679. loading.value = false
  680. isRefreshing.value = false
  681. // 移除事件监听
  682. uni.$off('refreshOrderList',{})
  683. uni.$off('switchOrderStatus',{})
  684. })</script>
  685. <style lang="scss">
  686. .list-page {
  687. flex: 1;
  688. background-color: #e8f0f9;
  689. }
  690. .search-bar {
  691. padding: 20rpx 30rpx;
  692. background-color: #d7eafe;
  693. }
  694. .search-box {
  695. flex-direction: row;
  696. align-items: center;
  697. height: 72rpx;
  698. padding: 0 24rpx;
  699. background-color: #f5f5f5;
  700. border-radius: 36rpx;
  701. .search-icon {
  702. width: 32rpx;
  703. height: 32rpx;
  704. margin-right: 12rpx;
  705. }
  706. .search-input {
  707. flex: 1;
  708. font-size: 28rpx;
  709. color: #333333;
  710. }
  711. .clear-icon {
  712. margin-left: 12rpx;
  713. font-size: 28rpx;
  714. color: #999999;
  715. }
  716. }
  717. .status-bar{
  718. padding-bottom: 10px;
  719. padding-left:30rpx;
  720. background-color: #d7eafe;
  721. }
  722. .status-box {
  723. flex-direction: row;
  724. align-items: center;
  725. height: 72rpx;
  726. flex: 1;
  727. .status-txt{
  728. padding: 8px 12px;
  729. text-align: center;
  730. margin-right: 12rpx;
  731. border-radius: 36rpx;
  732. background-color: #fff;
  733. font-size: 28rpx;
  734. // cursor: pointer;
  735. }
  736. .stauts-sel{
  737. background-color: #007AFF;
  738. color: #fff;
  739. }
  740. }
  741. .list-item {
  742. margin: 24rpx 30rpx;
  743. background-color: #ffffff;
  744. border-radius: 16rpx;
  745. }
  746. .item-container {
  747. padding: 30rpx;
  748. }
  749. .item-address {
  750. font-size: 26rpx;
  751. color: #999999;
  752. margin-bottom: 20rpx;
  753. line-height: 40rpx;
  754. }
  755. .btn-primary {
  756. z-index: 999;
  757. border-radius: 10rpx;
  758. font-size: 24rpx;
  759. // white-space: nowrap;
  760. margin-left: 20rpx;
  761. background-color: #165DFF;
  762. line-height: 45rpx;
  763. color: #ffffff;
  764. .btn-text{
  765. color: #ffffff;
  766. font-size: 24rpx;
  767. padding: 5px 15px;
  768. }
  769. }
  770. .item-header {
  771. flex-direction: row;
  772. align-items: flex-start;
  773. margin-bottom: 16rpx;
  774. justify-content: space-between; /* 主轴两端对齐 */
  775. min-height: 55rpx;
  776. .item-title {
  777. font-size: 30rpx;
  778. color: #333333;
  779. font-weight: bold;
  780. flex-wrap: wrap;
  781. flex: 0 1 75%;
  782. min-width: 0;
  783. }
  784. .info-value {
  785. font-size: 28rpx;
  786. color: #999999;
  787. margin-left: auto;
  788. flex: 0 0 auto;
  789. white-space: nowrap;
  790. }
  791. }
  792. .info-row {
  793. flex-direction: row;
  794. justify-content: space-between;
  795. align-items: center;
  796. .info-label {
  797. font-size: 26rpx;
  798. color: #666;
  799. }
  800. .info-value {
  801. font-size: 26rpx;
  802. // color: #666;
  803. }
  804. }
  805. .text-gray{
  806. font-size: 26rpx;
  807. color: #666;
  808. }
  809. .status-view {
  810. border: 1rpx solid;
  811. border-radius: 20rpx;
  812. }
  813. .status-tag {
  814. padding: 8rpx 20rpx;
  815. border-radius: 20rpx;
  816. font-size: 24rpx;
  817. white-space: nowrap;
  818. // margin-left: 50rpx;
  819. // justify-content: flex-end;
  820. border: 1rpx solid;
  821. }
  822. /* 待接单 */
  823. .status-assigned {
  824. background-color: #ebf5ff;
  825. color: #409eff;
  826. border-color: #d8ebff;
  827. }
  828. /* 待结单 */
  829. .status-to_finish {
  830. background-color: #fff7e6;
  831. color: #fa8c16;
  832. border-color: #ffd591;
  833. }
  834. /* 待审批 */
  835. .status-to_approve {
  836. background-color: #fff7e6;
  837. color: #fa8c16;
  838. border-color: #ffd591;
  839. }
  840. /* 已挂起 */
  841. .status-suspended {
  842. background-color: #fff2f0;
  843. color: #ff4d4f;
  844. border-color: #ffccc7;
  845. }
  846. /* 自动挂起 */
  847. .status-auto_suspend {
  848. background-color: #fff2f0;
  849. color: #ff4d4f;
  850. border-color: #ffccc7;
  851. }
  852. /* 已完成 */
  853. .status-completed {
  854. background-color: #f0f9eb;
  855. color: #5cb87a;
  856. border-color: #c2e7b0;
  857. }
  858. /* 待下发 */
  859. .status-to_issue {
  860. background-color: #f4f4f5;
  861. color: #909399;
  862. border-color: #e9e9eb;
  863. }
  864. /* 已归档 */
  865. .status-archived {
  866. background-color: #f0f9eb;
  867. color: #5cb87a;
  868. border-color: #c2e7b0;
  869. }
  870. /* 退回 */
  871. .status-return {
  872. background-color: #fff2f0;
  873. color: #ff4d4f;
  874. border-color: #ffccc7;
  875. }
  876. /* 退回 */
  877. .status-accept_return {
  878. background-color: #fff2f0;
  879. color: #ff4d4f;
  880. border-color: #ffccc7;
  881. }
  882. /* 作废 */
  883. .status-invalid {
  884. background-color: #fff2f0;
  885. color: #ff4d4f;
  886. border-color: #ffccc7;
  887. }
  888. .scroll-view_H {
  889. width: 100%;
  890. flex-direction: row;
  891. }
  892. .scroll-view-item_H {
  893. justify-content: center;
  894. align-items: center;
  895. .status-txt{
  896. padding: 8px 12px;
  897. text-align: center;
  898. margin-right: 12rpx;
  899. border-radius: 36rpx;
  900. background-color: #fff;
  901. font-size: 28rpx;
  902. }
  903. .stauts-sel{
  904. background-color: #007AFF;
  905. color: #fff;
  906. }
  907. }
  908. .btn-group {
  909. flex-direction: row;
  910. align-items: center;
  911. justify-content: flex-end;
  912. margin-top: 20rpx;
  913. }
  914. </style>