project-detail.ts 197 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786
  1. import { Component, OnInit, OnDestroy, ChangeDetectorRef, ViewChild } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { ActivatedRoute, Router } from '@angular/router';
  4. import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
  5. import { ProjectService } from '../../../services/project.service';
  6. import { PaymentVoucherRecognitionService } from '../../../services/payment-voucher-recognition.service';
  7. import { ProjectReviewService, ReviewReportExportRequest, ReviewReportShareRequest } from '../../../services/project-review.service';
  8. import {
  9. Project,
  10. RenderProgress,
  11. CustomerFeedback,
  12. DesignerChange,
  13. Settlement,
  14. ProjectStage,
  15. PanoramicSynthesis,
  16. ModelCheckItem
  17. } from '../../../models/project.model';
  18. import { RequirementsConfirmCardComponent } from '../../../shared/components/requirements-confirm-card/requirements-confirm-card';
  19. import { SettlementCardComponent } from '../../../shared/components/settlement-card/settlement-card';
  20. import { CustomerReviewCardComponent, DetailedCustomerReview } from '../../../shared/components/customer-review-card/customer-review-card';
  21. import { CustomerReviewFormComponent } from '../../../shared/components/customer-review-form/customer-review-form';
  22. import { ComplaintCardComponent } from '../../../shared/components/complaint-card/complaint-card';
  23. import { PanoramicSynthesisCardComponent } from '../../../shared/components/panoramic-synthesis-card/panoramic-synthesis-card';
  24. import { QuotationDetailsComponent, QuotationData } from './components/quotation-details/quotation-details.component';
  25. import { DesignerAssignmentComponent, DesignerAssignmentData, Designer as AssignmentDesigner } from './components/designer-assignment/designer-assignment.component';
  26. // 引入客户服务模块的设计师日历组件
  27. import { DesignerCalendarComponent, Designer as CalendarDesigner, ProjectGroup as CalendarProjectGroup } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
  28. // 引入参考图管理组件
  29. import { ReferenceImageManagerComponent } from './components/reference-image-manager/reference-image-manager.component';
  30. // 引入可视化组件
  31. import { ColorWheelVisualizerComponent } from '../../../shared/components/color-wheel-visualizer/color-wheel-visualizer';
  32. import { FurnitureFormSelectorComponent } from '../../../shared/components/furniture-form-selector/furniture-form-selector';
  33. import { TextureComparisonVisualizerComponent } from '../../../shared/components/texture-comparison-visualizer/texture-comparison-visualizer';
  34. import { PatternVisualizerComponent } from '../../../shared/components/pattern-visualizer/pattern-visualizer';
  35. // 引入订单审批面板组件
  36. import { OrderApprovalPanelComponent } from '../../../shared/components/order-approval-panel/order-approval-panel.component';
  37. import { ColorAnalysisResult, ColorAnalysisService } from '../../../shared/services/color-analysis.service';
  38. interface ExceptionHistory {
  39. id: string;
  40. type: 'failed' | 'stuck' | 'quality' | 'other';
  41. description: string;
  42. submitTime: Date;
  43. status: '待处理' | '处理中' | '已解决';
  44. response?: string;
  45. }
  46. interface ProjectMember {
  47. id: string;
  48. name: string;
  49. role: string;
  50. avatar: string;
  51. skillMatch: number;
  52. progress: number;
  53. contribution: number;
  54. }
  55. interface ProjectFile {
  56. id: string;
  57. name: string;
  58. type: string;
  59. size: string;
  60. date: string;
  61. url: string;
  62. }
  63. interface TimelineEvent {
  64. id: string;
  65. time: string;
  66. title: string;
  67. action: string;
  68. description: string;
  69. }
  70. // 新增:四大板块键类型(顶层声明,供组件与模板共同使用)
  71. type SectionKey = 'order' | 'requirements' | 'delivery' | 'aftercare';
  72. // 素材解析后的方案数据结构
  73. interface MaterialAnalysis {
  74. category: string;
  75. specifications: {
  76. type: string;
  77. grade: string;
  78. thickness?: string;
  79. finish?: string;
  80. durability: string;
  81. };
  82. usage: {
  83. area: string;
  84. percentage: number;
  85. priority: 'primary' | 'secondary' | 'accent';
  86. };
  87. properties: {
  88. texture: string;
  89. color: string;
  90. maintenance: string;
  91. };
  92. }
  93. interface DesignStyleAnalysis {
  94. primaryStyle: string;
  95. styleElements: {
  96. element: string;
  97. description: string;
  98. influence: number; // 影响程度 0-100
  99. }[];
  100. characteristics: {
  101. feature: string;
  102. value: string;
  103. importance: 'high' | 'medium' | 'low';
  104. }[];
  105. compatibility: {
  106. withMaterials: string[];
  107. withColors: string[];
  108. score: number; // 兼容性评分 0-100
  109. };
  110. }
  111. interface ColorSchemeAnalysis {
  112. palette: {
  113. color: string;
  114. hex: string;
  115. rgb: string;
  116. percentage: number;
  117. role: 'dominant' | 'secondary' | 'accent' | 'neutral';
  118. }[];
  119. harmony: {
  120. type: string; // 如:互补色、类似色、三角色等
  121. temperature: 'warm' | 'cool' | 'neutral';
  122. contrast: number; // 对比度 0-100
  123. };
  124. psychology: {
  125. mood: string;
  126. atmosphere: string;
  127. suitability: string[];
  128. };
  129. }
  130. interface SpaceAnalysis {
  131. dimensions: {
  132. length: number;
  133. width: number;
  134. height: number;
  135. area: number;
  136. volume: number;
  137. };
  138. functionalZones: {
  139. zone: string;
  140. area: number;
  141. percentage: number;
  142. requirements: string[];
  143. furniture: string[];
  144. }[];
  145. circulation: {
  146. mainPaths: string[];
  147. pathWidth: number;
  148. efficiency: number; // 动线效率 0-100
  149. };
  150. lighting: {
  151. natural: {
  152. direction: string[];
  153. intensity: string;
  154. duration: string;
  155. };
  156. artificial: {
  157. zones: string[];
  158. requirements: string[];
  159. };
  160. };
  161. }
  162. // 新增:项目复盘数据结构
  163. interface ProjectReview {
  164. id: string;
  165. projectId: string;
  166. generatedAt: Date;
  167. overallScore: number; // 项目总评分 0-100
  168. sopAnalysis: {
  169. stageName: string;
  170. plannedDuration: number; // 计划天数
  171. actualDuration: number; // 实际天数
  172. score: number; // 阶段评分 0-100
  173. executionStatus: 'excellent' | 'good' | 'average' | 'poor';
  174. issues?: string[]; // 问题列表
  175. }[];
  176. keyHighlights: string[]; // 项目亮点
  177. improvementSuggestions: string[]; // 改进建议
  178. customerSatisfaction: {
  179. overallRating: number; // 1-5星
  180. feedback?: string; // 客户反馈
  181. responseTime: number; // 响应时间(小时)
  182. completionTime: number; // 完成时间(天)
  183. };
  184. teamPerformance: {
  185. designerScore: number;
  186. communicationScore: number;
  187. timelinessScore: number;
  188. qualityScore: number;
  189. };
  190. budgetAnalysis: {
  191. plannedBudget: number;
  192. actualBudget: number;
  193. variance: number; // 预算偏差百分比
  194. costBreakdown: {
  195. category: string;
  196. planned: number;
  197. actual: number;
  198. }[];
  199. };
  200. lessonsLearned: string[]; // 经验教训
  201. recommendations: string[]; // 后续项目建议
  202. }
  203. interface ProposalAnalysis {
  204. id: string;
  205. name: string;
  206. version: string;
  207. createdAt: Date;
  208. status: 'analyzing' | 'completed' | 'approved' | 'rejected';
  209. materials: MaterialAnalysis[];
  210. designStyle: DesignStyleAnalysis;
  211. colorScheme: ColorSchemeAnalysis;
  212. spaceLayout: SpaceAnalysis;
  213. budget: {
  214. total: number;
  215. breakdown: {
  216. category: string;
  217. amount: number;
  218. percentage: number;
  219. }[];
  220. };
  221. timeline: {
  222. phase: string;
  223. duration: number;
  224. dependencies: string[];
  225. }[];
  226. feasibility: {
  227. technical: number; // 技术可行性 0-100
  228. budget: number; // 预算可行性 0-100
  229. timeline: number; // 时间可行性 0-100
  230. overall: number; // 综合可行性 0-100
  231. };
  232. }
  233. // 交付执行板块数据结构
  234. interface DeliverySpace {
  235. id: string;
  236. name: string; // 空间名称:卧室、餐厅、厨房等
  237. isExpanded: boolean; // 是否展开
  238. order: number; // 排序
  239. }
  240. interface DeliveryProcess {
  241. id: string;
  242. name: string; // 流程名称:建模、软装、渲染、后期
  243. type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
  244. spaces: DeliverySpace[]; // 该流程下的空间列表
  245. isExpanded: boolean; // 是否展开
  246. content: {
  247. [spaceId: string]: {
  248. // 每个空间的具体内容
  249. images: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected' }>;
  250. progress: number; // 进度百分比
  251. status: 'pending' | 'in_progress' | 'completed' | 'approved';
  252. notes: string; // 备注
  253. lastUpdated: Date;
  254. };
  255. };
  256. }
  257. @Component({
  258. selector: 'app-project-detail',
  259. standalone: true,
  260. imports: [
  261. CommonModule,
  262. FormsModule,
  263. ReactiveFormsModule,
  264. RequirementsConfirmCardComponent,
  265. SettlementCardComponent,
  266. CustomerReviewCardComponent,
  267. CustomerReviewFormComponent,
  268. ComplaintCardComponent,
  269. PanoramicSynthesisCardComponent,
  270. QuotationDetailsComponent,
  271. DesignerAssignmentComponent,
  272. DesignerCalendarComponent,
  273. ReferenceImageManagerComponent,
  274. ColorWheelVisualizerComponent,
  275. FurnitureFormSelectorComponent,
  276. TextureComparisonVisualizerComponent,
  277. PatternVisualizerComponent,
  278. OrderApprovalPanelComponent
  279. ],
  280. templateUrl: './project-detail.html',
  281. styleUrls: ['./project-detail.scss', './debug-styles.scss', './horizontal-panel.scss', './suggestion-detail-modal.scss']
  282. })
  283. export class ProjectDetail implements OnInit, OnDestroy {
  284. // 获取需求沟通组件实例,用于在方案确认阶段显示分析数据
  285. @ViewChild('requirementsCard') requirementsCard?: RequirementsConfirmCardComponent;
  286. // 审批相关属性
  287. showApprovalPanel = false;
  288. companyId: string = '';
  289. // 项目基本数据
  290. projectId: string = '';
  291. project: Project | undefined;
  292. renderProgress: RenderProgress | undefined;
  293. feedbacks: CustomerFeedback[] = [];
  294. detailedReviews: DetailedCustomerReview[] = [];
  295. designerChanges: DesignerChange[] = [];
  296. settlements: Settlement[] = [];
  297. requirementChecklist: string[] = [];
  298. reminderMessage: string = '';
  299. isLoadingRenderProgress: boolean = false;
  300. errorLoadingRenderProgress: boolean = false;
  301. feedbackTimeoutCountdown: number = 0;
  302. private countdownInterval: any;
  303. projects: {id: string, name: string, status: string}[] = [];
  304. showDropdown: boolean = false;
  305. currentStage: string = '';
  306. // 新增:只读复盘模式(从财务工作台跳转)
  307. isReviewOnlyMode: boolean = false;
  308. // 新增:尾款结算完成状态
  309. isSettlementCompleted: boolean = false;
  310. isConfirmingSettlement: boolean = false;
  311. isSettlementInitiated: boolean = false;
  312. // 新增:自动化功能状态跟踪
  313. miniprogramPaymentStatus: 'active' | 'inactive' = 'inactive';
  314. voucherRecognitionCount: number = 0;
  315. notificationsSent: number = 0;
  316. isAutoSettling: boolean = false;
  317. isPaymentVerified: boolean = false;
  318. // 客户信息卡片展开/收起状态
  319. isCustomerInfoExpanded: boolean = false;
  320. // 新增:订单分配表单相关
  321. orderCreationForm!: FormGroup;
  322. optionalForm!: FormGroup;
  323. isOptionalFormExpanded: boolean = false;
  324. orderCreationData: any = null;
  325. // 新增:方案分析相关数据
  326. proposalAnalysis: ProposalAnalysis | null = null;
  327. isAnalyzing: boolean = false;
  328. analysisProgress: number = 0;
  329. // 新增:右侧色彩分析结果展示
  330. colorAnalysisResult: ColorAnalysisResult | null = null;
  331. dominantColorHex: string | null = null;
  332. // 新增:区分展示的参考图片与CAD文件(来源于需求确认子组件)
  333. referenceImages: any[] = [];
  334. cadFiles: any[] = [];
  335. // 新增:详细分析数据属性
  336. enhancedColorAnalysis: any = null;
  337. formAnalysis: any = null;
  338. textureAnalysis: any = null;
  339. patternAnalysis: any = null;
  340. lightingAnalysis: any = null;
  341. materialAnalysisData: any[] = [];
  342. // 新增:需求映射数据(用于右侧面板显示)
  343. mappingUploadedFiles: any[] = [];
  344. mappingAnalysisResult: any = null;
  345. mappingRequirementMapping: any = null;
  346. mappingTestSteps: any[] = [];
  347. mappingIsAnalyzing: boolean = false;
  348. mappingIsGeneratingMapping: boolean = false;
  349. // 上传成功弹窗相关(从子组件移到父组件以解决定位问题)
  350. showUploadSuccessModal = false;
  351. uploadedFiles: { id: string; name: string; url: string; size?: number; type?: 'image' | 'cad' | 'text'; preview?: string }[] = [];
  352. uploadType: 'image' | 'document' | 'mixed' = 'image';
  353. isAnalyzingColors = false;
  354. // 新增:9阶段顺序(串式流程)- 包含后期阶段
  355. stageOrder: ProjectStage[] = ['订单分配', '需求沟通', '方案确认', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价', '投诉处理'];
  356. // 新增:阶段展开状态(默认全部收起,当前阶段在数据加载后自动展开)
  357. expandedStages: Partial<Record<ProjectStage, boolean>> = {
  358. '订单分配': false,
  359. '需求沟通': false,
  360. '方案确认': false,
  361. '建模': false,
  362. '软装': false,
  363. '渲染': false,
  364. '后期': false,
  365. '尾款结算': false,
  366. '客户评价': false,
  367. '投诉处理': false,
  368. };
  369. // 新增:四大板块定义与展开状态 - 交付执行板块调整为三个阶段
  370. sections: Array<{ key: SectionKey; label: string; stages: ProjectStage[] }> = [
  371. { key: 'order', label: '订单分配', stages: ['订单分配'] },
  372. { key: 'requirements', label: '确认需求', stages: ['需求沟通', '方案确认'] },
  373. { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染', '后期'] },
  374. { key: 'aftercare', label: '售后', stages: [] }
  375. ];
  376. expandedSection: SectionKey | null = null;
  377. // 渲染异常反馈相关属性
  378. exceptionType: 'failed' | 'stuck' | 'quality' | 'other' = 'failed';
  379. exceptionDescription: string = '';
  380. exceptionScreenshotUrl: string | null = null;
  381. exceptionHistories: ExceptionHistory[] = [];
  382. isSubmittingFeedback: boolean = false;
  383. selectedScreenshot: File | null = null;
  384. screenshotPreview: string | null = null;
  385. showExceptionForm: boolean = false;
  386. // 标签页相关
  387. activeTab: 'progress' | 'members' | 'files' | 'reference' = 'progress';
  388. tabs: Array<{ id: 'progress' | 'members' | 'files' | 'reference'; name: string }> = [
  389. { id: 'progress', name: '项目进度' },
  390. { id: 'members', name: '项目成员' },
  391. { id: 'files', name: '项目文件' },
  392. { id: 'reference', name: '参考图管理' }
  393. ];
  394. // 标准化阶段(视图层映射)
  395. standardPhases: Array<'待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'> = ['待分配', '需求方案', '项目执行', '收尾验收', '归档'];
  396. // 文件上传(通用)
  397. acceptedFileTypes: string = '.doc,.docx,.pdf,.jpg,.jpeg,.png,.zip,.rar,.max,.obj';
  398. isUploadingFile: boolean = false;
  399. projectMembers: ProjectMember[] = [];
  400. // 项目文件数据
  401. projectFiles: ProjectFile[] = [];
  402. // 团队协作时间轴
  403. timelineEvents: TimelineEvent[] = [];
  404. // 团队分配弹窗相关
  405. selectedDesigner: any = null;
  406. projectData: any = null;
  407. // ============ 阶段图片上传状态(新增) ============
  408. allowedImageTypes: string = '.jpg,.jpeg,.png';
  409. // 增加审核状态reviewStatus与是否已同步synced标记(仅由组长操作)
  410. whiteModelImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
  411. softDecorImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
  412. renderLargeImages: Array<{ id: string; name: string; url: string; size?: string; locked?: boolean; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
  413. postProcessImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
  414. showRenderUploadModal: boolean = false;
  415. pendingRenderLargeItems: Array<{ id: string; name: string; url: string; file: File }> = [];
  416. // 视图上下文:根据路由前缀识别角色视角(客服/设计师/组长)
  417. roleContext: 'customer-service' | 'designer' | 'team-leader' | 'technical' = 'designer';
  418. // ============ 模型检查项数据 ============
  419. modelCheckItems: ModelCheckItem[] = [
  420. { id: 'check-1', name: '户型匹配度检查', isPassed: false, notes: '' },
  421. { id: 'check-2', name: '尺寸精度验证', isPassed: false, notes: '' },
  422. { id: 'check-3', name: '材质贴图检查', isPassed: false, notes: '' },
  423. { id: 'check-4', name: '光影效果验证', isPassed: false, notes: '' },
  424. { id: 'check-5', name: '细节完整性检查', isPassed: false, notes: '' }
  425. ];
  426. // ============ 交付执行板块数据 ============
  427. deliveryProcesses: DeliveryProcess[] = [
  428. {
  429. id: 'modeling',
  430. name: '建模',
  431. type: 'modeling',
  432. isExpanded: true, // 默认展开第一个
  433. spaces: [
  434. { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
  435. { id: 'living', name: '客厅', isExpanded: false, order: 2 },
  436. { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
  437. ],
  438. content: {
  439. 'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  440. 'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  441. 'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
  442. }
  443. },
  444. {
  445. id: 'softDecor',
  446. name: '软装',
  447. type: 'softDecor',
  448. isExpanded: false,
  449. spaces: [
  450. { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
  451. { id: 'living', name: '客厅', isExpanded: false, order: 2 },
  452. { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
  453. ],
  454. content: {
  455. 'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  456. 'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  457. 'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
  458. }
  459. },
  460. {
  461. id: 'rendering',
  462. name: '渲染',
  463. type: 'rendering',
  464. isExpanded: false,
  465. spaces: [
  466. { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
  467. { id: 'living', name: '客厅', isExpanded: false, order: 2 },
  468. { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
  469. ],
  470. content: {
  471. 'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  472. 'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  473. 'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
  474. }
  475. },
  476. {
  477. id: 'postProcess',
  478. name: '后期',
  479. type: 'postProcess',
  480. isExpanded: false,
  481. spaces: [
  482. { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
  483. { id: 'living', name: '客厅', isExpanded: false, order: 2 },
  484. { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
  485. ],
  486. content: {
  487. 'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  488. 'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
  489. 'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
  490. }
  491. }
  492. ];
  493. // 新增空间输入框状态
  494. newSpaceName: { [processId: string]: string } = {};
  495. showAddSpaceInput: { [processId: string]: boolean } = {};
  496. constructor(
  497. private route: ActivatedRoute,
  498. private projectService: ProjectService,
  499. private router: Router,
  500. private fb: FormBuilder,
  501. private cdr: ChangeDetectorRef,
  502. private paymentVoucherService: PaymentVoucherRecognitionService,
  503. private projectReviewService: ProjectReviewService,
  504. private colorAnalysisService: ColorAnalysisService
  505. ) {
  506. // 初始化企业微信认证
  507. this.loadProfile();
  508. }
  509. // 当前用户信息
  510. currentUser: any = {};
  511. /**
  512. * 加载当前用户Profile
  513. */
  514. async loadProfile() {
  515. try {
  516. const cid = localStorage.getItem("company");
  517. if (cid) {
  518. const { WxworkAuth } = await import('fmode-ng/core');
  519. const wwAuth = new WxworkAuth({ cid: cid });
  520. const profile = await wwAuth.currentProfile();
  521. if (profile) {
  522. this.currentUser = {
  523. name: profile.get("name") || profile.get("mobile"),
  524. avatar: profile.get("avatar"),
  525. roleName: profile.get("roleName")
  526. };
  527. console.log('✅ 用户Profile加载成功:', this.currentUser);
  528. }
  529. } else {
  530. console.warn('⚠️ localStorage中未找到company(cid)');
  531. }
  532. } catch (error) {
  533. console.error('❌ 加载用户Profile失败:', error);
  534. }
  535. }
  536. // 切换标签页
  537. switchTab(tabId: 'progress' | 'members' | 'files' | 'reference') {
  538. this.activeTab = tabId;
  539. }
  540. // 类型安全的标签页检查方法
  541. isActiveTab(tabId: 'progress' | 'members' | 'files' | 'reference'): boolean {
  542. return this.activeTab === tabId;
  543. }
  544. // 根据事件类型获取作者名称
  545. getEventAuthor(action: string): string {
  546. // 根据不同的action类型返回对应的作者名称
  547. switch(action) {
  548. case '完成':
  549. case '更新':
  550. case '优化':
  551. return '李设计师';
  552. case '收到':
  553. return '李客服';
  554. case '提交':
  555. return '赵建模师';
  556. default:
  557. return '王组长';
  558. }
  559. }
  560. // 切换项目
  561. switchProject(projectId: string): void {
  562. this.projectId = projectId;
  563. this.loadProjectData();
  564. this.loadProjectMembers();
  565. this.loadProjectFiles();
  566. this.loadTimelineEvents();
  567. // 更新URL但不触发组件重载
  568. this.router.navigate([], { relativeTo: this.route, queryParamsHandling: 'merge', queryParams: { id: projectId } });
  569. }
  570. // 检查是否处于订单分配阶段,用于红色高亮显示
  571. isCurrentOrderCreation(): boolean {
  572. // 只有当订单分配阶段是当前活跃阶段时才标红显示
  573. // 修复逻辑:完成前序环节后才标红当前阶段
  574. const currentStage = this.project?.currentStage;
  575. if (!currentStage) return false;
  576. // 如果当前阶段就是订单分配阶段,说明需要标红显示
  577. if (currentStage === '订单分配') return true;
  578. // 如果当前阶段在订单分配之后,说明订单分配已完成,不应标红
  579. const stageOrder = [
  580. '订单分配', '需求沟通', '方案确认', '建模', '软装',
  581. '渲染', '尾款结算', '客户评价', '投诉处理'
  582. ];
  583. const currentIndex = stageOrder.indexOf(currentStage);
  584. const orderCreationIndex = stageOrder.indexOf('订单分配');
  585. // 如果当前阶段在订单分配之后,说明订单分配已完成
  586. return currentIndex <= orderCreationIndex;
  587. }
  588. // 返回工作台
  589. backToWorkbench(): void {
  590. this.router.navigate(['/designer/dashboard']);
  591. }
  592. // 检查阶段是否已完成
  593. isStageCompleted(stage: ProjectStage): boolean {
  594. if (!this.project) return false;
  595. // 定义阶段顺序
  596. const stageOrder = [
  597. '订单分配', '需求沟通', '方案确认', '建模', '软装',
  598. '渲染', '尾款结算', '客户评价', '投诉处理'
  599. ];
  600. // 获取当前阶段和检查阶段的索引
  601. const currentStageIndex = stageOrder.indexOf(this.project.currentStage);
  602. const checkStageIndex = stageOrder.indexOf(stage);
  603. // 如果检查阶段在当前阶段之前,则已完成
  604. return checkStageIndex < currentStageIndex;
  605. }
  606. // 获取阶段状态:completed/active/pending
  607. getStageStatus(stage: ProjectStage): 'completed' | 'active' | 'pending' {
  608. const order = this.stageOrder;
  609. // 优先使用 currentStage 属性,如果没有则使用 project.currentStage
  610. const current = (this.currentStage as ProjectStage) || (this.project?.currentStage as ProjectStage | undefined);
  611. const currentIdx = current ? order.indexOf(current) : -1;
  612. const idx = order.indexOf(stage);
  613. if (idx === -1) return 'pending';
  614. if (currentIdx === -1) return 'pending';
  615. if (idx < currentIdx) return 'completed';
  616. if (idx === currentIdx) return 'active';
  617. return 'pending';
  618. }
  619. // 切换阶段展开/收起,并保持单展开
  620. toggleStage(stage: ProjectStage): void {
  621. // 已移除所有展开按钮,本方法保留以兼容模板其它引用,如无需可进一步删除调用点和方法
  622. const exclusivelyOpen = true;
  623. if (exclusivelyOpen) {
  624. Object.keys(this.expandedStages).forEach((key) => (this.expandedStages[key as ProjectStage] = false));
  625. this.expandedStages[stage] = true;
  626. } else {
  627. this.expandedStages[stage] = !this.expandedStages[stage];
  628. }
  629. }
  630. // 查看阶段详情(已不再通过按钮触发,保留以兼容日志或未来调用)
  631. viewStageDetails(stage: ProjectStage): void {
  632. // 以往这里有 alert/导航行为,现清空用户交互,避免误触
  633. return;
  634. }
  635. ngOnInit(): void {
  636. // 检查是否为只读复盘模式(从财务工作台跳转)
  637. this.route.queryParams.subscribe(params => {
  638. if (params['view'] === 'review-only') {
  639. this.isReviewOnlyMode = true;
  640. }
  641. // 检查是否需要直接定位到售后板块
  642. if (params['section'] === 'aftercare' || params['view'] === 'review-only') {
  643. // 自动切换到售后板块
  644. this.expandedSection = 'aftercare';
  645. // 滚动到项目复盘区域
  646. setTimeout(() => {
  647. const reviewSection = document.querySelector('.project-review-section');
  648. if (reviewSection) {
  649. reviewSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
  650. }
  651. }, 500);
  652. }
  653. });
  654. // 初始化表单
  655. this.initializeForms();
  656. // 初始化需求关键信息数据
  657. this.ensureRequirementData();
  658. // 重置方案分析状态,确保需求信息展示区域能够显示
  659. this.resetProposalAnalysis();
  660. // 初始化售后模块示例数据
  661. this.initializeAftercareData();
  662. // 获取公司ID
  663. this.companyId = localStorage.getItem('company') || '';
  664. this.route.paramMap.subscribe({
  665. next: (params) => {
  666. this.projectId = params.get('id') || '';
  667. // 根据当前URL检测视图上下文
  668. this.roleContext = this.detectRoleContextFromUrl();
  669. this.loadProjectData();
  670. this.loadExceptionHistories();
  671. this.loadProjectMembers();
  672. this.loadProjectFiles();
  673. this.loadTimelineEvents();
  674. // 启动客户信息自动同步
  675. this.startAutoSync();
  676. },
  677. error: (error) => {
  678. console.error('路由参数订阅失败:', error);
  679. }
  680. });
  681. // 新增:监听查询参数,支持通过 activeTab 设置初始标签页和 currentStage 设置当前阶段
  682. this.route.queryParamMap.subscribe({
  683. next: (qp) => {
  684. const raw = qp.get('activeTab');
  685. const alias: Record<string, 'progress' | 'members' | 'files' | 'reference'> = {
  686. requirements: 'progress',
  687. overview: 'progress'
  688. };
  689. const tab = raw && (raw in alias ? alias[raw] : raw);
  690. if (tab === 'progress' || tab === 'members' || tab === 'files' || tab === 'reference') {
  691. this.activeTab = tab;
  692. }
  693. // 处理 currentStage 查询参数
  694. const currentStageParam = qp.get('currentStage');
  695. if (currentStageParam) {
  696. this.currentStage = currentStageParam;
  697. // 根据当前阶段设置项目状态和展开相应区域
  698. this.initializeStageFromRoute(currentStageParam as ProjectStage);
  699. // 根据当前阶段自动展开对应的区域
  700. const sectionKey = this.getSectionKeyForStage(currentStageParam as ProjectStage);
  701. if (sectionKey) {
  702. this.expandedSection = sectionKey;
  703. }
  704. // 延迟滚动到对应阶段
  705. setTimeout(() => {
  706. this.scrollToStage(currentStageParam as ProjectStage);
  707. }, 500);
  708. }
  709. // 处理从客服项目列表传递的同步数据
  710. const syncDataParam = qp.get('syncData');
  711. if (syncDataParam) {
  712. try {
  713. const syncData = JSON.parse(syncDataParam);
  714. console.log('接收到客服同步数据:', syncData);
  715. // 设置同步状态
  716. this.isSyncingCustomerInfo = true;
  717. // 存储订单分配数据用于显示
  718. this.orderCreationData = syncData;
  719. // 更新projectData以传递给子组件
  720. this.projectData = {
  721. customerInfo: syncData.customerInfo,
  722. requirementInfo: syncData.requirementInfo,
  723. preferenceTags: syncData.preferenceTags
  724. };
  725. // 同步客户信息到表单
  726. if (syncData.customerInfo) {
  727. this.customerForm.patchValue({
  728. name: syncData.customerInfo.name || '',
  729. phone: syncData.customerInfo.phone || '',
  730. wechat: syncData.customerInfo.wechat || '',
  731. customerType: syncData.customerInfo.customerType || '新客户',
  732. source: syncData.customerInfo.source || '小程序',
  733. remark: syncData.customerInfo.remark || '',
  734. demandType: syncData.customerInfo.demandType || '',
  735. followUpStatus: syncData.customerInfo.followUpStatus || '待分配'
  736. });
  737. // 设置选中的客户
  738. this.selectedOrderCustomer = {
  739. id: syncData.customerInfo.id || 'temp-' + Date.now(),
  740. name: syncData.customerInfo.name,
  741. phone: syncData.customerInfo.phone,
  742. wechat: syncData.customerInfo.wechat,
  743. customerType: syncData.customerInfo.customerType,
  744. source: syncData.customerInfo.source,
  745. remark: syncData.customerInfo.remark
  746. };
  747. }
  748. // 同步需求信息
  749. if (syncData.requirementInfo) {
  750. this.syncRequirementKeyInfo(syncData.requirementInfo);
  751. }
  752. // 同步偏好标签到项目数据
  753. if (syncData.preferenceTags && this.project) {
  754. this.project.customerTags = syncData.preferenceTags.map((tag: string) => ({
  755. source: '客服填写',
  756. needType: tag,
  757. preference: tag,
  758. colorAtmosphere: tag
  759. }));
  760. }
  761. // 模拟同步完成
  762. setTimeout(() => {
  763. this.isSyncingCustomerInfo = false;
  764. this.lastSyncTime = new Date();
  765. this.cdr.detectChanges();
  766. console.log('客户信息同步完成');
  767. }, 1500);
  768. // 触发界面更新
  769. this.cdr.detectChanges();
  770. console.log('客服数据同步完成,orderCreationData:', this.orderCreationData);
  771. } catch (error) {
  772. console.error('解析同步数据失败:', error);
  773. this.isSyncingCustomerInfo = false;
  774. }
  775. }
  776. },
  777. error: (error) => {
  778. console.error('查询参数订阅失败:', error);
  779. }
  780. });
  781. // 添加点击事件监听器,当点击页面其他位置时关闭下拉菜单
  782. document.addEventListener('click', this.closeDropdownOnClickOutside);
  783. // 初始化客户表单(与客服端保持一致)
  784. this.customerForm = this.fb.group({
  785. name: ['', Validators.required],
  786. phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
  787. wechat: [''],
  788. customerType: ['新客户'],
  789. source: [''],
  790. remark: [''],
  791. demandType: [''],
  792. followUpStatus: ['']
  793. });
  794. // 自动生成下单时间
  795. this.orderTime = new Date().toLocaleString('zh-CN', {
  796. year: 'numeric', month: '2-digit', day: '2-digit',
  797. hour: '2-digit', minute: '2-digit', second: '2-digit'
  798. });
  799. }
  800. // 新增:根据路由参数初始化阶段状态
  801. private initializeStageFromRoute(targetStage: ProjectStage): void {
  802. // 设置当前阶段
  803. this.currentStage = targetStage;
  804. // 根据目标阶段设置之前的阶段为已完成
  805. const targetIndex = this.stageOrder.indexOf(targetStage);
  806. if (targetIndex > 0) {
  807. // 将目标阶段之前的所有阶段设置为已完成
  808. for (let i = 0; i < targetIndex; i++) {
  809. const stage = this.stageOrder[i];
  810. this.expandedStages[stage] = false; // 已完成的阶段收起
  811. }
  812. }
  813. // 展开当前阶段
  814. this.expandedStages[targetStage] = true;
  815. // 如果项目对象存在,更新项目的当前阶段
  816. if (this.project) {
  817. this.project.currentStage = targetStage;
  818. }
  819. // 触发变更检测以更新UI
  820. this.cdr.detectChanges();
  821. }
  822. ngOnDestroy(): void {
  823. if (this.countdownInterval) {
  824. clearInterval(this.countdownInterval);
  825. }
  826. // 停止自动同步
  827. this.stopAutoSync();
  828. document.removeEventListener('click', this.closeDropdownOnClickOutside);
  829. // 释放所有 blob 预览 URL
  830. const revokeList: string[] = [];
  831. this.whiteModelImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
  832. this.softDecorImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
  833. this.renderLargeImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
  834. this.pendingRenderLargeItems.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
  835. revokeList.forEach(u => URL.revokeObjectURL(u));
  836. }
  837. // ============ 角色视图与只读控制(新增) ============
  838. private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' | 'technical' {
  839. const url = this.router.url || '';
  840. // 首先检查查询参数中的role
  841. const queryParams = this.route.snapshot.queryParamMap;
  842. const roleParam = queryParams.get('roleName');
  843. if (roleParam === 'customer-service') {
  844. return 'customer-service';
  845. }
  846. if (roleParam === 'technical') {
  847. return 'technical';
  848. }
  849. // 如果没有role查询参数,则根据URL路径判断
  850. if (url.includes('/customer-service/')) return 'customer-service';
  851. if (url.includes('/team-leader/')) return 'team-leader';
  852. if (url.includes('/technical/')) return 'technical';
  853. return 'designer';
  854. }
  855. isDesignerView(): boolean { return this.roleContext === 'designer'; }
  856. isTeamLeaderView(): boolean { return this.roleContext === 'team-leader'; }
  857. isCustomerServiceView(): boolean { return this.roleContext === 'customer-service'; }
  858. isTechnicalView(): boolean { return this.roleContext === 'technical'; }
  859. // 只读规则:客服视角为只读
  860. isReadOnly(): boolean { return this.isCustomerServiceView(); }
  861. // 权限控制:客服只能编辑订单分配、确认需求、售后板块
  862. canEditSection(sectionKey: SectionKey): boolean {
  863. if (this.isCustomerServiceView()) {
  864. return sectionKey === 'order' || sectionKey === 'requirements' || sectionKey === 'aftercare';
  865. }
  866. return true; // 设计师和组长可以编辑所有板块
  867. }
  868. // 权限控制:客服只能编辑特定阶段
  869. canEditStage(stage: ProjectStage): boolean {
  870. if (this.isCustomerServiceView()) {
  871. const editableStages: ProjectStage[] = [
  872. '订单分配', '需求沟通', '方案确认', // 订单分配和确认需求板块
  873. '尾款结算', '客户评价', '投诉处理' // 售后板块
  874. ];
  875. return editableStages.includes(stage);
  876. }
  877. return true; // 设计师和组长可以编辑所有阶段
  878. }
  879. // 计算当前激活板块:优先用户点击的 expandedSection;否则取当前阶段所属板块;再否则回退首个板块
  880. private getActiveSectionKey(): SectionKey {
  881. if (this.expandedSection) return this.expandedSection;
  882. const current = this.project?.currentStage as ProjectStage | undefined;
  883. return current ? this.getSectionKeyForStage(current) : this.sections[0].key;
  884. }
  885. // 返回当前板块的全部阶段(所有角色一致):
  886. // 设计师也可查看 订单分配/确认需求/售后 板块内容
  887. getVisibleStages(): ProjectStage[] {
  888. const activeKey = this.getActiveSectionKey();
  889. const sec = this.sections.find(s => s.key === activeKey);
  890. return sec ? sec.stages : [];
  891. }
  892. // ============ 组长:同步上传与审核(新增,模拟实现) ============
  893. syncUploadedImages(phase: 'white' | 'soft' | 'render' | 'postProcess'): void {
  894. if (!this.isTeamLeaderView()) return;
  895. const markSynced = (arr: Array<{ reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
  896. arr.forEach(img => {
  897. if (!img.synced) img.synced = true;
  898. if (!img.reviewStatus) img.reviewStatus = 'pending';
  899. });
  900. };
  901. if (phase === 'white') markSynced(this.whiteModelImages);
  902. if (phase === 'soft') markSynced(this.softDecorImages);
  903. if (phase === 'render') markSynced(this.renderLargeImages);
  904. if (phase === 'postProcess') markSynced(this.postProcessImages);
  905. alert('已同步该阶段的图片信息(模拟)');
  906. }
  907. reviewImage(imageId: string, phase: 'white' | 'soft' | 'render' | 'postProcess', status: 'approved' | 'rejected'): void {
  908. if (!this.isTeamLeaderView()) return;
  909. const setStatus = (arr: Array<{ id: string; reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
  910. const target = arr.find(i => i.id === imageId);
  911. if (target) {
  912. target.reviewStatus = status;
  913. if (!target.synced) target.synced = true; // 审核时自动视为已同步
  914. }
  915. };
  916. if (phase === 'white') setStatus(this.whiteModelImages);
  917. if (phase === 'soft') setStatus(this.softDecorImages);
  918. if (phase === 'render') setStatus(this.renderLargeImages);
  919. if (phase === 'postProcess') setStatus(this.postProcessImages);
  920. }
  921. getImageReviewStatusText(img: { reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }): string {
  922. const synced = img.synced ? '已同步' : '未同步';
  923. const map: Record<string, string> = {
  924. 'pending': '待审',
  925. 'approved': '已通过',
  926. 'rejected': '已驳回'
  927. };
  928. const st = img.reviewStatus ? map[img.reviewStatus] : '未标记';
  929. return `${st} · ${synced}`;
  930. }
  931. // 点击页面其他位置时关闭下拉菜单
  932. private closeDropdownOnClickOutside = (event: MouseEvent): void => {
  933. const targetElement = event.target as HTMLElement;
  934. const projectSwitcher = targetElement.closest('.project-switcher');
  935. if (!projectSwitcher && this.showDropdown) {
  936. this.showDropdown = false;
  937. }
  938. };
  939. loadProjectData(): void {
  940. if (this.projectId) {
  941. this.loadProjectDetails();
  942. this.loadRenderProgress();
  943. this.loadCustomerFeedbacks();
  944. this.loadDesignerChanges();
  945. this.loadSettlements();
  946. this.loadRequirementChecklist();
  947. }
  948. // 初始化项目列表数据(模拟)
  949. this.projects = [
  950. { id: '1', name: '现代风格客厅设计', status: '进行中' },
  951. { id: '2', name: '北欧风卧室装修', status: '已完成' },
  952. { id: '3', name: '新中式书房改造', status: '进行中' },
  953. { id: '4', name: '工业风餐厅设计', status: '待处理' }
  954. ];
  955. }
  956. // 加载项目成员数据
  957. loadProjectMembers(): void {
  958. // 模拟API请求获取项目成员数据
  959. setTimeout(() => {
  960. this.projectMembers = [
  961. {
  962. id: '1',
  963. name: '李设计师',
  964. role: '主设计师',
  965. avatar: '李',
  966. skillMatch: 95,
  967. progress: 65,
  968. contribution: 75
  969. },
  970. {
  971. id: '2',
  972. name: '陈设计师',
  973. role: '助理设计师',
  974. avatar: '陈',
  975. skillMatch: 88,
  976. progress: 80,
  977. contribution: 60
  978. },
  979. {
  980. id: '3',
  981. name: '王组长',
  982. role: '项目组长',
  983. avatar: '王',
  984. skillMatch: 92,
  985. progress: 70,
  986. contribution: 70
  987. },
  988. {
  989. id: '4',
  990. name: '赵建模师',
  991. role: '3D建模师',
  992. avatar: '赵',
  993. skillMatch: 90,
  994. progress: 90,
  995. contribution: 85
  996. }
  997. ];
  998. }, 600);
  999. }
  1000. // 加载项目文件数据
  1001. loadProjectFiles(): void {
  1002. // 模拟API请求获取项目文件数据
  1003. setTimeout(() => {
  1004. this.projectFiles = [
  1005. {
  1006. id: '1',
  1007. name: '客厅设计方案V2.0.pdf',
  1008. type: 'pdf',
  1009. size: '2.5MB',
  1010. date: '2024-02-10',
  1011. url: '#'
  1012. },
  1013. {
  1014. id: '2',
  1015. name: '材质库集合.rar',
  1016. type: 'rar',
  1017. size: '45.8MB',
  1018. date: '2024-02-08',
  1019. url: '#'
  1020. },
  1021. {
  1022. id: '3',
  1023. name: '客厅渲染预览1.jpg',
  1024. type: 'jpg',
  1025. size: '3.2MB',
  1026. date: '2024-02-14',
  1027. url: '#'
  1028. },
  1029. {
  1030. id: '4',
  1031. name: '3D模型文件.max',
  1032. type: 'max',
  1033. size: '87.5MB',
  1034. date: '2024-02-12',
  1035. url: '#'
  1036. },
  1037. {
  1038. id: '5',
  1039. name: '客户需求文档.docx',
  1040. type: 'docx',
  1041. size: '1.2MB',
  1042. date: '2024-01-15',
  1043. url: '#'
  1044. },
  1045. {
  1046. id: '6',
  1047. name: '客厅渲染预览2.jpg',
  1048. type: 'jpg',
  1049. size: '3.8MB',
  1050. date: '2024-02-15',
  1051. url: '#'
  1052. }
  1053. ];
  1054. }, 700);
  1055. }
  1056. // 加载团队协作时间轴数据
  1057. loadTimelineEvents(): void {
  1058. // 模拟API请求获取时间轴数据
  1059. setTimeout(() => {
  1060. this.timelineEvents = [
  1061. {
  1062. id: '1',
  1063. time: '2024-02-15 14:30',
  1064. title: '渲染完成',
  1065. action: '完成',
  1066. description: '客厅主视角渲染已完成,等待客户确认'
  1067. },
  1068. {
  1069. id: '2',
  1070. time: '2024-02-14 10:15',
  1071. title: '材质调整',
  1072. action: '更新',
  1073. description: '根据客户反馈调整了沙发和窗帘材质'
  1074. },
  1075. {
  1076. id: '3',
  1077. time: '2024-02-12 16:45',
  1078. title: '模型优化',
  1079. action: '优化',
  1080. description: '优化了模型面数,提高渲染效率'
  1081. },
  1082. {
  1083. id: '4',
  1084. time: '2024-02-10 09:30',
  1085. title: '客户反馈',
  1086. action: '收到',
  1087. description: '收到客户关于颜色和储物空间的反馈意见'
  1088. },
  1089. {
  1090. id: '5',
  1091. time: '2024-02-08 15:20',
  1092. title: '模型提交',
  1093. action: '提交',
  1094. description: '完成3D模型搭建并提交审核'
  1095. }
  1096. ];
  1097. }, 800);
  1098. }
  1099. // 加载历史反馈记录
  1100. loadExceptionHistories(): void {
  1101. this.projectService.getExceptionHistories(this.projectId).subscribe({
  1102. next: (histories) => {
  1103. this.exceptionHistories = histories;
  1104. },
  1105. error: (error) => {
  1106. console.error('加载异常历史失败:', error);
  1107. }
  1108. });
  1109. }
  1110. loadProjectDetails(): void {
  1111. console.log('=== loadProjectDetails 开始加载项目数据 ===');
  1112. console.log('当前项目ID:', this.projectId);
  1113. this.projectService.getProjectById(this.projectId).subscribe({
  1114. next: (project) => {
  1115. console.log('获取到的项目数据:', project);
  1116. if (!project) {
  1117. console.error('未找到项目数据,项目ID:', this.projectId);
  1118. // 如果找不到项目,尝试使用默认项目ID
  1119. console.log('尝试使用默认项目ID: proj-001');
  1120. this.projectService.getProjectById('proj-001').subscribe({
  1121. next: (defaultProject) => {
  1122. console.log('默认项目数据:', defaultProject);
  1123. if (defaultProject) {
  1124. this.project = defaultProject;
  1125. this.currentStage = defaultProject.currentStage || '';
  1126. console.log('使用默认项目,设置当前阶段:', this.currentStage);
  1127. this.setupStageExpansion(defaultProject);
  1128. }
  1129. },
  1130. error: (error) => {
  1131. console.error('加载默认项目失败:', error);
  1132. }
  1133. });
  1134. return;
  1135. }
  1136. this.project = project;
  1137. // 设置当前阶段
  1138. if (project) {
  1139. this.currentStage = project.currentStage || '';
  1140. console.log('设置当前阶段:', this.currentStage);
  1141. this.setupStageExpansion(project);
  1142. // 检查是否需要显示审批面板
  1143. this.checkApprovalStatus();
  1144. }
  1145. // 检查技能匹配度 - 已注释掉以取消弹窗警告
  1146. // this.checkSkillMismatch();
  1147. },
  1148. error: (error) => {
  1149. console.error('加载项目详情失败:', error);
  1150. }
  1151. });
  1152. }
  1153. private setupStageExpansion(project: any): void {
  1154. // 重置展开状态并默认展开当前阶段
  1155. this.stageOrder.forEach(s => this.expandedStages[s] = false);
  1156. const currentStage = project.currentStage as ProjectStage;
  1157. if (this.stageOrder.includes(currentStage)) {
  1158. this.expandedStages[currentStage] = true;
  1159. console.log('展开当前阶段:', currentStage);
  1160. }
  1161. // 新增:根据当前阶段默认展开所属板块
  1162. const currentSec = this.getSectionKeyForStage(currentStage);
  1163. this.expandedSection = currentSec;
  1164. console.log('展开板块:', currentSec);
  1165. // 新增:如果当前阶段是建模、软装或渲染,自动展开对应的折叠面板
  1166. if (currentStage === '建模' || currentStage === '软装' || currentStage === '渲染') {
  1167. const processTypeMap = {
  1168. '建模': 'modeling',
  1169. '软装': 'softDecor',
  1170. '渲染': 'rendering'
  1171. };
  1172. const processType = processTypeMap[currentStage] as 'modeling' | 'softDecor' | 'rendering';
  1173. const targetProcess = this.deliveryProcesses.find(p => p.type === processType);
  1174. if (targetProcess) {
  1175. // 展开对应的流程面板
  1176. targetProcess.isExpanded = true;
  1177. // 展开第一个空间以便用户操作
  1178. if (targetProcess.spaces.length > 0) {
  1179. targetProcess.spaces[0].isExpanded = true;
  1180. // 关闭其他空间
  1181. targetProcess.spaces.slice(1).forEach(space => space.isExpanded = false);
  1182. }
  1183. console.log('自动展开折叠面板:', currentStage, processType);
  1184. }
  1185. }
  1186. }
  1187. // 整理项目详情
  1188. organizeProject(): void {
  1189. // 模拟整理项目逻辑
  1190. alert('项目详情已整理');
  1191. }
  1192. // 检查当前阶段是否显示特定卡片
  1193. shouldShowCard(cardType: string): boolean {
  1194. // 改为始终显示:各阶段详情在看板下方就地展示,不再受当前阶段限制
  1195. return true;
  1196. }
  1197. loadRenderProgress(): void {
  1198. this.isLoadingRenderProgress = true;
  1199. this.errorLoadingRenderProgress = false;
  1200. // 模拟API加载过程
  1201. setTimeout(() => {
  1202. this.projectService.getRenderProgress(this.projectId).subscribe({
  1203. next: (progress) => {
  1204. this.renderProgress = progress;
  1205. this.isLoadingRenderProgress = false;
  1206. // 模拟API加载失败的情况
  1207. if (!progress) {
  1208. this.errorLoadingRenderProgress = true;
  1209. // 通知技术组长
  1210. this.notifyTeamLeader('render-failed');
  1211. } else {
  1212. // 检查是否需要显示超时预警
  1213. this.checkRenderTimeout();
  1214. }
  1215. },
  1216. error: (error) => {
  1217. console.error('加载渲染进度失败:', error);
  1218. this.isLoadingRenderProgress = false;
  1219. this.errorLoadingRenderProgress = true;
  1220. }
  1221. });
  1222. }, 1000);
  1223. }
  1224. loadCustomerFeedbacks(): void {
  1225. this.projectService.getCustomerFeedbacks().subscribe({
  1226. next: (feedbacks) => {
  1227. this.feedbacks = feedbacks.filter(f => f.projectId === this.projectId);
  1228. // 为反馈添加分类标签
  1229. this.tagCustomerFeedbacks();
  1230. // 检查是否有需要处理的反馈并启动倒计时
  1231. this.checkFeedbackTimeout();
  1232. },
  1233. error: (error) => {
  1234. console.error('加载客户反馈失败:', error);
  1235. }
  1236. });
  1237. }
  1238. loadDesignerChanges(): void {
  1239. // 在实际应用中,这里应该从服务中获取设计师变更记录
  1240. // 这里使用模拟数据
  1241. this.designerChanges = [
  1242. {
  1243. id: 'dc1',
  1244. projectId: this.projectId,
  1245. oldDesignerId: 'designer2',
  1246. oldDesignerName: '设计师B',
  1247. newDesignerId: 'designer1',
  1248. newDesignerName: '设计师A',
  1249. changeTime: new Date('2025-09-05'),
  1250. acceptanceTime: new Date('2025-09-05'),
  1251. historicalAchievements: ['完成初步建模', '确定色彩方案'],
  1252. completedWorkload: 30
  1253. }
  1254. ];
  1255. }
  1256. loadSettlements(): void {
  1257. this.projectService.getSettlements().subscribe({
  1258. next: (settlements) => {
  1259. this.settlements = settlements.filter(s => s.projectId === this.projectId);
  1260. },
  1261. error: (error) => {
  1262. console.error('加载结算信息失败:', error);
  1263. }
  1264. });
  1265. }
  1266. loadRequirementChecklist(): void {
  1267. this.projectService.generateRequirementChecklist(this.projectId).subscribe({
  1268. next: (checklist) => {
  1269. this.requirementChecklist = checklist;
  1270. },
  1271. error: (error) => {
  1272. console.error('加载需求清单失败:', error);
  1273. }
  1274. });
  1275. }
  1276. updateFeedbackStatus(feedbackId: string, status: '处理中' | '已解决'): void {
  1277. this.projectService.updateFeedbackStatus(feedbackId, status).subscribe({
  1278. next: () => {
  1279. this.loadCustomerFeedbacks(); // 重新加载反馈
  1280. // 清除倒计时
  1281. if (this.countdownInterval) {
  1282. clearInterval(this.countdownInterval);
  1283. this.feedbackTimeoutCountdown = 0;
  1284. }
  1285. },
  1286. error: (error) => {
  1287. console.error('更新反馈状态失败:', error);
  1288. }
  1289. });
  1290. }
  1291. updateProjectStage(stage: ProjectStage): void {
  1292. if (this.project) {
  1293. this.projectService.updateProjectStage(this.projectId, stage).subscribe({
  1294. next: () => {
  1295. this.currentStage = stage; // 同步更新本地状态
  1296. this.project!.currentStage = stage; // 同步更新project对象的currentStage
  1297. this.loadProjectDetails(); // 重新加载项目详情
  1298. this.cdr.detectChanges(); // 触发变更检测以更新导航栏颜色
  1299. },
  1300. error: (error) => {
  1301. console.error('更新项目阶段失败:', error);
  1302. }
  1303. });
  1304. }
  1305. }
  1306. // 新增:根据给定阶段跳转到下一阶段
  1307. advanceToNextStage(afterStage: ProjectStage): void {
  1308. const idx = this.stageOrder.indexOf(afterStage);
  1309. if (idx >= 0 && idx < this.stageOrder.length - 1) {
  1310. const next = this.stageOrder[idx + 1];
  1311. this.updateProjectStage(next);
  1312. // 更新展开状态,折叠当前、展开下一阶段,提升体验
  1313. if (this.expandedStages[afterStage] !== undefined) this.expandedStages[afterStage] = false as any;
  1314. if (this.expandedStages[next] !== undefined) this.expandedStages[next] = true as any;
  1315. // 更新板块展开状态
  1316. const nextSection = this.getSectionKeyForStage(next);
  1317. this.expandedSection = nextSection;
  1318. // 新增:自动展开对应阶段的折叠面板
  1319. if (next === '软装' || next === '渲染') {
  1320. const processType = next === '软装' ? 'softDecor' : 'rendering';
  1321. const targetProcess = this.deliveryProcesses.find(p => p.type === processType);
  1322. if (targetProcess) {
  1323. // 展开对应的流程面板
  1324. targetProcess.isExpanded = true;
  1325. // 展开第一个空间以便用户操作
  1326. if (targetProcess.spaces.length > 0) {
  1327. targetProcess.spaces[0].isExpanded = true;
  1328. // 关闭其他空间
  1329. targetProcess.spaces.slice(1).forEach(space => space.isExpanded = false);
  1330. }
  1331. }
  1332. }
  1333. // 触发变更检测以更新导航栏颜色
  1334. this.cdr.detectChanges();
  1335. }
  1336. }
  1337. generateReminderMessage(): void {
  1338. this.projectService.generateReminderMessage('stagnation').subscribe({
  1339. next: (message) => {
  1340. this.reminderMessage = message;
  1341. // 3秒后自动清除提醒
  1342. setTimeout(() => {
  1343. this.reminderMessage = '';
  1344. }, 3000);
  1345. },
  1346. error: (error) => {
  1347. console.error('生成提醒消息失败:', error);
  1348. }
  1349. });
  1350. }
  1351. // ============ 新增:标准化阶段映射与紧急程度 ============
  1352. // 计算距离截止日期的天数(向下取整)
  1353. getDaysToDeadline(): number | null {
  1354. if (!this.project?.deadline) return null;
  1355. const now = new Date();
  1356. const deadline = new Date(this.project.deadline);
  1357. const diffMs = deadline.getTime() - now.getTime();
  1358. return Math.floor(diffMs / (1000 * 60 * 60 * 24));
  1359. }
  1360. // 是否延期/临期/提示
  1361. getUrgencyBadge(): 'overdue' | 'due_3' | 'due_7' | null {
  1362. const d = this.getDaysToDeadline();
  1363. if (d === null) return null;
  1364. if (d < 0) return 'overdue';
  1365. if (d <= 3) return 'due_3';
  1366. if (d <= 7) return 'due_7';
  1367. return null;
  1368. }
  1369. // 是否存在不满意或待处理投诉/反馈
  1370. hasPendingComplaint(): boolean {
  1371. return this.feedbacks.some(f => !f.isSatisfied || f.status === '待处理');
  1372. }
  1373. // 将现有细分阶段映射为标准化阶段
  1374. mapToStandardPhase(stage: ProjectStage): '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档' {
  1375. const mapping: Record<ProjectStage, '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'> = {
  1376. '订单分配': '待分配',
  1377. '需求沟通': '需求方案',
  1378. '方案确认': '需求方案',
  1379. '建模': '项目执行',
  1380. '软装': '项目执行',
  1381. '渲染': '项目执行',
  1382. '后期': '项目执行',
  1383. '尾款结算': '收尾验收',
  1384. '客户评价': '收尾验收',
  1385. '投诉处理': '收尾验收'
  1386. };
  1387. return mapping[stage] ?? '待分配';
  1388. }
  1389. getStandardPhaseIndex(): number {
  1390. if (!this.project?.currentStage) return 0;
  1391. const phase = this.mapToStandardPhase(this.project.currentStage);
  1392. return this.standardPhases.indexOf(phase);
  1393. }
  1394. isStandardPhaseCompleted(phase: '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'): boolean {
  1395. return this.standardPhases.indexOf(phase) < this.getStandardPhaseIndex();
  1396. }
  1397. isStandardPhaseCurrent(phase: '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'): boolean {
  1398. return this.standardPhases.indexOf(phase) === this.getStandardPhaseIndex();
  1399. }
  1400. // ============ 新增:项目报告导出 ============
  1401. exportProjectReport(): void {
  1402. if (!this.project) return;
  1403. const lines: string[] = [];
  1404. const d = this.getDaysToDeadline();
  1405. lines.push(`项目名称: ${this.project.name}`);
  1406. lines.push(`当前阶段(细分): ${this.project.currentStage}`);
  1407. lines.push(`当前阶段(标准化): ${this.mapToStandardPhase(this.project.currentStage)}`);
  1408. if (this.project.deadline) {
  1409. lines.push(`截止日期: ${this.formatDate(this.project.deadline)}`);
  1410. lines.push(`剩余天数: ${d !== null ? d : '-'}天`);
  1411. }
  1412. lines.push(`技能需求: ${(this.project.skillsRequired || []).join('、')}`);
  1413. lines.push('—— 渲染进度 ——');
  1414. lines.push(this.renderProgress ? `状态: ${this.renderProgress.status}, 完成度: ${this.renderProgress.completionRate}%` : '无渲染进度数据');
  1415. lines.push('—— 客户反馈 ——');
  1416. lines.push(this.feedbacks.length ? `${this.feedbacks.length} 条` : '暂无');
  1417. lines.push('—— 设计师变更 ——');
  1418. lines.push(this.designerChanges.length ? `${this.designerChanges.length} 条` : '暂无');
  1419. lines.push('—— 交付文件 ——');
  1420. lines.push(this.projectFiles.length ? this.projectFiles.map(f => `• ${f.name} (${f.type}, ${f.size})`).join('\n') : '暂无');
  1421. const content = lines.join('\n');
  1422. const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
  1423. const url = URL.createObjectURL(blob);
  1424. const a = document.createElement('a');
  1425. a.href = url;
  1426. a.download = `${this.project.name || '项目'}-阶段报告.txt`;
  1427. a.click();
  1428. URL.revokeObjectURL(url);
  1429. }
  1430. // ============ 新增:通用文件上传(含4K图片校验) ============
  1431. async onGeneralFilesSelected(event: Event): Promise<void> {
  1432. const input = event.target as HTMLInputElement;
  1433. if (!input.files || input.files.length === 0) return;
  1434. const files = Array.from(input.files);
  1435. this.isUploadingFile = true;
  1436. for (const file of files) {
  1437. // 对图片进行4K校验(最大边 >= 4000px)
  1438. if (/\.(jpg|jpeg|png)$/i.test(file.name)) {
  1439. const ok = await this.validateImage4K(file).catch(() => false);
  1440. if (!ok) {
  1441. alert(`图片不符合4K标准(最大边需≥4000像素):${file.name}`);
  1442. continue;
  1443. }
  1444. }
  1445. // 简化:直接追加到本地列表(实际应上传到服务器)
  1446. const fakeType = (file.name.split('.').pop() || '').toLowerCase();
  1447. const sizeMB = (file.size / (1024 * 1024)).toFixed(1) + 'MB';
  1448. const nowStr = this.formatDate(new Date());
  1449. this.projectFiles.unshift({
  1450. id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
  1451. name: file.name,
  1452. type: fakeType,
  1453. size: sizeMB,
  1454. date: nowStr,
  1455. url: '#'
  1456. });
  1457. }
  1458. this.isUploadingFile = false;
  1459. // 清空选择
  1460. input.value = '';
  1461. }
  1462. validateImage4K(file: File): Promise<boolean> {
  1463. return new Promise((resolve, reject) => {
  1464. const reader = new FileReader();
  1465. reader.onload = () => {
  1466. const img = new Image();
  1467. img.onload = () => {
  1468. const maxSide = Math.max(img.width, img.height);
  1469. resolve(maxSide >= 4000);
  1470. };
  1471. img.onerror = () => reject('image load error');
  1472. img.src = reader.result as string;
  1473. };
  1474. reader.onerror = () => reject('read error');
  1475. reader.readAsDataURL(file);
  1476. });
  1477. }
  1478. // 可选:列表 trackBy,优化渲染
  1479. trackById(_: number, item: { id: string }): string { return item.id; }
  1480. retryLoadRenderProgress(): void {
  1481. this.loadRenderProgress();
  1482. }
  1483. // 检查是否所有模型检查项都已通过
  1484. // 获取技能匹配度警告
  1485. getSkillMismatchWarning(): string | null {
  1486. if (!this.project) return null;
  1487. // 模拟技能匹配度检查
  1488. const designerSkills = ['现代风格', '硬装'];
  1489. const requiredSkills = this.project.skillsRequired;
  1490. const mismatchedSkills = requiredSkills.filter(skill => !designerSkills.includes(skill));
  1491. if (mismatchedSkills.length > 0) {
  1492. return `警告:您不擅长${mismatchedSkills.join('、')},建议联系组长协调`;
  1493. }
  1494. return null;
  1495. }
  1496. // 检查渲染是否超时
  1497. checkRenderTimeout(): void {
  1498. if (!this.renderProgress || !this.project) return;
  1499. // 模拟交付前3小时预警
  1500. const deliveryTime = new Date(this.project.deadline);
  1501. const currentTime = new Date();
  1502. const timeDifference = deliveryTime.getTime() - currentTime.getTime();
  1503. const hoursRemaining = Math.floor(timeDifference / (1000 * 60 * 60));
  1504. if (hoursRemaining <= 3 && hoursRemaining > 0) {
  1505. // 弹窗预警
  1506. alert('渲染进度预警:交付前3小时,请关注渲染进度');
  1507. }
  1508. if (hoursRemaining <= 1 && hoursRemaining > 0) {
  1509. // 更严重的预警
  1510. alert('渲染进度严重预警:交付前1小时,渲染可能无法按时完成!');
  1511. }
  1512. }
  1513. // 为客户反馈添加分类标签
  1514. tagCustomerFeedbacks(): void {
  1515. this.feedbacks.forEach(feedback => {
  1516. // 添加分类标签
  1517. if (feedback.content.includes('色彩') || feedback.problemLocation?.includes('色彩')) {
  1518. (feedback as any).tag = '色彩问题';
  1519. } else if (feedback.content.includes('家具') || feedback.problemLocation?.includes('家具')) {
  1520. (feedback as any).tag = '家具款式问题';
  1521. } else if (feedback.content.includes('光线') || feedback.content.includes('照明')) {
  1522. (feedback as any).tag = '光线问题';
  1523. } else {
  1524. (feedback as any).tag = '其他问题';
  1525. }
  1526. });
  1527. }
  1528. // 获取反馈标签的辅助方法
  1529. getFeedbackTag(feedback: CustomerFeedback): string {
  1530. return (feedback as any).tag || '';
  1531. }
  1532. // 检查反馈超时
  1533. checkFeedbackTimeout(): void {
  1534. const pendingFeedbacks = this.feedbacks.filter(f => f.status === '待处理');
  1535. if (pendingFeedbacks.length > 0) {
  1536. // 启动1小时倒计时
  1537. this.feedbackTimeoutCountdown = 3600; // 3600秒 = 1小时
  1538. this.startCountdown();
  1539. }
  1540. }
  1541. // 启动倒计时
  1542. startCountdown(): void {
  1543. this.countdownInterval = setInterval(() => {
  1544. if (this.feedbackTimeoutCountdown > 0) {
  1545. this.feedbackTimeoutCountdown--;
  1546. } else {
  1547. clearInterval(this.countdownInterval);
  1548. // 超时提醒
  1549. alert('客户反馈已超过1小时未响应,请立即处理!');
  1550. this.notifyTeamLeader('feedback-overdue');
  1551. }
  1552. }, 1000);
  1553. }
  1554. // 通知技术组长
  1555. notifyTeamLeader(type: 'render-failed' | 'feedback-overdue' | 'skill-mismatch'): void {
  1556. // 实际应用中应调用消息服务通知组长
  1557. console.log(`通知技术组长:${type} - 项目ID: ${this.projectId}`);
  1558. }
  1559. // 检查技能匹配度并提示
  1560. checkSkillMismatch(): void {
  1561. const warning = this.getSkillMismatchWarning();
  1562. if (warning) {
  1563. // 显示技能不匹配警告
  1564. if (confirm(`${warning}\n是否联系技术组长协调支持?`)) {
  1565. this.notifyTeamLeader('skill-mismatch');
  1566. }
  1567. }
  1568. }
  1569. // 发起设计师变更
  1570. initiateDesignerChange(reason: string): void {
  1571. // 实际应用中应调用API发起变更
  1572. console.log(`发起设计师变更,原因:${reason}`);
  1573. alert('已发起设计师变更申请,请等待新设计师承接');
  1574. }
  1575. // 确认承接变更项目
  1576. acceptDesignerChange(changeId: string): void {
  1577. // 实际应用中应调用API确认承接
  1578. console.log(`确认承接设计师变更:${changeId}`);
  1579. alert('已确认承接项目,系统已记录时间戳和责任人');
  1580. }
  1581. // 格式化倒计时显示
  1582. formatCountdown(seconds: number): string {
  1583. const hours = Math.floor(seconds / 3600);
  1584. const minutes = Math.floor((seconds % 3600) / 60);
  1585. const remainingSeconds = seconds % 60;
  1586. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
  1587. }
  1588. // 提交异常反馈
  1589. submitExceptionFeedback(): void {
  1590. if (!this.exceptionDescription.trim() || this.isSubmittingFeedback) {
  1591. alert('请填写异常类型和描述');
  1592. return;
  1593. }
  1594. this.isSubmittingFeedback = true;
  1595. // 模拟提交反馈到服务器
  1596. setTimeout(() => {
  1597. const newException: ExceptionHistory = {
  1598. id: `exception-${Date.now()}`,
  1599. type: this.exceptionType,
  1600. description: this.exceptionDescription,
  1601. submitTime: new Date(),
  1602. status: '待处理'
  1603. };
  1604. // 添加到历史记录中
  1605. this.exceptionHistories.unshift(newException);
  1606. // 通知客服和技术支持
  1607. this.notifyTechnicalSupport(newException);
  1608. // 清空表单
  1609. this.exceptionDescription = '';
  1610. this.clearExceptionScreenshot();
  1611. this.showExceptionForm = false;
  1612. // 显示成功消息
  1613. alert('异常反馈已提交,技术支持将尽快处理');
  1614. this.isSubmittingFeedback = false;
  1615. }, 1000);
  1616. }
  1617. // 上传异常截图
  1618. uploadExceptionScreenshot(event: Event): void {
  1619. const input = event.target as HTMLInputElement;
  1620. if (input.files && input.files[0]) {
  1621. const file = input.files[0];
  1622. // 在实际应用中,这里应该上传文件到服务器
  1623. // 这里我们使用FileReader来生成一个预览URL
  1624. const reader = new FileReader();
  1625. reader.onload = (e) => {
  1626. this.exceptionScreenshotUrl = e.target?.result as string;
  1627. };
  1628. reader.readAsDataURL(file);
  1629. }
  1630. }
  1631. // 清除异常截图
  1632. clearExceptionScreenshot(): void {
  1633. this.exceptionScreenshotUrl = null;
  1634. const input = document.getElementById('screenshot-upload') as HTMLInputElement;
  1635. if (input) {
  1636. input.value = '';
  1637. }
  1638. }
  1639. // 联系组长
  1640. contactTeamLeader() {
  1641. alert(`已联系${this.project?.assigneeName || '项目组长'}`);
  1642. }
  1643. // 处理渲染超时预警
  1644. handleRenderTimeout() {
  1645. alert('已发送渲染超时预警通知');
  1646. }
  1647. // 通知技术支持
  1648. notifyTechnicalSupport(exception: ExceptionHistory): void {
  1649. // 实际应用中应调用消息服务通知技术支持和客服
  1650. console.log(`通知技术支持和客服:渲染异常 - 项目ID: ${this.projectId}`);
  1651. console.log(`异常类型: ${this.getExceptionTypeText(exception.type)}, 描述: ${exception.description}`);
  1652. }
  1653. // 获取异常类型文本
  1654. getExceptionTypeText(type: string): string {
  1655. const typeMap: Record<string, string> = {
  1656. 'failed': '渲染失败',
  1657. 'stuck': '渲染卡顿',
  1658. 'quality': '渲染质量问题',
  1659. 'other': '其他问题'
  1660. };
  1661. return typeMap[type] || type;
  1662. }
  1663. // 格式化日期
  1664. formatDate(date: Date | string): string {
  1665. const d = typeof date === 'string' ? new Date(date) : date;
  1666. const year = d.getFullYear();
  1667. const month = String(d.getMonth() + 1).padStart(2, '0');
  1668. const day = String(d.getDate()).padStart(2, '0');
  1669. const hours = String(d.getHours()).padStart(2, '0');
  1670. const minutes = String(d.getMinutes()).padStart(2, '0');
  1671. return `${year}-${month}-${day} ${hours}:${minutes}`;
  1672. }
  1673. // 将字节格式化为易读尺寸
  1674. private formatFileSize(bytes: number): string {
  1675. if (bytes < 1024) return `${bytes}B`;
  1676. const kb = bytes / 1024;
  1677. if (kb < 1024) return `${kb.toFixed(1)}KB`;
  1678. const mb = kb / 1024;
  1679. if (mb < 1024) return `${mb.toFixed(1)}MB`;
  1680. const gb = mb / 1024;
  1681. return `${gb.toFixed(2)}GB`;
  1682. }
  1683. // 生成缩略图条目(并创建本地预览URL)
  1684. private makeImageItem(file: File): { id: string; name: string; url: string; size: string } {
  1685. const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  1686. const url = URL.createObjectURL(file);
  1687. return { id, name: file.name, url, size: this.formatFileSize(file.size) };
  1688. }
  1689. // 释放对象URL
  1690. private revokeUrl(url: string): void {
  1691. try { if (url && url.startsWith('blob:')) URL.revokeObjectURL(url); } catch {}
  1692. }
  1693. // =========== 建模阶段:白模上传 ===========
  1694. onWhiteModelSelected(event: Event): void {
  1695. const input = event.target as HTMLInputElement;
  1696. if (!input.files || input.files.length === 0) return;
  1697. const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
  1698. const items = files.map(f => this.makeImageItem(f));
  1699. this.whiteModelImages.unshift(...items);
  1700. input.value = '';
  1701. }
  1702. removeWhiteModelImage(id: string): void {
  1703. const target = this.whiteModelImages.find(i => i.id === id);
  1704. if (target) this.revokeUrl(target.url);
  1705. this.whiteModelImages = this.whiteModelImages.filter(i => i.id !== id);
  1706. }
  1707. // 新增:建模阶段 确认上传并自动进入下一阶段(软装)
  1708. confirmWhiteModelUpload(): void {
  1709. // 检查建模阶段的图片数据
  1710. const modelingProcess = this.deliveryProcesses.find(p => p.id === 'modeling');
  1711. if (!modelingProcess) return;
  1712. // 检查是否有任何空间上传了图片
  1713. const hasImages = modelingProcess.spaces.some(space => {
  1714. const content = modelingProcess.content[space.id];
  1715. return content && content.images && content.images.length > 0;
  1716. });
  1717. if (!hasImages) return;
  1718. this.advanceToNextStage('建模');
  1719. }
  1720. // =========== 软装阶段:小图上传(建议≤1MB,不强制) ===========
  1721. onSoftDecorSmallPicsSelected(event: Event): void {
  1722. const input = event.target as HTMLInputElement;
  1723. if (!input.files || input.files.length === 0) return;
  1724. const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
  1725. const warnOversize = files.filter(f => f.size > 1024 * 1024);
  1726. if (warnOversize.length > 0) {
  1727. // 仅提示,不阻断
  1728. console.warn('软装小图建议≤1MB,以下文件较大:', warnOversize.map(f => f.name));
  1729. }
  1730. const items = files.map(f => this.makeImageItem(f));
  1731. this.softDecorImages.unshift(...items);
  1732. input.value = '';
  1733. }
  1734. // 拖拽上传相关属性
  1735. isDragOver: boolean = false;
  1736. // 图片预览相关属性
  1737. showImagePreview: boolean = false;
  1738. previewImageData: any = null;
  1739. // 图片预览方法(含渲染大图加锁校验)
  1740. previewImage(img: any): void {
  1741. const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
  1742. if (isRenderLarge && img?.locked) {
  1743. alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可预览。');
  1744. return;
  1745. }
  1746. this.previewImageData = img;
  1747. this.showImagePreview = true;
  1748. }
  1749. closeImagePreview(): void {
  1750. this.showImagePreview = false;
  1751. this.previewImageData = null;
  1752. }
  1753. downloadImage(img: any): void {
  1754. const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
  1755. if (isRenderLarge && img?.locked) {
  1756. alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可下载。');
  1757. return;
  1758. }
  1759. if (img) {
  1760. const link = document.createElement('a');
  1761. link.href = img.url;
  1762. link.download = img.name;
  1763. link.click();
  1764. }
  1765. }
  1766. removeImageFromPreview(): void {
  1767. if (this.previewImageData) {
  1768. // 首先检查新的 deliveryProcesses 结构
  1769. let imageFound = false;
  1770. for (const process of this.deliveryProcesses) {
  1771. for (const space of process.spaces) {
  1772. if (process.content[space.id]?.images) {
  1773. const imageIndex = process.content[space.id].images.findIndex(img => img.id === this.previewImageData.id);
  1774. if (imageIndex > -1) {
  1775. this.removeSpaceImage(process.id, space.id, this.previewImageData.id);
  1776. imageFound = true;
  1777. break;
  1778. }
  1779. }
  1780. }
  1781. if (imageFound) break;
  1782. }
  1783. // 如果在新结构中没找到,检查旧的图片数组
  1784. if (!imageFound) {
  1785. if (this.whiteModelImages.find(i => i.id === this.previewImageData.id)) {
  1786. this.removeWhiteModelImage(this.previewImageData.id);
  1787. } else if (this.softDecorImages.find(i => i.id === this.previewImageData.id)) {
  1788. this.removeSoftDecorImage(this.previewImageData.id);
  1789. } else if (this.renderLargeImages.find(i => i.id === this.previewImageData.id)) {
  1790. this.removeRenderLargeImage(this.previewImageData.id);
  1791. } else if (this.postProcessImages.find(i => i.id === this.previewImageData.id)) {
  1792. this.removePostProcessImage(this.previewImageData.id);
  1793. }
  1794. }
  1795. this.closeImagePreview();
  1796. }
  1797. }
  1798. // 拖拽事件处理
  1799. onDragOver(event: DragEvent): void {
  1800. event.preventDefault();
  1801. event.stopPropagation();
  1802. this.isDragOver = true;
  1803. }
  1804. onDragLeave(event: DragEvent): void {
  1805. event.preventDefault();
  1806. event.stopPropagation();
  1807. this.isDragOver = false;
  1808. }
  1809. onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
  1810. event.preventDefault();
  1811. event.stopPropagation();
  1812. this.isDragOver = false;
  1813. const files = event.dataTransfer?.files;
  1814. if (!files || files.length === 0) return;
  1815. // 创建模拟的input事件
  1816. const mockEvent = {
  1817. target: {
  1818. files: files
  1819. }
  1820. } as any;
  1821. // 根据类型调用相应的处理方法
  1822. switch (type) {
  1823. case 'whiteModel':
  1824. this.onWhiteModelSelected(mockEvent);
  1825. break;
  1826. case 'softDecor':
  1827. this.onSoftDecorSmallPicsSelected(mockEvent);
  1828. break;
  1829. case 'render':
  1830. this.onRenderLargePicsSelected(mockEvent);
  1831. break;
  1832. case 'postProcess':
  1833. this.onPostProcessPicsSelected(mockEvent);
  1834. break;
  1835. }
  1836. }
  1837. // 触发文件输入框
  1838. triggerFileInput(type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
  1839. let inputId: string;
  1840. switch (type) {
  1841. case 'whiteModel':
  1842. inputId = 'whiteModelFileInput';
  1843. break;
  1844. case 'softDecor':
  1845. inputId = 'softDecorFileInput';
  1846. break;
  1847. case 'render':
  1848. inputId = 'renderFileInput';
  1849. break;
  1850. case 'postProcess':
  1851. inputId = 'postProcessFileInput';
  1852. break;
  1853. }
  1854. const input = document.querySelector(`#${inputId}`) as HTMLInputElement;
  1855. if (input) {
  1856. input.click();
  1857. }
  1858. }
  1859. removeSoftDecorImage(id: string): void {
  1860. const target = this.softDecorImages.find(i => i.id === id);
  1861. if (target) this.revokeUrl(target.url);
  1862. this.softDecorImages = this.softDecorImages.filter(i => i.id !== id);
  1863. }
  1864. // 新增:后期阶段图片上传处理
  1865. async onPostProcessPicsSelected(event: Event): Promise<void> {
  1866. const input = event.target as HTMLInputElement;
  1867. if (!input.files || input.files.length === 0) return;
  1868. const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
  1869. for (const f of files) {
  1870. const item = this.makeImageItem(f);
  1871. this.postProcessImages.unshift({
  1872. id: item.id,
  1873. name: item.name,
  1874. url: item.url,
  1875. size: this.formatFileSize(f.size)
  1876. });
  1877. }
  1878. input.value = '';
  1879. }
  1880. removePostProcessImage(id: string): void {
  1881. const target = this.postProcessImages.find(i => i.id === id);
  1882. if (target) this.revokeUrl(target.url);
  1883. this.postProcessImages = this.postProcessImages.filter(i => i.id !== id);
  1884. }
  1885. // 新增:后期阶段 确认上传并自动进入下一阶段(尾款结算)
  1886. confirmPostProcessUpload(): void {
  1887. // 检查后期阶段的图片数据
  1888. const postProcessProcess = this.deliveryProcesses.find(p => p.id === 'post-processing');
  1889. if (!postProcessProcess) return;
  1890. // 检查是否有任何空间上传了图片
  1891. const hasImages = postProcessProcess.spaces.some(space => {
  1892. const content = postProcessProcess.content[space.id];
  1893. return content && content.images && content.images.length > 0;
  1894. });
  1895. if (!hasImages) return;
  1896. this.advanceToNextStage('后期');
  1897. }
  1898. // 新增:尾款结算阶段确认并自动进入下一阶段(客户评价)
  1899. confirmSettlement(): void {
  1900. if (this.isConfirmingSettlement) return;
  1901. this.isConfirmingSettlement = true;
  1902. // 模拟API调用延迟
  1903. setTimeout(() => {
  1904. this.isSettlementCompleted = true;
  1905. this.isConfirmingSettlement = false;
  1906. // 显示成功提示
  1907. alert('尾款结算已确认完成!');
  1908. // 进入下一阶段
  1909. this.advanceToNextStage('尾款结算');
  1910. }, 1500);
  1911. }
  1912. // 新增:投诉处理阶段确认并完成项目(基础版本,详细版本在售后模块中)
  1913. confirmComplaintBasic(): void {
  1914. console.log('确认投诉处理完成');
  1915. // 可以在这里添加更多逻辑,比如标记项目完成等
  1916. // 调用服务更新后端数据
  1917. // this.projectService.confirmComplaintResolution(this.projectId);
  1918. this.advanceToNextStage('投诉处理');
  1919. }
  1920. // 新增:软装阶段 确认上传并自动进入下一阶段(渲染)
  1921. confirmSoftDecorUpload(): void {
  1922. // 检查软装阶段的图片数据
  1923. const softDecorProcess = this.deliveryProcesses.find(p => p.id === 'soft-decoration');
  1924. if (!softDecorProcess) return;
  1925. // 检查是否有任何空间上传了图片
  1926. const hasImages = softDecorProcess.spaces.some(space => {
  1927. const content = softDecorProcess.content[space.id];
  1928. return content && content.images && content.images.length > 0;
  1929. });
  1930. if (!hasImages) return;
  1931. this.advanceToNextStage('软装');
  1932. }
  1933. // 新增:渲染阶段 确认上传并自动进入下一阶段(后期)
  1934. confirmRenderUpload(): void {
  1935. // 检查渲染阶段的图片数据
  1936. const renderProcess = this.deliveryProcesses.find(p => p.id === 'rendering');
  1937. if (!renderProcess) return;
  1938. // 检查是否有任何空间上传了图片
  1939. const hasImages = renderProcess.spaces.some(space => {
  1940. const content = renderProcess.content[space.id];
  1941. return content && content.images && content.images.length > 0;
  1942. });
  1943. if (!hasImages) return;
  1944. this.advanceToNextStage('渲染');
  1945. }
  1946. // =========== 渲染阶段:大图上传(弹窗 + 4K校验) ===========
  1947. openRenderUploadModal(): void {
  1948. this.showRenderUploadModal = true;
  1949. this.pendingRenderLargeItems = [];
  1950. }
  1951. closeRenderUploadModal(): void {
  1952. // 关闭时释放临时预览URL
  1953. this.pendingRenderLargeItems.forEach(i => this.revokeUrl(i.url));
  1954. this.pendingRenderLargeItems = [];
  1955. this.showRenderUploadModal = false;
  1956. }
  1957. async onRenderLargePicsSelected(event: Event): Promise<void> {
  1958. const input = event.target as HTMLInputElement;
  1959. if (!input.files || input.files.length === 0) return;
  1960. const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
  1961. for (const f of files) {
  1962. const ok = await this.validateImage4K(f).catch(() => false);
  1963. if (!ok) {
  1964. alert(`图片不符合4K标准(最大边需≥4000像素):${f.name}`);
  1965. continue;
  1966. }
  1967. const item = this.makeImageItem(f);
  1968. // 直接添加到正式列表,不再使用待确认列表
  1969. this.renderLargeImages.unshift({
  1970. id: item.id,
  1971. name: item.name,
  1972. url: item.url,
  1973. size: this.formatFileSize(f.size),
  1974. locked: true
  1975. });
  1976. }
  1977. input.value = '';
  1978. }
  1979. removeRenderLargeImage(id: string): void {
  1980. const target = this.renderLargeImages.find(i => i.id === id);
  1981. if (target) this.revokeUrl(target.url);
  1982. this.renderLargeImages = this.renderLargeImages.filter(i => i.id !== id);
  1983. }
  1984. // 根据阶段映射所属板块
  1985. getSectionKeyForStage(stage: ProjectStage): SectionKey {
  1986. switch (stage) {
  1987. case '订单分配':
  1988. return 'order';
  1989. case '需求沟通':
  1990. case '方案确认':
  1991. return 'requirements';
  1992. case '建模':
  1993. case '软装':
  1994. case '渲染':
  1995. case '后期':
  1996. return 'delivery';
  1997. case '尾款结算':
  1998. case '客户评价':
  1999. case '投诉处理':
  2000. return 'aftercare';
  2001. default:
  2002. return 'order';
  2003. }
  2004. }
  2005. // 获取板块状态:completed | 'active' | 'pending'
  2006. getSectionStatus(key: SectionKey): 'completed' | 'active' | 'pending' {
  2007. // 优先使用本地的currentStage,如果没有则使用project.currentStage
  2008. const current = (this.currentStage || this.project?.currentStage) as ProjectStage | undefined;
  2009. // 如果没有当前阶段(新创建的项目),默认订单分配板块为active(红色)
  2010. if (!current || current === '订单分配') {
  2011. return key === 'order' ? 'active' : 'pending';
  2012. }
  2013. // 获取当前阶段所属的板块
  2014. const currentSection = this.getSectionKeyForStage(current);
  2015. const sectionOrder = this.sections.map(s => s.key);
  2016. const currentIdx = sectionOrder.indexOf(currentSection);
  2017. const idx = sectionOrder.indexOf(key);
  2018. if (idx === -1 || currentIdx === -1) return 'pending';
  2019. // 已完成的板块:当前阶段所在板块之前的所有板块
  2020. if (idx < currentIdx) return 'completed';
  2021. // 当前进行中的板块:当前阶段所在的板块
  2022. if (idx === currentIdx) return 'active';
  2023. // 未开始的板块:当前阶段所在板块之后的所有板块
  2024. return 'pending';
  2025. }
  2026. // 切换四大板块(单展开)
  2027. toggleSection(key: SectionKey): void {
  2028. this.expandedSection = key;
  2029. // 点击板块按钮时,滚动到该板块的第一个可见阶段卡片
  2030. const sec = this.sections.find(s => s.key === key);
  2031. if (sec) {
  2032. // 设计师仅滚动到可见的三大执行阶段,否则取该板块第一个阶段
  2033. const candidate = this.isDesignerView()
  2034. ? sec.stages.find(st => ['建模', '软装', '渲染'].includes(st)) || sec.stages[0]
  2035. : sec.stages[0];
  2036. this.scrollToStage(candidate);
  2037. }
  2038. }
  2039. // 阶段到锚点的映射
  2040. stageToAnchor(stage: ProjectStage): string {
  2041. const map: Record<ProjectStage, string> = {
  2042. '订单分配': 'order',
  2043. '需求沟通': 'requirements-talk',
  2044. '方案确认': 'proposal-confirm',
  2045. '建模': 'modeling',
  2046. '软装': 'softdecor',
  2047. '渲染': 'render',
  2048. '后期': 'postprocess',
  2049. '尾款结算': 'settlement',
  2050. '客户评价': 'customer-review',
  2051. '投诉处理': 'complaint'
  2052. };
  2053. return `stage-${map[stage] || 'unknown'}`;
  2054. }
  2055. // 平滑滚动到指定阶段卡片
  2056. scrollToStage(stage: ProjectStage): void {
  2057. const anchor = this.stageToAnchor(stage);
  2058. const el = document.getElementById(anchor);
  2059. if (el) {
  2060. el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  2061. }
  2062. }
  2063. // 订单分配阶段:客户信息(迁移自客服端"客户信息"卡片)
  2064. orderCreationMethod: 'miniprogram' | 'manual' = 'miniprogram';
  2065. isSyncing: boolean = false;
  2066. orderTime: string = '';
  2067. // 客户信息实时同步相关变量
  2068. isSyncingCustomerInfo: boolean = false;
  2069. lastSyncTime: Date | null = null;
  2070. syncInterval: any = null;
  2071. customerForm!: FormGroup;
  2072. customerSearchKeyword: string = '';
  2073. customerSearchResults: Array<{ id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string }> = [];
  2074. selectedOrderCustomer: { id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string } | null = null;
  2075. demandTypes = [
  2076. { value: 'price', label: '价格敏感' },
  2077. { value: 'quality', label: '质量敏感' },
  2078. { value: 'comprehensive', label: '综合要求' }
  2079. ];
  2080. followUpStatus = [
  2081. { value: 'quotation', label: '待报价' },
  2082. { value: 'confirm', label: '待确认需求' },
  2083. { value: 'lost', label: '已失联' }
  2084. ];
  2085. // 需求关键信息同步数据
  2086. requirementKeyInfo = {
  2087. colorAtmosphere: {
  2088. description: '',
  2089. mainColor: '',
  2090. colorTemp: '',
  2091. materials: [] as string[]
  2092. },
  2093. spaceStructure: {
  2094. lineRatio: 0,
  2095. blankRatio: 0,
  2096. flowWidth: 0,
  2097. aspectRatio: 0,
  2098. ceilingHeight: 0
  2099. },
  2100. materialWeights: {
  2101. fabricRatio: 0,
  2102. woodRatio: 0,
  2103. metalRatio: 0,
  2104. smoothness: 0,
  2105. glossiness: 0
  2106. },
  2107. presetAtmosphere: {
  2108. name: '',
  2109. rgb: '',
  2110. colorTemp: '',
  2111. materials: [] as string[]
  2112. }
  2113. };
  2114. // 客户信息:搜索/选择/清空/同步/快速填写 逻辑
  2115. searchCustomer(): void {
  2116. if (this.customerSearchKeyword.trim().length >= 2) {
  2117. this.customerSearchResults = [
  2118. { id: '1', name: '张先生', phone: '138****5678', customerType: '老客户', source: '官网咨询', avatar: "data:image/svg+xml,%3Csvg width='64' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23E6E6E6'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E" },
  2119. { id: '2', name: '李女士', phone: '139****1234', customerType: 'VIP客户', source: '推荐介绍', avatar: "data:image/svg+xml,%3Csvg width='65' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23DCDCDC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E" }
  2120. ];
  2121. } else {
  2122. this.customerSearchResults = [];
  2123. }
  2124. }
  2125. selectCustomer(customer: { id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string }): void {
  2126. this.selectedOrderCustomer = customer;
  2127. this.customerForm.patchValue({
  2128. name: customer.name,
  2129. phone: customer.phone,
  2130. wechat: customer.wechat || '',
  2131. customerType: customer.customerType || '新客户',
  2132. source: customer.source || '',
  2133. remark: customer.remark || ''
  2134. });
  2135. this.customerSearchResults = [];
  2136. this.customerSearchKeyword = '';
  2137. }
  2138. clearSelectedCustomer(): void {
  2139. this.selectedOrderCustomer = null;
  2140. this.customerForm.reset({ customerType: '新客户' });
  2141. }
  2142. quickFillCustomerInfo(keyword: string): void {
  2143. const k = (keyword || '').trim();
  2144. if (!k) return;
  2145. // 模拟:若有搜索结果,选择第一条
  2146. if (this.customerSearchResults.length === 0) this.searchCustomer();
  2147. if (this.customerSearchResults.length > 0) {
  2148. this.selectCustomer(this.customerSearchResults[0]);
  2149. }
  2150. }
  2151. syncMiniprogramCustomerInfo(): void {
  2152. if (this.isSyncing) return;
  2153. this.isSyncing = true;
  2154. setTimeout(() => {
  2155. // 模拟从小程序同步到客户表单
  2156. this.customerForm.patchValue({
  2157. name: '小程序用户',
  2158. phone: '13800001234',
  2159. wechat: 'wx_user_001',
  2160. customerType: '新客户',
  2161. source: '小程序下单'
  2162. });
  2163. this.isSyncing = false;
  2164. // 触发客户信息同步显示
  2165. this.syncCustomerInfoDisplay();
  2166. }, 1000);
  2167. }
  2168. // 同步客户信息显示
  2169. syncCustomerInfoDisplay(): void {
  2170. this.isSyncingCustomerInfo = true;
  2171. // 模拟同步过程
  2172. setTimeout(() => {
  2173. this.lastSyncTime = new Date();
  2174. this.isSyncingCustomerInfo = false;
  2175. // 触发界面更新
  2176. this.cdr.detectChanges();
  2177. console.log('客户信息显示已同步:', this.lastSyncTime);
  2178. }, 800);
  2179. }
  2180. // 启动自动同步
  2181. startAutoSync(): void {
  2182. if (this.syncInterval) {
  2183. clearInterval(this.syncInterval);
  2184. }
  2185. // 每30秒自动同步一次
  2186. this.syncInterval = setInterval(() => {
  2187. this.syncCustomerInfoDisplay();
  2188. }, 30000);
  2189. }
  2190. // 停止自动同步
  2191. stopAutoSync(): void {
  2192. if (this.syncInterval) {
  2193. clearInterval(this.syncInterval);
  2194. this.syncInterval = null;
  2195. }
  2196. }
  2197. // 格式化时间显示
  2198. formatTime(date: Date): string {
  2199. const now = new Date();
  2200. const diff = now.getTime() - date.getTime();
  2201. const minutes = Math.floor(diff / 60000);
  2202. if (minutes < 1) {
  2203. return '刚刚';
  2204. } else if (minutes < 60) {
  2205. return `${minutes}分钟前`;
  2206. } else {
  2207. const hours = Math.floor(minutes / 60);
  2208. return `${hours}小时前`;
  2209. }
  2210. }
  2211. downloadFile(file: ProjectFile): void {
  2212. // 实现文件下载逻辑
  2213. const link = document.createElement('a');
  2214. link.href = file.url;
  2215. link.download = file.name;
  2216. link.click();
  2217. }
  2218. previewFile(file: ProjectFile): void {
  2219. // 预览文件逻辑
  2220. console.log('预览文件:', file.name);
  2221. }
  2222. // 处理参考图更新事件
  2223. onReferenceImagesUpdated(images: any[]): void {
  2224. console.log('参考图已更新:', images);
  2225. // 这里可以实现保存到后端的逻辑
  2226. // 也可以触发项目状态的更新
  2227. }
  2228. // 同步需求关键信息到客户信息卡片
  2229. syncRequirementKeyInfo(requirementData: any): void {
  2230. if (requirementData) {
  2231. // 同步色彩氛围信息
  2232. if (requirementData.colorIndicators) {
  2233. this.requirementKeyInfo.colorAtmosphere = {
  2234. description: requirementData.colorIndicators.colorRange || '',
  2235. mainColor: `rgb(${requirementData.colorIndicators.mainColor?.r || 0}, ${requirementData.colorIndicators.mainColor?.g || 0}, ${requirementData.colorIndicators.mainColor?.b || 0})`,
  2236. colorTemp: `${requirementData.colorIndicators.colorTemperature || 0}K`,
  2237. materials: []
  2238. };
  2239. }
  2240. // 同步空间结构信息
  2241. if (requirementData.spaceIndicators) {
  2242. this.requirementKeyInfo.spaceStructure = {
  2243. lineRatio: requirementData.spaceIndicators.lineRatio || 0,
  2244. blankRatio: requirementData.spaceIndicators.blankRatio || 0,
  2245. flowWidth: requirementData.spaceIndicators.flowWidth || 0,
  2246. aspectRatio: requirementData.spaceIndicators.aspectRatio || 0,
  2247. ceilingHeight: requirementData.spaceIndicators.ceilingHeight || 0
  2248. };
  2249. }
  2250. // 同步材质权重信息
  2251. if (requirementData.materialIndicators) {
  2252. this.requirementKeyInfo.materialWeights = {
  2253. fabricRatio: requirementData.materialIndicators.fabricRatio || 0,
  2254. woodRatio: requirementData.materialIndicators.woodRatio || 0,
  2255. metalRatio: requirementData.materialIndicators.metalRatio || 0,
  2256. smoothness: requirementData.materialIndicators.smoothness || 0,
  2257. glossiness: requirementData.materialIndicators.glossiness || 0
  2258. };
  2259. }
  2260. // 同步预设氛围信息
  2261. if (requirementData.selectedPresetAtmosphere) {
  2262. this.requirementKeyInfo.presetAtmosphere = {
  2263. name: requirementData.selectedPresetAtmosphere.name || '',
  2264. rgb: requirementData.selectedPresetAtmosphere.rgb || '',
  2265. colorTemp: requirementData.selectedPresetAtmosphere.colorTemp || '',
  2266. materials: requirementData.selectedPresetAtmosphere.materials || []
  2267. };
  2268. }
  2269. // 新增:实时更新左侧客户信息显示
  2270. if (this.project) {
  2271. // 更新项目的客户信息
  2272. if (requirementData.colorIndicators && requirementData.colorIndicators.length > 0) {
  2273. this.project.customerInfo = {
  2274. ...this.project.customerInfo,
  2275. colorPreference: requirementData.colorIndicators.map((indicator: any) => indicator.name).join(', ')
  2276. };
  2277. }
  2278. // 更新空间结构信息
  2279. if (requirementData.spaceIndicators && requirementData.spaceIndicators.length > 0) {
  2280. this.project.customerInfo = {
  2281. ...this.project.customerInfo,
  2282. spaceRequirements: requirementData.spaceIndicators.map((indicator: any) => `${indicator.name}: ${indicator.value}`).join(', ')
  2283. };
  2284. }
  2285. // 更新材质偏好
  2286. if (requirementData.materialIndicators && requirementData.materialIndicators.length > 0) {
  2287. this.project.customerInfo = {
  2288. ...this.project.customerInfo,
  2289. materialPreference: requirementData.materialIndicators.map((indicator: any) => `${indicator.name}: ${indicator.value}%`).join(', ')
  2290. };
  2291. }
  2292. // 触发变更检测以更新UI
  2293. this.cdr.detectChanges();
  2294. }
  2295. console.log('需求关键信息已同步:', this.requirementKeyInfo);
  2296. } else {
  2297. // 模拟数据用于演示
  2298. this.requirementKeyInfo = {
  2299. colorAtmosphere: {
  2300. description: '温馨暖调',
  2301. mainColor: 'rgb(255, 230, 180)',
  2302. colorTemp: '2700K',
  2303. materials: ['木质', '布艺']
  2304. },
  2305. spaceStructure: {
  2306. lineRatio: 60,
  2307. blankRatio: 30,
  2308. flowWidth: 0.9,
  2309. aspectRatio: 1.6,
  2310. ceilingHeight: 2.8
  2311. },
  2312. materialWeights: {
  2313. fabricRatio: 50,
  2314. woodRatio: 30,
  2315. metalRatio: 20,
  2316. smoothness: 7,
  2317. glossiness: 4
  2318. },
  2319. presetAtmosphere: {
  2320. name: '现代简约',
  2321. rgb: '200,220,240',
  2322. colorTemp: '5000K',
  2323. materials: ['金属', '玻璃']
  2324. }
  2325. };
  2326. }
  2327. }
  2328. // 新增:处理需求阶段完成事件
  2329. onRequirementsStageCompleted(event: { stage: string; allStagesCompleted: boolean }): void {
  2330. console.log('需求阶段完成事件:', event);
  2331. if (event.allStagesCompleted && event.stage === 'requirements-communication') {
  2332. // 自动推进到方案确认阶段
  2333. this.currentStage = '方案确认';
  2334. this.expandedStages['方案确认'] = true;
  2335. this.expandedStages['需求沟通'] = false;
  2336. // 更新项目状态
  2337. this.updateProjectStage('方案确认');
  2338. console.log('自动推进到方案确认阶段');
  2339. }
  2340. }
  2341. // 新增:确认方案方法
  2342. confirmProposal(): void {
  2343. console.log('确认方案按钮被点击');
  2344. // 使用统一的阶段推进方法
  2345. this.advanceToNextStage('方案确认');
  2346. console.log('已跳转到建模阶段');
  2347. }
  2348. // 获取同步的关键信息摘要
  2349. getRequirementSummary(): string[] {
  2350. const summary: string[] = [];
  2351. if (this.requirementKeyInfo.colorAtmosphere.description) {
  2352. summary.push(`色彩氛围: ${this.requirementKeyInfo.colorAtmosphere.description}`);
  2353. }
  2354. if (this.requirementKeyInfo.spaceStructure.aspectRatio > 0) {
  2355. summary.push(`空间比例: ${this.requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1)}`);
  2356. }
  2357. if (this.requirementKeyInfo.materialWeights.woodRatio > 0) {
  2358. summary.push(`木质占比: ${this.requirementKeyInfo.materialWeights.woodRatio}%`);
  2359. }
  2360. if (this.requirementKeyInfo.presetAtmosphere.name) {
  2361. summary.push(`预设氛围: ${this.requirementKeyInfo.presetAtmosphere.name}`);
  2362. }
  2363. return summary;
  2364. }
  2365. // 检查必需阶段是否全部完成(流程进度 > 确认需求 > 需求沟通四个流程)
  2366. areRequiredStagesCompleted(): boolean {
  2367. // 检查项目是否已经进入方案确认阶段或更后的阶段
  2368. if (!this.project) {
  2369. return false;
  2370. }
  2371. const stageOrder = [
  2372. '订单分配', '需求沟通', '方案确认', '建模', '软装',
  2373. '渲染', '尾款结算', '客户评价', '投诉处理'
  2374. ];
  2375. const currentStageIndex = stageOrder.indexOf(this.project.currentStage);
  2376. const proposalStageIndex = stageOrder.indexOf('方案确认');
  2377. const requirementStageIndex = stageOrder.indexOf('需求沟通');
  2378. // 如果当前阶段是方案确认或之后的阶段,则认为需求阶段已完成
  2379. if (currentStageIndex >= proposalStageIndex) {
  2380. // 确保有基本的需求信息数据,如果没有则初始化模拟数据
  2381. this.ensureRequirementData();
  2382. return true;
  2383. }
  2384. // 如果当前阶段是需求沟通,检查需求沟通是否已完成
  2385. if (currentStageIndex === requirementStageIndex) {
  2386. // 检查需求关键信息是否有数据,或者检查需求沟通组件的完成状态
  2387. const hasRequirementData = this.getRequirementSummary().length > 0 &&
  2388. (!!this.requirementKeyInfo.colorAtmosphere.description ||
  2389. this.requirementKeyInfo.spaceStructure.aspectRatio > 0 ||
  2390. this.requirementKeyInfo.materialWeights.woodRatio > 0 ||
  2391. !!this.requirementKeyInfo.presetAtmosphere.name);
  2392. // 只有在真正有需求数据时才返回true,不再自动初始化模拟数据
  2393. if (hasRequirementData) {
  2394. return true;
  2395. }
  2396. // 如果没有需求数据,返回false,不允许进入下一阶段
  2397. return false;
  2398. }
  2399. // 其他情况返回false
  2400. return false;
  2401. }
  2402. // 确保有需求数据用于方案确认显示
  2403. private ensureRequirementData(): void {
  2404. // console.log('=== ensureRequirementData 开始确保需求数据 ===');
  2405. // console.log('当前requirementKeyInfo:', this.requirementKeyInfo);
  2406. // 修复条件判断:检查是否需要初始化数据
  2407. const needsInitialization =
  2408. !this.requirementKeyInfo.colorAtmosphere.description ||
  2409. this.requirementKeyInfo.spaceStructure.aspectRatio === 0 ||
  2410. this.requirementKeyInfo.materialWeights.woodRatio === 0 ||
  2411. !this.requirementKeyInfo.presetAtmosphere.name;
  2412. console.log('是否需要初始化数据:', needsInitialization);
  2413. if (needsInitialization) {
  2414. console.log('需求关键信息为空,初始化默认数据');
  2415. // 初始化模拟的需求数据
  2416. this.requirementKeyInfo = {
  2417. colorAtmosphere: {
  2418. description: '现代简约风格,以白色和灰色为主调',
  2419. mainColor: '#F5F5F5',
  2420. colorTemp: '冷色调',
  2421. materials: ['木质', '金属', '玻璃']
  2422. },
  2423. spaceStructure: {
  2424. lineRatio: 0.6,
  2425. blankRatio: 0.4,
  2426. flowWidth: 1.2,
  2427. aspectRatio: 1.8,
  2428. ceilingHeight: 2.8
  2429. },
  2430. materialWeights: {
  2431. fabricRatio: 20,
  2432. woodRatio: 45,
  2433. metalRatio: 25,
  2434. smoothness: 0.7,
  2435. glossiness: 0.3
  2436. },
  2437. presetAtmosphere: {
  2438. name: '现代简约',
  2439. rgb: '#F5F5F5',
  2440. colorTemp: '5000K',
  2441. materials: ['木质', '金属']
  2442. }
  2443. };
  2444. // console.log('初始化后的requirementKeyInfo:', this.requirementKeyInfo);
  2445. } else {
  2446. // console.log('需求关键信息已存在,无需初始化');
  2447. }
  2448. }
  2449. // 获取项目状态文本
  2450. getProjectStatusText(): string {
  2451. const current = (this.currentStage || this.project?.currentStage) as ProjectStage | undefined;
  2452. if (!current || current === '订单分配') {
  2453. return '待开始';
  2454. }
  2455. // 检查是否已完成所有阶段
  2456. const allStages: ProjectStage[] = ['订单分配', '需求沟通', '方案确认', '建模', '软装', '渲染', '尾款结算', '客户评价', '投诉处理'];
  2457. const currentIndex = allStages.indexOf(current);
  2458. if (currentIndex === allStages.length - 1) {
  2459. return '已完成';
  2460. }
  2461. return '进行中';
  2462. }
  2463. // 获取当前阶段文本
  2464. getCurrentStageText(): string {
  2465. const current = (this.currentStage || this.project?.currentStage) as ProjectStage | undefined;
  2466. if (!current || current === '订单分配') {
  2467. return '订单分配阶段';
  2468. }
  2469. return `${current}阶段`;
  2470. }
  2471. // 获取整体进度百分比
  2472. getOverallProgress(): number {
  2473. const current = (this.currentStage || this.project?.currentStage) as ProjectStage | undefined;
  2474. if (!current) {
  2475. return 0;
  2476. }
  2477. // 定义所有阶段及其权重
  2478. const stageWeights: Record<ProjectStage, number> = {
  2479. '订单分配': 5,
  2480. '需求沟通': 15,
  2481. '方案确认': 25,
  2482. '建模': 40,
  2483. '软装': 55,
  2484. '渲染': 70,
  2485. '后期': 80,
  2486. '尾款结算': 90,
  2487. '客户评价': 95,
  2488. '投诉处理': 100
  2489. };
  2490. return stageWeights[current] || 0;
  2491. }
  2492. // 获取必需阶段的完成进度百分比
  2493. getRequiredStagesProgress(): number {
  2494. let completedCount = 0;
  2495. const totalCount = 4; // 四个必需流程
  2496. // 检查各个关键信息是否已确认
  2497. if (this.requirementKeyInfo.colorAtmosphere.description) completedCount++;
  2498. if (this.requirementKeyInfo.spaceStructure.aspectRatio > 0) completedCount++;
  2499. if (this.requirementKeyInfo.materialWeights.woodRatio > 0 ||
  2500. this.requirementKeyInfo.materialWeights.fabricRatio > 0 ||
  2501. this.requirementKeyInfo.materialWeights.metalRatio > 0) completedCount++;
  2502. if (this.requirementKeyInfo.presetAtmosphere.name) completedCount++;
  2503. return Math.round((completedCount / totalCount) * 100);
  2504. }
  2505. // 订单金额
  2506. orderAmount: number = 0;
  2507. // 报价明细
  2508. quotationDetails: Array<{
  2509. id: string;
  2510. room: string;
  2511. amount: number;
  2512. description?: string;
  2513. }> = [];
  2514. // AI生成报价明细
  2515. generateQuotationDetails(): void {
  2516. // 基于项目信息生成报价明细
  2517. const rooms = ['客餐厅', '主卧', '次卧', '厨房', '卫生间'];
  2518. this.quotationDetails = rooms.map((room, index) => ({
  2519. id: `quote_${index + 1}`,
  2520. room: room,
  2521. amount: Math.floor(Math.random() * 1000) + 300, // 示例金额
  2522. description: `${room}装修设计费用`
  2523. }));
  2524. // 更新总订单金额
  2525. this.orderAmount = this.quotationDetails.reduce((total, item) => total + item.amount, 0);
  2526. }
  2527. // 添加报价明细项
  2528. addQuotationItem(): void {
  2529. this.quotationDetails.push({
  2530. id: `quote_${Date.now()}`,
  2531. room: '',
  2532. amount: 0,
  2533. description: ''
  2534. });
  2535. }
  2536. // 删除报价明细项
  2537. removeQuotationItem(id: string): void {
  2538. this.quotationDetails = this.quotationDetails.filter(item => item.id !== id);
  2539. this.updateOrderAmount();
  2540. }
  2541. // 更新订单总金额
  2542. updateOrderAmount(): void {
  2543. this.orderAmount = this.quotationDetails.reduce((total, item) => total + item.amount, 0);
  2544. }
  2545. // 报价组件数据
  2546. quotationData: QuotationData = {
  2547. items: [],
  2548. totalAmount: 0,
  2549. materialCost: 0,
  2550. laborCost: 0,
  2551. designFee: 0,
  2552. managementFee: 0
  2553. };
  2554. // 设计师指派数据
  2555. designerAssignmentData?: DesignerAssignmentData;
  2556. // 设计师日历弹窗状态与数据
  2557. showDesignerCalendar: boolean = false;
  2558. selectedCalendarDate: Date = new Date();
  2559. calendarDesigners: CalendarDesigner[] = [];
  2560. calendarGroups: CalendarProjectGroup[] = [];
  2561. onQuotationDataChange(data: QuotationData): void {
  2562. this.quotationData = { ...data };
  2563. this.orderAmount = data.totalAmount || 0;
  2564. }
  2565. onDesignerAssignmentChange(data: DesignerAssignmentData): void {
  2566. this.designerAssignmentData = { ...data };
  2567. }
  2568. onDesignerClick(designer: AssignmentDesigner): void {
  2569. const mapped = this.mapAssignmentDesignerToCalendar(designer);
  2570. this.calendarDesigners = [mapped];
  2571. this.calendarGroups = [{ id: designer.teamId, name: designer.teamName, leaderId: designer.id, memberIds: [designer.id] }];
  2572. this.selectedCalendarDate = new Date();
  2573. this.showDesignerCalendar = true;
  2574. }
  2575. closeDesignerCalendar(): void {
  2576. this.showDesignerCalendar = false;
  2577. }
  2578. onCalendarDesignerSelected(designer: CalendarDesigner): void {
  2579. this.selectedDesigner = designer;
  2580. this.closeDesignerCalendar();
  2581. }
  2582. onCalendarAssignmentRequested(designer: CalendarDesigner): void {
  2583. this.selectedDesigner = designer;
  2584. this.closeDesignerCalendar();
  2585. }
  2586. private mapAssignmentDesignerToCalendar(d: AssignmentDesigner): CalendarDesigner {
  2587. return {
  2588. id: d.id,
  2589. name: d.name,
  2590. avatar: d.avatar,
  2591. groupId: d.teamId,
  2592. groupName: d.teamName,
  2593. isLeader: !!d.isTeamLeader,
  2594. status: d.status === 'idle' ? 'available' : 'busy',
  2595. currentProjects: Math.max(0, d.recentOrders || 0),
  2596. lastOrderDate: d.lastOrderDate,
  2597. idleDays: Math.max(0, d.idleDays || 0),
  2598. completedThisMonth: Math.max(0, d.recentOrders || 0),
  2599. averageCycle: 15,
  2600. upcomingEvents: (d.reviewDates || []).map((dateStr, idx) => ({
  2601. id: `${d.id}-review-${idx}`,
  2602. date: new Date(dateStr),
  2603. title: '对图评审',
  2604. type: 'review',
  2605. projectId: undefined,
  2606. duration: 2
  2607. })),
  2608. workload: Math.max(0, d.workload || 0),
  2609. nextAvailableDate: new Date()
  2610. };
  2611. }
  2612. // 处理咨询订单表单提交
  2613. // 存储订单分配时的客户信息和需求信息
  2614. onConsultationOrderSubmit(formData: any): void {
  2615. console.log('咨询订单表单提交:', formData);
  2616. // 保存订单分配数据
  2617. this.orderCreationData = formData;
  2618. // 更新projectData以便传递给子组件(集成报价与指派信息)
  2619. this.projectData = {
  2620. customerInfo: formData.customerInfo,
  2621. requirementInfo: formData.requirementInfo,
  2622. preferenceTags: formData.preferenceTags,
  2623. quotation: this.quotationData ? { ...this.quotationData } : undefined,
  2624. assignment: this.designerAssignmentData ? { ...this.designerAssignmentData } : undefined,
  2625. orderAmount: this.quotationData?.totalAmount ?? this.orderAmount ?? 0
  2626. };
  2627. // 实时更新左侧客户信息显示
  2628. this.updateCustomerInfoDisplay(formData);
  2629. // 根据角色上下文处理数据同步
  2630. if (formData.roleContext === 'customer-service') {
  2631. // 客服端创建的订单需要同步到设计师端
  2632. this.syncOrderToDesignerView(formData);
  2633. }
  2634. // 根据报价数据更新订单金额
  2635. this.orderAmount = this.quotationData?.totalAmount ?? this.orderAmount ?? 0;
  2636. this.updateOrderAmount();
  2637. // 触发变更检测以更新UI
  2638. this.cdr.detectChanges();
  2639. }
  2640. // 新增:更新客户信息显示 - 优化后只更新实际存在的字段
  2641. private updateCustomerInfoDisplay(formData: any): void {
  2642. if (formData.customerInfo) {
  2643. // 更新项目对象中的客户信息 - 只更新实际存在的字段
  2644. if (this.project) {
  2645. // 由于已移除客户姓名和手机号字段,只更新微信和客户类型
  2646. if (formData.customerInfo.wechat) {
  2647. this.project.customerWechat = formData.customerInfo.wechat;
  2648. }
  2649. if (formData.customerInfo.customerType) {
  2650. this.project.customerType = formData.customerInfo.customerType;
  2651. }
  2652. if (formData.customerInfo.source) {
  2653. this.project.customerSource = formData.customerInfo.source;
  2654. }
  2655. if (formData.customerInfo.remark) {
  2656. this.project.customerRemark = formData.customerInfo.remark;
  2657. }
  2658. }
  2659. // 更新客户标签
  2660. if (formData.preferenceTags) {
  2661. this.project = {
  2662. ...this.project,
  2663. customerTags: formData.preferenceTags
  2664. } as any;
  2665. }
  2666. // 更新需求信息 - 只更新实际存在的字段
  2667. if (formData.requirementInfo) {
  2668. this.project = {
  2669. ...this.project,
  2670. // 移除已删除的字段:decorationType, firstDraftDate, style, budget, area, houseType
  2671. downPayment: formData.requirementInfo.downPayment,
  2672. smallImageTime: formData.requirementInfo.smallImageTime,
  2673. spaceRequirements: formData.requirementInfo.spaceRequirements,
  2674. designAngles: formData.requirementInfo.designAngles,
  2675. specialAreaHandling: formData.requirementInfo.specialAreaHandling,
  2676. materialRequirements: formData.requirementInfo.materialRequirements,
  2677. lightingRequirements: formData.requirementInfo.lightingRequirements
  2678. } as any;
  2679. }
  2680. console.log('客户信息已实时更新:', this.project);
  2681. }
  2682. }
  2683. // 新增:同步订单数据到设计师视图
  2684. private syncOrderToDesignerView(formData: any): void {
  2685. // 创建项目数据
  2686. const projectData = {
  2687. customerId: formData.customerInfo.id || 'customer-' + Date.now(),
  2688. customerName: formData.customerInfo.name,
  2689. requirement: formData.requirementInfo,
  2690. referenceCases: [],
  2691. tags: {
  2692. demandType: formData.customerInfo.demandType,
  2693. preferenceTags: formData.preferenceTags,
  2694. followUpStatus: formData.customerInfo.followUpStatus
  2695. },
  2696. // 新增:报价与指派信息(可选)
  2697. quotation: this.quotationData ? { ...this.quotationData } : undefined,
  2698. assignment: this.designerAssignmentData ? { ...this.designerAssignmentData } : undefined,
  2699. orderAmount: this.quotationData?.totalAmount ?? this.orderAmount ?? 0
  2700. };
  2701. // 调用项目服务创建项目
  2702. this.projectService.createProject(projectData).subscribe(
  2703. result => {
  2704. if (result.success) {
  2705. console.log('订单数据已同步到设计师端,项目ID:', result.projectId);
  2706. // 可以在这里添加成功提示或其他处理逻辑
  2707. }
  2708. },
  2709. error => {
  2710. console.error('同步订单数据到设计师端失败:', error);
  2711. }
  2712. );
  2713. }
  2714. // 确认团队分配
  2715. confirmTeamAssignment(designer: any): void {
  2716. if (designer) {
  2717. this.selectedDesigner = designer;
  2718. console.log('团队分配确认:', designer);
  2719. // 这里可以添加实际的团队分配逻辑
  2720. // 例如调用服务来分配设计师到项目
  2721. // 进入下一个阶段:需求沟通
  2722. this.updateProjectStage('需求沟通');
  2723. this.expandedStages['需求沟通'] = true;
  2724. this.expandedStages['订单分配'] = false;
  2725. // 显示成功消息
  2726. alert('团队分配成功!已进入需求沟通阶段');
  2727. console.log('团队分配完成,已跳转到需求沟通阶段');
  2728. }
  2729. }
  2730. // 项目创建完成事件处理
  2731. onProjectCreated(projectData: any): void {
  2732. console.log('项目创建完成:', projectData);
  2733. this.projectData = projectData;
  2734. // 团队分配已在子组件中完成并触发该事件:推进到需求沟通阶段
  2735. this.updateProjectStage('需求沟通');
  2736. // 更新项目对象的当前阶段,确保四大板块状态正确显示
  2737. if (this.project) {
  2738. this.project.currentStage = '需求沟通';
  2739. }
  2740. // 展开需求沟通阶段,收起订单分配阶段
  2741. this.expandedStages['需求沟通'] = true;
  2742. this.expandedStages['订单分配'] = false;
  2743. // 自动展开确认需求板块
  2744. this.expandedSection = 'requirements';
  2745. // 强制触发变更检测,确保UI更新
  2746. this.cdr.detectChanges();
  2747. // 延迟滚动到需求沟通阶段,确保DOM更新完成
  2748. setTimeout(() => {
  2749. this.scrollToStage('需求沟通');
  2750. // 再次触发变更检测,确保所有状态都已正确更新
  2751. this.cdr.detectChanges();
  2752. }, 100);
  2753. console.log('项目创建成功,已推进到需求沟通阶段,四大板块状态已更新');
  2754. }
  2755. // 新增:处理实时需求数据更新
  2756. onRequirementDataUpdated(data: any): void {
  2757. console.log('收到需求数据更新:', data);
  2758. // 同步关键信息
  2759. this.syncRequirementKeyInfo(data);
  2760. }
  2761. // 新增:接收需求映射数据更新
  2762. onMappingDataUpdated(mappingData: any): void {
  2763. console.log('🔄 收到需求映射数据更新:', mappingData);
  2764. this.mappingUploadedFiles = mappingData.uploadedFiles || [];
  2765. this.mappingAnalysisResult = mappingData.analysisResult;
  2766. this.mappingRequirementMapping = mappingData.requirementMapping;
  2767. this.mappingTestSteps = mappingData.testSteps || [];
  2768. this.mappingIsAnalyzing = mappingData.isAnalyzing || false;
  2769. this.mappingIsGeneratingMapping = mappingData.isGeneratingMapping || false;
  2770. this.cdr.detectChanges(); // 触发界面更新
  2771. }
  2772. // 继续原有的onRequirementDataUpdated方法内容
  2773. private updateProjectInfoFromRequirementData(data: any): void {
  2774. // 更新客户信息显示
  2775. if (data && this.project) {
  2776. // 更新项目的客户信息
  2777. if (data.colorIndicators && data.colorIndicators.length > 0) {
  2778. this.project.customerInfo = {
  2779. ...this.project.customerInfo,
  2780. colorPreference: data.colorIndicators.map((indicator: any) => indicator.name).join(', ')
  2781. };
  2782. }
  2783. // 更新空间结构信息
  2784. if (data.spaceIndicators && data.spaceIndicators.length > 0) {
  2785. this.project.customerInfo = {
  2786. ...this.project.customerInfo,
  2787. spaceRequirements: data.spaceIndicators.map((indicator: any) => `${indicator.name}: ${indicator.value}`).join(', ')
  2788. };
  2789. }
  2790. // 更新材质偏好
  2791. if (data.materialIndicators && data.materialIndicators.length > 0) {
  2792. this.project.customerInfo = {
  2793. ...this.project.customerInfo,
  2794. materialPreference: data.materialIndicators.map((indicator: any) => `${indicator.name}: ${indicator.value}%`).join(', ')
  2795. };
  2796. }
  2797. // 更新需求项目
  2798. if (data.requirementItems && data.requirementItems.length > 0) {
  2799. this.project.requirements = data.requirementItems.map((item: any) => ({
  2800. id: item.id,
  2801. description: item.description,
  2802. status: item.status,
  2803. priority: item.priority || 'medium'
  2804. }));
  2805. }
  2806. // 接收色彩分析结果并存储用于右侧展示
  2807. if (data.colorAnalysisResult) {
  2808. console.log('接收到色彩分析结果:', data.colorAnalysisResult);
  2809. this.colorAnalysisResult = data.colorAnalysisResult as ColorAnalysisResult;
  2810. console.log('设置colorAnalysisResult后:', this.colorAnalysisResult);
  2811. // 计算主色:按占比最高的颜色
  2812. const colors = this.colorAnalysisResult?.colors || [];
  2813. if (colors.length > 0) {
  2814. const dominant = colors.reduce((max, cur) => cur.percentage > max.percentage ? cur : max, colors[0]);
  2815. this.dominantColorHex = dominant.hex;
  2816. console.log('计算出的主色:', this.dominantColorHex);
  2817. } else {
  2818. this.dominantColorHex = null;
  2819. console.log('没有颜色数据,主色设为null');
  2820. }
  2821. } else {
  2822. console.log('没有接收到色彩分析结果');
  2823. }
  2824. // 新增:处理详细的分析数据
  2825. if (data.detailedAnalysis) {
  2826. console.log('接收到详细分析数据:', data.detailedAnalysis);
  2827. // 存储各类分析结果
  2828. this.enhancedColorAnalysis = data.detailedAnalysis.enhancedColorAnalysis;
  2829. this.formAnalysis = data.detailedAnalysis.formAnalysis;
  2830. this.textureAnalysis = data.detailedAnalysis.textureAnalysis;
  2831. this.patternAnalysis = data.detailedAnalysis.patternAnalysis;
  2832. this.lightingAnalysis = data.detailedAnalysis.lightingAnalysis;
  2833. console.log('详细分析数据已存储:', {
  2834. enhancedColorAnalysis: this.enhancedColorAnalysis,
  2835. formAnalysis: this.formAnalysis,
  2836. textureAnalysis: this.textureAnalysis,
  2837. patternAnalysis: this.patternAnalysis,
  2838. lightingAnalysis: this.lightingAnalysis
  2839. });
  2840. }
  2841. // 新增:处理材料分析数据
  2842. if (data.materialAnalysisData && data.materialAnalysisData.length > 0) {
  2843. console.log('接收到材料分析数据:', data.materialAnalysisData);
  2844. this.materialAnalysisData = data.materialAnalysisData;
  2845. }
  2846. // 新增:根据上传来源拆分材料文件用于左右区展示
  2847. const materials = Array.isArray(data?.materials) ? data.materials : [];
  2848. this.referenceImages = materials.filter((m: any) => m?.type === 'image');
  2849. this.cadFiles = materials.filter((m: any) => m?.type === 'cad');
  2850. // 触发变更检测以更新UI
  2851. this.cdr.detectChanges();
  2852. console.log('客户信息已实时更新,当前colorAnalysisResult状态:', !!this.colorAnalysisResult);
  2853. }
  2854. }
  2855. // 预览右侧色彩分析参考图
  2856. previewColorRefImage(): void {
  2857. const url = this.colorAnalysisResult?.originalImage;
  2858. if (url) {
  2859. window.open(url, '_blank');
  2860. }
  2861. }
  2862. // 新增:点击预览参考图片与CAD文件
  2863. previewImageFile(url?: string): void {
  2864. if (!url) return;
  2865. window.open(url, '_blank');
  2866. }
  2867. previewCadFile(url?: string): void {
  2868. if (!url) return;
  2869. window.open(url, '_blank');
  2870. }
  2871. // 切换客户信息卡片展开状态
  2872. toggleCustomerInfo(): void {
  2873. this.isCustomerInfoExpanded = !this.isCustomerInfoExpanded;
  2874. }
  2875. // 新增:重置方案分析状态的方法
  2876. resetProposalAnalysis(): void {
  2877. this.proposalAnalysis = null;
  2878. this.isAnalyzing = false;
  2879. this.analysisProgress = 0;
  2880. console.log('方案分析状态已重置');
  2881. }
  2882. // 新增:模拟素材解析方法
  2883. startMaterialAnalysis(): void {
  2884. this.isAnalyzing = true;
  2885. this.analysisProgress = 0;
  2886. const progressInterval = setInterval(() => {
  2887. this.analysisProgress += Math.random() * 15;
  2888. if (this.analysisProgress >= 100) {
  2889. this.analysisProgress = 100;
  2890. clearInterval(progressInterval);
  2891. this.completeMaterialAnalysis();
  2892. }
  2893. }, 500);
  2894. }
  2895. // 完成素材解析,生成方案数据
  2896. private completeMaterialAnalysis(): void {
  2897. this.isAnalyzing = false;
  2898. // 生成模拟的方案分析数据
  2899. this.proposalAnalysis = {
  2900. id: 'proposal-' + Date.now(),
  2901. name: '现代简约风格方案',
  2902. version: 'v1.0',
  2903. createdAt: new Date(),
  2904. status: 'completed',
  2905. materials: [
  2906. {
  2907. category: '地面材料',
  2908. specifications: {
  2909. type: '复合木地板',
  2910. grade: 'E0级',
  2911. thickness: '12mm',
  2912. finish: '哑光面',
  2913. durability: '家用33级'
  2914. },
  2915. usage: {
  2916. area: '客厅、卧室',
  2917. percentage: 65,
  2918. priority: 'primary'
  2919. },
  2920. properties: {
  2921. texture: '木纹理',
  2922. color: '浅橡木色',
  2923. maintenance: '日常清洁'
  2924. }
  2925. },
  2926. {
  2927. category: '墙面材料',
  2928. specifications: {
  2929. type: '乳胶漆',
  2930. grade: '净味抗甲醛',
  2931. finish: '丝光面',
  2932. durability: '15年'
  2933. },
  2934. usage: {
  2935. area: '全屋墙面',
  2936. percentage: 80,
  2937. priority: 'primary'
  2938. },
  2939. properties: {
  2940. texture: '平滑',
  2941. color: '暖白色',
  2942. maintenance: '可擦洗'
  2943. }
  2944. },
  2945. {
  2946. category: '软装面料',
  2947. specifications: {
  2948. type: '亚麻混纺',
  2949. grade: 'A级',
  2950. finish: '防污处理',
  2951. durability: '5-8年'
  2952. },
  2953. usage: {
  2954. area: '沙发、窗帘',
  2955. percentage: 25,
  2956. priority: 'secondary'
  2957. },
  2958. properties: {
  2959. texture: '自然纹理',
  2960. color: '米灰色系',
  2961. maintenance: '干洗'
  2962. }
  2963. }
  2964. ],
  2965. designStyle: {
  2966. primaryStyle: '现代简约',
  2967. styleElements: [
  2968. {
  2969. element: '线条设计',
  2970. description: '简洁流畅的直线条,避免繁复装饰',
  2971. influence: 85
  2972. },
  2973. {
  2974. element: '色彩搭配',
  2975. description: '以中性色为主,局部点缀暖色',
  2976. influence: 75
  2977. },
  2978. {
  2979. element: '材质选择',
  2980. description: '天然材质与现代工艺结合',
  2981. influence: 70
  2982. }
  2983. ],
  2984. characteristics: [
  2985. {
  2986. feature: '空间感',
  2987. value: '开放通透',
  2988. importance: 'high'
  2989. },
  2990. {
  2991. feature: '功能性',
  2992. value: '实用至上',
  2993. importance: 'high'
  2994. },
  2995. {
  2996. feature: '装饰性',
  2997. value: '简约精致',
  2998. importance: 'medium'
  2999. }
  3000. ],
  3001. compatibility: {
  3002. withMaterials: ['木材', '金属', '玻璃', '石材'],
  3003. withColors: ['白色', '灰色', '米色', '原木色'],
  3004. score: 92
  3005. }
  3006. },
  3007. colorScheme: {
  3008. palette: [
  3009. {
  3010. color: '暖白色',
  3011. hex: '#F8F6F0',
  3012. rgb: '248, 246, 240',
  3013. percentage: 45,
  3014. role: 'dominant'
  3015. },
  3016. {
  3017. color: '浅灰色',
  3018. hex: '#E5E5E5',
  3019. rgb: '229, 229, 229',
  3020. percentage: 30,
  3021. role: 'secondary'
  3022. },
  3023. {
  3024. color: '原木色',
  3025. hex: '#D4A574',
  3026. rgb: '212, 165, 116',
  3027. percentage: 20,
  3028. role: 'accent'
  3029. },
  3030. {
  3031. color: '深灰色',
  3032. hex: '#4A4A4A',
  3033. rgb: '74, 74, 74',
  3034. percentage: 5,
  3035. role: 'neutral'
  3036. }
  3037. ],
  3038. harmony: {
  3039. type: '类似色调和',
  3040. temperature: 'warm',
  3041. contrast: 65
  3042. },
  3043. psychology: {
  3044. mood: '宁静舒适',
  3045. atmosphere: '温馨自然',
  3046. suitability: ['居住', '办公', '休闲']
  3047. }
  3048. },
  3049. spaceLayout: {
  3050. dimensions: {
  3051. length: 12.5,
  3052. width: 8.2,
  3053. height: 2.8,
  3054. area: 102.5,
  3055. volume: 287
  3056. },
  3057. functionalZones: [
  3058. {
  3059. zone: '客厅区域',
  3060. area: 35.2,
  3061. percentage: 34.3,
  3062. requirements: ['会客', '娱乐', '休息'],
  3063. furniture: ['沙发', '茶几', '电视柜', '边几']
  3064. },
  3065. {
  3066. zone: '餐厅区域',
  3067. area: 18.5,
  3068. percentage: 18.0,
  3069. requirements: ['用餐', '储物'],
  3070. furniture: ['餐桌', '餐椅', '餐边柜']
  3071. },
  3072. {
  3073. zone: '厨房区域',
  3074. area: 12.8,
  3075. percentage: 12.5,
  3076. requirements: ['烹饪', '储存', '清洁'],
  3077. furniture: ['橱柜', '岛台', '吧台椅']
  3078. },
  3079. {
  3080. zone: '主卧区域',
  3081. area: 25.6,
  3082. percentage: 25.0,
  3083. requirements: ['睡眠', '储衣', '梳妆'],
  3084. furniture: ['床', '衣柜', '梳妆台', '床头柜']
  3085. },
  3086. {
  3087. zone: '次卧区域',
  3088. area: 10.4,
  3089. percentage: 10.2,
  3090. requirements: ['睡眠', '学习'],
  3091. furniture: ['床', '书桌', '衣柜']
  3092. }
  3093. ],
  3094. circulation: {
  3095. mainPaths: ['入户-客厅', '客厅-餐厅', '餐厅-厨房', '客厅-卧室'],
  3096. pathWidth: 1.2,
  3097. efficiency: 88
  3098. },
  3099. lighting: {
  3100. natural: {
  3101. direction: ['南向', '东向'],
  3102. intensity: '充足',
  3103. duration: '8-10小时'
  3104. },
  3105. artificial: {
  3106. zones: ['主照明', '局部照明', '装饰照明'],
  3107. requirements: ['无主灯设计', '分层控制', '调光调色']
  3108. }
  3109. }
  3110. },
  3111. budget: {
  3112. total: 285000,
  3113. breakdown: [
  3114. {
  3115. category: '基础装修',
  3116. amount: 142500,
  3117. percentage: 50
  3118. },
  3119. {
  3120. category: '主材采购',
  3121. amount: 85500,
  3122. percentage: 30
  3123. },
  3124. {
  3125. category: '软装配饰',
  3126. amount: 42750,
  3127. percentage: 15
  3128. },
  3129. {
  3130. category: '设计费用',
  3131. amount: 14250,
  3132. percentage: 5
  3133. }
  3134. ]
  3135. },
  3136. timeline: [
  3137. {
  3138. phase: '设计深化',
  3139. duration: 7,
  3140. dependencies: ['需求确认']
  3141. },
  3142. {
  3143. phase: '材料采购',
  3144. duration: 5,
  3145. dependencies: ['设计深化']
  3146. },
  3147. {
  3148. phase: '基础施工',
  3149. duration: 30,
  3150. dependencies: ['材料采购']
  3151. },
  3152. {
  3153. phase: '软装进场',
  3154. duration: 7,
  3155. dependencies: ['基础施工']
  3156. },
  3157. {
  3158. phase: '验收交付',
  3159. duration: 3,
  3160. dependencies: ['软装进场']
  3161. }
  3162. ],
  3163. feasibility: {
  3164. technical: 95,
  3165. budget: 88,
  3166. timeline: 92,
  3167. overall: 92
  3168. }
  3169. };
  3170. }
  3171. // 获取材质分类统计
  3172. getMaterialCategories(): { category: string; count: number; percentage: number }[] {
  3173. if (!this.proposalAnalysis) return [];
  3174. const categories = this.proposalAnalysis.materials.reduce((acc, material) => {
  3175. acc[material.category] = (acc[material.category] || 0) + 1;
  3176. return acc;
  3177. }, {} as Record<string, number>);
  3178. const total = Object.values(categories).reduce((sum, count) => sum + count, 0);
  3179. return Object.entries(categories).map(([category, count]) => ({
  3180. category,
  3181. count,
  3182. percentage: Math.round((count / total) * 100)
  3183. }));
  3184. }
  3185. // 获取设计风格特征摘要
  3186. getStyleSummary(): string {
  3187. if (!this.proposalAnalysis) return '';
  3188. const style = this.proposalAnalysis.designStyle;
  3189. const topElements = style.styleElements
  3190. .sort((a, b) => b.influence - a.influence)
  3191. .slice(0, 2)
  3192. .map(el => el.element)
  3193. .join('、');
  3194. return `${style.primaryStyle}风格,主要体现在${topElements}等方面`;
  3195. }
  3196. // 获取色彩方案摘要
  3197. getColorSummary(): string {
  3198. if (!this.proposalAnalysis) return '';
  3199. const scheme = this.proposalAnalysis.colorScheme;
  3200. const dominantColor = scheme.palette.find(p => p.role === 'dominant')?.color || '';
  3201. const accentColor = scheme.palette.find(p => p.role === 'accent')?.color || '';
  3202. return `以${dominantColor}为主调,${accentColor}作点缀,营造${scheme.psychology.mood}的氛围`;
  3203. }
  3204. // 获取空间效率评分
  3205. getSpaceEfficiency(): number {
  3206. if (!this.proposalAnalysis) return 0;
  3207. return this.proposalAnalysis.spaceLayout.circulation.efficiency;
  3208. }
  3209. private handlePaymentProofUpload(file: File): void {
  3210. // 显示上传进度
  3211. const uploadingMessage = `正在上传支付凭证:${file.name}...`;
  3212. console.log(uploadingMessage);
  3213. // 使用支付凭证识别服务处理上传
  3214. const settlementId = this.project?.id || 'default_settlement';
  3215. this.paymentVoucherService.processPaymentVoucherUpload(file, settlementId).subscribe({
  3216. next: (result) => {
  3217. if (result.success && result.recognitionResult) {
  3218. const recognition = result.recognitionResult;
  3219. // 更新识别计数
  3220. this.voucherRecognitionCount++;
  3221. // 显示识别结果
  3222. const successMessage = `
  3223. 支付凭证识别成功!
  3224. 支付方式:${recognition.paymentMethod}
  3225. 支付金额:¥${recognition.amount}
  3226. 交易号:${recognition.transactionNumber}
  3227. 置信度:${(recognition.confidence * 100).toFixed(1)}%
  3228. `;
  3229. alert(successMessage);
  3230. console.log('支付凭证识别完成', recognition);
  3231. // 自动标记验证通过并解锁渲染大图
  3232. this.isPaymentVerified = true;
  3233. this.renderLargeImages = this.renderLargeImages.map(img => ({ ...img, locked: false }));
  3234. // 触发自动通知流程
  3235. this.triggerPaymentCompletedNotification(recognition);
  3236. } else {
  3237. const errorMessage = `支付凭证识别失败:${result.error || '未知错误'}`;
  3238. alert(errorMessage);
  3239. console.error('支付凭证识别失败', result.error);
  3240. }
  3241. },
  3242. error: (error) => {
  3243. const errorMessage = `支付凭证处理出错:${error.message || '网络错误'}`;
  3244. alert(errorMessage);
  3245. console.error('支付凭证处理出错', error);
  3246. }
  3247. });
  3248. }
  3249. /**
  3250. * 触发支付完成通知流程
  3251. */
  3252. private triggerPaymentCompletedNotification(recognition: any): void {
  3253. console.log('触发支付完成自动通知流程...');
  3254. // 模拟调用通知服务发送多渠道通知
  3255. this.sendMultiChannelNotifications(recognition);
  3256. // 模拟发送通知
  3257. setTimeout(() => {
  3258. const notificationMessage = `
  3259. 🎉 尾款已到账,大图已解锁!
  3260. 支付信息:
  3261. • 支付方式:${recognition.paymentMethod}
  3262. • 支付金额:¥${recognition.amount}
  3263. • 处理时间:${new Date().toLocaleString()}
  3264. 📱 系统已自动发送通知至:
  3265. • 短信通知:138****8888
  3266. • 微信通知:已推送至微信
  3267. • 邮件通知:customer@example.com
  3268. 🖼️ 高清渲染图下载链接已发送
  3269. 您现在可以下载4K高清渲染图了!
  3270. `;
  3271. alert(notificationMessage);
  3272. console.log('自动通知发送完成');
  3273. }, 1000);
  3274. }
  3275. /**
  3276. * 发送多渠道通知
  3277. */
  3278. private sendMultiChannelNotifications(recognition: any): void {
  3279. console.log('开始发送多渠道通知...');
  3280. // 更新通知发送计数
  3281. this.notificationsSent++;
  3282. // 模拟发送短信通知
  3283. console.log('📱 发送短信通知: 尾款已到账,大图已解锁');
  3284. // 模拟发送微信通知
  3285. console.log('💬 发送微信通知: 支付成功,高清图片已准备就绪');
  3286. // 模拟发送邮件通知
  3287. console.log('📧 发送邮件通知: 包含下载链接的详细通知');
  3288. // 模拟发送应用内通知
  3289. console.log('🔔 发送应用内通知: 实时推送支付状态更新');
  3290. // 模拟通知发送结果
  3291. setTimeout(() => {
  3292. console.log('✅ 所有渠道通知发送完成');
  3293. console.log(`通知发送统计: 短信✅ 微信✅ 邮件✅ 应用内✅ (总计: ${this.notificationsSent} 次)`);
  3294. }, 500);
  3295. }
  3296. // 获取当前设计师名称
  3297. getCurrentDesignerName(): string {
  3298. // 这里应该从用户服务或认证信息中获取当前用户名称
  3299. // 暂时返回一个默认值
  3300. return '张设计师';
  3301. }
  3302. // ==================== 售后相关变量 ====================
  3303. // 售后标签页控制
  3304. activeAftercareTab: string = 'services'; // 当前激活的售后标签页
  3305. // 售后状态管理
  3306. afterSalesStage: string = '未开始'; // 售后阶段:未开始、进行中、已完成
  3307. afterSalesStatus: 'pending' | 'in_progress' | 'completed' | 'cancelled' = 'pending'; // 售后状态
  3308. afterSalesProgress: number = 0; // 售后进度百分比
  3309. // 售后服务数据
  3310. afterSalesServices: Array<{
  3311. id: string;
  3312. type: string; // 服务类型:维修、保养、咨询、投诉处理
  3313. description: string;
  3314. assignedTo: string; // 指派给的人员
  3315. scheduledDate?: Date; // 计划服务日期
  3316. completedDate?: Date; // 完成日期
  3317. status: 'pending' | 'scheduled' | 'in_progress' | 'completed' | 'cancelled';
  3318. priority: 'low' | 'medium' | 'high';
  3319. }> = [];
  3320. // 售后评价和反馈
  3321. afterSalesRating: number = 0; // 售后评分(0-5)
  3322. afterSalesFeedback: string = ''; // 售后反馈内容
  3323. afterSalesFeedbackDate?: Date; // 反馈日期
  3324. // 售后时间跟踪
  3325. afterSalesResponseTime?: Date; // 首次响应时间
  3326. afterSalesStartTime?: Date; // 售后开始时间
  3327. afterSalesCompletionTime?: Date; // 售后完成时间
  3328. afterSalesTotalDuration: number = 0; // 总耗时(小时)
  3329. // 售后联系人信息
  3330. afterSalesContact: {
  3331. name: string;
  3332. phone: string;
  3333. wechat?: string;
  3334. email?: string;
  3335. } = {
  3336. name: '',
  3337. phone: ''
  3338. };
  3339. // 售后问题记录
  3340. afterSalesIssues: Array<{
  3341. id: string;
  3342. title: string;
  3343. description: string;
  3344. severity: 'low' | 'medium' | 'high' | 'critical';
  3345. reportedDate: Date;
  3346. resolvedDate?: Date;
  3347. status: 'reported' | 'investigating' | 'resolved' | 'closed';
  3348. }> = [];
  3349. // 售后文件记录
  3350. afterSalesDocuments: Array<{
  3351. id: string;
  3352. name: string;
  3353. type: string; // 文件类型:合同、报告、照片、视频
  3354. uploadDate: Date;
  3355. url: string;
  3356. }> = [];
  3357. // 售后费用记录
  3358. afterSalesCosts: Array<{
  3359. id: string;
  3360. description: string;
  3361. amount: number;
  3362. category: 'material' | 'labor' | 'transportation' | 'other';
  3363. date: Date;
  3364. status: 'pending' | 'approved' | 'paid' | 'rejected';
  3365. }> = [];
  3366. // 售后沟通记录
  3367. afterSalesCommunications: Array<{
  3368. id: string;
  3369. type: 'phone' | 'wechat' | 'email' | 'visit';
  3370. date: Date;
  3371. summary: string;
  3372. participants: string[];
  3373. }> = [];
  3374. // 新增:项目复盘相关属性
  3375. projectReview: ProjectReview | null = null;
  3376. isGeneratingReview: boolean = false;
  3377. // 项目复盘 Tab 切换
  3378. activeReviewTab: 'sop' | 'experience' | 'suggestions' = 'sop';
  3379. // SOP执行数据
  3380. sopMetrics: any = {
  3381. communicationCount: 6,
  3382. avgCommunication: 5,
  3383. revisionCount: 3,
  3384. avgRevision: 2,
  3385. deliveryCycle: 18,
  3386. standardCycle: 15,
  3387. customerSatisfaction: 4.5
  3388. };
  3389. sopStagesData: any[] = [
  3390. { name: '订单分配', plannedDuration: 1, actualDuration: 1, score: 95, status: 'completed', statusText: '已完成', isDelayed: false, issues: [] },
  3391. { name: '需求沟通', plannedDuration: 3, actualDuration: 4, score: 75, status: 'completed', statusText: '已完成', isDelayed: true, issues: ['沟通次数超标'] },
  3392. { name: '方案确认', plannedDuration: 2, actualDuration: 2, score: 90, status: 'completed', statusText: '已完成', isDelayed: false, issues: [] },
  3393. { name: '建模', plannedDuration: 4, actualDuration: 5, score: 80, status: 'completed', statusText: '已完成', isDelayed: true, issues: ['细节调整耗时'] },
  3394. { name: '软装', plannedDuration: 2, actualDuration: 2, score: 92, status: 'completed', statusText: '已完成', isDelayed: false, issues: [] },
  3395. { name: '渲染', plannedDuration: 3, actualDuration: 4, score: 85, status: 'ongoing', statusText: '进行中', isDelayed: false, issues: [] }
  3396. ];
  3397. // 经验复盘数据
  3398. experienceData: any = {
  3399. customerNeeds: [
  3400. { text: '希望客厅采用现代简约风格,注重收纳功能', timestamp: '2024-01-15 10:30', source: '初次沟通' },
  3401. { text: '卧室需要温馨氛围,色调以暖色为主', timestamp: '2024-01-16 14:20', source: '需求确认' },
  3402. { text: '厨房要求实用性强,采用白色橱柜', timestamp: '2024-01-17 09:15', source: '细节讨论' }
  3403. ],
  3404. customerConcerns: [
  3405. { text: '担心渲染图与实际效果有差异', timestamp: '2024-01-18 16:40', resolved: true },
  3406. { text: '预算控制在合理范围内', timestamp: '2024-01-19 11:25', resolved: true }
  3407. ],
  3408. complaintPoints: [
  3409. { text: '首版方案色彩搭配不符合预期', timestamp: '2024-01-20 15:10', severity: 'medium', severityText: '中等', resolution: '已调整为客户偏好的配色方案' }
  3410. ],
  3411. projectHighlights: [
  3412. { text: '设计师对空间的功能分区把握精准', category: '设计亮点', praised: true },
  3413. { text: '材质选择符合客户品味且性价比高', category: '材质选择', praised: true },
  3414. { text: '渲染效果图质量优秀,客户非常满意', category: '技术表现', praised: true }
  3415. ],
  3416. communications: [
  3417. { timestamp: '2024-01-15 10:30', participant: '客户', type: 'requirement', typeText: '需求提出', message: '希望整体风格简约现代,注重实用性...', attachments: [] },
  3418. { timestamp: '2024-01-16 09:00', participant: '设计师', type: 'response', typeText: '设计反馈', message: '根据您的需求,我们建议采用...', attachments: ['方案草图.jpg'] },
  3419. { timestamp: '2024-01-20 14:30', participant: '客户', type: 'concern', typeText: '疑虑表达', message: '对配色方案有些疑虑...', attachments: [] }
  3420. ]
  3421. };
  3422. // 优化建议数据
  3423. optimizationSuggestions: any[] = [
  3424. {
  3425. priority: 'high',
  3426. priorityText: '高优先级',
  3427. category: '需求沟通',
  3428. expectedImprovement: '减少30%改图次数',
  3429. problem: '该项目因需求理解不够深入导致改图3次,超出平均水平',
  3430. dataPoints: [
  3431. { label: '实际改图次数', value: '3次', isWarning: true },
  3432. { label: '平均改图次数', value: '2次', isWarning: false },
  3433. { label: '超出比例', value: '+50%', isWarning: true }
  3434. ],
  3435. solution: '建议在需求沟通阶段增加确认环节,通过详细的需求确认清单和参考案例,确保设计师完全理解客户需求',
  3436. actionPlan: [
  3437. '制定标准化的需求确认清单,覆盖风格、色彩、材质、功能等核心要素',
  3438. '在方案设计前与客户进行一次需求复核会议',
  3439. '准备3-5个同类型案例供客户参考,明确设计方向',
  3440. '使用可视化工具(如情绪板)帮助客户表达偏好'
  3441. ],
  3442. references: ['项目A-123', '项目B-456']
  3443. },
  3444. {
  3445. priority: 'medium',
  3446. priorityText: '中优先级',
  3447. category: '时间管理',
  3448. expectedImprovement: '缩短15%交付周期',
  3449. problem: '项目交付周期为18天,超出标准周期15天',
  3450. dataPoints: [
  3451. { label: '实际周期', value: '18天', isWarning: true },
  3452. { label: '标准周期', value: '15天', isWarning: false },
  3453. { label: '延期', value: '+3天', isWarning: true }
  3454. ],
  3455. solution: '优化建模和渲染阶段的时间分配,采用并行工作模式提高效率',
  3456. actionPlan: [
  3457. '建模和软装选择可以部分并行进行',
  3458. '提前准备常用材质库,减少临时查找时间',
  3459. '设置阶段里程碑提醒,避免单一阶段耗时过长'
  3460. ],
  3461. references: ['时间优化案例-001']
  3462. },
  3463. {
  3464. priority: 'low',
  3465. priorityText: '低优先级',
  3466. category: '客户体验',
  3467. expectedImprovement: '提升客户满意度至4.8分',
  3468. problem: '当前客户满意度为4.5分,仍有提升空间',
  3469. dataPoints: [
  3470. { label: '当前满意度', value: '4.5/5', isWarning: false },
  3471. { label: '目标满意度', value: '4.8/5', isWarning: false }
  3472. ],
  3473. solution: '增加交付过程中的沟通频率,及时展示阶段性成果',
  3474. actionPlan: [
  3475. '每个关键节点完成后主动向客户汇报进度',
  3476. '提供阶段性预览图,让客户参与到创作过程中',
  3477. '建立客户反馈快速响应机制'
  3478. ],
  3479. references: []
  3480. }
  3481. ];
  3482. switchAftercareTab(tab: string): void {
  3483. this.activeAftercareTab = tab;
  3484. console.log('切换到售后标签页:', tab);
  3485. }
  3486. // ==================== 自动结算相关 ====================
  3487. // 检查项目是否已完成验收
  3488. isProjectAccepted(): boolean {
  3489. // 检查所有交付阶段是否完成
  3490. const deliveryStages: ProjectStage[] = ['建模', '软装', '渲染', '后期'];
  3491. return deliveryStages.every(stage => this.getStageStatus(stage) === 'completed');
  3492. }
  3493. // 启动自动结算(只有技术人员可触发)
  3494. initiateAutoSettlement(): void {
  3495. if (this.isAutoSettling) return;
  3496. // 权限检查
  3497. if (!this.isTechnicalView()) {
  3498. alert('⚠️ 仅技术人员可以启动自动化结算流程');
  3499. return;
  3500. }
  3501. // 验收状态检查
  3502. if (!this.isProjectAccepted()) {
  3503. const confirmStart = confirm('项目尚未完成全部验收,确定要启动结算流程吗?');
  3504. if (!confirmStart) return;
  3505. }
  3506. this.isAutoSettling = true;
  3507. console.log('启动自动化结算流程...');
  3508. // 模拟启动各个自动化功能
  3509. setTimeout(() => {
  3510. // 1. 启动小程序支付监听
  3511. this.miniprogramPaymentStatus = 'active';
  3512. this.isSettlementInitiated = true;
  3513. console.log('✅ 自动化结算已启动');
  3514. console.log('🟢 小程序支付监听已激活');
  3515. console.log('🔍 支付凭证智能识别已就绪');
  3516. console.log('📱 自动通知系统已就绪');
  3517. // 2. 自动生成尾款结算记录
  3518. this.createFinalPaymentRecord();
  3519. // 3. 通知客服跟进尾款
  3520. this.notifyCustomerServiceForFinalPayment();
  3521. // 4. 设置支付监听和自动化流程
  3522. this.setupPaymentAutomation();
  3523. this.isAutoSettling = false;
  3524. // 显示启动成功消息
  3525. alert(`🚀 自动化结算已成功启动!
  3526. ✅ 已启动功能:
  3527. • 小程序支付自动监听
  3528. • 支付凭证智能识别
  3529. • 多渠道自动通知
  3530. • 大图自动解锁
  3531. • 已通知客服跟进尾款
  3532. 系统将自动处理后续支付流程。`);
  3533. }, 2000);
  3534. }
  3535. // 创建尾款结算记录
  3536. private createFinalPaymentRecord(): void {
  3537. const finalPaymentRecord: Settlement = {
  3538. id: `settlement_${Date.now()}`,
  3539. projectId: this.projectId,
  3540. projectName: this.project?.name || '',
  3541. type: 'final_payment',
  3542. amount: this.project?.finalPaymentAmount || 0,
  3543. percentage: 30,
  3544. status: 'pending',
  3545. createdAt: new Date(),
  3546. initiatedBy: 'technical',
  3547. initiatedAt: new Date(),
  3548. notifiedCustomerService: false,
  3549. paymentReceived: false,
  3550. imagesUnlocked: false
  3551. };
  3552. // 添加到结算列表
  3553. if (!this.settlements.find(s => s.type === 'final_payment')) {
  3554. this.settlements.unshift(finalPaymentRecord);
  3555. }
  3556. console.log('📝 已创建尾款结算记录:', finalPaymentRecord);
  3557. }
  3558. // 通知客服跟进尾款
  3559. private notifyCustomerServiceForFinalPayment(): void {
  3560. const projectInfo = {
  3561. projectId: this.projectId,
  3562. projectName: this.project?.name || '未知项目',
  3563. customerName: this.project?.customerName || '未知客户',
  3564. customerPhone: this.project?.customerPhone || '',
  3565. finalPaymentAmount: this.project?.finalPaymentAmount || 0,
  3566. notificationTime: new Date(),
  3567. status: 'pending_followup',
  3568. priority: 'high',
  3569. autoGenerated: true,
  3570. message: `项目【${this.project?.name}】已完成技术验收,请及时跟进客户尾款支付。`
  3571. };
  3572. // 发送通知到客服系统
  3573. console.log('📢 正在通知客服跟进尾款...', projectInfo);
  3574. // 模拟API调用到客服通知系统
  3575. // this.customerServiceAPI.addPendingTask(projectInfo).subscribe(...)
  3576. setTimeout(() => {
  3577. console.log('✅ 客服通知已发送成功');
  3578. // 更新结算记录状态
  3579. const settlement = this.settlements.find(s => s.type === 'final_payment');
  3580. if (settlement) {
  3581. settlement.notifiedCustomerService = true;
  3582. }
  3583. }, 500);
  3584. }
  3585. // 设置支付自动化流程
  3586. private setupPaymentAutomation(): void {
  3587. console.log('⚙️ 设置支付自动化监听...');
  3588. // 模拟支付监听(实际应使用WebSocket或轮询)
  3589. // 这里仅作演示
  3590. this.monitorPaymentStatus();
  3591. }
  3592. // 监听支付状态
  3593. private monitorPaymentStatus(): void {
  3594. // 实际应该使用WebSocket连接或定时轮询API
  3595. // 这里仅作演示用setTimeout模拟
  3596. console.log('👀 开始监听支付状态...');
  3597. // 当检测到支付完成时,自动触发后续流程
  3598. // this.onPaymentReceived();
  3599. }
  3600. // 支付到账回调
  3601. onPaymentReceived(paymentInfo?: any): void {
  3602. console.log('💰 检测到支付到账:', paymentInfo);
  3603. const settlement = this.settlements.find(s => s.type === 'final_payment');
  3604. if (!settlement) return;
  3605. // 更新结算状态
  3606. settlement.status = '已结算';
  3607. settlement.settledAt = new Date();
  3608. settlement.paymentReceived = true;
  3609. settlement.paymentReceivedAt = new Date();
  3610. // 自动解锁并发送大图
  3611. this.autoUnlockAndSendImages();
  3612. // 通知客户和客服
  3613. this.sendPaymentConfirmationNotifications();
  3614. }
  3615. // 自动解锁并发送大图
  3616. private autoUnlockAndSendImages(): void {
  3617. console.log('🔓 自动解锁大图...');
  3618. // 更新结算记录
  3619. const settlement = this.settlements.find(s => s.type === 'final_payment');
  3620. if (settlement) {
  3621. settlement.imagesUnlocked = true;
  3622. settlement.imagesUnlockedAt = new Date();
  3623. }
  3624. // 自动发送大图给客户(通过客服)
  3625. this.autoSendImagesToCustomer();
  3626. console.log('✅ 大图已解锁并准备发送');
  3627. }
  3628. // 自动发送图片给客户
  3629. private autoSendImagesToCustomer(): void {
  3630. console.log('📤 自动发送大图给客户...');
  3631. // 收集所有渲染大图
  3632. const renderProcess = this.deliveryProcesses.find(p => p.id === 'rendering');
  3633. const images: string[] = [];
  3634. if (renderProcess) {
  3635. Object.keys(renderProcess.content).forEach(spaceId => {
  3636. const content = renderProcess.content[spaceId];
  3637. if (content.images) {
  3638. content.images.forEach(img => {
  3639. images.push(img.url);
  3640. });
  3641. }
  3642. });
  3643. }
  3644. const sendInfo = {
  3645. projectId: this.projectId,
  3646. customerName: this.project?.customerName || '',
  3647. images: images,
  3648. sendMethod: 'wechat',
  3649. autoGenerated: true
  3650. };
  3651. console.log('📨 准备发送的大图信息:', sendInfo);
  3652. // 调用客服系统API一键发图
  3653. // this.customerServiceAPI.sendImagesToCustomer(sendInfo).subscribe(...)
  3654. }
  3655. // 发送支付确认通知
  3656. private sendPaymentConfirmationNotifications(): void {
  3657. console.log('📱 发送支付确认通知...');
  3658. // 通知客户
  3659. const customerMessage = `尊敬的${this.project?.customerName || '客户'},您的尾款支付已确认,大图已自动解锁并发送,请查收。感谢您的信任!`;
  3660. // 通知客服
  3661. const csMessage = `项目【${this.project?.name}】尾款已到账,大图已自动解锁,请一键发送给客户。`;
  3662. console.log('📧 客户通知:', customerMessage);
  3663. console.log('📧 客服通知:', csMessage);
  3664. // 实际发送通知
  3665. // this.notificationService.send({ to: 'customer', message: customerMessage });
  3666. // this.notificationService.send({ to: 'customer_service', message: csMessage });
  3667. }
  3668. // ==================== 全景图合成相关 ====================
  3669. // 全景图合成数据
  3670. panoramicSyntheses: PanoramicSynthesis[] = [];
  3671. isUploadingPanoramicImages: boolean = false;
  3672. panoramicUploadProgress: number = 0;
  3673. // 启动全景图合成流程
  3674. // 上传支付凭证
  3675. uploadPaymentProof(): void {
  3676. console.log('📎 打开支付凭证上传...');
  3677. const fileInput = document.createElement('input');
  3678. fileInput.type = 'file';
  3679. fileInput.accept = 'image/*';
  3680. fileInput.onchange = (event: any) => {
  3681. const file = (event.target as HTMLInputElement).files?.[0];
  3682. if (!file) return;
  3683. console.log('📄 上传的凭证文件:', file.name);
  3684. alert(`📎 支付凭证已上传:${file.name}\n\n系统将自动识别支付金额和支付方式。`);
  3685. // 模拟凭证识别和处理
  3686. setTimeout(() => {
  3687. const mockPaymentInfo = {
  3688. amount: this.project?.finalPaymentAmount || 5000,
  3689. method: '微信支付',
  3690. imageUrl: URL.createObjectURL(file),
  3691. uploadTime: new Date()
  3692. };
  3693. console.log('✅ 支付凭证识别完成:', mockPaymentInfo);
  3694. this.onPaymentReceived(mockPaymentInfo);
  3695. }, 1500);
  3696. };
  3697. fileInput.click();
  3698. }
  3699. startPanoramicSynthesis(): void {
  3700. console.log('🎨 启动全景图合成...');
  3701. // 显示提示信息
  3702. alert('📸 请选择需要合成全景图的图片\n\n提示:\n1. 建议选择同一空间的多个角度照片\n2. 图片文件名可以包含空间名称(如:客厅-角度1.jpg)\n3. 系统会自动识别并分类');
  3703. // 打开文件选择对话框,支持多文件选择
  3704. const fileInput = document.createElement('input');
  3705. fileInput.type = 'file';
  3706. fileInput.accept = 'image/*';
  3707. fileInput.multiple = true;
  3708. fileInput.onchange = (event: any) => {
  3709. const files = Array.from(event.target.files || []) as File[];
  3710. if (files.length === 0) return;
  3711. console.log(`📸 选择了 ${files.length} 张图片进行合成`);
  3712. this.processPanoramicImages(files);
  3713. };
  3714. fileInput.click();
  3715. }
  3716. // 处理全景图片上传和合成
  3717. private processPanoramicImages(files: File[]): void {
  3718. this.isUploadingPanoramicImages = true;
  3719. this.panoramicUploadProgress = 0;
  3720. console.log(`📸 开始处理 ${files.length} 张全景图片...`);
  3721. // 模拟上传进度
  3722. const uploadInterval = setInterval(() => {
  3723. this.panoramicUploadProgress += 10;
  3724. if (this.panoramicUploadProgress >= 100) {
  3725. clearInterval(uploadInterval);
  3726. this.panoramicUploadProgress = 100;
  3727. // 上传完成后开始合成
  3728. this.synthesizePanoramicView(files);
  3729. }
  3730. }, 300);
  3731. }
  3732. // 合成全景漫游
  3733. private synthesizePanoramicView(files: File[]): void {
  3734. console.log('🔄 开始合成全景漫游...');
  3735. // 创建合成记录
  3736. const synthesis: PanoramicSynthesis = {
  3737. id: 'panoramic-' + Date.now(),
  3738. projectId: this.projectId,
  3739. projectName: this.project?.name || '未知项目',
  3740. spaces: [],
  3741. status: 'processing',
  3742. quality: 'high',
  3743. createdAt: new Date(),
  3744. updatedAt: new Date(),
  3745. progress: 0
  3746. };
  3747. // 根据上传的文件创建空间列表
  3748. files.forEach((file, index) => {
  3749. // 从文件名提取空间名称(如"客厅-角度1.jpg")
  3750. const fileName = file.name.replace(/\.(jpg|jpeg|png|gif)$/i, '');
  3751. const match = fileName.match(/^(.+?)-/);
  3752. const spaceName = match ? match[1] : `空间${index + 1}`;
  3753. // 根据空间名称推断类型
  3754. let spaceType: 'living_room' | 'bedroom' | 'kitchen' | 'bathroom' | 'dining_room' | 'study' | 'balcony' = 'living_room';
  3755. if (spaceName.includes('客厅')) spaceType = 'living_room';
  3756. else if (spaceName.includes('卧室')) spaceType = 'bedroom';
  3757. else if (spaceName.includes('厨房')) spaceType = 'kitchen';
  3758. else if (spaceName.includes('卫生间') || spaceName.includes('浴室')) spaceType = 'bathroom';
  3759. else if (spaceName.includes('餐厅')) spaceType = 'dining_room';
  3760. else if (spaceName.includes('书房')) spaceType = 'study';
  3761. else if (spaceName.includes('阳台')) spaceType = 'balcony';
  3762. synthesis.spaces.push({
  3763. id: `space_${Date.now()}_${index}`,
  3764. name: spaceName,
  3765. type: spaceType,
  3766. imageCount: 1,
  3767. viewAngle: fileName
  3768. });
  3769. });
  3770. this.panoramicSyntheses.unshift(synthesis);
  3771. // 模拟KR Panel合成进度
  3772. let progress = 0;
  3773. const synthesisInterval = setInterval(() => {
  3774. progress += 15;
  3775. synthesis.progress = Math.min(progress, 100);
  3776. synthesis.updatedAt = new Date();
  3777. if (progress >= 100) {
  3778. clearInterval(synthesisInterval);
  3779. // 合成完成
  3780. synthesis.status = 'completed';
  3781. synthesis.completedAt = new Date();
  3782. synthesis.previewUrl = this.generateMockPanoramicUrl(synthesis.id);
  3783. synthesis.downloadUrl = this.generateMockDownloadUrl(synthesis.id);
  3784. synthesis.renderTime = 120 + Math.floor(Math.random() * 60);
  3785. synthesis.fileSize = files.reduce((sum, f) => sum + f.size, 0);
  3786. this.isUploadingPanoramicImages = false;
  3787. console.log('✅ 全景图合成完成:', synthesis);
  3788. // 自动生成分享链接
  3789. this.generatePanoramicShareLink(synthesis);
  3790. }
  3791. }, 500);
  3792. }
  3793. // 生成全景图分享链接
  3794. private generatePanoramicShareLink(synthesis: PanoramicSynthesis): void {
  3795. const shareLink = `https://panoramic.example.com/view/${synthesis.id}`;
  3796. synthesis.shareLink = shareLink;
  3797. console.log('🔗 全景图分享链接:', shareLink);
  3798. // 自动通知客服发送给客户
  3799. this.notifyCustomerServiceForPanoramicLink(synthesis);
  3800. }
  3801. // 通知客服发送全景图链接
  3802. private notifyCustomerServiceForPanoramicLink(synthesis: PanoramicSynthesis): void {
  3803. const notification = {
  3804. type: 'panoramic_ready',
  3805. projectId: this.projectId,
  3806. projectName: synthesis.projectName,
  3807. shareLink: synthesis.shareLink,
  3808. message: `项目【${synthesis.projectName}】的全景漫游已生成完成,请发送给客户查看。`,
  3809. timestamp: new Date()
  3810. };
  3811. console.log('📢 通知客服发送全景图链接:', notification);
  3812. // 调用客服通知API
  3813. // this.customerServiceAPI.notifyPanoramicReady(notification).subscribe(...)
  3814. }
  3815. // 生成模拟全景图URL
  3816. private generateMockPanoramicUrl(id: string): string {
  3817. return `https://panoramic.example.com/preview/${id}`;
  3818. }
  3819. // 生成模拟下载URL
  3820. private generateMockDownloadUrl(id: string): string {
  3821. return `https://panoramic.example.com/download/${id}`;
  3822. }
  3823. // 查看全景图画廊
  3824. viewPanoramicGallery(): void {
  3825. console.log('📁 打开全景图画廊');
  3826. if (this.panoramicSyntheses.length === 0) {
  3827. alert('📁 全景图画廊\n\n暂无全景图记录,请先进行全景图合成。');
  3828. return;
  3829. }
  3830. // 显示全景图列表提示
  3831. const galleryInfo = this.panoramicSyntheses.map((item, index) =>
  3832. `${index + 1}. ${item.projectName} - ${item.status === 'completed' ? '✅ 已完成' : '⏳ 处理中'} (${item.spaces?.length || 0}个空间)`
  3833. ).join('\n');
  3834. alert(`📁 全景图画廊(共${this.panoramicSyntheses.length}个)\n\n${galleryInfo}\n\n提示:点击列表中的全景图可查看详情或下载`);
  3835. }
  3836. // 复制全景图链接
  3837. copyPanoramicLink(synthesis: PanoramicSynthesis): void {
  3838. if (!synthesis.shareLink) {
  3839. alert('全景图链接尚未生成');
  3840. return;
  3841. }
  3842. navigator.clipboard.writeText(synthesis.shareLink).then(() => {
  3843. alert(`✅ 全景图链接已复制!\n\n${synthesis.shareLink}`);
  3844. }).catch(() => {
  3845. alert(`全景图链接:\n${synthesis.shareLink}`);
  3846. });
  3847. }
  3848. // ==================== 评价统计相关 ====================
  3849. // 评价统计数据
  3850. reviewStats: {
  3851. overallScore: number;
  3852. timelinessScore: number;
  3853. qualityScore: number;
  3854. communicationScore: number;
  3855. } = {
  3856. overallScore: 4.8,
  3857. timelinessScore: 4.7,
  3858. qualityScore: 4.9,
  3859. communicationScore: 4.6
  3860. };
  3861. // ==================== 客户评价相关 ====================
  3862. // 生成评价链接
  3863. generateReviewLink(): void {
  3864. console.log('📋 生成客户评价链接...');
  3865. // 生成唯一的评价令牌
  3866. const reviewToken = this.generateReviewToken();
  3867. const reviewLink = `https://review.yss.com/project/${this.projectId}?token=${reviewToken}`;
  3868. // 保存评价链接记录
  3869. const reviewRecord = {
  3870. projectId: this.projectId,
  3871. projectName: this.project?.name || '',
  3872. customerName: this.project?.customerName || '',
  3873. reviewLink: reviewLink,
  3874. token: reviewToken,
  3875. createdAt: new Date(),
  3876. expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天有效期
  3877. status: 'active',
  3878. accessed: false
  3879. };
  3880. console.log('✅ 评价链接已生成:', reviewRecord);
  3881. // 复制到剪贴板
  3882. navigator.clipboard.writeText(reviewLink).then(() => {
  3883. alert(`✅ 评价链接已复制到剪贴板!\n\n链接:${reviewLink}\n\n有效期:30天\n\n请通过企业微信发送给客户`);
  3884. }).catch(() => {
  3885. alert(`评价链接:\n\n${reviewLink}\n\n有效期:30天\n\n请通过企业微信发送给客户`);
  3886. });
  3887. // 通知客服发送评价链接
  3888. this.notifyCustomerServiceForReviewLink(reviewRecord);
  3889. }
  3890. // 生成评价令牌
  3891. private generateReviewToken(): string {
  3892. return `review_${this.projectId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  3893. }
  3894. // 通知客服发送评价链接
  3895. private notifyCustomerServiceForReviewLink(reviewRecord: any): void {
  3896. const notification = {
  3897. type: 'review_link_ready',
  3898. projectId: this.projectId,
  3899. projectName: reviewRecord.projectName,
  3900. customerName: reviewRecord.customerName,
  3901. reviewLink: reviewRecord.reviewLink,
  3902. message: `项目【${reviewRecord.projectName}】的客户评价链接已生成,请发送给客户。`,
  3903. timestamp: new Date()
  3904. };
  3905. console.log('📢 通知客服发送评价链接:', notification);
  3906. // 调用客服通知API
  3907. // this.customerServiceAPI.notifyReviewLinkReady(notification).subscribe(...)
  3908. }
  3909. // 确认客户评价完成
  3910. confirmCustomerReview(): void {
  3911. console.log('✅ 确认客户评价完成');
  3912. // 更新项目状态
  3913. if (this.project) {
  3914. this.project.customerReviewCompleted = true;
  3915. this.project.customerReviewCompletedAt = new Date();
  3916. }
  3917. alert('✅ 客户评价已确认完成!');
  3918. // 可选:自动进入下一阶段
  3919. // this.advanceToNextStage('客户评价');
  3920. }
  3921. // ==================== 投诉管理相关 ====================
  3922. // 关键词监控配置
  3923. complaintKeywords: string[] = ['不满意', '投诉', '退款', '差评', '质量问题', '延期', '态度差'];
  3924. isKeywordMonitoringActive: boolean = false;
  3925. // 手动创建投诉
  3926. createComplaintManually(): void {
  3927. console.log('📝 手动创建投诉');
  3928. // 显示创建投诉表单提示
  3929. alert('📝 创建投诉记录\n\n即将打开投诉创建表单,请填写以下信息:\n• 投诉环节\n• 核心问题\n• 客户反馈\n• 严重程度');
  3930. // 弹出创建投诉表单
  3931. const complaintReason = prompt('请输入投诉原因:');
  3932. if (!complaintReason || complaintReason.trim() === '') return;
  3933. const complaintStage = prompt('请输入投诉环节(如:需求沟通、建模、渲染等):') || '未指定';
  3934. // 创建投诉记录
  3935. const complaint: any = {
  3936. id: `complaint_${Date.now()}`,
  3937. projectId: this.projectId,
  3938. projectName: this.project?.name || '',
  3939. customerName: this.project?.customerName || '',
  3940. type: '人工创建',
  3941. stage: complaintStage,
  3942. reason: complaintReason,
  3943. severity: 'medium',
  3944. status: 'pending',
  3945. createdBy: 'manual',
  3946. createdAt: new Date(),
  3947. handler: '',
  3948. resolution: '',
  3949. resolvedAt: null
  3950. };
  3951. // 智能标注核心问题
  3952. complaint.tags = this.analyzeComplaintTags(complaintReason);
  3953. // 添加到投诉列表
  3954. this.exceptionHistories.unshift(complaint);
  3955. console.log('✅ 投诉记录已创建:', complaint);
  3956. alert(`✅ 投诉记录已创建!\n\n投诉环节:${complaintStage}\n核心问题:${complaint.tags.join('、')}\n\n系统将自动跟踪处理进度。`);
  3957. // 通知相关人员
  3958. this.notifyComplaintHandlers(complaint);
  3959. }
  3960. // 分析投诉标签
  3961. private analyzeComplaintTags(reason: string): string[] {
  3962. const tags: string[] = [];
  3963. const tagPatterns = {
  3964. '需求理解': ['需求', '理解', '沟通', '误解'],
  3965. '设计质量': ['质量', '效果', '不好', '不满意'],
  3966. '交付延期': ['延期', '超时', '慢', '着急'],
  3967. '服务态度': ['态度', '不礼貌', '敷衍', '回复慢'],
  3968. '价格问题': ['价格', '费用', '贵', '退款']
  3969. };
  3970. Object.entries(tagPatterns).forEach(([tag, keywords]) => {
  3971. if (keywords.some(keyword => reason.includes(keyword))) {
  3972. tags.push(tag);
  3973. }
  3974. });
  3975. return tags.length > 0 ? tags : ['其他'];
  3976. }
  3977. // 通知投诉处理人员
  3978. private notifyComplaintHandlers(complaint: any): void {
  3979. const notification = {
  3980. type: 'new_complaint',
  3981. projectId: this.projectId,
  3982. complaintId: complaint.id,
  3983. projectName: complaint.projectName,
  3984. customerName: complaint.customerName,
  3985. severity: complaint.severity,
  3986. tags: complaint.tags,
  3987. message: `项目【${complaint.projectName}】收到新投诉,请及时处理。`,
  3988. timestamp: new Date()
  3989. };
  3990. console.log('📢 通知投诉处理人员:', notification);
  3991. // 调用通知API
  3992. // this.complaintService.notifyHandlers(notification).subscribe(...)
  3993. }
  3994. // 设置关键词监控
  3995. setupKeywordMonitoring(): void {
  3996. console.log('⚙️ 设置关键词监控');
  3997. if (this.isKeywordMonitoringActive) {
  3998. const confirmStop = confirm('⚙️ 关键词监控管理\n\n状态:✅ 已激活\n监控关键词:' + this.complaintKeywords.join('、') + '\n\n是否停止监控?');
  3999. if (confirmStop) {
  4000. this.isKeywordMonitoringActive = false;
  4001. alert('⏸️ 关键词监控已停止\n\n系统将不再自动抓取企业微信群中的投诉关键词');
  4002. }
  4003. return;
  4004. }
  4005. // 显示设置说明和当前关键词
  4006. alert('⚙️ 关键词监控功能\n\n功能说明:\n• 自动监测企业微信群消息\n• 识别投诉相关关键词\n• 自动创建投诉记录并标注\n• 实时通知相关处理人员\n\n当前监控关键词:\n' + this.complaintKeywords.join('、'));
  4007. const currentKeywords = this.complaintKeywords.join('、');
  4008. const newKeywords = prompt(`当前监控关键词:\n${currentKeywords}\n\n请输入要添加的关键词(多个关键词用逗号分隔):`);
  4009. if (newKeywords && newKeywords.trim()) {
  4010. const keywords = newKeywords.split(/[,,、]/).map(k => k.trim()).filter(k => k);
  4011. this.complaintKeywords = [...new Set([...this.complaintKeywords, ...keywords])];
  4012. alert(`✅ 关键词已更新\n\n新增关键词:${keywords.join('、')}\n\n全部监控关键词:\n${this.complaintKeywords.join('、')}`);
  4013. }
  4014. // 激活监控
  4015. this.isKeywordMonitoringActive = true;
  4016. this.startKeywordMonitoring();
  4017. alert(`✅ 关键词监控已激活!\n\n监控关键词:${this.complaintKeywords.join('、')}\n\n系统将自动检测企业微信群消息中的关键词并创建投诉记录。`);
  4018. }
  4019. // 开始关键词监控
  4020. private startKeywordMonitoring(): void {
  4021. console.log('👀 开始关键词监控...');
  4022. console.log('监控关键词:', this.complaintKeywords);
  4023. // 模拟监控企业微信消息(实际应使用企业微信API或webhook)
  4024. // 这里仅作演示
  4025. // 监控到关键词后自动创建投诉
  4026. // this.onKeywordDetected(message, keyword);
  4027. }
  4028. // 关键词检测回调
  4029. onKeywordDetected(message: string, keyword: string): void {
  4030. console.log('🚨 检测到关键词:', keyword);
  4031. console.log('消息内容:', message);
  4032. // 自动创建投诉记录
  4033. const complaint: any = {
  4034. id: `complaint_auto_${Date.now()}`,
  4035. projectId: this.projectId,
  4036. projectName: this.project?.name || '',
  4037. customerName: this.project?.customerName || '',
  4038. type: '关键词自动抓取',
  4039. keyword: keyword,
  4040. message: message,
  4041. severity: this.assessComplaintSeverity(message),
  4042. status: 'pending',
  4043. createdBy: 'auto',
  4044. createdAt: new Date(),
  4045. handler: '',
  4046. resolution: '',
  4047. resolvedAt: null
  4048. };
  4049. // 智能标注问题环节和核心问题
  4050. complaint.stage = this.identifyComplaintStage(message);
  4051. complaint.tags = this.analyzeComplaintTags(message);
  4052. // 添加到投诉列表
  4053. this.exceptionHistories.unshift(complaint);
  4054. console.log('✅ 自动投诉记录已创建:', complaint);
  4055. // 实时通知相关人员
  4056. this.notifyComplaintHandlers(complaint);
  4057. }
  4058. // 评估投诉严重程度
  4059. private assessComplaintSeverity(message: string): 'low' | 'medium' | 'high' {
  4060. const highSeverityKeywords = ['退款', '投诉', '举报', '律师', '曝光'];
  4061. const mediumSeverityKeywords = ['不满意', '差评', '质量问题'];
  4062. if (highSeverityKeywords.some(k => message.includes(k))) return 'high';
  4063. if (mediumSeverityKeywords.some(k => message.includes(k))) return 'medium';
  4064. return 'low';
  4065. }
  4066. // 识别投诉环节
  4067. private identifyComplaintStage(message: string): string {
  4068. const stageKeywords = {
  4069. '需求沟通': ['需求', '沟通', '理解'],
  4070. '方案确认': ['方案', '设计', '效果'],
  4071. '建模': ['建模', '模型', '白模'],
  4072. '软装': ['软装', '家具', '配饰'],
  4073. '渲染': ['渲染', '出图', '大图'],
  4074. '交付': ['交付', '发送', '收到']
  4075. };
  4076. for (const [stage, keywords] of Object.entries(stageKeywords)) {
  4077. if (keywords.some(k => message.includes(k))) {
  4078. return stage;
  4079. }
  4080. }
  4081. return '未指定';
  4082. }
  4083. // 确认投诉处理完成
  4084. confirmComplaint(): void {
  4085. console.log('✅ 确认投诉处理完成');
  4086. // 检查是否有未处理的投诉
  4087. const pendingComplaints = this.exceptionHistories.filter(c => c.status === '待处理');
  4088. if (pendingComplaints.length > 0) {
  4089. const confirmAnyway = confirm(`还有 ${pendingComplaints.length} 个投诉未处理,确定要标记为已完成吗?`);
  4090. if (!confirmAnyway) return;
  4091. }
  4092. alert('✅ 所有投诉已确认处理完成!');
  4093. }
  4094. // 处理评价表单提交
  4095. onReviewSubmitted(reviewData: any): void {
  4096. console.log('客户评价已提交:', reviewData);
  4097. // 这里应该调用API将评价数据保存到服务器
  4098. // 模拟API调用
  4099. setTimeout(() => {
  4100. alert('客户评价提交成功!评价数据已保存。');
  4101. // 更新本地反馈数据
  4102. const newFeedback: CustomerFeedback = {
  4103. id: Date.now().toString(),
  4104. customerName: '客户', // 应该从项目信息中获取
  4105. rating: reviewData.overallRating,
  4106. content: reviewData.improvementSuggestions || '无具体建议',
  4107. createdAt: new Date(),
  4108. status: '已解决',
  4109. isSatisfied: reviewData.overallRating >= 4,
  4110. projectId: this.projectId
  4111. };
  4112. this.feedbacks = [...this.feedbacks, newFeedback];
  4113. // 自动标记客户评价完成
  4114. this.confirmCustomerReview();
  4115. }, 1000);
  4116. }
  4117. // 处理评价表单保存草稿
  4118. onReviewSaved(reviewData: any): void {
  4119. console.log('客户评价草稿已保存:', reviewData);
  4120. // 这里应该调用API将草稿数据保存到服务器
  4121. // 模拟API调用
  4122. setTimeout(() => {
  4123. alert('评价草稿保存成功!您可以稍后继续完善。');
  4124. }, 500);
  4125. }
  4126. // ============ 缺少的方法实现 ============
  4127. // 初始化售后模块数据
  4128. private initializeAftercareData(): void {
  4129. // 初始化一些示例全景图合成记录
  4130. this.panoramicSyntheses = [
  4131. {
  4132. id: 'panoramic_001',
  4133. projectId: this.projectId,
  4134. projectName: '示例项目',
  4135. spaces: [
  4136. { id: 'space_001', name: '客厅', type: 'living_room' as const, imageCount: 3, viewAngle: '客厅-角度1' },
  4137. { id: 'space_002', name: '卧室', type: 'bedroom' as const, imageCount: 2, viewAngle: '卧室-角度1' }
  4138. ],
  4139. status: 'completed' as const,
  4140. quality: 'high' as const,
  4141. createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
  4142. updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
  4143. completedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
  4144. previewUrl: 'https://panoramic.example.com/preview/panoramic_001',
  4145. downloadUrl: 'https://panoramic.example.com/download/panoramic_001',
  4146. shareLink: 'https://panoramic.example.com/view/panoramic_001',
  4147. renderTime: 135,
  4148. fileSize: 52428800,
  4149. progress: 100
  4150. }
  4151. ];
  4152. // 初始化一些示例结算记录
  4153. if (this.settlements.length === 0) {
  4154. this.settlements = [
  4155. {
  4156. id: 'settlement_001',
  4157. projectId: this.projectId,
  4158. projectName: '示例项目',
  4159. type: 'deposit',
  4160. amount: 5000,
  4161. percentage: 30,
  4162. status: '已结算',
  4163. createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
  4164. settledAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000)
  4165. },
  4166. {
  4167. id: 'settlement_002',
  4168. projectId: this.projectId,
  4169. projectName: '示例项目',
  4170. type: 'progress',
  4171. amount: 7000,
  4172. percentage: 40,
  4173. status: '已结算',
  4174. createdAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
  4175. settledAt: new Date(Date.now() - 13 * 24 * 60 * 60 * 1000)
  4176. }
  4177. ];
  4178. }
  4179. // 初始化一些示例客户反馈
  4180. if (this.feedbacks.length === 0) {
  4181. this.feedbacks = [
  4182. {
  4183. id: 'feedback_001',
  4184. projectId: this.projectId,
  4185. customerName: '张先生',
  4186. rating: 5,
  4187. content: '设计师非常专业,效果图很满意!',
  4188. isSatisfied: true,
  4189. status: '已解决',
  4190. createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000)
  4191. }
  4192. ];
  4193. }
  4194. // 初始化一些示例投诉记录
  4195. if (this.exceptionHistories.length === 0) {
  4196. this.exceptionHistories = [];
  4197. }
  4198. }
  4199. // 初始化表单
  4200. initializeForms(): void {
  4201. // 初始化订单分配表单(必填项)
  4202. this.orderCreationForm = this.fb.group({
  4203. orderAmount: ['', [Validators.required, Validators.min(0)]],
  4204. smallImageDeliveryTime: ['', Validators.required],
  4205. decorationType: ['', Validators.required],
  4206. requirementReason: ['', Validators.required],
  4207. isMultiDesigner: [false] // 移除requiredTrue验证,改为普通布尔值
  4208. });
  4209. // 初始化可选信息表单
  4210. this.optionalForm = this.fb.group({
  4211. largeImageDeliveryTime: [''],
  4212. spaceRequirements: [''],
  4213. designAngles: [''],
  4214. specialAreaHandling: [''],
  4215. materialRequirements: [''],
  4216. lightingRequirements: ['']
  4217. });
  4218. }
  4219. // 检查是否可以分配订单
  4220. canCreateOrder(): boolean {
  4221. return this.orderCreationForm ? this.orderCreationForm.valid : false;
  4222. }
  4223. // 分配订单
  4224. createOrder(): void {
  4225. if (!this.canCreateOrder()) {
  4226. // 标记所有字段为已触摸,以显示验证错误
  4227. this.orderCreationForm.markAllAsTouched();
  4228. return;
  4229. }
  4230. const orderData = {
  4231. ...this.orderCreationForm.value,
  4232. ...this.optionalForm.value,
  4233. customerInfo: this.orderCreationData?.customerInfo,
  4234. quotationData: this.quotationData,
  4235. designerAssignment: this.designerAssignmentData
  4236. };
  4237. console.log('分配订单:', orderData);
  4238. // 这里应该调用API分配订单
  4239. // 模拟API调用
  4240. setTimeout(() => {
  4241. alert('订单分配成功!');
  4242. // 订单分配成功后自动切换到下一环节
  4243. this.advanceToNextStage('订单分配');
  4244. }, 500);
  4245. }
  4246. // 处理空间文件选择
  4247. onSpaceFileSelected(event: Event, processId: string, spaceId: string): void {
  4248. const input = event.target as HTMLInputElement;
  4249. if (!input.files || input.files.length === 0) return;
  4250. const files = Array.from(input.files);
  4251. const process = this.deliveryProcesses.find(p => p.id === processId);
  4252. if (!process || !process.content[spaceId]) return;
  4253. files.forEach(file => {
  4254. if (/\.(jpg|jpeg|png)$/i.test(file.name)) {
  4255. const imageItem = this.makeImageItem(file);
  4256. process.content[spaceId].images.push({
  4257. id: imageItem.id,
  4258. name: imageItem.name,
  4259. url: imageItem.url,
  4260. size: this.formatFileSize(file.size)
  4261. });
  4262. }
  4263. });
  4264. // 清空输入
  4265. input.value = '';
  4266. }
  4267. // 更新模型检查项状态
  4268. updateModelCheckItem(itemId: string, isPassed: boolean): void {
  4269. const item = this.modelCheckItems.find(i => i.id === itemId);
  4270. if (item) {
  4271. item.isPassed = isPassed;
  4272. console.log(`模型检查项 ${item.name} 状态更新为: ${isPassed ? '已通过' : '待处理'}`);
  4273. }
  4274. }
  4275. // 删除空间图片
  4276. removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
  4277. const process = this.deliveryProcesses.find(p => p.id === processId);
  4278. if (process && process.content[spaceId]) {
  4279. const images = process.content[spaceId].images;
  4280. const imageIndex = images.findIndex(img => img.id === imageId);
  4281. if (imageIndex > -1) {
  4282. // 释放URL资源
  4283. const image = images[imageIndex];
  4284. if (image.url && image.url.startsWith('blob:')) {
  4285. URL.revokeObjectURL(image.url);
  4286. }
  4287. // 从数组中移除
  4288. images.splice(imageIndex, 1);
  4289. console.log(`已删除空间图片: ${processId}/${spaceId}/${imageId}`);
  4290. }
  4291. }
  4292. }
  4293. // 项目复盘相关方法
  4294. getReviewStatus(): 'not_started' | 'generating' | 'completed' {
  4295. if (this.isGeneratingReview) return 'generating';
  4296. if (this.projectReview) return 'completed';
  4297. return 'not_started';
  4298. }
  4299. getReviewStatusText(): string {
  4300. const status = this.getReviewStatus();
  4301. switch (status) {
  4302. case 'not_started': return '未开始';
  4303. case 'generating': return '生成中';
  4304. case 'completed': return '已完成';
  4305. default: return '未知';
  4306. }
  4307. }
  4308. getScoreClass(score: number): string {
  4309. if (score >= 90) return 'excellent';
  4310. if (score >= 80) return 'good';
  4311. if (score >= 70) return 'average';
  4312. return 'poor';
  4313. }
  4314. getExecutionStatusText(status: 'excellent' | 'good' | 'average' | 'poor'): string {
  4315. switch (status) {
  4316. case 'excellent': return '优秀';
  4317. case 'good': return '良好';
  4318. case 'average': return '一般';
  4319. case 'poor': return '较差';
  4320. default: return '未知';
  4321. }
  4322. }
  4323. generateReviewReport(): void {
  4324. if (this.isGeneratingReview) return;
  4325. this.isGeneratingReview = true;
  4326. // 基于真实项目数据生成复盘报告
  4327. setTimeout(() => {
  4328. const sopAnalysisData = this.analyzeSopExecution();
  4329. const experienceInsights = this.generateExperienceInsights();
  4330. const performanceMetrics = this.calculatePerformanceMetrics();
  4331. const plannedBudget = this.quotationData.totalAmount || 150000;
  4332. const actualBudget = this.calculateActualBudget();
  4333. this.projectReview = {
  4334. id: 'review_' + Date.now(),
  4335. projectId: this.projectId,
  4336. generatedAt: new Date(),
  4337. overallScore: this.calculateOverallScore(),
  4338. sopAnalysis: sopAnalysisData,
  4339. keyHighlights: experienceInsights.keyHighlights,
  4340. improvementSuggestions: experienceInsights.improvementSuggestions,
  4341. customerSatisfaction: {
  4342. overallRating: this.reviewStats.overallScore,
  4343. feedback: this.detailedReviews.length > 0 ? this.detailedReviews[0].overallFeedback : '客户反馈良好,对项目整体满意',
  4344. responseTime: this.calculateAverageResponseTime(),
  4345. completionTime: this.calculateProjectDuration()
  4346. },
  4347. teamPerformance: performanceMetrics,
  4348. budgetAnalysis: {
  4349. plannedBudget: plannedBudget,
  4350. actualBudget: actualBudget,
  4351. variance: this.calculateBudgetVariance(plannedBudget, actualBudget),
  4352. costBreakdown: [
  4353. { category: '设计费', planned: plannedBudget * 0.3, actual: actualBudget * 0.3 },
  4354. { category: '材料费', planned: plannedBudget * 0.6, actual: actualBudget * 0.57 },
  4355. { category: '人工费', planned: plannedBudget * 0.1, actual: actualBudget * 0.13 }
  4356. ]
  4357. },
  4358. lessonsLearned: experienceInsights.lessonsLearned,
  4359. recommendations: experienceInsights.recommendations
  4360. };
  4361. this.isGeneratingReview = false;
  4362. alert('复盘报告生成完成!基于真实SOP执行数据和智能分析生成。');
  4363. }, 3000);
  4364. }
  4365. regenerateReviewReport(): void {
  4366. this.projectReview = null;
  4367. this.generateReviewReport();
  4368. }
  4369. exportReviewReport(): void {
  4370. console.log('开始导出项目复盘报告...');
  4371. // 准备报告数据
  4372. const reportData = this.prepareReviewReportData();
  4373. // 生成Excel文件
  4374. this.generateExcelReport(reportData);
  4375. }
  4376. shareReviewReport(): void {
  4377. if (!this.projectReview) return;
  4378. const shareRequest: ReviewReportShareRequest = {
  4379. projectId: this.projectId,
  4380. reviewId: this.projectReview.id,
  4381. shareType: 'link',
  4382. expirationDays: 30,
  4383. allowDownload: true,
  4384. requirePassword: false
  4385. };
  4386. this.projectReviewService.shareReviewReport(shareRequest).subscribe({
  4387. next: (response) => {
  4388. if (response.success && response.shareUrl) {
  4389. // 复制到剪贴板
  4390. if (navigator.clipboard) {
  4391. navigator.clipboard.writeText(response.shareUrl).then(() => {
  4392. alert(`复盘报告分享链接已复制到剪贴板!\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
  4393. }).catch(() => {
  4394. alert(`复盘报告分享链接:\n${response.shareUrl}\n\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
  4395. });
  4396. } else {
  4397. alert(`复盘报告分享链接:\n${response.shareUrl}\n\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
  4398. }
  4399. } else {
  4400. alert('分享失败:' + (response.message || '未知错误'));
  4401. }
  4402. },
  4403. error: (error) => {
  4404. console.error('分享复盘报告失败:', error);
  4405. alert('分享失败,请稍后重试');
  4406. }
  4407. });
  4408. }
  4409. // 项目复盘工具方法
  4410. getMaxDuration(): number {
  4411. if (!this.sopStagesData || this.sopStagesData.length === 0) return 1;
  4412. return Math.max(...this.sopStagesData.map(s => Math.max(s.plannedDuration, s.actualDuration)));
  4413. }
  4414. getAverageScore(): number {
  4415. if (!this.sopStagesData || this.sopStagesData.length === 0) return 0;
  4416. const sum = this.sopStagesData.reduce((acc, s) => acc + s.score, 0);
  4417. return Math.round(sum / this.sopStagesData.length);
  4418. }
  4419. getSuggestionCountByPriority(priority: string): number {
  4420. if (!this.optimizationSuggestions) return 0;
  4421. return this.optimizationSuggestions.filter(s => s.priority === priority).length;
  4422. }
  4423. getAverageImprovementPercent(): number {
  4424. if (!this.optimizationSuggestions || this.optimizationSuggestions.length === 0) return 0;
  4425. return 25;
  4426. }
  4427. // 分析SOP执行情况
  4428. private analyzeSopExecution(): any[] {
  4429. const sopStages = [
  4430. { name: '需求沟通', planned: 3, actual: 2.5 },
  4431. { name: '方案确认', planned: 5, actual: 4 },
  4432. { name: '建模', planned: 7, actual: 8 },
  4433. { name: '软装', planned: 3, actual: 3.5 },
  4434. { name: '渲染', planned: 5, actual: 4.5 },
  4435. { name: '后期', planned: 2, actual: 2 }
  4436. ];
  4437. return sopStages.map(stage => {
  4438. const variance = ((stage.actual - stage.planned) / stage.planned) * 100;
  4439. let executionStatus: 'excellent' | 'good' | 'average' | 'poor';
  4440. let score: number;
  4441. if (variance <= -10) {
  4442. executionStatus = 'excellent';
  4443. score = 95;
  4444. } else if (variance <= 0) {
  4445. executionStatus = 'good';
  4446. score = 85;
  4447. } else if (variance <= 20) {
  4448. executionStatus = 'average';
  4449. score = 70;
  4450. } else {
  4451. executionStatus = 'poor';
  4452. score = 50;
  4453. }
  4454. const issues: string[] = [];
  4455. if (variance > 20) {
  4456. issues.push('执行时间超出计划较多');
  4457. }
  4458. if (stage.name === '建模' && variance > 0) {
  4459. issues.push('建模阶段需要优化流程');
  4460. }
  4461. return {
  4462. stageName: stage.name,
  4463. plannedDuration: stage.planned,
  4464. actualDuration: stage.actual,
  4465. score,
  4466. executionStatus,
  4467. issues: issues.length > 0 ? issues : undefined
  4468. };
  4469. });
  4470. }
  4471. // 生成经验洞察
  4472. private generateExperienceInsights(): { keyHighlights: string[]; improvementSuggestions: string[]; lessonsLearned: string[]; recommendations: string[] } {
  4473. return {
  4474. keyHighlights: [
  4475. '需求沟通阶段效率显著提升,客户满意度高',
  4476. '渲染质量获得客户高度认可',
  4477. '团队协作配合默契,沟通顺畅',
  4478. '项目交付时间控制良好'
  4479. ],
  4480. improvementSuggestions: [
  4481. '建模阶段可以进一步优化工作流程',
  4482. '加强前期需求确认的深度和准确性',
  4483. '建立更完善的质量检查机制',
  4484. '提升跨部门协作效率'
  4485. ],
  4486. lessonsLearned: [
  4487. '充分的前期沟通能显著减少后期修改',
  4488. '标准化流程有助于提高执行效率',
  4489. '及时的客户反馈对项目成功至关重要',
  4490. '团队技能匹配度直接影响项目质量'
  4491. ],
  4492. recommendations: [
  4493. '建议在类似项目中复用成功的沟通模式',
  4494. '可以将本项目的渲染标准作为团队参考',
  4495. '建议建立项目经验知识库',
  4496. '推荐定期进行团队技能培训'
  4497. ]
  4498. };
  4499. }
  4500. // 计算绩效指标
  4501. private calculatePerformanceMetrics(): { designerScore: number; communicationScore: number; timelinessScore: number; qualityScore: number } {
  4502. return {
  4503. designerScore: 88,
  4504. communicationScore: 92,
  4505. timelinessScore: 85,
  4506. qualityScore: 90
  4507. };
  4508. }
  4509. // 计算总体评分
  4510. private calculateOverallScore(): number {
  4511. const metrics = this.calculatePerformanceMetrics();
  4512. return Math.round((metrics.designerScore + metrics.communicationScore + metrics.timelinessScore + metrics.qualityScore) / 4);
  4513. }
  4514. // 计算平均响应时间
  4515. private calculateAverageResponseTime(): number {
  4516. // 模拟计算平均响应时间(小时)
  4517. return 2.5;
  4518. }
  4519. // 计算项目持续时间
  4520. private calculateProjectDuration(): number {
  4521. // 模拟计算项目持续时间(天)
  4522. return 28;
  4523. }
  4524. // 计算实际预算
  4525. private calculateActualBudget(): number {
  4526. // 基于订单金额计算实际预算
  4527. return this.orderAmount || 150000;
  4528. }
  4529. // 计算预算偏差
  4530. private calculateBudgetVariance(plannedBudget: number, actualBudget: number): number {
  4531. return ((actualBudget - plannedBudget) / plannedBudget) * 100;
  4532. }
  4533. formatDateTime(date: Date): string {
  4534. return date.toLocaleString('zh-CN', {
  4535. year: 'numeric',
  4536. month: '2-digit',
  4537. day: '2-digit',
  4538. hour: '2-digit',
  4539. minute: '2-digit'
  4540. });
  4541. }
  4542. // ============ 空间管理相关方法 ============
  4543. // 添加新空间
  4544. addSpace(processId: string): void {
  4545. const spaceName = this.newSpaceName[processId]?.trim();
  4546. if (!spaceName) return;
  4547. const process = this.deliveryProcesses.find(p => p.id === processId);
  4548. if (!process) return;
  4549. // 生成新的空间ID
  4550. const spaceId = `space_${Date.now()}`;
  4551. // 添加到spaces数组
  4552. const newSpace: DeliverySpace = {
  4553. id: spaceId,
  4554. name: spaceName,
  4555. isExpanded: false,
  4556. order: process.spaces.length + 1
  4557. };
  4558. process.spaces.push(newSpace);
  4559. // 初始化content数据
  4560. process.content[spaceId] = {
  4561. images: [],
  4562. progress: 0,
  4563. status: 'pending',
  4564. notes: '',
  4565. lastUpdated: new Date()
  4566. };
  4567. // 清空输入框并隐藏
  4568. this.newSpaceName[processId] = '';
  4569. this.showAddSpaceInput[processId] = false;
  4570. console.log(`已添加空间: ${spaceName} 到流程 ${process.name}`);
  4571. }
  4572. // 取消添加空间
  4573. cancelAddSpace(processId: string): void {
  4574. this.newSpaceName[processId] = '';
  4575. this.showAddSpaceInput[processId] = false;
  4576. }
  4577. // 获取指定流程的活跃空间列表
  4578. getActiveProcessSpaces(processId: string): DeliverySpace[] {
  4579. const process = this.deliveryProcesses.find(p => p.id === processId);
  4580. if (!process) return [];
  4581. return process.spaces.sort((a, b) => a.order - b.order);
  4582. }
  4583. // 切换空间展开状态
  4584. toggleSpace(processId: string, spaceId: string): void {
  4585. const process = this.deliveryProcesses.find(p => p.id === processId);
  4586. if (!process) return;
  4587. const space = process.spaces.find(s => s.id === spaceId);
  4588. if (space) {
  4589. space.isExpanded = !space.isExpanded;
  4590. }
  4591. }
  4592. // 获取空间进度
  4593. getSpaceProgress(processId: string, spaceId: string): number {
  4594. const process = this.deliveryProcesses.find(p => p.id === processId);
  4595. if (!process || !process.content[spaceId]) return 0;
  4596. return process.content[spaceId].progress || 0;
  4597. }
  4598. // 删除空间
  4599. removeSpace(processId: string, spaceId: string): void {
  4600. const process = this.deliveryProcesses.find(p => p.id === processId);
  4601. if (!process) return;
  4602. // 从spaces数组中移除
  4603. const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
  4604. if (spaceIndex > -1) {
  4605. const spaceName = process.spaces[spaceIndex].name;
  4606. process.spaces.splice(spaceIndex, 1);
  4607. // 清理content数据
  4608. if (process.content[spaceId]) {
  4609. // 释放图片URL资源
  4610. process.content[spaceId].images.forEach(img => {
  4611. if (img.url && img.url.startsWith('blob:')) {
  4612. URL.revokeObjectURL(img.url);
  4613. }
  4614. });
  4615. delete process.content[spaceId];
  4616. }
  4617. console.log(`已删除空间: ${spaceName} 从流程 ${process.name}`);
  4618. }
  4619. }
  4620. // 触发空间文件输入
  4621. triggerSpaceFileInput(processId: string, spaceId: string): void {
  4622. const inputId = `space-file-input-${processId}-${spaceId}`;
  4623. const input = document.getElementById(inputId) as HTMLInputElement;
  4624. if (input) {
  4625. input.click();
  4626. }
  4627. }
  4628. // 处理空间文件拖拽
  4629. onSpaceFileDrop(event: DragEvent, processId: string, spaceId: string): void {
  4630. event.preventDefault();
  4631. event.stopPropagation();
  4632. const files = event.dataTransfer?.files;
  4633. if (!files || files.length === 0) return;
  4634. this.handleSpaceFiles(Array.from(files), processId, spaceId);
  4635. }
  4636. // 处理空间文件
  4637. private handleSpaceFiles(files: File[], processId: string, spaceId: string): void {
  4638. const process = this.deliveryProcesses.find(p => p.id === processId);
  4639. if (!process || !process.content[spaceId]) return;
  4640. files.forEach(file => {
  4641. if (/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)) {
  4642. const imageItem = this.makeImageItem(file);
  4643. process.content[spaceId].images.push({
  4644. id: imageItem.id,
  4645. name: imageItem.name,
  4646. url: imageItem.url,
  4647. size: this.formatFileSize(file.size),
  4648. reviewStatus: 'pending'
  4649. });
  4650. // 更新进度
  4651. this.updateSpaceProgress(processId, spaceId);
  4652. }
  4653. });
  4654. }
  4655. // 获取空间图片列表
  4656. getSpaceImages(processId: string, spaceId: string): Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected' }> {
  4657. const process = this.deliveryProcesses.find(p => p.id === processId);
  4658. if (!process || !process.content[spaceId]) return [];
  4659. return process.content[spaceId].images || [];
  4660. }
  4661. // 确认阶段上传并推进工作流
  4662. confirmStageUpload(stageId: string): void {
  4663. const stageMap: { [key: string]: ProjectStage } = {
  4664. 'modeling': '建模',
  4665. 'softDecor': '软装',
  4666. 'rendering': '渲染',
  4667. 'postProduction': '后期'
  4668. };
  4669. const currentStage = stageMap[stageId];
  4670. if (!currentStage) return;
  4671. // 检查当前阶段是否有上传的图片
  4672. const process = this.deliveryProcesses.find(p => p.id === stageId);
  4673. if (!process) return;
  4674. const hasImages = Object.values(process.content).some(space =>
  4675. space.images && space.images.length > 0
  4676. );
  4677. if (!hasImages) {
  4678. alert('请先上传图片再确认');
  4679. return;
  4680. }
  4681. // 推进到下一阶段
  4682. const currentIndex = this.stageOrder.indexOf(currentStage);
  4683. if (currentIndex < this.stageOrder.length - 1) {
  4684. const nextStage = this.stageOrder[currentIndex + 1];
  4685. // 更新当前阶段
  4686. this.currentStage = nextStage;
  4687. if (this.project) {
  4688. this.project.currentStage = nextStage;
  4689. }
  4690. // 展开下一阶段
  4691. this.expandedStages[nextStage] = true;
  4692. // 滚动到下一阶段
  4693. setTimeout(() => {
  4694. this.scrollToStage(nextStage);
  4695. }, 100);
  4696. console.log(`阶段推进: ${currentStage} -> ${nextStage}`);
  4697. } else {
  4698. // 如果是最后一个阶段,标记为完成
  4699. console.log(`交付执行阶段完成: ${currentStage}`);
  4700. alert('交付执行阶段已完成!');
  4701. }
  4702. }
  4703. // 检查是否可以确认阶段上传
  4704. canConfirmStageUpload(stageId: string): boolean {
  4705. // 检查是否有上传的图片
  4706. const process = this.deliveryProcesses.find(p => p.id === stageId);
  4707. if (!process) return false;
  4708. return Object.values(process.content).some(space =>
  4709. space.images && space.images.length > 0
  4710. );
  4711. }
  4712. // 获取空间备注
  4713. getSpaceNotes(processId: string, spaceId: string): string {
  4714. const process = this.deliveryProcesses.find(p => p.id === processId);
  4715. if (!process || !process.content[spaceId]) return '';
  4716. return process.content[spaceId].notes || '';
  4717. }
  4718. // 更新空间备注
  4719. updateSpaceNotes(processId: string, spaceId: string, notes: string): void {
  4720. const process = this.deliveryProcesses.find(p => p.id === processId);
  4721. if (!process || !process.content[spaceId]) return;
  4722. process.content[spaceId].notes = notes;
  4723. process.content[spaceId].lastUpdated = new Date();
  4724. console.log(`已更新空间备注: ${processId}/${spaceId}`);
  4725. }
  4726. // 更新空间进度
  4727. private updateSpaceProgress(processId: string, spaceId: string): void {
  4728. const process = this.deliveryProcesses.find(p => p.id === processId);
  4729. if (!process || !process.content[spaceId]) return;
  4730. const content = process.content[spaceId];
  4731. const imageCount = content.images.length;
  4732. // 根据图片数量和状态计算进度
  4733. if (imageCount === 0) {
  4734. content.progress = 0;
  4735. content.status = 'pending';
  4736. } else if (imageCount < 3) {
  4737. content.progress = Math.min(imageCount * 30, 90);
  4738. content.status = 'in_progress';
  4739. } else {
  4740. content.progress = 100;
  4741. content.status = 'completed';
  4742. }
  4743. content.lastUpdated = new Date();
  4744. }
  4745. // ==================== 功能卡片点击事件 ====================
  4746. /**
  4747. * 显示功能详情
  4748. * @param title 功能标题
  4749. * @param description 功能详细描述
  4750. */
  4751. showFeatureDetail(title: string, description: string): void {
  4752. console.log(`📋 功能详情: ${title}`);
  4753. console.log(`📝 ${description}`);
  4754. alert(`✨ ${title}\n\n${description}\n\n点击确定关闭`);
  4755. }
  4756. // ==================== 需求映射辅助方法 ====================
  4757. getMaterialName(category: string | undefined): string {
  4758. if (!category) return '未识别';
  4759. const nameMap: { [key: string]: string } = {
  4760. 'wood': '木材', 'metal': '金属', 'fabric': '织物', 'leather': '皮革',
  4761. 'plastic': '塑料', 'glass': '玻璃', 'ceramic': '陶瓷', 'stone': '石材', 'composite': '复合材料'
  4762. };
  4763. return nameMap[category] || category;
  4764. }
  4765. getLightingMoodName(mood: string | undefined): string {
  4766. if (!mood) return '未识别';
  4767. const nameMap: { [key: string]: string } = {
  4768. 'dramatic': '戏剧性', 'romantic': '浪漫', 'energetic': '活力',
  4769. 'calm': '平静', 'mysterious': '神秘', 'cheerful': '愉悦', 'professional': '专业'
  4770. };
  4771. return nameMap[mood] || mood;
  4772. }
  4773. getColorHarmonyName(harmony: string | undefined): string {
  4774. if (!harmony) return '未知';
  4775. const nameMap: { [key: string]: string } = {
  4776. 'monochromatic': '单色调和', 'analogous': '类似色调和', 'complementary': '互补色调和',
  4777. 'triadic': '三角色调和', 'tetradic': '四角色调和', 'split-complementary': '分裂互补色调和'
  4778. };
  4779. return nameMap[harmony] || harmony;
  4780. }
  4781. getTemperatureName(temp: string | undefined): string {
  4782. if (!temp) return '未知';
  4783. const nameMap: { [key: string]: string } = {
  4784. 'warm': '暖色调', 'neutral': '中性色调', 'cool': '冷色调'
  4785. };
  4786. return nameMap[temp] || temp;
  4787. }
  4788. getLayoutTypeName(type: string | undefined): string {
  4789. if (!type) return '未知';
  4790. const nameMap: { [key: string]: string } = {
  4791. 'open': '开放式', 'enclosed': '封闭式', 'semi-open': '半开放式', 'multi-level': '多层次'
  4792. };
  4793. return nameMap[type] || type;
  4794. }
  4795. getFlowTypeName(flow: string | undefined): string {
  4796. if (!flow) return '未知';
  4797. const nameMap: { [key: string]: string } = {
  4798. 'linear': '线性流线', 'circular': '环形流线', 'grid': '网格流线', 'organic': '有机流线'
  4799. };
  4800. return nameMap[flow] || flow;
  4801. }
  4802. // ==================== 优化建议功能 ====================
  4803. // 显示建议详情弹窗的状态
  4804. showSuggestionDetailModal: boolean = false;
  4805. selectedSuggestion: any = null;
  4806. /**
  4807. * 查看建议详情
  4808. */
  4809. viewSuggestionDetail(suggestion: any): void {
  4810. this.selectedSuggestion = suggestion;
  4811. this.showSuggestionDetailModal = true;
  4812. }
  4813. /**
  4814. * 关闭建议详情弹窗
  4815. */
  4816. closeSuggestionDetailModal(): void {
  4817. this.showSuggestionDetailModal = false;
  4818. this.selectedSuggestion = null;
  4819. }
  4820. /**
  4821. * 采纳建议
  4822. */
  4823. acceptSuggestion(suggestion: any): void {
  4824. const confirmMessage = `确定要采纳这条优化建议吗?\n\n类别:${suggestion.category}\n预期提升:${suggestion.expectedImprovement}`;
  4825. if (confirm(confirmMessage)) {
  4826. // 标记建议为已采纳
  4827. suggestion.accepted = true;
  4828. suggestion.acceptedAt = new Date();
  4829. // 显示成功消息
  4830. alert(`✅ 已采纳优化建议!\n\n类别:${suggestion.category}\n建议已加入您的改进计划中。`);
  4831. console.log('已采纳建议:', suggestion);
  4832. }
  4833. }
  4834. /**
  4835. * 准备复盘报告数据
  4836. */
  4837. private prepareReviewReportData(): any {
  4838. return {
  4839. projectInfo: {
  4840. name: this.project?.name || '未命名项目',
  4841. id: this.projectId,
  4842. status: this.project?.currentStage || '进行中',
  4843. startDate: this.project?.createdAt || new Date(),
  4844. completedDate: new Date()
  4845. },
  4846. summary: {
  4847. totalDuration: this.calculateProjectDuration2(),
  4848. stagesCompleted: this.getCompletedStagesCount2(),
  4849. overallScore: this.calculateOverallScore2(),
  4850. strengths: this.getProjectStrengths2(),
  4851. weaknesses: this.getProjectWeaknesses2()
  4852. },
  4853. stageAnalysis: this.getStageAnalysisData2(),
  4854. optimizationSuggestions: this.optimizationSuggestions || [],
  4855. experienceSummary: {}
  4856. };
  4857. }
  4858. /**
  4859. * 生成Excel报告
  4860. */
  4861. private generateExcelReport(data: any): void {
  4862. try {
  4863. // 转换为CSV格式并下载(简化版本)
  4864. this.downloadAsCSV(data);
  4865. alert('✅ 报告导出成功!\n\n报告已下载到您的下载文件夹。');
  4866. } catch (error) {
  4867. console.error('导出报告失败:', error);
  4868. alert('❌ 导出报告失败,请稍后重试。');
  4869. }
  4870. }
  4871. /**
  4872. * 下载为CSV文件(简化实现)
  4873. */
  4874. private downloadAsCSV(data: any): void {
  4875. // 生成CSV内容
  4876. let csvContent = '\uFEFF'; // UTF-8 BOM
  4877. // 项目概况
  4878. csvContent += '项目复盘报告\n\n';
  4879. csvContent += '=== 项目概况 ===\n';
  4880. csvContent += `项目名称,${data.projectInfo.name}\n`;
  4881. csvContent += `项目ID,${data.projectInfo.id}\n`;
  4882. csvContent += `项目状态,${data.projectInfo.status}\n`;
  4883. csvContent += `总耗时,${data.summary.totalDuration}\n`;
  4884. csvContent += `综合评分,${data.summary.overallScore}\n\n`;
  4885. // 优化建议
  4886. csvContent += '=== 优化建议 ===\n';
  4887. csvContent += '优先级,类别,问题,建议,预期提升,是否采纳\n';
  4888. data.optimizationSuggestions.forEach((suggestion: any) => {
  4889. csvContent += `${suggestion.priorityText},${suggestion.category},"${suggestion.problem}","${suggestion.solution}",${suggestion.expectedImprovement},${suggestion.accepted ? '是' : '否'}\n`;
  4890. });
  4891. // 创建Blob并下载
  4892. const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  4893. const link = document.createElement('a');
  4894. const url = URL.createObjectURL(blob);
  4895. link.setAttribute('href', url);
  4896. link.setAttribute('download', `项目复盘报告_${data.projectInfo.name}_${this.formatDateHelper(new Date())}.csv`);
  4897. link.style.visibility = 'hidden';
  4898. document.body.appendChild(link);
  4899. link.click();
  4900. document.body.removeChild(link);
  4901. URL.revokeObjectURL(url);
  4902. }
  4903. // 辅助方法
  4904. private calculateProjectDuration2(): string {
  4905. return '15天';
  4906. }
  4907. private getCompletedStagesCount2(): number {
  4908. return 6;
  4909. }
  4910. private calculateOverallScore2(): string {
  4911. return '85分';
  4912. }
  4913. private getProjectStrengths2(): string[] {
  4914. return ['需求理解准确', '交付质量高', '客户满意度好'];
  4915. }
  4916. private getProjectWeaknesses2(): string[] {
  4917. return ['时间管理待优化', '沟通效率可提升'];
  4918. }
  4919. private getStageAnalysisData2(): any[] {
  4920. return [
  4921. { name: '需求沟通', status: '已完成', duration: '2', score: '90', issues: '0', notes: '沟通顺畅' },
  4922. { name: '方案确认', status: '已完成', duration: '3', score: '85', issues: '1', notes: '一次修改' },
  4923. { name: '建模', status: '已完成', duration: '4', score: '88', issues: '0', notes: '按时完成' },
  4924. { name: '软装', status: '已完成', duration: '2', score: '92', issues: '0', notes: '效果优秀' },
  4925. { name: '渲染', status: '已完成', duration: '3', score: '90', issues: '0', notes: '质量优秀' },
  4926. { name: '后期', status: '已完成', duration: '1', score: '88', issues: '0', notes: '及时交付' }
  4927. ];
  4928. }
  4929. private formatDateHelper(date: Date | string): string {
  4930. if (!date) return '';
  4931. const d = new Date(date);
  4932. return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  4933. }
  4934. // ==================== 上传弹窗请求处理 ====================
  4935. /**
  4936. * 处理上传弹窗请求事件
  4937. */
  4938. onUploadModalRequested(event: any): void {
  4939. console.log('收到上传弹窗请求:', event);
  4940. // 这里可以根据event中的信息打开相应的上传弹窗
  4941. // 目前只做日志记录,实际功能可根据需求扩展
  4942. // 如果需要打开特定的上传弹窗,可以在这里实现
  4943. // 例如:this.showUploadModal = true;
  4944. }
  4945. // ==================== 订单审批功能 ====================
  4946. /**
  4947. * 检查是否需要显示审批面板
  4948. */
  4949. private checkApprovalStatus(): void {
  4950. if (!this.project) return;
  4951. const isTeamLeader = this.roleContext === 'team-leader';
  4952. const currentStage = this.project.currentStage;
  4953. const data = (this.project as any).data || {};
  4954. const approvalStatus = data.approvalStatus;
  4955. // 组长视角 + 订单分配阶段 + 待审批状态
  4956. this.showApprovalPanel = isTeamLeader &&
  4957. currentStage === '订单分配' &&
  4958. approvalStatus === 'pending';
  4959. console.log('审批面板显示状态:', {
  4960. isTeamLeader,
  4961. currentStage,
  4962. approvalStatus,
  4963. showApprovalPanel: this.showApprovalPanel
  4964. });
  4965. }
  4966. /**
  4967. * 处理审批完成事件
  4968. */
  4969. async onApprovalCompleted(event: { action: 'approved' | 'rejected'; reason?: string; comment?: string }): Promise<void> {
  4970. if (!this.project || !this.currentUser) return;
  4971. try {
  4972. // 获取项目的 data 字段
  4973. const projectData = (this.project as any);
  4974. const data = projectData.data || {};
  4975. const approvalHistory = data.approvalHistory || [];
  4976. const latestRecord = approvalHistory[approvalHistory.length - 1];
  4977. if (!latestRecord) {
  4978. alert('审批记录不存在');
  4979. return;
  4980. }
  4981. // 更新最新的审批记录
  4982. latestRecord.status = event.action === 'approved' ? 'approved' : 'rejected';
  4983. latestRecord.approver = {
  4984. id: this.currentUser.id || 'unknown',
  4985. name: this.currentUser.name || '组长',
  4986. role: this.currentUser.roleName || '组长'
  4987. };
  4988. latestRecord.approvalTime = new Date();
  4989. if (event.reason) {
  4990. latestRecord.reason = event.reason;
  4991. }
  4992. if (event.comment) {
  4993. latestRecord.comment = event.comment;
  4994. }
  4995. // 更新项目状态
  4996. if (event.action === 'approved') {
  4997. // 通过:推进到"确认需求"阶段
  4998. projectData.currentStage = '确认需求';
  4999. this.currentStage = '确认需求';
  5000. data.approvalStatus = 'approved';
  5001. delete data.pendingApprovalBy;
  5002. } else {
  5003. // 驳回:保持在"订单分配"阶段,但标记为已驳回
  5004. data.approvalStatus = 'rejected';
  5005. data.lastRejectionReason = event.reason;
  5006. delete data.pendingApprovalBy;
  5007. }
  5008. data.approvalHistory = approvalHistory;
  5009. projectData.data = data;
  5010. // 保存到数据库(使用项目的Parse对象)
  5011. if (this.project && (this.project as any).save) {
  5012. // 直接在当前project对象上设置并保存
  5013. (this.project as any).currentStage = projectData.currentStage;
  5014. (this.project as any).data = data;
  5015. await (this.project as any).save(null, { useMasterKey: true });
  5016. }
  5017. // 提示用户
  5018. if (event.action === 'approved') {
  5019. alert('✅ 审批通过!项目已进入"确认需求"阶段');
  5020. } else {
  5021. alert('❌ 已驳回订单,客服将收到驳回通知');
  5022. }
  5023. // 刷新页面或返回列表
  5024. this.showApprovalPanel = false;
  5025. this.router.navigate(['/wxwork', this.companyId, 'team-leader', 'dashboard']);
  5026. } catch (error) {
  5027. console.error('审批操作失败:', error);
  5028. alert('操作失败,请重试');
  5029. }
  5030. }
  5031. }