| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099 |
- <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="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>{{ 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">
- <span>生效周期</span>
- <strong>
- {{ 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 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,
- 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 visibleDays = this.statCells;
- const workDays = visibleDays.filter((item) => !item.isRest).length;
- const scheduledDays = visibleDays.filter(
- (item) => !item.isRest && item.scheduleStatus
- ).length;
- const conflictDays = visibleDays.filter(
- (item) => item.isConflict
- ).length;
- const tempDays = visibleDays.filter((item) => item.isTempAdjust).length;
- const restDays = visibleDays.filter((item) => item.isRest).length;
- return [
- { key: 'work', label: '总工作日', value: workDays },
- { key: 'scheduled', label: '已排班天数', value: scheduledDays },
- {
- key: 'idle',
- label: '未排班天数',
- value: Math.max(workDays - scheduledDays, 0)
- },
- { key: 'conflict', label: '冲突天数', value: conflictDays },
- { key: 'temp', label: '临时调整天数', value: tempDays },
- { key: 'rest', label: '休息/节假日', value: restDays }
- ];
- },
- 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;
- const scheduledDays =
- this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
- return workDays ? Math.round((scheduledDays / 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 = this.statCells.filter(
- (item) => item.isConflict && dayjs(item.date).isSame(date, 'week')
- ).length;
- 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)
- );
- 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}-${index}`,
- 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) {
- const calendarIds = this.getCurrentViewCalendars().map(
- (item) => item.id
- );
- return this.details.filter(
- (item) =>
- calendarIds.includes(item.calendarId) && item.calendarDate === date
- );
- },
- 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('、');
- },
- 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.viewQuery.calendarType || ''
- ].join('|');
- },
- 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 {
- this.conflictCheckPromise = checkAllCalendarConflict({
- year: this.viewQuery.year,
- calendarType: this.viewQuery.calendarType || undefined
- });
- const data = await this.conflictCheckPromise;
- this.conflictList = 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>
|