videovip.vue 177 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220
  1. <template>
  2. <view class="content ">
  3. <view class="notice-box" v-if="isLogin&&isAddKf==1">
  4. <view class="notice-marquee-wrap">
  5. <view class="notice-marquee-track">
  6. <view class="notice-text"><image src="/static/images/course/notice.png"></image>{{notice || '央广网独家溯源&nbsp;&nbsp;全品类100%经过国标检测'}}</view>
  7. <view class="notice-text"><image src="/static/images/course/notice.png"></image>{{notice || '央广网独家溯源&nbsp;&nbsp;全品类100%经过国标检测'}}</view>
  8. <!-- <view class="notice-text"><image src="/static/images/notice.png"></image>央广网独家溯源&nbsp;&nbsp;全品类100%经过国标检测</view> -->
  9. </view>
  10. </view>
  11. </view>
  12. <view class="video-box" :style="{'height':isShu?'calc(100vh - 264rpx)':'420rpx'}" >
  13. <image v-if="!isLogin || isAddKf!=1" class="video-poster" :src="courseInfo.imgUrl" mode="aspectFill">
  14. </image>
  15. <view
  16. v-else
  17. class="video-container videovip-video-container"
  18. :style="{'height':isShu?'100%':'420rpx'}"
  19. :videovipVideoConfig="videovipVideoConfig"
  20. :change:videovipVideoConfig="xgplayer.initJs"
  21. :videoPlayStatus="videoPlayStatus"
  22. :change:videoPlayStatus="xgplayer.changeVideo"
  23. :exitFullscreen="exitFullscreen"
  24. :change:exitFullscreen="xgplayer.handleExitFullscreen"
  25. >
  26. <view
  27. v-if="videoUrl"
  28. id="video-content-box"
  29. class="videovip-xg-player"
  30. :style="{'width':'100%','height':isShu?'100%':'420rpx'}"
  31. ></view>
  32. <view v-if="remainTime > 0" class="progress-countdown videovip-video-overlay">
  33. <view class="progress-title">完课积分</view>
  34. <view class="progress-text">
  35. <view class="progress-text-label">倒计时</view>
  36. <view style="font-size: 13px;">{{ formattedCountdown.hours||'00' }}:{{ formattedCountdown.minutes||'00' }}:{{ formattedCountdown.seconds||"00" }}</view>
  37. </view>
  38. </view>
  39. <!-- 虚拟下单跑马灯:视频顶层覆盖,向上滚动淡出切换 -->
  40. <view class="vip-order-cover-wrap vip-order-cover-wrap--triple videovip-video-overlay" v-if="showFakeMarqueeOnVideoCover && isTripleMarqueeMode && marqueeTriplePayloads.length">
  41. <view class="vip-order-cover-triple-panel vip-order-cover-triple-current" :class="[marqueeTripleSwitching ? 'vip-order-triple-leave' : '']">
  42. <view
  43. class="vip-order-cover-item vip-order-cover-triple-item"
  44. v-for="(row, idx) in marqueeTriplePayloads"
  45. :key="'triple-current-' + idx + '-' + row.nickname + row.timeOffset"
  46. :class="[row.type === 'self' ? 'vip-order-cover-self' : '']"
  47. >
  48. <view class="vip-order-cover-content" v-if="row.type === 'virtual' || row.type === 'self'">
  49. <view class="vip-order-seg vip-order-seg-user">{{ row.nickname }}{{ row.maskedId }}</view>
  50. <view class="vip-order-seg vip-order-seg-time">{{ row.timeOffset }}{{ row.suffix }}</view>
  51. </view>
  52. <view class="vip-order-cover-content" v-else>
  53. {{ row.fullText }}
  54. </view>
  55. </view>
  56. </view>
  57. <view v-if="marqueeTripleSwitching && marqueeTripleIncomingPayloads.length" class="vip-order-cover-triple-panel vip-order-cover-triple-incoming vip-order-triple-enter">
  58. <view
  59. class="vip-order-cover-item vip-order-cover-triple-item"
  60. v-for="(row, idx) in marqueeTripleIncomingPayloads"
  61. :key="'triple-incoming-' + idx + '-' + row.nickname + row.timeOffset"
  62. :class="[row.type === 'self' ? 'vip-order-cover-self' : '']"
  63. >
  64. <view class="vip-order-cover-content" v-if="row.type === 'virtual' || row.type === 'self'">
  65. <view class="vip-order-seg vip-order-seg-user">{{ row.nickname }}{{ row.maskedId }}</view>
  66. <view class="vip-order-seg vip-order-seg-time">{{ row.timeOffset }}{{ row.suffix }}</view>
  67. </view>
  68. <view class="vip-order-cover-content" v-else>
  69. {{ row.fullText }}
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. <view class="vip-order-cover-wrap videovip-video-overlay" v-else-if="showFakeMarqueeOnVideoCover && marqueeDisplayPayload">
  75. <view
  76. class="vip-order-cover-item vip-order-cover-current"
  77. :class="[
  78. marqueeSwitching ? 'vip-order-leave' : '',
  79. marqueeDisplaySelf ? 'vip-order-cover-self' : ''
  80. ]"
  81. >
  82. <view class="vip-order-cover-content" v-if="marqueeDisplayPayload.type === 'virtual' || marqueeDisplayPayload.type === 'self'">
  83. <view class="vip-order-seg vip-order-seg-user">{{ marqueeDisplayPayload.nickname }}{{ marqueeDisplayPayload.maskedId }}</view>
  84. <view class="vip-order-seg vip-order-seg-time">{{ marqueeDisplayPayload.timeOffset }}{{ marqueeDisplayPayload.suffix }}</view>
  85. </view>
  86. <view class="vip-order-cover-content" v-else>
  87. {{ marqueeDisplayPayload.fullText }}
  88. </view>
  89. </view>
  90. <view
  91. v-if="marqueeSwitching && marqueeIncomingPayload"
  92. class="vip-order-cover-item vip-order-cover-incoming"
  93. :class="[
  94. 'vip-order-enter',
  95. marqueeIncomingSelf ? 'vip-order-cover-self' : ''
  96. ]"
  97. >
  98. <view class="vip-order-cover-content" v-if="marqueeIncomingPayload.type === 'virtual' || marqueeIncomingPayload.type === 'self'">
  99. <view class="vip-order-seg vip-order-seg-user">{{ marqueeIncomingPayload.nickname }}{{ marqueeIncomingPayload.maskedId }}</view>
  100. <view class="vip-order-seg vip-order-seg-time">{{ marqueeIncomingPayload.timeOffset }}{{ marqueeIncomingPayload.suffix }}</view>
  101. </view>
  102. <view class="vip-order-cover-content" v-else>
  103. {{ marqueeIncomingPayload.fullText }}
  104. </view>
  105. </view>
  106. </view>
  107. <view
  108. class="goods-cover videovip-video-overlay"
  109. v-if="isFull && showCardPopup"
  110. @tap.stop="goBuy(currentCardItem)"
  111. >
  112. <view class="goods-cover-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
  113. <image style="height: 30px;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></image>
  114. <view class="goods-cover-hot-text">{{ getProductHotCount(currentCardItem) }}</view>
  115. </view>
  116. <view class="goods-cover-inner">
  117. <view class="close-box" @tap.stop="closeCardPopup">
  118. <image style="width: 15px;height: 15px;" src="/static/images/course/close.png"></image>
  119. </view>
  120. <view class="goods-cover-img">
  121. <image
  122. v-if="currentCardItem && currentCardItem.images"
  123. style="width:150px;height:150px;object-fit:cover;"
  124. :src="currentCardItem.images"
  125. mode="aspectFill"
  126. ></image>
  127. </view>
  128. <view class="goods-cover-info">
  129. <view class="goods-cover-title">{{ currentCardItem.productName || '-' }}</view>
  130. <view class="goods-cover-bottom">
  131. <view class="goods-cover-tag">
  132. <image class="goods-cover-tag-bg" src="/static/images/course/pbg.png" mode="widthFix"></image>
  133. <view class="goods-cover-tag-text">惊喜价</view>
  134. </view>
  135. <view class="goods-cover-price">
  136. <view class="unit">¥</view>
  137. <view class="price">
  138. {{ currentCardItem.price || '0.00' }}
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. </view>
  144. </view>
  145. </view>
  146. </view>
  147. <!-- 商品卡片弹窗 -->
  148. <view class="goods-card" :style="{'bottom':isShu?'280rpx':'220rpx'}" v-if="!isFull&&showCardPopup" @click.stop="goBuy(currentCardItem)">
  149. <view class="close-box" @click.stop="closeCardPopup">
  150. <image src="/static/images/course/close.png"></image>
  151. </view>
  152. <view class="goods-card-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
  153. <image style="height: 56rpx;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></image>
  154. <text class="goods-card-hot-count">{{ getProductHotCount(currentCardItem) }}</text>
  155. </view>
  156. <view class="goods-card-inner">
  157. <image class="goods-card-img" :src="currentCardItem.images" mode="aspectFill"></image>
  158. <view class="goods-card-info">
  159. <view class="goods-card-title">{{ currentCardItem.productName || '-' }}</view>
  160. <view class="goods-card-bottom">
  161. <view class="goods-card-tag">
  162. <image class="goods-card-tag-bg" src="/static/images/course/pbg.png" mode="widthFix"></image>
  163. <view class="goods-card-tag-text">惊喜价</view>
  164. </view>
  165. <view class="goods-card-price">
  166. <view class="unit">¥</view>
  167. <text class="price">
  168. {{ currentCardItem.price || '0.00' }}
  169. </text>
  170. </view>
  171. </view>
  172. </view>
  173. </view>
  174. </view>
  175. <!-- 评价 -->
  176. <view class="rating-box" :style="{paddingTop:visibleRatingTabCount == 1?'30rpx':'24rpx'}" v-if="isLogin&&isAddKf==1&&!isShu&&hasVisibleRatingTab">
  177. <image class="bg" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/bg-back.png" mode="widthFix"></image>
  178. <view class="tab-Box" v-if="visibleRatingTabCount > 1">
  179. <view
  180. v-for="tab in ratingTabList"
  181. :key="tab.key"
  182. class="tab-item"
  183. :class="{ 'tab-item--active': tabIndex == tab.index }"
  184. @click="selecTab(tab.index)"
  185. >
  186. <view class="tab-item__surface">
  187. <image :src="tabIndex == tab.index ? tab.iconActive : tab.icon" mode="aspectFit"></image>
  188. <text class="tab-item__text">{{ tab.label }}</text>
  189. </view>
  190. </view>
  191. </view>
  192. <!-- 仅一个 Tab:单行左对齐标题,无选中态(顺序:精选留言 / 栏目介绍 / 评价得积分) -->
  193. <view class="tab-Box tab-Box--single" v-else-if="visibleRatingTabCount === 1">
  194. <view v-for="tab in ratingTabList" :key="tab.key" class="tab-item tab-item--single">
  195. <view class="tab-item__surface tab-item__surface--single">
  196. <image :src="tab.iconActive" mode="aspectFit"></image>
  197. <text class="tab-item__text tab-item__text--single">{{ tab.label }}</text>
  198. </view>
  199. </view>
  200. </view>
  201. <view class="rating-body" v-if="tabIndex == 2 && showTabReviewPoints">
  202. <view class="rating-ques-block" v-for="(item,index) in quesList" :key="index">
  203. <view class="title">{{item.title}}</view>
  204. <view class="sat-box">
  205. <view class="sat" v-for="(option,idx) in item.questionOption" :key="idx" :class="isRateOptionSelected(item, option)?'sat--active':''" @click="handleAnswer(item,option,index)">
  206. <view class="sat-img-wrap" :class="rateBounceIndex===(index + '_' + option.indexId)?'sat-img-bounce':''">
  207. <image :src="getRateOptionIcon(option, isRateOptionSelected(item, option), idx)" mode="aspectFit"></image>
  208. </view>
  209. <text :class="isRateOptionSelected(item, option)?'active':''">{{option.name}}</text>
  210. </view>
  211. </view>
  212. </view>
  213. </view>
  214. <view class="rating-body rating-body--plain" :style="{padding:courseInfo.courseIntroImg?'0rpx':'30rpx'}" v-if="tabIndex === 1 && showTabColumnIntro">
  215. <!-- <view class="title">{{ (courseInfo && courseInfo.title) || '栏目介绍' }}</view> -->
  216. <image
  217. class="rating-intro-cover-img"
  218. :src="courseInfo.courseIntroImg"
  219. mode="widthFix"
  220. ></image>
  221. </view>
  222. <view class="rating-body rating-body--featured" v-if="tabIndex == 0 && showTabFeaturedComments">
  223. <scroll-view
  224. scroll-y
  225. class="rating-msg-scroll"
  226. :show-scrollbar="false"
  227. lower-threshold="100"
  228. @scrolltolower="onFeaturedCommentScrollToLower"
  229. >
  230. <view v-if="featuredCommentLoading" class="rating-msg-empty">加载中...</view>
  231. <template v-else>
  232. <view
  233. class="rating-msg-item"
  234. v-for="(it, idx) in featuredCommentDisplayList"
  235. :key="it.commentId"
  236. >
  237. <view class="rating-msg-avatar-wrap">
  238. <image
  239. class="rating-msg-avatar"
  240. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/head.png"
  241. mode="aspectFill"
  242. />
  243. </view>
  244. <view class="rating-msg-main">
  245. <text class="rating-msg-name">{{ getCommentDisplayName(it) }}</text>
  246. <view class="rating-msg-text">{{ getCommentTextOnly(it) }}</view>
  247. <view v-if="getCommentVideoUrl(it)" class="rating-msg-media rating-msg-media--video">
  248. <view class="rating-msg-video-wrap">
  249. <featured-comment-xg-player
  250. ref="featuredCommentXg"
  251. :ref-in-for="true"
  252. :player-id="getFeaturedCommentVideoId(it, idx)"
  253. :video-url="getCommentVideoUrl(it)"
  254. :poster="getCommentVideoPoster(it)"
  255. />
  256. <view
  257. class="rating-msg-video-play-cover"
  258. @tap.stop="openFeaturedCommentMediaPreview(it, idx, 'video')"
  259. >
  260. <image
  261. class="rating-msg-video-play-icon"
  262. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/bofang.png"
  263. mode="aspectFit"
  264. />
  265. </view>
  266. </view>
  267. </view>
  268. <view
  269. v-else-if="commentImageUrls(it).length"
  270. class="rating-msg-media rating-msg-media--imgs"
  271. >
  272. <image
  273. v-for="(img, mi) in commentImageUrls(it)"
  274. :key="mi"
  275. :src="img"
  276. mode="widthFix"
  277. class="rating-msg-img"
  278. @click="openFeaturedCommentMediaPreview(it, idx, 'image', mi)"
  279. />
  280. </view>
  281. </view>
  282. </view>
  283. <view v-if="!featuredCommentDisplayList.length" class="rating-msg-empty">暂无精选留言</view>
  284. <view v-if="featuredCommentLoadingMore" class="rating-msg-load-more">加载中...</view>
  285. <view
  286. v-else-if="showFeaturedCommentNoMoreFooter"
  287. class="rating-msg-load-more rating-msg-load-more--done"
  288. >没有更多了</view>
  289. <!-- 底部锚点:发布成功后 scroll-into-view 定位到最新消息一侧,避免留在顶部 -->
  290. <view id="fc-scroll-anchor" class="rating-msg-scroll-anchor"></view>
  291. </template>
  292. </scroll-view>
  293. </view>
  294. </view>
  295. <!-- 温馨提示弹窗 -->
  296. <u-popup :show="tipsPopup" mode="center" :closeOnClickOverlay='true' @close='tipsPopup=!tipsPopup'
  297. :safeAreaInsetBottom='false' round='12'>
  298. <view class="tipsPopup-mask">
  299. <image class="red_envelope_top" :src="imgPath+'/app/image/red_envelope_img.png'" mode="aspectFill">
  300. </image>
  301. <view class="tipsPopup">
  302. <image class="tipsPopup-close" :src="imgPath+'/app/image/course_close_white_icon.png'"
  303. mode="aspectFill" @click="closeTipsPop"></image>
  304. <view class="tipsPopup-line">
  305. <view class="tipsPopup-box">
  306. <view class="tipsPopup-head">
  307. <image class="tipsPopup-head-title" :src="imgPath+'/app/image/tips_title_img.png'"
  308. mode="widthFix"></image>
  309. </view>
  310. <view class="tipsPopup-content">
  311. <view class="tipsPopup-content-title">亲爱的用户,</view>
  312. <view>您已经观看课程{{timepath}}的时间了,请注意休息并保持专注。</view>
  313. </view>
  314. <view class="tipsPopup-btn-box">
  315. <view class="tipsPopup-btn" @click="closeTipsPop">继续观看领奖励</view>
  316. </view>
  317. </view>
  318. </view>
  319. </view>
  320. </view>
  321. </u-popup>
  322. <!-- 答题弹窗 -->
  323. <u-popup :show="answerPopup" mode="center" @close="closepop()" @open="open" closeOnClickOverlay
  324. :safeAreaInsetBottom='false' round='12'>
  325. <view :class="errTitle == '恭喜你,回答正确' ? 'answerPopup-box bg':'answerPopup-box'">
  326. <!-- 正确 -->
  327. <image class="tipimg" v-if="errTitle == '恭喜你,回答正确'" :src="imgPath+'/app/image/course_answer_img.png'"
  328. mode="aspectFill"></image>
  329. <!-- 错误 -->
  330. <image class="tipimg" v-else :src="imgPath+'/app/image/course_answer_incorrectly_img.png'"
  331. mode="aspectFill">
  332. </image>
  333. <view class="answerPopup-title">{{errTitle}}</view>
  334. <view class="answerPopup-desc" v-html="errDesc"></view>
  335. <!-- 选择奖励 -->
  336. <!-- 错误题目 -->
  337. <view class="errQuesbox" v-if="errQues&&errQues.length>0">
  338. <view class="errQuesbox-item textOne" v-for="(it,index) in errQues" :key="index">{{it.title}}</view>
  339. </view>
  340. <view class="answerPopup-btn" v-if="errTitle == '恭喜你,回答正确'" @click="closeAnswerPopup">点击领取红包</view>
  341. <view class="tipsPopup-btn-box" v-else
  342. :style="{marginTop: errQues&&errQues.length>0 ? '40rpx':'54rpx'}">
  343. <view class="tipsPopup-btn" @click="closeAnswerPopup">{{remain > 0 ? '重新答题': '确认'}}</view>
  344. </view>
  345. </view>
  346. </u-popup>
  347. <!-- 客服二维码弹窗 -->
  348. <u-popup :show="kfPopup" mode="center" :mask-click="false" round='12' @close="kefpop">
  349. <view class="kfqrcode-box">
  350. <view class="justify-start align-center fs24 base-color-9">
  351. <view>{{userInfo.nickName || '用户'}}#</view>
  352. <view>{{userInfo.userId}}</view>
  353. </view>
  354. <view>请添加客服</view>
  355. <image class="kfqrcode" :src="qrcode" show-menu-by-longpress="true" mode="aspectFit"></image>
  356. <!-- <view v-show="qrcodeMsg" style="margin-top: 30rpx;" v-html="qrcodeMsg"></view> -->
  357. <!-- <image class="kfqrcode-close" :src="imgPath+'/app/image/course_close_white_icon.png'" mode="aspectFill"
  358. @click="closeKFPop"></image> -->
  359. </view>
  360. </u-popup>
  361. <u-popup :show="timepop" mode="center" round='12'>
  362. <view class="timepopbox center column">
  363. <view class="fs40 bold ">不在看课时间范围内</view>
  364. <view class="mtb20">看课时间</view>
  365. <view>{{videocont.startDateTime}}</view>
  366. <view>至</view>
  367. <view>{{videocont.endDateTime}}</view>
  368. <view class="base-bg-orange colorf p20 radius50 mt20" @click="getH5CourseVideoDetails">刷新时间</view>
  369. </view>
  370. </u-popup>
  371. <u-popup :show="isCart" @close="closeShop" round="20rpx" bgColor="#fff">
  372. <scroll-view class="scroll-view" style="height:500rpx;box-sizing: border-box;padding: 24rpx;" scroll-y="true" enable-flex>
  373. <goodsList ref="goodsList" :treatmentPackage="displayProductList" :hotCountMap="productHotCountMap" :urlOption="urlOption"></goodsList>
  374. </scroll-view>
  375. </u-popup>
  376. <u-popup :show="isMore" @close="closeMore" round="20rpx" bgColor="#fff">
  377. <view class="more-actions-popup">
  378. <view class="more-action-item"
  379. @click="navgetTo('/pages_user/user/storeOrder?status=')">
  380. <u-icon name="shopping-cart" color="#FF233C" size="40"></u-icon>
  381. <view class="action-label">我的订单</view>
  382. </view>
  383. <view class="more-action-item"
  384. @click="navgetTo('/pages_points/integralLogsList')">
  385. <u-icon name="calendar" color="#FF233C" size="40"></u-icon>
  386. <view class="action-label">积分记录</view>
  387. </view>
  388. <view class="more-action-item" @click="navgetTo('/pages_points/integralGoodsList')">
  389. <u-icon name="gift" color="#FF233C" size="40"></u-icon>
  390. <view class="action-label">兑换好礼</view>
  391. </view>
  392. </view>
  393. </u-popup>
  394. <u-popup
  395. :show="featuredMediaActionSheetShow"
  396. mode="bottom"
  397. round="20rpx"
  398. bgColor="#fff"
  399. :duration="300"
  400. @close="closeFeaturedMediaActionSheet"
  401. >
  402. <view class="fc-media-actions-sheet">
  403. <view class="fc-media-actions-sheet-item" @tap.stop="onFeaturedMediaSheetPick(0)">相册图片</view>
  404. <view class="fc-media-actions-sheet-item" @tap.stop="onFeaturedMediaSheetPick(1)">相册视频</view>
  405. <!-- <view class="fc-media-actions-sheet-gap"></view> -->
  406. <view
  407. class="fc-media-actions-sheet-item fc-media-actions-sheet-cancel"
  408. @tap.stop="closeFeaturedMediaActionSheet"
  409. >取消</view>
  410. </view>
  411. </u-popup>
  412. <!-- <view class="footer-tips">重庆云联融智提供技术支持</view> -->
  413. <!-- footer -->
  414. <view class="footer" v-if="videoId">
  415. <view class="btns" :style="{'justify-content':isLogin&&isAddKf==1&&remainTime==0&&hasVisibleRatingTab&&!showTabColumnIntro?'space-between':'flex-end'}">
  416. <template>
  417. <button class="author-btn" @click="openList" v-if="isLogin&&isAddKf==1&&isShu&&(showTabFeaturedComments || showTabColumnIntro || showTabReviewPoints)">{{ showTabFeaturedComments ? '去留言' : showTabColumnIntro ? '栏目介绍' : '评价得积分' }}
  418. <image v-if="showTabReviewPoints&&!showTabFeaturedComments&&!showTabColumnIntro" src="/static/images/course/jifen.png"></image></button>
  419. <button class="author-btn" @click="submit" v-if="isLogin&&isAddKf==1&&remainTime==0&&!isShu&&tabIndex==2">评价得积分
  420. <image src="/static/images/course/jifen.png"></image>
  421. </button>
  422. </template>
  423. <button class="author-btn" @click="submit" v-if="!isLogin&&isAddKf!==1">立即学习</button>
  424. <!-- 竖屏 isShu 下 rating 区隐藏;若仍停留在精选留言 tab,勿在 footer 展示留言框,只保留「评价得积分」按钮(与 _expandLayoutIfPortraitFeaturedCommentsOnly 竖屏体验一致) -->
  425. <view v-if="tabIndex==0 && showTabFeaturedComments&&!isShu" class="rating-msg-input-shell">
  426. <view
  427. v-if="featuredCommentMedia"
  428. class="rating-msg-media-pill"
  429. @click.stop="clearFeaturedCommentMedia"
  430. >
  431. <image
  432. v-if="featuredCommentMedia.type === 'image'"
  433. class="rating-msg-media-thumb"
  434. :src="featuredCommentMedia.path"
  435. mode="aspectFill"
  436. />
  437. <image
  438. v-else
  439. class="rating-msg-media-thumb"
  440. :src="featuredCommentMedia.thumbTempPath || featuredCommentMedia.path"
  441. mode="aspectFill"
  442. />
  443. <view v-if="featuredCommentMedia.type === 'video'" class="rating-msg-media-badge">视频</view>
  444. <text class="rating-msg-media-x">×</text>
  445. </view>
  446. <view class="rating-msg-input rating-msg-input-trigger" @tap.stop="openFeaturedCommentComposer">
  447. <text
  448. class="rating-msg-input-trigger-text"
  449. :class="{ 'is-ph': !featuredCommentInput }"
  450. >{{ featuredCommentInput || '善言善语结善缘~' }}</text>
  451. </view>
  452. <view class="rating-msg-input-icon" @tap.stop="openFeaturedCommentComposer">
  453. <image src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/upimg.png"></image>
  454. </view>
  455. </view>
  456. <view class="author-cart" v-show="isLogin && isAddKf==1">
  457. <view class="more-box">
  458. <image src="/static/images/course/more24.png" @click="showMore"></image>
  459. <view class="tips">更多</view>
  460. </view>
  461. <image class="xianshi" v-if="showTreatment==0&&displayProductList.length>0&&!cardPopup" src="/static/images/course/xg.png"></image>
  462. <view class="more-box" v-if="showTreatment==0">
  463. <image @click="showCart" src="/static/images/course/cart.png"></image>
  464. <view class="tips">去下单</view>
  465. </view>
  466. </view>
  467. </view>
  468. </view>
  469. <!-- 更多操作弹窗 -->
  470. <view>
  471. <u-modal :show="showfalse" title="提示" :content='contentmsg' @confirm='showfalse=false'></u-modal>
  472. </view>
  473. <u-loading-page :loading="viewload" iconSize="32" loadingColor="#3c9cff" fontSize="24"
  474. :loading-text="loadingtext"></u-loading-page>
  475. <template v-if="tabIndex==0 && showTabFeaturedComments && !(showTabReviewPoints && isShu)">
  476. <view
  477. v-if="featuredCommentComposerActive"
  478. class="featured-comment-composer-mask"
  479. @tap="onFeaturedCommentComposerMaskTap"
  480. ></view>
  481. <view
  482. v-if="featuredCommentComposerActive"
  483. class="featured-comment-composer-wrap"
  484. :style="{ bottom: featuredCommentKeyboardBottomPx + 'px' }"
  485. >
  486. <view class="featured-comment-composer-inner">
  487. <view
  488. v-if="featuredCommentMedia"
  489. class="fc-composer-media-pill"
  490. @click.stop="clearFeaturedCommentMedia"
  491. >
  492. <image
  493. v-if="featuredCommentMedia.type === 'image'"
  494. class="fc-composer-media-thumb"
  495. :src="featuredCommentMedia.path"
  496. mode="aspectFill"
  497. />
  498. <image
  499. v-else
  500. class="fc-composer-media-thumb"
  501. :src="featuredCommentMedia.thumbTempPath || featuredCommentMedia.path"
  502. mode="aspectFill"
  503. />
  504. <view v-if="featuredCommentMedia.type === 'video'" class="fc-composer-media-badge">视频</view>
  505. <text class="fc-composer-media-x">×</text>
  506. </view>
  507. <view class="featured-comment-composer-input-row">
  508. <textarea
  509. class="fc-composer-input"
  510. v-model="featuredCommentInput"
  511. type="text"
  512. confirm-type="done"
  513. :show-confirm-bar="false"
  514. placeholder="善言善语结善缘~"
  515. placeholder-class="fc-composer-ph"
  516. :focus="featuredCommentInputFocused"
  517. :adjust-position="false"
  518. @blur="onFeaturedCommentComposerInputBlur"
  519. />
  520. <view class="fc-composer-input-icon" @click.stop="onFeaturedPickMedia">
  521. <image src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/upimg.png"></image>
  522. </view>
  523. </view>
  524. <view class="featured-comment-composer-publish" @tap.stop="submitFeaturedComment">发布</view>
  525. </view>
  526. </view>
  527. </template>
  528. <!-- 精选留言:图片/视频放大查看(自定义弹窗,替代系统预览) -->
  529. <view
  530. v-if="featuredCommentMediaPreviewShow"
  531. class="fc-media-preview-mask"
  532. @tap="closeFeaturedCommentMediaPreview"
  533. >
  534. <view class="fc-media-preview-card" @tap.stop="featuredCommentPreviewCardTap">
  535. <view
  536. v-if="featuredCommentMediaPreviewMode === 'image' && featuredCommentMediaPreviewUrls.length"
  537. class="fc-media-preview-img-box"
  538. >
  539. <view class="fc-media-preview-slide">
  540. <image
  541. class="fc-media-preview-main-img"
  542. :src="featuredCommentMediaPreviewUrls[featuredCommentMediaPreviewIndex]"
  543. mode="aspectFit"
  544. />
  545. </view>
  546. </view>
  547. <view
  548. v-else-if="featuredCommentMediaPreviewMode === 'video' && featuredCommentMediaPreviewVideoUrl"
  549. class="fc-media-preview-video-box"
  550. >
  551. <video
  552. id="fc-media-preview-video"
  553. class="fc-media-preview-video"
  554. :src="featuredCommentMediaPreviewVideoUrl"
  555. :poster="featuredCommentMediaPreviewPoster"
  556. controls
  557. :show-center-play-btn="true"
  558. :show-play-btn="true"
  559. :show-fullscreen-btn="false"
  560. :auto-pause-if-open-native="false"
  561. :auto-pause-if-navigate="true"
  562. :enable-progress-gesture="true"
  563. :show-background-playback-button="false"
  564. @play="onFeaturedMediaPreviewModalVideoPlay"
  565. />
  566. </view>
  567. <view class="fc-media-preview-footer">
  568. <scroll-view
  569. v-if="featuredCommentMediaPreviewMode === 'image' && featuredCommentMediaPreviewUrls.length > 1"
  570. scroll-x
  571. class="fc-media-preview-thumbs"
  572. :show-scrollbar="false"
  573. >
  574. <view class="fc-media-preview-thumbs-inner">
  575. <image
  576. v-for="(tu, ti) in featuredCommentMediaPreviewUrls"
  577. :key="'t-' + ti"
  578. class="fc-media-preview-thumb"
  579. :class="{ 'fc-media-preview-thumb--active': ti === featuredCommentMediaPreviewIndex }"
  580. :src="tu"
  581. mode="aspectFill"
  582. @tap.stop="setFeaturedMediaPreviewIndex(ti)"
  583. />
  584. </view>
  585. </scroll-view>
  586. <view v-else class="fc-media-preview-thumbs-spacer" />
  587. <view class="fc-media-preview-close" @tap.stop="closeFeaturedCommentMediaPreview">
  588. <!-- <text class="fc-media-preview-close-x">×</text> -->
  589. <u-icon name="close-circle" color="#fff" size="30"></u-icon>
  590. </view>
  591. </view>
  592. </view>
  593. </view>
  594. <courseExpiration v-if="showExpiration" :code="resCode" :msg="resMsg" :qrcode="qrcode"
  595. :userId="user && userInfo.userId ? userInfo.userId : ''"></courseExpiration>
  596. </view>
  597. </template>
  598. <script>
  599. import goodsList from "./components/goodsList.vue"
  600. import { hasHotSaleTag, getProductHotKey, clampHotSaleCount } from './utils/productHotSale.js'
  601. import {
  602. generateRandomString
  603. } from "@/utils/common.js"
  604. import {
  605. buildFakeOrderPool
  606. } from './utils/videovipFakeMarquee.js'
  607. import dayjs from 'dayjs';
  608. import {
  609. mapGetters
  610. } from 'vuex';
  611. import courseExpiration from './components/courseExpiration.vue'
  612. import FeaturedCommentXgPlayer from './components/FeaturedCommentXgPlayer.vue'
  613. import {
  614. getErrMsg,
  615. getH5CourseByVideoId,
  616. getH5CourseVideoDetails,
  617. courseAnswer,
  618. getFinishCourseVideo,
  619. getIsAddKf,
  620. getInternetTraffic,
  621. getIntegralByH5Video,
  622. sendReward,
  623. getRemainTime,
  624. getCommentList,
  625. addCommentWithMedia
  626. } from "@/api/courseLook.js"
  627. import {
  628. getConfigByKey
  629. } from "@/api/user.js"
  630. export default {
  631. components: {
  632. courseExpiration,
  633. goodsList,
  634. FeaturedCommentXgPlayer
  635. },
  636. data() {
  637. return {
  638. displayType:'',
  639. isMore:false,//更多
  640. isCart:false,//购物车
  641. isShu:false,//竖屏默认
  642. rateBounceIndex: null,
  643. _rateBounceTimer: null,
  644. rateList:[{
  645. id:0,
  646. icon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/hsat.png',
  647. name:'非常满意',
  648. selIcon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/hsat_sel.png'
  649. },
  650. {
  651. id:1,
  652. icon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/sat.png',
  653. name:'满意',
  654. selIcon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/sat_sel.png'
  655. },
  656. {
  657. id:2,
  658. icon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/fari.png',
  659. name:'一般',
  660. selIcon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/fari_sel.png'
  661. },
  662. {
  663. id:3,
  664. icon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/unsat.png',
  665. name:'不满意',
  666. selIcon:'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/app/vip/unsat_sel.png'
  667. }
  668. ],
  669. resMsg: "",
  670. resCode: '',
  671. showExpiration: false,
  672. videoItem: {},
  673. viewload: true,
  674. loadingtext: "数据加载中...",
  675. baseUrl: uni.getStorageSync('requestPath'),
  676. // 1 红包 2 积分
  677. rewardType: [{
  678. name: '红包奖励',
  679. value: 1
  680. }, {
  681. name: '积分奖励',
  682. value: 2
  683. }],
  684. currentReward: 1,
  685. player: null,
  686. /** 西瓜播放器是否已初始化 */
  687. xgPlayerInited: false,
  688. videoReloadKey: 0,
  689. exitFullscreen: '',
  690. videoPlayStatus: {
  691. status: '',
  692. seekTime: 0,
  693. time: 0
  694. },
  695. loading: true,
  696. progress: 0,
  697. code: null,
  698. statusBarHeight: uni.getSystemInfoSync().statusBarHeight,
  699. scrollTop: 0,
  700. height: '0px',
  701. isLogin: false,
  702. videoUrl: "",
  703. videoId: "",
  704. /** 恢复前台 / seek 后短时间内跳过「快进」判定,避免 iOS、鸿蒙息屏后首帧 currentTime 抖动误报 */
  705. _antiSeekSuppressExpireAt: 0,
  706. _videoPageHadShownOnce: false,
  707. //现在的时长
  708. playTime: 0,
  709. //总时长
  710. duration: 0,
  711. playDuration: 0,
  712. // 用于续播
  713. playDurationSeek: 0,
  714. // 温馨提醒时间节点,
  715. tipsTime: 0,
  716. tipsOpen: false,
  717. config: {},
  718. courseInfo: {},
  719. quesList: [],
  720. lineList: [],
  721. // 错题
  722. errQues: [],
  723. // 答题机会
  724. remain: 0,
  725. errTitle: "",
  726. errDesc: "",
  727. showPlay: true,
  728. showControls: false,
  729. playStatus: "",
  730. isFull: false,
  731. fullscreenToggleLock: false,
  732. fullscreenToggleTimer: null,
  733. isAddKf: 0,
  734. lineIndex: 0,
  735. // 是否展开
  736. isExpand: true,
  737. textHeight: 0, //文本高度
  738. qwUserId: "",
  739. qrcode: "",
  740. corpId: "",
  741. periodId: "", //营期id
  742. companyUserId: "", //销售id
  743. companyId: "", //公司id
  744. courseId: "", //课程id
  745. qrcodeMsg: "",
  746. urlOption: {},
  747. bufferRate: 0, // 缓冲时间
  748. uuId: "",
  749. isEnded: false,
  750. /** 当前是否处于播放中 */
  751. videoPlaying: false,
  752. // 是否允许拖动进度条
  753. // linkType: 0,
  754. ip: null,
  755. checked: true,
  756. isFinish: 0, // 是否完课
  757. interval: null,
  758. intervalIntegral: null, // 积分定时
  759. options: {
  760. sources: [{
  761. src: ""
  762. }],
  763. poster: "",
  764. live: false /* 是否直播 */ ,
  765. controls: true,
  766. autoplay: false,
  767. licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1323137866_1/v_cube.license', // license 地址,参考准备工作部分,在视立方控制台申请 license 后可获得 licenseUrl,
  768. LicenseKey: 'bcc5bd9a14b798b48c52ff005a21d926',
  769. controlBar: {
  770. volumePanel: false,
  771. playbackRateMenuButton: false,
  772. QualitySwitcherMenuButton: false,
  773. // progressControl: false
  774. },
  775. plugins: {
  776. // ProgressMarker: false,
  777. ContextMenu: {
  778. statistic: false
  779. }
  780. },
  781. },
  782. // 错误请求次数
  783. errorCount: 0,
  784. answerPopup: false,
  785. sortLink: "",
  786. // 课程是否过期
  787. isExpire: false,
  788. menuButtonLeft: 281,
  789. menuButtonH: 45,
  790. timer: null,
  791. flag: false,
  792. msg: '',
  793. poster: '',
  794. userInfo: {},
  795. timeid: '',
  796. videocont: {},
  797. timepop: false,
  798. appToken: '',
  799. kfPopup: false,
  800. iskftype: 0,
  801. courseLogo: '',
  802. isquestion: false,
  803. tipsPopup: false,
  804. timepath: '3分之1',
  805. tipsTime2: 0,
  806. projectId: '',
  807. showfalse: false,
  808. contentmsg: null,
  809. currentId: 0,
  810. showTreatment: 1,
  811. treatmentPackage: [],
  812. // 商品栏展示列表(根据上架/下架时间过滤)
  813. displayProductList: [],
  814. // 商品卡片弹窗
  815. cardPopup: false,
  816. currentCardItem: null,
  817. // 热卖标签数量(小黄车与弹窗卡片共用,按 productId)
  818. productHotCountMap: {},
  819. productHotTimer: null,
  820. // 手动关闭后,当前卡片在其有效时段内不再重复弹出
  821. dismissedCardKey: '',
  822. // 完课积分倒计时(秒)
  823. remainTime: 0,
  824. // 完课积分总倒计时时长(秒),用于计算百分比
  825. totalRemainTime: 0,
  826. // 完课积分倒计时定时器
  827. countdownTimer: null,
  828. // 是否已从接口拉取到倒计时数据(避免 remainTime 初始为 0 时提前放开答题)
  829. remainTimeReady: false,
  830. // 倒计时结束后是否已补发过一次看课记录
  831. hasReportedAfterCountdown: false,
  832. notice:'',//跑马灯,
  833. isHeight:false,
  834. // 跑马灯数据就绪门禁:避免首次进入时使用旧状态导致闪现
  835. marqueeDataReady: false,
  836. // 虚拟下单跑马灯
  837. fakeOrderPool: [],
  838. marqueeTriplePayloads: [],
  839. marqueeTripleIncomingPayloads: [],
  840. marqueeTripleSwitching: false,
  841. marqueeCurrentText: '',
  842. marqueeSelfHighlight: false,
  843. marqueeDisplayPayload: null,
  844. marqueeDisplaySelf: false,
  845. marqueeIncomingPayload: null,
  846. marqueeIncomingSelf: false,
  847. marqueeSwitching: false,
  848. pendingSelfMarqueeFirst: false,
  849. _fakeMarqueeStarted: false,
  850. /** 与 computed fakeMarqueeEligible 上次值比较,避免无 watch 时在 updateProductAndCardDisplay 高频调用中反复重置防抖 */
  851. _fakeMarqueeEligiblePrev: undefined,
  852. _marqueeStartDebounceTimer: null,
  853. _fakeMarqueeTimer: null,
  854. _marqueeSwitchTimer: null,
  855. _marqueeTripleSwitchTimer: null,
  856. /** 本人下单条按缓存 ts 与当前时间差定时回显刷新 */
  857. _selfMarqueeClockTimer: null,
  858. tabIndex:0,
  859. /** 与 quesList 同源:接口 questionBankList,用于 Tab「评价得积分」是否展示 */
  860. questionBankList: [],
  861. /** getCommentList 的 defaultPageInfo.list:控制精选留言 Tab;与用户 featuredCommentList 在 featuredCommentDisplayList 中合并展示 */
  862. defaultPageInfoList: [],
  863. featuredCommentList: [],
  864. featuredCommentInput: '',
  865. featuredCommentLoading: false,
  866. featuredCommentLoadingMore: false,
  867. /** 下一页页码,与 getCommentList pageNum 一致 */
  868. featuredCommentPageNum: 1,
  869. featuredCommentPageSize: 10,
  870. featuredCommentHasMore: true,
  871. /** getCommentList 最近一次返回:用户评论总条数(与运营 defaultPageInfo.total 分离) */
  872. featuredCommentUserTotal: NaN,
  873. /** getCommentList 最近一次返回:运营精选总条数 */
  874. featuredCommentDefaultTotal: NaN,
  875. featuredCommentSending: false,
  876. /** 精选留言待发送的本地媒体:type image|video,path 临时路径,thumbTempPath 视频封面 */
  877. featuredCommentMedia: null,
  878. /**
  879. * 从相册/相机选图或选视频返回时小程序会触发 onShow,否则会误跑 getH5CourseByVideo / getH5CourseVideoDetails。
  880. * 与 uni.uploadFile 发布评论无直接关系;选媒体前置 true,onShow 消费一次或 complete 兜底清除。
  881. */
  882. videovipSuppressOnShowCourseFetchOnce: false,
  883. /** 从相册选留言图/视频前主课若在播,上传(含压缩)结束后恢复播放 */
  884. _shouldResumeCourseVideoAfterFeaturedUpload: false,
  885. /**
  886. * 打开精选留言输入前主课是否在播(快照)。
  887. * 键盘/选图页返回时常先触发 @pause,videoPlaying 已为 false,仅靠选媒体瞬间的 videoPlaying 会漏标,导致发布后不调 play。
  888. */
  889. _courseWasPlayingBeforeFeaturedCommentOpen: false,
  890. /** 精选留言选图/选视频:用底部 u-popup 替代 uni.showActionSheet,避免鸿蒙等机型原生弹层未贴底 */
  891. featuredMediaActionSheetShow: false,
  892. /** 精选留言:自定义大图/视频预览弹窗 */
  893. featuredCommentMediaPreviewShow: false,
  894. featuredCommentMediaPreviewMode: 'image',
  895. featuredCommentMediaPreviewUrls: [],
  896. featuredCommentMediaPreviewIndex: 0,
  897. featuredCommentMediaPreviewVideoUrl: '',
  898. featuredCommentMediaPreviewPoster: '',
  899. /** 精选留言 scroll-view 滚到底部(与 id=fc-scroll-anchor 配合) */
  900. featuredCommentScrollIntoView: '',
  901. /** 精选留言:键盘上方白底输入条 */
  902. featuredCommentComposerActive: false,
  903. featuredCommentInputFocused: false,
  904. featuredCommentKeyboardBottomPx: 0,
  905. }
  906. },
  907. filters: {
  908. numberToChinese(number) {
  909. if (number) {
  910. const chineseNumber = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
  911. return chineseNumber[number - 1];
  912. } else {
  913. return ''
  914. }
  915. },
  916. },
  917. computed: {
  918. isAnswer() {
  919. return (item, name) => {
  920. if (item.type == 1) {
  921. return item.answer == name
  922. } else if (item.type == 2) {
  923. const array = item.answer.split(',')
  924. return array.some(i => i == name)
  925. } else {
  926. return false
  927. }
  928. }
  929. },
  930. imgPath() {
  931. return this.$store.state.imgpath
  932. },
  933. appid() {
  934. //console.log('this.$store.state.appid',this.$store.state.appid)
  935. return this.$store.state.appid
  936. },
  937. // 格式化后的倒计时
  938. formattedCountdown() {
  939. return this.formatCountdown(this.remainTime);
  940. },
  941. // 倒计时百分比
  942. countdownPercentage() {
  943. return this.getCountdownPercentage();
  944. },
  945. /** 评价得积分:questionBankList 有题 */
  946. showTabReviewPoints() {
  947. return Array.isArray(this.questionBankList) && this.questionBankList.length > 0
  948. },
  949. /** 栏目介绍:已配置 courseIntroImg */
  950. showTabColumnIntro() {
  951. const img = this.courseInfo && this.courseInfo.courseIntroImg
  952. return typeof img === 'string' && img.trim() !== ''
  953. },
  954. /** 精选留言:仅当运营配置的 defaultPageInfo.list 有数据时展示 Tab */
  955. showTabFeaturedComments() {
  956. return Array.isArray(this.defaultPageInfoList) && this.defaultPageInfoList.length > 0
  957. },
  958. /** 精选留言列表:有运营 defaultPageInfo 时先展示用户新评论 featuredCommentList,再拼接运营配置项(同 id 去重) */
  959. featuredCommentDisplayList() {
  960. const def = Array.isArray(this.defaultPageInfoList) ? this.defaultPageInfoList : []
  961. if (!def.length) return []
  962. const user = Array.isArray(this.featuredCommentList) ? this.featuredCommentList : []
  963. return this._mergeFeaturedUserComments(user.slice(), def)
  964. },
  965. /** 用户评论已加载完且无加载中时展示 */
  966. showFeaturedCommentNoMoreFooter() {
  967. if (this.featuredCommentLoading || this.featuredCommentLoadingMore) return false
  968. const list = this.featuredCommentDisplayList
  969. if (!Array.isArray(list) || !list.length) return false
  970. return !this.featuredCommentHasMore
  971. },
  972. visibleRatingTabCount() {
  973. return this.ratingTabList.length
  974. },
  975. /** 评价区 Tab 展示顺序:精选留言 → 栏目介绍 → 评价得积分(仅渲染有数据的项) */
  976. ratingTabList() {
  977. const tabs = []
  978. if (this.showTabFeaturedComments) {
  979. tabs.push({
  980. key: 'featured',
  981. index: 0,
  982. label: '精选留言',
  983. icon: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/pl.png',
  984. iconActive: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/pl-sel.png'
  985. })
  986. }
  987. if (this.showTabColumnIntro) {
  988. tabs.push({
  989. key: 'column',
  990. index: 1,
  991. label: '栏目介绍',
  992. icon: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/des.png',
  993. iconActive: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/des-sel.png'
  994. })
  995. }
  996. if (this.showTabReviewPoints) {
  997. tabs.push({
  998. key: 'review',
  999. index: 2,
  1000. label: '评价得积分',
  1001. icon: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/sel.png',
  1002. iconActive: 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/zan-sel.png'
  1003. })
  1004. }
  1005. return tabs
  1006. },
  1007. hasVisibleRatingTab() {
  1008. return this.visibleRatingTabCount > 0
  1009. },
  1010. //...mapGetters(['coureLogin']),
  1011. /** 小黄车商品中「最早上架」时间点(秒),用于跑马灯起始展示 */
  1012. firstShelfSecondsAmongProducts() {
  1013. const list = this.treatmentPackage || []
  1014. if (!list.length) return Infinity
  1015. let minSec = Infinity
  1016. for (let i = 0; i < list.length; i++) {
  1017. const t = list[i].onShelfTime
  1018. if (!this._vipShelfTimeValid(t)) continue
  1019. const sec = this._vipTimeStrToSec(t)
  1020. if (sec > 0 && sec < minSec) minSec = sec
  1021. }
  1022. return minSec === Infinity ? Infinity : minSec
  1023. },
  1024. fakeMarqueeEligible() {
  1025. // 商品/回放数据未完成刷新前,不允许启动展示,避免首次闪现
  1026. if (!this.marqueeDataReady) return false
  1027. if (this.showTreatment !== 0) return false
  1028. // 以“当前应展示的商品列表非空”作为跑马灯出现条件
  1029. // 这样能对齐你说的“商品上架才出现”,避免只用最早上架秒点导致的一帧差异
  1030. return Array.isArray(this.displayProductList) && this.displayProductList.length > 0
  1031. },
  1032. // 竖屏课打开小黄车 goodsList 弹窗,或打开精选留言大图/视频预览 fc-media-preview-mask 时,先藏住视频上的虚拟下单条,关闭后再显(不影响底层跑马灯计时)
  1033. showFakeMarqueeOnVideoCover() {
  1034. if (!this.fakeMarqueeEligible) return false
  1035. if (this.displayType === 'portrait' && (this.isCart || this.featuredCommentMediaPreviewShow)) return false
  1036. return true
  1037. },
  1038. isTripleMarqueeMode() {
  1039. return !!(this.isShu || this.isFull)
  1040. },
  1041. /** 精选留言输入框弹键盘时暂时隐藏商品卡片,收起后按 cardPopup 逻辑恢复 */
  1042. showCardPopup() {
  1043. if (!this.cardPopup || !this.currentCardItem) return false
  1044. if (
  1045. this.featuredCommentComposerActive &&
  1046. (this.featuredCommentKeyboardBottomPx > 0 || this.featuredCommentInputFocused)
  1047. ) return false
  1048. return true
  1049. },
  1050. videovipVideoConfig() {
  1051. const allowSeek = this.isFinish == 1 || this.isEnded
  1052. return {
  1053. url: this.videoUrl || '',
  1054. poster: this.poster || '',
  1055. playerId: 'video-content-box',
  1056. isFast: allowSeek ? 1 : 0,
  1057. videoReloadKey: this.videoReloadKey,
  1058. displayType: this.displayType || 'landscape',
  1059. layoutShuKey: this.isShu ? 1 : 0,
  1060. time: Date.now()
  1061. }
  1062. },
  1063. },
  1064. watch: {
  1065. // coureLogin: {
  1066. // immediate: true, // 页面一进入就检查一次
  1067. // handler(val) {
  1068. // console.log(val, '----')
  1069. // if (val == 2 && this.isLogin) {
  1070. // console.log("看课AppToken失效,请重新登录")
  1071. // this.isLogin = false
  1072. // this.isAddKf = 0
  1073. // this.$showLoginPage()
  1074. // }
  1075. // }
  1076. // },
  1077. isTripleMarqueeMode(now, was) {
  1078. if (!now || was) return
  1079. if (!this.fakeMarqueeEligible || !this._fakeMarqueeStarted) return
  1080. this.syncTripleMarqueeImmediate()
  1081. },
  1082. /** 精选留言 Tab 出现后默认高亮 */
  1083. showTabFeaturedComments(now, was) {
  1084. if (now && was !== true) {
  1085. this._selectDefaultRatingTab()
  1086. }
  1087. },
  1088. /**
  1089. * 评价 Tab 晚于栏目介绍返回时,_syncRatingTabIndex 可能暂落在 tab=1;
  1090. * 若有精选留言则仍默认高亮 tab=0,否则仅校正到当前第一个可见 Tab。
  1091. */
  1092. showTabReviewPoints(now, was) {
  1093. if (now && was !== true) {
  1094. this._selectDefaultRatingTab()
  1095. }
  1096. },
  1097. /** Tab 数量增加且含精选留言时,默认高亮精选留言 */
  1098. visibleRatingTabCount(n, o) {
  1099. if (n <= 0 || !this.showTabFeaturedComments) return
  1100. if (o != null && n <= o) return
  1101. this._selectDefaultRatingTab()
  1102. },
  1103. // tabIndex(n) {
  1104. // if (n !== 2) {
  1105. // this._closeFeaturedCommentComposerUi()
  1106. // this.closeFeaturedMediaActionSheet()
  1107. // }
  1108. // }
  1109. displayProductList: {
  1110. handler() {
  1111. this.syncProductHotCounts()
  1112. },
  1113. deep: true,
  1114. },
  1115. cardPopup() {
  1116. this.syncProductHotCounts()
  1117. },
  1118. currentCardItem() {
  1119. this.syncProductHotCounts()
  1120. },
  1121. },
  1122. onLoad(option) {
  1123. this.getWebviewUrl()
  1124. // uni.$on('vipMsg', (data) => {
  1125. // console.log(4444, data)
  1126. // this.contentmsg = data
  1127. // this.showfalse = true
  1128. // uni.showToast({
  1129. // icon: 'none',
  1130. // title: data,
  1131. // duration: 5000
  1132. // });
  1133. // })
  1134. this.getVideoContainerHeight()
  1135. this._lockAppPortrait()
  1136. // this.code = option.code
  1137. this.userInfo = this.$getUserInfo()
  1138. this.appToken = uni.getStorageSync('companyUserInfo')
  1139. console.log('option', option)
  1140. if (!option.course) {
  1141. const keys = decodeURIComponent(Object.keys(option)[0]);
  1142. this.urlOption = JSON.parse(keys.split('course=')[1])
  1143. } else {
  1144. this.urlOption = option.course ? JSON.parse(decodeURIComponent(option.course)) : {}
  1145. }
  1146. console.log(this.urlOption)
  1147. uni.setStorageSync('H5course', this.urlOption)
  1148. this.videoId = this.urlOption.videoId
  1149. this.courseId = this.urlOption.courseId
  1150. this.periodId = this.urlOption.periodId
  1151. this.companyId = this.urlOption.companyId
  1152. this.companyUserId = this.urlOption.companyUserId
  1153. this.projectId = this.urlOption.projectId
  1154. this.timeid = this.urlOption.id
  1155. console.log(this.urlOption)
  1156. console.log(decodeURIComponent(option.course))
  1157. // this.sortLink = this.urlOption.link || ''
  1158. //this.getMenuButton()
  1159. // #ifndef H5
  1160. this._featuredCommentKbHandler = (res) => {
  1161. const h = res && res.height != null ? res.height : 0
  1162. const prev = this.featuredCommentKeyboardBottomPx
  1163. this.featuredCommentKeyboardBottomPx = h
  1164. if (
  1165. this.featuredCommentComposerActive &&
  1166. prev > 0 &&
  1167. h === 0
  1168. ) {
  1169. this.featuredCommentComposerActive = false
  1170. this.featuredCommentInputFocused = false
  1171. }
  1172. }
  1173. //uni.onKeyboardHeightChange(this._featuredCommentKbHandler)
  1174. // #endif
  1175. },
  1176. onShow() {
  1177. this.userInfo = this.$getUserInfo()
  1178. this.tipsOpen = false
  1179. this.isExpand = true
  1180. this.uuId = generateRandomString(16)
  1181. this.refreshVipPurchaseMarqueeFlag()
  1182. if (this._videoPageHadShownOnce) {
  1183. this._touchAntiSeekSuppress(2000)
  1184. } else {
  1185. this._videoPageHadShownOnce = true
  1186. }
  1187. if (this.videovipSuppressOnShowCourseFetchOnce) {
  1188. this.videovipSuppressOnShowCourseFetchOnce = false
  1189. return
  1190. }
  1191. // 每次进入页面先关闭门禁,并清空旧跑马灯状态,等 getH5CourseVideoDetails 拉完数据再开启
  1192. this.stopFakeMarqueeLoop()
  1193. this.marqueeDataReady = false
  1194. this._fakeMarqueeEligiblePrev = undefined
  1195. if (this.videoId) {
  1196. this.getH5CourseByVideo()
  1197. }
  1198. if (this.$isLogin()) {
  1199. this.isLogin = true
  1200. if (this.isAddKf == 1 && this.userInfo.userId) {
  1201. this.getH5CourseVideoDetails()
  1202. } else {
  1203. this.getIsAddKf()
  1204. }
  1205. } else {
  1206. this.isLogin = false
  1207. }
  1208. // if(this.sortLink){
  1209. // this.getLink()
  1210. // } else {
  1211. // uni.showToast({
  1212. // title: 'sortLink is not found',
  1213. // icon: 'none'
  1214. // });
  1215. // }
  1216. },
  1217. mounted() {
  1218. // this.getIP()
  1219. //this.getHeight()
  1220. },
  1221. onHide() {
  1222. this.closeFeaturedCommentMediaPreview()
  1223. this._closeFeaturedCommentComposerUi()
  1224. this._lockAppPortrait()
  1225. this._xgPause()
  1226. // 精选留言调起相册/相机时也会触发 onHide,勿停虚拟跑马灯(与 onShow 抑制拉课共用同一标志)
  1227. if (!this.videovipSuppressOnShowCourseFetchOnce) {
  1228. this.stopFakeMarqueeLoop()
  1229. }
  1230. // 页面隐藏时停止完课积分倒计时
  1231. this.stopCountdown()
  1232. this.stopProductHotTimer()
  1233. this._flushWatchProgress()
  1234. // if (this.interval != null) {
  1235. // clearInterval(this.interval)
  1236. // this.interval = null
  1237. // }
  1238. },
  1239. onPageResize(){
  1240. this.getVideoContainerHeight();
  1241. },
  1242. onUnload() {
  1243. this.closeFeaturedCommentMediaPreview()
  1244. this._lockAppPortrait()
  1245. this._videoPageHadShownOnce = false
  1246. if (this.interval != null) {
  1247. clearInterval(this.interval)
  1248. this.interval = null
  1249. }
  1250. if (this.fullscreenToggleTimer) {
  1251. clearTimeout(this.fullscreenToggleTimer)
  1252. this.fullscreenToggleTimer = null
  1253. }
  1254. if (this._rateBounceTimer) {
  1255. clearTimeout(this._rateBounceTimer)
  1256. this._rateBounceTimer = null
  1257. }
  1258. this.fullscreenToggleLock = false
  1259. // 页面卸载时清理完课积分倒计时
  1260. this.stopCountdown()
  1261. this.stopProductHotTimer()
  1262. this.clearIntegral()
  1263. this.stopFakeMarqueeLoop()
  1264. this.fakeOrderPool = []
  1265. this._flushWatchProgress()
  1266. // #ifndef H5
  1267. if (this._featuredCommentKbHandler) {
  1268. uni.offKeyboardHeightChange(this._featuredCommentKbHandler)
  1269. this._featuredCommentKbHandler = null
  1270. }
  1271. // #endif
  1272. },
  1273. beforeDestroy() {
  1274. this.closeFeaturedCommentMediaPreview()
  1275. this._xgStop()
  1276. this.xgPlayerInited = false
  1277. if (this.interval != null) {
  1278. clearInterval(this.interval)
  1279. this.interval = null
  1280. }
  1281. if (this.fullscreenToggleTimer) {
  1282. clearTimeout(this.fullscreenToggleTimer)
  1283. this.fullscreenToggleTimer = null
  1284. }
  1285. if (this._rateBounceTimer) {
  1286. clearTimeout(this._rateBounceTimer)
  1287. this._rateBounceTimer = null
  1288. }
  1289. this.fullscreenToggleLock = false
  1290. // 组件销毁前清理完课积分倒计时
  1291. this.stopCountdown()
  1292. this.stopProductHotTimer()
  1293. this.clearIntegral()
  1294. this.stopFakeMarqueeLoop()
  1295. this._flushWatchProgress()
  1296. // #ifndef H5
  1297. if (this._featuredCommentKbHandler) {
  1298. uni.offKeyboardHeightChange(this._featuredCommentKbHandler)
  1299. this._featuredCommentKbHandler = null
  1300. }
  1301. // #endif
  1302. },
  1303. methods: {
  1304. _updateVideoPlayStatus(status, seekTime = 0) {
  1305. this.videoPlayStatus = {
  1306. status,
  1307. seekTime,
  1308. url: this.videoUrl || '',
  1309. time: Date.now(),
  1310. videoReloadKey: this.videoReloadKey
  1311. }
  1312. },
  1313. _xgPlay(seekTime = 0) {
  1314. this._updateVideoPlayStatus('play', seekTime)
  1315. },
  1316. _xgPause() {
  1317. this._updateVideoPlayStatus('pause', 0)
  1318. },
  1319. _xgStop() {
  1320. this._updateVideoPlayStatus('stop', 0)
  1321. },
  1322. _xgSeek(seekTime = 0) {
  1323. this._updateVideoPlayStatus('seek', seekTime)
  1324. },
  1325. _xgExitFullscreen() {
  1326. this.exitFullscreen = 'yes-' + Date.now()
  1327. },
  1328. /** App 端锁定竖屏,避免系统自动旋转影响页面小窗布局 */
  1329. _lockAppPortrait() {
  1330. // #ifdef APP-PLUS
  1331. try {
  1332. plus.screen.lockOrientation('portrait-primary')
  1333. } catch (e) {}
  1334. // #endif
  1335. },
  1336. _lockAppLandscape() {
  1337. // #ifdef APP-PLUS
  1338. try {
  1339. plus.screen.lockOrientation('landscape-primary')
  1340. } catch (e) {}
  1341. // #endif
  1342. },
  1343. onXgPlayerReady() {
  1344. this.xgPlayerInited = true
  1345. },
  1346. onXgVideoPlay() {
  1347. this.getPlay()
  1348. },
  1349. onXgVideoPause() {
  1350. this.getPause()
  1351. },
  1352. onXgVideoEnded() {
  1353. this.getEnded()
  1354. },
  1355. onXgVideoTimeUpdate(e) {
  1356. this.onTimeUpdate({
  1357. detail: {
  1358. currentTime: e && e.currentTime != null ? e.currentTime : 0
  1359. }
  1360. })
  1361. },
  1362. onXgVideoProgress(e) {
  1363. const buffered = e && e.buffered != null ? e.buffered : 0
  1364. this.progressChange({
  1365. detail: {
  1366. buffered
  1367. }
  1368. })
  1369. },
  1370. onXgVideoError(e) {
  1371. this.videoErrorCallback({
  1372. target: {
  1373. errMsg: e && e.error != null ? String(e.error) : 'xgplayer error'
  1374. }
  1375. })
  1376. },
  1377. onXgFullscreenChange(e) {
  1378. const isFullscreen = !!(e && e.isFullscreen)
  1379. // 横屏课退出全屏:先锁竖屏再更新布局,避免受系统方向开关影响
  1380. if (this.displayType !== 'portrait' && !isFullscreen) {
  1381. this._lockAppPortrait()
  1382. }
  1383. this.fullscreenchange({
  1384. detail: {
  1385. fullScreen: isFullscreen
  1386. },
  1387. stopPropagation() {},
  1388. preventDefault() {}
  1389. })
  1390. },
  1391. _closeFeaturedCommentComposerUi() {
  1392. this.featuredCommentComposerActive = false
  1393. this.featuredCommentInputFocused = false
  1394. this.featuredCommentKeyboardBottomPx = 0
  1395. },
  1396. openFeaturedCommentComposer() {
  1397. this._expandLayoutIfPortraitFeaturedCommentsOnly()
  1398. this._courseWasPlayingBeforeFeaturedCommentOpen = this.videoPlaying && !this.isEnded
  1399. this.featuredCommentComposerActive = true
  1400. this.$nextTick(() => {
  1401. this.featuredCommentInputFocused = true
  1402. })
  1403. },
  1404. onFeaturedCommentComposerMaskTap() {
  1405. this._closeFeaturedCommentComposerUi()
  1406. },
  1407. onFeaturedCommentComposerInputFocus() {
  1408. this._expandLayoutIfPortraitFeaturedCommentsOnly()
  1409. },
  1410. onFeaturedCommentComposerInputBlur() {
  1411. this.featuredCommentInputFocused = false
  1412. },
  1413. /** 有精选留言时默认 tabIndex=0,否则落到第一个可见 Tab */
  1414. _selectDefaultRatingTab() {
  1415. if (this.showTabFeaturedComments) {
  1416. this.tabIndex = 0
  1417. this.loadFeaturedComments()
  1418. return
  1419. }
  1420. this._syncRatingTabIndex()
  1421. },
  1422. _syncRatingTabIndex() {
  1423. const tabs = [
  1424. this.showTabFeaturedComments,
  1425. this.showTabColumnIntro,
  1426. this.showTabReviewPoints
  1427. ]
  1428. if (!tabs.some(Boolean)) return
  1429. if (tabs[this.tabIndex]) return
  1430. const idx = tabs.findIndex(Boolean)
  1431. if (idx < 0) return
  1432. this.tabIndex = idx
  1433. if (idx === 0) {
  1434. this.loadFeaturedComments()
  1435. }
  1436. },
  1437. selecTab(idx){
  1438. if (idx === 0 && !this.showTabFeaturedComments) return
  1439. if (idx === 1 && !this.showTabColumnIntro) return
  1440. if (idx === 2 && !this.showTabReviewPoints) return
  1441. this.tabIndex = idx
  1442. if (idx === 0) {
  1443. this.loadFeaturedComments()
  1444. }
  1445. },
  1446. _resolveUserInfoObj() {
  1447. let u = this.userInfo
  1448. if (!u) return {}
  1449. if (typeof u === 'string') {
  1450. try {
  1451. return JSON.parse(u) || {}
  1452. } catch (e) {
  1453. return {}
  1454. }
  1455. }
  1456. return u
  1457. },
  1458. /** 从 getCommentList 结果中取出用户评论列表(兼容 data 为数组或 data.list / list 等),顺序与接口一致 */
  1459. _pickUserCommentListFromCommentListRes(res) {
  1460. if (!res || res.code != 200) return []
  1461. const d = res.data
  1462. if (d && Array.isArray(d.list)) return d.list.slice()
  1463. if (Array.isArray(d)) return d.slice()
  1464. if (Array.isArray(res.list)) return res.list.slice()
  1465. if (d && Array.isArray(d.records)) return d.records.slice()
  1466. return []
  1467. },
  1468. _pickDefaultPageInfoListFromCommentListRes(res) {
  1469. if (!res || res.code != 200) return []
  1470. const dpi = res.defaultPageInfo || (res.data && res.data.defaultPageInfo)
  1471. if (dpi && Array.isArray(dpi.list)) return dpi.list.slice()
  1472. return []
  1473. },
  1474. /** 用户新评论分页 total(与运营列表 total 分离;兼容 userTotal / listTotal / data.total) */
  1475. _pickFeaturedUserCommentTotalFromRes(res) {
  1476. if (!res || res.code != 200) return NaN
  1477. const d = res.data
  1478. const tryNum = (v) => {
  1479. const t = Number(v)
  1480. return Number.isFinite(t) && t >= 0 ? t : NaN
  1481. }
  1482. if (d && typeof d === 'object' && !Array.isArray(d)) {
  1483. if (typeof d.userTotal !== 'undefined') {
  1484. const t = tryNum(d.userTotal)
  1485. if (Number.isFinite(t)) return t
  1486. }
  1487. if (typeof d.commentTotal !== 'undefined') {
  1488. const t = tryNum(d.commentTotal)
  1489. if (Number.isFinite(t)) return t
  1490. }
  1491. if (typeof d.listTotal !== 'undefined') {
  1492. const t = tryNum(d.listTotal)
  1493. if (Number.isFinite(t)) return t
  1494. }
  1495. if (typeof d.total !== 'undefined') {
  1496. const t = tryNum(d.total)
  1497. if (Number.isFinite(t)) return t
  1498. }
  1499. }
  1500. if (typeof res.userTotal !== 'undefined') {
  1501. const t = tryNum(res.userTotal)
  1502. if (Number.isFinite(t)) return t
  1503. }
  1504. if (typeof res.total !== 'undefined') {
  1505. const t = tryNum(res.total)
  1506. if (Number.isFinite(t)) return t
  1507. }
  1508. return NaN
  1509. },
  1510. /** 运营 defaultPageInfo 条数 total(不参与用户分页 hasMore,仅缓存/排查) */
  1511. _pickFeaturedDefaultTotalFromRes(res) {
  1512. if (!res || res.code != 200) return NaN
  1513. const dpi = res.defaultPageInfo || (res.data && res.data.defaultPageInfo)
  1514. if (dpi && typeof dpi.total !== 'undefined') {
  1515. const t = Number(dpi.total)
  1516. return Number.isFinite(t) && t >= 0 ? t : NaN
  1517. }
  1518. return NaN
  1519. },
  1520. _syncFeaturedCommentTotalsFromRes(res) {
  1521. const u = this._pickFeaturedUserCommentTotalFromRes(res)
  1522. const dt = this._pickFeaturedDefaultTotalFromRes(res)
  1523. this.featuredCommentUserTotal = Number.isFinite(u) ? u : NaN
  1524. this.featuredCommentDefaultTotal = Number.isFinite(dt) ? dt : NaN
  1525. },
  1526. _mergeFeaturedUserComments(prev, incoming) {
  1527. const a = Array.isArray(prev) ? prev : []
  1528. const b = Array.isArray(incoming) ? incoming : []
  1529. const keyOf = (it) => {
  1530. if (!it) return ''
  1531. if (it.commentId != null && it.commentId !== '') return 'c:' + it.commentId
  1532. if (it.id != null && it.id !== '') return 'i:' + it.id
  1533. return ''
  1534. }
  1535. const seen = new Set()
  1536. const out = []
  1537. for (let i = 0; i < a.length; i++) {
  1538. const k = keyOf(a[i])
  1539. if (k) seen.add(k)
  1540. out.push(a[i])
  1541. }
  1542. for (let j = 0; j < b.length; j++) {
  1543. const it = b[j]
  1544. const k = keyOf(it)
  1545. if (k) {
  1546. if (seen.has(k)) continue
  1547. seen.add(k)
  1548. }
  1549. out.push(it)
  1550. }
  1551. return out
  1552. },
  1553. /**
  1554. * 是否还有下一页用户评论(仅按用户 total 与已合并的 featuredCommentList 长度比较;
  1555. * 运营 defaultPageInfo 另有 total,不参与此处 hasMore)。
  1556. */
  1557. _applyFeaturedCommentHasMore(res, pageUserList, mergedUserList) {
  1558. const pageLen = Array.isArray(pageUserList) ? pageUserList.length : 0
  1559. const mergedLen = Array.isArray(mergedUserList) ? mergedUserList.length : 0
  1560. const userTotal = this._pickFeaturedUserCommentTotalFromRes(res)
  1561. if (Number.isFinite(userTotal)) {
  1562. this.featuredCommentHasMore = mergedLen < userTotal
  1563. } else {
  1564. this.featuredCommentHasMore = pageLen >= this.featuredCommentPageSize
  1565. }
  1566. if (pageLen === 0) this.featuredCommentHasMore = false
  1567. },
  1568. onFeaturedCommentScrollToLower() {
  1569. if (this.tabIndex !== 0 || !this.showTabFeaturedComments) return
  1570. this.loadFeaturedComments({ append: true })
  1571. },
  1572. loadFeaturedComments(opts) {
  1573. if (!this.videoId || !this.courseId) {
  1574. return Promise.resolve()
  1575. }
  1576. const o = opts || {}
  1577. const scrollToBottom = !!o.scrollToBottom
  1578. const append = !!o.append
  1579. if (append) {
  1580. if (!this.featuredCommentHasMore) return Promise.resolve()
  1581. if (this.featuredCommentLoading || this.featuredCommentLoadingMore) return Promise.resolve()
  1582. this.featuredCommentLoadingMore = true
  1583. } else {
  1584. if (this.featuredCommentLoading) return Promise.resolve()
  1585. this.featuredCommentPageNum = 1
  1586. this.featuredCommentHasMore = true
  1587. this.featuredCommentLoading = true
  1588. }
  1589. const pageNum = this.featuredCommentPageNum
  1590. return getCommentList({
  1591. pageNum,
  1592. pageSize: this.featuredCommentPageSize,
  1593. courseId: this.courseId,
  1594. videoId:this.videoId,
  1595. }).then(res => {
  1596. if (append) {
  1597. this.featuredCommentLoadingMore = false
  1598. } else {
  1599. this.featuredCommentLoading = false
  1600. }
  1601. if (res.code == 200) {
  1602. const pageUser = this._pickUserCommentListFromCommentListRes(res)
  1603. this._syncFeaturedCommentTotalsFromRes(res)
  1604. if (!append) {
  1605. this.featuredCommentList = pageUser
  1606. this.defaultPageInfoList = this._pickDefaultPageInfoListFromCommentListRes(res)
  1607. this._applyFeaturedCommentHasMore(res, pageUser, this.featuredCommentList)
  1608. if (pageUser.length > 0) this.featuredCommentPageNum = pageNum + 1
  1609. } else {
  1610. this.featuredCommentList = this._mergeFeaturedUserComments(
  1611. this.featuredCommentList,
  1612. pageUser
  1613. )
  1614. this._applyFeaturedCommentHasMore(res, pageUser, this.featuredCommentList)
  1615. if (pageUser.length > 0) this.featuredCommentPageNum = pageNum + 1
  1616. }
  1617. } else {
  1618. if (!append) {
  1619. this.defaultPageInfoList = []
  1620. this.featuredCommentList = []
  1621. this.featuredCommentHasMore = false
  1622. this.featuredCommentUserTotal = NaN
  1623. this.featuredCommentDefaultTotal = NaN
  1624. }
  1625. }
  1626. this._syncRatingTabIndex()
  1627. if (scrollToBottom) {
  1628. this.$nextTick(() => {
  1629. this._scrollFeaturedCommentsToBottom()
  1630. setTimeout(() => this._scrollFeaturedCommentsToBottom(), 280)
  1631. })
  1632. }
  1633. }).catch(() => {
  1634. if (append) {
  1635. this.featuredCommentLoadingMore = false
  1636. } else {
  1637. this.featuredCommentLoading = false
  1638. this.featuredCommentList = []
  1639. this.defaultPageInfoList = []
  1640. this.featuredCommentHasMore = false
  1641. this.featuredCommentUserTotal = NaN
  1642. this.featuredCommentDefaultTotal = NaN
  1643. }
  1644. this._syncRatingTabIndex()
  1645. })
  1646. },
  1647. _scrollFeaturedCommentsToBottom() {
  1648. this.featuredCommentScrollIntoView = ''
  1649. this.$nextTick(() => {
  1650. this.featuredCommentScrollIntoView = 'fc-scroll-anchor'
  1651. setTimeout(() => {
  1652. this.featuredCommentScrollIntoView = ''
  1653. }, 400)
  1654. })
  1655. },
  1656. getCommentAvatar(it) {
  1657. if (!it) return ''
  1658. return it.avatar || it.headImg || it.headimg || it.userAvatar || ''
  1659. },
  1660. getCommentDisplayName(it) {
  1661. if (!it) return '用户'
  1662. if (it.nickName) return it.nickName
  1663. if (it.userId != null && it.userId !== '') {
  1664. return it.toNickName
  1665. }
  1666. },
  1667. _urlsEmbeddedInCommentContent(it) {
  1668. const c = it && it.content
  1669. if (c == null || typeof c !== 'string') return []
  1670. const text = String(c).replace(/\u21b5/g, '\n')
  1671. const m = text.match(/https?:\/\/[^\s]+/gi) || []
  1672. const cleaned = m.map(u => u.replace(/[),.;!?]+$/g, ''))
  1673. return [...new Set(cleaned)]
  1674. },
  1675. _isEmbeddedCommentVideoUrl(u) {
  1676. if (!u) return false
  1677. return /\/comment\/video\//i.test(u) || /\.(mp4|m3u8|mov|webm|avi|mkv)(\?|#|$)/i.test(u)
  1678. },
  1679. _isEmbeddedCommentImageUrl(u) {
  1680. if (!u) return false
  1681. return /\/comment\/image\//i.test(u) || /\.(jpg|jpeg|png|gif|webp|bmp)(\?|#|$)/i.test(u)
  1682. },
  1683. _videoUrlFromCommentContent(it) {
  1684. const urls = this._urlsEmbeddedInCommentContent(it)
  1685. for (let i = 0; i < urls.length; i++) {
  1686. if (this._isEmbeddedCommentVideoUrl(urls[i])) return urls[i]
  1687. }
  1688. return ''
  1689. },
  1690. getCommentTextOnly(it) {
  1691. if (!it || it.content == null) return ''
  1692. let text = String(it.content).replace(/\u21b5/g, '\n')
  1693. const strip = new Set()
  1694. const v = this.getCommentVideoUrl(it)
  1695. if (v) strip.add(v)
  1696. const urls = this._urlsEmbeddedInCommentContent(it)
  1697. for (let i = 0; i < urls.length; i++) {
  1698. const u = urls[i]
  1699. if (this._isEmbeddedCommentVideoUrl(u)) strip.add(u)
  1700. else if (this._isEmbeddedCommentImageUrl(u) && u !== v) strip.add(u)
  1701. }
  1702. strip.forEach(u => {
  1703. text = text.split(u).join('')
  1704. })
  1705. return text.replace(/(\r?\n\s*){2,}/g, '\n').replace(/^\s+|\s+$/g, '').trim()
  1706. },
  1707. getCommentVideoUrl(it) {
  1708. if (!it) return ''
  1709. if (it.videoUrl) return it.videoUrl
  1710. if (it.video) return it.video
  1711. if (it.mediaType === 'video' && it.mediaUrl) return it.mediaUrl
  1712. return this._videoUrlFromCommentContent(it)
  1713. },
  1714. getCommentVideoPoster(it) {
  1715. if (!it) return ''
  1716. const raw =
  1717. it.videoCover || it.coverUrl || it.poster || it.thumbnail || ''
  1718. return typeof raw === 'string' ? raw.trim() : String(raw || '').trim()
  1719. },
  1720. commentImageUrls(it) {
  1721. if (!it) return []
  1722. const raw = it.images || it.imageUrl || it.imgUrl || it.picUrl || it.pic
  1723. let fromFields = []
  1724. if (raw) {
  1725. if (Array.isArray(raw)) fromFields = raw.filter(Boolean)
  1726. else fromFields = String(raw).split(',').map(s => s.trim()).filter(Boolean)
  1727. }
  1728. const videoUrl = this.getCommentVideoUrl(it)
  1729. const fromContent = this._urlsEmbeddedInCommentContent(it).filter(u =>
  1730. this._isEmbeddedCommentImageUrl(u) && !this._isEmbeddedCommentVideoUrl(u) && u !== videoUrl
  1731. )
  1732. return [...new Set([...fromFields, ...fromContent])]
  1733. },
  1734. /** 精选留言预览图 / 选图 / 留言内嵌视频播放前:暂停主课视频,避免与原生页或第二条音轨叠放 */
  1735. _pauseCourseVideo() {
  1736. this._xgPause()
  1737. },
  1738. /** 精选留言带图/视频上传结束后:若选媒体前主课在播则恢复 */
  1739. _resumeCourseVideoAfterFeaturedJob() {
  1740. if (!this._shouldResumeCourseVideoAfterFeaturedUpload) return
  1741. this._shouldResumeCourseVideoAfterFeaturedUpload = false
  1742. this._courseWasPlayingBeforeFeaturedCommentOpen = false
  1743. if (this.isEnded || !this.videoUrl || !this.videoId) return
  1744. const tryPlay = () => this._xgPlay(this.playTime || 0)
  1745. this.$nextTick(() => {
  1746. tryPlay()
  1747. setTimeout(tryPlay, 80)
  1748. setTimeout(tryPlay, 260)
  1749. })
  1750. },
  1751. getFeaturedCommentRowKey(it, idx) {
  1752. const id =
  1753. it && it.commentId != null && it.commentId !== ''
  1754. ? String(it.commentId)
  1755. : it && it.id != null
  1756. ? String(it.id)
  1757. : ''
  1758. return 'fc-' + idx + '-' + (id || 'n')
  1759. },
  1760. getFeaturedCommentVideoId(it, idx) {
  1761. return 'feat-comment-video-' + this.getFeaturedCommentRowKey(it, idx)
  1762. },
  1763. /** 暂停精选留言列表内所有西瓜/原生内联播放器(弹窗预览仍用独立 video) */
  1764. _pauseFeaturedCommentListInlinePlayers() {
  1765. const refs = this.$refs.featuredCommentXg
  1766. if (!refs) return
  1767. const arr = Array.isArray(refs) ? refs : [refs]
  1768. arr.forEach((r) => {
  1769. if (r && typeof r.pause === 'function') r.pause()
  1770. })
  1771. },
  1772. openFeaturedCommentMediaPreview(it, listIdx, mode, imageIndex) {
  1773. const m = mode === 'video' ? 'video' : 'image'
  1774. if (m === 'image') {
  1775. const urls = this.commentImageUrls(it).filter(u => u && !/^blob:/i.test(u))
  1776. if (!urls.length) return
  1777. const i = Math.min(
  1778. Math.max(0, Number(imageIndex) || 0),
  1779. urls.length - 1
  1780. )
  1781. this._pauseCourseVideo()
  1782. this._pauseFeaturedCommentListInlinePlayers()
  1783. this.featuredCommentMediaPreviewMode = 'image'
  1784. this.featuredCommentMediaPreviewUrls = urls
  1785. this.featuredCommentMediaPreviewIndex = i
  1786. this.featuredCommentMediaPreviewVideoUrl = ''
  1787. this.featuredCommentMediaPreviewPoster = ''
  1788. this.featuredCommentMediaPreviewShow = true
  1789. return
  1790. }
  1791. const vurl = this.getCommentVideoUrl(it)
  1792. if (!vurl) return
  1793. this._pauseCourseVideo()
  1794. this._pauseFeaturedCommentListInlinePlayers()
  1795. this.featuredCommentMediaPreviewMode = 'video'
  1796. this.featuredCommentMediaPreviewUrls = []
  1797. this.featuredCommentMediaPreviewIndex = 0
  1798. this.featuredCommentMediaPreviewVideoUrl = vurl
  1799. this.featuredCommentMediaPreviewPoster = this.getCommentVideoPoster(it) || ''
  1800. this.featuredCommentMediaPreviewShow = true
  1801. this._tryPlayFeaturedModalVideo()
  1802. },
  1803. _tryPlayFeaturedModalVideo() {
  1804. if (this.featuredCommentMediaPreviewMode !== 'video' || !this.featuredCommentMediaPreviewVideoUrl) {
  1805. return
  1806. }
  1807. const run = () => {
  1808. try {
  1809. const c = uni.createVideoContext('fc-media-preview-video', this)
  1810. if (c && c.play) c.play()
  1811. } catch (e) {}
  1812. }
  1813. this.$nextTick(() => {
  1814. run()
  1815. setTimeout(run, 100)
  1816. setTimeout(run, 280)
  1817. })
  1818. },
  1819. /** 微信小程序编译不接受 @tap.stop="",需绑定空实现以阻止冒泡到遮罩 */
  1820. featuredCommentPreviewCardTap() {},
  1821. closeFeaturedCommentMediaPreview() {
  1822. if (!this.featuredCommentMediaPreviewShow) return
  1823. try {
  1824. const c = uni.createVideoContext('fc-media-preview-video', this)
  1825. if (c && c.pause) c.pause()
  1826. } catch (e) {}
  1827. this.featuredCommentMediaPreviewShow = false
  1828. this.featuredCommentMediaPreviewUrls = []
  1829. this.featuredCommentMediaPreviewVideoUrl = ''
  1830. this.featuredCommentMediaPreviewPoster = ''
  1831. },
  1832. setFeaturedMediaPreviewIndex(ti) {
  1833. const n = Number(ti)
  1834. if (
  1835. !Number.isFinite(n) ||
  1836. n < 0 ||
  1837. n >= (this.featuredCommentMediaPreviewUrls || []).length
  1838. ) {
  1839. return
  1840. }
  1841. this.featuredCommentMediaPreviewIndex = n
  1842. },
  1843. onFeaturedMediaPreviewModalVideoPlay() {
  1844. this._pauseCourseVideo()
  1845. this._pauseFeaturedCommentListInlinePlayers()
  1846. },
  1847. _featuredMediaMaxBytes() {
  1848. return 100 * 1024 * 1024
  1849. },
  1850. /** 相册/选中返回的本地路径大小:安卓上 chooseMedia 常给 size=0,getFileInfo 也可能失真,改用 statSync 兜底 */
  1851. _readFeaturedLocalFileSizeBytes(filePath) {
  1852. return new Promise((resolve) => {
  1853. const tryFsStat = () => {
  1854. let n = 0
  1855. // #ifdef MP-WEIXIN
  1856. try {
  1857. const fs = wx.getFileSystemManager && wx.getFileSystemManager()
  1858. if (fs && fs.statSync) {
  1859. const st = fs.statSync(filePath)
  1860. if (st && typeof st.size === 'number' && st.size > 0) n = st.size
  1861. }
  1862. } catch (e) {}
  1863. // #endif
  1864. return n
  1865. }
  1866. if (!filePath || typeof filePath !== 'string') {
  1867. resolve(0)
  1868. return
  1869. }
  1870. uni.getFileInfo({
  1871. filePath,
  1872. success: (fi) => {
  1873. let sz =
  1874. fi && fi.size != null ? Math.floor(Number(fi.size)) : 0
  1875. if (!Number.isFinite(sz) || sz <= 0) sz = tryFsStat()
  1876. resolve(sz)
  1877. },
  1878. fail: () => resolve(tryFsStat())
  1879. })
  1880. })
  1881. },
  1882. _alertFeaturedMediaLimit(content) {
  1883. uni.showModal({
  1884. title: '提示',
  1885. content:
  1886. content ||
  1887. '图片或视频大小不能超过100MB,请压缩或更换后再试。',
  1888. showCancel: false,
  1889. confirmText: '知道了'
  1890. })
  1891. },
  1892. _assertFeaturedLocalMediaUnderMaxBytes(filePath) {
  1893. const max = this._featuredMediaMaxBytes()
  1894. return new Promise((resolve) => {
  1895. if (!filePath || typeof filePath !== 'string') {
  1896. this._alertFeaturedMediaLimit('文件路径无效,请重新选择。')
  1897. resolve(false)
  1898. return
  1899. }
  1900. this._readFeaturedLocalFileSizeBytes(filePath).then((sz) => {
  1901. if (sz <= 0) {
  1902. uni.showToast({
  1903. title: '本地暂无法读取大小,请勿超过100MB',
  1904. icon: 'none',
  1905. duration: 2600
  1906. })
  1907. resolve(true)
  1908. return
  1909. }
  1910. if (sz > max) {
  1911. this._alertFeaturedMediaLimit()
  1912. resolve(false)
  1913. return
  1914. }
  1915. resolve(true)
  1916. })
  1917. })
  1918. },
  1919. _applyFeaturedMediaIfSizeOk(type, path, size, thumbTempPath) {
  1920. const max = this._featuredMediaMaxBytes()
  1921. const applyAccepted = () => {
  1922. this.featuredCommentMedia = {
  1923. type,
  1924. path,
  1925. thumbTempPath: thumbTempPath || ''
  1926. }
  1927. }
  1928. let bytes =
  1929. size != null && Number(size) > 0
  1930. ? Math.floor(Number(size))
  1931. : 0
  1932. if (!Number.isFinite(bytes)) bytes = 0
  1933. const finalize = (n) => {
  1934. if (n > max) {
  1935. this._alertFeaturedMediaLimit()
  1936. return
  1937. }
  1938. applyAccepted()
  1939. }
  1940. if (bytes > 0) {
  1941. finalize(bytes)
  1942. return
  1943. }
  1944. this._readFeaturedLocalFileSizeBytes(path).then(finalize)
  1945. },
  1946. clearFeaturedCommentMedia() {
  1947. this.featuredCommentMedia = null
  1948. this._resumeCourseVideoAfterFeaturedJob()
  1949. },
  1950. /**
  1951. * 竖屏课且仅「精选留言」一个 Tab 时,竖版布局下留言区被隐藏;
  1952. * 切到横版布局(isShu=false),与全屏按钮切换布局一致。
  1953. */
  1954. _expandLayoutIfPortraitFeaturedCommentsOnly() {
  1955. if (this.displayType !== 'portrait') return
  1956. if (!this.isShu) return
  1957. if (!(this.visibleRatingTabCount === 1 && this.showTabFeaturedComments)) return
  1958. this.isShu = false
  1959. this.tabIndex = 0
  1960. },
  1961. /** 避免选图/选视频从原生页返回时 onShow 重复拉课程详情;若未触发 onShow 则在 complete 后兜底清除标志 */
  1962. _armFeaturedMediaPickerOnShowSuppress() {
  1963. this.videovipSuppressOnShowCourseFetchOnce = true
  1964. const clearIfStillArmed = () => {
  1965. setTimeout(() => {
  1966. if (this.videovipSuppressOnShowCourseFetchOnce) {
  1967. this.videovipSuppressOnShowCourseFetchOnce = false
  1968. }
  1969. }, 600)
  1970. }
  1971. return clearIfStillArmed
  1972. },
  1973. onFeaturedPickMedia() {
  1974. this._expandLayoutIfPortraitFeaturedCommentsOnly()
  1975. this.featuredMediaActionSheetShow = true
  1976. },
  1977. closeFeaturedMediaActionSheet() {
  1978. this.featuredMediaActionSheetShow = false
  1979. },
  1980. /** 精选留言选相册视频:小程序用 chooseMedia,App/H5 用 chooseVideo */
  1981. _pickFeaturedAlbumVideo(clearIfStillArmed) {
  1982. const onFail = () => {
  1983. // 勿在此处清 suppress:须由 onShow 消费,避免先于 onShow 清标志导致停跑马灯
  1984. this._resumeCourseVideoAfterFeaturedJob()
  1985. }
  1986. const onSuccess = (path, size, thumbTempPath) => {
  1987. if (!path) return
  1988. const sz = size != null ? size : 0
  1989. this._applyFeaturedMediaIfSizeOk('video', path, sz, thumbTempPath || '')
  1990. }
  1991. // #ifdef MP-WEIXIN
  1992. wx.chooseMedia({
  1993. count: 1,
  1994. mediaType: ['video'],
  1995. sourceType: ['album'],
  1996. maxDuration: 30,
  1997. success: (res) => {
  1998. const f0 = res.tempFiles && res.tempFiles[0]
  1999. if (!f0) return
  2000. onSuccess(f0.tempFilePath, f0.size, f0.thumbTempFilePath)
  2001. },
  2002. fail: onFail,
  2003. complete: clearIfStillArmed
  2004. })
  2005. // #endif
  2006. // #ifndef MP-WEIXIN
  2007. uni.chooseVideo({
  2008. sourceType: ['album'],
  2009. maxDuration: 30,
  2010. success: (res) => {
  2011. onSuccess(res.tempFilePath, res.size, res.thumbTempFilePath)
  2012. },
  2013. fail: onFail,
  2014. complete: clearIfStillArmed
  2015. })
  2016. // #endif
  2017. },
  2018. onFeaturedMediaSheetPick(tapIndex) {
  2019. this.closeFeaturedMediaActionSheet()
  2020. // 立即调起相册/视频会与 u-popup 遮罩关闭叠加,小程序端易残留半透明层;对齐关闭动画后再打开系统选择器
  2021. const afterSheetCloseMs = 350
  2022. setTimeout(() => {
  2023. if (tapIndex === 0) {
  2024. this._shouldResumeCourseVideoAfterFeaturedUpload =
  2025. !this.isEnded &&
  2026. (this.videoPlaying || this._courseWasPlayingBeforeFeaturedCommentOpen)
  2027. this._pauseCourseVideo()
  2028. const clearIfStillArmed = this._armFeaturedMediaPickerOnShowSuppress()
  2029. uni.chooseImage({
  2030. count: 1,
  2031. sizeType: ['compressed', 'original'],
  2032. sourceType: ['album', 'camera'],
  2033. success: (res) => {
  2034. const f0 = res.tempFiles && res.tempFiles[0]
  2035. const path =
  2036. (res.tempFilePaths && res.tempFilePaths[0]) ||
  2037. (f0 && f0.path) ||
  2038. ''
  2039. if (!path) return
  2040. const size = f0 && f0.size != null ? f0.size : 0
  2041. this._applyFeaturedMediaIfSizeOk('image', path, size, '')
  2042. },
  2043. fail: () => {
  2044. // 勿在此处清 suppress:须由 onShow 消费,避免先于 onShow 清标志导致停跑马灯
  2045. this._resumeCourseVideoAfterFeaturedJob()
  2046. },
  2047. complete: clearIfStillArmed
  2048. })
  2049. } else if (tapIndex === 1) {
  2050. this._shouldResumeCourseVideoAfterFeaturedUpload =
  2051. !this.isEnded &&
  2052. (this.videoPlaying || this._courseWasPlayingBeforeFeaturedCommentOpen)
  2053. this._pauseCourseVideo()
  2054. const clearIfStillArmed = this._armFeaturedMediaPickerOnShowSuppress()
  2055. this._pickFeaturedAlbumVideo(clearIfStillArmed)
  2056. }
  2057. }, afterSheetCloseMs)
  2058. },
  2059. async submitFeaturedComment() {
  2060. const raw = this.featuredCommentInput || ''
  2061. const text = raw.trim()
  2062. if (raw.length > 50) {
  2063. uni.showToast({
  2064. title: '留言不能超过50个字',
  2065. icon: 'none'
  2066. })
  2067. this._resumeCourseVideoAfterFeaturedJob()
  2068. return
  2069. }
  2070. const media = this.featuredCommentMedia
  2071. const hasMedia = !!(media && media.path)
  2072. if (!text && !hasMedia) {
  2073. uni.showToast({
  2074. title: '请输入留言或选择图片/视频',
  2075. icon: 'none'
  2076. })
  2077. return
  2078. }
  2079. const u = this._resolveUserInfoObj()
  2080. const userId = u.userId || ''
  2081. if (!userId) {
  2082. uni.showToast({
  2083. title: '请先登录',
  2084. icon: 'none'
  2085. })
  2086. this._resumeCourseVideoAfterFeaturedJob()
  2087. return
  2088. }
  2089. if (this.featuredCommentSending) return
  2090. if (hasMedia && media.path) {
  2091. const ok = await this._assertFeaturedLocalMediaUnderMaxBytes(media.path)
  2092. if (!ok) {
  2093. this._resumeCourseVideoAfterFeaturedJob()
  2094. return
  2095. }
  2096. }
  2097. this.featuredCommentSending = true
  2098. const done = (res) => {
  2099. this.featuredCommentSending = false
  2100. if (res.code == 200) {
  2101. this.featuredCommentInput = ''
  2102. this.featuredCommentMedia = null
  2103. this._closeFeaturedCommentComposerUi()
  2104. uni.showToast({
  2105. title: '发送成功',
  2106. icon: 'none'
  2107. })
  2108. this.loadFeaturedComments({
  2109. scrollToBottom: true
  2110. })
  2111. } else {
  2112. uni.showToast({
  2113. title: res.msg || '发送失败',
  2114. icon: 'none'
  2115. })
  2116. }
  2117. }
  2118. const fail = (err) => {
  2119. this.featuredCommentSending = false
  2120. const msg = err && err.message ? err.message : '网络异常,请重试'
  2121. uni.showToast({
  2122. title: msg,
  2123. icon: 'none'
  2124. })
  2125. }
  2126. const payload = {
  2127. toUserId: userId,
  2128. courseId: this.courseId,
  2129. videoId: this.videoId,
  2130. type: 1,
  2131. content: text
  2132. }
  2133. if (hasMedia) {
  2134. if (media.type === 'video') {
  2135. payload.videoFiles = media.path
  2136. } else {
  2137. payload.imageFiles = media.path
  2138. }
  2139. }
  2140. uni.showLoading({
  2141. title: '发布中',
  2142. mask: true
  2143. })
  2144. addCommentWithMedia(payload)
  2145. .then((res) => {
  2146. uni.hideLoading()
  2147. done(res)
  2148. })
  2149. .catch((err) => {
  2150. uni.hideLoading()
  2151. fail(err)
  2152. })
  2153. .finally(() => {
  2154. // 与 hideLoading、列表刷新错开一帧,避免部分机型上 play 被吞
  2155. this.$nextTick(() => {
  2156. setTimeout(() => {
  2157. this._resumeCourseVideoAfterFeaturedJob()
  2158. }, 60)
  2159. })
  2160. })
  2161. },
  2162. _vipTimeStrToSec(timeStr) {
  2163. if (!timeStr || typeof timeStr !== 'string') return 0
  2164. const parts = timeStr.split(':')
  2165. if (parts.length !== 3) return 0
  2166. const h = parseInt(parts[0], 10) || 0
  2167. const m = parseInt(parts[1], 10) || 0
  2168. const s = parseInt(parts[2], 10) || 0
  2169. return h * 3600 + m * 60 + s
  2170. },
  2171. _vipShelfTimeValid(timeStr) {
  2172. if (!timeStr || typeof timeStr !== 'string') return false
  2173. if (timeStr === '00:00:00') return false
  2174. return this._vipTimeStrToSec(timeStr) > 0
  2175. },
  2176. /** 缓存下单时间戳(videovip_myPurchase_*)序列化后可能为字符串,统一为毫秒数 */
  2177. _normalizePurchaseTs(ts) {
  2178. if (ts == null || ts === '') return 0
  2179. const n = typeof ts === 'number' ? ts : Number(ts)
  2180. return Number.isFinite(n) && n > 0 ? n : 0
  2181. },
  2182. _readVipSelfPurchaseTsMs() {
  2183. const row = uni.getStorageSync('videovip_myPurchase_' + this.videoId)
  2184. return this._normalizePurchaseTs(row && row.ts)
  2185. },
  2186. refreshVipPurchaseMarqueeFlag() {
  2187. const key = 'videovip_myPurchase_' + this.videoId
  2188. const consumedKey = 'videovip_myPurchaseMarqueeConsumed_' + this.videoId
  2189. const row = uni.getStorageSync(key)
  2190. const consumed = uni.getStorageSync(consumedKey)
  2191. const tsMs = this._normalizePurchaseTs(row && row.ts)
  2192. if (tsMs && Date.now() - tsMs <= 10 * 60 * 1000) {
  2193. // 同一笔下单 ts 仅允许跑马灯展示一次;中途退出再进不再展示(新下单 ts 变化后会再展示一次)
  2194. const consumedTs = this._normalizePurchaseTs(consumed && consumed.ts)
  2195. this.pendingSelfMarqueeFirst = !(consumedTs > 0 && consumedTs === tsMs)
  2196. } else {
  2197. this.pendingSelfMarqueeFirst = false
  2198. if (row && row.ts != null && row.ts !== '') {
  2199. uni.removeStorageSync(key)
  2200. const consumedTs = this._normalizePurchaseTs(consumed && consumed.ts)
  2201. if (consumedTs > 0 && consumedTs === tsMs) uni.removeStorageSync(consumedKey)
  2202. }
  2203. }
  2204. },
  2205. _markSelfPurchaseMarqueeConsumed() {
  2206. const tsMs = this._readVipSelfPurchaseTsMs()
  2207. if (!tsMs) return
  2208. try {
  2209. uni.setStorageSync('videovip_myPurchaseMarqueeConsumed_' + this.videoId, { ts: tsMs })
  2210. } catch (e) {}
  2211. },
  2212. formatSelfPurchaseMarqueePayload() {
  2213. const tsMs = this._readVipSelfPurchaseTsMs()
  2214. if (!tsMs) {
  2215. return {
  2216. type: 'self',
  2217. nickname: '您本人',
  2218. maskedId: '',
  2219. timeOffset: '刚刚',
  2220. suffix: '已下单',
  2221. fullText: '您本人刚刚已下单'
  2222. }
  2223. }
  2224. const secTotal = Math.floor((Date.now() - tsMs) / 1000)
  2225. if (secTotal < 60) {
  2226. const text = `${Math.max(1, secTotal)}秒前`
  2227. return {
  2228. type: 'self',
  2229. nickname: '您本人',
  2230. maskedId: '',
  2231. timeOffset: text,
  2232. suffix: '已下单',
  2233. fullText: `您本人${text}已下单`
  2234. }
  2235. }
  2236. const min = Math.min(10, Math.floor(secTotal / 60))
  2237. const text = `${Math.max(1, min)}分钟前`
  2238. return {
  2239. type: 'self',
  2240. nickname: '您本人',
  2241. maskedId: '',
  2242. timeOffset: text,
  2243. suffix: '已下单',
  2244. fullText: `您本人${text}已下单`
  2245. }
  2246. },
  2247. _syncFakeMarqueeIfEligibleChanged() {
  2248. const val = this.fakeMarqueeEligible
  2249. if (val === this._fakeMarqueeEligiblePrev) return
  2250. this._fakeMarqueeEligiblePrev = val
  2251. if (val) {
  2252. if (this._marqueeStartDebounceTimer) {
  2253. clearTimeout(this._marqueeStartDebounceTimer)
  2254. this._marqueeStartDebounceTimer = null
  2255. }
  2256. this._marqueeStartDebounceTimer = setTimeout(() => {
  2257. this._marqueeStartDebounceTimer = null
  2258. if (this.fakeMarqueeEligible && !this._fakeMarqueeStarted) {
  2259. this.ensureFakeMarqueeStarted()
  2260. }
  2261. }, 300)
  2262. } else {
  2263. if (this._marqueeStartDebounceTimer) {
  2264. clearTimeout(this._marqueeStartDebounceTimer)
  2265. this._marqueeStartDebounceTimer = null
  2266. }
  2267. this.stopFakeMarqueeLoop()
  2268. }
  2269. },
  2270. ensureFakeMarqueeStarted() {
  2271. if (!this.fakeMarqueeEligible || this._fakeMarqueeStarted) return
  2272. this._fakeMarqueeStarted = true
  2273. if (!this.fakeOrderPool.length) {
  2274. this.fakeOrderPool = buildFakeOrderPool()
  2275. }
  2276. this.applyFakeMarqueeTick()
  2277. this.scheduleFakeMarqueeNext()
  2278. this._startSelfMarqueeClockIfNeeded()
  2279. },
  2280. applyFakeMarqueeTick() {
  2281. if (this.isTripleMarqueeMode) {
  2282. const rows = this.buildTripleMarqueePayloads()
  2283. this.setTripleMarqueeDisplayWithAnim(rows)
  2284. // 本人下单在三联模式也只展示一轮,下一轮起恢复纯虚拟随机
  2285. if (rows.some(item => item && item.type === 'self')) {
  2286. this.pendingSelfMarqueeFirst = false
  2287. this._markSelfPurchaseMarqueeConsumed()
  2288. }
  2289. if (rows.length) {
  2290. this.marqueeDisplayPayload = rows[0]
  2291. this.marqueeDisplaySelf = this.marqueeDisplayPayload.type === 'self'
  2292. }
  2293. return
  2294. }
  2295. this.marqueeTriplePayloads = []
  2296. this.marqueeTripleIncomingPayloads = []
  2297. this.marqueeTripleSwitching = false
  2298. const tsMs = this._readVipSelfPurchaseTsMs()
  2299. const purchaseOk = !!(tsMs && Date.now() - tsMs <= 10 * 60 * 1000)
  2300. if (purchaseOk && this.pendingSelfMarqueeFirst) {
  2301. const selfPayload = this.formatSelfPurchaseMarqueePayload()
  2302. this.marqueeCurrentText = selfPayload.fullText
  2303. this.marqueeSelfHighlight = true
  2304. this.pendingSelfMarqueeFirst = false
  2305. this._markSelfPurchaseMarqueeConsumed()
  2306. this.setMarqueeDisplayWithAnim(selfPayload, true)
  2307. return
  2308. }
  2309. this.marqueeSelfHighlight = false
  2310. if (!this.fakeOrderPool.length) {
  2311. this.fakeOrderPool = buildFakeOrderPool()
  2312. }
  2313. const line = this.fakeOrderPool.shift()
  2314. if (line) {
  2315. this.fakeOrderPool.push(line)
  2316. this.marqueeCurrentText = `${line.nickname}${line.maskedId}${line.timeOffset}${line.suffix}`
  2317. this.setMarqueeDisplayWithAnim({
  2318. type: 'virtual',
  2319. nickname: line.nickname,
  2320. maskedId: line.maskedId,
  2321. timeOffset: line.timeOffset,
  2322. suffix: line.suffix
  2323. }, false)
  2324. }
  2325. },
  2326. buildTripleMarqueePayloads() {
  2327. const rows = []
  2328. const hasSelf = this.hasRecentSelfPurchase() && this.pendingSelfMarqueeFirst
  2329. if (!this.fakeOrderPool.length) {
  2330. this.fakeOrderPool = buildFakeOrderPool()
  2331. }
  2332. let guard = 0
  2333. // 有本人记录时,把本人下单放第一条,其它再补足虚拟数据
  2334. if (hasSelf) {
  2335. rows.push(this.formatSelfPurchaseMarqueePayload())
  2336. }
  2337. const targetVirtualCount = hasSelf ? 2 : 3
  2338. let virtualCount = 0
  2339. while (virtualCount < targetVirtualCount && this.fakeOrderPool.length && guard < 10) {
  2340. const line = this.fakeOrderPool.shift()
  2341. guard += 1
  2342. if (!line) break
  2343. this.fakeOrderPool.push(line)
  2344. rows.push({
  2345. type: 'virtual',
  2346. nickname: line.nickname,
  2347. maskedId: line.maskedId,
  2348. timeOffset: line.timeOffset,
  2349. suffix: line.suffix
  2350. })
  2351. virtualCount += 1
  2352. }
  2353. return rows.slice(0, 3)
  2354. },
  2355. hasRecentSelfPurchase() {
  2356. const tsMs = this._readVipSelfPurchaseTsMs()
  2357. return !!(tsMs && Date.now() - tsMs <= 10 * 60 * 1000)
  2358. },
  2359. /** 按缓存 ts 与当前时间重新计算本人下单文案(停留同一轮跑马灯时也能更新相对时间) */
  2360. refreshSelfPurchaseMarqueeDisplayedTiming() {
  2361. const fresh = this.formatSelfPurchaseMarqueePayload()
  2362. if (this.isTripleMarqueeMode && this.marqueeTriplePayloads.length) {
  2363. const upd = (arr) =>
  2364. arr.map((r) => (r && r.type === 'self' ? Object.assign({}, fresh) : r))
  2365. this.marqueeTriplePayloads = upd(this.marqueeTriplePayloads)
  2366. if (this.marqueeTripleIncomingPayloads.length) {
  2367. this.marqueeTripleIncomingPayloads = upd(this.marqueeTripleIncomingPayloads)
  2368. }
  2369. }
  2370. if (this.marqueeDisplayPayload && this.marqueeDisplayPayload.type === 'self') {
  2371. this.marqueeDisplayPayload = Object.assign({}, fresh)
  2372. }
  2373. if (this.marqueeIncomingPayload && this.marqueeIncomingPayload.type === 'self') {
  2374. this.marqueeIncomingPayload = Object.assign({}, fresh)
  2375. }
  2376. },
  2377. _startSelfMarqueeClockIfNeeded() {
  2378. if (this._selfMarqueeClockTimer != null) return
  2379. if (!this.hasRecentSelfPurchase()) return
  2380. this._selfMarqueeClockTimer = setInterval(() => {
  2381. if (!this._fakeMarqueeStarted || !this.hasRecentSelfPurchase()) {
  2382. this._stopSelfMarqueeClock()
  2383. return
  2384. }
  2385. this.refreshSelfPurchaseMarqueeDisplayedTiming()
  2386. }, 10000)
  2387. },
  2388. _stopSelfMarqueeClock() {
  2389. if (this._selfMarqueeClockTimer != null) {
  2390. clearInterval(this._selfMarqueeClockTimer)
  2391. this._selfMarqueeClockTimer = null
  2392. }
  2393. },
  2394. /** 进入三联布局时立即补足 marqueeTriplePayloads,否则 v-if 会因 length 为 0 仍走单条分支直到下一轮定时 tick */
  2395. syncTripleMarqueeImmediate() {
  2396. if (!this.fakeMarqueeEligible || !this._fakeMarqueeStarted || !this.isTripleMarqueeMode) return
  2397. const rows = this.buildTripleMarqueePayloads()
  2398. if (!rows.length) return
  2399. this.setTripleMarqueeDisplayWithAnim(rows)
  2400. if (rows.some(item => item && item.type === 'self')) {
  2401. this.pendingSelfMarqueeFirst = false
  2402. this._markSelfPurchaseMarqueeConsumed()
  2403. }
  2404. if (rows[0]) {
  2405. this.marqueeDisplayPayload = rows[0]
  2406. this.marqueeDisplaySelf = rows[0].type === 'self'
  2407. }
  2408. },
  2409. setTripleMarqueeDisplayWithAnim(rows) {
  2410. if (!rows || !rows.length) return
  2411. if (!this.marqueeTriplePayloads.length) {
  2412. this.marqueeTriplePayloads = rows
  2413. this.marqueeTripleIncomingPayloads = []
  2414. this.marqueeTripleSwitching = false
  2415. return
  2416. }
  2417. if (this._marqueeTripleSwitchTimer) {
  2418. clearTimeout(this._marqueeTripleSwitchTimer)
  2419. this._marqueeTripleSwitchTimer = null
  2420. }
  2421. this.marqueeTripleIncomingPayloads = rows
  2422. this.marqueeTripleSwitching = true
  2423. this._marqueeTripleSwitchTimer = setTimeout(() => {
  2424. this.marqueeTriplePayloads = this.marqueeTripleIncomingPayloads
  2425. this.marqueeTripleIncomingPayloads = []
  2426. this.marqueeTripleSwitching = false
  2427. this._marqueeTripleSwitchTimer = null
  2428. }, 460)
  2429. },
  2430. setMarqueeDisplayWithAnim(payload, isSelf) {
  2431. if (!payload) return
  2432. if (!this.marqueeDisplayPayload) {
  2433. this.marqueeDisplayPayload = payload
  2434. this.marqueeDisplaySelf = !!isSelf
  2435. this.marqueeSwitching = false
  2436. this.marqueeIncomingPayload = null
  2437. this.marqueeIncomingSelf = false
  2438. return
  2439. }
  2440. if (this._marqueeSwitchTimer) {
  2441. clearTimeout(this._marqueeSwitchTimer)
  2442. this._marqueeSwitchTimer = null
  2443. }
  2444. this.marqueeIncomingPayload = payload
  2445. this.marqueeIncomingSelf = !!isSelf
  2446. this.marqueeSwitching = true
  2447. this._marqueeSwitchTimer = setTimeout(() => {
  2448. this.marqueeDisplayPayload = this.marqueeIncomingPayload
  2449. this.marqueeDisplaySelf = this.marqueeIncomingSelf
  2450. this.marqueeIncomingPayload = null
  2451. this.marqueeIncomingSelf = false
  2452. this.marqueeSwitching = false
  2453. this._marqueeSwitchTimer = null
  2454. }, 460)
  2455. },
  2456. scheduleFakeMarqueeNext() {
  2457. if (this._fakeMarqueeTimer) {
  2458. clearTimeout(this._fakeMarqueeTimer)
  2459. this._fakeMarqueeTimer = null
  2460. }
  2461. const delay = 4000 + Math.floor(Math.random() * 3001)
  2462. this._fakeMarqueeTimer = setTimeout(() => {
  2463. this._fakeMarqueeTimer = null
  2464. if (!this.fakeMarqueeEligible || !this._fakeMarqueeStarted) return
  2465. this.applyFakeMarqueeTick()
  2466. this.scheduleFakeMarqueeNext()
  2467. }, delay)
  2468. },
  2469. stopFakeMarqueeLoop() {
  2470. if (this._marqueeStartDebounceTimer) {
  2471. clearTimeout(this._marqueeStartDebounceTimer)
  2472. this._marqueeStartDebounceTimer = null
  2473. }
  2474. if (this._fakeMarqueeTimer) {
  2475. clearTimeout(this._fakeMarqueeTimer)
  2476. this._fakeMarqueeTimer = null
  2477. }
  2478. if (this._marqueeSwitchTimer) {
  2479. clearTimeout(this._marqueeSwitchTimer)
  2480. this._marqueeSwitchTimer = null
  2481. }
  2482. if (this._marqueeTripleSwitchTimer) {
  2483. clearTimeout(this._marqueeTripleSwitchTimer)
  2484. this._marqueeTripleSwitchTimer = null
  2485. }
  2486. this._fakeMarqueeStarted = false
  2487. this.marqueeSwitching = false
  2488. // 重要:清空非三联跑马灯展示 payload,避免下一次门禁打开前渲染旧内容
  2489. this.marqueeDisplayPayload = null
  2490. this.marqueeDisplaySelf = false
  2491. this.marqueeCurrentText = ''
  2492. this.marqueeSelfHighlight = false
  2493. this.marqueeIncomingPayload = null
  2494. this.marqueeIncomingSelf = false
  2495. this.marqueeTriplePayloads = []
  2496. this.marqueeTripleIncomingPayloads = []
  2497. this.marqueeTripleSwitching = false
  2498. this._stopSelfMarqueeClock()
  2499. },
  2500. getRateByIndexId(indexId, optionIdx) {
  2501. const list = this.rateList || []
  2502. if (!list.length) return null
  2503. const n = Number(indexId)
  2504. if (Number.isFinite(n)) {
  2505. const byId = list.find((r) => r && Number(r.id) === n)
  2506. if (byId) return byId
  2507. if (list[n]) return list[n]
  2508. }
  2509. const i = Number(optionIdx)
  2510. if (Number.isFinite(i) && list[i]) return list[i]
  2511. return list[0]
  2512. },
  2513. getRateOptionIcon(option, active, optionIdx) {
  2514. const rate = this.getRateByIndexId(option && option.indexId, optionIdx)
  2515. if (!rate) return ''
  2516. return active ? (rate.selIcon || rate.icon || '') : (rate.icon || '')
  2517. },
  2518. isRateOptionSelected(item, option) {
  2519. if (!item || !option) return false
  2520. if (Number(item.type) === 2) {
  2521. const answer = item.answer ? item.answer.split(',') : []
  2522. return answer.indexOf(option.name) !== -1
  2523. }
  2524. return item.answer === option.name
  2525. },
  2526. goRate(questionIndex, optionIndexId) {
  2527. const bounceKey = `${questionIndex}_${optionIndexId}`
  2528. if (this._rateBounceTimer) {
  2529. clearTimeout(this._rateBounceTimer)
  2530. this._rateBounceTimer = null
  2531. }
  2532. this.rateBounceIndex = null
  2533. this.$nextTick(() => {
  2534. this.rateBounceIndex = bounceKey
  2535. this._rateBounceTimer = setTimeout(() => {
  2536. this.rateBounceIndex = null
  2537. this._rateBounceTimer = null
  2538. }, 480)
  2539. })
  2540. },
  2541. onVideoMetaLoaded(e) {
  2542. // const width = e.detail.width;
  2543. // const height = e.detail.height;
  2544. // const res = uni.getSystemInfoSync();
  2545. // const winH = res.windowHeight;
  2546. // const winW = res.windowWidth;
  2547. // //横屏
  2548. // const wW = (winW / winH) * 100;
  2549. // console.log(winH,winW,'高度')
  2550. // console.log(winH,winW,'高度')
  2551. // //竖屏
  2552. // if (height < width) {
  2553. // this.isHeight = true
  2554. // }
  2555. },
  2556. // 获取视频容器高度
  2557. getVideoContainerHeight() {
  2558. const { windowWidth, windowHeight } = uni.getSystemInfoSync();
  2559. this.isHeight = windowWidth > windowHeight;
  2560. console.log('当前是否横屏:', this.isHeight);
  2561. },
  2562. fullscreenchange(event){
  2563. console.log("[---]",this.isShu)
  2564. const isFullScreen = !!(event && event.detail && event.detail.fullScreen);
  2565. this.isFull = isFullScreen;
  2566. // 竖屏课程下:把全屏按钮当作布局切换按钮,禁止原生全屏停留
  2567. if (this.displayType === 'portrait') {
  2568. if (event && event.stopPropagation) event.stopPropagation();
  2569. if (event && event.preventDefault) event.preventDefault();
  2570. // 只在“进入全屏”时切换,避免 exitFullScreen 触发的 false 事件二次反转
  2571. if (!isFullScreen || this.fullscreenToggleLock) return;
  2572. this.fullscreenToggleLock = true;
  2573. this.isShu = !this.isShu;
  2574. this.$nextTick(() => {
  2575. this._xgExitFullscreen();
  2576. this.isFull = false;
  2577. this._lockAppPortrait();
  2578. this.fullscreenToggleTimer = setTimeout(() => {
  2579. this.fullscreenToggleLock = false;
  2580. this.fullscreenToggleTimer = null;
  2581. }, 280);
  2582. });
  2583. return;
  2584. }
  2585. // 横屏课程:全屏锁横屏,退出全屏锁竖屏并恢复顶部小窗布局
  2586. if (isFullScreen) {
  2587. this._lockAppLandscape();
  2588. } else {
  2589. this._lockAppPortrait();
  2590. this.isFull = false;
  2591. this.$nextTick(() => {
  2592. this.getVideoContainerHeight();
  2593. });
  2594. }
  2595. },
  2596. //购物车
  2597. showCart(){
  2598. ///console.log("[---]")
  2599. this.isCart=true
  2600. },
  2601. showMore(){
  2602. ///console.log("[---]")
  2603. this.isMore=true
  2604. },
  2605. closeShop() {
  2606. this.isCart = false;
  2607. },
  2608. closeMore() {
  2609. this.isMore = false;
  2610. },
  2611. navgetTo(url){
  2612. uni.navigateTo({
  2613. url: url
  2614. });
  2615. },
  2616. openList(){
  2617. const wasShu = this.isShu
  2618. this.isShu = !this.isShu
  2619. if (wasShu && this.showTabFeaturedComments) this.tabIndex = 0
  2620. // 竖屏课经 openList 切布局后,需重置播放器 CSS 全屏态,否则再次点 f 只会触发退出事件
  2621. if (this.displayType === 'portrait') {
  2622. this._xgExitFullscreen()
  2623. }
  2624. },
  2625. // 倒计时格式化函数
  2626. formatCountdown(seconds) {
  2627. const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
  2628. const hours = Math.floor(totalSeconds / 3600);
  2629. const remainingAfterHours = totalSeconds % 3600;
  2630. const minutes = Math.floor(remainingAfterHours / 60);
  2631. const secs = remainingAfterHours % 60;
  2632. return {
  2633. hours: this.padZero(hours),
  2634. minutes: this.padZero(minutes),
  2635. seconds: this.padZero(secs),
  2636. total: totalSeconds
  2637. };
  2638. },
  2639. getCountdownPercentage() {
  2640. const total = Number(this.totalRemainTime) || 0;
  2641. if (!total) return 0;
  2642. const remain = Math.max(0, Number(this.remainTime) || 0);
  2643. const watched = Math.max(0, total - remain);
  2644. const percentage = (watched / total) * 100;
  2645. return Math.min(100, Math.max(0, Number(percentage.toFixed(2))));
  2646. },
  2647. // 补零函数
  2648. padZero(num) {
  2649. return num < 10 ? `0${num}` : num.toString();
  2650. },
  2651. // 启动完课积分倒计时(仅在播放时递减)
  2652. startCountdown() {
  2653. if (this.remainTime <= 0 || this.countdownTimer) return;
  2654. this.countdownTimer = setInterval(() => {
  2655. if (this.remainTime > 0) {
  2656. this.remainTime--;
  2657. }
  2658. if (this.remainTime <= 0 && this.countdownTimer) {
  2659. clearInterval(this.countdownTimer);
  2660. this.countdownTimer = null;
  2661. if (!this.hasReportedAfterCountdown) {
  2662. this.hasReportedAfterCountdown = true;
  2663. this.getFinishCourseVideo();
  2664. this.getInternetTraffic();
  2665. }
  2666. }
  2667. }, 1000);
  2668. },
  2669. // 停止完课积分倒计时(暂停/离开页面时调用)
  2670. stopCountdown() {
  2671. if (this.countdownTimer) {
  2672. clearInterval(this.countdownTimer);
  2673. this.countdownTimer = null;
  2674. }
  2675. },
  2676. //剩余时间
  2677. getRemainTime(userId) {
  2678. var data = {
  2679. "videoId": this.videoId,
  2680. "fsUserId": userId,
  2681. "courseId": this.courseId,
  2682. "projectId":this.projectId,
  2683. "periodId":this.periodId,
  2684. "companyUserId": this.companyUserId
  2685. }
  2686. getRemainTime(data).then(res => {
  2687. if (res.code == 200) {
  2688. const remain = Number(res.remainTime) || 0; // 积分看课剩余时间(秒)
  2689. this.remainTime = remain;
  2690. this.remainTimeReady = true;
  2691. this.hasReportedAfterCountdown = remain <= 0;
  2692. // 初始化或刷新总时长,用于进度条百分比
  2693. if (!this.totalRemainTime || remain > this.totalRemainTime) {
  2694. this.totalRemainTime = remain;
  2695. }
  2696. } else {
  2697. this.remainTimeReady = false
  2698. uni.showToast({
  2699. icon: 'none',
  2700. title: res.msg,
  2701. });
  2702. }
  2703. })
  2704. },
  2705. getWebviewUrl() {
  2706. var data = {
  2707. key: 'course.config'
  2708. }
  2709. getConfigByKey(data).then(res => {
  2710. if (res.code == 200) {
  2711. console.log("getConfigByKey====", JSON.parse(res.data))
  2712. let data = JSON.parse(res.data)
  2713. this.notice = data.camelCase
  2714. uni.setStorageSync('setWebviewUrl', data.userCourseAuthDomain)
  2715. } else {
  2716. uni.showToast({
  2717. icon: 'none',
  2718. title: res.msg,
  2719. });
  2720. }
  2721. })
  2722. },
  2723. tabChange(type) {
  2724. this.currentId = type
  2725. },
  2726. kefpop() {
  2727. if (this.appToken) {
  2728. this.kfPopup == false
  2729. }
  2730. },
  2731. tosales() {
  2732. uni.switchTab({
  2733. url: "/pages/index/index"
  2734. })
  2735. },
  2736. closepop() {
  2737. this.answerPopup = false
  2738. },
  2739. open() {
  2740. },
  2741. getMenuButton() {
  2742. const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
  2743. this.menuButtonLeft = menuButtonInfo.left
  2744. this.menuButtonH = menuButtonInfo.height
  2745. },
  2746. _touchAntiSeekSuppress(ms = 2000) {
  2747. const until = Date.now() + ms
  2748. if (until > (this._antiSeekSuppressExpireAt || 0)) this._antiSeekSuppressExpireAt = until
  2749. },
  2750. //播放时间更新事件方法
  2751. onTimeUpdate(e) {
  2752. let currentTime = Math.round(e.detail.currentTime)
  2753. if (this.playDurationSeek > 0) {
  2754. this.playTime = this.playDurationSeek
  2755. this.throttle(() => this.changeTime(this), 1000, false)
  2756. } else {
  2757. // console.log(this.isFinish)
  2758. const suppressAntiSeek = Date.now() < (this._antiSeekSuppressExpireAt || 0)
  2759. if (!suppressAntiSeek && (currentTime - this.playTime > 3 || currentTime - this
  2760. .playTime < -3) && this
  2761. .isFinish != 1) {
  2762. uni.showToast({
  2763. title: '不能快进哦',
  2764. icon: 'none',
  2765. });
  2766. currentTime = this.playTime
  2767. this._xgSeek(this.playTime);
  2768. }
  2769. if (Math.floor(e.detail.currentTime) != this.flagTime) {
  2770. this.flagTime = Math.floor(e.detail.currentTime)
  2771. // 中途弹窗逻辑
  2772. if (!this.tipsOpen && this.tipsTime && this.playTime == this.tipsTime) {
  2773. this.timepath = '33%'
  2774. this.openTipsPop()
  2775. }
  2776. if (!this.tipsOpen && this.tipsTime2 && this.playTime == this.tipsTime2) {
  2777. this.timepath = '66%'
  2778. this.openTipsPop()
  2779. }
  2780. }
  2781. this.playTime = currentTime
  2782. this.updateProductAndCardDisplay(currentTime)
  2783. }
  2784. },
  2785. // 根据当前播放时间:1)扫描商品栏展示(上架/下架时间)2)控制商品卡片弹窗(弹出/关闭时间)
  2786. updateProductAndCardDisplay(currentSec) {
  2787. const toSeconds = (timeStr) => {
  2788. if (!timeStr || typeof timeStr !== 'string') return 0
  2789. const parts = timeStr.split(':')
  2790. if (parts.length !== 3) return 0
  2791. const h = parseInt(parts[0], 10) || 0
  2792. const m = parseInt(parts[1], 10) || 0
  2793. const s = parseInt(parts[2], 10) || 0
  2794. return h * 3600 + m * 60 + s
  2795. }
  2796. const hasValidShelfTime = (timeStr) => {
  2797. if (!timeStr || typeof timeStr !== 'string') return false
  2798. if (timeStr === '00:00:00') return false
  2799. return !!toSeconds(timeStr)
  2800. }
  2801. if (this.treatmentPackage.length === 0) {
  2802. this.displayProductList = []
  2803. this.cardPopup = false
  2804. this.currentCardItem = null
  2805. this._syncFakeMarqueeIfEligibleChanged()
  2806. return
  2807. }
  2808. // 按“上架时间”倒序排序,保证后上架商品展示在列表最前
  2809. const sortedTreatmentPackage = this.treatmentPackage.slice().sort((a, b) => {
  2810. const aOn = hasValidShelfTime(a.onShelfTime) ? toSeconds(a.onShelfTime) : 0
  2811. const bOn = hasValidShelfTime(b.onShelfTime) ? toSeconds(b.onShelfTime) : 0
  2812. if (bOn !== aOn) return bOn - aOn
  2813. // 上架时间相同则按下架时间倒序,进一步保证新近优先
  2814. const aOff = hasValidShelfTime(a.offShelfTime) ? toSeconds(a.offShelfTime) : 0
  2815. const bOff = hasValidShelfTime(b.offShelfTime) ? toSeconds(b.offShelfTime) : 0
  2816. return bOff - aOff
  2817. })
  2818. const videoDuration = this.duration || 0
  2819. // 1. 商品栏展示:按上架/下架时间过滤
  2820. this.displayProductList = sortedTreatmentPackage.filter(item => {
  2821. // 上架时间必填;下架时间可为空(为空表示一直展示到视频结束)
  2822. if (!hasValidShelfTime(item.onShelfTime)) return false
  2823. const onShelfSec = toSeconds(item.onShelfTime)
  2824. if (currentSec < onShelfSec) return false
  2825. const hasOffShelfTime = hasValidShelfTime(item.offShelfTime)
  2826. if (!hasOffShelfTime) return true
  2827. const offShelfSec = toSeconds(item.offShelfTime)
  2828. if (videoDuration > 0 && offShelfSec > videoDuration) return true
  2829. return currentSec < offShelfSec
  2830. })
  2831. //console.log('this.displayProductList',this.displayProductList)
  2832. // 2. 商品卡片弹窗:按弹出/关闭时间查找当前应展示的卡片
  2833. let activeCard = null
  2834. let activePopupSec = -1
  2835. for (let i = 0; i < sortedTreatmentPackage.length; i++) {
  2836. const item = sortedTreatmentPackage[i]
  2837. const popupStr = item.cardPopupTime
  2838. const closeStr = item.cardCloseTime
  2839. if (!popupStr || popupStr === '00:00:00') continue
  2840. const popupSec = toSeconds(popupStr)
  2841. if (!popupSec) continue
  2842. const closeSec = (closeStr && closeStr !== '00:00:00') ? toSeconds(closeStr) : null
  2843. if (currentSec >= popupSec && (closeSec == null || currentSec < closeSec)) {
  2844. // 同时命中多个弹窗时:优先展示“启动时间更晚”的后续弹窗
  2845. if (popupSec > activePopupSec) {
  2846. activePopupSec = popupSec
  2847. activeCard = item
  2848. } else if (popupSec === activePopupSec && activeCard) {
  2849. // 上架时间更晚优先(兜底)
  2850. const activeOn = hasValidShelfTime(activeCard.onShelfTime) ? toSeconds(activeCard.onShelfTime) : 0
  2851. const itemOn = hasValidShelfTime(item.onShelfTime) ? toSeconds(item.onShelfTime) : 0
  2852. if (itemOn > activeOn) activeCard = item
  2853. }
  2854. }
  2855. }
  2856. const activeCardKey = activeCard ?
  2857. `${activeCard.productId || ''}_${activeCard.cardPopupTime || ''}_${activeCard.cardCloseTime || ''}` :
  2858. ''
  2859. if (!activeCardKey) {
  2860. this.dismissedCardKey = ''
  2861. }
  2862. this.cardPopup = !!activeCard && this.dismissedCardKey !== activeCardKey
  2863. //this.getVideoContainerHeight()
  2864. this.currentCardItem = activeCard
  2865. this.syncProductHotCounts()
  2866. this._syncFakeMarqueeIfEligibleChanged()
  2867. },
  2868. closeCardPopup() {
  2869. if (this.currentCardItem) {
  2870. this.dismissedCardKey =
  2871. `${this.currentCardItem.productId || ''}_${this.currentCardItem.cardPopupTime || ''}_${this.currentCardItem.cardCloseTime || ''}`
  2872. }
  2873. this.cardPopup = false
  2874. },
  2875. hasHotSaleTag(item) {
  2876. return hasHotSaleTag(item)
  2877. },
  2878. getProductHotCount(item) {
  2879. if (!hasHotSaleTag(item)) return 0
  2880. const key = getProductHotKey(item)
  2881. return key ? clampHotSaleCount(this.productHotCountMap[key]) : 0
  2882. },
  2883. _collectHotSaleProducts() {
  2884. const seen = new Set()
  2885. const list = []
  2886. const add = (item) => {
  2887. if (!hasHotSaleTag(item)) return
  2888. const key = getProductHotKey(item)
  2889. if (!key || seen.has(key)) return
  2890. seen.add(key)
  2891. list.push(item)
  2892. }
  2893. ;(this.displayProductList || []).forEach(add)
  2894. if (this.cardPopup && this.currentCardItem) add(this.currentCardItem)
  2895. return list
  2896. },
  2897. _randomInt(min, max) {
  2898. return Math.floor(Math.random() * (max - min + 1)) + min
  2899. },
  2900. syncProductHotCounts() {
  2901. const products = this._collectHotSaleProducts()
  2902. const next = { ...this.productHotCountMap }
  2903. const activeKeys = new Set()
  2904. products.forEach((item) => {
  2905. const key = getProductHotKey(item)
  2906. if (!key) return
  2907. activeKeys.add(key)
  2908. if (!next[key]) {
  2909. next[key] = clampHotSaleCount(this._randomInt(100, 800))
  2910. }
  2911. })
  2912. Object.keys(next).forEach((key) => {
  2913. if (!activeKeys.has(key)) delete next[key]
  2914. })
  2915. this.productHotCountMap = next
  2916. if (activeKeys.size) {
  2917. this.startProductHotTimer()
  2918. } else {
  2919. this.stopProductHotTimer()
  2920. }
  2921. },
  2922. startProductHotTimer() {
  2923. if (this.productHotTimer) return
  2924. this.productHotTimer = setInterval(() => {
  2925. const products = this._collectHotSaleProducts()
  2926. if (!products.length) return
  2927. const next = { ...this.productHotCountMap }
  2928. products.forEach((item) => {
  2929. const key = getProductHotKey(item)
  2930. if (key && next[key]) {
  2931. next[key] = clampHotSaleCount(next[key] + this._randomInt(10, 20))
  2932. }
  2933. })
  2934. this.productHotCountMap = next
  2935. }, 30000)
  2936. },
  2937. stopProductHotTimer() {
  2938. if (this.productHotTimer) {
  2939. clearInterval(this.productHotTimer)
  2940. this.productHotTimer = null
  2941. }
  2942. },
  2943. changeTime(that, e) {
  2944. that.playDurationSeek = 0
  2945. },
  2946. videoErrorCallback(e) {
  2947. this.clearIntegral()
  2948. this.errorCount++
  2949. if (this.errorCount > 3) return
  2950. console.log(e)
  2951. this.getErrMsg(e.target.errMsg)
  2952. this.getH5CourseVideoDetails('error')
  2953. },
  2954. // 商品卡片跳转
  2955. goBuy(item) {
  2956. if (!item || !item.productId) return
  2957. const url = '/pages/shopping/productDetails?productId=' + item.productId +
  2958. '&companyId=' + (this.urlOption.companyId || '') +
  2959. '&companyUserId=' + (this.urlOption.companyUserId || '') +
  2960. '&courseId=' + (this.urlOption.courseId || '') +
  2961. '&videoId=' + (this.urlOption.videoId || '') +
  2962. '&projectId=' + (this.urlOption.projectId || '') +
  2963. '&periodId=' + (this.urlOption.periodId || '')
  2964. // 横屏全屏下先退出全屏再跳转,避免视频层遮挡商品详情页
  2965. if (this.isFull) {
  2966. this._xgExitFullscreen()
  2967. this._lockAppPortrait()
  2968. this.isFull = false
  2969. setTimeout(() => {
  2970. uni.navigateTo({ url })
  2971. }, 100)
  2972. return
  2973. }
  2974. uni.navigateTo({ url })
  2975. },
  2976. // 当开始/继续播放时触发play事件
  2977. getPlay() {
  2978. this.errorCount = 0
  2979. this.videoPlaying = true
  2980. // this.judgeDuration()
  2981. // 开始播放时启动完课积分倒计时
  2982. this.startCountdown()
  2983. },
  2984. getPause() {
  2985. this.videoPlaying = false
  2986. this.clearIntegral()
  2987. // 暂停时停止完课积分倒计时
  2988. this.stopCountdown()
  2989. },
  2990. getEnded() {
  2991. this.videoPlaying = false
  2992. this.clearIntegral()
  2993. // 播放结束时停止完课积分倒计时
  2994. this.stopCountdown()
  2995. this.isEnded = true
  2996. this.isFinish = 1
  2997. this.getFinishCourseVideo()
  2998. },
  2999. getIP() {
  3000. uni.request({
  3001. url: 'https://ipinfo.io/json', //仅为示例,并非真实接口地址。
  3002. method: 'GET',
  3003. success: (res) => {
  3004. this.ip = res.data.ip
  3005. }
  3006. });
  3007. },
  3008. getHeight() {
  3009. this.height =
  3010. `calc(100vh - 420rpx - 160rpx)`
  3011. // this.$nextTick(() => {
  3012. // const query = uni.createSelectorQuery().in(this);
  3013. // query
  3014. // .select("#title-content")
  3015. // .boundingClientRect((data) => {
  3016. // this.height =
  3017. // `calc(100vh - ${data.height}px - 420rpx)`
  3018. // })
  3019. // .exec();
  3020. // })
  3021. },
  3022. getDescHeight() {
  3023. // this.$nextTick(() => {
  3024. // const query = uni.createSelectorQuery().in(this);
  3025. // query
  3026. // .select("#descbox-desc")
  3027. // .boundingClientRect((data) => {
  3028. // this.textHeight = data.height
  3029. // })
  3030. // .exec();
  3031. // })
  3032. },
  3033. numberToLetter(num) {
  3034. // 将数字转换为字母的 ASCII 码
  3035. let letterCode = num + 65;
  3036. // 将 ASCII 码转换为大写字母
  3037. let letter = String.fromCharCode(letterCode);
  3038. return letter;
  3039. },
  3040. updateTime() {
  3041. var that = this;
  3042. if (this.interval != null) {
  3043. clearInterval(this.interval)
  3044. }
  3045. this.interval = setInterval(function() {
  3046. that.getFinishCourseVideo()
  3047. that.getInternetTraffic()
  3048. }, 60000);
  3049. },
  3050. judgeDuration() {
  3051. var that = this;
  3052. if (this.intervalIntegral != null) {
  3053. clearInterval(this.intervalIntegral)
  3054. this.intervalIntegral = null
  3055. }
  3056. // 观看10分钟获得积分
  3057. this.intervalIntegral = setInterval(function() {
  3058. that.getIntegralByH5Video()
  3059. }, 600000);
  3060. },
  3061. clearIntegral() {
  3062. if (this.intervalIntegral != null) {
  3063. clearInterval(this.intervalIntegral)
  3064. this.intervalIntegral = null
  3065. }
  3066. },
  3067. // 展开简介
  3068. handleExpand() {
  3069. this.isExpand = !this.isExpand
  3070. },
  3071. getH5CourseByVideo() {
  3072. this.loading = true
  3073. getH5CourseByVideoId({
  3074. videoId: this.videoId
  3075. }).then(res => {
  3076. this.loading = false
  3077. if (res.code == 200) {
  3078. this.viewload = false
  3079. this.courseInfo = res.data
  3080. uni.setNavigationBarTitle({
  3081. title: this.courseInfo.title||''
  3082. });
  3083. this.$nextTick(() => {
  3084. this._syncRatingTabIndex()
  3085. })
  3086. }
  3087. //this.getHeight()
  3088. //this.getDescHeight()
  3089. },
  3090. rej => {
  3091. this.loading = false
  3092. }
  3093. ).catch(() => {
  3094. this.loading = false
  3095. })
  3096. },
  3097. getH5CourseVideoDetails(type) {
  3098. uni.showLoading({
  3099. title: '加载中'
  3100. })
  3101. //var user = uni.getStorageSync('userInfo');
  3102. const data = {
  3103. videoId: this.videoId,
  3104. fsUserId: this.userInfo.userId,
  3105. courseId: this.courseId,
  3106. companyUserId: this.companyUserId,
  3107. periodId: this.periodId,
  3108. id: this.timeid
  3109. }
  3110. getH5CourseVideoDetails(data).then(res => {
  3111. if (res.code == 200) {
  3112. this.config = res.data.courseConfig || {}
  3113. this.isFinish = res.data.isFinish || 0
  3114. this.duration = res.data.courseVideoDetails &&
  3115. res.data.courseVideoDetails.duration ? res.data.courseVideoDetails.duration : 0
  3116. this.courseLogo = this.config && JSON.stringify(this.config) != '{}' ? this.config
  3117. .courseLogo : ''
  3118. this.playDuration = res.data.playDuration || 0
  3119. this.playDurationSeek = res.data.playDuration || 0
  3120. // this.tipsTime = 38
  3121. this.tipsTime = res.data.tipsTime || 0
  3122. this.tipsTime2 = res.data.tipsTime2 || 0
  3123. this.displayType = res.data.courseVideoDetails && res.data.courseVideoDetails
  3124. .displayType != '' ? res.data.courseVideoDetails.displayType :'landscape'
  3125. this.isShu = this.displayType=='landscape'?false:true
  3126. const showTreatmentFlag = res.data.courseVideoDetails && res.data.courseVideoDetails
  3127. .showProduct != null ? res.data.courseVideoDetails.showProduct : 1
  3128. this.treatmentPackage = res.data.courseVideoDetails && res.data.courseVideoDetails
  3129. .fsStoreProductScrms ? res.data.courseVideoDetails.fsStoreProductScrms : []
  3130. this.showTreatment = showTreatmentFlag == 0 && this.treatmentPackage.length > 0 ? 0 : 1;
  3131. //console.log('this.showTreatment',this.showTreatment)
  3132. let lineList = []
  3133. // if (res.course && res.course.lineOne) {
  3134. // lineList.push(res.course.lineOne)
  3135. // }
  3136. // if (res.course && res.course.lineTwo) {
  3137. // lineList.push(res.course.lineTwo)
  3138. // }
  3139. // if (res.course && res.course.lineThree) {
  3140. // lineList.push(res.course.lineThree)
  3141. // }
  3142. this.lineList = lineList
  3143. if (!this.xgPlayerInited || type == 'error') {
  3144. uni.hideLoading();
  3145. if (type == 'error') {
  3146. this.videoReloadKey++
  3147. this.xgPlayerInited = false
  3148. }
  3149. this.lineIndex = this.config.defaultLine
  3150. this.videoUrl = res.data.courseVideoDetails.videoUrl
  3151. this.poster = res.data.courseVideoDetails &&
  3152. res.data.courseVideoDetails.thumbnail ? res.data.courseVideoDetails.thumbnail : ''
  3153. this.videocont = res.data
  3154. this.playTime = this.playDuration >= this.duration ? 0 : this.playDuration
  3155. if (this.videocont.rang) {
  3156. setTimeout(() => {
  3157. this._xgPlay(this.playTime)
  3158. this._touchAntiSeekSuppress(2000)
  3159. }, 500);
  3160. this.timepop = false
  3161. } else {
  3162. this.timepop = true
  3163. }
  3164. } else {
  3165. this.playTime = this.playTime > this.playDuration ? this.playTime : this
  3166. .playDuration >= this.duration ? 0 : this.playDuration
  3167. this._xgPlay(this.playTime)
  3168. this._touchAntiSeekSuppress(2000)
  3169. }
  3170. this.updateTime();
  3171. // 初始扫描商品栏展示(根据上架/下架时间)
  3172. this.updateProductAndCardDisplay(this.playTime ?? this.playDuration ?? 0);
  3173. // 商品栏扫描完成后再允许虚拟跑马灯启动(延后到下一轮渲染,避免一帧闪现)
  3174. this.$nextTick(() => {
  3175. this.marqueeDataReady = true
  3176. this._syncFakeMarqueeIfEligibleChanged()
  3177. })
  3178. const qbl =
  3179. (res.data.courseVideoDetails && res.data.courseVideoDetails.questionBankList) || []
  3180. this.questionBankList = qbl
  3181. if (qbl.length === 0) {
  3182. this.isquestion = true
  3183. this.quesList = []
  3184. } else {
  3185. this.isquestion = false
  3186. this.quesList = qbl.map(item => ({
  3187. ...item,
  3188. questionOption: JSON.parse(item.question),
  3189. answer: ''
  3190. }))
  3191. }
  3192. this.$nextTick(() => {
  3193. this._syncRatingTabIndex()
  3194. })
  3195. }
  3196. //this.getHeight()
  3197. //this.getDescHeight()
  3198. },
  3199. rej => {}
  3200. )
  3201. },
  3202. handleAnswer(itemOrPayload, option, questionIndex) {
  3203. let item = itemOrPayload
  3204. let opt = option
  3205. if (itemOrPayload && itemOrPayload.item && itemOrPayload.option) {
  3206. item = itemOrPayload.item
  3207. opt = itemOrPayload.option
  3208. questionIndex = this.quesList.indexOf(item)
  3209. } else if (questionIndex == null || questionIndex < 0) {
  3210. questionIndex = this.quesList.indexOf(item)
  3211. }
  3212. // 以“完课积分倒计时”结束作为答题资格
  3213. // if (!this.remainTimeReady) {
  3214. // uni.showToast({
  3215. // title: "倒计时加载中,请稍后再答题哦~",
  3216. // icon: "none"
  3217. // })
  3218. // return
  3219. // }
  3220. if (questionIndex >= 0 && opt) {
  3221. this.goRate(questionIndex, opt.indexId)
  3222. }
  3223. if (Number(this.remainTime || 0) > 0) {
  3224. uni.showToast({
  3225. title: "完课倒计时结束再评分",
  3226. icon: "none"
  3227. })
  3228. return
  3229. }
  3230. if (item.type == 1) {
  3231. // 单选option
  3232. item.answer = opt.name
  3233. } else if (item.type == 2) {
  3234. // 多选
  3235. let answer = item.answer ? item.answer.split(',') : []
  3236. if (answer.indexOf(opt.name) === -1) {
  3237. answer.push(opt.name)
  3238. item.answer = answer.join(',')
  3239. } else {
  3240. answer.splice(answer.indexOf(opt.name), 1)
  3241. item.answer = answer.join(',')
  3242. }
  3243. }
  3244. },
  3245. submit() {
  3246. if (this.isExpire) {
  3247. uni.showToast({
  3248. title: '课程已过期或链接无效',
  3249. icon: 'none'
  3250. });
  3251. return
  3252. }
  3253. if (!this.$isLogin()) {
  3254. this.$showLoginPage()
  3255. return
  3256. }
  3257. this.isLogin = true
  3258. this.userInfo = this.$getUserInfo()
  3259. if (this.isAddKf == 1) {
  3260. this.courseAnswer()
  3261. } else if (this.videoId) {
  3262. this.getIsAddKf()
  3263. } else if (this.iskftype == 1) {
  3264. uni.showToast({
  3265. title: '已看过其他销售分享的此课程,不能重复观看',
  3266. icon: 'none'
  3267. })
  3268. } else {
  3269. this.$showLoginPage()
  3270. }
  3271. },
  3272. // 答题
  3273. courseAnswer() {
  3274. // 以“完课积分倒计时”结束作为答题资格
  3275. // if (!this.remainTimeReady) {
  3276. // uni.showToast({
  3277. // title: "倒计时加载中,请稍后再答题哦~",
  3278. // icon: "none"
  3279. // })
  3280. // return
  3281. // }
  3282. if (Number(this.remainTime || 0) > 0) {
  3283. uni.showToast({
  3284. title: "完课倒计时结束再评分",
  3285. icon: "none"
  3286. })
  3287. return
  3288. }
  3289. if (this.quesList.some(item => !item.answer)) {
  3290. uni.showToast({
  3291. title: "请选择评分",
  3292. icon: "none"
  3293. })
  3294. return
  3295. }
  3296. const questions = this.quesList.map(obj => {
  3297. const {
  3298. questionOption,
  3299. ...rest
  3300. } = obj;
  3301. return rest;
  3302. });
  3303. const param = {
  3304. ...this.urlOption,
  3305. userId: this.userInfo.userId,
  3306. questions: questions,
  3307. videoId: this.videoId,
  3308. duration: this.playTime,
  3309. }
  3310. this.errTitle = ""
  3311. this.errDesc = ""
  3312. this.errQues = []
  3313. courseAnswer(param).then(res => {
  3314. if (res.code == 200) {
  3315. if (res.incorrectQuestions) {
  3316. // 答题失败
  3317. if (res.incorrectQuestions.length > 0) {
  3318. this.errQues = res.incorrectQuestions
  3319. }
  3320. this.remain = res.remain || 0
  3321. if (res.remain > 0) {
  3322. this.answerPopup = true
  3323. this.errTitle = "很遗憾答错了"
  3324. this.errDesc = `<span style="color:#FF5C03">还有${res.remain}次机会,继续加油</span>`
  3325. // this.$refs.answerPopup.open("center")
  3326. this.answerPopup = true
  3327. }
  3328. } else {
  3329. // 答题成功
  3330. this.errTitle = "完课积分已到账"
  3331. //this.errDesc = `请选择奖励`
  3332. // this.$refs.answerPopup.open("center")
  3333. // this.answerPopup=true
  3334. if (res.msg == '答题成功') {
  3335. this.closeAnswerPopup()
  3336. // uni.login({
  3337. // provider: 'weixin',
  3338. // success: async loginRes => {
  3339. // console.log(loginRes)
  3340. // var code=loginRes
  3341. // this.closeAnswerPopup(loginRes.code)
  3342. // }
  3343. // })
  3344. }
  3345. }
  3346. } else {
  3347. if (res.msg == "该课题到达答错次数限制") {
  3348. this.errTitle = "答题次数超过限制"
  3349. this.errDesc = "以后的课程要认真学习哦"
  3350. // this.$refs.answerPopup.open("center")
  3351. this.answerPopup = true
  3352. } else {
  3353. if (res.msg.length > 14) {
  3354. if (res.msg == "该课程已答题完成,不可重复答题") {
  3355. uni.showToast({
  3356. title: '已评价请勿重复提交',
  3357. icon: 'none'
  3358. })
  3359. }else{
  3360. uni.showModal({
  3361. title: '提示',
  3362. content: res.msg,
  3363. showCancel: false,
  3364. confirmText: '我知道了'
  3365. })
  3366. }
  3367. } else {
  3368. uni.showToast({
  3369. title: res.msg,
  3370. icon: 'none'
  3371. })
  3372. }
  3373. }
  3374. }
  3375. },
  3376. rej => {}
  3377. )
  3378. },
  3379. // 选择
  3380. rewardChange(e) {
  3381. this.currentReward = e.detail.value
  3382. },
  3383. closeAnswerPopup() {
  3384. // this.$refs.answerPopup.close()
  3385. uni.showLoading({
  3386. title: "加载中..."
  3387. })
  3388. if (this.errTitle == '完课积分已到账') {
  3389. const param = {
  3390. ...this.urlOption,
  3391. userId: this.userInfo.userId,
  3392. // rewardType: Number(this.currentReward),
  3393. source: 2, // 小程序
  3394. appId: this.appid
  3395. }
  3396. sendReward(param).then(res => {
  3397. if (res.code == 200) {
  3398. uni.hideLoading()
  3399. this.answerPopup = false
  3400. console.log('红包', res)
  3401. if (res.isNew && res.isNew == 1) {
  3402. console.log('红包配置', res)
  3403. const packageInfo = res.data.packageInfo || ''
  3404. if (packageInfo) {
  3405. uni.setStorageSync('receive_package', packageInfo)
  3406. uni.setStorageSync('mchId', res.mchId)
  3407. uni.navigateTo({
  3408. url: '/pages_course/reward'
  3409. })
  3410. }
  3411. } else {
  3412. if (res.msg.length > 14) {
  3413. uni.showModal({
  3414. title: '提示',
  3415. content: res.msg,
  3416. showCancel: false,
  3417. confirmText: '我知道了'
  3418. })
  3419. } else {
  3420. uni.showToast({
  3421. title: res.msg,
  3422. icon: 'none'
  3423. })
  3424. }
  3425. }
  3426. } else {
  3427. uni.hideLoading()
  3428. if (res.msg.length > 14) {
  3429. uni.showModal({
  3430. title: '提示',
  3431. content: res.msg,
  3432. showCancel: false,
  3433. confirmText: '我知道了'
  3434. })
  3435. } else {
  3436. uni.showToast({
  3437. title: res.msg,
  3438. icon: 'none'
  3439. })
  3440. }
  3441. }
  3442. // if(res.code == 200) {
  3443. // //重构 发红包,后台通过OPENID发零钱到 账
  3444. // //this.initWXConfig(res.package)
  3445. // }else {
  3446. // uni.showToast({
  3447. // title: res.msg,
  3448. // icon: 'none'
  3449. // })
  3450. // }
  3451. })
  3452. } else {
  3453. uni.hideLoading()
  3454. this.answerPopup = !this.answerPopup
  3455. }
  3456. },
  3457. // 线路
  3458. openPop() {
  3459. this.$refs.popup.open('bottom')
  3460. },
  3461. close() {
  3462. this.$refs.popup.close()
  3463. },
  3464. handleLine(index) {
  3465. var that = this;
  3466. if (this.lineIndex == index && this.videoUrl == this.lineList[index]) {
  3467. this.close()
  3468. return
  3469. } else {
  3470. // let div = document.querySelector(".vjs-progress-control");
  3471. // if(div) {
  3472. // if (this.isFinish == 1 || this.isEnded || this.linkType == 1) {
  3473. // div.style.pointerEvents = "auto";
  3474. // } else {
  3475. // div.style.pointerEvents = "none"; //禁止所有事件
  3476. // }
  3477. // }
  3478. this.lineIndex = index
  3479. this.videoUrl = this.lineList[index]
  3480. this.tipsOpen = false
  3481. this.playDurationSeek = this.playTime || 0
  3482. this.videoReloadKey++
  3483. setTimeout(function() {
  3484. that._xgPlay(that.playDurationSeek)
  3485. }, 500);
  3486. // this.player.src(this.lineList[index])
  3487. // this.player.one('loadedmetadata', () => {
  3488. // this.player.currentTime(this.playDurationSeek);
  3489. // this.player.play();
  3490. // });
  3491. this.close()
  3492. }
  3493. },
  3494. // 温馨提示
  3495. openTipsPop() {
  3496. // this.$refs.tipsPopup.open()
  3497. this.tipsPopup = true
  3498. // 暂停视频
  3499. this._xgPause()
  3500. // this.tipsOpen = true
  3501. // this.pause()
  3502. },
  3503. closeTipsPop() {
  3504. // this.$refs.tipsPopup.close()
  3505. this._xgPlay(this.playTime || 0)
  3506. this.tipsPopup = !this.tipsPopup
  3507. },
  3508. // 客服
  3509. getIsAddKf() {
  3510. this.qrcode = ''
  3511. this.qrcodeMsg = ''
  3512. this.isAddKf = 0
  3513. this.userInfo = this.$getUserInfo()
  3514. if (!this.userInfo || !this.userInfo.userId) {
  3515. this.isLogin = false
  3516. return
  3517. }
  3518. const data = {
  3519. videoId: this.videoId,
  3520. userId: this.userInfo.userId,
  3521. companyUserId: this.companyUserId,
  3522. companyId: this.companyId,
  3523. courseId: this.courseId,
  3524. periodId: this.periodId,
  3525. projectId: this.projectId,
  3526. appId: this.appid,
  3527. }
  3528. // {videoId: this.videoId,qwUserId: this.qwUserId,corpId: this.corpId}
  3529. getIsAddKf(data).then(res => {
  3530. if (res.code == 200) {
  3531. this.isLogin = true
  3532. if (res.data) {
  3533. this.isAddKf = 1
  3534. if (this.userInfo) {
  3535. if (typeof this.userInfo === 'string') {
  3536. try {
  3537. this.userInfo = JSON.parse(this.userInfo);
  3538. } catch (e) {
  3539. this.userInfo = {};
  3540. }
  3541. }
  3542. if (this.userInfo.userId) {
  3543. this.getH5CourseVideoDetails();
  3544. this.getRemainTime(this.userInfo.userId);
  3545. this.loadFeaturedComments()
  3546. }
  3547. }
  3548. }
  3549. // else{
  3550. // uni.showToast({
  3551. // title: '请联系管理员,注册为会员!',
  3552. // icon: 'none'
  3553. // });
  3554. // }
  3555. } else if (res.code == 1002) {
  3556. this.isAddKf = 0
  3557. this.qrcode = res.ext
  3558. this.kfPopup = true
  3559. uni.showToast({
  3560. title: "请添加管理员微信,成为会员!",
  3561. icon: 'none'
  3562. });
  3563. this.initExpiration(res.msg, res.code)
  3564. } else if (res.code == 505) {
  3565. this.isAddKf = 0
  3566. this.qrcode = res.ext
  3567. this.kfPopup = true
  3568. uni.showToast({
  3569. title: "管理员开启了会员审核,请等待审核!",
  3570. icon: 'none'
  3571. });
  3572. this.initExpiration(res.msg, res.code)
  3573. } else if (res.code == 406) {
  3574. uni.hideLoading();
  3575. uni.showToast({
  3576. icon: 'none',
  3577. title: '该用户已成为其他销售会员',
  3578. });
  3579. this.initExpiration(res.msg, res.code)
  3580. } else if (res.code == 504) {
  3581. this.isAddKf = 0
  3582. this.iskftype = 1
  3583. uni.showToast({
  3584. title: res.msg,
  3585. icon: 'none'
  3586. });
  3587. this.initExpiration(res.msg, res.code)
  3588. } else {
  3589. this.isAddKf = 0
  3590. uni.showToast({
  3591. title: res.msg,
  3592. icon: 'none'
  3593. });
  3594. this.initExpiration(res.msg, res.code)
  3595. }
  3596. },
  3597. err => {}
  3598. );
  3599. },
  3600. initExpiration(resMsg, resCode) {
  3601. if (resCode == 401) return;
  3602. this.resMsg = resMsg
  3603. this.resCode = resCode
  3604. this.showExpiration = true
  3605. },
  3606. closeKFPop() {
  3607. // this.$refs.kfPopup.close()
  3608. // this.kfPopup=!this.kfPopup
  3609. },
  3610. /** 离开页面前补上报,避免 60s 定时未到导致再次进入从头播放 */
  3611. _flushWatchProgress() {
  3612. if (!this.playTime || !this.isLogin || this.isAddKf != 1) return
  3613. const baseDuration = this.playDuration >= this.duration ? 0 : (Number(this.playDuration) || 0)
  3614. if (this.playTime <= baseDuration && this.isFinish != 1) return
  3615. this.getFinishCourseVideo()
  3616. this.getInternetTraffic()
  3617. },
  3618. getFinishCourseVideo() {
  3619. if (!this.playTime || !this.isLogin || this.isAddKf != 1) return
  3620. const userId = this.userInfo && this.userInfo.userId
  3621. if (!userId) return
  3622. const param = {
  3623. duration: this.playTime,
  3624. videoId: this.videoId,
  3625. userId,
  3626. companyUserId: this.companyUserId
  3627. }
  3628. getFinishCourseVideo(param)
  3629. },
  3630. // 每十分钟获得积分
  3631. getIntegralByH5Video() {
  3632. const param = {
  3633. duration: this.playTime,
  3634. ...this.urlOption
  3635. }
  3636. getIntegralByH5Video(param).then(res => {
  3637. if (res.code == 200) {
  3638. uni.showToast({
  3639. title: "积分+10",
  3640. icon: "none"
  3641. })
  3642. }
  3643. })
  3644. },
  3645. progressChange(e) {
  3646. this.bufferRate = Math.ceil(e.detail.buffered)
  3647. // console.log('缓冲结果',this.bufferRate)
  3648. // console.log('缓冲',this.playTime,this.duration)
  3649. },
  3650. // 缓冲
  3651. getInternetTraffic() {
  3652. const playVideoTime = Math.ceil(this.playTime / this.duration * 100) // 播放百分比
  3653. // console.log('播放百分比',playVideoTime)
  3654. if (this.bufferRate == 0 || this.bufferRate < playVideoTime) {
  3655. this.bufferRate = playVideoTime
  3656. console.log(this.bufferRate)
  3657. }
  3658. if (this.bufferRate == 0 || Number(this.bufferRate.toFixed(2)) == 0) return
  3659. const param = {
  3660. uuId: dayjs().format('YYYYMMDD') + this.uuId,
  3661. duration: this.playTime,
  3662. bufferRate: Number(this.bufferRate.toFixed(2)),
  3663. userId: this.userId,
  3664. periodId: this.periodId,
  3665. ...this.urlOption
  3666. }
  3667. getInternetTraffic(param)
  3668. },
  3669. getErrMsg(err) {
  3670. let msgerr = {
  3671. videoUrl: this.videoUrl,
  3672. lineIndex: this.lineIndex,
  3673. errTime: new Date(),
  3674. // ip: this.ip,
  3675. errMsg: err
  3676. }
  3677. getErrMsg({
  3678. msg: JSON.stringify(msgerr)
  3679. })
  3680. },
  3681. getLink() {
  3682. if (!this.$isLogin()) {
  3683. this.$showLoginPage()
  3684. return
  3685. }
  3686. this.isLogin = true
  3687. this.userInfo = this.$getUserInfo()
  3688. if (this.isLogin && this.isAddKf == 1) {
  3689. this.getH5CourseVideoDetails()
  3690. }
  3691. if (this.videoId && this.isAddKf != 1) {
  3692. this.getIsAddKf()
  3693. }
  3694. },
  3695. /**
  3696. * 节流原理:在一定时间内,只能触发一次
  3697. *
  3698. * @param {Function} func 要执行的回调函数
  3699. * @param {Number} wait 延时的时间
  3700. * @param {Boolean} immediate 是否立即执行
  3701. * @return null
  3702. */
  3703. throttle(func, wait = 500, immediate = true) {
  3704. if (immediate) {
  3705. if (!this.flag) {
  3706. this.flag = true
  3707. // 如果是立即执行,则在wait毫秒内开始时执行
  3708. typeof func === 'function' && func()
  3709. this.timer = setTimeout(() => {
  3710. this.flag = false
  3711. }, wait)
  3712. }
  3713. } else if (!this.flag) {
  3714. this.flag = true
  3715. // 如果是非立即执行,则在wait毫秒内的结束处执行
  3716. this.timer = setTimeout(() => {
  3717. this.flag = false
  3718. typeof func === 'function' && func()
  3719. }, wait)
  3720. }
  3721. }
  3722. }
  3723. }
  3724. </script>
  3725. <script module="xgplayer" lang="renderjs">
  3726. const XG_SCRIPT = 'https://unpkg.byted-static.com/xgplayer/3.0.20/dist/index.min.js';
  3727. const XG_CSS = 'https://unpkg.byted-static.com/xgplayer/3.0.20/dist/index.min.css';
  3728. export default {
  3729. data() {
  3730. return {
  3731. player: null,
  3732. ownerInstance: null,
  3733. isFullscreen: false,
  3734. isCSSFull: false,
  3735. Events: null,
  3736. lastConfig: null,
  3737. pendingPlayStatus: null
  3738. };
  3739. },
  3740. beforeDestroy() {
  3741. this._destroyPlayer();
  3742. },
  3743. methods: {
  3744. _shouldReinitPlayer(newValue, oldValue) {
  3745. if (!this.player || !oldValue || !newValue) return true;
  3746. return newValue.url !== oldValue.url ||
  3747. newValue.playerId !== oldValue.playerId ||
  3748. newValue.isFast !== oldValue.isFast ||
  3749. newValue.videoReloadKey !== oldValue.videoReloadKey;
  3750. },
  3751. _getFullscreenTarget(playerId) {
  3752. const playerEl = document.getElementById(playerId);
  3753. if (playerEl) {
  3754. return playerEl.closest('.videovip-video-container') ||
  3755. playerEl.closest('.video-container') ||
  3756. playerEl.parentElement;
  3757. }
  3758. return document.querySelector('.videovip-video-container') || document.body;
  3759. },
  3760. _ensureXgVideoStyles() {
  3761. const attr = 'data-xg-videovip-fix-v2';
  3762. const old = document.querySelector('style[data-xg-videovip-fix]');
  3763. if (old) old.remove();
  3764. if (document.querySelector('style[' + attr + ']')) return;
  3765. const style = document.createElement('style');
  3766. style.setAttribute(attr, '1');
  3767. style.textContent = [
  3768. '.videovip-xg-player, .videovip-xg-player .xgplayer {',
  3769. ' width: 100% !important;',
  3770. ' height: 100% !important;',
  3771. '}',
  3772. '.videovip-xg-player video, .videovip-xg-player .xgplayer-video video {',
  3773. ' width: 100% !important;',
  3774. ' height: 100% !important;',
  3775. ' object-fit: contain !important;',
  3776. ' background: #000;',
  3777. '}',
  3778. '.xgplayer-is-fullscreen video,',
  3779. '.xgplayer-is-cssfullscreen video,',
  3780. '.xgplayer.xgplayer-is-fullscreen video,',
  3781. '.xgplayer.xgplayer-is-cssfullscreen video {',
  3782. ' object-fit: contain !important;',
  3783. '}'
  3784. ].join('\n');
  3785. document.head.appendChild(style);
  3786. },
  3787. _applyVideoFillMode(isFullscreen) {
  3788. if (!this.player) return;
  3789. const mode = isFullscreen ? 'contain' : 'cover';
  3790. try {
  3791. if (this.player.config) this.player.config.videoFillMode = mode;
  3792. if (typeof this.player.resize === 'function') this.player.resize();
  3793. } catch (e) {}
  3794. },
  3795. _destroyPlayer() {
  3796. if (!this.player) return;
  3797. if (this.isFullscreen) {
  3798. if (this.isCSSFull) {
  3799. this.player.exitCssFullscreen();
  3800. } else {
  3801. this.player.exitFullscreen();
  3802. }
  3803. }
  3804. try {
  3805. this.player.destroy();
  3806. } catch (e) {}
  3807. this.player = null;
  3808. },
  3809. _resetPortraitCssFullscreen() {
  3810. if (!this.player) return;
  3811. try {
  3812. this.player.exitCssFullscreen();
  3813. } catch (e) {}
  3814. this.isFullscreen = false;
  3815. },
  3816. _resizePlayerLayout() {
  3817. const portrait = this.lastConfig && this.lastConfig.displayType === 'portrait';
  3818. if (portrait) {
  3819. this._resetPortraitCssFullscreen();
  3820. }
  3821. const run = () => {
  3822. if (!this.player) return;
  3823. try {
  3824. this._applyVideoFillMode(!!this.isFullscreen);
  3825. if (typeof this.player.resize === 'function') this.player.resize();
  3826. } catch (e) {}
  3827. };
  3828. setTimeout(run, 0);
  3829. setTimeout(run, 80);
  3830. setTimeout(run, 220);
  3831. },
  3832. _notifyFullscreenToVue(isFullscreen) {
  3833. if (!this.ownerInstance) return;
  3834. const portrait = this.lastConfig && this.lastConfig.displayType === 'portrait';
  3835. if (portrait) {
  3836. const now = Date.now();
  3837. if (this._fsNotifyVal === isFullscreen && now - (this._fsNotifyAt || 0) < 150) return;
  3838. this._fsNotifyVal = isFullscreen;
  3839. this._fsNotifyAt = now;
  3840. }
  3841. this.ownerInstance.callMethod('onXgFullscreenChange', {
  3842. isFullscreen
  3843. });
  3844. },
  3845. initJs(newValue, oldValue, ownerInstance) {
  3846. if (!newValue || !newValue.url) return;
  3847. if (!this._shouldReinitPlayer(newValue, oldValue)) {
  3848. this.lastConfig = newValue;
  3849. this._syncProgressLock(!!newValue.isFast);
  3850. if (oldValue && newValue.layoutShuKey !== oldValue.layoutShuKey) {
  3851. this._resizePlayerLayout();
  3852. }
  3853. return;
  3854. }
  3855. const bootstrap = () => {
  3856. this._ensureXgVideoStyles();
  3857. this._waitForPlayerDom(newValue.playerId || 'video-content-box', () => {
  3858. this.initPlayer(newValue, oldValue, ownerInstance);
  3859. });
  3860. };
  3861. if (typeof window.Player === 'function') {
  3862. bootstrap();
  3863. return;
  3864. }
  3865. const script = document.createElement('script');
  3866. script.src = XG_SCRIPT;
  3867. script.onload = bootstrap;
  3868. document.head.appendChild(script);
  3869. if (!document.querySelector('link[data-xg-videovip-css]')) {
  3870. const link = document.createElement('link');
  3871. link.rel = 'stylesheet';
  3872. link.href = XG_CSS;
  3873. link.setAttribute('data-xg-videovip-css', '1');
  3874. document.body.appendChild(link);
  3875. }
  3876. },
  3877. _waitForPlayerDom(playerId, callback, attempt = 0) {
  3878. const el = document.getElementById(playerId);
  3879. if (el) {
  3880. callback(el);
  3881. return;
  3882. }
  3883. if (attempt >= 30) return;
  3884. setTimeout(() => this._waitForPlayerDom(playerId, callback, attempt + 1), 100);
  3885. },
  3886. _syncProgressLock(allowSeek) {
  3887. if (!this.player) return;
  3888. const progress = this.player.getPlugin && this.player.getPlugin('progress');
  3889. if (!progress) return;
  3890. progress.show && progress.show();
  3891. const lockSeek = !allowSeek;
  3892. if (progress.config) {
  3893. progress.config.isCloseClickSeek = lockSeek;
  3894. progress.config.closeMoveSeek = lockSeek;
  3895. progress.config.isDraggingSeek = false;
  3896. }
  3897. if (this.player.config && this.player.config.progress) {
  3898. this.player.config.progress.isCloseClickSeek = lockSeek;
  3899. this.player.config.progress.closeMoveSeek = lockSeek;
  3900. this.player.config.progress.isDraggingSeek = false;
  3901. }
  3902. },
  3903. initPlayer(newValue, oldValue, ownerInstance) {
  3904. const Player = window.Player;
  3905. this.Events = Player.Events;
  3906. if (!this.ownerInstance) this.ownerInstance = ownerInstance;
  3907. this.lastConfig = newValue;
  3908. const playerId = newValue.playerId || 'video-content-box';
  3909. const allowSeek = newValue.isFast == 1;
  3910. const portraitLayoutToggle = newValue.displayType === 'portrait';
  3911. const fsTarget = this._getFullscreenTarget(playerId);
  3912. const fullscreenOpt = portraitLayoutToggle ? {
  3913. useCssFullscreen: true
  3914. } : {
  3915. useScreenOrientation: true,
  3916. lockOrientationType: 'landscape'
  3917. };
  3918. const option = {
  3919. lang: 'zh',
  3920. url: newValue.url,
  3921. id: playerId,
  3922. autoplay: false,
  3923. loop: false,
  3924. height: '100%',
  3925. width: '100%',
  3926. poster: newValue.poster || '',
  3927. 'x5-video-player-type': 'h5',
  3928. playsinline: true,
  3929. videoFillMode: 'cover',
  3930. ignores: ['volume', 'miniscreen', 'keyboard', 'playbackrate'],
  3931. disableGesture: true,
  3932. closeVideoTouch: true,
  3933. closeVideoDblclick: true,
  3934. closeVideoClick: false,
  3935. progress: {
  3936. isDraggingSeek: false,
  3937. isCloseClickSeek: !allowSeek,
  3938. closeMoveSeek: !allowSeek
  3939. },
  3940. fullscreen: fullscreenOpt,
  3941. fullscreenTarget: fsTarget
  3942. };
  3943. if (this.player) {
  3944. this._destroyPlayer();
  3945. }
  3946. this.player = new Player(option);
  3947. this._syncProgressLock(allowSeek);
  3948. this._bindEvents();
  3949. this.ownerInstance.callMethod('onXgPlayerReady');
  3950. if (this.pendingPlayStatus) {
  3951. this.changeVideo(this.pendingPlayStatus, null, ownerInstance);
  3952. this.pendingPlayStatus = null;
  3953. }
  3954. },
  3955. _bindEvents() {
  3956. if (!this.player || !this.Events) return;
  3957. const Events = this.Events;
  3958. const owner = this.ownerInstance;
  3959. this.player.on(Events.PLAY, () => owner.callMethod('onXgVideoPlay'));
  3960. this.player.on(Events.PLAYING, () => owner.callMethod('onXgVideoPlay'));
  3961. this.player.on(Events.PAUSE, () => owner.callMethod('onXgVideoPause'));
  3962. this.player.on(Events.ENDED, () => owner.callMethod('onXgVideoEnded'));
  3963. this.player.on(Events.ERROR, (err) => owner.callMethod('onXgVideoError', {
  3964. error: err
  3965. }));
  3966. this.player.on(Events.TIME_UPDATE, () => owner.callMethod('onXgVideoTimeUpdate', {
  3967. currentTime: this.player.currentTime
  3968. }));
  3969. this.player.on(Events.PROGRESS, () => {
  3970. let buffered = 0;
  3971. try {
  3972. const buf = this.player.buffered;
  3973. if (buf && buf.length && this.player.duration) {
  3974. buffered = (buf.end(buf.length - 1) / this.player.duration) * 100;
  3975. }
  3976. } catch (e) {}
  3977. owner.callMethod('onXgVideoProgress', {
  3978. buffered
  3979. });
  3980. });
  3981. this.player.on(Events.FULLSCREEN_CHANGE, (isFullscreen) => {
  3982. if (this.isFullscreen == isFullscreen) return;
  3983. const portrait = this.lastConfig && this.lastConfig.displayType === 'portrait';
  3984. if (portrait) return;
  3985. this.isFullscreen = isFullscreen;
  3986. this._applyVideoFillMode(isFullscreen);
  3987. this._syncAppOrientationForFullscreen(isFullscreen);
  3988. this._notifyFullscreenToVue(isFullscreen);
  3989. });
  3990. this.player.on(Events.CSS_FULLSCREEN_CHANGE, (isFullscreen) => {
  3991. const portrait = this.lastConfig && this.lastConfig.displayType === 'portrait';
  3992. if (portrait) {
  3993. // 竖屏课:f 键仅切换页面布局,进入 CSS 全屏后立即退出,避免与 openList 切布局后状态不同步
  3994. if (isFullscreen) {
  3995. this.isCSSFull = true;
  3996. this._resetPortraitCssFullscreen();
  3997. this._notifyFullscreenToVue(true);
  3998. } else {
  3999. this._resetPortraitCssFullscreen();
  4000. }
  4001. return;
  4002. }
  4003. if (this.isFullscreen == isFullscreen) return;
  4004. this.isCSSFull = true;
  4005. this.isFullscreen = isFullscreen;
  4006. this._applyVideoFillMode(isFullscreen);
  4007. this._syncAppOrientationForFullscreen(isFullscreen);
  4008. this._notifyFullscreenToVue(isFullscreen);
  4009. });
  4010. },
  4011. _syncAppOrientationForFullscreen(isFullscreen) {
  4012. const isPortraitCourse = this.lastConfig && this.lastConfig.displayType === 'portrait';
  4013. try {
  4014. if (isPortraitCourse) {
  4015. if (!isFullscreen) {
  4016. plus.screen.lockOrientation('portrait-primary');
  4017. }
  4018. return;
  4019. }
  4020. if (isFullscreen) {
  4021. plus.screen.lockOrientation('landscape-primary');
  4022. } else {
  4023. plus.screen.lockOrientation('portrait-primary');
  4024. }
  4025. } catch (e) {}
  4026. },
  4027. handleExitFullscreen(newValue) {
  4028. if (!this.player || !newValue || String(newValue).indexOf('yes') === -1) return;
  4029. const portrait = this.lastConfig && this.lastConfig.displayType === 'portrait';
  4030. if (portrait) {
  4031. this._resetPortraitCssFullscreen();
  4032. this._syncAppOrientationForFullscreen(false);
  4033. return;
  4034. }
  4035. if (!this.isFullscreen) return;
  4036. if (this.isCSSFull) {
  4037. this.player.exitCssFullscreen();
  4038. } else {
  4039. this.player.exitFullscreen();
  4040. }
  4041. this.isFullscreen = false;
  4042. this._syncAppOrientationForFullscreen(false);
  4043. },
  4044. changeVideo(newValue, oldValue, ownerInstance) {
  4045. if (!newValue) return;
  4046. if (!this.ownerInstance) this.ownerInstance = ownerInstance;
  4047. if (!this.player) {
  4048. this.pendingPlayStatus = newValue;
  4049. return;
  4050. }
  4051. const status = newValue.status;
  4052. if (status === 'play') {
  4053. if (newValue.seekTime > 0) {
  4054. this.player.seek(newValue.seekTime);
  4055. this.player.once(this.Events.SEEKED, () => {
  4056. setTimeout(() => this.player.play(), 200);
  4057. });
  4058. } else {
  4059. setTimeout(() => this.player.play(), 200);
  4060. }
  4061. } else if (status === 'pause' || status === 'stop') {
  4062. this.player.pause();
  4063. } else if (status === 'seek') {
  4064. this.player.seek(newValue.seekTime || 0);
  4065. } else if (status === 'reload' && newValue.url) {
  4066. this.player.src = newValue.url;
  4067. if (newValue.seekTime > 0) this.player.seek(newValue.seekTime);
  4068. setTimeout(() => this.player.play(), 300);
  4069. }
  4070. }
  4071. }
  4072. };
  4073. </script>
  4074. <style scoped>
  4075. .full-width-popup {
  4076. width: 100%;
  4077. }
  4078. </style>
  4079. <style lang="scss" scoped>
  4080. .contact-btn {
  4081. display: inline-block;
  4082. position: absolute;
  4083. top: 0;
  4084. left: 0;
  4085. width: 100%;
  4086. height: 100%;
  4087. opacity: 0;
  4088. }
  4089. .notice-box{
  4090. padding: 12rpx 18rpx;
  4091. background: #FFF8F8;
  4092. display: flex;
  4093. align-items: center;
  4094. image{
  4095. width:40rpx;
  4096. height:40rpx;
  4097. margin-right: 10rpx;
  4098. flex-shrink: 0;
  4099. }
  4100. .notice-marquee-wrap{
  4101. flex: 1;
  4102. min-width: 0;
  4103. overflow: hidden;
  4104. display: flex;
  4105. align-items: center;
  4106. }
  4107. .notice-marquee-track{
  4108. display: inline-flex;
  4109. white-space: nowrap;
  4110. animation: notice-marquee-scroll 20s linear infinite;
  4111. }
  4112. .notice-text{
  4113. flex-shrink: 0;
  4114. padding-right: 60rpx;
  4115. font-family: PingFangSC, PingFang SC;
  4116. font-weight: 600;
  4117. font-size: 32rpx;
  4118. line-height: 44rpx;
  4119. color: #FF0C4D;
  4120. display: flex;
  4121. }
  4122. }
  4123. @keyframes notice-marquee-scroll{
  4124. 0%{ transform: translateX(0); }
  4125. 100%{ transform: translateX(-50%); }
  4126. }
  4127. .vip-order-cover-wrap {
  4128. position: absolute;
  4129. left: 10px;
  4130. bottom:40px;
  4131. height: 26px;
  4132. z-index: 999;
  4133. /* 容器按内容宽度自适应 */
  4134. display: inline-block;
  4135. overflow: visible;
  4136. pointer-events: none;
  4137. width: 200px;
  4138. }
  4139. .vip-order-cover-wrap--triple {
  4140. height: 110px;
  4141. //width: 240px;
  4142. }
  4143. .vip-order-cover-triple-panel {
  4144. display: flex;
  4145. align-items: flex-start;
  4146. flex-direction: column;
  4147. position: absolute;
  4148. left: 0;
  4149. right: auto;
  4150. top: 0;
  4151. }
  4152. .vip-order-cover-triple-current {
  4153. z-index: 1;
  4154. }
  4155. .vip-order-cover-triple-incoming {
  4156. z-index: 2;
  4157. }
  4158. .vip-order-cover-item {
  4159. position: absolute;
  4160. left: 0;
  4161. /* 不再撑满整块固定宽度,让背景宽度随内容变化 */
  4162. right: auto;
  4163. top: 0;
  4164. display: inline-flex;
  4165. align-items: center;
  4166. background: rgba(0,0,0,0.35);
  4167. border-radius: 26rpx;
  4168. text-align: center;
  4169. //border: 1px solid rgba(255, 12, 77, 0.2);
  4170. font-family: PingFangSC, PingFang SC;
  4171. font-size: 16px;
  4172. height: 26px;
  4173. line-height: 26px;
  4174. color: #fff;
  4175. font-weight: 400;
  4176. box-sizing: border-box;
  4177. }
  4178. .vip-order-cover-triple-item {
  4179. position: relative;
  4180. top: auto;
  4181. margin-bottom: 8px;
  4182. }
  4183. .vip-order-cover-triple-item:last-child {
  4184. margin-bottom: 0;
  4185. }
  4186. .vip-order-triple-enter {
  4187. animation: vip-order-triple-enter-up 0.46s ease-out forwards;
  4188. }
  4189. .vip-order-triple-leave {
  4190. animation: vip-order-triple-leave-up 0.46s ease-in forwards;
  4191. }
  4192. @keyframes vip-order-triple-enter-up {
  4193. 0% {
  4194. opacity: 0;
  4195. transform: translateY(38px);
  4196. }
  4197. 100% {
  4198. opacity: 1;
  4199. transform: translateY(0);
  4200. }
  4201. }
  4202. @keyframes vip-order-triple-leave-up {
  4203. 0% {
  4204. opacity: 1;
  4205. transform: translateY(0);
  4206. }
  4207. 78% {
  4208. opacity: 1;
  4209. transform: translateY(-34px);
  4210. }
  4211. 100% {
  4212. opacity: 0;
  4213. transform: translateY(-38px);
  4214. }
  4215. }
  4216. .vip-order-cover-content {
  4217. display: flex;
  4218. align-items: center;
  4219. flex-wrap: nowrap;
  4220. white-space: nowrap;
  4221. overflow: visible;
  4222. justify-content: flex-start;
  4223. height: 26px;
  4224. line-height: 26px;
  4225. padding:0 8px;
  4226. }
  4227. .vip-order-seg {
  4228. display: inline-block;
  4229. }
  4230. .vip-order-seg-user {
  4231. color: #fff;
  4232. //font-weight: 600;
  4233. }
  4234. .vip-order-seg-time {
  4235. // font-size: 14px;
  4236. margin-left: 6px;
  4237. }
  4238. .vip-order-seg-suffix {
  4239. color: #fff;
  4240. // margin-left: 2px;
  4241. }
  4242. .vip-order-cover-self {
  4243. // width: 180px !important;
  4244. background: rgba(230,0,26,0.5);
  4245. // border: 1px solid #FFB020;
  4246. color: #fff;
  4247. }
  4248. .vip-order-cover-self .vip-order-seg-user {
  4249. color: #fff;
  4250. }
  4251. .vip-order-cover-self .vip-order-seg-time {
  4252. color: #fff;
  4253. margin-left: 6px;
  4254. }
  4255. .vip-order-cover-self .vip-order-seg-suffix {
  4256. color: #fff;
  4257. }
  4258. .vip-order-enter {
  4259. animation: vip-order-enter-up 0.46s ease-out forwards;
  4260. }
  4261. .vip-order-leave {
  4262. animation: vip-order-leave-up 0.46s ease-in forwards;
  4263. }
  4264. @keyframes vip-order-enter-up {
  4265. 0% {
  4266. opacity: 0;
  4267. transform: translateY(24px);
  4268. }
  4269. 100% {
  4270. opacity: 1;
  4271. transform: translateY(0);
  4272. }
  4273. }
  4274. @keyframes vip-order-leave-up {
  4275. 0% {
  4276. opacity: 1;
  4277. transform: translateY(0);
  4278. }
  4279. 100% {
  4280. opacity: 0;
  4281. transform: translateY(-24px);
  4282. }
  4283. }
  4284. .rating-box{
  4285. position: relative;
  4286. padding:24rpx 0rpx 24rpx;
  4287. z-index:0;
  4288. height: calc(100vh - 488rpx);
  4289. .bg {
  4290. width: 100%;
  4291. // height: 500rpx;
  4292. position: absolute;
  4293. top: 0;
  4294. left: 0;
  4295. z-index: -1;
  4296. }
  4297. .tab-Box {
  4298. display: flex;
  4299. align-items: flex-end;
  4300. justify-content: space-around;
  4301. padding: 0 8rpx;
  4302. margin-bottom: -2rpx;
  4303. position: relative;
  4304. z-index: 3;
  4305. }
  4306. .tab-Box--single {
  4307. align-items: center;
  4308. justify-content: flex-start;
  4309. padding: 0 24rpx 0 16rpx;
  4310. margin-bottom: 30rpx;
  4311. }
  4312. .tab-item--single {
  4313. flex: 0 1 auto;
  4314. justify-content: flex-start;
  4315. // width: 100%;
  4316. padding: 0;
  4317. }
  4318. .tab-item__surface--single {
  4319. flex-direction: row;
  4320. align-items: center;
  4321. flex-direction: row !important;
  4322. justify-content: flex-start;
  4323. width: auto;
  4324. max-width: 100%;
  4325. border-radius: 0;
  4326. image {
  4327. margin-top: 0!important;
  4328. margin-right: 16rpx;
  4329. flex-shrink: 0;
  4330. }
  4331. }
  4332. .tab-item__text--single {
  4333. padding-top: 0!important;
  4334. padding-bottom: 0!important;
  4335. text-align: left;
  4336. font-weight: 600 !important;
  4337. font-size: 40rpx !important;
  4338. line-height: 56rpx !important;
  4339. }
  4340. .tab-item {
  4341. // flex: 1;
  4342. display: flex;
  4343. justify-content: center;
  4344. min-width: 0;
  4345. padding: 0 6rpx;
  4346. }
  4347. .tab-item__surface {
  4348. width: 100%;
  4349. display: flex;
  4350. flex-direction: column;
  4351. align-items: center;
  4352. justify-content: flex-end;
  4353. box-sizing: border-box;
  4354. border-radius: 28rpx 28rpx 0 0;
  4355. transition: background-color 0.28s ease, box-shadow 0.28s ease, transform 0.22s ease;
  4356. image{
  4357. margin-top: 12rpx;
  4358. width: 40rpx;
  4359. height: 40rpx;
  4360. }
  4361. }
  4362. .tab-item--active .tab-item__surface {
  4363. background:url('https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/video/active-bg.png') no-repeat center;
  4364. background-size: 100% 100%;
  4365. padding: 0 70rpx;
  4366. transform: translateY(-2rpx);
  4367. }
  4368. .tab-item__text {
  4369. padding-top: 10rpx;
  4370. padding-bottom: 12rpx;
  4371. font-family: PingFangSC, PingFang SC;
  4372. font-weight: 400;
  4373. font-size: 36rpx;
  4374. line-height: 50rpx;
  4375. color: #ff233c;
  4376. text-align: center;
  4377. }
  4378. .tab-item--active .tab-item__text{
  4379. font-weight: 600;
  4380. font-size: 40rpx;
  4381. }
  4382. .rating-ques-block + .rating-ques-block {
  4383. margin-top: 40rpx;
  4384. }
  4385. .rating-body--plain {
  4386. // padding-top: 36rpx;
  4387. // padding-bottom: 40rpx;
  4388. image {
  4389. width: 100%;
  4390. }
  4391. }
  4392. .rating-intro-cover-img {
  4393. width: 100%;
  4394. height: auto;
  4395. display: block;
  4396. }
  4397. .rating-intro-text {
  4398. margin-top: 24rpx;
  4399. font-size: 28rpx;
  4400. line-height: 44rpx;
  4401. color: rgba(0, 0, 0, 0.65);
  4402. white-space: pre-wrap;
  4403. word-break: break-word;
  4404. }
  4405. .rating-msg-empty {
  4406. padding: 80rpx 24rpx;
  4407. text-align: center;
  4408. font-size: 28rpx;
  4409. color: rgba(0, 0, 0, 0.35);
  4410. }
  4411. .rating-body.rating-body--featured {
  4412. display: flex;
  4413. flex-direction: column;
  4414. overflow: hidden;
  4415. padding: 30rpx 24rpx 20rpx;
  4416. }
  4417. .rating-msg-scroll {
  4418. height:100%;
  4419. width: 100%;
  4420. box-sizing: border-box;
  4421. }
  4422. .rating-msg-scroll-anchor {
  4423. width: 100%;
  4424. height: 1px;
  4425. }
  4426. .rating-msg-load-more {
  4427. padding: 24rpx 0 32rpx;
  4428. text-align: center;
  4429. font-size: 26rpx;
  4430. color: rgba(0, 0, 0, 0.4);
  4431. }
  4432. .rating-msg-load-more--done {
  4433. color: rgba(0, 0, 0, 0.28);
  4434. }
  4435. .rating-msg-item {
  4436. display: flex;
  4437. flex-direction: row;
  4438. align-items: flex-start;
  4439. gap: 20rpx;
  4440. padding-bottom: 40rpx;
  4441. &:not(:first-child) {
  4442. padding-top: 8rpx;
  4443. }
  4444. }
  4445. .rating-msg-avatar-wrap {
  4446. flex-shrink: 0;
  4447. width: 64rpx;
  4448. }
  4449. .rating-msg-main {
  4450. flex: 1;
  4451. min-width: 0;
  4452. display: flex;
  4453. flex-direction: column;
  4454. align-items: stretch;
  4455. }
  4456. .rating-msg-avatar {
  4457. width: 64rpx;
  4458. height: 64rpx;
  4459. border-radius: 50%;
  4460. flex-shrink: 0;
  4461. background: #ffe4ec;
  4462. display: block;
  4463. }
  4464. .rating-msg-avatar--ph {
  4465. display: flex;
  4466. align-items: center;
  4467. justify-content: center;
  4468. background: linear-gradient(145deg, #ffd6e8 0%, #ffc8df 100%);
  4469. }
  4470. .rating-msg-name {
  4471. font-size: 32rpx;
  4472. color: rgba(0,0,0,0.65);
  4473. font-family: PingFangSC, PingFang SC;
  4474. font-weight: 400;
  4475. line-height: 44rpx;
  4476. }
  4477. .rating-msg-text {
  4478. margin-top: 12rpx;
  4479. font-size: 28rpx;
  4480. line-height: 44rpx;
  4481. color: rgba(0, 0, 0, 0.85);
  4482. word-break: break-word;
  4483. }
  4484. .rating-msg-media {
  4485. margin-top: 12rpx;
  4486. //border-radius: 16rpx;
  4487. overflow: hidden;
  4488. }
  4489. .rating-msg-media--video {
  4490. overflow: visible;
  4491. }
  4492. .rating-msg-video-wrap {
  4493. position: relative;
  4494. width: 320rpx;
  4495. max-width: 100%;
  4496. height: 180rpx;
  4497. min-height: 180rpx;
  4498. background: #000;
  4499. overflow: hidden;
  4500. }
  4501. .rating-msg-video {
  4502. width: 100%;
  4503. height: 100%;
  4504. display: block;
  4505. }
  4506. /* 小程序列表内用封面图代替 video,固定比例避免布局抖动 */
  4507. .rating-msg-video--poster {
  4508. width: 320rpx;
  4509. max-width: 100%;
  4510. height: 180rpx;
  4511. vertical-align: top;
  4512. }
  4513. /* 仅有视频地址无封面时:不能用视频 URL 当 image.src */
  4514. .rating-msg-video--no-poster {
  4515. box-sizing: border-box;
  4516. width: 320rpx;
  4517. max-width: 100%;
  4518. height: 180rpx;
  4519. background: linear-gradient(160deg, #2a2a2e 0%, #121214 100%);
  4520. }
  4521. .rating-msg-video-play-cover {
  4522. position: absolute;
  4523. left: 0;
  4524. top: 0;
  4525. right: 0;
  4526. bottom: 0;
  4527. z-index: 2;
  4528. display: flex;
  4529. align-items: center;
  4530. justify-content: center;
  4531. background: rgba(0, 0, 0, 0.18);
  4532. }
  4533. .rating-msg-video-play-icon {
  4534. width: 88rpx;
  4535. height: 88rpx;
  4536. flex-shrink: 0;
  4537. }
  4538. .rating-msg-media--imgs {
  4539. display: flex;
  4540. flex-wrap: wrap;
  4541. justify-content: flex-start;
  4542. gap: 12rpx; /* 图片之间间距,跟朋友圈一致 */
  4543. }
  4544. .rating-msg-media--cols-1 {
  4545. grid-template-columns: 1fr;
  4546. }
  4547. .rating-msg-media--cols-2 {
  4548. grid-template-columns: repeat(2, 1fr);
  4549. }
  4550. .rating-msg-media--cols-3 {
  4551. grid-template-columns: repeat(3, 1fr);
  4552. }
  4553. .rating-msg-img {
  4554. flex-shrink: 0;
  4555. /* 关键:不设固定宽高,只限制最大宽度,和朋友圈一致 */
  4556. max-width: 320rpx;
  4557. height: auto;
  4558. }
  4559. .rating-msg-media--cols-1 .rating-msg-img {
  4560. }
  4561. .rating-msg-toolbar {
  4562. flex-shrink: 0;
  4563. padding-top: 20rpx;
  4564. margin-top: 8rpx;
  4565. border-top: 1rpx solid rgba(0, 0, 0, 0.06);
  4566. }
  4567. .rating-msg-toolbar-row {
  4568. display: flex;
  4569. align-items: flex-end;
  4570. justify-content: space-between;
  4571. gap: 16rpx;
  4572. }
  4573. .rating-msg-side-actions {
  4574. display: flex;
  4575. align-items: flex-end;
  4576. flex-shrink: 0;
  4577. gap: 20rpx;
  4578. }
  4579. .rating-msg-dash-btn {
  4580. display: flex;
  4581. flex-direction: column;
  4582. align-items: center;
  4583. width: 100rpx;
  4584. }
  4585. .rating-msg-dash-inner {
  4586. width: 80rpx;
  4587. height: 80rpx;
  4588. border: 2rpx dashed #c8c8c8;
  4589. border-radius: 16rpx;
  4590. display: flex;
  4591. align-items: center;
  4592. justify-content: center;
  4593. box-sizing: border-box;
  4594. background: #fafafa;
  4595. }
  4596. .rating-msg-dash-img {
  4597. width: 44rpx;
  4598. height: 44rpx;
  4599. }
  4600. .rating-msg-dash-label {
  4601. margin-top: 8rpx;
  4602. font-size: 24rpx;
  4603. color: #222222;
  4604. font-weight: 600;
  4605. line-height: 34rpx;
  4606. }
  4607. .rating-msg-cart-slot {
  4608. position: relative;
  4609. display: flex;
  4610. flex-direction: column;
  4611. align-items: center;
  4612. }
  4613. .rating-msg-badge {
  4614. position: absolute;
  4615. right: -28rpx;
  4616. top: -72rpx;
  4617. width: 180rpx;
  4618. height: 76rpx;
  4619. z-index: 2;
  4620. pointer-events: none;
  4621. }
  4622. .rating-body{
  4623. //margin-top: 40rpx;
  4624. position: relative;
  4625. z-index: 1;
  4626. padding:30rpx;
  4627. box-sizing: border-box;
  4628. background: #FFFFFF;
  4629. border-radius: 30rpx 30rpx;
  4630. height: calc(100% - 300rpx);
  4631. overflow-y: auto;
  4632. -webkit-overflow-scrolling: touch;
  4633. margin-top: 0;
  4634. .title{
  4635. font-family: PingFangSC, PingFang SC;
  4636. font-weight: 600;
  4637. font-size: 40rpx;
  4638. color: rgba(0,0,0,0.85);
  4639. line-height: 56rpx;
  4640. }
  4641. }
  4642. .sat-box{
  4643. display: grid;
  4644. grid-template-columns: repeat(2, 1fr);
  4645. gap: 28rpx 24rpx;
  4646. margin-top: 32rpx;
  4647. }
  4648. .sat{
  4649. display: flex;
  4650. flex-direction: column;
  4651. align-items: center;
  4652. justify-content: center;
  4653. text-align: center;
  4654. background: #F5F7FA;
  4655. padding: 20rpx;
  4656. border-radius: 20rpx;
  4657. transition: background 0.3s ease, box-shadow 0.3s ease;
  4658. .sat-img-wrap{
  4659. width: 100rpx;
  4660. height: 100rpx;
  4661. display: flex;
  4662. align-items: center;
  4663. justify-content: center;
  4664. will-change: transform;
  4665. image{
  4666. width: 100rpx;
  4667. height: 100rpx;
  4668. transition: transform 0.35s cubic-bezier(0.34, 1.3, 0.64, 1);
  4669. }
  4670. }
  4671. .sat-img-bounce{
  4672. animation: sat-rate-pop 0.48s cubic-bezier(0.34, 1.45, 0.64, 1);
  4673. }
  4674. text{
  4675. margin-top: 10rpx;
  4676. font-family: PingFangSC, PingFang SC;
  4677. font-weight: 400;
  4678. font-size: 36rpx;
  4679. line-height: 50rpx;
  4680. transition: color 0.32s ease, font-weight 0.28s ease, transform 0.28s ease;
  4681. color: rgba(0,0,0,0.42);
  4682. }
  4683. .active{
  4684. font-weight: 600;
  4685. color: #FF233C;
  4686. }
  4687. }
  4688. .sat.sat--active{
  4689. background: #FFEEF0;
  4690. //box-shadow: 0 4rpx 16rpx rgba(255, 92, 3, 0.12);
  4691. .sat-img-wrap image{
  4692. transform: scale(1.06);
  4693. }
  4694. }
  4695. }
  4696. @keyframes sat-rate-pop{
  4697. 0%{ transform: scale(1); }
  4698. 28%{ transform: scale(1.2); }
  4699. 52%{ transform: scale(0.94); }
  4700. 76%{ transform: scale(1.06); }
  4701. 100%{ transform: scale(1); }
  4702. }
  4703. /* 精选留言:相册图片/视频(替代系统 ActionSheet,底部贴齐含安全区) */
  4704. .fc-media-actions-sheet {
  4705. display: flex;
  4706. flex-direction: column;
  4707. width: 100%;
  4708. box-sizing: border-box;
  4709. padding-bottom:68rpx;
  4710. }
  4711. .fc-media-actions-sheet-item {
  4712. padding: 30rpx 32rpx;
  4713. font-size: 32rpx;
  4714. color: #1e1e1e;
  4715. text-align: center;
  4716. border-bottom: 1rpx solid #f0f0f0;
  4717. }
  4718. .fc-media-actions-sheet-gap {
  4719. height: 16rpx;
  4720. background: #f3f5f9;
  4721. border-bottom: none;
  4722. }
  4723. .fc-media-actions-sheet-cancel {
  4724. border-bottom: none;
  4725. color: #666;
  4726. }
  4727. // 更多操作弹窗
  4728. .more-actions-popup {
  4729. border-radius: 20rpx 0 0 20rpx;
  4730. padding: 30rpx;
  4731. display: flex;
  4732. justify-content: space-between;
  4733. .more-action-item {
  4734. display: flex;
  4735. flex-direction: column;
  4736. align-items: center;
  4737. .action-icon {
  4738. width: 48rpx;
  4739. height: 48rpx;
  4740. }
  4741. .action-label {
  4742. color: #1e1e1e;
  4743. text-align: center;
  4744. margin-top: 10rpx;
  4745. }
  4746. }
  4747. }
  4748. .progress-countdown {
  4749. position: absolute;
  4750. right: 6px;
  4751. top: 6px;
  4752. z-index: 99999999;
  4753. // width: 112px;
  4754. height: 50px;
  4755. background: rgba(0,0,0,0.45);
  4756. display: flex;
  4757. align-items: center;
  4758. justify-content: space-around;
  4759. padding:2px 8px;
  4760. flex-direction: column;
  4761. border-radius: 6px;
  4762. box-sizing: border-box; /* 避免padding撑大容器 */
  4763. }
  4764. .progress-title {
  4765. font-size: 16px;
  4766. color: #FF233C;
  4767. font-weight: 600;
  4768. }
  4769. .progress-img{
  4770. width: 63px;
  4771. height: 15px;
  4772. }
  4773. .progress-bar-bg {
  4774. width: 86px;
  4775. height: 6px;
  4776. //margin-top: 16rpx;
  4777. background: rgba(255,255,255,0.8);
  4778. border-radius: 3px;
  4779. overflow: hidden;
  4780. // margin-bottom: 16rpx;
  4781. }
  4782. .progress-bar-fill {
  4783. height: 100%;
  4784. background: #FF233C;
  4785. border-radius: 3px;
  4786. transition: width 1s linear;
  4787. }
  4788. .progress-text {
  4789. white-space: nowrap; /* 强制不换行 */
  4790. color: #fff;
  4791. font-size: 13px;
  4792. display: flex;
  4793. align-items: flex-start; /* 垂直居中 */
  4794. justify-content: center; /* 水平居中 */
  4795. }
  4796. .progress-text-label {
  4797. font-size: 12px;
  4798. margin-right: 2px;
  4799. }
  4800. .ques-flex {
  4801. display: flex;
  4802. align-items: center;
  4803. flex-direction: row;
  4804. justify-content: space-around;
  4805. }
  4806. @mixin u-flex($flexD, $alignI, $justifyC) {
  4807. display: flex;
  4808. flex-direction: $flexD;
  4809. align-items: $alignI;
  4810. justify-content: $justifyC;
  4811. }
  4812. .footer-tips {
  4813. position: fixed;
  4814. width: 100%;
  4815. bottom: 144rpx;
  4816. text-align: center;
  4817. font-family: PingFang SC, PingFang SC;
  4818. font-weight: 500;
  4819. font-size: 12px;
  4820. color: #bbb;
  4821. transform: scale(0.8);
  4822. }
  4823. .btns {
  4824. //position: relative;
  4825. width: 100%;
  4826. height: 96rpx;
  4827. display: flex;
  4828. align-items: center;
  4829. justify-content: space-between;
  4830. .rating-msg-input-shell {
  4831. flex: 1;
  4832. min-width: 0;
  4833. display: flex;
  4834. align-items: center;
  4835. background: #f5f5f5;
  4836. border-radius: 48rpx;
  4837. padding: 24rpx 30rpx;
  4838. box-sizing: border-box;
  4839. }
  4840. .rating-msg-media-pill {
  4841. position: relative;
  4842. flex-shrink: 0;
  4843. width: 64rpx;
  4844. height: 64rpx;
  4845. margin-right: 16rpx;
  4846. border-radius: 12rpx;
  4847. overflow: hidden;
  4848. }
  4849. .rating-msg-media-thumb {
  4850. width: 100%;
  4851. height: 100%;
  4852. display: block;
  4853. }
  4854. .rating-msg-media-badge {
  4855. position: absolute;
  4856. left: 0;
  4857. right: 0;
  4858. bottom: 0;
  4859. font-size: 18rpx;
  4860. color: #fff;
  4861. text-align: center;
  4862. line-height: 1.4;
  4863. background: rgba(0, 0, 0, 0.45);
  4864. }
  4865. .rating-msg-media-x {
  4866. position: absolute;
  4867. top: -4rpx;
  4868. right: -4rpx;
  4869. width: 32rpx;
  4870. height: 32rpx;
  4871. line-height: 32rpx;
  4872. text-align: center;
  4873. font-size: 28rpx;
  4874. color: #fff;
  4875. background: rgba(0, 0, 0, 0.5);
  4876. border-radius: 50%;
  4877. }
  4878. .rating-msg-send {
  4879. flex-shrink: 0;
  4880. padding: 0 12rpx 0 8rpx;
  4881. font-size: 28rpx;
  4882. color: #FF233C;
  4883. }
  4884. .rating-msg-input {
  4885. flex: 1;
  4886. min-width: 0;
  4887. font-size: 32rpx;
  4888. color: rgba(0, 0, 0, 0.85);
  4889. height: 48rpx;
  4890. line-height: 48rpx;
  4891. }
  4892. .rating-msg-input-trigger {
  4893. overflow: hidden;
  4894. }
  4895. .rating-msg-input-trigger-text {
  4896. display: block;
  4897. font-size: 32rpx;
  4898. overflow: hidden;
  4899. text-overflow: ellipsis;
  4900. white-space: nowrap;
  4901. color: rgba(0, 0, 0, 0.85);
  4902. &.is-ph {
  4903. color: rgba(0, 0, 0, 0.28);
  4904. }
  4905. }
  4906. .rating-msg-ph {
  4907. color: rgba(0, 0, 0, 0.28);
  4908. font-size: 32rpx;
  4909. }
  4910. .rating-msg-input-icon {
  4911. // width:40rpx;
  4912. // height:40rpx;
  4913. flex-shrink: 0;
  4914. padding-left:24rpx;
  4915. display:flex;
  4916. image{
  4917. width:40rpx;
  4918. height:40rpx
  4919. }
  4920. }
  4921. .author-btn {
  4922. // z-index: 100;
  4923. // position: absolute;
  4924. // width: 100%;
  4925. flex:1;
  4926. height: 96rpx;
  4927. background: linear-gradient( 135deg, #FF5267 0%, #FF233C 100%);
  4928. border-radius: 48rpx;
  4929. font-family: PingFangSC, PingFang SC;
  4930. font-weight: 600;
  4931. font-style: normal;
  4932. font-size: 40rpx;
  4933. color: #FFFFFF;
  4934. text-align: center;
  4935. line-height: 96rpx;
  4936. display: flex;
  4937. align-items: center;
  4938. justify-content: center;
  4939. image{
  4940. margin-left: 4rpx;
  4941. width: 40rpx;
  4942. height: 40rpx;
  4943. }
  4944. }
  4945. .author-cart{
  4946. padding-left: 40rpx;
  4947. display: flex;
  4948. align-items: center;
  4949. .more-box{
  4950. display: flex;
  4951. align-items: center;
  4952. flex-direction: column;
  4953. .tips{
  4954. font-family: PingFangSC, PingFang SC;
  4955. font-weight: 600;
  4956. font-size: 32rpx;
  4957. color: #222222;
  4958. line-height: 44rpx;
  4959. text-align: center;
  4960. margin-top: 12rpx;
  4961. }
  4962. image{
  4963. width: 80rpx;
  4964. height: 80rpx;
  4965. }
  4966. &:last-child{
  4967. margin-left: 40rpx;
  4968. }
  4969. }
  4970. .xianshi{
  4971. position: absolute;
  4972. z-index: 10;
  4973. right:20rpx;
  4974. bottom: 180rpx;
  4975. width: 214rpx;
  4976. height:90rpx;
  4977. }
  4978. }
  4979. }
  4980. .textOne {
  4981. overflow: hidden;
  4982. white-space: nowrap;
  4983. text-overflow: ellipsis;
  4984. }
  4985. .textTwo {
  4986. overflow: hidden;
  4987. text-overflow: ellipsis;
  4988. display: -webkit-box;
  4989. -webkit-line-clamp: 2;
  4990. -webkit-box-orient: vertical;
  4991. }
  4992. .header-nav {
  4993. height: 88rpx;
  4994. @include u-flex(row, center, flex-start);
  4995. overflow: hidden;
  4996. background-color: #fff;
  4997. box-sizing: border-box;
  4998. .header-title {
  4999. text-align: center;
  5000. overflow: hidden;
  5001. white-space: nowrap;
  5002. text-overflow: ellipsis;
  5003. padding: 0 10rpx 0 100rpx;
  5004. font-family: PingFang SC, PingFang SC;
  5005. font-weight: 500;
  5006. font-size: 15px;
  5007. color: #000;
  5008. box-sizing: border-box;
  5009. }
  5010. }
  5011. .reward-list {
  5012. width: 100%;
  5013. margin-top: 20rpx;
  5014. margin-bottom: -40rpx;
  5015. &-group {
  5016. font-family: PingFang SC, PingFang SC;
  5017. font-weight: 400;
  5018. font-size: 14px;
  5019. color: #222222;
  5020. @include u-flex(row, center, center);
  5021. }
  5022. &-option {
  5023. @include u-flex(row, center, flex-start);
  5024. &:first-child {
  5025. margin-right: 40rpx;
  5026. }
  5027. }
  5028. }
  5029. .err {
  5030. color: #f56c6c !important;
  5031. }
  5032. .kfqrcode-box {
  5033. background-color: #fff;
  5034. border-radius: 16rpx;
  5035. max-width: 560rpx;
  5036. padding: 60rpx 40rpx;
  5037. margin-top: -100rpx;
  5038. box-sizing: border-box;
  5039. @include u-flex(column, center, flex-start);
  5040. font-family: PingFang SC, PingFang SC;
  5041. font-weight: 400;
  5042. font-size: 34rpx;
  5043. color: #222;
  5044. position: relative;
  5045. text-align: center;
  5046. .kfqrcode {
  5047. // height: 460rpx;
  5048. width: 460rpx;
  5049. }
  5050. }
  5051. .kfqrcode-close {
  5052. width: 64rpx;
  5053. height: 64rpx;
  5054. position: absolute;
  5055. bottom: -100rpx;
  5056. left: 50%;
  5057. transform: translateX(-50%);
  5058. }
  5059. .tipsPopup-mask {
  5060. position: relative;
  5061. width: 560rpx;
  5062. background-color: transparent;
  5063. .red_envelope_top {
  5064. width: 480rpx;
  5065. height: 360rpx;
  5066. margin: 0 auto;
  5067. display: inherit;
  5068. }
  5069. }
  5070. .tipsPopup-btn-box {
  5071. width: 456rpx;
  5072. height: 104rpx;
  5073. padding: 4rpx;
  5074. box-sizing: border-box;
  5075. background: linear-gradient(180deg, rgba(252, 209, 94, 1), rgba(254, 253, 251, 1));
  5076. border-radius: 52rpx;
  5077. }
  5078. .tipsPopup-btn {
  5079. width: 100%;
  5080. height: 100%;
  5081. background: linear-gradient(180deg, #FF9F22 0%, #FA1E05 100%);
  5082. border-radius: 52rpx 52rpx 52rpx 52rpx;
  5083. font-family: PingFang SC, PingFang SC;
  5084. font-weight: 500;
  5085. font-size: 36rpx;
  5086. color: #FFFFFF;
  5087. line-height: 96rpx;
  5088. text-align: center;
  5089. }
  5090. .tipsPopup {
  5091. width: 560rpx;
  5092. padding: 12rpx;
  5093. margin-top: -72rpx;
  5094. box-sizing: border-box;
  5095. background: linear-gradient(180deg, #FFFBEF 0%, #FFFFF5 43%, #F5EAC2 100%);
  5096. border-radius: 32rpx 32rpx 32rpx 32rpx;
  5097. position: relative;
  5098. &-close {
  5099. width: 64rpx;
  5100. height: 64rpx;
  5101. position: absolute;
  5102. right: 0;
  5103. top: -188rpx;
  5104. }
  5105. &-line {
  5106. padding: 3rpx;
  5107. box-sizing: border-box;
  5108. background: linear-gradient(180deg, rgba(247, 245, 220, 1), rgba(250, 220, 157, 1));
  5109. border-radius: 24rpx;
  5110. }
  5111. &-box {
  5112. padding: 0 40rpx 40rpx 40rpx;
  5113. box-sizing: border-box;
  5114. background: linear-gradient(180deg, #FFFBEF 0%, #FFFFF5 43%, #F5EAC2 100%);
  5115. border-radius: 24rpx;
  5116. @include u-flex(column, center, flex-start);
  5117. }
  5118. &-head {
  5119. @include u-flex(row, center, center);
  5120. &-title {
  5121. width: 364rpx;
  5122. height: auto;
  5123. margin-top: -22rpx;
  5124. }
  5125. }
  5126. &-content {
  5127. margin: 48rpx 0;
  5128. font-family: PingFang SC, PingFang SC;
  5129. font-weight: 500;
  5130. font-size: 32rpx;
  5131. color: #222222;
  5132. text-align: center;
  5133. &-title {
  5134. margin-bottom: 26rpx;
  5135. font-weight: 600;
  5136. font-size: 40rpx;
  5137. color: #FF5C03;
  5138. }
  5139. }
  5140. }
  5141. // 商品卡片样式
  5142. .goods-card {
  5143. position: absolute;
  5144. right: 24rpx;
  5145. bottom: 280rpx;
  5146. z-index: 9999;
  5147. width: 300rpx;
  5148. background: #FFFFFF;
  5149. border-radius: 20rpx;
  5150. overflow: visible;
  5151. &-hot {
  5152. position: absolute;
  5153. top: -66rpx;
  5154. left: 30rpx;
  5155. z-index: 11;
  5156. display: flex;
  5157. flex-direction: row;
  5158. align-items: center;
  5159. }
  5160. &-hot-flame {
  5161. font-size: 24rpx;
  5162. line-height: 1;
  5163. margin-right: 4rpx;
  5164. }
  5165. &-hot-label {
  5166. font-size: 24rpx;
  5167. font-weight: 600;
  5168. color: #FFFFFF;
  5169. line-height: 44rpx;
  5170. }
  5171. &-hot-count {
  5172. position: absolute;
  5173. left:130rpx;
  5174. font-family: PingFangSC, PingFang SC;
  5175. font-weight: 600;
  5176. font-size: 40rpx;
  5177. color: #FFFFFF;
  5178. line-height: 56rpx;
  5179. text-align: justify;
  5180. }
  5181. &-inner {
  5182. display: flex;
  5183. flex-direction:column;
  5184. border-radius: 20rpx;
  5185. overflow: hidden;
  5186. }
  5187. &-img {
  5188. width:300rpx;
  5189. height:300rpx;
  5190. flex-shrink: 0;
  5191. }
  5192. &-info {
  5193. flex: 1;
  5194. padding: 12rpx;
  5195. display: flex;
  5196. flex-direction: column;
  5197. justify-content: space-between;
  5198. }
  5199. &-title {
  5200. font-family: PingFangSC, PingFang SC;
  5201. font-weight: 600;
  5202. font-size: 32rpx;
  5203. color: rgba(0,0,0,0.85);
  5204. line-height: 44rpx;
  5205. overflow: hidden;
  5206. text-overflow: ellipsis;
  5207. display: -webkit-box;
  5208. -webkit-line-clamp: 2;
  5209. -webkit-box-orient: vertical;
  5210. }
  5211. &-bottom {
  5212. margin-top: 12rpx;
  5213. display: flex;
  5214. flex-direction: row;
  5215. align-items: center;
  5216. //justify-content: space-between;
  5217. }
  5218. &-tag {
  5219. position: relative;
  5220. width: 120rpx;
  5221. height: 44rpx;
  5222. }
  5223. &-tag-bg {
  5224. width: 100%;
  5225. height: 100%;
  5226. }
  5227. &-tag-text {
  5228. position: absolute;
  5229. left: 0;
  5230. top: 0;
  5231. width: 100%;
  5232. height: 100%;
  5233. text-align: center;
  5234. line-height: 44rpx;
  5235. font-size: 24rpx;
  5236. color: #FFFFFF;
  5237. font-weight: 500;
  5238. }
  5239. &-price{
  5240. margin-left: 14rpx;
  5241. display: flex;
  5242. align-items:baseline;
  5243. .unit{
  5244. font-family: PingFangSC, PingFang SC;
  5245. font-weight: 600;
  5246. font-size: 32rpx;
  5247. color: #FF233C;
  5248. }
  5249. .price{
  5250. font-family: PingFangSC, PingFang SC;
  5251. font-weight: 600;
  5252. font-size: 48rpx;
  5253. color: #FF233C;
  5254. }
  5255. }
  5256. .close-box{
  5257. position: absolute;
  5258. top:20rpx;
  5259. right:20rpx;
  5260. z-index:9;
  5261. image{
  5262. width: 30rpx;
  5263. height: 30rpx;
  5264. }
  5265. }
  5266. }
  5267. /* 商品卡片样式 */
  5268. .goods-cover {
  5269. /* cover-view 在 video 内使用 absolute 比 fixed 更稳定 */
  5270. position:fixed;
  5271. right: 12px;
  5272. width: 150px;
  5273. z-index: 99999;
  5274. border-radius: 10px;
  5275. overflow: visible;
  5276. bottom: 40px;
  5277. &-hot {
  5278. // margin-left: 15px;
  5279. margin-bottom: 15px;
  5280. display: flex;
  5281. flex-direction: row;
  5282. align-items: center;
  5283. position: relative;
  5284. }
  5285. &-hot-text {
  5286. position: absolute;
  5287. left:85px;
  5288. font-family: PingFangSC, PingFang SC;
  5289. font-weight: 600;
  5290. font-size: 20px;
  5291. color: #FFFFFF;
  5292. line-height: 28px;
  5293. text-align: justify;
  5294. }
  5295. &-inner {
  5296. display: flex;
  5297. flex-direction:column;
  5298. border-radius: 10px;
  5299. overflow: hidden;
  5300. background: #FFFFFF;
  5301. position: relative;
  5302. }
  5303. &-img {
  5304. width:150px;
  5305. height:150px;
  5306. flex-shrink: 0;
  5307. }
  5308. &-info {
  5309. flex: 1;
  5310. padding: 6px;
  5311. display: flex;
  5312. flex-direction: column;
  5313. justify-content: space-between;
  5314. }
  5315. &-title {
  5316. font-family: PingFangSC, PingFang SC;
  5317. font-weight: 600;
  5318. font-size: 16px;
  5319. color: #000000D9;
  5320. line-height: 22px;
  5321. overflow: hidden;
  5322. width: 150px; /* 必须给宽度 */
  5323. white-space: nowrap; /* 关键 */
  5324. text-overflow: ellipsis;
  5325. }
  5326. &-bottom {
  5327. margin-top: 6px;
  5328. display: flex;
  5329. flex-direction: row;
  5330. align-items: center;
  5331. }
  5332. &-tag {
  5333. position: relative;
  5334. width: 60px;
  5335. height: 22px;
  5336. }
  5337. &-tag-bg {
  5338. width: 100%;
  5339. height: 100%;
  5340. }
  5341. &-tag-text {
  5342. position: absolute;
  5343. left: 0;
  5344. top: 0;
  5345. width: 100%;
  5346. height: 100%;
  5347. text-align: center;
  5348. line-height: 22px;
  5349. font-size: 12px;
  5350. color: #FFFFFF;
  5351. font-weight: 500;
  5352. }
  5353. &-price{
  5354. margin-left: 7px;
  5355. display: flex;
  5356. align-items:baseline;
  5357. .unit{
  5358. font-family: PingFangSC, PingFang SC;
  5359. font-weight: 600;
  5360. font-size: 16px;
  5361. color: #FF233C;
  5362. }
  5363. .price{
  5364. font-family: PingFangSC, PingFang SC;
  5365. font-weight: 600;
  5366. font-size: 24px;
  5367. color: #FF233C;
  5368. }
  5369. }
  5370. .close-box{
  5371. position: absolute;
  5372. top:10px;
  5373. right:10px;
  5374. z-index:9;
  5375. cover-image{
  5376. width: 15px;
  5377. height: 15px;
  5378. }
  5379. }
  5380. }
  5381. .video-controls-box {
  5382. width: 100%;
  5383. height: 300rpx;
  5384. overflow: hidden;
  5385. position: absolute;
  5386. bottom: 0;
  5387. left: 0;
  5388. z-index: 2;
  5389. background: rgba(0, 0, 0, 0.2);
  5390. .video-play {
  5391. height: 72rpx;
  5392. width: 72rpx;
  5393. position: absolute;
  5394. top: 50%;
  5395. left: 50%;
  5396. transform: translate(-50%, -50%);
  5397. }
  5398. }
  5399. .video-controls {
  5400. width: 100%;
  5401. height: 80rpx;
  5402. padding: 0 28rpx;
  5403. box-sizing: border-box;
  5404. position: absolute;
  5405. bottom: 0;
  5406. left: 0;
  5407. display: flex;
  5408. align-items: center;
  5409. justify-content: space-between;
  5410. background: linear-gradient(to top, #222 0%, transparent 80%);
  5411. .video-icon {
  5412. height: 44rpx;
  5413. width: 44rpx;
  5414. }
  5415. }
  5416. .errQuesbox {
  5417. width: 100%;
  5418. max-height: 260rpx;
  5419. overflow-y: auto;
  5420. margin-top: 24rpx;
  5421. font-family: PingFang SC, PingFang SC;
  5422. font-weight: 500;
  5423. font-size: 30rpx;
  5424. color: #222222;
  5425. &-item {
  5426. width: 100%;
  5427. height: 128rpx;
  5428. line-height: 128rpx;
  5429. margin-bottom: 24rpx;
  5430. padding: 0 30rpx;
  5431. box-sizing: border-box;
  5432. overflow: hidden;
  5433. background: #fff;
  5434. border-radius: 16rpx 16rpx 16rpx 16rpx;
  5435. position: relative;
  5436. &::after {
  5437. content: "题目";
  5438. min-width: 64rpx;
  5439. height: 36rpx;
  5440. padding: 0 12rpx;
  5441. line-height: 36rpx;
  5442. background: #FF5C03;
  5443. box-sizing: border-box;
  5444. border-radius: 0rpx 0rpx 16rpx 0rpx;
  5445. text-align: center;
  5446. font-family: PingFang SC, PingFang SC;
  5447. font-weight: 500;
  5448. font-size: 20rpx;
  5449. color: #fff;
  5450. position: absolute;
  5451. left: 0;
  5452. top: 0;
  5453. }
  5454. }
  5455. }
  5456. .bg {
  5457. background: #fff !important;
  5458. }
  5459. .timepopbox {
  5460. width: 560rpx;
  5461. padding: 32rpx;
  5462. box-sizing: border-box;
  5463. }
  5464. .answerPopup {
  5465. &-box {
  5466. width: 560rpx;
  5467. background: linear-gradient(180deg, #FFFAF6 0%, #FEECD8 100%);
  5468. border-radius: 32rpx 32rpx 32rpx 32rpx;
  5469. background-color: #fff;
  5470. font-weight: 400;
  5471. padding: 32rpx;
  5472. box-sizing: border-box;
  5473. position: relative;
  5474. @include u-flex(column, center, flex-start);
  5475. font-family: PingFang SC, PingFang SC;
  5476. font-weight: 400;
  5477. .tipimg {
  5478. width: 206rpx;
  5479. height: 206rpx;
  5480. margin-bottom: 16rpx;
  5481. }
  5482. }
  5483. &-title {
  5484. font-weight: 600;
  5485. font-size: 36rpx;
  5486. color: #222222;
  5487. }
  5488. &-desc {
  5489. margin-top: 10rpx;
  5490. font-size: 28rpx;
  5491. color: #757575;
  5492. }
  5493. &-btn {
  5494. width: 464rpx;
  5495. height: 84rpx;
  5496. margin-top: 54rpx;
  5497. margin-bottom: 16rpx;
  5498. background: #FF5C03;
  5499. border-radius: 42rpx;
  5500. font-weight: 500;
  5501. font-size: 32rpx;
  5502. color: #FFFFFF;
  5503. text-align: center;
  5504. line-height: 84rpx;
  5505. }
  5506. }
  5507. .popupbox {
  5508. width: 100%;
  5509. background-color: #fff;
  5510. border-radius: 16rpx 16rpx 0 0;
  5511. padding: 24rpx 32rpx;
  5512. position: relative;
  5513. &-head {
  5514. height: 60rpx;
  5515. margin-bottom: 30rpx;
  5516. text-align: center;
  5517. overflow-y: auto;
  5518. color: #414858;
  5519. font-size: 32rpx;
  5520. font-weight: bold;
  5521. position: relative;
  5522. .close-icon {
  5523. position: absolute;
  5524. right: 0;
  5525. top: 0;
  5526. height: 40rpx;
  5527. width: 40rpx;
  5528. }
  5529. }
  5530. &-content {
  5531. height: 20vh;
  5532. overflow-y: auto;
  5533. display: flex;
  5534. align-items: flex-start;
  5535. flex-wrap: wrap;
  5536. gap: 32rpx;
  5537. .line-item {
  5538. display: inline-block;
  5539. min-width: 200rpx;
  5540. min-height: 60rpx;
  5541. padding: 0 20rpx;
  5542. box-sizing: border-box;
  5543. border-radius: 50rpx;
  5544. overflow: hidden;
  5545. background-color: #f7f7f7;
  5546. text-align: center;
  5547. color: #414858;
  5548. font-size: 28rpx;
  5549. line-height: 60rpx;
  5550. }
  5551. .line-active {
  5552. color: #f56c6c !important;
  5553. background-color: #fef0f0 !important;
  5554. }
  5555. }
  5556. }
  5557. .content {
  5558. //padding-bottom: calc(var(--window-bottom) + 100rpx);
  5559. .video-box {
  5560. width: 100%;
  5561. height: 420rpx;
  5562. overflow: hidden;
  5563. position: relative;
  5564. #myVideo {
  5565. width: 100%;
  5566. height: 100%;
  5567. }
  5568. }
  5569. .videovip-video-container {
  5570. width: 100%;
  5571. position: relative;
  5572. background: #000;
  5573. overflow: hidden;
  5574. }
  5575. .videovip-xg-player {
  5576. width: 100%;
  5577. height: 100%;
  5578. }
  5579. .videovip-video-overlay {
  5580. position: absolute;
  5581. z-index: 10;
  5582. pointer-events: none;
  5583. }
  5584. .videovip-video-overlay .close-box,
  5585. .videovip-video-overlay.goods-cover {
  5586. pointer-events: auto;
  5587. }
  5588. .video-poster {
  5589. width: 100%;
  5590. height: 420rpx;
  5591. }
  5592. .miantitlebox {
  5593. padding: 30rpx 0;
  5594. border-bottom: 2rpx solid #F5F7FA;
  5595. font-family: PingFang SC, PingFang SC;
  5596. font-weight: 500;
  5597. font-size: 40rpx;
  5598. color: #222222;
  5599. }
  5600. .subtitlebox {
  5601. padding: 30rpx 0;
  5602. border-bottom: 2rpx solid #F5F7FA;
  5603. font-family: PingFang SC, PingFang SC;
  5604. font-weight: 500;
  5605. font-size: 40rpx;
  5606. color: #222222;
  5607. }
  5608. .title-content {
  5609. padding: 0 32rpx;
  5610. background-color: #fff;
  5611. font-size: 28rpx;
  5612. line-height: 1.6;
  5613. .title {
  5614. font-size: 36rpx;
  5615. font-weight: 500;
  5616. color: #414858;
  5617. }
  5618. .time-or-subtitle {
  5619. margin-top: 12rpx;
  5620. color: #666666;
  5621. }
  5622. }
  5623. .descbox {
  5624. padding: 36rpx 32rpx;
  5625. margin-bottom: 20rpx;
  5626. background-color: #fff;
  5627. font-family: PingFang SC, PingFang SC;
  5628. font-weight: 400;
  5629. font-size: 28rpx;
  5630. color: #222222;
  5631. line-height: 42rpx;
  5632. word-break: break-word;
  5633. &-title {
  5634. margin-bottom: 24rpx;
  5635. font-weight: 500;
  5636. font-size: 32rpx;
  5637. }
  5638. &-info {
  5639. margin-bottom: 24rpx;
  5640. @include u-flex(row, center, space-between);
  5641. font-size: 26rpx;
  5642. color: #757575;
  5643. &-l {
  5644. flex: 1;
  5645. @include u-flex(row, center, flex-start);
  5646. }
  5647. &-time {
  5648. margin-left: 18rpx;
  5649. padding-left: 18rpx;
  5650. position: relative;
  5651. &::after {
  5652. content: "";
  5653. width: 4rpx;
  5654. height: 4rpx;
  5655. background: #999999;
  5656. border-radius: 50%;
  5657. position: absolute;
  5658. left: 0;
  5659. top: 50%;
  5660. transform: translateY(-50%);
  5661. }
  5662. }
  5663. &-r {
  5664. background: transparent;
  5665. }
  5666. }
  5667. &-desc {
  5668. overflow: hidden;
  5669. position: relative;
  5670. }
  5671. }
  5672. .expand {
  5673. flex-shrink: 0;
  5674. @include u-flex(row, center, flex-end);
  5675. color: #FF5C03;
  5676. font-weight: 400;
  5677. font-size: 24rpx;
  5678. image {
  5679. width: 32rpx;
  5680. height: 32rpx;
  5681. }
  5682. }
  5683. .expand-ab {
  5684. position: absolute;
  5685. top: 0;
  5686. right: 0;
  5687. box-shadow: -50rpx 0 20rpx 8rpx #FFFFFF;
  5688. background-color: #fff;
  5689. }
  5690. .ques-content {
  5691. background-color: #fff;
  5692. padding: 40rpx 32rpx;
  5693. box-sizing: border-box;
  5694. font-family: PingFang SC, PingFang SC;
  5695. font-weight: 400;
  5696. font-size: 28rpx;
  5697. color: #222222;
  5698. }
  5699. .ques-content-tit {
  5700. font-family: PingFangSC, PingFang SC;
  5701. font-weight: 600;
  5702. font-size: 48rpx;
  5703. color: rgba(0,0,0,0.85);
  5704. line-height: 66rpx;
  5705. }
  5706. .ques-title {
  5707. margin: 48rpx 0 34rpx 0;
  5708. font-weight: 600;
  5709. font-size: 40rpx;
  5710. white-space: normal;
  5711. }
  5712. .ques-type {
  5713. flex-shrink: 0;
  5714. min-width: 72rpx;
  5715. min-height: 40rpx;
  5716. padding: 0 12rpx;
  5717. margin: 0 12rpx;
  5718. box-sizing: border-box;
  5719. background: #FF5C03;
  5720. border-radius: 8rpx 8rpx 8rpx 8rpx;
  5721. line-height: 40rpx;
  5722. text-align: center;
  5723. font-family: PingFang SC, PingFang SC;
  5724. font-weight: 400;
  5725. font-size: 24rpx;
  5726. color: #FFFFFF;
  5727. display: inline-block;
  5728. }
  5729. .ques-option {
  5730. min-height: 88rpx;
  5731. padding: 24rpx 32rpx;
  5732. box-sizing: border-box;
  5733. margin-bottom: 24rpx;
  5734. background: #F5F7FA;
  5735. border-radius: 16rpx 16rpx 16rpx 16rpx;
  5736. display: flex;
  5737. align-items: center;
  5738. &-active {
  5739. color: #FF5C03 !important;
  5740. background: #FCF0E7 !important;
  5741. }
  5742. }
  5743. .video-line {
  5744. min-width: 140rpx;
  5745. max-width: 200rpx;
  5746. height: 60rpx;
  5747. padding: 0 20rpx;
  5748. box-sizing: border-box;
  5749. border-radius: 50rpx 0 0 50rpx;
  5750. overflow: hidden;
  5751. background-color: #fff;
  5752. text-align: center;
  5753. color: #888;
  5754. font-size: 28rpx;
  5755. line-height: 60rpx;
  5756. display: inline-flex;
  5757. align-items: center;
  5758. justify-content: center;
  5759. position: fixed;
  5760. right: 0;
  5761. z-index: 9;
  5762. bottom: calc(var(--window-bottom) + 280rpx);
  5763. box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, .12);
  5764. image {
  5765. flex-shrink: 0;
  5766. height: 30rpx;
  5767. width: 30rpx;
  5768. margin-right: 6rpx;
  5769. }
  5770. }
  5771. /* 精选留言:图片/视频自定义预览(设计稿弹窗) */
  5772. .fc-media-preview-mask {
  5773. position: fixed;
  5774. left: 0;
  5775. right: 0;
  5776. top: 0;
  5777. bottom: 0;
  5778. z-index: 10050;
  5779. background: rgba(0, 0, 0, 0.72);
  5780. display: flex;
  5781. align-items: center;
  5782. justify-content: center;
  5783. padding: 32rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
  5784. box-sizing: border-box;
  5785. }
  5786. .fc-media-preview-card {
  5787. width: 100%;
  5788. max-width: 630rpx;
  5789. overflow: hidden;
  5790. display: flex;
  5791. flex-direction: column;
  5792. align-items: center;
  5793. }
  5794. .fc-media-preview-img-box {
  5795. width: 100%;
  5796. height: 62vh;
  5797. max-height: 820rpx;
  5798. }
  5799. .fc-media-preview-slide {
  5800. width: 100%;
  5801. height: 100%;
  5802. display: flex;
  5803. align-items: center;
  5804. justify-content: center;
  5805. padding: 16rpx;
  5806. box-sizing: border-box;
  5807. }
  5808. .fc-media-preview-main-img {
  5809. width: 100%;
  5810. height: 100%;
  5811. max-height: 100%;
  5812. }
  5813. .fc-media-preview-video-box {
  5814. width: 100%;
  5815. height: 62vh;
  5816. max-height: 820rpx;
  5817. background: #000;
  5818. display: flex;
  5819. align-items: center;
  5820. justify-content: center;
  5821. }
  5822. .fc-media-preview-video {
  5823. width: 100%;
  5824. height: 100%;
  5825. }
  5826. .fc-media-preview-footer {
  5827. display: flex;
  5828. flex-direction: row;
  5829. align-items: center;
  5830. padding:30rpx;
  5831. // gap: 20rpx;
  5832. }
  5833. .fc-media-preview-thumbs {
  5834. flex: 1;
  5835. min-width: 0;
  5836. height: 112rpx;
  5837. white-space: nowrap;
  5838. }
  5839. .fc-media-preview-thumbs-inner {
  5840. display: inline-flex;
  5841. flex-direction: row;
  5842. align-items: center;
  5843. gap: 16rpx;
  5844. padding-right: 8rpx;
  5845. }
  5846. .fc-media-preview-thumb {
  5847. width: 96rpx;
  5848. height: 96rpx;
  5849. flex-shrink: 0;
  5850. border-radius: 12rpx;
  5851. border: 3rpx solid transparent;
  5852. box-sizing: border-box;
  5853. background: #333;
  5854. }
  5855. .fc-media-preview-thumb--active {
  5856. border-color: #ff5c03;
  5857. }
  5858. .fc-media-preview-thumbs-spacer {
  5859. flex: 1;
  5860. min-height: 1rpx;
  5861. }
  5862. .fc-media-preview-close {
  5863. flex-shrink: 0;
  5864. display: flex;
  5865. align-items: center;
  5866. justify-content: center;
  5867. }
  5868. .fc-media-preview-close-x {
  5869. font-size: 44rpx;
  5870. line-height: 1;
  5871. color: #333;
  5872. font-weight: 300;
  5873. margin-top: -4rpx;
  5874. }
  5875. .featured-comment-composer-mask {
  5876. position: fixed;
  5877. left: 0;
  5878. right: 0;
  5879. top: 0;
  5880. bottom: 0;
  5881. background: rgba(0, 0, 0, 0.35);
  5882. z-index: 9998;
  5883. }
  5884. .featured-comment-composer-wrap {
  5885. position: fixed;
  5886. left: 0;
  5887. right: 0;
  5888. z-index: 9999;
  5889. background: #fff;
  5890. border-radius: 24rpx 24rpx 0 0;
  5891. padding: 20rpx 24rpx;
  5892. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  5893. box-sizing: border-box;
  5894. box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
  5895. }
  5896. .featured-comment-composer-inner {
  5897. display: flex;
  5898. align-items: center;
  5899. gap: 24rpx;
  5900. }
  5901. .featured-comment-composer-input-row {
  5902. flex: 1;
  5903. min-width: 0;
  5904. display: flex;
  5905. align-items: flex-start;
  5906. background: #f5f5f5;
  5907. border-radius: 24rpx;
  5908. padding:20rpx;
  5909. box-sizing: border-box;
  5910. }
  5911. .fc-composer-input {
  5912. flex: 1;
  5913. min-width: 0;
  5914. font-size: 32rpx;
  5915. color: rgba(0, 0, 0, 0.85);
  5916. height:88rpx;
  5917. line-height: 44rpx;
  5918. }
  5919. .fc-composer-ph {
  5920. color: rgba(0, 0, 0, 0.28);
  5921. font-size: 32rpx;
  5922. }
  5923. .fc-composer-input-icon {
  5924. flex-shrink: 0;
  5925. margin-left: 12rpx;
  5926. display: flex;
  5927. align-items: center;
  5928. justify-content: center;
  5929. image {
  5930. width: 40rpx;
  5931. height: 40rpx;
  5932. }
  5933. }
  5934. .fc-composer-media-pill {
  5935. position: relative;
  5936. flex-shrink: 0;
  5937. width: 64rpx;
  5938. height: 64rpx;
  5939. border-radius: 12rpx;
  5940. overflow: hidden;
  5941. }
  5942. .fc-composer-media-thumb {
  5943. width: 100%;
  5944. height: 100%;
  5945. display: block;
  5946. }
  5947. .fc-composer-media-badge {
  5948. position: absolute;
  5949. left: 0;
  5950. right: 0;
  5951. bottom: 0;
  5952. font-size: 18rpx;
  5953. color: #fff;
  5954. text-align: center;
  5955. line-height: 1.4;
  5956. background: rgba(0, 0, 0, 0.45);
  5957. }
  5958. .fc-composer-media-x {
  5959. position: absolute;
  5960. top: -4rpx;
  5961. right: -4rpx;
  5962. width: 32rpx;
  5963. height: 32rpx;
  5964. line-height: 32rpx;
  5965. text-align: center;
  5966. font-size: 28rpx;
  5967. color: #fff;
  5968. background: rgba(0, 0, 0, 0.5);
  5969. border-radius: 50%;
  5970. }
  5971. .featured-comment-composer-publish {
  5972. flex-shrink: 0;
  5973. padding: 20rpx 40rpx;
  5974. background: linear-gradient( 135deg, #FF5267 0%, #FF233C 100%);
  5975. font-family: PingFangSC, PingFang SC;
  5976. font-weight: 600;
  5977. font-size: 40rpx;
  5978. color: #FFFFFF;
  5979. line-height: 56rpx;
  5980. text-align: center;
  5981. border-radius: 48rpx;
  5982. }
  5983. .footer {
  5984. //border-top: 1rpx solid #ededef;
  5985. background: #fff;
  5986. width: 100%;
  5987. position: fixed;
  5988. bottom: 0;
  5989. padding: 32rpx;
  5990. padding-bottom:68rpx;
  5991. box-sizing: border-box;
  5992. z-index: 0;
  5993. &-btn {
  5994. width: 100%;
  5995. height: 98rpx;
  5996. background: #FF5C03;
  5997. border-radius: 49rpx 49rpx 49rpx 49rpx;
  5998. line-height: 98rpx;
  5999. text-align: center;
  6000. font-family: PingFang SC, PingFang SC;
  6001. font-weight: 600;
  6002. font-size: 32rpx;
  6003. color: #FFFFFF;
  6004. @include u-flex(row, center, center);
  6005. &-img {
  6006. flex-shrink: 0;
  6007. width: 144rpx;
  6008. height: 144rpx;
  6009. margin-right: 8rpx;
  6010. margin-top: -24rpx;
  6011. }
  6012. }
  6013. &-btn-border {
  6014. position: relative;
  6015. &::after {
  6016. content: "";
  6017. background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
  6018. position: absolute;
  6019. top: -2rpx;
  6020. left: 0;
  6021. height: 103rpx;
  6022. width: 100%;
  6023. z-index: -1;
  6024. border-radius: 49rpx 49rpx 49rpx 49rpx;
  6025. box-shadow: 0rpx 8rpx 11rpx 0rpx rgba(255, 92, 3, 0.3);
  6026. overflow: hidden;
  6027. }
  6028. }
  6029. }
  6030. }
  6031. .agreement {
  6032. display: inline-flex;
  6033. margin-top: 16rpx;
  6034. font-size: 24rpx;
  6035. color: #525252;
  6036. align-items: center;
  6037. justify-content: center;
  6038. }
  6039. .nocourse {
  6040. display: flex;
  6041. flex-direction: column;
  6042. align-items: center;
  6043. padding-bottom: 88rpx;
  6044. justify-content: center;
  6045. font-family: PingFang SC, PingFang SC;
  6046. font-weight: 400;
  6047. font-size: 32rpx;
  6048. color: #757575;
  6049. line-height: 48rpx;
  6050. text-align: center;
  6051. image {
  6052. width: 328rpx;
  6053. height: auto;
  6054. margin-bottom: 30rpx;
  6055. }
  6056. }
  6057. .logo {
  6058. display: inline-block;
  6059. width: 30px;
  6060. height: auto;
  6061. margin: 20px 0 0 10px;
  6062. pointer-events: none;
  6063. object-fit: cover;
  6064. }
  6065. .logo-full {
  6066. display: inline-block;
  6067. width: 40px;
  6068. height: auto;
  6069. margin: 50px 0 0 30px;
  6070. pointer-events: none;
  6071. object-fit: cover;
  6072. }
  6073. </style>