index.vue 166 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586
  1. <template>
  2. <div class="parameter-settings">
  3. <!-- 顶部二级菜单 -->
  4. <div class="sub-tabs">
  5. <div
  6. v-for="tab in subTabs"
  7. :key="tab.value"
  8. :class="['sub-tab-item', { active: activeSubTab === tab.value }]"
  9. @click="activeSubTab = tab.value"
  10. >
  11. {{ tab.label }}
  12. </div>
  13. </div>
  14. <!-- 内容区域 -->
  15. <div class="content-body">
  16. <!-- 搜索栏编辑区 -->
  17. <div v-if="activeSubTab === 'search'" class="search-editor">
  18. <!-- 实时预览区 (参照图1) -->
  19. <div class="preview-section">
  20. <div class="preview-title">实时预览</div>
  21. <div class="live-preview-box">
  22. <div class="search-bar-mockup">
  23. <!-- 左侧标题 -->
  24. <div class="mockup-left">
  25. <div class="main-title" :style="{ color: form.themeColor }">{{ form.mainTitle }}</div>
  26. <div class="sub-title">{{ form.subTitle }}</div>
  27. </div>
  28. <!-- 中间搜索框 -->
  29. <div class="mockup-center" :style="{ '--theme-color': form.themeColor }">
  30. <div class="search-input-wrapper" :style="{ borderColor: form.themeColor }">
  31. <div class="placeholder-scroll">
  32. <transition-group name="list-scroll" tag="div" class="scroll-container">
  33. <div v-for="(text, index) in searchPlaceholderList" :key="text" v-show="index === currentPlaceholderIndex" class="scroll-item">
  34. {{ text }}
  35. </div>
  36. </transition-group>
  37. </div>
  38. <div class="search-btn" :style="{ backgroundColor: form.themeColor }">搜 索</div>
  39. </div>
  40. <div class="hot-words">
  41. <span v-for="item in form.hotWordsList" :key="item.name" class="hot-word">{{ item.name }}</span>
  42. </div>
  43. </div>
  44. <!-- 右侧功能按钮 (可配置) -->
  45. <div class="mockup-right">
  46. <div class="cart-btn" :style="{ borderColor: form.themeColor, color: form.themeColor }">
  47. <img v-if="form.rightBtnIcon" :src="form.rightBtnIcon" style="width: 16px; height: 16px; margin-right: 4px; object-fit: contain" />
  48. <span v-if="form.rightBtnText">{{ form.rightBtnText }}</span>
  49. <span v-else-if="!form.rightBtnIcon">购物车</span>
  50. </div>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. <!-- 配置项表单 -->
  56. <div class="settings-section">
  57. <el-form :model="form" label-width="120px" label-position="left">
  58. <el-form-item label="主标题:">
  59. <el-input v-model="form.mainTitle" placeholder="如:优易企业购" class="settings-input" />
  60. </el-form-item>
  61. <el-form-item label="副标题:">
  62. <el-input v-model="form.subTitle" placeholder="如:省钱 · 省心 · 省时间" class="settings-input" />
  63. </el-form-item>
  64. <el-form-item label="搜索框文字:">
  65. <el-input v-model="form.placeholderText" placeholder="支持输入多个,请用英文逗号(,)隔开实现上下滚动效果" class="settings-input" />
  66. </el-form-item>
  67. <el-form-item label="搜索热词:">
  68. <div class="hot-words-config">
  69. <div v-for="(item, index) in form.hotWordsList" :key="index" class="hot-word-row">
  70. <el-input v-model="item.name" placeholder="热词名称" class="hot-word-input-name" />
  71. <el-input v-model="item.link" placeholder="跳转地址" class="hot-word-input-link" />
  72. <el-button type="danger" icon="Delete" circle plain size="small" @click="removeHotWord(index)" />
  73. </div>
  74. <el-button type="primary" icon="Plus" link @click="addHotWord" class="add-hotword-btn">添加热词</el-button>
  75. </div>
  76. </el-form-item>
  77. <el-form-item label="按钮图标:">
  78. <div class="upload-placeholder-square" @click="triggerUpload('rightBtnIcon')">
  79. <img v-if="form.rightBtnIcon" :src="form.rightBtnIcon" class="form-preview-img-square" style="width: 24px; height: 24px" />
  80. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  81. </div>
  82. <div class="field-tip">建议尺寸: 32*32, 格式: PNG/SVG</div>
  83. </el-form-item>
  84. <el-form-item label="按钮文字:">
  85. <el-input v-model="form.rightBtnText" placeholder="如:购物车" maxlength="6" show-word-limit class="settings-input" />
  86. </el-form-item>
  87. <el-form-item label="跳转地址:">
  88. <el-input v-model="form.rightBtnLink" placeholder="请输入跳转地址" class="settings-input" />
  89. </el-form-item>
  90. <el-form-item label="主题色:">
  91. <div class="color-picker-wrap">
  92. <el-color-picker v-model="form.themeColor" />
  93. <span class="color-val">{{ form.themeColor }}</span>
  94. </div>
  95. </el-form-item>
  96. </el-form>
  97. </div>
  98. </div>
  99. <!-- 广告图编辑区 -->
  100. <div v-else-if="activeSubTab === 'carousel'" class="carousel-editor">
  101. <!-- 模块一:左侧广告设置 -->
  102. <div class="editor-section">
  103. <div class="section-header">
  104. <span class="section-title">模块一:左侧广告设置</span>
  105. <span class="section-desc">尺寸要求:790 * 460,支持上传本地图片并设置跳转链接</span>
  106. </div>
  107. <div class="left-ad-container">
  108. <div
  109. class="left-ad-preview-wrapper"
  110. :class="{ expanded: leftAdHover }"
  111. @mouseenter="leftAdHover = true"
  112. @mouseleave="leftAdHover = false"
  113. >
  114. <img v-if="leftAdForm.leftAdImage" :src="leftAdForm.leftAdImage" class="left-ad-img" alt="左侧广告" />
  115. <div v-else class="left-ad-empty" @click="triggerUpload('leftAd')">
  116. <el-icon :size="40"><Plus /></el-icon>
  117. <p>点击上传广告图</p>
  118. </div>
  119. <div class="ad-actions" v-show="leftAdHover && leftAdForm.leftAdImage">
  120. <el-button type="primary" icon="Edit" circle @click="triggerUpload('leftAd')" title="更换图片" />
  121. <el-button type="danger" icon="Delete" circle @click="handleDeleteLeftAd" title="删除图片" />
  122. </div>
  123. </div>
  124. <div class="left-ad-settings" v-if="leftAdForm.leftAdImage">
  125. <el-form label-width="90px" label-position="left">
  126. <el-form-item label="跳转地址:">
  127. <el-input v-model="leftAdForm.leftAdLink" placeholder="请输入以 http:// 或 https:// 开头的地址" class="settings-input-ad" />
  128. </el-form-item>
  129. </el-form>
  130. <div class="left-ad-tip">
  131. <el-icon><InfoFilled /></el-icon>
  132. <span>鼠标悬停查看全图预览效果,点击在新窗口打开</span>
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. <!-- 模块二:轮播图管理 -->
  138. <div class="editor-section">
  139. <div class="section-header">
  140. <span class="section-title">模块二:轮播图设置</span>
  141. <span class="section-desc">尺寸要求:552 * 190,支持拖拽排序与实时状态切换</span>
  142. </div>
  143. <!-- 实时预览区 -->
  144. <div class="carousel-preview-box">
  145. <div class="preview-mockup">
  146. <el-carousel height="190px" trigger="click" indicator-position="inside" arrow="never">
  147. <el-carousel-item v-for="item in activeCarouselList" :key="item.id">
  148. <div class="carousel-slide">
  149. <img :src="item.image" alt="轮播图" />
  150. </div>
  151. </el-carousel-item>
  152. <div v-if="activeCarouselList.length === 0" class="carousel-empty">
  153. <el-icon :size="40"><Picture /></el-icon>
  154. <p>暂无启用的轮播图预览</p>
  155. </div>
  156. </el-carousel>
  157. </div>
  158. </div>
  159. <!-- 列表管理区 -->
  160. <div class="carousel-list-box">
  161. <div class="list-toolbar">
  162. <el-button type="primary" icon="Plus" @click="handleAddCarousel" class="btn-add-carousel">新增轮播图</el-button>
  163. <span class="drag-tip">提示:列表支持状态切换及拖拽排序管理</span>
  164. </div>
  165. <el-table :data="carouselList" style="width: 100%" row-key="id" border header-cell-class-name="table-header-custom">
  166. <el-table-column label="排序" width="70" align="center">
  167. <template #default="scope">
  168. <div class="rank-box">
  169. <el-icon v-if="scope.$index > 0" class="rank-icon" @click="moveRow(scope.$index, -1)"><CaretTop /></el-icon>
  170. <el-icon v-if="scope.$index < carouselList.length - 1" class="rank-icon" @click="moveRow(scope.$index, 1)"
  171. ><CaretBottom
  172. /></el-icon>
  173. </div>
  174. </template>
  175. </el-table-column>
  176. <el-table-column label="序号" type="index" width="60" align="center" />
  177. <el-table-column label="图片" width="180">
  178. <template #default="scope">
  179. <el-image :src="scope.row.image" :preview-src-list="[scope.row.image]" fit="cover" class="table-img" preview-teleported />
  180. </template>
  181. </el-table-column>
  182. <el-table-column prop="link" label="跳转地址" show-overflow-tooltip />
  183. <el-table-column label="打开方式" width="120" align="center">
  184. <template #default="scope">
  185. <el-tag :type="scope.row.target === '_blank' ? 'success' : 'info'" size="small">
  186. {{ scope.row.target === '_blank' ? '新窗口' : '当前窗口' }}
  187. </el-tag>
  188. </template>
  189. </el-table-column>
  190. <el-table-column label="状态" width="100" align="center">
  191. <template #default="scope">
  192. <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  193. </template>
  194. </el-table-column>
  195. <el-table-column label="操作" width="150" align="center">
  196. <template #default="scope">
  197. <el-button link type="primary" @click="handleEditCarousel(scope.row)">修改</el-button>
  198. <el-button link type="danger" @click="handleRemoveCarousel(scope.$index)">删除</el-button>
  199. </template>
  200. </el-table-column>
  201. </el-table>
  202. </div>
  203. </div>
  204. </div>
  205. <!-- 快捷入口编辑区 -->
  206. <div v-else-if="activeSubTab === 'quick-entry'" class="quick-entry-editor">
  207. <div class="preview-section">
  208. <div class="preview-title">快捷入口实时预览 (尺寸: 230 * 167)</div>
  209. <div class="quick-entry-preview-outer">
  210. <!-- 仿真预览模块 (230*167) -->
  211. <div class="qe-mockup-card">
  212. <div class="qe-card-header">
  213. <span class="qe-card-title">{{ quickEntrySettings.moduleName }}</span>
  214. <el-icon class="qe-header-arrow"><ArrowRight /></el-icon>
  215. </div>
  216. <div class="qe-grid-container">
  217. <div class="qe-grid-wrapper" :style="{ transform: `translateX(-${qePageIndex * 198}px)` }">
  218. <!-- 网格页 -->
  219. <div v-for="pageIdx in qePageCount" :key="pageIdx" class="qe-grid-page">
  220. <div v-for="(item, idx) in getPageItems(pageIdx - 1)" :key="idx" class="qe-item">
  221. <div class="qe-icon-wrap">
  222. <img v-if="item.icon" :src="item.icon" class="qe-icon-img" />
  223. <el-icon v-else class="qe-icon-placeholder"><Menu /></el-icon>
  224. <div v-if="item.tag" class="qe-tag-bubble">{{ item.tag }}</div>
  225. </div>
  226. <span class="qe-name">{{ item.name }}</span>
  227. </div>
  228. </div>
  229. </div>
  230. <!-- 左右翻页按钮 (仅在多于8项时显示) -->
  231. <div v-if="quickEntryList.length > 8" class="qe-nav-btns">
  232. <div v-if="qePageIndex > 0" class="qe-nav-btn prev" @click="qePageIndex--">
  233. <el-icon><ArrowLeft /></el-icon>
  234. </div>
  235. <div v-if="qePageIndex < qePageCount - 1" class="qe-nav-btn next" @click="qePageIndex++">
  236. <el-icon><ArrowRight /></el-icon>
  237. </div>
  238. </div>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. <!-- 列表设置 -->
  244. <div class="editor-section">
  245. <div class="section-header">
  246. <span class="section-title">模块基础配置</span>
  247. </div>
  248. <div class="config-form-inline">
  249. <el-form :inline="true" :model="quickEntrySettings" label-width="100px">
  250. <el-form-item label="模块名称:">
  251. <el-input v-model="quickEntrySettings.moduleName" placeholder="如:企业工作台" />
  252. </el-form-item>
  253. <el-form-item label="跳转地址:">
  254. <el-input v-model="quickEntrySettings.jumpLink" placeholder="标题点击跳转地址" style="width: 300px" />
  255. </el-form-item>
  256. </el-form>
  257. </div>
  258. <div class="section-header m-t-20">
  259. <span class="section-title">入口项管理列表</span>
  260. </div>
  261. <div class="list-toolbar">
  262. <div class="drag-tip">提示:一页显示8个,超过8个将自动启用预览区右滑功能</div>
  263. <el-button type="primary" icon="Plus" @click="handleAddQuickEntry">新增入口</el-button>
  264. </div>
  265. <el-table :data="quickEntryList" border style="width: 100%" header-cell-class-name="table-header-custom">
  266. <el-table-column label="排序" width="80" align="center">
  267. <template #default="{ $index }">
  268. <div class="rank-box">
  269. <el-icon v-if="$index > 0" class="rank-icon" @click="moveQE($index, -1)"><CaretTop /></el-icon>
  270. <el-icon v-if="$index < quickEntryList.length - 1" class="rank-icon" @click="moveQE($index, 1)"><CaretBottom /></el-icon>
  271. </div>
  272. </template>
  273. </el-table-column>
  274. <el-table-column label="图标" width="100" align="center">
  275. <template #default="{ row }">
  276. <div class="table-icon-cell">
  277. <img v-if="row.icon" :src="row.icon" style="width: 24px; height: 24px" />
  278. <el-icon v-else style="font-size: 20px; color: #ccc"><Menu /></el-icon>
  279. </div>
  280. </template>
  281. </el-table-column>
  282. <el-table-column prop="name" label="名称" width="150" />
  283. <el-table-column label="标签" width="120" align="center">
  284. <template #default="{ row }">
  285. <el-tag v-if="row.tag" type="danger" size="small" effect="plain">{{ row.tag }}</el-tag>
  286. <span v-else>-</span>
  287. </template>
  288. </el-table-column>
  289. <el-table-column prop="link" label="跳转地址" show-overflow-tooltip />
  290. <el-table-column label="状态" width="100" align="center">
  291. <template #default="{ row }">
  292. <el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
  293. </template>
  294. </el-table-column>
  295. <el-table-column label="操作" width="150" fixed="right" align="center">
  296. <template #default="{ row, $index }">
  297. <el-button type="primary" link @click="handleEditQuickEntry(row, $index)">编辑</el-button>
  298. <el-button type="danger" link @click="handleDeleteQuickEntry($index)">删除</el-button>
  299. </template>
  300. </el-table-column>
  301. </el-table>
  302. </div>
  303. </div>
  304. <!-- 分类设置编辑区 -->
  305. <div v-else-if="activeSubTab === 'category'" class="category-editor">
  306. <!-- 模块一:实时预览 (高度还原图1、2) -->
  307. <div class="editor-section">
  308. <div class="section-header">
  309. <span class="section-title">分类实时预览</span>
  310. <span class="section-desc">尺寸要求:280 * 398,悬停可查看右滑面板效果 (图1、图2)</span>
  311. </div>
  312. <div class="category-preview-container">
  313. <!-- 图1: 分类菜单 -->
  314. <div class="category-menu-mockup">
  315. <div v-for="item in categoryList.filter((c) => c.status === 1).slice(0, 10)" :key="item.id" class="menu-item">
  316. <div class="menu-icon">
  317. <img v-if="item.icon" :src="item.icon" alt="" />
  318. <el-icon v-else><Menu /></el-icon>
  319. </div>
  320. <div class="menu-name">{{ item.name }}</div>
  321. <!-- 图2: 悬停右滑面板 - CSS 控制显示 -->
  322. <div class="category-panel-mockup">
  323. <div class="panel-header-line"></div>
  324. <div class="panel-content">
  325. <!-- 顶部标签栏 -->
  326. <div class="panel-tabs">
  327. <span v-for="tag in item.tags" :key="tag.name" class="panel-tab-item">{{ tag.name }}</span>
  328. </div>
  329. <!-- 品牌位 (图3) - 绝对定位至右上角 -->
  330. <div class="brand-box">
  331. <div class="brand-main-title">
  332. {{ item.panelData.mainTitle }}<span class="brand-strong">{{ item.panelData.subTitle }}</span>
  333. </div>
  334. <div class="brand-notes">
  335. <template v-for="(note, nIdx) in item.panelData.notes" :key="nIdx">
  336. <span class="note-item">{{ note.name }}</span>
  337. <span v-if="nIdx < item.panelData.notes.length - 1" class="note-sep">|</span>
  338. </template>
  339. </div>
  340. </div>
  341. <div class="panel-body">
  342. <!-- 左侧分类列表 -->
  343. <div class="panel-main">
  344. <div v-for="group in item.panelData.groups" :key="group.title" class="category-group">
  345. <div class="group-title">{{ group.title }}</div>
  346. <div class="group-items">
  347. <span v-for="sub in group.items" :key="sub" class="group-item">{{ sub }}</span>
  348. </div>
  349. </div>
  350. </div>
  351. </div>
  352. </div>
  353. </div>
  354. </div>
  355. </div>
  356. </div>
  357. </div>
  358. <!-- 模块二:列表设置 -->
  359. <div class="editor-section">
  360. <div class="section-header">
  361. <span class="section-title">分类列表设置</span>
  362. <span class="section-desc">管理首页分类菜单的显示内容、图标、标签及关联分类</span>
  363. </div>
  364. <div class="list-toolbar">
  365. <div class="theme-color-setting-pro">
  366. <span class="label">主题色:</span>
  367. <el-color-picker v-model="categoryThemeColor" />
  368. <span class="value">{{ categoryThemeColor }}</span>
  369. </div>
  370. <el-button type="primary" icon="Plus" @click="handleAddCategory">新增分类</el-button>
  371. </div>
  372. <el-table :data="categoryList" border style="width: 100%" header-cell-class-name="table-header-custom">
  373. <el-table-column label="排序" width="70" align="center">
  374. <template #default="scope">
  375. <div class="rank-box">
  376. <el-icon v-if="scope.$index > 0" class="rank-icon" @click="moveCategory(scope.$index, -1)"><CaretTop /></el-icon>
  377. <el-icon v-if="scope.$index < categoryList.length - 1" class="rank-icon" @click="moveCategory(scope.$index, 1)"
  378. ><CaretBottom
  379. /></el-icon>
  380. </div>
  381. </template>
  382. </el-table-column>
  383. <el-table-column label="菜单名称" prop="name" min-width="200" show-overflow-tooltip />
  384. <el-table-column label="图标" width="80" align="center">
  385. <template #default="scope">
  386. <el-image v-if="scope.row.icon" :src="scope.row.icon" fit="contain" class="table-icon-preview" />
  387. <span v-else class="text-gray">-</span>
  388. </template>
  389. </el-table-column>
  390. <el-table-column label="同步分类" prop="syncCategory" width="120" />
  391. <el-table-column label="标签" min-width="180">
  392. <template #default="scope">
  393. <div class="tag-wrap">
  394. <el-tag v-for="tag in scope.row.tags" :key="tag.name" size="small" class="m-r-5">{{ tag.name }}</el-tag>
  395. </div>
  396. </template>
  397. </el-table-column>
  398. <el-table-column label="状态" width="100" align="center">
  399. <template #default="scope">
  400. <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  401. </template>
  402. </el-table-column>
  403. <el-table-column label="操作" width="150" align="center" fixed="right">
  404. <template #default="scope">
  405. <el-button link type="primary" @click="handleEditCategory(scope.row)">编辑</el-button>
  406. <el-button link type="danger" @click="handleRemoveCategory(scope.$index)">删除</el-button>
  407. </template>
  408. </el-table-column>
  409. </el-table>
  410. </div>
  411. </div>
  412. <!-- 头部分类编辑区 -->
  413. <div v-else-if="activeSubTab === 'headerCategory'" class="header-category-editor">
  414. <!-- 实时预览区 (1350*60) -->
  415. <div class="preview-section">
  416. <div class="preview-title">头部分类实时预览</div>
  417. <div class="header-preview-outer">
  418. <div class="header-preview-box">
  419. <!-- 左侧箭头 -->
  420. <div v-show="showLeftArrow" class="nav-arrow left-arrow" @click="scrollHeaderNav('left')">
  421. <el-icon><ArrowLeft /></el-icon>
  422. </div>
  423. <div class="header-nav-scroll" ref="headerNavScrollRef" @scroll="updateNavArrows">
  424. <div class="header-nav-list">
  425. <div v-for="item in headerCategoryList.filter((i) => i.status === 1)" :key="item.id" class="header-nav-item">
  426. <div class="item-icon">
  427. <img v-if="item.icon" :src="item.icon" alt="" />
  428. <el-icon v-else :style="{ color: headerThemeColor }"><Menu /></el-icon>
  429. </div>
  430. <span class="item-text" :style="{ '--hover-color': headerThemeColor }">{{ item.title }}</span>
  431. </div>
  432. </div>
  433. </div>
  434. <!-- 右侧箭头 -->
  435. <div v-show="showRightArrow" class="nav-arrow right-arrow" @click="scrollHeaderNav('right')">
  436. <el-icon><ArrowRight /></el-icon>
  437. </div>
  438. </div>
  439. </div>
  440. <div class="preview-desc">尺寸要求:1350 * 60,导航项横向排列,支持图标与主题色联动</div>
  441. </div>
  442. <!-- 列表设置 -->
  443. <div class="editor-section">
  444. <div class="section-header">
  445. <span class="section-title">分类列表设置管理</span>
  446. </div>
  447. <div class="list-toolbar">
  448. <div class="theme-color-setting-pro">
  449. <span class="label">主题色设置:</span>
  450. <el-color-picker v-model="headerThemeColor" />
  451. <span class="value">{{ headerThemeColor }}</span>
  452. </div>
  453. <el-button type="primary" icon="Plus" @click="handleAddHeaderCategory">新增分类</el-button>
  454. </div>
  455. <el-table :data="headerCategoryList" border style="width: 100%" header-cell-class-name="table-header-custom">
  456. <el-table-column label="排序" width="80" align="center">
  457. <template #default="{ $index }">
  458. <div class="rank-box">
  459. <el-icon v-if="$index > 0" class="rank-icon" @click="moveHeader($index, -1)"><CaretTop /></el-icon>
  460. <el-icon v-if="$index < headerCategoryList.length - 1" class="rank-icon" @click="moveHeader($index, 1)"><CaretBottom /></el-icon>
  461. </div>
  462. </template>
  463. </el-table-column>
  464. <el-table-column prop="title" label="分类标题名称" min-width="150" />
  465. <el-table-column label="图标" width="100" align="center">
  466. <template #default="{ row }">
  467. <img v-if="row.icon" :src="row.icon" class="table-icon-preview" style="width: 22px; height: 22px" />
  468. <span v-else>-</span>
  469. </template>
  470. </el-table-column>
  471. <el-table-column prop="link" label="跳转地址" show-overflow-tooltip />
  472. <el-table-column label="打开方式" width="120" align="center">
  473. <template #default="{ row }">
  474. <el-tag :type="row.openMode === 'new' ? 'success' : 'info'" size="small">
  475. {{ row.openMode === 'new' ? '新窗口' : '当前页' }}
  476. </el-tag>
  477. </template>
  478. </el-table-column>
  479. <el-table-column label="状态" width="100" align="center">
  480. <template #default="{ row }">
  481. <el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  482. </template>
  483. </el-table-column>
  484. <el-table-column label="操作" width="150" fixed="right" align="center">
  485. <template #default="{ row, $index }">
  486. <el-button type="primary" link @click="handleEditHeaderCategory(row, $index)">编辑</el-button>
  487. <el-button type="danger" link @click="handleDeleteHeaderCategory($index)">删除</el-button>
  488. </template>
  489. </el-table-column>
  490. </el-table>
  491. </div>
  492. </div>
  493. <!-- 广告模块编辑区 -->
  494. <div v-else-if="activeSubTab === 'ad-module'" class="ad-module-editor">
  495. <div class="preview-section">
  496. <div class="preview-title">广告模块实时预览</div>
  497. <div class="ad-preview-grid">
  498. <!-- 广告一: 企业购x百亿补贴 (484*190) -->
  499. <div class="ad-item ad-subsidy" :style="{ width: '484px', height: '190px' }">
  500. <div class="ad-header">
  501. <div class="ad-title-main" :style="getAdTitleStyle(0, 'main')">{{ getAdTitleText(0, 'main') }}</div>
  502. <div class="ad-title-sub" :style="getAdTitleStyle(0, 'sub')">{{ getAdTitleText(0, 'sub') }}</div>
  503. </div>
  504. <div class="ad-products-subsidy">
  505. <div v-for="item in adModules[0].items" :key="item.id" class="product-item">
  506. <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
  507. <div class="product-price">¥{{ item.price }}</div>
  508. </div>
  509. </div>
  510. <div class="ad-hover-mask">
  511. <el-button type="primary" size="small" @click="handleEditAd(0)">设置</el-button>
  512. </div>
  513. </div>
  514. <!-- 广告二: 企采榜单 (253*196) -->
  515. <div class="ad-item ad-ranking" :style="{ width: '253px', height: '196px' }">
  516. <div class="ad-header">
  517. <div class="ad-title-main" :style="getAdTitleStyle(1, 'main')">{{ getAdTitleText(1, 'main') }}</div>
  518. <div class="ad-title-sub" :style="getAdTitleStyle(1, 'sub')">{{ getAdTitleText(1, 'sub') }}</div>
  519. </div>
  520. <div class="ad-products-ranking">
  521. <div v-for="item in adModules[1].items" :key="item.id" class="ranking-item">
  522. <div class="ranking-badge">{{ item.tagText || '排行榜' }} ></div>
  523. <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
  524. <div class="ranking-footer">已售{{ item.salesCount }}件</div>
  525. </div>
  526. </div>
  527. <div class="ad-hover-mask">
  528. <el-button type="primary" size="small" @click="handleEditAd(1)">设置</el-button>
  529. </div>
  530. </div>
  531. <!-- 广告三: 品牌好店 (253*196) -->
  532. <div class="ad-item ad-brand" :style="{ width: '253px', height: '196px' }">
  533. <div class="ad-header">
  534. <div class="ad-title-main" :style="getAdTitleStyle(2, 'main')">{{ getAdTitleText(2, 'main') }}</div>
  535. <div class="ad-title-sub" :style="getAdTitleStyle(2, 'sub')">{{ getAdTitleText(2, 'sub') }}</div>
  536. </div>
  537. <div class="ad-brands-content">
  538. <div v-for="item in adModules[2].items" :key="item.id" class="brand-item">
  539. <div class="brand-logo"><img :src="item.imageUrl" alt="" /></div>
  540. <div class="brand-name">{{ item.tagLink || item.productName }}</div>
  541. <div class="brand-tag-btn">{{ item.tagText || '品质保障' }}</div>
  542. </div>
  543. </div>
  544. <div class="ad-hover-mask">
  545. <el-button type="primary" size="small" @click="handleEditAd(2)">设置</el-button>
  546. </div>
  547. </div>
  548. <!-- 广告四: 企业精选 (253*196) -->
  549. <div class="ad-item ad-selection" :style="{ width: '253px', height: '196px' }">
  550. <div class="ad-header">
  551. <div class="ad-title-main" :style="getAdTitleStyle(3, 'main')">{{ getAdTitleText(3, 'main') }}</div>
  552. <div class="ad-title-sub" :style="getAdTitleStyle(3, 'sub')">{{ getAdTitleText(3, 'sub') }}</div>
  553. </div>
  554. <div class="ad-products-selection">
  555. <div v-for="item in adModules[3].items" :key="item.id" class="selection-item">
  556. <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
  557. <div class="product-price-row">
  558. <span class="p-unit">¥</span>
  559. <span class="p-val">{{ item.price }}</span>
  560. </div>
  561. </div>
  562. </div>
  563. <div class="ad-hover-mask">
  564. <el-button type="primary" size="small" @click="handleEditAd(3)">设置</el-button>
  565. </div>
  566. </div>
  567. <!-- 广告五: 企业购x京东新品 (253*196) -->
  568. <div class="ad-item ad-new" :style="{ width: '253px', height: '196px' }">
  569. <div class="ad-header">
  570. <div class="ad-title-main" :style="getAdTitleStyle(4, 'main')">{{ getAdTitleText(4, 'main') }}</div>
  571. <div class="ad-title-sub" :style="getAdTitleStyle(4, 'sub')">{{ getAdTitleText(4, 'sub') }}</div>
  572. </div>
  573. <div class="ad-products-selection">
  574. <div v-for="item in adModules[4].items" :key="item.id" class="selection-item">
  575. <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
  576. <div class="product-price-row center">
  577. <span class="p-unit">¥</span>
  578. <span class="p-val">{{ item.price }}</span>
  579. </div>
  580. </div>
  581. </div>
  582. <div class="ad-hover-mask">
  583. <el-button type="primary" size="small" @click="handleEditAd(4)">设置</el-button>
  584. </div>
  585. </div>
  586. </div>
  587. <div class="preview-desc">提示:支持 5 个独立广告模块配置,悬停模块显示“设置”按钮进行内容管理</div>
  588. </div>
  589. </div>
  590. <!-- 场景方案编辑区 -->
  591. <!-- 场景方案编辑区 -->
  592. <div v-else-if="activeSubTab === 'scenario'" class="scenario-editor-container">
  593. <!-- 实时预览区 -->
  594. <div class="preview-section-standard">
  595. <div class="section-title-standard">场景方案实时预览</div>
  596. <div class="scenario-preview-outer">
  597. <div class="scenario-preview-box-clean" :style="{ '--s-theme-color': scenarioSettings.themeColor }">
  598. <!-- 左侧标题区 -->
  599. <div class="scenario-header-left">
  600. <div class="s-title-group">
  601. <span class="s-main-title">{{ scenarioSettings.mainTitle }}</span>
  602. <span class="s-sub-title-inline">{{ scenarioSettings.subTitle }}</span>
  603. </div>
  604. <div class="s-btn-wrap">
  605. <div class="s-btn-premium">
  606. {{ scenarioSettings.btnText }}
  607. <el-icon class="m-l-5"><CaretRight /></el-icon>
  608. </div>
  609. </div>
  610. </div>
  611. <!-- 右侧方案卡片 -->
  612. <div class="scenario-cards-wrap">
  613. <div
  614. v-for="(item, idx) in scenarioList"
  615. :key="item.id"
  616. class="scenario-card-premium"
  617. :class="{ 'hidden-card-fourth': idx === 3 }"
  618. :style="{ backgroundColor: hexToRgba(item.bgColor, item.opacity) }"
  619. >
  620. <div class="card-top-header">
  621. <div class="card-titles-group">
  622. <span class="card-main-title" :style="{ color: item.titleColor }">{{ item.title }}</span>
  623. <span class="card-sub-title" :style="{ color: item.subTitleColor }">{{ item.subTitle }}</span>
  624. </div>
  625. <div class="card-arrow-icon" :style="{ backgroundColor: item.titleColor }">
  626. <el-icon><ArrowRight /></el-icon>
  627. </div>
  628. </div>
  629. <div class="card-image-content">
  630. <img :src="item.image || defaultPlaceholder" alt="" />
  631. </div>
  632. </div>
  633. </div>
  634. </div>
  635. </div>
  636. <div class="preview-tip">尺寸要求:1600 * 158,支持响应式隐藏及主题色联动</div>
  637. </div>
  638. <!-- 全局配置 -->
  639. <div class="config-section-standard">
  640. <div class="section-title-standard">场景解决方案全局设置</div>
  641. <div class="settings-form-standard">
  642. <el-form :model="scenarioSettings" label-width="120px" label-position="left">
  643. <el-form-item label="场景主标题:">
  644. <el-input v-model="scenarioSettings.mainTitle" placeholder="请输入主标题" style="width: 400px" />
  645. </el-form-item>
  646. <el-form-item label="场景副标题:">
  647. <el-input v-model="scenarioSettings.subTitle" placeholder="请输入副标题" style="width: 400px" />
  648. </el-form-item>
  649. <el-form-item label="按钮文字:">
  650. <el-input v-model="scenarioSettings.btnText" placeholder="请输入按钮文字" style="width: 400px" />
  651. </el-form-item>
  652. <el-form-item label="跳转链接:">
  653. <el-input v-model="scenarioSettings.jumpLink" placeholder="请输入跳转地址" style="width: 400px" />
  654. </el-form-item>
  655. <el-form-item label="主题背景色:">
  656. <div class="theme-color-setting-pro">
  657. <el-color-picker v-model="scenarioSettings.themeColor" />
  658. <span class="value">{{ scenarioSettings.themeColor }}</span>
  659. </div>
  660. </el-form-item>
  661. </el-form>
  662. </div>
  663. </div>
  664. <!-- 列表管理 -->
  665. <div class="config-section-standard">
  666. <div class="section-title-standard">方案卡片设置管理</div>
  667. <el-table :data="scenarioList" border style="width: 100%" header-cell-class-name="table-header-custom" class="standard-table">
  668. <el-table-column label="排序" width="80" align="center">
  669. <template #default="{ $index }">
  670. <div class="rank-action-btns">
  671. <el-icon v-if="$index > 0" class="rank-btn-mini" @click="moveScenario($index, -1)"><ArrowUp /></el-icon>
  672. <el-icon v-if="$index < scenarioList.length - 1" class="rank-btn-mini" @click="moveScenario($index, 1)"><ArrowDown /></el-icon>
  673. </div>
  674. </template>
  675. </el-table-column>
  676. <el-table-column label="分类方案名称" min-width="250">
  677. <template #default="{ row }">
  678. <div class="flex-column">
  679. <span :style="{ color: row.titleColor, fontSize: '14px', fontWeight: 'bold' }">{{ row.title }}</span>
  680. <span class="text-gray" style="font-size: 12px; margin-top: 4px">{{ row.subTitle }}</span>
  681. </div>
  682. </template>
  683. </el-table-column>
  684. <el-table-column label="图标/封面图" width="200" align="center">
  685. <template #default="{ row }">
  686. <el-image :src="row.image" fit="cover" style="width: 160px; height: 48px; border-radius: 4px" />
  687. </template>
  688. </el-table-column>
  689. <el-table-column prop="link" label="跳转链接" min-width="300" show-overflow-tooltip />
  690. <el-table-column label="操作" width="120" align="center" fixed="right">
  691. <template #default="{ row, $index }">
  692. <el-button type="primary" link @click="handleEditScenario(row, $index)">编辑内容</el-button>
  693. </template>
  694. </el-table-column>
  695. </el-table>
  696. </div>
  697. </div>
  698. <!-- 推荐设置编辑区 -->
  699. <div v-else-if="activeSubTab === 'recommend'" class="recommend-editor-container">
  700. <!-- 实时预览区 -->
  701. <div class="preview-section-standard">
  702. <div class="section-title-standard">为你推荐实时预览</div>
  703. <div class="recommend-preview-outer">
  704. <div class="recommend-preview-container">
  705. <!-- 左侧箭头 -->
  706. <div v-if="recShowLeft" class="recommend-nav-btn prev" @click="scrollRecommend('left')">
  707. <el-icon><CaretLeft /></el-icon>
  708. </div>
  709. <div
  710. ref="recommendScrollRef"
  711. class="recommend-preview-box"
  712. :style="{ '--r-theme-color': recommendThemeColor }"
  713. @scroll="updateRecArrows"
  714. >
  715. <div
  716. v-for="item in activeRecommendList"
  717. :key="item.id"
  718. :class="['recommend-item', { active: recommendActiveId === item.id }]"
  719. @click="recommendActiveId = item.id"
  720. >
  721. <div class="recommend-icon">
  722. <img :src="item.icon || defaultPlaceholder" alt="" />
  723. </div>
  724. <div class="recommend-text">
  725. <div class="r-main-title" :style="{ color: recommendActiveId === item.id ? recommendThemeColor : '#333' }">{{ item.name }}</div>
  726. <div
  727. class="r-sub-title"
  728. :style="{
  729. color: recommendActiveId === item.id ? recommendThemeColor : '#999',
  730. opacity: recommendActiveId === item.id ? 0.8 : 1
  731. }"
  732. >
  733. {{ item.subTitle }}
  734. </div>
  735. </div>
  736. </div>
  737. </div>
  738. <!-- 右侧箭头 -->
  739. <div v-if="recShowRight" class="recommend-nav-btn next" @click="scrollRecommend('right')">
  740. <el-icon><CaretRight /></el-icon>
  741. </div>
  742. </div>
  743. </div>
  744. <div class="preview-desc">尺寸要求:1600 * 88,分类图标 32 * 32,支持响应式平滑滚动及选中态色值联动</div>
  745. </div>
  746. <!-- 配置表单区域 -->
  747. <div class="config-section-standard">
  748. <div class="section-title-standard">为你推荐配置设置</div>
  749. <el-form label-width="120px" inline class="m-b-20">
  750. <el-form-item label="主题选中色调:">
  751. <div class="theme-color-setting-pro">
  752. <el-color-picker v-model="recommendThemeColor" />
  753. <span class="value">{{ recommendThemeColor }}</span>
  754. </div>
  755. </el-form-item>
  756. <el-form-item label="商品列表主题色:">
  757. <div class="theme-color-setting-pro">
  758. <el-color-picker v-model="recommendProductThemeColor" />
  759. <span class="value">{{ recommendProductThemeColor }}</span>
  760. </div>
  761. </el-form-item>
  762. <el-form-item>
  763. <el-button type="primary" icon="Plus" @click="handleAddRecommend">新增推荐分类</el-button>
  764. </el-form-item>
  765. </el-form>
  766. <el-table :data="recommendList" border style="width: 100%" header-cell-class-name="table-header-custom" class="standard-table">
  767. <el-table-column label="位置" width="80" align="center">
  768. <template #default="{ $index }">
  769. <div class="rank-action-btns">
  770. <el-icon v-if="$index > 0" class="rank-btn-mini" @click="moveRecommend($index, -1)"><ArrowUp /></el-icon>
  771. <el-icon v-if="$index < recommendList.length - 1" class="rank-btn-mini" @click="moveRecommend($index, 1)"><ArrowDown /></el-icon>
  772. </div>
  773. </template>
  774. </el-table-column>
  775. <el-table-column label="分类名称" min-width="120">
  776. <template #default="{ row }">
  777. <span style="font-weight: bold; color: #333">{{ row.name }}</span>
  778. </template>
  779. </el-table-column>
  780. <el-table-column prop="subTitle" label="副标题" min-width="150" />
  781. <el-table-column label="图标" width="100" align="center">
  782. <template #default="{ row }">
  783. <div class="table-icon-preview">
  784. <img :src="row.icon || defaultPlaceholder" class="row-icon" />
  785. </div>
  786. </template>
  787. </el-table-column>
  788. <el-table-column label="数据类型" width="120" align="center">
  789. <template #default="{ row }">
  790. <el-tag :type="row.type === 'category' ? 'success' : 'warning'">
  791. {{ row.type === 'category' ? '分类映射' : '商品自选' }}
  792. </el-tag>
  793. </template>
  794. </el-table-column>
  795. <el-table-column label="关联类目/商品数" min-width="180">
  796. <template #default="{ row }">
  797. <div v-if="row.type === 'category'" class="text-gray">{{ row.categoryLabel || '未设置' }}</div>
  798. <div v-else class="text-gray">已选 {{ row.selectedProducts?.length || 0 }} 个商品</div>
  799. </template>
  800. </el-table-column>
  801. <el-table-column label="启用状态" width="100" align="center">
  802. <template #default="{ row }">
  803. <el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  804. </template>
  805. </el-table-column>
  806. <el-table-column label="操作" width="220" align="center" fixed="right">
  807. <template #default="{ row, $index }">
  808. <el-button type="primary" link @click="handleEditRecommend(row, $index)">编辑</el-button>
  809. <el-button v-if="row.type === 'select'" type="primary" link @click="openRecommendProductSelect($index)">选商品</el-button>
  810. <el-button type="danger" link @click="handleDeleteRecommend($index)">删除</el-button>
  811. </template>
  812. </el-table-column>
  813. </el-table>
  814. </div>
  815. </div>
  816. </div>
  817. <!-- 广告模块设置弹窗 -->
  818. <el-dialog
  819. v-model="adDialogVisible"
  820. :title="`设置广告模块 - ${adModules[currentAdIdx]?.title}`"
  821. width="1050px"
  822. custom-class="ad-setup-dialog"
  823. destroy-on-close
  824. >
  825. <el-form :model="adForm" label-width="100px" class="dialog-form-inner">
  826. <el-form-item label="模块标题:">
  827. <div class="theme-color-setting-pro">
  828. <el-input v-model="adForm.title" placeholder="请输入主标题" style="width: 300px" />
  829. <el-color-picker v-model="adForm.titleColor" />
  830. <span class="value">{{ adForm.titleColor }}</span>
  831. </div>
  832. </el-form-item>
  833. <el-form-item label="副标题:">
  834. <div class="theme-color-setting-pro">
  835. <el-input v-model="adForm.subTitle" placeholder="请输入副标题" style="width: 300px" />
  836. <el-color-picker v-model="adForm.titleColor" />
  837. <span class="value">{{ adForm.titleColor }}</span>
  838. </div>
  839. </el-form-item>
  840. <div class="config-subtitle">商品/品牌列表 (固定 {{ adForm.items.length }} 项)</div>
  841. <el-table :data="adForm.items" border style="width: 100%" class="m-t-10">
  842. <el-table-column label="位置" width="60" align="center">
  843. <template #default="scope">{{ scope.$index + 1 }}</template>
  844. </el-table-column>
  845. <el-table-column label="图片" width="100" align="center">
  846. <template #default="{ row }">
  847. <img :src="row.imageUrl" style="width: 50px; height: 50px; object-fit: contain" />
  848. </template>
  849. </el-table-column>
  850. <!-- 通用商品信息列 (百亿补贴、企采精选、京东新品) -->
  851. <el-table-column v-if="currentAdIdx === 0 || currentAdIdx === 3 || currentAdIdx === 4" label="商品信息" min-width="180">
  852. <template #default="{ row }">
  853. <div class="table-info-cell">
  854. <div class="info-name">{{ row.productName || '未选择' }}</div>
  855. <div class="info-price">价格:¥{{ row.price }}</div>
  856. <div class="info-id">ID: {{ row.id || '-' }}</div>
  857. </div>
  858. </template>
  859. </el-table-column>
  860. <!-- 企采榜单 专属列 -->
  861. <template v-if="currentAdIdx === 1">
  862. <el-table-column label="商品信息" min-width="150">
  863. <template #default="{ row }">
  864. <div class="info-name">{{ row.productName || '未选择' }}</div>
  865. <div class="info-id">ID: {{ row.id || '-' }}</div>
  866. </template>
  867. </el-table-column>
  868. <el-table-column label="排行标签" width="220">
  869. <template #default="{ row }">
  870. <el-input v-model="row.tagText" placeholder="标签文字" size="small" class="m-b-5" />
  871. <el-input v-model="row.tagLink" placeholder="跳转链接" size="small" />
  872. </template>
  873. </el-table-column>
  874. <el-table-column label="销量数据" width="130">
  875. <template #default="{ row }">
  876. <el-input v-model="row.salesCount" placeholder="销量" size="small">
  877. <template #prepend>已售</template>
  878. </el-input>
  879. </template>
  880. </el-table-column>
  881. </template>
  882. <!-- 品牌好店 专属列 -->
  883. <template v-if="currentAdIdx === 2">
  884. <el-table-column label="品牌名称" min-width="180">
  885. <template #default="{ row }">
  886. <div class="brand-name-display">{{ row.productName || '未选择' }}</div>
  887. <div class="info-id m-t-5">ID: {{ row.id || '-' }}</div>
  888. </template>
  889. </el-table-column>
  890. <el-table-column label="品牌标签" width="300">
  891. <template #default="{ row }">
  892. <div class="flex-column gap-10">
  893. <el-input v-model="row.tagText" placeholder="标签文字 (如: 品质保障)" />
  894. <el-input v-model="row.tagLink" placeholder="描述文字 (控制预览品牌名称)" />
  895. </div>
  896. </template>
  897. </el-table-column>
  898. </template>
  899. <el-table-column label="操作" width="110" align="center" fixed="right">
  900. <template #default="scope">
  901. <el-button type="primary" link @click="openProductSelect(scope.$index)">
  902. <el-icon class="m-r-5"><Edit /></el-icon>
  903. {{ scope.row?.id ? '修改' : '选择' }}
  904. </el-button>
  905. </template>
  906. </el-table-column>
  907. </el-table>
  908. </el-form>
  909. <template #footer>
  910. <el-button @click="adDialogVisible = false">取消</el-button>
  911. <el-button type="primary" @click="submitAdForm">保存设置</el-button>
  912. </template>
  913. </el-dialog>
  914. <!-- 选择商品/品牌抽屉 (加宽版) -->
  915. <el-drawer v-model="selectDialogVisible" :title="currentAdIdx === 2 ? '选择品牌' : '选择商品'" size="850px" append-to-body>
  916. <div class="select-dialog-content">
  917. <el-input
  918. v-model="productQueryParams.itemName"
  919. :placeholder="currentAdIdx === 2 ? '输入品牌名称搜索' : '输入商品名称或ID搜索'"
  920. prefix-icon="Search"
  921. clearable
  922. style="margin-bottom: 20px"
  923. @input="getProductList"
  924. />
  925. <div class="select-list">
  926. <div
  927. v-for="item in pagedSelectList"
  928. :key="item.id"
  929. class="select-item-row"
  930. :class="{ active: selectedTempId === item.id }"
  931. @click="selectedTempId = item.id"
  932. >
  933. <img :src="item.image" class="select-item-img" />
  934. <div class="select-item-info">
  935. <div class="select-item-name">{{ item.name }}</div>
  936. <div v-if="currentAdIdx !== 2" class="select-item-price">¥{{ item.price }}</div>
  937. <div class="select-item-id">ID: {{ item.id }}</div>
  938. </div>
  939. <el-radio v-model="selectedTempId" :label="item.id"><span></span></el-radio>
  940. </div>
  941. </div>
  942. <!-- 分页组件 -->
  943. <div class="select-pagination">
  944. <el-pagination
  945. v-model:current-page="selectCurrentPage"
  946. v-model:page-size="selectPageSize"
  947. :total="productTotal"
  948. :page-sizes="[8, 12, 20, 50]"
  949. layout="total, sizes, prev, pager, next, jumper"
  950. background
  951. @current-change="onProductPageChange"
  952. @size-change="onProductPageSizeChange"
  953. />
  954. </div>
  955. </div>
  956. <template #footer>
  957. <div style="flex: auto; text-align: right; padding: 20px">
  958. <el-button @click="selectDialogVisible = false">取消</el-button>
  959. <el-button type="primary" @click="confirmSelect">确定选择</el-button>
  960. </div>
  961. </template>
  962. </el-drawer>
  963. <!-- 轮播图编辑弹窗 -->
  964. <el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增轮播图' : '修改轮播图'" width="800px" destroy-on-close>
  965. <el-form :model="carouselForm" label-width="100px" class="dialog-form-inner">
  966. <el-form-item label="轮播图片:">
  967. <div class="upload-placeholder" @click="triggerUpload('carousel')">
  968. <img v-if="carouselForm.image" :src="carouselForm.image" class="form-preview-img" />
  969. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  970. </div>
  971. <div class="upload-tip">推荐尺寸:552 * 190,支持上传本地图片</div>
  972. </el-form-item>
  973. <el-form-item label="跳转地址:">
  974. <el-input v-model="carouselForm.link" placeholder="请输入以 http:// 或 https:// 开头的地址" />
  975. </el-form-item>
  976. <el-form-item label="打开方式:">
  977. <el-radio-group v-model="carouselForm.target">
  978. <el-radio label="_self">当前窗口</el-radio>
  979. <el-radio label="_blank">新窗口</el-radio>
  980. </el-radio-group>
  981. </el-form-item>
  982. <el-form-item label="启用状态:">
  983. <el-switch v-model="carouselForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  984. </el-form-item>
  985. </el-form>
  986. <template #footer>
  987. <el-button @click="dialogVisible = false">取消</el-button>
  988. <el-button type="primary" @click="submitCarouselForm">确定</el-button>
  989. </template>
  990. </el-dialog>
  991. <!-- 分类设置编辑弹窗 -->
  992. <el-dialog v-model="categoryDialogVisible" :title="categoryDialogType === 'add' ? '新增分类' : '修改分类'" width="900px" destroy-on-close>
  993. <el-form :model="categoryForm" label-width="100px" class="dialog-form-inner">
  994. <el-tabs type="border-card">
  995. <el-tab-pane label="基础设置">
  996. <el-form-item label="菜单名称:">
  997. <el-input v-model="categoryForm.name" placeholder="如:办公电脑 / 办公打印 / 电脑组件" />
  998. </el-form-item>
  999. <el-form-item label="图标:">
  1000. <div class="upload-placeholder-square" @click="triggerUpload('categoryIcon')">
  1001. <img v-if="categoryForm.icon" :src="categoryForm.icon" class="form-preview-img-square" />
  1002. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1003. </div>
  1004. <div class="upload-tip">推荐尺寸:16 * 16,支持上传透明背景 PNG</div>
  1005. </el-form-item>
  1006. <el-form-item label="同步分类:">
  1007. <el-select
  1008. v-model="categoryForm.syncCategory"
  1009. placeholder="请选择或输入关联的一级分类"
  1010. style="width: 100%"
  1011. filterable
  1012. allow-create
  1013. default-first-option
  1014. >
  1015. <el-option v-for="opt in syncCategoryOptions" :key="opt" :label="opt" :value="opt" />
  1016. </el-select>
  1017. </el-form-item>
  1018. <el-form-item label="标签设置:">
  1019. <div class="notes-config-list">
  1020. <div v-for="(tag, index) in categoryForm.tags" :key="index" class="note-config-row">
  1021. <el-input v-model="tag.name" placeholder="标签名称" style="width: 120px" />
  1022. <el-input v-model="tag.link" placeholder="跳转地址" style="flex: 1" />
  1023. <el-button type="danger" icon="Delete" circle plain size="small" @click="removeCategoryTag(index)" />
  1024. </div>
  1025. <el-button type="primary" icon="Plus" link @click="addCategoryTag">添加标签</el-button>
  1026. </div>
  1027. <div class="field-tip">用于在右滑面板顶部显示的业务标签,支持配置独立跳转地址</div>
  1028. </el-form-item>
  1029. <el-form-item label="启用状态:">
  1030. <el-switch v-model="categoryForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1031. </el-form-item>
  1032. </el-tab-pane>
  1033. <el-tab-pane label="右滑面板配置">
  1034. <div class="panel-config-section">
  1035. <div class="config-subtitle">品牌位设置 (图3)</div>
  1036. <el-row :gutter="20">
  1037. <el-col :span="12">
  1038. <el-form-item label="主标题:">
  1039. <el-input v-model="categoryForm.panelData.mainTitle" placeholder="如:京东" />
  1040. </el-form-item>
  1041. </el-col>
  1042. <el-col :span="12">
  1043. <el-form-item label="副标题:">
  1044. <el-input v-model="categoryForm.panelData.subTitle" placeholder="如:3C数码" />
  1045. </el-form-item>
  1046. </el-col>
  1047. </el-row>
  1048. <el-form-item label="便签列表:">
  1049. <div class="notes-config-list">
  1050. <div v-for="(note, index) in categoryForm.panelData.notes" :key="index" class="note-config-row">
  1051. <el-input v-model="note.name" placeholder="便签名称" style="width: 120px" />
  1052. <el-input v-model="note.link" placeholder="跳转地址" style="flex: 1" />
  1053. <el-button type="danger" icon="Delete" circle plain size="small" @click="removePanelNote(index)" />
  1054. </div>
  1055. <el-button type="primary" icon="Plus" link @click="addPanelNote">添加便签</el-button>
  1056. </div>
  1057. </el-form-item>
  1058. </div>
  1059. <div class="panel-config-section m-t-20">
  1060. <div class="config-subtitle">分类分组设置</div>
  1061. <div class="field-tip">目前支持在代码中静态配置,弹窗界面待进一步完善。</div>
  1062. </div>
  1063. </el-tab-pane>
  1064. </el-tabs>
  1065. </el-form>
  1066. <template #footer>
  1067. <el-button @click="categoryDialogVisible = false">取消</el-button>
  1068. <el-button type="primary" @click="submitCategoryForm">确定保存</el-button>
  1069. </template>
  1070. </el-dialog>
  1071. <!-- 头部分类编辑弹窗 -->
  1072. <el-dialog v-model="headerDialogVisible" :title="headerEditIndex > -1 ? '编辑头部分类' : '新增头部分类'" width="600px" destroy-on-close>
  1073. <el-form :model="headerForm" label-width="100px" class="dialog-form-inner">
  1074. <el-form-item label="分类名称:">
  1075. <el-input v-model="headerForm.title" placeholder="如:公共采购" />
  1076. </el-form-item>
  1077. <el-form-item label="分类图标:">
  1078. <div class="upload-placeholder-square" @click="triggerUpload('headerIcon')">
  1079. <img v-if="headerForm.icon" :src="headerForm.icon" class="form-preview-img-square" style="width: 20px; height: 20px" />
  1080. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1081. </div>
  1082. <div class="upload-tip">建议尺寸:20*20,透明背景 PNG</div>
  1083. </el-form-item>
  1084. <el-form-item label="跳转地址:">
  1085. <el-input v-model="headerForm.link" placeholder="请输入跳转链接" />
  1086. </el-form-item>
  1087. <el-form-item label="打开方式:">
  1088. <el-radio-group v-model="headerForm.openMode">
  1089. <el-radio label="current">当前页</el-radio>
  1090. <el-radio label="new">新窗口</el-radio>
  1091. </el-radio-group>
  1092. </el-form-item>
  1093. <el-form-item label="启用状态:">
  1094. <el-switch v-model="headerForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1095. </el-form-item>
  1096. </el-form>
  1097. <template #footer>
  1098. <el-button @click="headerDialogVisible = false">取消</el-button>
  1099. <el-button type="primary" @click="submitHeaderForm">确定保存</el-button>
  1100. </template>
  1101. </el-dialog>
  1102. <!-- 场景方案编辑弹窗 -->
  1103. <!-- 已选商品管理抽屉 (占据 45% 屏幕) -->
  1104. <el-drawer
  1105. v-model="selectedProductDialogVisible"
  1106. :title="`已选商品管理 - ${recommendList[currentRecommendIndex]?.name}`"
  1107. size="45%"
  1108. direction="rtl"
  1109. custom-class="selected-products-drawer"
  1110. destroy-on-close
  1111. >
  1112. <div class="drawer-content-wrapper">
  1113. <div class="selected-products-header">
  1114. <div class="left-actions">
  1115. <el-button type="primary" icon="Plus" @click="openProductDrawer">添加商品</el-button>
  1116. <el-button
  1117. type="danger"
  1118. plain
  1119. icon="Delete"
  1120. :disabled="!recommendList[currentRecommendIndex]?.selectedProducts?.length"
  1121. @click="batchRemoveSelectedProducts"
  1122. >批量移除</el-button
  1123. >
  1124. </div>
  1125. <div class="right-info">
  1126. 共 <span class="count">{{ recommendList[currentRecommendIndex]?.selectedProducts?.length || 0 }}</span> 个商品
  1127. </div>
  1128. </div>
  1129. <el-table
  1130. ref="selectedProductsTableRef"
  1131. :data="pagedSelectedProducts"
  1132. border
  1133. height="calc(100vh - 280px)"
  1134. style="width: 100%"
  1135. header-cell-class-name="table-header-custom"
  1136. class="standard-table"
  1137. >
  1138. <el-table-column type="selection" width="55" align="center" />
  1139. <el-table-column label="商品图片" width="100" align="center">
  1140. <template #default="{ row }">
  1141. <el-image :src="row.image" style="width: 50px; height: 50px; border-radius: 4px; border: 1px solid #f0f0f0" fit="contain" />
  1142. </template>
  1143. </el-table-column>
  1144. <el-table-column prop="name" label="商品名称" min-width="250" show-overflow-tooltip />
  1145. <el-table-column prop="id" label="商品ID" width="120" align="center" />
  1146. <el-table-column label="单价" width="120" align="center">
  1147. <template #default="{ row }">
  1148. <span class="price-text">¥{{ row.price }}</span>
  1149. </template>
  1150. </el-table-column>
  1151. <el-table-column label="操作" width="100" align="center" fixed="right">
  1152. <template #default="{ $index }">
  1153. <el-button type="danger" link @click="removeSelectedProduct($index)">移除</el-button>
  1154. </template>
  1155. </el-table-column>
  1156. <template #empty>
  1157. <div class="empty-placeholder">
  1158. <el-empty description="暂未添加任何商品" :image-size="160">
  1159. <el-button type="primary" size="large" @click="openProductDrawer">去添加商品</el-button>
  1160. </el-empty>
  1161. </div>
  1162. </template>
  1163. </el-table>
  1164. <div class="drawer-pagination">
  1165. <el-pagination
  1166. v-model:current-page="selectedCurrentPage"
  1167. v-model:page-size="selectedPageSize"
  1168. :total="recommendList[currentRecommendIndex]?.selectedProducts?.length || 0"
  1169. :page-sizes="[10, 20, 50, 100]"
  1170. layout="total, sizes, prev, pager, next, jumper"
  1171. background
  1172. />
  1173. </div>
  1174. </div>
  1175. <template #footer>
  1176. <div class="drawer-footer-actions">
  1177. <el-button @click="selectedProductDialogVisible = false">取消</el-button>
  1178. <el-button type="primary" @click="submitSelectedProducts">确定保存</el-button>
  1179. </div>
  1180. </template>
  1181. </el-drawer>
  1182. <!-- 商品多选选择抽屉 (占据 1/3 屏幕) -->
  1183. <el-drawer
  1184. v-model="productSelectionDrawerVisible"
  1185. title="从商品库选择商品"
  1186. size="45%"
  1187. direction="rtl"
  1188. destroy-on-close
  1189. custom-class="product-selection-drawer"
  1190. >
  1191. <div class="drawer-content-wrapper">
  1192. <div class="drawer-search-bar">
  1193. <el-input v-model="productQueryParams.itemName" placeholder="输入商品名称或ID搜索" prefix-icon="Search" clearable @input="getProductList" />
  1194. </div>
  1195. <div class="drawer-stat-bar">
  1196. <el-icon class="info-icon"><InfoFilled /></el-icon>
  1197. <span
  1198. >已勾选 <span class="highlight">{{ drawerSelection.length }}</span> 个商品</span
  1199. >
  1200. </div>
  1201. <el-table
  1202. ref="drawerTableRef"
  1203. :data="pagedSelectList"
  1204. border
  1205. height="calc(100vh - 280px)"
  1206. style="width: 100%"
  1207. row-key="id"
  1208. @selection-change="handleDrawerSelectionChange"
  1209. >
  1210. <el-table-column type="selection" width="50" align="center" :reserve-selection="true" />
  1211. <el-table-column label="商品信息" min-width="200">
  1212. <template #default="{ row }">
  1213. <div class="drawer-product-info">
  1214. <el-image :src="row.image" class="mini-img" fit="contain" />
  1215. <div class="detail">
  1216. <div class="name">{{ row.name }}</div>
  1217. <div class="id">ID: {{ row.id }}</div>
  1218. <div class="price">¥{{ row.price }}</div>
  1219. </div>
  1220. </div>
  1221. </template>
  1222. </el-table-column>
  1223. </el-table>
  1224. <div class="drawer-pagination">
  1225. <el-pagination
  1226. v-model:current-page="selectCurrentPage"
  1227. v-model:page-size="selectPageSize"
  1228. :total="productTotal"
  1229. :page-sizes="[10, 20, 50, 100]"
  1230. layout="total, sizes, prev, pager, next, jumper"
  1231. background
  1232. @current-change="onProductPageChange"
  1233. @size-change="onProductPageSizeChange"
  1234. />
  1235. </div>
  1236. </div>
  1237. <template #footer>
  1238. <div class="drawer-footer-actions">
  1239. <el-button @click="productSelectionDrawerVisible = false">取消</el-button>
  1240. <el-button type="primary" :disabled="!drawerSelection.length" @click="confirmDrawerSelection"
  1241. >确认添加 ({{ drawerSelection.length }})</el-button
  1242. >
  1243. </div>
  1244. </template>
  1245. </el-drawer>
  1246. <el-dialog v-model="scenarioDialogVisible" title="编辑场景方案卡片" width="600px" destroy-on-close>
  1247. <el-form :model="scenarioForm" label-width="100px" class="dialog-form-inner">
  1248. <el-form-item label="主标题:">
  1249. <div class="flex-center gap-10">
  1250. <el-input v-model="scenarioForm.title" style="width: 200px" />
  1251. <div class="theme-color-setting-pro">
  1252. <el-color-picker v-model="scenarioForm.titleColor" />
  1253. <span class="value">{{ scenarioForm.titleColor }}</span>
  1254. </div>
  1255. </div>
  1256. </el-form-item>
  1257. <el-form-item label="副标题:">
  1258. <div class="flex-center gap-10">
  1259. <el-input v-model="scenarioForm.subTitle" style="width: 200px" />
  1260. <div class="theme-color-setting-pro">
  1261. <el-color-picker v-model="scenarioForm.subTitleColor" />
  1262. <span class="value">{{ scenarioForm.subTitleColor }}</span>
  1263. </div>
  1264. </div>
  1265. </el-form-item>
  1266. <el-form-item label="卡片背景:">
  1267. <div class="flex-center gap-10">
  1268. <div class="theme-color-setting-pro">
  1269. <el-color-picker v-model="scenarioForm.bgColor" />
  1270. <span class="value">{{ scenarioForm.bgColor }}</span>
  1271. </div>
  1272. <span class="text-gray m-l-10">不透明度:</span>
  1273. <el-slider v-model="scenarioForm.opacity" :min="0" :max="1" :step="0.05" style="width: 120px" />
  1274. <span class="value m-l-5">{{ Math.round(scenarioForm.opacity * 100) }}%</span>
  1275. </div>
  1276. </el-form-item>
  1277. <el-form-item label="封面图片:">
  1278. <div class="upload-placeholder-square" style="width: 272px; height: 80px" @click="triggerUpload('scenarioImg')">
  1279. <img v-if="scenarioForm.image" :src="scenarioForm.image" style="width: 100%; height: 100%; object-fit: cover" />
  1280. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1281. </div>
  1282. <div class="upload-tip">建议尺寸:272 * 80</div>
  1283. </el-form-item>
  1284. <el-form-item label="跳转链接:">
  1285. <el-input v-model="scenarioForm.link" placeholder="请输入跳转地址" />
  1286. </el-form-item>
  1287. </el-form>
  1288. <template #footer>
  1289. <el-button @click="scenarioDialogVisible = false">取消</el-button>
  1290. <el-button type="primary" @click="submitScenarioForm">保存修改</el-button>
  1291. </template>
  1292. </el-dialog>
  1293. <!-- 推荐设置编辑弹窗 -->
  1294. <el-dialog v-model="recommendDialogVisible" :title="recommendEditIndex > -1 ? '修改推荐分类' : '新增推荐分类'" width="600px" destroy-on-close>
  1295. <el-form :model="recommendForm" label-width="110px" class="dialog-form-inner">
  1296. <el-form-item label="主标题:">
  1297. <el-input v-model="recommendForm.name" placeholder="请输入分类名称" maxlength="6" show-word-limit />
  1298. </el-form-item>
  1299. <el-form-item label="副标题:">
  1300. <el-input v-model="recommendForm.subTitle" placeholder="请输入副标题" maxlength="12" show-word-limit />
  1301. </el-form-item>
  1302. <el-form-item label="分类图标:">
  1303. <div class="upload-placeholder-square" style="width: 64px; height: 64px" @click="triggerUpload('recommendIcon')">
  1304. <img v-if="recommendForm.icon" :src="recommendForm.icon" style="width: 100%; height: 100%; object-fit: contain" />
  1305. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1306. </div>
  1307. <div class="upload-tip">建议尺寸:32 * 32,透明背景 PNG</div>
  1308. </el-form-item>
  1309. <el-form-item label="数据类型:">
  1310. <el-radio-group v-model="recommendForm.type">
  1311. <el-radio label="select">商品自选</el-radio>
  1312. <el-radio label="category">分类映射</el-radio>
  1313. </el-radio-group>
  1314. </el-form-item>
  1315. <el-form-item v-if="recommendForm.type === 'category'" label="映射分类:">
  1316. <el-cascader
  1317. v-model="recommendForm.categoryValue"
  1318. :options="mockCategoryOptions"
  1319. :props="{ checkStrictly: true, expandTrigger: 'hover' }"
  1320. placeholder="请选择一级/二级/三级分类"
  1321. style="width: 100%"
  1322. filterable
  1323. clearable
  1324. @change="handleRecommendCategoryChange"
  1325. />
  1326. <div class="field-tip">选择后,该页签将自动抓取对应分类下的最新商品</div>
  1327. </el-form-item>
  1328. <el-form-item label="启用状态:">
  1329. <el-switch v-model="recommendForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1330. </el-form-item>
  1331. </el-form>
  1332. <template #footer>
  1333. <el-button @click="recommendDialogVisible = false">取消</el-button>
  1334. <el-button type="primary" @click="submitRecommendForm">保存设置</el-button>
  1335. </template>
  1336. </el-dialog>
  1337. <!-- 快捷入口编辑弹窗 -->
  1338. <el-dialog
  1339. v-model="quickEntryDialogVisible"
  1340. :title="quickEntryDialogType === 'add' ? '新增快捷入口' : '编辑快捷入口'"
  1341. width="600px"
  1342. append-to-body
  1343. destroy-on-close
  1344. >
  1345. <el-form :model="quickEntryForm" label-width="100px" class="dialog-form-inner">
  1346. <el-form-item label="入口名称:">
  1347. <el-input v-model="quickEntryForm.name" placeholder="请输入入口名称(建议4-5个字)" maxlength="6" show-word-limit />
  1348. </el-form-item>
  1349. <el-form-item label="图标:">
  1350. <div class="upload-placeholder-square" @click="triggerUpload('quickEntryIcon')">
  1351. <img v-if="quickEntryForm.icon" :src="quickEntryForm.icon" class="form-preview-img-square" style="width: 24px; height: 24px" />
  1352. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1353. </div>
  1354. <div class="upload-tip">建议尺寸: 48*48, 格式: PNG/SVG</div>
  1355. </el-form-item>
  1356. <el-form-item label="标签文字:">
  1357. <el-input v-model="quickEntryForm.tag" placeholder="如:返100" maxlength="4" style="width: 200px" />
  1358. <div class="field-tip">显示在图标右上角的红色气泡,留空则不显示</div>
  1359. </el-form-item>
  1360. <el-form-item label="跳转地址:">
  1361. <el-input v-model="quickEntryForm.link" placeholder="请输入跳转地址" />
  1362. </el-form-item>
  1363. <el-form-item label="启用状态:">
  1364. <el-switch v-model="quickEntryForm.status" :active-value="1" :inactive-value="0" />
  1365. </el-form-item>
  1366. </el-form>
  1367. <template #footer>
  1368. <el-button @click="quickEntryDialogVisible = false">取消</el-button>
  1369. <el-button type="primary" @click="submitQuickEntryForm">确定</el-button>
  1370. </template>
  1371. </el-dialog>
  1372. <!-- 底部固定操作栏 -->
  1373. <div class="footer-actions">
  1374. <el-button type="primary" class="btn-confirm" @click="handleMainSave">保存当前页配置</el-button>
  1375. </div>
  1376. <!-- 隐藏的文件选择框 -->
  1377. <input type="file" ref="fileInput" style="display: none" accept="image/*" @change="onFileChange" />
  1378. </div>
  1379. </template>
  1380. <script lang="ts" setup>
  1381. import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
  1382. import { ElMessage, ElMessageBox } from 'element-plus';
  1383. import {
  1384. listSearchConfig,
  1385. getSearchConfig,
  1386. delSearchConfig,
  1387. addSearchConfig,
  1388. updateSearchConfig,
  1389. getCurrentSearchConfig
  1390. } from '@/api/enterprisePurchase/searchConfig';
  1391. import { SearchConfigVO, SearchConfigQuery, SearchConfigForm } from '@/api/enterprisePurchase/searchConfig/types';
  1392. import { listAdLeft, getAdLeft, delAdLeft, addAdLeft, updateAdLeft, getCurrentAdLeft } from '@/api/enterprisePurchase/adLeft';
  1393. import { AdLeftVO, AdLeftQuery, AdLeftForm } from '@/api/enterprisePurchase/adLeft/types';
  1394. import { listCarousel, getCarousel, delCarousel, addCarousel, updateCarousel } from '@/api/enterprisePurchase/carousel';
  1395. import { CarouselVO, CarouselQuery, CarouselForm } from '@/api/enterprisePurchase/carousel/types';
  1396. import { listCategoryMain, getCategoryMain, delCategoryMain, addCategoryMain, updateCategoryMain } from '@/api/enterprisePurchase/categoryMain';
  1397. import { CategoryMainVO, CategoryMainQuery, CategoryMainForm } from '@/api/enterprisePurchase/categoryMain/types';
  1398. import { listScenarioCards, getScenarioCards, delScenarioCards, addScenarioCards, updateScenarioCards } from '@/api/enterprisePurchase/scenarioCards';
  1399. import { ScenarioCardsVO, ScenarioCardsQuery, ScenarioCardsForm } from '@/api/enterprisePurchase/scenarioCards/types';
  1400. import { listScenarioGlobalSettings, addScenarioGlobalSettings, updateScenarioGlobalSettings } from '@/api/enterprisePurchase/scenarioGlobalSettings';
  1401. import { ScenarioGlobalSettingsForm } from '@/api/enterprisePurchase/scenarioGlobalSettings/types';
  1402. import { listQuickEntryModule, addQuickEntryModule, updateQuickEntryModule } from '@/api/enterprisePurchase/quickEntryModule';
  1403. import { QuickEntryModuleVO, QuickEntryModuleQuery, QuickEntryModuleForm } from '@/api/enterprisePurchase/quickEntryModule/types';
  1404. import {
  1405. listQuickEntryItems,
  1406. getQuickEntryItems,
  1407. delQuickEntryItems,
  1408. addQuickEntryItems,
  1409. updateQuickEntryItems
  1410. } from '@/api/enterprisePurchase/quickEntryItems';
  1411. import { QuickEntryItemsVO, QuickEntryItemsQuery, QuickEntryItemsForm } from '@/api/enterprisePurchase/quickEntryItems/types';
  1412. import { listBase } from '@/api/pmsProduct/base';
  1413. import { listBrand } from '@/api/product/brand';
  1414. import {
  1415. listAdModuleConfig,
  1416. getAdModuleConfig,
  1417. delAdModuleConfig,
  1418. addAdModuleConfig,
  1419. updateAdModuleConfig
  1420. } from '@/api/enterprisePurchase/adModuleConfig';
  1421. import { AdModuleConfigVO, AdModuleConfigQuery, AdModuleConfigForm } from '@/api/enterprisePurchase/adModuleConfig/types';
  1422. import {
  1423. listHeaderCategory,
  1424. getHeaderCategory,
  1425. delHeaderCategory,
  1426. addHeaderCategory,
  1427. updateHeaderCategory
  1428. } from '@/api/enterprisePurchase/headerCategory';
  1429. import {
  1430. listRecommendThemeConfig,
  1431. getRecommendThemeConfig,
  1432. delRecommendThemeConfig,
  1433. addRecommendThemeConfig,
  1434. updateRecommendThemeConfig
  1435. } from '@/api/enterprisePurchase/recommendThemeConfig';
  1436. import { RecommendThemeConfigVO, RecommendThemeConfigQuery, RecommendThemeConfigForm } from '@/api/enterprisePurchase/recommendThemeConfig/types';
  1437. import {
  1438. listRecommendCategoryConfig,
  1439. getRecommendCategoryConfig,
  1440. delRecommendCategoryConfig,
  1441. addRecommendCategoryConfig,
  1442. updateRecommendCategoryConfig
  1443. } from '@/api/enterprisePurchase/recommendCategoryConfig';
  1444. import {
  1445. RecommendCategoryConfigVO,
  1446. RecommendCategoryConfigQuery,
  1447. RecommendCategoryConfigForm
  1448. } from '@/api/enterprisePurchase/recommendCategoryConfig/types';
  1449. import { HeaderCategoryVO, HeaderCategoryQuery, HeaderCategoryForm } from '@/api/enterprisePurchase/headerCategory/types';
  1450. import {
  1451. ShoppingCart,
  1452. Plus,
  1453. Edit,
  1454. Delete,
  1455. Picture,
  1456. CaretTop,
  1457. CaretBottom,
  1458. Menu,
  1459. ArrowLeft,
  1460. ArrowRight,
  1461. Search,
  1462. InfoFilled,
  1463. ArrowUp,
  1464. ArrowDown
  1465. } from '@element-plus/icons-vue';
  1466. import { any } from 'vue-types';
  1467. const uploadAction = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
  1468. const defaultPlaceholder =
  1469. 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNmMmYyZjIiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iI2JmYmZiZiI+5pys5peg5Zu+54mHPC90ZXh0Pjwvc3ZnPg==';
  1470. // 基础导航状态
  1471. const activeSubTab = ref('search');
  1472. const subTabs = [
  1473. { label: '搜索栏', value: 'search' },
  1474. { label: '广告图', value: 'carousel' },
  1475. { label: '分类设置', value: 'category' },
  1476. { label: '头部分类', value: 'headerCategory' },
  1477. { label: '广告模块', value: 'ad-module' },
  1478. { label: '快捷入口', value: 'quick-entry' },
  1479. { label: '场景方案', value: 'scenario' },
  1480. { label: '推荐设置', value: 'recommend' }
  1481. ];
  1482. // 首页核心配置表单 (搜索栏与基本设置)
  1483. const form = reactive({
  1484. id: null,
  1485. mainTitle: '',
  1486. subTitle: '',
  1487. placeholderText: '',
  1488. hotWordsList: [{ name: '', link: '' }],
  1489. themeColor: '',
  1490. rightBtnIcon: '',
  1491. rightBtnText: '',
  1492. rightBtnLink: '',
  1493. categoryThemeColor: '',
  1494. headerThemeColor: ''
  1495. });
  1496. const leftAdForm = reactive({
  1497. id: null,
  1498. leftAdImage: '',
  1499. leftAdLink: ''
  1500. });
  1501. // 左侧广告配置ID(单例)
  1502. const leftAdId = ref<string | number>('');
  1503. // 保存配置
  1504. const handleMainSave = async () => {
  1505. try {
  1506. let res;
  1507. if (activeSubTab.value == 'search' || activeSubTab.value == 'category' || activeSubTab.value == 'headerCategory') {
  1508. // 构建提交数据,过滤空的热词
  1509. form.categoryThemeColor = categoryThemeColor.value;
  1510. form.headerThemeColor = headerThemeColor.value;
  1511. const submitData = {
  1512. ...form,
  1513. hotWordList: form.hotWordsList.filter((item) => item.name && item.name.trim() !== '')
  1514. };
  1515. if (form.id) {
  1516. res = await updateSearchConfig(submitData);
  1517. } else {
  1518. // 无ID则新增
  1519. res = await addSearchConfig(submitData);
  1520. }
  1521. if (res.code === 200) {
  1522. ElMessage.success('配置保存成功');
  1523. // 如果是新增,保存返回的ID
  1524. if (!form.id && res.data) {
  1525. form.id = res.data.id;
  1526. }
  1527. // 重新获取最新配置
  1528. await getCurrentSearch();
  1529. } else {
  1530. ElMessage.error(res.msg || '保存失败');
  1531. }
  1532. } else if (activeSubTab.value == 'carousel') {
  1533. saveLeftAdConfig();
  1534. } else if (activeSubTab.value == 'scenario') {
  1535. saveScenarioGlobalSettings();
  1536. } else if (activeSubTab.value == 'quick-entry') {
  1537. saveQuickEntryModule();
  1538. } else if (activeSubTab.value == 'recommend') {
  1539. saveRecommendThemeConfig();
  1540. }
  1541. } catch (error) {
  1542. console.error('保存配置失败:', error);
  1543. ElMessage.error('保存配置失败');
  1544. }
  1545. };
  1546. // 获取当前配置
  1547. const getCurrentAdLeftBtn = async () => {
  1548. try {
  1549. const res = await getCurrentAdLeft();
  1550. if (res.code === 200 && res.data) {
  1551. // 回显数据到表单
  1552. leftAdForm.id = res.data.id || null;
  1553. leftAdForm.leftAdImage = res.data.imageUrl || '';
  1554. leftAdForm.leftAdLink = res.data.link || '';
  1555. }
  1556. } catch (error) {
  1557. console.error('获取配置失败:', error);
  1558. ElMessage.error('获取配置失败');
  1559. }
  1560. };
  1561. // 获取当前配置
  1562. const getCurrentSearch = async () => {
  1563. try {
  1564. const res = await getCurrentSearchConfig();
  1565. if (res.code === 200 && res.data) {
  1566. // 回显数据到表单
  1567. form.id = res.data.id || null;
  1568. form.mainTitle = res.data.mainTitle || '';
  1569. form.subTitle = res.data.subTitle || '';
  1570. form.placeholderText = res.data.placeholderText || '';
  1571. form.themeColor = res.data.themeColor || '';
  1572. form.rightBtnIcon = res.data.rightBtnIcon || '';
  1573. form.rightBtnText = res.data.rightBtnText || '';
  1574. form.rightBtnLink = res.data.rightBtnLink || '';
  1575. form.categoryThemeColor = res.data.categoryThemeColor || '';
  1576. form.headerThemeColor = res.data.headerThemeColor || '';
  1577. categoryThemeColor.value = form.categoryThemeColor;
  1578. headerThemeColor.value = form.headerThemeColor;
  1579. // 处理热词列表
  1580. if (res.data.hotWordList && Array.isArray(res.data.hotWordList)) {
  1581. form.hotWordsList =
  1582. res.data.hotWordList.length > 0
  1583. ? res.data.hotWordList.map((item) => ({
  1584. name: item.name || '',
  1585. link: item.link || ''
  1586. }))
  1587. : [{ name: '', link: '' }];
  1588. } else {
  1589. form.hotWordsList = [{ name: '', link: '' }];
  1590. }
  1591. }
  1592. } catch (error) {
  1593. console.error('获取配置失败:', error);
  1594. ElMessage.error('获取配置失败');
  1595. }
  1596. };
  1597. const handleMainReset = () => {
  1598. ElMessageBox.confirm('确定要重置当前页面的所有修改吗?', '提示', {
  1599. confirmButtonText: '确定重置',
  1600. cancelButtonText: '取消',
  1601. type: 'warning'
  1602. }).then(() => {
  1603. ElMessage.info('已还原至初始状态');
  1604. });
  1605. };
  1606. // 搜索栏逻辑
  1607. const currentPlaceholderIndex = ref(0);
  1608. let timer = null;
  1609. const searchPlaceholderList = computed(() => {
  1610. return form.placeholderText
  1611. .split(',')
  1612. .map((t) => t.trim())
  1613. .filter((t) => t !== '');
  1614. });
  1615. const addHotWord = () => {
  1616. form.hotWordsList.push({ name: '', link: '' });
  1617. };
  1618. const removeHotWord = (index) => {
  1619. form.hotWordsList.splice(index, 1);
  1620. };
  1621. const startPlaceholderScroll = () => {
  1622. if (timer) clearInterval(timer);
  1623. if (searchPlaceholderList.value.length <= 1) return;
  1624. timer = setInterval(() => {
  1625. currentPlaceholderIndex.value = (currentPlaceholderIndex.value + 1) % searchPlaceholderList.value.length;
  1626. }, 3000);
  1627. };
  1628. // 广告图模块逻辑
  1629. const leftAdHover = ref(false);
  1630. const carouselList = ref<CarouselVO[]>([]);
  1631. // 获取轮播图列表
  1632. const getCarouselList = async () => {
  1633. try {
  1634. const res = await listCarousel();
  1635. if (res.code === 200 && res.rows) {
  1636. carouselList.value = res.rows.map((item: CarouselVO) => ({
  1637. ...item,
  1638. image: item.imageUrl || ''
  1639. }));
  1640. }
  1641. } catch (error) {
  1642. console.error('获取轮播图列表失败:', error);
  1643. ElMessage.error('获取轮播图列表失败');
  1644. }
  1645. };
  1646. const activeCarouselList = computed(() => {
  1647. return carouselList.value.filter((item) => item.status === 1);
  1648. });
  1649. // 轮播图弹窗逻辑
  1650. const dialogVisible = ref(false);
  1651. const dialogType = ref('add'); // add | edit
  1652. const currentEditIndex = ref(-1);
  1653. const carouselForm = reactive({
  1654. image: '',
  1655. link: '',
  1656. target: '_self',
  1657. status: 1
  1658. });
  1659. const handleAddCarousel = () => {
  1660. dialogType.value = 'add';
  1661. carouselForm.image = '';
  1662. carouselForm.link = '';
  1663. carouselForm.target = '_self';
  1664. carouselForm.status = 1;
  1665. dialogVisible.value = true;
  1666. };
  1667. const handleEditCarousel = (row) => {
  1668. dialogType.value = 'edit';
  1669. currentEditIndex.value = carouselList.value.findIndex((item) => item.id === row.id);
  1670. carouselForm.image = row.image;
  1671. carouselForm.link = row.link;
  1672. carouselForm.target = row.target;
  1673. carouselForm.status = row.status;
  1674. dialogVisible.value = true;
  1675. };
  1676. const submitCarouselForm = async () => {
  1677. if (!carouselForm.image) {
  1678. return ElMessage.warning('请先上传轮播图片');
  1679. }
  1680. const data: CarouselForm = {
  1681. imageUrl: carouselForm.image,
  1682. link: carouselForm.link,
  1683. target: carouselForm.target,
  1684. status: carouselForm.status,
  1685. sortOrder: carouselList.value.length
  1686. };
  1687. try {
  1688. if (dialogType.value === 'add') {
  1689. const res = await addCarousel(data);
  1690. if (res.code === 200) {
  1691. ElMessage.success('新增成功');
  1692. dialogVisible.value = false;
  1693. getCarouselList();
  1694. } else {
  1695. ElMessage.error(res.msg || '新增失败');
  1696. }
  1697. } else {
  1698. const id = carouselList.value[currentEditIndex.value]?.id;
  1699. const res = await updateCarousel({ ...data, id });
  1700. if (res.code === 200) {
  1701. ElMessage.success('修改成功');
  1702. dialogVisible.value = false;
  1703. getCarouselList();
  1704. } else {
  1705. ElMessage.error(res.msg || '修改失败');
  1706. }
  1707. }
  1708. } catch (error) {
  1709. console.error('操作失败:', error);
  1710. ElMessage.error('操作失败');
  1711. }
  1712. };
  1713. const handleRemoveCarousel = async (index: number) => {
  1714. const item = carouselList.value[index];
  1715. try {
  1716. await ElMessageBox.confirm('确定要删除该轮播图吗?', '提示', { type: 'warning' });
  1717. const res = await delCarousel(item.id);
  1718. if (res.code === 200) {
  1719. ElMessage.success('删除成功');
  1720. getCarouselList();
  1721. } else {
  1722. ElMessage.error(res.msg || '删除失败');
  1723. }
  1724. } catch (error) {
  1725. if (error !== 'cancel') {
  1726. console.error('删除失败:', error);
  1727. ElMessage.error('删除失败');
  1728. }
  1729. }
  1730. };
  1731. const moveRow = async (index: number, direction: number) => {
  1732. const newIndex = index + direction;
  1733. if (newIndex < 0 || newIndex >= carouselList.value.length) return;
  1734. const item = carouselList.value.splice(index, 1)[0];
  1735. carouselList.value.splice(newIndex, 0, item);
  1736. // 同步排序到后端
  1737. try {
  1738. const start = Math.min(index, newIndex);
  1739. const end = Math.max(index, newIndex);
  1740. for (let i = start; i <= end; i++) {
  1741. const row = carouselList.value[i];
  1742. await updateCarousel({
  1743. id: row.id,
  1744. imageUrl: row.image || row.imageUrl,
  1745. link: row.link,
  1746. target: row.target,
  1747. status: row.status,
  1748. sortOrder: i
  1749. });
  1750. }
  1751. } catch (error) {
  1752. console.error('排序更新失败:', error);
  1753. ElMessage.error('排序更新失败');
  1754. }
  1755. };
  1756. // 颜色转换工具:Hex 转 RGBA
  1757. const hexToRgba = (hex, opacity) => {
  1758. if (!hex) return `rgba(255, 255, 255, ${opacity})`;
  1759. let r = parseInt(hex.slice(1, 3), 16) ;
  1760. let g = parseInt(hex.slice(3, 5), 16);
  1761. let b = parseInt(hex.slice(5, 7), 16);
  1762. return `rgba(${r}, ${g}, ${b}, ${opacity})`;
  1763. };
  1764. const handleSelectSearch = () => {
  1765. // 模拟搜索过滤,computed 会自动处理
  1766. };
  1767. // 文件上传相关
  1768. const fileInput = ref(null);
  1769. const uploadTarget = ref(''); // 'leftAd' | 'carousel' | 'categoryIcon' | 'headerIcon'
  1770. const triggerUpload = (target) => {
  1771. uploadTarget.value = target;
  1772. fileInput.value.click();
  1773. };
  1774. const onFileChange = (e) => {
  1775. const file = e.target.files[0];
  1776. if (!file) return;
  1777. const reader = new FileReader();
  1778. reader.onload = (event) => {
  1779. const base64 = event.target.result;
  1780. if (uploadTarget.value === 'leftAd') {
  1781. leftAdForm.leftAdImage = base64 as any;
  1782. } else if (uploadTarget.value === 'carousel') {
  1783. carouselForm.image = base64 as any;
  1784. } else if (uploadTarget.value === 'rightBtnIcon') {
  1785. form.rightBtnIcon = base64 as any;
  1786. } else if (uploadTarget.value === 'categoryIcon') {
  1787. categoryForm.icon = base64 as any;
  1788. } else if (uploadTarget.value === 'headerIcon') {
  1789. headerForm.icon = base64 as any;
  1790. } else if (uploadTarget.value === 'scenarioImg') {
  1791. scenarioForm.image = base64 as any;
  1792. } else if (uploadTarget.value === 'quickEntryIcon') {
  1793. quickEntryForm.icon = base64 as any;
  1794. } else if (uploadTarget.value === 'recommendIcon') {
  1795. recommendForm.icon = base64 as any;
  1796. }
  1797. ElMessage.success('图片上传成功');
  1798. e.target.value = '';
  1799. };
  1800. reader.readAsDataURL(file);
  1801. };
  1802. // ==================== 左侧广告单例配置 ====================
  1803. // 获取左侧广告配置
  1804. const fetchLeftAdConfig = async () => {
  1805. try {
  1806. const res = await listAdLeft({ pageNum: 1, pageSize: 1 });
  1807. if (res.code === 200 && res.rows && res.rows.length > 0) {
  1808. const adData = res.rows[0];
  1809. leftAdId.value = adData.id;
  1810. leftAdForm.id = adData.id;
  1811. leftAdForm.leftAdImage = adData.imageUrl || '';
  1812. leftAdForm.leftAdLink = adData.link || '';
  1813. }
  1814. } catch (error) {
  1815. console.error('获取左侧广告配置失败:', error);
  1816. }
  1817. };
  1818. // 保存左侧广告配置
  1819. const saveLeftAdConfig = async () => {
  1820. try {
  1821. if (!leftAdForm.leftAdImage) {
  1822. ElMessage.warning('请先上传广告图片');
  1823. return;
  1824. }
  1825. const submitData: AdLeftForm = {
  1826. id: leftAdId.value || leftAdForm.id || undefined,
  1827. imageUrl: leftAdForm.leftAdImage,
  1828. link: leftAdForm.leftAdLink,
  1829. status: 1 // 默认启用
  1830. };
  1831. let res;
  1832. if (submitData.id) {
  1833. // 有ID则更新
  1834. res = await updateAdLeft(submitData);
  1835. } else {
  1836. // 无ID则新增
  1837. res = await addAdLeft(submitData);
  1838. }
  1839. if (res.code === 200) {
  1840. ElMessage.success('左侧广告保存成功');
  1841. // 如果是新增,保存返回的ID
  1842. if (!leftAdId.value && res.data) {
  1843. leftAdId.value = res.data.id;
  1844. leftAdForm.id = leftAdId.value;
  1845. }
  1846. // 重新获取最新配置
  1847. await fetchLeftAdConfig();
  1848. } else {
  1849. ElMessage.error(res.msg || '保存失败');
  1850. }
  1851. } catch (error) {
  1852. console.error('保存左侧广告失败:', error);
  1853. ElMessage.error('保存左侧广告失败');
  1854. }
  1855. };
  1856. // 删除左侧广告
  1857. const handleDeleteLeftAd = () => {
  1858. ElMessageBox.confirm('确定要删除左侧广告吗?', '提示', {
  1859. confirmButtonText: '确定删除',
  1860. cancelButtonText: '取消',
  1861. type: 'warning'
  1862. })
  1863. .then(async () => {
  1864. try {
  1865. if (leftAdId.value) {
  1866. const res = await delAdLeft(leftAdId.value);
  1867. if (res.code === 200) {
  1868. ElMessage.success('删除成功');
  1869. // 清空表单数据
  1870. leftAdId.value = '';
  1871. leftAdForm.id = leftAdId.value;
  1872. leftAdForm.leftAdImage = '';
  1873. leftAdForm.leftAdLink = '';
  1874. } else {
  1875. ElMessage.error(res.msg || '删除失败');
  1876. }
  1877. } else {
  1878. // 如果没有ID,直接清空表单
  1879. leftAdForm.leftAdImage = '';
  1880. leftAdForm.leftAdLink = '';
  1881. ElMessage.success('已清除');
  1882. }
  1883. } catch (error) {
  1884. console.error('删除左侧广告失败:', error);
  1885. ElMessage.error('删除失败');
  1886. }
  1887. })
  1888. .catch(() => {
  1889. // 用户取消操作
  1890. });
  1891. };
  1892. // 分类设置模块逻辑
  1893. const categoryThemeColor = ref('#e60012');
  1894. const syncCategoryOptions = ['办公电脑', '办公设备', '办公家电', '文具耗材', '日用百货', '工业品', '食品饮料', '车载用品', '运动户外'];
  1895. const categoryList = ref<CategoryMainVO[]>([]);
  1896. // 获取分类列表
  1897. const getCategoryList = async () => {
  1898. try {
  1899. const res = await listCategoryMain();
  1900. if (res.code === 200 && res.rows) {
  1901. categoryList.value = res.rows.map((item: CategoryMainVO) => {
  1902. let extra = { tags: [] as any[], notes: [] as any[], groups: [] as any[] };
  1903. try {
  1904. if ((item as any).remark) {
  1905. extra = JSON.parse((item as any).remark);
  1906. }
  1907. } catch (e) {
  1908. /* remark JSON 解析失败使用默认值 */
  1909. }
  1910. return {
  1911. ...item,
  1912. tags: extra.tags || [],
  1913. panelData: {
  1914. mainTitle: (item as any).panelMainTitle || '',
  1915. subTitle: (item as any).panelSubTitle || '',
  1916. notes: extra.notes || [],
  1917. groups: extra.groups || []
  1918. }
  1919. };
  1920. });
  1921. }
  1922. } catch (error) {
  1923. console.error('获取分类列表失败:', error);
  1924. ElMessage.error('获取分类列表失败');
  1925. }
  1926. };
  1927. const addCategoryTag = () => {
  1928. categoryForm.tags.push({ name: '', link: '' });
  1929. };
  1930. const removeCategoryTag = (index) => {
  1931. categoryForm.tags.splice(index, 1);
  1932. };
  1933. const categoryDialogVisible = ref(false);
  1934. const categoryDialogType = ref('add');
  1935. const categoryForm = reactive({
  1936. id: null,
  1937. name: '',
  1938. icon: '',
  1939. syncCategory: '',
  1940. tags: [],
  1941. status: 1,
  1942. panelData: {
  1943. mainTitle: '',
  1944. subTitle: '',
  1945. notes: [],
  1946. groups: []
  1947. }
  1948. });
  1949. const handleAddCategory = () => {
  1950. categoryDialogType.value = 'add';
  1951. Object.assign(categoryForm, {
  1952. id: null,
  1953. name: '',
  1954. icon: '',
  1955. syncCategory: '',
  1956. tags: [],
  1957. status: 1,
  1958. panelData: { mainTitle: '', subTitle: '', notes: [], groups: [] }
  1959. });
  1960. categoryDialogVisible.value = true;
  1961. };
  1962. const handleEditCategory = (row) => {
  1963. categoryDialogType.value = 'edit';
  1964. Object.assign(categoryForm, JSON.parse(JSON.stringify(row)));
  1965. categoryDialogVisible.value = true;
  1966. };
  1967. const submitCategoryForm = async () => {
  1968. if (!categoryForm.name) return ElMessage.warning('请输入菜单名称');
  1969. const data: CategoryMainForm = {
  1970. name: categoryForm.name,
  1971. icon: categoryForm.icon,
  1972. syncCategory: categoryForm.syncCategory,
  1973. status: categoryForm.status,
  1974. panelMainTitle: categoryForm.panelData.mainTitle,
  1975. panelSubTitle: categoryForm.panelData.subTitle,
  1976. remark: JSON.stringify({
  1977. tags: categoryForm.tags,
  1978. notes: categoryForm.panelData.notes,
  1979. groups: categoryForm.panelData.groups
  1980. })
  1981. };
  1982. try {
  1983. if (categoryDialogType.value === 'add') {
  1984. const res = await addCategoryMain(data);
  1985. if (res.code === 200) {
  1986. ElMessage.success('新增成功');
  1987. categoryDialogVisible.value = false;
  1988. getCategoryList();
  1989. } else {
  1990. ElMessage.error(res.msg || '新增失败');
  1991. }
  1992. } else {
  1993. const id = categoryForm.id;
  1994. const res = await updateCategoryMain({ ...data, id });
  1995. if (res.code === 200) {
  1996. ElMessage.success('修改成功');
  1997. categoryDialogVisible.value = false;
  1998. getCategoryList();
  1999. } else {
  2000. ElMessage.error(res.msg || '修改失败');
  2001. }
  2002. }
  2003. } catch (error) {
  2004. console.error('操作失败:', error);
  2005. ElMessage.error('操作失败');
  2006. }
  2007. };
  2008. // 场景方案模块逻辑
  2009. const scenarioSettings = reactive({
  2010. id: null as string | number | null,
  2011. mainTitle: '场景解决方案',
  2012. subTitle: '一站全买齐',
  2013. btnText: '进入全场景',
  2014. jumpLink: '',
  2015. themeColor: '#66e0a3'
  2016. });
  2017. // 获取场景全局设置
  2018. const getScenarioGlobalSettingsList = async () => {
  2019. try {
  2020. const res = await listScenarioGlobalSettings();
  2021. if (res.code === 200 && res.rows && res.rows.length > 0) {
  2022. const data = res.rows[0];
  2023. scenarioSettings.id = data.id;
  2024. scenarioSettings.mainTitle = data.mainTitle || '';
  2025. scenarioSettings.subTitle = data.subTitle || '';
  2026. scenarioSettings.btnText = data.btnText || '';
  2027. scenarioSettings.jumpLink = data.jumpLink || '';
  2028. scenarioSettings.themeColor = data.themeColor || '#66e0a3';
  2029. }
  2030. } catch (error) {
  2031. console.error('获取场景全局设置失败:', error);
  2032. ElMessage.error('获取场景全局设置失败');
  2033. }
  2034. };
  2035. // 保存场景全局设置
  2036. const saveScenarioGlobalSettings = async () => {
  2037. const data: ScenarioGlobalSettingsForm = {
  2038. mainTitle: scenarioSettings.mainTitle,
  2039. subTitle: scenarioSettings.subTitle,
  2040. btnText: scenarioSettings.btnText,
  2041. jumpLink: scenarioSettings.jumpLink,
  2042. themeColor: scenarioSettings.themeColor
  2043. };
  2044. try {
  2045. let res;
  2046. if (scenarioSettings.id) {
  2047. res = await updateScenarioGlobalSettings({ ...data, id: scenarioSettings.id });
  2048. } else {
  2049. res = await addScenarioGlobalSettings(data);
  2050. if (res.code === 200 && res.data) {
  2051. scenarioSettings.id = res.data.id;
  2052. }
  2053. }
  2054. if (res.code === 200) {
  2055. ElMessage.success('场景全局设置保存成功');
  2056. } else {
  2057. ElMessage.error(res.msg || '保存失败');
  2058. }
  2059. } catch (error) {
  2060. console.error('保存场景全局设置失败:', error);
  2061. ElMessage.error('保存失败');
  2062. }
  2063. };
  2064. const scenarioList = ref<ScenarioCardsVO[]>([]);
  2065. // 获取场景卡片列表
  2066. const getScenarioList = async () => {
  2067. try {
  2068. const res = await listScenarioCards();
  2069. if (res.code === 200 && res.rows) {
  2070. scenarioList.value = res.rows.map((item: ScenarioCardsVO) => {
  2071. let extra = { subTitleColor: '#333333', bgColor: '#ffffff', opacity: 1 };
  2072. try {
  2073. if ((item as any).remark) {
  2074. extra = { ...extra, ...JSON.parse((item as any).remark) };
  2075. }
  2076. } catch (e) {
  2077. /* remark JSON 解析失败使用默认值 */
  2078. }
  2079. return {
  2080. ...item,
  2081. image: (item as any).imageUrl || '',
  2082. link: (item as any).jumpLink || '',
  2083. subTitleColor: extra.subTitleColor,
  2084. bgColor: extra.bgColor,
  2085. opacity: extra.opacity
  2086. };
  2087. });
  2088. }
  2089. } catch (error) {
  2090. console.error('获取场景卡片列表失败:', error);
  2091. ElMessage.error('获取场景卡片列表失败');
  2092. }
  2093. };
  2094. const scenarioDialogVisible = ref(false);
  2095. const scenarioEditIndex = ref(-1);
  2096. const scenarioForm = reactive({
  2097. title: '',
  2098. titleColor: '#008b4e',
  2099. subTitle: '',
  2100. subTitleColor: '#333333',
  2101. image: '',
  2102. link: '',
  2103. bgColor: '#ffffff',
  2104. opacity: 1
  2105. });
  2106. const handleEditScenario = (row, index) => {
  2107. scenarioEditIndex.value = index;
  2108. Object.assign(scenarioForm, JSON.parse(JSON.stringify(row)));
  2109. scenarioDialogVisible.value = true;
  2110. };
  2111. const submitScenarioForm = async () => {
  2112. const item = scenarioList.value[scenarioEditIndex.value];
  2113. if (!item) return;
  2114. const data: ScenarioCardsForm = {
  2115. id: item.id,
  2116. title: scenarioForm.title,
  2117. titleColor: scenarioForm.titleColor,
  2118. subTitle: scenarioForm.subTitle,
  2119. imageUrl: scenarioForm.image,
  2120. jumpLink: scenarioForm.link,
  2121. sortOrder: (item as any).sortOrder ?? scenarioEditIndex.value,
  2122. remark: JSON.stringify({
  2123. subTitleColor: scenarioForm.subTitleColor,
  2124. bgColor: scenarioForm.bgColor,
  2125. opacity: scenarioForm.opacity
  2126. })
  2127. };
  2128. try {
  2129. const res = await updateScenarioCards(data);
  2130. if (res.code === 200) {
  2131. ElMessage.success('方案修改成功');
  2132. scenarioDialogVisible.value = false;
  2133. getScenarioList();
  2134. } else {
  2135. ElMessage.error(res.msg || '修改失败');
  2136. }
  2137. } catch (error) {
  2138. console.error('修改失败:', error);
  2139. ElMessage.error('修改失败');
  2140. }
  2141. };
  2142. const moveScenario = async (index: number, direction: number) => {
  2143. const newIndex = index + direction;
  2144. if (newIndex < 0 || newIndex >= scenarioList.value.length) return;
  2145. const item = scenarioList.value.splice(index, 1)[0];
  2146. scenarioList.value.splice(newIndex, 0, item);
  2147. // 同步排序到后端
  2148. try {
  2149. const start = Math.min(index, newIndex);
  2150. const end = Math.max(index, newIndex);
  2151. for (let i = start; i <= end; i++) {
  2152. const row = scenarioList.value[i] as any;
  2153. await updateScenarioCards({
  2154. id: row.id,
  2155. title: row.title,
  2156. titleColor: row.titleColor,
  2157. subTitle: row.subTitle,
  2158. imageUrl: row.image || row.imageUrl,
  2159. jumpLink: row.link || row.jumpLink,
  2160. sortOrder: i,
  2161. remark: JSON.stringify({
  2162. subTitleColor: row.subTitleColor || '#333333',
  2163. bgColor: row.bgColor || '#ffffff',
  2164. opacity: row.opacity ?? 1
  2165. })
  2166. });
  2167. }
  2168. } catch (error) {
  2169. console.error('排序更新失败:', error);
  2170. ElMessage.error('排序更新失败');
  2171. }
  2172. };
  2173. const handleRemoveCategory = async (index: number) => {
  2174. const item = categoryList.value[index];
  2175. try {
  2176. await ElMessageBox.confirm('确定要删除该分类吗?', '提示');
  2177. const res = await delCategoryMain(item.id);
  2178. if (res.code === 200) {
  2179. ElMessage.success('删除成功');
  2180. getCategoryList();
  2181. } else {
  2182. ElMessage.error(res.msg || '删除失败');
  2183. }
  2184. } catch (error) {
  2185. if (error !== 'cancel') {
  2186. console.error('删除失败:', error);
  2187. ElMessage.error('删除失败');
  2188. }
  2189. }
  2190. };
  2191. const addPanelNote = () => {
  2192. categoryForm.panelData.notes.push({ name: '', link: '' });
  2193. };
  2194. const removePanelNote = (index) => {
  2195. categoryForm.panelData.notes.splice(index, 1);
  2196. };
  2197. const moveCategory = (index, direction) => {
  2198. const newIndex = index + direction;
  2199. if (newIndex < 0 || newIndex >= categoryList.value.length) return;
  2200. const item = categoryList.value.splice(index, 1)[0];
  2201. categoryList.value.splice(newIndex, 0, item);
  2202. };
  2203. onMounted(() => {
  2204. startPlaceholderScroll();
  2205. });
  2206. // 头部分类模块逻辑
  2207. const headerThemeColor = ref('#e60012');
  2208. const headerCategoryList = ref<HeaderCategoryVO[]>([]);
  2209. // 获取头部分类列表
  2210. const getHeaderCategoryList = async () => {
  2211. try {
  2212. const res = await listHeaderCategory();
  2213. if (res.code === 200 && res.rows) {
  2214. headerCategoryList.value = res.rows;
  2215. }
  2216. } catch (error) {
  2217. console.error('获取头部分类列表失败:', error);
  2218. ElMessage.error('获取头部分类列表失败');
  2219. }
  2220. };
  2221. const headerDialogVisible = ref(false);
  2222. const headerEditIndex = ref(-1);
  2223. const headerForm = reactive({ title: '', icon: '', link: '', openMode: 'current', status: 1 });
  2224. const handleAddHeaderCategory = () => {
  2225. headerEditIndex.value = -1;
  2226. Object.assign(headerForm, { title: '', icon: '', link: '', openMode: 'current', status: 1 });
  2227. headerDialogVisible.value = true;
  2228. };
  2229. const handleEditHeaderCategory = (row, index) => {
  2230. headerEditIndex.value = index;
  2231. Object.assign(headerForm, JSON.parse(JSON.stringify(row)));
  2232. headerDialogVisible.value = true;
  2233. };
  2234. const submitHeaderForm = async () => {
  2235. const data: HeaderCategoryForm = {
  2236. title: headerForm.title,
  2237. icon: headerForm.icon,
  2238. link: headerForm.link,
  2239. openMode: headerForm.openMode,
  2240. status: headerForm.status
  2241. };
  2242. try {
  2243. if (headerEditIndex.value > -1) {
  2244. const id = headerCategoryList.value[headerEditIndex.value]?.id;
  2245. const res = await updateHeaderCategory({ ...data, id });
  2246. if (res.code === 200) {
  2247. ElMessage.success('修改成功');
  2248. headerDialogVisible.value = false;
  2249. getHeaderCategoryList();
  2250. } else {
  2251. ElMessage.error(res.msg || '修改失败');
  2252. }
  2253. } else {
  2254. const res = await addHeaderCategory(data);
  2255. if (res.code === 200) {
  2256. ElMessage.success('新增成功');
  2257. headerDialogVisible.value = false;
  2258. getHeaderCategoryList();
  2259. } else {
  2260. ElMessage.error(res.msg || '新增失败');
  2261. }
  2262. }
  2263. } catch (error) {
  2264. console.error('操作失败:', error);
  2265. ElMessage.error('操作失败');
  2266. }
  2267. };
  2268. const handleDeleteHeaderCategory = async (index: number) => {
  2269. const item = headerCategoryList.value[index];
  2270. try {
  2271. await ElMessageBox.confirm('确定删除该分类吗?', '提示');
  2272. const res = await delHeaderCategory(item.id);
  2273. if (res.code === 200) {
  2274. ElMessage.success('删除成功');
  2275. getHeaderCategoryList();
  2276. } else {
  2277. ElMessage.error(res.msg || '删除失败');
  2278. }
  2279. } catch (error) {
  2280. if (error !== 'cancel') {
  2281. console.error('删除失败:', error);
  2282. ElMessage.error('删除失败');
  2283. }
  2284. }
  2285. };
  2286. const moveHeader = (index, delta) => {
  2287. const target = index + delta;
  2288. if (target < 0 || target >= headerCategoryList.value.length) return;
  2289. const temp = headerCategoryList.value[index];
  2290. headerCategoryList.value[index] = headerCategoryList.value[target];
  2291. headerCategoryList.value[target] = temp;
  2292. };
  2293. // 导航滚动逻辑
  2294. const headerNavScrollRef = ref(null);
  2295. const showLeftArrow = ref(false);
  2296. const showRightArrow = ref(false);
  2297. const updateNavArrows = () => {
  2298. if (!headerNavScrollRef.value) return;
  2299. const { scrollLeft, scrollWidth, clientWidth } = headerNavScrollRef.value;
  2300. showLeftArrow.value = scrollLeft > 5;
  2301. showRightArrow.value = scrollLeft + clientWidth < scrollWidth - 5;
  2302. };
  2303. const scrollHeaderNav = (direction) => {
  2304. const container = headerNavScrollRef.value;
  2305. if (!container) return;
  2306. const scrollAmount = 350;
  2307. if (direction === 'left') {
  2308. container.scrollLeft -= scrollAmount;
  2309. } else {
  2310. container.scrollLeft += scrollAmount;
  2311. }
  2312. };
  2313. // 广告模块逻辑
  2314. const adModules = ref([
  2315. {
  2316. id: 1,
  2317. title: '企业购x百亿补贴',
  2318. titleColor: '#333333',
  2319. subTitle: '先采后付 享底价',
  2320. subTitleColor: '#999999',
  2321. type: 'subsidy',
  2322. items: [
  2323. { id: 101, productName: '企业商用台式机', imageUrl: '/static/images/purchase/pc_desktop.jpg', price: '69.9' },
  2324. { id: 102, productName: '商务笔记本', imageUrl: '/static/images/purchase/laptop_hp.jpg', price: '84.8' },
  2325. { id: 103, productName: '智能打印机', imageUrl: '/static/images/purchase/printer_office.jpg', price: '139.9' },
  2326. { id: 104, productName: '高效办公组网', imageUrl: '/static/images/purchase/network_router.jpg', price: '1749' }
  2327. ]
  2328. },
  2329. {
  2330. id: 2,
  2331. title: '企采榜单',
  2332. titleColor: '#333333',
  2333. subTitle: '同行都在买',
  2334. subTitleColor: '#f58220',
  2335. type: 'ranking',
  2336. items: [
  2337. {
  2338. id: 201,
  2339. productName: '办公电脑榜',
  2340. imageUrl: '/static/images/purchase/laptop_lenovo.jpg',
  2341. price: '0',
  2342. tagText: '办公电脑榜',
  2343. tagLink: '',
  2344. salesCount: '1543'
  2345. },
  2346. {
  2347. id: 202,
  2348. productName: '文具榜',
  2349. imageUrl: '/static/images/purchase/stationery_ranking.jpg',
  2350. price: '0',
  2351. tagText: '文具榜',
  2352. tagLink: '',
  2353. salesCount: '1200'
  2354. }
  2355. ]
  2356. },
  2357. {
  2358. id: 3,
  2359. title: '品牌好店',
  2360. titleColor: '#333333',
  2361. subTitle: '返2000元E卡',
  2362. subTitleColor: '#f58220',
  2363. type: 'brand',
  2364. items: [
  2365. { id: 301, productName: '鲁花', imageUrl: '/static/images/purchase/oil_luhua.jpg', tagText: '品质保障', tagLink: '鲁花京东自营旗舰店' },
  2366. { id: 302, productName: '金龙鱼', imageUrl: '/static/images/purchase/oil_jinlongyu.jpg', tagText: '热销品牌', tagLink: '金龙鱼京东自营旗舰店' }
  2367. ]
  2368. },
  2369. {
  2370. id: 4,
  2371. title: '企业精选',
  2372. titleColor: '#333333',
  2373. subTitle: '品牌专供 库存充足',
  2374. subTitleColor: '#999999',
  2375. type: 'selection',
  2376. items: [
  2377. { id: 401, productName: '高性能工作站', imageUrl: '/static/images/purchase/pc_desktop.jpg', price: '10740' },
  2378. { id: 402, productName: '办公咖啡机', imageUrl: '/static/images/purchase/coffee_machine.jpg', price: '877' }
  2379. ]
  2380. },
  2381. {
  2382. id: 5,
  2383. title: '企业购x京东新品',
  2384. titleColor: '#333333',
  2385. subTitle: '美的新鲜',
  2386. subTitleColor: '#f58220',
  2387. type: 'new',
  2388. items: [
  2389. { id: 501, productName: '商用冷柜', imageUrl: '/static/images/purchase/freezer.jpg', price: '7188' },
  2390. { id: 502, productName: '得力笔记本', imageUrl: '/static/images/purchase/notebook_deli.jpg', price: '34.9' }
  2391. ]
  2392. }
  2393. ]);
  2394. const adDialogVisible = ref(false);
  2395. const currentAdIdx = ref(-1);
  2396. const currentItemIdx = ref(-1);
  2397. const adForm = reactive({ title: '', titleColor: '#333333', subTitle: '', subTitleColor: '#f58220', items: [] });
  2398. // 实时预览辅助函数
  2399. const getAdTitleStyle = (index, type) => {
  2400. const isEditing = adDialogVisible.value && currentAdIdx.value === index;
  2401. const key = type === 'main' ? 'titleColor' : 'subTitleColor';
  2402. const defaultColor = type === 'main' ? '#333' : '#999';
  2403. const color = isEditing ? adForm[key] : adModules.value[index][key] || defaultColor;
  2404. return { color };
  2405. };
  2406. const getAdTitleText = (index, type) => {
  2407. const isEditing = adDialogVisible.value && currentAdIdx.value === index;
  2408. const key = type === 'main' ? 'title' : 'subTitle';
  2409. return isEditing ? adForm[key] : adModules.value[index][key];
  2410. };
  2411. const handleEditAd = (index) => {
  2412. currentAdIdx.value = index;
  2413. const ad = adModules.value[index];
  2414. Object.assign(adForm, JSON.parse(JSON.stringify(ad)));
  2415. adDialogVisible.value = true;
  2416. };
  2417. const submitAdForm = async () => {
  2418. const moduleData = adModules.value[currentAdIdx.value];
  2419. if (!moduleData) return;
  2420. const data: AdModuleConfigForm = {
  2421. moduleCode: moduleData.type || '',
  2422. moduleName: moduleData.title || '',
  2423. mainTitle: adForm.title,
  2424. mainTitleColor: adForm.titleColor,
  2425. subTitle: adForm.subTitle,
  2426. subTitleColor: adForm.subTitleColor,
  2427. jumpLink: '',
  2428. status: 1,
  2429. sortOrder: currentAdIdx.value,
  2430. adModuleItemList: adForm.items.map((item: any) => ({
  2431. productId: item.productId || item.id,
  2432. productName: item.productName || '',
  2433. imageUrl: item.imageUrl || '',
  2434. price: item.price || 0,
  2435. tagText: item.tagText || '',
  2436. tagLink: item.tagLink || '',
  2437. salesCount: item.salesCount || 0
  2438. }))
  2439. };
  2440. try {
  2441. let res;
  2442. // 从 API 加载的真实数据才有有效 id,mock id(1-5)用 POST 新增
  2443. const isFromApi = moduleData.moduleCode && moduleData.id && String(moduleData.id).length > 10;
  2444. if (isFromApi) {
  2445. res = await updateAdModuleConfig({ ...data, id: moduleData.id });
  2446. } else {
  2447. res = await addAdModuleConfig(data);
  2448. }
  2449. if (res.code === 200) {
  2450. // 新增时回写后端返回的 id
  2451. const savedData = JSON.parse(JSON.stringify(adForm));
  2452. if (!isFromApi && res.data?.id) {
  2453. savedData.id = res.data.id;
  2454. }
  2455. adModules.value[currentAdIdx.value] = savedData;
  2456. adDialogVisible.value = false;
  2457. ElMessage.success('配置保存成功');
  2458. } else {
  2459. ElMessage.error(res.msg || '保存失败');
  2460. }
  2461. } catch (error) {
  2462. console.error('保存广告模块配置失败:', error);
  2463. ElMessage.error('保存失败');
  2464. }
  2465. };
  2466. // 获取广告模块配置列表
  2467. const getAdModuleList = async () => {
  2468. try {
  2469. const res = await listAdModuleConfig();
  2470. if (res.code === 200 && res.rows && res.rows.length > 0) {
  2471. adModules.value = res.rows.map((item: AdModuleConfigVO) => ({
  2472. ...item,
  2473. title: item.mainTitle || '',
  2474. titleColor: item.mainTitleColor || '#333333',
  2475. subTitle: item.subTitle || '',
  2476. subTitleColor: item.subTitleColor || '#999999',
  2477. type: item.moduleCode || '',
  2478. items: (item.adModuleItemList || []).map((sub: any) => ({
  2479. id: sub.id,
  2480. productId: sub.productId || '',
  2481. productName: sub.productName || '',
  2482. imageUrl: sub.imageUrl || '',
  2483. price: sub.price || 0,
  2484. tagText: sub.tagText || '',
  2485. tagLink: sub.tagLink || '',
  2486. salesCount: sub.salesCount || 0
  2487. }))
  2488. }));
  2489. }
  2490. } catch (error) {
  2491. console.error('获取广告模块配置失败:', error);
  2492. ElMessage.error('获取广告模块配置失败');
  2493. }
  2494. };
  2495. // 推荐设置模块逻辑
  2496. const recommendThemeColor = ref('#e60012');
  2497. const recommendProductThemeColor = ref('#e60012');
  2498. const recommendActiveId = ref(1);
  2499. const recommendThemeConfigId = ref<string | number | null>(null);
  2500. const getRecommendThemeConfigList = async () => {
  2501. try {
  2502. const res = await listRecommendThemeConfig();
  2503. if (res.code === 200 && res.rows && res.rows.length > 0) {
  2504. const data = res.rows[0];
  2505. recommendThemeConfigId.value = data.id;
  2506. recommendThemeColor.value = data.themeColor || '#e60012';
  2507. recommendProductThemeColor.value = data.productThemeColor || '#e60012';
  2508. }
  2509. } catch (error) {
  2510. console.error('获取推荐主题配置失败:', error);
  2511. }
  2512. };
  2513. const saveRecommendThemeConfig = async () => {
  2514. const data: RecommendThemeConfigForm = {
  2515. themeColor: recommendThemeColor.value,
  2516. productThemeColor: recommendProductThemeColor.value
  2517. };
  2518. try {
  2519. let res;
  2520. if (recommendThemeConfigId.value) {
  2521. res = await updateRecommendThemeConfig({ ...data, id: recommendThemeConfigId.value });
  2522. } else {
  2523. res = await addRecommendThemeConfig(data);
  2524. if (res.code === 200 && res.data) {
  2525. recommendThemeConfigId.value = res.data.id;
  2526. }
  2527. }
  2528. if (res.code === 200) {
  2529. ElMessage.success('推荐主题配置保存成功');
  2530. } else {
  2531. ElMessage.error(res.msg || '保存失败');
  2532. }
  2533. } catch (error) {
  2534. console.error('保存推荐主题配置失败:', error);
  2535. ElMessage.error('保存失败');
  2536. }
  2537. };
  2538. const recommendList = ref<RecommendCategoryConfigVO[]>([]);
  2539. const getRecommendCategoryList = async () => {
  2540. try {
  2541. const res = await listRecommendCategoryConfig();
  2542. if (res.code === 200 && res.rows) {
  2543. recommendList.value = res.rows.map((item: RecommendCategoryConfigVO) => ({
  2544. ...item,
  2545. type: (item as any).dataType || 'select',
  2546. icon: (item as any).iconUrl || '',
  2547. categoryValue: [],
  2548. selectedProducts: (() => {
  2549. try {
  2550. return (item as any).selectedProductIds ? JSON.parse((item as any).selectedProductIds) : [];
  2551. } catch {
  2552. return [];
  2553. }
  2554. })()
  2555. }));
  2556. }
  2557. } catch (error) {
  2558. console.error('获取推荐分类列表失败:', error);
  2559. ElMessage.error('获取推荐分类列表失败');
  2560. }
  2561. };
  2562. const activeRecommendList = computed(() => {
  2563. return recommendList.value.filter((item) => item.status === 1);
  2564. });
  2565. const recommendDialogVisible = ref(false);
  2566. const recommendEditIndex = ref(-1);
  2567. const recommendForm = reactive({
  2568. name: '',
  2569. subTitle: '',
  2570. icon: '',
  2571. type: 'select',
  2572. categoryValue: [],
  2573. categoryLabel: '',
  2574. selectedProducts: [],
  2575. status: 1
  2576. });
  2577. const mockCategoryOptions = [
  2578. {
  2579. value: 'bg',
  2580. label: '办公设备',
  2581. children: [
  2582. {
  2583. value: 'dn',
  2584. label: '办公电脑',
  2585. children: [
  2586. { value: 'bjb', label: '笔记本' },
  2587. { value: 'tsj', label: '台式机' }
  2588. ]
  2589. }
  2590. ]
  2591. },
  2592. {
  2593. value: 'wj',
  2594. label: '文具耗材',
  2595. children: [
  2596. {
  2597. value: 'dy',
  2598. label: '打印耗材',
  2599. children: [
  2600. { value: 'fyz', label: '复印纸' },
  2601. { value: 'xhg', label: '硒鼓' }
  2602. ]
  2603. }
  2604. ]
  2605. }
  2606. ];
  2607. const handleAddRecommend = () => {
  2608. recommendEditIndex.value = -1;
  2609. Object.assign(recommendForm, {
  2610. name: '',
  2611. subTitle: '',
  2612. icon: '',
  2613. type: 'select',
  2614. categoryValue: [],
  2615. categoryLabel: '',
  2616. selectedProducts: [],
  2617. status: 1
  2618. });
  2619. recommendDialogVisible.value = true;
  2620. };
  2621. const handleEditRecommend = (row, index) => {
  2622. recommendEditIndex.value = index;
  2623. Object.assign(recommendForm, JSON.parse(JSON.stringify(row)));
  2624. recommendDialogVisible.value = true;
  2625. };
  2626. const getLabelsByValues = (values, options) => {
  2627. const labels = [];
  2628. let currentOptions = options;
  2629. for (const val of values) {
  2630. const target = currentOptions.find((opt) => opt.value === val);
  2631. if (target) {
  2632. labels.push(target.label);
  2633. currentOptions = target.children || [];
  2634. }
  2635. }
  2636. return labels.join(' > ');
  2637. };
  2638. const handleRecommendCategoryChange = (val) => {
  2639. if (val && val.length > 0) {
  2640. recommendForm.categoryLabel = getLabelsByValues(val, mockCategoryOptions);
  2641. } else {
  2642. recommendForm.categoryLabel = '';
  2643. }
  2644. };
  2645. const submitRecommendForm = async () => {
  2646. // 序列化已选商品
  2647. const selectedProducts = recommendEditIndex.value > -1 ? recommendList.value[recommendEditIndex.value]?.selectedProducts || [] : [];
  2648. const data: RecommendCategoryConfigForm = {
  2649. name: recommendForm.name,
  2650. subTitle: recommendForm.subTitle,
  2651. iconUrl: recommendForm.icon,
  2652. dataType: recommendForm.type,
  2653. categoryLabel: recommendForm.categoryLabel || '',
  2654. categoryPath: (recommendForm.categoryValue || []).join(','),
  2655. selectedProductIds: JSON.stringify(selectedProducts),
  2656. status: recommendForm.status,
  2657. sortOrder: recommendList.value.length
  2658. };
  2659. try {
  2660. if (recommendEditIndex.value > -1) {
  2661. const id = recommendList.value[recommendEditIndex.value]?.id;
  2662. const isFromApi = id && String(id).length > 10;
  2663. const res = isFromApi ? await updateRecommendCategoryConfig({ ...data, id }) : await addRecommendCategoryConfig(data);
  2664. if (res.code === 200) {
  2665. ElMessage.success('修改成功');
  2666. recommendDialogVisible.value = false;
  2667. getRecommendCategoryList();
  2668. } else {
  2669. ElMessage.error(res.msg || '保存失败');
  2670. }
  2671. } else {
  2672. const res = await addRecommendCategoryConfig(data);
  2673. if (res.code === 200) {
  2674. ElMessage.success('新增成功');
  2675. recommendDialogVisible.value = false;
  2676. getRecommendCategoryList();
  2677. } else {
  2678. ElMessage.error(res.msg || '保存失败');
  2679. }
  2680. }
  2681. } catch (error) {
  2682. console.error('保存失败:', error);
  2683. ElMessage.error('保存失败');
  2684. }
  2685. };
  2686. const handleDeleteRecommend = async (index: number) => {
  2687. const item = recommendList.value[index];
  2688. try {
  2689. await ElMessageBox.confirm('确定要删除该推荐分类吗?', '提示');
  2690. const res = await delRecommendCategoryConfig(item.id);
  2691. if (res.code === 200) {
  2692. ElMessage.success('删除成功');
  2693. getRecommendCategoryList();
  2694. } else {
  2695. ElMessage.error(res.msg || '删除失败');
  2696. }
  2697. } catch (error) {
  2698. if (error !== 'cancel') {
  2699. console.error('删除失败:', error);
  2700. ElMessage.error('删除失败');
  2701. }
  2702. }
  2703. };
  2704. const moveRecommend = async (index: number, direction: number) => {
  2705. const newIndex = index + direction;
  2706. if (newIndex < 0 || newIndex >= recommendList.value.length) return;
  2707. const item = recommendList.value.splice(index, 1)[0];
  2708. recommendList.value.splice(newIndex, 0, item);
  2709. try {
  2710. const start = Math.min(index, newIndex);
  2711. const end = Math.max(index, newIndex);
  2712. for (let i = start; i <= end; i++) {
  2713. const row = recommendList.value[i] as any;
  2714. await updateRecommendCategoryConfig({
  2715. id: row.id,
  2716. name: row.name,
  2717. subTitle: row.subTitle,
  2718. iconUrl: row.icon || row.iconUrl,
  2719. dataType: row.type || row.dataType,
  2720. categoryLabel: row.categoryLabel || '',
  2721. status: row.status,
  2722. sortOrder: i
  2723. });
  2724. }
  2725. } catch (error) {
  2726. console.error('排序更新失败:', error);
  2727. ElMessage.error('排序更新失败');
  2728. }
  2729. };
  2730. // 推荐商品选择增强逻辑
  2731. const selectedProductDialogVisible = ref(false);
  2732. const productSelectionDrawerVisible = ref(false);
  2733. const currentRecommendIndex = ref(-1);
  2734. const drawerSelection = ref([]);
  2735. const selectedProductsTableRef = ref(null);
  2736. const drawerTableRef = ref(null);
  2737. const selectedCurrentPage = ref(1);
  2738. const selectedPageSize = ref(10);
  2739. const pagedSelectedProducts = computed(() => {
  2740. const list = recommendList.value[currentRecommendIndex.value]?.selectedProducts || [];
  2741. const start = (selectedCurrentPage.value - 1) * selectedPageSize.value;
  2742. const end = start + selectedPageSize.value;
  2743. return list.slice(start, end);
  2744. });
  2745. const handleSelectedProductsSelectionChange = (val) => {
  2746. currentItemIdx.value = val; // 临时借用 currentItemIdx 存储选中的行
  2747. };
  2748. const openRecommendProductSelect = (index) => {
  2749. currentRecommendIndex.value = index;
  2750. selectedCurrentPage.value = 1;
  2751. selectedProductDialogVisible.value = true;
  2752. };
  2753. const submitSelectedProducts = async () => {
  2754. const item = recommendList.value[currentRecommendIndex.value];
  2755. if (!item) return;
  2756. const selectedProducts = item.selectedProducts || [];
  2757. const data: RecommendCategoryConfigForm = {
  2758. id: item.id,
  2759. name: item.name,
  2760. subTitle: item.subTitle,
  2761. iconUrl: (item as any).icon || (item as any).iconUrl || '',
  2762. dataType: (item as any).type || (item as any).dataType || 'select',
  2763. categoryLabel: (item as any).categoryLabel || '',
  2764. selectedProductIds: JSON.stringify(selectedProducts),
  2765. status: item.status
  2766. };
  2767. try {
  2768. const res = await updateRecommendCategoryConfig(data);
  2769. if (res.code === 200) {
  2770. selectedProductDialogVisible.value = false;
  2771. ElMessage.success('已选商品配置已保存');
  2772. } else {
  2773. ElMessage.error(res.msg || '保存失败');
  2774. }
  2775. } catch (error) {
  2776. console.error('保存已选商品失败:', error);
  2777. ElMessage.error('保存失败');
  2778. }
  2779. };
  2780. const openProductDrawer = async () => {
  2781. const currentList = recommendList.value[currentRecommendIndex.value]?.selectedProducts || [];
  2782. drawerSelection.value = JSON.parse(JSON.stringify(currentList));
  2783. productQueryParams.itemName = '';
  2784. productQueryParams.pageNum = 1;
  2785. productQueryParams.pageSize = selectPageSize.value;
  2786. selectCurrentPage.value = 1;
  2787. productSelectionDrawerVisible.value = true;
  2788. await getProductList();
  2789. // 回显勾选逻辑
  2790. nextTick(() => {
  2791. if (drawerTableRef.value) {
  2792. drawerTableRef.value.clearSelection();
  2793. productList.value.forEach((item) => {
  2794. if (currentList.some((exist) => exist.id === item.id)) {
  2795. drawerTableRef.value.toggleRowSelection(item, true);
  2796. }
  2797. });
  2798. }
  2799. });
  2800. };
  2801. const handleDrawerSelectionChange = (val) => {
  2802. drawerSelection.value = val;
  2803. };
  2804. const confirmDrawerSelection = () => {
  2805. // 直接全量覆盖,支持在抽屉里取消勾选来删除
  2806. recommendList.value[currentRecommendIndex.value].selectedProducts = JSON.parse(JSON.stringify(drawerSelection.value));
  2807. ElMessage.success('商品列表同步成功');
  2808. productSelectionDrawerVisible.value = false;
  2809. };
  2810. const removeSelectedProduct = (index) => {
  2811. recommendList.value[currentRecommendIndex.value].selectedProducts.splice(index, 1);
  2812. ElMessage.success('移除成功');
  2813. };
  2814. const batchRemoveSelectedProducts = () => {
  2815. const selectedRows = selectedProductsTableRef.value?.getSelectionRows() || [];
  2816. if (selectedRows.length === 0) return ElMessage.warning('请先勾选要移除的商品');
  2817. ElMessageBox.confirm(`确定移除选中的 ${selectedRows.length} 个商品吗?`, '提示').then(() => {
  2818. const currentList = recommendList.value[currentRecommendIndex.value].selectedProducts;
  2819. const selectedIds = selectedRows.map((r) => r.id);
  2820. recommendList.value[currentRecommendIndex.value].selectedProducts = currentList.filter((item) => !selectedIds.includes(item.id));
  2821. ElMessage.success('批量移除成功');
  2822. });
  2823. };
  2824. // 推荐预览滚动逻辑
  2825. const recommendScrollRef = ref(null);
  2826. const recShowLeft = ref(false);
  2827. const recShowRight = ref(false);
  2828. const updateRecArrows = () => {
  2829. if (!recommendScrollRef.value) return;
  2830. const { scrollLeft, scrollWidth, clientWidth } = recommendScrollRef.value;
  2831. recShowLeft.value = scrollLeft > 5;
  2832. recShowRight.value = scrollLeft + clientWidth < scrollWidth - 5;
  2833. };
  2834. const scrollRecommend = (direction) => {
  2835. const container = recommendScrollRef.value;
  2836. if (!container) return;
  2837. const scrollAmount = 400;
  2838. if (direction === 'left') {
  2839. container.scrollLeft -= scrollAmount;
  2840. } else {
  2841. container.scrollLeft += scrollAmount;
  2842. }
  2843. };
  2844. // 商品选择逻辑
  2845. const selectDialogVisible = ref(false);
  2846. const selectedTempId = ref(null);
  2847. const selectCurrentPage = ref(1);
  2848. const selectPageSize = ref(8);
  2849. // 商品列表(API数据)
  2850. const productList = ref<any[]>([]);
  2851. const productTotal = ref(0);
  2852. const productQueryParams = reactive({
  2853. pageNum: 1,
  2854. pageSize: 8,
  2855. itemName: '',
  2856. productStatus: 1
  2857. });
  2858. const brandQueryParams = reactive({
  2859. pageNum: 1,
  2860. pageSize: 8,
  2861. brandName: ''
  2862. });
  2863. /** 获取商品/品牌列表(根据广告模块类型自动切换) */
  2864. const getProductList = async () => {
  2865. try {
  2866. if (selectDialogVisible.value && currentAdIdx.value === 2) {
  2867. // 品牌好店:查询品牌
  2868. brandQueryParams.brandName = productQueryParams.itemName;
  2869. brandQueryParams.pageNum = productQueryParams.pageNum;
  2870. brandQueryParams.pageSize = productQueryParams.pageSize;
  2871. const res = await listBrand(brandQueryParams);
  2872. productList.value = (res.rows || []).map((item: any) => ({
  2873. id: item.id,
  2874. name: item.brandName || '',
  2875. image: item.brandLogo || '',
  2876. price: ''
  2877. }));
  2878. productTotal.value = res.total || 0;
  2879. } else {
  2880. // 其他广告模块:查询商品
  2881. const res = await listBase(productQueryParams);
  2882. productList.value = (res.rows || []).map((item: any) => ({
  2883. id: item.id,
  2884. name: item.itemName || '',
  2885. image: item.productImage || item.productImageUrl || '',
  2886. price: item.memberPrice ?? item.minSellingPrice ?? item.marketPrice ?? ''
  2887. }));
  2888. productTotal.value = res.total || 0;
  2889. }
  2890. } catch (error) {
  2891. console.error('获取列表失败:', error);
  2892. ElMessage.error('获取列表失败');
  2893. }
  2894. };
  2895. const filteredSelectList = computed(() => productList.value);
  2896. const pagedSelectList = computed(() => productList.value);
  2897. const onProductPageChange = (page: number) => {
  2898. productQueryParams.pageNum = page;
  2899. selectCurrentPage.value = page;
  2900. getProductList();
  2901. };
  2902. const onProductPageSizeChange = (size: number) => {
  2903. productQueryParams.pageSize = size;
  2904. productQueryParams.pageNum = 1;
  2905. selectCurrentPage.value = 1;
  2906. selectPageSize.value = size;
  2907. getProductList();
  2908. };
  2909. const openProductSelect = (index: number) => {
  2910. currentItemIdx.value = index;
  2911. selectedTempId.value = adForm.items[index].id;
  2912. productQueryParams.itemName = '';
  2913. productQueryParams.pageNum = 1;
  2914. productQueryParams.pageSize = selectPageSize.value;
  2915. selectCurrentPage.value = 1;
  2916. selectDialogVisible.value = true;
  2917. getProductList();
  2918. };
  2919. const confirmSelect = () => {
  2920. const item = productList.value.find((i) => i.id === selectedTempId.value);
  2921. if (item) {
  2922. const target = adForm.items[currentItemIdx.value];
  2923. target.id = item.id;
  2924. target.productId = item.id;
  2925. target.productName = item.name;
  2926. target.imageUrl = item.image;
  2927. target.price = item.price;
  2928. selectDialogVisible.value = false;
  2929. ElMessage.success('选择成功');
  2930. }
  2931. };
  2932. // 快捷入口模块逻辑
  2933. const qePageIndex = ref(0);
  2934. const quickEntrySettings = reactive({
  2935. id: null as string | number | null,
  2936. moduleName: '企业工作台',
  2937. jumpLink: ''
  2938. });
  2939. // 获取快捷入口模块配置
  2940. const getQuickEntryModuleList = async () => {
  2941. try {
  2942. const res = await listQuickEntryModule();
  2943. if (res.code === 200 && res.rows && res.rows.length > 0) {
  2944. const data = res.rows[0];
  2945. quickEntrySettings.id = data.id;
  2946. quickEntrySettings.moduleName = data.moduleName || '';
  2947. quickEntrySettings.jumpLink = data.jumpLink || '';
  2948. }
  2949. } catch (error) {
  2950. console.error('获取快捷入口模块配置失败:', error);
  2951. ElMessage.error('获取快捷入口模块配置失败');
  2952. }
  2953. };
  2954. // 保存快捷入口模块配置
  2955. const saveQuickEntryModule = async () => {
  2956. const data: QuickEntryModuleForm = {
  2957. moduleName: quickEntrySettings.moduleName,
  2958. jumpLink: quickEntrySettings.jumpLink
  2959. };
  2960. try {
  2961. let res;
  2962. if (quickEntrySettings.id) {
  2963. res = await updateQuickEntryModule({ ...data, id: quickEntrySettings.id });
  2964. } else {
  2965. res = await addQuickEntryModule(data);
  2966. if (res.code === 200 && res.data) {
  2967. quickEntrySettings.id = res.data.id;
  2968. }
  2969. }
  2970. if (res.code === 200) {
  2971. ElMessage.success('快捷入口模块配置保存成功');
  2972. } else {
  2973. ElMessage.error(res.msg || '保存失败');
  2974. }
  2975. } catch (error) {
  2976. console.error('保存快捷入口模块配置失败:', error);
  2977. ElMessage.error('保存失败');
  2978. }
  2979. };
  2980. const quickEntryList = ref<QuickEntryItemsVO[]>([]);
  2981. // 获取快捷入口项列表
  2982. const getQuickEntryItemsList = async () => {
  2983. try {
  2984. const res = await listQuickEntryItems();
  2985. if (res.code === 200 && res.rows) {
  2986. quickEntryList.value = res.rows.map((item: QuickEntryItemsVO) => ({
  2987. ...item,
  2988. icon: (item as any).iconUrl || '',
  2989. tag: (item as any).tagText || '',
  2990. link: (item as any).jumpLink || ''
  2991. }));
  2992. }
  2993. } catch (error) {
  2994. console.error('获取快捷入口项列表失败:', error);
  2995. ElMessage.error('获取快捷入口项列表失败');
  2996. }
  2997. };
  2998. const qePageCount = computed(() => Math.ceil(quickEntryList.value.filter((i) => i.status === 1).length / 8));
  2999. const getPageItems = (pageIdx) => {
  3000. const activeItems = quickEntryList.value.filter((i) => i.status === 1);
  3001. return activeItems.slice(pageIdx * 8, (pageIdx + 1) * 8);
  3002. };
  3003. const quickEntryDialogVisible = ref(false);
  3004. const quickEntryDialogType = ref('add');
  3005. const qeEditIndex = ref(-1);
  3006. const quickEntryForm = reactive({ name: '', icon: '', tag: '', link: '', status: 1 });
  3007. const handleAddQuickEntry = () => {
  3008. quickEntryDialogType.value = 'add';
  3009. Object.assign(quickEntryForm, { name: '', icon: '', tag: '', link: '', status: 1 });
  3010. quickEntryDialogVisible.value = true;
  3011. };
  3012. const handleEditQuickEntry = (row, index) => {
  3013. quickEntryDialogType.value = 'edit';
  3014. qeEditIndex.value = index;
  3015. Object.assign(quickEntryForm, JSON.parse(JSON.stringify(row)));
  3016. quickEntryDialogVisible.value = true;
  3017. };
  3018. const submitQuickEntryForm = async () => {
  3019. if (!quickEntryForm.name) return ElMessage.warning('请输入入口名称');
  3020. const data: QuickEntryItemsForm = {
  3021. name: quickEntryForm.name,
  3022. iconUrl: quickEntryForm.icon,
  3023. tagText: quickEntryForm.tag,
  3024. jumpLink: quickEntryForm.link,
  3025. status: quickEntryForm.status,
  3026. sortOrder: quickEntryList.value.length
  3027. };
  3028. try {
  3029. if (quickEntryDialogType.value === 'add') {
  3030. const res = await addQuickEntryItems(data);
  3031. if (res.code === 200) {
  3032. ElMessage.success('新增成功');
  3033. quickEntryDialogVisible.value = false;
  3034. getQuickEntryItemsList();
  3035. } else {
  3036. ElMessage.error(res.msg || '新增失败');
  3037. }
  3038. } else {
  3039. const id = quickEntryList.value[qeEditIndex.value]?.id;
  3040. const res = await updateQuickEntryItems({ ...data, id });
  3041. if (res.code === 200) {
  3042. ElMessage.success('修改成功');
  3043. quickEntryDialogVisible.value = false;
  3044. getQuickEntryItemsList();
  3045. } else {
  3046. ElMessage.error(res.msg || '修改失败');
  3047. }
  3048. }
  3049. } catch (error) {
  3050. console.error('操作失败:', error);
  3051. ElMessage.error('操作失败');
  3052. }
  3053. };
  3054. const handleDeleteQuickEntry = async (index: number) => {
  3055. const item = quickEntryList.value[index];
  3056. try {
  3057. await ElMessageBox.confirm('确定要删除该入口吗?', '提示', { type: 'warning' });
  3058. const res = await delQuickEntryItems(item.id);
  3059. if (res.code === 200) {
  3060. ElMessage.success('删除成功');
  3061. getQuickEntryItemsList();
  3062. } else {
  3063. ElMessage.error(res.msg || '删除失败');
  3064. }
  3065. } catch (error) {
  3066. if (error !== 'cancel') {
  3067. console.error('删除失败:', error);
  3068. ElMessage.error('删除失败');
  3069. }
  3070. }
  3071. };
  3072. const moveQE = async (index: number, direction: number) => {
  3073. const newIndex = index + direction;
  3074. if (newIndex < 0 || newIndex >= quickEntryList.value.length) return;
  3075. const item = quickEntryList.value.splice(index, 1)[0];
  3076. quickEntryList.value.splice(newIndex, 0, item);
  3077. // 同步排序到后端
  3078. try {
  3079. const start = Math.min(index, newIndex);
  3080. const end = Math.max(index, newIndex);
  3081. for (let i = start; i <= end; i++) {
  3082. const row = quickEntryList.value[i] as any;
  3083. await updateQuickEntryItems({
  3084. id: row.id,
  3085. name: row.name,
  3086. iconUrl: row.icon || row.iconUrl,
  3087. tagText: row.tag || row.tagText,
  3088. jumpLink: row.link || row.jumpLink,
  3089. status: row.status,
  3090. sortOrder: i
  3091. });
  3092. }
  3093. } catch (error) {
  3094. console.error('排序更新失败:', error);
  3095. ElMessage.error('排序更新失败');
  3096. }
  3097. };
  3098. watch(
  3099. headerCategoryList,
  3100. () => {
  3101. nextTick(() => updateNavArrows());
  3102. },
  3103. { deep: true }
  3104. );
  3105. onMounted(() => {
  3106. startPlaceholderScroll();
  3107. // 获取当前配置
  3108. getCurrentSearch();
  3109. getCurrentAdLeftBtn();
  3110. getCarouselList();
  3111. getHeaderCategoryList();
  3112. getCategoryList();
  3113. getScenarioGlobalSettingsList();
  3114. getScenarioList();
  3115. getQuickEntryModuleList();
  3116. getQuickEntryItemsList();
  3117. getAdModuleList();
  3118. getRecommendThemeConfigList();
  3119. getRecommendCategoryList();
  3120. // 获取左侧广告配置
  3121. nextTick(() => {
  3122. updateNavArrows();
  3123. updateRecArrows();
  3124. });
  3125. // 延迟再次检测,确保图片加载后布局稳定
  3126. setTimeout(() => {
  3127. updateRecArrows();
  3128. }, 500);
  3129. window.addEventListener('resize', updateNavArrows);
  3130. window.addEventListener('resize', updateRecArrows);
  3131. });
  3132. onUnmounted(() => {
  3133. if (timer) clearInterval(timer);
  3134. window.removeEventListener('resize', updateNavArrows);
  3135. window.removeEventListener('resize', updateRecArrows);
  3136. });
  3137. watch(
  3138. activeRecommendList,
  3139. () => {
  3140. nextTick(() => {
  3141. updateRecArrows();
  3142. });
  3143. },
  3144. { deep: true }
  3145. );
  3146. // 监听子页签切换,确保切换回推荐设置时更新箭头状态
  3147. watch(activeSubTab, (newVal) => {
  3148. if (newVal === 'recommend') {
  3149. nextTick(() => {
  3150. setTimeout(() => {
  3151. updateRecArrows();
  3152. }, 300);
  3153. });
  3154. }
  3155. });
  3156. </script>
  3157. <style scoped>
  3158. /* 基础辅助类 */
  3159. .flex-column {
  3160. display: flex;
  3161. flex-direction: column;
  3162. }
  3163. .flex-center {
  3164. display: flex;
  3165. align-items: center;
  3166. }
  3167. .gap-5 {
  3168. gap: 5px;
  3169. }
  3170. .gap-10 {
  3171. gap: 10px;
  3172. }
  3173. .m-b-30 {
  3174. margin-bottom: 30px;
  3175. }
  3176. .scenario-editor-container {
  3177. padding: 32px 40px;
  3178. display: flex;
  3179. flex-direction: column;
  3180. background-color: #fff; /* 纯白背景 */
  3181. min-height: 100%;
  3182. }
  3183. .preview-section-standard {
  3184. background: #fcfdfe;
  3185. border: 1px solid #eef2f6;
  3186. border-radius: 8px;
  3187. padding: 32px;
  3188. margin-bottom: 32px;
  3189. }
  3190. .section-title-standard {
  3191. font-size: 16px;
  3192. font-weight: bold;
  3193. color: #333;
  3194. margin-bottom: 24px;
  3195. display: flex;
  3196. align-items: center;
  3197. }
  3198. .section-title-standard::before {
  3199. content: '';
  3200. width: 4px;
  3201. height: 16px;
  3202. background-color: #e60012;
  3203. margin-right: 12px;
  3204. border-radius: 2px;
  3205. }
  3206. .config-section-standard {
  3207. padding: 32px 0;
  3208. border-bottom: 1px solid #f0f0f0;
  3209. }
  3210. .settings-form-standard {
  3211. padding-left: 16px;
  3212. }
  3213. .preview-tip {
  3214. font-size: 12px;
  3215. color: #999;
  3216. margin-top: 16px;
  3217. }
  3218. .rank-action-btns {
  3219. display: flex;
  3220. flex-direction: column;
  3221. align-items: center;
  3222. gap: 4px;
  3223. }
  3224. .rank-btn-mini {
  3225. cursor: pointer;
  3226. font-size: 14px;
  3227. color: #909399;
  3228. transition: color 0.2s;
  3229. }
  3230. .rank-btn-mini:hover {
  3231. color: #e60012;
  3232. }
  3233. .standard-table :deep(.el-table__header) th {
  3234. background-color: #f8fafc;
  3235. color: #606266;
  3236. font-weight: bold;
  3237. }
  3238. .parameter-settings {
  3239. flex: 1;
  3240. min-height: 0; /* 关键:防止 flex 子项被内容撑开 */
  3241. display: flex;
  3242. flex-direction: column;
  3243. background-color: #fff;
  3244. overflow: hidden;
  3245. }
  3246. .sub-tabs {
  3247. display: flex;
  3248. padding: 0 20px;
  3249. border-bottom: 1px solid #f0f0f0;
  3250. flex-shrink: 0;
  3251. height: 48px; /* 固定高度,防止切换时抖动 */
  3252. align-items: center;
  3253. }
  3254. .sub-tab-item {
  3255. padding: 12px 20px;
  3256. cursor: pointer;
  3257. font-size: 14px;
  3258. color: #666;
  3259. font-weight: 500;
  3260. position: relative;
  3261. }
  3262. .sub-tab-item.active {
  3263. color: var(--primary-color);
  3264. }
  3265. .sub-tab-item.active::after {
  3266. content: '';
  3267. position: absolute;
  3268. bottom: 0;
  3269. left: 20px;
  3270. right: 20px;
  3271. height: 2px;
  3272. background-color: var(--primary-color);
  3273. }
  3274. .content-body {
  3275. flex: 1;
  3276. overflow-y: auto;
  3277. }
  3278. /* 搜索编辑区布局 */
  3279. .search-editor {
  3280. display: flex;
  3281. flex-direction: column;
  3282. padding: 20px 40px;
  3283. gap: 30px;
  3284. }
  3285. /* 预览区域 */
  3286. .preview-section {
  3287. background-color: #fff;
  3288. border: 1px dashed #dcdfe6;
  3289. border-radius: 8px;
  3290. padding: 20px;
  3291. }
  3292. .preview-title {
  3293. font-size: 13px;
  3294. color: #909399;
  3295. margin-bottom: 15px;
  3296. font-weight: bold;
  3297. }
  3298. .live-preview-box {
  3299. background-color: #fff;
  3300. padding: 40px 20px;
  3301. border-radius: 4px;
  3302. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
  3303. }
  3304. /* 仿真 Mockup */
  3305. .search-bar-mockup {
  3306. display: flex;
  3307. align-items: center;
  3308. max-width: 1400px; /* 进一步加宽容器 */
  3309. margin: 0 auto;
  3310. gap: 30px;
  3311. }
  3312. .mockup-left {
  3313. flex-shrink: 0;
  3314. text-align: center;
  3315. }
  3316. .main-title {
  3317. font-size: 32px;
  3318. font-weight: 800;
  3319. line-height: 1.2;
  3320. }
  3321. .sub-title {
  3322. font-size: 12px;
  3323. color: #999;
  3324. margin-top: 4px;
  3325. letter-spacing: 1px;
  3326. }
  3327. .mockup-center {
  3328. flex: 2; /* 增加中心区域的弹性占比,使搜索框更宽 */
  3329. }
  3330. .search-input-wrapper {
  3331. height: 44px;
  3332. border: 2px solid transparent; /* 由动态绑定控制颜色 */
  3333. border-radius: 8px;
  3334. display: flex;
  3335. align-items: center;
  3336. overflow: hidden;
  3337. position: relative;
  3338. }
  3339. .placeholder-scroll {
  3340. flex: 1;
  3341. padding: 0 15px;
  3342. height: 100%;
  3343. overflow: hidden;
  3344. }
  3345. .scroll-container {
  3346. height: 100%;
  3347. position: relative;
  3348. }
  3349. .scroll-item {
  3350. height: 100%;
  3351. display: flex;
  3352. align-items: center;
  3353. color: #999;
  3354. font-size: 15px;
  3355. }
  3356. .search-btn {
  3357. width: 90px;
  3358. height: 34px;
  3359. color: #fff;
  3360. display: flex;
  3361. align-items: center;
  3362. justify-content: center;
  3363. font-weight: bold;
  3364. font-size: 15px;
  3365. cursor: pointer;
  3366. border-radius: 6px;
  3367. letter-spacing: 2px;
  3368. margin-right: 4px;
  3369. transition: opacity 0.2s;
  3370. }
  3371. .search-btn:hover {
  3372. opacity: 0.9;
  3373. }
  3374. .hot-words {
  3375. margin-top: 8px;
  3376. display: flex;
  3377. gap: 15px;
  3378. padding-left: 5px;
  3379. }
  3380. .hot-word {
  3381. font-size: 12px;
  3382. color: #999;
  3383. cursor: pointer;
  3384. transition: color 0.2s;
  3385. }
  3386. .hot-word:hover {
  3387. color: var(--theme-color);
  3388. }
  3389. .mockup-right {
  3390. flex-shrink: 0;
  3391. }
  3392. .cart-btn {
  3393. display: flex;
  3394. align-items: center;
  3395. gap: 8px;
  3396. padding: 8px 16px;
  3397. border: 1px solid transparent; /* 由动态绑定控制颜色 */
  3398. border-radius: 8px;
  3399. font-size: 14px;
  3400. cursor: pointer;
  3401. background-color: #fff;
  3402. transition: all 0.2s;
  3403. }
  3404. .cart-btn:hover {
  3405. filter: brightness(0.95);
  3406. }
  3407. /* 广告图编辑区 */
  3408. .carousel-editor {
  3409. padding: 20px 40px;
  3410. display: flex;
  3411. flex-direction: column;
  3412. gap: 40px;
  3413. }
  3414. .editor-section {
  3415. display: flex;
  3416. flex-direction: column;
  3417. gap: 20px;
  3418. }
  3419. .section-header {
  3420. display: flex;
  3421. flex-direction: column;
  3422. gap: 4px;
  3423. }
  3424. .section-title {
  3425. font-size: 16px;
  3426. font-weight: bold;
  3427. color: #333;
  3428. }
  3429. .section-desc {
  3430. font-size: 13px;
  3431. color: #999;
  3432. }
  3433. /* 左侧广告设置 */
  3434. .left-ad-container {
  3435. display: flex;
  3436. flex-direction: column;
  3437. gap: 10px;
  3438. }
  3439. .left-ad-preview-wrapper {
  3440. width: 80px;
  3441. height: 460px;
  3442. border-radius: 8px;
  3443. overflow: hidden;
  3444. position: relative;
  3445. cursor: pointer;
  3446. transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  3447. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  3448. border: 1px solid #eee;
  3449. }
  3450. .left-ad-preview-wrapper.expanded {
  3451. width: 790px;
  3452. }
  3453. .left-ad-img {
  3454. width: 790px; /* 固定宽度,外层容器裁剪 */
  3455. height: 460px;
  3456. object-fit: cover;
  3457. display: block;
  3458. }
  3459. .left-ad-empty {
  3460. width: 100%;
  3461. height: 100%;
  3462. background-color: #f5f7fa;
  3463. display: flex;
  3464. flex-direction: column;
  3465. align-items: center;
  3466. justify-content: center;
  3467. color: #909399;
  3468. border: 1px dashed #dcdfe6;
  3469. }
  3470. .ad-actions {
  3471. position: absolute;
  3472. top: 20px;
  3473. right: 20px;
  3474. display: flex;
  3475. gap: 10px;
  3476. background: rgba(255, 255, 255, 0.9);
  3477. padding: 8px;
  3478. border-radius: 30px;
  3479. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  3480. }
  3481. .left-ad-settings {
  3482. flex: 1;
  3483. display: flex;
  3484. flex-direction: column;
  3485. gap: 15px;
  3486. padding: 10px 0; /* 移除背景和边框 */
  3487. }
  3488. .settings-input-ad {
  3489. max-width: 500px;
  3490. }
  3491. .left-ad-tip {
  3492. display: flex;
  3493. align-items: center;
  3494. gap: 6px;
  3495. font-size: 13px;
  3496. color: #a8abb2;
  3497. }
  3498. /* 轮播图预览 */
  3499. .carousel-preview-box {
  3500. padding: 10px 0;
  3501. }
  3502. .preview-mockup {
  3503. width: 552px;
  3504. height: 190px;
  3505. border-radius: 8px;
  3506. overflow: hidden;
  3507. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  3508. background-color: #f5f7fa;
  3509. }
  3510. /* 强力锁定轮播图指示器至左下角 */
  3511. :deep(.el-carousel__indicators--horizontal) {
  3512. left: 20px !important;
  3513. right: auto !important;
  3514. bottom: 15px !important;
  3515. transform: none !important;
  3516. width: fit-content !important;
  3517. margin: 0 !important;
  3518. padding: 0 !important;
  3519. display: flex !important;
  3520. justify-content: flex-start !important;
  3521. }
  3522. :deep(.el-carousel__indicator--horizontal) {
  3523. display: inline-block !important;
  3524. padding: 0 3px !important;
  3525. }
  3526. :deep(.el-carousel__indicators--inside) {
  3527. left: 20px !important;
  3528. transform: none !important;
  3529. }
  3530. :deep(.el-carousel__indicator) {
  3531. padding: 0 3px;
  3532. }
  3533. :deep(.el-carousel__button) {
  3534. width: 6px;
  3535. height: 6px;
  3536. border-radius: 50%;
  3537. background-color: rgba(255, 255, 255, 0.4) !important;
  3538. opacity: 1 !important;
  3539. transition: all 0.3s;
  3540. }
  3541. :deep(.el-carousel__indicator.is-active .el-carousel__button) {
  3542. width: 20px; /* 激活状态变为长胶囊 */
  3543. border-radius: 10px;
  3544. background-color: #ffffff !important;
  3545. }
  3546. .carousel-slide {
  3547. width: 100%;
  3548. height: 100%;
  3549. }
  3550. .carousel-slide img {
  3551. width: 100%;
  3552. height: 100%;
  3553. object-fit: cover;
  3554. }
  3555. .carousel-empty {
  3556. height: 100%;
  3557. display: flex;
  3558. flex-direction: column;
  3559. align-items: center;
  3560. justify-content: center;
  3561. color: #ccc;
  3562. background-color: #f9f9f9;
  3563. }
  3564. /* 轮播列表 */
  3565. .carousel-list-box {
  3566. display: flex;
  3567. flex-direction: column;
  3568. gap: 15px;
  3569. margin-top: 10px;
  3570. }
  3571. .list-toolbar {
  3572. display: flex;
  3573. justify-content: space-between;
  3574. align-items: center;
  3575. padding: 0 5px;
  3576. }
  3577. .btn-add-carousel {
  3578. padding: 0 25px;
  3579. font-weight: bold;
  3580. }
  3581. /* 自定义表头样式 */
  3582. :deep(.table-header-custom) {
  3583. background-color: #f8f9fb !important;
  3584. color: #333 !important;
  3585. font-weight: bold !important;
  3586. height: 50px;
  3587. }
  3588. .drag-tip {
  3589. font-size: 12px;
  3590. color: #a8abb2;
  3591. }
  3592. .table-img {
  3593. width: 140px;
  3594. height: 48px;
  3595. border-radius: 4px;
  3596. border: 1px solid #f0f0f0;
  3597. display: block;
  3598. }
  3599. .rank-box {
  3600. display: flex;
  3601. flex-direction: column;
  3602. align-items: center;
  3603. gap: 2px;
  3604. }
  3605. .rank-icon {
  3606. cursor: pointer;
  3607. color: #909399;
  3608. font-size: 16px;
  3609. transition: color 0.2s;
  3610. }
  3611. .rank-icon:hover {
  3612. color: #409eff;
  3613. }
  3614. /* 弹窗上传样式 */
  3615. .upload-placeholder {
  3616. width: 240px;
  3617. height: 82px;
  3618. border: 1px dashed #dcdfe6;
  3619. border-radius: 6px;
  3620. cursor: pointer;
  3621. overflow: hidden;
  3622. display: flex;
  3623. align-items: center;
  3624. justify-content: center;
  3625. background-color: #f5f7fa;
  3626. transition: border-color 0.2s;
  3627. }
  3628. .upload-placeholder:hover {
  3629. border-color: #409eff;
  3630. }
  3631. .form-preview-img {
  3632. width: 100%;
  3633. height: 100%;
  3634. object-fit: cover;
  3635. }
  3636. .upload-icon {
  3637. font-size: 28px;
  3638. color: #8c939d;
  3639. }
  3640. .upload-tip {
  3641. font-size: 12px;
  3642. color: #999;
  3643. margin-top: 5px;
  3644. line-height: 1.4;
  3645. }
  3646. .dialog-form-inner {
  3647. padding: 30px 40px; /* 大幅增加内边距,使其更高、更大气 */
  3648. }
  3649. .dialog-form-inner :deep(.el-form-item) {
  3650. margin-bottom: 25px; /* 增加表单项间距 */
  3651. }
  3652. .list-scroll-enter-active,
  3653. .list-scroll-leave-active {
  3654. transition: all 0.5s ease;
  3655. position: absolute;
  3656. width: 100%;
  3657. }
  3658. .list-scroll-enter-from {
  3659. transform: translateY(100%);
  3660. opacity: 0;
  3661. }
  3662. .list-scroll-leave-to {
  3663. transform: translateY(-100%);
  3664. opacity: 0;
  3665. }
  3666. /* 设置表单 */
  3667. .settings-section {
  3668. padding: 0 10px;
  3669. }
  3670. .settings-input {
  3671. max-width: 600px;
  3672. }
  3673. .color-picker-wrap {
  3674. display: flex;
  3675. align-items: center;
  3676. }
  3677. .tip-text {
  3678. font-size: 12px;
  3679. color: #999;
  3680. margin-top: 4px;
  3681. }
  3682. .color-val {
  3683. margin-left: 10px;
  3684. color: #666;
  3685. font-family: monospace;
  3686. }
  3687. /* 热词配置样式 */
  3688. .hot-words-config {
  3689. display: flex;
  3690. flex-direction: column;
  3691. gap: 10px;
  3692. width: 800px; /* 加宽配置区域 */
  3693. }
  3694. .hot-word-row {
  3695. display: flex;
  3696. align-items: center;
  3697. gap: 10px;
  3698. }
  3699. .hot-word-input-name {
  3700. width: 200px; /* 稍微加宽名称框 */
  3701. }
  3702. .hot-word-input-link {
  3703. flex: 1; /* 此时地址框会占据剩余更多空间 */
  3704. }
  3705. .add-hotword-btn {
  3706. width: fit-content;
  3707. padding: 0;
  3708. margin-top: 5px;
  3709. }
  3710. .footer-actions {
  3711. padding: 20px 40px;
  3712. border-top: 1px solid #f0f0f0;
  3713. display: flex;
  3714. gap: 15px;
  3715. flex-shrink: 0;
  3716. }
  3717. .btn-confirm {
  3718. padding: 0 30px;
  3719. }
  3720. .btn-reset {
  3721. padding: 0 30px;
  3722. }
  3723. /* 分类设置编辑区 */
  3724. .category-editor {
  3725. padding: 20px 40px;
  3726. display: flex;
  3727. flex-direction: column;
  3728. gap: 40px;
  3729. }
  3730. .category-preview-container {
  3731. padding: 20px 0;
  3732. display: flex;
  3733. justify-content: flex-start;
  3734. }
  3735. /* 图1: 仿真菜单 */
  3736. .category-menu-mockup {
  3737. width: 280px;
  3738. height: 398px;
  3739. background: #f8f9fa;
  3740. border-radius: 4px;
  3741. padding: 10px 0;
  3742. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  3743. position: relative;
  3744. z-index: 10;
  3745. overflow: visible; /* 必须可见,否则遮罩层无法伸出 */
  3746. }
  3747. /* 仿真菜单项 */
  3748. .menu-item {
  3749. height: 44px;
  3750. display: flex;
  3751. align-items: center;
  3752. padding: 0 15px;
  3753. cursor: pointer;
  3754. position: relative;
  3755. transition: all 0.1s;
  3756. background: transparent;
  3757. box-sizing: border-box;
  3758. }
  3759. .menu-item:hover {
  3760. background-color: #fff !important;
  3761. color: v-bind(categoryThemeColor);
  3762. border: 1px solid v-bind(categoryThemeColor);
  3763. border-right: none;
  3764. border-radius: 12px 0 0 12px;
  3765. z-index: 1000;
  3766. margin-left: 10px;
  3767. padding-left: 15px;
  3768. width: calc(100% - 10px);
  3769. }
  3770. .menu-icon {
  3771. width: 16px;
  3772. height: 16px;
  3773. margin-right: 15px; /* 增加间距 */
  3774. color: #999;
  3775. display: flex;
  3776. align-items: center;
  3777. justify-content: center;
  3778. flex-shrink: 0;
  3779. }
  3780. .menu-icon img {
  3781. width: 100%;
  3782. height: 100%;
  3783. object-fit: contain;
  3784. }
  3785. .menu-name {
  3786. font-size: 14px;
  3787. color: #333;
  3788. flex: 1;
  3789. white-space: nowrap;
  3790. overflow: hidden;
  3791. text-overflow: ellipsis;
  3792. }
  3793. /* 遮盖层:仅抹除中间垂直线,保留上下边框连贯 */
  3794. .menu-item:hover::after {
  3795. content: '';
  3796. position: absolute;
  3797. top: 0; /* 保持在边框内侧 */
  3798. bottom: 0; /* 保持在边框内侧 */
  3799. right: -1px; /* 贴合右边缘 */
  3800. width: 2px; /* 覆盖面板左边框 */
  3801. background: #fff;
  3802. z-index: 1001;
  3803. }
  3804. .menu-item:hover .menu-icon {
  3805. color: v-bind(categoryThemeColor);
  3806. }
  3807. .menu-name {
  3808. font-size: 14px;
  3809. color: #333;
  3810. }
  3811. .menu-item:hover .menu-name {
  3812. color: v-bind(categoryThemeColor);
  3813. font-weight: bold;
  3814. }
  3815. /* 图2: 右滑面板 */
  3816. .category-panel-mockup {
  3817. position: absolute;
  3818. left: 100%; /* 紧贴菜单项右侧 */
  3819. margin-left: 0;
  3820. top: -1px; /* 顶部边框对齐 */
  3821. width: 980px;
  3822. min-height: 480px;
  3823. background: #fff;
  3824. box-shadow: 15px 15px 40px rgba(0, 0, 0, 0.1);
  3825. border-radius: 0 12px 12px 12px;
  3826. z-index: 500;
  3827. border: 1px solid v-bind(categoryThemeColor);
  3828. display: none;
  3829. flex-direction: column;
  3830. box-sizing: border-box;
  3831. cursor: default;
  3832. }
  3833. /* 悬停时显示面板 */
  3834. .menu-item:hover .category-panel-mockup {
  3835. display: flex;
  3836. }
  3837. .panel-content {
  3838. flex: 1;
  3839. padding: 25px 30px;
  3840. position: relative; /* 为品牌位定位提供基准 */
  3841. overflow: hidden;
  3842. }
  3843. .panel-tabs {
  3844. display: flex;
  3845. gap: 15px;
  3846. margin-bottom: 25px;
  3847. padding-right: 200px; /* 为右上角品牌位留出空间 */
  3848. }
  3849. .panel-tab-item {
  3850. padding: 6px 14px;
  3851. background-color: #f5f5f5;
  3852. color: #666;
  3853. font-size: 12px;
  3854. border-radius: 4px;
  3855. cursor: pointer;
  3856. transition: all 0.2s;
  3857. }
  3858. .panel-tab-item:hover {
  3859. background-color: #f5f5f5; /* 保持浅灰或根据主题调整 */
  3860. color: v-bind(categoryThemeColor);
  3861. filter: brightness(0.95);
  3862. }
  3863. .panel-body {
  3864. display: flex;
  3865. flex-direction: column; /* 改为垂直布局,内容横向撑开 */
  3866. flex: 1;
  3867. }
  3868. .panel-main {
  3869. flex: 1;
  3870. overflow-y: auto;
  3871. max-height: 400px;
  3872. }
  3873. .category-group {
  3874. margin-bottom: 15px;
  3875. display: flex;
  3876. }
  3877. .group-title {
  3878. width: 80px;
  3879. font-size: 12px;
  3880. font-weight: bold;
  3881. color: #333;
  3882. flex-shrink: 0;
  3883. line-height: 1.6;
  3884. }
  3885. .group-items {
  3886. flex: 1;
  3887. display: flex;
  3888. flex-wrap: wrap;
  3889. gap: 6px 15px;
  3890. }
  3891. .group-item {
  3892. font-size: 12px;
  3893. color: #666;
  3894. cursor: pointer;
  3895. transition: color 0.2s;
  3896. }
  3897. .group-item:hover {
  3898. color: v-bind(categoryThemeColor);
  3899. }
  3900. /* 品牌位移至右上角 (图3) */
  3901. .panel-side {
  3902. display: none; /* 移除侧边栏布局 */
  3903. }
  3904. .brand-box {
  3905. position: absolute;
  3906. top: 25px;
  3907. right: 30px;
  3908. display: flex;
  3909. flex-direction: column;
  3910. align-items: flex-end; /* 右对齐更美观 */
  3911. z-index: 105;
  3912. }
  3913. .brand-main-title {
  3914. font-size: 20px;
  3915. font-weight: 900;
  3916. color: v-bind(categoryThemeColor);
  3917. margin-bottom: 4px;
  3918. display: flex;
  3919. align-items: baseline;
  3920. }
  3921. .brand-strong {
  3922. color: #333;
  3923. margin-left: 2px;
  3924. font-size: 20px;
  3925. }
  3926. .brand-notes {
  3927. display: flex;
  3928. gap: 5px;
  3929. color: v-bind(categoryThemeColor);
  3930. font-size: 12px;
  3931. justify-content: flex-end;
  3932. }
  3933. .note-item {
  3934. cursor: pointer;
  3935. }
  3936. .note-sep {
  3937. color: #eee;
  3938. margin: 0 2px;
  3939. }
  3940. /* 工具栏主题色设置 - 专业版 */
  3941. .theme-color-setting-pro {
  3942. display: flex;
  3943. align-items: center;
  3944. gap: 8px;
  3945. margin-right: 20px;
  3946. }
  3947. .theme-color-setting-pro .label {
  3948. font-size: 14px;
  3949. color: #333;
  3950. }
  3951. .theme-color-setting-pro .value {
  3952. font-size: 13px;
  3953. color: #666;
  3954. margin-left: 5px;
  3955. font-family: monospace;
  3956. }
  3957. /* 覆盖 el-color-picker 样式 */
  3958. :deep(.theme-color-setting-pro .el-color-picker__trigger) {
  3959. width: 28px;
  3960. height: 28px;
  3961. padding: 2px;
  3962. border-radius: 4px;
  3963. }
  3964. /* 列表样式 */
  3965. .table-icon-preview {
  3966. width: 24px;
  3967. height: 24px;
  3968. }
  3969. .tag-wrap {
  3970. display: flex;
  3971. flex-wrap: wrap;
  3972. gap: 5px;
  3973. }
  3974. .m-r-5 {
  3975. margin-right: 5px;
  3976. }
  3977. .m-t-20 {
  3978. margin-top: 20px;
  3979. }
  3980. /* 弹窗样式 */
  3981. .panel-config-section {
  3982. background: #fafafa;
  3983. padding: 15px;
  3984. border-radius: 4px;
  3985. border: 1px dashed #eee;
  3986. }
  3987. /* 移除冗余的 .config-subtitle 定义,统一使用下方全局定义的样式 */
  3988. .notes-config-list {
  3989. display: flex;
  3990. flex-direction: column;
  3991. gap: 10px;
  3992. }
  3993. .note-config-row {
  3994. display: flex;
  3995. gap: 10px;
  3996. align-items: center;
  3997. }
  3998. .field-tip {
  3999. font-size: 12px;
  4000. color: #999;
  4001. margin-top: 5px;
  4002. }
  4003. /* 正方形图标上传 */
  4004. .upload-placeholder-square {
  4005. width: 80px;
  4006. height: 80px;
  4007. border: 1px dashed #d9d9d9;
  4008. border-radius: 4px;
  4009. cursor: pointer;
  4010. display: flex;
  4011. align-items: center;
  4012. justify-content: center;
  4013. background-color: #fafafa;
  4014. transition: border-color 0.2s;
  4015. }
  4016. .upload-placeholder-square:hover {
  4017. border-color: v-bind(categoryThemeColor);
  4018. }
  4019. .upload-placeholder-square .upload-icon {
  4020. font-size: 20px;
  4021. color: #999;
  4022. }
  4023. .form-preview-img-square {
  4024. width: 16px;
  4025. height: 16px;
  4026. object-fit: contain;
  4027. }
  4028. /* 头部分类样式 */
  4029. .header-category-editor {
  4030. padding: 20px 40px;
  4031. display: flex;
  4032. flex-direction: column;
  4033. gap: 40px;
  4034. }
  4035. .header-preview-outer {
  4036. width: 100%;
  4037. background: #f8f9fa;
  4038. padding: 40px 20px;
  4039. border-radius: 8px;
  4040. display: block; /* 改为 block 以便支持内部滚动 */
  4041. overflow-x: auto; /* 允许内部滚动,防止溢出 */
  4042. box-sizing: border-box;
  4043. }
  4044. .header-preview-box {
  4045. width: 1350px; /* 强制保持 1350px 宽度以高度还原设计 */
  4046. height: 60px;
  4047. background: #fff;
  4048. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  4049. border-radius: 4px;
  4050. display: flex;
  4051. align-items: center;
  4052. padding: 0 40px; /* 为箭头预留空间 */
  4053. margin: 0 auto; /* 在容器足够大时居中 */
  4054. flex-shrink: 0; /* 禁止缩小 */
  4055. position: relative;
  4056. overflow: hidden; /* 关键:确保内部超出部分被剪裁,由滚动容器处理 */
  4057. }
  4058. .header-nav-scroll {
  4059. flex: 1;
  4060. overflow-x: auto;
  4061. scrollbar-width: none;
  4062. display: flex;
  4063. align-items: center;
  4064. scroll-behavior: smooth;
  4065. }
  4066. .header-nav-scroll::-webkit-scrollbar {
  4067. display: none;
  4068. }
  4069. .header-nav-list {
  4070. display: flex;
  4071. align-items: center;
  4072. gap: 30px;
  4073. flex-shrink: 0;
  4074. }
  4075. .nav-arrow {
  4076. position: absolute;
  4077. top: 50%;
  4078. transform: translateY(-50%);
  4079. width: 24px;
  4080. height: 24px;
  4081. background: #eee;
  4082. border-radius: 50%;
  4083. display: flex;
  4084. align-items: center;
  4085. justify-content: center;
  4086. cursor: pointer;
  4087. z-index: 10;
  4088. color: #666;
  4089. transition: all 0.2s;
  4090. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  4091. }
  4092. .nav-arrow:hover {
  4093. background: #e0e0e0;
  4094. color: #333;
  4095. }
  4096. .left-arrow {
  4097. left: 8px;
  4098. }
  4099. .right-arrow {
  4100. right: 8px;
  4101. }
  4102. .header-nav-item {
  4103. display: flex;
  4104. align-items: center;
  4105. gap: 8px;
  4106. cursor: pointer;
  4107. white-space: nowrap;
  4108. flex-shrink: 0;
  4109. }
  4110. .header-nav-item .item-icon {
  4111. width: 22px;
  4112. height: 22px;
  4113. display: flex;
  4114. align-items: center;
  4115. justify-content: center;
  4116. }
  4117. .header-nav-item .item-icon img {
  4118. width: 100%;
  4119. height: 100%;
  4120. object-fit: contain;
  4121. }
  4122. .header-nav-item .item-text {
  4123. font-size: 16px;
  4124. font-weight: bold;
  4125. color: #333;
  4126. transition: color 0.2s;
  4127. }
  4128. .header-nav-item:hover .item-text {
  4129. color: var(--hover-color);
  4130. }
  4131. /* 广告模块样式 */
  4132. .ad-preview-grid {
  4133. display: flex;
  4134. flex-wrap: nowrap;
  4135. gap: 20px;
  4136. max-width: 1600px;
  4137. margin: 0;
  4138. }
  4139. .ad-item {
  4140. background: #f8f9fa;
  4141. border-radius: 12px;
  4142. padding: 15px;
  4143. position: relative;
  4144. overflow: hidden;
  4145. box-sizing: border-box;
  4146. }
  4147. .ad-item:hover .ad-hover-mask {
  4148. display: flex;
  4149. }
  4150. .ad-hover-mask {
  4151. position: absolute;
  4152. top: 0;
  4153. left: 0;
  4154. right: 0;
  4155. bottom: 0;
  4156. background: rgba(0, 0, 0, 0.4);
  4157. display: none;
  4158. align-items: center;
  4159. justify-content: center;
  4160. z-index: 100;
  4161. }
  4162. .ad-header {
  4163. display: flex;
  4164. justify-content: space-between;
  4165. align-items: baseline;
  4166. margin-bottom: 12px;
  4167. }
  4168. .ad-title-main {
  4169. font-size: 18px;
  4170. font-weight: 800;
  4171. color: #333;
  4172. }
  4173. .ad-title-sub {
  4174. font-size: 13px;
  4175. }
  4176. .ad-title-sub.orange {
  4177. color: #f58220;
  4178. }
  4179. .ad-title-sub.gray {
  4180. color: #999;
  4181. }
  4182. /* Ad 1: 百亿补贴 */
  4183. .ad-products-subsidy {
  4184. display: flex;
  4185. gap: 15px;
  4186. }
  4187. .ad-subsidy .product-item {
  4188. flex: 1;
  4189. text-align: center;
  4190. }
  4191. .ad-subsidy .product-img {
  4192. width: 94px;
  4193. height: 94px;
  4194. background: #fff;
  4195. border-radius: 8px;
  4196. margin-bottom: 8px;
  4197. overflow: hidden;
  4198. }
  4199. .ad-subsidy .product-img img {
  4200. width: 100%;
  4201. height: 100%;
  4202. object-fit: contain;
  4203. }
  4204. .ad-subsidy .product-price {
  4205. color: #e60012;
  4206. font-weight: bold;
  4207. font-size: 15px;
  4208. }
  4209. .ad-ranking {
  4210. padding: 12px !important;
  4211. }
  4212. .ad-ranking .ad-header {
  4213. margin-bottom: 8px;
  4214. }
  4215. /* Ad 2: 榜单 */
  4216. .ad-products-ranking {
  4217. display: flex;
  4218. gap: 8px;
  4219. flex: 1;
  4220. }
  4221. .ranking-item {
  4222. flex: 1;
  4223. background: #fff;
  4224. border-radius: 8px;
  4225. padding: 6px 5px 0;
  4226. position: relative;
  4227. text-align: center;
  4228. display: flex;
  4229. flex-direction: column;
  4230. align-items: center;
  4231. overflow: hidden;
  4232. height: 135px; /* 高度调小一点 */
  4233. }
  4234. .ranking-badge {
  4235. display: inline-block;
  4236. background: #fff3e5;
  4237. color: #f58220;
  4238. font-size: 11px;
  4239. padding: 2px 10px;
  4240. border-radius: 20px;
  4241. white-space: nowrap;
  4242. margin-bottom: 4px;
  4243. transform: scale(0.9); /* 略微缩小标签以腾出空间 */
  4244. }
  4245. .ranking-item .product-img {
  4246. width: 80px; /* 略微缩小图片从 84 降至 80 */
  4247. height: 80px;
  4248. margin-bottom: 4px;
  4249. display: flex;
  4250. align-items: center;
  4251. justify-content: center;
  4252. }
  4253. .ranking-item .product-img img {
  4254. width: 100%;
  4255. height: 100%;
  4256. object-fit: contain;
  4257. }
  4258. .ranking-footer {
  4259. background: #fff1f1;
  4260. color: #e60012;
  4261. font-size: 12px;
  4262. padding: 5px 0;
  4263. font-weight: bold;
  4264. width: 100%;
  4265. margin-top: auto;
  4266. line-height: 1.2;
  4267. }
  4268. /* Ad 3: 品牌 */
  4269. .ad-brands-content {
  4270. display: flex;
  4271. gap: 10px;
  4272. }
  4273. .brand-item {
  4274. flex: 1;
  4275. text-align: center;
  4276. }
  4277. .brand-logo {
  4278. width: 50px;
  4279. height: 50px;
  4280. margin: 0 auto 8px;
  4281. background: #fff;
  4282. border-radius: 4px;
  4283. padding: 5px;
  4284. }
  4285. .brand-logo img {
  4286. width: 100%;
  4287. height: 100%;
  4288. object-fit: contain;
  4289. }
  4290. .brand-name {
  4291. font-size: 11px;
  4292. color: #0071bc;
  4293. margin-bottom: 5px;
  4294. height: 20px;
  4295. line-height: 20px;
  4296. white-space: nowrap;
  4297. overflow: hidden;
  4298. text-overflow: ellipsis;
  4299. width: 100%;
  4300. }
  4301. .brand-tag-btn {
  4302. display: inline-block;
  4303. padding: 2px 10px;
  4304. border: 1px solid #e60012;
  4305. color: #e60012;
  4306. border-radius: 10px;
  4307. font-size: 11px;
  4308. }
  4309. /* Ad 4 & 5: 精选/新品 */
  4310. .ad-products-selection {
  4311. display: flex;
  4312. gap: 10px;
  4313. }
  4314. .selection-item {
  4315. flex: 1;
  4316. text-align: center;
  4317. }
  4318. .selection-item .product-img {
  4319. width: 84px;
  4320. height: 84px;
  4321. background: #fff;
  4322. border-radius: 8px;
  4323. margin-bottom: 5px;
  4324. }
  4325. .selection-item .product-img img {
  4326. width: 100%;
  4327. height: 100%;
  4328. object-fit: contain;
  4329. }
  4330. .product-price-row {
  4331. display: flex;
  4332. align-items: baseline;
  4333. justify-content: flex-start;
  4334. gap: 2px;
  4335. }
  4336. .product-price-row.center {
  4337. justify-content: center;
  4338. }
  4339. .p-unit {
  4340. font-size: 12px;
  4341. color: #e60012;
  4342. font-weight: bold;
  4343. }
  4344. .p-val {
  4345. font-size: 16px;
  4346. color: #e60012;
  4347. font-weight: 800;
  4348. }
  4349. .p-tag {
  4350. background: #3fa9f5;
  4351. color: #fff;
  4352. font-size: 10px;
  4353. padding: 0 4px;
  4354. border-radius: 2px;
  4355. margin-left: 2px;
  4356. }
  4357. /* 选择弹窗样式 */
  4358. .select-item-row {
  4359. display: flex;
  4360. align-items: center;
  4361. padding: 12px;
  4362. border: 1px solid #eee;
  4363. border-radius: 8px;
  4364. margin-bottom: 10px;
  4365. cursor: pointer;
  4366. transition: all 0.2s;
  4367. }
  4368. .select-item-row:hover {
  4369. border-color: #409eff;
  4370. background: #f0f7ff;
  4371. }
  4372. .select-item-row.active {
  4373. border-color: #409eff;
  4374. background: #f0f7ff;
  4375. }
  4376. .select-item-img {
  4377. width: 80px;
  4378. height: 80px;
  4379. object-fit: contain;
  4380. margin-right: 20px;
  4381. background: #f9f9f9;
  4382. border-radius: 4px;
  4383. }
  4384. .select-item-info {
  4385. flex: 1;
  4386. }
  4387. .select-item-name {
  4388. font-size: 14px;
  4389. color: #333;
  4390. margin-bottom: 4px;
  4391. }
  4392. .select-item-price {
  4393. color: #e60012;
  4394. font-weight: bold;
  4395. }
  4396. .select-pagination {
  4397. margin-top: 30px;
  4398. display: flex;
  4399. justify-content: center;
  4400. padding-bottom: 20px;
  4401. }
  4402. .select-item-id {
  4403. font-size: 11px;
  4404. color: #999;
  4405. margin-top: 2px;
  4406. }
  4407. /* 场景方案预览样式 */
  4408. .scenario-preview-outer {
  4409. width: 100%;
  4410. overflow-x: auto;
  4411. padding: 10px 0;
  4412. }
  4413. .scenario-preview-box-clean {
  4414. width: 1600px;
  4415. max-width: 100%;
  4416. height: 158px;
  4417. background: var(--s-theme-color, #66e0a3);
  4418. border-radius: 16px; /* 加大圆角 */
  4419. display: flex;
  4420. align-items: center;
  4421. padding: 0 20px 0 32px; /* 减小左边距使标题区左移 */
  4422. box-sizing: border-box;
  4423. position: relative;
  4424. overflow: hidden;
  4425. }
  4426. .s-title-group {
  4427. display: flex;
  4428. align-items: center;
  4429. gap: 12px;
  4430. margin-bottom: 12px;
  4431. white-space: nowrap; /* 强制不换行,向后追加 */
  4432. }
  4433. .s-main-title {
  4434. font-size: 24px; /* 调整为 24px */
  4435. font-weight: 900;
  4436. color: #fff;
  4437. flex-shrink: 0; /* 确保不收缩 */
  4438. }
  4439. .s-sub-title-inline {
  4440. font-size: 24px; /* 调整为 24px */
  4441. font-weight: 900;
  4442. color: #fff;
  4443. flex-shrink: 0; /* 确保不收缩 */
  4444. }
  4445. .s-btn-wrap {
  4446. margin-top: 15px;
  4447. }
  4448. .s-btn-premium {
  4449. display: inline-flex;
  4450. align-items: center;
  4451. background-color: #fff;
  4452. color: #e60012;
  4453. font-size: 14px;
  4454. font-weight: bold;
  4455. padding: 6px 20px;
  4456. border-radius: 20px;
  4457. }
  4458. .scenario-card-premium {
  4459. width: 288px;
  4460. height: 130px;
  4461. border-radius: 8px;
  4462. display: flex;
  4463. flex-direction: column;
  4464. position: relative;
  4465. }
  4466. .card-top-header {
  4467. height: 42px; /* 固定高度,确保下方图片间距准确 */
  4468. display: flex;
  4469. justify-content: space-between;
  4470. align-items: center;
  4471. padding: 0 12px;
  4472. box-sizing: border-box;
  4473. }
  4474. .card-titles-group {
  4475. display: flex;
  4476. align-items: center;
  4477. gap: 8px;
  4478. }
  4479. .card-main-title {
  4480. font-size: 15px;
  4481. font-weight: bold;
  4482. }
  4483. .card-sub-title {
  4484. font-size: 13px;
  4485. font-weight: bold;
  4486. }
  4487. .card-arrow-icon {
  4488. width: 18px;
  4489. height: 18px;
  4490. border-radius: 50%;
  4491. color: #fff;
  4492. display: flex;
  4493. align-items: center;
  4494. justify-content: center;
  4495. font-size: 10px;
  4496. }
  4497. .card-image-content {
  4498. width: 272px;
  4499. height: 80px;
  4500. margin: 0 8px 8px 8px; /* 左右下保留相同间距 (8px) */
  4501. border-radius: 4px;
  4502. overflow: hidden;
  4503. }
  4504. .card-image-content img {
  4505. width: 100%;
  4506. height: 100%;
  4507. object-fit: cover;
  4508. }
  4509. /* 响应式逻辑:优先隐藏最后一个方案卡片,再隐藏副标题 */
  4510. @media (max-width: 1680px) {
  4511. .scenario-preview-box-clean {
  4512. width: 1300px;
  4513. }
  4514. .hidden-card-fourth {
  4515. display: none !important;
  4516. }
  4517. }
  4518. @media (max-width: 1400px) {
  4519. .scenario-preview-box-clean {
  4520. width: 1000px;
  4521. }
  4522. .s-sub-title-inline {
  4523. display: none !important;
  4524. }
  4525. }
  4526. .scenario-header-left {
  4527. width: 320px;
  4528. flex-shrink: 0;
  4529. display: flex;
  4530. flex-direction: column;
  4531. }
  4532. .scenario-cards-wrap {
  4533. flex: 1;
  4534. display: flex;
  4535. justify-content: flex-end;
  4536. gap: 12px; /* 间距调小 */
  4537. }
  4538. .scenario-card {
  4539. width: 288px;
  4540. height: 130px;
  4541. background-color: #fff;
  4542. border-radius: 12px;
  4543. padding: 12px;
  4544. box-sizing: border-box;
  4545. display: flex;
  4546. flex-direction: column;
  4547. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  4548. cursor: pointer;
  4549. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  4550. border: 1px solid rgba(0, 0, 0, 0.02);
  4551. }
  4552. .scenario-card:hover {
  4553. transform: translateY(-8px);
  4554. box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
  4555. border-color: rgba(102, 224, 163, 0.3);
  4556. }
  4557. .card-top {
  4558. display: flex;
  4559. justify-content: space-between;
  4560. align-items: center;
  4561. margin-bottom: 8px;
  4562. padding: 0 5px;
  4563. }
  4564. .card-titles {
  4565. display: flex;
  4566. align-items: center;
  4567. gap: 8px;
  4568. }
  4569. .card-main-title {
  4570. font-size: 18px;
  4571. font-weight: bold;
  4572. }
  4573. .card-sub-title {
  4574. font-size: 12px;
  4575. }
  4576. .card-arrow {
  4577. width: 18px;
  4578. height: 18px;
  4579. border-radius: 50%;
  4580. color: #fff;
  4581. display: flex;
  4582. align-items: center;
  4583. justify-content: center;
  4584. font-size: 10px;
  4585. }
  4586. .card-img {
  4587. width: 272px;
  4588. height: 80px;
  4589. border-radius: 6px;
  4590. overflow: hidden;
  4591. background-color: #f5f7fa;
  4592. }
  4593. .card-img img {
  4594. width: 100%;
  4595. height: 100%;
  4596. object-fit: cover;
  4597. }
  4598. /* 响应式调整 */
  4599. @media (max-width: 1500px) {
  4600. .scenario-preview-box {
  4601. width: 1300px;
  4602. }
  4603. .hidden-card-fourth {
  4604. display: none !important;
  4605. }
  4606. }
  4607. @media (max-width: 1200px) {
  4608. .scenario-preview-box {
  4609. width: 1000px;
  4610. }
  4611. .s-sub-title {
  4612. display: none !important;
  4613. }
  4614. }
  4615. /* 广告模块弹窗精修 */
  4616. .ad-setup-dialog :deep(.el-dialog__body) {
  4617. padding: 30px 40px;
  4618. max-height: 700px;
  4619. overflow-y: auto;
  4620. }
  4621. .brand-name-display {
  4622. font-size: 15px;
  4623. font-weight: bold;
  4624. color: #333;
  4625. }
  4626. .ad-setup-dialog :deep(.el-table__row) {
  4627. height: 90px; /* 增加行高,大气美观 */
  4628. }
  4629. .config-subtitle {
  4630. font-size: 15px;
  4631. font-weight: bold;
  4632. color: #333;
  4633. margin: 25px 0 15px;
  4634. display: flex;
  4635. align-items: center;
  4636. line-height: 1;
  4637. }
  4638. .config-subtitle::before {
  4639. content: '';
  4640. width: 3px;
  4641. height: 14px;
  4642. background: #e60012;
  4643. margin-right: 8px;
  4644. }
  4645. /* 快捷入口预览样式 */
  4646. .quick-entry-preview-outer {
  4647. display: flex;
  4648. flex-direction: column;
  4649. align-items: flex-start;
  4650. padding: 20px 0;
  4651. }
  4652. .qe-mockup-card {
  4653. width: 230px;
  4654. height: 167px;
  4655. background: #fff;
  4656. border-radius: 12px;
  4657. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  4658. padding: 12px 16px;
  4659. box-sizing: border-box;
  4660. display: flex;
  4661. flex-direction: column;
  4662. overflow: hidden;
  4663. position: relative;
  4664. }
  4665. .qe-card-header {
  4666. display: flex;
  4667. justify-content: space-between;
  4668. align-items: center;
  4669. margin-bottom: 12px;
  4670. }
  4671. .qe-card-title {
  4672. font-size: 16px;
  4673. font-weight: bold;
  4674. color: #333;
  4675. }
  4676. .qe-header-arrow {
  4677. font-size: 12px;
  4678. color: #999;
  4679. }
  4680. .qe-grid-container {
  4681. width: 198px;
  4682. height: 108px;
  4683. margin: 0 auto;
  4684. position: relative;
  4685. overflow: hidden;
  4686. }
  4687. .qe-grid-wrapper {
  4688. display: flex;
  4689. transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  4690. height: 100%;
  4691. }
  4692. .qe-grid-page {
  4693. width: 198px;
  4694. flex-shrink: 0;
  4695. display: grid;
  4696. grid-template-columns: repeat(4, 1fr);
  4697. grid-template-rows: repeat(2, 1fr);
  4698. gap: 12px 0;
  4699. }
  4700. .qe-item {
  4701. display: flex;
  4702. flex-direction: column;
  4703. align-items: center;
  4704. gap: 4px;
  4705. }
  4706. .qe-icon-wrap {
  4707. width: 24px;
  4708. height: 24px;
  4709. position: relative;
  4710. display: flex;
  4711. align-items: center;
  4712. justify-content: center;
  4713. }
  4714. .qe-icon-img {
  4715. width: 24px;
  4716. height: 24px;
  4717. object-fit: contain;
  4718. }
  4719. .qe-icon-placeholder {
  4720. font-size: 20px;
  4721. color: #666;
  4722. }
  4723. .qe-tag-bubble {
  4724. position: absolute;
  4725. top: -8px;
  4726. right: -15px;
  4727. background: #ff4d4f;
  4728. color: #fff;
  4729. font-size: 9px; /* 精确 9px */
  4730. padding: 1px 5px;
  4731. border-radius: 10px;
  4732. white-space: nowrap;
  4733. transform: scale(0.9);
  4734. z-index: 100; /* 提升至最高图层 */
  4735. }
  4736. .qe-name {
  4737. font-size: 11px; /* 精确 11px */
  4738. color: #333;
  4739. text-align: center;
  4740. white-space: nowrap;
  4741. overflow: hidden;
  4742. text-overflow: ellipsis;
  4743. width: 100%;
  4744. }
  4745. .qe-nav-btns {
  4746. position: absolute;
  4747. top: 0;
  4748. left: 0;
  4749. right: 0;
  4750. bottom: 0;
  4751. pointer-events: none;
  4752. }
  4753. .qe-nav-btn {
  4754. position: absolute;
  4755. top: 50%;
  4756. transform: translateY(-50%);
  4757. width: 22px;
  4758. height: 22px;
  4759. background: rgba(255, 255, 255, 0.9);
  4760. border-radius: 50%;
  4761. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  4762. display: flex;
  4763. align-items: center;
  4764. justify-content: center;
  4765. cursor: pointer;
  4766. pointer-events: auto;
  4767. color: #666;
  4768. z-index: 110; /* 提升至最高图层,确保不被内容遮挡 */
  4769. }
  4770. .qe-nav-btn:hover {
  4771. background: #fff;
  4772. color: #e60012;
  4773. }
  4774. .qe-nav-btn.prev {
  4775. left: -10px;
  4776. }
  4777. .qe-nav-btn.next {
  4778. right: -10px;
  4779. }
  4780. /* 快捷入口编辑器特定样式 */
  4781. .quick-entry-editor {
  4782. display: flex;
  4783. flex-direction: column;
  4784. padding: 20px 40px;
  4785. gap: 30px;
  4786. }
  4787. .config-form-inline {
  4788. background: #fcfdfe;
  4789. padding: 20px;
  4790. border-radius: 8px;
  4791. border: 1px solid #eef2f6;
  4792. }
  4793. .table-icon-cell {
  4794. display: flex;
  4795. align-items: center;
  4796. justify-content: center;
  4797. }
  4798. /* 推荐设置编辑区容器 */
  4799. .recommend-editor-container {
  4800. padding: 24px;
  4801. border-bottom: none !important;
  4802. }
  4803. /* 推荐设置预览样式 */
  4804. .recommend-preview-outer {
  4805. width: 100%;
  4806. padding: 10px 0 20px;
  4807. position: relative;
  4808. }
  4809. .recommend-preview-container {
  4810. width: 1600px !important; /* 严格按照要求设置背景尺寸 */
  4811. height: 88px !important;
  4812. position: relative;
  4813. background: #fff;
  4814. border-radius: 8px;
  4815. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  4816. overflow: hidden;
  4817. margin-bottom: 10px;
  4818. }
  4819. .recommend-preview-box {
  4820. width: 100% !important;
  4821. height: 100% !important;
  4822. display: flex;
  4823. align-items: center;
  4824. padding: 0 40px;
  4825. box-sizing: border-box;
  4826. overflow-x: auto;
  4827. scroll-behavior: smooth;
  4828. -ms-overflow-style: none;
  4829. scrollbar-width: none;
  4830. }
  4831. .recommend-preview-box::-webkit-scrollbar {
  4832. display: none;
  4833. }
  4834. .recommend-item {
  4835. display: flex;
  4836. align-items: center;
  4837. padding: 10px 24px;
  4838. margin: 0 4px;
  4839. cursor: pointer;
  4840. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  4841. height: 60px;
  4842. border-radius: 8px;
  4843. flex-shrink: 0;
  4844. }
  4845. .recommend-item:hover {
  4846. background: #f5f5f5;
  4847. }
  4848. .recommend-item.active {
  4849. background: transparent;
  4850. }
  4851. .recommend-icon {
  4852. width: 32px;
  4853. height: 32px;
  4854. margin-right: 12px;
  4855. flex-shrink: 0;
  4856. display: flex;
  4857. align-items: center;
  4858. justify-content: center;
  4859. }
  4860. .table-icon-preview {
  4861. width: 40px;
  4862. height: 40px;
  4863. background: #f5f5f5;
  4864. border-radius: 4px;
  4865. display: flex;
  4866. align-items: center;
  4867. justify-content: center;
  4868. margin: 0 auto;
  4869. overflow: hidden;
  4870. }
  4871. .row-icon {
  4872. width: 100%;
  4873. height: 100%;
  4874. object-fit: contain;
  4875. }
  4876. .recommend-text {
  4877. display: flex;
  4878. flex-direction: column;
  4879. white-space: nowrap;
  4880. }
  4881. .r-main-title {
  4882. font-size: 16px;
  4883. font-weight: bold;
  4884. line-height: 1.4;
  4885. white-space: nowrap;
  4886. }
  4887. .r-sub-title {
  4888. font-size: 12px;
  4889. color: #999;
  4890. margin-top: 2px;
  4891. white-space: nowrap;
  4892. }
  4893. .recommend-nav-btn {
  4894. position: absolute;
  4895. top: 50%;
  4896. transform: translateY(-50%);
  4897. width: 32px;
  4898. height: 32px;
  4899. background: rgba(255, 255, 255, 0.9);
  4900. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  4901. border-radius: 50%;
  4902. display: flex;
  4903. align-items: center;
  4904. justify-content: center;
  4905. cursor: pointer;
  4906. color: #666;
  4907. z-index: 100;
  4908. transition: all 0.2s;
  4909. border: 1px solid #eee;
  4910. }
  4911. .recommend-nav-btn:hover {
  4912. color: var(--r-theme-color);
  4913. background: #fff;
  4914. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  4915. }
  4916. .recommend-nav-btn.prev {
  4917. left: 10px;
  4918. }
  4919. .recommend-nav-btn.next {
  4920. right: 10px;
  4921. }
  4922. /* 修复色块标签换行 */
  4923. .config-section-standard :deep(.el-form-item__label) {
  4924. white-space: nowrap;
  4925. }
  4926. /* 去除底部冗余线条 */
  4927. .recommend-editor-container :deep(.el-table__inner-wrapper::before),
  4928. .recommend-editor-container :deep(.el-table--border::after),
  4929. .recommend-editor-container :deep(.el-table--group::after),
  4930. .recommend-editor-container :deep(.el-table::before) {
  4931. display: none !important;
  4932. }
  4933. .recommend-icon img {
  4934. width: 100%;
  4935. height: 100%;
  4936. object-fit: contain;
  4937. background: #f2f2f2;
  4938. border-radius: 4px;
  4939. }
  4940. /* 已选商品管理弹窗增强 */
  4941. .selected-products-header {
  4942. display: flex;
  4943. justify-content: space-between;
  4944. align-items: center;
  4945. margin-bottom: 20px;
  4946. padding: 10px 0;
  4947. }
  4948. .selected-products-header .right-info {
  4949. font-size: 14px;
  4950. color: #666;
  4951. }
  4952. .selected-products-header .count {
  4953. color: #e60012;
  4954. font-weight: 800;
  4955. margin: 0 4px;
  4956. }
  4957. .price-text {
  4958. color: #e60012;
  4959. font-weight: bold;
  4960. }
  4961. .empty-placeholder {
  4962. padding: 60px 0;
  4963. }
  4964. /* 商品多选抽屉样式 */
  4965. .product-selection-drawer :deep(.el-drawer__body) {
  4966. padding: 0;
  4967. background: #fcfdfe;
  4968. }
  4969. .drawer-content-wrapper {
  4970. display: flex;
  4971. flex-direction: column;
  4972. height: 100%;
  4973. }
  4974. .drawer-search-bar {
  4975. padding: 20px;
  4976. background: #fff;
  4977. border-bottom: 1px solid #f0f2f5;
  4978. }
  4979. .drawer-stat-bar {
  4980. padding: 12px 20px;
  4981. background: #e6f7ff;
  4982. border: 1px solid #91d5ff;
  4983. color: #1890ff;
  4984. font-size: 13px;
  4985. display: flex;
  4986. align-items: center;
  4987. gap: 8px;
  4988. margin: 15px 20px 0;
  4989. border-radius: 4px;
  4990. }
  4991. .drawer-stat-bar .highlight {
  4992. font-weight: bold;
  4993. font-size: 16px;
  4994. margin: 0 2px;
  4995. }
  4996. .drawer-content-wrapper :deep(.el-table) {
  4997. margin: 15px 20px;
  4998. width: auto !important;
  4999. }
  5000. .drawer-product-info {
  5001. display: flex;
  5002. align-items: center;
  5003. gap: 12px;
  5004. }
  5005. .drawer-product-info .mini-img {
  5006. width: 50px;
  5007. height: 50px;
  5008. border-radius: 4px;
  5009. border: 1px solid #f0f0f0;
  5010. flex-shrink: 0;
  5011. }
  5012. .drawer-product-info .detail {
  5013. flex: 1;
  5014. overflow: hidden;
  5015. }
  5016. .drawer-product-info .name {
  5017. font-size: 13px;
  5018. color: #333;
  5019. line-height: 1.4;
  5020. margin-bottom: 4px;
  5021. display: -webkit-box;
  5022. -webkit-line-clamp: 2;
  5023. -webkit-box-orient: vertical;
  5024. overflow: hidden;
  5025. }
  5026. .drawer-product-info .id {
  5027. font-size: 11px;
  5028. color: #999;
  5029. }
  5030. .drawer-product-info .price {
  5031. color: #e60012;
  5032. font-weight: bold;
  5033. margin-top: 2px;
  5034. }
  5035. .drawer-pagination {
  5036. padding: 20px;
  5037. display: flex;
  5038. justify-content: center;
  5039. background: #fff;
  5040. border-top: 1px solid #f0f2f5;
  5041. }
  5042. .drawer-footer-actions {
  5043. display: flex;
  5044. justify-content: flex-end;
  5045. gap: 12px;
  5046. padding: 20px;
  5047. }
  5048. .m-l-10 {
  5049. margin-left: 10px;
  5050. }
  5051. </style>