index.vue 96 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459
  1. <template>
  2. <el-dialog v-model="visible" :title="isView ? '查看' : isCollect ? '领取' : '归还'" width="75%" :close-on-click-modal="false" @close="onCancel">
  3. <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
  4. <el-tab-pane label="借用单信息" name="first">
  5. <el-form :model="form" label-width="110px" label-position="top" :rules="rules" ref="formRef">
  6. <el-row :gutter="16">
  7. <el-col :span="8">
  8. <el-form-item label="借用单编号" prop="application_no">
  9. <el-input v-model="form.application_no" placeholder="自动生成" disabled />
  10. </el-form-item>
  11. </el-col>
  12. <el-col :span="8">
  13. <el-form-item label="借用人类型" prop="borrower_type">
  14. <el-select v-model="form.borrower_type" :placeholder="isView?'':'请选择借用人角色'" :disabled="true">
  15. <el-option label="管理员用户" :value=0 />
  16. <el-option label="应用用户" :value=1 />
  17. <el-option label="外部用户" :value=2 />
  18. </el-select>
  19. </el-form-item>
  20. </el-col>
  21. <el-col :span="8" >
  22. <!-- <el-form-item label="借用人" prop="external_borrower_name" v-if="form.borrower_type !== 2">
  23. <el-input v-model="form.borrower_info.name" placeholder="请输入借用人" :disabled="true" />
  24. </el-form-item> -->
  25. <el-form-item label="借用人" prop="admin_borrower" v-if="form.borrower_type == 0">
  26. <el-select v-model="form.admin_borrower" :placeholder="isView?'':'请选择借用人'" :disabled="true" >
  27. <el-option :label="adminList.name" :value="adminList.id" />
  28. </el-select>
  29. </el-form-item>
  30. <el-form-item label="借用人" prop="app_user_borrower" v-if="form.borrower_type == 1">
  31. <el-select v-model="form.app_user_borrower.id" :placeholder="isView?'':'请选择借用人'" :disabled="true" >
  32. <el-option v-for="item in allUserList" :key="item.id" :label="item.name" :value="item.id" />
  33. </el-select>
  34. </el-form-item>
  35. <el-form-item label="借用人" prop="external_borrower_name" v-if="form.borrower_type == 2">
  36. <el-input v-model="form.external_borrower_name" :placeholder="isView?'':'请输入借用人'" :disabled="true" />
  37. </el-form-item>
  38. </el-col>
  39. <el-col :span="8" v-if="form.borrower_type !== 2">
  40. <el-form-item label="学号/工号" prop="user_code" >
  41. <!-- {{ form.borrower_info.user_code }} -->
  42. <el-input v-model="form.app_user_borrower.user_code" :placeholder="isView?'':'请输入学号/工号'" :disabled="true"/>
  43. </el-form-item>
  44. </el-col>
  45. <el-col :span="8">
  46. <el-form-item label="联系电话" v-if="form.borrower_type !== 2">
  47. <el-input v-model="form.app_user_borrower.mobile" :placeholder="isView?'':'请输入联系电话'" :disabled="true" />
  48. </el-form-item>
  49. <el-form-item label="联系电话" v-if="form.borrower_type == 2">
  50. <el-input v-model="form.external_borrower_phone" :placeholder="isView?'':'请输入联系电话'" :disabled="true" />
  51. </el-form-item>
  52. </el-col>
  53. <el-col :span="8">
  54. <el-form-item label="借用部门" prop="borrower_dept">
  55. <el-input v-model="form.borrower_dept" placeholder="请输入借用部门" :disabled="true" />
  56. </el-form-item>
  57. </el-col>
  58. <el-col :span="8">
  59. <el-form-item label="预计归还时间" prop="expected_end_time">
  60. <el-date-picker v-model="form.expected_end_time" type="date"
  61. format="YYYY-MM-DD"
  62. value-format="YYYY-MM-DD"
  63. :disabledDate="disabledDate" placeholder="请选择预计归还时间" style="width: 100%" :disabled="true"/>
  64. </el-form-item>
  65. </el-col>
  66. <el-col :span="4">
  67. <el-form-item label="个人/团队" prop="team_type" >
  68. <el-select v-model="form.team_type" placeholder="请选择" :disabled="true">
  69. <el-option label="个人" :value="0" />
  70. <el-option label="团队" :value="1" />
  71. </el-select>
  72. </el-form-item>
  73. </el-col>
  74. <el-col :span="4" v-show="form.team_type===1">
  75. <el-form-item label="团队人数" prop="team_members_count">
  76. <el-input v-model="form.team_members_count" :placeholder="isView?'':'请输入'" :disabled="true"/>
  77. </el-form-item>
  78. </el-col>
  79. <el-col :span="16" v-show="form.team_type===1">
  80. <el-form-item label="团队其他人员名单" prop="team_members">
  81. <el-input v-model="form.team_members" :placeholder="isView?'':'请输入团队其他人员名单'" :disabled="true"/>
  82. </el-form-item>
  83. </el-col>
  84. <el-col :span="24">
  85. <el-form-item label="借用原因" prop="purpose">
  86. <el-input v-model="form.purpose" type="textarea" :placeholder="isView?'':'请输入借用原因'" :disabled="true" />
  87. </el-form-item>
  88. </el-col>
  89. <el-col :span="4">
  90. <!-- ||form.approver_info.name -->
  91. <el-form-item label="审批人" prop="applicant_remark_type">
  92. <el-select v-model="form.approver_info.name" placeholder="请选择" :disabled="true" >
  93. <el-option label="管理员" value="1"/>
  94. </el-select>
  95. </el-form-item>
  96. </el-col>
  97. <el-col :span="20">
  98. <el-form-item label="备注" prop="applicant_remark">
  99. <el-input v-model="form.applicant_remark" :placeholder="isView?'':'请输入备注'" :disabled="true"/>
  100. </el-form-item>
  101. </el-col>
  102. </el-row>
  103. <div style="margin: 16px 0 8px 0; font-weight: bold"> {{ isView ? '借用列表' : (isCollect ? '借用列表' : '归还列表') }}</div>
  104. <el-table :data="form.items.filter(item => item.device === null)" border style="width: 50%; margin-bottom: 12px">
  105. <el-table-column type="index" label="序号" width="60" />
  106. <el-table-column prop="device_category_name" label="设备分类" />
  107. <el-table-column :label="isView ? '数量' : (isCollect ? '借用数量' : '归还数量')">
  108. <template #default="{ row }">
  109. <el-input v-model="row.quantity" type="number" :min="1" prop="quantity" size="small" :disabled="true" />
  110. </template>
  111. </el-table-column>
  112. <el-table-column label="操作" width="80">
  113. <template #default="{ $index }">
  114. <el-button type="text" style="color: red" @click="removeItem($index)" :disabled="true">移除</el-button>
  115. </template>
  116. </el-table-column>
  117. </el-table>
  118. <el-button type="primary" @click="addItem" v-if="!isView" :disabled="true">选择设备分类</el-button>
  119. <div style="margin: 16px 0 8px 0; font-weight: bold">预借配件信息</div>
  120. <el-input v-model="form.accessories" :placeholder="isView?'':'请输入预借配件信息'" :disabled="true"/>
  121. <div style="margin: 16px 0 8px 0; font-weight: bold" >附件</div>
  122. <!-- <el-upload
  123. action="#"
  124. drag
  125. :list-type="'picture'"
  126. accept=".bmp,.gif,.jpeg,.jpg,.png,‌.webp,.svg"
  127. multiple
  128. :show-file-list="true"
  129. :file-list="attachmentFileList"
  130. :on-success="handleUploadSuccess"
  131. :on-error="handleUploadError"
  132. :disabled="true"
  133. >
  134. <i class="el-icon-upload"></i>
  135. <div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div>
  136. </el-upload> -->
  137. <el-upload
  138. :disabled="true"
  139. action="#"
  140. list-type="picture-card"
  141. :on-preview="handlePictureCardPreview"
  142. :on-remove="handleRemoves"
  143. :file-list="attachmentFileList"
  144. >
  145. <el-icon><Plus /></el-icon>
  146. </el-upload>
  147. <div v-if="isView && form.latest_renewal" style="margin-top: 8px;">
  148. <div style="font-weight: bold; margin-bottom: 8px">续借信息</div>
  149. <el-row :gutter="16">
  150. <el-col :span="4">
  151. <el-form-item label="续借天数">
  152. <el-input :model-value="form.latest_renewal?.renewal_days" :disabled="true">
  153. <template #append>天</template>
  154. </el-input>
  155. </el-form-item>
  156. </el-col>
  157. <el-col :span="21">
  158. <el-form-item label="续借理由">
  159. <el-input :model-value="form.latest_renewal?.renewal_reason" type="textarea" :disabled="true" />
  160. </el-form-item>
  161. </el-col>
  162. </el-row>
  163. </div>
  164. </el-form>
  165. <!-- 审批信息流程展示 -->
  166. <!-- <div v-if="isView" style="margin-top: 24px; padding: 0 12px">
  167. <div style="font-weight: bold; margin-bottom: 12px">审批流程进度</div>
  168. <el-steps :active="activeStep" align-center>
  169. <el-step
  170. title="创建申请"
  171. :description="`创建时间:${ timeline.create }`"
  172. icon="CircleCheck"
  173. :status="activeStep >= 1 ? 'finish' : 'wait'"
  174. />
  175. <el-step
  176. title="待审批"
  177. description="待审批人审批"
  178. icon="CircleCheck"
  179. :status="activeStep >= 2 ? 'finish' : 'wait'"
  180. />
  181. <el-step
  182. title="审批通过"
  183. :description="`审批人:${timeline.approver},时间:${timeline.approve}`"
  184. icon="CircleCheck"
  185. :status="activeStep >= 3 ? 'finish' : 'wait'"
  186. />
  187. <el-step
  188. title="流程完成"
  189. description="流程已结束"
  190. icon="CircleCheck"
  191. :status="activeStep >= 4 ? 'finish' : 'wait'"
  192. />
  193. </el-steps>
  194. </div> -->
  195. <div v-if="isView" style="margin-top: 24px; padding: 0 12px">
  196. <div style="font-weight: bold; margin-bottom: 12px">审批流程进度</div>
  197. <el-steps :active="steps.length" align-center>
  198. <el-step
  199. v-for="(step, index) in steps"
  200. :key="index"
  201. :title="step.title"
  202. :description="step.description"
  203. icon="CircleCheck"
  204. :status="step.status"
  205. />
  206. </el-steps>
  207. </div>
  208. </el-tab-pane>
  209. <el-tab-pane v-if="isCollect && !shouldHideDeviceList" label="设备清单" name="second">
  210. <el-form :model="form" label-width="110px">
  211. <el-row :gutter="16">
  212. <el-col :span="24">
  213. <div style="margin: 8px 0">
  214. <textarea
  215. ref="searchInputRef"
  216. v-model="searchText"
  217. class="device-code-input"
  218. placeholder="设备编号(支持扫码枪/RFID批量录入)"
  219. :disabled="loading"
  220. autocomplete="off"
  221. rows="1"
  222. style="width: 300px; vertical-align: top;"
  223. @input="handleDeviceInput"
  224. @keydown="handleRFIDKeydown"
  225. @blur="handleDeviceBlur"
  226. @keydown.enter.prevent="handleDeviceEnter"
  227. ></textarea>
  228. <el-button type="primary" style="margin-left: 16px" @click="handleAdd">手动借用</el-button>
  229. </div>
  230. <el-table :data="deviceList" border style="width: 100%">
  231. <el-table-column type="index" label="序号" width="60" align="center" />
  232. <el-table-column prop="device_code" label="设备编号" align="center" />
  233. <el-table-column prop="device_type" label="设备分类" align="center" />
  234. <el-table-column prop="device_name" label="设备名称" align="center" />
  235. <el-table-column prop="borrow_count" label="借用数量" align="center" width="150">
  236. <template #default="{ row }">
  237. <el-input-number v-model="row.borrow_count" :disabled="isView" :min="1" :max="row.available_quantity" size="small" />
  238. </template>
  239. </el-table-column>
  240. <el-table-column prop="brand" label="品牌" align="center" />
  241. <el-table-column prop="device_specification" label="规格型号" align="center" />
  242. <!-- <el-table-column prop="warehouse" label="存放仓库" align="center" /> -->
  243. <el-table-column label="操作" width="80" align="center">
  244. <template #default="{ $index }">
  245. <el-button type="text" style="color: red" :disabled="isView" @click="handleRemove($index)">移除</el-button>
  246. </template>
  247. </el-table-column>
  248. </el-table>
  249. </el-col>
  250. </el-row>
  251. <el-row :gutter="16" style="margin-top: 16px">
  252. <el-col :span="24">
  253. <div style="font-weight: bold; margin-bottom: 8px">实借配件信息</div>
  254. <el-input v-if="isCollect" v-model="collectAccessories" :disabled="isView" type="textarea" :placeholder="isView?'':'请输入实借配件信息'" />
  255. <el-input v-else v-model="form.pre_borrow_accessories" :disabled="isView" type="textarea" :placeholder="isView?'':'请输入实借配件信息'" />
  256. </el-col>
  257. <el-col :span="24" style="margin-top: 16px">
  258. <div style="font-weight: bold; margin-bottom: 8px">备注</div>
  259. <el-input v-model="remark" type="textarea" :disabled="isView" :placeholder="isView?'':'请输入备注'" />
  260. </el-col>
  261. </el-row>
  262. <el-row :gutter="16" style="margin-top: 16px">
  263. <el-col :span="24">
  264. <div style="font-weight: bold; margin-bottom: 8px">异常记录 <span>{{
  265. abnormalList?.length ?
  266. abnormalList.reduce((sum, item) => sum + (Number(item?.condition) || 0), 0)
  267. : 0
  268. }}</span> 次</div>
  269. <el-table v-if="abnormalList.length > 0" :data="abnormalList" border style="width: 100%">
  270. <!-- <el-table-column type="index" label="序号" width="60" align="center" />
  271. <el-table-column prop="application_no" label="借用单编号" align="center" /> -->
  272. <el-table-column prop="operator_name" label="借用人" align="center" />
  273. <el-table-column prop="user_code" label="学号" align="center" />
  274. <el-table-column prop="emergency_phone" label="借用人电话" align="center" />
  275. <!-- <el-table-column prop="time" label="发生时间" align="center" /> -->
  276. <el-table-column prop="type" label="异常" align="center">
  277. <template #default="{ row }">
  278. {{ row.type }}
  279. </template>
  280. </el-table-column>
  281. <el-table-column prop="condition" label="数量" align="center">
  282. <template #default="{ row }">
  283. {{ row.condition }}
  284. </template>
  285. </el-table-column>
  286. </el-table>
  287. </el-col>
  288. </el-row>
  289. </el-form>
  290. </el-tab-pane>
  291. <el-tab-pane v-else-if="!shouldHideDeviceList" label="设备清单" name="third">
  292. <el-form :model="form" label-width="110px">
  293. <el-row :gutter="16">
  294. <el-col :span="24">
  295. <div style="margin: 8px 0" v-if="!isView" >
  296. <textarea
  297. ref="returnSearchInputRef"
  298. v-model="returnSearchText"
  299. class="device-code-input"
  300. placeholder="设备编号(支持扫码枪/RFID批量录入)"
  301. :disabled="loading"
  302. autocomplete="off"
  303. rows="1"
  304. style="width: 300px; vertical-align: top;"
  305. @input="handleReturnDeviceInput"
  306. @keydown="handleReturnRFIDKeydown"
  307. @blur="handleReturnDeviceBlur"
  308. @keydown.enter.prevent="handleReturnDeviceEnter"
  309. ></textarea>
  310. <el-button type="primary" style="margin-left: 16px" @click="handleAdd" :disabled="isView">手动归还</el-button>
  311. </div>
  312. <el-table :data="returnDeviceList" border style="width: 100%" >
  313. <!-- <el-table-column type="selection" width="55" align="center" />@selection-change="handleSelectionChange" -->
  314. <el-table-column type="index" label="序号" width="60" align="center" />
  315. <el-table-column prop="device_code" label="设备编号" align="center" />
  316. <!-- <el-table-column prop="device_type" label="设备分类" align="center" /> -->
  317. <el-table-column prop="device_name" label="设备名称" align="center" />
  318. <el-table-column prop="borrow_count" label="借用数量" align="center" width="150">
  319. <template #default="{ row }">
  320. <el-input-number v-model="row.borrow_count" :min="1" :max="row.available_quantity" size="small" :disabled="true" />
  321. </template>
  322. </el-table-column>
  323. <el-table-column prop="brand" label="品牌" align="center" />
  324. <el-table-column prop="model" label="规格型号" align="center" />
  325. <!-- <el-table-column prop="warehouse" label="存放仓库" align="center" /> -->
  326. <el-table-column prop="status" label="状态" align="center">
  327. <template #default="{ row }">
  328. {{ row.return_status.label }}
  329. <!-- <el-tag v-if="!isView" :type="row.status === '已归还' ? 'success' : 'warning'">{{ row.status }}</el-tag>
  330. <el-tag v-else-if="['已领取', '已归还'].includes(form.status_label)">{{ form.status_label }}</el-tag> -->
  331. <!-- <span v-else>{{ form.status_label }}</span> -->
  332. </template>
  333. </el-table-column>
  334. <el-table-column prop="status" label="归还时间" align="center">
  335. <template #default="{ row }">
  336. {{ row.return_time ? dayjs(row.return_time).format('YYYY-MM-DD HH:mm:ss') : (row.return_status?.label === '已归还' ? dayjs().format('YYYY-MM-DD HH:mm:ss') : '') }}
  337. </template>
  338. </el-table-column>
  339. <el-table-column label="操作" width="120" align="center">
  340. <template #default="{ $index,row }">
  341. <!-- && (!returnDeviceList[$index]?.condition || returnDeviceList[$index]?.photos?.length==0)&& row.problem_records?.length ==0 -->
  342. <el-button :disabled="isView" type="text" style="color: red;"
  343. v-if="!isView"
  344. @click="handleAbnormal($index)">异常操作</el-button>
  345. <el-button
  346. type="text"
  347. style="color: #409EFF;margin-left:0"
  348. @click="handleViewAbnormal($index)"
  349. v-if="row.problem_records?.length || (returnDeviceList[$index]?.condition || returnDeviceList[$index]?.photos?.length)"
  350. >查看异常</el-button>
  351. <!-- <el-button type="text" v-if="!isView" :disabled="isView" style="color: red" @click="handleReturnRemove($index)">移除</el-button> -->
  352. </template>
  353. </el-table-column>
  354. </el-table>
  355. </el-col>
  356. </el-row>
  357. <el-row :gutter="16" style="margin-top: 16px">
  358. <el-col :span="16">
  359. <div style="font-weight: bold; margin-bottom: 8px">实借配件信息</div>
  360. <el-input v-model="form.pre_borrow_accessories" :disabled="true" :placeholder="isView?'':'请输入实借配件信息'" />
  361. </el-col>
  362. <el-col :span="8">
  363. <div style="font-weight: bold; margin-bottom: 8px">当前状态</div>
  364. <el-input v-model="returnStatus" v-if="!isView" :disabled="true" placeholder="" style="width: 150px" />
  365. <el-input :value="form.status==7?'已归还':returnStatus" v-else :disabled="true" placeholder="" style="width: 150px" />
  366. <el-button type="primary" v-if="!isView" style="margin-left: 16px" @click="clickReturn"
  367. :disabled="returnStatus=='已归还'||isView">归还</el-button>
  368. </el-col>
  369. <el-col :span="24" style="margin-top: 16px">
  370. <div style="font-weight: bold; margin-bottom: 8px">备注</div>
  371. <el-input
  372. v-model="displayReturnRemark"
  373. type="textarea"
  374. :disabled="isView"
  375. :placeholder="isView?'':'请输入备注'"
  376. />
  377. </el-col>
  378. </el-row>
  379. <el-row :gutter="16" style="margin-top: 16px">
  380. <el-col :span="24">
  381. <div style="font-weight: bold; margin-bottom: 8px">异常记录 <span>{{
  382. returnAbnormalList?.length ?
  383. returnAbnormalList.reduce((sum, item) => sum + (Number(item?.condition) || 0), 0)
  384. : 0
  385. }}</span> 次</div>
  386. <el-table v-if="returnAbnormalList?.length > 0" :data="returnAbnormalList" border style="width: 100%">
  387. <!-- <el-table-column type="index" label="序号" width="60" align="center" />
  388. <el-table-column prop="application_no" label="借用单编号" align="center" /> -->
  389. <el-table-column prop="operator_name" label="借用人" align="center" />
  390. <el-table-column prop="user_code" label="学号" align="center" />
  391. <el-table-column prop="emergency_phone" label="借用人电话" align="center" />
  392. <!-- <el-table-column prop="time" label="发生时间" align="center" /> -->
  393. <el-table-column prop="type" label="异常" align="center">
  394. <template #default="{ row }">
  395. {{ row.type }}
  396. </template>
  397. </el-table-column>
  398. <el-table-column prop="condition" label="数量" align="center">
  399. <template #default="{ row }">
  400. {{ row.condition }}
  401. </template>
  402. </el-table-column>
  403. </el-table>
  404. </el-col>
  405. </el-row>
  406. </el-form>
  407. </el-tab-pane>
  408. </el-tabs>
  409. <template #footer>
  410. <el-button @click="onCancel">关闭</el-button>
  411. <el-button type="primary" @click="onSubmit" v-if="!isView"> {{ isCollect ? '提交' : '确认' }}</el-button>
  412. </template>
  413. </el-dialog>
  414. <SelectDeviceDialog
  415. v-model:visible="showSelectDeviceDialog"
  416. @confirm="onDeviceSelected"
  417. :device-ids="unreturnedDeviceIds"
  418. :return-mode="activeName === 'third'"
  419. :device-status-map="deviceStatusMap"
  420. :selected-device-codes="selectedDeviceCodes"
  421. :disabled-device-ids="disabledDeviceIds"
  422. />
  423. <SettlementDialog v-model:visible="showSettlementDialog" :settlement-data="settlementData" :modelValue="borrowForm" />
  424. <AbnormalDialog v-model:visible="showAbnormalDialog" @confirm="onAbnormalConfirm" />
  425. <ViewAbnormalDialog
  426. v-model:visible="showViewAbnormalDialog"
  427. :abnormal-data="currentAbnormalData"
  428. />
  429. <!-- 图片预览对话框 -->
  430. <el-dialog v-model="dialogVisible" title="图片预览" width="auto" center>
  431. <img width="100%" :src="dialogImageUrl" alt="Preview Image" style="max-width: 100%; max-height: 80vh;" />
  432. </el-dialog>
  433. </template>
  434. <script setup lang="ts">
  435. import { ref, watch, defineProps, defineEmits, onMounted, onBeforeUnmount, computed, nextTick } from 'vue';
  436. import SelectDeviceDialog from './SelectDeviceDialog/index.vue';
  437. import SettlementDialog from './SettlementDialog.vue';
  438. import AbnormalDialog from './AbnormalDialog/index.vue';
  439. import ViewAbnormalDialog from './ViewAbnormalDialog/index.vue';
  440. import dayjs from 'dayjs';
  441. import { ElMessage } from 'element-plus';
  442. import * as api from '../../api';
  443. import * as deviceApi from '../../../device/api';
  444. import { CollectEquipment, ReturnEquipment } from '../../api';
  445. import type { TabsPaneContext } from 'element-plus';
  446. import { getUserInfo } from '../../../login/api';
  447. import { request } from '/@/utils/service';
  448. // 禁止选择今天之前的日期
  449. const disabledDate = (time: Date) => {
  450. // 获取今天的开始时间(准确到日)
  451. const today = new Date()
  452. today.setHours(0, 0, 0, 0)
  453. // 返回true表示禁用,禁用今天之前的所有日期
  454. return time.getTime() < today.getTime()
  455. }
  456. /* 获取管理员列表 */
  457. const adminList = ref([]);
  458. async function getAdminList() {
  459. const res = await getUserInfo();
  460. adminList.value = res.data;
  461. console.log("adminList.value:::",adminList.value)
  462. }
  463. /* */
  464. /* 获取所有用户 */
  465. const allUserList = ref([]);
  466. let allUserLoaded = false;
  467. const allUserLoading = ref(false);
  468. async function getAllUserList() {
  469. if (allUserLoaded || allUserLoading.value) return;
  470. allUserLoading.value = true;
  471. try {
  472. // 分页拉取,避免一次性超大 limit 硬编码
  473. const pageSize = 100;
  474. let page = 1;
  475. const users: any[] = [];
  476. while (true) {
  477. const res = await api.GetAllUser({ page, limit: pageSize });
  478. const list = Array.isArray(res?.data) ? res.data : [];
  479. users.push(...list);
  480. if (list.length < pageSize) break;
  481. page += 1;
  482. }
  483. allUserList.value = users;
  484. // 设置默认选择(仅在当前未有选中时)
  485. if ((!form.value.app_user_borrower || !form.value.app_user_borrower.id) && allUserList.value.length > 0) {
  486. const first: any = allUserList.value[0];
  487. form.value.app_user_borrower = {
  488. id: first.id,
  489. name: first.name,
  490. user_code: first.user_code,
  491. mobile: first.mobile
  492. };
  493. }
  494. allUserLoaded = true;
  495. } catch (e) {
  496. /* empty */
  497. } finally {
  498. allUserLoading.value = false;
  499. }
  500. }
  501. interface ApiResponse {
  502. code: number;
  503. data?: any;
  504. message?: string;
  505. }
  506. // 用户最小结构用于表单回显
  507. interface AppUserBorrower {
  508. id: number | string;
  509. name?: string;
  510. user_code?: string;
  511. mobile?: string;
  512. }
  513. // 定义接口
  514. interface Device {
  515. id: string | number;
  516. category_name: string;
  517. name?: string;
  518. brand?: string;
  519. model?: string;
  520. warehouse: string;
  521. device_specification?: string;
  522. device_name?: string;
  523. }
  524. interface DeviceListItem {
  525. device_no: string | number;
  526. device_code?: string;
  527. device_type: string;
  528. device_name?: string; // 改为可选
  529. borrow_count: number;
  530. brand: string;
  531. model?: string; // 改为可选
  532. warehouse: string;
  533. status?: string;
  534. return_status?: {
  535. value?: number;
  536. label?: string;
  537. };
  538. return_time?: string;
  539. is_return?: boolean;
  540. condition?: string;
  541. photos?: Array<{
  542. filename: string;
  543. data: string;
  544. }>;
  545. device_specification?: string;
  546. available_quantity?:number;
  547. }
  548. interface FormItem {
  549. device: string | number;
  550. remark: string;
  551. quantity: number;
  552. location: string;
  553. device_name?: string;
  554. device_specification?: string;
  555. condition?: string;
  556. photos?: Array<{
  557. filename: string;
  558. data: string;
  559. }>;
  560. }
  561. interface FormData {
  562. application_no: string;
  563. emergency_contact: string;
  564. emergency_phone: string;
  565. app_user_borrower: AppUserBorrower | any;
  566. team_type: number;
  567. team_members_count: number;
  568. team_members: string;
  569. expected_start_time: string;
  570. expected_end_time: string;
  571. borrower_dept: string;
  572. purpose: string;
  573. applicant_remark_type: string;
  574. applicant_remark: string;
  575. items: FormItem[];
  576. accessory_info: string;
  577. borrow_type: number;
  578. return_location: string;
  579. status_label?: string;
  580. id?: number;
  581. records?: any[];
  582. borrower_info?: {
  583. user_code: string;
  584. };
  585. borrower_damage_count?: number;
  586. borrower_overdue_count?:number;
  587. accessories?: string;
  588. pre_borrow_accessories?:string;
  589. latest_renewal?: {
  590. renewal_days?: number | string;
  591. renewal_reason?: string;
  592. };
  593. problem_remarks?: string[];
  594. }
  595. interface AbnormalData {
  596. damage_type:number;
  597. condition: string;
  598. photos: Array<{
  599. filename: string;
  600. data: string;
  601. }>;
  602. }
  603. interface AbnormalRecord {
  604. application_no: string;
  605. operator_name: string;
  606. user_code: string;
  607. emergency_phone: string;
  608. type: '损坏' | '逾期';
  609. condition: number;
  610. create_time: string;
  611. }
  612. const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
  613. const props = defineProps<{ visible: boolean; modelValue?: any }>();
  614. const isEdit = computed(() => props.modelValue?.mode === 'edit');
  615. const isView = computed(() => props.modelValue?.mode === 'view');
  616. const isAdd = computed(() => props.modelValue?.mode === 'add');
  617. const isCollect = computed(() => props.modelValue?.mode === 'collect');
  618. const isReturn = computed(() => props.modelValue?.mode === 'return');
  619. // 计算是否应该隐藏设备清单标签页
  620. const shouldHideDeviceList = computed(() => {
  621. if (!isView.value) return false;
  622. // 检查状态
  623. const status = (form.value as any).status;
  624. const statusLabel = (form.value as any).status_label;
  625. // 审批拒绝:隐藏设备清单
  626. const isRejected = status === 3 || (typeof statusLabel === 'string' && statusLabel.includes('拒绝'));
  627. if (isRejected) {
  628. return true;
  629. }
  630. // 强制显示:状态大于 5 或状态标签为"已借出"
  631. if ((typeof status === 'number' && status > 5) || statusLabel === '已借出') {
  632. return false;
  633. }
  634. // 审批通过:隐藏设备清单(不影响已借出/更高级状态的强制显示)
  635. const isApproved = typeof statusLabel === 'string' && statusLabel.includes('已批准');
  636. if (isApproved) {
  637. return true;
  638. }
  639. // 待审批状态:隐藏设备清单
  640. const isPendingApproval = status < 2 || statusLabel === '待审批';
  641. // 其他情况默认显示
  642. return isPendingApproval;
  643. });
  644. // 计算设备状态映射
  645. const deviceStatusMap = computed(() => {
  646. const statusMap: Record<string | number, any> = {};
  647. returnDeviceList.value.forEach(device => {
  648. if (device.device_no && device.return_status) {
  649. statusMap[device.device_no] = device.return_status;
  650. }
  651. });
  652. return statusMap;
  653. });
  654. const emit = defineEmits(['update:visible', 'update:modelValue', 'submit']);
  655. const visible = ref(props.visible);
  656. const formRef = ref();
  657. // 图片预览相关
  658. const dialogImageUrl = ref('');
  659. const dialogVisible = ref(false);
  660. const timeline = ref({
  661. create: '',
  662. approve: '',
  663. approver: ''
  664. });
  665. // const activeStep = ref(1);
  666. const activeName = ref('first');
  667. const returnStatus = ref('未归还'); // 添加独立的状态变量
  668. // 输入框引用
  669. const searchInputRef = ref<any>(null);
  670. const returnSearchInputRef = ref<any>(null);
  671. // 借用列表相关数据
  672. const searchText = ref('');
  673. const deviceList = ref<DeviceListItem[]>([]);
  674. const accessoryInfo = ref('');
  675. const remark = ref('');
  676. // 领取模式下的"实借配件信息"本地输入,不回显历史
  677. const collectAccessories = ref('');
  678. // RFID高速扫码缓冲相关
  679. const rfidInputBuffer = ref('');
  680. let rfidInputTimer: ReturnType<typeof setTimeout> | null = null;
  681. const loading = ref(false);
  682. // 记录最后一次输入时间,用于检测是否是高速扫码输入
  683. let lastInputTime = 0;
  684. // 记录连续输入次数,用于判断是否是扫码输入
  685. let consecutiveInputCount = 0;
  686. // RFID读取规则配置(简化版,可根据需要扩展)
  687. const readRuleConfig = ref({
  688. enableRFIDBuffer: true, // 是否启用RFID缓冲
  689. rfidInputTimeout: 120, // RFID缓冲判定时间(ms)
  690. separator: ' ', // 分隔符
  691. minInputSpeed: 30, // 最小输入速度(ms),低于此值认为是扫码输入
  692. });
  693. // 归还设备相关数据
  694. const returnSearchText = ref('');
  695. const returnDeviceList = ref<DeviceListItem[]>([]);
  696. const returnAccessoryInfo = ref('');
  697. const returnRemark = ref('');
  698. const selectedDevices = ref<DeviceListItem[]>([]); // 添加选中设备列表
  699. // 归还流程 RFID高速扫码缓冲相关
  700. const returnRfidInputBuffer = ref('');
  701. let returnRfidInputTimer: ReturnType<typeof setTimeout> | null = null;
  702. // 记录归还流程最后一次输入时间,用于检测是否是高速扫码输入
  703. let returnLastInputTime = 0;
  704. // 记录归还流程连续输入次数,用于判断是否是扫码输入
  705. let returnConsecutiveInputCount = 0;
  706. // 备注显示值计算属性:查看模式下显示 problem_remarks[0],否则显示 returnRemark
  707. const displayReturnRemark = computed({
  708. get: () => {
  709. if (isView.value && form.value.problem_remarks?.[0]) {
  710. return form.value.problem_remarks[0];
  711. }
  712. return returnRemark.value;
  713. },
  714. set: (val: string) => {
  715. if (!isView.value) {
  716. returnRemark.value = val;
  717. }
  718. }
  719. });
  720. // 统一判定设备是否已归还
  721. const isDeviceReturned = (device: any) => {
  722. if (device?.is_return === true) return true;
  723. const rs = device?.return_status;
  724. if (rs && (rs.value === 2 || rs.label === '已归还')) return true;
  725. if (device?.status === '已归还') return true;
  726. return false;
  727. };
  728. // 过滤未归还设备的 device_no 列表,用于设备选择弹窗的排除
  729. const unreturnedDeviceIds = computed(() =>
  730. returnDeviceList.value
  731. .filter((d) => !isDeviceReturned(d))
  732. .map((d) => d.device_no)
  733. );
  734. // 是否全部已归还(仅在归还页签生效)
  735. const allReturned = computed(() => unreturnedDeviceIds.value.length === 0);
  736. //审批步骤
  737. const steps = ref<any[]>([]);
  738. // 仓库映射(id -> name)
  739. const warehouseMap = ref<Record<string, string>>({});
  740. async function loadWarehouses() {
  741. try {
  742. const res = await request({
  743. url: '/api/system/warehouse/',
  744. method: 'get',
  745. params: { page: 1, limit: 999 }
  746. });
  747. if (res?.code === 2000 && Array.isArray(res.data)) {
  748. warehouseMap.value = Object.fromEntries(
  749. res.data.map((w: any) => [String(w.id), w.name])
  750. );
  751. }
  752. } catch (e) {
  753. /* empty */
  754. }
  755. }
  756. function getWarehouseName(id: any) {
  757. const key = id != null ? String(id) : '';
  758. return warehouseMap.value[key] ?? id;
  759. }
  760. // 添加结算单相关的状态
  761. const showSettlementDialog = ref(false);
  762. const settlementData = ref<any>(null);
  763. const borrowForm = ref<any>(null);
  764. // 添加异常操作相关的状态
  765. const showAbnormalDialog = ref(false);
  766. const currentAbnormalIndex = ref(-1);
  767. // 添加查看异常相关的状态
  768. const showViewAbnormalDialog = ref(false);
  769. const currentAbnormalData = ref<AbnormalData>({
  770. damage_type:0,
  771. condition: '',
  772. photos: []
  773. });
  774. //审批
  775. function buildSteps(data: any) {
  776. steps.value = [
  777. {
  778. title: '创建申请',
  779. description: `创建时间:${data.create_datetime ? dayjs(data.create_datetime).format('YYYY-MM-DD HH:mm:ss') : ''}`,
  780. status: 'finish'
  781. },
  782. {
  783. title: '待审批',
  784. description: `待审批人:${data.approval_records ?
  785. (() => {
  786. console.log('approval_records:', data.approval_records); // 调试信息
  787. const names = data.approval_records
  788. .filter((item:any) => {
  789. // 当状态为待审批时,只显示未通过的审批记录(过滤掉已通过的记录)
  790. if (data.status === 1) {
  791. return item.status !== 1; // 过滤掉审批通过的记录
  792. }
  793. // 其他状态下保持原有逻辑
  794. return item.status !== 0;
  795. })
  796. .map((item:any) => {
  797. console.log('item:', item, 'approver_name:', item.approver_name); // 调试信息
  798. // 支持多种字段名
  799. return item.approver_name || item.app_user_approver_name || item.name;
  800. })
  801. .filter(name => name && name.trim()); // 过滤空值和空字符串
  802. console.log('filtered names:', names); // 调试信息
  803. return names.join('、') || data.approver_info.name;
  804. })()
  805. : '未知'} 审批`,
  806. status: data.status >= 2 ? 'finish' : 'process'
  807. }
  808. ];
  809. const approverinfo = data.approver_info ?? data.admin_approver_info
  810. if (data.status >= 2&&data.status!=3 && data.borrow_type!=1) {
  811. // 将审批记录倒序排列
  812. const reversedApprovalRecords = [...data.approval_records]
  813. // 先找出所有审批通过的记录
  814. const approvedRecords = reversedApprovalRecords.filter((item:any) => item.status === 1)
  815. const lastApprovedIndex = approvedRecords.length - 1
  816. reversedApprovalRecords.forEach((item:any,index:number) => {
  817. if(item.status === 1){
  818. // 找到当前记录在审批通过记录中的索引
  819. const currentApprovedIndex = approvedRecords.findIndex((record:any) => record === item)
  820. // 判断是否是最后一个审批通过的记录
  821. const isLastApproved = currentApprovedIndex === lastApprovedIndex
  822. /* if(index == 1){1
  823. steps.value.push({
  824. title: '审批通过',
  825. description: `审批人:${item.approver_name||item.app_user_approver_name},时 间:${item.approve_time}`,
  826. status: 'finish'
  827. });
  828. }else{ */
  829. // 只在最后一个审批通过的记录中显示领取仓库
  830. const warehouseInfo = isLastApproved ? `,领取仓库:${getWarehouseName(data.warehouse)||'暂无仓库'}` : ''
  831. steps.value.push({
  832. title: '审批通过',
  833. description: `审批人:${item.approver_name||item.app_user_approver_name},时 间:${item.create_datetime ? dayjs(item.create_datetime).format('YYYY-MM-DD HH:mm:ss') : ''}${warehouseInfo}`,
  834. status: 'finish'
  835. });
  836. /* } */
  837. }
  838. });
  839. } else if (data.status === 3) {
  840. // console.log("data.approver_name:::",data.approver_info.name)
  841. const rejectNames = data.approval_records
  842. .filter((item:any) => item.status === 2)
  843. .map((item:any) => item.approver_name || item.app_user_approver_name || item.name)
  844. .filter((name:any) => name && name.trim());
  845. const rejectReason = data.approval_records
  846. .filter((item:any) => item.status === 2)
  847. .map((item:any) => item.approve_remark)
  848. .filter((reason:any) => reason && reason.trim());
  849. const rejectTime = data.approval_records
  850. .filter((item:any) => item.status === 2)
  851. .map((item:any) => item.approve_time)
  852. .filter((time:any) => time && time.trim());
  853. steps.value.push({
  854. title: '审批拒绝',
  855. description: `审批人:${rejectNames.join('、')},拒绝原因: ${rejectReason.join('、')},时间:${rejectTime.join('、')}`,
  856. status: 'error'
  857. });
  858. }
  859. if (data.status >= 4 ||data.borrow_type==1) {
  860. steps.value.push({
  861. title: '流程完成',
  862. description: '流程已结束',
  863. status: 'finish'
  864. });
  865. }
  866. }
  867. // 处理异常操作
  868. const handleAbnormal = (index: number) => {
  869. currentAbnormalIndex.value = index;
  870. showAbnormalDialog.value = true;
  871. console.log("showAbnormalDialog:::", showAbnormalDialog.value, "currentAbnormalIndex:::", currentAbnormalIndex.value);
  872. };
  873. // 处理异常操作确认
  874. const onAbnormalConfirm = (data: { condition: string; photos: any[] }) => {
  875. console.log("异常操作确认数据:", data);
  876. if (currentAbnormalIndex.value !== -1) {
  877. const device = returnDeviceList.value[currentAbnormalIndex.value];
  878. device.condition = data.condition;
  879. device.photos = data.photos;
  880. device.damage_type = data.damage_type;
  881. ElMessage.success('异常信息已保存');
  882. }
  883. console.log("returnDeviceList.value:::",returnDeviceList.value);
  884. };
  885. // 处理查看异常
  886. const handleViewAbnormal = (index: number) => {
  887. const device = returnDeviceList.value[index];
  888. console.log("device:::",device);
  889. if (device) {
  890. if(device.problem_records?.length){
  891. console.log(device.problem_records?.[0].damage_type.value)
  892. currentAbnormalData.value = {
  893. damage_type: Number(device.problem_records?.[0].damage_type.value) || 0,/* ||device.damage_type.value */
  894. condition: device.problem_records?.[0].device_condition_after ||'',
  895. photos: device.problem_records?.[0].returned_images.map((item:any)=>item.image_url) ||[]
  896. };
  897. }else{
  898. currentAbnormalData.value = {
  899. damage_type: device.damage_type ||0,
  900. condition: device.condition ||'',
  901. photos: device.photos ||[]
  902. };
  903. }
  904. showViewAbnormalDialog.value = true;
  905. }
  906. };
  907. // 清理RFID输入定时器
  908. const clearRFIDInputTimer = () => {
  909. if (rfidInputTimer) {
  910. clearTimeout(rfidInputTimer);
  911. rfidInputTimer = null;
  912. }
  913. };
  914. // 清理归还流程RFID输入定时器
  915. const clearReturnRFIDInputTimer = () => {
  916. if (returnRfidInputTimer) {
  917. clearTimeout(returnRfidInputTimer);
  918. returnRfidInputTimer = null;
  919. }
  920. };
  921. // 提交RFID缓冲区内容
  922. const submitRFIDBuffer = () => {
  923. if (!readRuleConfig.value.enableRFIDBuffer) {
  924. // 如果未启用RFID缓冲,直接使用 searchText 进行搜索
  925. if (searchText.value.trim()) {
  926. handleScanSearch();
  927. }
  928. return;
  929. }
  930. if (!rfidInputBuffer.value) {
  931. // 如果没有缓冲内容,使用 searchText 进行搜索
  932. if (searchText.value.trim()) {
  933. handleScanSearch();
  934. }
  935. return;
  936. }
  937. const bufferedValue = rfidInputBuffer.value;
  938. rfidInputBuffer.value = '';
  939. clearRFIDInputTimer();
  940. consecutiveInputCount = 0; // 重置计数
  941. lastInputTime = 0; // 重置时间
  942. searchText.value = bufferedValue;
  943. handleScanSearch();
  944. };
  945. // 调度RFID缓冲区提交(延迟提交)
  946. const scheduleRFIDBufferSubmit = () => {
  947. if (!readRuleConfig.value.enableRFIDBuffer) return;
  948. clearRFIDInputTimer();
  949. const delay = Math.max(readRuleConfig.value.rfidInputTimeout || 120, 30);
  950. rfidInputTimer = setTimeout(() => {
  951. submitRFIDBuffer();
  952. }, delay);
  953. };
  954. // 处理设备输入(用于同步 textarea 内容,仅在缓冲模式下使用)
  955. const handleDeviceInput = () => {
  956. // 如果缓冲区有内容,确保 textarea 显示与缓冲区一致
  957. // 这主要用于处理退格键等操作后的同步
  958. if (rfidInputBuffer.value && searchText.value !== rfidInputBuffer.value) {
  959. searchText.value = rfidInputBuffer.value;
  960. }
  961. // 注意:正常输入时,不干预 textarea 的内容,让默认行为处理
  962. };
  963. // 检测是否是高速扫码输入(通过输入速度判断)
  964. // 返回值:{ isHighSpeed: boolean, isFirstInput: boolean, isNormalInput: boolean, timeSinceLastInput: number }
  965. const checkInputSpeed = () => {
  966. const now = Date.now();
  967. const timeSinceLastInput = lastInputTime > 0 ? now - lastInputTime : 0;
  968. const isFirstInput = lastInputTime === 0;
  969. if (!isFirstInput) {
  970. lastInputTime = now;
  971. }
  972. return {
  973. isFirstInput,
  974. timeSinceLastInput,
  975. isHighSpeed: timeSinceLastInput > 0 && timeSinceLastInput < readRuleConfig.value.minInputSpeed,
  976. isNormalInput: timeSinceLastInput > 200
  977. };
  978. };
  979. // 处理RFID键盘输入(高速扫码)
  980. const handleRFIDKeydown = (event: KeyboardEvent) => {
  981. // 如果未启用RFID缓冲,直接返回,不拦截
  982. if (!readRuleConfig.value.enableRFIDBuffer) {
  983. return;
  984. }
  985. // 不拦截修饰键组合(如 Ctrl+C, Ctrl+V 等)
  986. if (event.ctrlKey || event.metaKey || event.altKey) {
  987. return;
  988. }
  989. // 处理回车键
  990. if (event.key === 'Enter') {
  991. // 回车键由 @keydown.enter.prevent="handleDeviceEnter" 处理
  992. // 这里不处理,让 handleDeviceEnter 统一处理
  993. return;
  994. }
  995. // 处理退格键
  996. if (event.key === 'Backspace') {
  997. // 如果有缓冲内容,处理缓冲
  998. if (rfidInputBuffer.value) {
  999. event.preventDefault();
  1000. rfidInputBuffer.value = rfidInputBuffer.value.slice(0, -1);
  1001. searchText.value = rfidInputBuffer.value;
  1002. }
  1003. // 否则让默认行为处理
  1004. return;
  1005. }
  1006. // 处理普通字符输入
  1007. if (event.key.length === 1) {
  1008. const inputInfo = checkInputSpeed();
  1009. const { isFirstInput, isHighSpeed, isNormalInput, timeSinceLastInput } = inputInfo;
  1010. // 如果是第一次输入,不拦截,让默认行为处理,只记录时间戳
  1011. if (isFirstInput) {
  1012. lastInputTime = Date.now();
  1013. // 不拦截,让字符正常输入到 textarea
  1014. return;
  1015. }
  1016. // 如果检测到是正常输入(间隔 > 200ms),处理之前的缓冲内容
  1017. if (isNormalInput) {
  1018. // 如果之前有缓冲内容,先提交
  1019. if (rfidInputBuffer.value) {
  1020. submitRFIDBuffer();
  1021. }
  1022. // 重置状态
  1023. consecutiveInputCount = 0;
  1024. lastInputTime = Date.now();
  1025. // 不拦截,让字符正常输入到 textarea
  1026. return;
  1027. }
  1028. // 如果检测到是高速输入(间隔 < 30ms),使用缓冲模式
  1029. if (isHighSpeed) {
  1030. event.preventDefault();
  1031. event.stopPropagation(); // 阻止事件冒泡,避免影响其他元素
  1032. // 如果是第一次检测到高速输入(之前没有缓冲内容),需要将 textarea 中已有的内容也加入缓冲区
  1033. if (rfidInputBuffer.value === '' && searchText.value) {
  1034. // textarea 中已经有内容(第一个字符已经通过默认行为输入了)
  1035. // 将整个内容作为缓冲区的初始值,然后追加当前字符
  1036. rfidInputBuffer.value = searchText.value + event.key;
  1037. } else {
  1038. // 否则直接追加当前字符
  1039. rfidInputBuffer.value += event.key;
  1040. }
  1041. searchText.value = rfidInputBuffer.value;
  1042. consecutiveInputCount++;
  1043. scheduleRFIDBufferSubmit();
  1044. return;
  1045. }
  1046. // 如果输入间隔在 30-200ms 之间,可能是正常输入,不拦截
  1047. // 但如果之前有缓冲内容,说明高速输入已结束,先提交
  1048. if (rfidInputBuffer.value) {
  1049. submitRFIDBuffer();
  1050. consecutiveInputCount = 0;
  1051. }
  1052. lastInputTime = Date.now();
  1053. // 不拦截,让字符正常输入到 textarea
  1054. }
  1055. };
  1056. // 处理输入框失焦
  1057. const handleDeviceBlur = (event: FocusEvent) => {
  1058. // 如果焦点转移到其他输入元素(如表单输入框),不自动聚焦
  1059. const relatedTarget = event.relatedTarget as HTMLElement;
  1060. if (relatedTarget && (
  1061. relatedTarget.tagName === 'INPUT' ||
  1062. relatedTarget.tagName === 'TEXTAREA' ||
  1063. relatedTarget.closest('.el-input') ||
  1064. relatedTarget.closest('.el-textarea') ||
  1065. relatedTarget.closest('form')
  1066. )) {
  1067. // 如果有缓冲内容,先提交
  1068. if (rfidInputBuffer.value) {
  1069. submitRFIDBuffer();
  1070. }
  1071. return;
  1072. }
  1073. // 其他情况(如点击按钮等),延迟后自动聚焦
  1074. setTimeout(() => {
  1075. if (searchInputRef.value && !loading.value) {
  1076. searchInputRef.value.focus();
  1077. }
  1078. }, 10);
  1079. };
  1080. // 处理回车键
  1081. const handleDeviceEnter = (event: KeyboardEvent) => {
  1082. if (event.shiftKey) return;
  1083. event.preventDefault();
  1084. // 无论是否启用RFID缓冲,都调用 submitRFIDBuffer
  1085. // submitRFIDBuffer 内部会判断是否有缓冲内容,如果没有则直接使用 searchText 搜索
  1086. submitRFIDBuffer();
  1087. };
  1088. // 分割多个设备编号(支持分隔符)
  1089. const splitDeviceCodes = (input: string): string[] => {
  1090. const trimmed = input.trim();
  1091. if (!trimmed) return [];
  1092. const { separator } = readRuleConfig.value;
  1093. // 优先处理换行符
  1094. if (/[\r\n]/.test(trimmed)) {
  1095. return trimmed.split(/[\r\n]+/).filter(code => code.trim());
  1096. }
  1097. // 处理配置的分隔符
  1098. if (separator && separator.trim() !== '') {
  1099. const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1100. const separatorPattern = new RegExp(escapedSeparator + '+', 'g');
  1101. return trimmed.split(separatorPattern).filter(code => code.trim());
  1102. }
  1103. // 处理空格分隔
  1104. if (/\s/.test(trimmed)) {
  1105. return trimmed.split(/\s+/).filter(code => code.trim());
  1106. }
  1107. // 单个设备编号
  1108. return [trimmed];
  1109. };
  1110. // 处理扫码搜索(支持批量,优化并发请求)
  1111. const handleScanSearch = async (event?: Event) => {
  1112. // 阻止默认行为,防止表单提交导致页面刷新
  1113. if (event) {
  1114. event.preventDefault();
  1115. event.stopPropagation();
  1116. }
  1117. // 如果启用了RFID缓冲,先提交缓冲区
  1118. if (readRuleConfig.value.enableRFIDBuffer && rfidInputBuffer.value) {
  1119. const bufferedValue = rfidInputBuffer.value;
  1120. rfidInputBuffer.value = '';
  1121. clearRFIDInputTimer();
  1122. searchText.value = bufferedValue;
  1123. }
  1124. if (!searchText.value.trim()) return;
  1125. // 分割多个设备编号
  1126. const codes = splitDeviceCodes(searchText.value);
  1127. if (codes.length === 0) return;
  1128. // 去重处理,避免重复请求同一个设备编号
  1129. const uniqueCodes = Array.from(new Set(codes.map(code => code.trim()))).filter(Boolean);
  1130. if (uniqueCodes.length === 0) return;
  1131. loading.value = true;
  1132. let successCount = 0;
  1133. let failCount = 0;
  1134. const errorMessages: string[] = [];
  1135. try {
  1136. // 批量并发处理设备编号(使用 Promise.allSettled 确保所有请求都完成)
  1137. // 限制并发数量,避免同时发起过多请求
  1138. const batchSize = 10; // 每批处理10个
  1139. const batches: string[][] = [];
  1140. for (let i = 0; i < uniqueCodes.length; i += batchSize) {
  1141. batches.push(uniqueCodes.slice(i, i + batchSize));
  1142. }
  1143. // 逐批处理,每批内部并发
  1144. for (const batch of batches) {
  1145. const promises = batch.map(async (code) => {
  1146. try {
  1147. const res = await deviceApi.GetList({
  1148. page: 1,
  1149. limit: 1,
  1150. code: code,
  1151. status: 1
  1152. });
  1153. if (res.data && res.data.length > 0) {
  1154. const device = res.data[0];
  1155. if (device.available_quantity <= 0) {
  1156. return { success: false, code, message: `设备 ${code} 当前无可用数量` };
  1157. }
  1158. // 检查是否已存在
  1159. const existingDevice = deviceList.value.find(d => d.device_no === device.id);
  1160. if (existingDevice) {
  1161. existingDevice.borrow_count++;
  1162. return { success: true, code, device: null };
  1163. } else {
  1164. // 添加新设备到列表
  1165. const newDevice = {
  1166. device_no: device.id,
  1167. device_code: device.code,
  1168. device_type: device.category_name,
  1169. device_name: device.name || '',
  1170. borrow_count: 1,
  1171. brand: device.brand || device.device_brand || '',
  1172. device_specification: device.specification,
  1173. model: device.model || '',
  1174. warehouse: device.warehouse,
  1175. available_quantity: device.available_quantity
  1176. };
  1177. deviceList.value.push(newDevice);
  1178. return { success: true, code, device: newDevice };
  1179. }
  1180. } else {
  1181. return { success: false, code, message: `未找到设备编号: ${code}` };
  1182. }
  1183. } catch (error) {
  1184. console.error(`处理设备编号 ${code} 失败:`, error);
  1185. return { success: false, code, message: `处理设备编号 ${code} 失败` };
  1186. }
  1187. });
  1188. // 等待当前批次完成
  1189. const results = await Promise.allSettled(promises);
  1190. // 统计结果
  1191. results.forEach((result, index) => {
  1192. if (result.status === 'fulfilled') {
  1193. const { success, message } = result.value;
  1194. if (success) {
  1195. successCount++;
  1196. } else {
  1197. failCount++;
  1198. if (message) {
  1199. errorMessages.push(message);
  1200. }
  1201. }
  1202. } else {
  1203. failCount++;
  1204. errorMessages.push(`处理设备编号 ${batch[index]} 时发生错误`);
  1205. }
  1206. });
  1207. }
  1208. // 显示批量处理结果
  1209. if (uniqueCodes.length > 1) {
  1210. if (successCount > 0 && failCount === 0) {
  1211. ElMessage.success(`成功添加 ${successCount} 个设备`);
  1212. } else if (successCount > 0 && failCount > 0) {
  1213. ElMessage.warning(`成功添加 ${successCount} 个设备,失败 ${failCount} 个`);
  1214. // 只显示前3个错误信息,避免消息过多
  1215. if (errorMessages.length > 0) {
  1216. console.warn('批量处理错误详情:', errorMessages.slice(0, 3));
  1217. }
  1218. } else if (failCount > 0) {
  1219. ElMessage.error(`添加失败,共 ${failCount} 个设备编号`);
  1220. }
  1221. } else if (successCount > 0) {
  1222. ElMessage.success('设备已添加到列表');
  1223. } else if (failCount > 0 && errorMessages.length > 0) {
  1224. ElMessage.warning(errorMessages[0]);
  1225. }
  1226. } catch (error) {
  1227. console.error('批量处理设备失败:', error);
  1228. ElMessage.error('搜索设备失败');
  1229. } finally {
  1230. searchText.value = ''; // 清空搜索框
  1231. loading.value = false;
  1232. // 重新聚焦输入框,保持选中状态
  1233. nextTick(() => {
  1234. if (searchInputRef.value) {
  1235. searchInputRef.value.focus();
  1236. searchInputRef.value.select();
  1237. }
  1238. });
  1239. }
  1240. };
  1241. const normalizeReturnStatus = (status?: { value?: number | string; label?: string }) => {
  1242. const candidate = status?.value;
  1243. const numericValue =
  1244. candidate === undefined || candidate === null || candidate === ''
  1245. ? 2
  1246. : Number(candidate);
  1247. return {
  1248. value: Number.isNaN(numericValue) ? 2 : numericValue,
  1249. label: status?.label || '已归还'
  1250. };
  1251. };
  1252. // 提交归还流程RFID缓冲区内容
  1253. const submitReturnRFIDBuffer = () => {
  1254. if (!readRuleConfig.value.enableRFIDBuffer) {
  1255. // 如果未启用RFID缓冲,直接使用 returnSearchText 进行搜索
  1256. if (returnSearchText.value.trim()) {
  1257. handleReturnScanSearch();
  1258. }
  1259. return;
  1260. }
  1261. if (!returnRfidInputBuffer.value) {
  1262. // 如果没有缓冲内容,使用 returnSearchText 进行搜索
  1263. if (returnSearchText.value.trim()) {
  1264. handleReturnScanSearch();
  1265. }
  1266. return;
  1267. }
  1268. const bufferedValue = returnRfidInputBuffer.value;
  1269. returnRfidInputBuffer.value = '';
  1270. clearReturnRFIDInputTimer();
  1271. returnConsecutiveInputCount = 0; // 重置计数
  1272. returnLastInputTime = 0; // 重置时间
  1273. returnSearchText.value = bufferedValue;
  1274. handleReturnScanSearch();
  1275. };
  1276. // 调度归还流程RFID缓冲区提交(延迟提交)
  1277. const scheduleReturnRFIDBufferSubmit = () => {
  1278. if (!readRuleConfig.value.enableRFIDBuffer) return;
  1279. clearReturnRFIDInputTimer();
  1280. const delay = Math.max(readRuleConfig.value.rfidInputTimeout || 120, 30);
  1281. returnRfidInputTimer = setTimeout(() => {
  1282. submitReturnRFIDBuffer();
  1283. }, delay);
  1284. };
  1285. // 处理归还设备输入(用于同步 textarea 内容,仅在缓冲模式下使用)
  1286. const handleReturnDeviceInput = () => {
  1287. // 如果缓冲区有内容,确保 textarea 显示与缓冲区一致
  1288. // 这主要用于处理退格键等操作后的同步
  1289. if (returnRfidInputBuffer.value && returnSearchText.value !== returnRfidInputBuffer.value) {
  1290. returnSearchText.value = returnRfidInputBuffer.value;
  1291. }
  1292. // 注意:正常输入时,不干预 textarea 的内容,让默认行为处理
  1293. };
  1294. // 检测归还流程是否是高速扫码输入(通过输入速度判断)
  1295. // 返回值:{ isHighSpeed: boolean, isFirstInput: boolean, timeSinceLastInput: number }
  1296. const checkReturnInputSpeed = () => {
  1297. const now = Date.now();
  1298. const timeSinceLastInput = returnLastInputTime > 0 ? now - returnLastInputTime : 0;
  1299. const isFirstInput = returnLastInputTime === 0;
  1300. if (!isFirstInput) {
  1301. returnLastInputTime = now;
  1302. }
  1303. return {
  1304. isFirstInput,
  1305. timeSinceLastInput,
  1306. isHighSpeed: timeSinceLastInput > 0 && timeSinceLastInput < readRuleConfig.value.minInputSpeed,
  1307. isNormalInput: timeSinceLastInput > 200
  1308. };
  1309. };
  1310. // 处理归还流程RFID键盘输入(高速扫码)
  1311. const handleReturnRFIDKeydown = (event: KeyboardEvent) => {
  1312. // 如果未启用RFID缓冲,直接返回,不拦截
  1313. if (!readRuleConfig.value.enableRFIDBuffer) {
  1314. return;
  1315. }
  1316. // 不拦截修饰键组合(如 Ctrl+C, Ctrl+V 等)
  1317. if (event.ctrlKey || event.metaKey || event.altKey) {
  1318. return;
  1319. }
  1320. // 处理回车键
  1321. if (event.key === 'Enter') {
  1322. // 回车键由 @keydown.enter.prevent="handleReturnDeviceEnter" 处理
  1323. // 这里不处理,让 handleReturnDeviceEnter 统一处理
  1324. return;
  1325. }
  1326. // 处理退格键
  1327. if (event.key === 'Backspace') {
  1328. // 如果有缓冲内容,处理缓冲
  1329. if (returnRfidInputBuffer.value) {
  1330. event.preventDefault();
  1331. returnRfidInputBuffer.value = returnRfidInputBuffer.value.slice(0, -1);
  1332. returnSearchText.value = returnRfidInputBuffer.value;
  1333. }
  1334. // 否则让默认行为处理
  1335. return;
  1336. }
  1337. // 处理普通字符输入
  1338. if (event.key.length === 1) {
  1339. const inputInfo = checkReturnInputSpeed();
  1340. const { isFirstInput, isHighSpeed, isNormalInput, timeSinceLastInput } = inputInfo;
  1341. // 如果是第一次输入,不拦截,让默认行为处理,只记录时间戳
  1342. if (isFirstInput) {
  1343. returnLastInputTime = Date.now();
  1344. // 不拦截,让字符正常输入到 textarea
  1345. return;
  1346. }
  1347. // 如果检测到是正常输入(间隔 > 200ms),处理之前的缓冲内容
  1348. if (isNormalInput) {
  1349. // 如果之前有缓冲内容,先提交
  1350. if (returnRfidInputBuffer.value) {
  1351. submitReturnRFIDBuffer();
  1352. }
  1353. // 重置状态
  1354. returnConsecutiveInputCount = 0;
  1355. returnLastInputTime = Date.now();
  1356. // 不拦截,让字符正常输入到 textarea
  1357. return;
  1358. }
  1359. // 如果检测到是高速输入(间隔 < 30ms),使用缓冲模式
  1360. if (isHighSpeed) {
  1361. event.preventDefault();
  1362. event.stopPropagation(); // 阻止事件冒泡,避免影响其他元素
  1363. // 如果是第一次检测到高速输入(之前没有缓冲内容),需要将 textarea 中已有的内容也加入缓冲区
  1364. if (returnRfidInputBuffer.value === '' && returnSearchText.value) {
  1365. // textarea 中已经有内容(第一个字符已经通过默认行为输入了)
  1366. // 将整个内容作为缓冲区的初始值,然后追加当前字符
  1367. returnRfidInputBuffer.value = returnSearchText.value + event.key;
  1368. } else {
  1369. // 否则直接追加当前字符
  1370. returnRfidInputBuffer.value += event.key;
  1371. }
  1372. returnSearchText.value = returnRfidInputBuffer.value;
  1373. returnConsecutiveInputCount++;
  1374. scheduleReturnRFIDBufferSubmit();
  1375. return;
  1376. }
  1377. // 如果输入间隔在 30-200ms 之间,可能是正常输入,不拦截
  1378. // 但如果之前有缓冲内容,说明高速输入已结束,先提交
  1379. if (returnRfidInputBuffer.value) {
  1380. submitReturnRFIDBuffer();
  1381. returnConsecutiveInputCount = 0;
  1382. }
  1383. returnLastInputTime = Date.now();
  1384. // 不拦截,让字符正常输入到 textarea
  1385. }
  1386. };
  1387. // 处理归还输入框失焦
  1388. const handleReturnDeviceBlur = (event: FocusEvent) => {
  1389. // 如果焦点转移到其他输入元素(如表单输入框),不自动聚焦
  1390. const relatedTarget = event.relatedTarget as HTMLElement;
  1391. if (relatedTarget && (
  1392. relatedTarget.tagName === 'INPUT' ||
  1393. relatedTarget.tagName === 'TEXTAREA' ||
  1394. relatedTarget.closest('.el-input') ||
  1395. relatedTarget.closest('.el-textarea') ||
  1396. relatedTarget.closest('form')
  1397. )) {
  1398. // 如果有缓冲内容,先提交
  1399. if (returnRfidInputBuffer.value) {
  1400. submitReturnRFIDBuffer();
  1401. }
  1402. return;
  1403. }
  1404. // 其他情况(如点击按钮等),延迟后自动聚焦
  1405. setTimeout(() => {
  1406. if (returnSearchInputRef.value && !loading.value) {
  1407. returnSearchInputRef.value.focus();
  1408. }
  1409. }, 10);
  1410. };
  1411. // 处理归还回车键
  1412. const handleReturnDeviceEnter = (event: KeyboardEvent) => {
  1413. if (event.shiftKey) return;
  1414. event.preventDefault();
  1415. // 无论是否启用RFID缓冲,都调用 submitReturnRFIDBuffer
  1416. // submitReturnRFIDBuffer 内部会判断是否有缓冲内容,如果没有则直接使用 returnSearchText 搜索
  1417. submitReturnRFIDBuffer();
  1418. };
  1419. // 处理归还扫码搜索(支持批量,优化并发请求)
  1420. const handleReturnScanSearch = async (event?: Event) => {
  1421. // 阻止默认行为,防止表单提交导致页面刷新
  1422. if (event) {
  1423. event.preventDefault();
  1424. event.stopPropagation();
  1425. }
  1426. // 如果启用了RFID缓冲,先提交缓冲区
  1427. if (readRuleConfig.value.enableRFIDBuffer && returnRfidInputBuffer.value) {
  1428. const bufferedValue = returnRfidInputBuffer.value;
  1429. returnRfidInputBuffer.value = '';
  1430. clearReturnRFIDInputTimer();
  1431. returnSearchText.value = bufferedValue;
  1432. }
  1433. if (!returnSearchText.value.trim()) return;
  1434. // 分割多个设备编号
  1435. const codes = splitDeviceCodes(returnSearchText.value);
  1436. if (codes.length === 0) return;
  1437. // 去重处理,避免重复请求同一个设备编号
  1438. const uniqueCodes = Array.from(new Set(codes.map(code => code.trim()))).filter(Boolean);
  1439. if (uniqueCodes.length === 0) return;
  1440. loading.value = true;
  1441. let successCount = 0;
  1442. let failCount = 0;
  1443. const errorMessages: string[] = [];
  1444. try {
  1445. // 批量并发处理设备编号(使用 Promise.allSettled 确保所有请求都完成)
  1446. // 限制并发数量,避免同时发起过多请求
  1447. const batchSize = 10; // 每批处理10个
  1448. const batches: string[][] = [];
  1449. for (let i = 0; i < uniqueCodes.length; i += batchSize) {
  1450. batches.push(uniqueCodes.slice(i, i + batchSize));
  1451. }
  1452. // 逐批处理,每批内部并发
  1453. for (const batch of batches) {
  1454. const promises = batch.map(async (code) => {
  1455. try {
  1456. const res = await deviceApi.GetList({
  1457. page: 1,
  1458. limit: 1,
  1459. code: code,
  1460. status: 1
  1461. });
  1462. if (res.data && res.data.length > 0) {
  1463. const device = res.data[0];
  1464. const normalizedStatus = normalizeReturnStatus(device.return_status);
  1465. // 检查是否已存在
  1466. const existingDeviceIndex = returnDeviceList.value.findIndex(d => d.device_no === device.id);
  1467. if (existingDeviceIndex !== -1) {
  1468. // 如果设备已存在,更新其状态
  1469. returnDeviceList.value[existingDeviceIndex].return_status = normalizedStatus;
  1470. returnDeviceList.value[existingDeviceIndex].is_return = true;
  1471. returnDeviceList.value[existingDeviceIndex].return_time = dayjs().format('YYYY-MM-DD HH:mm:ss');
  1472. return { success: true, code };
  1473. } else {
  1474. // 设备不存在于归还列表中
  1475. return {
  1476. success: false,
  1477. code,
  1478. message: `设备 ${device.code || device.category_name} 不在当前归还列表中,请先通过手动归还添加该设备`
  1479. };
  1480. }
  1481. } else {
  1482. return { success: false, code, message: `未找到设备编号: ${code}` };
  1483. }
  1484. } catch (error) {
  1485. console.error(`处理设备编号 ${code} 失败:`, error);
  1486. return { success: false, code, message: `处理设备编号 ${code} 失败` };
  1487. }
  1488. });
  1489. // 等待当前批次完成
  1490. const results = await Promise.allSettled(promises);
  1491. // 统计结果
  1492. results.forEach((result, index) => {
  1493. if (result.status === 'fulfilled') {
  1494. const { success, message } = result.value;
  1495. if (success) {
  1496. successCount++;
  1497. } else {
  1498. failCount++;
  1499. if (message) {
  1500. errorMessages.push(message);
  1501. }
  1502. }
  1503. } else {
  1504. failCount++;
  1505. errorMessages.push(`处理设备编号 ${batch[index]} 时发生错误`);
  1506. }
  1507. });
  1508. }
  1509. // 显示批量处理结果
  1510. if (uniqueCodes.length > 1) {
  1511. if (successCount > 0 && failCount === 0) {
  1512. ElMessage.success(`成功更新 ${successCount} 个设备状态为已归还`);
  1513. } else if (successCount > 0 && failCount > 0) {
  1514. ElMessage.warning(`成功更新 ${successCount} 个设备状态,失败 ${failCount} 个`);
  1515. // 只显示前3个错误信息,避免消息过多
  1516. if (errorMessages.length > 0) {
  1517. console.warn('批量处理错误详情:', errorMessages.slice(0, 3));
  1518. }
  1519. } else if (failCount > 0) {
  1520. ElMessage.error(`更新失败,共 ${failCount} 个设备编号`);
  1521. }
  1522. } else if (successCount > 0) {
  1523. ElMessage.success('设备状态已更新为已归还');
  1524. } else if (failCount > 0 && errorMessages.length > 0) {
  1525. ElMessage.warning(errorMessages[0]);
  1526. }
  1527. } catch (error) {
  1528. console.error('批量处理设备失败:', error);
  1529. ElMessage.error('搜索设备失败');
  1530. } finally {
  1531. returnSearchText.value = ''; // 清空搜索框
  1532. loading.value = false;
  1533. // 重新聚焦输入框,保持选中状态
  1534. nextTick(() => {
  1535. if (returnSearchInputRef.value) {
  1536. returnSearchInputRef.value.focus();
  1537. returnSearchInputRef.value.select();
  1538. }
  1539. });
  1540. }
  1541. };
  1542. // 异常记录列表数据
  1543. const abnormalList = ref([
  1544. /* {
  1545. borrowNo: '202409002',
  1546. borrower: '张同学',
  1547. time: '2024-09-12 10:21:32',
  1548. type: '逾期'
  1549. },
  1550. {
  1551. borrowNo: '202505013',
  1552. borrower: '张同学',
  1553. time: '2025-05-13 15:12:35',
  1554. type: '损坏'
  1555. } */
  1556. ]);
  1557. /* 归还设备异常记录 */
  1558. const returnAbnormalList = ref([
  1559. ]);
  1560. // 附件文件列表
  1561. const attachmentFileList = ref<any[]>([]);
  1562. // 查看异常记录详情
  1563. const handleView = (row: any) => {
  1564. console.log('查看异常记录:', row);
  1565. // 这里可以添加查看详情的逻辑
  1566. };
  1567. const selectedDeviceCodes = ref<any[]>([]);
  1568. // 计算已选中设备的ID列表(用于禁用已选中的设备)
  1569. const disabledDeviceIds = computed(() => {
  1570. if (activeName.value === 'first') {
  1571. // first tab: 从 form.value.items 中获取 device ID
  1572. return form.value.items.map((item: any) => item.device).filter(Boolean);
  1573. } else if (activeName.value === 'second') {
  1574. // second tab: 从 deviceList.value 中获取 device_no ID
  1575. return deviceList.value.map((item: any) => item.device_no).filter(Boolean);
  1576. }
  1577. // third tab (归还模式) 不需要禁用,因为已经有特殊的处理逻辑
  1578. return [];
  1579. });
  1580. // 借用列表相关方法
  1581. const handleAdd = () => {
  1582. if (activeName.value === 'third' && allReturned.value) {
  1583. ElMessage.info('当前列表设备均已归还,无可选择设备');
  1584. return;
  1585. }
  1586. if(activeName.value === 'third'){
  1587. selectedDeviceCodes.value = [];
  1588. returnDeviceList.value.forEach(item => {
  1589. console.log("item:::",item);
  1590. if(item.device_no){
  1591. selectedDeviceCodes.value.push(item.device_code);
  1592. }
  1593. });
  1594. console.log("selectedDeviceCodes.value:::",selectedDeviceCodes.value);
  1595. }
  1596. showSelectDeviceDialog.value = true;
  1597. };
  1598. const handleRemove = (index: number) => {
  1599. deviceList.value.splice(index, 1);
  1600. };
  1601. const handleReturnRemove = (index: number) => {
  1602. returnDeviceList.value.splice(index, 1);
  1603. };
  1604. // 处理表格多选变化
  1605. const handleSelectionChange = (selection: DeviceListItem[]) => {
  1606. selectedDevices.value = selection;
  1607. };
  1608. // 设备选择处理
  1609. function onDeviceSelected(devices: Device[]) {
  1610. console.log("devices:::",devices,"activeName:::",activeName.value);
  1611. if (activeName.value === 'first') {
  1612. // 获取已存在的设备ID列表
  1613. const existingIds = new Set(form.value.items.map((item: any) => item.device).filter(Boolean));
  1614. // 过滤掉已存在的设备,只添加新的
  1615. const newDevices = devices.filter((d) => !existingIds.has(d.id));
  1616. if (newDevices.length === 0) {
  1617. ElMessage.warning('所选设备已存在,未添加重复项');
  1618. return;
  1619. }
  1620. // 如果有部分重复,提示用户
  1621. if (newDevices.length < devices.length) {
  1622. ElMessage.warning(`已过滤 ${devices.length - newDevices.length} 个重复的设备`);
  1623. }
  1624. form.value.items.push(
  1625. ...newDevices.map((d) => ({
  1626. device: d.id,
  1627. device_code: d.code,
  1628. remark: d.category_name,
  1629. quantity: 1,
  1630. location: d.warehouse,
  1631. device_name: d.name,
  1632. device_specification: d.device_specification
  1633. }))
  1634. );
  1635. } else if (activeName.value === 'second') {
  1636. // 获取已存在的设备ID列表
  1637. const existingIds = new Set(deviceList.value.map((item: any) => item.device_no).filter(Boolean));
  1638. // 过滤掉已存在的设备,只添加新的
  1639. const newDevices = devices.filter((d) => !existingIds.has(d.id));
  1640. if (newDevices.length === 0) {
  1641. ElMessage.warning('所选设备已存在,未添加重复项');
  1642. return;
  1643. }
  1644. // 如果有部分重复,提示用户
  1645. if (newDevices.length < devices.length) {
  1646. ElMessage.warning(`已过滤 ${devices.length - newDevices.length} 个重复的设备`);
  1647. }
  1648. deviceList.value.push(
  1649. ...newDevices.map(d => ({
  1650. device_no: d.id,
  1651. device_code: d.code,
  1652. device_type: d.category_name,
  1653. device_name: d.name || '',
  1654. borrow_count: 1,
  1655. brand: d.brand || '',
  1656. model: d.model,
  1657. device_specification: d.specification,
  1658. warehouse: d.warehouse,
  1659. available_quantity:d.available_quantity
  1660. }))
  1661. );
  1662. }else if(activeName.value === 'third'){
  1663. // 在添加前检查每个设备是否已存在
  1664. devices.forEach(d => {
  1665. const existingDeviceIndex = returnDeviceList.value.findIndex(item => item.device_no === d.id);
  1666. if (existingDeviceIndex !== -1) {
  1667. // 如果设备已存在,更新其状态
  1668. returnDeviceList.value[existingDeviceIndex].return_status.value = 2;
  1669. returnDeviceList.value[existingDeviceIndex].return_status.label = '已归还';
  1670. returnDeviceList.value[existingDeviceIndex].is_return = true;
  1671. returnDeviceList.value[existingDeviceIndex].return_time = dayjs().format('YYYY-MM-DD HH:mm:ss');
  1672. returnDeviceList.value[existingDeviceIndex].brand = d.brand || '';
  1673. ElMessage.success(`设备 ${d.category_name} 状态已更新为已归还`);
  1674. } else {
  1675. // 如果设备不存在,添加新记录
  1676. /* returnDeviceList.value.push({
  1677. device_no: d.id,
  1678. device_type: d.category_name,
  1679. device_name: d.name || '',
  1680. borrow_count: 1,
  1681. brand: d.brand || '',
  1682. model: d.model || '',
  1683. warehouse: d.warehouse,
  1684. status: '已归还'
  1685. }); */
  1686. /* ElMessage.success(`设备 ${d.category_name} 已添加到归还列表`); */
  1687. }
  1688. });
  1689. }
  1690. }
  1691. // 移除同步相关的 watch
  1692. // const handleClick = (tab: TabsPaneContext, event: Event) => {
  1693. // if (tab.props.name === 'second') {
  1694. // deviceList.value = form.value.items.map(item => ({
  1695. // device_no: item.device,
  1696. // device_type: item.remark,
  1697. // device_name: '',
  1698. // borrow_count: item.quantity,
  1699. // brand: '',
  1700. // model: '',
  1701. // warehouse: item.location
  1702. // }));
  1703. // }
  1704. // }
  1705. function handleClick(tab: TabsPaneContext) {
  1706. console.log('Tab clicked:', tab.props.name);
  1707. // 当切换到设备清单标签页时,自动聚焦输入框
  1708. if (tab.props.name === 'second') {
  1709. nextTick(() => {
  1710. if (searchInputRef.value) {
  1711. searchInputRef.value.focus();
  1712. searchInputRef.value.select();
  1713. }
  1714. });
  1715. } else if (tab.props.name === 'third') {
  1716. // 当切换到归还设备清单标签页时,自动聚焦输入框
  1717. nextTick(() => {
  1718. returnSearchInputRef.value?.focus?.();
  1719. });
  1720. }
  1721. }
  1722. function handleUploadSuccess(response: any, file: any, fileList: any) {
  1723. // eslint-disable-next-line no-console
  1724. console.log('上传成功:', file.name);
  1725. }
  1726. function handleUploadError(err: any, file: any, fileList: any) {
  1727. // eslint-disable-next-line no-console
  1728. console.error('上传失败:', file.name);
  1729. }
  1730. // 图片预览处理函数
  1731. const handlePictureCardPreview = (uploadFile: any) => {
  1732. dialogImageUrl.value = uploadFile.url || uploadFile.response?.url || form.value.attachments;
  1733. dialogVisible.value = true;
  1734. };
  1735. // 处理附件删除
  1736. const handleRemoves = (file: any, fileList: any) => {
  1737. // 由于是禁用状态,这里只是示例,实际不会执行删除
  1738. console.log('删除附件:', file.name);
  1739. };
  1740. const form = ref<FormData>({
  1741. borrower_info:{
  1742. user_code:'',
  1743. mobile:''
  1744. },
  1745. application_no: '',
  1746. emergency_contact: '',
  1747. emergency_phone: '',
  1748. app_user_borrower: { id: '', name: '', user_code: '', mobile: '' },
  1749. team_type: 0,
  1750. team_members_count: 1,
  1751. team_members: '',
  1752. expected_start_time:'',
  1753. expected_end_time: '',
  1754. borrower_dept: '',
  1755. purpose: '',
  1756. applicant_remark_type: "1",
  1757. applicant_remark: '',
  1758. items: [],
  1759. accessory_info: '',
  1760. borrow_type: 2,
  1761. return_location:'',
  1762. approver_info: {
  1763. name: ''
  1764. }
  1765. });
  1766. const showSelectDeviceDialog = ref(false);
  1767. const rules = {
  1768. emergency_contact: [
  1769. { required: true, message: '请输入借用人', trigger: 'change' },
  1770. ],
  1771. app_user_borrower: [
  1772. { required: true, message: '请输入学号或工号', trigger: 'change' },
  1773. ],
  1774. emergency_phone: [
  1775. { required: true, message: '请输入联系电话', trigger: 'change' },
  1776. ],
  1777. /* borrower_dept: [
  1778. { required: true, message: '请输入借用部门', trigger: 'change' },
  1779. ], */
  1780. expected_end_time: [
  1781. { required: true, message: '请输选择归还时间', trigger: 'change' },
  1782. ],
  1783. team_type: [
  1784. { required: true, message: '请选择个人或者团队', trigger: 'change' },
  1785. ],
  1786. purpose: [
  1787. { required: true, message: '请输入借用原因', trigger: 'change' },
  1788. ],
  1789. applicant_remark_type: [
  1790. { required: true, message: '请选择审批人', trigger: 'change' },
  1791. ],
  1792. applicant_remark: [
  1793. { required: false, message: '请输入备注', trigger: 'change' },
  1794. ],
  1795. };
  1796. // 自动填充用户信息
  1797. onMounted(() => {
  1798. loadWarehouses()
  1799. getAdminList()
  1800. getAllUserList()
  1801. try {
  1802. const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
  1803. if (userInfo) {
  1804. form.value.emergency_contact = userInfo.username || '';
  1805. // 借用人从用户列表加载完成后再统一回显/设置
  1806. form.value.emergency_phone = userInfo.mobile || '';
  1807. form.value.borrower_dept = userInfo.dept_info?.dept_name || '';
  1808. }
  1809. } catch (e) {
  1810. /* empty */
  1811. }
  1812. });
  1813. watch(
  1814. () => props.visible,
  1815. (v) => {
  1816. visible.value = v;
  1817. // 当对话框打开且当前标签页是设备清单时,自动聚焦输入框
  1818. if (v && activeName.value === 'second' && isCollect.value) {
  1819. nextTick(() => {
  1820. if (searchInputRef.value) {
  1821. searchInputRef.value.focus();
  1822. searchInputRef.value.select();
  1823. }
  1824. });
  1825. } else if (v && activeName.value === 'third' && !isView.value) {
  1826. // 当对话框打开且当前标签页是归还设备清单时,自动聚焦输入框
  1827. nextTick(() => {
  1828. returnSearchInputRef.value?.focus?.();
  1829. });
  1830. }
  1831. // 清理RFID缓冲状态
  1832. if (v) {
  1833. rfidInputBuffer.value = '';
  1834. clearRFIDInputTimer();
  1835. consecutiveInputCount = 0;
  1836. lastInputTime = 0;
  1837. // 清理归还流程RFID缓冲状态
  1838. returnRfidInputBuffer.value = '';
  1839. clearReturnRFIDInputTimer();
  1840. returnConsecutiveInputCount = 0;
  1841. returnLastInputTime = 0;
  1842. }
  1843. }
  1844. );
  1845. watch(() => props.modelValue, async (val) => {
  1846. if (val && val.id) {
  1847. try {
  1848. const res = await api.GetApplicationDetail(val.id);
  1849. console.log("res:::::",res)
  1850. if (res.code === 2000 && res.data) {
  1851. const data = res.data;
  1852. Object.assign(form.value, data);
  1853. buildSteps(data);
  1854. // 领取模式下不回显历史配件内容
  1855. if (isCollect.value) {
  1856. collectAccessories.value = '';
  1857. }
  1858. timeline.value.create = data.create_datetime ? dayjs(data.create_datetime).format('YYYY-MM-DD HH:mm:ss') : '';
  1859. timeline.value.approve = data.approve_time || '';
  1860. const approverinfo = data.approver_info ?? data.admin_approver_info
  1861. timeline.value.approver = approverinfo?.name || '审批人'
  1862. // timeline.value.approver = data.admin_approver_info?.name || '审批人';
  1863. // console.log("data:::",form.value);
  1864. // activeStep.value = 1; // 创建
  1865. // if (data.status >= 1) activeStep.value = 2; // 待审批
  1866. // if (data.status >= 2) activeStep.value = 3; // 审批通过
  1867. // if (data.status >= 3) activeStep.value = 4; // 完成
  1868. // 同步 form.items 到 returnDeviceList
  1869. if (form.value.items && form.value.items.length > 0) {
  1870. returnDeviceList.value = form.value.items
  1871. .filter(item => item.device !== null)
  1872. .map(item => ({
  1873. device_no: item.device,
  1874. device_code: item.device_code,
  1875. device_type: item.remark,
  1876. device_name: item.device_name,
  1877. borrow_count: item.quantity,
  1878. brand: item.brand ||item.device_brand || '',
  1879. model: item.device_specification,
  1880. warehouse: item.location,
  1881. problem_records: item.problem_records,
  1882. /* status: '未归还' */
  1883. return_status:item.return_status,
  1884. return_time:item.return_times
  1885. }));
  1886. }
  1887. // 同步 form.records 到 returnAbnormalList
  1888. if (form.value.records && form.value.records.length > 0) {
  1889. // 添加损坏类型记录
  1890. const damageRecord: AbnormalRecord = {
  1891. application_no: form.value.application_no,
  1892. operator_name: form.value.app_user_borrower?.name||form.value.external_borrower_name||'',
  1893. user_code: form.value.borrower_info?.user_code || form.value.app_user_borrower?.user_code||'',
  1894. emergency_phone: form.value.external_borrower_phone || form.value.app_user_borrower?.mobile||'',
  1895. type: '损坏',
  1896. condition: form.value.borrower_damage_count || 0,
  1897. create_time: dayjs().format('YYYY-MM-DD HH:mm:ss')
  1898. };
  1899. // 添加逾期类型记录
  1900. const overdueRecord: AbnormalRecord = {
  1901. application_no: form.value.application_no,
  1902. operator_name: form.value.app_user_borrower?.name||form.value.external_borrower_name||'',
  1903. user_code: form.value.borrower_info?.user_code || form.value.app_user_borrower?.user_code||'',
  1904. emergency_phone: form.value.external_borrower_phone || form.value.app_user_borrower?.mobile||'',
  1905. type: '逾期',
  1906. condition: form.value.borrower_overdue_count || 0, // 逾期数量设为1
  1907. create_time: dayjs().format('YYYY-MM-DD HH:mm:ss')
  1908. };
  1909. returnAbnormalList.value = [damageRecord, overdueRecord];
  1910. abnormalList.value = [damageRecord, overdueRecord];
  1911. }
  1912. // 处理附件回显
  1913. if (data.attachments) {
  1914. if (typeof data.attachments === 'string') {
  1915. // 如果attachments是字符串URL
  1916. attachmentFileList.value = [{
  1917. name: data.attachments.split('/').pop() || '附件',
  1918. url: data.attachments,
  1919. status: 'success'
  1920. }];
  1921. } else if (Array.isArray(data.attachments)) {
  1922. // 如果attachments是数组
  1923. attachmentFileList.value = data.attachments.map((item: any, index: number) => {
  1924. if (typeof item === 'string') {
  1925. return {
  1926. name: item.split('/').pop() || `附件${index + 1}`,
  1927. url: item,
  1928. status: 'success'
  1929. };
  1930. } else if (typeof item === 'object' && item.url) {
  1931. return {
  1932. name: item.name || item.url.split('/').pop() || `附件${index + 1}`,
  1933. url: item.url,
  1934. status: 'success'
  1935. };
  1936. }
  1937. return null;
  1938. }).filter(Boolean);
  1939. }
  1940. }
  1941. }
  1942. } catch (e) {
  1943. console.log("审判失败:::",e)
  1944. ElMessage.error('获取审批信息失败');
  1945. }
  1946. }else if(val&& isEdit.value){
  1947. try {
  1948. const res = await api.GetApplicationDetail(val.id);
  1949. if (res.code === 2000 && res.data) {
  1950. const data = res.data;
  1951. Object.assign(form.value, data);
  1952. // 同步 form.items 到 returnDeviceList
  1953. if (form.value.items && form.value.items.length > 0) {
  1954. returnDeviceList.value = form.value.items
  1955. .filter(item => item.device !== null)
  1956. .map(item => ({
  1957. device_no: item.device,
  1958. device_type: item.remark,
  1959. device_name: item.device_name,
  1960. borrow_count: item.quantity,
  1961. brand: item.brand ||item.device_brand || '',
  1962. model: item.device_specification,
  1963. warehouse: item.location,
  1964. problem_records: item.problem_records,
  1965. return_status:item.return_status,
  1966. return_time:item.return_times
  1967. }));
  1968. }
  1969. // 处理附件回显 (编辑模式)
  1970. if (data.attachments) {
  1971. if (typeof data.attachments === 'string') {
  1972. attachmentFileList.value = [{
  1973. name: data.attachments.split('/').pop() || '附件',
  1974. url: data.attachments,
  1975. status: 'success'
  1976. }];
  1977. } else if (Array.isArray(data.attachments)) {
  1978. attachmentFileList.value = data.attachments.map((item: any, index: number) => {
  1979. if (typeof item === 'string') {
  1980. return {
  1981. name: item.split('/').pop() || `附件${index + 1}`,
  1982. url: item,
  1983. status: 'success'
  1984. };
  1985. } else if (typeof item === 'object' && item.url) {
  1986. return {
  1987. name: item.name || item.url.split('/').pop() || `附件${index + 1}`,
  1988. url: item.url,
  1989. status: 'success'
  1990. };
  1991. }
  1992. return null;
  1993. }).filter(Boolean);
  1994. }
  1995. }
  1996. }
  1997. } catch (e) {
  1998. ElMessage.error('获取审批信息失败');
  1999. }
  2000. }
  2001. }, { immediate: true });
  2002. // watch(
  2003. // () => props.modelValue,
  2004. // (v) => {
  2005. // if (v) {
  2006. // // eslint-disable-next-line no-console
  2007. // console.log("val:::::",v);
  2008. // Object.assign(form.value, v);
  2009. // }
  2010. // }, { immediate: true }
  2011. // );
  2012. // 监听借用人选择变化
  2013. watch(() => form.value.app_user_borrower?.id, (newId) => {
  2014. if (!newId || allUserList.value.length === 0) return;
  2015. const selectedUser: any = allUserList.value.find((u: any) => String(u.id) === String(newId));
  2016. if (selectedUser) {
  2017. form.value.app_user_borrower = {
  2018. id: selectedUser.id,
  2019. name: selectedUser.name,
  2020. user_code: selectedUser.user_code || '',
  2021. mobile: selectedUser.mobile || ''
  2022. };
  2023. }
  2024. });
  2025. function addItem() {
  2026. if (activeName.value === 'third' && allReturned.value) {
  2027. ElMessage.info('当前列表设备均已归还,无可选择设备');
  2028. return;
  2029. }
  2030. showSelectDeviceDialog.value = true;
  2031. }
  2032. function removeItem(index: number) {
  2033. form.value.items.splice(index, 1);
  2034. }
  2035. function onCancel() {
  2036. // 重置标签页到第一个
  2037. activeName.value = 'first';
  2038. // 清理异常记录列表
  2039. returnAbnormalList.value = [];
  2040. abnormalList.value = [];
  2041. // 清理设备列表
  2042. deviceList.value = [];
  2043. returnDeviceList.value = [];
  2044. // 清理搜索框
  2045. searchText.value = '';
  2046. returnSearchText.value = '';
  2047. // 清理备注
  2048. remark.value = '';
  2049. // 清理领取模式配件信息
  2050. collectAccessories.value = '';
  2051. returnRemark.value = '';
  2052. // 重置状态
  2053. returnStatus.value = '未归还';
  2054. // 清理选中的设备
  2055. selectedDevices.value = [];
  2056. // 清理附件文件列表
  2057. attachmentFileList.value = [];
  2058. // 清理RFID缓冲
  2059. rfidInputBuffer.value = '';
  2060. clearRFIDInputTimer();
  2061. consecutiveInputCount = 0;
  2062. lastInputTime = 0;
  2063. // 清理归还流程RFID缓冲
  2064. returnRfidInputBuffer.value = '';
  2065. clearReturnRFIDInputTimer();
  2066. returnConsecutiveInputCount = 0;
  2067. returnLastInputTime = 0;
  2068. // 关闭所有对话框
  2069. showSelectDeviceDialog.value = false;
  2070. showSettlementDialog.value = false;
  2071. showAbnormalDialog.value = false;
  2072. showViewAbnormalDialog.value = false;
  2073. // 重置当前异常数据
  2074. currentAbnormalData.value = {
  2075. damage_type: 0,
  2076. condition: '',
  2077. photos: []
  2078. };
  2079. // 发出关闭事件
  2080. emit('update:visible', false);
  2081. }
  2082. // 提交时合并数据
  2083. function onSubmit() {
  2084. if(isCollect.value){
  2085. // 验证是否有设备信息
  2086. const hasDevicesInFirstTab = form.value.items && form.value.items.length > 0;
  2087. const hasDevicesInSecondTab = deviceList.value && deviceList.value.length > 0;
  2088. if (!hasDevicesInFirstTab && !hasDevicesInSecondTab) {
  2089. ElMessage.warning('请添加设备信息后再提交');
  2090. activeName.value = 'second'; // 自动切换到借用列表标签页
  2091. return;
  2092. }
  2093. // 验证设备数量
  2094. const hasInvalidQuantityInFirstTab = form.value.items.some(item => !item.quantity || item.quantity <= 0);
  2095. const hasInvalidQuantityInSecondTab = deviceList.value.some(item => !item.borrow_count || item.borrow_count <= 0);
  2096. if (hasInvalidQuantityInFirstTab || hasInvalidQuantityInSecondTab) {
  2097. ElMessage.warning('请确保所有设备的数量大于0');
  2098. activeName.value = 'second'; // 自动切换到借用列表标签页
  2099. return;
  2100. }
  2101. // 合并两个标签页的设备数据
  2102. const allDevices = [
  2103. ...deviceList.value.map(item => ({
  2104. condition: remark.value,
  2105. quantity: item.borrow_count,
  2106. device_id: Number(item.device_no) || 0
  2107. }))
  2108. ];
  2109. const submitData = {
  2110. id:form.value.id,
  2111. item_id:form.value.id,
  2112. device_assignments: allDevices,
  2113. remark:remark.value,
  2114. pre_borrow_accessories: collectAccessories.value,
  2115. };
  2116. CollectEquipment(submitData).then((res: ApiResponse) => {
  2117. if(res.code===2000){
  2118. ElMessage.success("领取成功");
  2119. // 准备结算单数据
  2120. settlementData.value = {
  2121. ...form.value,
  2122. items: deviceList.value.map(item => ({
  2123. ...item,
  2124. device_category_name: item.device_type,
  2125. device_name: item.device_name,
  2126. device_specification: item.device_specification,
  2127. device_storage_location: item.warehouse,
  2128. remark: item.device_type,
  2129. quantity: item.borrow_count,
  2130. device_id: item.device_no,
  2131. device_category: item.device_type,
  2132. device_code: item.device_no,
  2133. borrow_type: form.value.borrow_type,
  2134. borrower_dept: form.value.borrower_dept,
  2135. emergency_contact: form.value.emergency_contact,
  2136. emergency_phone: form.value.emergency_phone,
  2137. team_type: form.value.team_type,
  2138. })),
  2139. accessory_info: accessoryInfo.value,
  2140. pre_borrow_accessories:collectAccessories.value,
  2141. remark: remark.value,
  2142. expected_start_time: dayjs().format('YYYY-MM-DD HH:mm:ss')
  2143. };
  2144. // 先触发提交事件进行页面刷新
  2145. emit('submit');
  2146. // 关闭当前对话框
  2147. emit('update:visible', false);
  2148. // 使用 nextTick 确保在 DOM 更新后再显示结算单
  2149. nextTick(() => {
  2150. borrowForm.value = {...settlementData.value,mode: 'settlement' };
  2151. showSettlementDialog.value = true;
  2152. });
  2153. }
  2154. });
  2155. } else {
  2156. // 归还逻辑修改
  2157. // 使用所有设备列表而不是选中的设备
  2158. const allDevices = returnDeviceList.value
  2159. .filter(item => item.is_return === true) // 只选择 return_status 为 2 的设备
  2160. .map(item => ({
  2161. device: item.device_no,
  2162. quantity: item.borrow_count,
  2163. remark: returnRemark.value,
  2164. device_id: Number(item.device_no) || 0,
  2165. // 添加异常相关字段
  2166. condition: item.condition, // 异常说明
  2167. damage_type:item.damage_type,
  2168. photo_urls: item.photos || [], // 异常照片
  2169. }));
  2170. const submitData = {
  2171. id: form.value.id,
  2172. items: allDevices,
  2173. status: returnStatus.value,
  2174. pre_borrow_accessories: form.value.pre_borrow_accessories,
  2175. };
  2176. ReturnEquipment(submitData).then(async (res: ApiResponse) => {
  2177. if(res.code===2000){
  2178. ElMessage.success("归还成功");
  2179. try {
  2180. if (form.value.id) {
  2181. const detailRes = await api.GetApplicationDetail(form.value.id);
  2182. if (detailRes.code === 2000 && detailRes.data) {
  2183. returnStatus.value = detailRes.data.status_label || returnStatus.value;
  2184. }
  2185. }
  2186. } catch (e) {
  2187. ElMessage.error('获取最新状态失败');
  2188. }
  2189. emit('submit');
  2190. emit('update:visible', false);
  2191. }
  2192. });
  2193. }
  2194. }
  2195. function clickReturn(){
  2196. returnStatus.value = '已归还';
  2197. // 自动选择所有设备
  2198. const tableRef = document.querySelector('.el-table__header-wrapper th.el-table__cell.is-leaf .el-checkbox');
  2199. if (tableRef) {
  2200. (tableRef as HTMLElement).click();
  2201. }
  2202. }
  2203. // 组件卸载时清理定时器和状态
  2204. onBeforeUnmount(() => {
  2205. clearRFIDInputTimer();
  2206. rfidInputBuffer.value = '';
  2207. consecutiveInputCount = 0;
  2208. lastInputTime = 0;
  2209. // 清理归还流程定时器和状态
  2210. clearReturnRFIDInputTimer();
  2211. returnRfidInputBuffer.value = '';
  2212. returnConsecutiveInputCount = 0;
  2213. returnLastInputTime = 0;
  2214. });
  2215. /* formRef.value.validate((valid: boolean) => {
  2216. if (valid) {
  2217. form.value.items = form.value.items.map(item => ({
  2218. ...item,
  2219. quantity: Number(item.quantity) || 0
  2220. }));
  2221. const firstLocation = form.value.items[0]?.location || '';
  2222. form.value.return_location = firstLocation;
  2223. form.value.expected_start_time = now;
  2224. form.value.expected_end_time = dayjs(form.value.expected_end_time).format('YYYY-MM-DD HH:mm:ss')|| now;
  2225. form.team_members_count=Number(form.team_members_count)||1;
  2226. emit('submit', { ...form.value });
  2227. emit('update:visible', false);
  2228. ElMessage.success("添加成功!");
  2229. }
  2230. }); */
  2231. </script>
  2232. <style>
  2233. .avatar-uploader .el-upload {
  2234. border: 1px dashed #d9d9d9;
  2235. border-radius: 6px;
  2236. cursor: pointer;
  2237. position: relative;
  2238. overflow: hidden;
  2239. }
  2240. .avatar-uploader .el-upload:hover {
  2241. border-color: #409EFF;
  2242. }
  2243. .avatar-uploader-icon {
  2244. font-size: 28px;
  2245. color: #8c939d;
  2246. width: 178px;
  2247. height: 178px;
  2248. line-height: 178px;
  2249. text-align: center;
  2250. }
  2251. .avatar {
  2252. width: 178px;
  2253. height: 178px;
  2254. display: block;
  2255. }
  2256. .device-code-input {
  2257. padding: 8px 12px;
  2258. font-size: 14px;
  2259. border: 1px solid #dcdfe6;
  2260. border-radius: 4px;
  2261. outline: none;
  2262. resize: vertical;
  2263. transition: all 0.3s;
  2264. font-family: 'Microsoft YaHei', sans-serif;
  2265. line-height: 1.5;
  2266. }
  2267. .device-code-input:focus {
  2268. border-color: #409EFF;
  2269. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
  2270. }
  2271. .device-code-input:disabled {
  2272. background-color: #f5f7fa;
  2273. cursor: not-allowed;
  2274. }
  2275. </style>