index.vue 162 KB

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