| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244 |
- <template>
- <div class="ele-body factory-calendar">
- <el-card shadow="never" class="factory-calendar-card">
- <div class="page-header">
- <div class="header-actions">
- <el-button
- size="small"
- type="primary"
- icon="el-icon-plus"
- class="ele-btn-icon"
- @click="openCalendarDialog()"
- >
- 新增日历
- </el-button>
- </div>
- </div>
- <el-tabs v-model="activeTab" @tab-click="handleTabChange">
- <el-tab-pane label="基础管理" name="base">
- <base-manage ref="baseManage" :vm="vm" />
- </el-tab-pane>
- <el-tab-pane label="多维日历视图" name="view">
- <multi-calendar-view :vm="vm" />
- </el-tab-pane>
- <el-tab-pane label="冲突检测" name="conflict">
- <conflict-panel ref="conflictPanel" :vm="vm" />
- </el-tab-pane>
- <el-tab-pane label="临时调整审批" name="adjust">
- <adjust-approval ref="adjustApproval" :vm="vm" />
- </el-tab-pane>
- <el-tab-pane label="数据统计" name="stat">
- <statistics-view :vm="vm" />
- </el-tab-pane>
- </el-tabs>
- </el-card>
- <ele-modal
- :visible.sync="calendarDialogVisible"
- :title="calendarDialogTitle"
- width="900px"
- custom-class="ele-dialog-form factory-calendar-dialog"
- :close-on-click-modal="false"
- >
- <el-form
- ref="calendarForm"
- :model="calendarForm"
- :rules="calendarRules"
- label-width="110px"
- class="el-form-box"
- >
- <el-form-item label="日历编码">
- <el-input v-model="calendarForm.calendarCode" disabled />
- </el-form-item>
- <el-form-item label="日历名称" prop="calendarName">
- <el-input v-model="calendarForm.calendarName" maxlength="128" />
- </el-form-item>
- <el-form-item label="日历类型" prop="calendarType">
- <el-select
- v-model="calendarForm.calendarType"
- :disabled="!!calendarForm.id"
- style="width: 100%"
- >
- <el-option
- v-for="item in calendarTypeOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="适用年份" prop="applyYear">
- <el-select v-model="calendarFormApplyYear" style="width: 100%">
- <el-option
- v-for="year in yearOptions"
- :key="year"
- :label="year"
- :value="year"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="适用月份" prop="applyMonth">
- <el-select
- v-model="calendarForm.applyMonth"
- multiple
- style="width: 100%"
- @change="handleApplyMonthChange"
- >
- <el-option
- v-for="month in monthOptions"
- :key="month.value"
- :label="month.label"
- :value="month.value"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-switch
- v-model="calendarForm.status"
- :active-value="1"
- :inactive-value="0"
- active-text="启用"
- inactive-text="禁用"
- />
- </el-form-item>
- <el-form-item label="周休设置">
- <div class="rest-rule-editor">
- <el-radio-group v-model="calendarForm.restMode">
- <el-radio-button
- v-for="item in restModeOptions"
- :key="item.value"
- :label="item.value"
- >
- {{ item.label }}
- </el-radio-button>
- </el-radio-group>
- <el-checkbox-group
- v-if="calendarForm.restMode === 'custom'"
- v-model="calendarForm.restWeekdays"
- class="custom-rest-weekdays"
- >
- <el-checkbox-button
- v-for="item in restWeekdayOptions"
- :key="item.value"
- :label="item.value"
- >
- {{ item.label }}
- </el-checkbox-button>
- </el-checkbox-group>
- <div class="rule-tip">
- 双休为周六周日休,单休为周日休,大小周为周日固定休且隔周周六休,自定义可勾选任意休息星期。
- </div>
- </div>
- </el-form-item>
- <el-form-item v-if="showLegalHolidaySection" label="法定节假日">
- <div class="legal-holiday-section">
- <div class="legal-holiday-header">
- <div>
- <div class="legal-holiday-title">法定节假日</div>
- <div class="legal-holiday-desc">
- 默认休息,点选日期后按上班处理
- </div>
- </div>
- <span class="legal-holiday-count">
- {{
- legalHolidayLoading
- ? '同步节假日中...'
- : `${legalHolidaySourceText},已设上班 ${legalHolidayVisibleWorkCount} 天`
- }}
- </span>
- </div>
- <div v-if="legalHolidayGroups.length" class="legal-holiday-list">
- <div
- v-for="group in legalHolidayGroups"
- :key="group.name"
- class="legal-holiday-group"
- >
- <div class="legal-holiday-group-head">
- <strong>{{ group.name }}</strong>
- <span>{{ group.dates.length }}天</span>
- </div>
- <div class="legal-holiday-dates">
- <button
- v-for="item in group.dates"
- :key="item.calendarDate"
- type="button"
- class="legal-holiday-date"
- :class="{
- 'is-work': calendarForm.legalHolidayWorkDates.includes(
- item.calendarDate
- )
- }"
- @click="toggleLegalHolidayWorkStatus(item.calendarDate)"
- >
- <span>{{ formatHolidayDate(item.calendarDate) }}</span>
- <em>
- {{
- calendarForm.legalHolidayWorkDates.includes(
- item.calendarDate
- )
- ? '上班'
- : '休息'
- }}
- </em>
- </button>
- </div>
- </div>
- </div>
- </div>
- </el-form-item>
- <el-form-item label="特殊日期">
- <div class="holiday-rule-editor">
- <div
- v-for="(rule, index) in calendarForm.holidayRules"
- :key="rule.id"
- class="holiday-rule-row"
- >
- <el-date-picker
- v-model="rule.calendarDate"
- type="date"
- value-format="yyyy-MM-dd"
- placeholder="选择日期"
- style="width: 160px"
- />
- <el-select
- v-model="rule.dateType"
- placeholder="日期类型"
- style="width: 130px"
- >
- <el-option label="上班" :value="1" />
- <el-option label="休息" :value="2" />
- <el-option label="法定节假日" :value="3" />
- </el-select>
- <el-input
- v-model="rule.remark"
- placeholder="备注"
- maxlength="80"
- style="flex: 1"
- />
- <el-button
- type="text"
- icon="el-icon-delete"
- class="danger-link"
- @click="removeHolidayRule(index)"
- >
- 删除
- </el-button>
- </div>
- <el-button size="small" icon="el-icon-plus" @click="addHolidayRule">
- 新增特殊日期
- </el-button>
- <div class="rule-tip">
- 特殊日期优先级高于周休设置,可把周末设为上班,也可把工作日设为休息或法定节假日。
- </div>
- </div>
- </el-form-item>
- <el-form-item label="备注">
- <el-input
- v-model="calendarForm.remark"
- type="textarea"
- :rows="3"
- maxlength="500"
- show-word-limit
- />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button type="primary" @click="saveCalendar">确定</el-button>
- <el-button @click="calendarDialogVisible = false">取消</el-button>
- </div>
- </ele-modal>
- <ele-modal
- :visible.sync="copyDialogVisible"
- title="复制日历配置"
- width="520px"
- custom-class="ele-dialog-form"
- >
- <el-form :model="copyForm" label-width="100px" class="el-form-box">
- <el-form-item label="源日历">
- <el-select v-model="copyForm.sourceId" style="width: 100%">
- <el-option
- v-for="item in calendars"
- :key="item.id"
- :label="item.calendarName"
- :value="item.id"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="目标年份">
- <el-select v-model="copyForm.targetYear" style="width: 100%">
- <el-option
- v-for="year in yearOptions"
- :key="year"
- :label="year"
- :value="year"
- />
- </el-select>
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button type="primary" @click="copyCalendar">确定</el-button>
- <el-button @click="copyDialogVisible = false">取消</el-button>
- </div>
- </ele-modal>
- <ele-modal
- :visible.sync="adjustDialogVisible"
- title="临时调整申请"
- width="720px"
- custom-class="ele-dialog-form"
- :close-on-click-modal="false"
- >
- <el-form
- ref="adjustForm"
- :model="adjustForm"
- :rules="adjustRules"
- label-width="120px"
- class="el-form-box"
- >
- <el-form-item label="关联日历" prop="calendarId">
- <el-select v-model="adjustForm.calendarId" style="width: 100%">
- <el-option
- v-for="item in enabledCalendars"
- :key="item.id"
- :label="item.calendarName"
- :value="item.id"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="调整类型" prop="adjustType">
- <el-select v-model="adjustForm.adjustType" style="width: 100%">
- <el-option
- v-for="item in adjustTypeOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="调整日期" prop="adjustDate">
- <el-date-picker
- v-model="adjustForm.adjustDate"
- type="date"
- value-format="yyyy-MM-dd"
- style="width: 100%"
- />
- </el-form-item>
- <el-form-item label="调整时段" required>
- <div class="inline-time">
- <el-time-select
- v-model="adjustForm.startTime"
- placeholder="开始时间"
- :picker-options="timePickerOptions"
- />
- <span>至</span>
- <el-time-select
- v-model="adjustForm.endTime"
- placeholder="结束时间"
- :picker-options="getTimePickerOptions(adjustForm.startTime)"
- />
- </div>
- </el-form-item>
- <el-form-item label="生效时间" prop="effectiveTime">
- <el-date-picker
- v-model="adjustForm.effectiveTime"
- type="datetime"
- value-format="yyyy-MM-dd HH:mm:ss"
- style="width: 100%"
- />
- </el-form-item>
- <el-form-item label="失效时间" prop="expireTime">
- <el-date-picker
- v-model="adjustForm.expireTime"
- type="datetime"
- value-format="yyyy-MM-dd HH:mm:ss"
- style="width: 100%"
- />
- </el-form-item>
- <el-form-item label="调整原因" prop="applyReason">
- <el-input
- v-model="adjustForm.applyReason"
- type="textarea"
- :rows="3"
- maxlength="500"
- show-word-limit
- />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button type="primary" @click="submitAdjust">提交审批</el-button>
- <el-button @click="adjustDialogVisible = false">取消</el-button>
- </div>
- </ele-modal>
- <el-drawer
- :visible.sync="dayDrawerVisible"
- :title="dayDrawerTitle"
- size="460px"
- append-to-body
- custom-class="calendar-day-drawer"
- >
- <div class="drawer-body" v-if="currentDay.date">
- <div class="drawer-summary">
- <div class="summary-date">
- <span>{{ drawerDateMonth }}</span>
- <strong>{{ drawerDateDay }}</strong>
- </div>
- <div class="summary-info">
- <div class="summary-title">
- {{ getDateTypeText(currentDay) }}
- <el-tag
- size="mini"
- :type="currentDay.scheduleStatus ? 'success' : 'info'"
- >
- {{ currentDay.scheduleStatus ? '已排班' : '未排班' }}
- </el-tag>
- </div>
- <p>{{ getRangeText(currentDay.details) }}</p>
- <div class="summary-tags">
- <el-tag v-if="currentDay.isRest" size="mini" type="info">
- 休息/节假日
- </el-tag>
- <el-tag v-if="currentDay.isTempAdjust" size="mini">
- 临时调整
- </el-tag>
- <el-tag v-if="currentDay.isConflict" size="mini" type="danger">
- 存在冲突
- </el-tag>
- <el-tag v-if="currentDay.planCount" size="mini" type="warning">
- {{ currentDay.planCount }} 个计划
- </el-tag>
- </div>
- </div>
- </div>
- <div class="drawer-section">
- <div class="drawer-title">
- <span>时段明细</span>
- <em>{{ currentDay.details.length }} 条</em>
- </div>
- <div class="detail-timeline">
- <div
- v-for="detail in currentDay.details"
- :key="detail.id"
- class="detail-item"
- >
- <div class="detail-time">
- <strong>{{ detail.startTime }} - {{ detail.endTime }}</strong>
- </div>
- <div class="detail-content">
- <div>
- <strong>{{
- detail.dutyName ||
- getLabel(dateTypeOptions, detail.dateType)
- }}</strong>
- <span>{{ detail.startTime }}-{{ detail.endTime }}</span>
- </div>
- <el-tag
- size="mini"
- :type="detail.scheduleStatus ? 'success' : 'info'"
- >
- {{ detail.scheduleStatus ? '已排班' : '未排班' }}
- </el-tag>
- <div
- v-if="
- detail.productionLineName ||
- detail.workStationName ||
- detail.productionShift ||
- detail.workingHours ||
- detail.restTime
- "
- class="detail-meta"
- >
- <span v-if="detail.productionLineName"
- >产线:{{ detail.productionLineName }}</span
- >
- <span v-if="detail.workStationName"
- >工位:{{ detail.workStationName }}</span
- >
- <span v-if="detail.productionShift"
- >班次:{{ detail.productionShift }}</span
- >
- <span v-if="detail.workingHours"
- >工时:{{ detail.workingHours }}分钟</span
- >
- <span v-if="detail.restTime"
- >休息:{{ detail.restTime }}分钟</span
- >
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="drawer-section">
- <div class="drawer-title">
- <span>{{ relatedSectionTitle }}</span>
- <em>{{ relatedPlans.length }} 条</em>
- </div>
- <div v-if="relatedPlans.length" class="plan-list">
- <el-tooltip
- v-for="plan in relatedPlans"
- :key="plan.id"
- placement="top"
- effect="light"
- >
- <div slot="content" class="plan-tooltip">
- <template v-if="isTeamQueueDrawer">
- <div>排班名称:{{ getRelatedQueueName(plan) }}</div>
- <div>人员:{{ getRelatedQueueUserName(plan) || '-' }}</div>
- </template>
- <template v-else>
- <div>
- {{ isEquipmentDrawer ? '设备编码' : '计划编号' }}:{{
- getRelatedPlanCode(plan)
- }}
- </div>
- <div>
- {{ isEquipmentDrawer ? '设备名称' : '计划名称' }}:{{
- getRelatedPlanName(plan)
- }}
- </div>
- <div v-if="!isEquipmentDrawer">
- 订单编号:{{ plan.orderNo || '-' }}
- </div>
- <div v-if="!isEquipmentDrawer">
- 计划数量:{{ plan.requiredFormingNum || '-' }}
- </div>
- <div>开始时间:{{ plan.displayStartTime || '-' }}</div>
- <div>结束时间:{{ plan.displayEndTime || '-' }}</div>
- </template>
- </div>
- <div
- class="plan-item"
- :class="{ 'team-queue-item': isTeamQueueDrawer }"
- >
- <div v-if="isTeamQueueDrawer" class="plan-main team-queue-plan">
- <div class="queue-avatar">
- <i class="el-icon-user-solid"></i>
- </div>
- <div class="queue-info">
- <div class="plan-name queue-name-only">
- <span class="queue-name-label">排班名称:</span>
- <span class="queue-name-text">
- {{ getRelatedQueueName(plan) }}
- </span>
- </div>
- <div class="queue-user-name">
- <i class="el-icon-user"></i>
- <span>人员:</span>
- <strong>
- {{ getRelatedQueueUserName(plan) || '-' }}
- </strong>
- </div>
- </div>
- <div class="queue-badge">排班</div>
- </div>
- <div v-else class="plan-main">
- <div class="plan-topline">
- <strong>{{ plan.planNo }}</strong>
- <span v-if="plan.orderNo">{{ plan.orderNo }}</span>
- </div>
- <div class="plan-name">{{ plan.taskName }}</div>
- <div class="plan-bottomline">
- <span class="plan-metrics">
- <em
- v-if="
- plan.requiredFormingNum !== undefined &&
- plan.requiredFormingNum !== null
- "
- >
- 数量 {{ plan.requiredFormingNum }}
- </em>
- <em v-if="plan.displayStartTime || plan.displayEndTime">
- {{ plan.displayStartTime || '-' }} -
- {{ plan.displayEndTime || '-' }}
- </em>
- </span>
- </div>
- </div>
- </div>
- </el-tooltip>
- </div>
- <el-empty
- v-else
- class="drawer-empty"
- description="暂无关联计划"
- :image-size="92"
- />
- </div>
- </div>
- <!-- <div class="drawer-actions" v-if="currentDay.date">
- <el-button
- size="small"
- icon="el-icon-check"
- @click="toggleScheduleStatus(1)"
- >
- 完成排班
- </el-button>
- <el-button
- size="small"
- icon="el-icon-refresh-left"
- @click="toggleScheduleStatus(0)"
- >
- 取消排班
- </el-button>
- <el-button
- size="small"
- type="primary"
- icon="el-icon-plus"
- @click="openAdjustDialog(currentDay)"
- >
- 临时调整
- </el-button>
- <el-button
- size="small"
- icon="el-icon-warning-outline"
- @click="checkSingleDay(currentDay)"
- >
- 实时检测
- </el-button>
- </div> -->
- </el-drawer>
- <ele-modal
- :visible.sync="approveDialogVisible"
- :title="approveForm.status === 1 ? '审批通过' : '审批驳回'"
- width="520px"
- custom-class="ele-dialog-form"
- :close-on-click-modal="false"
- >
- <el-form :model="approveForm" label-width="90px" class="el-form-box">
- <el-form-item label="审批意见">
- <el-input
- v-model="approveForm.opinion"
- type="textarea"
- :rows="4"
- maxlength="300"
- show-word-limit
- :placeholder="
- approveForm.status === 1 ? '请输入审批意见' : '请输入驳回原因'
- "
- />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button type="primary" @click="confirmApproveAdjust">确定</el-button>
- <el-button @click="approveDialogVisible = false">取消</el-button>
- </div>
- </ele-modal>
- <ele-modal
- :visible.sync="adjustDetailVisible"
- title="调整记录追溯"
- width="860px"
- custom-class="ele-dialog-form adjust-detail-dialog"
- >
- <div v-if="adjustDetail.id" class="adjust-detail-view">
- <div class="adjust-detail-grid">
- <div class="adjust-detail-field">
- <span>申请单号</span>
- <strong>{{ adjustDetail.applyNo || '-' }}</strong>
- </div>
- <div class="adjust-detail-field">
- <span>申请人</span>
- <strong>{{ getAdjustApplicantName(adjustDetail) }}</strong>
- </div>
- <div class="adjust-detail-field">
- <span>关联日历</span>
- <strong>{{ adjustDetail.calendarName || '-' }}</strong>
- </div>
- <div class="adjust-detail-field">
- <span>调整类型</span>
- <strong>{{
- getLabel(adjustTypeOptions, adjustDetail.adjustType)
- }}</strong>
- </div>
- <div class="adjust-detail-field">
- <span>审批状态</span>
- <el-tag
- size="mini"
- :type="approvalStatusTag(adjustDetail.applyStatus)"
- >
- {{ getLabel(approvalStatusOptions, adjustDetail.applyStatus) }}
- </el-tag>
- </div>
- <div class="adjust-detail-field">
- <span>调整日期</span>
- <strong>{{ adjustDetail.adjustDate || '-' }}</strong>
- </div>
- <div class="adjust-detail-field is-wide">
- <span>生效周期</span>
- <strong class="adjust-period-value">
- {{ adjustDetail.effectiveTime || '-' }} 至
- {{ adjustDetail.expireTime || '-' }}
- </strong>
- </div>
- </div>
- <div class="adjust-compare">
- <div class="adjust-data-card before">
- <span>调整前数据</span>
- <strong>{{ adjustDetail.oldContent || '-' }}</strong>
- </div>
- <div class="adjust-data-card after">
- <span>调整后数据</span>
- <strong>{{
- formatSegmentContent(adjustDetail.newContent, adjustDetail) || '-'
- }}</strong>
- </div>
- </div>
- <div class="adjust-detail-section">
- <div class="section-title">调整原因</div>
- <p>{{ adjustDetail.applyReason || '-' }}</p>
- </div>
- <div class="adjust-detail-section">
- <div class="section-title">审批节点</div>
- <div
- v-if="formatApproveNodes(adjustDetail.approveNode).length"
- class="approve-timeline"
- >
- <div
- v-for="(node, index) in formatApproveNodes(
- adjustDetail.approveNode
- )"
- :key="index"
- class="approve-node"
- >
- <div class="node-dot"></div>
- <div class="node-content">
- <div>
- <strong>{{ node.statusText }}</strong>
- <span>{{ node.approveTime || '-' }}</span>
- </div>
- <p>{{ node.approveOpinion || '-' }}</p>
- <em v-if="node.approveUserId"
- >审批人ID:{{ node.approveUserId }}</em
- >
- </div>
- </div>
- </div>
- <div v-else class="approve-empty">暂无审批记录</div>
- </div>
- </div>
- <div slot="footer">
- <!-- <el-button icon="el-icon-download" @click="exportAdjustRecord">
- 导出追溯
- </el-button> -->
- <el-button @click="adjustDetailVisible = false">关闭</el-button>
- </div>
- </ele-modal>
- </div>
- </template>
- <script>
- import dayjs from 'dayjs';
- import { solarToLunar } from 'lunar-calendar';
- import BaseManage from './components/BaseManage.vue';
- import MultiCalendarView from './components/MultiCalendarView.vue';
- import ConflictPanel from './components/ConflictPanel.vue';
- import AdjustApproval from './components/AdjustApproval.vue';
- import StatisticsView from './components/StatisticsView.vue';
- import {
- addCalendar,
- editCalendar,
- deleteCalendar,
- updateCalendarStatus,
- pageCalendar,
- copyCalendar,
- viewCalendar,
- saveCalendarDetail,
- checkCalendarConflict,
- checkAllCalendarConflict,
- applyCalendarAdjust,
- approveCalendarAdjust,
- pageCalendarAdjust,
- refreshCalendarPlan,
- getPublicLegalHolidays
- } from '@/api/productionScheduling/factoryCalendar';
- const currentYear = Number(dayjs().format('YYYY'));
- const calendarTypeOptions = [
- { label: '标准生产日历', value: 1 },
- { label: '设备维护日历', value: 2 },
- { label: '人员排班日历', value: 3 }
- ];
- const dateTypeOptions = [
- { label: '正常工作日', value: 1 },
- { label: '休息日', value: 2 },
- { label: '节假日', value: 3 },
- { label: '设备检修期', value: 4 },
- { label: '临时调整时段', value: 5 }
- ];
- const adjustTypeOptions = [
- { label: '临时加班', value: 1 },
- { label: '临时停工', value: 2 },
- { label: '临时设备检修', value: 3 },
- { label: '临时人员调班', value: 4 }
- ];
- const approvalStatusOptions = [
- { label: '待审批', value: 0 },
- { label: '审批通过', value: 1 },
- { label: '审批驳回', value: 2 },
- { label: '已失效', value: 3 }
- ];
- const legalHolidayKeywords = [
- { keyword: '元旦节', label: '元旦节' },
- { keyword: '春节', label: '春节' },
- { keyword: '清明', label: '清明节' },
- { keyword: '劳动节', label: '劳动节' },
- { keyword: '端午节', label: '端午节' },
- { keyword: '中秋节', label: '中秋节' },
- { keyword: '国庆节', label: '国庆节' }
- ];
- const officialHolidayRanges = {
- 2026: [
- { name: '元旦节', start: '2026-01-01', end: '2026-01-03' },
- { name: '春节', start: '2026-02-15', end: '2026-02-23' },
- { name: '清明节', start: '2026-04-04', end: '2026-04-06' },
- { name: '劳动节', start: '2026-05-01', end: '2026-05-05' },
- { name: '端午节', start: '2026-06-19', end: '2026-06-21' },
- { name: '中秋节', start: '2026-09-25', end: '2026-09-27' },
- { name: '国庆节', start: '2026-10-01', end: '2026-10-07' }
- ]
- };
- const inferredHolidayRules = {
- 元旦节: { before: 0, after: 2 },
- 春节: { before: 2, after: 6 },
- 清明节: { before: 1, after: 1 },
- 劳动节: { before: 0, after: 4 },
- 端午节: { before: 0, after: 2 },
- 中秋节: { before: 0, after: 2 },
- 国庆节: { before: 0, after: 6 }
- };
- const inferredHolidayRangeCache = {};
- function clone(data) {
- return JSON.parse(JSON.stringify(data));
- }
- function createCode(prefix) {
- return `${prefix}${dayjs().format('YYYYMMDD')}${String(
- Math.floor(Math.random() * 1000000)
- ).padStart(6, '0')}`;
- }
- function toMinute(time) {
- const [hour, minute] = time.split(':').map(Number);
- return hour * 60 + minute;
- }
- function isOverlap(aStart, aEnd, bStart, bEnd) {
- return (
- toMinute(aStart) < toMinute(bEnd) && toMinute(bStart) < toMinute(aEnd)
- );
- }
- function createHolidayRule(dateType = 3, calendarDate = '', remark = '') {
- return {
- id: Date.now() + Math.random(),
- calendarDate,
- dateType,
- remark
- };
- }
- function normalizePage(data) {
- const list =
- data?.records || data?.list || data?.rows || data?.data || data || [];
- const count =
- data?.total || data?.count || data?.totalCount || list.length || 0;
- return {
- list: Array.isArray(list) ? list : [],
- count
- };
- }
- function normalizeYearList(applyYear) {
- if (Array.isArray(applyYear)) {
- return applyYear.map(Number).filter(Boolean);
- }
- return String(applyYear || '')
- .split(',')
- .map((item) => Number(item.trim()))
- .filter(Boolean);
- }
- export default {
- name: 'FactoryCalendar',
- components: {
- BaseManage,
- MultiCalendarView,
- ConflictPanel,
- AdjustApproval,
- StatisticsView
- },
- data() {
- return {
- activeTab: 'base',
- query: {
- year: currentYear,
- month: '',
- calendarType: '',
- status: ''
- },
- viewQuery: {
- calendarType: 1,
- year: currentYear,
- month: Number(dayjs().format('M')),
- date: dayjs().format('YYYY-MM-DD'),
- viewType: 'month',
- statusFilter: ''
- },
- viewMonth: dayjs().format('YYYY-MM'),
- calendars: [],
- details: [],
- adjusts: [],
- plans: [],
- conflictList: [],
- filteredCalendars: [],
- legalHolidayLoading: false,
- legalHolidayCache: {},
- legalHolidayStatusMap: {},
- legalHolidayPromises: {},
- conflictCheckPromise: null,
- conflictDataKey: '',
- viewDataKey: '',
- viewDataPromise: null,
- planDataKey: '',
- planDataPromise: null,
- pageSize: this.$store.state.tablePageSize,
- tableResponse: {
- dataName: 'list',
- countName: 'count'
- },
- calendarDialogVisible: false,
- copyDialogVisible: false,
- adjustDialogVisible: false,
- dayDrawerVisible: false,
- approveDialogVisible: false,
- adjustDetailVisible: false,
- calendarForm: this.getDefaultCalendarForm(),
- copyForm: {
- sourceId: '',
- targetYear: currentYear + 1
- },
- adjustForm: this.getDefaultAdjustForm(),
- approveForm: {
- row: null,
- status: 1,
- opinion: ''
- },
- adjustDetail: {},
- currentDay: {},
- calendarTypeOptions,
- dateTypeOptions,
- adjustTypeOptions,
- approvalStatusOptions,
- yearOptions: [
- currentYear - 1,
- currentYear,
- currentYear + 1,
- currentYear + 2
- ],
- monthOptions: Array.from({ length: 12 }, (_, index) => ({
- label: `${index + 1}月`,
- value: index + 1
- })),
- weekNames: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
- restModeOptions: [
- { label: '双休', value: 'double' },
- { label: '单休', value: 'single' },
- { label: '大小周', value: 'alternate' },
- { label: '无休', value: 'none' },
- { label: '自定义', value: 'custom' }
- ],
- restWeekdayOptions: [
- { label: '周一', value: 1 },
- { label: '周二', value: 2 },
- { label: '周三', value: 3 },
- { label: '周四', value: 4 },
- { label: '周五', value: 5 },
- { label: '周六', value: 6 },
- { label: '周日', value: 0 }
- ],
- timePickerOptions: {
- start: '00:00',
- step: '00:30',
- end: '23:30'
- },
- statusFilterOptions: [
- { label: '已排班/已排产', value: 'scheduled' },
- { label: '未排班/空闲', value: 'idle' },
- { label: '休息日/节假日', value: 'rest' },
- { label: '设备检修期', value: 'maintenance' },
- { label: '临时调整时段', value: 'temp' },
- { label: '时间冲突时段', value: 'conflict' }
- ],
- legendList: [
- {
- key: 'scheduled',
- label: '已排班/已排产',
- desc: '已完成生产、维保或人员排班'
- },
- {
- key: 'idle',
- label: '未排班/空闲',
- desc: '有效工作时间,暂无计划'
- },
- {
- key: 'rest',
- label: '休息日/节假日',
- desc: '不支持排产排班'
- },
- {
- key: 'holiday',
- label: '法定节假日',
- desc: '未配置为上班时默认休息'
- },
- {
- key: 'maintenance',
- label: '设备检修期',
- desc: '设备不可用时段'
- },
- {
- key: 'temp',
- label: '临时调整时段',
- desc: '审批通过后的加班、停工、调班'
- },
- {
- key: 'conflict',
- label: '时间冲突时段',
- desc: '存在多维度时间冲突'
- }
- ],
- calendarColumns: [
- {
- width: 60,
- label: '序号',
- type: 'index',
- columnKey: 'index',
- align: 'center'
- },
- {
- minWidth: 170,
- prop: 'calendarCode',
- label: '日历编码',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 180,
- prop: 'calendarName',
- label: '日历名称',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 140,
- prop: 'calendarType',
- label: '日历类型',
- align: 'center',
- slot: 'calendarType'
- },
- {
- minWidth: 130,
- prop: 'applyYear',
- label: '适用年份',
- align: 'center',
- formatter: (row) => row.applyYear.join(',')
- },
- // {
- // minWidth: 240,
- // prop: 'timeRanges',
- // label: '每日时段配置',
- // align: 'center',
- // slot: 'timeRanges'
- // },
- // {
- // minWidth: 150,
- // prop: 'productionLineName',
- // label: '产线名称',
- // align: 'center',
- // showOverflowTooltip: true
- // },
- // {
- // minWidth: 140,
- // prop: 'workStationName',
- // label: '工位名称',
- // align: 'center',
- // showOverflowTooltip: true
- // },
- {
- minWidth: 150,
- prop: 'status',
- label: '状态',
- align: 'center',
- slot: 'status'
- },
- {
- minWidth: 130,
- prop: 'createUserName',
- label: '创建人',
- align: 'center'
- },
- {
- minWidth: 170,
- prop: 'updateTime',
- label: '创建时间',
- align: 'center'
- },
- {
- columnKey: 'action',
- label: '操作',
- width: 230,
- align: 'center',
- slot: 'action',
- fixed: 'right'
- }
- ],
- conflictColumns: [
- { width: 60, label: '序号', type: 'index', align: 'center' },
- {
- minWidth: 130,
- prop: 'calendarDate',
- label: '冲突日期',
- align: 'center'
- },
- {
- minWidth: 150,
- prop: 'timeRange',
- label: '冲突时段',
- align: 'center'
- },
- { minWidth: 180, prop: 'scene', label: '冲突场景', align: 'center' },
- {
- minWidth: 120,
- prop: 'level',
- label: '等级',
- align: 'center',
- slot: 'level'
- },
- {
- minWidth: 300,
- prop: 'message',
- label: '提示信息',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- columnKey: 'action',
- label: '操作',
- width: 120,
- align: 'center',
- slot: 'action',
- fixed: 'right'
- }
- ],
- adjustColumns: [
- { width: 60, label: '序号', type: 'index', align: 'center' },
- {
- minWidth: 160,
- prop: 'applyNo',
- label: '申请单号',
- align: 'center'
- },
- {
- minWidth: 180,
- prop: 'calendarName',
- label: '关联日历',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 130,
- prop: 'adjustType',
- label: '调整类型',
- align: 'center',
- slot: 'adjustType'
- },
- {
- minWidth: 120,
- prop: 'adjustDate',
- label: '调整日期',
- align: 'center'
- },
- {
- minWidth: 220,
- prop: 'newContent',
- label: '调整后数据',
- align: 'center',
- slot: 'newContent',
- showOverflowTooltip: true
- },
- {
- minWidth: 130,
- prop: 'applyStatus',
- label: '审批状态',
- align: 'center',
- slot: 'applyStatus'
- },
- {
- minWidth: 130,
- prop: 'applyUserName',
- label: '申请人',
- align: 'center'
- },
- {
- minWidth: 170,
- prop: 'applyTime',
- label: '申请时间',
- align: 'center'
- },
- {
- columnKey: 'action',
- label: '操作',
- width: 120,
- align: 'center',
- slot: 'action',
- fixed: 'right'
- }
- ],
- planColumns: [
- { width: 60, label: '序号', type: 'index', align: 'center' },
- { minWidth: 150, prop: 'planNo', label: '计划编号', align: 'center' },
- {
- minWidth: 150,
- prop: 'orderNo',
- label: '订单编号',
- align: 'center'
- },
- {
- minWidth: 180,
- prop: 'taskName',
- label: '任务名称',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 130,
- prop: 'calendarDate',
- label: '计划日期',
- align: 'center'
- },
- {
- minWidth: 150,
- prop: 'timeRange',
- label: '计划时段',
- align: 'center',
- formatter: (row) =>
- row.timeRange || `${row.startTime}-${row.endTime}`
- },
- {
- minWidth: 150,
- prop: 'productionLineName',
- label: '产线名称',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 140,
- prop: 'workStationName',
- label: '工位名称',
- align: 'center',
- showOverflowTooltip: true
- },
- {
- minWidth: 120,
- prop: 'workingHours',
- label: '工时(分钟)',
- align: 'center'
- },
- {
- minWidth: 120,
- prop: 'restTime',
- label: '休息(分钟)',
- align: 'center'
- },
- {
- minWidth: 110,
- prop: 'status',
- label: '状态',
- align: 'center',
- slot: 'status'
- },
- {
- columnKey: 'action',
- label: '操作',
- width: 120,
- align: 'center',
- slot: 'action',
- fixed: 'right'
- }
- ],
- calendarRules: {
- calendarName: [
- { required: true, message: '请输入日历名称', trigger: 'blur' }
- ],
- calendarType: [
- { required: true, message: '请选择日历类型', trigger: 'change' }
- ],
- applyYear: [
- { required: true, message: '请选择适用年份', trigger: 'change' }
- ],
- applyMonth: [
- { required: true, message: '请选择适用月份', trigger: 'change' }
- ]
- },
- adjustRules: {
- calendarId: [
- { required: true, message: '请选择关联日历', trigger: 'change' }
- ],
- adjustType: [
- { required: true, message: '请选择调整类型', trigger: 'change' }
- ],
- adjustDate: [
- { required: true, message: '请选择调整日期', trigger: 'change' }
- ],
- effectiveTime: [
- { required: true, message: '请选择生效时间', trigger: 'change' }
- ],
- expireTime: [
- { required: true, message: '请选择失效时间', trigger: 'change' }
- ],
- applyReason: [
- { required: true, message: '请输入调整原因', trigger: 'blur' }
- ]
- }
- };
- },
- computed: {
- vm() {
- return this;
- },
- enabledCalendars() {
- return this.calendars.filter((item) => item.status === 1);
- },
- calendarDialogTitle() {
- return this.calendarForm.id ? '编辑日历' : '新增日历';
- },
- calendarFormApplyYear: {
- get() {
- return normalizeYearList(this.calendarForm.applyYear)[0] || '';
- },
- set(value) {
- this.calendarForm.applyYear = value ? [Number(value)] : [];
- this.calendarForm.legalHolidayWorkDates = [];
- this.loadLegalHolidaysByYears(this.calendarForm.applyYear);
- }
- },
- showWeekHeader() {
- return ['week', 'month'].includes(this.viewQuery.viewType);
- },
- calendarViewTitle() {
- const date = dayjs(this.viewQuery.date);
- if (this.viewQuery.viewType === 'day') {
- return `${date.format('YYYY年MM月DD日')} 日视图`;
- }
- if (this.viewQuery.viewType === 'week') {
- const start = date.startOf('week').add(1, 'day');
- const end = start.add(6, 'day');
- return `${start.format('YYYY年MM月DD日')} - ${end.format(
- 'MM月DD日'
- )} 周视图`;
- }
- if (this.viewQuery.viewType === 'quarter') {
- const startMonth = Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
- const endMonth = startMonth + 2;
- return `${this.viewQuery.year}年${startMonth}月 - ${endMonth}月 季视图`;
- }
- return `${this.viewQuery.year}年${this.viewQuery.month}月 月视图`;
- },
- calendarDateRange() {
- const current = dayjs(this.viewQuery.date);
- if (this.viewQuery.viewType === 'day') {
- return [current];
- }
- if (this.viewQuery.viewType === 'week') {
- const start = current.startOf('week').add(1, 'day');
- return Array.from({ length: 7 }).map((_, index) =>
- start.add(index, 'day')
- );
- }
- if (this.viewQuery.viewType === 'quarter') {
- const startMonth = Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
- const start = dayjs(
- `${this.viewQuery.year}-${String(startMonth).padStart(2, '0')}-01`
- );
- const dates = [];
- for (let i = 0; i < 3; i += 1) {
- const month = start.add(i, 'month');
- for (let day = 1; day <= month.daysInMonth(); day += 1) {
- dates.push(month.date(day));
- }
- }
- return dates;
- }
- const firstDay = dayjs(
- `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
- 2,
- '0'
- )}-01`
- );
- const dates = [];
- for (let day = 1; day <= firstDay.daysInMonth(); day += 1) {
- dates.push(firstDay.date(day));
- }
- return dates;
- },
- calendarCells() {
- return this.buildCalendarCells(true);
- },
- statCells() {
- const dates = this.getMonthDateRange();
- return this.buildCalendarCellsByDates(dates, false, 'month').filter(
- (item) => item.date
- );
- },
- monthStats() {
- const details = this.getStatMonthDetails();
- const countDistinctDates = (predicate) =>
- new Set(
- details
- .filter(predicate)
- .map((item) => item.calendarDate)
- .filter(Boolean)
- ).size;
- const workDays = countDistinctDates(
- (item) => Number(item.dateType) === 1
- );
- const scheduledDateSet = new Set(
- details
- .filter((item) => this.isDetailScheduled(item))
- .map((item) => item.calendarDate)
- .filter(Boolean)
- );
- const scheduledDays = scheduledDateSet.size;
- const scheduledWorkdaySet = new Set(
- details
- .filter(
- (item) =>
- Number(item.dateType) === 1 &&
- scheduledDateSet.has(item.calendarDate)
- )
- .map((item) => item.calendarDate)
- );
- const emptyWorkdaySet = new Set(
- details
- .filter(
- (item) =>
- Number(item.dateType) === 1 &&
- !scheduledDateSet.has(item.calendarDate)
- )
- .map((item) => item.calendarDate)
- );
- const idleDays = Math.max(
- emptyWorkdaySet.size,
- workDays - scheduledWorkdaySet.size,
- 0
- );
- const conflictDays = countDistinctDates(
- (item) => Number(item.isConflict) === 1
- );
- const tempDays = countDistinctDates(
- (item) => Number(item.isTempAdjust) === 1
- );
- const restDays = countDistinctDates((item) =>
- [2, 3].includes(Number(item.dateType))
- );
- return [
- { key: 'work', label: '总工作日', value: workDays },
- { key: 'scheduled', label: '已排班天数', value: scheduledDays },
- {
- key: 'idle',
- label: '未排班天数',
- value: idleDays
- },
- { key: 'conflict', label: '冲突天数', value: conflictDays },
- { key: 'temp', label: '临时调整天数', value: tempDays },
- { key: 'rest', label: '休息/节假日', value: restDays }
- ];
- },
- scheduledWorkdayDays() {
- return new Set(
- this.getStatMonthDetails()
- .filter(
- (item) =>
- Number(item.dateType) === 1 && this.isDetailScheduled(item)
- )
- .map((item) => item.calendarDate)
- .filter(Boolean)
- ).size;
- },
- dayDrawerTitle() {
- return this.currentDay.date
- ? `${this.currentDay.date} 日历详情`
- : '日历详情';
- },
- drawerDateMonth() {
- return this.currentDay.date
- ? dayjs(this.currentDay.date).format('YYYY-MM')
- : '';
- },
- drawerDateDay() {
- return this.currentDay.date
- ? dayjs(this.currentDay.date).format('DD')
- : '';
- },
- relatedPlans() {
- return this.currentDay.relatedPlans || [];
- },
- relatedSectionTitle() {
- return this.isTeamQueueDrawer ? '排班详情' : '关联计划';
- },
- isEquipmentDrawer() {
- return (
- Number(this.viewQuery.calendarType) === 2 ||
- (this.currentDay.details || []).some(
- (item) => Number(item.calendarType) === 2
- )
- );
- },
- isTeamQueueDrawer() {
- return (
- Number(this.viewQuery.calendarType) === 3 ||
- (this.currentDay.details || []).some(
- (item) => Number(item.calendarType) === 3
- )
- );
- },
- legalHolidayOptions() {
- return this.getLegalHolidayOptions(
- this.calendarForm.applyYear,
- this.calendarForm.applyMonth
- );
- },
- legalHolidayGroups() {
- const groupMap = {};
- this.legalHolidayOptions.forEach((item) => {
- if (!groupMap[item.name]) {
- groupMap[item.name] = {
- name: item.name,
- dates: []
- };
- }
- groupMap[item.name].dates.push(item);
- });
- return Object.values(groupMap);
- },
- showLegalHolidaySection() {
- return this.legalHolidayLoading || this.legalHolidayGroups.length > 0;
- },
- legalHolidayVisibleWorkCount() {
- const visibleDates = new Set(
- this.legalHolidayOptions.map((item) => item.calendarDate)
- );
- return (this.calendarForm.legalHolidayWorkDates || []).filter((date) =>
- visibleDates.has(date)
- ).length;
- },
- legalHolidaySourceText() {
- const year = normalizeYearList(this.calendarForm.applyYear)[0];
- if (this.legalHolidayStatusMap[year] === 'remote') {
- return '已同步公共节假日';
- }
- if (this.legalHolidayStatusMap[year] === 'fallback') {
- return '使用本地节假日';
- }
- return '使用本地节假日';
- },
- visualKpis() {
- const workDays =
- this.monthStats.find((item) => item.key === 'work')?.value || 0;
- const scheduledDays =
- this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
- const tempDays =
- this.monthStats.find((item) => item.key === 'temp')?.value || 0;
- const conflictDays =
- this.monthStats.find((item) => item.key === 'conflict')?.value || 0;
- return [
- {
- key: 'work',
- label: '总工作日',
- value: workDays,
- desc: `${this.viewQuery.year}年${this.viewQuery.month}月`,
- icon: 'el-icon-date'
- },
- {
- key: 'scheduled',
- label: '已排班',
- value: scheduledDays,
- desc: `完成率 ${this.scheduleRate}%`,
- icon: 'el-icon-finished'
- },
- {
- key: 'conflict',
- label: '冲突天数',
- value: conflictDays,
- desc: '当前月需确认修正',
- icon: 'el-icon-warning-outline'
- },
- {
- key: 'temp',
- label: '临时调整',
- value: tempDays,
- desc: '审批通过后生效',
- icon: 'el-icon-refresh'
- }
- ];
- },
- scheduleRate() {
- const workDays =
- this.monthStats.find((item) => item.key === 'work')?.value || 0;
- return workDays
- ? Math.min(
- 100,
- Math.round((this.scheduledWorkdayDays / workDays) * 100)
- )
- : 0;
- },
- scheduleSegments() {
- const scheduled =
- this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
- const idle =
- this.monthStats.find((item) => item.key === 'idle')?.value || 0;
- const rest =
- this.monthStats.find((item) => item.key === 'rest')?.value || 0;
- return [
- {
- key: 'scheduled',
- label: '已排班',
- value: scheduled,
- color: '#409eff'
- },
- { key: 'idle', label: '未排班', value: idle, color: '#67c23a' },
- { key: 'rest', label: '休息/节假日', value: rest, color: '#c0c4cc' }
- ];
- },
- scheduleDonutBackground() {
- const total = this.scheduleSegments.reduce(
- (sum, item) => sum + item.value,
- 0
- );
- if (!total) {
- return '#ebeef5';
- }
- let start = 0;
- const segments = this.scheduleSegments.map((item) => {
- const end = start + (item.value / total) * 100;
- const value = `${item.color} ${start}% ${end}%`;
- start = end;
- return value;
- });
- return `conic-gradient(${segments.join(', ')})`;
- },
- scheduleTooltip() {
- return `已排班率 ${this.scheduleRate}%,已排班 ${this.scheduleSegments[0].value} 天,未排班 ${this.scheduleSegments[1].value} 天,休息/节假日 ${this.scheduleSegments[2].value} 天`;
- },
- visualMonthBars() {
- const colorMap = {
- work: '#409eff',
- scheduled: '#36cfc9',
- idle: '#67c23a',
- conflict: '#f56c6c',
- temp: '#9254de',
- rest: '#909399'
- };
- const max = Math.max(...this.monthStats.map((item) => item.value), 1);
- return this.monthStats.map((item) => ({
- ...item,
- color: colorMap[item.key],
- percent: Math.round((item.value / max) * 100)
- }));
- },
- calendarTypeStats() {
- const colorMap = {
- 1: '#409eff',
- 2: '#e6a23c',
- 3: '#67c23a'
- };
- return this.calendarTypeOptions.map((item) => {
- const list = this.calendars.filter(
- (calendar) => calendar.calendarType === item.value
- );
- return {
- ...item,
- color: colorMap[item.value],
- count: list.length,
- enabled: list.filter((calendar) => calendar.status === 1).length,
- disabled: list.filter((calendar) => calendar.status === 0).length
- };
- });
- },
- conflictTrend() {
- const days = Array.from({ length: 6 }).map((_, index) => {
- const date = dayjs(
- `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
- 2,
- '0'
- )}-01`
- ).date(index * 5 + 1);
- const count = new Set(
- this.getStatMonthDetails()
- .filter(
- (item) =>
- Number(item.isConflict) === 1 &&
- dayjs(item.calendarDate).isSame(date, 'week')
- )
- .map((item) => item.calendarDate)
- ).size;
- return {
- label: date.format('MM/DD'),
- count
- };
- });
- const max = Math.max(...days.map((item) => item.count), 1);
- return days.map((item) => ({
- ...item,
- height: Math.max(12, Math.round((item.count / max) * 100))
- }));
- }
- },
- created() {
- this.loadRemoteCalendarData();
- },
- methods: {
- getDefaultCalendarForm() {
- return {
- id: '',
- calendarCode: createCode('CLD'),
- calendarName: '',
- calendarType: 1,
- applyYear: [currentYear],
- applyMonth: Array.from({ length: 12 }, (_, index) => index + 1),
- status: 0,
- shiftId: '',
- shiftName: '',
- shiftCode: '',
- timeRanges: [],
- restMode: 'double',
- restWeekdays: [6, 0],
- legalHolidayWorkDates: [],
- holidayRules: [],
- remark: ''
- };
- },
- getDefaultAdjustForm(day = {}) {
- const enabledCalendars = (this.calendars || []).filter(
- (item) => item.status === 1
- );
- return {
- calendarId: enabledCalendars[0]?.id || '',
- adjustType: 1,
- adjustDate: day.date || dayjs().format('YYYY-MM-DD'),
- startTime: '08:00',
- endTime: '17:00',
- effectiveTime: `${day.date || dayjs().format('YYYY-MM-DD')} 08:00:00`,
- expireTime: `${day.date || dayjs().format('YYYY-MM-DD')} 17:00:00`,
- applyReason: ''
- };
- },
- buildCalendarCells(applyFilter = true) {
- return this.buildCalendarCellsByDates(
- this.calendarDateRange,
- applyFilter,
- this.viewQuery.viewType
- );
- },
- getMonthDateRange(
- year = this.viewQuery.year,
- month = this.viewQuery.month
- ) {
- const firstDay = dayjs(`${year}-${String(month).padStart(2, '0')}-01`);
- const dates = [];
- for (let day = 1; day <= firstDay.daysInMonth(); day += 1) {
- dates.push(firstDay.date(day));
- }
- return dates;
- },
- buildCalendarCellsByDates(
- dates,
- applyFilter = true,
- viewType = this.viewQuery.viewType
- ) {
- const startOffset =
- viewType === 'month' && dates.length ? (dates[0].day() + 6) % 7 : 0;
- const cells = [];
- for (let i = 0; i < startOffset; i += 1) {
- cells.push({ key: `empty-${i}`, date: '', dayText: '', details: [] });
- }
- dates.forEach((dateItem) => {
- const date = dateItem.format('YYYY-MM-DD');
- const details = this.getViewDetails(date);
- const planCount = this.getDetailPlanCount(details);
- const disabled = !this.hasEnabledCalendarForView();
- const holidayName = this.getLegalHolidayName(dateItem);
- const hasWorkDetail = details.some(
- (item) => Number(item.dateType || 1) === 1
- );
- const cell = {
- key: date,
- date,
- dayText:
- viewType === 'quarter'
- ? dateItem.format('M/D')
- : dateItem.format('D'),
- details,
- planCount: disabled ? 0 : planCount,
- scheduleStatus:
- !disabled &&
- (details.some((item) => item.scheduleStatus === 1) ||
- planCount > 0),
- isConflict:
- !disabled && details.some((item) => item.isConflict === 1),
- isTempAdjust:
- !disabled && details.some((item) => item.isTempAdjust === 1),
- isMaintenance:
- !disabled && details.some((item) => item.dateType === 4),
- isHoliday:
- !disabled &&
- (details.some((item) => Number(item.dateType) === 3) ||
- (holidayName && !hasWorkDetail)),
- holidayName,
- isRest: disabled ? true : this.isRestDay(date, details),
- isDisabledCalendar: disabled,
- conflictText: disabled ? '日历已禁用' : this.getConflictText(date)
- };
- if (!applyFilter || this.matchStatusFilter(cell)) {
- cells.push(cell);
- } else {
- cells.push({ ...cell, hiddenByFilter: true });
- }
- });
- return cells;
- },
- hasEnabledCalendarForView() {
- return !!this.getCurrentViewCalendar();
- },
- getCurrentViewCalendar() {
- return this.enabledCalendars.find(
- (item) => item.calendarType === this.viewQuery.calendarType
- );
- },
- getCurrentViewCalendars() {
- return this.enabledCalendars.filter(
- (item) => item.calendarType === this.viewQuery.calendarType
- );
- },
- getRemoteCalendarId(calendar = this.getCurrentViewCalendar()) {
- return calendar?.isRemote ? calendar.id : null;
- },
- getScheduleSegmentTooltip(item) {
- const total = this.scheduleSegments.reduce(
- (sum, segment) => sum + segment.value,
- 0
- );
- const percent = total ? Math.round((item.value / total) * 100) : 0;
- return `${item.label}:${item.value} 天,占比 ${percent}%`;
- },
- getMonthBarTooltip(item) {
- return `${this.viewQuery.year}年${this.viewQuery.month}月,${item.label}:${item.value} 天,占当前最大值 ${item.percent}%`;
- },
- getCalendarTypeTooltip(item) {
- return `${item.label}:共 ${item.count} 个,启用 ${item.enabled} 个,禁用 ${item.disabled} 个`;
- },
- getConflictTrendTooltip(item) {
- return `${item.label} 所在周:${item.count} 条冲突`;
- },
- loadData() {
- this.calendars = [];
- this.details = [];
- this.adjusts = [];
- this.plans = [];
- },
- getCalendarQueryParams(query = this.query) {
- return {
- year: query.year || undefined,
- calendarType: query.calendarType || undefined,
- status: query.status === '' ? undefined : query.status
- };
- },
- async loadRemoteCalendarData() {
- try {
- const data = await pageCalendar({
- pageNum: 1,
- size: 999,
- ...this.getCalendarQueryParams()
- });
- const { list } = normalizePage(data);
- this.calendars = list.map((item) =>
- this.normalizeRemoteCalendar(item)
- );
- this.details = list
- .map((item) =>
- (item.detailList || []).map((detail) =>
- this.normalizeRemoteDetail(detail, item)
- )
- )
- .flat();
- await this.loadRemoteViewData(true);
- await this.loadRemoteAdjustData();
- this.syncFilteredCalendars();
- } catch (e) {
- this.calendars = [];
- this.details = [];
- this.adjusts = [];
- this.plans = [];
- this.syncFilteredCalendars();
- }
- },
- getViewDataKey(query = this.viewQuery) {
- return [
- query.calendarType || '',
- query.year || '',
- query.month || '',
- query.viewType || ''
- ].join('|');
- },
- async loadRemoteViewData(force = false, queryOverride = null) {
- const query = {
- ...this.viewQuery,
- ...(queryOverride || {})
- };
- const dataKey = this.getViewDataKey(query);
- if (!force && this.viewDataKey === dataKey) {
- return;
- }
- if (!force && this.viewDataPromise) {
- await this.viewDataPromise;
- return;
- }
- try {
- this.viewDataPromise = viewCalendar({
- calendarType: query.calendarType,
- year: query.year,
- month: query.month,
- viewType: query.viewType
- });
- const data = await this.viewDataPromise;
- if (!Array.isArray(data)) {
- this.viewDataKey = '';
- this.viewDataPromise = null;
- return;
- }
- const remoteDetails = data.map((item) => ({
- ...this.normalizeRemoteDetail(item),
- isViewDetail: true
- }));
- const visibleDates =
- queryOverride && query.viewType === 'month'
- ? this.getMonthDateRange(query.year, query.month).map((item) =>
- item.format('YYYY-MM-DD')
- )
- : this.calendarDateRange.map((item) => item.format('YYYY-MM-DD'));
- const viewCalendarIds = this.calendars
- .filter((item) => item.calendarType === query.calendarType)
- .map((item) => item.id);
- this.details = [
- ...this.details.filter(
- (item) =>
- !(
- item.calendarType === query.calendarType ||
- viewCalendarIds.includes(item.calendarId)
- ) || !visibleDates.includes(item.calendarDate)
- ),
- ...remoteDetails
- ];
- this.viewDataKey = dataKey;
- } catch (e) {
- this.viewDataKey = '';
- }
- this.viewDataPromise = null;
- },
- async loadRemoteAdjustData() {
- try {
- const data = await pageCalendarAdjust({
- pageNum: 1,
- size: 999
- });
- const { list } = normalizePage(data);
- this.adjusts = list;
- } catch (e) {
- this.adjusts = [];
- }
- },
- normalizeRemoteCalendar(item) {
- const detailList = Array.isArray(item.detailList)
- ? item.detailList
- : [];
- const ruleConfig = this.parseCalendarRuleConfig(item.remark);
- const workdayDetails = detailList.filter(
- (detail) => Number(detail.dateType || 1) === 1
- );
- const applyMonth = normalizeYearList(item.applyMonth);
- const inferredApplyMonth = this.getApplyMonthsFromDetails(detailList);
- const inferredRestRule = this.getRestRuleFromDetails(detailList);
- const restWeekdays = Array.isArray(item.restWeekdays)
- ? item.restWeekdays.map(Number)
- : ruleConfig.restWeekdays === undefined
- ? inferredRestRule.restWeekdays
- : ruleConfig.restWeekdays;
- return {
- ...item,
- remark: this.stripCalendarRuleConfig(item.remark),
- isRemote: true,
- applyYear: normalizeYearList(item.applyYear),
- applyMonth: applyMonth.length
- ? applyMonth
- : ruleConfig.applyMonth ||
- inferredApplyMonth ||
- this.getFullMonthList(),
- shiftId: item.shiftId || ruleConfig.shiftId || '',
- shiftName: item.shiftName || ruleConfig.shiftName || '',
- shiftCode: item.shiftCode || ruleConfig.shiftCode || '',
- timeRanges: workdayDetails.length
- ? this.mergeTimeRanges(
- [],
- workdayDetails.map((detail, index) => ({
- id: detail.id || `${item.id}-${index}`,
- name:
- detail.dateTypeName || detail.remark || `时段${index + 1}`,
- startTime: String(detail.startTime || '').slice(0, 5),
- endTime: String(detail.endTime || '').slice(0, 5),
- timeType: '1'
- }))
- )
- : item.timeRanges || [],
- restWeekdays,
- legalHolidayWorkDates: Array.isArray(item.legalHolidayWorkDates)
- ? item.legalHolidayWorkDates
- : ruleConfig.legalHolidayWorkDates || [],
- restMode:
- item.restMode ||
- ruleConfig.restMode ||
- inferredRestRule.restMode ||
- this.getRestModeByWeekdays(restWeekdays),
- holidayRules: Array.isArray(item.holidayRules)
- ? item.holidayRules
- : ruleConfig.holidayRules || []
- };
- },
- getApplyMonthsFromDetails(detailList = []) {
- const months = [
- ...new Set(
- detailList
- .map((detail) => {
- const month = dayjs(detail.calendarDate).month() + 1;
- return Number.isNaN(month) ? null : month;
- })
- .filter(Boolean)
- )
- ].sort((a, b) => a - b);
- return months.length ? months : null;
- },
- getRestRuleFromDetails(detailList = []) {
- const dayMap = {};
- detailList.forEach((detail) => {
- if (!detail.calendarDate) {
- return;
- }
- if (!dayMap[detail.calendarDate]) {
- dayMap[detail.calendarDate] = [];
- }
- dayMap[detail.calendarDate].push(detail);
- });
- const days = Object.keys(dayMap).map((date) => {
- const details = dayMap[date];
- return {
- date,
- weekDay: dayjs(date).day(),
- isRest: details.every((detail) =>
- [2, 3].includes(Number(detail.dateType))
- )
- };
- });
- const restWeekdays = [
- ...new Set(
- days.filter((item) => item.isRest).map((item) => item.weekDay)
- )
- ].sort((a, b) => a - b);
- const weekdayText = restWeekdays.join(',');
- if (!restWeekdays.length) {
- return { restMode: 'none', restWeekdays: [] };
- }
- if (weekdayText === '0') {
- return { restMode: 'single', restWeekdays };
- }
- if (weekdayText === '0,6') {
- const saturdayDays = days.filter((item) => item.weekDay === 6);
- const saturdayRestCount = saturdayDays.filter(
- (item) => item.isRest
- ).length;
- if (
- saturdayDays.length &&
- saturdayRestCount > 0 &&
- saturdayRestCount < saturdayDays.length
- ) {
- return { restMode: 'alternate', restWeekdays };
- }
- return { restMode: 'double', restWeekdays };
- }
- return { restMode: 'custom', restWeekdays };
- },
- parseCalendarRuleConfig(remark) {
- const match = String(remark || '').match(
- /\[calendarRules\]([\s\S]*?)\[\/calendarRules\]/
- );
- if (!match) {
- return {};
- }
- try {
- const config = JSON.parse(match[1]);
- return {
- applyMonth: Array.isArray(config.applyMonth)
- ? config.applyMonth.map(Number).filter(Boolean)
- : undefined,
- shiftId: config.shiftId || undefined,
- shiftName: config.shiftName || undefined,
- shiftCode: config.shiftCode || undefined,
- restMode: config.restMode || undefined,
- restWeekdays: Array.isArray(config.restWeekdays)
- ? config.restWeekdays.map(Number)
- : undefined,
- legalHolidayWorkDates: Array.isArray(config.legalHolidayWorkDates)
- ? config.legalHolidayWorkDates.filter(Boolean)
- : undefined,
- holidayRules: Array.isArray(config.holidayRules)
- ? config.holidayRules
- .filter((item) => item && item.calendarDate)
- .map((item) =>
- createHolidayRule(
- Number(item.dateType || 3),
- item.calendarDate,
- item.remark || ''
- )
- )
- : undefined
- };
- } catch (e) {
- return {};
- }
- },
- stripCalendarRuleConfig(remark) {
- return String(remark || '')
- .replace(/\s*\[calendarRules\][\s\S]*?\[\/calendarRules\]\s*/g, '')
- .trim();
- },
- getCalendarRulePayload(form) {
- return {
- applyMonth: form.applyMonth || this.getFullMonthList(),
- shiftId: form.shiftId || '',
- shiftName: form.shiftName || '',
- shiftCode: form.shiftCode || '',
- restMode: form.restMode || 'double',
- restWeekdays: form.restWeekdays || [],
- legalHolidayWorkDates: form.legalHolidayWorkDates || [],
- holidayRules: (form.holidayRules || [])
- .filter((item) => item.calendarDate)
- .map((item) => ({
- calendarDate: item.calendarDate,
- dateType: Number(item.dateType || 3),
- remark: item.remark || ''
- }))
- };
- },
- mergeTimeRanges(...groups) {
- const map = {};
- groups.flat().forEach((item, index) => {
- if (!item) {
- return;
- }
- const startTime = String(item.startTime || '').slice(0, 5);
- const endTime = String(item.endTime || '').slice(0, 5);
- if (!startTime || !endTime) {
- return;
- }
- const name = item.name || item.remark || `时段${index + 1}`;
- const key = `${name}-${startTime}-${endTime}`;
- if (!map[key]) {
- map[key] = {
- ...item,
- id: item.id || key,
- name,
- startTime,
- endTime,
- timeType: item.timeType || '1'
- };
- }
- });
- return Object.values(map);
- },
- normalizeRemoteDetail(detail, calendar = {}) {
- const calendarType = detail.calendarType || calendar.calendarType;
- const relationPlanList = this.normalizeRemotePlans(
- detail.relationPlanList || []
- );
- const relationEamPlanList = this.normalizeRemotePlans(
- detail.relationEamPlanList || []
- );
- const relationTeamQueueList = this.normalizeRemotePlans(
- detail.relationTeamQueueList || []
- );
- const relationPlanCountMap = {
- 2: relationEamPlanList.length,
- 3: relationTeamQueueList.length
- };
- const relationPlanCount =
- relationPlanCountMap[Number(calendarType)] ??
- Number(detail.relationPlanCount || relationPlanList.length || 0);
- return {
- ...detail,
- id:
- detail.id ||
- `${detail.calendarId}-${detail.calendarDate}-${detail.startTime}`,
- calendarId: detail.calendarId || calendar.id,
- calendarName: detail.calendarName || calendar.calendarName,
- calendarType,
- calendarDate: detail.calendarDate,
- startTime: String(detail.startTime || '').slice(0, 5),
- endTime: String(detail.endTime || '').slice(0, 5),
- dateType: Number(detail.dateType || 1),
- scheduleStatus: Number(detail.scheduleStatus || 0),
- isConflict: Number(detail.isConflict || 0),
- isTempAdjust: Number(detail.isTempAdjust || 0),
- relationPlanCount,
- relationPlanList,
- relationEamPlanList,
- relationTeamQueueList
- };
- },
- saveData() {
- // 工厂日历页面以后台接口为唯一数据源,不再写入本地演示缓存。
- },
- openImport() {
- this.$message.warning(
- '批量导入需要后端导入接口支持,当前页面不再写入本地数据'
- );
- },
- async calendarDatasource({
- page = 1,
- limit = this.pageSize,
- where = {},
- order = {}
- } = {}) {
- try {
- const data = await pageCalendar({
- ...this.getCalendarQueryParams({
- ...this.query,
- ...where
- }),
- ...order,
- pageNum: page,
- size: limit
- });
- const { list, count } = normalizePage(data);
- const rows = list.map((item) => this.normalizeRemoteCalendar(item));
- return { code: 0, list: rows, count };
- } catch (e) {
- return this.getPagedResult([], page, limit);
- }
- },
- getPagedResult(list, page = 1, limit = this.pageSize) {
- const start = (page - 1) * limit;
- return {
- code: 0,
- list: list.slice(start, start + limit),
- count: list.length
- };
- },
- reloadConflictTable(page = 1) {
- this.$nextTick(() => {
- this.$refs.conflictPanel?.reload?.({ page });
- });
- },
- reloadAdjustTable(page = 1) {
- this.$nextTick(() => {
- this.$refs.adjustApproval?.reload?.({ page });
- });
- },
- reloadPlanTable(page = 1) {
- this.$nextTick(() => {
- this.$refs.planLinkage?.reload?.({ page });
- });
- },
- reloadBaseTable(page = 1) {
- this.$nextTick(() => {
- this.$refs.baseManage?.reload?.({ page });
- });
- },
- async adjustDatasource({
- page = 1,
- limit = this.pageSize,
- where = {},
- order = {}
- } = {}) {
- try {
- const data = await pageCalendarAdjust({
- ...where,
- ...order,
- pageNum: page,
- size: limit
- });
- const { list, count } = normalizePage(data);
- return { code: 0, list, count };
- } catch (e) {
- return this.getPagedResult([], page, limit);
- }
- },
- async planDatasource({ page = 1, limit = this.pageSize } = {}) {
- await this.ensurePlanRefreshData(false);
- if (this.plans.length) {
- return this.getPagedResult(this.plans, page, limit);
- }
- const calendars = this.getCurrentViewCalendars();
- if (calendars.some((item) => this.getRemoteCalendarId(item))) {
- try {
- await this.loadRemoteViewData();
- const details = this.getViewDetails(this.viewQuery.date);
- const plans = this.getDetailRelatedPlans(details);
- return this.getPagedResult(plans, page, limit);
- } catch (e) {
- return this.getPagedResult([], page, limit);
- }
- }
- return this.getPagedResult([], page, limit);
- },
- normalizeRemoteConflicts(data = {}) {
- return (data.conflictDetail || []).map((item, index) => ({
- id:
- item.currentDetailId ||
- [
- item.calendarDate,
- item.startTime,
- item.endTime,
- item.currentCalendarType,
- item.conflictCalendarType,
- item.conflictCalendarId,
- item.conflictDetailId,
- item.conflictMsg || data.msg || '',
- index
- ].join('-'),
- calendarDate: item.calendarDate,
- timeRange: `${String(item.startTime || '').slice(0, 5)}-${String(
- item.endTime || ''
- ).slice(0, 5)}`,
- scene: `${item.currentCalendarType || ''} VS ${
- item.conflictCalendarType || ''
- }`,
- level: data.isConflict ? '高' : '低',
- message: item.conflictMsg || data.msg || ''
- }));
- },
- normalizeRemotePlans(data = {}) {
- const list = Array.isArray(data)
- ? data
- : [
- ...(data.records || []),
- ...(data.list || []),
- ...(data.orderList || []),
- ...(data.taskList || []),
- ...(data.schedulePlanList || []),
- ...(data.relationPlanList || []),
- ...(data.relationEamPlanList || []),
- ...(data.relationTeamQueueList || []),
- ...(data.affectedPlanList || [])
- ];
- return list.map((item, index) => {
- const planNo =
- item.planNo ||
- item.orderNo ||
- item.code ||
- item.teamQueueName ||
- item.teamQueueId ||
- `PLAN-${index + 1}`;
- const salesCode = Array.isArray(item.salesCode)
- ? item.salesCode.join(',')
- : item.salesCode;
- const rawStartTime =
- item.rawStartTime ||
- item.planStartTime ||
- item.queueDate ||
- item.startTime;
- const rawEndTime =
- item.rawEndTime ||
- item.planEndTime ||
- item.queueEndTime ||
- item.endTime;
- const startTime = this.formatPlanTime(rawStartTime);
- const endTime = this.formatPlanTime(rawEndTime);
- const displayStartTime = this.formatPlanDateTime(rawStartTime);
- const displayEndTime = this.formatPlanDateTime(rawEndTime);
- const hasDateRange =
- /^\d{4}-\d{2}-\d{2}/.test(String(rawStartTime || '')) ||
- /^\d{4}-\d{2}-\d{2}/.test(String(rawEndTime || ''));
- return {
- ...item,
- id: item.id || planNo || index,
- planNo,
- orderNo:
- item.orderNo ||
- item.salesCodeList ||
- salesCode ||
- item.batchNo ||
- item.teamName ||
- item.teamId ||
- item.userName ||
- item.userId ||
- '',
- taskName:
- item.taskName ||
- item.productName ||
- item.bomCategoryName ||
- item.teamQueueName ||
- item.queueName ||
- item.teamName ||
- item.userName ||
- item.name ||
- item.title ||
- '关联计划',
- calendarId: item.calendarId,
- calendarDate:
- item.calendarDate ||
- this.formatPlanDate(
- item.planDeliveryTime ||
- item.reqMoldTime ||
- item.planEndTime ||
- item.planStartTime ||
- item.queueDate ||
- item.endTime ||
- item.startTime
- ),
- startTime,
- endTime,
- rawStartTime,
- rawEndTime,
- displayStartTime,
- displayEndTime,
- timeRange: hasDateRange
- ? `${displayStartTime} 至 ${displayEndTime}`
- : `${startTime}-${endTime}`,
- status: item.statusName || item.status || '正常',
- canReschedule: !!item.canReschedule,
- productionLineName:
- item.productionLineName ||
- item.productionLine ||
- item.productionCodes ||
- '',
- workStationName: item.workStationName || item.workStation || ''
- };
- });
- },
- formatPlanDate(value) {
- const text = String(value || '');
- const matched = text.match(/^\d{4}-\d{2}-\d{2}/);
- return matched ? matched[0] : this.viewQuery.date;
- },
- formatPlanTime(value) {
- const text = String(value || '');
- const matched = text.match(/\b(\d{2}):(\d{2})(?::\d{2})?\b/);
- return matched ? `${matched[1]}:${matched[2]}` : text.slice(0, 5);
- },
- formatPlanDateTime(value) {
- const text = String(value || '');
- const matched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/);
- if (matched) {
- return `${matched[1]} ${matched[2]}:${matched[3]}`;
- }
- return this.formatPlanTime(value);
- },
- getDetailRelatedPlans(details = []) {
- const map = {};
- details.forEach((detail) => {
- [
- ...(detail.relationPlanList || []),
- ...(detail.relationEamPlanList || []),
- ...(detail.relationTeamQueueList || [])
- ].forEach((plan, index) => {
- const normalized = this.normalizeRemotePlans([plan])[0];
- if (!normalized) {
- return;
- }
- const key =
- normalized.id ||
- normalized.planNo ||
- normalized.code ||
- `${detail.id}-${index}`;
- map[key] = {
- ...normalized,
- calendarId: normalized.calendarId || detail.calendarId,
- calendarDate: detail.calendarDate || normalized.calendarDate,
- calendarType: normalized.calendarType || detail.calendarType
- };
- });
- });
- return Object.values(map);
- },
- getRelatedPlanCode(plan = {}) {
- return (
- (this.isEquipmentDrawer
- ? plan.deviceCode ||
- plan.equipmentCode ||
- plan.eamCode ||
- plan.assetCode ||
- plan.planNo
- : plan.planNo) || '-'
- );
- },
- getRelatedPlanName(plan = {}) {
- return (
- (this.isEquipmentDrawer
- ? plan.deviceName ||
- plan.equipmentName ||
- plan.eamName ||
- plan.assetName ||
- plan.taskName
- : plan.taskName) || '-'
- );
- },
- getRelatedQueueName(plan = {}) {
- return plan.teamQueueName || '-';
- },
- getRelatedQueueUserName(plan = {}) {
- return plan.userName || '';
- },
- getDetailPlanCount(details = []) {
- const relatedPlans = this.getDetailRelatedPlans(details);
- if (relatedPlans.length) {
- return relatedPlans.length;
- }
- return details.reduce(
- (sum, item) => sum + Number(item.relationPlanCount || 0),
- 0
- );
- },
- syncFilteredCalendars() {
- this.filteredCalendars = this.calendars.filter((item) => {
- const yearMatch =
- !this.query.year || item.applyYear.includes(this.query.year);
- const typeMatch =
- !this.query.calendarType ||
- item.calendarType === this.query.calendarType;
- const statusMatch =
- this.query.status === '' || item.status === this.query.status;
- return yearMatch && typeMatch && statusMatch;
- });
- },
- async filterCalendar() {
- this.viewDataKey = '';
- this.invalidateConflictCache();
- await this.loadRemoteCalendarData();
- this.reloadBaseTable(1);
- },
- resetQuery() {
- this.query = {
- year: currentYear,
- month: '',
- calendarType: '',
- status: ''
- };
- this.filterCalendar();
- },
- openCalendarDialog(row) {
- this.calendarForm = row ? clone(row) : this.getDefaultCalendarForm();
- this.calendarDialogVisible = true;
- this.loadLegalHolidaysByYears(this.calendarForm.applyYear);
- this.$nextTick(() => {
- this.$refs.calendarForm?.clearValidate?.();
- this.resetCalendarDialogScroll();
- });
- },
- resetCalendarDialogScroll() {
- const dialogBody = document.querySelector(
- '.factory-calendar-dialog .el-dialog__body'
- );
- if (dialogBody) {
- dialogBody.scrollTop = 0;
- }
- document
- .querySelectorAll('.factory-calendar-dialog .legal-holiday-list')
- .forEach((item) => {
- item.scrollTop = 0;
- });
- },
- saveCalendar() {
- this.$refs.calendarForm.validate(async (valid) => {
- if (!valid) {
- return;
- }
- if (this.hasInvalidRestRule()) {
- return;
- }
- if (this.hasInvalidHolidayRules()) {
- return;
- }
- if (this.hasDuplicateCalendarTemplate()) {
- this.$message.error('同类型同年份日历已存在,请勿重复创建');
- return;
- }
- try {
- const payload = this.toRemoteCalendarPayload(this.calendarForm);
- if (this.calendarForm.id) {
- await editCalendar(payload);
- } else {
- const result = await addCalendar(payload);
- if (result?.id) {
- this.calendarForm.id = result.id;
- await updateCalendarStatus({
- id: result.id,
- status: Number(this.calendarForm.status || 0)
- });
- }
- if (result?.calendarCode) {
- this.calendarForm.calendarCode = result.calendarCode;
- }
- }
- await this.loadRemoteCalendarData();
- this.invalidateConflictCache();
- this.reloadBaseTable(1);
- this.calendarDialogVisible = false;
- this.$message.success('保存成功');
- return;
- } catch (e) {
- this.$message.error(e?.message || '保存失败,请检查接口返回');
- }
- });
- },
- hasDuplicateCalendarTemplate() {
- return this.calendars.some((item) => {
- if (item.id === this.calendarForm.id) {
- return false;
- }
- const sameType = item.calendarType === this.calendarForm.calendarType;
- const sameYear = item.applyYear.some((year) =>
- this.calendarForm.applyYear.includes(year)
- );
- return (
- sameType &&
- sameYear &&
- item.calendarName === this.calendarForm.calendarName
- );
- });
- },
- toRemoteCalendarPayload(form) {
- const rulePayload = this.getCalendarRulePayload(form);
- return {
- id: form.id || undefined,
- calendarName: form.calendarName,
- calendarType: form.calendarType,
- status: Number(form.status || 0),
- applyYear: Array.isArray(form.applyYear)
- ? form.applyYear.join(',')
- : form.applyYear,
- remark: this.stripCalendarRuleConfig(form.remark),
- ...rulePayload,
- segmentList: this.buildCalendarSegments(form)
- };
- },
- getFullMonthList() {
- return Array.from({ length: 12 }, (_, index) => index + 1);
- },
- getRestModeByWeekdays(weekdays) {
- const values = (weekdays || []).map(Number).sort().join(',');
- if (values === '0') {
- return 'single';
- }
- if (values === '0,6') {
- return 'double';
- }
- if (!values) {
- return 'none';
- }
- return 'double';
- },
- isRestDateByMode(dateItem, restMode) {
- const weekDay = dateItem.day();
- if (restMode === 'none') {
- return false;
- }
- if (restMode === 'single') {
- return weekDay === 0;
- }
- if (restMode === 'alternate') {
- const yearStart = dayjs(`${dateItem.year()}-01-01`);
- const weekIndex = Math.floor(dateItem.diff(yearStart, 'day') / 7);
- return weekDay === 0 || (weekDay === 6 && weekIndex % 2 === 0);
- }
- if (restMode === 'custom') {
- return false;
- }
- return weekDay === 0 || weekDay === 6;
- },
- getLegalHolidayInfo(dateItem) {
- if (!dateItem || !dateItem.isValid()) {
- return null;
- }
- const holidayInfo = this.getConfiguredHolidayRanges(
- dateItem.year()
- ).find(
- (item) =>
- !dateItem.isBefore(dayjs(item.start), 'day') &&
- !dateItem.isAfter(dayjs(item.end), 'day')
- );
- if (holidayInfo) {
- return {
- name: holidayInfo.name,
- calendarDate: dateItem.format('YYYY-MM-DD')
- };
- }
- const lunar = solarToLunar(
- dateItem.year(),
- dateItem.month() + 1,
- dateItem.date()
- );
- const festivalText = [
- lunar.solarFestival,
- lunar.lunarFestival,
- lunar.term
- ]
- .filter(Boolean)
- .join(' ');
- const match = legalHolidayKeywords.find((item) =>
- festivalText.includes(item.keyword)
- );
- return match
- ? {
- name: match.label,
- calendarDate: dateItem.format('YYYY-MM-DD')
- }
- : null;
- },
- getLegalHolidayFestivalDates(year) {
- const festivalMap = {};
- const firstDay = dayjs(`${year}-01-01`);
- const end = firstDay.endOf('year');
- let cursor = firstDay;
- while (!cursor.isAfter(end, 'day')) {
- const lunar = solarToLunar(
- cursor.year(),
- cursor.month() + 1,
- cursor.date()
- );
- const festivalText = [
- lunar.solarFestival,
- lunar.lunarFestival,
- lunar.term
- ]
- .filter(Boolean)
- .join(' ');
- const match = legalHolidayKeywords.find((item) =>
- festivalText.includes(item.keyword)
- );
- if (match && !festivalMap[match.label]) {
- festivalMap[match.label] = cursor.format('YYYY-MM-DD');
- }
- cursor = cursor.add(1, 'day');
- }
- return festivalMap;
- },
- getInferredHolidayRanges(year) {
- if (inferredHolidayRangeCache[year]) {
- return inferredHolidayRangeCache[year];
- }
- const festivalDates = this.getLegalHolidayFestivalDates(year);
- const ranges = Object.keys(festivalDates).map((name) => {
- const rule = inferredHolidayRules[name] || { before: 0, after: 0 };
- const date = dayjs(festivalDates[name]);
- return {
- name,
- start: date.subtract(rule.before, 'day').format('YYYY-MM-DD'),
- end: date.add(rule.after, 'day').format('YYYY-MM-DD')
- };
- });
- inferredHolidayRangeCache[year] = ranges;
- return ranges;
- },
- getConfiguredHolidayRanges(year) {
- const remoteRanges = this.legalHolidayCache[year];
- if (remoteRanges && remoteRanges.length) {
- return remoteRanges;
- }
- return (
- officialHolidayRanges[year] || this.getInferredHolidayRanges(year)
- );
- },
- async loadLegalHolidaysByYears(applyYear) {
- const years = normalizeYearList(applyYear);
- if (!years.length) {
- return;
- }
- this.legalHolidayLoading = true;
- try {
- await Promise.all(
- years.map((year) => this.loadLegalHolidayYear(year))
- );
- } finally {
- this.legalHolidayLoading = false;
- }
- },
- async loadLegalHolidayYear(year) {
- if (
- this.legalHolidayCache[year] ||
- this.legalHolidayStatusMap[year] === 'fallback'
- ) {
- return this.legalHolidayCache[year];
- }
- if (!this.legalHolidayPromises[year]) {
- this.$set(this.legalHolidayStatusMap, year, 'loading');
- this.legalHolidayPromises[year] = getPublicLegalHolidays(year)
- .then((data) => {
- const ranges = this.normalizePublicHolidayRanges(data, year);
- if (ranges.length) {
- this.$set(this.legalHolidayCache, year, ranges);
- this.$set(this.legalHolidayStatusMap, year, 'remote');
- } else {
- this.$set(this.legalHolidayStatusMap, year, 'fallback');
- }
- return ranges;
- })
- .catch(() => {
- this.$set(this.legalHolidayStatusMap, year, 'fallback');
- return [];
- })
- .finally(() => {
- this.$delete(this.legalHolidayPromises, year);
- });
- }
- return this.legalHolidayPromises[year];
- },
- normalizePublicHolidayRanges(data, year) {
- const rawList = this.extractPublicHolidayList(data);
- const dateMap = {};
- rawList.forEach((item) => {
- const source = this.normalizePublicHolidayItem(item);
- const dateText = this.normalizePublicHolidayDate(
- source.calendarDate,
- year
- );
- if (!dateText) {
- return;
- }
- const isWorkday =
- source.isWorkday === true ||
- source.is_workday === true ||
- source.workday === true ||
- source.is_holiday === false ||
- source.holiday === false ||
- source.type === 'workday' ||
- source.type === 'work' ||
- source.type === 0;
- if (isWorkday) {
- return;
- }
- dateMap[dateText] = {
- name:
- source.name ||
- source.holidayName ||
- source.festival ||
- source.title ||
- '法定节假日',
- calendarDate: dateText
- };
- });
- return this.mergeHolidayDatesToRanges(Object.values(dateMap));
- },
- normalizePublicHolidayItem(item) {
- if (typeof item === 'string') {
- return {
- calendarDate: item
- };
- }
- if (!item || typeof item !== 'object') {
- return {};
- }
- return {
- ...item,
- calendarDate:
- item.date ||
- item.calendarDate ||
- item.holidayDate ||
- item.day ||
- item.time
- };
- },
- normalizePublicHolidayDate(value, year) {
- if (!value && value !== 0) {
- return '';
- }
- const text = String(value).trim();
- let match = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
- if (match) {
- const date = dayjs(
- `${match[1]}-${String(match[2]).padStart(2, '0')}-${String(
- match[3]
- ).padStart(2, '0')}`
- );
- return date.isValid() && date.year() === Number(year)
- ? date.format('YYYY-MM-DD')
- : '';
- }
- match = text.match(/^(\d{4})(\d{2})(\d{2})$/);
- if (match) {
- const date = dayjs(`${match[1]}-${match[2]}-${match[3]}`);
- return date.isValid() && date.year() === Number(year)
- ? date.format('YYYY-MM-DD')
- : '';
- }
- match = text.match(/^(\d{1,2})[-/](\d{1,2})$/);
- if (match) {
- const date = dayjs(
- `${year}-${String(match[1]).padStart(2, '0')}-${String(
- match[2]
- ).padStart(2, '0')}`
- );
- return date.isValid() ? date.format('YYYY-MM-DD') : '';
- }
- return '';
- },
- extractPublicHolidayList(data) {
- if (Array.isArray(data)) {
- return data;
- }
- if (Array.isArray(data?.data)) {
- return data.data;
- }
- if (Array.isArray(data?.holidays)) {
- return data.holidays;
- }
- if (Array.isArray(data?.data?.holidays)) {
- return data.data.holidays;
- }
- const source =
- data?.data && typeof data.data === 'object' ? data.data : data;
- if (source && typeof source === 'object') {
- return Object.keys(source).map((key) => {
- const value = source[key];
- if (typeof value === 'string') {
- return {
- calendarDate: key,
- name: value
- };
- }
- return {
- calendarDate: key,
- ...(value || {})
- };
- });
- }
- return [];
- },
- mergeHolidayDatesToRanges(holidayList) {
- const list = holidayList
- .filter((item) => item.calendarDate)
- .sort((a, b) => a.calendarDate.localeCompare(b.calendarDate));
- const ranges = [];
- list.forEach((item) => {
- const last = ranges[ranges.length - 1];
- if (
- last &&
- last.name === item.name &&
- dayjs(item.calendarDate).diff(dayjs(last.end), 'day') === 1
- ) {
- last.end = item.calendarDate;
- } else {
- ranges.push({
- name: item.name,
- start: item.calendarDate,
- end: item.calendarDate
- });
- }
- });
- return ranges;
- },
- getLegalHolidayName(dateItem) {
- return this.getLegalHolidayInfo(dateItem)?.name || '';
- },
- getLegalHolidayOptions(applyYear, applyMonth) {
- const monthSet = new Set(
- (applyMonth || this.getFullMonthList()).map(Number)
- );
- const holidayMap = {};
- normalizeYearList(applyYear).forEach((year) => {
- const ranges = this.getConfiguredHolidayRanges(year);
- ranges.forEach((range) => {
- let cursor = dayjs(range.start);
- const end = dayjs(range.end);
- while (!cursor.isAfter(end, 'day')) {
- if (monthSet.has(cursor.month() + 1)) {
- const calendarDate = cursor.format('YYYY-MM-DD');
- if (!holidayMap[calendarDate]) {
- holidayMap[calendarDate] = {
- name: range.name,
- calendarDate
- };
- }
- }
- cursor = cursor.add(1, 'day');
- }
- });
- });
- return Object.values(holidayMap).sort((a, b) =>
- a.calendarDate.localeCompare(b.calendarDate)
- );
- },
- getCalendarDateType(cursor, restMode, customRestWeekdays, specialRule) {
- if (specialRule) {
- return specialRule.dateType;
- }
- if (this.getLegalHolidayName(cursor)) {
- return 3;
- }
- if (restMode === 'custom') {
- return customRestWeekdays.has(cursor.day()) ? 2 : 1;
- }
- return this.isRestDateByMode(cursor, restMode) ? 2 : 1;
- },
- getLegalHolidayRules(form) {
- const holidayInfoMap = {};
- this.getLegalHolidayOptions(form.applyYear, form.applyMonth).forEach(
- (item) => {
- holidayInfoMap[item.calendarDate] = item;
- }
- );
- return (form.legalHolidayWorkDates || [])
- .filter((date) => holidayInfoMap[date])
- .map((date) => ({
- calendarDate: date,
- dateType: 1,
- remark: holidayInfoMap[date].name
- }));
- },
- buildCalendarSegments(form) {
- const ranges = form.timeRanges || [];
- const monthList = (form.applyMonth || this.getFullMonthList())
- .map(Number)
- .filter(Boolean);
- const applyMonths = new Set(monthList);
- const restMode =
- form.restMode || this.getRestModeByWeekdays(form.restWeekdays);
- const customRestWeekdays = new Set(
- (form.restWeekdays || []).map(Number)
- );
- const specialRuleMap = {};
- const specialRules = [
- ...this.getLegalHolidayRules(form),
- ...(form.holidayRules || [])
- ];
- specialRules.forEach((rule) => {
- if (rule.calendarDate) {
- specialRuleMap[rule.calendarDate] = {
- ...rule,
- dateType: Number(rule.dateType || 3)
- };
- }
- });
- const segments = [];
- if (!applyMonths.size) {
- return segments;
- }
- normalizeYearList(form.applyYear).forEach((year) => {
- const months = Array.from(applyMonths).sort((a, b) => a - b);
- let cursor = dayjs(
- `${year}-${String(months[0]).padStart(2, '0')}-01`
- );
- const lastMonth = months[months.length - 1];
- const end = dayjs(
- `${year}-${String(lastMonth).padStart(2, '0')}-01`
- ).endOf('month');
- while (!cursor.isAfter(end, 'day')) {
- if (!applyMonths.has(cursor.month() + 1)) {
- cursor = cursor.add(1, 'day');
- continue;
- }
- const calendarDate = cursor.format('YYYY-MM-DD');
- const specialRule = specialRuleMap[calendarDate];
- const legalHolidayName = this.getLegalHolidayName(cursor);
- const dateType = this.getCalendarDateType(
- cursor,
- restMode,
- customRestWeekdays,
- specialRule
- );
- if (dateType === 1) {
- const workRanges = ranges.length
- ? ranges
- : [
- {
- startTime: '00:00',
- endTime: '23:59',
- scheduleStatus: 0,
- remark:
- specialRule?.remark || legalHolidayName || '工作日'
- }
- ];
- workRanges.forEach((range) => {
- segments.push({
- calendarDate,
- startTime: range.startTime,
- endTime: range.endTime,
- dateType: 1,
- scheduleStatus:
- range.scheduleStatus === undefined
- ? 0
- : range.scheduleStatus,
- remark:
- range.name ||
- range.remark ||
- specialRule?.remark ||
- legalHolidayName ||
- ''
- });
- });
- } else {
- segments.push({
- calendarDate,
- startTime: '00:00',
- endTime: '23:59',
- dateType,
- scheduleStatus: 0,
- remark:
- specialRule?.remark ||
- legalHolidayName ||
- (dateType === 3 ? '法定节假日' : '休息日')
- });
- }
- cursor = cursor.add(1, 'day');
- }
- });
- return segments;
- },
- hasInvalidRestRule() {
- if (
- this.calendarForm.restMode === 'custom' &&
- !(this.calendarForm.restWeekdays || []).length
- ) {
- this.$message.error('自定义休息请至少选择一个休息星期');
- return true;
- }
- return false;
- },
- hasInvalidHolidayRules() {
- const dateMap = {};
- (this.calendarForm.legalHolidayWorkDates || []).forEach((date) => {
- dateMap[date] = true;
- });
- const years = new Set(
- normalizeYearList(this.calendarForm.applyYear).map(String)
- );
- const months = new Set(
- (this.calendarForm.applyMonth || []).map((item) => Number(item))
- );
- for (const rule of this.calendarForm.holidayRules || []) {
- if (!rule.calendarDate) {
- this.$message.error('特殊日期存在未选择日期的配置,请补充或删除');
- return true;
- }
- if (!years.has(dayjs(rule.calendarDate).format('YYYY'))) {
- this.$message.error('特殊日期必须在适用年份范围内');
- return true;
- }
- if (!months.has(dayjs(rule.calendarDate).month() + 1)) {
- this.$message.error('特殊日期必须在适用月份范围内');
- return true;
- }
- if (dateMap[rule.calendarDate]) {
- this.$message.error('特殊日期存在重复日期,请调整后保存');
- return true;
- }
- dateMap[rule.calendarDate] = true;
- }
- return false;
- },
- hasDuplicateDetail(
- calendarId,
- calendarDate,
- startTime,
- endTime,
- excludeId
- ) {
- const calendar = this.calendars.find((item) => item.id === calendarId);
- if (!calendar) {
- return false;
- }
- return this.details.some((item) => {
- const itemCalendar = this.calendars.find(
- (calendarItem) => calendarItem.id === item.calendarId
- );
- return (
- item.id !== excludeId &&
- item.calendarDate === calendarDate &&
- itemCalendar?.calendarType === calendar.calendarType &&
- isOverlap(startTime, endTime, item.startTime, item.endTime)
- );
- });
- },
- addHolidayRule() {
- this.calendarForm.holidayRules.push(createHolidayRule(1));
- },
- removeHolidayRule(index) {
- this.calendarForm.holidayRules.splice(index, 1);
- },
- handleApplyMonthChange() {
- this.pruneLegalHolidayWorkDates();
- },
- pruneLegalHolidayWorkDates() {
- const visibleDates = new Set(
- this.legalHolidayOptions.map((item) => item.calendarDate)
- );
- this.calendarForm.legalHolidayWorkDates = (
- this.calendarForm.legalHolidayWorkDates || []
- ).filter((date) => visibleDates.has(date));
- },
- changeLegalHolidayWorkStatus(calendarDate, checked) {
- const dates = new Set(this.calendarForm.legalHolidayWorkDates || []);
- if (checked) {
- dates.add(calendarDate);
- } else {
- dates.delete(calendarDate);
- }
- this.calendarForm.legalHolidayWorkDates = Array.from(dates).sort();
- },
- toggleLegalHolidayWorkStatus(calendarDate) {
- const checked = !(
- this.calendarForm.legalHolidayWorkDates || []
- ).includes(calendarDate);
- this.changeLegalHolidayWorkStatus(calendarDate, checked);
- },
- formatHolidayDate(calendarDate) {
- return dayjs(calendarDate).format('MM-DD');
- },
- getTimePickerOptions(minTime) {
- return minTime
- ? {
- ...this.timePickerOptions,
- minTime
- }
- : this.timePickerOptions;
- },
- async changeCalendarStatus(row) {
- try {
- await updateCalendarStatus({
- id: row.id,
- status: row.status
- });
- await this.loadRemoteCalendarData();
- this.invalidateConflictCache();
- this.reloadBaseTable(1);
- this.$message.success(row.status ? '已启用' : '已禁用');
- return;
- } catch (e) {
- row.status = row.status === 1 ? 0 : 1;
- this.$message.error(e?.message || '状态更新失败,请检查接口返回');
- }
- },
- async deleteCalendarRow(row) {
- if (!row?.id) {
- this.$message.error('未获取到日历ID,无法删除');
- return;
- }
- try {
- const calendarName = row.calendarName || row.calendarCode || row.id;
- await this.$confirm(`确认删除日历 ${calendarName} 吗?`, '删除确认', {
- type: 'warning'
- });
- await deleteCalendar([row.id]);
- await this.loadRemoteCalendarData();
- this.invalidateConflictCache();
- this.reloadBaseTable(1);
- await this.refreshRemoteView();
- this.$message.success('删除成功');
- } catch (e) {
- if (e === 'cancel' || e === 'close') {
- return;
- }
- this.$message.error(e?.message || '删除失败,请检查接口返回');
- }
- },
- openCopyDialog() {
- this.copyForm = {
- sourceId: this.calendars[0]?.id || '',
- targetYear: currentYear + 1
- };
- this.copyDialogVisible = true;
- },
- async copyCalendar() {
- const source = this.calendars.find(
- (item) => item.id === this.copyForm.sourceId
- );
- if (!source || !this.copyForm.targetYear) {
- this.$message.error('请选择源日历和目标年份');
- return;
- }
- try {
- await copyCalendar({
- sourceId: this.copyForm.sourceId,
- targetYear: this.copyForm.targetYear
- });
- await this.loadRemoteCalendarData();
- this.invalidateConflictCache();
- this.reloadBaseTable(1);
- this.copyDialogVisible = false;
- this.$message.success('复制成功,新日历默认禁用');
- return;
- } catch (e) {
- this.$message.error(e?.message || '复制失败,请检查接口返回');
- }
- },
- async locateCalendar(row) {
- this.activeTab = 'view';
- this.viewQuery.calendarType = row.calendarType;
- this.viewQuery.year = row.applyYear[0] || currentYear;
- this.viewQuery.date = `${this.viewQuery.year}-${String(
- this.viewQuery.month
- ).padStart(2, '0')}-01`;
- this.viewMonth = `${this.viewQuery.year}-${String(
- this.viewQuery.month
- ).padStart(2, '0')}`;
- await this.refreshRemoteView();
- },
- async changeViewCalendarType() {
- await this.refreshRemoteView();
- },
- async changeViewMonth(value) {
- if (!value) {
- return;
- }
- const [year, month] = value.split('-');
- this.viewQuery.year = Number(year);
- this.viewQuery.month = Number(month);
- this.viewQuery.date = `${year}-${month}-01`;
- await this.refreshRemoteView();
- },
- async changeViewType(type) {
- if (type === 'quarter') {
- const quarterStartMonth =
- Math.floor((this.viewQuery.month - 1) / 3) * 3 + 1;
- this.viewQuery.month = quarterStartMonth;
- this.viewQuery.date = `${this.viewQuery.year}-${String(
- quarterStartMonth
- ).padStart(2, '0')}-01`;
- this.viewMonth = `${this.viewQuery.year}-${String(
- quarterStartMonth
- ).padStart(2, '0')}`;
- await this.refreshRemoteView();
- return;
- }
- if (type === 'month') {
- this.viewQuery.date = `${this.viewQuery.year}-${String(
- this.viewQuery.month
- ).padStart(2, '0')}-01`;
- }
- await this.refreshRemoteView();
- },
- async refreshRemoteView() {
- await this.loadRemoteViewData(true);
- this.plans = [];
- this.planDataKey = '';
- this.reloadPlanTable(1);
- },
- async refreshStatView() {
- await this.loadRemoteViewData(true, {
- viewType: 'month',
- date: `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
- 2,
- '0'
- )}-01`
- });
- },
- getViewDetails(date) {
- return this.details.filter(
- (item) =>
- this.isDetailInCurrentView(item) && item.calendarDate === date
- );
- },
- getStatMonthDetails() {
- const monthStart = dayjs(
- `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
- 2,
- '0'
- )}-01`
- );
- const monthEnd = monthStart.endOf('month');
- return this.details.filter((item) => {
- const date = dayjs(item.calendarDate);
- return (
- this.isDetailInCurrentView(item) &&
- date.isValid() &&
- (date.isSame(monthStart, 'day') || date.isAfter(monthStart)) &&
- (date.isSame(monthEnd, 'day') || date.isBefore(monthEnd))
- );
- });
- },
- isDetailInCurrentView(item = {}) {
- const targetType = Number(this.viewQuery.calendarType);
- const detailType = Number(item.calendarType);
- const typeMatched = targetType ? detailType === targetType : true;
- const calendarIds = new Set(
- this.getCurrentViewCalendars().map((calendar) => String(calendar.id))
- );
- const hasCalendarId =
- item.calendarId !== undefined &&
- item.calendarId !== null &&
- item.calendarId !== '';
- if (hasCalendarId && calendarIds.has(String(item.calendarId))) {
- return true;
- }
- if (item.isViewDetail) {
- return typeMatched;
- }
- return !hasCalendarId && typeMatched;
- },
- isDetailScheduled(item = {}) {
- return (
- Number(item.scheduleStatus) === 1 ||
- Number(item.relationPlanCount || 0) > 0 ||
- this.getDetailRelatedPlans([item]).length > 0
- );
- },
- isWeekend(date) {
- const week = dayjs(date).day();
- return week === 0 || week === 6;
- },
- isRestDay(date, details) {
- if (!details.length) {
- return this.isWeekend(date);
- }
- return details.every((item) => [2, 3].includes(item.dateType));
- },
- getConflictText(date) {
- const conflict = this.conflictList.find(
- (item) => item.calendarDate === date
- );
- return conflict?.message || '';
- },
- matchStatusFilter(day) {
- const filter = this.viewQuery.statusFilter;
- if (!filter || !day.date) {
- return true;
- }
- const matchMap = {
- scheduled: day.scheduleStatus,
- idle: !day.scheduleStatus && !day.isRest,
- rest: day.isRest,
- maintenance: day.isMaintenance,
- temp: day.isTempAdjust,
- conflict: day.isConflict
- };
- return matchMap[filter];
- },
- getDayClass(day) {
- if (!day.date) {
- return ['empty'];
- }
- if (day.isDisabledCalendar) {
- return ['disabled-calendar'];
- }
- if (day.hiddenByFilter) {
- return ['muted'];
- }
- if (day.isConflict) {
- return ['conflict'];
- }
- if (day.isTempAdjust) {
- return ['temp'];
- }
- if (day.isMaintenance) {
- return ['maintenance'];
- }
- if (day.isHoliday) {
- return ['holiday'];
- }
- if (day.isRest) {
- return ['rest'];
- }
- if (day.scheduleStatus) {
- return ['scheduled'];
- }
- return ['idle'];
- },
- getDateTypeText(day) {
- if (!day.date) {
- return '';
- }
- if (day.isHoliday) {
- return day.holidayName || '法定节假日';
- }
- if (day.isRest && !day.details.length) {
- return '休息日';
- }
- const first = day.details[0];
- return first
- ? this.getLabel(dateTypeOptions, first.dateType)
- : '正常工作日';
- },
- getRangeText(details) {
- if (!details.length) {
- return '无有效时段';
- }
- return details
- .map((item) => `${item.startTime}-${item.endTime}`)
- .join('、');
- },
- buildAdjustSegments() {
- return [
- {
- calendarDate: this.adjustForm.adjustDate,
- startTime: this.adjustForm.startTime,
- endTime: this.adjustForm.endTime,
- dateType: this.getAdjustDateType(this.adjustForm.adjustType),
- scheduleStatus: this.adjustForm.adjustType === 2 ? 0 : 1,
- remark: this.adjustForm.applyReason || ''
- }
- ];
- },
- stringifySegments(segments) {
- return JSON.stringify(segments || []);
- },
- parseSegmentContent(content) {
- if (Array.isArray(content)) {
- return content;
- }
- if (!content || typeof content !== 'string') {
- return [];
- }
- try {
- const parsed = JSON.parse(content);
- return Array.isArray(parsed) ? parsed : [parsed];
- } catch (e) {
- const [timeRange] = content.split(' ');
- const [startTime, endTime] = timeRange.split('-');
- return startTime && endTime ? [{ startTime, endTime }] : [];
- }
- },
- formatSegmentContent(content, row = {}) {
- const segments = this.parseSegmentContent(content);
- if (!segments.length) {
- return content || '';
- }
- return segments
- .map((segment) => {
- const startTime = String(segment.startTime || '').slice(0, 5);
- const endTime = String(segment.endTime || '').slice(0, 5);
- const dateType =
- segment.dateType || this.getAdjustDateType(row.adjustType);
- const dateText = segment.calendarDate
- ? `${segment.calendarDate} `
- : '';
- const typeText = this.getLabel(dateTypeOptions, dateType);
- return `${dateText}${startTime}-${endTime} ${typeText}`.trim();
- })
- .join('、');
- },
- getAdjustApplicantName(row = {}) {
- return (
- row.applyUserName ||
- row.applicantName ||
- row.createUserName ||
- row.userName ||
- row.applyUserId ||
- '-'
- );
- },
- formatApproveNodes(approveNode) {
- if (!approveNode) {
- return [];
- }
- let nodes = approveNode;
- if (typeof approveNode === 'string') {
- try {
- nodes = JSON.parse(approveNode);
- } catch (e) {
- return [
- {
- statusText: '审批记录',
- approveOpinion: approveNode,
- approveTime: '',
- approveUserId: ''
- }
- ];
- }
- }
- const list = Array.isArray(nodes) ? nodes : [nodes];
- return list
- .filter((item) => item && typeof item === 'object')
- .map((item) => {
- const status = Number(item.applyStatus ?? item.approveStatus);
- return {
- statusText:
- this.getLabel(approvalStatusOptions, status) ||
- item.statusText ||
- '审批记录',
- approveOpinion:
- item.approveOpinion || item.opinion || item.remark || '',
- approveTime: String(item.approveTime || item.createTime || '')
- .replace('T', ' ')
- .slice(0, 19),
- approveUserId: item.approveUserId || item.userId || ''
- };
- });
- },
- async openDayDrawer(day) {
- if (!day.date || day.hiddenByFilter) {
- return;
- }
- if (day.isDisabledCalendar) {
- this.$message.warning('当前类型日历已禁用,不可编辑');
- return;
- }
- await this.loadRemoteViewData();
- const details = this.getViewDetails(day.date);
- const relatedPlans = this.getDetailRelatedPlans(details);
- this.currentDay = {
- ...day,
- details,
- relatedPlans,
- planCount: this.getDetailPlanCount(details)
- };
- this.dayDrawerVisible = true;
- },
- openAdjustDialog(day = {}) {
- this.adjustForm = this.getDefaultAdjustForm(day);
- const calendar = this.enabledCalendars.find(
- (item) => item.calendarType === this.viewQuery.calendarType
- );
- if (calendar) {
- this.adjustForm.calendarId = calendar.id;
- }
- this.adjustDialogVisible = true;
- this.$nextTick(() => {
- this.$refs.adjustForm?.clearValidate?.();
- });
- },
- submitAdjust() {
- this.$refs.adjustForm.validate((valid) => {
- if (!valid) {
- return;
- }
- if (!this.adjustForm.startTime || !this.adjustForm.endTime) {
- this.$message.error('请选择调整时段');
- return;
- }
- const calendar = this.calendars.find(
- (item) => item.id === this.adjustForm.calendarId
- );
- if (!calendar) {
- this.$message.error('请选择关联日历');
- return;
- }
- const saveApply = async () => {
- const originalDetails = this.details.filter(
- (item) =>
- item.calendarId === calendar.id &&
- item.calendarDate === this.adjustForm.adjustDate
- );
- const newSegments = this.buildAdjustSegments();
- const adjustRow = {
- id: Date.now(),
- applyNo: createCode('ADJ'),
- calendarId: calendar.id,
- calendarName: calendar.calendarName,
- adjustType: this.adjustForm.adjustType,
- adjustDate: this.adjustForm.adjustDate,
- oldContent: this.getRangeText(originalDetails),
- originalDetails: clone(originalDetails),
- newContent: this.stringifySegments(newSegments),
- newContentText: `${this.adjustForm.startTime}-${
- this.adjustForm.endTime
- } ${this.getLabel(
- adjustTypeOptions,
- this.adjustForm.adjustType
- )}`,
- effectiveTime: this.adjustForm.effectiveTime,
- expireTime: this.adjustForm.expireTime,
- applyReason: this.adjustForm.applyReason,
- applyStatus: 0,
- isConflict: 0,
- applyUserName: '当前用户',
- applyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
- approveNode: '',
- canReactivate: true
- };
- try {
- await applyCalendarAdjust({
- calendarId: calendar.id,
- adjustType: this.adjustForm.adjustType,
- adjustDate: this.adjustForm.adjustDate,
- oldContent: adjustRow.oldContent,
- newContent: adjustRow.newContent,
- effectiveTime: this.adjustForm.effectiveTime,
- expireTime: this.adjustForm.expireTime,
- applyReason: this.adjustForm.applyReason
- });
- await this.loadRemoteAdjustData();
- this.invalidateConflictCache();
- this.reloadAdjustTable(1);
- this.adjustDialogVisible = false;
- this.$message.success('已提交审批');
- return;
- } catch (e) {
- this.$message.error(e?.message || '提交审批失败,请检查接口返回');
- }
- };
- saveApply();
- });
- },
- openApproveDialog(row, status) {
- if (row.applyStatus === 3) {
- this.$message.warning('已失效单据仅可查询追溯,无法重新生效');
- return;
- }
- this.approveForm = {
- row,
- status,
- opinion: status === 1 ? '审批通过' : ''
- };
- this.approveDialogVisible = true;
- },
- async confirmApproveAdjust() {
- if (this.approveForm.status === 2 && !this.approveForm.opinion) {
- this.$message.error('请输入驳回原因');
- return;
- }
- await this.approveAdjust(
- this.approveForm.row,
- this.approveForm.status,
- this.approveForm.opinion
- );
- this.approveDialogVisible = false;
- },
- async approveAdjust(row, status, opinion = '') {
- try {
- await approveCalendarAdjust({
- id: row.id,
- applyStatus: status,
- approveOpinion: opinion
- });
- await this.loadRemoteAdjustData();
- await this.loadRemoteViewData(true);
- this.invalidateConflictCache();
- this.reloadAdjustTable();
- if (status === 1) {
- this.reloadPlanTable(1);
- }
- this.$message.success(status === 1 ? '审批通过并已生效' : '已驳回');
- return;
- } catch (e) {
- this.$message.error(e?.message || '审批失败,请检查接口返回');
- }
- },
- getAdjustDateType(adjustType) {
- return (
- {
- 1: 5,
- 2: 2,
- 3: 4,
- 4: 5
- }[adjustType] || 5
- );
- },
- expireAdjustments() {
- // 已停用:临时调整失效由后端任务/接口处理。
- },
- async checkSingleDay(day) {
- if (!day.date || !day.details.length) {
- this.$message.success('当前日期未发现冲突');
- return;
- }
- try {
- const results = await Promise.all(
- day.details.map((detail) =>
- checkCalendarConflict({
- calendarType:
- detail.calendarType || this.viewQuery.calendarType,
- calendarDate: day.date,
- startTime: detail.startTime,
- endTime: detail.endTime,
- dateType: detail.dateType,
- scheduleStatus: detail.scheduleStatus,
- excludeDetailId: detail.id
- })
- )
- );
- const conflicts = results
- .map((item) => item?.conflictDetail || [])
- .flat();
- if (conflicts.length) {
- this.$message.warning(`发现 ${conflicts.length} 条冲突`);
- } else {
- this.$message.success('当前日期未发现冲突');
- }
- } catch (e) {
- this.$message.error(e?.message || '冲突检测失败,请检查接口返回');
- }
- },
- async toggleScheduleStatus(status) {
- if (!this.currentDay.date || this.currentDay.isDisabledCalendar) {
- this.$message.warning('当前日历不可编辑');
- return;
- }
- const calendar = this.enabledCalendars.find(
- (item) => item.calendarType === this.viewQuery.calendarType
- );
- if (!calendar) {
- this.$message.warning('当前类型日历已禁用');
- return;
- }
- const currentDetails = this.getViewDetails(this.currentDay.date);
- const nextDetails = currentDetails.map((detail) => ({
- ...detail,
- calendarId: calendar.id,
- calendarDate: this.currentDay.date,
- scheduleStatus: status
- }));
- try {
- await Promise.all(
- nextDetails.map((nextDetail) =>
- saveCalendarDetail({
- id: nextDetail.id,
- calendarId: nextDetail.calendarId,
- calendarDate: nextDetail.calendarDate,
- startTime: nextDetail.startTime,
- endTime: nextDetail.endTime,
- dateType: nextDetail.dateType || 1,
- scheduleStatus: nextDetail.scheduleStatus,
- remark: nextDetail.remark,
- forceSave: true
- })
- )
- );
- await this.loadRemoteViewData(true);
- const details = this.getViewDetails(this.currentDay.date);
- const relatedPlans = this.getDetailRelatedPlans(details);
- this.currentDay = {
- ...this.currentDay,
- details,
- relatedPlans,
- planCount: this.getDetailPlanCount(details),
- scheduleStatus: !!status
- };
- this.reloadPlanTable(1);
- this.invalidateConflictCache();
- this.$message.success(status ? '已完成排班并回写状态' : '已取消排班');
- return;
- } catch (e) {
- this.$message.error(e?.message || '排班状态回写失败,请检查接口返回');
- }
- },
- getConflictDataKey() {
- return [
- this.viewQuery.year || '',
- this.getConflictCheckTypes().join(',')
- ].join('|');
- },
- getConflictCheckTypes() {
- return this.calendarTypeOptions
- .map((item) => Number(item.value))
- .filter((type) => [1, 2, 3].includes(type));
- },
- invalidateConflictCache() {
- this.conflictDataKey = '';
- },
- async runGlobalConflictCheck(showMessage = true, force = showMessage) {
- const dataKey = this.getConflictDataKey();
- if (!force && this.conflictDataKey === dataKey) {
- this.reloadConflictTable(1);
- return;
- }
- if (this.conflictCheckPromise) {
- await this.conflictCheckPromise;
- return;
- }
- try {
- const conflictTypes = this.getConflictCheckTypes();
- this.conflictCheckPromise = Promise.all(
- conflictTypes.map((calendarType) =>
- checkAllCalendarConflict({
- year: this.viewQuery.year,
- calendarType
- })
- )
- );
- const list = await this.conflictCheckPromise;
- this.conflictList = this.uniqueConflicts(
- list.flatMap((data) => this.normalizeRemoteConflicts(data))
- );
- this.conflictDataKey = dataKey;
- this.reloadConflictTable(1);
- if (showMessage) {
- this.$message[this.conflictList.length ? 'warning' : 'success'](
- this.conflictList.length
- ? `检测完成,发现 ${this.conflictList.length} 条冲突`
- : '检测完成,未发现冲突'
- );
- }
- return;
- } catch (e) {
- this.conflictList = [];
- this.conflictDataKey = '';
- if (showMessage) {
- this.$message.error(e?.message || '冲突检测失败,请检查接口返回');
- }
- } finally {
- this.conflictCheckPromise = null;
- }
- this.reloadConflictTable(1);
- },
- checkConflict({
- calendarType,
- calendarDate,
- startTime,
- endTime,
- dateType = 1,
- scheduleStatus = 1,
- sourceId
- }) {
- const sameDay = this.details.filter((item) => {
- const calendar = this.calendars.find(
- (calendarItem) => calendarItem.id === item.calendarId
- );
- return (
- item.id !== sourceId &&
- item.calendarDate === calendarDate &&
- calendar?.status === 1 &&
- isOverlap(startTime, endTime, item.startTime, item.endTime)
- );
- });
- const conflicts = [];
- sameDay.forEach((item) => {
- const targetCalendar = this.calendars.find(
- (calendarItem) => calendarItem.id === item.calendarId
- );
- const targetType = targetCalendar?.calendarType;
- const source = {
- calendarType,
- dateType,
- scheduleStatus
- };
- const target = {
- calendarType: targetType,
- dateType: item.dateType,
- scheduleStatus: item.scheduleStatus
- };
- const timeRange = `${Math.max(
- toMinute(startTime),
- toMinute(item.startTime)
- )}-${Math.min(toMinute(endTime), toMinute(item.endTime))}`;
- const readableRange = `${startTime}-${endTime}`;
- if (
- [source.calendarType, target.calendarType].includes(1) &&
- [source.calendarType, target.calendarType].includes(2) &&
- [source, target].some(
- (segment) => segment.calendarType === 2 && segment.dateType === 4
- )
- ) {
- conflicts.push({
- id: `${calendarDate}-${timeRange}-1-2`,
- calendarDate,
- timeRange: readableRange,
- scene: '生产日历 VS 设备维护日历',
- level: '高',
- message: '当前生产作业时段设备停机,无法排产'
- });
- }
- if (
- [source.calendarType, target.calendarType].includes(1) &&
- [source.calendarType, target.calendarType].includes(3) &&
- [source, target].some(
- (segment) =>
- segment.calendarType === 3 &&
- (segment.dateType === 2 ||
- segment.dateType === 3 ||
- segment.scheduleStatus === 0)
- )
- ) {
- conflicts.push({
- id: `${calendarDate}-${timeRange}-1-3`,
- calendarDate,
- timeRange: readableRange,
- scene: '生产日历 VS 人员排班日历',
- level: '高',
- message: '当前生产时段无在岗人员,人力缺失'
- });
- }
- if (
- [source.calendarType, target.calendarType].includes(2) &&
- [source.calendarType, target.calendarType].includes(3) &&
- [source, target].some(
- (segment) =>
- segment.calendarType === 3 &&
- (segment.dateType === 2 ||
- segment.dateType === 3 ||
- segment.scheduleStatus === 0)
- )
- ) {
- conflicts.push({
- id: `${calendarDate}-${timeRange}-2-3`,
- calendarDate,
- timeRange: readableRange,
- scene: '设备维护日历 VS 人员排班日历',
- level: '中',
- message: '设备检修时段维保人员缺失'
- });
- }
- });
- if (calendarType === 1) {
- const hasStaff = sameDay.some((item) => {
- const targetCalendar = this.calendars.find(
- (calendarItem) => calendarItem.id === item.calendarId
- );
- return (
- targetCalendar?.calendarType === 3 &&
- item.scheduleStatus === 1 &&
- ![2, 3].includes(item.dateType)
- );
- });
- if (!hasStaff) {
- conflicts.push({
- id: `${calendarDate}-${startTime}-${endTime}-1-3-missing`,
- calendarDate,
- timeRange: `${startTime}-${endTime}`,
- scene: '生产日历 VS 人员排班日历',
- level: '高',
- message: '当前生产时段无在岗人员,人力缺失'
- });
- }
- }
- if (calendarType === 2) {
- const hasMaintainer = sameDay.some((item) => {
- const targetCalendar = this.calendars.find(
- (calendarItem) => calendarItem.id === item.calendarId
- );
- return (
- targetCalendar?.calendarType === 3 &&
- item.scheduleStatus === 1 &&
- ![2, 3].includes(item.dateType)
- );
- });
- if (!hasMaintainer) {
- conflicts.push({
- id: `${calendarDate}-${startTime}-${endTime}-2-3-missing`,
- calendarDate,
- timeRange: `${startTime}-${endTime}`,
- scene: '设备维护日历 VS 人员排班日历',
- level: '中',
- message: '设备检修时段维保人员缺失'
- });
- }
- }
- return conflicts;
- },
- uniqueConflicts(conflicts) {
- const map = {};
- conflicts.forEach((item) => {
- map[item.id] = item;
- });
- return Object.values(map);
- },
- async locateConflict(row) {
- this.activeTab = 'view';
- this.viewQuery.viewType = 'day';
- this.viewMonth = dayjs(row.calendarDate).format('YYYY-MM');
- this.viewQuery.year = Number(dayjs(row.calendarDate).format('YYYY'));
- this.viewQuery.month = Number(dayjs(row.calendarDate).format('M'));
- this.viewQuery.date = row.calendarDate;
- this.viewQuery.statusFilter = 'conflict';
- await this.refreshRemoteView();
- },
- handleTabChange() {
- if (this.activeTab === 'base') {
- this.reloadBaseTable(1);
- }
- if (this.activeTab === 'view') {
- this.refreshRemoteView();
- }
- if (this.activeTab === 'stat') {
- this.refreshStatView();
- }
- if (this.activeTab === 'conflict') {
- this.runGlobalConflictCheck(false);
- }
- if (this.activeTab === 'adjust') {
- this.reloadAdjustTable(1);
- }
- if (this.activeTab === 'plan') {
- this.reloadPlanTable(1);
- }
- },
- getPlanDataKey() {
- const calendar = this.getCurrentViewCalendar();
- return [
- this.getRemoteCalendarId(calendar) || '',
- this.viewQuery.year || '',
- this.viewQuery.month || ''
- ].join('|');
- },
- async ensurePlanRefreshData(force = false) {
- const calendar = this.getCurrentViewCalendar();
- const calendarId = this.getRemoteCalendarId(calendar);
- if (!calendarId) {
- this.plans = [];
- this.planDataKey = '';
- return;
- }
- const dataKey = this.getPlanDataKey();
- if (!force && this.planDataKey === dataKey && this.plans.length) {
- return;
- }
- if (!force && this.planDataPromise) {
- await this.planDataPromise;
- return;
- }
- try {
- this.planDataPromise = refreshCalendarPlan({
- calendarId,
- startDate: `${this.viewQuery.year}-${String(
- this.viewQuery.month
- ).padStart(2, '0')}-01`,
- endDate: dayjs(
- `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
- 2,
- '0'
- )}-01`
- )
- .endOf('month')
- .format('YYYY-MM-DD')
- });
- const data = await this.planDataPromise;
- this.plans = this.normalizeRemotePlans(data);
- this.planDataKey = dataKey;
- } catch (e) {
- this.plans = [];
- this.planDataKey = '';
- } finally {
- this.planDataPromise = null;
- }
- },
- async refreshPlans() {
- try {
- await this.ensurePlanRefreshData(true);
- await this.loadRemoteViewData(true);
- this.reloadPlanTable(1);
- this.$message.success(`已刷新受影响计划,共 ${this.plans.length} 条`);
- } catch (e) {
- this.plans = [];
- this.reloadPlanTable(1);
- }
- },
- reschedulePlans() {
- this.$message.warning('批量重排需要后端返回可重排计划后执行');
- this.reloadPlanTable(1);
- },
- async jumpPlanToCalendar(row) {
- const calendar = this.calendars.find(
- (item) => item.id === row.calendarId
- );
- this.activeTab = 'view';
- this.viewQuery.calendarType = calendar?.calendarType || 1;
- this.viewQuery.viewType = 'day';
- this.viewQuery.date = row.calendarDate;
- this.viewMonth = dayjs(row.calendarDate).format('YYYY-MM');
- await this.changeViewMonth(this.viewMonth);
- this.viewQuery.date = row.calendarDate;
- this.$nextTick(async () => {
- await this.loadRemoteViewData(true);
- const details = this.getViewDetails(row.calendarDate);
- const relatedPlans = this.getDetailRelatedPlans(details);
- const day = {
- key: row.calendarDate,
- date: row.calendarDate,
- dayText: dayjs(row.calendarDate).format('D'),
- details,
- relatedPlans,
- planCount: this.getDetailPlanCount(details),
- scheduleStatus: details.some((item) => item.scheduleStatus === 1),
- isConflict: details.some((item) => item.isConflict === 1),
- isTempAdjust: details.some((item) => item.isTempAdjust === 1),
- isMaintenance: details.some((item) => item.dateType === 4),
- isRest: this.isRestDay(row.calendarDate, details),
- isDisabledCalendar: !this.hasEnabledCalendarForView(),
- conflictText: this.getConflictText(row.calendarDate)
- };
- this.openDayDrawer(day);
- });
- },
- exportCalendar() {
- const rows = this.filteredCalendars.map((item) => ({
- calendarCode: item.calendarCode,
- calendarName: item.calendarName,
- calendarType: this.getLabel(calendarTypeOptions, item.calendarType),
- applyYear: item.applyYear.join(','),
- status: item.status ? '启用' : '禁用'
- }));
- this.downloadJson(
- rows,
- `factory-calendar-${dayjs().format('YYYYMMDDHHmmss')}.json`
- );
- this.$message.success(`导出成功,共 ${rows.length} 条`);
- },
- openAdjustDetail(row) {
- this.adjustDetail = clone(row);
- this.adjustDetailVisible = true;
- },
- exportAdjustRecord() {
- if (!this.adjustDetail.id) {
- return;
- }
- this.downloadJson(
- this.adjustDetail,
- `calendar-adjust-${this.adjustDetail.applyNo}.json`
- );
- this.$message.success('追溯记录已导出');
- },
- downloadJson(data, filename) {
- const blob = new Blob([JSON.stringify(data, null, 2)], {
- type: 'application/json;charset=utf-8'
- });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- link.click();
- URL.revokeObjectURL(url);
- },
- getCalendarCount(type) {
- return this.calendars.filter((item) => item.calendarType === type)
- .length;
- },
- getStatPercent(value) {
- const total = Math.max(...this.monthStats.map((item) => item.value), 1);
- return Math.round((value / total) * 100);
- },
- calendarTypeTag(type) {
- return {
- 1: '',
- 2: 'warning',
- 3: 'success'
- }[type];
- },
- approvalStatusTag(status) {
- return {
- 0: 'warning',
- 1: 'success',
- 2: 'danger',
- 3: 'info'
- }[status];
- },
- getLabel(options, value) {
- return options.find((item) => item.value === value)?.label || '';
- }
- }
- };
- </script>
- <style lang="scss">
- @import './components/factoryCalendar.scss';
- </style>
|