index.vue 136 KB

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