index.vue 160 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309
  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="form.leftAdImage" :src="form.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 && form.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="form.leftAdImage">
  125. <el-form label-width="90px" label-position="left">
  126. <el-form-item label="跳转地址:">
  127. <el-input v-model="form.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.image" 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.tag || '排行榜' }} ></div>
  523. <div class="product-img"><img :src="item.image" alt="" /></div>
  524. <div class="ranking-footer">已售{{ item.sales }}件</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.image" alt="" /></div>
  540. <div class="brand-name">{{ item.tagLink || item.name }}</div>
  541. <div class="brand-tag-btn">{{ item.tag || '品质保障' }}</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.image" 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.image" 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.image" 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.name || '未选择' }}</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.name || '未选择' }}</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.tag" 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.sales" 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.name || '未选择' }}</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.tag" 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="selectSearchQuery"
  919. :placeholder="currentAdIdx === 2 ? '输入品牌名称搜索' : '输入商品名称或ID搜索'"
  920. prefix-icon="Search"
  921. clearable
  922. style="margin-bottom: 20px"
  923. @input="selectCurrentPage = 1"
  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="filteredSelectList.length"
  948. :page-sizes="[8, 12, 20, 50]"
  949. layout="total, sizes, prev, pager, next, jumper"
  950. background
  951. />
  952. </div>
  953. </div>
  954. <template #footer>
  955. <div style="flex: auto; text-align: right; padding: 20px">
  956. <el-button @click="selectDialogVisible = false">取消</el-button>
  957. <el-button type="primary" @click="confirmSelect">确定选择</el-button>
  958. </div>
  959. </template>
  960. </el-drawer>
  961. <!-- 轮播图编辑弹窗 -->
  962. <el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增轮播图' : '修改轮播图'" width="800px" destroy-on-close>
  963. <el-form :model="carouselForm" label-width="100px" class="dialog-form-inner">
  964. <el-form-item label="轮播图片:">
  965. <div class="upload-placeholder" @click="triggerUpload('carousel')">
  966. <img v-if="carouselForm.image" :src="carouselForm.image" class="form-preview-img" />
  967. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  968. </div>
  969. <div class="upload-tip">推荐尺寸:552 * 190,支持上传本地图片</div>
  970. </el-form-item>
  971. <el-form-item label="跳转地址:">
  972. <el-input v-model="carouselForm.link" placeholder="请输入以 http:// 或 https:// 开头的地址" />
  973. </el-form-item>
  974. <el-form-item label="打开方式:">
  975. <el-radio-group v-model="carouselForm.target">
  976. <el-radio label="_self">当前窗口</el-radio>
  977. <el-radio label="_blank">新窗口</el-radio>
  978. </el-radio-group>
  979. </el-form-item>
  980. <el-form-item label="启用状态:">
  981. <el-switch v-model="carouselForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  982. </el-form-item>
  983. </el-form>
  984. <template #footer>
  985. <el-button @click="dialogVisible = false">取消</el-button>
  986. <el-button type="primary" @click="submitCarouselForm">确定</el-button>
  987. </template>
  988. </el-dialog>
  989. <!-- 分类设置编辑弹窗 -->
  990. <el-dialog v-model="categoryDialogVisible" :title="categoryDialogType === 'add' ? '新增分类' : '修改分类'" width="900px" destroy-on-close>
  991. <el-form :model="categoryForm" label-width="100px" class="dialog-form-inner">
  992. <el-tabs type="border-card">
  993. <el-tab-pane label="基础设置">
  994. <el-form-item label="菜单名称:">
  995. <el-input v-model="categoryForm.name" placeholder="如:办公电脑 / 办公打印 / 电脑组件" />
  996. </el-form-item>
  997. <el-form-item label="图标:">
  998. <div class="upload-placeholder-square" @click="triggerUpload('categoryIcon')">
  999. <img v-if="categoryForm.icon" :src="categoryForm.icon" class="form-preview-img-square" />
  1000. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1001. </div>
  1002. <div class="upload-tip">推荐尺寸:16 * 16,支持上传透明背景 PNG</div>
  1003. </el-form-item>
  1004. <el-form-item label="同步分类:">
  1005. <el-select
  1006. v-model="categoryForm.syncCategory"
  1007. placeholder="请选择或输入关联的一级分类"
  1008. style="width: 100%"
  1009. filterable
  1010. allow-create
  1011. default-first-option
  1012. >
  1013. <el-option v-for="opt in syncCategoryOptions" :key="opt" :label="opt" :value="opt" />
  1014. </el-select>
  1015. </el-form-item>
  1016. <el-form-item label="标签设置:">
  1017. <div class="notes-config-list">
  1018. <div v-for="(tag, index) in categoryForm.tags" :key="index" class="note-config-row">
  1019. <el-input v-model="tag.name" placeholder="标签名称" style="width: 120px" />
  1020. <el-input v-model="tag.link" placeholder="跳转地址" style="flex: 1" />
  1021. <el-button type="danger" icon="Delete" circle plain size="small" @click="removeCategoryTag(index)" />
  1022. </div>
  1023. <el-button type="primary" icon="Plus" link @click="addCategoryTag">添加标签</el-button>
  1024. </div>
  1025. <div class="field-tip">用于在右滑面板顶部显示的业务标签,支持配置独立跳转地址</div>
  1026. </el-form-item>
  1027. <el-form-item label="启用状态:">
  1028. <el-switch v-model="categoryForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1029. </el-form-item>
  1030. </el-tab-pane>
  1031. <el-tab-pane label="右滑面板配置">
  1032. <div class="panel-config-section">
  1033. <div class="config-subtitle">品牌位设置 (图3)</div>
  1034. <el-row :gutter="20">
  1035. <el-col :span="12">
  1036. <el-form-item label="主标题:">
  1037. <el-input v-model="categoryForm.panelData.mainTitle" placeholder="如:京东" />
  1038. </el-form-item>
  1039. </el-col>
  1040. <el-col :span="12">
  1041. <el-form-item label="副标题:">
  1042. <el-input v-model="categoryForm.panelData.subTitle" placeholder="如:3C数码" />
  1043. </el-form-item>
  1044. </el-col>
  1045. </el-row>
  1046. <el-form-item label="便签列表:">
  1047. <div class="notes-config-list">
  1048. <div v-for="(note, index) in categoryForm.panelData.notes" :key="index" class="note-config-row">
  1049. <el-input v-model="note.name" placeholder="便签名称" style="width: 120px" />
  1050. <el-input v-model="note.link" placeholder="跳转地址" style="flex: 1" />
  1051. <el-button type="danger" icon="Delete" circle plain size="small" @click="removePanelNote(index)" />
  1052. </div>
  1053. <el-button type="primary" icon="Plus" link @click="addPanelNote">添加便签</el-button>
  1054. </div>
  1055. </el-form-item>
  1056. </div>
  1057. <div class="panel-config-section m-t-20">
  1058. <div class="config-subtitle">分类分组设置</div>
  1059. <div class="field-tip">目前支持在代码中静态配置,弹窗界面待进一步完善。</div>
  1060. </div>
  1061. </el-tab-pane>
  1062. </el-tabs>
  1063. </el-form>
  1064. <template #footer>
  1065. <el-button @click="categoryDialogVisible = false">取消</el-button>
  1066. <el-button type="primary" @click="submitCategoryForm">确定保存</el-button>
  1067. </template>
  1068. </el-dialog>
  1069. <!-- 头部分类编辑弹窗 -->
  1070. <el-dialog v-model="headerDialogVisible" :title="headerEditIndex > -1 ? '编辑头部分类' : '新增头部分类'" width="600px" destroy-on-close>
  1071. <el-form :model="headerForm" label-width="100px" class="dialog-form-inner">
  1072. <el-form-item label="分类名称:">
  1073. <el-input v-model="headerForm.title" placeholder="如:公共采购" />
  1074. </el-form-item>
  1075. <el-form-item label="分类图标:">
  1076. <div class="upload-placeholder-square" @click="triggerUpload('headerIcon')">
  1077. <img v-if="headerForm.icon" :src="headerForm.icon" class="form-preview-img-square" style="width: 20px; height: 20px" />
  1078. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1079. </div>
  1080. <div class="upload-tip">建议尺寸:20*20,透明背景 PNG</div>
  1081. </el-form-item>
  1082. <el-form-item label="跳转地址:">
  1083. <el-input v-model="headerForm.link" placeholder="请输入跳转链接" />
  1084. </el-form-item>
  1085. <el-form-item label="打开方式:">
  1086. <el-radio-group v-model="headerForm.openMode">
  1087. <el-radio label="current">当前页</el-radio>
  1088. <el-radio label="new">新窗口</el-radio>
  1089. </el-radio-group>
  1090. </el-form-item>
  1091. <el-form-item label="启用状态:">
  1092. <el-switch v-model="headerForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1093. </el-form-item>
  1094. </el-form>
  1095. <template #footer>
  1096. <el-button @click="headerDialogVisible = false">取消</el-button>
  1097. <el-button type="primary" @click="submitHeaderForm">确定保存</el-button>
  1098. </template>
  1099. </el-dialog>
  1100. <!-- 场景方案编辑弹窗 -->
  1101. <!-- 已选商品管理抽屉 (占据 45% 屏幕) -->
  1102. <el-drawer
  1103. v-model="selectedProductDialogVisible"
  1104. :title="`已选商品管理 - ${recommendList[currentRecommendIndex]?.name}`"
  1105. size="45%"
  1106. direction="rtl"
  1107. custom-class="selected-products-drawer"
  1108. destroy-on-close
  1109. >
  1110. <div class="drawer-content-wrapper">
  1111. <div class="selected-products-header">
  1112. <div class="left-actions">
  1113. <el-button type="primary" icon="Plus" @click="openProductDrawer">添加商品</el-button>
  1114. <el-button
  1115. type="danger"
  1116. plain
  1117. icon="Delete"
  1118. :disabled="!recommendList[currentRecommendIndex]?.selectedProducts?.length"
  1119. @click="batchRemoveSelectedProducts"
  1120. >批量移除</el-button
  1121. >
  1122. </div>
  1123. <div class="right-info">
  1124. 共 <span class="count">{{ recommendList[currentRecommendIndex]?.selectedProducts?.length || 0 }}</span> 个商品
  1125. </div>
  1126. </div>
  1127. <el-table
  1128. ref="selectedProductsTableRef"
  1129. :data="pagedSelectedProducts"
  1130. border
  1131. height="calc(100vh - 280px)"
  1132. style="width: 100%"
  1133. header-cell-class-name="table-header-custom"
  1134. class="standard-table"
  1135. >
  1136. <el-table-column type="selection" width="55" align="center" />
  1137. <el-table-column label="商品图片" width="100" align="center">
  1138. <template #default="{ row }">
  1139. <el-image :src="row.image" style="width: 50px; height: 50px; border-radius: 4px; border: 1px solid #f0f0f0" fit="contain" />
  1140. </template>
  1141. </el-table-column>
  1142. <el-table-column prop="name" label="商品名称" min-width="250" show-overflow-tooltip />
  1143. <el-table-column prop="id" label="商品ID" width="120" align="center" />
  1144. <el-table-column label="单价" width="120" align="center">
  1145. <template #default="{ row }">
  1146. <span class="price-text">¥{{ row.price }}</span>
  1147. </template>
  1148. </el-table-column>
  1149. <el-table-column label="操作" width="100" align="center" fixed="right">
  1150. <template #default="{ $index }">
  1151. <el-button type="danger" link @click="removeSelectedProduct($index)">移除</el-button>
  1152. </template>
  1153. </el-table-column>
  1154. <template #empty>
  1155. <div class="empty-placeholder">
  1156. <el-empty description="暂未添加任何商品" :image-size="160">
  1157. <el-button type="primary" size="large" @click="openProductDrawer">去添加商品</el-button>
  1158. </el-empty>
  1159. </div>
  1160. </template>
  1161. </el-table>
  1162. <div class="drawer-pagination">
  1163. <el-pagination
  1164. v-model:current-page="selectedCurrentPage"
  1165. v-model:page-size="selectedPageSize"
  1166. :total="recommendList[currentRecommendIndex]?.selectedProducts?.length || 0"
  1167. :page-sizes="[10, 20, 50, 100]"
  1168. layout="total, sizes, prev, pager, next, jumper"
  1169. background
  1170. />
  1171. </div>
  1172. </div>
  1173. <template #footer>
  1174. <div class="drawer-footer-actions">
  1175. <el-button @click="selectedProductDialogVisible = false">取消</el-button>
  1176. <el-button type="primary" @click="submitSelectedProducts">确定保存</el-button>
  1177. </div>
  1178. </template>
  1179. </el-drawer>
  1180. <!-- 商品多选选择抽屉 (占据 1/3 屏幕) -->
  1181. <el-drawer
  1182. v-model="productSelectionDrawerVisible"
  1183. title="从商品库选择商品"
  1184. size="45%"
  1185. direction="rtl"
  1186. destroy-on-close
  1187. custom-class="product-selection-drawer"
  1188. >
  1189. <div class="drawer-content-wrapper">
  1190. <div class="drawer-search-bar">
  1191. <el-input v-model="selectSearchQuery" placeholder="输入商品名称或ID搜索" prefix-icon="Search" clearable @input="selectCurrentPage = 1" />
  1192. </div>
  1193. <div class="drawer-stat-bar">
  1194. <el-icon class="info-icon"><InfoFilled /></el-icon>
  1195. <span
  1196. >已勾选 <span class="highlight">{{ drawerSelection.length }}</span> 个商品</span
  1197. >
  1198. </div>
  1199. <el-table
  1200. ref="drawerTableRef"
  1201. :data="pagedSelectList"
  1202. border
  1203. height="calc(100vh - 280px)"
  1204. style="width: 100%"
  1205. row-key="id"
  1206. @selection-change="handleDrawerSelectionChange"
  1207. >
  1208. <el-table-column type="selection" width="50" align="center" :reserve-selection="true" />
  1209. <el-table-column label="商品信息" min-width="200">
  1210. <template #default="{ row }">
  1211. <div class="drawer-product-info">
  1212. <el-image :src="row.image" class="mini-img" fit="contain" />
  1213. <div class="detail">
  1214. <div class="name">{{ row.name }}</div>
  1215. <div class="id">ID: {{ row.id }}</div>
  1216. <div class="price">¥{{ row.price }}</div>
  1217. </div>
  1218. </div>
  1219. </template>
  1220. </el-table-column>
  1221. </el-table>
  1222. <div class="drawer-pagination">
  1223. <el-pagination
  1224. v-model:current-page="selectCurrentPage"
  1225. v-model:page-size="selectPageSize"
  1226. :total="filteredSelectList.length"
  1227. :page-sizes="[10, 20, 50, 100]"
  1228. layout="total, sizes, prev, pager, next, jumper"
  1229. background
  1230. />
  1231. </div>
  1232. </div>
  1233. <template #footer>
  1234. <div class="drawer-footer-actions">
  1235. <el-button @click="productSelectionDrawerVisible = false">取消</el-button>
  1236. <el-button type="primary" :disabled="!drawerSelection.length" @click="confirmDrawerSelection"
  1237. >确认添加 ({{ drawerSelection.length }})</el-button
  1238. >
  1239. </div>
  1240. </template>
  1241. </el-drawer>
  1242. <el-dialog v-model="scenarioDialogVisible" title="编辑场景方案卡片" width="600px" destroy-on-close>
  1243. <el-form :model="scenarioForm" label-width="100px" class="dialog-form-inner">
  1244. <el-form-item label="主标题:">
  1245. <div class="flex-center gap-10">
  1246. <el-input v-model="scenarioForm.title" style="width: 200px" />
  1247. <div class="theme-color-setting-pro">
  1248. <el-color-picker v-model="scenarioForm.titleColor" />
  1249. <span class="value">{{ scenarioForm.titleColor }}</span>
  1250. </div>
  1251. </div>
  1252. </el-form-item>
  1253. <el-form-item label="副标题:">
  1254. <div class="flex-center gap-10">
  1255. <el-input v-model="scenarioForm.subTitle" style="width: 200px" />
  1256. <div class="theme-color-setting-pro">
  1257. <el-color-picker v-model="scenarioForm.subTitleColor" />
  1258. <span class="value">{{ scenarioForm.subTitleColor }}</span>
  1259. </div>
  1260. </div>
  1261. </el-form-item>
  1262. <el-form-item label="卡片背景:">
  1263. <div class="flex-center gap-10">
  1264. <div class="theme-color-setting-pro">
  1265. <el-color-picker v-model="scenarioForm.bgColor" />
  1266. <span class="value">{{ scenarioForm.bgColor }}</span>
  1267. </div>
  1268. <span class="text-gray m-l-10">不透明度:</span>
  1269. <el-slider v-model="scenarioForm.opacity" :min="0" :max="1" :step="0.05" style="width: 120px" />
  1270. <span class="value m-l-5">{{ Math.round(scenarioForm.opacity * 100) }}%</span>
  1271. </div>
  1272. </el-form-item>
  1273. <el-form-item label="封面图片:">
  1274. <div class="upload-placeholder-square" style="width: 272px; height: 80px" @click="triggerUpload('scenarioImg')">
  1275. <img v-if="scenarioForm.image" :src="scenarioForm.image" style="width: 100%; height: 100%; object-fit: cover" />
  1276. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1277. </div>
  1278. <div class="upload-tip">建议尺寸:272 * 80</div>
  1279. </el-form-item>
  1280. <el-form-item label="跳转链接:">
  1281. <el-input v-model="scenarioForm.link" placeholder="请输入跳转地址" />
  1282. </el-form-item>
  1283. </el-form>
  1284. <template #footer>
  1285. <el-button @click="scenarioDialogVisible = false">取消</el-button>
  1286. <el-button type="primary" @click="submitScenarioForm">保存修改</el-button>
  1287. </template>
  1288. </el-dialog>
  1289. <!-- 推荐设置编辑弹窗 -->
  1290. <el-dialog v-model="recommendDialogVisible" :title="recommendEditIndex > -1 ? '修改推荐分类' : '新增推荐分类'" width="600px" destroy-on-close>
  1291. <el-form :model="recommendForm" label-width="110px" class="dialog-form-inner">
  1292. <el-form-item label="主标题:">
  1293. <el-input v-model="recommendForm.name" placeholder="请输入分类名称" maxlength="6" show-word-limit />
  1294. </el-form-item>
  1295. <el-form-item label="副标题:">
  1296. <el-input v-model="recommendForm.subTitle" placeholder="请输入副标题" maxlength="12" show-word-limit />
  1297. </el-form-item>
  1298. <el-form-item label="分类图标:">
  1299. <div class="upload-placeholder-square" style="width: 64px; height: 64px" @click="triggerUpload('recommendIcon')">
  1300. <img v-if="recommendForm.icon" :src="recommendForm.icon" style="width: 100%; height: 100%; object-fit: contain" />
  1301. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1302. </div>
  1303. <div class="upload-tip">建议尺寸:32 * 32,透明背景 PNG</div>
  1304. </el-form-item>
  1305. <el-form-item label="数据类型:">
  1306. <el-radio-group v-model="recommendForm.type">
  1307. <el-radio label="select">商品自选</el-radio>
  1308. <el-radio label="category">分类映射</el-radio>
  1309. </el-radio-group>
  1310. </el-form-item>
  1311. <el-form-item v-if="recommendForm.type === 'category'" label="映射分类:">
  1312. <el-cascader
  1313. v-model="recommendForm.categoryValue"
  1314. :options="mockCategoryOptions"
  1315. :props="{ checkStrictly: true, expandTrigger: 'hover' }"
  1316. placeholder="请选择一级/二级/三级分类"
  1317. style="width: 100%"
  1318. filterable
  1319. clearable
  1320. @change="handleRecommendCategoryChange"
  1321. />
  1322. <div class="field-tip">选择后,该页签将自动抓取对应分类下的最新商品</div>
  1323. </el-form-item>
  1324. <el-form-item label="启用状态:">
  1325. <el-switch v-model="recommendForm.status" :active-value="1" :inactive-value="0" active-color="#13ce66" />
  1326. </el-form-item>
  1327. </el-form>
  1328. <template #footer>
  1329. <el-button @click="recommendDialogVisible = false">取消</el-button>
  1330. <el-button type="primary" @click="submitRecommendForm">保存设置</el-button>
  1331. </template>
  1332. </el-dialog>
  1333. <!-- 快捷入口编辑弹窗 -->
  1334. <el-dialog
  1335. v-model="quickEntryDialogVisible"
  1336. :title="quickEntryDialogType === 'add' ? '新增快捷入口' : '编辑快捷入口'"
  1337. width="600px"
  1338. append-to-body
  1339. destroy-on-close
  1340. >
  1341. <el-form :model="quickEntryForm" label-width="100px" class="dialog-form-inner">
  1342. <el-form-item label="入口名称:">
  1343. <el-input v-model="quickEntryForm.name" placeholder="请输入入口名称(建议4-5个字)" maxlength="6" show-word-limit />
  1344. </el-form-item>
  1345. <el-form-item label="图标:">
  1346. <div class="upload-placeholder-square" @click="triggerUpload('quickEntryIcon')">
  1347. <img v-if="quickEntryForm.icon" :src="quickEntryForm.icon" class="form-preview-img-square" style="width: 24px; height: 24px" />
  1348. <el-icon v-else class="upload-icon"><Plus /></el-icon>
  1349. </div>
  1350. <div class="upload-tip">建议尺寸: 48*48, 格式: PNG/SVG</div>
  1351. </el-form-item>
  1352. <el-form-item label="标签文字:">
  1353. <el-input v-model="quickEntryForm.tag" placeholder="如:返100" maxlength="4" style="width: 200px" />
  1354. <div class="field-tip">显示在图标右上角的红色气泡,留空则不显示</div>
  1355. </el-form-item>
  1356. <el-form-item label="跳转地址:">
  1357. <el-input v-model="quickEntryForm.link" placeholder="请输入跳转地址" />
  1358. </el-form-item>
  1359. <el-form-item label="启用状态:">
  1360. <el-switch v-model="quickEntryForm.status" :active-value="1" :inactive-value="0" />
  1361. </el-form-item>
  1362. </el-form>
  1363. <template #footer>
  1364. <el-button @click="quickEntryDialogVisible = false">取消</el-button>
  1365. <el-button type="primary" @click="submitQuickEntryForm">确定</el-button>
  1366. </template>
  1367. </el-dialog>
  1368. <!-- 底部固定操作栏 -->
  1369. <div class="footer-actions">
  1370. <el-button type="primary" class="btn-confirm" @click="handleMainSave">保存当前页配置</el-button>
  1371. </div>
  1372. <!-- 隐藏的文件选择框 -->
  1373. <input type="file" ref="fileInput" style="display: none" accept="image/*" @change="onFileChange" />
  1374. </div>
  1375. </template>
  1376. <script lang="ts" setup>
  1377. import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
  1378. import { ElMessage, ElMessageBox } from 'element-plus';
  1379. import {
  1380. listSearchConfig,
  1381. getSearchConfig,
  1382. delSearchConfig,
  1383. addSearchConfig,
  1384. updateSearchConfig,
  1385. getCurrentSearchConfig
  1386. } from '@/api/enterprisePurchase/searchConfig';
  1387. import { SearchConfigVO, SearchConfigQuery, SearchConfigForm } from '@/api/enterprisePurchase/searchConfig/types';
  1388. import { listAdLeft, getAdLeft, delAdLeft, addAdLeft, updateAdLeft, getCurrentAdLeft } from '@/api/enterprisePurchase/adLeft';
  1389. import { AdLeftVO, AdLeftQuery, AdLeftForm } from '@/api/enterprisePurchase/adLeft/types';
  1390. import { listCarousel, getCarousel, delCarousel, addCarousel, updateCarousel } from '@/api/enterprisePurchase/carousel';
  1391. import { CarouselVO, CarouselQuery, CarouselForm } from '@/api/enterprisePurchase/carousel/types';
  1392. import { listCategoryMain, getCategoryMain, delCategoryMain, addCategoryMain, updateCategoryMain } from '@/api/enterprisePurchase/categoryMain';
  1393. import { CategoryMainVO, CategoryMainQuery, CategoryMainForm } from '@/api/enterprisePurchase/categoryMain/types';
  1394. import { listScenarioCards, getScenarioCards, delScenarioCards, addScenarioCards, updateScenarioCards } from '@/api/enterprisePurchase/scenarioCards';
  1395. import { ScenarioCardsVO, ScenarioCardsQuery, ScenarioCardsForm } from '@/api/enterprisePurchase/scenarioCards/types';
  1396. import { listScenarioGlobalSettings, addScenarioGlobalSettings, updateScenarioGlobalSettings } from '@/api/enterprisePurchase/scenarioGlobalSettings';
  1397. import { ScenarioGlobalSettingsForm } from '@/api/enterprisePurchase/scenarioGlobalSettings/types';
  1398. import { listQuickEntryModule, addQuickEntryModule, updateQuickEntryModule } from '@/api/enterprisePurchase/quickEntryModule';
  1399. import { QuickEntryModuleVO, QuickEntryModuleQuery, QuickEntryModuleForm } from '@/api/enterprisePurchase/quickEntryModule/types';
  1400. import {
  1401. listQuickEntryItems,
  1402. getQuickEntryItems,
  1403. delQuickEntryItems,
  1404. addQuickEntryItems,
  1405. updateQuickEntryItems
  1406. } from '@/api/enterprisePurchase/quickEntryItems';
  1407. import { QuickEntryItemsVO, QuickEntryItemsQuery, QuickEntryItemsForm } from '@/api/enterprisePurchase/quickEntryItems/types';
  1408. import {
  1409. listHeaderCategory,
  1410. getHeaderCategory,
  1411. delHeaderCategory,
  1412. addHeaderCategory,
  1413. updateHeaderCategory
  1414. } from '@/api/enterprisePurchase/headerCategory';
  1415. import { HeaderCategoryVO, HeaderCategoryQuery, HeaderCategoryForm } from '@/api/enterprisePurchase/headerCategory/types';
  1416. import {
  1417. ShoppingCart,
  1418. Plus,
  1419. Edit,
  1420. Delete,
  1421. Picture,
  1422. CaretTop,
  1423. CaretBottom,
  1424. Menu,
  1425. ArrowLeft,
  1426. ArrowRight,
  1427. Search,
  1428. InfoFilled,
  1429. ArrowUp,
  1430. ArrowDown
  1431. } from '@element-plus/icons-vue';
  1432. const defaultPlaceholder =
  1433. 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNmMmYyZjIiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iI2JmYmZiZiI+5pys5peg5Zu+54mHPC90ZXh0Pjwvc3ZnPg==';
  1434. // 基础导航状态
  1435. const activeSubTab = ref('search');
  1436. const subTabs = [
  1437. { label: '搜索栏', value: 'search' },
  1438. { label: '广告图', value: 'carousel' },
  1439. { label: '分类设置', value: 'category' },
  1440. { label: '头部分类', value: 'headerCategory' },
  1441. { label: '广告模块', value: 'ad-module' },
  1442. { label: '快捷入口', value: 'quick-entry' },
  1443. { label: '场景方案', value: 'scenario' },
  1444. { label: '推荐设置', value: 'recommend' }
  1445. ];
  1446. // 首页核心配置表单 (搜索栏与基本设置)
  1447. const form = reactive({
  1448. id: null,
  1449. mainTitle: '',
  1450. subTitle: '',
  1451. placeholderText: '',
  1452. hotWordsList: [{ name: '', link: '' }],
  1453. themeColor: '',
  1454. rightBtnIcon: '',
  1455. rightBtnText: '',
  1456. rightBtnLink: '',
  1457. leftAdImage: '',
  1458. leftAdLink: ''
  1459. });
  1460. // 左侧广告配置ID(单例)
  1461. const leftAdId = ref<string | number>('');
  1462. // 保存配置
  1463. const handleMainSave = async () => {
  1464. try {
  1465. let res;
  1466. if (activeSubTab.value == 'search') {
  1467. // 构建提交数据,过滤空的热词
  1468. const submitData = {
  1469. ...form,
  1470. hotWordList: form.hotWordsList.filter((item) => item.name && item.name.trim() !== '')
  1471. };
  1472. // 无ID则新增
  1473. res = await addSearchConfig(submitData);
  1474. if (res.code === 200) {
  1475. ElMessage.success('配置保存成功');
  1476. // 如果是新增,保存返回的ID
  1477. if (!form.id && res.data) {
  1478. form.id = res.data.id;
  1479. }
  1480. // 重新获取最新配置
  1481. await getCurrentSearch();
  1482. } else {
  1483. ElMessage.error(res.msg || '保存失败');
  1484. }
  1485. } else if (activeSubTab.value == 'carousel') {
  1486. saveLeftAdConfig();
  1487. } else if (activeSubTab.value == 'scenario') {
  1488. saveScenarioGlobalSettings();
  1489. } else if (activeSubTab.value == 'quick-entry') {
  1490. saveQuickEntryModule();
  1491. }
  1492. } catch (error) {
  1493. console.error('保存配置失败:', error);
  1494. ElMessage.error('保存配置失败');
  1495. }
  1496. };
  1497. // 获取当前配置
  1498. const getCurrentAdLeftBtn = async () => {
  1499. try {
  1500. const res = await getCurrentAdLeft();
  1501. if (res.code === 200 && res.data) {
  1502. // 回显数据到表单
  1503. form.id = res.data.id || null;
  1504. form.leftAdImage = res.data.imageUrl || '';
  1505. form.leftAdLink = res.data.link || '';
  1506. }
  1507. } catch (error) {
  1508. console.error('获取配置失败:', error);
  1509. ElMessage.error('获取配置失败');
  1510. }
  1511. };
  1512. // 获取当前配置
  1513. const getCurrentSearch = async () => {
  1514. try {
  1515. const res = await getCurrentSearchConfig();
  1516. if (res.code === 200 && res.data) {
  1517. // 回显数据到表单
  1518. form.id = res.data.id || null;
  1519. form.mainTitle = res.data.mainTitle || '';
  1520. form.subTitle = res.data.subTitle || '';
  1521. form.placeholderText = res.data.placeholderText || '';
  1522. form.themeColor = res.data.themeColor || '';
  1523. form.rightBtnIcon = res.data.rightBtnIcon || '';
  1524. form.rightBtnText = res.data.rightBtnText || '';
  1525. form.rightBtnLink = res.data.rightBtnLink || '';
  1526. form.leftAdImage = res.data.leftAdImage || '';
  1527. form.leftAdLink = res.data.leftAdLink || '';
  1528. // 处理热词列表
  1529. if (res.data.hotWordList && Array.isArray(res.data.hotWordList)) {
  1530. form.hotWordsList =
  1531. res.data.hotWordList.length > 0
  1532. ? res.data.hotWordList.map((item) => ({
  1533. name: item.name || '',
  1534. link: item.link || ''
  1535. }))
  1536. : [{ name: '', link: '' }];
  1537. } else {
  1538. form.hotWordsList = [{ name: '', link: '' }];
  1539. }
  1540. }
  1541. } catch (error) {
  1542. console.error('获取配置失败:', error);
  1543. ElMessage.error('获取配置失败');
  1544. }
  1545. };
  1546. const handleMainReset = () => {
  1547. ElMessageBox.confirm('确定要重置当前页面的所有修改吗?', '提示', {
  1548. confirmButtonText: '确定重置',
  1549. cancelButtonText: '取消',
  1550. type: 'warning'
  1551. }).then(() => {
  1552. ElMessage.info('已还原至初始状态');
  1553. });
  1554. };
  1555. // 搜索栏逻辑
  1556. const currentPlaceholderIndex = ref(0);
  1557. let timer = null;
  1558. const searchPlaceholderList = computed(() => {
  1559. return form.placeholderText
  1560. .split(',')
  1561. .map((t) => t.trim())
  1562. .filter((t) => t !== '');
  1563. });
  1564. const addHotWord = () => {
  1565. form.hotWordsList.push({ name: '', link: '' });
  1566. };
  1567. const removeHotWord = (index) => {
  1568. form.hotWordsList.splice(index, 1);
  1569. };
  1570. const startPlaceholderScroll = () => {
  1571. if (timer) clearInterval(timer);
  1572. if (searchPlaceholderList.value.length <= 1) return;
  1573. timer = setInterval(() => {
  1574. currentPlaceholderIndex.value = (currentPlaceholderIndex.value + 1) % searchPlaceholderList.value.length;
  1575. }, 3000);
  1576. };
  1577. // 广告图模块逻辑
  1578. const leftAdHover = ref(false);
  1579. const carouselList = ref<CarouselVO[]>([]);
  1580. // 获取轮播图列表
  1581. const getCarouselList = async () => {
  1582. try {
  1583. const res = await listCarousel();
  1584. if (res.code === 200 && res.rows) {
  1585. carouselList.value = res.rows.map((item: CarouselVO) => ({
  1586. ...item,
  1587. image: item.imageUrl || ''
  1588. }));
  1589. }
  1590. } catch (error) {
  1591. console.error('获取轮播图列表失败:', error);
  1592. ElMessage.error('获取轮播图列表失败');
  1593. }
  1594. };
  1595. const activeCarouselList = computed(() => {
  1596. return carouselList.value.filter((item) => item.status === 1);
  1597. });
  1598. // 轮播图弹窗逻辑
  1599. const dialogVisible = ref(false);
  1600. const dialogType = ref('add'); // add | edit
  1601. const currentEditIndex = ref(-1);
  1602. const carouselForm = reactive({
  1603. image: '',
  1604. link: '',
  1605. target: '_self',
  1606. status: 1
  1607. });
  1608. const handleAddCarousel = () => {
  1609. dialogType.value = 'add';
  1610. carouselForm.image = '';
  1611. carouselForm.link = '';
  1612. carouselForm.target = '_self';
  1613. carouselForm.status = 1;
  1614. dialogVisible.value = true;
  1615. };
  1616. const handleEditCarousel = (row) => {
  1617. dialogType.value = 'edit';
  1618. currentEditIndex.value = carouselList.value.findIndex((item) => item.id === row.id);
  1619. carouselForm.image = row.image;
  1620. carouselForm.link = row.link;
  1621. carouselForm.target = row.target;
  1622. carouselForm.status = row.status;
  1623. dialogVisible.value = true;
  1624. };
  1625. const submitCarouselForm = async () => {
  1626. if (!carouselForm.image) {
  1627. return ElMessage.warning('请先上传轮播图片');
  1628. }
  1629. const data: CarouselForm = {
  1630. imageUrl: carouselForm.image,
  1631. link: carouselForm.link,
  1632. target: carouselForm.target,
  1633. status: carouselForm.status,
  1634. sortOrder: carouselList.value.length
  1635. };
  1636. try {
  1637. if (dialogType.value === 'add') {
  1638. const res = await addCarousel(data);
  1639. if (res.code === 200) {
  1640. ElMessage.success('新增成功');
  1641. dialogVisible.value = false;
  1642. getCarouselList();
  1643. } else {
  1644. ElMessage.error(res.msg || '新增失败');
  1645. }
  1646. } else {
  1647. const id = carouselList.value[currentEditIndex.value]?.id;
  1648. const res = await updateCarousel({ ...data, id });
  1649. if (res.code === 200) {
  1650. ElMessage.success('修改成功');
  1651. dialogVisible.value = false;
  1652. getCarouselList();
  1653. } else {
  1654. ElMessage.error(res.msg || '修改失败');
  1655. }
  1656. }
  1657. } catch (error) {
  1658. console.error('操作失败:', error);
  1659. ElMessage.error('操作失败');
  1660. }
  1661. };
  1662. const handleRemoveCarousel = async (index: number) => {
  1663. const item = carouselList.value[index];
  1664. try {
  1665. await ElMessageBox.confirm('确定要删除该轮播图吗?', '提示', { type: 'warning' });
  1666. const res = await delCarousel(item.id);
  1667. if (res.code === 200) {
  1668. ElMessage.success('删除成功');
  1669. getCarouselList();
  1670. } else {
  1671. ElMessage.error(res.msg || '删除失败');
  1672. }
  1673. } catch (error) {
  1674. if (error !== 'cancel') {
  1675. console.error('删除失败:', error);
  1676. ElMessage.error('删除失败');
  1677. }
  1678. }
  1679. };
  1680. const moveRow = async (index: number, direction: number) => {
  1681. const newIndex = index + direction;
  1682. if (newIndex < 0 || newIndex >= carouselList.value.length) return;
  1683. const item = carouselList.value.splice(index, 1)[0];
  1684. carouselList.value.splice(newIndex, 0, item);
  1685. // 同步排序到后端
  1686. try {
  1687. const start = Math.min(index, newIndex);
  1688. const end = Math.max(index, newIndex);
  1689. for (let i = start; i <= end; i++) {
  1690. const row = carouselList.value[i];
  1691. await updateCarousel({
  1692. id: row.id,
  1693. imageUrl: row.image || row.imageUrl,
  1694. link: row.link,
  1695. target: row.target,
  1696. status: row.status,
  1697. sortOrder: i
  1698. });
  1699. }
  1700. } catch (error) {
  1701. console.error('排序更新失败:', error);
  1702. ElMessage.error('排序更新失败');
  1703. }
  1704. };
  1705. // 颜色转换工具:Hex 转 RGBA
  1706. const hexToRgba = (hex, opacity) => {
  1707. if (!hex) return `rgba(255, 255, 255, ${opacity})`;
  1708. let r = parseInt(hex.slice(1, 3), 16);
  1709. let g = parseInt(hex.slice(3, 5), 16);
  1710. let b = parseInt(hex.slice(5, 7), 16);
  1711. return `rgba(${r}, ${g}, ${b}, ${opacity})`;
  1712. };
  1713. const handleSelectSearch = () => {
  1714. // 模拟搜索过滤,computed 会自动处理
  1715. };
  1716. // 文件上传相关
  1717. const fileInput = ref(null);
  1718. const uploadTarget = ref(''); // 'leftAd' | 'carousel' | 'categoryIcon' | 'headerIcon'
  1719. const triggerUpload = (target) => {
  1720. uploadTarget.value = target;
  1721. fileInput.value.click();
  1722. };
  1723. const onFileChange = (e) => {
  1724. const file = e.target.files[0];
  1725. if (!file) return;
  1726. const reader = new FileReader();
  1727. reader.onload = (event) => {
  1728. const base64 = event.target.result;
  1729. if (uploadTarget.value === 'leftAd') {
  1730. form.leftAdImage = base64;
  1731. } else if (uploadTarget.value === 'carousel') {
  1732. carouselForm.image = base64;
  1733. } else if (uploadTarget.value === 'rightBtnIcon') {
  1734. form.rightBtnIcon = base64;
  1735. } else if (uploadTarget.value === 'categoryIcon') {
  1736. categoryForm.icon = base64;
  1737. } else if (uploadTarget.value === 'headerIcon') {
  1738. headerForm.icon = base64;
  1739. } else if (uploadTarget.value === 'scenarioImg') {
  1740. scenarioForm.image = base64;
  1741. } else if (uploadTarget.value === 'quickEntryIcon') {
  1742. quickEntryForm.icon = base64;
  1743. } else if (uploadTarget.value === 'recommendIcon') {
  1744. recommendForm.icon = base64;
  1745. }
  1746. ElMessage.success('图片上传成功');
  1747. e.target.value = '';
  1748. };
  1749. reader.readAsDataURL(file);
  1750. };
  1751. // ==================== 左侧广告单例配置 ====================
  1752. // 获取左侧广告配置
  1753. const fetchLeftAdConfig = async () => {
  1754. try {
  1755. const res = await listAdLeft({ pageNum: 1, pageSize: 1 });
  1756. if (res.code === 200 && res.rows && res.rows.length > 0) {
  1757. const adData = res.rows[0];
  1758. leftAdId.value = adData.id;
  1759. form.leftAdImage = adData.imageUrl || '';
  1760. form.leftAdLink = adData.link || '';
  1761. }
  1762. } catch (error) {
  1763. console.error('获取左侧广告配置失败:', error);
  1764. }
  1765. };
  1766. // 保存左侧广告配置
  1767. const saveLeftAdConfig = async () => {
  1768. try {
  1769. if (!form.leftAdImage) {
  1770. ElMessage.warning('请先上传广告图片');
  1771. return;
  1772. }
  1773. const submitData: AdLeftForm = {
  1774. id: leftAdId.value || undefined,
  1775. imageUrl: form.leftAdImage,
  1776. link: form.leftAdLink,
  1777. status: 1 // 默认启用
  1778. };
  1779. let res;
  1780. if (leftAdId.value) {
  1781. // 有ID则更新
  1782. res = await updateAdLeft(submitData);
  1783. } else {
  1784. // 无ID则新增
  1785. res = await addAdLeft(submitData);
  1786. }
  1787. if (res.code === 200) {
  1788. ElMessage.success('左侧广告保存成功');
  1789. // 如果是新增,保存返回的ID
  1790. if (!leftAdId.value && res.data) {
  1791. leftAdId.value = res.data.id;
  1792. }
  1793. // 重新获取最新配置
  1794. await fetchLeftAdConfig();
  1795. } else {
  1796. ElMessage.error(res.msg || '保存失败');
  1797. }
  1798. } catch (error) {
  1799. console.error('保存左侧广告失败:', error);
  1800. ElMessage.error('保存左侧广告失败');
  1801. }
  1802. };
  1803. // 删除左侧广告
  1804. const handleDeleteLeftAd = () => {
  1805. ElMessageBox.confirm('确定要删除左侧广告吗?', '提示', {
  1806. confirmButtonText: '确定删除',
  1807. cancelButtonText: '取消',
  1808. type: 'warning'
  1809. })
  1810. .then(async () => {
  1811. try {
  1812. if (leftAdId.value) {
  1813. const res = await delAdLeft(leftAdId.value);
  1814. if (res.code === 200) {
  1815. ElMessage.success('删除成功');
  1816. // 清空表单数据
  1817. leftAdId.value = '';
  1818. form.leftAdImage = '';
  1819. form.leftAdLink = '';
  1820. } else {
  1821. ElMessage.error(res.msg || '删除失败');
  1822. }
  1823. } else {
  1824. // 如果没有ID,直接清空表单
  1825. form.leftAdImage = '';
  1826. form.leftAdLink = '';
  1827. ElMessage.success('已清除');
  1828. }
  1829. } catch (error) {
  1830. console.error('删除左侧广告失败:', error);
  1831. ElMessage.error('删除失败');
  1832. }
  1833. })
  1834. .catch(() => {
  1835. // 用户取消操作
  1836. });
  1837. };
  1838. // 分类设置模块逻辑
  1839. const categoryThemeColor = ref('#e60012');
  1840. const syncCategoryOptions = ['办公电脑', '办公设备', '办公家电', '文具耗材', '日用百货', '工业品', '食品饮料', '车载用品', '运动户外'];
  1841. const categoryList = ref<CategoryMainVO[]>([]);
  1842. // 获取分类列表
  1843. const getCategoryList = async () => {
  1844. try {
  1845. const res = await listCategoryMain();
  1846. if (res.code === 200 && res.rows) {
  1847. categoryList.value = res.rows.map((item: CategoryMainVO) => {
  1848. let extra = { tags: [] as any[], notes: [] as any[], groups: [] as any[] };
  1849. try {
  1850. if ((item as any).remark) {
  1851. extra = JSON.parse((item as any).remark);
  1852. }
  1853. } catch (e) {
  1854. /* remark JSON 解析失败使用默认值 */
  1855. }
  1856. return {
  1857. ...item,
  1858. tags: extra.tags || [],
  1859. panelData: {
  1860. mainTitle: (item as any).panelMainTitle || '',
  1861. subTitle: (item as any).panelSubTitle || '',
  1862. notes: extra.notes || [],
  1863. groups: extra.groups || []
  1864. }
  1865. };
  1866. });
  1867. }
  1868. } catch (error) {
  1869. console.error('获取分类列表失败:', error);
  1870. ElMessage.error('获取分类列表失败');
  1871. }
  1872. };
  1873. const addCategoryTag = () => {
  1874. categoryForm.tags.push({ name: '', link: '' });
  1875. };
  1876. const removeCategoryTag = (index) => {
  1877. categoryForm.tags.splice(index, 1);
  1878. };
  1879. const categoryDialogVisible = ref(false);
  1880. const categoryDialogType = ref('add');
  1881. const categoryForm = reactive({
  1882. id: null,
  1883. name: '',
  1884. icon: '',
  1885. syncCategory: '',
  1886. tags: [],
  1887. status: 1,
  1888. panelData: {
  1889. mainTitle: '',
  1890. subTitle: '',
  1891. notes: [],
  1892. groups: []
  1893. }
  1894. });
  1895. const handleAddCategory = () => {
  1896. categoryDialogType.value = 'add';
  1897. Object.assign(categoryForm, {
  1898. id: null,
  1899. name: '',
  1900. icon: '',
  1901. syncCategory: '',
  1902. tags: [],
  1903. status: 1,
  1904. panelData: { mainTitle: '', subTitle: '', notes: [], groups: [] }
  1905. });
  1906. categoryDialogVisible.value = true;
  1907. };
  1908. const handleEditCategory = (row) => {
  1909. categoryDialogType.value = 'edit';
  1910. Object.assign(categoryForm, JSON.parse(JSON.stringify(row)));
  1911. categoryDialogVisible.value = true;
  1912. };
  1913. const submitCategoryForm = async () => {
  1914. if (!categoryForm.name) return ElMessage.warning('请输入菜单名称');
  1915. const data: CategoryMainForm = {
  1916. name: categoryForm.name,
  1917. icon: categoryForm.icon,
  1918. syncCategory: categoryForm.syncCategory,
  1919. status: categoryForm.status,
  1920. panelMainTitle: categoryForm.panelData.mainTitle,
  1921. panelSubTitle: categoryForm.panelData.subTitle,
  1922. remark: JSON.stringify({
  1923. tags: categoryForm.tags,
  1924. notes: categoryForm.panelData.notes,
  1925. groups: categoryForm.panelData.groups
  1926. })
  1927. };
  1928. try {
  1929. if (categoryDialogType.value === 'add') {
  1930. const res = await addCategoryMain(data);
  1931. if (res.code === 200) {
  1932. ElMessage.success('新增成功');
  1933. categoryDialogVisible.value = false;
  1934. getCategoryList();
  1935. } else {
  1936. ElMessage.error(res.msg || '新增失败');
  1937. }
  1938. } else {
  1939. const id = categoryForm.id;
  1940. const res = await updateCategoryMain({ ...data, id });
  1941. if (res.code === 200) {
  1942. ElMessage.success('修改成功');
  1943. categoryDialogVisible.value = false;
  1944. getCategoryList();
  1945. } else {
  1946. ElMessage.error(res.msg || '修改失败');
  1947. }
  1948. }
  1949. } catch (error) {
  1950. console.error('操作失败:', error);
  1951. ElMessage.error('操作失败');
  1952. }
  1953. };
  1954. // 场景方案模块逻辑
  1955. const scenarioSettings = reactive({
  1956. id: null as string | number | null,
  1957. mainTitle: '场景解决方案',
  1958. subTitle: '一站全买齐',
  1959. btnText: '进入全场景',
  1960. jumpLink: '',
  1961. themeColor: '#66e0a3'
  1962. });
  1963. // 获取场景全局设置
  1964. const getScenarioGlobalSettingsList = async () => {
  1965. try {
  1966. const res = await listScenarioGlobalSettings();
  1967. if (res.code === 200 && res.rows && res.rows.length > 0) {
  1968. const data = res.rows[0];
  1969. scenarioSettings.id = data.id;
  1970. scenarioSettings.mainTitle = data.mainTitle || '';
  1971. scenarioSettings.subTitle = data.subTitle || '';
  1972. scenarioSettings.btnText = data.btnText || '';
  1973. scenarioSettings.jumpLink = data.jumpLink || '';
  1974. scenarioSettings.themeColor = data.themeColor || '#66e0a3';
  1975. }
  1976. } catch (error) {
  1977. console.error('获取场景全局设置失败:', error);
  1978. ElMessage.error('获取场景全局设置失败');
  1979. }
  1980. };
  1981. // 保存场景全局设置
  1982. const saveScenarioGlobalSettings = async () => {
  1983. const data: ScenarioGlobalSettingsForm = {
  1984. mainTitle: scenarioSettings.mainTitle,
  1985. subTitle: scenarioSettings.subTitle,
  1986. btnText: scenarioSettings.btnText,
  1987. jumpLink: scenarioSettings.jumpLink,
  1988. themeColor: scenarioSettings.themeColor
  1989. };
  1990. try {
  1991. let res;
  1992. if (scenarioSettings.id) {
  1993. res = await updateScenarioGlobalSettings({ ...data, id: scenarioSettings.id });
  1994. } else {
  1995. res = await addScenarioGlobalSettings(data);
  1996. if (res.code === 200 && res.data) {
  1997. scenarioSettings.id = res.data.id;
  1998. }
  1999. }
  2000. if (res.code === 200) {
  2001. ElMessage.success('场景全局设置保存成功');
  2002. } else {
  2003. ElMessage.error(res.msg || '保存失败');
  2004. }
  2005. } catch (error) {
  2006. console.error('保存场景全局设置失败:', error);
  2007. ElMessage.error('保存失败');
  2008. }
  2009. };
  2010. const scenarioList = ref<ScenarioCardsVO[]>([]);
  2011. // 获取场景卡片列表
  2012. const getScenarioList = async () => {
  2013. try {
  2014. const res = await listScenarioCards();
  2015. if (res.code === 200 && res.rows) {
  2016. scenarioList.value = res.rows.map((item: ScenarioCardsVO) => {
  2017. let extra = { subTitleColor: '#333333', bgColor: '#ffffff', opacity: 1 };
  2018. try {
  2019. if ((item as any).remark) {
  2020. extra = { ...extra, ...JSON.parse((item as any).remark) };
  2021. }
  2022. } catch (e) {
  2023. /* remark JSON 解析失败使用默认值 */
  2024. }
  2025. return {
  2026. ...item,
  2027. image: (item as any).imageUrl || '',
  2028. link: (item as any).jumpLink || '',
  2029. subTitleColor: extra.subTitleColor,
  2030. bgColor: extra.bgColor,
  2031. opacity: extra.opacity
  2032. };
  2033. });
  2034. }
  2035. } catch (error) {
  2036. console.error('获取场景卡片列表失败:', error);
  2037. ElMessage.error('获取场景卡片列表失败');
  2038. }
  2039. };
  2040. const scenarioDialogVisible = ref(false);
  2041. const scenarioEditIndex = ref(-1);
  2042. const scenarioForm = reactive({
  2043. title: '',
  2044. titleColor: '#008b4e',
  2045. subTitle: '',
  2046. subTitleColor: '#333333',
  2047. image: '',
  2048. link: '',
  2049. bgColor: '#ffffff',
  2050. opacity: 1
  2051. });
  2052. const handleEditScenario = (row, index) => {
  2053. scenarioEditIndex.value = index;
  2054. Object.assign(scenarioForm, JSON.parse(JSON.stringify(row)));
  2055. scenarioDialogVisible.value = true;
  2056. };
  2057. const submitScenarioForm = async () => {
  2058. const item = scenarioList.value[scenarioEditIndex.value];
  2059. if (!item) return;
  2060. const data: ScenarioCardsForm = {
  2061. id: item.id,
  2062. title: scenarioForm.title,
  2063. titleColor: scenarioForm.titleColor,
  2064. subTitle: scenarioForm.subTitle,
  2065. imageUrl: scenarioForm.image,
  2066. jumpLink: scenarioForm.link,
  2067. sortOrder: (item as any).sortOrder ?? scenarioEditIndex.value,
  2068. remark: JSON.stringify({
  2069. subTitleColor: scenarioForm.subTitleColor,
  2070. bgColor: scenarioForm.bgColor,
  2071. opacity: scenarioForm.opacity
  2072. })
  2073. };
  2074. try {
  2075. const res = await updateScenarioCards(data);
  2076. if (res.code === 200) {
  2077. ElMessage.success('方案修改成功');
  2078. scenarioDialogVisible.value = false;
  2079. getScenarioList();
  2080. } else {
  2081. ElMessage.error(res.msg || '修改失败');
  2082. }
  2083. } catch (error) {
  2084. console.error('修改失败:', error);
  2085. ElMessage.error('修改失败');
  2086. }
  2087. };
  2088. const moveScenario = async (index: number, direction: number) => {
  2089. const newIndex = index + direction;
  2090. if (newIndex < 0 || newIndex >= scenarioList.value.length) return;
  2091. const item = scenarioList.value.splice(index, 1)[0];
  2092. scenarioList.value.splice(newIndex, 0, item);
  2093. // 同步排序到后端
  2094. try {
  2095. const start = Math.min(index, newIndex);
  2096. const end = Math.max(index, newIndex);
  2097. for (let i = start; i <= end; i++) {
  2098. const row = scenarioList.value[i] as any;
  2099. await updateScenarioCards({
  2100. id: row.id,
  2101. title: row.title,
  2102. titleColor: row.titleColor,
  2103. subTitle: row.subTitle,
  2104. imageUrl: row.image || row.imageUrl,
  2105. jumpLink: row.link || row.jumpLink,
  2106. sortOrder: i,
  2107. remark: JSON.stringify({
  2108. subTitleColor: row.subTitleColor || '#333333',
  2109. bgColor: row.bgColor || '#ffffff',
  2110. opacity: row.opacity ?? 1
  2111. })
  2112. });
  2113. }
  2114. } catch (error) {
  2115. console.error('排序更新失败:', error);
  2116. ElMessage.error('排序更新失败');
  2117. }
  2118. };
  2119. const handleRemoveCategory = async (index: number) => {
  2120. const item = categoryList.value[index];
  2121. try {
  2122. await ElMessageBox.confirm('确定要删除该分类吗?', '提示');
  2123. const res = await delCategoryMain(item.id);
  2124. if (res.code === 200) {
  2125. ElMessage.success('删除成功');
  2126. getCategoryList();
  2127. } else {
  2128. ElMessage.error(res.msg || '删除失败');
  2129. }
  2130. } catch (error) {
  2131. if (error !== 'cancel') {
  2132. console.error('删除失败:', error);
  2133. ElMessage.error('删除失败');
  2134. }
  2135. }
  2136. };
  2137. const addPanelNote = () => {
  2138. categoryForm.panelData.notes.push({ name: '', link: '' });
  2139. };
  2140. const removePanelNote = (index) => {
  2141. categoryForm.panelData.notes.splice(index, 1);
  2142. };
  2143. const moveCategory = (index, direction) => {
  2144. const newIndex = index + direction;
  2145. if (newIndex < 0 || newIndex >= categoryList.value.length) return;
  2146. const item = categoryList.value.splice(index, 1)[0];
  2147. categoryList.value.splice(newIndex, 0, item);
  2148. };
  2149. onMounted(() => {
  2150. startPlaceholderScroll();
  2151. });
  2152. // 头部分类模块逻辑
  2153. const headerThemeColor = ref('#e60012');
  2154. const headerCategoryList = ref<HeaderCategoryVO[]>([]);
  2155. // 获取头部分类列表
  2156. const getHeaderCategoryList = async () => {
  2157. try {
  2158. const res = await listHeaderCategory();
  2159. if (res.code === 200 && res.rows) {
  2160. headerCategoryList.value = res.rows;
  2161. }
  2162. } catch (error) {
  2163. console.error('获取头部分类列表失败:', error);
  2164. ElMessage.error('获取头部分类列表失败');
  2165. }
  2166. };
  2167. const headerDialogVisible = ref(false);
  2168. const headerEditIndex = ref(-1);
  2169. const headerForm = reactive({ title: '', icon: '', link: '', openMode: 'current', status: 1 });
  2170. const handleAddHeaderCategory = () => {
  2171. headerEditIndex.value = -1;
  2172. Object.assign(headerForm, { title: '', icon: '', link: '', openMode: 'current', status: 1 });
  2173. headerDialogVisible.value = true;
  2174. };
  2175. const handleEditHeaderCategory = (row, index) => {
  2176. headerEditIndex.value = index;
  2177. Object.assign(headerForm, JSON.parse(JSON.stringify(row)));
  2178. headerDialogVisible.value = true;
  2179. };
  2180. const submitHeaderForm = async () => {
  2181. const data: HeaderCategoryForm = {
  2182. title: headerForm.title,
  2183. icon: headerForm.icon,
  2184. link: headerForm.link,
  2185. openMode: headerForm.openMode,
  2186. status: headerForm.status
  2187. };
  2188. try {
  2189. if (headerEditIndex.value > -1) {
  2190. const id = headerCategoryList.value[headerEditIndex.value]?.id;
  2191. const res = await updateHeaderCategory({ ...data, id });
  2192. if (res.code === 200) {
  2193. ElMessage.success('修改成功');
  2194. headerDialogVisible.value = false;
  2195. getHeaderCategoryList();
  2196. } else {
  2197. ElMessage.error(res.msg || '修改失败');
  2198. }
  2199. } else {
  2200. const res = await addHeaderCategory(data);
  2201. if (res.code === 200) {
  2202. ElMessage.success('新增成功');
  2203. headerDialogVisible.value = false;
  2204. getHeaderCategoryList();
  2205. } else {
  2206. ElMessage.error(res.msg || '新增失败');
  2207. }
  2208. }
  2209. } catch (error) {
  2210. console.error('操作失败:', error);
  2211. ElMessage.error('操作失败');
  2212. }
  2213. };
  2214. const handleDeleteHeaderCategory = async (index: number) => {
  2215. const item = headerCategoryList.value[index];
  2216. try {
  2217. await ElMessageBox.confirm('确定删除该分类吗?', '提示');
  2218. const res = await delHeaderCategory(item.id);
  2219. if (res.code === 200) {
  2220. ElMessage.success('删除成功');
  2221. getHeaderCategoryList();
  2222. } else {
  2223. ElMessage.error(res.msg || '删除失败');
  2224. }
  2225. } catch (error) {
  2226. if (error !== 'cancel') {
  2227. console.error('删除失败:', error);
  2228. ElMessage.error('删除失败');
  2229. }
  2230. }
  2231. };
  2232. const moveHeader = (index, delta) => {
  2233. const target = index + delta;
  2234. if (target < 0 || target >= headerCategoryList.value.length) return;
  2235. const temp = headerCategoryList.value[index];
  2236. headerCategoryList.value[index] = headerCategoryList.value[target];
  2237. headerCategoryList.value[target] = temp;
  2238. };
  2239. // 导航滚动逻辑
  2240. const headerNavScrollRef = ref(null);
  2241. const showLeftArrow = ref(false);
  2242. const showRightArrow = ref(false);
  2243. const updateNavArrows = () => {
  2244. if (!headerNavScrollRef.value) return;
  2245. const { scrollLeft, scrollWidth, clientWidth } = headerNavScrollRef.value;
  2246. showLeftArrow.value = scrollLeft > 5;
  2247. showRightArrow.value = scrollLeft + clientWidth < scrollWidth - 5;
  2248. };
  2249. const scrollHeaderNav = (direction) => {
  2250. const container = headerNavScrollRef.value;
  2251. if (!container) return;
  2252. const scrollAmount = 350;
  2253. if (direction === 'left') {
  2254. container.scrollLeft -= scrollAmount;
  2255. } else {
  2256. container.scrollLeft += scrollAmount;
  2257. }
  2258. };
  2259. // 广告模块逻辑
  2260. const adModules = ref([
  2261. {
  2262. id: 1,
  2263. title: '企业购x百亿补贴',
  2264. titleColor: '#333333',
  2265. subTitle: '先采后付 享底价',
  2266. subTitleColor: '#999999',
  2267. type: 'subsidy',
  2268. items: [
  2269. { id: 101, name: '企业商用台式机', image: '/static/images/purchase/pc_desktop.jpg', price: '69.9' },
  2270. { id: 102, name: '商务笔记本', image: '/static/images/purchase/laptop_hp.jpg', price: '84.8' },
  2271. { id: 103, name: '智能打印机', image: '/static/images/purchase/printer_office.jpg', price: '139.9' },
  2272. { id: 104, name: '高效办公组网', image: '/static/images/purchase/network_router.jpg', price: '1749' }
  2273. ]
  2274. },
  2275. {
  2276. id: 2,
  2277. title: '企采榜单',
  2278. titleColor: '#333333',
  2279. subTitle: '同行都在买',
  2280. subTitleColor: '#f58220',
  2281. type: 'ranking',
  2282. items: [
  2283. { id: 201, name: '办公电脑榜', image: '/static/images/purchase/laptop_lenovo.jpg', price: '0', tag: '办公电脑榜', tagLink: '', sales: '1543' },
  2284. { id: 202, name: '文具榜', image: '/static/images/purchase/stationery_ranking.jpg', price: '0', tag: '文具榜', tagLink: '', sales: '2.3万' }
  2285. ]
  2286. },
  2287. {
  2288. id: 3,
  2289. title: '品牌好店',
  2290. titleColor: '#333333',
  2291. subTitle: '返2000元E卡',
  2292. subTitleColor: '#f58220',
  2293. type: 'brand',
  2294. items: [
  2295. { id: 301, name: '鲁花', image: '/static/images/purchase/oil_luhua.jpg', tag: '品质保障', tagLink: '鲁花京东自营旗舰店' },
  2296. { id: 302, name: '金龙鱼', image: '/static/images/purchase/oil_jinlongyu.jpg', tag: '热销品牌', tagLink: '金龙鱼京东自营旗舰店' }
  2297. ]
  2298. },
  2299. {
  2300. id: 4,
  2301. title: '企业精选',
  2302. titleColor: '#333333',
  2303. subTitle: '品牌专供 库存充足',
  2304. subTitleColor: '#999999',
  2305. type: 'selection',
  2306. items: [
  2307. { id: 401, name: '高性能工作站', image: '/static/images/purchase/pc_desktop.jpg', price: '10740' },
  2308. { id: 402, name: '办公咖啡机', image: '/static/images/purchase/coffee_machine.jpg', price: '877' }
  2309. ]
  2310. },
  2311. {
  2312. id: 5,
  2313. title: '企业购x京东新品',
  2314. titleColor: '#333333',
  2315. subTitle: '美的新鲜',
  2316. subTitleColor: '#f58220',
  2317. type: 'new',
  2318. items: [
  2319. { id: 501, name: '商用冷柜', image: '/static/images/purchase/freezer.jpg', price: '7188' },
  2320. { id: 502, name: '得力笔记本', image: '/static/images/purchase/notebook_deli.jpg', price: '34.9' }
  2321. ]
  2322. }
  2323. ]);
  2324. const adDialogVisible = ref(false);
  2325. const currentAdIdx = ref(-1);
  2326. const currentItemIdx = ref(-1);
  2327. const adForm = reactive({ title: '', titleColor: '#333333', subTitle: '', subTitleColor: '#f58220', items: [] });
  2328. // 实时预览辅助函数
  2329. const getAdTitleStyle = (index, type) => {
  2330. const isEditing = adDialogVisible.value && currentAdIdx.value === index;
  2331. const key = type === 'main' ? 'titleColor' : 'subTitleColor';
  2332. const defaultColor = type === 'main' ? '#333' : '#999';
  2333. const color = isEditing ? adForm[key] : adModules.value[index][key] || defaultColor;
  2334. return { color };
  2335. };
  2336. const getAdTitleText = (index, type) => {
  2337. const isEditing = adDialogVisible.value && currentAdIdx.value === index;
  2338. const key = type === 'main' ? 'title' : 'subTitle';
  2339. return isEditing ? adForm[key] : adModules.value[index][key];
  2340. };
  2341. const handleEditAd = (index) => {
  2342. currentAdIdx.value = index;
  2343. const ad = adModules.value[index];
  2344. Object.assign(adForm, JSON.parse(JSON.stringify(ad)));
  2345. adDialogVisible.value = true;
  2346. };
  2347. const submitAdForm = () => {
  2348. adModules.value[currentAdIdx.value] = JSON.parse(JSON.stringify(adForm));
  2349. adDialogVisible.value = false;
  2350. ElMessage.success('配置保存成功');
  2351. };
  2352. // 推荐设置模块逻辑
  2353. const recommendThemeColor = ref('#e60012');
  2354. const recommendProductThemeColor = ref('#e60012');
  2355. const recommendActiveId = ref(1);
  2356. const recommendList = ref([
  2357. {
  2358. id: 1,
  2359. name: '为你推荐',
  2360. subTitle: '每日低价 妙券',
  2361. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTE2IDJsNC4zNSA4LjgxbDkuNzIgMS40MWwtNy4wMyA2Ljg1bDEuNjYgOS42OEwxNiAyNS4yN2wtOC43IDQuNTdsMS42Ni05LjY4bC03LjAzLTYuODVsOS43Mi0xLjQxTDE2IDJ6Ii8+PC9zdmc+',
  2362. type: 'select',
  2363. status: 1,
  2364. selectedProducts: [
  2365. { id: 1001, name: 'Lenovo 联想 昭阳K4e 轻薄办公本 14英寸 i7/16G/512G', image: '/static/images/purchase/laptop_lenovo.jpg', price: '4599' },
  2366. { id: 1002, name: 'HP 惠普 战66 六代 商务笔记本电脑 锐龙版 15.6英寸', image: '/static/images/purchase/laptop_hp.jpg', price: '3899' }
  2367. ]
  2368. },
  2369. {
  2370. id: 2,
  2371. name: '新客专享',
  2372. subTitle: '精选爆品 低价',
  2373. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTI2IDE0djEwaC00VjhIMTB2MTZINnYtMTBMNCAxMmw4LThoOHoiLz48cGF0aCBmaWxsPSIjZTYwMDEyIiBkPSJNMjIgMjRIMTB2LTRoMTJ6Ii8+PC9zdmc+',
  2374. type: 'select',
  2375. status: 1,
  2376. selectedProducts: []
  2377. },
  2378. {
  2379. id: 3,
  2380. name: '办公设备',
  2381. subTitle: '电脑设备 批量',
  2382. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTI4IDI0SDR2LTJoMjR6bS0yLTJIMnYtNGgyNHptMC00SDJ2LTRoMjR6bTAtNEgydi00aDI0eiIvPjxwYXRoIGZpbGw9IiNlNjAwMTIiIGQ9Ik04IDI4aDE2di0ySDh6Ii8+PC9zdmc+',
  2383. type: 'category',
  2384. categoryLabel: '办公设备 > 办公电脑 > 笔记本',
  2385. status: 1,
  2386. selectedProducts: []
  2387. },
  2388. {
  2389. id: 4,
  2390. name: '办公耗材',
  2391. subTitle: '低价直供 囤货',
  2392. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTI0IDRINFYyOGgyMHYtNHoiLz48cGF0aCBmaWxsPSIjZTYwMDEyIiBkPSJNMjggMTJsLTQgNHY4aDR6Ii8+PC9zdmc+',
  2393. type: 'category',
  2394. categoryLabel: '文具耗材 > 打印耗材 > 复印纸',
  2395. status: 1,
  2396. selectedProducts: []
  2397. },
  2398. {
  2399. id: 5,
  2400. name: '工业生产',
  2401. subTitle: '车间工具 规格',
  2402. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTE2IDhsLTIuNDUgNC4yNUg4LjY0djQuOTFMNi4xOCAxOWwyLjQ1IDQuMjV2NC45MWg0LjkxbDIuNDUgNC4yNWwyLjQ1LTQuMjVoNC45MXYtNC45MWwyLjQ1LTQuMjVsLTIuNDUtNC4yNXYtNC45MWgtNC45MUwxNiA4eiIvPjwvc3ZnPg==',
  2403. type: 'category',
  2404. categoryLabel: '工业品 > 五金工具 > 电动工具',
  2405. status: 1,
  2406. selectedProducts: []
  2407. },
  2408. {
  2409. id: 6,
  2410. name: '员工福利',
  2411. subTitle: '节日礼盒 一键',
  2412. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTE2IDI4czEyLTYuNDIgMTItMTZjMC01LjM2LTQuNjQtOS05LTlhOC43OCA4Ljc4IDAgMC0wIDMtM2MtNC4zNiAwLTkgMy42NC05IDlzMTIgMTYgMTIgMTZ6Ii8+PC9zdmc+',
  2413. type: 'select',
  2414. status: 1,
  2415. selectedProducts: []
  2416. },
  2417. {
  2418. id: 7,
  2419. name: '办公环境',
  2420. subTitle: '电器家具 焕新',
  2421. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTE2IDhsOCAxNkg4bDgtMTZ6Ii8+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTI2IDI4SDR2LTRoMjJ6Ii8+PC9zdmc+',
  2422. type: 'select',
  2423. status: 1,
  2424. selectedProducts: []
  2425. },
  2426. {
  2427. id: 8,
  2428. name: '营销礼品',
  2429. subTitle: '客户礼包 特惠',
  2430. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTI2IDEwaC02VjhIMTJ2Mkg2djE2aDIwVjEweiIvPjxwYXRoIGZpbGw9IiNlNjAwMTIiIGQ9Ik0xOCAxMHYtMmgLTR2MnoiLz48L3N2Zz4=',
  2431. type: 'select',
  2432. status: 1,
  2433. selectedProducts: []
  2434. },
  2435. {
  2436. id: 9,
  2437. name: '个人防护',
  2438. subTitle: '劳保用品 齐全',
  2439. icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2U2MDAxMiIgZD0iTTE2IDJsLTEyIDR2MTBzMCAxMiAxMiAxNmMxMi00IDEyLTE2IDEyLTE2VjZMMTYgMnoiLz48L3N2Zz4=',
  2440. type: 'category',
  2441. categoryLabel: '个人防护 > 头部防护 > 安全帽',
  2442. status: 1,
  2443. selectedProducts: []
  2444. },
  2445. { id: 10, name: '清洁用品', subTitle: '扫把 拖把 纸巾', icon: '', type: 'select', status: 1, selectedProducts: [] },
  2446. { id: 11, name: '运动器材', subTitle: '跑步机 哑铃', icon: '', type: 'select', status: 1, selectedProducts: [] },
  2447. { id: 12, name: '生鲜水果', subTitle: '苹果 橙子 香蕉', icon: '', type: 'select', status: 1, selectedProducts: [] },
  2448. { id: 13, name: '零食小吃', subTitle: '坚果 薯片 饼干', icon: '', type: 'select', status: 1, selectedProducts: [] },
  2449. { id: 14, name: '美妆护肤', subTitle: '洗面奶 补水', icon: '', type: 'select', status: 1, selectedProducts: [] }
  2450. ]);
  2451. const activeRecommendList = computed(() => {
  2452. return recommendList.value.filter((item) => item.status === 1);
  2453. });
  2454. const recommendDialogVisible = ref(false);
  2455. const recommendEditIndex = ref(-1);
  2456. const recommendForm = reactive({
  2457. name: '',
  2458. subTitle: '',
  2459. icon: '',
  2460. type: 'select',
  2461. categoryValue: [],
  2462. categoryLabel: '',
  2463. selectedProducts: [],
  2464. status: 1
  2465. });
  2466. const mockCategoryOptions = [
  2467. {
  2468. value: 'bg',
  2469. label: '办公设备',
  2470. children: [
  2471. {
  2472. value: 'dn',
  2473. label: '办公电脑',
  2474. children: [
  2475. { value: 'bjb', label: '笔记本' },
  2476. { value: 'tsj', label: '台式机' }
  2477. ]
  2478. }
  2479. ]
  2480. },
  2481. {
  2482. value: 'wj',
  2483. label: '文具耗材',
  2484. children: [
  2485. {
  2486. value: 'dy',
  2487. label: '打印耗材',
  2488. children: [
  2489. { value: 'fyz', label: '复印纸' },
  2490. { value: 'xhg', label: '硒鼓' }
  2491. ]
  2492. }
  2493. ]
  2494. }
  2495. ];
  2496. const handleAddRecommend = () => {
  2497. recommendEditIndex.value = -1;
  2498. Object.assign(recommendForm, {
  2499. name: '',
  2500. subTitle: '',
  2501. icon: '',
  2502. type: 'select',
  2503. categoryValue: [],
  2504. categoryLabel: '',
  2505. selectedProducts: [],
  2506. status: 1
  2507. });
  2508. recommendDialogVisible.value = true;
  2509. };
  2510. const handleEditRecommend = (row, index) => {
  2511. recommendEditIndex.value = index;
  2512. Object.assign(recommendForm, JSON.parse(JSON.stringify(row)));
  2513. recommendDialogVisible.value = true;
  2514. };
  2515. const getLabelsByValues = (values, options) => {
  2516. const labels = [];
  2517. let currentOptions = options;
  2518. for (const val of values) {
  2519. const target = currentOptions.find((opt) => opt.value === val);
  2520. if (target) {
  2521. labels.push(target.label);
  2522. currentOptions = target.children || [];
  2523. }
  2524. }
  2525. return labels.join(' > ');
  2526. };
  2527. const handleRecommendCategoryChange = (val) => {
  2528. if (val && val.length > 0) {
  2529. recommendForm.categoryLabel = getLabelsByValues(val, mockCategoryOptions);
  2530. } else {
  2531. recommendForm.categoryLabel = '';
  2532. }
  2533. };
  2534. const submitRecommendForm = () => {
  2535. if (recommendEditIndex.value > -1) {
  2536. recommendList.value[recommendEditIndex.value] = { ...recommendForm };
  2537. } else {
  2538. recommendList.value.push({ id: Date.now(), ...recommendForm });
  2539. }
  2540. recommendDialogVisible.value = false;
  2541. ElMessage.success('保存成功');
  2542. };
  2543. // 推荐商品选择增强逻辑
  2544. const selectedProductDialogVisible = ref(false);
  2545. const productSelectionDrawerVisible = ref(false);
  2546. const currentRecommendIndex = ref(-1);
  2547. const drawerSelection = ref([]);
  2548. const selectedProductsTableRef = ref(null);
  2549. const drawerTableRef = ref(null);
  2550. const selectedCurrentPage = ref(1);
  2551. const selectedPageSize = ref(10);
  2552. const pagedSelectedProducts = computed(() => {
  2553. const list = recommendList.value[currentRecommendIndex.value]?.selectedProducts || [];
  2554. const start = (selectedCurrentPage.value - 1) * selectedPageSize.value;
  2555. const end = start + selectedPageSize.value;
  2556. return list.slice(start, end);
  2557. });
  2558. const handleSelectedProductsSelectionChange = (val) => {
  2559. currentItemIdx.value = val; // 临时借用 currentItemIdx 存储选中的行
  2560. };
  2561. const openRecommendProductSelect = (index) => {
  2562. currentRecommendIndex.value = index;
  2563. selectedCurrentPage.value = 1;
  2564. selectedProductDialogVisible.value = true;
  2565. };
  2566. const submitSelectedProducts = () => {
  2567. selectedProductDialogVisible.value = false;
  2568. ElMessage.success('已选商品配置已保存');
  2569. };
  2570. const openProductDrawer = () => {
  2571. const currentList = recommendList.value[currentRecommendIndex.value]?.selectedProducts || [];
  2572. drawerSelection.value = JSON.parse(JSON.stringify(currentList));
  2573. selectSearchQuery.value = '';
  2574. selectCurrentPage.value = 1;
  2575. productSelectionDrawerVisible.value = true;
  2576. // 回显勾选逻辑
  2577. nextTick(() => {
  2578. if (drawerTableRef.value) {
  2579. drawerTableRef.value.clearSelection();
  2580. // 由于启用了 reserve-selection,我们直接遍历库中匹配的项进行勾选
  2581. mockProductList.forEach((item) => {
  2582. if (currentList.some((exist) => exist.id === item.id)) {
  2583. drawerTableRef.value.toggleRowSelection(item, true);
  2584. }
  2585. });
  2586. }
  2587. });
  2588. };
  2589. const handleDrawerSelectionChange = (val) => {
  2590. drawerSelection.value = val;
  2591. };
  2592. const confirmDrawerSelection = () => {
  2593. // 直接全量覆盖,支持在抽屉里取消勾选来删除
  2594. recommendList.value[currentRecommendIndex.value].selectedProducts = JSON.parse(JSON.stringify(drawerSelection.value));
  2595. ElMessage.success('商品列表同步成功');
  2596. productSelectionDrawerVisible.value = false;
  2597. };
  2598. const removeSelectedProduct = (index) => {
  2599. recommendList.value[currentRecommendIndex.value].selectedProducts.splice(index, 1);
  2600. ElMessage.success('移除成功');
  2601. };
  2602. const batchRemoveSelectedProducts = () => {
  2603. const selectedRows = selectedProductsTableRef.value?.getSelectionRows() || [];
  2604. if (selectedRows.length === 0) return ElMessage.warning('请先勾选要移除的商品');
  2605. ElMessageBox.confirm(`确定移除选中的 ${selectedRows.length} 个商品吗?`, '提示').then(() => {
  2606. const currentList = recommendList.value[currentRecommendIndex.value].selectedProducts;
  2607. const selectedIds = selectedRows.map((r) => r.id);
  2608. recommendList.value[currentRecommendIndex.value].selectedProducts = currentList.filter((item) => !selectedIds.includes(item.id));
  2609. ElMessage.success('批量移除成功');
  2610. });
  2611. };
  2612. // 推荐预览滚动逻辑
  2613. const recommendScrollRef = ref(null);
  2614. const recShowLeft = ref(false);
  2615. const recShowRight = ref(false);
  2616. const updateRecArrows = () => {
  2617. if (!recommendScrollRef.value) return;
  2618. const { scrollLeft, scrollWidth, clientWidth } = recommendScrollRef.value;
  2619. recShowLeft.value = scrollLeft > 5;
  2620. recShowRight.value = scrollLeft + clientWidth < scrollWidth - 5;
  2621. };
  2622. const scrollRecommend = (direction) => {
  2623. const container = recommendScrollRef.value;
  2624. if (!container) return;
  2625. const scrollAmount = 400;
  2626. if (direction === 'left') {
  2627. container.scrollLeft -= scrollAmount;
  2628. } else {
  2629. container.scrollLeft += scrollAmount;
  2630. }
  2631. };
  2632. // 商品选择逻辑
  2633. const selectDialogVisible = ref(false);
  2634. const selectSearchQuery = ref('');
  2635. const selectedTempId = ref(null);
  2636. const selectCurrentPage = ref(1);
  2637. const selectPageSize = ref(8);
  2638. const mockProductList = [
  2639. { id: 1001, name: 'Lenovo 联想 昭阳K4e 轻薄办公本 14英寸 i7/16G/512G', image: '/static/images/purchase/laptop_lenovo.jpg', price: '4599' },
  2640. { id: 1002, name: 'HP 惠普 战66 六代 商务笔记本电脑 锐龙版 15.6英寸', image: '/static/images/purchase/laptop_hp.jpg', price: '3899' },
  2641. { id: 1003, name: 'Apple iPad Air 10.9英寸 256G 银色 蜂窝版', image: '/static/images/purchase/ipad.jpg', price: '4799' },
  2642. { id: 1004, name: 'Logitech 罗技 MX Master 3S 无线鼠标 石墨黑', image: '/static/images/purchase/mouse.jpg', price: '699' },
  2643. { id: 1005, name: 'Dell 戴尔 U2723QE 27英寸 4K 核心显示器 IPS黑', image: '/static/images/purchase/monitor.jpg', price: '3299' },
  2644. { id: 3001, name: '鲁花 5S一级压榨花生油 5L 礼盒装 物理压榨', image: '/static/images/purchase/oil_luhua.jpg', price: '159.9' },
  2645. { id: 3002, name: '金龙鱼 阳光葵花籽油 5L 瓶装 零反式脂肪', image: '/static/images/purchase/oil_jinlongyu.jpg', price: '69.9' },
  2646. { id: 3003, name: '蒙牛 特仑苏 纯牛奶 250ml*12 提装 高钙低脂', image: '/static/images/purchase/water_nongfu.jpg', price: '65.0' },
  2647. { id: 3004, name: '三只松鼠 坚果礼盒 1.5kg 聚享版 办公室零食', image: '/static/images/purchase/coffee_machine.jpg', price: '128.0' },
  2648. { id: 3005, name: '农夫山泉 饮用天然水 550ml*24 整箱 弱碱性', image: '/static/images/purchase/water_nongfu.jpg', price: '28.8' },
  2649. { id: 5001, name: '得力 办公A4复印纸 80g 500张/包 5包/箱', image: '/static/images/purchase/paper_deli.jpg', price: '145.0' },
  2650. { id: 5002, name: '公牛 插座 10位 5米总控 办公专用排插', image: '/static/images/purchase/socket_bull.jpg', price: '89.0' }
  2651. ];
  2652. const filteredSelectList = computed(() => {
  2653. if (!selectSearchQuery.value) return mockProductList;
  2654. return mockProductList.filter((item) => item.name.includes(selectSearchQuery.value) || item.id.toString().includes(selectSearchQuery.value));
  2655. });
  2656. const pagedSelectList = computed(() => {
  2657. const start = (selectCurrentPage.value - 1) * selectPageSize.value;
  2658. const end = start + selectPageSize.value;
  2659. return filteredSelectList.value.slice(start, end);
  2660. });
  2661. const openProductSelect = (index) => {
  2662. currentItemIdx.value = index;
  2663. selectedTempId.value = adForm.items[index].id;
  2664. selectSearchQuery.value = '';
  2665. selectDialogVisible.value = true;
  2666. };
  2667. const confirmSelect = () => {
  2668. const item = mockProductList.find((i) => i.id === selectedTempId.value);
  2669. if (item) {
  2670. const target = adForm.items[currentItemIdx.value];
  2671. target.id = item.id;
  2672. target.name = item.name;
  2673. target.image = item.image;
  2674. target.price = item.price;
  2675. selectDialogVisible.value = false;
  2676. ElMessage.success('选择成功');
  2677. }
  2678. };
  2679. // 快捷入口模块逻辑
  2680. const qePageIndex = ref(0);
  2681. const quickEntrySettings = reactive({
  2682. id: null as string | number | null,
  2683. moduleName: '企业工作台',
  2684. jumpLink: ''
  2685. });
  2686. // 获取快捷入口模块配置
  2687. const getQuickEntryModuleList = async () => {
  2688. try {
  2689. const res = await listQuickEntryModule();
  2690. if (res.code === 200 && res.rows && res.rows.length > 0) {
  2691. const data = res.rows[0];
  2692. quickEntrySettings.id = data.id;
  2693. quickEntrySettings.moduleName = data.moduleName || '';
  2694. quickEntrySettings.jumpLink = data.jumpLink || '';
  2695. }
  2696. } catch (error) {
  2697. console.error('获取快捷入口模块配置失败:', error);
  2698. ElMessage.error('获取快捷入口模块配置失败');
  2699. }
  2700. };
  2701. // 保存快捷入口模块配置
  2702. const saveQuickEntryModule = async () => {
  2703. const data: QuickEntryModuleForm = {
  2704. moduleName: quickEntrySettings.moduleName,
  2705. jumpLink: quickEntrySettings.jumpLink
  2706. };
  2707. try {
  2708. let res;
  2709. if (quickEntrySettings.id) {
  2710. res = await updateQuickEntryModule({ ...data, id: quickEntrySettings.id });
  2711. } else {
  2712. res = await addQuickEntryModule(data);
  2713. if (res.code === 200 && res.data) {
  2714. quickEntrySettings.id = res.data.id;
  2715. }
  2716. }
  2717. if (res.code === 200) {
  2718. ElMessage.success('快捷入口模块配置保存成功');
  2719. } else {
  2720. ElMessage.error(res.msg || '保存失败');
  2721. }
  2722. } catch (error) {
  2723. console.error('保存快捷入口模块配置失败:', error);
  2724. ElMessage.error('保存失败');
  2725. }
  2726. };
  2727. const quickEntryList = ref<QuickEntryItemsVO[]>([]);
  2728. // 获取快捷入口项列表
  2729. const getQuickEntryItemsList = async () => {
  2730. try {
  2731. const res = await listQuickEntryItems();
  2732. if (res.code === 200 && res.rows) {
  2733. quickEntryList.value = res.rows.map((item: QuickEntryItemsVO) => ({
  2734. ...item,
  2735. icon: (item as any).iconUrl || '',
  2736. tag: (item as any).tagText || '',
  2737. link: (item as any).jumpLink || ''
  2738. }));
  2739. }
  2740. } catch (error) {
  2741. console.error('获取快捷入口项列表失败:', error);
  2742. ElMessage.error('获取快捷入口项列表失败');
  2743. }
  2744. };
  2745. const qePageCount = computed(() => Math.ceil(quickEntryList.value.filter((i) => i.status === 1).length / 8));
  2746. const getPageItems = (pageIdx) => {
  2747. const activeItems = quickEntryList.value.filter((i) => i.status === 1);
  2748. return activeItems.slice(pageIdx * 8, (pageIdx + 1) * 8);
  2749. };
  2750. const quickEntryDialogVisible = ref(false);
  2751. const quickEntryDialogType = ref('add');
  2752. const qeEditIndex = ref(-1);
  2753. const quickEntryForm = reactive({ name: '', icon: '', tag: '', link: '', status: 1 });
  2754. const handleAddQuickEntry = () => {
  2755. quickEntryDialogType.value = 'add';
  2756. Object.assign(quickEntryForm, { name: '', icon: '', tag: '', link: '', status: 1 });
  2757. quickEntryDialogVisible.value = true;
  2758. };
  2759. const handleEditQuickEntry = (row, index) => {
  2760. quickEntryDialogType.value = 'edit';
  2761. qeEditIndex.value = index;
  2762. Object.assign(quickEntryForm, JSON.parse(JSON.stringify(row)));
  2763. quickEntryDialogVisible.value = true;
  2764. };
  2765. const submitQuickEntryForm = async () => {
  2766. if (!quickEntryForm.name) return ElMessage.warning('请输入入口名称');
  2767. const data: QuickEntryItemsForm = {
  2768. name: quickEntryForm.name,
  2769. iconUrl: quickEntryForm.icon,
  2770. tagText: quickEntryForm.tag,
  2771. jumpLink: quickEntryForm.link,
  2772. status: quickEntryForm.status,
  2773. sortOrder: quickEntryList.value.length
  2774. };
  2775. try {
  2776. if (quickEntryDialogType.value === 'add') {
  2777. const res = await addQuickEntryItems(data);
  2778. if (res.code === 200) {
  2779. ElMessage.success('新增成功');
  2780. quickEntryDialogVisible.value = false;
  2781. getQuickEntryItemsList();
  2782. } else {
  2783. ElMessage.error(res.msg || '新增失败');
  2784. }
  2785. } else {
  2786. const id = quickEntryList.value[qeEditIndex.value]?.id;
  2787. const res = await updateQuickEntryItems({ ...data, id });
  2788. if (res.code === 200) {
  2789. ElMessage.success('修改成功');
  2790. quickEntryDialogVisible.value = false;
  2791. getQuickEntryItemsList();
  2792. } else {
  2793. ElMessage.error(res.msg || '修改失败');
  2794. }
  2795. }
  2796. } catch (error) {
  2797. console.error('操作失败:', error);
  2798. ElMessage.error('操作失败');
  2799. }
  2800. };
  2801. const handleDeleteQuickEntry = async (index: number) => {
  2802. const item = quickEntryList.value[index];
  2803. try {
  2804. await ElMessageBox.confirm('确定要删除该入口吗?', '提示', { type: 'warning' });
  2805. const res = await delQuickEntryItems(item.id);
  2806. if (res.code === 200) {
  2807. ElMessage.success('删除成功');
  2808. getQuickEntryItemsList();
  2809. } else {
  2810. ElMessage.error(res.msg || '删除失败');
  2811. }
  2812. } catch (error) {
  2813. if (error !== 'cancel') {
  2814. console.error('删除失败:', error);
  2815. ElMessage.error('删除失败');
  2816. }
  2817. }
  2818. };
  2819. const moveQE = async (index: number, direction: number) => {
  2820. const newIndex = index + direction;
  2821. if (newIndex < 0 || newIndex >= quickEntryList.value.length) return;
  2822. const item = quickEntryList.value.splice(index, 1)[0];
  2823. quickEntryList.value.splice(newIndex, 0, item);
  2824. // 同步排序到后端
  2825. try {
  2826. const start = Math.min(index, newIndex);
  2827. const end = Math.max(index, newIndex);
  2828. for (let i = start; i <= end; i++) {
  2829. const row = quickEntryList.value[i] as any;
  2830. await updateQuickEntryItems({
  2831. id: row.id,
  2832. name: row.name,
  2833. iconUrl: row.icon || row.iconUrl,
  2834. tagText: row.tag || row.tagText,
  2835. jumpLink: row.link || row.jumpLink,
  2836. status: row.status,
  2837. sortOrder: i
  2838. });
  2839. }
  2840. } catch (error) {
  2841. console.error('排序更新失败:', error);
  2842. ElMessage.error('排序更新失败');
  2843. }
  2844. };
  2845. watch(
  2846. headerCategoryList,
  2847. () => {
  2848. nextTick(() => updateNavArrows());
  2849. },
  2850. { deep: true }
  2851. );
  2852. onMounted(() => {
  2853. startPlaceholderScroll();
  2854. // 获取当前配置
  2855. getCurrentSearch();
  2856. getCurrentAdLeftBtn();
  2857. getCarouselList();
  2858. getHeaderCategoryList();
  2859. getCategoryList();
  2860. getScenarioGlobalSettingsList();
  2861. getScenarioList();
  2862. getQuickEntryModuleList();
  2863. getQuickEntryItemsList();
  2864. // 获取左侧广告配置
  2865. nextTick(() => {
  2866. updateNavArrows();
  2867. updateRecArrows();
  2868. });
  2869. // 延迟再次检测,确保图片加载后布局稳定
  2870. setTimeout(() => {
  2871. updateRecArrows();
  2872. }, 500);
  2873. window.addEventListener('resize', updateNavArrows);
  2874. window.addEventListener('resize', updateRecArrows);
  2875. });
  2876. onUnmounted(() => {
  2877. if (timer) clearInterval(timer);
  2878. window.removeEventListener('resize', updateNavArrows);
  2879. window.removeEventListener('resize', updateRecArrows);
  2880. });
  2881. watch(
  2882. activeRecommendList,
  2883. () => {
  2884. nextTick(() => {
  2885. updateRecArrows();
  2886. });
  2887. },
  2888. { deep: true }
  2889. );
  2890. // 监听子页签切换,确保切换回推荐设置时更新箭头状态
  2891. watch(activeSubTab, (newVal) => {
  2892. if (newVal === 'recommend') {
  2893. nextTick(() => {
  2894. setTimeout(() => {
  2895. updateRecArrows();
  2896. }, 300);
  2897. });
  2898. }
  2899. });
  2900. </script>
  2901. <style scoped>
  2902. /* 基础辅助类 */
  2903. .flex-column {
  2904. display: flex;
  2905. flex-direction: column;
  2906. }
  2907. .flex-center {
  2908. display: flex;
  2909. align-items: center;
  2910. }
  2911. .gap-5 {
  2912. gap: 5px;
  2913. }
  2914. .gap-10 {
  2915. gap: 10px;
  2916. }
  2917. .m-b-30 {
  2918. margin-bottom: 30px;
  2919. }
  2920. .scenario-editor-container {
  2921. padding: 32px 40px;
  2922. display: flex;
  2923. flex-direction: column;
  2924. background-color: #fff; /* 纯白背景 */
  2925. min-height: 100%;
  2926. }
  2927. .preview-section-standard {
  2928. background: #fcfdfe;
  2929. border: 1px solid #eef2f6;
  2930. border-radius: 8px;
  2931. padding: 32px;
  2932. margin-bottom: 32px;
  2933. }
  2934. .section-title-standard {
  2935. font-size: 16px;
  2936. font-weight: bold;
  2937. color: #333;
  2938. margin-bottom: 24px;
  2939. display: flex;
  2940. align-items: center;
  2941. }
  2942. .section-title-standard::before {
  2943. content: '';
  2944. width: 4px;
  2945. height: 16px;
  2946. background-color: #e60012;
  2947. margin-right: 12px;
  2948. border-radius: 2px;
  2949. }
  2950. .config-section-standard {
  2951. padding: 32px 0;
  2952. border-bottom: 1px solid #f0f0f0;
  2953. }
  2954. .settings-form-standard {
  2955. padding-left: 16px;
  2956. }
  2957. .preview-tip {
  2958. font-size: 12px;
  2959. color: #999;
  2960. margin-top: 16px;
  2961. }
  2962. .rank-action-btns {
  2963. display: flex;
  2964. flex-direction: column;
  2965. align-items: center;
  2966. gap: 4px;
  2967. }
  2968. .rank-btn-mini {
  2969. cursor: pointer;
  2970. font-size: 14px;
  2971. color: #909399;
  2972. transition: color 0.2s;
  2973. }
  2974. .rank-btn-mini:hover {
  2975. color: #e60012;
  2976. }
  2977. .standard-table :deep(.el-table__header) th {
  2978. background-color: #f8fafc;
  2979. color: #606266;
  2980. font-weight: bold;
  2981. }
  2982. .parameter-settings {
  2983. flex: 1;
  2984. min-height: 0; /* 关键:防止 flex 子项被内容撑开 */
  2985. display: flex;
  2986. flex-direction: column;
  2987. background-color: #fff;
  2988. overflow: hidden;
  2989. }
  2990. .sub-tabs {
  2991. display: flex;
  2992. padding: 0 20px;
  2993. border-bottom: 1px solid #f0f0f0;
  2994. flex-shrink: 0;
  2995. height: 48px; /* 固定高度,防止切换时抖动 */
  2996. align-items: center;
  2997. }
  2998. .sub-tab-item {
  2999. padding: 12px 20px;
  3000. cursor: pointer;
  3001. font-size: 14px;
  3002. color: #666;
  3003. font-weight: 500;
  3004. position: relative;
  3005. }
  3006. .sub-tab-item.active {
  3007. color: var(--primary-color);
  3008. }
  3009. .sub-tab-item.active::after {
  3010. content: '';
  3011. position: absolute;
  3012. bottom: 0;
  3013. left: 20px;
  3014. right: 20px;
  3015. height: 2px;
  3016. background-color: var(--primary-color);
  3017. }
  3018. .content-body {
  3019. flex: 1;
  3020. overflow-y: auto;
  3021. }
  3022. /* 搜索编辑区布局 */
  3023. .search-editor {
  3024. display: flex;
  3025. flex-direction: column;
  3026. padding: 20px 40px;
  3027. gap: 30px;
  3028. }
  3029. /* 预览区域 */
  3030. .preview-section {
  3031. background-color: #fff;
  3032. border: 1px dashed #dcdfe6;
  3033. border-radius: 8px;
  3034. padding: 20px;
  3035. }
  3036. .preview-title {
  3037. font-size: 13px;
  3038. color: #909399;
  3039. margin-bottom: 15px;
  3040. font-weight: bold;
  3041. }
  3042. .live-preview-box {
  3043. background-color: #fff;
  3044. padding: 40px 20px;
  3045. border-radius: 4px;
  3046. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
  3047. }
  3048. /* 仿真 Mockup */
  3049. .search-bar-mockup {
  3050. display: flex;
  3051. align-items: center;
  3052. max-width: 1400px; /* 进一步加宽容器 */
  3053. margin: 0 auto;
  3054. gap: 30px;
  3055. }
  3056. .mockup-left {
  3057. flex-shrink: 0;
  3058. text-align: center;
  3059. }
  3060. .main-title {
  3061. font-size: 32px;
  3062. font-weight: 800;
  3063. line-height: 1.2;
  3064. }
  3065. .sub-title {
  3066. font-size: 12px;
  3067. color: #999;
  3068. margin-top: 4px;
  3069. letter-spacing: 1px;
  3070. }
  3071. .mockup-center {
  3072. flex: 2; /* 增加中心区域的弹性占比,使搜索框更宽 */
  3073. }
  3074. .search-input-wrapper {
  3075. height: 44px;
  3076. border: 2px solid transparent; /* 由动态绑定控制颜色 */
  3077. border-radius: 8px;
  3078. display: flex;
  3079. align-items: center;
  3080. overflow: hidden;
  3081. position: relative;
  3082. }
  3083. .placeholder-scroll {
  3084. flex: 1;
  3085. padding: 0 15px;
  3086. height: 100%;
  3087. overflow: hidden;
  3088. }
  3089. .scroll-container {
  3090. height: 100%;
  3091. position: relative;
  3092. }
  3093. .scroll-item {
  3094. height: 100%;
  3095. display: flex;
  3096. align-items: center;
  3097. color: #999;
  3098. font-size: 15px;
  3099. }
  3100. .search-btn {
  3101. width: 90px;
  3102. height: 34px;
  3103. color: #fff;
  3104. display: flex;
  3105. align-items: center;
  3106. justify-content: center;
  3107. font-weight: bold;
  3108. font-size: 15px;
  3109. cursor: pointer;
  3110. border-radius: 6px;
  3111. letter-spacing: 2px;
  3112. margin-right: 4px;
  3113. transition: opacity 0.2s;
  3114. }
  3115. .search-btn:hover {
  3116. opacity: 0.9;
  3117. }
  3118. .hot-words {
  3119. margin-top: 8px;
  3120. display: flex;
  3121. gap: 15px;
  3122. padding-left: 5px;
  3123. }
  3124. .hot-word {
  3125. font-size: 12px;
  3126. color: #999;
  3127. cursor: pointer;
  3128. transition: color 0.2s;
  3129. }
  3130. .hot-word:hover {
  3131. color: var(--theme-color);
  3132. }
  3133. .mockup-right {
  3134. flex-shrink: 0;
  3135. }
  3136. .cart-btn {
  3137. display: flex;
  3138. align-items: center;
  3139. gap: 8px;
  3140. padding: 8px 16px;
  3141. border: 1px solid transparent; /* 由动态绑定控制颜色 */
  3142. border-radius: 8px;
  3143. font-size: 14px;
  3144. cursor: pointer;
  3145. background-color: #fff;
  3146. transition: all 0.2s;
  3147. }
  3148. .cart-btn:hover {
  3149. filter: brightness(0.95);
  3150. }
  3151. /* 广告图编辑区 */
  3152. .carousel-editor {
  3153. padding: 20px 40px;
  3154. display: flex;
  3155. flex-direction: column;
  3156. gap: 40px;
  3157. }
  3158. .editor-section {
  3159. display: flex;
  3160. flex-direction: column;
  3161. gap: 20px;
  3162. }
  3163. .section-header {
  3164. display: flex;
  3165. flex-direction: column;
  3166. gap: 4px;
  3167. }
  3168. .section-title {
  3169. font-size: 16px;
  3170. font-weight: bold;
  3171. color: #333;
  3172. }
  3173. .section-desc {
  3174. font-size: 13px;
  3175. color: #999;
  3176. }
  3177. /* 左侧广告设置 */
  3178. .left-ad-container {
  3179. display: flex;
  3180. flex-direction: column;
  3181. gap: 10px;
  3182. }
  3183. .left-ad-preview-wrapper {
  3184. width: 80px;
  3185. height: 460px;
  3186. border-radius: 8px;
  3187. overflow: hidden;
  3188. position: relative;
  3189. cursor: pointer;
  3190. transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  3191. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  3192. border: 1px solid #eee;
  3193. }
  3194. .left-ad-preview-wrapper.expanded {
  3195. width: 790px;
  3196. }
  3197. .left-ad-img {
  3198. width: 790px; /* 固定宽度,外层容器裁剪 */
  3199. height: 460px;
  3200. object-fit: cover;
  3201. display: block;
  3202. }
  3203. .left-ad-empty {
  3204. width: 100%;
  3205. height: 100%;
  3206. background-color: #f5f7fa;
  3207. display: flex;
  3208. flex-direction: column;
  3209. align-items: center;
  3210. justify-content: center;
  3211. color: #909399;
  3212. border: 1px dashed #dcdfe6;
  3213. }
  3214. .ad-actions {
  3215. position: absolute;
  3216. top: 20px;
  3217. right: 20px;
  3218. display: flex;
  3219. gap: 10px;
  3220. background: rgba(255, 255, 255, 0.9);
  3221. padding: 8px;
  3222. border-radius: 30px;
  3223. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  3224. }
  3225. .left-ad-settings {
  3226. flex: 1;
  3227. display: flex;
  3228. flex-direction: column;
  3229. gap: 15px;
  3230. padding: 10px 0; /* 移除背景和边框 */
  3231. }
  3232. .settings-input-ad {
  3233. max-width: 500px;
  3234. }
  3235. .left-ad-tip {
  3236. display: flex;
  3237. align-items: center;
  3238. gap: 6px;
  3239. font-size: 13px;
  3240. color: #a8abb2;
  3241. }
  3242. /* 轮播图预览 */
  3243. .carousel-preview-box {
  3244. padding: 10px 0;
  3245. }
  3246. .preview-mockup {
  3247. width: 552px;
  3248. height: 190px;
  3249. border-radius: 8px;
  3250. overflow: hidden;
  3251. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  3252. background-color: #f5f7fa;
  3253. }
  3254. /* 强力锁定轮播图指示器至左下角 */
  3255. :deep(.el-carousel__indicators--horizontal) {
  3256. left: 20px !important;
  3257. right: auto !important;
  3258. bottom: 15px !important;
  3259. transform: none !important;
  3260. width: fit-content !important;
  3261. margin: 0 !important;
  3262. padding: 0 !important;
  3263. display: flex !important;
  3264. justify-content: flex-start !important;
  3265. }
  3266. :deep(.el-carousel__indicator--horizontal) {
  3267. display: inline-block !important;
  3268. padding: 0 3px !important;
  3269. }
  3270. :deep(.el-carousel__indicators--inside) {
  3271. left: 20px !important;
  3272. transform: none !important;
  3273. }
  3274. :deep(.el-carousel__indicator) {
  3275. padding: 0 3px;
  3276. }
  3277. :deep(.el-carousel__button) {
  3278. width: 6px;
  3279. height: 6px;
  3280. border-radius: 50%;
  3281. background-color: rgba(255, 255, 255, 0.4) !important;
  3282. opacity: 1 !important;
  3283. transition: all 0.3s;
  3284. }
  3285. :deep(.el-carousel__indicator.is-active .el-carousel__button) {
  3286. width: 20px; /* 激活状态变为长胶囊 */
  3287. border-radius: 10px;
  3288. background-color: #ffffff !important;
  3289. }
  3290. .carousel-slide {
  3291. width: 100%;
  3292. height: 100%;
  3293. }
  3294. .carousel-slide img {
  3295. width: 100%;
  3296. height: 100%;
  3297. object-fit: cover;
  3298. }
  3299. .carousel-empty {
  3300. height: 100%;
  3301. display: flex;
  3302. flex-direction: column;
  3303. align-items: center;
  3304. justify-content: center;
  3305. color: #ccc;
  3306. background-color: #f9f9f9;
  3307. }
  3308. /* 轮播列表 */
  3309. .carousel-list-box {
  3310. display: flex;
  3311. flex-direction: column;
  3312. gap: 15px;
  3313. margin-top: 10px;
  3314. }
  3315. .list-toolbar {
  3316. display: flex;
  3317. justify-content: space-between;
  3318. align-items: center;
  3319. padding: 0 5px;
  3320. }
  3321. .btn-add-carousel {
  3322. padding: 0 25px;
  3323. font-weight: bold;
  3324. }
  3325. /* 自定义表头样式 */
  3326. :deep(.table-header-custom) {
  3327. background-color: #f8f9fb !important;
  3328. color: #333 !important;
  3329. font-weight: bold !important;
  3330. height: 50px;
  3331. }
  3332. .drag-tip {
  3333. font-size: 12px;
  3334. color: #a8abb2;
  3335. }
  3336. .table-img {
  3337. width: 140px;
  3338. height: 48px;
  3339. border-radius: 4px;
  3340. border: 1px solid #f0f0f0;
  3341. display: block;
  3342. }
  3343. .rank-box {
  3344. display: flex;
  3345. flex-direction: column;
  3346. align-items: center;
  3347. gap: 2px;
  3348. }
  3349. .rank-icon {
  3350. cursor: pointer;
  3351. color: #909399;
  3352. font-size: 16px;
  3353. transition: color 0.2s;
  3354. }
  3355. .rank-icon:hover {
  3356. color: #409eff;
  3357. }
  3358. /* 弹窗上传样式 */
  3359. .upload-placeholder {
  3360. width: 240px;
  3361. height: 82px;
  3362. border: 1px dashed #dcdfe6;
  3363. border-radius: 6px;
  3364. cursor: pointer;
  3365. overflow: hidden;
  3366. display: flex;
  3367. align-items: center;
  3368. justify-content: center;
  3369. background-color: #f5f7fa;
  3370. transition: border-color 0.2s;
  3371. }
  3372. .upload-placeholder:hover {
  3373. border-color: #409eff;
  3374. }
  3375. .form-preview-img {
  3376. width: 100%;
  3377. height: 100%;
  3378. object-fit: cover;
  3379. }
  3380. .upload-icon {
  3381. font-size: 28px;
  3382. color: #8c939d;
  3383. }
  3384. .upload-tip {
  3385. font-size: 12px;
  3386. color: #999;
  3387. margin-top: 5px;
  3388. line-height: 1.4;
  3389. }
  3390. .dialog-form-inner {
  3391. padding: 30px 40px; /* 大幅增加内边距,使其更高、更大气 */
  3392. }
  3393. .dialog-form-inner :deep(.el-form-item) {
  3394. margin-bottom: 25px; /* 增加表单项间距 */
  3395. }
  3396. .list-scroll-enter-active,
  3397. .list-scroll-leave-active {
  3398. transition: all 0.5s ease;
  3399. position: absolute;
  3400. width: 100%;
  3401. }
  3402. .list-scroll-enter-from {
  3403. transform: translateY(100%);
  3404. opacity: 0;
  3405. }
  3406. .list-scroll-leave-to {
  3407. transform: translateY(-100%);
  3408. opacity: 0;
  3409. }
  3410. /* 设置表单 */
  3411. .settings-section {
  3412. padding: 0 10px;
  3413. }
  3414. .settings-input {
  3415. max-width: 600px;
  3416. }
  3417. .color-picker-wrap {
  3418. display: flex;
  3419. align-items: center;
  3420. }
  3421. .tip-text {
  3422. font-size: 12px;
  3423. color: #999;
  3424. margin-top: 4px;
  3425. }
  3426. .color-val {
  3427. margin-left: 10px;
  3428. color: #666;
  3429. font-family: monospace;
  3430. }
  3431. /* 热词配置样式 */
  3432. .hot-words-config {
  3433. display: flex;
  3434. flex-direction: column;
  3435. gap: 10px;
  3436. width: 800px; /* 加宽配置区域 */
  3437. }
  3438. .hot-word-row {
  3439. display: flex;
  3440. align-items: center;
  3441. gap: 10px;
  3442. }
  3443. .hot-word-input-name {
  3444. width: 200px; /* 稍微加宽名称框 */
  3445. }
  3446. .hot-word-input-link {
  3447. flex: 1; /* 此时地址框会占据剩余更多空间 */
  3448. }
  3449. .add-hotword-btn {
  3450. width: fit-content;
  3451. padding: 0;
  3452. margin-top: 5px;
  3453. }
  3454. .footer-actions {
  3455. padding: 20px 40px;
  3456. border-top: 1px solid #f0f0f0;
  3457. display: flex;
  3458. gap: 15px;
  3459. flex-shrink: 0;
  3460. }
  3461. .btn-confirm {
  3462. padding: 0 30px;
  3463. }
  3464. .btn-reset {
  3465. padding: 0 30px;
  3466. }
  3467. /* 分类设置编辑区 */
  3468. .category-editor {
  3469. padding: 20px 40px;
  3470. display: flex;
  3471. flex-direction: column;
  3472. gap: 40px;
  3473. }
  3474. .category-preview-container {
  3475. padding: 20px 0;
  3476. display: flex;
  3477. justify-content: flex-start;
  3478. }
  3479. /* 图1: 仿真菜单 */
  3480. .category-menu-mockup {
  3481. width: 280px;
  3482. height: 398px;
  3483. background: #f8f9fa;
  3484. border-radius: 4px;
  3485. padding: 10px 0;
  3486. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  3487. position: relative;
  3488. z-index: 10;
  3489. overflow: visible; /* 必须可见,否则遮罩层无法伸出 */
  3490. }
  3491. /* 仿真菜单项 */
  3492. .menu-item {
  3493. height: 44px;
  3494. display: flex;
  3495. align-items: center;
  3496. padding: 0 15px;
  3497. cursor: pointer;
  3498. position: relative;
  3499. transition: all 0.1s;
  3500. background: transparent;
  3501. box-sizing: border-box;
  3502. }
  3503. .menu-item:hover {
  3504. background-color: #fff !important;
  3505. color: v-bind(categoryThemeColor);
  3506. border: 1px solid v-bind(categoryThemeColor);
  3507. border-right: none;
  3508. border-radius: 12px 0 0 12px;
  3509. z-index: 1000;
  3510. margin-left: 10px;
  3511. padding-left: 15px;
  3512. width: calc(100% - 10px);
  3513. }
  3514. .menu-icon {
  3515. width: 16px;
  3516. height: 16px;
  3517. margin-right: 15px; /* 增加间距 */
  3518. color: #999;
  3519. display: flex;
  3520. align-items: center;
  3521. justify-content: center;
  3522. flex-shrink: 0;
  3523. }
  3524. .menu-icon img {
  3525. width: 100%;
  3526. height: 100%;
  3527. object-fit: contain;
  3528. }
  3529. .menu-name {
  3530. font-size: 14px;
  3531. color: #333;
  3532. flex: 1;
  3533. white-space: nowrap;
  3534. overflow: hidden;
  3535. text-overflow: ellipsis;
  3536. }
  3537. /* 遮盖层:仅抹除中间垂直线,保留上下边框连贯 */
  3538. .menu-item:hover::after {
  3539. content: '';
  3540. position: absolute;
  3541. top: 0; /* 保持在边框内侧 */
  3542. bottom: 0; /* 保持在边框内侧 */
  3543. right: -1px; /* 贴合右边缘 */
  3544. width: 2px; /* 覆盖面板左边框 */
  3545. background: #fff;
  3546. z-index: 1001;
  3547. }
  3548. .menu-item:hover .menu-icon {
  3549. color: v-bind(categoryThemeColor);
  3550. }
  3551. .menu-name {
  3552. font-size: 14px;
  3553. color: #333;
  3554. }
  3555. .menu-item:hover .menu-name {
  3556. color: v-bind(categoryThemeColor);
  3557. font-weight: bold;
  3558. }
  3559. /* 图2: 右滑面板 */
  3560. .category-panel-mockup {
  3561. position: absolute;
  3562. left: 100%; /* 紧贴菜单项右侧 */
  3563. margin-left: 0;
  3564. top: -1px; /* 顶部边框对齐 */
  3565. width: 980px;
  3566. min-height: 480px;
  3567. background: #fff;
  3568. box-shadow: 15px 15px 40px rgba(0, 0, 0, 0.1);
  3569. border-radius: 0 12px 12px 12px;
  3570. z-index: 500;
  3571. border: 1px solid v-bind(categoryThemeColor);
  3572. display: none;
  3573. flex-direction: column;
  3574. box-sizing: border-box;
  3575. cursor: default;
  3576. }
  3577. /* 悬停时显示面板 */
  3578. .menu-item:hover .category-panel-mockup {
  3579. display: flex;
  3580. }
  3581. .panel-content {
  3582. flex: 1;
  3583. padding: 25px 30px;
  3584. position: relative; /* 为品牌位定位提供基准 */
  3585. overflow: hidden;
  3586. }
  3587. .panel-tabs {
  3588. display: flex;
  3589. gap: 15px;
  3590. margin-bottom: 25px;
  3591. padding-right: 200px; /* 为右上角品牌位留出空间 */
  3592. }
  3593. .panel-tab-item {
  3594. padding: 6px 14px;
  3595. background-color: #f5f5f5;
  3596. color: #666;
  3597. font-size: 12px;
  3598. border-radius: 4px;
  3599. cursor: pointer;
  3600. transition: all 0.2s;
  3601. }
  3602. .panel-tab-item:hover {
  3603. background-color: #f5f5f5; /* 保持浅灰或根据主题调整 */
  3604. color: v-bind(categoryThemeColor);
  3605. filter: brightness(0.95);
  3606. }
  3607. .panel-body {
  3608. display: flex;
  3609. flex-direction: column; /* 改为垂直布局,内容横向撑开 */
  3610. flex: 1;
  3611. }
  3612. .panel-main {
  3613. flex: 1;
  3614. overflow-y: auto;
  3615. max-height: 400px;
  3616. }
  3617. .category-group {
  3618. margin-bottom: 15px;
  3619. display: flex;
  3620. }
  3621. .group-title {
  3622. width: 80px;
  3623. font-size: 12px;
  3624. font-weight: bold;
  3625. color: #333;
  3626. flex-shrink: 0;
  3627. line-height: 1.6;
  3628. }
  3629. .group-items {
  3630. flex: 1;
  3631. display: flex;
  3632. flex-wrap: wrap;
  3633. gap: 6px 15px;
  3634. }
  3635. .group-item {
  3636. font-size: 12px;
  3637. color: #666;
  3638. cursor: pointer;
  3639. transition: color 0.2s;
  3640. }
  3641. .group-item:hover {
  3642. color: v-bind(categoryThemeColor);
  3643. }
  3644. /* 品牌位移至右上角 (图3) */
  3645. .panel-side {
  3646. display: none; /* 移除侧边栏布局 */
  3647. }
  3648. .brand-box {
  3649. position: absolute;
  3650. top: 25px;
  3651. right: 30px;
  3652. display: flex;
  3653. flex-direction: column;
  3654. align-items: flex-end; /* 右对齐更美观 */
  3655. z-index: 105;
  3656. }
  3657. .brand-main-title {
  3658. font-size: 20px;
  3659. font-weight: 900;
  3660. color: v-bind(categoryThemeColor);
  3661. margin-bottom: 4px;
  3662. display: flex;
  3663. align-items: baseline;
  3664. }
  3665. .brand-strong {
  3666. color: #333;
  3667. margin-left: 2px;
  3668. font-size: 20px;
  3669. }
  3670. .brand-notes {
  3671. display: flex;
  3672. gap: 5px;
  3673. color: v-bind(categoryThemeColor);
  3674. font-size: 12px;
  3675. justify-content: flex-end;
  3676. }
  3677. .note-item {
  3678. cursor: pointer;
  3679. }
  3680. .note-sep {
  3681. color: #eee;
  3682. margin: 0 2px;
  3683. }
  3684. /* 工具栏主题色设置 - 专业版 */
  3685. .theme-color-setting-pro {
  3686. display: flex;
  3687. align-items: center;
  3688. gap: 8px;
  3689. margin-right: 20px;
  3690. }
  3691. .theme-color-setting-pro .label {
  3692. font-size: 14px;
  3693. color: #333;
  3694. }
  3695. .theme-color-setting-pro .value {
  3696. font-size: 13px;
  3697. color: #666;
  3698. margin-left: 5px;
  3699. font-family: monospace;
  3700. }
  3701. /* 覆盖 el-color-picker 样式 */
  3702. :deep(.theme-color-setting-pro .el-color-picker__trigger) {
  3703. width: 28px;
  3704. height: 28px;
  3705. padding: 2px;
  3706. border-radius: 4px;
  3707. }
  3708. /* 列表样式 */
  3709. .table-icon-preview {
  3710. width: 24px;
  3711. height: 24px;
  3712. }
  3713. .tag-wrap {
  3714. display: flex;
  3715. flex-wrap: wrap;
  3716. gap: 5px;
  3717. }
  3718. .m-r-5 {
  3719. margin-right: 5px;
  3720. }
  3721. .m-t-20 {
  3722. margin-top: 20px;
  3723. }
  3724. /* 弹窗样式 */
  3725. .panel-config-section {
  3726. background: #fafafa;
  3727. padding: 15px;
  3728. border-radius: 4px;
  3729. border: 1px dashed #eee;
  3730. }
  3731. /* 移除冗余的 .config-subtitle 定义,统一使用下方全局定义的样式 */
  3732. .notes-config-list {
  3733. display: flex;
  3734. flex-direction: column;
  3735. gap: 10px;
  3736. }
  3737. .note-config-row {
  3738. display: flex;
  3739. gap: 10px;
  3740. align-items: center;
  3741. }
  3742. .field-tip {
  3743. font-size: 12px;
  3744. color: #999;
  3745. margin-top: 5px;
  3746. }
  3747. /* 正方形图标上传 */
  3748. .upload-placeholder-square {
  3749. width: 80px;
  3750. height: 80px;
  3751. border: 1px dashed #d9d9d9;
  3752. border-radius: 4px;
  3753. cursor: pointer;
  3754. display: flex;
  3755. align-items: center;
  3756. justify-content: center;
  3757. background-color: #fafafa;
  3758. transition: border-color 0.2s;
  3759. }
  3760. .upload-placeholder-square:hover {
  3761. border-color: v-bind(categoryThemeColor);
  3762. }
  3763. .upload-placeholder-square .upload-icon {
  3764. font-size: 20px;
  3765. color: #999;
  3766. }
  3767. .form-preview-img-square {
  3768. width: 16px;
  3769. height: 16px;
  3770. object-fit: contain;
  3771. }
  3772. /* 头部分类样式 */
  3773. .header-category-editor {
  3774. padding: 20px 40px;
  3775. display: flex;
  3776. flex-direction: column;
  3777. gap: 40px;
  3778. }
  3779. .header-preview-outer {
  3780. width: 100%;
  3781. background: #f8f9fa;
  3782. padding: 40px 20px;
  3783. border-radius: 8px;
  3784. display: block; /* 改为 block 以便支持内部滚动 */
  3785. overflow-x: auto; /* 允许内部滚动,防止溢出 */
  3786. box-sizing: border-box;
  3787. }
  3788. .header-preview-box {
  3789. width: 1350px; /* 强制保持 1350px 宽度以高度还原设计 */
  3790. height: 60px;
  3791. background: #fff;
  3792. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  3793. border-radius: 4px;
  3794. display: flex;
  3795. align-items: center;
  3796. padding: 0 40px; /* 为箭头预留空间 */
  3797. margin: 0 auto; /* 在容器足够大时居中 */
  3798. flex-shrink: 0; /* 禁止缩小 */
  3799. position: relative;
  3800. overflow: hidden; /* 关键:确保内部超出部分被剪裁,由滚动容器处理 */
  3801. }
  3802. .header-nav-scroll {
  3803. flex: 1;
  3804. overflow-x: auto;
  3805. scrollbar-width: none;
  3806. display: flex;
  3807. align-items: center;
  3808. scroll-behavior: smooth;
  3809. }
  3810. .header-nav-scroll::-webkit-scrollbar {
  3811. display: none;
  3812. }
  3813. .header-nav-list {
  3814. display: flex;
  3815. align-items: center;
  3816. gap: 30px;
  3817. flex-shrink: 0;
  3818. }
  3819. .nav-arrow {
  3820. position: absolute;
  3821. top: 50%;
  3822. transform: translateY(-50%);
  3823. width: 24px;
  3824. height: 24px;
  3825. background: #eee;
  3826. border-radius: 50%;
  3827. display: flex;
  3828. align-items: center;
  3829. justify-content: center;
  3830. cursor: pointer;
  3831. z-index: 10;
  3832. color: #666;
  3833. transition: all 0.2s;
  3834. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  3835. }
  3836. .nav-arrow:hover {
  3837. background: #e0e0e0;
  3838. color: #333;
  3839. }
  3840. .left-arrow {
  3841. left: 8px;
  3842. }
  3843. .right-arrow {
  3844. right: 8px;
  3845. }
  3846. .header-nav-item {
  3847. display: flex;
  3848. align-items: center;
  3849. gap: 8px;
  3850. cursor: pointer;
  3851. white-space: nowrap;
  3852. flex-shrink: 0;
  3853. }
  3854. .header-nav-item .item-icon {
  3855. width: 22px;
  3856. height: 22px;
  3857. display: flex;
  3858. align-items: center;
  3859. justify-content: center;
  3860. }
  3861. .header-nav-item .item-icon img {
  3862. width: 100%;
  3863. height: 100%;
  3864. object-fit: contain;
  3865. }
  3866. .header-nav-item .item-text {
  3867. font-size: 16px;
  3868. font-weight: bold;
  3869. color: #333;
  3870. transition: color 0.2s;
  3871. }
  3872. .header-nav-item:hover .item-text {
  3873. color: var(--hover-color);
  3874. }
  3875. /* 广告模块样式 */
  3876. .ad-preview-grid {
  3877. display: flex;
  3878. flex-wrap: nowrap;
  3879. gap: 20px;
  3880. max-width: 1600px;
  3881. margin: 0;
  3882. }
  3883. .ad-item {
  3884. background: #f8f9fa;
  3885. border-radius: 12px;
  3886. padding: 15px;
  3887. position: relative;
  3888. overflow: hidden;
  3889. box-sizing: border-box;
  3890. }
  3891. .ad-item:hover .ad-hover-mask {
  3892. display: flex;
  3893. }
  3894. .ad-hover-mask {
  3895. position: absolute;
  3896. top: 0;
  3897. left: 0;
  3898. right: 0;
  3899. bottom: 0;
  3900. background: rgba(0, 0, 0, 0.4);
  3901. display: none;
  3902. align-items: center;
  3903. justify-content: center;
  3904. z-index: 100;
  3905. }
  3906. .ad-header {
  3907. display: flex;
  3908. justify-content: space-between;
  3909. align-items: baseline;
  3910. margin-bottom: 12px;
  3911. }
  3912. .ad-title-main {
  3913. font-size: 18px;
  3914. font-weight: 800;
  3915. color: #333;
  3916. }
  3917. .ad-title-sub {
  3918. font-size: 13px;
  3919. }
  3920. .ad-title-sub.orange {
  3921. color: #f58220;
  3922. }
  3923. .ad-title-sub.gray {
  3924. color: #999;
  3925. }
  3926. /* Ad 1: 百亿补贴 */
  3927. .ad-products-subsidy {
  3928. display: flex;
  3929. gap: 15px;
  3930. }
  3931. .ad-subsidy .product-item {
  3932. flex: 1;
  3933. text-align: center;
  3934. }
  3935. .ad-subsidy .product-img {
  3936. width: 94px;
  3937. height: 94px;
  3938. background: #fff;
  3939. border-radius: 8px;
  3940. margin-bottom: 8px;
  3941. overflow: hidden;
  3942. }
  3943. .ad-subsidy .product-img img {
  3944. width: 100%;
  3945. height: 100%;
  3946. object-fit: contain;
  3947. }
  3948. .ad-subsidy .product-price {
  3949. color: #e60012;
  3950. font-weight: bold;
  3951. font-size: 15px;
  3952. }
  3953. .ad-ranking {
  3954. padding: 12px !important;
  3955. }
  3956. .ad-ranking .ad-header {
  3957. margin-bottom: 8px;
  3958. }
  3959. /* Ad 2: 榜单 */
  3960. .ad-products-ranking {
  3961. display: flex;
  3962. gap: 8px;
  3963. flex: 1;
  3964. }
  3965. .ranking-item {
  3966. flex: 1;
  3967. background: #fff;
  3968. border-radius: 8px;
  3969. padding: 6px 5px 0;
  3970. position: relative;
  3971. text-align: center;
  3972. display: flex;
  3973. flex-direction: column;
  3974. align-items: center;
  3975. overflow: hidden;
  3976. height: 135px; /* 高度调小一点 */
  3977. }
  3978. .ranking-badge {
  3979. display: inline-block;
  3980. background: #fff3e5;
  3981. color: #f58220;
  3982. font-size: 11px;
  3983. padding: 2px 10px;
  3984. border-radius: 20px;
  3985. white-space: nowrap;
  3986. margin-bottom: 4px;
  3987. transform: scale(0.9); /* 略微缩小标签以腾出空间 */
  3988. }
  3989. .ranking-item .product-img {
  3990. width: 80px; /* 略微缩小图片从 84 降至 80 */
  3991. height: 80px;
  3992. margin-bottom: 4px;
  3993. display: flex;
  3994. align-items: center;
  3995. justify-content: center;
  3996. }
  3997. .ranking-item .product-img img {
  3998. width: 100%;
  3999. height: 100%;
  4000. object-fit: contain;
  4001. }
  4002. .ranking-footer {
  4003. background: #fff1f1;
  4004. color: #e60012;
  4005. font-size: 12px;
  4006. padding: 5px 0;
  4007. font-weight: bold;
  4008. width: 100%;
  4009. margin-top: auto;
  4010. line-height: 1.2;
  4011. }
  4012. /* Ad 3: 品牌 */
  4013. .ad-brands-content {
  4014. display: flex;
  4015. gap: 10px;
  4016. }
  4017. .brand-item {
  4018. flex: 1;
  4019. text-align: center;
  4020. }
  4021. .brand-logo {
  4022. width: 50px;
  4023. height: 50px;
  4024. margin: 0 auto 8px;
  4025. background: #fff;
  4026. border-radius: 4px;
  4027. padding: 5px;
  4028. }
  4029. .brand-logo img {
  4030. width: 100%;
  4031. height: 100%;
  4032. object-fit: contain;
  4033. }
  4034. .brand-name {
  4035. font-size: 11px;
  4036. color: #0071bc;
  4037. margin-bottom: 5px;
  4038. height: 20px;
  4039. line-height: 20px;
  4040. white-space: nowrap;
  4041. overflow: hidden;
  4042. text-overflow: ellipsis;
  4043. width: 100%;
  4044. }
  4045. .brand-tag-btn {
  4046. display: inline-block;
  4047. padding: 2px 10px;
  4048. border: 1px solid #e60012;
  4049. color: #e60012;
  4050. border-radius: 10px;
  4051. font-size: 11px;
  4052. }
  4053. /* Ad 4 & 5: 精选/新品 */
  4054. .ad-products-selection {
  4055. display: flex;
  4056. gap: 10px;
  4057. }
  4058. .selection-item {
  4059. flex: 1;
  4060. text-align: center;
  4061. }
  4062. .selection-item .product-img {
  4063. width: 84px;
  4064. height: 84px;
  4065. background: #fff;
  4066. border-radius: 8px;
  4067. margin-bottom: 5px;
  4068. }
  4069. .selection-item .product-img img {
  4070. width: 100%;
  4071. height: 100%;
  4072. object-fit: contain;
  4073. }
  4074. .product-price-row {
  4075. display: flex;
  4076. align-items: baseline;
  4077. justify-content: flex-start;
  4078. gap: 2px;
  4079. }
  4080. .product-price-row.center {
  4081. justify-content: center;
  4082. }
  4083. .p-unit {
  4084. font-size: 12px;
  4085. color: #e60012;
  4086. font-weight: bold;
  4087. }
  4088. .p-val {
  4089. font-size: 16px;
  4090. color: #e60012;
  4091. font-weight: 800;
  4092. }
  4093. .p-tag {
  4094. background: #3fa9f5;
  4095. color: #fff;
  4096. font-size: 10px;
  4097. padding: 0 4px;
  4098. border-radius: 2px;
  4099. margin-left: 2px;
  4100. }
  4101. /* 选择弹窗样式 */
  4102. .select-item-row {
  4103. display: flex;
  4104. align-items: center;
  4105. padding: 12px;
  4106. border: 1px solid #eee;
  4107. border-radius: 8px;
  4108. margin-bottom: 10px;
  4109. cursor: pointer;
  4110. transition: all 0.2s;
  4111. }
  4112. .select-item-row:hover {
  4113. border-color: #409eff;
  4114. background: #f0f7ff;
  4115. }
  4116. .select-item-row.active {
  4117. border-color: #409eff;
  4118. background: #f0f7ff;
  4119. }
  4120. .select-item-img {
  4121. width: 80px;
  4122. height: 80px;
  4123. object-fit: contain;
  4124. margin-right: 20px;
  4125. background: #f9f9f9;
  4126. border-radius: 4px;
  4127. }
  4128. .select-item-info {
  4129. flex: 1;
  4130. }
  4131. .select-item-name {
  4132. font-size: 14px;
  4133. color: #333;
  4134. margin-bottom: 4px;
  4135. }
  4136. .select-item-price {
  4137. color: #e60012;
  4138. font-weight: bold;
  4139. }
  4140. .select-pagination {
  4141. margin-top: 30px;
  4142. display: flex;
  4143. justify-content: center;
  4144. padding-bottom: 20px;
  4145. }
  4146. .select-item-id {
  4147. font-size: 11px;
  4148. color: #999;
  4149. margin-top: 2px;
  4150. }
  4151. /* 场景方案预览样式 */
  4152. .scenario-preview-outer {
  4153. width: 100%;
  4154. overflow-x: auto;
  4155. padding: 10px 0;
  4156. }
  4157. .scenario-preview-box-clean {
  4158. width: 1600px;
  4159. max-width: 100%;
  4160. height: 158px;
  4161. background: var(--s-theme-color, #66e0a3);
  4162. border-radius: 16px; /* 加大圆角 */
  4163. display: flex;
  4164. align-items: center;
  4165. padding: 0 20px 0 32px; /* 减小左边距使标题区左移 */
  4166. box-sizing: border-box;
  4167. position: relative;
  4168. overflow: hidden;
  4169. }
  4170. .s-title-group {
  4171. display: flex;
  4172. align-items: center;
  4173. gap: 12px;
  4174. margin-bottom: 12px;
  4175. white-space: nowrap; /* 强制不换行,向后追加 */
  4176. }
  4177. .s-main-title {
  4178. font-size: 24px; /* 调整为 24px */
  4179. font-weight: 900;
  4180. color: #fff;
  4181. flex-shrink: 0; /* 确保不收缩 */
  4182. }
  4183. .s-sub-title-inline {
  4184. font-size: 24px; /* 调整为 24px */
  4185. font-weight: 900;
  4186. color: #fff;
  4187. flex-shrink: 0; /* 确保不收缩 */
  4188. }
  4189. .s-btn-wrap {
  4190. margin-top: 15px;
  4191. }
  4192. .s-btn-premium {
  4193. display: inline-flex;
  4194. align-items: center;
  4195. background-color: #fff;
  4196. color: #e60012;
  4197. font-size: 14px;
  4198. font-weight: bold;
  4199. padding: 6px 20px;
  4200. border-radius: 20px;
  4201. }
  4202. .scenario-card-premium {
  4203. width: 288px;
  4204. height: 130px;
  4205. border-radius: 8px;
  4206. display: flex;
  4207. flex-direction: column;
  4208. position: relative;
  4209. }
  4210. .card-top-header {
  4211. height: 42px; /* 固定高度,确保下方图片间距准确 */
  4212. display: flex;
  4213. justify-content: space-between;
  4214. align-items: center;
  4215. padding: 0 12px;
  4216. box-sizing: border-box;
  4217. }
  4218. .card-titles-group {
  4219. display: flex;
  4220. align-items: center;
  4221. gap: 8px;
  4222. }
  4223. .card-main-title {
  4224. font-size: 15px;
  4225. font-weight: bold;
  4226. }
  4227. .card-sub-title {
  4228. font-size: 13px;
  4229. font-weight: bold;
  4230. }
  4231. .card-arrow-icon {
  4232. width: 18px;
  4233. height: 18px;
  4234. border-radius: 50%;
  4235. color: #fff;
  4236. display: flex;
  4237. align-items: center;
  4238. justify-content: center;
  4239. font-size: 10px;
  4240. }
  4241. .card-image-content {
  4242. width: 272px;
  4243. height: 80px;
  4244. margin: 0 8px 8px 8px; /* 左右下保留相同间距 (8px) */
  4245. border-radius: 4px;
  4246. overflow: hidden;
  4247. }
  4248. .card-image-content img {
  4249. width: 100%;
  4250. height: 100%;
  4251. object-fit: cover;
  4252. }
  4253. /* 响应式逻辑:优先隐藏最后一个方案卡片,再隐藏副标题 */
  4254. @media (max-width: 1680px) {
  4255. .scenario-preview-box-clean {
  4256. width: 1300px;
  4257. }
  4258. .hidden-card-fourth {
  4259. display: none !important;
  4260. }
  4261. }
  4262. @media (max-width: 1400px) {
  4263. .scenario-preview-box-clean {
  4264. width: 1000px;
  4265. }
  4266. .s-sub-title-inline {
  4267. display: none !important;
  4268. }
  4269. }
  4270. .scenario-header-left {
  4271. width: 320px;
  4272. flex-shrink: 0;
  4273. display: flex;
  4274. flex-direction: column;
  4275. }
  4276. .scenario-cards-wrap {
  4277. flex: 1;
  4278. display: flex;
  4279. justify-content: flex-end;
  4280. gap: 12px; /* 间距调小 */
  4281. }
  4282. .scenario-card {
  4283. width: 288px;
  4284. height: 130px;
  4285. background-color: #fff;
  4286. border-radius: 12px;
  4287. padding: 12px;
  4288. box-sizing: border-box;
  4289. display: flex;
  4290. flex-direction: column;
  4291. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  4292. cursor: pointer;
  4293. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  4294. border: 1px solid rgba(0, 0, 0, 0.02);
  4295. }
  4296. .scenario-card:hover {
  4297. transform: translateY(-8px);
  4298. box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
  4299. border-color: rgba(102, 224, 163, 0.3);
  4300. }
  4301. .card-top {
  4302. display: flex;
  4303. justify-content: space-between;
  4304. align-items: center;
  4305. margin-bottom: 8px;
  4306. padding: 0 5px;
  4307. }
  4308. .card-titles {
  4309. display: flex;
  4310. align-items: center;
  4311. gap: 8px;
  4312. }
  4313. .card-main-title {
  4314. font-size: 18px;
  4315. font-weight: bold;
  4316. }
  4317. .card-sub-title {
  4318. font-size: 12px;
  4319. }
  4320. .card-arrow {
  4321. width: 18px;
  4322. height: 18px;
  4323. border-radius: 50%;
  4324. color: #fff;
  4325. display: flex;
  4326. align-items: center;
  4327. justify-content: center;
  4328. font-size: 10px;
  4329. }
  4330. .card-img {
  4331. width: 272px;
  4332. height: 80px;
  4333. border-radius: 6px;
  4334. overflow: hidden;
  4335. background-color: #f5f7fa;
  4336. }
  4337. .card-img img {
  4338. width: 100%;
  4339. height: 100%;
  4340. object-fit: cover;
  4341. }
  4342. /* 响应式调整 */
  4343. @media (max-width: 1500px) {
  4344. .scenario-preview-box {
  4345. width: 1300px;
  4346. }
  4347. .hidden-card-fourth {
  4348. display: none !important;
  4349. }
  4350. }
  4351. @media (max-width: 1200px) {
  4352. .scenario-preview-box {
  4353. width: 1000px;
  4354. }
  4355. .s-sub-title {
  4356. display: none !important;
  4357. }
  4358. }
  4359. /* 广告模块弹窗精修 */
  4360. .ad-setup-dialog :deep(.el-dialog__body) {
  4361. padding: 30px 40px;
  4362. max-height: 700px;
  4363. overflow-y: auto;
  4364. }
  4365. .brand-name-display {
  4366. font-size: 15px;
  4367. font-weight: bold;
  4368. color: #333;
  4369. }
  4370. .ad-setup-dialog :deep(.el-table__row) {
  4371. height: 90px; /* 增加行高,大气美观 */
  4372. }
  4373. .config-subtitle {
  4374. font-size: 15px;
  4375. font-weight: bold;
  4376. color: #333;
  4377. margin: 25px 0 15px;
  4378. display: flex;
  4379. align-items: center;
  4380. line-height: 1;
  4381. }
  4382. .config-subtitle::before {
  4383. content: '';
  4384. width: 3px;
  4385. height: 14px;
  4386. background: #e60012;
  4387. margin-right: 8px;
  4388. }
  4389. /* 快捷入口预览样式 */
  4390. .quick-entry-preview-outer {
  4391. display: flex;
  4392. flex-direction: column;
  4393. align-items: flex-start;
  4394. padding: 20px 0;
  4395. }
  4396. .qe-mockup-card {
  4397. width: 230px;
  4398. height: 167px;
  4399. background: #fff;
  4400. border-radius: 12px;
  4401. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  4402. padding: 12px 16px;
  4403. box-sizing: border-box;
  4404. display: flex;
  4405. flex-direction: column;
  4406. overflow: hidden;
  4407. position: relative;
  4408. }
  4409. .qe-card-header {
  4410. display: flex;
  4411. justify-content: space-between;
  4412. align-items: center;
  4413. margin-bottom: 12px;
  4414. }
  4415. .qe-card-title {
  4416. font-size: 16px;
  4417. font-weight: bold;
  4418. color: #333;
  4419. }
  4420. .qe-header-arrow {
  4421. font-size: 12px;
  4422. color: #999;
  4423. }
  4424. .qe-grid-container {
  4425. width: 198px;
  4426. height: 108px;
  4427. margin: 0 auto;
  4428. position: relative;
  4429. overflow: hidden;
  4430. }
  4431. .qe-grid-wrapper {
  4432. display: flex;
  4433. transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  4434. height: 100%;
  4435. }
  4436. .qe-grid-page {
  4437. width: 198px;
  4438. flex-shrink: 0;
  4439. display: grid;
  4440. grid-template-columns: repeat(4, 1fr);
  4441. grid-template-rows: repeat(2, 1fr);
  4442. gap: 12px 0;
  4443. }
  4444. .qe-item {
  4445. display: flex;
  4446. flex-direction: column;
  4447. align-items: center;
  4448. gap: 4px;
  4449. }
  4450. .qe-icon-wrap {
  4451. width: 24px;
  4452. height: 24px;
  4453. position: relative;
  4454. display: flex;
  4455. align-items: center;
  4456. justify-content: center;
  4457. }
  4458. .qe-icon-img {
  4459. width: 24px;
  4460. height: 24px;
  4461. object-fit: contain;
  4462. }
  4463. .qe-icon-placeholder {
  4464. font-size: 20px;
  4465. color: #666;
  4466. }
  4467. .qe-tag-bubble {
  4468. position: absolute;
  4469. top: -8px;
  4470. right: -15px;
  4471. background: #ff4d4f;
  4472. color: #fff;
  4473. font-size: 9px; /* 精确 9px */
  4474. padding: 1px 5px;
  4475. border-radius: 10px;
  4476. white-space: nowrap;
  4477. transform: scale(0.9);
  4478. z-index: 100; /* 提升至最高图层 */
  4479. }
  4480. .qe-name {
  4481. font-size: 11px; /* 精确 11px */
  4482. color: #333;
  4483. text-align: center;
  4484. white-space: nowrap;
  4485. overflow: hidden;
  4486. text-overflow: ellipsis;
  4487. width: 100%;
  4488. }
  4489. .qe-nav-btns {
  4490. position: absolute;
  4491. top: 0;
  4492. left: 0;
  4493. right: 0;
  4494. bottom: 0;
  4495. pointer-events: none;
  4496. }
  4497. .qe-nav-btn {
  4498. position: absolute;
  4499. top: 50%;
  4500. transform: translateY(-50%);
  4501. width: 22px;
  4502. height: 22px;
  4503. background: rgba(255, 255, 255, 0.9);
  4504. border-radius: 50%;
  4505. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  4506. display: flex;
  4507. align-items: center;
  4508. justify-content: center;
  4509. cursor: pointer;
  4510. pointer-events: auto;
  4511. color: #666;
  4512. z-index: 110; /* 提升至最高图层,确保不被内容遮挡 */
  4513. }
  4514. .qe-nav-btn:hover {
  4515. background: #fff;
  4516. color: #e60012;
  4517. }
  4518. .qe-nav-btn.prev {
  4519. left: -10px;
  4520. }
  4521. .qe-nav-btn.next {
  4522. right: -10px;
  4523. }
  4524. /* 快捷入口编辑器特定样式 */
  4525. .quick-entry-editor {
  4526. display: flex;
  4527. flex-direction: column;
  4528. padding: 20px 40px;
  4529. gap: 30px;
  4530. }
  4531. .config-form-inline {
  4532. background: #fcfdfe;
  4533. padding: 20px;
  4534. border-radius: 8px;
  4535. border: 1px solid #eef2f6;
  4536. }
  4537. .table-icon-cell {
  4538. display: flex;
  4539. align-items: center;
  4540. justify-content: center;
  4541. }
  4542. /* 推荐设置编辑区容器 */
  4543. .recommend-editor-container {
  4544. padding: 24px;
  4545. border-bottom: none !important;
  4546. }
  4547. /* 推荐设置预览样式 */
  4548. .recommend-preview-outer {
  4549. width: 100%;
  4550. padding: 10px 0 20px;
  4551. position: relative;
  4552. }
  4553. .recommend-preview-container {
  4554. width: 1600px !important; /* 严格按照要求设置背景尺寸 */
  4555. height: 88px !important;
  4556. position: relative;
  4557. background: #fff;
  4558. border-radius: 8px;
  4559. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  4560. overflow: hidden;
  4561. margin-bottom: 10px;
  4562. }
  4563. .recommend-preview-box {
  4564. width: 100% !important;
  4565. height: 100% !important;
  4566. display: flex;
  4567. align-items: center;
  4568. padding: 0 40px;
  4569. box-sizing: border-box;
  4570. overflow-x: auto;
  4571. scroll-behavior: smooth;
  4572. -ms-overflow-style: none;
  4573. scrollbar-width: none;
  4574. }
  4575. .recommend-preview-box::-webkit-scrollbar {
  4576. display: none;
  4577. }
  4578. .recommend-item {
  4579. display: flex;
  4580. align-items: center;
  4581. padding: 10px 24px;
  4582. margin: 0 4px;
  4583. cursor: pointer;
  4584. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  4585. height: 60px;
  4586. border-radius: 8px;
  4587. flex-shrink: 0;
  4588. }
  4589. .recommend-item:hover {
  4590. background: #f5f5f5;
  4591. }
  4592. .recommend-item.active {
  4593. background: transparent;
  4594. }
  4595. .recommend-icon {
  4596. width: 32px;
  4597. height: 32px;
  4598. margin-right: 12px;
  4599. flex-shrink: 0;
  4600. display: flex;
  4601. align-items: center;
  4602. justify-content: center;
  4603. }
  4604. .table-icon-preview {
  4605. width: 40px;
  4606. height: 40px;
  4607. background: #f5f5f5;
  4608. border-radius: 4px;
  4609. display: flex;
  4610. align-items: center;
  4611. justify-content: center;
  4612. margin: 0 auto;
  4613. overflow: hidden;
  4614. }
  4615. .row-icon {
  4616. width: 100%;
  4617. height: 100%;
  4618. object-fit: contain;
  4619. }
  4620. .recommend-text {
  4621. display: flex;
  4622. flex-direction: column;
  4623. white-space: nowrap;
  4624. }
  4625. .r-main-title {
  4626. font-size: 16px;
  4627. font-weight: bold;
  4628. line-height: 1.4;
  4629. white-space: nowrap;
  4630. }
  4631. .r-sub-title {
  4632. font-size: 12px;
  4633. color: #999;
  4634. margin-top: 2px;
  4635. white-space: nowrap;
  4636. }
  4637. .recommend-nav-btn {
  4638. position: absolute;
  4639. top: 50%;
  4640. transform: translateY(-50%);
  4641. width: 32px;
  4642. height: 32px;
  4643. background: rgba(255, 255, 255, 0.9);
  4644. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  4645. border-radius: 50%;
  4646. display: flex;
  4647. align-items: center;
  4648. justify-content: center;
  4649. cursor: pointer;
  4650. color: #666;
  4651. z-index: 100;
  4652. transition: all 0.2s;
  4653. border: 1px solid #eee;
  4654. }
  4655. .recommend-nav-btn:hover {
  4656. color: var(--r-theme-color);
  4657. background: #fff;
  4658. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  4659. }
  4660. .recommend-nav-btn.prev {
  4661. left: 10px;
  4662. }
  4663. .recommend-nav-btn.next {
  4664. right: 10px;
  4665. }
  4666. /* 修复色块标签换行 */
  4667. .config-section-standard :deep(.el-form-item__label) {
  4668. white-space: nowrap;
  4669. }
  4670. /* 去除底部冗余线条 */
  4671. .recommend-editor-container :deep(.el-table__inner-wrapper::before),
  4672. .recommend-editor-container :deep(.el-table--border::after),
  4673. .recommend-editor-container :deep(.el-table--group::after),
  4674. .recommend-editor-container :deep(.el-table::before) {
  4675. display: none !important;
  4676. }
  4677. .recommend-icon img {
  4678. width: 100%;
  4679. height: 100%;
  4680. object-fit: contain;
  4681. background: #f2f2f2;
  4682. border-radius: 4px;
  4683. }
  4684. /* 已选商品管理弹窗增强 */
  4685. .selected-products-header {
  4686. display: flex;
  4687. justify-content: space-between;
  4688. align-items: center;
  4689. margin-bottom: 20px;
  4690. padding: 10px 0;
  4691. }
  4692. .selected-products-header .right-info {
  4693. font-size: 14px;
  4694. color: #666;
  4695. }
  4696. .selected-products-header .count {
  4697. color: #e60012;
  4698. font-weight: 800;
  4699. margin: 0 4px;
  4700. }
  4701. .price-text {
  4702. color: #e60012;
  4703. font-weight: bold;
  4704. }
  4705. .empty-placeholder {
  4706. padding: 60px 0;
  4707. }
  4708. /* 商品多选抽屉样式 */
  4709. .product-selection-drawer :deep(.el-drawer__body) {
  4710. padding: 0;
  4711. background: #fcfdfe;
  4712. }
  4713. .drawer-content-wrapper {
  4714. display: flex;
  4715. flex-direction: column;
  4716. height: 100%;
  4717. }
  4718. .drawer-search-bar {
  4719. padding: 20px;
  4720. background: #fff;
  4721. border-bottom: 1px solid #f0f2f5;
  4722. }
  4723. .drawer-stat-bar {
  4724. padding: 12px 20px;
  4725. background: #e6f7ff;
  4726. border: 1px solid #91d5ff;
  4727. color: #1890ff;
  4728. font-size: 13px;
  4729. display: flex;
  4730. align-items: center;
  4731. gap: 8px;
  4732. margin: 15px 20px 0;
  4733. border-radius: 4px;
  4734. }
  4735. .drawer-stat-bar .highlight {
  4736. font-weight: bold;
  4737. font-size: 16px;
  4738. margin: 0 2px;
  4739. }
  4740. .drawer-content-wrapper :deep(.el-table) {
  4741. margin: 15px 20px;
  4742. width: auto !important;
  4743. }
  4744. .drawer-product-info {
  4745. display: flex;
  4746. align-items: center;
  4747. gap: 12px;
  4748. }
  4749. .drawer-product-info .mini-img {
  4750. width: 50px;
  4751. height: 50px;
  4752. border-radius: 4px;
  4753. border: 1px solid #f0f0f0;
  4754. flex-shrink: 0;
  4755. }
  4756. .drawer-product-info .detail {
  4757. flex: 1;
  4758. overflow: hidden;
  4759. }
  4760. .drawer-product-info .name {
  4761. font-size: 13px;
  4762. color: #333;
  4763. line-height: 1.4;
  4764. margin-bottom: 4px;
  4765. display: -webkit-box;
  4766. -webkit-line-clamp: 2;
  4767. -webkit-box-orient: vertical;
  4768. overflow: hidden;
  4769. }
  4770. .drawer-product-info .id {
  4771. font-size: 11px;
  4772. color: #999;
  4773. }
  4774. .drawer-product-info .price {
  4775. color: #e60012;
  4776. font-weight: bold;
  4777. margin-top: 2px;
  4778. }
  4779. .drawer-pagination {
  4780. padding: 20px;
  4781. display: flex;
  4782. justify-content: center;
  4783. background: #fff;
  4784. border-top: 1px solid #f0f2f5;
  4785. }
  4786. .drawer-footer-actions {
  4787. display: flex;
  4788. justify-content: flex-end;
  4789. gap: 12px;
  4790. padding: 20px;
  4791. }
  4792. .m-l-10 {
  4793. margin-left: 10px;
  4794. }
  4795. </style>