index.vue 141 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244
  1. <template>
  2. <div class="ele-body factory-calendar">
  3. <el-card shadow="never" class="factory-calendar-card">
  4. <div class="page-header">
  5. <div class="header-actions">
  6. <el-button
  7. size="small"
  8. type="primary"
  9. icon="el-icon-plus"
  10. class="ele-btn-icon"
  11. @click="openCalendarDialog()"
  12. >
  13. 新增日历
  14. </el-button>
  15. </div>
  16. </div>
  17. <el-tabs v-model="activeTab" @tab-click="handleTabChange">
  18. <el-tab-pane label="基础管理" name="base">
  19. <base-manage ref="baseManage" :vm="vm" />
  20. </el-tab-pane>
  21. <el-tab-pane label="多维日历视图" name="view">
  22. <multi-calendar-view :vm="vm" />
  23. </el-tab-pane>
  24. <el-tab-pane label="冲突检测" name="conflict">
  25. <conflict-panel ref="conflictPanel" :vm="vm" />
  26. </el-tab-pane>
  27. <el-tab-pane label="临时调整审批" name="adjust">
  28. <adjust-approval ref="adjustApproval" :vm="vm" />
  29. </el-tab-pane>
  30. <el-tab-pane label="数据统计" name="stat">
  31. <statistics-view :vm="vm" />
  32. </el-tab-pane>
  33. </el-tabs>
  34. </el-card>
  35. <ele-modal
  36. :visible.sync="calendarDialogVisible"
  37. :title="calendarDialogTitle"
  38. width="900px"
  39. custom-class="ele-dialog-form factory-calendar-dialog"
  40. :close-on-click-modal="false"
  41. >
  42. <el-form
  43. ref="calendarForm"
  44. :model="calendarForm"
  45. :rules="calendarRules"
  46. label-width="110px"
  47. class="el-form-box"
  48. >
  49. <el-form-item label="日历编码">
  50. <el-input v-model="calendarForm.calendarCode" disabled />
  51. </el-form-item>
  52. <el-form-item label="日历名称" prop="calendarName">
  53. <el-input v-model="calendarForm.calendarName" maxlength="128" />
  54. </el-form-item>
  55. <el-form-item label="日历类型" prop="calendarType">
  56. <el-select
  57. v-model="calendarForm.calendarType"
  58. :disabled="!!calendarForm.id"
  59. style="width: 100%"
  60. >
  61. <el-option
  62. v-for="item in calendarTypeOptions"
  63. :key="item.value"
  64. :label="item.label"
  65. :value="item.value"
  66. />
  67. </el-select>
  68. </el-form-item>
  69. <el-form-item label="适用年份" prop="applyYear">
  70. <el-select v-model="calendarFormApplyYear" style="width: 100%">
  71. <el-option
  72. v-for="year in yearOptions"
  73. :key="year"
  74. :label="year"
  75. :value="year"
  76. />
  77. </el-select>
  78. </el-form-item>
  79. <el-form-item label="适用月份" prop="applyMonth">
  80. <el-select
  81. v-model="calendarForm.applyMonth"
  82. multiple
  83. style="width: 100%"
  84. @change="handleApplyMonthChange"
  85. >
  86. <el-option
  87. v-for="month in monthOptions"
  88. :key="month.value"
  89. :label="month.label"
  90. :value="month.value"
  91. />
  92. </el-select>
  93. </el-form-item>
  94. <el-form-item label="状态">
  95. <el-switch
  96. v-model="calendarForm.status"
  97. :active-value="1"
  98. :inactive-value="0"
  99. active-text="启用"
  100. inactive-text="禁用"
  101. />
  102. </el-form-item>
  103. <el-form-item label="周休设置">
  104. <div class="rest-rule-editor">
  105. <el-radio-group v-model="calendarForm.restMode">
  106. <el-radio-button
  107. v-for="item in restModeOptions"
  108. :key="item.value"
  109. :label="item.value"
  110. >
  111. {{ item.label }}
  112. </el-radio-button>
  113. </el-radio-group>
  114. <el-checkbox-group
  115. v-if="calendarForm.restMode === 'custom'"
  116. v-model="calendarForm.restWeekdays"
  117. class="custom-rest-weekdays"
  118. >
  119. <el-checkbox-button
  120. v-for="item in restWeekdayOptions"
  121. :key="item.value"
  122. :label="item.value"
  123. >
  124. {{ item.label }}
  125. </el-checkbox-button>
  126. </el-checkbox-group>
  127. <div class="rule-tip">
  128. 双休为周六周日休,单休为周日休,大小周为周日固定休且隔周周六休,自定义可勾选任意休息星期。
  129. </div>
  130. </div>
  131. </el-form-item>
  132. <el-form-item v-if="showLegalHolidaySection" label="法定节假日">
  133. <div class="legal-holiday-section">
  134. <div class="legal-holiday-header">
  135. <div>
  136. <div class="legal-holiday-title">法定节假日</div>
  137. <div class="legal-holiday-desc">
  138. 默认休息,点选日期后按上班处理
  139. </div>
  140. </div>
  141. <span class="legal-holiday-count">
  142. {{
  143. legalHolidayLoading
  144. ? '同步节假日中...'
  145. : `${legalHolidaySourceText},已设上班 ${legalHolidayVisibleWorkCount} 天`
  146. }}
  147. </span>
  148. </div>
  149. <div v-if="legalHolidayGroups.length" class="legal-holiday-list">
  150. <div
  151. v-for="group in legalHolidayGroups"
  152. :key="group.name"
  153. class="legal-holiday-group"
  154. >
  155. <div class="legal-holiday-group-head">
  156. <strong>{{ group.name }}</strong>
  157. <span>{{ group.dates.length }}天</span>
  158. </div>
  159. <div class="legal-holiday-dates">
  160. <button
  161. v-for="item in group.dates"
  162. :key="item.calendarDate"
  163. type="button"
  164. class="legal-holiday-date"
  165. :class="{
  166. 'is-work': calendarForm.legalHolidayWorkDates.includes(
  167. item.calendarDate
  168. )
  169. }"
  170. @click="toggleLegalHolidayWorkStatus(item.calendarDate)"
  171. >
  172. <span>{{ formatHolidayDate(item.calendarDate) }}</span>
  173. <em>
  174. {{
  175. calendarForm.legalHolidayWorkDates.includes(
  176. item.calendarDate
  177. )
  178. ? '上班'
  179. : '休息'
  180. }}
  181. </em>
  182. </button>
  183. </div>
  184. </div>
  185. </div>
  186. </div>
  187. </el-form-item>
  188. <el-form-item label="特殊日期">
  189. <div class="holiday-rule-editor">
  190. <div
  191. v-for="(rule, index) in calendarForm.holidayRules"
  192. :key="rule.id"
  193. class="holiday-rule-row"
  194. >
  195. <el-date-picker
  196. v-model="rule.calendarDate"
  197. type="date"
  198. value-format="yyyy-MM-dd"
  199. placeholder="选择日期"
  200. style="width: 160px"
  201. />
  202. <el-select
  203. v-model="rule.dateType"
  204. placeholder="日期类型"
  205. style="width: 130px"
  206. >
  207. <el-option label="上班" :value="1" />
  208. <el-option label="休息" :value="2" />
  209. <el-option label="法定节假日" :value="3" />
  210. </el-select>
  211. <el-input
  212. v-model="rule.remark"
  213. placeholder="备注"
  214. maxlength="80"
  215. style="flex: 1"
  216. />
  217. <el-button
  218. type="text"
  219. icon="el-icon-delete"
  220. class="danger-link"
  221. @click="removeHolidayRule(index)"
  222. >
  223. 删除
  224. </el-button>
  225. </div>
  226. <el-button size="small" icon="el-icon-plus" @click="addHolidayRule">
  227. 新增特殊日期
  228. </el-button>
  229. <div class="rule-tip">
  230. 特殊日期优先级高于周休设置,可把周末设为上班,也可把工作日设为休息或法定节假日。
  231. </div>
  232. </div>
  233. </el-form-item>
  234. <el-form-item label="备注">
  235. <el-input
  236. v-model="calendarForm.remark"
  237. type="textarea"
  238. :rows="3"
  239. maxlength="500"
  240. show-word-limit
  241. />
  242. </el-form-item>
  243. </el-form>
  244. <div slot="footer">
  245. <el-button type="primary" @click="saveCalendar">确定</el-button>
  246. <el-button @click="calendarDialogVisible = false">取消</el-button>
  247. </div>
  248. </ele-modal>
  249. <ele-modal
  250. :visible.sync="copyDialogVisible"
  251. title="复制日历配置"
  252. width="520px"
  253. custom-class="ele-dialog-form"
  254. >
  255. <el-form :model="copyForm" label-width="100px" class="el-form-box">
  256. <el-form-item label="源日历">
  257. <el-select v-model="copyForm.sourceId" style="width: 100%">
  258. <el-option
  259. v-for="item in calendars"
  260. :key="item.id"
  261. :label="item.calendarName"
  262. :value="item.id"
  263. />
  264. </el-select>
  265. </el-form-item>
  266. <el-form-item label="目标年份">
  267. <el-select v-model="copyForm.targetYear" style="width: 100%">
  268. <el-option
  269. v-for="year in yearOptions"
  270. :key="year"
  271. :label="year"
  272. :value="year"
  273. />
  274. </el-select>
  275. </el-form-item>
  276. </el-form>
  277. <div slot="footer">
  278. <el-button type="primary" @click="copyCalendar">确定</el-button>
  279. <el-button @click="copyDialogVisible = false">取消</el-button>
  280. </div>
  281. </ele-modal>
  282. <ele-modal
  283. :visible.sync="adjustDialogVisible"
  284. title="临时调整申请"
  285. width="720px"
  286. custom-class="ele-dialog-form"
  287. :close-on-click-modal="false"
  288. >
  289. <el-form
  290. ref="adjustForm"
  291. :model="adjustForm"
  292. :rules="adjustRules"
  293. label-width="120px"
  294. class="el-form-box"
  295. >
  296. <el-form-item label="关联日历" prop="calendarId">
  297. <el-select v-model="adjustForm.calendarId" style="width: 100%">
  298. <el-option
  299. v-for="item in enabledCalendars"
  300. :key="item.id"
  301. :label="item.calendarName"
  302. :value="item.id"
  303. />
  304. </el-select>
  305. </el-form-item>
  306. <el-form-item label="调整类型" prop="adjustType">
  307. <el-select v-model="adjustForm.adjustType" style="width: 100%">
  308. <el-option
  309. v-for="item in adjustTypeOptions"
  310. :key="item.value"
  311. :label="item.label"
  312. :value="item.value"
  313. />
  314. </el-select>
  315. </el-form-item>
  316. <el-form-item label="调整日期" prop="adjustDate">
  317. <el-date-picker
  318. v-model="adjustForm.adjustDate"
  319. type="date"
  320. value-format="yyyy-MM-dd"
  321. style="width: 100%"
  322. />
  323. </el-form-item>
  324. <el-form-item label="调整时段" required>
  325. <div class="inline-time">
  326. <el-time-select
  327. v-model="adjustForm.startTime"
  328. placeholder="开始时间"
  329. :picker-options="timePickerOptions"
  330. />
  331. <span>至</span>
  332. <el-time-select
  333. v-model="adjustForm.endTime"
  334. placeholder="结束时间"
  335. :picker-options="getTimePickerOptions(adjustForm.startTime)"
  336. />
  337. </div>
  338. </el-form-item>
  339. <el-form-item label="生效时间" prop="effectiveTime">
  340. <el-date-picker
  341. v-model="adjustForm.effectiveTime"
  342. type="datetime"
  343. value-format="yyyy-MM-dd HH:mm:ss"
  344. style="width: 100%"
  345. />
  346. </el-form-item>
  347. <el-form-item label="失效时间" prop="expireTime">
  348. <el-date-picker
  349. v-model="adjustForm.expireTime"
  350. type="datetime"
  351. value-format="yyyy-MM-dd HH:mm:ss"
  352. style="width: 100%"
  353. />
  354. </el-form-item>
  355. <el-form-item label="调整原因" prop="applyReason">
  356. <el-input
  357. v-model="adjustForm.applyReason"
  358. type="textarea"
  359. :rows="3"
  360. maxlength="500"
  361. show-word-limit
  362. />
  363. </el-form-item>
  364. </el-form>
  365. <div slot="footer">
  366. <el-button type="primary" @click="submitAdjust">提交审批</el-button>
  367. <el-button @click="adjustDialogVisible = false">取消</el-button>
  368. </div>
  369. </ele-modal>
  370. <el-drawer
  371. :visible.sync="dayDrawerVisible"
  372. :title="dayDrawerTitle"
  373. size="460px"
  374. append-to-body
  375. custom-class="calendar-day-drawer"
  376. >
  377. <div class="drawer-body" v-if="currentDay.date">
  378. <div class="drawer-summary">
  379. <div class="summary-date">
  380. <span>{{ drawerDateMonth }}</span>
  381. <strong>{{ drawerDateDay }}</strong>
  382. </div>
  383. <div class="summary-info">
  384. <div class="summary-title">
  385. {{ getDateTypeText(currentDay) }}
  386. <el-tag
  387. size="mini"
  388. :type="currentDay.scheduleStatus ? 'success' : 'info'"
  389. >
  390. {{ currentDay.scheduleStatus ? '已排班' : '未排班' }}
  391. </el-tag>
  392. </div>
  393. <p>{{ getRangeText(currentDay.details) }}</p>
  394. <div class="summary-tags">
  395. <el-tag v-if="currentDay.isRest" size="mini" type="info">
  396. 休息/节假日
  397. </el-tag>
  398. <el-tag v-if="currentDay.isTempAdjust" size="mini">
  399. 临时调整
  400. </el-tag>
  401. <el-tag v-if="currentDay.isConflict" size="mini" type="danger">
  402. 存在冲突
  403. </el-tag>
  404. <el-tag v-if="currentDay.planCount" size="mini" type="warning">
  405. {{ currentDay.planCount }} 个计划
  406. </el-tag>
  407. </div>
  408. </div>
  409. </div>
  410. <div class="drawer-section">
  411. <div class="drawer-title">
  412. <span>时段明细</span>
  413. <em>{{ currentDay.details.length }} 条</em>
  414. </div>
  415. <div class="detail-timeline">
  416. <div
  417. v-for="detail in currentDay.details"
  418. :key="detail.id"
  419. class="detail-item"
  420. >
  421. <div class="detail-time">
  422. <strong>{{ detail.startTime }} - {{ detail.endTime }}</strong>
  423. </div>
  424. <div class="detail-content">
  425. <div>
  426. <strong>{{
  427. detail.dutyName ||
  428. getLabel(dateTypeOptions, detail.dateType)
  429. }}</strong>
  430. <span>{{ detail.startTime }}-{{ detail.endTime }}</span>
  431. </div>
  432. <el-tag
  433. size="mini"
  434. :type="detail.scheduleStatus ? 'success' : 'info'"
  435. >
  436. {{ detail.scheduleStatus ? '已排班' : '未排班' }}
  437. </el-tag>
  438. <div
  439. v-if="
  440. detail.productionLineName ||
  441. detail.workStationName ||
  442. detail.productionShift ||
  443. detail.workingHours ||
  444. detail.restTime
  445. "
  446. class="detail-meta"
  447. >
  448. <span v-if="detail.productionLineName"
  449. >产线:{{ detail.productionLineName }}</span
  450. >
  451. <span v-if="detail.workStationName"
  452. >工位:{{ detail.workStationName }}</span
  453. >
  454. <span v-if="detail.productionShift"
  455. >班次:{{ detail.productionShift }}</span
  456. >
  457. <span v-if="detail.workingHours"
  458. >工时:{{ detail.workingHours }}分钟</span
  459. >
  460. <span v-if="detail.restTime"
  461. >休息:{{ detail.restTime }}分钟</span
  462. >
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. </div>
  468. <div class="drawer-section">
  469. <div class="drawer-title">
  470. <span>{{ relatedSectionTitle }}</span>
  471. <em>{{ relatedPlans.length }} 条</em>
  472. </div>
  473. <div v-if="relatedPlans.length" class="plan-list">
  474. <el-tooltip
  475. v-for="plan in relatedPlans"
  476. :key="plan.id"
  477. placement="top"
  478. effect="light"
  479. >
  480. <div slot="content" class="plan-tooltip">
  481. <template v-if="isTeamQueueDrawer">
  482. <div>排班名称:{{ getRelatedQueueName(plan) }}</div>
  483. <div>人员:{{ getRelatedQueueUserName(plan) || '-' }}</div>
  484. </template>
  485. <template v-else>
  486. <div>
  487. {{ isEquipmentDrawer ? '设备编码' : '计划编号' }}:{{
  488. getRelatedPlanCode(plan)
  489. }}
  490. </div>
  491. <div>
  492. {{ isEquipmentDrawer ? '设备名称' : '计划名称' }}:{{
  493. getRelatedPlanName(plan)
  494. }}
  495. </div>
  496. <div v-if="!isEquipmentDrawer">
  497. 订单编号:{{ plan.orderNo || '-' }}
  498. </div>
  499. <div v-if="!isEquipmentDrawer">
  500. 计划数量:{{ plan.requiredFormingNum || '-' }}
  501. </div>
  502. <div>开始时间:{{ plan.displayStartTime || '-' }}</div>
  503. <div>结束时间:{{ plan.displayEndTime || '-' }}</div>
  504. </template>
  505. </div>
  506. <div
  507. class="plan-item"
  508. :class="{ 'team-queue-item': isTeamQueueDrawer }"
  509. >
  510. <div v-if="isTeamQueueDrawer" class="plan-main team-queue-plan">
  511. <div class="queue-avatar">
  512. <i class="el-icon-user-solid"></i>
  513. </div>
  514. <div class="queue-info">
  515. <div class="plan-name queue-name-only">
  516. <span class="queue-name-label">排班名称:</span>
  517. <span class="queue-name-text">
  518. {{ getRelatedQueueName(plan) }}
  519. </span>
  520. </div>
  521. <div class="queue-user-name">
  522. <i class="el-icon-user"></i>
  523. <span>人员:</span>
  524. <strong>
  525. {{ getRelatedQueueUserName(plan) || '-' }}
  526. </strong>
  527. </div>
  528. </div>
  529. <div class="queue-badge">排班</div>
  530. </div>
  531. <div v-else class="plan-main">
  532. <div class="plan-topline">
  533. <strong>{{ plan.planNo }}</strong>
  534. <span v-if="plan.orderNo">{{ plan.orderNo }}</span>
  535. </div>
  536. <div class="plan-name">{{ plan.taskName }}</div>
  537. <div class="plan-bottomline">
  538. <span class="plan-metrics">
  539. <em
  540. v-if="
  541. plan.requiredFormingNum !== undefined &&
  542. plan.requiredFormingNum !== null
  543. "
  544. >
  545. 数量 {{ plan.requiredFormingNum }}
  546. </em>
  547. <em v-if="plan.displayStartTime || plan.displayEndTime">
  548. {{ plan.displayStartTime || '-' }} -
  549. {{ plan.displayEndTime || '-' }}
  550. </em>
  551. </span>
  552. </div>
  553. </div>
  554. </div>
  555. </el-tooltip>
  556. </div>
  557. <el-empty
  558. v-else
  559. class="drawer-empty"
  560. description="暂无关联计划"
  561. :image-size="92"
  562. />
  563. </div>
  564. </div>
  565. <!-- <div class="drawer-actions" v-if="currentDay.date">
  566. <el-button
  567. size="small"
  568. icon="el-icon-check"
  569. @click="toggleScheduleStatus(1)"
  570. >
  571. 完成排班
  572. </el-button>
  573. <el-button
  574. size="small"
  575. icon="el-icon-refresh-left"
  576. @click="toggleScheduleStatus(0)"
  577. >
  578. 取消排班
  579. </el-button>
  580. <el-button
  581. size="small"
  582. type="primary"
  583. icon="el-icon-plus"
  584. @click="openAdjustDialog(currentDay)"
  585. >
  586. 临时调整
  587. </el-button>
  588. <el-button
  589. size="small"
  590. icon="el-icon-warning-outline"
  591. @click="checkSingleDay(currentDay)"
  592. >
  593. 实时检测
  594. </el-button>
  595. </div> -->
  596. </el-drawer>
  597. <ele-modal
  598. :visible.sync="approveDialogVisible"
  599. :title="approveForm.status === 1 ? '审批通过' : '审批驳回'"
  600. width="520px"
  601. custom-class="ele-dialog-form"
  602. :close-on-click-modal="false"
  603. >
  604. <el-form :model="approveForm" label-width="90px" class="el-form-box">
  605. <el-form-item label="审批意见">
  606. <el-input
  607. v-model="approveForm.opinion"
  608. type="textarea"
  609. :rows="4"
  610. maxlength="300"
  611. show-word-limit
  612. :placeholder="
  613. approveForm.status === 1 ? '请输入审批意见' : '请输入驳回原因'
  614. "
  615. />
  616. </el-form-item>
  617. </el-form>
  618. <div slot="footer">
  619. <el-button type="primary" @click="confirmApproveAdjust">确定</el-button>
  620. <el-button @click="approveDialogVisible = false">取消</el-button>
  621. </div>
  622. </ele-modal>
  623. <ele-modal
  624. :visible.sync="adjustDetailVisible"
  625. title="调整记录追溯"
  626. width="860px"
  627. custom-class="ele-dialog-form adjust-detail-dialog"
  628. >
  629. <div v-if="adjustDetail.id" class="adjust-detail-view">
  630. <div class="adjust-detail-grid">
  631. <div class="adjust-detail-field">
  632. <span>申请单号</span>
  633. <strong>{{ adjustDetail.applyNo || '-' }}</strong>
  634. </div>
  635. <div class="adjust-detail-field">
  636. <span>申请人</span>
  637. <strong>{{ getAdjustApplicantName(adjustDetail) }}</strong>
  638. </div>
  639. <div class="adjust-detail-field">
  640. <span>关联日历</span>
  641. <strong>{{ adjustDetail.calendarName || '-' }}</strong>
  642. </div>
  643. <div class="adjust-detail-field">
  644. <span>调整类型</span>
  645. <strong>{{
  646. getLabel(adjustTypeOptions, adjustDetail.adjustType)
  647. }}</strong>
  648. </div>
  649. <div class="adjust-detail-field">
  650. <span>审批状态</span>
  651. <el-tag
  652. size="mini"
  653. :type="approvalStatusTag(adjustDetail.applyStatus)"
  654. >
  655. {{ getLabel(approvalStatusOptions, adjustDetail.applyStatus) }}
  656. </el-tag>
  657. </div>
  658. <div class="adjust-detail-field">
  659. <span>调整日期</span>
  660. <strong>{{ adjustDetail.adjustDate || '-' }}</strong>
  661. </div>
  662. <div class="adjust-detail-field is-wide">
  663. <span>生效周期</span>
  664. <strong class="adjust-period-value">
  665. {{ adjustDetail.effectiveTime || '-' }} 至
  666. {{ adjustDetail.expireTime || '-' }}
  667. </strong>
  668. </div>
  669. </div>
  670. <div class="adjust-compare">
  671. <div class="adjust-data-card before">
  672. <span>调整前数据</span>
  673. <strong>{{ adjustDetail.oldContent || '-' }}</strong>
  674. </div>
  675. <div class="adjust-data-card after">
  676. <span>调整后数据</span>
  677. <strong>{{
  678. formatSegmentContent(adjustDetail.newContent, adjustDetail) || '-'
  679. }}</strong>
  680. </div>
  681. </div>
  682. <div class="adjust-detail-section">
  683. <div class="section-title">调整原因</div>
  684. <p>{{ adjustDetail.applyReason || '-' }}</p>
  685. </div>
  686. <div class="adjust-detail-section">
  687. <div class="section-title">审批节点</div>
  688. <div
  689. v-if="formatApproveNodes(adjustDetail.approveNode).length"
  690. class="approve-timeline"
  691. >
  692. <div
  693. v-for="(node, index) in formatApproveNodes(
  694. adjustDetail.approveNode
  695. )"
  696. :key="index"
  697. class="approve-node"
  698. >
  699. <div class="node-dot"></div>
  700. <div class="node-content">
  701. <div>
  702. <strong>{{ node.statusText }}</strong>
  703. <span>{{ node.approveTime || '-' }}</span>
  704. </div>
  705. <p>{{ node.approveOpinion || '-' }}</p>
  706. <em v-if="node.approveUserId"
  707. >审批人ID:{{ node.approveUserId }}</em
  708. >
  709. </div>
  710. </div>
  711. </div>
  712. <div v-else class="approve-empty">暂无审批记录</div>
  713. </div>
  714. </div>
  715. <div slot="footer">
  716. <!-- <el-button icon="el-icon-download" @click="exportAdjustRecord">
  717. 导出追溯
  718. </el-button> -->
  719. <el-button @click="adjustDetailVisible = false">关闭</el-button>
  720. </div>
  721. </ele-modal>
  722. </div>
  723. </template>
  724. <script>
  725. import dayjs from 'dayjs';
  726. import { solarToLunar } from 'lunar-calendar';
  727. import BaseManage from './components/BaseManage.vue';
  728. import MultiCalendarView from './components/MultiCalendarView.vue';
  729. import ConflictPanel from './components/ConflictPanel.vue';
  730. import AdjustApproval from './components/AdjustApproval.vue';
  731. import StatisticsView from './components/StatisticsView.vue';
  732. import {
  733. addCalendar,
  734. editCalendar,
  735. deleteCalendar,
  736. updateCalendarStatus,
  737. pageCalendar,
  738. copyCalendar,
  739. viewCalendar,
  740. saveCalendarDetail,
  741. checkCalendarConflict,
  742. checkAllCalendarConflict,
  743. applyCalendarAdjust,
  744. approveCalendarAdjust,
  745. pageCalendarAdjust,
  746. refreshCalendarPlan,
  747. getPublicLegalHolidays
  748. } from '@/api/productionScheduling/factoryCalendar';
  749. const currentYear = Number(dayjs().format('YYYY'));
  750. const calendarTypeOptions = [
  751. { label: '标准生产日历', value: 1 },
  752. { label: '设备维护日历', value: 2 },
  753. { label: '人员排班日历', value: 3 }
  754. ];
  755. const dateTypeOptions = [
  756. { label: '正常工作日', value: 1 },
  757. { label: '休息日', value: 2 },
  758. { label: '节假日', value: 3 },
  759. { label: '设备检修期', value: 4 },
  760. { label: '临时调整时段', value: 5 }
  761. ];
  762. const adjustTypeOptions = [
  763. { label: '临时加班', value: 1 },
  764. { label: '临时停工', value: 2 },
  765. { label: '临时设备检修', value: 3 },
  766. { label: '临时人员调班', value: 4 }
  767. ];
  768. const approvalStatusOptions = [
  769. { label: '待审批', value: 0 },
  770. { label: '审批通过', value: 1 },
  771. { label: '审批驳回', value: 2 },
  772. { label: '已失效', value: 3 }
  773. ];
  774. const legalHolidayKeywords = [
  775. { keyword: '元旦节', label: '元旦节' },
  776. { keyword: '春节', label: '春节' },
  777. { keyword: '清明', label: '清明节' },
  778. { keyword: '劳动节', label: '劳动节' },
  779. { keyword: '端午节', label: '端午节' },
  780. { keyword: '中秋节', label: '中秋节' },
  781. { keyword: '国庆节', label: '国庆节' }
  782. ];
  783. const officialHolidayRanges = {
  784. 2026: [
  785. { name: '元旦节', start: '2026-01-01', end: '2026-01-03' },
  786. { name: '春节', start: '2026-02-15', end: '2026-02-23' },
  787. { name: '清明节', start: '2026-04-04', end: '2026-04-06' },
  788. { name: '劳动节', start: '2026-05-01', end: '2026-05-05' },
  789. { name: '端午节', start: '2026-06-19', end: '2026-06-21' },
  790. { name: '中秋节', start: '2026-09-25', end: '2026-09-27' },
  791. { name: '国庆节', start: '2026-10-01', end: '2026-10-07' }
  792. ]
  793. };
  794. const inferredHolidayRules = {
  795. 元旦节: { before: 0, after: 2 },
  796. 春节: { before: 2, after: 6 },
  797. 清明节: { before: 1, after: 1 },
  798. 劳动节: { before: 0, after: 4 },
  799. 端午节: { before: 0, after: 2 },
  800. 中秋节: { before: 0, after: 2 },
  801. 国庆节: { before: 0, after: 6 }
  802. };
  803. const inferredHolidayRangeCache = {};
  804. function clone(data) {
  805. return JSON.parse(JSON.stringify(data));
  806. }
  807. function createCode(prefix) {
  808. return `${prefix}${dayjs().format('YYYYMMDD')}${String(
  809. Math.floor(Math.random() * 1000000)
  810. ).padStart(6, '0')}`;
  811. }
  812. function toMinute(time) {
  813. const [hour, minute] = time.split(':').map(Number);
  814. return hour * 60 + minute;
  815. }
  816. function isOverlap(aStart, aEnd, bStart, bEnd) {
  817. return (
  818. toMinute(aStart) < toMinute(bEnd) && toMinute(bStart) < toMinute(aEnd)
  819. );
  820. }
  821. function createHolidayRule(dateType = 3, calendarDate = '', remark = '') {
  822. return {
  823. id: Date.now() + Math.random(),
  824. calendarDate,
  825. dateType,
  826. remark
  827. };
  828. }
  829. function normalizePage(data) {
  830. const list =
  831. data?.records || data?.list || data?.rows || data?.data || data || [];
  832. const count =
  833. data?.total || data?.count || data?.totalCount || list.length || 0;
  834. return {
  835. list: Array.isArray(list) ? list : [],
  836. count
  837. };
  838. }
  839. function normalizeYearList(applyYear) {
  840. if (Array.isArray(applyYear)) {
  841. return applyYear.map(Number).filter(Boolean);
  842. }
  843. return String(applyYear || '')
  844. .split(',')
  845. .map((item) => Number(item.trim()))
  846. .filter(Boolean);
  847. }
  848. export default {
  849. name: 'FactoryCalendar',
  850. components: {
  851. BaseManage,
  852. MultiCalendarView,
  853. ConflictPanel,
  854. AdjustApproval,
  855. StatisticsView
  856. },
  857. data() {
  858. return {
  859. activeTab: 'base',
  860. query: {
  861. year: currentYear,
  862. month: '',
  863. calendarType: '',
  864. status: ''
  865. },
  866. viewQuery: {
  867. calendarType: 1,
  868. year: currentYear,
  869. month: Number(dayjs().format('M')),
  870. date: dayjs().format('YYYY-MM-DD'),
  871. viewType: 'month',
  872. statusFilter: ''
  873. },
  874. viewMonth: dayjs().format('YYYY-MM'),
  875. calendars: [],
  876. details: [],
  877. adjusts: [],
  878. plans: [],
  879. conflictList: [],
  880. filteredCalendars: [],
  881. legalHolidayLoading: false,
  882. legalHolidayCache: {},
  883. legalHolidayStatusMap: {},
  884. legalHolidayPromises: {},
  885. conflictCheckPromise: null,
  886. conflictDataKey: '',
  887. viewDataKey: '',
  888. viewDataPromise: null,
  889. planDataKey: '',
  890. planDataPromise: null,
  891. pageSize: this.$store.state.tablePageSize,
  892. tableResponse: {
  893. dataName: 'list',
  894. countName: 'count'
  895. },
  896. calendarDialogVisible: false,
  897. copyDialogVisible: false,
  898. adjustDialogVisible: false,
  899. dayDrawerVisible: false,
  900. approveDialogVisible: false,
  901. adjustDetailVisible: false,
  902. calendarForm: this.getDefaultCalendarForm(),
  903. copyForm: {
  904. sourceId: '',
  905. targetYear: currentYear + 1
  906. },
  907. adjustForm: this.getDefaultAdjustForm(),
  908. approveForm: {
  909. row: null,
  910. status: 1,
  911. opinion: ''
  912. },
  913. adjustDetail: {},
  914. currentDay: {},
  915. calendarTypeOptions,
  916. dateTypeOptions,
  917. adjustTypeOptions,
  918. approvalStatusOptions,
  919. yearOptions: [
  920. currentYear - 1,
  921. currentYear,
  922. currentYear + 1,
  923. currentYear + 2
  924. ],
  925. monthOptions: Array.from({ length: 12 }, (_, index) => ({
  926. label: `${index + 1}月`,
  927. value: index + 1
  928. })),
  929. weekNames: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
  930. restModeOptions: [
  931. { label: '双休', value: 'double' },
  932. { label: '单休', value: 'single' },
  933. { label: '大小周', value: 'alternate' },
  934. { label: '无休', value: 'none' },
  935. { label: '自定义', value: 'custom' }
  936. ],
  937. restWeekdayOptions: [
  938. { label: '周一', value: 1 },
  939. { label: '周二', value: 2 },
  940. { label: '周三', value: 3 },
  941. { label: '周四', value: 4 },
  942. { label: '周五', value: 5 },
  943. { label: '周六', value: 6 },
  944. { label: '周日', value: 0 }
  945. ],
  946. timePickerOptions: {
  947. start: '00:00',
  948. step: '00:30',
  949. end: '23:30'
  950. },
  951. statusFilterOptions: [
  952. { label: '已排班/已排产', value: 'scheduled' },
  953. { label: '未排班/空闲', value: 'idle' },
  954. { label: '休息日/节假日', value: 'rest' },
  955. { label: '设备检修期', value: 'maintenance' },
  956. { label: '临时调整时段', value: 'temp' },
  957. { label: '时间冲突时段', value: 'conflict' }
  958. ],
  959. legendList: [
  960. {
  961. key: 'scheduled',
  962. label: '已排班/已排产',
  963. desc: '已完成生产、维保或人员排班'
  964. },
  965. {
  966. key: 'idle',
  967. label: '未排班/空闲',
  968. desc: '有效工作时间,暂无计划'
  969. },
  970. {
  971. key: 'rest',
  972. label: '休息日/节假日',
  973. desc: '不支持排产排班'
  974. },
  975. {
  976. key: 'holiday',
  977. label: '法定节假日',
  978. desc: '未配置为上班时默认休息'
  979. },
  980. {
  981. key: 'maintenance',
  982. label: '设备检修期',
  983. desc: '设备不可用时段'
  984. },
  985. {
  986. key: 'temp',
  987. label: '临时调整时段',
  988. desc: '审批通过后的加班、停工、调班'
  989. },
  990. {
  991. key: 'conflict',
  992. label: '时间冲突时段',
  993. desc: '存在多维度时间冲突'
  994. }
  995. ],
  996. calendarColumns: [
  997. {
  998. width: 60,
  999. label: '序号',
  1000. type: 'index',
  1001. columnKey: 'index',
  1002. align: 'center'
  1003. },
  1004. {
  1005. minWidth: 170,
  1006. prop: 'calendarCode',
  1007. label: '日历编码',
  1008. align: 'center',
  1009. showOverflowTooltip: true
  1010. },
  1011. {
  1012. minWidth: 180,
  1013. prop: 'calendarName',
  1014. label: '日历名称',
  1015. align: 'center',
  1016. showOverflowTooltip: true
  1017. },
  1018. {
  1019. minWidth: 140,
  1020. prop: 'calendarType',
  1021. label: '日历类型',
  1022. align: 'center',
  1023. slot: 'calendarType'
  1024. },
  1025. {
  1026. minWidth: 130,
  1027. prop: 'applyYear',
  1028. label: '适用年份',
  1029. align: 'center',
  1030. formatter: (row) => row.applyYear.join(',')
  1031. },
  1032. // {
  1033. // minWidth: 240,
  1034. // prop: 'timeRanges',
  1035. // label: '每日时段配置',
  1036. // align: 'center',
  1037. // slot: 'timeRanges'
  1038. // },
  1039. // {
  1040. // minWidth: 150,
  1041. // prop: 'productionLineName',
  1042. // label: '产线名称',
  1043. // align: 'center',
  1044. // showOverflowTooltip: true
  1045. // },
  1046. // {
  1047. // minWidth: 140,
  1048. // prop: 'workStationName',
  1049. // label: '工位名称',
  1050. // align: 'center',
  1051. // showOverflowTooltip: true
  1052. // },
  1053. {
  1054. minWidth: 150,
  1055. prop: 'status',
  1056. label: '状态',
  1057. align: 'center',
  1058. slot: 'status'
  1059. },
  1060. {
  1061. minWidth: 130,
  1062. prop: 'createUserName',
  1063. label: '创建人',
  1064. align: 'center'
  1065. },
  1066. {
  1067. minWidth: 170,
  1068. prop: 'updateTime',
  1069. label: '创建时间',
  1070. align: 'center'
  1071. },
  1072. {
  1073. columnKey: 'action',
  1074. label: '操作',
  1075. width: 230,
  1076. align: 'center',
  1077. slot: 'action',
  1078. fixed: 'right'
  1079. }
  1080. ],
  1081. conflictColumns: [
  1082. { width: 60, label: '序号', type: 'index', align: 'center' },
  1083. {
  1084. minWidth: 130,
  1085. prop: 'calendarDate',
  1086. label: '冲突日期',
  1087. align: 'center'
  1088. },
  1089. {
  1090. minWidth: 150,
  1091. prop: 'timeRange',
  1092. label: '冲突时段',
  1093. align: 'center'
  1094. },
  1095. { minWidth: 180, prop: 'scene', label: '冲突场景', align: 'center' },
  1096. {
  1097. minWidth: 120,
  1098. prop: 'level',
  1099. label: '等级',
  1100. align: 'center',
  1101. slot: 'level'
  1102. },
  1103. {
  1104. minWidth: 300,
  1105. prop: 'message',
  1106. label: '提示信息',
  1107. align: 'center',
  1108. showOverflowTooltip: true
  1109. },
  1110. {
  1111. columnKey: 'action',
  1112. label: '操作',
  1113. width: 120,
  1114. align: 'center',
  1115. slot: 'action',
  1116. fixed: 'right'
  1117. }
  1118. ],
  1119. adjustColumns: [
  1120. { width: 60, label: '序号', type: 'index', align: 'center' },
  1121. {
  1122. minWidth: 160,
  1123. prop: 'applyNo',
  1124. label: '申请单号',
  1125. align: 'center'
  1126. },
  1127. {
  1128. minWidth: 180,
  1129. prop: 'calendarName',
  1130. label: '关联日历',
  1131. align: 'center',
  1132. showOverflowTooltip: true
  1133. },
  1134. {
  1135. minWidth: 130,
  1136. prop: 'adjustType',
  1137. label: '调整类型',
  1138. align: 'center',
  1139. slot: 'adjustType'
  1140. },
  1141. {
  1142. minWidth: 120,
  1143. prop: 'adjustDate',
  1144. label: '调整日期',
  1145. align: 'center'
  1146. },
  1147. {
  1148. minWidth: 220,
  1149. prop: 'newContent',
  1150. label: '调整后数据',
  1151. align: 'center',
  1152. slot: 'newContent',
  1153. showOverflowTooltip: true
  1154. },
  1155. {
  1156. minWidth: 130,
  1157. prop: 'applyStatus',
  1158. label: '审批状态',
  1159. align: 'center',
  1160. slot: 'applyStatus'
  1161. },
  1162. {
  1163. minWidth: 130,
  1164. prop: 'applyUserName',
  1165. label: '申请人',
  1166. align: 'center'
  1167. },
  1168. {
  1169. minWidth: 170,
  1170. prop: 'applyTime',
  1171. label: '申请时间',
  1172. align: 'center'
  1173. },
  1174. {
  1175. columnKey: 'action',
  1176. label: '操作',
  1177. width: 120,
  1178. align: 'center',
  1179. slot: 'action',
  1180. fixed: 'right'
  1181. }
  1182. ],
  1183. planColumns: [
  1184. { width: 60, label: '序号', type: 'index', align: 'center' },
  1185. { minWidth: 150, prop: 'planNo', label: '计划编号', align: 'center' },
  1186. {
  1187. minWidth: 150,
  1188. prop: 'orderNo',
  1189. label: '订单编号',
  1190. align: 'center'
  1191. },
  1192. {
  1193. minWidth: 180,
  1194. prop: 'taskName',
  1195. label: '任务名称',
  1196. align: 'center',
  1197. showOverflowTooltip: true
  1198. },
  1199. {
  1200. minWidth: 130,
  1201. prop: 'calendarDate',
  1202. label: '计划日期',
  1203. align: 'center'
  1204. },
  1205. {
  1206. minWidth: 150,
  1207. prop: 'timeRange',
  1208. label: '计划时段',
  1209. align: 'center',
  1210. formatter: (row) =>
  1211. row.timeRange || `${row.startTime}-${row.endTime}`
  1212. },
  1213. {
  1214. minWidth: 150,
  1215. prop: 'productionLineName',
  1216. label: '产线名称',
  1217. align: 'center',
  1218. showOverflowTooltip: true
  1219. },
  1220. {
  1221. minWidth: 140,
  1222. prop: 'workStationName',
  1223. label: '工位名称',
  1224. align: 'center',
  1225. showOverflowTooltip: true
  1226. },
  1227. {
  1228. minWidth: 120,
  1229. prop: 'workingHours',
  1230. label: '工时(分钟)',
  1231. align: 'center'
  1232. },
  1233. {
  1234. minWidth: 120,
  1235. prop: 'restTime',
  1236. label: '休息(分钟)',
  1237. align: 'center'
  1238. },
  1239. {
  1240. minWidth: 110,
  1241. prop: 'status',
  1242. label: '状态',
  1243. align: 'center',
  1244. slot: 'status'
  1245. },
  1246. {
  1247. columnKey: 'action',
  1248. label: '操作',
  1249. width: 120,
  1250. align: 'center',
  1251. slot: 'action',
  1252. fixed: 'right'
  1253. }
  1254. ],
  1255. calendarRules: {
  1256. calendarName: [
  1257. { required: true, message: '请输入日历名称', trigger: 'blur' }
  1258. ],
  1259. calendarType: [
  1260. { required: true, message: '请选择日历类型', trigger: 'change' }
  1261. ],
  1262. applyYear: [
  1263. { required: true, message: '请选择适用年份', trigger: 'change' }
  1264. ],
  1265. applyMonth: [
  1266. { required: true, message: '请选择适用月份', trigger: 'change' }
  1267. ]
  1268. },
  1269. adjustRules: {
  1270. calendarId: [
  1271. { required: true, message: '请选择关联日历', trigger: 'change' }
  1272. ],
  1273. adjustType: [
  1274. { required: true, message: '请选择调整类型', trigger: 'change' }
  1275. ],
  1276. adjustDate: [
  1277. { required: true, message: '请选择调整日期', trigger: 'change' }
  1278. ],
  1279. effectiveTime: [
  1280. { required: true, message: '请选择生效时间', trigger: 'change' }
  1281. ],
  1282. expireTime: [
  1283. { required: true, message: '请选择失效时间', trigger: 'change' }
  1284. ],
  1285. applyReason: [
  1286. { required: true, message: '请输入调整原因', trigger: 'blur' }
  1287. ]
  1288. }
  1289. };
  1290. },
  1291. computed: {
  1292. vm() {
  1293. return this;
  1294. },
  1295. enabledCalendars() {
  1296. return this.calendars.filter((item) => item.status === 1);
  1297. },
  1298. calendarDialogTitle() {
  1299. return this.calendarForm.id ? '编辑日历' : '新增日历';
  1300. },
  1301. calendarFormApplyYear: {
  1302. get() {
  1303. return normalizeYearList(this.calendarForm.applyYear)[0] || '';
  1304. },
  1305. set(value) {
  1306. this.calendarForm.applyYear = value ? [Number(value)] : [];
  1307. this.calendarForm.legalHolidayWorkDates = [];
  1308. this.loadLegalHolidaysByYears(this.calendarForm.applyYear);
  1309. }
  1310. },
  1311. showWeekHeader() {
  1312. return ['week', 'month'].includes(this.viewQuery.viewType);
  1313. },
  1314. calendarViewTitle() {
  1315. const date = dayjs(this.viewQuery.date);
  1316. if (this.viewQuery.viewType === 'day') {
  1317. return `${date.format('YYYY年MM月DD日')} 日视图`;
  1318. }
  1319. if (this.viewQuery.viewType === 'week') {
  1320. const start = date.startOf('week').add(1, 'day');
  1321. const end = start.add(6, 'day');
  1322. return `${start.format('YYYY年MM月DD日')} - ${end.format(
  1323. 'MM月DD日'
  1324. )} 周视图`;
  1325. }
  1326. if (this.viewQuery.viewType === 'quarter') {
  1327. const startMonth = Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
  1328. const endMonth = startMonth + 2;
  1329. return `${this.viewQuery.year}年${startMonth}月 - ${endMonth}月 季视图`;
  1330. }
  1331. return `${this.viewQuery.year}年${this.viewQuery.month}月 月视图`;
  1332. },
  1333. calendarDateRange() {
  1334. const current = dayjs(this.viewQuery.date);
  1335. if (this.viewQuery.viewType === 'day') {
  1336. return [current];
  1337. }
  1338. if (this.viewQuery.viewType === 'week') {
  1339. const start = current.startOf('week').add(1, 'day');
  1340. return Array.from({ length: 7 }).map((_, index) =>
  1341. start.add(index, 'day')
  1342. );
  1343. }
  1344. if (this.viewQuery.viewType === 'quarter') {
  1345. const startMonth = Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
  1346. const start = dayjs(
  1347. `${this.viewQuery.year}-${String(startMonth).padStart(2, '0')}-01`
  1348. );
  1349. const dates = [];
  1350. for (let i = 0; i < 3; i += 1) {
  1351. const month = start.add(i, 'month');
  1352. for (let day = 1; day <= month.daysInMonth(); day += 1) {
  1353. dates.push(month.date(day));
  1354. }
  1355. }
  1356. return dates;
  1357. }
  1358. const firstDay = dayjs(
  1359. `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
  1360. 2,
  1361. '0'
  1362. )}-01`
  1363. );
  1364. const dates = [];
  1365. for (let day = 1; day <= firstDay.daysInMonth(); day += 1) {
  1366. dates.push(firstDay.date(day));
  1367. }
  1368. return dates;
  1369. },
  1370. calendarCells() {
  1371. return this.buildCalendarCells(true);
  1372. },
  1373. statCells() {
  1374. const dates = this.getMonthDateRange();
  1375. return this.buildCalendarCellsByDates(dates, false, 'month').filter(
  1376. (item) => item.date
  1377. );
  1378. },
  1379. monthStats() {
  1380. const details = this.getStatMonthDetails();
  1381. const countDistinctDates = (predicate) =>
  1382. new Set(
  1383. details
  1384. .filter(predicate)
  1385. .map((item) => item.calendarDate)
  1386. .filter(Boolean)
  1387. ).size;
  1388. const workDays = countDistinctDates(
  1389. (item) => Number(item.dateType) === 1
  1390. );
  1391. const scheduledDateSet = new Set(
  1392. details
  1393. .filter((item) => this.isDetailScheduled(item))
  1394. .map((item) => item.calendarDate)
  1395. .filter(Boolean)
  1396. );
  1397. const scheduledDays = scheduledDateSet.size;
  1398. const scheduledWorkdaySet = new Set(
  1399. details
  1400. .filter(
  1401. (item) =>
  1402. Number(item.dateType) === 1 &&
  1403. scheduledDateSet.has(item.calendarDate)
  1404. )
  1405. .map((item) => item.calendarDate)
  1406. );
  1407. const emptyWorkdaySet = new Set(
  1408. details
  1409. .filter(
  1410. (item) =>
  1411. Number(item.dateType) === 1 &&
  1412. !scheduledDateSet.has(item.calendarDate)
  1413. )
  1414. .map((item) => item.calendarDate)
  1415. );
  1416. const idleDays = Math.max(
  1417. emptyWorkdaySet.size,
  1418. workDays - scheduledWorkdaySet.size,
  1419. 0
  1420. );
  1421. const conflictDays = countDistinctDates(
  1422. (item) => Number(item.isConflict) === 1
  1423. );
  1424. const tempDays = countDistinctDates(
  1425. (item) => Number(item.isTempAdjust) === 1
  1426. );
  1427. const restDays = countDistinctDates((item) =>
  1428. [2, 3].includes(Number(item.dateType))
  1429. );
  1430. return [
  1431. { key: 'work', label: '总工作日', value: workDays },
  1432. { key: 'scheduled', label: '已排班天数', value: scheduledDays },
  1433. {
  1434. key: 'idle',
  1435. label: '未排班天数',
  1436. value: idleDays
  1437. },
  1438. { key: 'conflict', label: '冲突天数', value: conflictDays },
  1439. { key: 'temp', label: '临时调整天数', value: tempDays },
  1440. { key: 'rest', label: '休息/节假日', value: restDays }
  1441. ];
  1442. },
  1443. scheduledWorkdayDays() {
  1444. return new Set(
  1445. this.getStatMonthDetails()
  1446. .filter(
  1447. (item) =>
  1448. Number(item.dateType) === 1 && this.isDetailScheduled(item)
  1449. )
  1450. .map((item) => item.calendarDate)
  1451. .filter(Boolean)
  1452. ).size;
  1453. },
  1454. dayDrawerTitle() {
  1455. return this.currentDay.date
  1456. ? `${this.currentDay.date} 日历详情`
  1457. : '日历详情';
  1458. },
  1459. drawerDateMonth() {
  1460. return this.currentDay.date
  1461. ? dayjs(this.currentDay.date).format('YYYY-MM')
  1462. : '';
  1463. },
  1464. drawerDateDay() {
  1465. return this.currentDay.date
  1466. ? dayjs(this.currentDay.date).format('DD')
  1467. : '';
  1468. },
  1469. relatedPlans() {
  1470. return this.currentDay.relatedPlans || [];
  1471. },
  1472. relatedSectionTitle() {
  1473. return this.isTeamQueueDrawer ? '排班详情' : '关联计划';
  1474. },
  1475. isEquipmentDrawer() {
  1476. return (
  1477. Number(this.viewQuery.calendarType) === 2 ||
  1478. (this.currentDay.details || []).some(
  1479. (item) => Number(item.calendarType) === 2
  1480. )
  1481. );
  1482. },
  1483. isTeamQueueDrawer() {
  1484. return (
  1485. Number(this.viewQuery.calendarType) === 3 ||
  1486. (this.currentDay.details || []).some(
  1487. (item) => Number(item.calendarType) === 3
  1488. )
  1489. );
  1490. },
  1491. legalHolidayOptions() {
  1492. return this.getLegalHolidayOptions(
  1493. this.calendarForm.applyYear,
  1494. this.calendarForm.applyMonth
  1495. );
  1496. },
  1497. legalHolidayGroups() {
  1498. const groupMap = {};
  1499. this.legalHolidayOptions.forEach((item) => {
  1500. if (!groupMap[item.name]) {
  1501. groupMap[item.name] = {
  1502. name: item.name,
  1503. dates: []
  1504. };
  1505. }
  1506. groupMap[item.name].dates.push(item);
  1507. });
  1508. return Object.values(groupMap);
  1509. },
  1510. showLegalHolidaySection() {
  1511. return this.legalHolidayLoading || this.legalHolidayGroups.length > 0;
  1512. },
  1513. legalHolidayVisibleWorkCount() {
  1514. const visibleDates = new Set(
  1515. this.legalHolidayOptions.map((item) => item.calendarDate)
  1516. );
  1517. return (this.calendarForm.legalHolidayWorkDates || []).filter((date) =>
  1518. visibleDates.has(date)
  1519. ).length;
  1520. },
  1521. legalHolidaySourceText() {
  1522. const year = normalizeYearList(this.calendarForm.applyYear)[0];
  1523. if (this.legalHolidayStatusMap[year] === 'remote') {
  1524. return '已同步公共节假日';
  1525. }
  1526. if (this.legalHolidayStatusMap[year] === 'fallback') {
  1527. return '使用本地节假日';
  1528. }
  1529. return '使用本地节假日';
  1530. },
  1531. visualKpis() {
  1532. const workDays =
  1533. this.monthStats.find((item) => item.key === 'work')?.value || 0;
  1534. const scheduledDays =
  1535. this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
  1536. const tempDays =
  1537. this.monthStats.find((item) => item.key === 'temp')?.value || 0;
  1538. const conflictDays =
  1539. this.monthStats.find((item) => item.key === 'conflict')?.value || 0;
  1540. return [
  1541. {
  1542. key: 'work',
  1543. label: '总工作日',
  1544. value: workDays,
  1545. desc: `${this.viewQuery.year}年${this.viewQuery.month}月`,
  1546. icon: 'el-icon-date'
  1547. },
  1548. {
  1549. key: 'scheduled',
  1550. label: '已排班',
  1551. value: scheduledDays,
  1552. desc: `完成率 ${this.scheduleRate}%`,
  1553. icon: 'el-icon-finished'
  1554. },
  1555. {
  1556. key: 'conflict',
  1557. label: '冲突天数',
  1558. value: conflictDays,
  1559. desc: '当前月需确认修正',
  1560. icon: 'el-icon-warning-outline'
  1561. },
  1562. {
  1563. key: 'temp',
  1564. label: '临时调整',
  1565. value: tempDays,
  1566. desc: '审批通过后生效',
  1567. icon: 'el-icon-refresh'
  1568. }
  1569. ];
  1570. },
  1571. scheduleRate() {
  1572. const workDays =
  1573. this.monthStats.find((item) => item.key === 'work')?.value || 0;
  1574. return workDays
  1575. ? Math.min(
  1576. 100,
  1577. Math.round((this.scheduledWorkdayDays / workDays) * 100)
  1578. )
  1579. : 0;
  1580. },
  1581. scheduleSegments() {
  1582. const scheduled =
  1583. this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
  1584. const idle =
  1585. this.monthStats.find((item) => item.key === 'idle')?.value || 0;
  1586. const rest =
  1587. this.monthStats.find((item) => item.key === 'rest')?.value || 0;
  1588. return [
  1589. {
  1590. key: 'scheduled',
  1591. label: '已排班',
  1592. value: scheduled,
  1593. color: '#409eff'
  1594. },
  1595. { key: 'idle', label: '未排班', value: idle, color: '#67c23a' },
  1596. { key: 'rest', label: '休息/节假日', value: rest, color: '#c0c4cc' }
  1597. ];
  1598. },
  1599. scheduleDonutBackground() {
  1600. const total = this.scheduleSegments.reduce(
  1601. (sum, item) => sum + item.value,
  1602. 0
  1603. );
  1604. if (!total) {
  1605. return '#ebeef5';
  1606. }
  1607. let start = 0;
  1608. const segments = this.scheduleSegments.map((item) => {
  1609. const end = start + (item.value / total) * 100;
  1610. const value = `${item.color} ${start}% ${end}%`;
  1611. start = end;
  1612. return value;
  1613. });
  1614. return `conic-gradient(${segments.join(', ')})`;
  1615. },
  1616. scheduleTooltip() {
  1617. return `已排班率 ${this.scheduleRate}%,已排班 ${this.scheduleSegments[0].value} 天,未排班 ${this.scheduleSegments[1].value} 天,休息/节假日 ${this.scheduleSegments[2].value} 天`;
  1618. },
  1619. visualMonthBars() {
  1620. const colorMap = {
  1621. work: '#409eff',
  1622. scheduled: '#36cfc9',
  1623. idle: '#67c23a',
  1624. conflict: '#f56c6c',
  1625. temp: '#9254de',
  1626. rest: '#909399'
  1627. };
  1628. const max = Math.max(...this.monthStats.map((item) => item.value), 1);
  1629. return this.monthStats.map((item) => ({
  1630. ...item,
  1631. color: colorMap[item.key],
  1632. percent: Math.round((item.value / max) * 100)
  1633. }));
  1634. },
  1635. calendarTypeStats() {
  1636. const colorMap = {
  1637. 1: '#409eff',
  1638. 2: '#e6a23c',
  1639. 3: '#67c23a'
  1640. };
  1641. return this.calendarTypeOptions.map((item) => {
  1642. const list = this.calendars.filter(
  1643. (calendar) => calendar.calendarType === item.value
  1644. );
  1645. return {
  1646. ...item,
  1647. color: colorMap[item.value],
  1648. count: list.length,
  1649. enabled: list.filter((calendar) => calendar.status === 1).length,
  1650. disabled: list.filter((calendar) => calendar.status === 0).length
  1651. };
  1652. });
  1653. },
  1654. conflictTrend() {
  1655. const days = Array.from({ length: 6 }).map((_, index) => {
  1656. const date = dayjs(
  1657. `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
  1658. 2,
  1659. '0'
  1660. )}-01`
  1661. ).date(index * 5 + 1);
  1662. const count = new Set(
  1663. this.getStatMonthDetails()
  1664. .filter(
  1665. (item) =>
  1666. Number(item.isConflict) === 1 &&
  1667. dayjs(item.calendarDate).isSame(date, 'week')
  1668. )
  1669. .map((item) => item.calendarDate)
  1670. ).size;
  1671. return {
  1672. label: date.format('MM/DD'),
  1673. count
  1674. };
  1675. });
  1676. const max = Math.max(...days.map((item) => item.count), 1);
  1677. return days.map((item) => ({
  1678. ...item,
  1679. height: Math.max(12, Math.round((item.count / max) * 100))
  1680. }));
  1681. }
  1682. },
  1683. created() {
  1684. this.loadRemoteCalendarData();
  1685. },
  1686. methods: {
  1687. getDefaultCalendarForm() {
  1688. return {
  1689. id: '',
  1690. calendarCode: createCode('CLD'),
  1691. calendarName: '',
  1692. calendarType: 1,
  1693. applyYear: [currentYear],
  1694. applyMonth: Array.from({ length: 12 }, (_, index) => index + 1),
  1695. status: 0,
  1696. shiftId: '',
  1697. shiftName: '',
  1698. shiftCode: '',
  1699. timeRanges: [],
  1700. restMode: 'double',
  1701. restWeekdays: [6, 0],
  1702. legalHolidayWorkDates: [],
  1703. holidayRules: [],
  1704. remark: ''
  1705. };
  1706. },
  1707. getDefaultAdjustForm(day = {}) {
  1708. const enabledCalendars = (this.calendars || []).filter(
  1709. (item) => item.status === 1
  1710. );
  1711. return {
  1712. calendarId: enabledCalendars[0]?.id || '',
  1713. adjustType: 1,
  1714. adjustDate: day.date || dayjs().format('YYYY-MM-DD'),
  1715. startTime: '08:00',
  1716. endTime: '17:00',
  1717. effectiveTime: `${day.date || dayjs().format('YYYY-MM-DD')} 08:00:00`,
  1718. expireTime: `${day.date || dayjs().format('YYYY-MM-DD')} 17:00:00`,
  1719. applyReason: ''
  1720. };
  1721. },
  1722. buildCalendarCells(applyFilter = true) {
  1723. return this.buildCalendarCellsByDates(
  1724. this.calendarDateRange,
  1725. applyFilter,
  1726. this.viewQuery.viewType
  1727. );
  1728. },
  1729. getMonthDateRange(
  1730. year = this.viewQuery.year,
  1731. month = this.viewQuery.month
  1732. ) {
  1733. const firstDay = dayjs(`${year}-${String(month).padStart(2, '0')}-01`);
  1734. const dates = [];
  1735. for (let day = 1; day <= firstDay.daysInMonth(); day += 1) {
  1736. dates.push(firstDay.date(day));
  1737. }
  1738. return dates;
  1739. },
  1740. buildCalendarCellsByDates(
  1741. dates,
  1742. applyFilter = true,
  1743. viewType = this.viewQuery.viewType
  1744. ) {
  1745. const startOffset =
  1746. viewType === 'month' && dates.length ? (dates[0].day() + 6) % 7 : 0;
  1747. const cells = [];
  1748. for (let i = 0; i < startOffset; i += 1) {
  1749. cells.push({ key: `empty-${i}`, date: '', dayText: '', details: [] });
  1750. }
  1751. dates.forEach((dateItem) => {
  1752. const date = dateItem.format('YYYY-MM-DD');
  1753. const details = this.getViewDetails(date);
  1754. const planCount = this.getDetailPlanCount(details);
  1755. const disabled = !this.hasEnabledCalendarForView();
  1756. const holidayName = this.getLegalHolidayName(dateItem);
  1757. const hasWorkDetail = details.some(
  1758. (item) => Number(item.dateType || 1) === 1
  1759. );
  1760. const cell = {
  1761. key: date,
  1762. date,
  1763. dayText:
  1764. viewType === 'quarter'
  1765. ? dateItem.format('M/D')
  1766. : dateItem.format('D'),
  1767. details,
  1768. planCount: disabled ? 0 : planCount,
  1769. scheduleStatus:
  1770. !disabled &&
  1771. (details.some((item) => item.scheduleStatus === 1) ||
  1772. planCount > 0),
  1773. isConflict:
  1774. !disabled && details.some((item) => item.isConflict === 1),
  1775. isTempAdjust:
  1776. !disabled && details.some((item) => item.isTempAdjust === 1),
  1777. isMaintenance:
  1778. !disabled && details.some((item) => item.dateType === 4),
  1779. isHoliday:
  1780. !disabled &&
  1781. (details.some((item) => Number(item.dateType) === 3) ||
  1782. (holidayName && !hasWorkDetail)),
  1783. holidayName,
  1784. isRest: disabled ? true : this.isRestDay(date, details),
  1785. isDisabledCalendar: disabled,
  1786. conflictText: disabled ? '日历已禁用' : this.getConflictText(date)
  1787. };
  1788. if (!applyFilter || this.matchStatusFilter(cell)) {
  1789. cells.push(cell);
  1790. } else {
  1791. cells.push({ ...cell, hiddenByFilter: true });
  1792. }
  1793. });
  1794. return cells;
  1795. },
  1796. hasEnabledCalendarForView() {
  1797. return !!this.getCurrentViewCalendar();
  1798. },
  1799. getCurrentViewCalendar() {
  1800. return this.enabledCalendars.find(
  1801. (item) => item.calendarType === this.viewQuery.calendarType
  1802. );
  1803. },
  1804. getCurrentViewCalendars() {
  1805. return this.enabledCalendars.filter(
  1806. (item) => item.calendarType === this.viewQuery.calendarType
  1807. );
  1808. },
  1809. getRemoteCalendarId(calendar = this.getCurrentViewCalendar()) {
  1810. return calendar?.isRemote ? calendar.id : null;
  1811. },
  1812. getScheduleSegmentTooltip(item) {
  1813. const total = this.scheduleSegments.reduce(
  1814. (sum, segment) => sum + segment.value,
  1815. 0
  1816. );
  1817. const percent = total ? Math.round((item.value / total) * 100) : 0;
  1818. return `${item.label}:${item.value} 天,占比 ${percent}%`;
  1819. },
  1820. getMonthBarTooltip(item) {
  1821. return `${this.viewQuery.year}年${this.viewQuery.month}月,${item.label}:${item.value} 天,占当前最大值 ${item.percent}%`;
  1822. },
  1823. getCalendarTypeTooltip(item) {
  1824. return `${item.label}:共 ${item.count} 个,启用 ${item.enabled} 个,禁用 ${item.disabled} 个`;
  1825. },
  1826. getConflictTrendTooltip(item) {
  1827. return `${item.label} 所在周:${item.count} 条冲突`;
  1828. },
  1829. loadData() {
  1830. this.calendars = [];
  1831. this.details = [];
  1832. this.adjusts = [];
  1833. this.plans = [];
  1834. },
  1835. getCalendarQueryParams(query = this.query) {
  1836. return {
  1837. year: query.year || undefined,
  1838. calendarType: query.calendarType || undefined,
  1839. status: query.status === '' ? undefined : query.status
  1840. };
  1841. },
  1842. async loadRemoteCalendarData() {
  1843. try {
  1844. const data = await pageCalendar({
  1845. pageNum: 1,
  1846. size: 999,
  1847. ...this.getCalendarQueryParams()
  1848. });
  1849. const { list } = normalizePage(data);
  1850. this.calendars = list.map((item) =>
  1851. this.normalizeRemoteCalendar(item)
  1852. );
  1853. this.details = list
  1854. .map((item) =>
  1855. (item.detailList || []).map((detail) =>
  1856. this.normalizeRemoteDetail(detail, item)
  1857. )
  1858. )
  1859. .flat();
  1860. await this.loadRemoteViewData(true);
  1861. await this.loadRemoteAdjustData();
  1862. this.syncFilteredCalendars();
  1863. } catch (e) {
  1864. this.calendars = [];
  1865. this.details = [];
  1866. this.adjusts = [];
  1867. this.plans = [];
  1868. this.syncFilteredCalendars();
  1869. }
  1870. },
  1871. getViewDataKey(query = this.viewQuery) {
  1872. return [
  1873. query.calendarType || '',
  1874. query.year || '',
  1875. query.month || '',
  1876. query.viewType || ''
  1877. ].join('|');
  1878. },
  1879. async loadRemoteViewData(force = false, queryOverride = null) {
  1880. const query = {
  1881. ...this.viewQuery,
  1882. ...(queryOverride || {})
  1883. };
  1884. const dataKey = this.getViewDataKey(query);
  1885. if (!force && this.viewDataKey === dataKey) {
  1886. return;
  1887. }
  1888. if (!force && this.viewDataPromise) {
  1889. await this.viewDataPromise;
  1890. return;
  1891. }
  1892. try {
  1893. this.viewDataPromise = viewCalendar({
  1894. calendarType: query.calendarType,
  1895. year: query.year,
  1896. month: query.month,
  1897. viewType: query.viewType
  1898. });
  1899. const data = await this.viewDataPromise;
  1900. if (!Array.isArray(data)) {
  1901. this.viewDataKey = '';
  1902. this.viewDataPromise = null;
  1903. return;
  1904. }
  1905. const remoteDetails = data.map((item) => ({
  1906. ...this.normalizeRemoteDetail(item),
  1907. isViewDetail: true
  1908. }));
  1909. const visibleDates =
  1910. queryOverride && query.viewType === 'month'
  1911. ? this.getMonthDateRange(query.year, query.month).map((item) =>
  1912. item.format('YYYY-MM-DD')
  1913. )
  1914. : this.calendarDateRange.map((item) => item.format('YYYY-MM-DD'));
  1915. const viewCalendarIds = this.calendars
  1916. .filter((item) => item.calendarType === query.calendarType)
  1917. .map((item) => item.id);
  1918. this.details = [
  1919. ...this.details.filter(
  1920. (item) =>
  1921. !(
  1922. item.calendarType === query.calendarType ||
  1923. viewCalendarIds.includes(item.calendarId)
  1924. ) || !visibleDates.includes(item.calendarDate)
  1925. ),
  1926. ...remoteDetails
  1927. ];
  1928. this.viewDataKey = dataKey;
  1929. } catch (e) {
  1930. this.viewDataKey = '';
  1931. }
  1932. this.viewDataPromise = null;
  1933. },
  1934. async loadRemoteAdjustData() {
  1935. try {
  1936. const data = await pageCalendarAdjust({
  1937. pageNum: 1,
  1938. size: 999
  1939. });
  1940. const { list } = normalizePage(data);
  1941. this.adjusts = list;
  1942. } catch (e) {
  1943. this.adjusts = [];
  1944. }
  1945. },
  1946. normalizeRemoteCalendar(item) {
  1947. const detailList = Array.isArray(item.detailList)
  1948. ? item.detailList
  1949. : [];
  1950. const ruleConfig = this.parseCalendarRuleConfig(item.remark);
  1951. const workdayDetails = detailList.filter(
  1952. (detail) => Number(detail.dateType || 1) === 1
  1953. );
  1954. const applyMonth = normalizeYearList(item.applyMonth);
  1955. const inferredApplyMonth = this.getApplyMonthsFromDetails(detailList);
  1956. const inferredRestRule = this.getRestRuleFromDetails(detailList);
  1957. const restWeekdays = Array.isArray(item.restWeekdays)
  1958. ? item.restWeekdays.map(Number)
  1959. : ruleConfig.restWeekdays === undefined
  1960. ? inferredRestRule.restWeekdays
  1961. : ruleConfig.restWeekdays;
  1962. return {
  1963. ...item,
  1964. remark: this.stripCalendarRuleConfig(item.remark),
  1965. isRemote: true,
  1966. applyYear: normalizeYearList(item.applyYear),
  1967. applyMonth: applyMonth.length
  1968. ? applyMonth
  1969. : ruleConfig.applyMonth ||
  1970. inferredApplyMonth ||
  1971. this.getFullMonthList(),
  1972. shiftId: item.shiftId || ruleConfig.shiftId || '',
  1973. shiftName: item.shiftName || ruleConfig.shiftName || '',
  1974. shiftCode: item.shiftCode || ruleConfig.shiftCode || '',
  1975. timeRanges: workdayDetails.length
  1976. ? this.mergeTimeRanges(
  1977. [],
  1978. workdayDetails.map((detail, index) => ({
  1979. id: detail.id || `${item.id}-${index}`,
  1980. name:
  1981. detail.dateTypeName || detail.remark || `时段${index + 1}`,
  1982. startTime: String(detail.startTime || '').slice(0, 5),
  1983. endTime: String(detail.endTime || '').slice(0, 5),
  1984. timeType: '1'
  1985. }))
  1986. )
  1987. : item.timeRanges || [],
  1988. restWeekdays,
  1989. legalHolidayWorkDates: Array.isArray(item.legalHolidayWorkDates)
  1990. ? item.legalHolidayWorkDates
  1991. : ruleConfig.legalHolidayWorkDates || [],
  1992. restMode:
  1993. item.restMode ||
  1994. ruleConfig.restMode ||
  1995. inferredRestRule.restMode ||
  1996. this.getRestModeByWeekdays(restWeekdays),
  1997. holidayRules: Array.isArray(item.holidayRules)
  1998. ? item.holidayRules
  1999. : ruleConfig.holidayRules || []
  2000. };
  2001. },
  2002. getApplyMonthsFromDetails(detailList = []) {
  2003. const months = [
  2004. ...new Set(
  2005. detailList
  2006. .map((detail) => {
  2007. const month = dayjs(detail.calendarDate).month() + 1;
  2008. return Number.isNaN(month) ? null : month;
  2009. })
  2010. .filter(Boolean)
  2011. )
  2012. ].sort((a, b) => a - b);
  2013. return months.length ? months : null;
  2014. },
  2015. getRestRuleFromDetails(detailList = []) {
  2016. const dayMap = {};
  2017. detailList.forEach((detail) => {
  2018. if (!detail.calendarDate) {
  2019. return;
  2020. }
  2021. if (!dayMap[detail.calendarDate]) {
  2022. dayMap[detail.calendarDate] = [];
  2023. }
  2024. dayMap[detail.calendarDate].push(detail);
  2025. });
  2026. const days = Object.keys(dayMap).map((date) => {
  2027. const details = dayMap[date];
  2028. return {
  2029. date,
  2030. weekDay: dayjs(date).day(),
  2031. isRest: details.every((detail) =>
  2032. [2, 3].includes(Number(detail.dateType))
  2033. )
  2034. };
  2035. });
  2036. const restWeekdays = [
  2037. ...new Set(
  2038. days.filter((item) => item.isRest).map((item) => item.weekDay)
  2039. )
  2040. ].sort((a, b) => a - b);
  2041. const weekdayText = restWeekdays.join(',');
  2042. if (!restWeekdays.length) {
  2043. return { restMode: 'none', restWeekdays: [] };
  2044. }
  2045. if (weekdayText === '0') {
  2046. return { restMode: 'single', restWeekdays };
  2047. }
  2048. if (weekdayText === '0,6') {
  2049. const saturdayDays = days.filter((item) => item.weekDay === 6);
  2050. const saturdayRestCount = saturdayDays.filter(
  2051. (item) => item.isRest
  2052. ).length;
  2053. if (
  2054. saturdayDays.length &&
  2055. saturdayRestCount > 0 &&
  2056. saturdayRestCount < saturdayDays.length
  2057. ) {
  2058. return { restMode: 'alternate', restWeekdays };
  2059. }
  2060. return { restMode: 'double', restWeekdays };
  2061. }
  2062. return { restMode: 'custom', restWeekdays };
  2063. },
  2064. parseCalendarRuleConfig(remark) {
  2065. const match = String(remark || '').match(
  2066. /\[calendarRules\]([\s\S]*?)\[\/calendarRules\]/
  2067. );
  2068. if (!match) {
  2069. return {};
  2070. }
  2071. try {
  2072. const config = JSON.parse(match[1]);
  2073. return {
  2074. applyMonth: Array.isArray(config.applyMonth)
  2075. ? config.applyMonth.map(Number).filter(Boolean)
  2076. : undefined,
  2077. shiftId: config.shiftId || undefined,
  2078. shiftName: config.shiftName || undefined,
  2079. shiftCode: config.shiftCode || undefined,
  2080. restMode: config.restMode || undefined,
  2081. restWeekdays: Array.isArray(config.restWeekdays)
  2082. ? config.restWeekdays.map(Number)
  2083. : undefined,
  2084. legalHolidayWorkDates: Array.isArray(config.legalHolidayWorkDates)
  2085. ? config.legalHolidayWorkDates.filter(Boolean)
  2086. : undefined,
  2087. holidayRules: Array.isArray(config.holidayRules)
  2088. ? config.holidayRules
  2089. .filter((item) => item && item.calendarDate)
  2090. .map((item) =>
  2091. createHolidayRule(
  2092. Number(item.dateType || 3),
  2093. item.calendarDate,
  2094. item.remark || ''
  2095. )
  2096. )
  2097. : undefined
  2098. };
  2099. } catch (e) {
  2100. return {};
  2101. }
  2102. },
  2103. stripCalendarRuleConfig(remark) {
  2104. return String(remark || '')
  2105. .replace(/\s*\[calendarRules\][\s\S]*?\[\/calendarRules\]\s*/g, '')
  2106. .trim();
  2107. },
  2108. getCalendarRulePayload(form) {
  2109. return {
  2110. applyMonth: form.applyMonth || this.getFullMonthList(),
  2111. shiftId: form.shiftId || '',
  2112. shiftName: form.shiftName || '',
  2113. shiftCode: form.shiftCode || '',
  2114. restMode: form.restMode || 'double',
  2115. restWeekdays: form.restWeekdays || [],
  2116. legalHolidayWorkDates: form.legalHolidayWorkDates || [],
  2117. holidayRules: (form.holidayRules || [])
  2118. .filter((item) => item.calendarDate)
  2119. .map((item) => ({
  2120. calendarDate: item.calendarDate,
  2121. dateType: Number(item.dateType || 3),
  2122. remark: item.remark || ''
  2123. }))
  2124. };
  2125. },
  2126. mergeTimeRanges(...groups) {
  2127. const map = {};
  2128. groups.flat().forEach((item, index) => {
  2129. if (!item) {
  2130. return;
  2131. }
  2132. const startTime = String(item.startTime || '').slice(0, 5);
  2133. const endTime = String(item.endTime || '').slice(0, 5);
  2134. if (!startTime || !endTime) {
  2135. return;
  2136. }
  2137. const name = item.name || item.remark || `时段${index + 1}`;
  2138. const key = `${name}-${startTime}-${endTime}`;
  2139. if (!map[key]) {
  2140. map[key] = {
  2141. ...item,
  2142. id: item.id || key,
  2143. name,
  2144. startTime,
  2145. endTime,
  2146. timeType: item.timeType || '1'
  2147. };
  2148. }
  2149. });
  2150. return Object.values(map);
  2151. },
  2152. normalizeRemoteDetail(detail, calendar = {}) {
  2153. const calendarType = detail.calendarType || calendar.calendarType;
  2154. const relationPlanList = this.normalizeRemotePlans(
  2155. detail.relationPlanList || []
  2156. );
  2157. const relationEamPlanList = this.normalizeRemotePlans(
  2158. detail.relationEamPlanList || []
  2159. );
  2160. const relationTeamQueueList = this.normalizeRemotePlans(
  2161. detail.relationTeamQueueList || []
  2162. );
  2163. const relationPlanCountMap = {
  2164. 2: relationEamPlanList.length,
  2165. 3: relationTeamQueueList.length
  2166. };
  2167. const relationPlanCount =
  2168. relationPlanCountMap[Number(calendarType)] ??
  2169. Number(detail.relationPlanCount || relationPlanList.length || 0);
  2170. return {
  2171. ...detail,
  2172. id:
  2173. detail.id ||
  2174. `${detail.calendarId}-${detail.calendarDate}-${detail.startTime}`,
  2175. calendarId: detail.calendarId || calendar.id,
  2176. calendarName: detail.calendarName || calendar.calendarName,
  2177. calendarType,
  2178. calendarDate: detail.calendarDate,
  2179. startTime: String(detail.startTime || '').slice(0, 5),
  2180. endTime: String(detail.endTime || '').slice(0, 5),
  2181. dateType: Number(detail.dateType || 1),
  2182. scheduleStatus: Number(detail.scheduleStatus || 0),
  2183. isConflict: Number(detail.isConflict || 0),
  2184. isTempAdjust: Number(detail.isTempAdjust || 0),
  2185. relationPlanCount,
  2186. relationPlanList,
  2187. relationEamPlanList,
  2188. relationTeamQueueList
  2189. };
  2190. },
  2191. saveData() {
  2192. // 工厂日历页面以后台接口为唯一数据源,不再写入本地演示缓存。
  2193. },
  2194. openImport() {
  2195. this.$message.warning(
  2196. '批量导入需要后端导入接口支持,当前页面不再写入本地数据'
  2197. );
  2198. },
  2199. async calendarDatasource({
  2200. page = 1,
  2201. limit = this.pageSize,
  2202. where = {},
  2203. order = {}
  2204. } = {}) {
  2205. try {
  2206. const data = await pageCalendar({
  2207. ...this.getCalendarQueryParams({
  2208. ...this.query,
  2209. ...where
  2210. }),
  2211. ...order,
  2212. pageNum: page,
  2213. size: limit
  2214. });
  2215. const { list, count } = normalizePage(data);
  2216. const rows = list.map((item) => this.normalizeRemoteCalendar(item));
  2217. return { code: 0, list: rows, count };
  2218. } catch (e) {
  2219. return this.getPagedResult([], page, limit);
  2220. }
  2221. },
  2222. getPagedResult(list, page = 1, limit = this.pageSize) {
  2223. const start = (page - 1) * limit;
  2224. return {
  2225. code: 0,
  2226. list: list.slice(start, start + limit),
  2227. count: list.length
  2228. };
  2229. },
  2230. reloadConflictTable(page = 1) {
  2231. this.$nextTick(() => {
  2232. this.$refs.conflictPanel?.reload?.({ page });
  2233. });
  2234. },
  2235. reloadAdjustTable(page = 1) {
  2236. this.$nextTick(() => {
  2237. this.$refs.adjustApproval?.reload?.({ page });
  2238. });
  2239. },
  2240. reloadPlanTable(page = 1) {
  2241. this.$nextTick(() => {
  2242. this.$refs.planLinkage?.reload?.({ page });
  2243. });
  2244. },
  2245. reloadBaseTable(page = 1) {
  2246. this.$nextTick(() => {
  2247. this.$refs.baseManage?.reload?.({ page });
  2248. });
  2249. },
  2250. async adjustDatasource({
  2251. page = 1,
  2252. limit = this.pageSize,
  2253. where = {},
  2254. order = {}
  2255. } = {}) {
  2256. try {
  2257. const data = await pageCalendarAdjust({
  2258. ...where,
  2259. ...order,
  2260. pageNum: page,
  2261. size: limit
  2262. });
  2263. const { list, count } = normalizePage(data);
  2264. return { code: 0, list, count };
  2265. } catch (e) {
  2266. return this.getPagedResult([], page, limit);
  2267. }
  2268. },
  2269. async planDatasource({ page = 1, limit = this.pageSize } = {}) {
  2270. await this.ensurePlanRefreshData(false);
  2271. if (this.plans.length) {
  2272. return this.getPagedResult(this.plans, page, limit);
  2273. }
  2274. const calendars = this.getCurrentViewCalendars();
  2275. if (calendars.some((item) => this.getRemoteCalendarId(item))) {
  2276. try {
  2277. await this.loadRemoteViewData();
  2278. const details = this.getViewDetails(this.viewQuery.date);
  2279. const plans = this.getDetailRelatedPlans(details);
  2280. return this.getPagedResult(plans, page, limit);
  2281. } catch (e) {
  2282. return this.getPagedResult([], page, limit);
  2283. }
  2284. }
  2285. return this.getPagedResult([], page, limit);
  2286. },
  2287. normalizeRemoteConflicts(data = {}) {
  2288. return (data.conflictDetail || []).map((item, index) => ({
  2289. id:
  2290. item.currentDetailId ||
  2291. [
  2292. item.calendarDate,
  2293. item.startTime,
  2294. item.endTime,
  2295. item.currentCalendarType,
  2296. item.conflictCalendarType,
  2297. item.conflictCalendarId,
  2298. item.conflictDetailId,
  2299. item.conflictMsg || data.msg || '',
  2300. index
  2301. ].join('-'),
  2302. calendarDate: item.calendarDate,
  2303. timeRange: `${String(item.startTime || '').slice(0, 5)}-${String(
  2304. item.endTime || ''
  2305. ).slice(0, 5)}`,
  2306. scene: `${item.currentCalendarType || ''} VS ${
  2307. item.conflictCalendarType || ''
  2308. }`,
  2309. level: data.isConflict ? '高' : '低',
  2310. message: item.conflictMsg || data.msg || ''
  2311. }));
  2312. },
  2313. normalizeRemotePlans(data = {}) {
  2314. const list = Array.isArray(data)
  2315. ? data
  2316. : [
  2317. ...(data.records || []),
  2318. ...(data.list || []),
  2319. ...(data.orderList || []),
  2320. ...(data.taskList || []),
  2321. ...(data.schedulePlanList || []),
  2322. ...(data.relationPlanList || []),
  2323. ...(data.relationEamPlanList || []),
  2324. ...(data.relationTeamQueueList || []),
  2325. ...(data.affectedPlanList || [])
  2326. ];
  2327. return list.map((item, index) => {
  2328. const planNo =
  2329. item.planNo ||
  2330. item.orderNo ||
  2331. item.code ||
  2332. item.teamQueueName ||
  2333. item.teamQueueId ||
  2334. `PLAN-${index + 1}`;
  2335. const salesCode = Array.isArray(item.salesCode)
  2336. ? item.salesCode.join(',')
  2337. : item.salesCode;
  2338. const rawStartTime =
  2339. item.rawStartTime ||
  2340. item.planStartTime ||
  2341. item.queueDate ||
  2342. item.startTime;
  2343. const rawEndTime =
  2344. item.rawEndTime ||
  2345. item.planEndTime ||
  2346. item.queueEndTime ||
  2347. item.endTime;
  2348. const startTime = this.formatPlanTime(rawStartTime);
  2349. const endTime = this.formatPlanTime(rawEndTime);
  2350. const displayStartTime = this.formatPlanDateTime(rawStartTime);
  2351. const displayEndTime = this.formatPlanDateTime(rawEndTime);
  2352. const hasDateRange =
  2353. /^\d{4}-\d{2}-\d{2}/.test(String(rawStartTime || '')) ||
  2354. /^\d{4}-\d{2}-\d{2}/.test(String(rawEndTime || ''));
  2355. return {
  2356. ...item,
  2357. id: item.id || planNo || index,
  2358. planNo,
  2359. orderNo:
  2360. item.orderNo ||
  2361. item.salesCodeList ||
  2362. salesCode ||
  2363. item.batchNo ||
  2364. item.teamName ||
  2365. item.teamId ||
  2366. item.userName ||
  2367. item.userId ||
  2368. '',
  2369. taskName:
  2370. item.taskName ||
  2371. item.productName ||
  2372. item.bomCategoryName ||
  2373. item.teamQueueName ||
  2374. item.queueName ||
  2375. item.teamName ||
  2376. item.userName ||
  2377. item.name ||
  2378. item.title ||
  2379. '关联计划',
  2380. calendarId: item.calendarId,
  2381. calendarDate:
  2382. item.calendarDate ||
  2383. this.formatPlanDate(
  2384. item.planDeliveryTime ||
  2385. item.reqMoldTime ||
  2386. item.planEndTime ||
  2387. item.planStartTime ||
  2388. item.queueDate ||
  2389. item.endTime ||
  2390. item.startTime
  2391. ),
  2392. startTime,
  2393. endTime,
  2394. rawStartTime,
  2395. rawEndTime,
  2396. displayStartTime,
  2397. displayEndTime,
  2398. timeRange: hasDateRange
  2399. ? `${displayStartTime} 至 ${displayEndTime}`
  2400. : `${startTime}-${endTime}`,
  2401. status: item.statusName || item.status || '正常',
  2402. canReschedule: !!item.canReschedule,
  2403. productionLineName:
  2404. item.productionLineName ||
  2405. item.productionLine ||
  2406. item.productionCodes ||
  2407. '',
  2408. workStationName: item.workStationName || item.workStation || ''
  2409. };
  2410. });
  2411. },
  2412. formatPlanDate(value) {
  2413. const text = String(value || '');
  2414. const matched = text.match(/^\d{4}-\d{2}-\d{2}/);
  2415. return matched ? matched[0] : this.viewQuery.date;
  2416. },
  2417. formatPlanTime(value) {
  2418. const text = String(value || '');
  2419. const matched = text.match(/\b(\d{2}):(\d{2})(?::\d{2})?\b/);
  2420. return matched ? `${matched[1]}:${matched[2]}` : text.slice(0, 5);
  2421. },
  2422. formatPlanDateTime(value) {
  2423. const text = String(value || '');
  2424. const matched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/);
  2425. if (matched) {
  2426. return `${matched[1]} ${matched[2]}:${matched[3]}`;
  2427. }
  2428. return this.formatPlanTime(value);
  2429. },
  2430. getDetailRelatedPlans(details = []) {
  2431. const map = {};
  2432. details.forEach((detail) => {
  2433. [
  2434. ...(detail.relationPlanList || []),
  2435. ...(detail.relationEamPlanList || []),
  2436. ...(detail.relationTeamQueueList || [])
  2437. ].forEach((plan, index) => {
  2438. const normalized = this.normalizeRemotePlans([plan])[0];
  2439. if (!normalized) {
  2440. return;
  2441. }
  2442. const key =
  2443. normalized.id ||
  2444. normalized.planNo ||
  2445. normalized.code ||
  2446. `${detail.id}-${index}`;
  2447. map[key] = {
  2448. ...normalized,
  2449. calendarId: normalized.calendarId || detail.calendarId,
  2450. calendarDate: detail.calendarDate || normalized.calendarDate,
  2451. calendarType: normalized.calendarType || detail.calendarType
  2452. };
  2453. });
  2454. });
  2455. return Object.values(map);
  2456. },
  2457. getRelatedPlanCode(plan = {}) {
  2458. return (
  2459. (this.isEquipmentDrawer
  2460. ? plan.deviceCode ||
  2461. plan.equipmentCode ||
  2462. plan.eamCode ||
  2463. plan.assetCode ||
  2464. plan.planNo
  2465. : plan.planNo) || '-'
  2466. );
  2467. },
  2468. getRelatedPlanName(plan = {}) {
  2469. return (
  2470. (this.isEquipmentDrawer
  2471. ? plan.deviceName ||
  2472. plan.equipmentName ||
  2473. plan.eamName ||
  2474. plan.assetName ||
  2475. plan.taskName
  2476. : plan.taskName) || '-'
  2477. );
  2478. },
  2479. getRelatedQueueName(plan = {}) {
  2480. return plan.teamQueueName || '-';
  2481. },
  2482. getRelatedQueueUserName(plan = {}) {
  2483. return plan.userName || '';
  2484. },
  2485. getDetailPlanCount(details = []) {
  2486. const relatedPlans = this.getDetailRelatedPlans(details);
  2487. if (relatedPlans.length) {
  2488. return relatedPlans.length;
  2489. }
  2490. return details.reduce(
  2491. (sum, item) => sum + Number(item.relationPlanCount || 0),
  2492. 0
  2493. );
  2494. },
  2495. syncFilteredCalendars() {
  2496. this.filteredCalendars = this.calendars.filter((item) => {
  2497. const yearMatch =
  2498. !this.query.year || item.applyYear.includes(this.query.year);
  2499. const typeMatch =
  2500. !this.query.calendarType ||
  2501. item.calendarType === this.query.calendarType;
  2502. const statusMatch =
  2503. this.query.status === '' || item.status === this.query.status;
  2504. return yearMatch && typeMatch && statusMatch;
  2505. });
  2506. },
  2507. async filterCalendar() {
  2508. this.viewDataKey = '';
  2509. this.invalidateConflictCache();
  2510. await this.loadRemoteCalendarData();
  2511. this.reloadBaseTable(1);
  2512. },
  2513. resetQuery() {
  2514. this.query = {
  2515. year: currentYear,
  2516. month: '',
  2517. calendarType: '',
  2518. status: ''
  2519. };
  2520. this.filterCalendar();
  2521. },
  2522. openCalendarDialog(row) {
  2523. this.calendarForm = row ? clone(row) : this.getDefaultCalendarForm();
  2524. this.calendarDialogVisible = true;
  2525. this.loadLegalHolidaysByYears(this.calendarForm.applyYear);
  2526. this.$nextTick(() => {
  2527. this.$refs.calendarForm?.clearValidate?.();
  2528. this.resetCalendarDialogScroll();
  2529. });
  2530. },
  2531. resetCalendarDialogScroll() {
  2532. const dialogBody = document.querySelector(
  2533. '.factory-calendar-dialog .el-dialog__body'
  2534. );
  2535. if (dialogBody) {
  2536. dialogBody.scrollTop = 0;
  2537. }
  2538. document
  2539. .querySelectorAll('.factory-calendar-dialog .legal-holiday-list')
  2540. .forEach((item) => {
  2541. item.scrollTop = 0;
  2542. });
  2543. },
  2544. saveCalendar() {
  2545. this.$refs.calendarForm.validate(async (valid) => {
  2546. if (!valid) {
  2547. return;
  2548. }
  2549. if (this.hasInvalidRestRule()) {
  2550. return;
  2551. }
  2552. if (this.hasInvalidHolidayRules()) {
  2553. return;
  2554. }
  2555. if (this.hasDuplicateCalendarTemplate()) {
  2556. this.$message.error('同类型同年份日历已存在,请勿重复创建');
  2557. return;
  2558. }
  2559. try {
  2560. const payload = this.toRemoteCalendarPayload(this.calendarForm);
  2561. if (this.calendarForm.id) {
  2562. await editCalendar(payload);
  2563. } else {
  2564. const result = await addCalendar(payload);
  2565. if (result?.id) {
  2566. this.calendarForm.id = result.id;
  2567. await updateCalendarStatus({
  2568. id: result.id,
  2569. status: Number(this.calendarForm.status || 0)
  2570. });
  2571. }
  2572. if (result?.calendarCode) {
  2573. this.calendarForm.calendarCode = result.calendarCode;
  2574. }
  2575. }
  2576. await this.loadRemoteCalendarData();
  2577. this.invalidateConflictCache();
  2578. this.reloadBaseTable(1);
  2579. this.calendarDialogVisible = false;
  2580. this.$message.success('保存成功');
  2581. return;
  2582. } catch (e) {
  2583. this.$message.error(e?.message || '保存失败,请检查接口返回');
  2584. }
  2585. });
  2586. },
  2587. hasDuplicateCalendarTemplate() {
  2588. return this.calendars.some((item) => {
  2589. if (item.id === this.calendarForm.id) {
  2590. return false;
  2591. }
  2592. const sameType = item.calendarType === this.calendarForm.calendarType;
  2593. const sameYear = item.applyYear.some((year) =>
  2594. this.calendarForm.applyYear.includes(year)
  2595. );
  2596. return (
  2597. sameType &&
  2598. sameYear &&
  2599. item.calendarName === this.calendarForm.calendarName
  2600. );
  2601. });
  2602. },
  2603. toRemoteCalendarPayload(form) {
  2604. const rulePayload = this.getCalendarRulePayload(form);
  2605. return {
  2606. id: form.id || undefined,
  2607. calendarName: form.calendarName,
  2608. calendarType: form.calendarType,
  2609. status: Number(form.status || 0),
  2610. applyYear: Array.isArray(form.applyYear)
  2611. ? form.applyYear.join(',')
  2612. : form.applyYear,
  2613. remark: this.stripCalendarRuleConfig(form.remark),
  2614. ...rulePayload,
  2615. segmentList: this.buildCalendarSegments(form)
  2616. };
  2617. },
  2618. getFullMonthList() {
  2619. return Array.from({ length: 12 }, (_, index) => index + 1);
  2620. },
  2621. getRestModeByWeekdays(weekdays) {
  2622. const values = (weekdays || []).map(Number).sort().join(',');
  2623. if (values === '0') {
  2624. return 'single';
  2625. }
  2626. if (values === '0,6') {
  2627. return 'double';
  2628. }
  2629. if (!values) {
  2630. return 'none';
  2631. }
  2632. return 'double';
  2633. },
  2634. isRestDateByMode(dateItem, restMode) {
  2635. const weekDay = dateItem.day();
  2636. if (restMode === 'none') {
  2637. return false;
  2638. }
  2639. if (restMode === 'single') {
  2640. return weekDay === 0;
  2641. }
  2642. if (restMode === 'alternate') {
  2643. const yearStart = dayjs(`${dateItem.year()}-01-01`);
  2644. const weekIndex = Math.floor(dateItem.diff(yearStart, 'day') / 7);
  2645. return weekDay === 0 || (weekDay === 6 && weekIndex % 2 === 0);
  2646. }
  2647. if (restMode === 'custom') {
  2648. return false;
  2649. }
  2650. return weekDay === 0 || weekDay === 6;
  2651. },
  2652. getLegalHolidayInfo(dateItem) {
  2653. if (!dateItem || !dateItem.isValid()) {
  2654. return null;
  2655. }
  2656. const holidayInfo = this.getConfiguredHolidayRanges(
  2657. dateItem.year()
  2658. ).find(
  2659. (item) =>
  2660. !dateItem.isBefore(dayjs(item.start), 'day') &&
  2661. !dateItem.isAfter(dayjs(item.end), 'day')
  2662. );
  2663. if (holidayInfo) {
  2664. return {
  2665. name: holidayInfo.name,
  2666. calendarDate: dateItem.format('YYYY-MM-DD')
  2667. };
  2668. }
  2669. const lunar = solarToLunar(
  2670. dateItem.year(),
  2671. dateItem.month() + 1,
  2672. dateItem.date()
  2673. );
  2674. const festivalText = [
  2675. lunar.solarFestival,
  2676. lunar.lunarFestival,
  2677. lunar.term
  2678. ]
  2679. .filter(Boolean)
  2680. .join(' ');
  2681. const match = legalHolidayKeywords.find((item) =>
  2682. festivalText.includes(item.keyword)
  2683. );
  2684. return match
  2685. ? {
  2686. name: match.label,
  2687. calendarDate: dateItem.format('YYYY-MM-DD')
  2688. }
  2689. : null;
  2690. },
  2691. getLegalHolidayFestivalDates(year) {
  2692. const festivalMap = {};
  2693. const firstDay = dayjs(`${year}-01-01`);
  2694. const end = firstDay.endOf('year');
  2695. let cursor = firstDay;
  2696. while (!cursor.isAfter(end, 'day')) {
  2697. const lunar = solarToLunar(
  2698. cursor.year(),
  2699. cursor.month() + 1,
  2700. cursor.date()
  2701. );
  2702. const festivalText = [
  2703. lunar.solarFestival,
  2704. lunar.lunarFestival,
  2705. lunar.term
  2706. ]
  2707. .filter(Boolean)
  2708. .join(' ');
  2709. const match = legalHolidayKeywords.find((item) =>
  2710. festivalText.includes(item.keyword)
  2711. );
  2712. if (match && !festivalMap[match.label]) {
  2713. festivalMap[match.label] = cursor.format('YYYY-MM-DD');
  2714. }
  2715. cursor = cursor.add(1, 'day');
  2716. }
  2717. return festivalMap;
  2718. },
  2719. getInferredHolidayRanges(year) {
  2720. if (inferredHolidayRangeCache[year]) {
  2721. return inferredHolidayRangeCache[year];
  2722. }
  2723. const festivalDates = this.getLegalHolidayFestivalDates(year);
  2724. const ranges = Object.keys(festivalDates).map((name) => {
  2725. const rule = inferredHolidayRules[name] || { before: 0, after: 0 };
  2726. const date = dayjs(festivalDates[name]);
  2727. return {
  2728. name,
  2729. start: date.subtract(rule.before, 'day').format('YYYY-MM-DD'),
  2730. end: date.add(rule.after, 'day').format('YYYY-MM-DD')
  2731. };
  2732. });
  2733. inferredHolidayRangeCache[year] = ranges;
  2734. return ranges;
  2735. },
  2736. getConfiguredHolidayRanges(year) {
  2737. const remoteRanges = this.legalHolidayCache[year];
  2738. if (remoteRanges && remoteRanges.length) {
  2739. return remoteRanges;
  2740. }
  2741. return (
  2742. officialHolidayRanges[year] || this.getInferredHolidayRanges(year)
  2743. );
  2744. },
  2745. async loadLegalHolidaysByYears(applyYear) {
  2746. const years = normalizeYearList(applyYear);
  2747. if (!years.length) {
  2748. return;
  2749. }
  2750. this.legalHolidayLoading = true;
  2751. try {
  2752. await Promise.all(
  2753. years.map((year) => this.loadLegalHolidayYear(year))
  2754. );
  2755. } finally {
  2756. this.legalHolidayLoading = false;
  2757. }
  2758. },
  2759. async loadLegalHolidayYear(year) {
  2760. if (
  2761. this.legalHolidayCache[year] ||
  2762. this.legalHolidayStatusMap[year] === 'fallback'
  2763. ) {
  2764. return this.legalHolidayCache[year];
  2765. }
  2766. if (!this.legalHolidayPromises[year]) {
  2767. this.$set(this.legalHolidayStatusMap, year, 'loading');
  2768. this.legalHolidayPromises[year] = getPublicLegalHolidays(year)
  2769. .then((data) => {
  2770. const ranges = this.normalizePublicHolidayRanges(data, year);
  2771. if (ranges.length) {
  2772. this.$set(this.legalHolidayCache, year, ranges);
  2773. this.$set(this.legalHolidayStatusMap, year, 'remote');
  2774. } else {
  2775. this.$set(this.legalHolidayStatusMap, year, 'fallback');
  2776. }
  2777. return ranges;
  2778. })
  2779. .catch(() => {
  2780. this.$set(this.legalHolidayStatusMap, year, 'fallback');
  2781. return [];
  2782. })
  2783. .finally(() => {
  2784. this.$delete(this.legalHolidayPromises, year);
  2785. });
  2786. }
  2787. return this.legalHolidayPromises[year];
  2788. },
  2789. normalizePublicHolidayRanges(data, year) {
  2790. const rawList = this.extractPublicHolidayList(data);
  2791. const dateMap = {};
  2792. rawList.forEach((item) => {
  2793. const source = this.normalizePublicHolidayItem(item);
  2794. const dateText = this.normalizePublicHolidayDate(
  2795. source.calendarDate,
  2796. year
  2797. );
  2798. if (!dateText) {
  2799. return;
  2800. }
  2801. const isWorkday =
  2802. source.isWorkday === true ||
  2803. source.is_workday === true ||
  2804. source.workday === true ||
  2805. source.is_holiday === false ||
  2806. source.holiday === false ||
  2807. source.type === 'workday' ||
  2808. source.type === 'work' ||
  2809. source.type === 0;
  2810. if (isWorkday) {
  2811. return;
  2812. }
  2813. dateMap[dateText] = {
  2814. name:
  2815. source.name ||
  2816. source.holidayName ||
  2817. source.festival ||
  2818. source.title ||
  2819. '法定节假日',
  2820. calendarDate: dateText
  2821. };
  2822. });
  2823. return this.mergeHolidayDatesToRanges(Object.values(dateMap));
  2824. },
  2825. normalizePublicHolidayItem(item) {
  2826. if (typeof item === 'string') {
  2827. return {
  2828. calendarDate: item
  2829. };
  2830. }
  2831. if (!item || typeof item !== 'object') {
  2832. return {};
  2833. }
  2834. return {
  2835. ...item,
  2836. calendarDate:
  2837. item.date ||
  2838. item.calendarDate ||
  2839. item.holidayDate ||
  2840. item.day ||
  2841. item.time
  2842. };
  2843. },
  2844. normalizePublicHolidayDate(value, year) {
  2845. if (!value && value !== 0) {
  2846. return '';
  2847. }
  2848. const text = String(value).trim();
  2849. let match = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
  2850. if (match) {
  2851. const date = dayjs(
  2852. `${match[1]}-${String(match[2]).padStart(2, '0')}-${String(
  2853. match[3]
  2854. ).padStart(2, '0')}`
  2855. );
  2856. return date.isValid() && date.year() === Number(year)
  2857. ? date.format('YYYY-MM-DD')
  2858. : '';
  2859. }
  2860. match = text.match(/^(\d{4})(\d{2})(\d{2})$/);
  2861. if (match) {
  2862. const date = dayjs(`${match[1]}-${match[2]}-${match[3]}`);
  2863. return date.isValid() && date.year() === Number(year)
  2864. ? date.format('YYYY-MM-DD')
  2865. : '';
  2866. }
  2867. match = text.match(/^(\d{1,2})[-/](\d{1,2})$/);
  2868. if (match) {
  2869. const date = dayjs(
  2870. `${year}-${String(match[1]).padStart(2, '0')}-${String(
  2871. match[2]
  2872. ).padStart(2, '0')}`
  2873. );
  2874. return date.isValid() ? date.format('YYYY-MM-DD') : '';
  2875. }
  2876. return '';
  2877. },
  2878. extractPublicHolidayList(data) {
  2879. if (Array.isArray(data)) {
  2880. return data;
  2881. }
  2882. if (Array.isArray(data?.data)) {
  2883. return data.data;
  2884. }
  2885. if (Array.isArray(data?.holidays)) {
  2886. return data.holidays;
  2887. }
  2888. if (Array.isArray(data?.data?.holidays)) {
  2889. return data.data.holidays;
  2890. }
  2891. const source =
  2892. data?.data && typeof data.data === 'object' ? data.data : data;
  2893. if (source && typeof source === 'object') {
  2894. return Object.keys(source).map((key) => {
  2895. const value = source[key];
  2896. if (typeof value === 'string') {
  2897. return {
  2898. calendarDate: key,
  2899. name: value
  2900. };
  2901. }
  2902. return {
  2903. calendarDate: key,
  2904. ...(value || {})
  2905. };
  2906. });
  2907. }
  2908. return [];
  2909. },
  2910. mergeHolidayDatesToRanges(holidayList) {
  2911. const list = holidayList
  2912. .filter((item) => item.calendarDate)
  2913. .sort((a, b) => a.calendarDate.localeCompare(b.calendarDate));
  2914. const ranges = [];
  2915. list.forEach((item) => {
  2916. const last = ranges[ranges.length - 1];
  2917. if (
  2918. last &&
  2919. last.name === item.name &&
  2920. dayjs(item.calendarDate).diff(dayjs(last.end), 'day') === 1
  2921. ) {
  2922. last.end = item.calendarDate;
  2923. } else {
  2924. ranges.push({
  2925. name: item.name,
  2926. start: item.calendarDate,
  2927. end: item.calendarDate
  2928. });
  2929. }
  2930. });
  2931. return ranges;
  2932. },
  2933. getLegalHolidayName(dateItem) {
  2934. return this.getLegalHolidayInfo(dateItem)?.name || '';
  2935. },
  2936. getLegalHolidayOptions(applyYear, applyMonth) {
  2937. const monthSet = new Set(
  2938. (applyMonth || this.getFullMonthList()).map(Number)
  2939. );
  2940. const holidayMap = {};
  2941. normalizeYearList(applyYear).forEach((year) => {
  2942. const ranges = this.getConfiguredHolidayRanges(year);
  2943. ranges.forEach((range) => {
  2944. let cursor = dayjs(range.start);
  2945. const end = dayjs(range.end);
  2946. while (!cursor.isAfter(end, 'day')) {
  2947. if (monthSet.has(cursor.month() + 1)) {
  2948. const calendarDate = cursor.format('YYYY-MM-DD');
  2949. if (!holidayMap[calendarDate]) {
  2950. holidayMap[calendarDate] = {
  2951. name: range.name,
  2952. calendarDate
  2953. };
  2954. }
  2955. }
  2956. cursor = cursor.add(1, 'day');
  2957. }
  2958. });
  2959. });
  2960. return Object.values(holidayMap).sort((a, b) =>
  2961. a.calendarDate.localeCompare(b.calendarDate)
  2962. );
  2963. },
  2964. getCalendarDateType(cursor, restMode, customRestWeekdays, specialRule) {
  2965. if (specialRule) {
  2966. return specialRule.dateType;
  2967. }
  2968. if (this.getLegalHolidayName(cursor)) {
  2969. return 3;
  2970. }
  2971. if (restMode === 'custom') {
  2972. return customRestWeekdays.has(cursor.day()) ? 2 : 1;
  2973. }
  2974. return this.isRestDateByMode(cursor, restMode) ? 2 : 1;
  2975. },
  2976. getLegalHolidayRules(form) {
  2977. const holidayInfoMap = {};
  2978. this.getLegalHolidayOptions(form.applyYear, form.applyMonth).forEach(
  2979. (item) => {
  2980. holidayInfoMap[item.calendarDate] = item;
  2981. }
  2982. );
  2983. return (form.legalHolidayWorkDates || [])
  2984. .filter((date) => holidayInfoMap[date])
  2985. .map((date) => ({
  2986. calendarDate: date,
  2987. dateType: 1,
  2988. remark: holidayInfoMap[date].name
  2989. }));
  2990. },
  2991. buildCalendarSegments(form) {
  2992. const ranges = form.timeRanges || [];
  2993. const monthList = (form.applyMonth || this.getFullMonthList())
  2994. .map(Number)
  2995. .filter(Boolean);
  2996. const applyMonths = new Set(monthList);
  2997. const restMode =
  2998. form.restMode || this.getRestModeByWeekdays(form.restWeekdays);
  2999. const customRestWeekdays = new Set(
  3000. (form.restWeekdays || []).map(Number)
  3001. );
  3002. const specialRuleMap = {};
  3003. const specialRules = [
  3004. ...this.getLegalHolidayRules(form),
  3005. ...(form.holidayRules || [])
  3006. ];
  3007. specialRules.forEach((rule) => {
  3008. if (rule.calendarDate) {
  3009. specialRuleMap[rule.calendarDate] = {
  3010. ...rule,
  3011. dateType: Number(rule.dateType || 3)
  3012. };
  3013. }
  3014. });
  3015. const segments = [];
  3016. if (!applyMonths.size) {
  3017. return segments;
  3018. }
  3019. normalizeYearList(form.applyYear).forEach((year) => {
  3020. const months = Array.from(applyMonths).sort((a, b) => a - b);
  3021. let cursor = dayjs(
  3022. `${year}-${String(months[0]).padStart(2, '0')}-01`
  3023. );
  3024. const lastMonth = months[months.length - 1];
  3025. const end = dayjs(
  3026. `${year}-${String(lastMonth).padStart(2, '0')}-01`
  3027. ).endOf('month');
  3028. while (!cursor.isAfter(end, 'day')) {
  3029. if (!applyMonths.has(cursor.month() + 1)) {
  3030. cursor = cursor.add(1, 'day');
  3031. continue;
  3032. }
  3033. const calendarDate = cursor.format('YYYY-MM-DD');
  3034. const specialRule = specialRuleMap[calendarDate];
  3035. const legalHolidayName = this.getLegalHolidayName(cursor);
  3036. const dateType = this.getCalendarDateType(
  3037. cursor,
  3038. restMode,
  3039. customRestWeekdays,
  3040. specialRule
  3041. );
  3042. if (dateType === 1) {
  3043. const workRanges = ranges.length
  3044. ? ranges
  3045. : [
  3046. {
  3047. startTime: '00:00',
  3048. endTime: '23:59',
  3049. scheduleStatus: 0,
  3050. remark:
  3051. specialRule?.remark || legalHolidayName || '工作日'
  3052. }
  3053. ];
  3054. workRanges.forEach((range) => {
  3055. segments.push({
  3056. calendarDate,
  3057. startTime: range.startTime,
  3058. endTime: range.endTime,
  3059. dateType: 1,
  3060. scheduleStatus:
  3061. range.scheduleStatus === undefined
  3062. ? 0
  3063. : range.scheduleStatus,
  3064. remark:
  3065. range.name ||
  3066. range.remark ||
  3067. specialRule?.remark ||
  3068. legalHolidayName ||
  3069. ''
  3070. });
  3071. });
  3072. } else {
  3073. segments.push({
  3074. calendarDate,
  3075. startTime: '00:00',
  3076. endTime: '23:59',
  3077. dateType,
  3078. scheduleStatus: 0,
  3079. remark:
  3080. specialRule?.remark ||
  3081. legalHolidayName ||
  3082. (dateType === 3 ? '法定节假日' : '休息日')
  3083. });
  3084. }
  3085. cursor = cursor.add(1, 'day');
  3086. }
  3087. });
  3088. return segments;
  3089. },
  3090. hasInvalidRestRule() {
  3091. if (
  3092. this.calendarForm.restMode === 'custom' &&
  3093. !(this.calendarForm.restWeekdays || []).length
  3094. ) {
  3095. this.$message.error('自定义休息请至少选择一个休息星期');
  3096. return true;
  3097. }
  3098. return false;
  3099. },
  3100. hasInvalidHolidayRules() {
  3101. const dateMap = {};
  3102. (this.calendarForm.legalHolidayWorkDates || []).forEach((date) => {
  3103. dateMap[date] = true;
  3104. });
  3105. const years = new Set(
  3106. normalizeYearList(this.calendarForm.applyYear).map(String)
  3107. );
  3108. const months = new Set(
  3109. (this.calendarForm.applyMonth || []).map((item) => Number(item))
  3110. );
  3111. for (const rule of this.calendarForm.holidayRules || []) {
  3112. if (!rule.calendarDate) {
  3113. this.$message.error('特殊日期存在未选择日期的配置,请补充或删除');
  3114. return true;
  3115. }
  3116. if (!years.has(dayjs(rule.calendarDate).format('YYYY'))) {
  3117. this.$message.error('特殊日期必须在适用年份范围内');
  3118. return true;
  3119. }
  3120. if (!months.has(dayjs(rule.calendarDate).month() + 1)) {
  3121. this.$message.error('特殊日期必须在适用月份范围内');
  3122. return true;
  3123. }
  3124. if (dateMap[rule.calendarDate]) {
  3125. this.$message.error('特殊日期存在重复日期,请调整后保存');
  3126. return true;
  3127. }
  3128. dateMap[rule.calendarDate] = true;
  3129. }
  3130. return false;
  3131. },
  3132. hasDuplicateDetail(
  3133. calendarId,
  3134. calendarDate,
  3135. startTime,
  3136. endTime,
  3137. excludeId
  3138. ) {
  3139. const calendar = this.calendars.find((item) => item.id === calendarId);
  3140. if (!calendar) {
  3141. return false;
  3142. }
  3143. return this.details.some((item) => {
  3144. const itemCalendar = this.calendars.find(
  3145. (calendarItem) => calendarItem.id === item.calendarId
  3146. );
  3147. return (
  3148. item.id !== excludeId &&
  3149. item.calendarDate === calendarDate &&
  3150. itemCalendar?.calendarType === calendar.calendarType &&
  3151. isOverlap(startTime, endTime, item.startTime, item.endTime)
  3152. );
  3153. });
  3154. },
  3155. addHolidayRule() {
  3156. this.calendarForm.holidayRules.push(createHolidayRule(1));
  3157. },
  3158. removeHolidayRule(index) {
  3159. this.calendarForm.holidayRules.splice(index, 1);
  3160. },
  3161. handleApplyMonthChange() {
  3162. this.pruneLegalHolidayWorkDates();
  3163. },
  3164. pruneLegalHolidayWorkDates() {
  3165. const visibleDates = new Set(
  3166. this.legalHolidayOptions.map((item) => item.calendarDate)
  3167. );
  3168. this.calendarForm.legalHolidayWorkDates = (
  3169. this.calendarForm.legalHolidayWorkDates || []
  3170. ).filter((date) => visibleDates.has(date));
  3171. },
  3172. changeLegalHolidayWorkStatus(calendarDate, checked) {
  3173. const dates = new Set(this.calendarForm.legalHolidayWorkDates || []);
  3174. if (checked) {
  3175. dates.add(calendarDate);
  3176. } else {
  3177. dates.delete(calendarDate);
  3178. }
  3179. this.calendarForm.legalHolidayWorkDates = Array.from(dates).sort();
  3180. },
  3181. toggleLegalHolidayWorkStatus(calendarDate) {
  3182. const checked = !(
  3183. this.calendarForm.legalHolidayWorkDates || []
  3184. ).includes(calendarDate);
  3185. this.changeLegalHolidayWorkStatus(calendarDate, checked);
  3186. },
  3187. formatHolidayDate(calendarDate) {
  3188. return dayjs(calendarDate).format('MM-DD');
  3189. },
  3190. getTimePickerOptions(minTime) {
  3191. return minTime
  3192. ? {
  3193. ...this.timePickerOptions,
  3194. minTime
  3195. }
  3196. : this.timePickerOptions;
  3197. },
  3198. async changeCalendarStatus(row) {
  3199. try {
  3200. await updateCalendarStatus({
  3201. id: row.id,
  3202. status: row.status
  3203. });
  3204. await this.loadRemoteCalendarData();
  3205. this.invalidateConflictCache();
  3206. this.reloadBaseTable(1);
  3207. this.$message.success(row.status ? '已启用' : '已禁用');
  3208. return;
  3209. } catch (e) {
  3210. row.status = row.status === 1 ? 0 : 1;
  3211. this.$message.error(e?.message || '状态更新失败,请检查接口返回');
  3212. }
  3213. },
  3214. async deleteCalendarRow(row) {
  3215. if (!row?.id) {
  3216. this.$message.error('未获取到日历ID,无法删除');
  3217. return;
  3218. }
  3219. try {
  3220. const calendarName = row.calendarName || row.calendarCode || row.id;
  3221. await this.$confirm(`确认删除日历 ${calendarName} 吗?`, '删除确认', {
  3222. type: 'warning'
  3223. });
  3224. await deleteCalendar([row.id]);
  3225. await this.loadRemoteCalendarData();
  3226. this.invalidateConflictCache();
  3227. this.reloadBaseTable(1);
  3228. await this.refreshRemoteView();
  3229. this.$message.success('删除成功');
  3230. } catch (e) {
  3231. if (e === 'cancel' || e === 'close') {
  3232. return;
  3233. }
  3234. this.$message.error(e?.message || '删除失败,请检查接口返回');
  3235. }
  3236. },
  3237. openCopyDialog() {
  3238. this.copyForm = {
  3239. sourceId: this.calendars[0]?.id || '',
  3240. targetYear: currentYear + 1
  3241. };
  3242. this.copyDialogVisible = true;
  3243. },
  3244. async copyCalendar() {
  3245. const source = this.calendars.find(
  3246. (item) => item.id === this.copyForm.sourceId
  3247. );
  3248. if (!source || !this.copyForm.targetYear) {
  3249. this.$message.error('请选择源日历和目标年份');
  3250. return;
  3251. }
  3252. try {
  3253. await copyCalendar({
  3254. sourceId: this.copyForm.sourceId,
  3255. targetYear: this.copyForm.targetYear
  3256. });
  3257. await this.loadRemoteCalendarData();
  3258. this.invalidateConflictCache();
  3259. this.reloadBaseTable(1);
  3260. this.copyDialogVisible = false;
  3261. this.$message.success('复制成功,新日历默认禁用');
  3262. return;
  3263. } catch (e) {
  3264. this.$message.error(e?.message || '复制失败,请检查接口返回');
  3265. }
  3266. },
  3267. async locateCalendar(row) {
  3268. this.activeTab = 'view';
  3269. this.viewQuery.calendarType = row.calendarType;
  3270. this.viewQuery.year = row.applyYear[0] || currentYear;
  3271. this.viewQuery.date = `${this.viewQuery.year}-${String(
  3272. this.viewQuery.month
  3273. ).padStart(2, '0')}-01`;
  3274. this.viewMonth = `${this.viewQuery.year}-${String(
  3275. this.viewQuery.month
  3276. ).padStart(2, '0')}`;
  3277. await this.refreshRemoteView();
  3278. },
  3279. async changeViewCalendarType() {
  3280. await this.refreshRemoteView();
  3281. },
  3282. async changeViewMonth(value) {
  3283. if (!value) {
  3284. return;
  3285. }
  3286. const [year, month] = value.split('-');
  3287. this.viewQuery.year = Number(year);
  3288. this.viewQuery.month = Number(month);
  3289. this.viewQuery.date = `${year}-${month}-01`;
  3290. await this.refreshRemoteView();
  3291. },
  3292. async changeViewType(type) {
  3293. if (type === 'quarter') {
  3294. const quarterStartMonth =
  3295. Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
  3296. this.viewQuery.month = quarterStartMonth;
  3297. this.viewQuery.date = `${this.viewQuery.year}-${String(
  3298. quarterStartMonth
  3299. ).padStart(2, '0')}-01`;
  3300. this.viewMonth = `${this.viewQuery.year}-${String(
  3301. quarterStartMonth
  3302. ).padStart(2, '0')}`;
  3303. await this.refreshRemoteView();
  3304. return;
  3305. }
  3306. if (type === 'month') {
  3307. this.viewQuery.date = `${this.viewQuery.year}-${String(
  3308. this.viewQuery.month
  3309. ).padStart(2, '0')}-01`;
  3310. }
  3311. await this.refreshRemoteView();
  3312. },
  3313. async refreshRemoteView() {
  3314. await this.loadRemoteViewData(true);
  3315. this.plans = [];
  3316. this.planDataKey = '';
  3317. this.reloadPlanTable(1);
  3318. },
  3319. async refreshStatView() {
  3320. await this.loadRemoteViewData(true, {
  3321. viewType: 'month',
  3322. date: `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
  3323. 2,
  3324. '0'
  3325. )}-01`
  3326. });
  3327. },
  3328. getViewDetails(date) {
  3329. return this.details.filter(
  3330. (item) =>
  3331. this.isDetailInCurrentView(item) && item.calendarDate === date
  3332. );
  3333. },
  3334. getStatMonthDetails() {
  3335. const monthStart = dayjs(
  3336. `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
  3337. 2,
  3338. '0'
  3339. )}-01`
  3340. );
  3341. const monthEnd = monthStart.endOf('month');
  3342. return this.details.filter((item) => {
  3343. const date = dayjs(item.calendarDate);
  3344. return (
  3345. this.isDetailInCurrentView(item) &&
  3346. date.isValid() &&
  3347. (date.isSame(monthStart, 'day') || date.isAfter(monthStart)) &&
  3348. (date.isSame(monthEnd, 'day') || date.isBefore(monthEnd))
  3349. );
  3350. });
  3351. },
  3352. isDetailInCurrentView(item = {}) {
  3353. const targetType = Number(this.viewQuery.calendarType);
  3354. const detailType = Number(item.calendarType);
  3355. const typeMatched = targetType ? detailType === targetType : true;
  3356. const calendarIds = new Set(
  3357. this.getCurrentViewCalendars().map((calendar) => String(calendar.id))
  3358. );
  3359. const hasCalendarId =
  3360. item.calendarId !== undefined &&
  3361. item.calendarId !== null &&
  3362. item.calendarId !== '';
  3363. if (hasCalendarId && calendarIds.has(String(item.calendarId))) {
  3364. return true;
  3365. }
  3366. if (item.isViewDetail) {
  3367. return typeMatched;
  3368. }
  3369. return !hasCalendarId && typeMatched;
  3370. },
  3371. isDetailScheduled(item = {}) {
  3372. return (
  3373. Number(item.scheduleStatus) === 1 ||
  3374. Number(item.relationPlanCount || 0) > 0 ||
  3375. this.getDetailRelatedPlans([item]).length > 0
  3376. );
  3377. },
  3378. isWeekend(date) {
  3379. const week = dayjs(date).day();
  3380. return week === 0 || week === 6;
  3381. },
  3382. isRestDay(date, details) {
  3383. if (!details.length) {
  3384. return this.isWeekend(date);
  3385. }
  3386. return details.every((item) => [2, 3].includes(item.dateType));
  3387. },
  3388. getConflictText(date) {
  3389. const conflict = this.conflictList.find(
  3390. (item) => item.calendarDate === date
  3391. );
  3392. return conflict?.message || '';
  3393. },
  3394. matchStatusFilter(day) {
  3395. const filter = this.viewQuery.statusFilter;
  3396. if (!filter || !day.date) {
  3397. return true;
  3398. }
  3399. const matchMap = {
  3400. scheduled: day.scheduleStatus,
  3401. idle: !day.scheduleStatus && !day.isRest,
  3402. rest: day.isRest,
  3403. maintenance: day.isMaintenance,
  3404. temp: day.isTempAdjust,
  3405. conflict: day.isConflict
  3406. };
  3407. return matchMap[filter];
  3408. },
  3409. getDayClass(day) {
  3410. if (!day.date) {
  3411. return ['empty'];
  3412. }
  3413. if (day.isDisabledCalendar) {
  3414. return ['disabled-calendar'];
  3415. }
  3416. if (day.hiddenByFilter) {
  3417. return ['muted'];
  3418. }
  3419. if (day.isConflict) {
  3420. return ['conflict'];
  3421. }
  3422. if (day.isTempAdjust) {
  3423. return ['temp'];
  3424. }
  3425. if (day.isMaintenance) {
  3426. return ['maintenance'];
  3427. }
  3428. if (day.isHoliday) {
  3429. return ['holiday'];
  3430. }
  3431. if (day.isRest) {
  3432. return ['rest'];
  3433. }
  3434. if (day.scheduleStatus) {
  3435. return ['scheduled'];
  3436. }
  3437. return ['idle'];
  3438. },
  3439. getDateTypeText(day) {
  3440. if (!day.date) {
  3441. return '';
  3442. }
  3443. if (day.isHoliday) {
  3444. return day.holidayName || '法定节假日';
  3445. }
  3446. if (day.isRest && !day.details.length) {
  3447. return '休息日';
  3448. }
  3449. const first = day.details[0];
  3450. return first
  3451. ? this.getLabel(dateTypeOptions, first.dateType)
  3452. : '正常工作日';
  3453. },
  3454. getRangeText(details) {
  3455. if (!details.length) {
  3456. return '无有效时段';
  3457. }
  3458. return details
  3459. .map((item) => `${item.startTime}-${item.endTime}`)
  3460. .join('、');
  3461. },
  3462. buildAdjustSegments() {
  3463. return [
  3464. {
  3465. calendarDate: this.adjustForm.adjustDate,
  3466. startTime: this.adjustForm.startTime,
  3467. endTime: this.adjustForm.endTime,
  3468. dateType: this.getAdjustDateType(this.adjustForm.adjustType),
  3469. scheduleStatus: this.adjustForm.adjustType === 2 ? 0 : 1,
  3470. remark: this.adjustForm.applyReason || ''
  3471. }
  3472. ];
  3473. },
  3474. stringifySegments(segments) {
  3475. return JSON.stringify(segments || []);
  3476. },
  3477. parseSegmentContent(content) {
  3478. if (Array.isArray(content)) {
  3479. return content;
  3480. }
  3481. if (!content || typeof content !== 'string') {
  3482. return [];
  3483. }
  3484. try {
  3485. const parsed = JSON.parse(content);
  3486. return Array.isArray(parsed) ? parsed : [parsed];
  3487. } catch (e) {
  3488. const [timeRange] = content.split(' ');
  3489. const [startTime, endTime] = timeRange.split('-');
  3490. return startTime && endTime ? [{ startTime, endTime }] : [];
  3491. }
  3492. },
  3493. formatSegmentContent(content, row = {}) {
  3494. const segments = this.parseSegmentContent(content);
  3495. if (!segments.length) {
  3496. return content || '';
  3497. }
  3498. return segments
  3499. .map((segment) => {
  3500. const startTime = String(segment.startTime || '').slice(0, 5);
  3501. const endTime = String(segment.endTime || '').slice(0, 5);
  3502. const dateType =
  3503. segment.dateType || this.getAdjustDateType(row.adjustType);
  3504. const dateText = segment.calendarDate
  3505. ? `${segment.calendarDate} `
  3506. : '';
  3507. const typeText = this.getLabel(dateTypeOptions, dateType);
  3508. return `${dateText}${startTime}-${endTime} ${typeText}`.trim();
  3509. })
  3510. .join('、');
  3511. },
  3512. getAdjustApplicantName(row = {}) {
  3513. return (
  3514. row.applyUserName ||
  3515. row.applicantName ||
  3516. row.createUserName ||
  3517. row.userName ||
  3518. row.applyUserId ||
  3519. '-'
  3520. );
  3521. },
  3522. formatApproveNodes(approveNode) {
  3523. if (!approveNode) {
  3524. return [];
  3525. }
  3526. let nodes = approveNode;
  3527. if (typeof approveNode === 'string') {
  3528. try {
  3529. nodes = JSON.parse(approveNode);
  3530. } catch (e) {
  3531. return [
  3532. {
  3533. statusText: '审批记录',
  3534. approveOpinion: approveNode,
  3535. approveTime: '',
  3536. approveUserId: ''
  3537. }
  3538. ];
  3539. }
  3540. }
  3541. const list = Array.isArray(nodes) ? nodes : [nodes];
  3542. return list
  3543. .filter((item) => item && typeof item === 'object')
  3544. .map((item) => {
  3545. const status = Number(item.applyStatus ?? item.approveStatus);
  3546. return {
  3547. statusText:
  3548. this.getLabel(approvalStatusOptions, status) ||
  3549. item.statusText ||
  3550. '审批记录',
  3551. approveOpinion:
  3552. item.approveOpinion || item.opinion || item.remark || '',
  3553. approveTime: String(item.approveTime || item.createTime || '')
  3554. .replace('T', ' ')
  3555. .slice(0, 19),
  3556. approveUserId: item.approveUserId || item.userId || ''
  3557. };
  3558. });
  3559. },
  3560. async openDayDrawer(day) {
  3561. if (!day.date || day.hiddenByFilter) {
  3562. return;
  3563. }
  3564. if (day.isDisabledCalendar) {
  3565. this.$message.warning('当前类型日历已禁用,不可编辑');
  3566. return;
  3567. }
  3568. await this.loadRemoteViewData();
  3569. const details = this.getViewDetails(day.date);
  3570. const relatedPlans = this.getDetailRelatedPlans(details);
  3571. this.currentDay = {
  3572. ...day,
  3573. details,
  3574. relatedPlans,
  3575. planCount: this.getDetailPlanCount(details)
  3576. };
  3577. this.dayDrawerVisible = true;
  3578. },
  3579. openAdjustDialog(day = {}) {
  3580. this.adjustForm = this.getDefaultAdjustForm(day);
  3581. const calendar = this.enabledCalendars.find(
  3582. (item) => item.calendarType === this.viewQuery.calendarType
  3583. );
  3584. if (calendar) {
  3585. this.adjustForm.calendarId = calendar.id;
  3586. }
  3587. this.adjustDialogVisible = true;
  3588. this.$nextTick(() => {
  3589. this.$refs.adjustForm?.clearValidate?.();
  3590. });
  3591. },
  3592. submitAdjust() {
  3593. this.$refs.adjustForm.validate((valid) => {
  3594. if (!valid) {
  3595. return;
  3596. }
  3597. if (!this.adjustForm.startTime || !this.adjustForm.endTime) {
  3598. this.$message.error('请选择调整时段');
  3599. return;
  3600. }
  3601. const calendar = this.calendars.find(
  3602. (item) => item.id === this.adjustForm.calendarId
  3603. );
  3604. if (!calendar) {
  3605. this.$message.error('请选择关联日历');
  3606. return;
  3607. }
  3608. const saveApply = async () => {
  3609. const originalDetails = this.details.filter(
  3610. (item) =>
  3611. item.calendarId === calendar.id &&
  3612. item.calendarDate === this.adjustForm.adjustDate
  3613. );
  3614. const newSegments = this.buildAdjustSegments();
  3615. const adjustRow = {
  3616. id: Date.now(),
  3617. applyNo: createCode('ADJ'),
  3618. calendarId: calendar.id,
  3619. calendarName: calendar.calendarName,
  3620. adjustType: this.adjustForm.adjustType,
  3621. adjustDate: this.adjustForm.adjustDate,
  3622. oldContent: this.getRangeText(originalDetails),
  3623. originalDetails: clone(originalDetails),
  3624. newContent: this.stringifySegments(newSegments),
  3625. newContentText: `${this.adjustForm.startTime}-${
  3626. this.adjustForm.endTime
  3627. } ${this.getLabel(
  3628. adjustTypeOptions,
  3629. this.adjustForm.adjustType
  3630. )}`,
  3631. effectiveTime: this.adjustForm.effectiveTime,
  3632. expireTime: this.adjustForm.expireTime,
  3633. applyReason: this.adjustForm.applyReason,
  3634. applyStatus: 0,
  3635. isConflict: 0,
  3636. applyUserName: '当前用户',
  3637. applyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  3638. approveNode: '',
  3639. canReactivate: true
  3640. };
  3641. try {
  3642. await applyCalendarAdjust({
  3643. calendarId: calendar.id,
  3644. adjustType: this.adjustForm.adjustType,
  3645. adjustDate: this.adjustForm.adjustDate,
  3646. oldContent: adjustRow.oldContent,
  3647. newContent: adjustRow.newContent,
  3648. effectiveTime: this.adjustForm.effectiveTime,
  3649. expireTime: this.adjustForm.expireTime,
  3650. applyReason: this.adjustForm.applyReason
  3651. });
  3652. await this.loadRemoteAdjustData();
  3653. this.invalidateConflictCache();
  3654. this.reloadAdjustTable(1);
  3655. this.adjustDialogVisible = false;
  3656. this.$message.success('已提交审批');
  3657. return;
  3658. } catch (e) {
  3659. this.$message.error(e?.message || '提交审批失败,请检查接口返回');
  3660. }
  3661. };
  3662. saveApply();
  3663. });
  3664. },
  3665. openApproveDialog(row, status) {
  3666. if (row.applyStatus === 3) {
  3667. this.$message.warning('已失效单据仅可查询追溯,无法重新生效');
  3668. return;
  3669. }
  3670. this.approveForm = {
  3671. row,
  3672. status,
  3673. opinion: status === 1 ? '审批通过' : ''
  3674. };
  3675. this.approveDialogVisible = true;
  3676. },
  3677. async confirmApproveAdjust() {
  3678. if (this.approveForm.status === 2 && !this.approveForm.opinion) {
  3679. this.$message.error('请输入驳回原因');
  3680. return;
  3681. }
  3682. await this.approveAdjust(
  3683. this.approveForm.row,
  3684. this.approveForm.status,
  3685. this.approveForm.opinion
  3686. );
  3687. this.approveDialogVisible = false;
  3688. },
  3689. async approveAdjust(row, status, opinion = '') {
  3690. try {
  3691. await approveCalendarAdjust({
  3692. id: row.id,
  3693. applyStatus: status,
  3694. approveOpinion: opinion
  3695. });
  3696. await this.loadRemoteAdjustData();
  3697. await this.loadRemoteViewData(true);
  3698. this.invalidateConflictCache();
  3699. this.reloadAdjustTable();
  3700. if (status === 1) {
  3701. this.reloadPlanTable(1);
  3702. }
  3703. this.$message.success(status === 1 ? '审批通过并已生效' : '已驳回');
  3704. return;
  3705. } catch (e) {
  3706. this.$message.error(e?.message || '审批失败,请检查接口返回');
  3707. }
  3708. },
  3709. getAdjustDateType(adjustType) {
  3710. return (
  3711. {
  3712. 1: 5,
  3713. 2: 2,
  3714. 3: 4,
  3715. 4: 5
  3716. }[adjustType] || 5
  3717. );
  3718. },
  3719. expireAdjustments() {
  3720. // 已停用:临时调整失效由后端任务/接口处理。
  3721. },
  3722. async checkSingleDay(day) {
  3723. if (!day.date || !day.details.length) {
  3724. this.$message.success('当前日期未发现冲突');
  3725. return;
  3726. }
  3727. try {
  3728. const results = await Promise.all(
  3729. day.details.map((detail) =>
  3730. checkCalendarConflict({
  3731. calendarType:
  3732. detail.calendarType || this.viewQuery.calendarType,
  3733. calendarDate: day.date,
  3734. startTime: detail.startTime,
  3735. endTime: detail.endTime,
  3736. dateType: detail.dateType,
  3737. scheduleStatus: detail.scheduleStatus,
  3738. excludeDetailId: detail.id
  3739. })
  3740. )
  3741. );
  3742. const conflicts = results
  3743. .map((item) => item?.conflictDetail || [])
  3744. .flat();
  3745. if (conflicts.length) {
  3746. this.$message.warning(`发现 ${conflicts.length} 条冲突`);
  3747. } else {
  3748. this.$message.success('当前日期未发现冲突');
  3749. }
  3750. } catch (e) {
  3751. this.$message.error(e?.message || '冲突检测失败,请检查接口返回');
  3752. }
  3753. },
  3754. async toggleScheduleStatus(status) {
  3755. if (!this.currentDay.date || this.currentDay.isDisabledCalendar) {
  3756. this.$message.warning('当前日历不可编辑');
  3757. return;
  3758. }
  3759. const calendar = this.enabledCalendars.find(
  3760. (item) => item.calendarType === this.viewQuery.calendarType
  3761. );
  3762. if (!calendar) {
  3763. this.$message.warning('当前类型日历已禁用');
  3764. return;
  3765. }
  3766. const currentDetails = this.getViewDetails(this.currentDay.date);
  3767. const nextDetails = currentDetails.map((detail) => ({
  3768. ...detail,
  3769. calendarId: calendar.id,
  3770. calendarDate: this.currentDay.date,
  3771. scheduleStatus: status
  3772. }));
  3773. try {
  3774. await Promise.all(
  3775. nextDetails.map((nextDetail) =>
  3776. saveCalendarDetail({
  3777. id: nextDetail.id,
  3778. calendarId: nextDetail.calendarId,
  3779. calendarDate: nextDetail.calendarDate,
  3780. startTime: nextDetail.startTime,
  3781. endTime: nextDetail.endTime,
  3782. dateType: nextDetail.dateType || 1,
  3783. scheduleStatus: nextDetail.scheduleStatus,
  3784. remark: nextDetail.remark,
  3785. forceSave: true
  3786. })
  3787. )
  3788. );
  3789. await this.loadRemoteViewData(true);
  3790. const details = this.getViewDetails(this.currentDay.date);
  3791. const relatedPlans = this.getDetailRelatedPlans(details);
  3792. this.currentDay = {
  3793. ...this.currentDay,
  3794. details,
  3795. relatedPlans,
  3796. planCount: this.getDetailPlanCount(details),
  3797. scheduleStatus: !!status
  3798. };
  3799. this.reloadPlanTable(1);
  3800. this.invalidateConflictCache();
  3801. this.$message.success(status ? '已完成排班并回写状态' : '已取消排班');
  3802. return;
  3803. } catch (e) {
  3804. this.$message.error(e?.message || '排班状态回写失败,请检查接口返回');
  3805. }
  3806. },
  3807. getConflictDataKey() {
  3808. return [
  3809. this.viewQuery.year || '',
  3810. this.getConflictCheckTypes().join(',')
  3811. ].join('|');
  3812. },
  3813. getConflictCheckTypes() {
  3814. return this.calendarTypeOptions
  3815. .map((item) => Number(item.value))
  3816. .filter((type) => [1, 2, 3].includes(type));
  3817. },
  3818. invalidateConflictCache() {
  3819. this.conflictDataKey = '';
  3820. },
  3821. async runGlobalConflictCheck(showMessage = true, force = showMessage) {
  3822. const dataKey = this.getConflictDataKey();
  3823. if (!force && this.conflictDataKey === dataKey) {
  3824. this.reloadConflictTable(1);
  3825. return;
  3826. }
  3827. if (this.conflictCheckPromise) {
  3828. await this.conflictCheckPromise;
  3829. return;
  3830. }
  3831. try {
  3832. const conflictTypes = this.getConflictCheckTypes();
  3833. this.conflictCheckPromise = Promise.all(
  3834. conflictTypes.map((calendarType) =>
  3835. checkAllCalendarConflict({
  3836. year: this.viewQuery.year,
  3837. calendarType
  3838. })
  3839. )
  3840. );
  3841. const list = await this.conflictCheckPromise;
  3842. this.conflictList = this.uniqueConflicts(
  3843. list.flatMap((data) => this.normalizeRemoteConflicts(data))
  3844. );
  3845. this.conflictDataKey = dataKey;
  3846. this.reloadConflictTable(1);
  3847. if (showMessage) {
  3848. this.$message[this.conflictList.length ? 'warning' : 'success'](
  3849. this.conflictList.length
  3850. ? `检测完成,发现 ${this.conflictList.length} 条冲突`
  3851. : '检测完成,未发现冲突'
  3852. );
  3853. }
  3854. return;
  3855. } catch (e) {
  3856. this.conflictList = [];
  3857. this.conflictDataKey = '';
  3858. if (showMessage) {
  3859. this.$message.error(e?.message || '冲突检测失败,请检查接口返回');
  3860. }
  3861. } finally {
  3862. this.conflictCheckPromise = null;
  3863. }
  3864. this.reloadConflictTable(1);
  3865. },
  3866. checkConflict({
  3867. calendarType,
  3868. calendarDate,
  3869. startTime,
  3870. endTime,
  3871. dateType = 1,
  3872. scheduleStatus = 1,
  3873. sourceId
  3874. }) {
  3875. const sameDay = this.details.filter((item) => {
  3876. const calendar = this.calendars.find(
  3877. (calendarItem) => calendarItem.id === item.calendarId
  3878. );
  3879. return (
  3880. item.id !== sourceId &&
  3881. item.calendarDate === calendarDate &&
  3882. calendar?.status === 1 &&
  3883. isOverlap(startTime, endTime, item.startTime, item.endTime)
  3884. );
  3885. });
  3886. const conflicts = [];
  3887. sameDay.forEach((item) => {
  3888. const targetCalendar = this.calendars.find(
  3889. (calendarItem) => calendarItem.id === item.calendarId
  3890. );
  3891. const targetType = targetCalendar?.calendarType;
  3892. const source = {
  3893. calendarType,
  3894. dateType,
  3895. scheduleStatus
  3896. };
  3897. const target = {
  3898. calendarType: targetType,
  3899. dateType: item.dateType,
  3900. scheduleStatus: item.scheduleStatus
  3901. };
  3902. const timeRange = `${Math.max(
  3903. toMinute(startTime),
  3904. toMinute(item.startTime)
  3905. )}-${Math.min(toMinute(endTime), toMinute(item.endTime))}`;
  3906. const readableRange = `${startTime}-${endTime}`;
  3907. if (
  3908. [source.calendarType, target.calendarType].includes(1) &&
  3909. [source.calendarType, target.calendarType].includes(2) &&
  3910. [source, target].some(
  3911. (segment) => segment.calendarType === 2 && segment.dateType === 4
  3912. )
  3913. ) {
  3914. conflicts.push({
  3915. id: `${calendarDate}-${timeRange}-1-2`,
  3916. calendarDate,
  3917. timeRange: readableRange,
  3918. scene: '生产日历 VS 设备维护日历',
  3919. level: '高',
  3920. message: '当前生产作业时段设备停机,无法排产'
  3921. });
  3922. }
  3923. if (
  3924. [source.calendarType, target.calendarType].includes(1) &&
  3925. [source.calendarType, target.calendarType].includes(3) &&
  3926. [source, target].some(
  3927. (segment) =>
  3928. segment.calendarType === 3 &&
  3929. (segment.dateType === 2 ||
  3930. segment.dateType === 3 ||
  3931. segment.scheduleStatus === 0)
  3932. )
  3933. ) {
  3934. conflicts.push({
  3935. id: `${calendarDate}-${timeRange}-1-3`,
  3936. calendarDate,
  3937. timeRange: readableRange,
  3938. scene: '生产日历 VS 人员排班日历',
  3939. level: '高',
  3940. message: '当前生产时段无在岗人员,人力缺失'
  3941. });
  3942. }
  3943. if (
  3944. [source.calendarType, target.calendarType].includes(2) &&
  3945. [source.calendarType, target.calendarType].includes(3) &&
  3946. [source, target].some(
  3947. (segment) =>
  3948. segment.calendarType === 3 &&
  3949. (segment.dateType === 2 ||
  3950. segment.dateType === 3 ||
  3951. segment.scheduleStatus === 0)
  3952. )
  3953. ) {
  3954. conflicts.push({
  3955. id: `${calendarDate}-${timeRange}-2-3`,
  3956. calendarDate,
  3957. timeRange: readableRange,
  3958. scene: '设备维护日历 VS 人员排班日历',
  3959. level: '中',
  3960. message: '设备检修时段维保人员缺失'
  3961. });
  3962. }
  3963. });
  3964. if (calendarType === 1) {
  3965. const hasStaff = sameDay.some((item) => {
  3966. const targetCalendar = this.calendars.find(
  3967. (calendarItem) => calendarItem.id === item.calendarId
  3968. );
  3969. return (
  3970. targetCalendar?.calendarType === 3 &&
  3971. item.scheduleStatus === 1 &&
  3972. ![2, 3].includes(item.dateType)
  3973. );
  3974. });
  3975. if (!hasStaff) {
  3976. conflicts.push({
  3977. id: `${calendarDate}-${startTime}-${endTime}-1-3-missing`,
  3978. calendarDate,
  3979. timeRange: `${startTime}-${endTime}`,
  3980. scene: '生产日历 VS 人员排班日历',
  3981. level: '高',
  3982. message: '当前生产时段无在岗人员,人力缺失'
  3983. });
  3984. }
  3985. }
  3986. if (calendarType === 2) {
  3987. const hasMaintainer = sameDay.some((item) => {
  3988. const targetCalendar = this.calendars.find(
  3989. (calendarItem) => calendarItem.id === item.calendarId
  3990. );
  3991. return (
  3992. targetCalendar?.calendarType === 3 &&
  3993. item.scheduleStatus === 1 &&
  3994. ![2, 3].includes(item.dateType)
  3995. );
  3996. });
  3997. if (!hasMaintainer) {
  3998. conflicts.push({
  3999. id: `${calendarDate}-${startTime}-${endTime}-2-3-missing`,
  4000. calendarDate,
  4001. timeRange: `${startTime}-${endTime}`,
  4002. scene: '设备维护日历 VS 人员排班日历',
  4003. level: '中',
  4004. message: '设备检修时段维保人员缺失'
  4005. });
  4006. }
  4007. }
  4008. return conflicts;
  4009. },
  4010. uniqueConflicts(conflicts) {
  4011. const map = {};
  4012. conflicts.forEach((item) => {
  4013. map[item.id] = item;
  4014. });
  4015. return Object.values(map);
  4016. },
  4017. async locateConflict(row) {
  4018. this.activeTab = 'view';
  4019. this.viewQuery.viewType = 'day';
  4020. this.viewMonth = dayjs(row.calendarDate).format('YYYY-MM');
  4021. this.viewQuery.year = Number(dayjs(row.calendarDate).format('YYYY'));
  4022. this.viewQuery.month = Number(dayjs(row.calendarDate).format('M'));
  4023. this.viewQuery.date = row.calendarDate;
  4024. this.viewQuery.statusFilter = 'conflict';
  4025. await this.refreshRemoteView();
  4026. },
  4027. handleTabChange() {
  4028. if (this.activeTab === 'base') {
  4029. this.reloadBaseTable(1);
  4030. }
  4031. if (this.activeTab === 'view') {
  4032. this.refreshRemoteView();
  4033. }
  4034. if (this.activeTab === 'stat') {
  4035. this.refreshStatView();
  4036. }
  4037. if (this.activeTab === 'conflict') {
  4038. this.runGlobalConflictCheck(false);
  4039. }
  4040. if (this.activeTab === 'adjust') {
  4041. this.reloadAdjustTable(1);
  4042. }
  4043. if (this.activeTab === 'plan') {
  4044. this.reloadPlanTable(1);
  4045. }
  4046. },
  4047. getPlanDataKey() {
  4048. const calendar = this.getCurrentViewCalendar();
  4049. return [
  4050. this.getRemoteCalendarId(calendar) || '',
  4051. this.viewQuery.year || '',
  4052. this.viewQuery.month || ''
  4053. ].join('|');
  4054. },
  4055. async ensurePlanRefreshData(force = false) {
  4056. const calendar = this.getCurrentViewCalendar();
  4057. const calendarId = this.getRemoteCalendarId(calendar);
  4058. if (!calendarId) {
  4059. this.plans = [];
  4060. this.planDataKey = '';
  4061. return;
  4062. }
  4063. const dataKey = this.getPlanDataKey();
  4064. if (!force && this.planDataKey === dataKey && this.plans.length) {
  4065. return;
  4066. }
  4067. if (!force && this.planDataPromise) {
  4068. await this.planDataPromise;
  4069. return;
  4070. }
  4071. try {
  4072. this.planDataPromise = refreshCalendarPlan({
  4073. calendarId,
  4074. startDate: `${this.viewQuery.year}-${String(
  4075. this.viewQuery.month
  4076. ).padStart(2, '0')}-01`,
  4077. endDate: dayjs(
  4078. `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
  4079. 2,
  4080. '0'
  4081. )}-01`
  4082. )
  4083. .endOf('month')
  4084. .format('YYYY-MM-DD')
  4085. });
  4086. const data = await this.planDataPromise;
  4087. this.plans = this.normalizeRemotePlans(data);
  4088. this.planDataKey = dataKey;
  4089. } catch (e) {
  4090. this.plans = [];
  4091. this.planDataKey = '';
  4092. } finally {
  4093. this.planDataPromise = null;
  4094. }
  4095. },
  4096. async refreshPlans() {
  4097. try {
  4098. await this.ensurePlanRefreshData(true);
  4099. await this.loadRemoteViewData(true);
  4100. this.reloadPlanTable(1);
  4101. this.$message.success(`已刷新受影响计划,共 ${this.plans.length} 条`);
  4102. } catch (e) {
  4103. this.plans = [];
  4104. this.reloadPlanTable(1);
  4105. }
  4106. },
  4107. reschedulePlans() {
  4108. this.$message.warning('批量重排需要后端返回可重排计划后执行');
  4109. this.reloadPlanTable(1);
  4110. },
  4111. async jumpPlanToCalendar(row) {
  4112. const calendar = this.calendars.find(
  4113. (item) => item.id === row.calendarId
  4114. );
  4115. this.activeTab = 'view';
  4116. this.viewQuery.calendarType = calendar?.calendarType || 1;
  4117. this.viewQuery.viewType = 'day';
  4118. this.viewQuery.date = row.calendarDate;
  4119. this.viewMonth = dayjs(row.calendarDate).format('YYYY-MM');
  4120. await this.changeViewMonth(this.viewMonth);
  4121. this.viewQuery.date = row.calendarDate;
  4122. this.$nextTick(async () => {
  4123. await this.loadRemoteViewData(true);
  4124. const details = this.getViewDetails(row.calendarDate);
  4125. const relatedPlans = this.getDetailRelatedPlans(details);
  4126. const day = {
  4127. key: row.calendarDate,
  4128. date: row.calendarDate,
  4129. dayText: dayjs(row.calendarDate).format('D'),
  4130. details,
  4131. relatedPlans,
  4132. planCount: this.getDetailPlanCount(details),
  4133. scheduleStatus: details.some((item) => item.scheduleStatus === 1),
  4134. isConflict: details.some((item) => item.isConflict === 1),
  4135. isTempAdjust: details.some((item) => item.isTempAdjust === 1),
  4136. isMaintenance: details.some((item) => item.dateType === 4),
  4137. isRest: this.isRestDay(row.calendarDate, details),
  4138. isDisabledCalendar: !this.hasEnabledCalendarForView(),
  4139. conflictText: this.getConflictText(row.calendarDate)
  4140. };
  4141. this.openDayDrawer(day);
  4142. });
  4143. },
  4144. exportCalendar() {
  4145. const rows = this.filteredCalendars.map((item) => ({
  4146. calendarCode: item.calendarCode,
  4147. calendarName: item.calendarName,
  4148. calendarType: this.getLabel(calendarTypeOptions, item.calendarType),
  4149. applyYear: item.applyYear.join(','),
  4150. status: item.status ? '启用' : '禁用'
  4151. }));
  4152. this.downloadJson(
  4153. rows,
  4154. `factory-calendar-${dayjs().format('YYYYMMDDHHmmss')}.json`
  4155. );
  4156. this.$message.success(`导出成功,共 ${rows.length} 条`);
  4157. },
  4158. openAdjustDetail(row) {
  4159. this.adjustDetail = clone(row);
  4160. this.adjustDetailVisible = true;
  4161. },
  4162. exportAdjustRecord() {
  4163. if (!this.adjustDetail.id) {
  4164. return;
  4165. }
  4166. this.downloadJson(
  4167. this.adjustDetail,
  4168. `calendar-adjust-${this.adjustDetail.applyNo}.json`
  4169. );
  4170. this.$message.success('追溯记录已导出');
  4171. },
  4172. downloadJson(data, filename) {
  4173. const blob = new Blob([JSON.stringify(data, null, 2)], {
  4174. type: 'application/json;charset=utf-8'
  4175. });
  4176. const url = URL.createObjectURL(blob);
  4177. const link = document.createElement('a');
  4178. link.href = url;
  4179. link.download = filename;
  4180. link.click();
  4181. URL.revokeObjectURL(url);
  4182. },
  4183. getCalendarCount(type) {
  4184. return this.calendars.filter((item) => item.calendarType === type)
  4185. .length;
  4186. },
  4187. getStatPercent(value) {
  4188. const total = Math.max(...this.monthStats.map((item) => item.value), 1);
  4189. return Math.round((value / total) * 100);
  4190. },
  4191. calendarTypeTag(type) {
  4192. return {
  4193. 1: '',
  4194. 2: 'warning',
  4195. 3: 'success'
  4196. }[type];
  4197. },
  4198. approvalStatusTag(status) {
  4199. return {
  4200. 0: 'warning',
  4201. 1: 'success',
  4202. 2: 'danger',
  4203. 3: 'info'
  4204. }[status];
  4205. },
  4206. getLabel(options, value) {
  4207. return options.find((item) => item.value === value)?.label || '';
  4208. }
  4209. }
  4210. };
  4211. </script>
  4212. <style lang="scss">
  4213. @import './components/factoryCalendar.scss';
  4214. </style>