index.vue 125 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833
  1. <template>
  2. <div class="p-2 page-container">
  3. <!-- 搜索区域 -->
  4. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  5. <div v-show="showSearch" class="mb-[10px]">
  6. <el-card shadow="hover">
  7. <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
  8. <!-- 第一行:4个搜索条件 -->
  9. <el-row :gutter="20">
  10. <el-col :span="6">
  11. <el-form-item label="工单编号" prop="orderNo">
  12. <el-input v-model="queryParams.orderNo" placeholder="请输入工单编号" clearable @keyup.enter="handleQuery" />
  13. </el-form-item>
  14. </el-col>
  15. <el-col :span="6">
  16. <el-form-item label="报修标题" prop="title">
  17. <el-input v-model="queryParams.title" placeholder="请输入报修标题" clearable @keyup.enter="handleQuery" />
  18. </el-form-item>
  19. </el-col>
  20. <el-col :span="6">
  21. <el-form-item label="报修地点" prop="repairLocation">
  22. <el-input v-model="queryParams.repairLocation" placeholder="请输入报修地点" clearable @keyup.enter="handleQuery" />
  23. </el-form-item>
  24. </el-col>
  25. <el-col :span="6">
  26. <el-form-item label="维修类型" prop="categoryId">
  27. <el-tree-select
  28. v-model="queryParams.categoryId"
  29. :data="categoryTreeOptions"
  30. :props="{ value: 'id', label: 'categoryName', children: 'children' }"
  31. value-key="id"
  32. placeholder="请选择维修类型"
  33. check-strictly
  34. :render-after-expand="false"
  35. clearable
  36. filterable
  37. style="width: 100%"
  38. />
  39. </el-form-item>
  40. </el-col>
  41. </el-row>
  42. <!-- 第二行:4个搜索条件 + 操作按钮 -->
  43. <el-row :gutter="20">
  44. <el-col :span="6">
  45. <el-form-item label="紧急程度" prop="priorityLevel">
  46. <el-select v-model="queryParams.priorityLevel" placeholder="请选择紧急程度" clearable style="width: 100%">
  47. <el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  48. </el-select>
  49. </el-form-item>
  50. </el-col>
  51. <el-col :span="6">
  52. <el-form-item label="工单状态" prop="orderStatus">
  53. <el-select v-model="queryParams.orderStatus" placeholder="请选择工单状态" clearable style="width: 100%">
  54. <el-option v-for="dict in pm_order_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  55. </el-select>
  56. </el-form-item>
  57. </el-col>
  58. <el-col :span="6">
  59. <el-form-item label="报修人" prop="reporterName">
  60. <el-input v-model="queryParams.reporterName" placeholder="请输入报修人姓名" clearable @keyup.enter="handleQuery" />
  61. </el-form-item>
  62. </el-col>
  63. <el-col :span="6">
  64. <el-form-item label="创建时间">
  65. <el-date-picker
  66. v-model="daterangeCreateTime"
  67. value-format="YYYY-MM-DD"
  68. type="daterange"
  69. range-separator="-"
  70. start-placeholder="开始日期"
  71. end-placeholder="结束日期"
  72. @change="handleQuery"
  73. style="width: 100%"
  74. ></el-date-picker>
  75. </el-form-item>
  76. </el-col>
  77. </el-row>
  78. <!-- 操作按钮行 -->
  79. <el-row>
  80. <el-col :span="24" class="search-btn-container">
  81. <el-form-item>
  82. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  83. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  84. </el-form-item>
  85. </el-col>
  86. </el-row>
  87. </el-form>
  88. </el-card>
  89. </div>
  90. </transition>
  91. <!-- 统计卡片区域 - 仅维护管理员和超级管理员可见 -->
  92. <!-- <div v-if="canViewStatistics" class="mb-[10px]">
  93. <el-row :gutter="16">
  94. <el-col :span="3">
  95. <el-card shadow="hover" class="stat-card stat-card-primary">
  96. <div class="stat-content">
  97. <div class="stat-icon">
  98. <el-icon><Files /></el-icon>
  99. </div>
  100. <div class="stat-info">
  101. <div class="stat-value">{{ statistics.totalOrders }}</div>
  102. <div class="stat-label">总工单数</div>
  103. </div>
  104. </div>
  105. </el-card>
  106. </el-col>
  107. <el-col :span="3">
  108. <el-card shadow="hover" class="stat-card stat-card-warning">
  109. <div class="stat-content">
  110. <div class="stat-icon">
  111. <el-icon><Clock /></el-icon>
  112. </div>
  113. <div class="stat-info">
  114. <div class="stat-value">{{ statistics.pendingAssign }}</div>
  115. <div class="stat-label">待分派</div>
  116. </div>
  117. </div>
  118. </el-card>
  119. </el-col>
  120. <el-col :span="3">
  121. <el-card shadow="hover" class="stat-card stat-card-info">
  122. <div class="stat-content">
  123. <div class="stat-icon">
  124. <el-icon><Loading /></el-icon>
  125. </div>
  126. <div class="stat-info">
  127. <div class="stat-value">{{ statistics.inProgress }}</div>
  128. <div class="stat-label">进行中</div>
  129. </div>
  130. </div>
  131. </el-card>
  132. </el-col>
  133. <el-col :span="3">
  134. <el-card shadow="hover" class="stat-card stat-card-success">
  135. <div class="stat-content">
  136. <div class="stat-icon">
  137. <el-icon><CircleCheck /></el-icon>
  138. </div>
  139. <div class="stat-info">
  140. <div class="stat-value">{{ statistics.completed }}</div>
  141. <div class="stat-label">已完成</div>
  142. </div>
  143. </div>
  144. </el-card>
  145. </el-col>
  146. <el-col :span="3">
  147. <el-card shadow="hover" class="stat-card stat-card-info">
  148. <div class="stat-content">
  149. <div class="stat-icon">
  150. <el-icon><Timer /></el-icon>
  151. </div>
  152. <div class="stat-info">
  153. <div class="stat-value">{{ statistics.avgProcessTime }}</div>
  154. <div class="stat-label">平均时长(小时)</div>
  155. </div>
  156. </div>
  157. </el-card>
  158. </el-col>
  159. <el-col :span="3">
  160. <el-card shadow="hover" class="stat-card stat-card-primary">
  161. <div class="stat-content">
  162. <div class="stat-icon">
  163. <el-icon><Star /></el-icon>
  164. </div>
  165. <div class="stat-info">
  166. <div class="stat-value">{{ statistics.avgEvaluationScore }}</div>
  167. <div class="stat-label">平均评分</div>
  168. </div>
  169. </div>
  170. </el-card>
  171. </el-col>
  172. <el-col :span="3">
  173. <el-card shadow="hover" class="stat-card stat-card-success">
  174. <div class="stat-content">
  175. <div class="stat-icon">
  176. <el-icon><TrendCharts /></el-icon>
  177. </div>
  178. <div class="stat-info">
  179. <div class="stat-value">{{ statistics.monthlyNewOrders }}</div>
  180. <div class="stat-label">本月新增</div>
  181. </div>
  182. </div>
  183. </el-card>
  184. </el-col>
  185. <el-col :span="3">
  186. <el-card shadow="hover" class="stat-card stat-card-warning">
  187. <div class="stat-content">
  188. <div class="stat-icon">
  189. <el-icon><Finished /></el-icon>
  190. </div>
  191. <div class="stat-info">
  192. <div class="stat-value">{{ statistics.monthlyCompletedOrders }}</div>
  193. <div class="stat-label">本月完成</div>
  194. </div>
  195. </div>
  196. </el-card>
  197. </el-col>
  198. </el-row>
  199. </div> -->
  200. <!-- 表格区域 -->
  201. <el-card shadow="never" class="button-card table-card">
  202. <template #header>
  203. <el-row :gutter="10" class="mb8">
  204. <el-col :span="1.5">
  205. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['pm:repairOrder:add']">新增报修</el-button>
  206. </el-col>
  207. <el-col :span="1.5">
  208. <el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['pm:repairOrder:edit']">修改</el-button>
  209. </el-col>
  210. <el-col :span="1.5">
  211. <el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']">删除</el-button>
  212. </el-col>
  213. <el-col :span="1.5">
  214. <el-button icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
  215. </el-col>
  216. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  217. </el-row>
  218. </template>
  219. <div class="table-container">
  220. <el-table
  221. v-loading="loading"
  222. border
  223. :data="repairOrderList"
  224. @selection-change="handleSelectionChange"
  225. @row-dblclick="handleDetail"
  226. style="width: 100%"
  227. :scroll-x="true"
  228. class="scroll-table"
  229. >
  230. <el-table-column type="selection" width="55" align="center" fixed="left" />
  231. <el-table-column label="工单编号" align="center" prop="orderNo" width="140" show-overflow-tooltip fixed="left" />
  232. <el-table-column label="报修标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
  233. <el-table-column label="报修地点" align="center" prop="repairLocation" min-width="180" show-overflow-tooltip />
  234. <el-table-column label="维修类型" align="center" prop="categoryName" width="120" show-overflow-tooltip>
  235. <template #default="scope">
  236. {{ getCategoryNameById(scope.row.categoryId) }}
  237. </template>
  238. </el-table-column>
  239. <el-table-column label="紧急程度" align="center" prop="priorityLevel" width="100">
  240. <template #default="scope">
  241. <el-tag :type="getPriorityTagType(scope.row.priorityLevel) as any" size="small">
  242. {{ getPriorityText(scope.row.priorityLevel) }}
  243. </el-tag>
  244. </template>
  245. </el-table-column>
  246. <el-table-column label="报修人" align="center" prop="reporterName" width="100" show-overflow-tooltip />
  247. <el-table-column label="报修人电话" align="center" prop="reporterPhone" width="120" show-overflow-tooltip />
  248. <el-table-column label="工单状态" align="center" prop="orderStatus" width="100">
  249. <template #default="scope">
  250. <el-tag :type="getStatusTagType(scope.row.orderStatus) as any" size="small">
  251. {{ getStatusText(scope.row.orderStatus) }}
  252. </el-tag>
  253. </template>
  254. </el-table-column>
  255. <el-table-column label="分派维修员" align="center" prop="assigneeName" width="120" show-overflow-tooltip />
  256. <el-table-column label="期望完成时间" align="center" prop="expectedFinishTime" width="180">
  257. <template #default="scope">
  258. <span v-if="scope.row.expectedFinishTime">{{ proxy.parseTime(scope.row.expectedFinishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
  259. <span v-else>-</span>
  260. </template>
  261. </el-table-column>
  262. <el-table-column label="实际完成时间" align="center" prop="finishTime" width="180">
  263. <template #default="scope">
  264. <span v-if="scope.row.finishTime">{{ proxy.parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
  265. <span v-else>-</span>
  266. </template>
  267. </el-table-column>
  268. <el-table-column label="处理时长" align="center" prop="totalDuration" width="100">
  269. <template #default="scope">
  270. <span v-if="scope.row.totalDuration">{{ formatDuration(scope.row.totalDuration) }}</span>
  271. <span v-else>-</span>
  272. </template>
  273. </el-table-column>
  274. <el-table-column label="评价得分" align="center" prop="evaluationScore" width="200">
  275. <template #default="scope">
  276. <el-rate
  277. v-if="scope.row.evaluationScore"
  278. :model-value="scope.row.evaluationScore"
  279. disabled
  280. show-score
  281. text-color="#ff9900"
  282. score-template="{value}分"
  283. />
  284. <span v-else>-</span>
  285. </template>
  286. </el-table-column>
  287. <el-table-column label="附件" align="center" prop="attachmentCount" width="80">
  288. <template #default="scope">
  289. <el-link v-if="scope.row.attachmentCount > 0" @click="handleDetail(scope.row)" type="primary" :underline="false">
  290. <el-icon><Paperclip /></el-icon>{{ scope.row.attachmentCount }}
  291. </el-link>
  292. <span v-else>-</span>
  293. </template>
  294. </el-table-column>
  295. <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  296. <template #default="scope">
  297. <span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
  298. </template>
  299. </el-table-column>
  300. <el-table-column label="操作" align="center" width="280" fixed="right" class-name="small-padding fixed-width">
  301. <template #default="scope">
  302. <!-- 待分派状态 (1) - 可以编辑、分派、删除 -->
  303. <template v-if="scope.row.orderStatus === '1' || scope.row.orderStatus === 1">
  304. <el-tooltip content="编辑" placement="top">
  305. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['pm:repairOrder:edit']"></el-button>
  306. </el-tooltip>
  307. <el-tooltip content="分派" placement="top" v-if="canAssign">
  308. <el-button
  309. link
  310. type="success"
  311. icon="UserFilled"
  312. @click="handleAssign(scope.row)"
  313. v-hasPermi="['pm:repairOrder:assign']"
  314. ></el-button>
  315. </el-tooltip>
  316. <el-tooltip content="查看详情" placement="top">
  317. <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
  318. </el-tooltip>
  319. <el-tooltip content="删除" placement="top">
  320. <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['pm:repairOrder:remove']"></el-button>
  321. </el-tooltip>
  322. </template>
  323. <!-- 已分派状态 (2) - 可以查看详情、查看进度 -->
  324. <template v-else-if="scope.row.orderStatus === '2' || scope.row.orderStatus === 2">
  325. <el-tooltip content="查看详情" placement="top">
  326. <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
  327. </el-tooltip>
  328. <el-tooltip content="查看进度" placement="top">
  329. <el-button
  330. link
  331. type="primary"
  332. icon="Histogram"
  333. @click="handleProgress(scope.row)"
  334. v-hasPermi="['pm:repairOrder:progress']"
  335. ></el-button>
  336. </el-tooltip>
  337. <el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
  338. <el-button
  339. link
  340. type="warning"
  341. icon="collection"
  342. @click="handleUpdateProgress(scope.row)"
  343. v-hasPermi="['pm:repairOrder:updateProgress']"
  344. ></el-button>
  345. </el-tooltip>
  346. </template>
  347. <!-- 进行中状态 (3) - 可以查看详情、查看进度、完成工单 -->
  348. <template v-else-if="scope.row.orderStatus === '3' || scope.row.orderStatus === 3">
  349. <el-tooltip content="查看详情" placement="top">
  350. <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
  351. </el-tooltip>
  352. <el-tooltip content="查看进度" placement="top">
  353. <el-button
  354. link
  355. type="primary"
  356. icon="Histogram"
  357. @click="handleProgress(scope.row)"
  358. v-hasPermi="['pm:repairOrder:progress']"
  359. ></el-button>
  360. </el-tooltip>
  361. <el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
  362. <el-button
  363. link
  364. type="warning"
  365. icon="collection"
  366. @click="handleUpdateProgress(scope.row)"
  367. v-hasPermi="['pm:repairOrder:updateProgress']"
  368. ></el-button>
  369. </el-tooltip>
  370. <el-tooltip content="完成工单" placement="top" v-if="canComplete(scope.row)">
  371. <el-button
  372. link
  373. type="success"
  374. icon="CircleCheck"
  375. @click="handleComplete(scope.row)"
  376. v-hasPermi="['pm:repairOrder:complete']"
  377. ></el-button>
  378. </el-tooltip>
  379. </template>
  380. <!-- 已完成状态 (4) - 可以查看详情、查看进度、服务评价 -->
  381. <template v-else-if="scope.row.orderStatus === '4' || scope.row.orderStatus === 4">
  382. <el-tooltip content="查看详情" placement="top">
  383. <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
  384. </el-tooltip>
  385. <el-tooltip content="查看进度" placement="top">
  386. <el-button
  387. link
  388. type="primary"
  389. icon="Histogram"
  390. @click="handleProgress(scope.row)"
  391. v-hasPermi="['pm:repairOrder:progress']"
  392. ></el-button>
  393. </el-tooltip>
  394. <el-tooltip content="服务评价" placement="top" v-if="canEvaluate(scope.row)">
  395. <el-button link type="warning" icon="Star" @click="handleEvaluate(scope.row)" v-hasPermi="['pm:repairOrder:evaluate']"></el-button>
  396. </el-tooltip>
  397. </template>
  398. <!-- 其他状态的默认操作 -->
  399. <template v-else>
  400. <el-tooltip content="查看详情" placement="top">
  401. <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
  402. </el-tooltip>
  403. <el-tooltip content="查看进度" placement="top">
  404. <el-button
  405. link
  406. type="primary"
  407. icon="Histogram"
  408. @click="handleProgress(scope.row)"
  409. v-hasPermi="['pm:repairOrder:progress']"
  410. ></el-button>
  411. </el-tooltip>
  412. </template>
  413. </template>
  414. </el-table-column>
  415. </el-table>
  416. </div>
  417. <!-- 分页组件 -->
  418. <div class="pagination-container">
  419. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  420. </div>
  421. </el-card>
  422. <!-- 添加或修改报修工单对话框 -->
  423. <el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
  424. <el-form ref="repairOrderFormRef" :model="form" :rules="rules" label-width="120px">
  425. <el-row :gutter="20">
  426. <el-col :span="12">
  427. <el-form-item label="报修标题" prop="title">
  428. <el-input v-model="form.title" placeholder="请输入报修标题" />
  429. </el-form-item>
  430. </el-col>
  431. <el-col :span="12">
  432. <el-form-item label="维修类型" prop="categoryId">
  433. <el-tree-select
  434. v-model="form.categoryId"
  435. :data="categoryTreeOptions"
  436. :props="{ value: 'id', label: 'categoryName', children: 'children' }"
  437. value-key="id"
  438. placeholder="请选择维修类型"
  439. check-strictly
  440. :render-after-expand="false"
  441. clearable
  442. filterable
  443. style="width: 100%"
  444. />
  445. </el-form-item>
  446. </el-col>
  447. </el-row>
  448. <el-row :gutter="20">
  449. <el-col :span="12">
  450. <el-form-item label="报修地点" prop="repairLocation">
  451. <el-input v-model="form.repairLocation" placeholder="请输入报修地点" />
  452. </el-form-item>
  453. </el-col>
  454. <el-col :span="12">
  455. <el-form-item label="紧急程度" prop="priorityLevel">
  456. <el-select v-model="form.priorityLevel" placeholder="请选择紧急程度" style="width: 100%">
  457. <el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  458. </el-select>
  459. </el-form-item>
  460. </el-col>
  461. </el-row>
  462. <el-row :gutter="20">
  463. <el-col :span="12">
  464. <el-form-item label="报修人姓名" prop="reporterName">
  465. <el-input
  466. v-model="form.reporterName"
  467. placeholder="自动填充当前用户姓名"
  468. readonly
  469. disabled
  470. style="background-color: #f5f7fa; color: #909399"
  471. />
  472. </el-form-item>
  473. </el-col>
  474. <el-col :span="12">
  475. <el-form-item label="报修人电话" prop="reporterPhone">
  476. <el-input v-model="form.reporterPhone" placeholder="请输入报修人电话" />
  477. </el-form-item>
  478. </el-col>
  479. </el-row>
  480. <el-row :gutter="20">
  481. <el-col :span="12">
  482. <el-form-item label="报修人部门" prop="reporterDept">
  483. <el-input
  484. v-model="form.reporterDept"
  485. placeholder="自动填充当前用户部门"
  486. readonly
  487. disabled
  488. style="background-color: #f5f7fa; color: #909399"
  489. />
  490. </el-form-item>
  491. </el-col>
  492. <el-col :span="12">
  493. <el-form-item label="期望完成时间" prop="expectedFinishTime">
  494. <el-date-picker
  495. v-model="form.expectedFinishTime"
  496. type="datetime"
  497. value-format="YYYY-MM-DD HH:mm:ss"
  498. placeholder="请选择期望完成时间"
  499. style="width: 100%"
  500. />
  501. </el-form-item>
  502. </el-col>
  503. </el-row>
  504. <el-form-item label="问题描述" prop="description">
  505. <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请详细描述遇到的问题..." />
  506. </el-form-item>
  507. <el-form-item label="备注信息" prop="remark">
  508. <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息..." />
  509. </el-form-item>
  510. <!-- 附件上传 -->
  511. <el-form-item label="上传附件">
  512. <div class="attachment-upload-container">
  513. <el-upload
  514. v-model:file-list="attachmentList"
  515. class="upload-demo"
  516. drag
  517. list-type="picture-card"
  518. :on-preview="handleFilePreview"
  519. :action="uploadAction"
  520. :headers="uploadHeaders"
  521. :data="uploadData"
  522. :auto-upload="true"
  523. :before-upload="handleBeforeUpload"
  524. :on-success="handleUploadSuccess"
  525. :on-error="handleUploadError"
  526. :on-remove="handleRemoveAttachment"
  527. :on-change="handleFileChange"
  528. multiple
  529. :limit="15"
  530. :show-file-list="true"
  531. accept="image/*,video/*,.pdf,.doc,.docx,.txt"
  532. >
  533. <!-- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  534. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> -->
  535. <template #tip>
  536. <div class="el-upload__tip">
  537. 支持图片、视频、PDF、Word文档等格式,单个文件大小不超过10MB,最多上传15个文件。
  538. <span class="el-upload__tip" v-if="!form.id" style="color: #ff0000">(新增时文件会立即上传,保存工单后自动关联)</span>
  539. <span class="el-upload__tip" v-else style="color: #ff0000">(编辑时文件会立即上传并关联到当前工单)</span>
  540. </div>
  541. </template>
  542. <!-- 调整拖拽上传图标与文字在同一行、居中对齐 -->
  543. <div class="el-upload-dragger1">
  544. <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  545. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  546. </div>
  547. <!-- 自定义文件卡片缩略图(含文件名) -->
  548. <template #file="{ file }">
  549. <div class="upload-file-card">
  550. <!-- 缩略图区域 -->
  551. <div class="upload-file-thumb">
  552. <el-image
  553. v-if="isImageFile(file) && file.url"
  554. :src="file.url"
  555. fit="cover"
  556. :preview-src-list="[file.url]"
  557. class="upload-thumb-image"
  558. />
  559. <div v-else class="upload-file-placeholder">
  560. <el-icon><Document /></el-icon>
  561. <span class="file-ext">{{ getFileExt(file.name) }}</span>
  562. </div>
  563. </div>
  564. <!-- 文件名称 -->
  565. <div class="upload-file-name" :title="file.name">
  566. {{ file.name }}
  567. </div>
  568. <!-- 操作按钮区域 (与预览界面样式一致) -->
  569. <div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
  570. <el-button-group size="small">
  571. <el-button type="success" @click.stop="handleDownloadAttachment(file)" title="下载" :disabled="file.status !== 'success'">
  572. <el-icon><Download /></el-icon>
  573. </el-button>
  574. <el-button type="danger" @click.stop="handleRemoveAttachmentConfirm(file)" title="删除">
  575. <el-icon><Delete /></el-icon>
  576. </el-button>
  577. </el-button-group>
  578. </div>
  579. </div>
  580. </template>
  581. </el-upload>
  582. </div>
  583. </el-form-item>
  584. </el-form>
  585. <template #footer>
  586. <div class="dialog-footer-right">
  587. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  588. <el-button @click="cancel">取 消</el-button>
  589. </div>
  590. </template>
  591. </el-dialog>
  592. <!-- 详情对话框 -->
  593. <el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="900px" append-to-body>
  594. <div v-loading="detailLoading" class="repair-order-detail">
  595. <template v-if="orderDetail">
  596. <!-- 基本信息 -->
  597. <el-card class="detail-card" shadow="never">
  598. <template #header>
  599. <div class="card-header">
  600. <span class="card-title"
  601. ><el-icon><Document /></el-icon> 基本信息</span
  602. >
  603. </div>
  604. </template>
  605. <div class="detail-grid">
  606. <div class="detail-item">
  607. <label>工单编号:</label>
  608. <span class="detail-value">{{ orderDetail.orderNo }}</span>
  609. </div>
  610. <div class="detail-item">
  611. <label>工单状态:</label>
  612. <el-tag :type="getStatusTagType(orderDetail.orderStatus) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
  613. {{ getStatusText(orderDetail.orderStatus) }}
  614. </el-tag>
  615. </div>
  616. <div class="detail-item">
  617. <label>紧急程度:</label>
  618. <el-tag :type="getPriorityTagType(orderDetail.priorityLevel) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
  619. {{ getPriorityText(orderDetail.priorityLevel) }}
  620. </el-tag>
  621. </div>
  622. <div class="detail-item">
  623. <label>创建时间:</label>
  624. <span class="detail-value">{{ orderDetail.createTime }}</span>
  625. </div>
  626. <div class="detail-item">
  627. <label>维修类型:</label>
  628. <span class="detail-value">{{ getCategoryNameById(orderDetail.categoryId) }}</span>
  629. </div>
  630. <div class="detail-item full-width">
  631. <label>报修标题:</label>
  632. <span class="detail-value">{{ orderDetail.title }}</span>
  633. </div>
  634. <div class="detail-item full-width">
  635. <label>问题描述:</label>
  636. <span class="detail-value description">{{ orderDetail.description }}</span>
  637. </div>
  638. <div class="detail-item full-width" v-if="orderDetail.cancelReason">
  639. <label>取消原因:</label>
  640. <span class="detail-value description">{{ orderDetail.cancelReason }}</span>
  641. </div>
  642. <div class="detail-item full-width" v-if="orderDetail.remark">
  643. <label>备注信息:</label>
  644. <span class="detail-value description">{{ orderDetail.remark }}</span>
  645. </div>
  646. </div>
  647. </el-card>
  648. <!-- 报修信息 -->
  649. <el-card class="detail-card" shadow="never">
  650. <template #header>
  651. <div class="card-header">
  652. <span class="card-title"
  653. ><el-icon><User /></el-icon> 报修信息</span
  654. >
  655. </div>
  656. </template>
  657. <div class="detail-grid">
  658. <div class="detail-item">
  659. <label>报修人:</label>
  660. <span class="detail-value">{{ orderDetail.reporterName }}</span>
  661. </div>
  662. <div class="detail-item">
  663. <label>联系电话:</label>
  664. <span class="detail-value">{{ orderDetail.reporterPhone }}</span>
  665. </div>
  666. <div class="detail-item">
  667. <label>所属部门:</label>
  668. <span class="detail-value">{{ orderDetail.reporterDept || '暂无' }}</span>
  669. </div>
  670. <div class="detail-item">
  671. <label>期望完成时间:</label>
  672. <span class="detail-value">{{ orderDetail.expectedFinishTime || '无要求' }}</span>
  673. </div>
  674. <div class="detail-item full-width">
  675. <label>报修地点:</label>
  676. <span class="detail-value">{{ orderDetail.repairLocation }}</span>
  677. </div>
  678. </div>
  679. </el-card>
  680. <!-- 维修信息 -->
  681. <el-card class="detail-card" shadow="never" v-if="orderDetail.assigneeId">
  682. <template #header>
  683. <div class="card-header">
  684. <span class="card-title"
  685. ><el-icon><Tools /></el-icon> 维修信息</span
  686. >
  687. </div>
  688. </template>
  689. <div class="detail-grid">
  690. <div class="detail-item">
  691. <label>维修员:</label>
  692. <span class="detail-value">{{ orderDetail.assigneeName || '暂未分派' }}</span>
  693. </div>
  694. <div class="detail-item">
  695. <label>分派时间:</label>
  696. <span class="detail-value">{{ orderDetail.assignTime || '暂未分派' }}</span>
  697. </div>
  698. <div class="detail-item">
  699. <label>开始时间:</label>
  700. <span class="detail-value">{{ orderDetail.startTime || '暂未开始' }}</span>
  701. </div>
  702. <div class="detail-item">
  703. <label>完成时间:</label>
  704. <span class="detail-value">{{ orderDetail.finishTime || '暂未完成' }}</span>
  705. </div>
  706. <div class="detail-item">
  707. <label>处理时长:</label>
  708. <span class="detail-value">
  709. {{ orderDetail.totalDuration ? formatDuration(orderDetail.totalDuration) : '暂无' }}
  710. </span>
  711. </div>
  712. <div class="detail-item">
  713. <label>进度记录:</label>
  714. <span class="detail-value">{{ orderDetail.progressCount }}条</span>
  715. </div>
  716. <div class="detail-item full-width" v-if="orderDetail.assignRemark">
  717. <label>分派备注:</label>
  718. <span class="detail-value">{{ orderDetail.assignRemark }}</span>
  719. </div>
  720. </div>
  721. </el-card>
  722. <!-- 维修结果 -->
  723. <el-card class="detail-card" shadow="never" v-if="orderDetail.repairResult || orderDetail.usedMaterials || orderDetail.followSuggestions">
  724. <template #header>
  725. <div class="card-header">
  726. <span class="card-title"
  727. ><el-icon><Check /></el-icon> 维修结果</span
  728. >
  729. </div>
  730. </template>
  731. <div class="detail-grid">
  732. <div class="detail-item full-width" v-if="orderDetail.repairResult">
  733. <label>维修结果:</label>
  734. <span class="detail-value description">{{ orderDetail.repairResult }}</span>
  735. </div>
  736. <div class="detail-item full-width" v-if="orderDetail.usedMaterials">
  737. <label>使用材料:</label>
  738. <span class="detail-value">{{ orderDetail.usedMaterials }}</span>
  739. </div>
  740. <div class="detail-item full-width" v-if="orderDetail.followSuggestions">
  741. <label>后续建议:</label>
  742. <span class="detail-value description">{{ orderDetail.followSuggestions }}</span>
  743. </div>
  744. </div>
  745. </el-card>
  746. <!-- 评价信息 -->
  747. <el-card class="detail-card" shadow="never" v-if="orderDetail.evaluationScore">
  748. <template #header>
  749. <div class="card-header">
  750. <span class="card-title"
  751. ><el-icon><Star /></el-icon> 服务评价</span
  752. >
  753. </div>
  754. </template>
  755. <div class="detail-grid">
  756. <div class="detail-item">
  757. <label>综合评分:</label>
  758. <span class="detail-value">
  759. <el-rate v-model="orderDetail.evaluationScore" disabled show-score text-color="#ff9900" />
  760. </span>
  761. </div>
  762. <div class="detail-item">
  763. <label>评价时间:</label>
  764. <span class="detail-value">{{ orderDetail.evaluationTime || '暂未评价' }}</span>
  765. </div>
  766. </div>
  767. </el-card>
  768. <!-- 附件信息 -->
  769. <el-card class="detail-card" shadow="never" v-if="orderDetail.attachmentCount && orderDetail.attachmentCount > 0">
  770. <template #header>
  771. <div class="card-header">
  772. <span class="card-title">
  773. <el-icon><Paperclip /></el-icon> 附件信息 ({{ orderDetail.attachmentCount }}个文件)
  774. </span>
  775. <el-button
  776. type="primary"
  777. size="small"
  778. @click="refreshAttachments(orderDetail.id)"
  779. :loading="attachmentLoading"
  780. style="margin-left: auto"
  781. >
  782. <el-icon><Refresh /></el-icon>
  783. 刷新
  784. </el-button>
  785. </div>
  786. </template>
  787. <div v-loading="attachmentLoading" class="attachment-detail-container">
  788. <div v-if="currentAttachments.length === 0" class="no-attachment-detail">
  789. <el-empty description="暂无附件" :image-size="60" />
  790. </div>
  791. <div v-else class="attachment-grid-container">
  792. <div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-card">
  793. <!-- 附件缩略图 -->
  794. <div class="attachment-thumbnail-wrapper">
  795. <el-image
  796. v-if="attachment.fileType === 'image'"
  797. :src="attachment.filePath"
  798. :preview-src-list="getImagePreviewList()"
  799. fit="cover"
  800. class="attachment-image"
  801. lazy
  802. />
  803. <div v-else class="attachment-file-icon">
  804. <el-icon :size="28">
  805. <Document v-if="attachment.fileType === 'document'" />
  806. <VideoPlay v-else-if="attachment.fileType === 'video'" />
  807. <Files v-else />
  808. </el-icon>
  809. <span class="file-type-text">{{ getFileTypeLabel(attachment.fileType) }}</span>
  810. </div>
  811. </div>
  812. <!-- 附件信息 -->
  813. <div class="attachment-card-body">
  814. <div class="attachment-title" :title="attachment.fileName">
  815. {{ attachment.fileName }}
  816. </div>
  817. <div class="attachment-meta">
  818. <span class="file-size">{{ formatFileSize(attachment.fileSize) }}</span>
  819. <span class="upload-time">{{ formatUploadTime(attachment.createTime) }}</span>
  820. </div>
  821. <div v-if="attachment.description" class="attachment-desc" :title="attachment.description">
  822. {{ attachment.description }}
  823. </div>
  824. </div>
  825. <!-- 附件操作 -->
  826. <div class="attachment-card-actions">
  827. <el-button-group size="small">
  828. <el-button
  829. type="primary"
  830. @click="previewAttachment(attachment)"
  831. :disabled="!canPreview(attachment)"
  832. :title="canPreview(attachment) ? '预览' : '不支持预览'"
  833. >
  834. <el-icon><View /></el-icon>
  835. </el-button>
  836. <el-button type="success" @click="downloadAttachment(attachment)" title="下载">
  837. <el-icon><Download /></el-icon>
  838. </el-button>
  839. <el-button type="info" @click="showAttachmentDetail(attachment)" title="详情">
  840. <el-icon><InfoFilled /></el-icon>
  841. </el-button>
  842. <!-- 在详情查看模式下不显示删除按钮 -->
  843. <el-button
  844. v-if="canDeleteAttachment(attachment) && !detailDialog.visible"
  845. type="danger"
  846. @click="confirmDeleteAttachment(attachment)"
  847. title="删除"
  848. >
  849. <el-icon><Delete /></el-icon>
  850. </el-button>
  851. </el-button-group>
  852. </div>
  853. </div>
  854. </div>
  855. </div>
  856. </el-card>
  857. </template>
  858. </div>
  859. <template #footer>
  860. <div class="dialog-footer-right">
  861. <el-button @click="detailDialog.visible = false">关 闭</el-button>
  862. </div>
  863. </template>
  864. </el-dialog>
  865. <!-- 工单分派对话框 -->
  866. <el-dialog title="工单分派" v-model="assignDialog.visible" width="600px" append-to-body>
  867. <el-form ref="assignFormRef" :model="assignForm" :rules="assignRules" label-width="120px">
  868. <el-form-item label="工单信息">
  869. <div class="order-info">
  870. <p><strong>工单编号:</strong>{{ currentOrder?.orderNo }}</p>
  871. <p><strong>报修标题:</strong>{{ currentOrder?.title }}</p>
  872. <p><strong>报修地点:</strong>{{ currentOrder?.repairLocation }}</p>
  873. <p><strong>维修类型:</strong>{{ getCategoryNameById(currentOrder?.categoryId) }}</p>
  874. <p><strong>问题描述:</strong>{{ currentOrder?.description }}</p>
  875. <p><strong>备注信息:</strong>{{ currentOrder?.remark }}</p>
  876. </div>
  877. </el-form-item>
  878. <el-form-item label="分派维修员" prop="assigneeId">
  879. <el-select v-model="assignForm.assigneeId" placeholder="请选择维修员" style="width: 100%" @change="handleWorkerChange">
  880. <el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
  881. </el-select>
  882. </el-form-item>
  883. <el-form-item label="分派说明" prop="assignRemark">
  884. <el-input v-model="assignForm.assignRemark" type="textarea" :rows="4" placeholder="请输入分派说明,如特殊要求、注意事项等..." />
  885. </el-form-item>
  886. </el-form>
  887. <template #footer>
  888. <div class="dialog-footer-right">
  889. <el-button :loading="buttonLoading" type="primary" @click="submitAssign">确认分派</el-button>
  890. <el-button @click="cancelAssign">取 消</el-button>
  891. </div>
  892. </template>
  893. </el-dialog>
  894. <!-- 进度查看对话框 -->
  895. <el-dialog title="进度查看" v-model="progressDialog.visible" width="700px" append-to-body>
  896. <div v-loading="progressLoading" class="progress-container">
  897. <!-- 空状态 -->
  898. <div v-if="progressList.length === 0 && !progressLoading" class="no-progress">
  899. <el-empty description="暂无进度记录" :image-size="80" />
  900. </div>
  901. <!-- 进度时间线 -->
  902. <div v-else class="progress-timeline">
  903. <div v-for="(progress, index) in progressList" :key="index" class="progress-item">
  904. <!-- 时间线图标 -->
  905. <div class="timeline-icon">
  906. <el-icon :size="16" :color="getProgressIconColor(progress.progressType)">
  907. <Clock v-if="progress.progressType === '1'" />
  908. <UserFilled v-else-if="progress.progressType === '2'" />
  909. <Location v-else-if="progress.progressType === '3'" />
  910. <Tools v-else-if="progress.progressType === '4'" />
  911. <Edit v-else-if="progress.progressType === '5'" />
  912. <CircleCheck v-else-if="progress.progressType === '6'" />
  913. <Star v-else-if="progress.progressType === '7'" />
  914. <Files v-else-if="progress.progressType === '8'" />
  915. <More v-else />
  916. </el-icon>
  917. </div>
  918. <!-- 时间线连接线 -->
  919. <div v-if="index < progressList.length - 1" class="timeline-line"></div>
  920. <!-- 进度卡片 -->
  921. <div class="progress-card">
  922. <div class="progress-header">
  923. <div class="progress-type-status">
  924. <el-tag :type="getProgressTypeTag(progress.progressType)" size="small">
  925. {{ getProgressTypeText(progress.progressType) }}
  926. </el-tag>
  927. <span class="progress-status">{{ progress.progressStatus }}</span>
  928. </div>
  929. <div class="progress-time">
  930. <el-icon><Clock /></el-icon>
  931. {{ progress.createTime }}
  932. </div>
  933. </div>
  934. <div class="progress-content">
  935. <div class="progress-detail">
  936. <p class="detail-text">{{ progress.progressDetail }}</p>
  937. </div>
  938. <!-- <div class="progress-info-grid" v-if="hasAdditionalInfo(progress)">
  939. <div class="info-item" v-if="progress.locationInfo">
  940. <label
  941. ><el-icon><Location /></el-icon> 位置信息:</label
  942. >
  943. <span>{{ progress.locationInfo }}</span>
  944. </div>
  945. <div class="info-item" v-if="progress.estimatedDuration">
  946. <label
  947. ><el-icon><Timer /></el-icon> 预计时长:</label
  948. >
  949. <span>{{ progress.estimatedDuration }}分钟</span>
  950. </div>
  951. <div class="info-item" v-if="progress.nextStep">
  952. <label
  953. ><el-icon><Right /></el-icon> 下一步:</label
  954. >
  955. <span>{{ progress.nextStep }}</span>
  956. </div>
  957. <div class="info-item" v-if="progress.issuesEncountered">
  958. <label
  959. ><el-icon><Warning /></el-icon> 遇到问题:</label
  960. >
  961. <span>{{ progress.issuesEncountered }}</span>
  962. </div>
  963. <div class="info-item" v-if="progress.materialsNeeded">
  964. <label
  965. ><el-icon><ShoppingBag /></el-icon> 需要材料:</label
  966. >
  967. <span>{{ progress.materialsNeeded }}</span>
  968. </div>
  969. <div class="info-item" v-if="progress.remark">
  970. <label
  971. ><el-icon><EditPen /></el-icon> 备注:</label
  972. >
  973. <span>{{ progress.remark }}</span>
  974. </div>
  975. </div> -->
  976. </div>
  977. </div>
  978. </div>
  979. </div>
  980. </div>
  981. <template #footer>
  982. <div class="dialog-footer-right">
  983. <el-button @click="progressDialog.visible = false">关 闭</el-button>
  984. </div>
  985. </template>
  986. </el-dialog>
  987. <!-- 进度更新对话框 -->
  988. <el-dialog title="进度更新" v-model="updateProgressDialog.visible" width="600px" append-to-body>
  989. <el-form ref="progressFormRef" :model="progressForm" :rules="progressRules" label-width="120px">
  990. <el-form-item label="工单编号" prop="orderNo">
  991. <el-input v-model="progressForm.orderNo" placeholder="请输入工单编号" />
  992. </el-form-item>
  993. <!-- <el-form-item label="进度类型" prop="progressType">
  994. <el-select v-model="progressForm.progressType" placeholder="请选择进度类型" style="width: 100%">
  995. <el-option v-for="type in progressTypeOptions" :key="type.value" :label="type.label" :value="type.value" />
  996. </el-select>
  997. </el-form-item> -->
  998. <el-form-item label="进度状态" prop="progressStatus">
  999. <el-input v-model="progressForm.progressStatus" placeholder="请输入进度状态" />
  1000. </el-form-item>
  1001. <el-form-item label="进度详细描述" prop="progressDetail">
  1002. <el-input v-model="progressForm.progressDetail" type="textarea" placeholder="请输入进度详细描述" />
  1003. </el-form-item>
  1004. <!-- <el-form-item label="进度位置信息" prop="locationInfo">
  1005. <el-input v-model="progressForm.locationInfo" placeholder="请输入进度位置信息" />
  1006. </el-form-item>
  1007. <el-form-item label="进度估计时长" prop="estimatedDuration">
  1008. <el-input v-model="progressForm.estimatedDuration" placeholder="请输入进度估计时长" />
  1009. </el-form-item>
  1010. <el-form-item label="进度下一步" prop="nextStep">
  1011. <el-input v-model="progressForm.nextStep" placeholder="请输入进度下一步" />
  1012. </el-form-item>
  1013. <el-form-item label="遇到的问题" prop="issuesEncountered">
  1014. <el-input v-model="progressForm.issuesEncountered" placeholder="请输入遇到的问题" />
  1015. </el-form-item>
  1016. <el-form-item label="需要的材料" prop="materialsNeeded">
  1017. <el-input v-model="progressForm.materialsNeeded" placeholder="请输入需要的材料" />
  1018. </el-form-item>
  1019. <el-form-item label="进度备注" prop="remark">
  1020. <el-input v-model="progressForm.remark" placeholder="请输入进度备注" />
  1021. </el-form-item> -->
  1022. </el-form>
  1023. <template #footer>
  1024. <div class="dialog-footer-right">
  1025. <el-button :loading="buttonLoading" type="primary" @click="submitProgress">确认更新</el-button>
  1026. <el-button @click="cancelProgress">取 消</el-button>
  1027. </div>
  1028. </template>
  1029. </el-dialog>
  1030. <!-- 完成工单对话框 -->
  1031. <el-dialog title="完成工单" v-model="completeDialog.visible" width="600px" append-to-body>
  1032. <el-form ref="completeFormRef" :model="completeForm" :rules="completeRules" label-width="120px">
  1033. <el-form-item label="工单编号" prop="orderNo">
  1034. <el-input v-model="completeForm.orderNo" placeholder="请输入工单编号" readonly disabled />
  1035. </el-form-item>
  1036. <el-form-item label="维修结果" prop="repairResult">
  1037. <el-input v-model="completeForm.repairResult" placeholder="请输入维修结果" />
  1038. </el-form-item>
  1039. <el-form-item label="使用材料" prop="usedMaterials">
  1040. <el-input v-model="completeForm.usedMaterials" type="textarea" placeholder="请输入使用材料" />
  1041. </el-form-item>
  1042. <el-form-item label="后续建议" prop="followSuggestions">
  1043. <el-input v-model="completeForm.followSuggestions" type="textarea" placeholder="请输入后续建议" />
  1044. </el-form-item>
  1045. </el-form>
  1046. <template #footer>
  1047. <div class="dialog-footer-right">
  1048. <el-button :loading="buttonLoading" type="primary" @click="submitComplete">确认完成</el-button>
  1049. <el-button @click="cancelComplete">取 消</el-button>
  1050. </div>
  1051. </template>
  1052. </el-dialog>
  1053. <!-- 服务评价对话框 -->
  1054. <el-dialog title="服务评价" v-model="evaluationDialog.visible" width="600px" append-to-body>
  1055. <el-form ref="evaluationFormRef" :model="evaluationForm" :rules="evaluationRules" label-width="120px">
  1056. <el-form-item label="工单编号" prop="orderId">
  1057. <el-input
  1058. v-model="evaluationForm.orderId"
  1059. placeholder="自动填充工单编号"
  1060. readonly
  1061. disabled
  1062. style="background-color: #f5f7fa; color: #909399"
  1063. />
  1064. </el-form-item>
  1065. <el-form-item label="评价员工" prop="evaluatedWorkerId">
  1066. <el-select v-model="evaluationForm.evaluatedWorkerId" placeholder="请选择评价员工" style="width: 100%">
  1067. <el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
  1068. </el-select>
  1069. </el-form-item>
  1070. <el-form-item label="综合评分" prop="overallScore">
  1071. <el-rate v-model="evaluationForm.overallScore" :max="5" show-score />
  1072. </el-form-item>
  1073. <el-form-item label="质量评分" prop="qualityScore">
  1074. <el-rate v-model="evaluationForm.qualityScore" :max="5" show-score />
  1075. </el-form-item>
  1076. <el-form-item label="态度评分" prop="attitudeScore">
  1077. <el-rate v-model="evaluationForm.attitudeScore" :max="5" show-score />
  1078. </el-form-item>
  1079. <el-form-item label="响应速度评分" prop="responseSpeedScore">
  1080. <el-rate v-model="evaluationForm.responseSpeedScore" :max="5" show-score />
  1081. </el-form-item>
  1082. <el-form-item label="沟通能力评分" prop="communicationScore">
  1083. <el-rate v-model="evaluationForm.communicationScore" :max="5" show-score />
  1084. </el-form-item>
  1085. <el-form-item label="评价内容" prop="evaluationContent">
  1086. <el-input v-model="evaluationForm.evaluationContent" type="textarea" placeholder="请输入评价内容" />
  1087. </el-form-item>
  1088. <el-form-item label="满意度等级" prop="satisfactionLevel">
  1089. <el-select v-model="evaluationForm.satisfactionLevel" placeholder="请选择满意度等级" style="width: 100%">
  1090. <el-option v-for="level in satisfactionLevelOptions" :key="level.value" :label="level.label" :value="level.value" />
  1091. </el-select>
  1092. </el-form-item>
  1093. <!-- <el-form-item label="是否匿名" prop="isAnonymous">
  1094. <el-switch v-model="evaluationForm.isAnonymous" />
  1095. </el-form-item>
  1096. <el-form-item label="是否公开" prop="isPublic">
  1097. <el-switch v-model="evaluationForm.isPublic" />
  1098. </el-form-item>
  1099. <el-form-item label="正面标签" prop="positiveTags">
  1100. <el-input v-model="evaluationForm.positiveTags" placeholder="请输入正面标签" />
  1101. </el-form-item>
  1102. <el-form-item label="负面标签" prop="negativeTags">
  1103. <el-input v-model="evaluationForm.negativeTags" placeholder="请输入负面标签" />
  1104. </el-form-item>
  1105. <el-form-item label="改进建议" prop="improvementSuggestions">
  1106. <el-input v-model="evaluationForm.improvementSuggestions" placeholder="请输入改进建议" />
  1107. </el-form-item>
  1108. <el-form-item label="备注信息" prop="remark">
  1109. <el-input v-model="evaluationForm.remark" placeholder="请输入备注信息" />
  1110. </el-form-item> -->
  1111. </el-form>
  1112. <template #footer>
  1113. <div class="dialog-footer-right">
  1114. <el-button :loading="buttonLoading" type="primary" @click="submitEvaluation">确认评价</el-button>
  1115. <el-button @click="cancelEvaluation">取 消</el-button>
  1116. </div>
  1117. </template>
  1118. </el-dialog>
  1119. <!-- 附件查看对话框 -->
  1120. <el-dialog title="附件查看" v-model="attachmentDialog.visible" width="800px" append-to-body>
  1121. <div v-loading="attachmentLoading" class="attachment-container">
  1122. <div v-if="currentAttachments.length === 0" class="no-attachment">
  1123. <el-empty description="暂无附件" />
  1124. </div>
  1125. <div v-else class="attachment-list">
  1126. <div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-item">
  1127. <div class="attachment-info">
  1128. <div class="attachment-preview">
  1129. <el-image
  1130. v-if="attachment.fileType === 'image'"
  1131. :src="attachment.filePath"
  1132. :preview-src-list="[attachment.filePath]"
  1133. fit="cover"
  1134. style="width: 80px; height: 80px"
  1135. lazy
  1136. />
  1137. <el-icon v-else class="file-icon" :size="48">
  1138. <Document v-if="attachment.fileType === 'document'" />
  1139. <VideoPlay v-else-if="attachment.fileType === 'video'" />
  1140. <Folder v-else />
  1141. </el-icon>
  1142. </div>
  1143. <div class="attachment-details">
  1144. <div class="file-name">{{ attachment.fileName }}</div>
  1145. <div class="file-meta">
  1146. <span>{{ formatFileSize(attachment.fileSize) }}</span>
  1147. <span>{{ attachment.uploaderName }}</span>
  1148. <span>{{ attachment.createTime }}</span>
  1149. </div>
  1150. <div v-if="attachment.description" class="file-description">
  1151. {{ attachment.description }}
  1152. </div>
  1153. </div>
  1154. </div>
  1155. <div class="attachment-actions">
  1156. <el-button type="primary" size="small" @click="downloadAttachment(attachment)">
  1157. <el-icon><Download /></el-icon>
  1158. 下载
  1159. </el-button>
  1160. <el-button type="success" size="small" @click="previewAttachment(attachment)">
  1161. <el-icon><View /></el-icon>
  1162. 预览
  1163. </el-button>
  1164. </div>
  1165. </div>
  1166. </div>
  1167. </div>
  1168. <template #footer>
  1169. <div class="dialog-footer-right">
  1170. <el-button @click="attachmentDialog.visible = false">关 闭</el-button>
  1171. </div>
  1172. </template>
  1173. </el-dialog>
  1174. </div>
  1175. </template>
  1176. <script setup lang="ts" name="RepairOrder">
  1177. import {
  1178. listRepairOrder,
  1179. getRepairOrder,
  1180. delRepairOrder,
  1181. addRepairOrder,
  1182. updateRepairOrder,
  1183. exportRepairOrders,
  1184. assignRepairOrder,
  1185. getRepairStatistics,
  1186. getMaintenanceWorkers,
  1187. getRepairProgress,
  1188. updateRepairProgress,
  1189. finishRepairOrder,
  1190. createServiceEvaluation,
  1191. getServiceEvaluation,
  1192. getRepairAttachments,
  1193. uploadAttachment,
  1194. uploadAttachments,
  1195. deleteAttachment,
  1196. batchUpdateAttachmentOrderId
  1197. } from '@/api/hq/pm/repairOrder';
  1198. import { getRepairCategoryOptions, getRepairCategoryTree } from '@/api/hq/pm/repairCategory';
  1199. import {
  1200. RepairOrderVO,
  1201. RepairOrderForm,
  1202. RepairOrderQuery,
  1203. AssignOrderForm,
  1204. RepairStatisticsVO,
  1205. RepairProgressVO,
  1206. ProgressUpdateForm,
  1207. ServiceEvaluationForm,
  1208. ServiceEvaluationVO,
  1209. RepairCategoryVO
  1210. } from '@/api/hq/pm/types';
  1211. import { useUserStore } from '@/store/modules/user';
  1212. import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
  1213. import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
  1214. import { vLoading, ElLoading } from 'element-plus';
  1215. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  1216. // 用户store
  1217. const userStore = useUserStore();
  1218. const repairOrderList = ref<RepairOrderVO[]>([]);
  1219. const loading = ref(true);
  1220. const showSearch = ref(true);
  1221. const ids = ref<Array<string | number>>([]);
  1222. const single = ref(true);
  1223. const multiple = ref(true);
  1224. const total = ref(0);
  1225. const daterangeCreateTime = ref<[string, string]>(['', '']);
  1226. // 当前用户信息
  1227. const currentUser = ref<any>(null);
  1228. // 统计数据
  1229. const statistics = ref<RepairStatisticsVO>({
  1230. totalOrders: 0,
  1231. pendingAssign: 0,
  1232. inProgress: 0,
  1233. completed: 0,
  1234. cancelled: 0,
  1235. avgProcessTime: 0,
  1236. avgEvaluationScore: 0,
  1237. monthlyNewOrders: 0,
  1238. monthlyCompletedOrders: 0,
  1239. statusDistribution: [],
  1240. categoryDistribution: [],
  1241. priorityDistribution: []
  1242. });
  1243. // 下拉选项数据
  1244. const categoryOptions = ref<Array<{ value: string | number; label: string }>>([]);
  1245. const categoryTreeOptions = ref<RepairCategoryVO[]>([]);
  1246. const workerOptions = ref<Array<{ value: string | number; label: string; userName?: string; deptName?: string }>>([]);
  1247. // 附件相关数据
  1248. const attachmentList = ref<any[]>([]);
  1249. const uploadAction = ref('/dev-api/hq/pm/attachment/upload'); // 修复:使用正确的API路径
  1250. const uploadHeaders = ref({
  1251. 'Authorization': `Bearer ${getToken() || ''}`,
  1252. 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
  1253. }); // 修复:添加认证头
  1254. const uploadData = ref<any>({});
  1255. // 新增:临时文件存储,用于新增时收集文件
  1256. const tempFileList = ref<File[]>([]);
  1257. // 新增:临时附件ID列表,用于新增时收集已上传附件的ID
  1258. const tempAttachmentIds = ref<string[]>([]);
  1259. // 待删除的附件ID列表 (保存时统一删除)
  1260. const pendingDeleteAttachmentIds = ref<string[]>([]);
  1261. // 字典数据
  1262. const pm_priority_level = ref([
  1263. { value: '1', label: '非常紧急' },
  1264. { value: '2', label: '紧急' },
  1265. { value: '3', label: '一般' },
  1266. { value: '4', label: '不紧急' },
  1267. { value: '5', label: '可延后' }
  1268. ]);
  1269. const pm_order_status = ref([
  1270. { value: '1', label: '待分派' },
  1271. { value: '2', label: '已分派' },
  1272. { value: '3', label: '进行中' },
  1273. { value: '4', label: '已完成' },
  1274. { value: '5', label: '已评价' },
  1275. // { value: '6', label: '已归档' },
  1276. { value: '9', label: '已取消' }
  1277. ]);
  1278. const queryParams = ref<RepairOrderQuery>({
  1279. pageNum: 1,
  1280. pageSize: 10,
  1281. orderNo: '',
  1282. title: '',
  1283. repairLocation: '',
  1284. categoryId: '',
  1285. priorityLevel: undefined,
  1286. reporterName: '',
  1287. reporterPhone: '',
  1288. orderStatus: '',
  1289. assigneeId: '',
  1290. assigneeName: '',
  1291. createTimeRange: [],
  1292. finishTimeRange: [],
  1293. params: {}
  1294. });
  1295. const form = ref<RepairOrderForm>({});
  1296. const queryFormRef = ref<ElFormInstance>();
  1297. const repairOrderFormRef = ref<ElFormInstance>();
  1298. const rules = reactive<ElFormRules>({
  1299. title: [{ required: true, message: '报修标题不能为空', trigger: 'blur' }],
  1300. description: [{ required: true, message: '问题描述不能为空', trigger: 'blur' }],
  1301. repairLocation: [{ required: true, message: '报修地点不能为空', trigger: 'blur' }],
  1302. categoryId: [{ required: true, message: '维修类型不能为空', trigger: 'change' }],
  1303. priorityLevel: [{ required: true, message: '紧急程度不能为空', trigger: 'change' }],
  1304. reporterName: [{ required: true, message: '报修人姓名不能为空', trigger: 'blur' }],
  1305. reporterPhone: [
  1306. { required: true, message: '报修人电话不能为空', trigger: 'blur' },
  1307. { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
  1308. ]
  1309. });
  1310. const dialog = reactive<DialogOption>({
  1311. visible: false,
  1312. title: ''
  1313. });
  1314. const buttonLoading = ref(false);
  1315. // 分派相关
  1316. const assignDialog = reactive<DialogOption>({
  1317. visible: false,
  1318. title: ''
  1319. });
  1320. const assignForm = ref<AssignOrderForm>({
  1321. orderId: '',
  1322. assigneeId: '',
  1323. assigneeName: '',
  1324. assignRemark: ''
  1325. });
  1326. const assignFormRef = ref<ElFormInstance>();
  1327. const assignRules = reactive<ElFormRules>({
  1328. assigneeId: [{ required: true, message: '请选择维修员', trigger: 'change' }]
  1329. });
  1330. const currentOrder = ref<RepairOrderVO>();
  1331. // 详情对话框
  1332. const detailDialog = reactive<DialogOption>({
  1333. visible: false,
  1334. title: ''
  1335. });
  1336. const detailLoading = ref(false);
  1337. const orderDetail = ref<RepairOrderVO>();
  1338. // 进度查看对话框
  1339. const progressDialog = reactive<DialogOption>({
  1340. visible: false,
  1341. title: ''
  1342. });
  1343. const progressLoading = ref(false);
  1344. const progressList = ref<RepairProgressVO[]>([]);
  1345. // 进度更新对话框
  1346. const updateProgressDialog = reactive<DialogOption>({
  1347. visible: false,
  1348. title: ''
  1349. });
  1350. const progressForm = ref<ProgressUpdateForm>({
  1351. orderId: '',
  1352. progressType: '5',
  1353. progressStatus: '',
  1354. progressDetail: '',
  1355. locationInfo: '',
  1356. estimatedDuration: undefined,
  1357. nextStep: '',
  1358. issuesEncountered: '',
  1359. materialsNeeded: '',
  1360. remark: ''
  1361. });
  1362. const progressFormRef = ref<ElFormInstance>();
  1363. const progressRules = reactive<ElFormRules>({
  1364. progressStatus: [{ required: true, message: '进度状态不能为空', trigger: 'blur' }],
  1365. progressDetail: [{ required: true, message: '进度详细描述不能为空', trigger: 'blur' }]
  1366. });
  1367. // 完成工单对话框
  1368. const completeDialog = reactive<DialogOption>({
  1369. visible: false,
  1370. title: ''
  1371. });
  1372. const completeForm = ref({
  1373. orderId: '',
  1374. repairResult: '',
  1375. usedMaterials: '',
  1376. followSuggestions: ''
  1377. });
  1378. const completeFormRef = ref<ElFormInstance>();
  1379. const completeRules = reactive<ElFormRules>({
  1380. repairResult: [{ required: true, message: '维修结果不能为空', trigger: 'blur' }]
  1381. });
  1382. // 服务评价对话框
  1383. const evaluationDialog = reactive<DialogOption>({
  1384. visible: false,
  1385. title: ''
  1386. });
  1387. const attachmentDialog = reactive<DialogOption>({
  1388. visible: false,
  1389. title: ''
  1390. });
  1391. // 附件相关数据
  1392. const attachmentLoading = ref(false);
  1393. const currentAttachments = ref<any[]>([]);
  1394. const evaluationForm = ref<ServiceEvaluationForm>({
  1395. orderId: '',
  1396. evaluatedWorkerId: '',
  1397. overallScore: 5,
  1398. qualityScore: 5,
  1399. attitudeScore: 5,
  1400. responseSpeedScore: 5,
  1401. communicationScore: 5,
  1402. evaluationContent: '',
  1403. satisfactionLevel: '1',
  1404. isAnonymous: 0,
  1405. isPublic: 1,
  1406. positiveTags: '',
  1407. negativeTags: '',
  1408. improvementSuggestions: '',
  1409. remark: ''
  1410. });
  1411. const evaluationFormRef = ref<ElFormInstance>();
  1412. const evaluationRules = reactive<ElFormRules>({
  1413. orderId: [{ required: true, message: '工单ID不能为空', trigger: 'blur' }],
  1414. evaluatedWorkerId: [{ required: true, message: '请选择要评价的维修员', trigger: 'change' }],
  1415. overallScore: [{ required: true, message: '请给出综合评分', trigger: 'change' }],
  1416. evaluationContent: [{ required: true, message: '请填写评价内容', trigger: 'blur' }],
  1417. satisfactionLevel: [{ required: true, message: '请选择满意度等级', trigger: 'change' }]
  1418. });
  1419. // 进度类型字典
  1420. const progressTypeOptions = ref([
  1421. { value: '1', label: '工单分派' },
  1422. { value: '2', label: '接收确认' },
  1423. { value: '3', label: '到达现场' },
  1424. { value: '4', label: '开始维修' },
  1425. { value: '5', label: '进度更新' },
  1426. { value: '6', label: '完成维修' },
  1427. { value: '7', label: '用户评价' },
  1428. { value: '8', label: '工单归档' },
  1429. { value: '9', label: '其他' }
  1430. ]);
  1431. // 满意度等级字典
  1432. const satisfactionLevelOptions = ref([
  1433. { value: '1', label: '非常满意' },
  1434. { value: '2', label: '满意' },
  1435. { value: '3', label: '一般' },
  1436. { value: '4', label: '不满意' },
  1437. { value: '5', label: '非常不满意' }
  1438. ]);
  1439. /** 查询报修工单列表 */
  1440. const getList = async () => {
  1441. loading.value = true;
  1442. queryParams.value.createTimeRange = daterangeCreateTime.value;
  1443. // 设置创建时间范围
  1444. queryParams.value.createTimeRange = daterangeCreateTime.value;
  1445. // 将创建时间范围写入 params,供后台 between 过滤使用
  1446. if (daterangeCreateTime.value && daterangeCreateTime.value.length === 2 && daterangeCreateTime.value[0] && daterangeCreateTime.value[1]) {
  1447. queryParams.value.params = {
  1448. ...queryParams.value.params,
  1449. beginCreateTime: daterangeCreateTime.value[0],
  1450. endCreateTime: daterangeCreateTime.value[1]
  1451. };
  1452. } else if (queryParams.value.params) {
  1453. delete queryParams.value.params.beginCreateTime;
  1454. delete queryParams.value.params.endCreateTime;
  1455. }
  1456. const res = await listRepairOrder(queryParams.value);
  1457. // 适配后端返回格式:支持新格式{total, rows, code, msg}和旧格式{code, msg, data: {total, rows}}
  1458. if ((res as any).rows && (res as any).total !== undefined) {
  1459. // 新格式:直接返回TableDataInfo
  1460. repairOrderList.value = (res as any).rows;
  1461. total.value = (res as any).total;
  1462. } else if ((res as any).data && (res as any).data.rows) {
  1463. // 旧格式:R类型包装的TableDataInfo
  1464. repairOrderList.value = (res as any).data.rows;
  1465. total.value = (res as any).data.total || 0;
  1466. } else {
  1467. // 兜底处理
  1468. repairOrderList.value = [];
  1469. total.value = 0;
  1470. }
  1471. loading.value = false;
  1472. };
  1473. /** 获取统计数据 */
  1474. const getStatistics = async () => {
  1475. try {
  1476. const res = await getRepairStatistics();
  1477. // 适配后端返回格式
  1478. statistics.value = res.data;
  1479. } catch (error) {
  1480. console.error('获取统计数据失败:', error);
  1481. }
  1482. };
  1483. /** 获取维修类型选项 */
  1484. const getCategoryOptions = async () => {
  1485. try {
  1486. const res = await getRepairCategoryOptions();
  1487. categoryOptions.value = res.data;
  1488. } catch (error) {
  1489. console.error('获取维修类型选项失败:', error);
  1490. }
  1491. };
  1492. /** 获取维修类型树形结构 */
  1493. const getCategoryTreeOptions = async () => {
  1494. try {
  1495. const res = await getRepairCategoryTree();
  1496. categoryTreeOptions.value = res.data || [];
  1497. } catch (error) {
  1498. console.error('获取维修类型树形结构失败:', error);
  1499. categoryTreeOptions.value = [];
  1500. }
  1501. };
  1502. /** 根据categoryId获取维修类型名称 */
  1503. const getCategoryNameById = (categoryId: string | number | undefined): string => {
  1504. if (!categoryId) {
  1505. return '未分类';
  1506. }
  1507. // 递归查找树形结构中的分类名称
  1508. const findCategoryName = (categories: RepairCategoryVO[], targetId: string | number): string | null => {
  1509. for (const category of categories) {
  1510. // 数据类型统一转换为字符串进行比较
  1511. if (String(category.id) === String(targetId)) {
  1512. return category.categoryName || '未命名分类';
  1513. }
  1514. // 如果有子分类,递归查找
  1515. if (category.children && category.children.length > 0) {
  1516. const found = findCategoryName(category.children, targetId);
  1517. if (found) {
  1518. return found;
  1519. }
  1520. }
  1521. }
  1522. return null;
  1523. };
  1524. // 先从树形结构中查找
  1525. const foundInTree = findCategoryName(categoryTreeOptions.value, categoryId);
  1526. if (foundInTree) {
  1527. return foundInTree;
  1528. }
  1529. // 如果树形结构中没找到,从普通选项中查找
  1530. const foundInOptions = categoryOptions.value.find((option) => String(option.value) === String(categoryId));
  1531. if (foundInOptions) {
  1532. return foundInOptions.label;
  1533. }
  1534. // 都没找到,返回ID
  1535. return `未知类型(${categoryId})`;
  1536. };
  1537. /** 获取维修人员选项 */
  1538. const getWorkerOptions = async () => {
  1539. try {
  1540. const res = await getMaintenanceWorkers();
  1541. workerOptions.value = res.data;
  1542. } catch (error) {
  1543. console.error('获取维修人员选项失败:', error);
  1544. }
  1545. };
  1546. /** 获取当前用户详细信息 */
  1547. const getCurrentUserDetailInfo = async () => {
  1548. try {
  1549. const res = await getInfo();
  1550. console.log('用户详细信息:', res.data);
  1551. return res.data.user;
  1552. } catch (error) {
  1553. console.error('获取用户详细信息失败:', error);
  1554. return null;
  1555. }
  1556. };
  1557. /** 获取当前用户信息 */
  1558. const getCurrentUserInfo = async () => {
  1559. try {
  1560. // 首先确保用户信息已加载
  1561. if (!userStore.userId) {
  1562. await userStore.getInfo();
  1563. }
  1564. // 构建当前用户信息对象,包含角色信息
  1565. currentUser.value = {
  1566. userId: userStore.userId,
  1567. userName: userStore.nickname, // 使用nickname作为用户名
  1568. nickName: userStore.nickname,
  1569. tenantId: userStore.tenantId,
  1570. avatar: userStore.avatar,
  1571. roles: userStore.roles?.map((roleKey: string) => ({ roleKey })) || [],
  1572. permissions: userStore.permissions || [],
  1573. admin: userStore.roles?.includes('admin') || userStore.roles?.includes('superadmin') || false
  1574. };
  1575. } catch (error) {
  1576. console.error('获取用户信息失败:', error);
  1577. // 设置默认值,避免后续计算属性报错
  1578. currentUser.value = {
  1579. userId: null,
  1580. userName: '',
  1581. nickName: '',
  1582. tenantId: '',
  1583. avatar: '',
  1584. roles: [],
  1585. permissions: [],
  1586. admin: false
  1587. };
  1588. }
  1589. };
  1590. /** 检查是否为维修管理员 */
  1591. const isMaintenanceAdmin = computed(() => {
  1592. if (!currentUser.value) return false;
  1593. const roles = currentUser.value.roles || [];
  1594. return roles.some((role: any) => role.roleKey === 'property_maintenance_admin');
  1595. });
  1596. /** 检查是否为维修人员 */
  1597. const isMaintenanceWorker = computed(() => {
  1598. if (!currentUser.value) return false;
  1599. const roles = currentUser.value.roles || [];
  1600. return roles.some((role: any) => role.roleKey === 'maintenance_worker');
  1601. });
  1602. /** 检查是否为超级管理员 */
  1603. const isSuperAdmin = computed(() => {
  1604. if (!currentUser.value) return false;
  1605. return currentUser.value.admin || false;
  1606. });
  1607. /** 检查是否为报修人 */
  1608. const isReporter = (reporterId: number | string) => {
  1609. if (!currentUser.value) return false;
  1610. return currentUser.value.userId === reporterId;
  1611. };
  1612. /** 检查是否为维修负责人 */
  1613. const isAssignee = (assigneeId: number | string) => {
  1614. if (!currentUser.value) return false;
  1615. return currentUser.value.userId === assigneeId;
  1616. };
  1617. /** 检查分派按钮权限 - 仅维修管理员 */
  1618. const canAssign = computed(() => {
  1619. return isSuperAdmin.value || isMaintenanceAdmin.value;
  1620. });
  1621. /** 检查更新进度按钮权限 - 仅维修人员且为负责人 */
  1622. const canUpdateProgress = (row: RepairOrderVO) => {
  1623. console.log(isSuperAdmin.value, isMaintenanceWorker.value, isAssignee(row.assigneeId));
  1624. if (isSuperAdmin.value) return true;
  1625. return isMaintenanceWorker.value && isAssignee(row.assigneeId);
  1626. };
  1627. /** 检查完成工单按钮权限 - 仅维修人员且为负责人 */
  1628. const canComplete = (row: RepairOrderVO) => {
  1629. if (isSuperAdmin.value) return true;
  1630. return isMaintenanceWorker.value && isAssignee(row.assigneeId);
  1631. };
  1632. /** 检查服务评价按钮权限 - 仅报修人 */
  1633. const canEvaluate = (row: RepairOrderVO) => {
  1634. if (isSuperAdmin.value) return true;
  1635. return isReporter(row.reporterId);
  1636. };
  1637. /** 检查统计卡片查看权限 - 仅维护管理员和超级管理员 */
  1638. const canViewStatistics = computed(() => {
  1639. return isSuperAdmin.value || isMaintenanceAdmin.value;
  1640. });
  1641. /** 搜索按钮操作 */
  1642. const handleQuery = () => {
  1643. queryParams.value.pageNum = 1;
  1644. getList();
  1645. };
  1646. /** 重置按钮操作 */
  1647. const resetQuery = () => {
  1648. daterangeCreateTime.value = ['', ''];
  1649. queryFormRef.value?.resetFields();
  1650. queryParams.value.createTimeRange = [];
  1651. queryParams.value.pageNum = 1;
  1652. getList();
  1653. };
  1654. /** 多选框选中数据 */
  1655. const handleSelectionChange = (selection: RepairOrderVO[]) => {
  1656. ids.value = selection.map((item) => item.id);
  1657. single.value = selection.length !== 1;
  1658. multiple.value = !selection.length;
  1659. };
  1660. /** 新增按钮操作 */
  1661. const handleAdd = async () => {
  1662. await reset(true); // 传入true表示是新增操作,需要自动填充用户信息
  1663. dialog.visible = true;
  1664. dialog.title = '添加报修工单';
  1665. };
  1666. /** 修改按钮操作 */
  1667. const handleUpdate = async (row?: RepairOrderVO) => {
  1668. await reset(); // 修改操作不自动填充用户信息
  1669. const id = row?.id || ids.value[0];
  1670. try {
  1671. // 加载工单基本信息
  1672. const res = await getRepairOrder(id);
  1673. Object.assign(form.value, res.data);
  1674. // 加载已有的附件信息
  1675. if (form.value.id) {
  1676. try {
  1677. const attachmentRes = await getRepairAttachments(form.value.id);
  1678. const existingAttachments = attachmentRes.data || [];
  1679. // 转换已有附件为el-upload组件需要的格式
  1680. attachmentList.value = existingAttachments.map((attachment: any) => ({
  1681. uid: attachment.id,
  1682. name: attachment.fileName,
  1683. status: 'success',
  1684. size: attachment.fileSize,
  1685. url: attachment.filePath,
  1686. response: {
  1687. code: 200,
  1688. data: {
  1689. id: attachment.id,
  1690. fileName: attachment.fileName,
  1691. filePath: attachment.filePath
  1692. }
  1693. },
  1694. // 保存原始附件信息
  1695. attachmentInfo: attachment
  1696. }));
  1697. console.log('已加载附件信息,数量:', attachmentList.value.length);
  1698. } catch (error) {
  1699. console.error('加载附件信息失败:', error);
  1700. attachmentList.value = [];
  1701. }
  1702. }
  1703. dialog.visible = true;
  1704. dialog.title = '修改报修工单';
  1705. } catch (error) {
  1706. console.error('加载工单信息失败:', error);
  1707. proxy?.$modal.msgError('加载工单信息失败');
  1708. }
  1709. };
  1710. /** 提交按钮 */
  1711. const submitForm = () => {
  1712. repairOrderFormRef.value?.validate(async (valid: boolean) => {
  1713. if (valid) {
  1714. buttonLoading.value = true;
  1715. try {
  1716. let orderId: string | number;
  1717. if (form.value.id) {
  1718. await updateRepairOrder(form.value);
  1719. orderId = form.value.id;
  1720. proxy?.$modal.msgSuccess('修改成功');
  1721. } else {
  1722. const res = await addRepairOrder(form.value);
  1723. orderId = res.data?.id || res.data; // 修复:移除res.id,避免类型错误
  1724. console.log('新增工单响应:', res); // 调试信息
  1725. proxy?.$modal.msgSuccess('新增成功');
  1726. }
  1727. // 关闭对话框并刷新列表(确保这两步先执行)
  1728. dialog.visible = false;
  1729. await getList();
  1730. // 处理附件上传(移到最后,避免影响界面关闭)
  1731. if (orderId) {
  1732. try {
  1733. await handlePendingAttachments(orderId);
  1734. } catch (error) {
  1735. console.error('附件上传失败:', error);
  1736. proxy?.$modal.msgWarning('工单保存成功,但附件上传失败,请稍后重试');
  1737. }
  1738. }
  1739. } catch (error) {
  1740. console.error('保存工单失败:', error);
  1741. proxy?.$modal.msgError('保存失败,请重试');
  1742. } finally {
  1743. buttonLoading.value = false;
  1744. }
  1745. }
  1746. });
  1747. };
  1748. /** 处理待关联的附件 */
  1749. const handlePendingAttachments = async (orderId: string | number) => {
  1750. let hasOperations = false;
  1751. let successMessages: string[] = [];
  1752. let errorMessages: string[] = [];
  1753. // 处理待删除的附件(编辑模式)
  1754. if (pendingDeleteAttachmentIds.value.length > 0) {
  1755. try {
  1756. console.log('开始删除标记的附件,数量:', pendingDeleteAttachmentIds.value.length);
  1757. // 批量删除附件
  1758. for (const attachmentId of pendingDeleteAttachmentIds.value) {
  1759. await deleteAttachment(attachmentId);
  1760. }
  1761. console.log('成功删除附件IDs:', pendingDeleteAttachmentIds.value);
  1762. successMessages.push(`成功删除 ${pendingDeleteAttachmentIds.value.length} 个附件`);
  1763. // 清空待删除列表
  1764. pendingDeleteAttachmentIds.value = [];
  1765. hasOperations = true;
  1766. } catch (error) {
  1767. console.error('附件删除失败:', error);
  1768. errorMessages.push('部分附件删除失败');
  1769. }
  1770. }
  1771. // 处理新增模式下已上传的附件(更新order_id)
  1772. if (tempAttachmentIds.value.length > 0) {
  1773. try {
  1774. console.log('开始关联临时附件到工单,附件数量:', tempAttachmentIds.value.length);
  1775. // 调用API批量更新附件的order_id
  1776. await batchUpdateAttachmentOrderId(tempAttachmentIds.value, String(orderId));
  1777. console.log('成功关联附件IDs:', tempAttachmentIds.value, '到工单:', orderId);
  1778. successMessages.push(`成功关联 ${tempAttachmentIds.value.length} 个附件到工单`);
  1779. // 清空临时附件ID列表
  1780. tempAttachmentIds.value = [];
  1781. tempFileList.value = [];
  1782. attachmentList.value = [];
  1783. hasOperations = true;
  1784. } catch (error) {
  1785. console.error('附件关联失败:', error);
  1786. errorMessages.push('部分附件关联失败');
  1787. }
  1788. }
  1789. // 处理编辑模式下新添加的文件(attachmentList中status不为success的文件)
  1790. const newFiles = attachmentList.value.filter((file) => file.status !== 'success' && file.raw && !file.response);
  1791. if (newFiles.length > 0) {
  1792. try {
  1793. console.log('开始上传编辑模式下新添加的文件,文件数量:', newFiles.length);
  1794. for (const fileItem of newFiles) {
  1795. if (fileItem.raw) {
  1796. await uploadAttachment(String(orderId), '1', fileItem.raw, '');
  1797. console.log('成功上传编辑模式新文件:', fileItem.name);
  1798. // 更新文件状态
  1799. fileItem.status = 'success';
  1800. }
  1801. }
  1802. successMessages.push(`成功上传 ${newFiles.length} 个新附件`);
  1803. hasOperations = true;
  1804. } catch (error) {
  1805. console.error('新附件上传失败:', error);
  1806. errorMessages.push('部分新附件上传失败');
  1807. }
  1808. }
  1809. // 显示操作结果消息
  1810. if (hasOperations) {
  1811. if (successMessages.length > 0) {
  1812. proxy?.$modal.msgSuccess(successMessages.join(','));
  1813. }
  1814. if (errorMessages.length > 0) {
  1815. proxy?.$modal.msgWarning(`工单保存成功,但${errorMessages.join(',')}`);
  1816. }
  1817. }
  1818. };
  1819. /** 删除按钮操作 */
  1820. const handleDelete = async (row?: RepairOrderVO) => {
  1821. const orderIds = row?.id || ids.value;
  1822. await proxy?.$modal.confirm('是否确认删除报修工单编号为"' + orderIds + '"的数据项?');
  1823. await delRepairOrder(orderIds);
  1824. proxy?.$modal.msgSuccess('删除成功');
  1825. await getList();
  1826. };
  1827. /** 导出按钮操作 */
  1828. const handleExport = () => {
  1829. proxy?.$download.zip('repair_order_export.zip', `repair_order_${new Date().getTime()}.xlsx`);
  1830. };
  1831. /** 分派按钮操作 */
  1832. const handleAssign = (row: RepairOrderVO) => {
  1833. currentOrder.value = row;
  1834. assignForm.value = {
  1835. orderId: row.id,
  1836. assigneeId: '',
  1837. assigneeName: '',
  1838. assignRemark: ''
  1839. };
  1840. assignDialog.visible = true;
  1841. };
  1842. /** 维修员选择变更 */
  1843. const handleWorkerChange = (value: string | number) => {
  1844. const worker = workerOptions.value.find((w) => w.value === value);
  1845. assignForm.value.assigneeName = worker?.label || '';
  1846. };
  1847. /** 提交分派 */
  1848. const submitAssign = () => {
  1849. assignFormRef.value?.validate(async (valid: boolean) => {
  1850. if (valid) {
  1851. buttonLoading.value = true;
  1852. try {
  1853. await assignRepairOrder(assignForm.value);
  1854. proxy?.$modal.msgSuccess('工单分派成功');
  1855. assignDialog.visible = false;
  1856. await getList();
  1857. } finally {
  1858. buttonLoading.value = false;
  1859. }
  1860. }
  1861. });
  1862. };
  1863. /** 取消分派 */
  1864. const cancelAssign = () => {
  1865. assignDialog.visible = false;
  1866. };
  1867. /** 查看详情 */
  1868. const handleDetail = async (row: RepairOrderVO) => {
  1869. detailLoading.value = true;
  1870. detailDialog.title = `工单详情 - ${row.orderNo}`;
  1871. detailDialog.visible = true;
  1872. try {
  1873. const res = await getRepairOrder(row.id);
  1874. // 适配不同的响应格式
  1875. orderDetail.value = (res as any).data || res;
  1876. // 自动加载附件信息
  1877. if (orderDetail.value.attachmentCount && orderDetail.value.attachmentCount > 0) {
  1878. await loadAttachments(orderDetail.value.id);
  1879. }
  1880. } catch (error) {
  1881. console.error('获取工单详情失败:', error);
  1882. proxy?.$modal.msgError('获取工单详情失败');
  1883. } finally {
  1884. detailLoading.value = false;
  1885. }
  1886. };
  1887. /** 加载附件信息 */
  1888. const loadAttachments = async (orderId: string | number) => {
  1889. try {
  1890. attachmentLoading.value = true;
  1891. const res = await getRepairAttachments(orderId);
  1892. currentAttachments.value = res.data || [];
  1893. } catch (error) {
  1894. console.error('获取附件失败:', error);
  1895. currentAttachments.value = [];
  1896. } finally {
  1897. attachmentLoading.value = false;
  1898. }
  1899. };
  1900. /** 刷新附件列表 */
  1901. const refreshAttachments = async (orderId: string | number) => {
  1902. await loadAttachments(orderId);
  1903. proxy?.$modal.msgSuccess('附件列表已刷新');
  1904. };
  1905. /** 获取图片预览列表 */
  1906. const getImagePreviewList = () => {
  1907. return currentAttachments.value.filter((att) => att.fileType === 'image').map((att) => att.filePath);
  1908. };
  1909. /** 获取文件类型标签 */
  1910. const getFileTypeLabel = (fileType: string) => {
  1911. const typeMap: Record<string, string> = {
  1912. 'image': '图片',
  1913. 'video': '视频',
  1914. 'document': '文档',
  1915. 'audio': '音频',
  1916. 'archive': '压缩包'
  1917. };
  1918. return typeMap[fileType] || '文件';
  1919. };
  1920. /** 格式化上传时间 */
  1921. const formatUploadTime = (timeStr: string) => {
  1922. if (!timeStr) return '未知时间';
  1923. const time = new Date(timeStr);
  1924. const now = new Date();
  1925. const diff = now.getTime() - time.getTime();
  1926. const days = Math.floor(diff / (1000 * 60 * 60 * 24));
  1927. if (days === 0) {
  1928. return '今天上传';
  1929. } else if (days === 1) {
  1930. return '昨天上传';
  1931. } else if (days < 7) {
  1932. return `${days}天前上传`;
  1933. } else {
  1934. return proxy.parseTime(timeStr, '{y}-{m}-{d}');
  1935. }
  1936. };
  1937. /** 获取上传阶段文本 */
  1938. const getUploadStageText = (stage: string | number) => {
  1939. const stageMap: Record<string, string> = {
  1940. '1': '报修申请',
  1941. '2': '维修过程',
  1942. '3': '维修完成',
  1943. '4': '服务评价'
  1944. };
  1945. return stageMap[String(stage)] || '未知阶段';
  1946. };
  1947. /** 获取文件存储信息 */
  1948. const getFileStorageInfo = (filePath: string) => {
  1949. if (!filePath) return '未知路径';
  1950. const parts = filePath.split('/');
  1951. return parts.length > 2 ? `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}` : filePath;
  1952. };
  1953. /** 检查是否可以预览 */
  1954. const canPreview = (attachment: any) => {
  1955. const previewableTypes = ['image', 'document'];
  1956. const previewableExtensions = ['.pdf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
  1957. if (previewableTypes.includes(attachment.fileType)) {
  1958. return true;
  1959. }
  1960. if (attachment.fileExtension) {
  1961. return previewableExtensions.includes(attachment.fileExtension.toLowerCase());
  1962. }
  1963. return false;
  1964. };
  1965. /** 显示附件详情 */
  1966. const showAttachmentDetail = (attachment: any) => {
  1967. const details = `文件名称:${attachment.fileName}<br/>
  1968. 文件大小:${formatFileSize(attachment.fileSize)}<br/>
  1969. 文件类型:${getFileTypeLabel(attachment.fileType)}<br/>
  1970. 上传者:${attachment.uploaderName || '未知'}<br/>
  1971. 上传时间:${attachment.createTime}<br/>
  1972. 上传阶段:${getUploadStageText(attachment.uploadStage)}<br/>
  1973. 存储路径:${attachment.filePath}<br/>
  1974. 文件描述:${attachment.description || '无'}`;
  1975. proxy?.$modal.alert(details);
  1976. };
  1977. /** 检查是否可以删除附件 */
  1978. const canDeleteAttachment = (attachment: any) => {
  1979. // 只有超级管理员、维修管理员或上传者本人可以删除附件
  1980. if (isSuperAdmin.value || isMaintenanceAdmin.value) {
  1981. return true;
  1982. }
  1983. // 检查是否为上传者本人
  1984. if (currentUser.value && attachment.uploaderId) {
  1985. return currentUser.value.userId === attachment.uploaderId;
  1986. }
  1987. return false;
  1988. };
  1989. /** 确认删除附件 */
  1990. const confirmDeleteAttachment = async (attachment: any) => {
  1991. try {
  1992. await proxy?.$modal.confirm(`确定要删除附件 "${attachment.fileName}" 吗?删除后无法恢复!`);
  1993. await deleteAttachment(attachment.id);
  1994. proxy?.$modal.msgSuccess('附件删除成功');
  1995. // 刷新附件列表和工单详情
  1996. if (orderDetail.value) {
  1997. await loadAttachments(orderDetail.value.id);
  1998. // 更新附件数量
  1999. if (orderDetail.value.attachmentCount > 0) {
  2000. orderDetail.value.attachmentCount--;
  2001. }
  2002. }
  2003. } catch (error) {
  2004. if (error !== 'cancel') {
  2005. console.error('删除附件失败:', error);
  2006. proxy?.$modal.msgError('删除附件失败');
  2007. }
  2008. }
  2009. };
  2010. /** 查看图片集 */
  2011. const viewImageGallery = (attachment: any) => {
  2012. const imageList = currentAttachments.value.filter((att) => att.fileType === 'image');
  2013. const currentIndex = imageList.findIndex((img) => img.id === attachment.id);
  2014. // 使用Element Plus的图片预览功能
  2015. const previewList = imageList.map((img) => img.filePath);
  2016. const imageViewer = document.createElement('div');
  2017. imageViewer.innerHTML = `<el-image :src="'${attachment.filePath}'" :preview-src-list="previewList" style="display: none;"></el-image>`;
  2018. document.body.appendChild(imageViewer);
  2019. // 触发预览
  2020. setTimeout(() => {
  2021. const img = imageViewer.querySelector('img') as HTMLImageElement;
  2022. if (img) {
  2023. img.click();
  2024. }
  2025. // 清理临时元素
  2026. setTimeout(() => {
  2027. document.body.removeChild(imageViewer);
  2028. }, 100);
  2029. }, 50);
  2030. };
  2031. /** 在新窗口打开 */
  2032. const openInNewTab = (attachment: any) => {
  2033. window.open(attachment.filePath, '_blank');
  2034. };
  2035. /** 查看进度 */
  2036. const handleProgress = async (row: RepairOrderVO) => {
  2037. progressLoading.value = true;
  2038. progressDialog.title = `进度查看 - ${row.orderNo}`;
  2039. progressDialog.visible = true;
  2040. try {
  2041. const res = await getRepairProgress(row.id);
  2042. // 适配后端返回格式
  2043. progressList.value = (res as any).data || (res as any);
  2044. } catch (error) {
  2045. console.error('获取进度失败:', error);
  2046. proxy?.$modal.msgError('获取进度失败');
  2047. } finally {
  2048. progressLoading.value = false;
  2049. }
  2050. };
  2051. /** 更新进度 */
  2052. const handleUpdateProgress = (row: RepairOrderVO) => {
  2053. updateProgressDialog.title = `进度更新 - ${row.orderNo}`;
  2054. updateProgressDialog.visible = true;
  2055. progressForm.value = {
  2056. orderId: row.id,
  2057. progressType: '5',
  2058. progressStatus: '',
  2059. progressDetail: '',
  2060. locationInfo: '',
  2061. estimatedDuration: undefined,
  2062. nextStep: '',
  2063. issuesEncountered: '',
  2064. materialsNeeded: '',
  2065. remark: ''
  2066. };
  2067. };
  2068. /** 完成工单 */
  2069. const handleComplete = (row: RepairOrderVO) => {
  2070. completeDialog.title = `完成工单 - ${row.orderNo}`;
  2071. completeDialog.visible = true;
  2072. completeForm.value = {
  2073. orderId: String(row.id),
  2074. repairResult: '',
  2075. usedMaterials: '',
  2076. followSuggestions: ''
  2077. };
  2078. };
  2079. /** 服务评价 */
  2080. const handleEvaluate = (row: RepairOrderVO) => {
  2081. evaluationDialog.title = `服务评价 - ${row.orderNo}`;
  2082. evaluationDialog.visible = true;
  2083. // 验证工单状态,只有已完成的工单才能评价
  2084. if (String(row.orderStatus) !== '4') {
  2085. proxy?.$modal.msgWarning('只有已完成的工单才能进行评价');
  2086. return;
  2087. }
  2088. // 验证是否有分派的维修员
  2089. if (!row.assigneeId) {
  2090. proxy?.$modal.msgWarning('该工单没有分派维修员,无法进行评价');
  2091. return;
  2092. }
  2093. console.log('初始化评价表单,工单信息:', {
  2094. orderId: row.id,
  2095. orderNo: row.orderNo,
  2096. assigneeId: row.assigneeId,
  2097. assigneeName: row.assigneeName,
  2098. orderStatus: row.orderStatus
  2099. });
  2100. evaluationForm.value = {
  2101. orderId: row.id,
  2102. evaluatedWorkerId: row.assigneeId,
  2103. overallScore: 5,
  2104. qualityScore: 5,
  2105. attitudeScore: 5,
  2106. responseSpeedScore: 5,
  2107. communicationScore: 5,
  2108. evaluationContent: '',
  2109. satisfactionLevel: '1',
  2110. isAnonymous: 0,
  2111. isPublic: 1,
  2112. positiveTags: '',
  2113. negativeTags: '',
  2114. improvementSuggestions: '',
  2115. remark: ''
  2116. };
  2117. };
  2118. /** 上传前检查 */
  2119. const handleBeforeUpload = (file: File) => {
  2120. const isLt10M = file.size / 1024 / 1024 < 10;
  2121. if (!isLt10M) {
  2122. proxy?.$modal.msgError('上传文件大小不能超过 10MB!');
  2123. return false;
  2124. }
  2125. // 设置上传参数
  2126. const uploadParams: any = {
  2127. uploadStage: '1', // 报修申请阶段
  2128. description: `${form.value.title || '报修工单'}相关附件` // 添加描述信息
  2129. };
  2130. // 只有编辑模式时才传递orderId
  2131. if (form.value.id) {
  2132. uploadParams.orderId = form.value.id;
  2133. }
  2134. uploadData.value = uploadParams;
  2135. console.log('准备上传文件:', {
  2136. 文件名: file.name,
  2137. 文件大小: (file.size / 1024 / 1024).toFixed(2) + 'MB',
  2138. 工单ID: uploadParams.orderId || '待关联',
  2139. 上传阶段: uploadParams.uploadStage,
  2140. 描述: uploadParams.description,
  2141. 模式: form.value.id ? '编辑模式' : '新增模式'
  2142. });
  2143. return true;
  2144. };
  2145. /** 上传成功 */
  2146. const handleUploadSuccess = (response: any, file: any) => {
  2147. console.log('上传成功:', response, file);
  2148. if (response.code === 200) {
  2149. proxy?.$modal.msgSuccess(`文件 ${file.name} 上传成功`);
  2150. // 立即记录附件数据
  2151. const attachmentData = response.data;
  2152. if (attachmentData && attachmentData.id) {
  2153. // 如果是新增模式,收集附件ID到临时列表
  2154. if (!form.value.id) {
  2155. tempAttachmentIds.value.push(attachmentData.id);
  2156. console.log('新增模式:已收集附件ID', attachmentData.id, '当前临时附件ID列表:', tempAttachmentIds.value);
  2157. }
  2158. // 更新文件对象的相关信息
  2159. file.attachmentId = attachmentData.id;
  2160. file.status = 'success';
  2161. file.url = attachmentData.filePath;
  2162. // 如果是编辑模式,附件已经自动关联到工单,直接更新UI
  2163. if (form.value.id) {
  2164. console.log('编辑模式:附件已关联到工单', form.value.id, '附件ID:', attachmentData.id);
  2165. // 可以选择刷新附件列表以确保数据同步
  2166. // loadAttachments(form.value.id);
  2167. }
  2168. console.log('附件记录成功:', {
  2169. 附件ID: attachmentData.id,
  2170. 文件名: attachmentData.fileName,
  2171. 文件路径: attachmentData.filePath,
  2172. 工单ID: form.value.id || '待关联',
  2173. 模式: form.value.id ? '编辑' : '新增'
  2174. });
  2175. }
  2176. } else {
  2177. proxy?.$modal.msgError(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`);
  2178. }
  2179. };
  2180. /** 上传失败 */
  2181. const handleUploadError = (error: Error, file: any) => {
  2182. console.error('上传失败:', error, file);
  2183. proxy?.$modal.msgError(`文件 ${file.name} 上传失败`);
  2184. };
  2185. /** 下载附件 */
  2186. const handleDownloadAttachment = async (file: any) => {
  2187. let downloadUrl = '';
  2188. let fileName = file.name || 'attachment';
  2189. // 如果文件已经上传成功,使用服务器返回的路径
  2190. if (file.response && file.response.data && file.response.data.filePath) {
  2191. downloadUrl = file.response.data.filePath;
  2192. fileName = file.response.data.fileName || fileName;
  2193. }
  2194. // 如果是已有的附件,使用文件的url
  2195. else if (file.url) {
  2196. downloadUrl = file.url;
  2197. }
  2198. // 如果是本地文件,直接使用raw文件
  2199. else if (file.raw) {
  2200. const link = document.createElement('a');
  2201. link.href = URL.createObjectURL(file.raw);
  2202. link.download = fileName;
  2203. link.style.display = 'none';
  2204. document.body.appendChild(link);
  2205. link.click();
  2206. document.body.removeChild(link);
  2207. URL.revokeObjectURL(link.href);
  2208. console.log('下载本地文件:', fileName);
  2209. return;
  2210. }
  2211. if (!downloadUrl) {
  2212. proxy?.$modal.msgError('无法获取文件下载地址');
  2213. return;
  2214. }
  2215. // 对于服务器文件,使用fetch获取内容强制下载
  2216. try {
  2217. const loading = ElLoading.service({
  2218. text: '正在下载文件...',
  2219. target: document.body
  2220. });
  2221. const response = await fetch(downloadUrl);
  2222. if (!response.ok) {
  2223. throw new Error(`HTTP error! status: ${response.status}`);
  2224. }
  2225. const blob = await response.blob();
  2226. const url = window.URL.createObjectURL(blob);
  2227. const link = document.createElement('a');
  2228. link.href = url;
  2229. link.download = fileName;
  2230. link.style.display = 'none';
  2231. document.body.appendChild(link);
  2232. link.click();
  2233. document.body.removeChild(link);
  2234. window.URL.revokeObjectURL(url);
  2235. loading?.close();
  2236. console.log('下载服务器文件成功:', fileName);
  2237. } catch (error) {
  2238. console.error('下载文件失败:', error);
  2239. proxy?.$modal.msgError('下载文件失败,请稍后重试');
  2240. }
  2241. };
  2242. /** 删除附件确认 */
  2243. const handleRemoveAttachmentConfirm = async (file: any) => {
  2244. await proxy?.$modal.confirm('确定要删除该附件吗?删除操作将在保存时生效。');
  2245. handleRemoveAttachment(file);
  2246. };
  2247. /** 移除附件 (修改为延迟删除) */
  2248. const handleRemoveAttachment = (file: any) => {
  2249. // 如果是已上传的文件(编辑模式下的已有附件或新增模式下已上传的附件)
  2250. if (file.response && file.response.data && file.response.data.id) {
  2251. // 不立即删除数据库记录,而是标记为待删除
  2252. const attachmentId = file.response.data.id;
  2253. // 如果是新增模式,从临时附件ID列表中移除
  2254. if (!form.value.id) {
  2255. const attachmentIndex = tempAttachmentIds.value.indexOf(attachmentId);
  2256. if (attachmentIndex > -1) {
  2257. tempAttachmentIds.value.splice(attachmentIndex, 1);
  2258. console.log('新增模式:已从临时附件ID列表中移除:', attachmentId, '剩余附件数:', tempAttachmentIds.value.length);
  2259. }
  2260. } else {
  2261. // 编辑模式:添加到待删除列表
  2262. if (!pendingDeleteAttachmentIds.value.includes(attachmentId)) {
  2263. pendingDeleteAttachmentIds.value.push(attachmentId);
  2264. console.log('编辑模式:已标记附件为待删除:', attachmentId, '待删除列表:', pendingDeleteAttachmentIds.value);
  2265. }
  2266. }
  2267. // 从attachmentList中移除
  2268. const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
  2269. if (index > -1) {
  2270. attachmentList.value.splice(index, 1);
  2271. }
  2272. proxy?.$modal.msgSuccess('附件已移除,将在保存时生效');
  2273. }
  2274. // 如果是临时文件(新增时添加但未上传的文件),从临时列表中删除
  2275. else if (file.raw && !form.value.id) {
  2276. const index = tempFileList.value.findIndex((f) => f.name === file.raw.name && f.size === file.raw.size);
  2277. if (index > -1) {
  2278. tempFileList.value.splice(index, 1);
  2279. console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
  2280. }
  2281. }
  2282. // 如果是编辑模式下新添加但还未上传的文件
  2283. else if (file.raw && form.value.id) {
  2284. // 编辑模式下新添加的文件,直接从attachmentList中移除
  2285. const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
  2286. if (index > -1) {
  2287. attachmentList.value.splice(index, 1);
  2288. console.log('已从附件列表中移除未上传文件:', file.name);
  2289. }
  2290. }
  2291. };
  2292. /** 表单重置 */
  2293. const reset = async (autoFillUserInfo: boolean = false) => {
  2294. form.value = {
  2295. id: undefined,
  2296. title: '',
  2297. description: '',
  2298. repairLocation: '',
  2299. categoryId: '',
  2300. priorityLevel: undefined,
  2301. expectedFinishTime: '',
  2302. reporterName: '',
  2303. reporterPhone: '',
  2304. reporterDept: '',
  2305. orderStatus: '',
  2306. assigneeId: '',
  2307. assignRemark: '',
  2308. repairResult: '',
  2309. usedMaterials: '',
  2310. followSuggestions: '',
  2311. cancelReason: '',
  2312. remark: ''
  2313. };
  2314. // 清空附件相关数据
  2315. attachmentList.value = [];
  2316. tempFileList.value = [];
  2317. tempAttachmentIds.value = [];
  2318. pendingDeleteAttachmentIds.value = [];
  2319. console.log('已清空附件列表、临时文件列表、临时附件ID列表和待删除附件列表');
  2320. // 如果是新增操作,自动填充当前用户信息
  2321. if (autoFillUserInfo) {
  2322. try {
  2323. const userInfo = await getCurrentUserDetailInfo();
  2324. if (userInfo) {
  2325. form.value.reporterName = userInfo.nickName || userInfo.userName || '';
  2326. form.value.reporterPhone = userInfo.phonenumber || '';
  2327. form.value.reporterDept = userInfo.deptName || '';
  2328. console.log('自动填充报修人信息:', {
  2329. 姓名: form.value.reporterName,
  2330. 电话: form.value.reporterPhone,
  2331. 部门: form.value.reporterDept
  2332. });
  2333. // 显示提示信息
  2334. proxy?.$modal.msgSuccess('已自动填充报修人信息');
  2335. }
  2336. } catch (error) {
  2337. console.error('自动填充用户信息失败:', error);
  2338. proxy?.$modal.msgWarning('无法获取用户信息,请手动填写报修人信息');
  2339. }
  2340. }
  2341. repairOrderFormRef.value?.resetFields();
  2342. };
  2343. /** 取消按钮 */
  2344. const cancel = () => {
  2345. dialog.visible = false;
  2346. reset();
  2347. };
  2348. /** 获取紧急程度标签类型 */
  2349. const getPriorityTagType = (level: number) => {
  2350. const types: Record<number, string> = {
  2351. 1: 'danger',
  2352. 2: 'warning',
  2353. 3: 'primary',
  2354. 4: 'info',
  2355. 5: 'success'
  2356. };
  2357. return types[level] || 'info';
  2358. };
  2359. /** 获取紧急程度文本 */
  2360. const getPriorityText = (level: number) => {
  2361. const dict = pm_priority_level.value.find((item) => parseInt(item.value) === level);
  2362. return dict?.label || level.toString();
  2363. };
  2364. /** 获取状态标签类型 */
  2365. const getStatusTagType = (status: string) => {
  2366. const types: Record<string, string> = {
  2367. '1': 'info', // 待分派
  2368. '2': 'warning', // 已分派
  2369. '3': 'primary', // 进行中
  2370. '4': 'success', // 已完成
  2371. '5': 'success', // 已评价
  2372. '6': 'info', // 已归档
  2373. '9': 'danger' // 已取消
  2374. };
  2375. return types[status] || 'info';
  2376. };
  2377. /** 获取状态文本 */
  2378. const getStatusText = (status: string) => {
  2379. const dict = pm_order_status.value.find((item) => item.value === status);
  2380. return dict?.label || status;
  2381. };
  2382. /** 格式化时长 */
  2383. const formatDuration = (minutes: number) => {
  2384. if (minutes < 60) {
  2385. return `${minutes}分钟`;
  2386. } else {
  2387. const hours = Math.floor(minutes / 60);
  2388. const remainingMinutes = minutes % 60;
  2389. return remainingMinutes > 0 ? `${hours}小时${remainingMinutes}分钟` : `${hours}小时`;
  2390. }
  2391. };
  2392. /** 提交进度更新 */
  2393. const submitProgress = () => {
  2394. progressFormRef.value?.validate(async (valid: boolean) => {
  2395. if (valid) {
  2396. buttonLoading.value = true;
  2397. try {
  2398. await updateRepairProgress(progressForm.value);
  2399. proxy?.$modal.msgSuccess('进度更新成功');
  2400. updateProgressDialog.visible = false;
  2401. await getList();
  2402. } finally {
  2403. buttonLoading.value = false;
  2404. }
  2405. }
  2406. });
  2407. };
  2408. /** 取消进度更新 */
  2409. const cancelProgress = () => {
  2410. updateProgressDialog.visible = false;
  2411. };
  2412. /** 提交完成工单 */
  2413. const submitComplete = () => {
  2414. completeFormRef.value?.validate(async (valid: boolean) => {
  2415. if (valid) {
  2416. buttonLoading.value = true;
  2417. try {
  2418. await finishRepairOrder(completeForm.value.orderId, completeForm.value);
  2419. proxy?.$modal.msgSuccess('工单完成成功');
  2420. completeDialog.visible = false;
  2421. await getList();
  2422. } finally {
  2423. buttonLoading.value = false;
  2424. }
  2425. }
  2426. });
  2427. };
  2428. /** 取消完成工单 */
  2429. const cancelComplete = () => {
  2430. completeDialog.visible = false;
  2431. };
  2432. /** 提交服务评价 */
  2433. const submitEvaluation = () => {
  2434. evaluationFormRef.value?.validate(async (valid: boolean) => {
  2435. if (valid) {
  2436. buttonLoading.value = true;
  2437. try {
  2438. console.log('提交评价数据:', evaluationForm.value);
  2439. // 验证必要字段
  2440. if (!evaluationForm.value.orderId) {
  2441. proxy?.$modal.msgError('工单ID不能为空');
  2442. return;
  2443. }
  2444. if (!evaluationForm.value.evaluatedWorkerId) {
  2445. proxy?.$modal.msgError('请选择要评价的维修员');
  2446. return;
  2447. }
  2448. const response = await createServiceEvaluation(evaluationForm.value);
  2449. console.log('评价提交响应:', response);
  2450. proxy?.$modal.msgSuccess('服务评价提交成功');
  2451. evaluationDialog.visible = false;
  2452. // 刷新列表,确保状态更新
  2453. await getList();
  2454. // 如果工单状态没有自动更新,可能需要手动调用状态更新接口
  2455. // 这里可以根据后端API设计添加相应的状态更新逻辑
  2456. } catch (error: any) {
  2457. console.error('评价提交失败:', error);
  2458. // 显示详细的错误信息
  2459. const errorMsg = error?.response?.data?.msg || error?.message || '评价提交失败,请重试';
  2460. proxy?.$modal.msgError(errorMsg);
  2461. } finally {
  2462. buttonLoading.value = false;
  2463. }
  2464. }
  2465. });
  2466. };
  2467. /** 取消服务评价 */
  2468. const cancelEvaluation = () => {
  2469. evaluationDialog.visible = false;
  2470. };
  2471. /** 下载附件 */
  2472. const downloadAttachment = async (attachment: any) => {
  2473. if (!attachment.filePath) {
  2474. proxy?.$modal.msgError('无法获取文件下载地址');
  2475. return;
  2476. }
  2477. try {
  2478. // 显示加载提示
  2479. const loading = ElLoading.service({
  2480. text: '正在下载文件...',
  2481. target: document.body
  2482. });
  2483. // 通过fetch获取文件内容,强制下载
  2484. const response = await fetch(attachment.filePath);
  2485. if (!response.ok) {
  2486. throw new Error(`HTTP error! status: ${response.status}`);
  2487. }
  2488. const blob = await response.blob();
  2489. const url = window.URL.createObjectURL(blob);
  2490. // 创建下载链接
  2491. const link = document.createElement('a');
  2492. link.href = url;
  2493. link.download = attachment.fileName || '附件';
  2494. link.style.display = 'none';
  2495. document.body.appendChild(link);
  2496. link.click();
  2497. document.body.removeChild(link);
  2498. // 释放blob URL
  2499. window.URL.revokeObjectURL(url);
  2500. loading?.close();
  2501. console.log('下载附件成功:', attachment.fileName || '附件');
  2502. } catch (error) {
  2503. console.error('下载附件失败:', error);
  2504. proxy?.$modal.msgError('下载文件失败,请稍后重试');
  2505. }
  2506. };
  2507. /** 预览附件 */
  2508. const previewAttachment = (attachment: any) => {
  2509. if (attachment.fileType === 'image') {
  2510. // 图片预览已内置在el-image组件中
  2511. return;
  2512. }
  2513. // 其他类型文件在新窗口打开
  2514. window.open(attachment.filePath, '_blank');
  2515. };
  2516. /** 格式化文件大小 */
  2517. const formatFileSize = (bytes: number): string => {
  2518. if (bytes === 0) return '0 B';
  2519. const k = 1024;
  2520. const sizes = ['B', 'KB', 'MB', 'GB'];
  2521. const i = Math.floor(Math.log(bytes) / Math.log(k));
  2522. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  2523. };
  2524. /** 文件变更处理 */
  2525. const handleFileChange = (file: any, fileList: any[]) => {
  2526. console.log('文件变更:', file, fileList);
  2527. // 新增时收集文件到临时列表
  2528. if (!form.value.id && file.raw && file.status !== 'removed') {
  2529. // 检查文件大小
  2530. const isLt10M = file.raw.size / 1024 / 1024 < 10;
  2531. if (!isLt10M) {
  2532. proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
  2533. // 从文件列表中移除这个文件
  2534. const index = fileList.findIndex((f) => f.uid === file.uid);
  2535. if (index > -1) {
  2536. fileList.splice(index, 1);
  2537. }
  2538. return;
  2539. }
  2540. // 如果是添加文件状态,添加到临时列表
  2541. if (file.status === 'ready' || !file.status) {
  2542. // 检查是否已存在(避免重复添加)
  2543. const existingFile = tempFileList.value.find(
  2544. (f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
  2545. );
  2546. if (!existingFile) {
  2547. tempFileList.value.push(file.raw);
  2548. console.log('已添加文件到临时列表:', file.raw.name, '总文件数:', tempFileList.value.length);
  2549. }
  2550. }
  2551. }
  2552. // 编辑模式下新添加文件的处理
  2553. if (form.value.id && file.raw && file.status !== 'removed' && (file.status === 'ready' || !file.status)) {
  2554. // 检查文件大小
  2555. const isLt10M = file.raw.size / 1024 / 1024 < 10;
  2556. if (!isLt10M) {
  2557. proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
  2558. // 从文件列表中移除这个文件
  2559. const index = fileList.findIndex((f) => f.uid === file.uid);
  2560. if (index > -1) {
  2561. fileList.splice(index, 1);
  2562. }
  2563. return;
  2564. }
  2565. console.log('编辑模式下添加新文件:', file.name);
  2566. }
  2567. // 如果是删除文件,相应处理
  2568. if (file.status === 'removed') {
  2569. if (!form.value.id && file.raw) {
  2570. // 新增模式下,从临时列表中移除
  2571. const index = tempFileList.value.findIndex(
  2572. (f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
  2573. );
  2574. if (index > -1) {
  2575. tempFileList.value.splice(index, 1);
  2576. console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
  2577. }
  2578. }
  2579. // 编辑模式下的删除在handleRemoveAttachment中处理
  2580. }
  2581. };
  2582. /** 获取进度类型图标颜色 */
  2583. const getProgressIconColor = (progressType: string) => {
  2584. const colorMap: Record<string, string> = {
  2585. '1': '#409eff', // 工单分派 - 蓝色
  2586. '2': '#67c23a', // 接收确认 - 绿色
  2587. '3': '#e6a23c', // 到达现场 - 橙色
  2588. '4': '#409eff', // 开始维修 - 蓝色
  2589. '5': '#909399', // 进度更新 - 灰色
  2590. '6': '#67c23a', // 完成维修 - 绿色
  2591. '7': '#f56c6c', // 用户评价 - 红色
  2592. '8': '#606266', // 工单归档 - 深灰色
  2593. '9': '#909399' // 其他 - 灰色
  2594. };
  2595. return colorMap[progressType] || '#909399';
  2596. };
  2597. /** 获取进度类型标签类型 */
  2598. const getProgressTypeTag = (progressType: string): 'success' | 'warning' | 'info' | 'primary' | 'danger' => {
  2599. const tagMap: Record<string, 'success' | 'warning' | 'info' | 'primary' | 'danger'> = {
  2600. '1': 'primary',
  2601. '2': 'success',
  2602. '3': 'warning',
  2603. '4': 'primary',
  2604. '5': 'info',
  2605. '6': 'success',
  2606. '7': 'danger',
  2607. '8': 'info',
  2608. '9': 'info'
  2609. };
  2610. return tagMap[progressType] || 'info';
  2611. };
  2612. /** 获取进度类型文本 */
  2613. const getProgressTypeText = (progressType: string) => {
  2614. const textMap: Record<string, string> = {
  2615. '1': '工单分派',
  2616. '2': '接收确认',
  2617. '3': '到达现场',
  2618. '4': '开始维修',
  2619. '5': '进度更新',
  2620. '6': '完成维修',
  2621. '7': '用户评价',
  2622. '8': '工单归档',
  2623. '9': '其他'
  2624. };
  2625. return textMap[progressType] || '未知类型';
  2626. };
  2627. /** 检查是否有额外信息 */
  2628. const hasAdditionalInfo = (progress: any) => {
  2629. return (
  2630. progress.locationInfo ||
  2631. progress.estimatedDuration ||
  2632. progress.nextStep ||
  2633. progress.issuesEncountered ||
  2634. progress.materialsNeeded ||
  2635. progress.remark
  2636. );
  2637. };
  2638. onMounted(() => {
  2639. getList();
  2640. getStatistics();
  2641. getCategoryOptions();
  2642. getCategoryTreeOptions();
  2643. getWorkerOptions();
  2644. getCurrentUserInfo();
  2645. });
  2646. /** 卡片预览 */
  2647. const handleFilePreview = (file: any) => {
  2648. // 如果文件已经上传成功,优先使用服务器返回的url
  2649. let url = file.url || (file.response && file.response.data && file.response.data.filePath);
  2650. // 若还未上传,则尝试使用本地预览地址
  2651. if (!url && file.raw) {
  2652. url = URL.createObjectURL(file.raw);
  2653. }
  2654. if (!url) return;
  2655. // 对图片使用图片预览,其余直接新窗口打开
  2656. const isImage = file.type ? file.type.startsWith('image') : file.raw && file.raw.type.startsWith('image');
  2657. if (isImage) {
  2658. // 利用 Element Plus ImageViewer 进行预览
  2659. const viewer = (proxy as any)?.$imageViewer;
  2660. viewer && viewer([url]);
  2661. } else {
  2662. window.open(url, '_blank');
  2663. }
  2664. };
  2665. /** 判断是否为图片文件 */
  2666. const isImageFile = (file: any): boolean => {
  2667. // 优先使用文件 MIME type 判断
  2668. const type = file.type || (file.raw && file.raw.type) || '';
  2669. if (type && type.startsWith('image')) {
  2670. return true;
  2671. }
  2672. // 编辑场景下从数据库加载的附件通常缺少 MIME type
  2673. // 通过文件名或 URL 后缀进行判断
  2674. const name: string = file.name || file.response?.data?.fileName || '';
  2675. const url: string = file.url || file.response?.data?.filePath || '';
  2676. const source = (name || url).toLowerCase();
  2677. // 常见图片扩展名
  2678. const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico'];
  2679. return IMAGE_EXTS.some((ext) => source.endsWith(ext));
  2680. };
  2681. /** 获取文件扩展名小写 */
  2682. const getFileExt = (fileName: string): string => {
  2683. if (!fileName) return '';
  2684. const idx = fileName.lastIndexOf('.');
  2685. return idx !== -1 ? fileName.substring(idx + 1).toLowerCase() : '';
  2686. };
  2687. </script>
  2688. <style lang="scss" scoped>
  2689. /* 页面容器样式 */
  2690. .page-container {
  2691. padding: 8px;
  2692. }
  2693. /* 搜索按钮容器样式 */
  2694. .search-btn-container {
  2695. display: flex;
  2696. justify-content: flex-end;
  2697. margin-top: 10px;
  2698. .el-form-item {
  2699. margin-bottom: 0 !important;
  2700. }
  2701. }
  2702. /* 表格卡片布局 */
  2703. .table-card {
  2704. margin-top: 0;
  2705. }
  2706. .table-card :deep(.el-card__body) {
  2707. padding: 0px 20px 20px 20px !important;
  2708. }
  2709. /* 表格主体 */
  2710. .table-container {
  2711. margin-bottom: 0;
  2712. }
  2713. /* 移除按钮栏底部边框 */
  2714. .button-card :deep(.el-card__header) {
  2715. border-bottom: none !important;
  2716. }
  2717. /* 统计卡片样式 */
  2718. .stat-card {
  2719. border-radius: 8px;
  2720. transition: all 0.3s ease;
  2721. height: 90px;
  2722. &:hover {
  2723. transform: translateY(-2px);
  2724. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
  2725. }
  2726. .stat-content {
  2727. display: flex;
  2728. align-items: center;
  2729. padding: 5px 12px;
  2730. height: 100%;
  2731. box-sizing: border-box;
  2732. .stat-icon {
  2733. font-size: 24px;
  2734. margin-right: 12px;
  2735. opacity: 0.8;
  2736. }
  2737. .stat-info {
  2738. flex: 1;
  2739. .stat-value {
  2740. font-size: 20px;
  2741. font-weight: bold;
  2742. line-height: 1;
  2743. margin-bottom: 3px;
  2744. }
  2745. .stat-label {
  2746. font-size: 13px;
  2747. opacity: 0.7;
  2748. line-height: 1;
  2749. }
  2750. }
  2751. }
  2752. &.stat-card-primary {
  2753. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2754. color: white;
  2755. }
  2756. &.stat-card-success {
  2757. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  2758. color: white;
  2759. }
  2760. &.stat-card-warning {
  2761. background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
  2762. color: white;
  2763. }
  2764. &.stat-card-info {
  2765. background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
  2766. color: #333;
  2767. }
  2768. }
  2769. /* 表格样式 */
  2770. .scroll-table {
  2771. :deep(.el-table__header-wrapper) {
  2772. .el-table__header {
  2773. th {
  2774. background-color: #f8f9fa;
  2775. color: #333;
  2776. font-weight: 600;
  2777. }
  2778. }
  2779. }
  2780. :deep(.el-table__body-wrapper) {
  2781. .el-table__row {
  2782. &:hover {
  2783. background-color: #f5f7fa;
  2784. }
  2785. }
  2786. }
  2787. }
  2788. /* 工单信息样式 */
  2789. .order-info {
  2790. width: 100%;
  2791. box-sizing: border-box; // 使内边距计入总宽度,避免超出表单内容区域
  2792. background-color: #f8f9fa;
  2793. padding: 16px;
  2794. border-radius: 8px;
  2795. border-left: 4px solid #409eff;
  2796. p {
  2797. margin: 8px 0;
  2798. line-height: 1.5;
  2799. &:first-child {
  2800. margin-top: 0;
  2801. }
  2802. &:last-child {
  2803. margin-bottom: 0;
  2804. }
  2805. }
  2806. }
  2807. /* 对话框样式 */
  2808. .dialog-footer-right {
  2809. display: flex;
  2810. justify-content: flex-end;
  2811. gap: 10px;
  2812. }
  2813. /* 分页容器样式 */
  2814. .pagination-container {
  2815. display: flex;
  2816. justify-content: flex-end;
  2817. padding: 16px 20px 0 0 !important;
  2818. background-color: #fff;
  2819. margin-top: 0px !important;
  2820. }
  2821. /* 附件上传容器样式 */
  2822. .attachment-upload-container {
  2823. .upload-demo {
  2824. .el-upload {
  2825. /* border: 2px dashed #d9d9d9; */
  2826. border-radius: 6px;
  2827. cursor: pointer;
  2828. position: relative;
  2829. overflow: hidden;
  2830. transition: all 0.3s;
  2831. &:hover {
  2832. border-color: #409eff;
  2833. }
  2834. }
  2835. .el-upload__tip {
  2836. font-size: 12px;
  2837. color: #000000;
  2838. margin-top: 7px;
  2839. }
  2840. // 调整拖拽上传图标与文字在同一行、居中对齐
  2841. .el-upload-dragger1 {
  2842. display: flex;
  2843. flex-direction: column;
  2844. align-items: center;
  2845. justify-content: center;
  2846. gap: 15px;
  2847. padding: 0px !important;
  2848. }
  2849. .el-upload-dragger1 .el-icon--upload {
  2850. margin: 0;
  2851. padding: 0px !important;
  2852. }
  2853. /* 文件卡片占位封面样式 */
  2854. .upload-file-placeholder {
  2855. width: 100%;
  2856. height: 100%;
  2857. display: flex;
  2858. flex-direction: column;
  2859. align-items: center;
  2860. justify-content: center;
  2861. color: #909399;
  2862. font-size: 14px;
  2863. .el-icon {
  2864. font-size: 28px;
  2865. margin-bottom: 4px;
  2866. }
  2867. .file-ext {
  2868. font-size: 12px;
  2869. text-transform: uppercase;
  2870. text-align: center;
  2871. }
  2872. }
  2873. }
  2874. /* 让附件卡片一行展示 4 个 */
  2875. :deep(.el-upload-list--picture-card) {
  2876. display: grid;
  2877. grid-template-columns: repeat(4, 1fr);
  2878. gap: 9px;
  2879. margin-top: 12px;
  2880. /* 确保卡片宽度自适应 */
  2881. .el-upload-list__item {
  2882. margin: 0;
  2883. }
  2884. }
  2885. }
  2886. :deep(.el-upload-dragger) {
  2887. border: 0px !important;
  2888. padding: 0px !important;
  2889. .el-upload-dragger1 {
  2890. border: 0px !important;
  2891. padding: 0px !important;
  2892. }
  2893. }
  2894. /* 附件查看对话框样式 */
  2895. .attachment-container {
  2896. .no-attachment {
  2897. text-align: center;
  2898. padding: 40px 0;
  2899. }
  2900. .attachment-list {
  2901. .attachment-item {
  2902. display: flex;
  2903. align-items: center;
  2904. justify-content: space-between;
  2905. padding: 12px;
  2906. border: 1px solid #e8e8e8;
  2907. border-radius: 8px;
  2908. margin-bottom: 8px;
  2909. transition: all 0.3s;
  2910. &:last-child {
  2911. margin-bottom: 0;
  2912. }
  2913. &:hover {
  2914. border-color: #409eff;
  2915. background-color: #f0f9ff;
  2916. }
  2917. .attachment-info {
  2918. display: flex;
  2919. align-items: center;
  2920. flex: 1;
  2921. .attachment-preview {
  2922. margin-right: 12px;
  2923. .file-icon {
  2924. color: #909399;
  2925. }
  2926. }
  2927. .attachment-details {
  2928. flex: 1;
  2929. .file-name {
  2930. font-weight: 500;
  2931. color: #333;
  2932. margin-bottom: 4px;
  2933. word-break: break-all;
  2934. }
  2935. .file-meta {
  2936. font-size: 12px;
  2937. color: #909399;
  2938. display: flex;
  2939. gap: 12px;
  2940. }
  2941. .file-description {
  2942. font-size: 12px;
  2943. color: #666;
  2944. margin-top: 4px;
  2945. font-style: italic;
  2946. }
  2947. }
  2948. }
  2949. .attachment-actions {
  2950. display: flex;
  2951. gap: 8px;
  2952. }
  2953. }
  2954. }
  2955. }
  2956. /* 详情弹窗样式 */
  2957. .repair-order-detail {
  2958. .detail-card {
  2959. margin-bottom: 16px;
  2960. border: 1px solid #e8e8e8;
  2961. border-radius: 8px;
  2962. &:last-child {
  2963. margin-bottom: 0;
  2964. }
  2965. .card-header {
  2966. display: flex;
  2967. align-items: center;
  2968. font-weight: 600;
  2969. color: #333;
  2970. .card-title {
  2971. display: flex;
  2972. align-items: center;
  2973. font-size: 16px;
  2974. .el-icon {
  2975. margin-right: 8px;
  2976. font-size: 18px;
  2977. color: #409eff;
  2978. }
  2979. }
  2980. }
  2981. .detail-grid {
  2982. display: grid;
  2983. grid-template-columns: 1fr 1fr;
  2984. gap: 16px 24px;
  2985. .detail-item {
  2986. display: flex;
  2987. align-items: flex-start;
  2988. &.full-width {
  2989. grid-column: 1 / -1;
  2990. }
  2991. label {
  2992. flex-shrink: 0;
  2993. width: 120px;
  2994. font-weight: 500;
  2995. color: #666;
  2996. font-size: 14px;
  2997. line-height: 1.5;
  2998. }
  2999. .detail-value {
  3000. flex: 1;
  3001. color: #333;
  3002. font-size: 14px;
  3003. line-height: 1.5;
  3004. &.description {
  3005. line-height: 1.6;
  3006. word-break: break-word;
  3007. white-space: pre-wrap;
  3008. background-color: #f8f9fa;
  3009. padding: 8px 12px;
  3010. border-radius: 4px;
  3011. border-left: 3px solid #409eff;
  3012. }
  3013. }
  3014. .el-tag {
  3015. font-weight: 500;
  3016. }
  3017. .el-rate {
  3018. display: flex;
  3019. align-items: center;
  3020. }
  3021. }
  3022. }
  3023. }
  3024. /* 响应式设计 */
  3025. @media (max-width: 768px) {
  3026. .detail-card .detail-grid {
  3027. grid-template-columns: 1fr;
  3028. gap: 12px;
  3029. .detail-item {
  3030. flex-direction: column;
  3031. align-items: flex-start;
  3032. label {
  3033. width: auto;
  3034. margin-bottom: 4px;
  3035. }
  3036. }
  3037. }
  3038. }
  3039. }
  3040. /* 附件网格卡片样式 */
  3041. .attachment-detail-container {
  3042. .no-attachment-detail {
  3043. text-align: center;
  3044. padding: 40px 0;
  3045. color: #909399;
  3046. }
  3047. .attachment-grid-container {
  3048. display: grid;
  3049. grid-template-columns: repeat(3, 1fr);
  3050. gap: 16px;
  3051. padding: 8px 0;
  3052. .attachment-card {
  3053. background: #ffffff;
  3054. border: 1px solid #e8e8e8;
  3055. border-radius: 8px;
  3056. overflow: hidden;
  3057. transition: all 0.3s ease;
  3058. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  3059. &:hover {
  3060. border-color: #409eff;
  3061. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
  3062. transform: translateY(-2px);
  3063. }
  3064. .attachment-thumbnail-wrapper {
  3065. position: relative;
  3066. height: 120px;
  3067. background: #f5f7fa;
  3068. display: flex;
  3069. align-items: center;
  3070. justify-content: center;
  3071. overflow: hidden;
  3072. .attachment-image {
  3073. width: 100%;
  3074. height: 100%;
  3075. object-fit: cover;
  3076. }
  3077. .attachment-file-icon {
  3078. display: flex;
  3079. flex-direction: column;
  3080. align-items: center;
  3081. gap: 8px;
  3082. color: #409eff;
  3083. .file-type-text {
  3084. font-size: 12px;
  3085. color: #666;
  3086. font-weight: 500;
  3087. }
  3088. }
  3089. }
  3090. .attachment-card-body {
  3091. padding: 12px;
  3092. height: 80px;
  3093. display: flex;
  3094. flex-direction: column;
  3095. justify-content: space-between;
  3096. .attachment-title {
  3097. font-size: 13px;
  3098. font-weight: 600;
  3099. color: #333;
  3100. line-height: 1.3;
  3101. margin-bottom: 6px;
  3102. overflow: hidden;
  3103. text-overflow: ellipsis;
  3104. white-space: nowrap;
  3105. }
  3106. .attachment-meta {
  3107. display: flex;
  3108. justify-content: space-between;
  3109. font-size: 11px;
  3110. color: #999;
  3111. margin-bottom: 4px;
  3112. .file-size {
  3113. font-weight: 500;
  3114. }
  3115. .upload-time {
  3116. font-style: italic;
  3117. }
  3118. }
  3119. .attachment-desc {
  3120. font-size: 11px;
  3121. color: #666;
  3122. line-height: 1.3;
  3123. overflow: hidden;
  3124. text-overflow: ellipsis;
  3125. white-space: nowrap;
  3126. margin-top: auto;
  3127. }
  3128. }
  3129. .attachment-card-actions {
  3130. padding: 8px 12px;
  3131. border-top: 1px solid #f0f0f0;
  3132. display: flex;
  3133. justify-content: center;
  3134. .el-button-group {
  3135. .el-button {
  3136. padding: 4px 6px;
  3137. font-size: 12px;
  3138. .el-icon {
  3139. font-size: 14px;
  3140. }
  3141. }
  3142. }
  3143. }
  3144. }
  3145. }
  3146. }
  3147. /* 卡片头部样式增强 */
  3148. .detail-card .card-header {
  3149. display: flex;
  3150. align-items: center;
  3151. justify-content: space-between;
  3152. width: 100%;
  3153. .card-title {
  3154. flex: 1;
  3155. }
  3156. }
  3157. /* 进度查看样式 */
  3158. .progress-container {
  3159. .no-progress {
  3160. text-align: center;
  3161. padding: 40px 0;
  3162. color: #909399;
  3163. }
  3164. .progress-timeline {
  3165. position: relative;
  3166. padding: 16px 0;
  3167. .progress-item {
  3168. position: relative;
  3169. display: flex;
  3170. align-items: flex-start;
  3171. margin-bottom: 24px;
  3172. &:last-child {
  3173. margin-bottom: 0;
  3174. .timeline-line {
  3175. display: none;
  3176. }
  3177. }
  3178. .timeline-icon {
  3179. position: relative;
  3180. z-index: 2;
  3181. width: 32px;
  3182. height: 32px;
  3183. border-radius: 50%;
  3184. background: white;
  3185. border: 2px solid #e8e8e8;
  3186. display: flex;
  3187. align-items: center;
  3188. justify-content: center;
  3189. margin-right: 16px;
  3190. flex-shrink: 0;
  3191. margin-top: 8px;
  3192. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  3193. }
  3194. .timeline-line {
  3195. position: absolute;
  3196. left: 15px;
  3197. top: 40px;
  3198. width: 2px;
  3199. height: calc(100% + 8px);
  3200. background: linear-gradient(to bottom, #e8e8e8 0%, #f0f0f0 100%);
  3201. z-index: 1;
  3202. }
  3203. .progress-card {
  3204. flex: 1;
  3205. background: white;
  3206. border: 1px solid #e8e8e8;
  3207. border-radius: 8px;
  3208. padding: 16px;
  3209. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  3210. transition: all 0.3s ease;
  3211. &:hover {
  3212. border-color: #409eff;
  3213. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
  3214. transform: translateY(-1px);
  3215. }
  3216. .progress-header {
  3217. display: flex;
  3218. justify-content: space-between;
  3219. align-items: center;
  3220. margin-bottom: 12px;
  3221. padding-bottom: 12px;
  3222. border-bottom: 1px solid #f0f0f0;
  3223. .progress-type-status {
  3224. display: flex;
  3225. align-items: center;
  3226. gap: 8px;
  3227. .progress-status {
  3228. font-weight: 600;
  3229. color: #333;
  3230. font-size: 14px;
  3231. }
  3232. }
  3233. .progress-time {
  3234. display: flex;
  3235. align-items: center;
  3236. gap: 4px;
  3237. font-size: 13px;
  3238. color: #666;
  3239. .el-icon {
  3240. font-size: 14px;
  3241. }
  3242. }
  3243. }
  3244. .progress-content {
  3245. .progress-detail {
  3246. margin-bottom: 12px;
  3247. .detail-text {
  3248. margin: 0;
  3249. line-height: 1.6;
  3250. color: #333;
  3251. font-size: 14px;
  3252. background: #f8f9fa;
  3253. padding: 12px;
  3254. border-radius: 6px;
  3255. border-left: 3px solid #409eff;
  3256. }
  3257. }
  3258. .progress-info-grid {
  3259. display: grid;
  3260. grid-template-columns: 1fr;
  3261. gap: 8px;
  3262. .info-item {
  3263. display: flex;
  3264. align-items: flex-start;
  3265. font-size: 13px;
  3266. line-height: 1.5;
  3267. label {
  3268. display: flex;
  3269. align-items: center;
  3270. gap: 4px;
  3271. min-width: 80px;
  3272. color: #666;
  3273. font-weight: 500;
  3274. margin-right: 8px;
  3275. .el-icon {
  3276. font-size: 14px;
  3277. color: #409eff;
  3278. }
  3279. }
  3280. span {
  3281. flex: 1;
  3282. color: #333;
  3283. word-break: break-word;
  3284. }
  3285. }
  3286. }
  3287. }
  3288. }
  3289. }
  3290. }
  3291. }
  3292. /* 响应式设计 */
  3293. @media (max-width: 1200px) {
  3294. .attachment-detail-container .attachment-grid-container {
  3295. grid-template-columns: repeat(2, 1fr);
  3296. gap: 14px;
  3297. }
  3298. }
  3299. @media (max-width: 768px) {
  3300. .attachment-detail-container .attachment-grid-container {
  3301. grid-template-columns: 1fr;
  3302. gap: 12px;
  3303. .attachment-card {
  3304. .attachment-thumbnail-wrapper {
  3305. height: 100px;
  3306. }
  3307. .attachment-card-body {
  3308. height: 70px;
  3309. padding: 10px;
  3310. .attachment-title {
  3311. font-size: 12px;
  3312. }
  3313. .attachment-meta {
  3314. font-size: 10px;
  3315. }
  3316. }
  3317. .attachment-card-actions {
  3318. padding: 6px 10px;
  3319. .el-button-group .el-button {
  3320. padding: 3px 5px;
  3321. font-size: 11px;
  3322. .el-icon {
  3323. font-size: 12px;
  3324. }
  3325. }
  3326. }
  3327. }
  3328. }
  3329. /* 进度查看响应式 */
  3330. .progress-container .progress-timeline .progress-item {
  3331. .timeline-icon {
  3332. width: 28px;
  3333. height: 28px;
  3334. margin-right: 12px;
  3335. }
  3336. .timeline-line {
  3337. left: 13px;
  3338. }
  3339. .progress-card {
  3340. padding: 12px;
  3341. .progress-header {
  3342. flex-direction: column;
  3343. align-items: flex-start;
  3344. gap: 8px;
  3345. .progress-time {
  3346. font-size: 12px;
  3347. }
  3348. }
  3349. .progress-content {
  3350. .progress-detail .detail-text {
  3351. padding: 8px;
  3352. font-size: 13px;
  3353. }
  3354. .progress-info-grid .info-item {
  3355. font-size: 12px;
  3356. label {
  3357. min-width: 70px;
  3358. font-size: 12px;
  3359. }
  3360. }
  3361. }
  3362. }
  3363. }
  3364. }
  3365. :deep(.el-icon) {
  3366. display: flex !important;
  3367. }
  3368. /* Element Plus upload 组件样式覆盖 */
  3369. :deep(.el-upload-list--picture-card) {
  3370. .el-upload-list__item {
  3371. width: 166px !important;
  3372. height: auto !important;
  3373. min-height: 180px !important;
  3374. margin: 0 8px 8px 0 !important;
  3375. border-radius: 6px !important;
  3376. overflow: hidden !important;
  3377. }
  3378. }
  3379. /* 卡片整体布局包含文件名 */
  3380. .upload-file-card {
  3381. width: 100%;
  3382. height: 100%;
  3383. display: flex;
  3384. flex-direction: column;
  3385. align-items: center;
  3386. min-height: 180px;
  3387. position: relative;
  3388. }
  3389. .upload-file-thumb {
  3390. width: 100%;
  3391. height: 120px;
  3392. position: relative;
  3393. overflow: hidden;
  3394. border-radius: 4px 4px 0 0;
  3395. }
  3396. .upload-file-thumb .el-image,
  3397. .upload-file-thumb .upload-file-placeholder {
  3398. width: 100%;
  3399. height: 100%;
  3400. }
  3401. .upload-file-placeholder {
  3402. display: flex;
  3403. flex-direction: column;
  3404. align-items: center;
  3405. justify-content: center;
  3406. background-color: #fafafa;
  3407. .el-icon {
  3408. font-size: 24px;
  3409. color: #c0c4cc;
  3410. margin-bottom: 8px;
  3411. }
  3412. .file-ext {
  3413. font-size: 12px;
  3414. color: #909399;
  3415. text-align: center;
  3416. word-break: break-all;
  3417. padding: 0 8px;
  3418. }
  3419. }
  3420. .upload-file-name {
  3421. width: 100%;
  3422. padding: 8px 4px 4px 4px;
  3423. font-size: 12px;
  3424. text-align: center;
  3425. overflow: hidden;
  3426. text-overflow: ellipsis;
  3427. white-space: nowrap;
  3428. color: #606266;
  3429. background-color: #fff;
  3430. flex-shrink: 0;
  3431. }
  3432. /* 上传文件卡片操作按钮样式 (与预览界面保持一致) */
  3433. .upload-file-card-actions {
  3434. width: 100%;
  3435. padding: 6px 8px;
  3436. border-top: 1px solid #f0f0f0;
  3437. display: flex;
  3438. justify-content: center;
  3439. background-color: #fafafa;
  3440. margin-top: auto;
  3441. flex-shrink: 0;
  3442. .el-button-group {
  3443. .el-button {
  3444. padding: 4px 6px;
  3445. font-size: 12px;
  3446. .el-icon {
  3447. font-size: 14px;
  3448. }
  3449. }
  3450. }
  3451. }
  3452. </style>