| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385 |
- <template>
- <section class="space-y-6">
- <!-- 返回按钮 -->
- <div class="flex items-center justify-between">
- <div class="flex items-center gap-3">
- <button
- @click="goBack"
- class="flex items-center gap-2 text-white/80 hover:text-white transition"
- >
- <svg
- class="w-5 h-5"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M15 19l-7-7 7-7"
- />
- </svg>
- <span class="text-sm">返回</span>
- </button>
- <div class="relative">
- <button
- @click="toggleShare"
- class="p-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition"
- >
- <svg
- class="w-5 h-5"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
- />
- </svg>
- </button>
- <!-- 分享提示弹窗 -->
- <div
- v-if="showShareModal"
- class="absolute top-0 left-full ml-2 z-50 bg-emerald-500 text-white px-3 py-1.5 rounded-lg shadow-lg flex items-center gap-1.5 whitespace-nowrap"
- >
- <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
- <path
- fill-rule="evenodd"
- d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1-0 011.414 0z"
- clip-rule="evenodd"
- />
- </svg>
- <span class="text-xs font-medium">已复制</span>
- </div>
- </div>
- </div>
- <!-- 购买按钮区域 -->
- <div
- v-if="!isSinglePurchased && !isVip"
- class="flex items-center gap-1.5"
- >
- <!-- 游客和免费用户显示试看时间信息 -->
- <div v-if="isGuestOrFree" class="text-xs text-white/60 px-2 py-1">
- 试看 {{ trialDuration }} 秒
- </div>
- <!-- 只有非游客用户才显示购买会员按钮 -->
- <button
- v-if="!isGuest"
- @click="handleMembershipClick"
- class="px-2 py-1 rounded-md bg-brand text-slate-900 text-xs font-medium hover:bg-brand/90 transition whitespace-nowrap min-w-fit"
- >
- 开通会员
- </button>
- <button
- @click="handleSinglePurchaseClick"
- class="px-3 py-2 rounded-lg bg-blue-500 text-white text-sm font-semibold hover:bg-blue-600 transition shadow-lg hover:shadow-xl flash-animation glow-animation hover:animate-none whitespace-nowrap min-w-fit"
- >
- 单片购买
- </button>
- </div>
- </div>
- <!-- 视频播放器区域 -->
- <div class="relative rounded-2xl overflow-hidden bg-black">
- <div class="aspect-video video-container">
- <VideoProcessor
- ref="videoProcessorRef"
- :cover-url="videoInfo.cover"
- :m3u8-url="videoInfo.m3u8"
- :alt="videoInfo.name"
- :auto-play="false"
- :hide-error="false"
- :enable-retry="true"
- cover-class="w-full h-full object-cover"
- video-class="w-full h-full object-contain"
- @cover-loaded="onCoverLoaded"
- @video-loaded="onVideoLoaded"
- @error="onVideoProcessorError"
- @retry="onVideoProcessorRetry"
- @play="onVideoPlay"
- @timeupdate="onVideoTimeUpdate"
- @seeking="onVideoSeeking"
- />
- <!-- 试看提示 -->
- <div
- v-if="isTrialMode"
- class="absolute top-4 right-4 text-white px-3 py-1.5 text-sm font-medium"
- >
- 试看中
- </div>
- <!-- 试看结束遮罩 -->
- <div
- v-if="showTrialEndModal && !isSinglePurchased && !isVip"
- class="absolute inset-0 bg-black/80 flex items-center justify-center z-50"
- >
- <div
- class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 max-w-sm mx-4 text-center"
- >
- <div class="text-white text-lg font-semibold mb-4">
- 试看时间已结束
- </div>
- <div class="text-white/70 text-sm mb-6">
- <span v-if="!isGuest">购买会员或单独购买本片继续观看</span>
- <span v-else>单独购买本片继续观看</span>
- </div>
- <div class="space-y-3">
- <!-- 只有非游客用户才显示购买会员按钮 -->
- <button
- v-if="!isGuest"
- @click="purchaseMembership"
- class="w-full bg-brand hover:bg-brand/90 text-slate-900 py-3 px-4 rounded-lg font-medium transition whitespace-nowrap"
- >
- 开通会员
- </button>
- <button
- @click="purchaseVideo"
- class="w-full bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold transition shadow-lg hover:shadow-xl flash-animation glow-animation hover:animate-none whitespace-nowrap"
- >
- 单独购买本片
- </button>
- <button
- @click="showTrialEndModal = false"
- class="w-full text-white/60 hover:text-white/80 py-2 text-sm transition"
- >
- 取消
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 单片购买确认弹窗 -->
- <div
- v-if="showSinglePurchaseModal"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showSinglePurchaseModal = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
- >
- <h3 class="text-lg font-semibold text-white/90 mb-2">确认购买</h3>
- <p class="text-sm text-white/70 mb-4">
- 确定要购买当前视频吗?<br />
- 购买后即可观看完整内容
- </p>
- <div class="text-lg font-semibold text-brand mb-6">
- ¥{{ priceStore.getSinglePrice() }}
- </div>
- <div class="space-y-3">
- <button
- @click="purchaseVideo"
- class="w-full bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold transition shadow-lg hover:shadow-xl flash-animation glow-animation hover:animate-none whitespace-nowrap"
- >
- 确认购买
- </button>
- <button
- @click="showSinglePurchaseModal = false"
- class="w-full text-white/60 hover:text-white/80 py-2 text-sm transition"
- >
- 取消
- </button>
- </div>
- </div>
- </div>
- <!-- 会员购买弹窗 -->
- <div
- v-if="showMembershipPurchaseModal"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showMembershipPurchaseModal = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-4 w-full max-w-sm mx-4"
- >
- <h3 class="text-lg font-semibold text-white/90 mb-4 text-center">
- 开通会员
- </h3>
- <!-- 会员套餐选择 -->
- <div class="space-y-2 mb-3">
- <h4 class="text-sm font-medium text-white/70 mb-2">选择套餐</h4>
- <div class="space-y-2">
- <label
- v-for="plan in membershipPlans"
- :key="plan.key"
- class="relative cursor-pointer"
- >
- <input
- v-model="selectedPlan"
- :value="plan.key"
- type="radio"
- name="membership-plan"
- class="sr-only"
- />
- <div
- class="border-2 rounded-lg p-2 transition-all"
- :class="
- selectedPlan === plan.key
- ? 'border-brand bg-brand/10'
- : 'border-white/20 hover:border-white/30'
- "
- >
- <div class="flex justify-between items-center">
- <div
- class="font-medium text-white/90 text-sm whitespace-nowrap"
- >
- {{ plan.label }} - {{ plan.duration }}
- </div>
- <div class="font-semibold text-white/90 text-sm">
- {{ plan.price }}
- </div>
- </div>
- </div>
- </label>
- </div>
- </div>
- <!-- 支付方式选择 -->
- <div class="mb-3">
- <h4 class="text-sm font-medium text-white/70 mb-2">支付方式</h4>
- <label
- class="flex items-center p-2 border border-white/20 rounded-lg cursor-pointer hover:bg-white/5"
- >
- <input
- v-model="selectedPayment"
- value="alipay"
- type="radio"
- name="payment-method"
- class="sr-only"
- />
- <div class="flex items-center">
- <div
- class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center mr-3"
- >
- <span class="text-white text-sm font-bold">支</span>
- </div>
- <span class="text-white/90 text-sm">支付宝</span>
- </div>
- </label>
- </div>
- <!-- 操作按钮 -->
- <div class="flex gap-2 pt-3 border-t border-white/10">
- <button
- type="button"
- @click="showMembershipPurchaseModal = false"
- class="flex-1 px-3 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition text-sm"
- >
- 取消
- </button>
- <button
- @click="handleMembershipPurchase"
- :disabled="!selectedPlan || isPaymentLoading"
- class="flex-1 px-3 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50 disabled:cursor-not-allowed text-sm"
- >
- {{ isPaymentLoading ? "处理中..." : "立即购买" }}
- </button>
- </div>
- </div>
- </div>
- <!-- 支付等待弹窗 -->
- <div
- v-if="showPaymentWaitingDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
- >
- <!-- 加载动画 -->
- <div class="mb-6">
- <div
- class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
- ></div>
- </div>
- <!-- 等待文字 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">
- 正在拉起支付页面,等待支付...
- </h3>
- <p class="text-sm text-white/60 mb-6">
- 请在支付页面完成支付<br />
- 支付完成后点击已完成支付
- </p>
- <!-- 操作按钮 -->
- <div class="flex gap-3">
- <button
- @click="showPaymentWaitingDialog = false"
- class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
- >
- 取消支付
- </button>
- <button
- @click="handleQueryOrder"
- class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 已完成支付
- </button>
- </div>
- </div>
- </div>
- <!-- 单片购买支付等待弹窗 -->
- <div
- v-if="showSinglePaymentWaitingDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
- >
- <!-- 加载动画 -->
- <div class="mb-6">
- <div
- class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
- ></div>
- </div>
- <!-- 等待文字 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">
- 正在拉起支付页面,等待支付...
- </h3>
- <p class="text-sm text-white/60 mb-6">
- 请在支付页面完成支付<br />
- 支付完成后点击已完成支付
- </p>
- <!-- 操作按钮 -->
- <div class="flex gap-3">
- <button
- @click="showSinglePaymentWaitingDialog = false"
- class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
- >
- 取消支付
- </button>
- <button
- @click="handleSingleQueryOrder"
- class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 已完成支付
- </button>
- </div>
- </div>
- </div>
- <!-- 错误提示弹窗 -->
- <div
- v-if="showErrorDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showErrorDialog = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
- >
- <!-- 错误图标 -->
- <div class="mb-4">
- <div
- class="w-12 h-12 mx-auto bg-red-500/20 rounded-full flex items-center justify-center"
- >
- <svg
- class="w-6 h-6 text-red-400"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
- />
- </svg>
- </div>
- </div>
- <!-- 错误信息 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">操作失败</h3>
- <p class="text-sm text-white/70 mb-6">
- {{ errorMessage }}
- </p>
- <!-- 确认按钮 -->
- <button
- @click="showErrorDialog = false"
- class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 确定
- </button>
- </div>
- </div>
- <!-- 成功提示弹窗 -->
- <div
- v-if="showSuccessDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showSuccessDialog = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
- >
- <!-- 成功图标 -->
- <div class="mb-4">
- <div
- class="w-12 h-12 mx-auto bg-green-500/20 rounded-full flex items-center justify-center"
- >
- <svg
- class="w-6 h-6 text-green-400"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M5 13l4 4L19 7"
- />
- </svg>
- </div>
- </div>
- <!-- 成功信息 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">操作成功</h3>
- <p class="text-sm text-white/70 mb-6">
- {{ successMessage }}
- </p>
- <!-- 确认按钮 -->
- <button
- @click="showSuccessDialog = false"
- class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 确定
- </button>
- </div>
- </div>
- <!-- 视频信息区域 -->
- <div class="space-y-6">
- <!-- 视频标题和基本信息 -->
- <div class="space-y-3">
- <h1 class="text-xl font-semibold text-white leading-tight">
- {{ videoInfo.name || "视频标题" }}
- </h1>
- <div class="flex items-center gap-4 text-sm text-white/60">
- <div class="flex items-center gap-1">
- <svg
- class="w-4 h-4"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
- />
- </svg>
- <span>{{ formatDuration(videoInfo.duration) }}</span>
- </div>
- <div class="flex items-center gap-1">
- <svg
- class="w-4 h-4"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
- />
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
- />
- </svg>
- <span>{{ formatNumber(videoInfo.view) }} 次观看</span>
- </div>
- <div class="flex items-center gap-1">
- <svg
- class="w-4 h-4"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
- />
- </svg>
- <span>{{ formatNumber(videoInfo.like) }} 点赞</span>
- </div>
- </div>
- </div>
- <!-- 标签信息 -->
- <!-- <div
- v-if="videoInfo.taginfo && videoInfo.taginfo.length > 0"
- class="space-y-3"
- >
- <h3 class="text-sm font-medium text-white/80">标签</h3>
- <div class="flex flex-wrap gap-2">
- <span
- v-for="tag in videoInfo.taginfo"
- :key="tag.hash"
- class="px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/70 hover:bg-white/10 hover:text-white transition"
- >
- {{ tag.name }}
- </span>
- </div>
- </div> -->
- <!-- 相关推荐 -->
- <div v-if="relatedVideos.length > 0" class="space-y-4">
- <h3 class="text-sm font-medium text-white/80">相关推荐</h3>
- <div class="grid grid-cols-2 gap-3">
- <article
- v-for="video in relatedVideos.slice(0, 12)"
- :key="video.id"
- @click="playVideo(video)"
- class="group rounded-xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
- >
- <!-- 横屏封面 -->
- <div class="aspect-[16/9] relative">
- <VideoProcessor
- :cover-url="video.cover"
- :alt="video.name"
- cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
- />
- <div
- class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded"
- >
- {{ formatDuration(video.duration) }}
- </div>
- </div>
- <!-- 视频信息 -->
- <div class="p-3">
- <h4 class="text-sm font-medium text-white/90 line-clamp-2 mb-1">
- {{ video.name }}
- </h4>
- <p class="text-xs text-white/50 truncate">
- {{ formatNumber(video.view) }} 次观看 ·
- {{ formatNumber(video.like) }} 点赞
- </p>
- <!-- 标签信息(移动端隐藏) -->
- <div
- v-if="video.taginfo && video.taginfo.length > 0"
- class="hidden sm:flex flex-wrap gap-1 mt-2"
- >
- <span
- v-for="tag in video.taginfo.slice(0, 2)"
- :key="tag.hash"
- class="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/70"
- >
- {{ tag.name }}
- </span>
- <span
- v-if="video.taginfo.length > 2"
- class="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/50"
- >
- +{{ video.taginfo.length - 2 }}
- </span>
- </div>
- </div>
- </article>
- </div>
- </div>
- </div>
- </section>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, onUnmounted, computed, watch } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import {
- searchVideoByTags,
- getVideoDetail,
- purchaseSingle,
- purchaseMember,
- userQueryOrder,
- checkSinglePurchase,
- } from "@/services/api";
- import VideoProcessor from "@/components/VideoProcessor.vue";
- import { useUserStore } from "@/store/user";
- import { usePriceStore } from "@/store/price";
- import { VipLevel, canWatchFullVideo, getTrialDuration } from "@/types/vip";
- // 路由相关
- const route = useRoute();
- const router = useRouter();
- // 用户状态
- const userStore = useUserStore();
- const priceStore = usePriceStore();
- // 视频播放器引用(现在由 VideoProcessor 组件管理)
- const videoPlayer = ref<HTMLVideoElement>();
- const videoProcessorRef = ref<any>();
- // 视频信息
- const videoInfo = ref<any>({
- id: "",
- name: "",
- cover: "",
- m3u8: "",
- duration: 0,
- view: 0,
- like: 0,
- time: 0,
- taginfo: [],
- });
- // 相关视频
- const relatedVideos = ref<any[]>([]);
- // 状态管理
- const showShareModal = ref(false);
- const showTrialEndModal = ref(false);
- const showMembershipPurchaseModal = ref(false);
- const showSinglePurchaseModal = ref(false);
- const isTrialMode = ref(false);
- // 单片购买状态
- const isSinglePurchased = ref(false);
- // 使用动态价格配置
- const membershipPlans = computed(() => priceStore.getMembershipPlans);
- const selectedPlan = ref("");
- const selectedPayment = ref("alipay");
- const isPaymentLoading = ref(false);
- // 支付等待相关
- const showPaymentWaitingDialog = ref(false);
- const currentOrderNo = ref("");
- // 提示弹窗相关
- const showSuccessDialog = ref(false);
- const showErrorDialog = ref(false);
- const successMessage = ref("");
- const errorMessage = ref("");
- // 单片购买支付等待相关
- const showSinglePaymentWaitingDialog = ref(false);
- const singleCurrentOrderNo = ref("");
- // 检测是否为夸克浏览器
- const isQuarkBrowser = () => {
- const ua = navigator.userAgent.toLowerCase();
- return ua.includes('quark');
- };
- // 显示成功/错误提示的函数
- const showSuccess = (message: string) => {
- successMessage.value = message;
- showSuccessDialog.value = true;
-
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
- };
- const showError = (message: string) => {
- errorMessage.value = message;
- showErrorDialog.value = true;
-
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
- };
- // 生成设备标识
- const generateMacAddress = (): string => {
- const hex = "0123456789ABCDEF";
- let mac = "";
- for (let i = 0; i < 6; i++) {
- if (i > 0) mac += ":";
- mac += hex[Math.floor(Math.random() * 16)];
- mac += hex[Math.floor(Math.random() * 16)];
- }
- return mac;
- };
- const device = generateMacAddress();
- // Safari兼容性处理:打开支付页面
- const openPaymentPage = (url: string) => {
- // 检测是否为Safari浏览器
- const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
- if (isSafari) {
- // Safari浏览器
- setTimeout(() => window.open(url, "_blank"));
- // window.location.href = url;
- } else {
- // 其他浏览器:正常使用window.open
- window.open(url, "_blank");
- }
- };
- // 计算属性
- const currentVipLevel = computed(() => userStore.getVipLevel());
- const isGuestOrFree = computed(() => !canWatchFullVideo(currentVipLevel.value));
- const trialDuration = computed(() => getTrialDuration(priceStore));
- // 参考Account.vue的用户类型判断
- const isGuest = computed(() => {
- return userStore.userInfo?.vipLevel === "guest";
- });
- const isVip = computed(() => {
- const level = userStore.userInfo?.vipLevel;
- return level && level !== VipLevel.GUEST && level !== VipLevel.FREE;
- });
- // VideoProcessor 事件处理
- const onCoverLoaded = (url: string) => {
- // 封面加载完成
- };
- const onVideoLoaded = (url: string) => {
- // 视频加载完成
- };
- const onVideoProcessorError = (error: string) => {
- console.error("VideoProcessor 错误:", error);
- };
- const onVideoProcessorRetry = () => {
- // VideoProcessor 重试
- };
- const onVideoPlay = () => {
- // 如果是guest或free用户且还没有开始试看,且未购买当前视频,则开始试看
- if (isGuestOrFree.value && !isTrialMode.value && !isSinglePurchased.value) {
- startTrial();
- }
- };
- // 监听视频播放进度
- const onVideoTimeUpdate = () => {
- if (isGuestOrFree.value && isTrialMode.value) {
- const video = document.querySelector("video");
- if (video && video.currentTime > trialDuration.value) {
- // 超过试看时间,停止播放并显示购买弹窗
- video.pause();
- // 将视频时间重置到试看结束时间
- video.currentTime = trialDuration.value;
- // 停止HLS加载
- if (videoProcessorRef.value) {
- videoProcessorRef.value.stopHlsLoading();
- }
- showTrialEndModal.value = true;
- }
- }
- };
- // 监听视频时间变化(包括拖拽进度条)
- const onVideoSeeking = () => {
- if (isGuestOrFree.value && isTrialMode.value) {
- const video = document.querySelector("video");
- if (video && video.currentTime > trialDuration.value) {
- // 如果拖拽到超过试看时间,强制回到试看结束时间
- video.currentTime = trialDuration.value;
- }
- }
- };
- // 格式化时长
- const formatDuration = (duration: string | number): string => {
- const seconds = parseInt(String(duration));
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = seconds % 60;
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
- };
- // 格式化数字
- const formatNumber = (num: string | number): string => {
- const n = parseInt(String(num));
- if (n >= 10000) {
- return `${(n / 10000).toFixed(1)}万`;
- }
- return n.toString();
- };
- // 返回上一页
- const goBack = () => {
- router.back();
- };
- // 分享视频(直接复制链接并显示提示)
- const toggleShare = async () => {
- try {
- const videoUrl = window.location.href;
- await navigator.clipboard.writeText(videoUrl);
- showShareModal.value = true;
- // 1秒后自动隐藏提示
- setTimeout(() => {
- showShareModal.value = false;
- }, 1000);
- } catch (error) {
- console.error("复制链接失败:", error);
- }
- };
- // 播放视频
- const playVideo = (video: any) => {
- const vipLevel = userStore.getVipLevel();
- if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
- // guest和free用户通过URL参数传递cover和m3u8,同时包含视频ID
- router.push({
- name: "VideoPlayer",
- params: { id: video.id },
- query: {
- cover: video.cover,
- m3u8: video.m3u8,
- name: video.name,
- duration: video.duration,
- view: video.view,
- like: video.like,
- },
- });
- } else {
- // 其他VIP用户正常调用详情接口
- router.push({
- name: "VideoPlayer",
- params: { id: video.id },
- });
- }
- // 滚动到页面顶端
- window.scrollTo({
- top: 0,
- behavior: "smooth",
- });
- };
- // 开始试看
- const startTrial = () => {
- isTrialMode.value = true;
- };
- // 处理顶部会员购买按钮点击
- const handleMembershipClick = () => {
- showMembershipPurchaseModal.value = true;
-
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
- };
- // 处理顶部单片购买按钮点击
- const handleSinglePurchaseClick = () => {
- showSinglePurchaseModal.value = true;
-
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
- };
- // 购买会员
- const purchaseMembership = () => {
- // 关闭试看结束弹窗
- showTrialEndModal.value = false;
- // 打开会员购买弹窗
- showMembershipPurchaseModal.value = true;
-
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
- };
- // 处理会员购买
- const handleMembershipPurchase = async () => {
- if (!selectedPlan.value) {
- showError("请选择会员套餐");
- return;
- }
- isPaymentLoading.value = true;
- try {
- const response = await purchaseMember(
- userStore.userInfo.id,
- selectedPlan.value
- );
- if (response.code === 1) {
- // 保存订单号
- currentOrderNo.value = response.out_trade_no;
- // 关闭购买弹窗
- showMembershipPurchaseModal.value = false;
- // Safari兼容性处理:使用多种方式打开支付页面
- // openPaymentPage(response.code_url);
- setTimeout(() => window.open(response.code_url, "_blank"));
- // 显示支付等待弹窗
- showPaymentWaitingDialog.value = true;
- // 重置选择
- selectedPlan.value = "";
- } else {
- showError(`支付失败: ${response.msg || "未知错误"}`);
- }
- } catch (error) {
- console.error("购买会员失败", error);
- showError("购买失败,请重试");
- } finally {
- isPaymentLoading.value = false;
- }
- };
- // 查询订单状态
- const handleQueryOrder = async () => {
- if (!currentOrderNo.value) {
- showError("没有找到订单信息");
- return;
- }
- try {
- const response = await userQueryOrder(currentOrderNo.value);
- if (response.status === 1) {
- // 支付成功
- await userStore.sync();
- showPaymentWaitingDialog.value = false;
- showSuccess("会员购买成功!");
- currentOrderNo.value = "";
- } else {
- showError(response.msg || "会员购买失败");
- }
- } catch (error) {
- console.error("查询订单状态失败:", error);
- showError("会员购买失败,请重试");
- }
- };
- // 查询单片购买订单状态
- const handleSingleQueryOrder = async () => {
- if (!singleCurrentOrderNo.value) {
- showError("没有找到订单信息");
- return;
- }
- try {
- // 使用视频信息的真实ID查询订单
- const response = await userQueryOrder(
- singleCurrentOrderNo.value,
- videoInfo.value.id
- );
- if (response.status === 1) {
- // 支付成功
- await userStore.sync();
- showSinglePaymentWaitingDialog.value = false;
- showSuccess("视频购买成功!");
- singleCurrentOrderNo.value = "";
- // 重新检查购买状态(已完成支付时的检查)
- await checkVideoPurchaseStatus();
- } else {
- showError(response.msg || "视频购买失败");
- }
- } catch (error) {
- console.error("查询单片购买订单状态失败:", error);
- showError("视频购买失败,请重试");
- }
- };
- // 单独购买本片
- const purchaseVideo = async () => {
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }, 100);
- }
-
- try {
- // 使用视频信息的真实ID
- if (!videoInfo.value.id || videoInfo.value.id === "unknown") {
- showError("未找到视频资源ID");
- return;
- }
- const response = await purchaseSingle(
- userStore.userInfo.id,
- videoInfo.value.id
- );
- if (response.code === 1) {
- // 保存订单号
- singleCurrentOrderNo.value = response.out_trade_no;
- // 关闭弹窗
- showTrialEndModal.value = false;
- showSinglePurchaseModal.value = false;
- // Safari兼容性处理:使用多种方式打开支付页面
- // openPaymentPage(response.code_url);
- setTimeout(() => window.open(response.code_url, "_blank"));
- // 显示支付等待弹窗
- showSinglePaymentWaitingDialog.value = true;
- console.log("单片购买订单创建成功:", response.out_trade_no);
- } else {
- console.error("单片购买失败:", response.msg);
- showError(`购买失败: ${response.msg || "未知错误"}`);
- }
- } catch (error) {
- console.error("单片购买异常:", error);
- showError("购买失败,请重试");
- }
- };
- // 检查用户是否已购买当前视频
- const checkVideoPurchaseStatus = async () => {
- if (!videoInfo.value.id || videoInfo.value.id === "unknown") {
- return;
- }
- try {
- const response = await checkSinglePurchase(videoInfo.value.id);
- isSinglePurchased.value = response === true;
- } catch (error) {
- console.error("检查视频购买状态失败:", error);
- isSinglePurchased.value = false;
- }
- };
- // 加载视频信息
- const loadVideoInfo = async () => {
- const vipLevel = userStore.getVipLevel();
- // 获取视频ID(无论什么用户类型都使用相同的获取方式)
- const videoId = route.params.id || route.query.id;
- // 如果是guest或free用户,从query参数获取视频信息
- if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
- const { cover, m3u8, name, duration, view, like } = route.query;
- if (cover && m3u8) {
- videoInfo.value = {
- id: videoId || "unknown",
- name: name || "试看视频",
- cover: cover as string,
- m3u8: m3u8 as string,
- duration: parseInt(duration as string) || 0,
- view: parseInt(view as string) || 0,
- like: parseInt(like as string) || 0,
- time: 0,
- taginfo: [],
- };
- // 设置页面标题
- document.title = `${videoInfo.value.name} - 试看`;
- return;
- }
- }
- // 其他情况,通过API获取视频详情
- if (videoId) {
- try {
- // 通过API获取视频详情
- const response = await getVideoDetail(device, String(videoId));
- if (response.status === 0 && response.data) {
- const data = response.data;
- videoInfo.value = {
- id: data.id || videoId,
- name: data.name || `视频 ${videoId}`,
- cover: data.cover || "",
- m3u8: data.m3u8 || "",
- duration: data.duration || 0,
- view: data.view || 0,
- like: data.like || 0,
- time: data.time || 0,
- taginfo: data.tags
- ? data.tags
- .split(",")
- .map((tag: string) => ({ name: tag.trim(), hash: tag.trim() }))
- : [],
- };
- // 设置页面标题
- document.title = `${videoInfo.value.name} - 视频播放`;
- } else {
- console.error("获取视频详情失败-1:", response.msg);
- // 设置默认值
- videoInfo.value = {
- id: videoId,
- name: `视频 ${videoId}`,
- cover: "",
- m3u8: "",
- duration: 0,
- view: 0,
- like: 0,
- time: 0,
- taginfo: [],
- };
- }
- } catch (error) {
- console.error("获取视频详情失败-2:", error);
- // 设置默认值
- videoInfo.value = {
- id: videoId,
- name: `视频 ${videoId}`,
- cover: "",
- m3u8: "",
- duration: 0,
- view: 0,
- like: 0,
- time: 0,
- taginfo: [],
- };
- }
- } else {
- console.error("未找到视频ID");
- }
- };
- // 加载相关视频
- const loadRelatedVideos = async () => {
- try {
- // 获取当前视频的标签
- const currentVideoTags = videoInfo.value.taginfo || [];
- if (currentVideoTags.length > 0) {
- // 随机选择一个标签
- const randomTag =
- currentVideoTags[Math.floor(Math.random() * currentVideoTags.length)];
- // 生成1-10之间的随机页码
- const randomPage = Math.floor(Math.random() * 10) + 1;
- // 获取12个该标签下的最热视频
- const response = await searchVideoByTags(
- device,
- randomPage,
- 12,
- randomTag.hash,
- "long",
- "view"
- );
- if (response.status === 0 && response.data?.list) {
- // 过滤掉当前视频
- relatedVideos.value = response.data.list
- .filter((video: any) => video.id !== videoInfo.value.id)
- .slice(0, 12);
- }
- } else {
- // 如果没有标签,则获取全局最热视频
- const randomPage = Math.floor(Math.random() * 10) + 1;
- const response = await searchVideoByTags(
- device,
- randomPage,
- 12,
- undefined,
- "long",
- "view"
- );
- if (response.status === 0 && response.data?.list) {
- relatedVideos.value = response.data.list
- .filter((video: any) => video.id !== videoInfo.value.id)
- .slice(0, 12);
- }
- }
- } catch (error) {
- console.error("加载相关视频失败:", error);
- }
- };
- // 监听路由参数变化
- watch(
- () => route.params.id,
- async (newId, oldId) => {
- if (newId && newId !== oldId) {
- // 停止当前视频播放
- if (videoProcessorRef.value) {
- videoProcessorRef.value.stopVideo();
- }
- // 重置购买状态
- isSinglePurchased.value = false;
- await loadVideoInfo();
- await loadRelatedVideos();
- // 检查购买状态(首页点击进入视频时的检查)
- const vipLevel = userStore.getVipLevel();
- if (
- vipLevel !== VipLevel.GUEST &&
- videoInfo.value.id &&
- videoInfo.value.id !== "unknown"
- ) {
- await checkVideoPurchaseStatus();
- }
- } else if (oldId === undefined && newId) {
- await loadVideoInfo();
- await loadRelatedVideos();
- const vipLevel = userStore.getVipLevel();
- if (
- vipLevel !== VipLevel.GUEST &&
- videoInfo.value.id &&
- videoInfo.value.id !== "unknown"
- ) {
- await checkVideoPurchaseStatus();
- }
- }
- },
- { immediate: true }
- );
- onMounted(async () => {
- // 加载价格配置
- if (!priceStore.isPriceConfigLoaded) {
- await priceStore.fetchPriceConfig();
- }
- });
- onUnmounted(() => {
- // 组件卸载时的清理工作
- });
- </script>
- <style scoped>
- /* 视频容器样式 */
- .video-container {
- position: relative;
- }
- /* 自定义闪烁动画 */
- @keyframes flash {
- 0%,
- 50%,
- 100% {
- opacity: 1;
- transform: scale(1);
- }
- 25%,
- 75% {
- opacity: 0.8;
- transform: scale(1.05);
- }
- }
- @keyframes glow {
- 0%,
- 100% {
- box-shadow: 0 0 5px rgba(59, 130, 246, 0.5),
- 0 0 10px rgba(59, 130, 246, 0.3);
- }
- 50% {
- box-shadow: 0 0 20px rgba(59, 130, 246, 0.8),
- 0 0 30px rgba(59, 130, 246, 0.6);
- }
- }
- .flash-animation {
- animation: flash 2s ease-in-out infinite;
- }
- .glow-animation {
- animation: glow 2s ease-in-out infinite;
- }
- /* 移动端全屏样式 */
- @media screen and (max-width: 768px) {
- video {
- /* 强制横屏全屏 */
- object-fit: contain;
- }
- /* 全屏时的样式 */
- video:fullscreen {
- width: 100vw;
- height: 100vh;
- object-fit: contain;
- background: black;
- }
- /* WebKit全屏样式 */
- video:-webkit-full-screen {
- width: 100vw;
- height: 100vh;
- object-fit: contain;
- background: black;
- }
- /* Mozilla全屏样式 */
- video:-moz-full-screen {
- width: 100vw;
- height: 100vh;
- object-fit: contain;
- background: black;
- }
- /* MS全屏样式 */
- video:-ms-fullscreen {
- width: 100vw;
- height: 100vh;
- object-fit: contain;
- background: black;
- }
- }
- </style>
|