index.uvue 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  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
  9. class="search-input"
  10. placeholder="搜索工单编码、风机编号"
  11. v-model="searchKeyword"
  12. @confirm="handleSearch"
  13. @blur="handleSearch"
  14. />
  15. <text v-if="searchKeyword.length > 0" class="clear-icon" @click="clearSearch">✕</text>
  16. </view>
  17. </view>
  18. <!-- 工单评分状态筛选 -->
  19. <view class="status-bar">
  20. <view class="status-box">
  21. <text
  22. class="status-txt"
  23. :class="{ 'stauts-sel': statusFilter === '' }"
  24. @click="filterByStatus('')"
  25. >
  26. 全部
  27. </text>
  28. <text
  29. class="status-txt"
  30. :class="{ 'stauts-sel': statusFilter === 'to_self' }"
  31. @click="filterByStatus('to_self')"
  32. >
  33. 待自评
  34. </text>
  35. <text
  36. class="status-txt"
  37. :class="{ 'stauts-sel': statusFilter === 'to_re' }"
  38. @click="filterByStatus('to_re')"
  39. >
  40. 待复评
  41. </text>
  42. <text
  43. class="status-txt"
  44. :class="{ 'stauts-sel': statusFilter === 'to_confirm' }"
  45. @click="filterByStatus('to_confirm')"
  46. >
  47. 待确认
  48. </text>
  49. <text
  50. class="status-txt"
  51. :class="{ 'stauts-sel': statusFilter === 'to_final' }"
  52. @click="filterByStatus('to_final')"
  53. >
  54. 待终评
  55. </text>
  56. </view>
  57. </view>
  58. <!-- 工分统计 -->
  59. <view class="stats-section">
  60. <view class="stats-header">
  61. <text class="stats-title">{{ monthTitle }}月度工分</text>
  62. <view class="month-filters">
  63. <text
  64. class="month-filter"
  65. :class="{ 'month-filter-sel': selectedMonth === 'prev' }"
  66. @click="changeMonth('prev')"
  67. >
  68. 上月
  69. </text>
  70. <text
  71. class="month-filter"
  72. :class="{ 'month-filter-sel': selectedMonth === 'current' }"
  73. @click="changeMonth('current')"
  74. >
  75. 本月
  76. </text>
  77. <text
  78. class="month-filter"
  79. :class="{ 'month-filter-sel': selectedMonth === 'custom' }"
  80. @click="showCustomDatePicker"
  81. >
  82. 自定义
  83. </text>
  84. </view>
  85. </view>
  86. <!-- 统计数据 -->
  87. <view class="stats-content">
  88. <view class="total-score">
  89. <text class="score-value">{{ totalScore }}分</text>
  90. <text class="score-label">{{ monthTitle }}总工分</text>
  91. </view>
  92. <view class="score-breakdown">
  93. <view class="breakdown-item">
  94. <text class="breakdown-label">维保工分</text>
  95. <text class="breakdown-value">{{ maintenanceScore }}分</text>
  96. </view>
  97. <view class="breakdown-item">
  98. <text class="breakdown-label">维修工分</text>
  99. <text class="breakdown-value">{{ repairScore }}分</text>
  100. </view>
  101. <view v-if="rank !== null && totalRankingUsers !== null" class="breakdown-item">
  102. <text class="breakdown-label">排名</text>
  103. <text class="breakdown-value">{{ rank }}/{{ totalRankingUsers }}</text>
  104. </view>
  105. </view>
  106. </view>
  107. </view>
  108. <!-- 工单评分列表 -->
  109. <common-list
  110. :dataList="orderList"
  111. :loading="loading"
  112. :refreshing="refreshing"
  113. :hasMore="hasMore"
  114. @refresh="handleRefresh"
  115. @loadMore="loadMore"
  116. @itemClick="handleItemClick"
  117. class="list-with-padding"
  118. >
  119. <template #default="{ item, index }">
  120. <view class="list-item">
  121. <view class="item-container">
  122. <view class="item-header">
  123. <text class="item-title">{{ getPropertyValue(item, 'workOrderProjectNo') }}-风机编号{{ getPropertyValue(item, 'pcsDeviceName') }}的{{ getWorkOrderTypeText(getPropertyValue(item, 'orderType')) }}</text>
  124. <!-- <text class="info-value">{{ getScoringStatus(item) }}</text> -->
  125. <text class="status-tag" :class="getStatusClass(item)">{{ getScoringStatus(item) }}</text>
  126. </view>
  127. <view class="info-row">
  128. <view class="info-label">
  129. <text class="text-gray">{{ getWorkOrderTypeInfo(item) }}</text>
  130. </view>
  131. <view class="info-value-row">
  132. <text v-if="getPropertyValue(item, 'score') !== ''" class="score-text">{{ formatNumber(parseFloat(getPropertyValue(item, 'score'))) }}</text>
  133. <!-- <text class="status-text">{{ getOrderStatusText(getPropertyValue(item, 'scoreStatus')) }}</text> -->
  134. </view>
  135. </view>
  136. <view class="info-row">
  137. <view class="info-label">
  138. <text class="text-gray">工作结束时间: {{ formatDate(getPropertyValue(item, 'realEndTime')) }}</text>
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. </template>
  144. </common-list>
  145. <!-- 自定义时间选择弹窗 -->
  146. <l-popup v-model="showDatePickerPopup" position="bottom">
  147. <view class="date-picker-popup">
  148. <view class="popup-header">
  149. <text class="popup-title">选择年月</text>
  150. <view class="popup-actions">
  151. <text class="cancel-btn" @click="closeDatePicker">取消</text>
  152. </view>
  153. </view>
  154. <view class="date-picker-container">
  155. <view class="date-picker-item">
  156. <!-- <text class="date-label">选择年月</text> -->
  157. <view class="date-display" @click="openDatePicker">
  158. {{ customDate != null && customDate != '' ? customDate : '请选择年月' }}
  159. </view>
  160. </view>
  161. </view>
  162. </view>
  163. </l-popup>
  164. <!-- Date Picker -->
  165. <l-popup v-model="showDatePicker" position="bottom" :z-index="1000">
  166. <l-date-time-picker
  167. :mode="3"
  168. format="YYYY-MM"
  169. :modelValue="customDate"
  170. confirm-btn="确定"
  171. cancel-btn="取消"
  172. @confirm="onDateConfirm"
  173. @cancel="showDatePicker = false">
  174. </l-date-time-picker>
  175. </l-popup>
  176. <!-- 底部 TabBar -->
  177. <custom-tabbar :current="3" />
  178. </view>
  179. </template>
  180. <script setup lang="uts">
  181. import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
  182. import { listOrderScores, getOrderScoreStatistics, listMobileOrderScores } from '@/api/score/index'
  183. import { getDictDataByType } from '@/api/dict/index'
  184. import type { SysDictData } from '@/types/dict'
  185. // 数据状态
  186. const searchKeyword = ref<string>('')
  187. const statusFilter = ref<string>('')
  188. const selectedMonth = ref<string>('current')
  189. const loading = ref<boolean>(false)
  190. const refreshing = ref<boolean>(false)
  191. const hasMore = ref<boolean>(true)
  192. const orderList = ref<any[]>([])
  193. const currentPage = ref<number>(1)
  194. const totalScore = ref<number>(0)
  195. const maintenanceScore = ref<number>(0)
  196. const repairScore = ref<number>(0)
  197. const rank = ref<number | null>(null)
  198. const totalRankingUsers = ref<number | null>(null)
  199. const customDate = ref<string>('')
  200. // 防止重复请求的标志位
  201. const isSearching = ref<boolean>(false)
  202. // 添加防重复刷新的标志
  203. const isRefreshing = ref<boolean>(false)
  204. // 添加刷新时间戳,用于防抖
  205. const lastRefreshTime = ref<number>(0)
  206. // 添加防抖定时器
  207. let searchTimer: number | null = null
  208. // 弹窗显示状态
  209. const showDatePickerPopup = ref<boolean>(false)
  210. const showDatePicker = ref<boolean>(false)
  211. // 工单状态字典列表
  212. const statusDictList = ref<SysDictData[]>([])
  213. // 维保类型字典列表
  214. const inspectionTypeDictList = ref<SysDictData[]>([])
  215. // 检修类型字典列表
  216. const maintenanceTypeDictList = ref<SysDictData[]>([])
  217. const dictLoaded = ref<boolean>(false)
  218. // 计算属性
  219. const monthTitle = computed(() => {
  220. switch (selectedMonth.value) {
  221. case 'prev':
  222. return '上月'
  223. case 'current':
  224. return '本月'
  225. case 'custom':
  226. // 显示自定义选择的年月
  227. return (customDate.value != null && customDate.value != '') ? customDate.value : '自定义'
  228. default:
  229. return '本月'
  230. }
  231. })
  232. // 打开自定义日期选择弹窗
  233. function showCustomDatePicker() {
  234. showDatePickerPopup.value = true
  235. }
  236. function closeDatePicker() {
  237. showDatePicker.value = false
  238. showDatePickerPopup.value = false
  239. }
  240. // 打开日期选择器
  241. function openDatePicker() {
  242. showDatePicker.value = true
  243. }
  244. // 获取工单评分状态字典列表
  245. const loadStatusDictList = async (): Promise<void> => {
  246. try {
  247. const result = await getDictDataByType('gxt_scoring_status')
  248. const resultObj = result as UTSJSONObject
  249. if (resultObj['code'] == 200) {
  250. const data = resultObj['data'] as any[]
  251. const dictData: SysDictData[] = []
  252. if (data != null && data.length > 0) {
  253. for (let i = 0; i < data.length; i++) {
  254. const item = data[i] as UTSJSONObject
  255. // 只提取需要的字段
  256. const dictItem: SysDictData = {
  257. dictValue: item['dictValue'] as string | null,
  258. dictLabel: item['dictLabel'] as string | null,
  259. dictCode: null,
  260. dictSort: null,
  261. dictType: null,
  262. cssClass: null,
  263. listClass: null,
  264. isDefault: null,
  265. status: null,
  266. default: null,
  267. createTime: null,
  268. remark: null
  269. }
  270. dictData.push(dictItem)
  271. }
  272. }
  273. statusDictList.value = dictData
  274. }
  275. } catch (e: any) {
  276. console.error('获取工单评分状态字典失败:', e.message)
  277. }
  278. }
  279. // 获取维保类型字典列表
  280. const loadInspectionTypeDictList = async (): Promise<void> => {
  281. try {
  282. const result = await getDictDataByType('gxt_inspection_type')
  283. const resultObj = result as UTSJSONObject
  284. if (resultObj['code'] == 200) {
  285. const data = resultObj['data'] as any[]
  286. const dictData: SysDictData[] = []
  287. if (data != null && data.length > 0) {
  288. for (let i = 0; i < data.length; i++) {
  289. const item = data[i] as UTSJSONObject
  290. // 只提取需要的字段
  291. const dictItem: SysDictData = {
  292. dictValue: item['dictValue'] as string | null,
  293. dictLabel: item['dictLabel'] as string | null,
  294. dictCode: null,
  295. dictSort: null,
  296. dictType: null,
  297. cssClass: null,
  298. listClass: null,
  299. isDefault: null,
  300. status: null,
  301. default: null,
  302. createTime: null,
  303. remark: null
  304. }
  305. dictData.push(dictItem)
  306. }
  307. }
  308. inspectionTypeDictList.value = dictData
  309. }
  310. } catch (e: any) {
  311. console.error('获取维保类型字典失败:', e.message)
  312. }
  313. }
  314. // 获取检修类型字典列表
  315. const loadMaintenanceTypeDictList = async (): Promise<void> => {
  316. try {
  317. const result = await getDictDataByType('gxt_maintenance_type')
  318. const resultObj = result as UTSJSONObject
  319. if (resultObj['code'] == 200) {
  320. const data = resultObj['data'] as any[]
  321. const dictData: SysDictData[] = []
  322. if (data != null && data.length > 0) {
  323. for (let i = 0; i < data.length; i++) {
  324. const item = data[i] as UTSJSONObject
  325. // 只提取需要的字段
  326. const dictItem: SysDictData = {
  327. dictValue: item['dictValue'] as string | null,
  328. dictLabel: item['dictLabel'] as string | null,
  329. dictCode: null,
  330. dictSort: null,
  331. dictType: null,
  332. cssClass: null,
  333. listClass: null,
  334. isDefault: null,
  335. status: null,
  336. default: null,
  337. createTime: null,
  338. remark: null
  339. }
  340. dictData.push(dictItem)
  341. }
  342. }
  343. maintenanceTypeDictList.value = dictData
  344. }
  345. } catch (e: any) {
  346. console.error('获取检修类型字典失败:', e.message)
  347. }
  348. }
  349. // Helper function to safely extract properties from item
  350. function getPropertyValue(item: any | null, propertyName: string): string {
  351. if (item == null) return ''
  352. const itemObj = item as UTSJSONObject
  353. const value = itemObj[propertyName]
  354. return value != null ? '' + value : ''
  355. }
  356. // 获取工单类型文本
  357. function getWorkOrderTypeText(orderType: string | null): string {
  358. if (orderType != null) {
  359. if (orderType == "1") {
  360. return "维修工单"
  361. } else if (orderType == "2") {
  362. return "维保工单"
  363. }
  364. }
  365. return ""
  366. }
  367. // 获取工单类型相关信息(维保类型或检修类型)
  368. function getWorkOrderTypeInfo(item: any | null): string {
  369. if (item == null) return ''
  370. const orderType = getPropertyValue(item, 'orderType')
  371. // 维保工单显示维保类型
  372. if (orderType == "2") {
  373. const inspectionType = getPropertyValue(item, 'inspectionType')
  374. if (inspectionType != null && inspectionType != '') {
  375. // 如果字典尚未加载,返回原始值
  376. if (inspectionTypeDictList.value.length == 0) {
  377. return inspectionType
  378. }
  379. // 查找字典中对应的标签
  380. const dictItem = inspectionTypeDictList.value.find(dict => dict.dictValue == inspectionType)
  381. return (dictItem != null ? dictItem.dictLabel : inspectionType) as string
  382. }
  383. }
  384. // 维修工单显示检修类型
  385. else if (orderType == "1") {
  386. const maintenanceType = getPropertyValue(item, 'maintenanceType')
  387. if (maintenanceType != null && maintenanceType != '') {
  388. // 如果字典尚未加载,返回原始值
  389. if (maintenanceTypeDictList.value.length == 0) {
  390. return maintenanceType
  391. }
  392. // 查找字典中对应的标签
  393. const dictItem = maintenanceTypeDictList.value.find(dict => dict.dictValue == maintenanceType)
  394. return (dictItem != null ? dictItem.dictLabel : maintenanceType) as string
  395. }
  396. }
  397. return ""
  398. }
  399. // 获取工单状态文本
  400. function getScoringStatus(item: any | null): string | null {
  401. if (item == null) return ''
  402. const rawStatus = getPropertyValue(item, 'scoringStatus')
  403. if (rawStatus == null || rawStatus == '') return ''
  404. // 如果字典尚未加载,返回原始值
  405. if (!dictLoaded.value) {
  406. return rawStatus
  407. }
  408. // 查找字典中对应的标签
  409. const dictItem = statusDictList.value.find(dict => dict.dictValue == rawStatus)
  410. return dictItem != null ? dictItem.dictLabel : rawStatus
  411. }
  412. // 获取工单评分状态文本
  413. function getOrderStatusText(status: string | null): string {
  414. const statusMap: UTSJSONObject = {
  415. '1': '待自评',
  416. '2': '待复评',
  417. '3': '待确认',
  418. '4': '待终评'
  419. }
  420. if (status == null) return ''
  421. const result = statusMap[status]
  422. return result != null ? result as string : '未知状态'
  423. }
  424. // 获取统计数据
  425. function getStatistics() {
  426. // Convert 'current' and 'prev' values to actual date strings
  427. let monthValue = '';
  428. if (selectedMonth.value === 'custom') {
  429. monthValue = customDate.value;
  430. } else if (selectedMonth.value === 'current') {
  431. const now = new Date();
  432. monthValue = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`;
  433. } else if (selectedMonth.value === 'prev') {
  434. const now = new Date();
  435. const prevMonth = now.getMonth(); // getMonth() returns 0-11
  436. const prevYear = prevMonth === 0 ? now.getFullYear() - 1 : now.getFullYear();
  437. const monthStr = prevMonth === 0 ? '12' : prevMonth.toString().padStart(2, '0');
  438. monthValue = `${prevYear}-${monthStr}`;
  439. }
  440. const params: UTSJSONObject = {
  441. //scoringStatus: statusFilter.value,
  442. month: monthValue
  443. }
  444. getOrderScoreStatistics(params).then((response: any) => {
  445. const resultObj = response as UTSJSONObject
  446. const responseData = resultObj['data'] as UTSJSONObject
  447. if (responseData != null) {
  448. totalScore.value = (responseData['totalScore'] != null) ? parseFloat((responseData['totalScore'] as number).toFixed(2)) : 0
  449. maintenanceScore.value = (responseData['maintenanceScore'] != null) ? parseFloat((responseData['maintenanceScore'] as number).toFixed(2)) : 0
  450. repairScore.value = (responseData['repairScore'] != null) ? parseFloat((responseData['repairScore'] as number).toFixed(2)) : 0
  451. rank.value = (responseData['rank'] != null) ? responseData['rank'] as number : null
  452. totalRankingUsers.value = (responseData['totalRankingUsers'] != null) ? responseData['totalRankingUsers'] as number : null
  453. } else {
  454. totalScore.value = 0
  455. maintenanceScore.value = 0
  456. repairScore.value = 0
  457. rank.value = null
  458. totalRankingUsers.value = null
  459. }
  460. })
  461. }
  462. function onDateConfirm(value: string) {
  463. customDate.value = value
  464. showDatePicker.value = false
  465. showDatePickerPopup.value = false
  466. selectedMonth.value = 'custom'
  467. // 添加防重复调用检查
  468. if (loading.value) {
  469. return;
  470. }
  471. getStatistics()
  472. }
  473. // 方法
  474. function loadData(isRefresh: boolean, disablePullDown = false) {
  475. // 防止重复请求的核心机制 - 参考工单页面的简单有效方式
  476. if (loading.value) {
  477. // 确保刷新状态最终被重置,防止卡死
  478. if (!isRefresh) {
  479. refreshing.value = false;
  480. }
  481. return
  482. }
  483. const shouldRefresh = isRefresh !== false
  484. loading.value = true
  485. if (shouldRefresh && !disablePullDown) {
  486. currentPage.value = 1
  487. refreshing.value = true
  488. } else if (shouldRefresh && disablePullDown) {
  489. // 筛选条件变化时,重置页码但不触发下拉刷新
  490. currentPage.value = 1
  491. // 即使禁用下拉刷新,也要确保刷新状态最终被重置
  492. refreshing.value = false
  493. } else {
  494. // 对于加载更多操作,不需要显示下拉刷新状态
  495. refreshing.value = false;
  496. }
  497. // Convert 'current' and 'prev' values to actual date strings
  498. let monthValue = '';
  499. if (selectedMonth.value === 'custom') {
  500. monthValue = customDate.value;
  501. } else if (selectedMonth.value === 'current') {
  502. const now = new Date();
  503. const m = now.getMonth() + 1
  504. let mStr = m.toString()
  505. if (m < 10) {
  506. mStr = '0' + mStr
  507. }
  508. monthValue = `${now.getFullYear()}-${mStr}`;
  509. } else if (selectedMonth.value === 'prev') {
  510. const now = new Date();
  511. const prevMonth = now.getMonth(); // getMonth() returns 0-11
  512. const prevYear = prevMonth === 0 ? now.getFullYear() - 1 : now.getFullYear();
  513. let monthStr = prevMonth === 0 ? '12' : prevMonth.toString()
  514. if (prevMonth !== 0 && prevMonth < 10) {
  515. monthStr = '0' + monthStr
  516. }
  517. monthValue = `${prevYear}-${monthStr}`;
  518. }
  519. const params: UTSJSONObject = {
  520. pageNum: currentPage.value,
  521. pageSize: 5,
  522. keyword: searchKeyword.value,
  523. scoringStatus: statusFilter.value,
  524. month: monthValue
  525. }
  526. listMobileOrderScores(params).then((response: any) => {
  527. // 提取响应数据
  528. const resultObj = response as UTSJSONObject
  529. console.log('API响应数据:', resultObj)
  530. const responseData = resultObj['rows'] as any[]
  531. const responseTotal = resultObj['total'] as number
  532. if (shouldRefresh) {
  533. orderList.value = Array.isArray(responseData) ? responseData : []
  534. } else {
  535. const currentRows = Array.isArray(responseData) ? responseData : []
  536. orderList.value = [...orderList.value, ...currentRows]
  537. }
  538. hasMore.value = orderList.value.length < responseTotal
  539. }).catch((error) => {
  540. console.error('获取工单列表失败:', error);
  541. // 出错时也需要重置刷新状态
  542. if (shouldRefresh) {
  543. refreshing.value = false;
  544. isRefreshing.value = false;
  545. }
  546. }).finally(() => {
  547. // 无论成功还是失败,都重置所有加载状态
  548. loading.value = false;
  549. // 确保刷新状态最终被重置
  550. if (shouldRefresh) {
  551. refreshing.value = false;
  552. // 使用setTimeout确保状态彻底重置
  553. setTimeout(() => {
  554. isRefreshing.value = false;
  555. }, 50);
  556. }
  557. })
  558. }
  559. function handleSearch() {
  560. // 添加防重复调用检查
  561. if (loading.value) {
  562. return;
  563. }
  564. // 添加防抖和防止重复请求
  565. if (isSearching.value) {
  566. return
  567. }
  568. const timer = searchTimer
  569. if (timer != null) {
  570. clearTimeout(timer)
  571. searchTimer = null
  572. }
  573. searchTimer = setTimeout(() => {
  574. isSearching.value = true
  575. loadData(true, true); // 添加true参数表示这是筛选条件变化触发的加载,应禁用下拉刷新
  576. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  577. setTimeout(() => {
  578. isSearching.value = false
  579. }, 100)
  580. }, 300)
  581. }
  582. function clearSearch() {
  583. // 添加防重复调用检查
  584. if (loading.value) {
  585. return;
  586. }
  587. // 添加防抖和防止重复请求
  588. if (isSearching.value) {
  589. return
  590. }
  591. const timer = searchTimer
  592. if (timer != null) {
  593. clearTimeout(timer)
  594. searchTimer = null
  595. }
  596. searchKeyword.value = ""
  597. searchTimer = setTimeout(() => {
  598. isSearching.value = true
  599. loadData(true, true); // 添加true参数表示这是筛选条件变化触发的加载,应禁用下拉刷新
  600. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  601. setTimeout(() => {
  602. isSearching.value = false
  603. }, 100)
  604. }, 300)
  605. }
  606. function filterByStatus(status: string) {
  607. // 添加防重复调用检查
  608. if (loading.value) {
  609. return;
  610. }
  611. // 添加防止重复请求
  612. if (isSearching.value) {
  613. return
  614. }
  615. const timer = searchTimer
  616. if (timer != null) {
  617. clearTimeout(timer)
  618. searchTimer = null
  619. }
  620. statusFilter.value = status
  621. searchTimer = setTimeout(() => {
  622. isSearching.value = true
  623. loadData(true, true); // 添加true参数表示这是筛选条件变化触发的加载,应禁用下拉刷新
  624. getStatistics()
  625. // 延迟重置标志位,确保请求发送后才允许下一次搜索
  626. setTimeout(() => {
  627. isSearching.value = false
  628. }, 100)
  629. }, 300)
  630. }
  631. function changeMonth(month: string) {
  632. // 添加防重复调用检查
  633. if (loading.value) {
  634. return;
  635. }
  636. // 添加防止重复请求
  637. if (isSearching.value) {
  638. return
  639. }
  640. const timer = searchTimer
  641. if (timer != null) {
  642. clearTimeout(timer)
  643. searchTimer = null
  644. }
  645. selectedMonth.value = month
  646. searchTimer = setTimeout(() => {
  647. getStatistics()
  648. }, 300)
  649. }
  650. function loadMore() {
  651. console.log("loadMore被触发")
  652. if (!hasMore.value || loading.value) return
  653. currentPage.value++
  654. loadData(false)
  655. }
  656. function handleRefresh() {
  657. console.log("handleRefresh被触发")
  658. console.log("loading.value===",loading.value)
  659. console.log("isRefreshing.value===",isRefreshing.value)
  660. // 防抖处理,避免频繁触发
  661. const now = Date.now();
  662. if (now - lastRefreshTime.value < 1000) {
  663. console.log("刷新操作过于频繁,忽略本次触发");
  664. refreshing.value = false;
  665. return;
  666. }
  667. lastRefreshTime.value = now;
  668. // 添加防重复调用检查
  669. if (loading.value || isRefreshing.value) {
  670. // 如果已经在加载或正在刷新,直接重置刷新状态
  671. refreshing.value = false;
  672. return;
  673. }
  674. console.log("loading.value1===",loading.value)
  675. console.log("isRefreshing.value1===",isRefreshing.value)
  676. // 设置刷新标志
  677. isRefreshing.value = true;
  678. loadData(true); // 使用默认的下拉刷新行为
  679. // 确保在一定时间后重置刷新标志,防止意外情况
  680. setTimeout(() => {
  681. isRefreshing.value = false
  682. }, 100) // 延迟重置,确保状态完全更新
  683. }
  684. // 点击列表项
  685. function handleItemClick(item: any | null, index: number): void {
  686. if (item == null) return
  687. const itemObj = item as UTSJSONObject
  688. const id = itemObj.get('id')
  689. const orderType = itemObj.get('orderType')
  690. if (id != null && orderType != null) {
  691. // 转换为字符串
  692. const idStr = '' + id
  693. const orderTypeStr = '' + orderType
  694. uni.navigateTo({
  695. url: `/pages/score/detail/index?id=${idStr}&orderType=${orderTypeStr}`
  696. })
  697. }
  698. }
  699. function formatNumber(value: number | null) {
  700. if (value === null) return '0.0'
  701. return value.toFixed(2)
  702. }
  703. // 获取工单评分状态样式类
  704. function getStatusClass(item: any | null): string {
  705. if (item == null) return ''
  706. const rawStatus = getPropertyValue(item, 'scoringStatus')
  707. if (rawStatus == null || rawStatus == '') return ''
  708. return `status-${rawStatus}`
  709. }
  710. function formatDate(dateString: string) {
  711. if (dateString == '' || dateString == null) return ''
  712. const date = new Date(dateString)
  713. const year = date.getFullYear()
  714. const month = (date.getMonth() + 1).toString().padStart(2, '0')
  715. const day = date.getDate().toString().padStart(2, '0')
  716. const hours = date.getHours().toString().padStart(2, '0')
  717. const minutes = date.getMinutes().toString().padStart(2, '0')
  718. return `${year}-${month}-${day} ${hours}:${minutes}`
  719. }
  720. // 生命周期
  721. onMounted(() => {
  722. loadStatusDictList()
  723. loadInspectionTypeDictList()
  724. loadMaintenanceTypeDictList()
  725. loadData(false)
  726. getStatistics()
  727. dictLoaded.value = true
  728. })
  729. // 组件销毁时清除定时器
  730. onBeforeUnmount(() => {
  731. const timer = searchTimer
  732. if (timer != null) {
  733. clearTimeout(timer)
  734. searchTimer = null
  735. }
  736. // 如果组件销毁前还有未完成的刷新操作,确保状态被重置
  737. loading.value = false;
  738. refreshing.value = false;
  739. isRefreshing.value = false;
  740. })
  741. </script>
  742. <style lang="scss">
  743. .list-page {
  744. flex: 1;
  745. background-color: #e8f0f9;
  746. padding-bottom: 150rpx; // 为底部 TabBar 留出空间
  747. }
  748. /* 搜索栏样式 */
  749. .search-bar {
  750. padding: 20rpx 30rpx;
  751. background-color: #d7eafe;
  752. }
  753. .search-box {
  754. flex-direction: row;
  755. align-items: center;
  756. height: 72rpx;
  757. padding: 0 24rpx;
  758. background-color: #f5f5f5;
  759. border-radius: 36rpx;
  760. .search-icon {
  761. width: 32rpx;
  762. height: 32rpx;
  763. margin-right: 12rpx;
  764. }
  765. .search-input {
  766. flex: 1;
  767. font-size: 28rpx;
  768. color: #333333;
  769. }
  770. .clear-icon {
  771. margin-left: 12rpx;
  772. font-size: 28rpx;
  773. color: #999999;
  774. }
  775. }
  776. /* 工单状态筛选 */
  777. .status-bar {
  778. padding-bottom: 10px;
  779. padding-left: 30rpx;
  780. background-color: #d7eafe;
  781. }
  782. .status-box {
  783. flex-direction: row;
  784. align-items: center;
  785. height: 72rpx;
  786. flex: 1;
  787. .status-txt {
  788. padding: 8px 12px;
  789. text-align: center;
  790. margin-right: 12rpx;
  791. border-radius: 36rpx;
  792. background-color: #fff;
  793. font-size: 28rpx;
  794. }
  795. .stauts-sel {
  796. background-color: #007AFF;
  797. color: #fff;
  798. }
  799. }
  800. /* 工分统计 */
  801. .stats-section {
  802. margin: 15rpx 30rpx;
  803. background-color: #ffffff;
  804. border-radius: 16rpx;
  805. padding: 20rpx;
  806. }
  807. .stats-header {
  808. flex-direction: row;
  809. justify-content: space-between;
  810. align-items: center;
  811. margin-bottom: 20rpx;
  812. }
  813. .stats-title {
  814. font-size: 32rpx;
  815. font-weight: bold;
  816. flex: 1;
  817. }
  818. .month-filters {
  819. flex-direction: row;
  820. justify-content: flex-end;
  821. }
  822. .month-filter {
  823. padding: 6rpx 14rpx;
  824. margin-left: 12rpx;
  825. font-size: 24rpx;
  826. border-radius: 24rpx;
  827. background-color: #f2f3f5;
  828. color: #666;
  829. white-space: nowrap;
  830. }
  831. .month-filter-sel {
  832. background-color: #165dff;
  833. color: white;
  834. }
  835. .stats-content {
  836. margin-top: 20rpx;
  837. flex-direction: column;
  838. }
  839. .total-score {
  840. margin-bottom: 30rpx;
  841. flex-direction: column;
  842. align-items: center;
  843. }
  844. .score-label {
  845. font-size: 28rpx;
  846. color: #666;
  847. display: flex;
  848. margin-bottom: 8rpx;
  849. }
  850. .score-value {
  851. display: flex;
  852. font-size: 48rpx;
  853. font-weight: bold;
  854. color: #165dff;
  855. line-height: 1.2;
  856. }
  857. .score-breakdown {
  858. flex-direction: row;
  859. justify-content: space-between;
  860. }
  861. .breakdown-item {
  862. flex: 1;
  863. flex-direction: column;
  864. align-items: center;
  865. }
  866. .breakdown-label {
  867. display: flex;
  868. font-size: 32rpx;
  869. font-weight: bold;
  870. color: #333;
  871. line-height: 1.4;
  872. }
  873. .breakdown-value {
  874. font-size: 28rpx;
  875. color: #666;
  876. display: flex;
  877. margin-bottom: 8rpx;
  878. }
  879. /* 列表项样式 */
  880. .list-item {
  881. margin: 12rpx 30rpx;
  882. background-color: #ffffff;
  883. border-radius: 16rpx;
  884. }
  885. .item-container {
  886. padding: 30rpx;
  887. }
  888. .item-header {
  889. flex-direction: row;
  890. align-items: flex-start;
  891. margin-bottom: 16rpx;
  892. .item-title {
  893. font-size: 30rpx;
  894. color: #333333;
  895. font-weight: bold;
  896. flex-wrap: wrap;
  897. flex: 0 1 70%;
  898. min-width: 0;
  899. }
  900. .info-value {
  901. font-size: 28rpx;
  902. color: #999999;
  903. margin-left: auto;
  904. flex: 0 0 auto;
  905. white-space: nowrap;
  906. }
  907. }
  908. .info-row {
  909. flex-direction: row;
  910. justify-content: space-between;
  911. align-items: center;
  912. .info-label {
  913. font-size: 26rpx;
  914. color: #666;
  915. }
  916. .info-value-row {
  917. flex-direction: row;
  918. align-items: center;
  919. }
  920. .score-text {
  921. font-size: 28rpx;
  922. color: #ff9900;
  923. font-weight: bold;
  924. margin-right: 20rpx;
  925. }
  926. .status-text {
  927. font-size: 24rpx;
  928. padding: 4rpx 10rpx;
  929. border-radius: 24rpx;
  930. background-color: #f2f3f5;
  931. color: #666;
  932. }
  933. }
  934. .text-gray{
  935. font-size: 26rpx;
  936. color: #666;
  937. }
  938. /* 日期选择弹窗 */
  939. .date-picker-popup {
  940. background-color: white;
  941. border-top-left-radius: 30rpx;
  942. border-top-right-radius: 30rpx;
  943. padding: 40rpx;
  944. padding-bottom: 40rpx;
  945. margin-bottom: 150rpx; /* 提高弹窗,避免被底部导航栏遮挡 */
  946. }
  947. .popup-header {
  948. display: flex;
  949. justify-content: space-between;
  950. align-items: center;
  951. margin-bottom: 40rpx;
  952. }
  953. .popup-title {
  954. font-size: 34rpx;
  955. font-weight: bold;
  956. }
  957. .popup-actions {
  958. display: flex;
  959. flex-direction: row;
  960. }
  961. .cancel-btn {
  962. color: #999;
  963. padding: 10rpx 20rpx;
  964. }
  965. .confirm-btn {
  966. color: #165dff;
  967. padding: 10rpx 20rpx;
  968. font-weight: bold;
  969. }
  970. .date-picker-container {
  971. padding: 20rpx 0;
  972. padding-bottom: env(safe-area-inset-bottom); // 适配安全区域
  973. }
  974. .date-picker-item {
  975. margin-bottom: 40rpx;
  976. }
  977. .date-label {
  978. display: flex;
  979. margin-bottom: 20rpx;
  980. font-size: 30rpx;
  981. color: #333;
  982. }
  983. .date-display {
  984. width: 100%;
  985. padding: 20rpx;
  986. border: 2rpx solid #ddd;
  987. border-radius: 8rpx;
  988. font-size: 32rpx;
  989. color: #333;
  990. }
  991. // 添加底部填充的类
  992. .list-with-padding {
  993. padding-bottom: 40rpx;
  994. }
  995. /* 工单评分状态标签样式 */
  996. .status-tag {
  997. padding: 8rpx 20rpx;
  998. border-radius: 20rpx;
  999. font-size: 24rpx;
  1000. white-space: nowrap;
  1001. margin-left: 70rpx;
  1002. border: 1rpx solid;
  1003. }
  1004. /* 待自评 */
  1005. .status-to_self {
  1006. background-color: #fff7e6;
  1007. color: #fa8c16;
  1008. border-color: #ffd591;
  1009. }
  1010. /* 待复评 */
  1011. .status-to_re {
  1012. background-color: #ebf5ff;
  1013. color: #409eff;
  1014. border-color: #d8ebff;
  1015. }
  1016. /* 待确认 */
  1017. .status-to_confirm {
  1018. background-color: #f0f9eb;
  1019. color: #5cb87a;
  1020. border-color: #c2e7b0;
  1021. }
  1022. /* 待终评 */
  1023. .status-to_final {
  1024. background-color: #ebf5ff;
  1025. color: #409eff;
  1026. border-color: #d8ebff;
  1027. }
  1028. /* 待归档 */
  1029. .status-to_archive {
  1030. background-color: #f0f9eb;
  1031. color: #5cb87a;
  1032. border-color: #c2e7b0;
  1033. }
  1034. /* 已归档 */
  1035. .status-archived {
  1036. background-color: #f0f9eb;
  1037. color: #5cb87a;
  1038. border-color: #c2e7b0;
  1039. }
  1040. </style>