| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386 |
- <template>
- <section class="space-y-6">
- <!-- 返回按钮 -->
- <div class="flex items-center justify-between">
- <div class="flex items-center gap-2 sm:gap-3">
- <button
- @click="goBack"
- class="flex items-center gap-1 sm:gap-2 text-white/80 hover:text-white transition whitespace-nowrap"
- >
- <svg
- class="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0"
- 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-xs sm:text-sm">返回</span>
- </button>
- </div>
- <!-- 购买按钮区域 -->
- <div
- v-if="!isSinglePurchased && !isVip"
- class="flex items-center gap-1 sm:gap-1.5"
- >
- <!-- 游客和免费用户显示试看时间信息 -->
- <div
- v-if="isGuestOrFree"
- class="text-2xs sm:text-xs text-white/60 px-1 sm:px-2 py-0.5 sm:py-1"
- >
- 试看{{ trialDuration }}秒
- </div>
- <!-- 只有非游客用户才显示购买会员按钮 -->
- <button
- v-if="!isGuest"
- @click="handleMembershipClick"
- class="px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-md bg-brand text-slate-900 text-2xs sm:text-xs font-medium hover:bg-brand/90 transition whitespace-nowrap min-w-fit"
- >
- 开通会员
- </button>
- <button
- @click="handleSinglePurchaseClick"
- class="px-2 sm:px-3 py-1 sm:py-2 rounded-lg bg-blue-500 text-white text-2xs sm: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>
- <!-- 登录弹窗 -->
- <LoginDialog v-model="showLoginDialog" @login-success="onLoginSuccess" />
- </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 LoginDialog from "@/components/LoginDialog.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 showTrialEndModal = ref(false);
- const showMembershipPurchaseModal = ref(false);
- const showSinglePurchaseModal = ref(false);
- const showLoginDialog = 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 onLoginSuccess = async () => {
- // 登录成功后刷新用户信息
- await userStore.sync();
- // 检查购买状态
- if (videoInfo.value.id && videoInfo.value.id !== "unknown") {
- await checkVideoPurchaseStatus();
- }
- };
- 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()返回上一页,而不是直接导航到主页
- // 这样可以确保按照浏览历史顺序返回
- router.back();
- };
- // 播放视频
- 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 = () => {
- // 检查用户是否已登录
- if (!userStore.token) {
- // 未登录,显示登录弹窗
- showLoginDialog.value = true;
- return;
- }
- // 已登录,显示购买弹窗
- showMembershipPurchaseModal.value = true;
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: "smooth",
- });
- }, 100);
- }
- };
- // 处理顶部单片购买按钮点击
- const handleSinglePurchaseClick = () => {
- // 检查用户是否已登录
- if (!userStore.token) {
- // 未登录,显示登录弹窗
- showLoginDialog.value = true;
- return;
- }
- // 已登录,显示购买弹窗
- showSinglePurchaseModal.value = true;
- // 夸克浏览器自动滚动到底部
- if (isQuarkBrowser()) {
- setTimeout(() => {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: "smooth",
- });
- }, 100);
- }
- };
- // 购买会员
- const purchaseMembership = () => {
- // 检查用户是否已登录
- if (!userStore.token) {
- // 未登录,显示登录弹窗
- showTrialEndModal.value = false;
- showLoginDialog.value = true;
- return;
- }
- // 关闭试看结束弹窗
- 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 (!userStore.token) {
- // 未登录,显示登录弹窗
- showTrialEndModal.value = false;
- showSinglePurchaseModal.value = false;
- showLoginDialog.value = true;
- return;
- }
- // 夸克浏览器自动滚动到底部
- 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>
|