| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833 |
- <template>
- <div class="p-2 page-container">
- <!-- 搜索区域 -->
- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
- <div v-show="showSearch" class="mb-[10px]">
- <el-card shadow="hover">
- <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
- <!-- 第一行:4个搜索条件 -->
- <el-row :gutter="20">
- <el-col :span="6">
- <el-form-item label="工单编号" prop="orderNo">
- <el-input v-model="queryParams.orderNo" placeholder="请输入工单编号" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="报修标题" prop="title">
- <el-input v-model="queryParams.title" placeholder="请输入报修标题" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="报修地点" prop="repairLocation">
- <el-input v-model="queryParams.repairLocation" placeholder="请输入报修地点" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="维修类型" prop="categoryId">
- <el-tree-select
- v-model="queryParams.categoryId"
- :data="categoryTreeOptions"
- :props="{ value: 'id', label: 'categoryName', children: 'children' }"
- value-key="id"
- placeholder="请选择维修类型"
- check-strictly
- :render-after-expand="false"
- clearable
- filterable
- style="width: 100%"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <!-- 第二行:4个搜索条件 + 操作按钮 -->
- <el-row :gutter="20">
- <el-col :span="6">
- <el-form-item label="紧急程度" prop="priorityLevel">
- <el-select v-model="queryParams.priorityLevel" placeholder="请选择紧急程度" clearable style="width: 100%">
- <el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="工单状态" prop="orderStatus">
- <el-select v-model="queryParams.orderStatus" placeholder="请选择工单状态" clearable style="width: 100%">
- <el-option v-for="dict in pm_order_status" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="报修人" prop="reporterName">
- <el-input v-model="queryParams.reporterName" placeholder="请输入报修人姓名" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="创建时间">
- <el-date-picker
- v-model="daterangeCreateTime"
- value-format="YYYY-MM-DD"
- type="daterange"
- range-separator="-"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- @change="handleQuery"
- style="width: 100%"
- ></el-date-picker>
- </el-form-item>
- </el-col>
- </el-row>
- <!-- 操作按钮行 -->
- <el-row>
- <el-col :span="24" class="search-btn-container">
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-card>
- </div>
- </transition>
- <!-- 统计卡片区域 - 仅维护管理员和超级管理员可见 -->
- <!-- <div v-if="canViewStatistics" class="mb-[10px]">
- <el-row :gutter="16">
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-primary">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Files /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.totalOrders }}</div>
- <div class="stat-label">总工单数</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-warning">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Clock /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.pendingAssign }}</div>
- <div class="stat-label">待分派</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-info">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Loading /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.inProgress }}</div>
- <div class="stat-label">进行中</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-success">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><CircleCheck /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.completed }}</div>
- <div class="stat-label">已完成</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-info">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Timer /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.avgProcessTime }}</div>
- <div class="stat-label">平均时长(小时)</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-primary">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Star /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.avgEvaluationScore }}</div>
- <div class="stat-label">平均评分</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-success">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><TrendCharts /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.monthlyNewOrders }}</div>
- <div class="stat-label">本月新增</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="3">
- <el-card shadow="hover" class="stat-card stat-card-warning">
- <div class="stat-content">
- <div class="stat-icon">
- <el-icon><Finished /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ statistics.monthlyCompletedOrders }}</div>
- <div class="stat-label">本月完成</div>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- </div> -->
- <!-- 表格区域 -->
- <el-card shadow="never" class="button-card table-card">
- <template #header>
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['pm:repairOrder:add']">新增报修</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['pm:repairOrder:edit']">修改</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']">删除</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
- </el-col>
- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
- </el-row>
- </template>
- <div class="table-container">
- <el-table
- v-loading="loading"
- border
- :data="repairOrderList"
- @selection-change="handleSelectionChange"
- @row-dblclick="handleDetail"
- style="width: 100%"
- :scroll-x="true"
- class="scroll-table"
- >
- <el-table-column type="selection" width="55" align="center" fixed="left" />
- <el-table-column label="工单编号" align="center" prop="orderNo" width="140" show-overflow-tooltip fixed="left" />
- <el-table-column label="报修标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
- <el-table-column label="报修地点" align="center" prop="repairLocation" min-width="180" show-overflow-tooltip />
- <el-table-column label="维修类型" align="center" prop="categoryName" width="120" show-overflow-tooltip>
- <template #default="scope">
- {{ getCategoryNameById(scope.row.categoryId) }}
- </template>
- </el-table-column>
- <el-table-column label="紧急程度" align="center" prop="priorityLevel" width="100">
- <template #default="scope">
- <el-tag :type="getPriorityTagType(scope.row.priorityLevel) as any" size="small">
- {{ getPriorityText(scope.row.priorityLevel) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="报修人" align="center" prop="reporterName" width="100" show-overflow-tooltip />
- <el-table-column label="报修人电话" align="center" prop="reporterPhone" width="120" show-overflow-tooltip />
- <el-table-column label="工单状态" align="center" prop="orderStatus" width="100">
- <template #default="scope">
- <el-tag :type="getStatusTagType(scope.row.orderStatus) as any" size="small">
- {{ getStatusText(scope.row.orderStatus) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="分派维修员" align="center" prop="assigneeName" width="120" show-overflow-tooltip />
- <el-table-column label="期望完成时间" align="center" prop="expectedFinishTime" width="180">
- <template #default="scope">
- <span v-if="scope.row.expectedFinishTime">{{ proxy.parseTime(scope.row.expectedFinishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="实际完成时间" align="center" prop="finishTime" width="180">
- <template #default="scope">
- <span v-if="scope.row.finishTime">{{ proxy.parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="处理时长" align="center" prop="totalDuration" width="100">
- <template #default="scope">
- <span v-if="scope.row.totalDuration">{{ formatDuration(scope.row.totalDuration) }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="评价得分" align="center" prop="evaluationScore" width="200">
- <template #default="scope">
- <el-rate
- v-if="scope.row.evaluationScore"
- :model-value="scope.row.evaluationScore"
- disabled
- show-score
- text-color="#ff9900"
- score-template="{value}分"
- />
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="附件" align="center" prop="attachmentCount" width="80">
- <template #default="scope">
- <el-link v-if="scope.row.attachmentCount > 0" @click="handleDetail(scope.row)" type="primary" :underline="false">
- <el-icon><Paperclip /></el-icon>{{ scope.row.attachmentCount }}
- </el-link>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="创建时间" align="center" prop="createTime" width="180">
- <template #default="scope">
- <span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="280" fixed="right" class-name="small-padding fixed-width">
- <template #default="scope">
- <!-- 待分派状态 (1) - 可以编辑、分派、删除 -->
- <template v-if="scope.row.orderStatus === '1' || scope.row.orderStatus === 1">
- <el-tooltip content="编辑" placement="top">
- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['pm:repairOrder:edit']"></el-button>
- </el-tooltip>
- <el-tooltip content="分派" placement="top" v-if="canAssign">
- <el-button
- link
- type="success"
- icon="UserFilled"
- @click="handleAssign(scope.row)"
- v-hasPermi="['pm:repairOrder:assign']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="查看详情" placement="top">
- <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
- </el-tooltip>
- <el-tooltip content="删除" placement="top">
- <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['pm:repairOrder:remove']"></el-button>
- </el-tooltip>
- </template>
- <!-- 已分派状态 (2) - 可以查看详情、查看进度 -->
- <template v-else-if="scope.row.orderStatus === '2' || scope.row.orderStatus === 2">
- <el-tooltip content="查看详情" placement="top">
- <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
- </el-tooltip>
- <el-tooltip content="查看进度" placement="top">
- <el-button
- link
- type="primary"
- icon="Histogram"
- @click="handleProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:progress']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
- <el-button
- link
- type="warning"
- icon="collection"
- @click="handleUpdateProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:updateProgress']"
- ></el-button>
- </el-tooltip>
- </template>
- <!-- 进行中状态 (3) - 可以查看详情、查看进度、完成工单 -->
- <template v-else-if="scope.row.orderStatus === '3' || scope.row.orderStatus === 3">
- <el-tooltip content="查看详情" placement="top">
- <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
- </el-tooltip>
- <el-tooltip content="查看进度" placement="top">
- <el-button
- link
- type="primary"
- icon="Histogram"
- @click="handleProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:progress']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
- <el-button
- link
- type="warning"
- icon="collection"
- @click="handleUpdateProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:updateProgress']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="完成工单" placement="top" v-if="canComplete(scope.row)">
- <el-button
- link
- type="success"
- icon="CircleCheck"
- @click="handleComplete(scope.row)"
- v-hasPermi="['pm:repairOrder:complete']"
- ></el-button>
- </el-tooltip>
- </template>
- <!-- 已完成状态 (4) - 可以查看详情、查看进度、服务评价 -->
- <template v-else-if="scope.row.orderStatus === '4' || scope.row.orderStatus === 4">
- <el-tooltip content="查看详情" placement="top">
- <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
- </el-tooltip>
- <el-tooltip content="查看进度" placement="top">
- <el-button
- link
- type="primary"
- icon="Histogram"
- @click="handleProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:progress']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="服务评价" placement="top" v-if="canEvaluate(scope.row)">
- <el-button link type="warning" icon="Star" @click="handleEvaluate(scope.row)" v-hasPermi="['pm:repairOrder:evaluate']"></el-button>
- </el-tooltip>
- </template>
- <!-- 其他状态的默认操作 -->
- <template v-else>
- <el-tooltip content="查看详情" placement="top">
- <el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
- </el-tooltip>
- <el-tooltip content="查看进度" placement="top">
- <el-button
- link
- type="primary"
- icon="Histogram"
- @click="handleProgress(scope.row)"
- v-hasPermi="['pm:repairOrder:progress']"
- ></el-button>
- </el-tooltip>
- </template>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 分页组件 -->
- <div class="pagination-container">
- <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
- </div>
- </el-card>
- <!-- 添加或修改报修工单对话框 -->
- <el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
- <el-form ref="repairOrderFormRef" :model="form" :rules="rules" label-width="120px">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="报修标题" prop="title">
- <el-input v-model="form.title" placeholder="请输入报修标题" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="维修类型" prop="categoryId">
- <el-tree-select
- v-model="form.categoryId"
- :data="categoryTreeOptions"
- :props="{ value: 'id', label: 'categoryName', children: 'children' }"
- value-key="id"
- placeholder="请选择维修类型"
- check-strictly
- :render-after-expand="false"
- clearable
- filterable
- style="width: 100%"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="报修地点" prop="repairLocation">
- <el-input v-model="form.repairLocation" placeholder="请输入报修地点" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="紧急程度" prop="priorityLevel">
- <el-select v-model="form.priorityLevel" placeholder="请选择紧急程度" style="width: 100%">
- <el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="报修人姓名" prop="reporterName">
- <el-input
- v-model="form.reporterName"
- placeholder="自动填充当前用户姓名"
- readonly
- disabled
- style="background-color: #f5f7fa; color: #909399"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="报修人电话" prop="reporterPhone">
- <el-input v-model="form.reporterPhone" placeholder="请输入报修人电话" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="报修人部门" prop="reporterDept">
- <el-input
- v-model="form.reporterDept"
- placeholder="自动填充当前用户部门"
- readonly
- disabled
- style="background-color: #f5f7fa; color: #909399"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="期望完成时间" prop="expectedFinishTime">
- <el-date-picker
- v-model="form.expectedFinishTime"
- type="datetime"
- value-format="YYYY-MM-DD HH:mm:ss"
- placeholder="请选择期望完成时间"
- style="width: 100%"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="问题描述" prop="description">
- <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请详细描述遇到的问题..." />
- </el-form-item>
- <el-form-item label="备注信息" prop="remark">
- <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息..." />
- </el-form-item>
- <!-- 附件上传 -->
- <el-form-item label="上传附件">
- <div class="attachment-upload-container">
- <el-upload
- v-model:file-list="attachmentList"
- class="upload-demo"
- drag
- list-type="picture-card"
- :on-preview="handleFilePreview"
- :action="uploadAction"
- :headers="uploadHeaders"
- :data="uploadData"
- :auto-upload="true"
- :before-upload="handleBeforeUpload"
- :on-success="handleUploadSuccess"
- :on-error="handleUploadError"
- :on-remove="handleRemoveAttachment"
- :on-change="handleFileChange"
- multiple
- :limit="15"
- :show-file-list="true"
- accept="image/*,video/*,.pdf,.doc,.docx,.txt"
- >
- <!-- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
- <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> -->
- <template #tip>
- <div class="el-upload__tip">
- 支持图片、视频、PDF、Word文档等格式,单个文件大小不超过10MB,最多上传15个文件。
- <span class="el-upload__tip" v-if="!form.id" style="color: #ff0000">(新增时文件会立即上传,保存工单后自动关联)</span>
- <span class="el-upload__tip" v-else style="color: #ff0000">(编辑时文件会立即上传并关联到当前工单)</span>
- </div>
- </template>
- <!-- 调整拖拽上传图标与文字在同一行、居中对齐 -->
- <div class="el-upload-dragger1">
- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
- <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
- </div>
- <!-- 自定义文件卡片缩略图(含文件名) -->
- <template #file="{ file }">
- <div class="upload-file-card">
- <!-- 缩略图区域 -->
- <div class="upload-file-thumb">
- <el-image
- v-if="isImageFile(file) && file.url"
- :src="file.url"
- fit="cover"
- :preview-src-list="[file.url]"
- class="upload-thumb-image"
- />
- <div v-else class="upload-file-placeholder">
- <el-icon><Document /></el-icon>
- <span class="file-ext">{{ getFileExt(file.name) }}</span>
- </div>
- </div>
- <!-- 文件名称 -->
- <div class="upload-file-name" :title="file.name">
- {{ file.name }}
- </div>
- <!-- 操作按钮区域 (与预览界面样式一致) -->
- <div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
- <el-button-group size="small">
- <el-button type="success" @click.stop="handleDownloadAttachment(file)" title="下载" :disabled="file.status !== 'success'">
- <el-icon><Download /></el-icon>
- </el-button>
- <el-button type="danger" @click.stop="handleRemoveAttachmentConfirm(file)" title="删除">
- <el-icon><Delete /></el-icon>
- </el-button>
- </el-button-group>
- </div>
- </div>
- </template>
- </el-upload>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="cancel">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 详情对话框 -->
- <el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="900px" append-to-body>
- <div v-loading="detailLoading" class="repair-order-detail">
- <template v-if="orderDetail">
- <!-- 基本信息 -->
- <el-card class="detail-card" shadow="never">
- <template #header>
- <div class="card-header">
- <span class="card-title"
- ><el-icon><Document /></el-icon> 基本信息</span
- >
- </div>
- </template>
- <div class="detail-grid">
- <div class="detail-item">
- <label>工单编号:</label>
- <span class="detail-value">{{ orderDetail.orderNo }}</span>
- </div>
- <div class="detail-item">
- <label>工单状态:</label>
- <el-tag :type="getStatusTagType(orderDetail.orderStatus) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
- {{ getStatusText(orderDetail.orderStatus) }}
- </el-tag>
- </div>
- <div class="detail-item">
- <label>紧急程度:</label>
- <el-tag :type="getPriorityTagType(orderDetail.priorityLevel) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
- {{ getPriorityText(orderDetail.priorityLevel) }}
- </el-tag>
- </div>
- <div class="detail-item">
- <label>创建时间:</label>
- <span class="detail-value">{{ orderDetail.createTime }}</span>
- </div>
- <div class="detail-item">
- <label>维修类型:</label>
- <span class="detail-value">{{ getCategoryNameById(orderDetail.categoryId) }}</span>
- </div>
- <div class="detail-item full-width">
- <label>报修标题:</label>
- <span class="detail-value">{{ orderDetail.title }}</span>
- </div>
- <div class="detail-item full-width">
- <label>问题描述:</label>
- <span class="detail-value description">{{ orderDetail.description }}</span>
- </div>
- <div class="detail-item full-width" v-if="orderDetail.cancelReason">
- <label>取消原因:</label>
- <span class="detail-value description">{{ orderDetail.cancelReason }}</span>
- </div>
- <div class="detail-item full-width" v-if="orderDetail.remark">
- <label>备注信息:</label>
- <span class="detail-value description">{{ orderDetail.remark }}</span>
- </div>
- </div>
- </el-card>
- <!-- 报修信息 -->
- <el-card class="detail-card" shadow="never">
- <template #header>
- <div class="card-header">
- <span class="card-title"
- ><el-icon><User /></el-icon> 报修信息</span
- >
- </div>
- </template>
- <div class="detail-grid">
- <div class="detail-item">
- <label>报修人:</label>
- <span class="detail-value">{{ orderDetail.reporterName }}</span>
- </div>
- <div class="detail-item">
- <label>联系电话:</label>
- <span class="detail-value">{{ orderDetail.reporterPhone }}</span>
- </div>
- <div class="detail-item">
- <label>所属部门:</label>
- <span class="detail-value">{{ orderDetail.reporterDept || '暂无' }}</span>
- </div>
- <div class="detail-item">
- <label>期望完成时间:</label>
- <span class="detail-value">{{ orderDetail.expectedFinishTime || '无要求' }}</span>
- </div>
- <div class="detail-item full-width">
- <label>报修地点:</label>
- <span class="detail-value">{{ orderDetail.repairLocation }}</span>
- </div>
- </div>
- </el-card>
- <!-- 维修信息 -->
- <el-card class="detail-card" shadow="never" v-if="orderDetail.assigneeId">
- <template #header>
- <div class="card-header">
- <span class="card-title"
- ><el-icon><Tools /></el-icon> 维修信息</span
- >
- </div>
- </template>
- <div class="detail-grid">
- <div class="detail-item">
- <label>维修员:</label>
- <span class="detail-value">{{ orderDetail.assigneeName || '暂未分派' }}</span>
- </div>
- <div class="detail-item">
- <label>分派时间:</label>
- <span class="detail-value">{{ orderDetail.assignTime || '暂未分派' }}</span>
- </div>
- <div class="detail-item">
- <label>开始时间:</label>
- <span class="detail-value">{{ orderDetail.startTime || '暂未开始' }}</span>
- </div>
- <div class="detail-item">
- <label>完成时间:</label>
- <span class="detail-value">{{ orderDetail.finishTime || '暂未完成' }}</span>
- </div>
- <div class="detail-item">
- <label>处理时长:</label>
- <span class="detail-value">
- {{ orderDetail.totalDuration ? formatDuration(orderDetail.totalDuration) : '暂无' }}
- </span>
- </div>
- <div class="detail-item">
- <label>进度记录:</label>
- <span class="detail-value">{{ orderDetail.progressCount }}条</span>
- </div>
- <div class="detail-item full-width" v-if="orderDetail.assignRemark">
- <label>分派备注:</label>
- <span class="detail-value">{{ orderDetail.assignRemark }}</span>
- </div>
- </div>
- </el-card>
- <!-- 维修结果 -->
- <el-card class="detail-card" shadow="never" v-if="orderDetail.repairResult || orderDetail.usedMaterials || orderDetail.followSuggestions">
- <template #header>
- <div class="card-header">
- <span class="card-title"
- ><el-icon><Check /></el-icon> 维修结果</span
- >
- </div>
- </template>
- <div class="detail-grid">
- <div class="detail-item full-width" v-if="orderDetail.repairResult">
- <label>维修结果:</label>
- <span class="detail-value description">{{ orderDetail.repairResult }}</span>
- </div>
- <div class="detail-item full-width" v-if="orderDetail.usedMaterials">
- <label>使用材料:</label>
- <span class="detail-value">{{ orderDetail.usedMaterials }}</span>
- </div>
- <div class="detail-item full-width" v-if="orderDetail.followSuggestions">
- <label>后续建议:</label>
- <span class="detail-value description">{{ orderDetail.followSuggestions }}</span>
- </div>
- </div>
- </el-card>
- <!-- 评价信息 -->
- <el-card class="detail-card" shadow="never" v-if="orderDetail.evaluationScore">
- <template #header>
- <div class="card-header">
- <span class="card-title"
- ><el-icon><Star /></el-icon> 服务评价</span
- >
- </div>
- </template>
- <div class="detail-grid">
- <div class="detail-item">
- <label>综合评分:</label>
- <span class="detail-value">
- <el-rate v-model="orderDetail.evaluationScore" disabled show-score text-color="#ff9900" />
- </span>
- </div>
- <div class="detail-item">
- <label>评价时间:</label>
- <span class="detail-value">{{ orderDetail.evaluationTime || '暂未评价' }}</span>
- </div>
- </div>
- </el-card>
- <!-- 附件信息 -->
- <el-card class="detail-card" shadow="never" v-if="orderDetail.attachmentCount && orderDetail.attachmentCount > 0">
- <template #header>
- <div class="card-header">
- <span class="card-title">
- <el-icon><Paperclip /></el-icon> 附件信息 ({{ orderDetail.attachmentCount }}个文件)
- </span>
- <el-button
- type="primary"
- size="small"
- @click="refreshAttachments(orderDetail.id)"
- :loading="attachmentLoading"
- style="margin-left: auto"
- >
- <el-icon><Refresh /></el-icon>
- 刷新
- </el-button>
- </div>
- </template>
- <div v-loading="attachmentLoading" class="attachment-detail-container">
- <div v-if="currentAttachments.length === 0" class="no-attachment-detail">
- <el-empty description="暂无附件" :image-size="60" />
- </div>
- <div v-else class="attachment-grid-container">
- <div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-card">
- <!-- 附件缩略图 -->
- <div class="attachment-thumbnail-wrapper">
- <el-image
- v-if="attachment.fileType === 'image'"
- :src="attachment.filePath"
- :preview-src-list="getImagePreviewList()"
- fit="cover"
- class="attachment-image"
- lazy
- />
- <div v-else class="attachment-file-icon">
- <el-icon :size="28">
- <Document v-if="attachment.fileType === 'document'" />
- <VideoPlay v-else-if="attachment.fileType === 'video'" />
- <Files v-else />
- </el-icon>
- <span class="file-type-text">{{ getFileTypeLabel(attachment.fileType) }}</span>
- </div>
- </div>
- <!-- 附件信息 -->
- <div class="attachment-card-body">
- <div class="attachment-title" :title="attachment.fileName">
- {{ attachment.fileName }}
- </div>
- <div class="attachment-meta">
- <span class="file-size">{{ formatFileSize(attachment.fileSize) }}</span>
- <span class="upload-time">{{ formatUploadTime(attachment.createTime) }}</span>
- </div>
- <div v-if="attachment.description" class="attachment-desc" :title="attachment.description">
- {{ attachment.description }}
- </div>
- </div>
- <!-- 附件操作 -->
- <div class="attachment-card-actions">
- <el-button-group size="small">
- <el-button
- type="primary"
- @click="previewAttachment(attachment)"
- :disabled="!canPreview(attachment)"
- :title="canPreview(attachment) ? '预览' : '不支持预览'"
- >
- <el-icon><View /></el-icon>
- </el-button>
- <el-button type="success" @click="downloadAttachment(attachment)" title="下载">
- <el-icon><Download /></el-icon>
- </el-button>
- <el-button type="info" @click="showAttachmentDetail(attachment)" title="详情">
- <el-icon><InfoFilled /></el-icon>
- </el-button>
- <!-- 在详情查看模式下不显示删除按钮 -->
- <el-button
- v-if="canDeleteAttachment(attachment) && !detailDialog.visible"
- type="danger"
- @click="confirmDeleteAttachment(attachment)"
- title="删除"
- >
- <el-icon><Delete /></el-icon>
- </el-button>
- </el-button-group>
- </div>
- </div>
- </div>
- </div>
- </el-card>
- </template>
- </div>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button @click="detailDialog.visible = false">关 闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 工单分派对话框 -->
- <el-dialog title="工单分派" v-model="assignDialog.visible" width="600px" append-to-body>
- <el-form ref="assignFormRef" :model="assignForm" :rules="assignRules" label-width="120px">
- <el-form-item label="工单信息">
- <div class="order-info">
- <p><strong>工单编号:</strong>{{ currentOrder?.orderNo }}</p>
- <p><strong>报修标题:</strong>{{ currentOrder?.title }}</p>
- <p><strong>报修地点:</strong>{{ currentOrder?.repairLocation }}</p>
- <p><strong>维修类型:</strong>{{ getCategoryNameById(currentOrder?.categoryId) }}</p>
- <p><strong>问题描述:</strong>{{ currentOrder?.description }}</p>
- <p><strong>备注信息:</strong>{{ currentOrder?.remark }}</p>
- </div>
- </el-form-item>
- <el-form-item label="分派维修员" prop="assigneeId">
- <el-select v-model="assignForm.assigneeId" placeholder="请选择维修员" style="width: 100%" @change="handleWorkerChange">
- <el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="分派说明" prop="assignRemark">
- <el-input v-model="assignForm.assignRemark" type="textarea" :rows="4" placeholder="请输入分派说明,如特殊要求、注意事项等..." />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button :loading="buttonLoading" type="primary" @click="submitAssign">确认分派</el-button>
- <el-button @click="cancelAssign">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 进度查看对话框 -->
- <el-dialog title="进度查看" v-model="progressDialog.visible" width="700px" append-to-body>
- <div v-loading="progressLoading" class="progress-container">
- <!-- 空状态 -->
- <div v-if="progressList.length === 0 && !progressLoading" class="no-progress">
- <el-empty description="暂无进度记录" :image-size="80" />
- </div>
- <!-- 进度时间线 -->
- <div v-else class="progress-timeline">
- <div v-for="(progress, index) in progressList" :key="index" class="progress-item">
- <!-- 时间线图标 -->
- <div class="timeline-icon">
- <el-icon :size="16" :color="getProgressIconColor(progress.progressType)">
- <Clock v-if="progress.progressType === '1'" />
- <UserFilled v-else-if="progress.progressType === '2'" />
- <Location v-else-if="progress.progressType === '3'" />
- <Tools v-else-if="progress.progressType === '4'" />
- <Edit v-else-if="progress.progressType === '5'" />
- <CircleCheck v-else-if="progress.progressType === '6'" />
- <Star v-else-if="progress.progressType === '7'" />
- <Files v-else-if="progress.progressType === '8'" />
- <More v-else />
- </el-icon>
- </div>
- <!-- 时间线连接线 -->
- <div v-if="index < progressList.length - 1" class="timeline-line"></div>
- <!-- 进度卡片 -->
- <div class="progress-card">
- <div class="progress-header">
- <div class="progress-type-status">
- <el-tag :type="getProgressTypeTag(progress.progressType)" size="small">
- {{ getProgressTypeText(progress.progressType) }}
- </el-tag>
- <span class="progress-status">{{ progress.progressStatus }}</span>
- </div>
- <div class="progress-time">
- <el-icon><Clock /></el-icon>
- {{ progress.createTime }}
- </div>
- </div>
- <div class="progress-content">
- <div class="progress-detail">
- <p class="detail-text">{{ progress.progressDetail }}</p>
- </div>
- <!-- <div class="progress-info-grid" v-if="hasAdditionalInfo(progress)">
- <div class="info-item" v-if="progress.locationInfo">
- <label
- ><el-icon><Location /></el-icon> 位置信息:</label
- >
- <span>{{ progress.locationInfo }}</span>
- </div>
- <div class="info-item" v-if="progress.estimatedDuration">
- <label
- ><el-icon><Timer /></el-icon> 预计时长:</label
- >
- <span>{{ progress.estimatedDuration }}分钟</span>
- </div>
- <div class="info-item" v-if="progress.nextStep">
- <label
- ><el-icon><Right /></el-icon> 下一步:</label
- >
- <span>{{ progress.nextStep }}</span>
- </div>
- <div class="info-item" v-if="progress.issuesEncountered">
- <label
- ><el-icon><Warning /></el-icon> 遇到问题:</label
- >
- <span>{{ progress.issuesEncountered }}</span>
- </div>
- <div class="info-item" v-if="progress.materialsNeeded">
- <label
- ><el-icon><ShoppingBag /></el-icon> 需要材料:</label
- >
- <span>{{ progress.materialsNeeded }}</span>
- </div>
- <div class="info-item" v-if="progress.remark">
- <label
- ><el-icon><EditPen /></el-icon> 备注:</label
- >
- <span>{{ progress.remark }}</span>
- </div>
- </div> -->
- </div>
- </div>
- </div>
- </div>
- </div>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button @click="progressDialog.visible = false">关 闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 进度更新对话框 -->
- <el-dialog title="进度更新" v-model="updateProgressDialog.visible" width="600px" append-to-body>
- <el-form ref="progressFormRef" :model="progressForm" :rules="progressRules" label-width="120px">
- <el-form-item label="工单编号" prop="orderNo">
- <el-input v-model="progressForm.orderNo" placeholder="请输入工单编号" />
- </el-form-item>
- <!-- <el-form-item label="进度类型" prop="progressType">
- <el-select v-model="progressForm.progressType" placeholder="请选择进度类型" style="width: 100%">
- <el-option v-for="type in progressTypeOptions" :key="type.value" :label="type.label" :value="type.value" />
- </el-select>
- </el-form-item> -->
- <el-form-item label="进度状态" prop="progressStatus">
- <el-input v-model="progressForm.progressStatus" placeholder="请输入进度状态" />
- </el-form-item>
- <el-form-item label="进度详细描述" prop="progressDetail">
- <el-input v-model="progressForm.progressDetail" type="textarea" placeholder="请输入进度详细描述" />
- </el-form-item>
- <!-- <el-form-item label="进度位置信息" prop="locationInfo">
- <el-input v-model="progressForm.locationInfo" placeholder="请输入进度位置信息" />
- </el-form-item>
- <el-form-item label="进度估计时长" prop="estimatedDuration">
- <el-input v-model="progressForm.estimatedDuration" placeholder="请输入进度估计时长" />
- </el-form-item>
- <el-form-item label="进度下一步" prop="nextStep">
- <el-input v-model="progressForm.nextStep" placeholder="请输入进度下一步" />
- </el-form-item>
- <el-form-item label="遇到的问题" prop="issuesEncountered">
- <el-input v-model="progressForm.issuesEncountered" placeholder="请输入遇到的问题" />
- </el-form-item>
- <el-form-item label="需要的材料" prop="materialsNeeded">
- <el-input v-model="progressForm.materialsNeeded" placeholder="请输入需要的材料" />
- </el-form-item>
- <el-form-item label="进度备注" prop="remark">
- <el-input v-model="progressForm.remark" placeholder="请输入进度备注" />
- </el-form-item> -->
- </el-form>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button :loading="buttonLoading" type="primary" @click="submitProgress">确认更新</el-button>
- <el-button @click="cancelProgress">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 完成工单对话框 -->
- <el-dialog title="完成工单" v-model="completeDialog.visible" width="600px" append-to-body>
- <el-form ref="completeFormRef" :model="completeForm" :rules="completeRules" label-width="120px">
- <el-form-item label="工单编号" prop="orderNo">
- <el-input v-model="completeForm.orderNo" placeholder="请输入工单编号" readonly disabled />
- </el-form-item>
- <el-form-item label="维修结果" prop="repairResult">
- <el-input v-model="completeForm.repairResult" placeholder="请输入维修结果" />
- </el-form-item>
- <el-form-item label="使用材料" prop="usedMaterials">
- <el-input v-model="completeForm.usedMaterials" type="textarea" placeholder="请输入使用材料" />
- </el-form-item>
- <el-form-item label="后续建议" prop="followSuggestions">
- <el-input v-model="completeForm.followSuggestions" type="textarea" placeholder="请输入后续建议" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button :loading="buttonLoading" type="primary" @click="submitComplete">确认完成</el-button>
- <el-button @click="cancelComplete">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 服务评价对话框 -->
- <el-dialog title="服务评价" v-model="evaluationDialog.visible" width="600px" append-to-body>
- <el-form ref="evaluationFormRef" :model="evaluationForm" :rules="evaluationRules" label-width="120px">
- <el-form-item label="工单编号" prop="orderId">
- <el-input
- v-model="evaluationForm.orderId"
- placeholder="自动填充工单编号"
- readonly
- disabled
- style="background-color: #f5f7fa; color: #909399"
- />
- </el-form-item>
- <el-form-item label="评价员工" prop="evaluatedWorkerId">
- <el-select v-model="evaluationForm.evaluatedWorkerId" placeholder="请选择评价员工" style="width: 100%">
- <el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="综合评分" prop="overallScore">
- <el-rate v-model="evaluationForm.overallScore" :max="5" show-score />
- </el-form-item>
- <el-form-item label="质量评分" prop="qualityScore">
- <el-rate v-model="evaluationForm.qualityScore" :max="5" show-score />
- </el-form-item>
- <el-form-item label="态度评分" prop="attitudeScore">
- <el-rate v-model="evaluationForm.attitudeScore" :max="5" show-score />
- </el-form-item>
- <el-form-item label="响应速度评分" prop="responseSpeedScore">
- <el-rate v-model="evaluationForm.responseSpeedScore" :max="5" show-score />
- </el-form-item>
- <el-form-item label="沟通能力评分" prop="communicationScore">
- <el-rate v-model="evaluationForm.communicationScore" :max="5" show-score />
- </el-form-item>
- <el-form-item label="评价内容" prop="evaluationContent">
- <el-input v-model="evaluationForm.evaluationContent" type="textarea" placeholder="请输入评价内容" />
- </el-form-item>
- <el-form-item label="满意度等级" prop="satisfactionLevel">
- <el-select v-model="evaluationForm.satisfactionLevel" placeholder="请选择满意度等级" style="width: 100%">
- <el-option v-for="level in satisfactionLevelOptions" :key="level.value" :label="level.label" :value="level.value" />
- </el-select>
- </el-form-item>
- <!-- <el-form-item label="是否匿名" prop="isAnonymous">
- <el-switch v-model="evaluationForm.isAnonymous" />
- </el-form-item>
- <el-form-item label="是否公开" prop="isPublic">
- <el-switch v-model="evaluationForm.isPublic" />
- </el-form-item>
- <el-form-item label="正面标签" prop="positiveTags">
- <el-input v-model="evaluationForm.positiveTags" placeholder="请输入正面标签" />
- </el-form-item>
- <el-form-item label="负面标签" prop="negativeTags">
- <el-input v-model="evaluationForm.negativeTags" placeholder="请输入负面标签" />
- </el-form-item>
- <el-form-item label="改进建议" prop="improvementSuggestions">
- <el-input v-model="evaluationForm.improvementSuggestions" placeholder="请输入改进建议" />
- </el-form-item>
- <el-form-item label="备注信息" prop="remark">
- <el-input v-model="evaluationForm.remark" placeholder="请输入备注信息" />
- </el-form-item> -->
- </el-form>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button :loading="buttonLoading" type="primary" @click="submitEvaluation">确认评价</el-button>
- <el-button @click="cancelEvaluation">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 附件查看对话框 -->
- <el-dialog title="附件查看" v-model="attachmentDialog.visible" width="800px" append-to-body>
- <div v-loading="attachmentLoading" class="attachment-container">
- <div v-if="currentAttachments.length === 0" class="no-attachment">
- <el-empty description="暂无附件" />
- </div>
- <div v-else class="attachment-list">
- <div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-item">
- <div class="attachment-info">
- <div class="attachment-preview">
- <el-image
- v-if="attachment.fileType === 'image'"
- :src="attachment.filePath"
- :preview-src-list="[attachment.filePath]"
- fit="cover"
- style="width: 80px; height: 80px"
- lazy
- />
- <el-icon v-else class="file-icon" :size="48">
- <Document v-if="attachment.fileType === 'document'" />
- <VideoPlay v-else-if="attachment.fileType === 'video'" />
- <Folder v-else />
- </el-icon>
- </div>
- <div class="attachment-details">
- <div class="file-name">{{ attachment.fileName }}</div>
- <div class="file-meta">
- <span>{{ formatFileSize(attachment.fileSize) }}</span>
- <span>{{ attachment.uploaderName }}</span>
- <span>{{ attachment.createTime }}</span>
- </div>
- <div v-if="attachment.description" class="file-description">
- {{ attachment.description }}
- </div>
- </div>
- </div>
- <div class="attachment-actions">
- <el-button type="primary" size="small" @click="downloadAttachment(attachment)">
- <el-icon><Download /></el-icon>
- 下载
- </el-button>
- <el-button type="success" size="small" @click="previewAttachment(attachment)">
- <el-icon><View /></el-icon>
- 预览
- </el-button>
- </div>
- </div>
- </div>
- </div>
- <template #footer>
- <div class="dialog-footer-right">
- <el-button @click="attachmentDialog.visible = false">关 闭</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts" name="RepairOrder">
- import {
- listRepairOrder,
- getRepairOrder,
- delRepairOrder,
- addRepairOrder,
- updateRepairOrder,
- exportRepairOrders,
- assignRepairOrder,
- getRepairStatistics,
- getMaintenanceWorkers,
- getRepairProgress,
- updateRepairProgress,
- finishRepairOrder,
- createServiceEvaluation,
- getServiceEvaluation,
- getRepairAttachments,
- uploadAttachment,
- uploadAttachments,
- deleteAttachment,
- batchUpdateAttachmentOrderId
- } from '@/api/hq/pm/repairOrder';
- import { getRepairCategoryOptions, getRepairCategoryTree } from '@/api/hq/pm/repairCategory';
- import {
- RepairOrderVO,
- RepairOrderForm,
- RepairOrderQuery,
- AssignOrderForm,
- RepairStatisticsVO,
- RepairProgressVO,
- ProgressUpdateForm,
- ServiceEvaluationForm,
- ServiceEvaluationVO,
- RepairCategoryVO
- } from '@/api/hq/pm/types';
- import { useUserStore } from '@/store/modules/user';
- import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
- import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
- import { vLoading, ElLoading } from 'element-plus';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- // 用户store
- const userStore = useUserStore();
- const repairOrderList = ref<RepairOrderVO[]>([]);
- const loading = ref(true);
- const showSearch = ref(true);
- const ids = ref<Array<string | number>>([]);
- const single = ref(true);
- const multiple = ref(true);
- const total = ref(0);
- const daterangeCreateTime = ref<[string, string]>(['', '']);
- // 当前用户信息
- const currentUser = ref<any>(null);
- // 统计数据
- const statistics = ref<RepairStatisticsVO>({
- totalOrders: 0,
- pendingAssign: 0,
- inProgress: 0,
- completed: 0,
- cancelled: 0,
- avgProcessTime: 0,
- avgEvaluationScore: 0,
- monthlyNewOrders: 0,
- monthlyCompletedOrders: 0,
- statusDistribution: [],
- categoryDistribution: [],
- priorityDistribution: []
- });
- // 下拉选项数据
- const categoryOptions = ref<Array<{ value: string | number; label: string }>>([]);
- const categoryTreeOptions = ref<RepairCategoryVO[]>([]);
- const workerOptions = ref<Array<{ value: string | number; label: string; userName?: string; deptName?: string }>>([]);
- // 附件相关数据
- const attachmentList = ref<any[]>([]);
- const uploadAction = ref('/dev-api/hq/pm/attachment/upload'); // 修复:使用正确的API路径
- const uploadHeaders = ref({
- 'Authorization': `Bearer ${getToken() || ''}`,
- 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
- }); // 修复:添加认证头
- const uploadData = ref<any>({});
- // 新增:临时文件存储,用于新增时收集文件
- const tempFileList = ref<File[]>([]);
- // 新增:临时附件ID列表,用于新增时收集已上传附件的ID
- const tempAttachmentIds = ref<string[]>([]);
- // 待删除的附件ID列表 (保存时统一删除)
- const pendingDeleteAttachmentIds = ref<string[]>([]);
- // 字典数据
- const pm_priority_level = ref([
- { value: '1', label: '非常紧急' },
- { value: '2', label: '紧急' },
- { value: '3', label: '一般' },
- { value: '4', label: '不紧急' },
- { value: '5', label: '可延后' }
- ]);
- const pm_order_status = ref([
- { value: '1', label: '待分派' },
- { value: '2', label: '已分派' },
- { value: '3', label: '进行中' },
- { value: '4', label: '已完成' },
- { value: '5', label: '已评价' },
- // { value: '6', label: '已归档' },
- { value: '9', label: '已取消' }
- ]);
- const queryParams = ref<RepairOrderQuery>({
- pageNum: 1,
- pageSize: 10,
- orderNo: '',
- title: '',
- repairLocation: '',
- categoryId: '',
- priorityLevel: undefined,
- reporterName: '',
- reporterPhone: '',
- orderStatus: '',
- assigneeId: '',
- assigneeName: '',
- createTimeRange: [],
- finishTimeRange: [],
- params: {}
- });
- const form = ref<RepairOrderForm>({});
- const queryFormRef = ref<ElFormInstance>();
- const repairOrderFormRef = ref<ElFormInstance>();
- const rules = reactive<ElFormRules>({
- title: [{ required: true, message: '报修标题不能为空', trigger: 'blur' }],
- description: [{ required: true, message: '问题描述不能为空', trigger: 'blur' }],
- repairLocation: [{ required: true, message: '报修地点不能为空', trigger: 'blur' }],
- categoryId: [{ required: true, message: '维修类型不能为空', trigger: 'change' }],
- priorityLevel: [{ required: true, message: '紧急程度不能为空', trigger: 'change' }],
- reporterName: [{ required: true, message: '报修人姓名不能为空', trigger: 'blur' }],
- reporterPhone: [
- { required: true, message: '报修人电话不能为空', trigger: 'blur' },
- { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
- ]
- });
- const dialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const buttonLoading = ref(false);
- // 分派相关
- const assignDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const assignForm = ref<AssignOrderForm>({
- orderId: '',
- assigneeId: '',
- assigneeName: '',
- assignRemark: ''
- });
- const assignFormRef = ref<ElFormInstance>();
- const assignRules = reactive<ElFormRules>({
- assigneeId: [{ required: true, message: '请选择维修员', trigger: 'change' }]
- });
- const currentOrder = ref<RepairOrderVO>();
- // 详情对话框
- const detailDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const detailLoading = ref(false);
- const orderDetail = ref<RepairOrderVO>();
- // 进度查看对话框
- const progressDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const progressLoading = ref(false);
- const progressList = ref<RepairProgressVO[]>([]);
- // 进度更新对话框
- const updateProgressDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const progressForm = ref<ProgressUpdateForm>({
- orderId: '',
- progressType: '5',
- progressStatus: '',
- progressDetail: '',
- locationInfo: '',
- estimatedDuration: undefined,
- nextStep: '',
- issuesEncountered: '',
- materialsNeeded: '',
- remark: ''
- });
- const progressFormRef = ref<ElFormInstance>();
- const progressRules = reactive<ElFormRules>({
- progressStatus: [{ required: true, message: '进度状态不能为空', trigger: 'blur' }],
- progressDetail: [{ required: true, message: '进度详细描述不能为空', trigger: 'blur' }]
- });
- // 完成工单对话框
- const completeDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const completeForm = ref({
- orderId: '',
- repairResult: '',
- usedMaterials: '',
- followSuggestions: ''
- });
- const completeFormRef = ref<ElFormInstance>();
- const completeRules = reactive<ElFormRules>({
- repairResult: [{ required: true, message: '维修结果不能为空', trigger: 'blur' }]
- });
- // 服务评价对话框
- const evaluationDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const attachmentDialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- // 附件相关数据
- const attachmentLoading = ref(false);
- const currentAttachments = ref<any[]>([]);
- const evaluationForm = ref<ServiceEvaluationForm>({
- orderId: '',
- evaluatedWorkerId: '',
- overallScore: 5,
- qualityScore: 5,
- attitudeScore: 5,
- responseSpeedScore: 5,
- communicationScore: 5,
- evaluationContent: '',
- satisfactionLevel: '1',
- isAnonymous: 0,
- isPublic: 1,
- positiveTags: '',
- negativeTags: '',
- improvementSuggestions: '',
- remark: ''
- });
- const evaluationFormRef = ref<ElFormInstance>();
- const evaluationRules = reactive<ElFormRules>({
- orderId: [{ required: true, message: '工单ID不能为空', trigger: 'blur' }],
- evaluatedWorkerId: [{ required: true, message: '请选择要评价的维修员', trigger: 'change' }],
- overallScore: [{ required: true, message: '请给出综合评分', trigger: 'change' }],
- evaluationContent: [{ required: true, message: '请填写评价内容', trigger: 'blur' }],
- satisfactionLevel: [{ required: true, message: '请选择满意度等级', trigger: 'change' }]
- });
- // 进度类型字典
- const progressTypeOptions = ref([
- { value: '1', label: '工单分派' },
- { value: '2', label: '接收确认' },
- { value: '3', label: '到达现场' },
- { value: '4', label: '开始维修' },
- { value: '5', label: '进度更新' },
- { value: '6', label: '完成维修' },
- { value: '7', label: '用户评价' },
- { value: '8', label: '工单归档' },
- { value: '9', label: '其他' }
- ]);
- // 满意度等级字典
- const satisfactionLevelOptions = ref([
- { value: '1', label: '非常满意' },
- { value: '2', label: '满意' },
- { value: '3', label: '一般' },
- { value: '4', label: '不满意' },
- { value: '5', label: '非常不满意' }
- ]);
- /** 查询报修工单列表 */
- const getList = async () => {
- loading.value = true;
- queryParams.value.createTimeRange = daterangeCreateTime.value;
- // 设置创建时间范围
- queryParams.value.createTimeRange = daterangeCreateTime.value;
- // 将创建时间范围写入 params,供后台 between 过滤使用
- if (daterangeCreateTime.value && daterangeCreateTime.value.length === 2 && daterangeCreateTime.value[0] && daterangeCreateTime.value[1]) {
- queryParams.value.params = {
- ...queryParams.value.params,
- beginCreateTime: daterangeCreateTime.value[0],
- endCreateTime: daterangeCreateTime.value[1]
- };
- } else if (queryParams.value.params) {
- delete queryParams.value.params.beginCreateTime;
- delete queryParams.value.params.endCreateTime;
- }
- const res = await listRepairOrder(queryParams.value);
- // 适配后端返回格式:支持新格式{total, rows, code, msg}和旧格式{code, msg, data: {total, rows}}
- if ((res as any).rows && (res as any).total !== undefined) {
- // 新格式:直接返回TableDataInfo
- repairOrderList.value = (res as any).rows;
- total.value = (res as any).total;
- } else if ((res as any).data && (res as any).data.rows) {
- // 旧格式:R类型包装的TableDataInfo
- repairOrderList.value = (res as any).data.rows;
- total.value = (res as any).data.total || 0;
- } else {
- // 兜底处理
- repairOrderList.value = [];
- total.value = 0;
- }
- loading.value = false;
- };
- /** 获取统计数据 */
- const getStatistics = async () => {
- try {
- const res = await getRepairStatistics();
- // 适配后端返回格式
- statistics.value = res.data;
- } catch (error) {
- console.error('获取统计数据失败:', error);
- }
- };
- /** 获取维修类型选项 */
- const getCategoryOptions = async () => {
- try {
- const res = await getRepairCategoryOptions();
- categoryOptions.value = res.data;
- } catch (error) {
- console.error('获取维修类型选项失败:', error);
- }
- };
- /** 获取维修类型树形结构 */
- const getCategoryTreeOptions = async () => {
- try {
- const res = await getRepairCategoryTree();
- categoryTreeOptions.value = res.data || [];
- } catch (error) {
- console.error('获取维修类型树形结构失败:', error);
- categoryTreeOptions.value = [];
- }
- };
- /** 根据categoryId获取维修类型名称 */
- const getCategoryNameById = (categoryId: string | number | undefined): string => {
- if (!categoryId) {
- return '未分类';
- }
- // 递归查找树形结构中的分类名称
- const findCategoryName = (categories: RepairCategoryVO[], targetId: string | number): string | null => {
- for (const category of categories) {
- // 数据类型统一转换为字符串进行比较
- if (String(category.id) === String(targetId)) {
- return category.categoryName || '未命名分类';
- }
- // 如果有子分类,递归查找
- if (category.children && category.children.length > 0) {
- const found = findCategoryName(category.children, targetId);
- if (found) {
- return found;
- }
- }
- }
- return null;
- };
- // 先从树形结构中查找
- const foundInTree = findCategoryName(categoryTreeOptions.value, categoryId);
- if (foundInTree) {
- return foundInTree;
- }
- // 如果树形结构中没找到,从普通选项中查找
- const foundInOptions = categoryOptions.value.find((option) => String(option.value) === String(categoryId));
- if (foundInOptions) {
- return foundInOptions.label;
- }
- // 都没找到,返回ID
- return `未知类型(${categoryId})`;
- };
- /** 获取维修人员选项 */
- const getWorkerOptions = async () => {
- try {
- const res = await getMaintenanceWorkers();
- workerOptions.value = res.data;
- } catch (error) {
- console.error('获取维修人员选项失败:', error);
- }
- };
- /** 获取当前用户详细信息 */
- const getCurrentUserDetailInfo = async () => {
- try {
- const res = await getInfo();
- console.log('用户详细信息:', res.data);
- return res.data.user;
- } catch (error) {
- console.error('获取用户详细信息失败:', error);
- return null;
- }
- };
- /** 获取当前用户信息 */
- const getCurrentUserInfo = async () => {
- try {
- // 首先确保用户信息已加载
- if (!userStore.userId) {
- await userStore.getInfo();
- }
- // 构建当前用户信息对象,包含角色信息
- currentUser.value = {
- userId: userStore.userId,
- userName: userStore.nickname, // 使用nickname作为用户名
- nickName: userStore.nickname,
- tenantId: userStore.tenantId,
- avatar: userStore.avatar,
- roles: userStore.roles?.map((roleKey: string) => ({ roleKey })) || [],
- permissions: userStore.permissions || [],
- admin: userStore.roles?.includes('admin') || userStore.roles?.includes('superadmin') || false
- };
- } catch (error) {
- console.error('获取用户信息失败:', error);
- // 设置默认值,避免后续计算属性报错
- currentUser.value = {
- userId: null,
- userName: '',
- nickName: '',
- tenantId: '',
- avatar: '',
- roles: [],
- permissions: [],
- admin: false
- };
- }
- };
- /** 检查是否为维修管理员 */
- const isMaintenanceAdmin = computed(() => {
- if (!currentUser.value) return false;
- const roles = currentUser.value.roles || [];
- return roles.some((role: any) => role.roleKey === 'property_maintenance_admin');
- });
- /** 检查是否为维修人员 */
- const isMaintenanceWorker = computed(() => {
- if (!currentUser.value) return false;
- const roles = currentUser.value.roles || [];
- return roles.some((role: any) => role.roleKey === 'maintenance_worker');
- });
- /** 检查是否为超级管理员 */
- const isSuperAdmin = computed(() => {
- if (!currentUser.value) return false;
- return currentUser.value.admin || false;
- });
- /** 检查是否为报修人 */
- const isReporter = (reporterId: number | string) => {
- if (!currentUser.value) return false;
- return currentUser.value.userId === reporterId;
- };
- /** 检查是否为维修负责人 */
- const isAssignee = (assigneeId: number | string) => {
- if (!currentUser.value) return false;
- return currentUser.value.userId === assigneeId;
- };
- /** 检查分派按钮权限 - 仅维修管理员 */
- const canAssign = computed(() => {
- return isSuperAdmin.value || isMaintenanceAdmin.value;
- });
- /** 检查更新进度按钮权限 - 仅维修人员且为负责人 */
- const canUpdateProgress = (row: RepairOrderVO) => {
- console.log(isSuperAdmin.value, isMaintenanceWorker.value, isAssignee(row.assigneeId));
- if (isSuperAdmin.value) return true;
- return isMaintenanceWorker.value && isAssignee(row.assigneeId);
- };
- /** 检查完成工单按钮权限 - 仅维修人员且为负责人 */
- const canComplete = (row: RepairOrderVO) => {
- if (isSuperAdmin.value) return true;
- return isMaintenanceWorker.value && isAssignee(row.assigneeId);
- };
- /** 检查服务评价按钮权限 - 仅报修人 */
- const canEvaluate = (row: RepairOrderVO) => {
- if (isSuperAdmin.value) return true;
- return isReporter(row.reporterId);
- };
- /** 检查统计卡片查看权限 - 仅维护管理员和超级管理员 */
- const canViewStatistics = computed(() => {
- return isSuperAdmin.value || isMaintenanceAdmin.value;
- });
- /** 搜索按钮操作 */
- const handleQuery = () => {
- queryParams.value.pageNum = 1;
- getList();
- };
- /** 重置按钮操作 */
- const resetQuery = () => {
- daterangeCreateTime.value = ['', ''];
- queryFormRef.value?.resetFields();
- queryParams.value.createTimeRange = [];
- queryParams.value.pageNum = 1;
- getList();
- };
- /** 多选框选中数据 */
- const handleSelectionChange = (selection: RepairOrderVO[]) => {
- ids.value = selection.map((item) => item.id);
- single.value = selection.length !== 1;
- multiple.value = !selection.length;
- };
- /** 新增按钮操作 */
- const handleAdd = async () => {
- await reset(true); // 传入true表示是新增操作,需要自动填充用户信息
- dialog.visible = true;
- dialog.title = '添加报修工单';
- };
- /** 修改按钮操作 */
- const handleUpdate = async (row?: RepairOrderVO) => {
- await reset(); // 修改操作不自动填充用户信息
- const id = row?.id || ids.value[0];
- try {
- // 加载工单基本信息
- const res = await getRepairOrder(id);
- Object.assign(form.value, res.data);
- // 加载已有的附件信息
- if (form.value.id) {
- try {
- const attachmentRes = await getRepairAttachments(form.value.id);
- const existingAttachments = attachmentRes.data || [];
- // 转换已有附件为el-upload组件需要的格式
- attachmentList.value = existingAttachments.map((attachment: any) => ({
- uid: attachment.id,
- name: attachment.fileName,
- status: 'success',
- size: attachment.fileSize,
- url: attachment.filePath,
- response: {
- code: 200,
- data: {
- id: attachment.id,
- fileName: attachment.fileName,
- filePath: attachment.filePath
- }
- },
- // 保存原始附件信息
- attachmentInfo: attachment
- }));
- console.log('已加载附件信息,数量:', attachmentList.value.length);
- } catch (error) {
- console.error('加载附件信息失败:', error);
- attachmentList.value = [];
- }
- }
- dialog.visible = true;
- dialog.title = '修改报修工单';
- } catch (error) {
- console.error('加载工单信息失败:', error);
- proxy?.$modal.msgError('加载工单信息失败');
- }
- };
- /** 提交按钮 */
- const submitForm = () => {
- repairOrderFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- try {
- let orderId: string | number;
- if (form.value.id) {
- await updateRepairOrder(form.value);
- orderId = form.value.id;
- proxy?.$modal.msgSuccess('修改成功');
- } else {
- const res = await addRepairOrder(form.value);
- orderId = res.data?.id || res.data; // 修复:移除res.id,避免类型错误
- console.log('新增工单响应:', res); // 调试信息
- proxy?.$modal.msgSuccess('新增成功');
- }
- // 关闭对话框并刷新列表(确保这两步先执行)
- dialog.visible = false;
- await getList();
- // 处理附件上传(移到最后,避免影响界面关闭)
- if (orderId) {
- try {
- await handlePendingAttachments(orderId);
- } catch (error) {
- console.error('附件上传失败:', error);
- proxy?.$modal.msgWarning('工单保存成功,但附件上传失败,请稍后重试');
- }
- }
- } catch (error) {
- console.error('保存工单失败:', error);
- proxy?.$modal.msgError('保存失败,请重试');
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- /** 处理待关联的附件 */
- const handlePendingAttachments = async (orderId: string | number) => {
- let hasOperations = false;
- let successMessages: string[] = [];
- let errorMessages: string[] = [];
- // 处理待删除的附件(编辑模式)
- if (pendingDeleteAttachmentIds.value.length > 0) {
- try {
- console.log('开始删除标记的附件,数量:', pendingDeleteAttachmentIds.value.length);
- // 批量删除附件
- for (const attachmentId of pendingDeleteAttachmentIds.value) {
- await deleteAttachment(attachmentId);
- }
- console.log('成功删除附件IDs:', pendingDeleteAttachmentIds.value);
- successMessages.push(`成功删除 ${pendingDeleteAttachmentIds.value.length} 个附件`);
- // 清空待删除列表
- pendingDeleteAttachmentIds.value = [];
- hasOperations = true;
- } catch (error) {
- console.error('附件删除失败:', error);
- errorMessages.push('部分附件删除失败');
- }
- }
- // 处理新增模式下已上传的附件(更新order_id)
- if (tempAttachmentIds.value.length > 0) {
- try {
- console.log('开始关联临时附件到工单,附件数量:', tempAttachmentIds.value.length);
- // 调用API批量更新附件的order_id
- await batchUpdateAttachmentOrderId(tempAttachmentIds.value, String(orderId));
- console.log('成功关联附件IDs:', tempAttachmentIds.value, '到工单:', orderId);
- successMessages.push(`成功关联 ${tempAttachmentIds.value.length} 个附件到工单`);
- // 清空临时附件ID列表
- tempAttachmentIds.value = [];
- tempFileList.value = [];
- attachmentList.value = [];
- hasOperations = true;
- } catch (error) {
- console.error('附件关联失败:', error);
- errorMessages.push('部分附件关联失败');
- }
- }
- // 处理编辑模式下新添加的文件(attachmentList中status不为success的文件)
- const newFiles = attachmentList.value.filter((file) => file.status !== 'success' && file.raw && !file.response);
- if (newFiles.length > 0) {
- try {
- console.log('开始上传编辑模式下新添加的文件,文件数量:', newFiles.length);
- for (const fileItem of newFiles) {
- if (fileItem.raw) {
- await uploadAttachment(String(orderId), '1', fileItem.raw, '');
- console.log('成功上传编辑模式新文件:', fileItem.name);
- // 更新文件状态
- fileItem.status = 'success';
- }
- }
- successMessages.push(`成功上传 ${newFiles.length} 个新附件`);
- hasOperations = true;
- } catch (error) {
- console.error('新附件上传失败:', error);
- errorMessages.push('部分新附件上传失败');
- }
- }
- // 显示操作结果消息
- if (hasOperations) {
- if (successMessages.length > 0) {
- proxy?.$modal.msgSuccess(successMessages.join(','));
- }
- if (errorMessages.length > 0) {
- proxy?.$modal.msgWarning(`工单保存成功,但${errorMessages.join(',')}`);
- }
- }
- };
- /** 删除按钮操作 */
- const handleDelete = async (row?: RepairOrderVO) => {
- const orderIds = row?.id || ids.value;
- await proxy?.$modal.confirm('是否确认删除报修工单编号为"' + orderIds + '"的数据项?');
- await delRepairOrder(orderIds);
- proxy?.$modal.msgSuccess('删除成功');
- await getList();
- };
- /** 导出按钮操作 */
- const handleExport = () => {
- proxy?.$download.zip('repair_order_export.zip', `repair_order_${new Date().getTime()}.xlsx`);
- };
- /** 分派按钮操作 */
- const handleAssign = (row: RepairOrderVO) => {
- currentOrder.value = row;
- assignForm.value = {
- orderId: row.id,
- assigneeId: '',
- assigneeName: '',
- assignRemark: ''
- };
- assignDialog.visible = true;
- };
- /** 维修员选择变更 */
- const handleWorkerChange = (value: string | number) => {
- const worker = workerOptions.value.find((w) => w.value === value);
- assignForm.value.assigneeName = worker?.label || '';
- };
- /** 提交分派 */
- const submitAssign = () => {
- assignFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- try {
- await assignRepairOrder(assignForm.value);
- proxy?.$modal.msgSuccess('工单分派成功');
- assignDialog.visible = false;
- await getList();
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- /** 取消分派 */
- const cancelAssign = () => {
- assignDialog.visible = false;
- };
- /** 查看详情 */
- const handleDetail = async (row: RepairOrderVO) => {
- detailLoading.value = true;
- detailDialog.title = `工单详情 - ${row.orderNo}`;
- detailDialog.visible = true;
- try {
- const res = await getRepairOrder(row.id);
- // 适配不同的响应格式
- orderDetail.value = (res as any).data || res;
- // 自动加载附件信息
- if (orderDetail.value.attachmentCount && orderDetail.value.attachmentCount > 0) {
- await loadAttachments(orderDetail.value.id);
- }
- } catch (error) {
- console.error('获取工单详情失败:', error);
- proxy?.$modal.msgError('获取工单详情失败');
- } finally {
- detailLoading.value = false;
- }
- };
- /** 加载附件信息 */
- const loadAttachments = async (orderId: string | number) => {
- try {
- attachmentLoading.value = true;
- const res = await getRepairAttachments(orderId);
- currentAttachments.value = res.data || [];
- } catch (error) {
- console.error('获取附件失败:', error);
- currentAttachments.value = [];
- } finally {
- attachmentLoading.value = false;
- }
- };
- /** 刷新附件列表 */
- const refreshAttachments = async (orderId: string | number) => {
- await loadAttachments(orderId);
- proxy?.$modal.msgSuccess('附件列表已刷新');
- };
- /** 获取图片预览列表 */
- const getImagePreviewList = () => {
- return currentAttachments.value.filter((att) => att.fileType === 'image').map((att) => att.filePath);
- };
- /** 获取文件类型标签 */
- const getFileTypeLabel = (fileType: string) => {
- const typeMap: Record<string, string> = {
- 'image': '图片',
- 'video': '视频',
- 'document': '文档',
- 'audio': '音频',
- 'archive': '压缩包'
- };
- return typeMap[fileType] || '文件';
- };
- /** 格式化上传时间 */
- const formatUploadTime = (timeStr: string) => {
- if (!timeStr) return '未知时间';
- const time = new Date(timeStr);
- const now = new Date();
- const diff = now.getTime() - time.getTime();
- const days = Math.floor(diff / (1000 * 60 * 60 * 24));
- if (days === 0) {
- return '今天上传';
- } else if (days === 1) {
- return '昨天上传';
- } else if (days < 7) {
- return `${days}天前上传`;
- } else {
- return proxy.parseTime(timeStr, '{y}-{m}-{d}');
- }
- };
- /** 获取上传阶段文本 */
- const getUploadStageText = (stage: string | number) => {
- const stageMap: Record<string, string> = {
- '1': '报修申请',
- '2': '维修过程',
- '3': '维修完成',
- '4': '服务评价'
- };
- return stageMap[String(stage)] || '未知阶段';
- };
- /** 获取文件存储信息 */
- const getFileStorageInfo = (filePath: string) => {
- if (!filePath) return '未知路径';
- const parts = filePath.split('/');
- return parts.length > 2 ? `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}` : filePath;
- };
- /** 检查是否可以预览 */
- const canPreview = (attachment: any) => {
- const previewableTypes = ['image', 'document'];
- const previewableExtensions = ['.pdf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
- if (previewableTypes.includes(attachment.fileType)) {
- return true;
- }
- if (attachment.fileExtension) {
- return previewableExtensions.includes(attachment.fileExtension.toLowerCase());
- }
- return false;
- };
- /** 显示附件详情 */
- const showAttachmentDetail = (attachment: any) => {
- const details = `文件名称:${attachment.fileName}<br/>
- 文件大小:${formatFileSize(attachment.fileSize)}<br/>
- 文件类型:${getFileTypeLabel(attachment.fileType)}<br/>
- 上传者:${attachment.uploaderName || '未知'}<br/>
- 上传时间:${attachment.createTime}<br/>
- 上传阶段:${getUploadStageText(attachment.uploadStage)}<br/>
- 存储路径:${attachment.filePath}<br/>
- 文件描述:${attachment.description || '无'}`;
- proxy?.$modal.alert(details);
- };
- /** 检查是否可以删除附件 */
- const canDeleteAttachment = (attachment: any) => {
- // 只有超级管理员、维修管理员或上传者本人可以删除附件
- if (isSuperAdmin.value || isMaintenanceAdmin.value) {
- return true;
- }
- // 检查是否为上传者本人
- if (currentUser.value && attachment.uploaderId) {
- return currentUser.value.userId === attachment.uploaderId;
- }
- return false;
- };
- /** 确认删除附件 */
- const confirmDeleteAttachment = async (attachment: any) => {
- try {
- await proxy?.$modal.confirm(`确定要删除附件 "${attachment.fileName}" 吗?删除后无法恢复!`);
- await deleteAttachment(attachment.id);
- proxy?.$modal.msgSuccess('附件删除成功');
- // 刷新附件列表和工单详情
- if (orderDetail.value) {
- await loadAttachments(orderDetail.value.id);
- // 更新附件数量
- if (orderDetail.value.attachmentCount > 0) {
- orderDetail.value.attachmentCount--;
- }
- }
- } catch (error) {
- if (error !== 'cancel') {
- console.error('删除附件失败:', error);
- proxy?.$modal.msgError('删除附件失败');
- }
- }
- };
- /** 查看图片集 */
- const viewImageGallery = (attachment: any) => {
- const imageList = currentAttachments.value.filter((att) => att.fileType === 'image');
- const currentIndex = imageList.findIndex((img) => img.id === attachment.id);
- // 使用Element Plus的图片预览功能
- const previewList = imageList.map((img) => img.filePath);
- const imageViewer = document.createElement('div');
- imageViewer.innerHTML = `<el-image :src="'${attachment.filePath}'" :preview-src-list="previewList" style="display: none;"></el-image>`;
- document.body.appendChild(imageViewer);
- // 触发预览
- setTimeout(() => {
- const img = imageViewer.querySelector('img') as HTMLImageElement;
- if (img) {
- img.click();
- }
- // 清理临时元素
- setTimeout(() => {
- document.body.removeChild(imageViewer);
- }, 100);
- }, 50);
- };
- /** 在新窗口打开 */
- const openInNewTab = (attachment: any) => {
- window.open(attachment.filePath, '_blank');
- };
- /** 查看进度 */
- const handleProgress = async (row: RepairOrderVO) => {
- progressLoading.value = true;
- progressDialog.title = `进度查看 - ${row.orderNo}`;
- progressDialog.visible = true;
- try {
- const res = await getRepairProgress(row.id);
- // 适配后端返回格式
- progressList.value = (res as any).data || (res as any);
- } catch (error) {
- console.error('获取进度失败:', error);
- proxy?.$modal.msgError('获取进度失败');
- } finally {
- progressLoading.value = false;
- }
- };
- /** 更新进度 */
- const handleUpdateProgress = (row: RepairOrderVO) => {
- updateProgressDialog.title = `进度更新 - ${row.orderNo}`;
- updateProgressDialog.visible = true;
- progressForm.value = {
- orderId: row.id,
- progressType: '5',
- progressStatus: '',
- progressDetail: '',
- locationInfo: '',
- estimatedDuration: undefined,
- nextStep: '',
- issuesEncountered: '',
- materialsNeeded: '',
- remark: ''
- };
- };
- /** 完成工单 */
- const handleComplete = (row: RepairOrderVO) => {
- completeDialog.title = `完成工单 - ${row.orderNo}`;
- completeDialog.visible = true;
- completeForm.value = {
- orderId: String(row.id),
- repairResult: '',
- usedMaterials: '',
- followSuggestions: ''
- };
- };
- /** 服务评价 */
- const handleEvaluate = (row: RepairOrderVO) => {
- evaluationDialog.title = `服务评价 - ${row.orderNo}`;
- evaluationDialog.visible = true;
- // 验证工单状态,只有已完成的工单才能评价
- if (String(row.orderStatus) !== '4') {
- proxy?.$modal.msgWarning('只有已完成的工单才能进行评价');
- return;
- }
- // 验证是否有分派的维修员
- if (!row.assigneeId) {
- proxy?.$modal.msgWarning('该工单没有分派维修员,无法进行评价');
- return;
- }
- console.log('初始化评价表单,工单信息:', {
- orderId: row.id,
- orderNo: row.orderNo,
- assigneeId: row.assigneeId,
- assigneeName: row.assigneeName,
- orderStatus: row.orderStatus
- });
- evaluationForm.value = {
- orderId: row.id,
- evaluatedWorkerId: row.assigneeId,
- overallScore: 5,
- qualityScore: 5,
- attitudeScore: 5,
- responseSpeedScore: 5,
- communicationScore: 5,
- evaluationContent: '',
- satisfactionLevel: '1',
- isAnonymous: 0,
- isPublic: 1,
- positiveTags: '',
- negativeTags: '',
- improvementSuggestions: '',
- remark: ''
- };
- };
- /** 上传前检查 */
- const handleBeforeUpload = (file: File) => {
- const isLt10M = file.size / 1024 / 1024 < 10;
- if (!isLt10M) {
- proxy?.$modal.msgError('上传文件大小不能超过 10MB!');
- return false;
- }
- // 设置上传参数
- const uploadParams: any = {
- uploadStage: '1', // 报修申请阶段
- description: `${form.value.title || '报修工单'}相关附件` // 添加描述信息
- };
- // 只有编辑模式时才传递orderId
- if (form.value.id) {
- uploadParams.orderId = form.value.id;
- }
- uploadData.value = uploadParams;
- console.log('准备上传文件:', {
- 文件名: file.name,
- 文件大小: (file.size / 1024 / 1024).toFixed(2) + 'MB',
- 工单ID: uploadParams.orderId || '待关联',
- 上传阶段: uploadParams.uploadStage,
- 描述: uploadParams.description,
- 模式: form.value.id ? '编辑模式' : '新增模式'
- });
- return true;
- };
- /** 上传成功 */
- const handleUploadSuccess = (response: any, file: any) => {
- console.log('上传成功:', response, file);
- if (response.code === 200) {
- proxy?.$modal.msgSuccess(`文件 ${file.name} 上传成功`);
- // 立即记录附件数据
- const attachmentData = response.data;
- if (attachmentData && attachmentData.id) {
- // 如果是新增模式,收集附件ID到临时列表
- if (!form.value.id) {
- tempAttachmentIds.value.push(attachmentData.id);
- console.log('新增模式:已收集附件ID', attachmentData.id, '当前临时附件ID列表:', tempAttachmentIds.value);
- }
- // 更新文件对象的相关信息
- file.attachmentId = attachmentData.id;
- file.status = 'success';
- file.url = attachmentData.filePath;
- // 如果是编辑模式,附件已经自动关联到工单,直接更新UI
- if (form.value.id) {
- console.log('编辑模式:附件已关联到工单', form.value.id, '附件ID:', attachmentData.id);
- // 可以选择刷新附件列表以确保数据同步
- // loadAttachments(form.value.id);
- }
- console.log('附件记录成功:', {
- 附件ID: attachmentData.id,
- 文件名: attachmentData.fileName,
- 文件路径: attachmentData.filePath,
- 工单ID: form.value.id || '待关联',
- 模式: form.value.id ? '编辑' : '新增'
- });
- }
- } else {
- proxy?.$modal.msgError(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`);
- }
- };
- /** 上传失败 */
- const handleUploadError = (error: Error, file: any) => {
- console.error('上传失败:', error, file);
- proxy?.$modal.msgError(`文件 ${file.name} 上传失败`);
- };
- /** 下载附件 */
- const handleDownloadAttachment = async (file: any) => {
- let downloadUrl = '';
- let fileName = file.name || 'attachment';
- // 如果文件已经上传成功,使用服务器返回的路径
- if (file.response && file.response.data && file.response.data.filePath) {
- downloadUrl = file.response.data.filePath;
- fileName = file.response.data.fileName || fileName;
- }
- // 如果是已有的附件,使用文件的url
- else if (file.url) {
- downloadUrl = file.url;
- }
- // 如果是本地文件,直接使用raw文件
- else if (file.raw) {
- const link = document.createElement('a');
- link.href = URL.createObjectURL(file.raw);
- link.download = fileName;
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(link.href);
- console.log('下载本地文件:', fileName);
- return;
- }
- if (!downloadUrl) {
- proxy?.$modal.msgError('无法获取文件下载地址');
- return;
- }
- // 对于服务器文件,使用fetch获取内容强制下载
- try {
- const loading = ElLoading.service({
- text: '正在下载文件...',
- target: document.body
- });
- const response = await fetch(downloadUrl);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = fileName;
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- loading?.close();
- console.log('下载服务器文件成功:', fileName);
- } catch (error) {
- console.error('下载文件失败:', error);
- proxy?.$modal.msgError('下载文件失败,请稍后重试');
- }
- };
- /** 删除附件确认 */
- const handleRemoveAttachmentConfirm = async (file: any) => {
- await proxy?.$modal.confirm('确定要删除该附件吗?删除操作将在保存时生效。');
- handleRemoveAttachment(file);
- };
- /** 移除附件 (修改为延迟删除) */
- const handleRemoveAttachment = (file: any) => {
- // 如果是已上传的文件(编辑模式下的已有附件或新增模式下已上传的附件)
- if (file.response && file.response.data && file.response.data.id) {
- // 不立即删除数据库记录,而是标记为待删除
- const attachmentId = file.response.data.id;
- // 如果是新增模式,从临时附件ID列表中移除
- if (!form.value.id) {
- const attachmentIndex = tempAttachmentIds.value.indexOf(attachmentId);
- if (attachmentIndex > -1) {
- tempAttachmentIds.value.splice(attachmentIndex, 1);
- console.log('新增模式:已从临时附件ID列表中移除:', attachmentId, '剩余附件数:', tempAttachmentIds.value.length);
- }
- } else {
- // 编辑模式:添加到待删除列表
- if (!pendingDeleteAttachmentIds.value.includes(attachmentId)) {
- pendingDeleteAttachmentIds.value.push(attachmentId);
- console.log('编辑模式:已标记附件为待删除:', attachmentId, '待删除列表:', pendingDeleteAttachmentIds.value);
- }
- }
- // 从attachmentList中移除
- const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
- if (index > -1) {
- attachmentList.value.splice(index, 1);
- }
- proxy?.$modal.msgSuccess('附件已移除,将在保存时生效');
- }
- // 如果是临时文件(新增时添加但未上传的文件),从临时列表中删除
- else if (file.raw && !form.value.id) {
- const index = tempFileList.value.findIndex((f) => f.name === file.raw.name && f.size === file.raw.size);
- if (index > -1) {
- tempFileList.value.splice(index, 1);
- console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
- }
- }
- // 如果是编辑模式下新添加但还未上传的文件
- else if (file.raw && form.value.id) {
- // 编辑模式下新添加的文件,直接从attachmentList中移除
- const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
- if (index > -1) {
- attachmentList.value.splice(index, 1);
- console.log('已从附件列表中移除未上传文件:', file.name);
- }
- }
- };
- /** 表单重置 */
- const reset = async (autoFillUserInfo: boolean = false) => {
- form.value = {
- id: undefined,
- title: '',
- description: '',
- repairLocation: '',
- categoryId: '',
- priorityLevel: undefined,
- expectedFinishTime: '',
- reporterName: '',
- reporterPhone: '',
- reporterDept: '',
- orderStatus: '',
- assigneeId: '',
- assignRemark: '',
- repairResult: '',
- usedMaterials: '',
- followSuggestions: '',
- cancelReason: '',
- remark: ''
- };
- // 清空附件相关数据
- attachmentList.value = [];
- tempFileList.value = [];
- tempAttachmentIds.value = [];
- pendingDeleteAttachmentIds.value = [];
- console.log('已清空附件列表、临时文件列表、临时附件ID列表和待删除附件列表');
- // 如果是新增操作,自动填充当前用户信息
- if (autoFillUserInfo) {
- try {
- const userInfo = await getCurrentUserDetailInfo();
- if (userInfo) {
- form.value.reporterName = userInfo.nickName || userInfo.userName || '';
- form.value.reporterPhone = userInfo.phonenumber || '';
- form.value.reporterDept = userInfo.deptName || '';
- console.log('自动填充报修人信息:', {
- 姓名: form.value.reporterName,
- 电话: form.value.reporterPhone,
- 部门: form.value.reporterDept
- });
- // 显示提示信息
- proxy?.$modal.msgSuccess('已自动填充报修人信息');
- }
- } catch (error) {
- console.error('自动填充用户信息失败:', error);
- proxy?.$modal.msgWarning('无法获取用户信息,请手动填写报修人信息');
- }
- }
- repairOrderFormRef.value?.resetFields();
- };
- /** 取消按钮 */
- const cancel = () => {
- dialog.visible = false;
- reset();
- };
- /** 获取紧急程度标签类型 */
- const getPriorityTagType = (level: number) => {
- const types: Record<number, string> = {
- 1: 'danger',
- 2: 'warning',
- 3: 'primary',
- 4: 'info',
- 5: 'success'
- };
- return types[level] || 'info';
- };
- /** 获取紧急程度文本 */
- const getPriorityText = (level: number) => {
- const dict = pm_priority_level.value.find((item) => parseInt(item.value) === level);
- return dict?.label || level.toString();
- };
- /** 获取状态标签类型 */
- const getStatusTagType = (status: string) => {
- const types: Record<string, string> = {
- '1': 'info', // 待分派
- '2': 'warning', // 已分派
- '3': 'primary', // 进行中
- '4': 'success', // 已完成
- '5': 'success', // 已评价
- '6': 'info', // 已归档
- '9': 'danger' // 已取消
- };
- return types[status] || 'info';
- };
- /** 获取状态文本 */
- const getStatusText = (status: string) => {
- const dict = pm_order_status.value.find((item) => item.value === status);
- return dict?.label || status;
- };
- /** 格式化时长 */
- const formatDuration = (minutes: number) => {
- if (minutes < 60) {
- return `${minutes}分钟`;
- } else {
- const hours = Math.floor(minutes / 60);
- const remainingMinutes = minutes % 60;
- return remainingMinutes > 0 ? `${hours}小时${remainingMinutes}分钟` : `${hours}小时`;
- }
- };
- /** 提交进度更新 */
- const submitProgress = () => {
- progressFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- try {
- await updateRepairProgress(progressForm.value);
- proxy?.$modal.msgSuccess('进度更新成功');
- updateProgressDialog.visible = false;
- await getList();
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- /** 取消进度更新 */
- const cancelProgress = () => {
- updateProgressDialog.visible = false;
- };
- /** 提交完成工单 */
- const submitComplete = () => {
- completeFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- try {
- await finishRepairOrder(completeForm.value.orderId, completeForm.value);
- proxy?.$modal.msgSuccess('工单完成成功');
- completeDialog.visible = false;
- await getList();
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- /** 取消完成工单 */
- const cancelComplete = () => {
- completeDialog.visible = false;
- };
- /** 提交服务评价 */
- const submitEvaluation = () => {
- evaluationFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- try {
- console.log('提交评价数据:', evaluationForm.value);
- // 验证必要字段
- if (!evaluationForm.value.orderId) {
- proxy?.$modal.msgError('工单ID不能为空');
- return;
- }
- if (!evaluationForm.value.evaluatedWorkerId) {
- proxy?.$modal.msgError('请选择要评价的维修员');
- return;
- }
- const response = await createServiceEvaluation(evaluationForm.value);
- console.log('评价提交响应:', response);
- proxy?.$modal.msgSuccess('服务评价提交成功');
- evaluationDialog.visible = false;
- // 刷新列表,确保状态更新
- await getList();
- // 如果工单状态没有自动更新,可能需要手动调用状态更新接口
- // 这里可以根据后端API设计添加相应的状态更新逻辑
- } catch (error: any) {
- console.error('评价提交失败:', error);
- // 显示详细的错误信息
- const errorMsg = error?.response?.data?.msg || error?.message || '评价提交失败,请重试';
- proxy?.$modal.msgError(errorMsg);
- } finally {
- buttonLoading.value = false;
- }
- }
- });
- };
- /** 取消服务评价 */
- const cancelEvaluation = () => {
- evaluationDialog.visible = false;
- };
- /** 下载附件 */
- const downloadAttachment = async (attachment: any) => {
- if (!attachment.filePath) {
- proxy?.$modal.msgError('无法获取文件下载地址');
- return;
- }
- try {
- // 显示加载提示
- const loading = ElLoading.service({
- text: '正在下载文件...',
- target: document.body
- });
- // 通过fetch获取文件内容,强制下载
- const response = await fetch(attachment.filePath);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- // 创建下载链接
- const link = document.createElement('a');
- link.href = url;
- link.download = attachment.fileName || '附件';
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- // 释放blob URL
- window.URL.revokeObjectURL(url);
- loading?.close();
- console.log('下载附件成功:', attachment.fileName || '附件');
- } catch (error) {
- console.error('下载附件失败:', error);
- proxy?.$modal.msgError('下载文件失败,请稍后重试');
- }
- };
- /** 预览附件 */
- const previewAttachment = (attachment: any) => {
- if (attachment.fileType === 'image') {
- // 图片预览已内置在el-image组件中
- return;
- }
- // 其他类型文件在新窗口打开
- window.open(attachment.filePath, '_blank');
- };
- /** 格式化文件大小 */
- const formatFileSize = (bytes: number): string => {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- };
- /** 文件变更处理 */
- const handleFileChange = (file: any, fileList: any[]) => {
- console.log('文件变更:', file, fileList);
- // 新增时收集文件到临时列表
- if (!form.value.id && file.raw && file.status !== 'removed') {
- // 检查文件大小
- const isLt10M = file.raw.size / 1024 / 1024 < 10;
- if (!isLt10M) {
- proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
- // 从文件列表中移除这个文件
- const index = fileList.findIndex((f) => f.uid === file.uid);
- if (index > -1) {
- fileList.splice(index, 1);
- }
- return;
- }
- // 如果是添加文件状态,添加到临时列表
- if (file.status === 'ready' || !file.status) {
- // 检查是否已存在(避免重复添加)
- const existingFile = tempFileList.value.find(
- (f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
- );
- if (!existingFile) {
- tempFileList.value.push(file.raw);
- console.log('已添加文件到临时列表:', file.raw.name, '总文件数:', tempFileList.value.length);
- }
- }
- }
- // 编辑模式下新添加文件的处理
- if (form.value.id && file.raw && file.status !== 'removed' && (file.status === 'ready' || !file.status)) {
- // 检查文件大小
- const isLt10M = file.raw.size / 1024 / 1024 < 10;
- if (!isLt10M) {
- proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
- // 从文件列表中移除这个文件
- const index = fileList.findIndex((f) => f.uid === file.uid);
- if (index > -1) {
- fileList.splice(index, 1);
- }
- return;
- }
- console.log('编辑模式下添加新文件:', file.name);
- }
- // 如果是删除文件,相应处理
- if (file.status === 'removed') {
- if (!form.value.id && file.raw) {
- // 新增模式下,从临时列表中移除
- const index = tempFileList.value.findIndex(
- (f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
- );
- if (index > -1) {
- tempFileList.value.splice(index, 1);
- console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
- }
- }
- // 编辑模式下的删除在handleRemoveAttachment中处理
- }
- };
- /** 获取进度类型图标颜色 */
- const getProgressIconColor = (progressType: string) => {
- const colorMap: Record<string, string> = {
- '1': '#409eff', // 工单分派 - 蓝色
- '2': '#67c23a', // 接收确认 - 绿色
- '3': '#e6a23c', // 到达现场 - 橙色
- '4': '#409eff', // 开始维修 - 蓝色
- '5': '#909399', // 进度更新 - 灰色
- '6': '#67c23a', // 完成维修 - 绿色
- '7': '#f56c6c', // 用户评价 - 红色
- '8': '#606266', // 工单归档 - 深灰色
- '9': '#909399' // 其他 - 灰色
- };
- return colorMap[progressType] || '#909399';
- };
- /** 获取进度类型标签类型 */
- const getProgressTypeTag = (progressType: string): 'success' | 'warning' | 'info' | 'primary' | 'danger' => {
- const tagMap: Record<string, 'success' | 'warning' | 'info' | 'primary' | 'danger'> = {
- '1': 'primary',
- '2': 'success',
- '3': 'warning',
- '4': 'primary',
- '5': 'info',
- '6': 'success',
- '7': 'danger',
- '8': 'info',
- '9': 'info'
- };
- return tagMap[progressType] || 'info';
- };
- /** 获取进度类型文本 */
- const getProgressTypeText = (progressType: string) => {
- const textMap: Record<string, string> = {
- '1': '工单分派',
- '2': '接收确认',
- '3': '到达现场',
- '4': '开始维修',
- '5': '进度更新',
- '6': '完成维修',
- '7': '用户评价',
- '8': '工单归档',
- '9': '其他'
- };
- return textMap[progressType] || '未知类型';
- };
- /** 检查是否有额外信息 */
- const hasAdditionalInfo = (progress: any) => {
- return (
- progress.locationInfo ||
- progress.estimatedDuration ||
- progress.nextStep ||
- progress.issuesEncountered ||
- progress.materialsNeeded ||
- progress.remark
- );
- };
- onMounted(() => {
- getList();
- getStatistics();
- getCategoryOptions();
- getCategoryTreeOptions();
- getWorkerOptions();
- getCurrentUserInfo();
- });
- /** 卡片预览 */
- const handleFilePreview = (file: any) => {
- // 如果文件已经上传成功,优先使用服务器返回的url
- let url = file.url || (file.response && file.response.data && file.response.data.filePath);
- // 若还未上传,则尝试使用本地预览地址
- if (!url && file.raw) {
- url = URL.createObjectURL(file.raw);
- }
- if (!url) return;
- // 对图片使用图片预览,其余直接新窗口打开
- const isImage = file.type ? file.type.startsWith('image') : file.raw && file.raw.type.startsWith('image');
- if (isImage) {
- // 利用 Element Plus ImageViewer 进行预览
- const viewer = (proxy as any)?.$imageViewer;
- viewer && viewer([url]);
- } else {
- window.open(url, '_blank');
- }
- };
- /** 判断是否为图片文件 */
- const isImageFile = (file: any): boolean => {
- // 优先使用文件 MIME type 判断
- const type = file.type || (file.raw && file.raw.type) || '';
- if (type && type.startsWith('image')) {
- return true;
- }
- // 编辑场景下从数据库加载的附件通常缺少 MIME type
- // 通过文件名或 URL 后缀进行判断
- const name: string = file.name || file.response?.data?.fileName || '';
- const url: string = file.url || file.response?.data?.filePath || '';
- const source = (name || url).toLowerCase();
- // 常见图片扩展名
- const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico'];
- return IMAGE_EXTS.some((ext) => source.endsWith(ext));
- };
- /** 获取文件扩展名小写 */
- const getFileExt = (fileName: string): string => {
- if (!fileName) return '';
- const idx = fileName.lastIndexOf('.');
- return idx !== -1 ? fileName.substring(idx + 1).toLowerCase() : '';
- };
- </script>
- <style lang="scss" scoped>
- /* 页面容器样式 */
- .page-container {
- padding: 8px;
- }
- /* 搜索按钮容器样式 */
- .search-btn-container {
- display: flex;
- justify-content: flex-end;
- margin-top: 10px;
- .el-form-item {
- margin-bottom: 0 !important;
- }
- }
- /* 表格卡片布局 */
- .table-card {
- margin-top: 0;
- }
- .table-card :deep(.el-card__body) {
- padding: 0px 20px 20px 20px !important;
- }
- /* 表格主体 */
- .table-container {
- margin-bottom: 0;
- }
- /* 移除按钮栏底部边框 */
- .button-card :deep(.el-card__header) {
- border-bottom: none !important;
- }
- /* 统计卡片样式 */
- .stat-card {
- border-radius: 8px;
- transition: all 0.3s ease;
- height: 90px;
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
- }
- .stat-content {
- display: flex;
- align-items: center;
- padding: 5px 12px;
- height: 100%;
- box-sizing: border-box;
- .stat-icon {
- font-size: 24px;
- margin-right: 12px;
- opacity: 0.8;
- }
- .stat-info {
- flex: 1;
- .stat-value {
- font-size: 20px;
- font-weight: bold;
- line-height: 1;
- margin-bottom: 3px;
- }
- .stat-label {
- font-size: 13px;
- opacity: 0.7;
- line-height: 1;
- }
- }
- }
- &.stat-card-primary {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- }
- &.stat-card-success {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- }
- &.stat-card-warning {
- background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
- color: white;
- }
- &.stat-card-info {
- background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
- color: #333;
- }
- }
- /* 表格样式 */
- .scroll-table {
- :deep(.el-table__header-wrapper) {
- .el-table__header {
- th {
- background-color: #f8f9fa;
- color: #333;
- font-weight: 600;
- }
- }
- }
- :deep(.el-table__body-wrapper) {
- .el-table__row {
- &:hover {
- background-color: #f5f7fa;
- }
- }
- }
- }
- /* 工单信息样式 */
- .order-info {
- width: 100%;
- box-sizing: border-box; // 使内边距计入总宽度,避免超出表单内容区域
- background-color: #f8f9fa;
- padding: 16px;
- border-radius: 8px;
- border-left: 4px solid #409eff;
- p {
- margin: 8px 0;
- line-height: 1.5;
- &:first-child {
- margin-top: 0;
- }
- &:last-child {
- margin-bottom: 0;
- }
- }
- }
- /* 对话框样式 */
- .dialog-footer-right {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- }
- /* 分页容器样式 */
- .pagination-container {
- display: flex;
- justify-content: flex-end;
- padding: 16px 20px 0 0 !important;
- background-color: #fff;
- margin-top: 0px !important;
- }
- /* 附件上传容器样式 */
- .attachment-upload-container {
- .upload-demo {
- .el-upload {
- /* border: 2px dashed #d9d9d9; */
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: all 0.3s;
- &:hover {
- border-color: #409eff;
- }
- }
- .el-upload__tip {
- font-size: 12px;
- color: #000000;
- margin-top: 7px;
- }
- // 调整拖拽上传图标与文字在同一行、居中对齐
- .el-upload-dragger1 {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 15px;
- padding: 0px !important;
- }
- .el-upload-dragger1 .el-icon--upload {
- margin: 0;
- padding: 0px !important;
- }
- /* 文件卡片占位封面样式 */
- .upload-file-placeholder {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #909399;
- font-size: 14px;
- .el-icon {
- font-size: 28px;
- margin-bottom: 4px;
- }
- .file-ext {
- font-size: 12px;
- text-transform: uppercase;
- text-align: center;
- }
- }
- }
- /* 让附件卡片一行展示 4 个 */
- :deep(.el-upload-list--picture-card) {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 9px;
- margin-top: 12px;
- /* 确保卡片宽度自适应 */
- .el-upload-list__item {
- margin: 0;
- }
- }
- }
- :deep(.el-upload-dragger) {
- border: 0px !important;
- padding: 0px !important;
- .el-upload-dragger1 {
- border: 0px !important;
- padding: 0px !important;
- }
- }
- /* 附件查看对话框样式 */
- .attachment-container {
- .no-attachment {
- text-align: center;
- padding: 40px 0;
- }
- .attachment-list {
- .attachment-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px;
- border: 1px solid #e8e8e8;
- border-radius: 8px;
- margin-bottom: 8px;
- transition: all 0.3s;
- &:last-child {
- margin-bottom: 0;
- }
- &:hover {
- border-color: #409eff;
- background-color: #f0f9ff;
- }
- .attachment-info {
- display: flex;
- align-items: center;
- flex: 1;
- .attachment-preview {
- margin-right: 12px;
- .file-icon {
- color: #909399;
- }
- }
- .attachment-details {
- flex: 1;
- .file-name {
- font-weight: 500;
- color: #333;
- margin-bottom: 4px;
- word-break: break-all;
- }
- .file-meta {
- font-size: 12px;
- color: #909399;
- display: flex;
- gap: 12px;
- }
- .file-description {
- font-size: 12px;
- color: #666;
- margin-top: 4px;
- font-style: italic;
- }
- }
- }
- .attachment-actions {
- display: flex;
- gap: 8px;
- }
- }
- }
- }
- /* 详情弹窗样式 */
- .repair-order-detail {
- .detail-card {
- margin-bottom: 16px;
- border: 1px solid #e8e8e8;
- border-radius: 8px;
- &:last-child {
- margin-bottom: 0;
- }
- .card-header {
- display: flex;
- align-items: center;
- font-weight: 600;
- color: #333;
- .card-title {
- display: flex;
- align-items: center;
- font-size: 16px;
- .el-icon {
- margin-right: 8px;
- font-size: 18px;
- color: #409eff;
- }
- }
- }
- .detail-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 16px 24px;
- .detail-item {
- display: flex;
- align-items: flex-start;
- &.full-width {
- grid-column: 1 / -1;
- }
- label {
- flex-shrink: 0;
- width: 120px;
- font-weight: 500;
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- }
- .detail-value {
- flex: 1;
- color: #333;
- font-size: 14px;
- line-height: 1.5;
- &.description {
- line-height: 1.6;
- word-break: break-word;
- white-space: pre-wrap;
- background-color: #f8f9fa;
- padding: 8px 12px;
- border-radius: 4px;
- border-left: 3px solid #409eff;
- }
- }
- .el-tag {
- font-weight: 500;
- }
- .el-rate {
- display: flex;
- align-items: center;
- }
- }
- }
- }
- /* 响应式设计 */
- @media (max-width: 768px) {
- .detail-card .detail-grid {
- grid-template-columns: 1fr;
- gap: 12px;
- .detail-item {
- flex-direction: column;
- align-items: flex-start;
- label {
- width: auto;
- margin-bottom: 4px;
- }
- }
- }
- }
- }
- /* 附件网格卡片样式 */
- .attachment-detail-container {
- .no-attachment-detail {
- text-align: center;
- padding: 40px 0;
- color: #909399;
- }
- .attachment-grid-container {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
- padding: 8px 0;
- .attachment-card {
- background: #ffffff;
- border: 1px solid #e8e8e8;
- border-radius: 8px;
- overflow: hidden;
- transition: all 0.3s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- &:hover {
- border-color: #409eff;
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
- transform: translateY(-2px);
- }
- .attachment-thumbnail-wrapper {
- position: relative;
- height: 120px;
- background: #f5f7fa;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- .attachment-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .attachment-file-icon {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- color: #409eff;
- .file-type-text {
- font-size: 12px;
- color: #666;
- font-weight: 500;
- }
- }
- }
- .attachment-card-body {
- padding: 12px;
- height: 80px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- .attachment-title {
- font-size: 13px;
- font-weight: 600;
- color: #333;
- line-height: 1.3;
- margin-bottom: 6px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .attachment-meta {
- display: flex;
- justify-content: space-between;
- font-size: 11px;
- color: #999;
- margin-bottom: 4px;
- .file-size {
- font-weight: 500;
- }
- .upload-time {
- font-style: italic;
- }
- }
- .attachment-desc {
- font-size: 11px;
- color: #666;
- line-height: 1.3;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-top: auto;
- }
- }
- .attachment-card-actions {
- padding: 8px 12px;
- border-top: 1px solid #f0f0f0;
- display: flex;
- justify-content: center;
- .el-button-group {
- .el-button {
- padding: 4px 6px;
- font-size: 12px;
- .el-icon {
- font-size: 14px;
- }
- }
- }
- }
- }
- }
- }
- /* 卡片头部样式增强 */
- .detail-card .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- .card-title {
- flex: 1;
- }
- }
- /* 进度查看样式 */
- .progress-container {
- .no-progress {
- text-align: center;
- padding: 40px 0;
- color: #909399;
- }
- .progress-timeline {
- position: relative;
- padding: 16px 0;
- .progress-item {
- position: relative;
- display: flex;
- align-items: flex-start;
- margin-bottom: 24px;
- &:last-child {
- margin-bottom: 0;
- .timeline-line {
- display: none;
- }
- }
- .timeline-icon {
- position: relative;
- z-index: 2;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- background: white;
- border: 2px solid #e8e8e8;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 16px;
- flex-shrink: 0;
- margin-top: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .timeline-line {
- position: absolute;
- left: 15px;
- top: 40px;
- width: 2px;
- height: calc(100% + 8px);
- background: linear-gradient(to bottom, #e8e8e8 0%, #f0f0f0 100%);
- z-index: 1;
- }
- .progress-card {
- flex: 1;
- background: white;
- border: 1px solid #e8e8e8;
- border-radius: 8px;
- padding: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- transition: all 0.3s ease;
- &:hover {
- border-color: #409eff;
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
- transform: translateY(-1px);
- }
- .progress-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- padding-bottom: 12px;
- border-bottom: 1px solid #f0f0f0;
- .progress-type-status {
- display: flex;
- align-items: center;
- gap: 8px;
- .progress-status {
- font-weight: 600;
- color: #333;
- font-size: 14px;
- }
- }
- .progress-time {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 13px;
- color: #666;
- .el-icon {
- font-size: 14px;
- }
- }
- }
- .progress-content {
- .progress-detail {
- margin-bottom: 12px;
- .detail-text {
- margin: 0;
- line-height: 1.6;
- color: #333;
- font-size: 14px;
- background: #f8f9fa;
- padding: 12px;
- border-radius: 6px;
- border-left: 3px solid #409eff;
- }
- }
- .progress-info-grid {
- display: grid;
- grid-template-columns: 1fr;
- gap: 8px;
- .info-item {
- display: flex;
- align-items: flex-start;
- font-size: 13px;
- line-height: 1.5;
- label {
- display: flex;
- align-items: center;
- gap: 4px;
- min-width: 80px;
- color: #666;
- font-weight: 500;
- margin-right: 8px;
- .el-icon {
- font-size: 14px;
- color: #409eff;
- }
- }
- span {
- flex: 1;
- color: #333;
- word-break: break-word;
- }
- }
- }
- }
- }
- }
- }
- }
- /* 响应式设计 */
- @media (max-width: 1200px) {
- .attachment-detail-container .attachment-grid-container {
- grid-template-columns: repeat(2, 1fr);
- gap: 14px;
- }
- }
- @media (max-width: 768px) {
- .attachment-detail-container .attachment-grid-container {
- grid-template-columns: 1fr;
- gap: 12px;
- .attachment-card {
- .attachment-thumbnail-wrapper {
- height: 100px;
- }
- .attachment-card-body {
- height: 70px;
- padding: 10px;
- .attachment-title {
- font-size: 12px;
- }
- .attachment-meta {
- font-size: 10px;
- }
- }
- .attachment-card-actions {
- padding: 6px 10px;
- .el-button-group .el-button {
- padding: 3px 5px;
- font-size: 11px;
- .el-icon {
- font-size: 12px;
- }
- }
- }
- }
- }
- /* 进度查看响应式 */
- .progress-container .progress-timeline .progress-item {
- .timeline-icon {
- width: 28px;
- height: 28px;
- margin-right: 12px;
- }
- .timeline-line {
- left: 13px;
- }
- .progress-card {
- padding: 12px;
- .progress-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 8px;
- .progress-time {
- font-size: 12px;
- }
- }
- .progress-content {
- .progress-detail .detail-text {
- padding: 8px;
- font-size: 13px;
- }
- .progress-info-grid .info-item {
- font-size: 12px;
- label {
- min-width: 70px;
- font-size: 12px;
- }
- }
- }
- }
- }
- }
- :deep(.el-icon) {
- display: flex !important;
- }
- /* Element Plus upload 组件样式覆盖 */
- :deep(.el-upload-list--picture-card) {
- .el-upload-list__item {
- width: 166px !important;
- height: auto !important;
- min-height: 180px !important;
- margin: 0 8px 8px 0 !important;
- border-radius: 6px !important;
- overflow: hidden !important;
- }
- }
- /* 卡片整体布局包含文件名 */
- .upload-file-card {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- min-height: 180px;
- position: relative;
- }
- .upload-file-thumb {
- width: 100%;
- height: 120px;
- position: relative;
- overflow: hidden;
- border-radius: 4px 4px 0 0;
- }
- .upload-file-thumb .el-image,
- .upload-file-thumb .upload-file-placeholder {
- width: 100%;
- height: 100%;
- }
- .upload-file-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background-color: #fafafa;
- .el-icon {
- font-size: 24px;
- color: #c0c4cc;
- margin-bottom: 8px;
- }
- .file-ext {
- font-size: 12px;
- color: #909399;
- text-align: center;
- word-break: break-all;
- padding: 0 8px;
- }
- }
- .upload-file-name {
- width: 100%;
- padding: 8px 4px 4px 4px;
- font-size: 12px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #606266;
- background-color: #fff;
- flex-shrink: 0;
- }
- /* 上传文件卡片操作按钮样式 (与预览界面保持一致) */
- .upload-file-card-actions {
- width: 100%;
- padding: 6px 8px;
- border-top: 1px solid #f0f0f0;
- display: flex;
- justify-content: center;
- background-color: #fafafa;
- margin-top: auto;
- flex-shrink: 0;
- .el-button-group {
- .el-button {
- padding: 4px 6px;
- font-size: 12px;
- .el-icon {
- font-size: 14px;
- }
- }
- }
- }
- </style>
|