VideoPlayer.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386
  1. <template>
  2. <section class="space-y-6">
  3. <!-- 返回按钮 -->
  4. <div class="flex items-center justify-between">
  5. <div class="flex items-center gap-2 sm:gap-3">
  6. <button
  7. @click="goBack"
  8. class="flex items-center gap-1 sm:gap-2 text-white/80 hover:text-white transition whitespace-nowrap"
  9. >
  10. <svg
  11. class="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0"
  12. fill="none"
  13. stroke="currentColor"
  14. viewBox="0 0 24 24"
  15. >
  16. <path
  17. stroke-linecap="round"
  18. stroke-linejoin="round"
  19. stroke-width="2"
  20. d="M15 19l-7-7 7-7"
  21. />
  22. </svg>
  23. <span class="text-xs sm:text-sm">返回</span>
  24. </button>
  25. </div>
  26. <!-- 购买按钮区域 -->
  27. <div
  28. v-if="!isSinglePurchased && !isVip"
  29. class="flex items-center gap-1 sm:gap-1.5"
  30. >
  31. <!-- 游客和免费用户显示试看时间信息 -->
  32. <div
  33. v-if="isGuestOrFree"
  34. class="text-2xs sm:text-xs text-white/60 px-1 sm:px-2 py-0.5 sm:py-1"
  35. >
  36. 试看{{ trialDuration }}秒
  37. </div>
  38. <!-- 只有非游客用户才显示购买会员按钮 -->
  39. <button
  40. v-if="!isGuest"
  41. @click="handleMembershipClick"
  42. 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"
  43. >
  44. 开通会员
  45. </button>
  46. <button
  47. @click="handleSinglePurchaseClick"
  48. 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"
  49. >
  50. 单片购买
  51. </button>
  52. </div>
  53. </div>
  54. <!-- 视频播放器区域 -->
  55. <div class="relative rounded-2xl overflow-hidden bg-black">
  56. <div class="aspect-video video-container">
  57. <VideoProcessor
  58. ref="videoProcessorRef"
  59. :cover-url="videoInfo.cover"
  60. :m3u8-url="videoInfo.m3u8"
  61. :alt="videoInfo.name"
  62. :auto-play="false"
  63. :hide-error="false"
  64. :enable-retry="true"
  65. cover-class="w-full h-full object-cover"
  66. video-class="w-full h-full object-contain"
  67. @cover-loaded="onCoverLoaded"
  68. @video-loaded="onVideoLoaded"
  69. @error="onVideoProcessorError"
  70. @retry="onVideoProcessorRetry"
  71. @play="onVideoPlay"
  72. @timeupdate="onVideoTimeUpdate"
  73. @seeking="onVideoSeeking"
  74. />
  75. <!-- 试看提示 -->
  76. <div
  77. v-if="isTrialMode"
  78. class="absolute top-4 right-4 text-white px-3 py-1.5 text-sm font-medium"
  79. >
  80. 试看中
  81. </div>
  82. <!-- 试看结束遮罩 -->
  83. <div
  84. v-if="showTrialEndModal && !isSinglePurchased && !isVip"
  85. class="absolute inset-0 bg-black/80 flex items-center justify-center z-50"
  86. >
  87. <div
  88. class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 max-w-sm mx-4 text-center"
  89. >
  90. <div class="text-white text-lg font-semibold mb-4">
  91. 试看时间已结束
  92. </div>
  93. <div class="text-white/70 text-sm mb-6">
  94. <span v-if="!isGuest">购买会员或单独购买本片继续观看</span>
  95. <span v-else>单独购买本片继续观看</span>
  96. </div>
  97. <div class="space-y-3">
  98. <!-- 只有非游客用户才显示购买会员按钮 -->
  99. <button
  100. v-if="!isGuest"
  101. @click="purchaseMembership"
  102. class="w-full bg-brand hover:bg-brand/90 text-slate-900 py-3 px-4 rounded-lg font-medium transition whitespace-nowrap"
  103. >
  104. 开通会员
  105. </button>
  106. <button
  107. @click="purchaseVideo"
  108. 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"
  109. >
  110. 单独购买本片
  111. </button>
  112. <button
  113. @click="showTrialEndModal = false"
  114. class="w-full text-white/60 hover:text-white/80 py-2 text-sm transition"
  115. >
  116. 取消
  117. </button>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <!-- 单片购买确认弹窗 -->
  124. <div
  125. v-if="showSinglePurchaseModal"
  126. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  127. @click.self="showSinglePurchaseModal = false"
  128. >
  129. <div
  130. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
  131. >
  132. <h3 class="text-lg font-semibold text-white/90 mb-2">确认购买</h3>
  133. <p class="text-sm text-white/70 mb-4">
  134. 确定要购买当前视频吗?<br />
  135. 购买后即可观看完整内容
  136. </p>
  137. <div class="text-lg font-semibold text-brand mb-6">
  138. ¥{{ priceStore.getSinglePrice() }}
  139. </div>
  140. <div class="space-y-3">
  141. <button
  142. @click="purchaseVideo"
  143. 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"
  144. >
  145. 确认购买
  146. </button>
  147. <button
  148. @click="showSinglePurchaseModal = false"
  149. class="w-full text-white/60 hover:text-white/80 py-2 text-sm transition"
  150. >
  151. 取消
  152. </button>
  153. </div>
  154. </div>
  155. </div>
  156. <!-- 会员购买弹窗 -->
  157. <div
  158. v-if="showMembershipPurchaseModal"
  159. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  160. @click.self="showMembershipPurchaseModal = false"
  161. >
  162. <div
  163. class="bg-surface border border-white/10 rounded-2xl p-4 w-full max-w-sm mx-4"
  164. >
  165. <h3 class="text-lg font-semibold text-white/90 mb-4 text-center">
  166. 开通会员
  167. </h3>
  168. <!-- 会员套餐选择 -->
  169. <div class="space-y-2 mb-3">
  170. <h4 class="text-sm font-medium text-white/70 mb-2">选择套餐</h4>
  171. <div class="space-y-2">
  172. <label
  173. v-for="plan in membershipPlans"
  174. :key="plan.key"
  175. class="relative cursor-pointer"
  176. >
  177. <input
  178. v-model="selectedPlan"
  179. :value="plan.key"
  180. type="radio"
  181. name="membership-plan"
  182. class="sr-only"
  183. />
  184. <div
  185. class="border-2 rounded-lg p-2 transition-all"
  186. :class="
  187. selectedPlan === plan.key
  188. ? 'border-brand bg-brand/10'
  189. : 'border-white/20 hover:border-white/30'
  190. "
  191. >
  192. <div class="flex justify-between items-center">
  193. <div
  194. class="font-medium text-white/90 text-sm whitespace-nowrap"
  195. >
  196. {{ plan.label }} - {{ plan.duration }}
  197. </div>
  198. <div class="font-semibold text-white/90 text-sm">
  199. {{ plan.price }}
  200. </div>
  201. </div>
  202. </div>
  203. </label>
  204. </div>
  205. </div>
  206. <!-- 支付方式选择 -->
  207. <div class="mb-3">
  208. <h4 class="text-sm font-medium text-white/70 mb-2">支付方式</h4>
  209. <label
  210. class="flex items-center p-2 border border-white/20 rounded-lg cursor-pointer hover:bg-white/5"
  211. >
  212. <input
  213. v-model="selectedPayment"
  214. value="alipay"
  215. type="radio"
  216. name="payment-method"
  217. class="sr-only"
  218. />
  219. <div class="flex items-center">
  220. <div
  221. class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center mr-3"
  222. >
  223. <span class="text-white text-sm font-bold">支</span>
  224. </div>
  225. <span class="text-white/90 text-sm">支付宝</span>
  226. </div>
  227. </label>
  228. </div>
  229. <!-- 操作按钮 -->
  230. <div class="flex gap-2 pt-3 border-t border-white/10">
  231. <button
  232. type="button"
  233. @click="showMembershipPurchaseModal = false"
  234. class="flex-1 px-3 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition text-sm"
  235. >
  236. 取消
  237. </button>
  238. <button
  239. @click="handleMembershipPurchase"
  240. :disabled="!selectedPlan || isPaymentLoading"
  241. 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"
  242. >
  243. {{ isPaymentLoading ? "处理中..." : "立即购买" }}
  244. </button>
  245. </div>
  246. </div>
  247. </div>
  248. <!-- 支付等待弹窗 -->
  249. <div
  250. v-if="showPaymentWaitingDialog"
  251. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  252. >
  253. <div
  254. class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
  255. >
  256. <!-- 加载动画 -->
  257. <div class="mb-6">
  258. <div
  259. class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
  260. ></div>
  261. </div>
  262. <!-- 等待文字 -->
  263. <h3 class="text-lg font-semibold text-white/90 mb-2">
  264. 正在拉起支付页面,等待支付...
  265. </h3>
  266. <p class="text-sm text-white/60 mb-6">
  267. 请在支付页面完成支付<br />
  268. 支付完成后点击已完成支付
  269. </p>
  270. <!-- 操作按钮 -->
  271. <div class="flex gap-3">
  272. <button
  273. @click="showPaymentWaitingDialog = false"
  274. class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
  275. >
  276. 取消支付
  277. </button>
  278. <button
  279. @click="handleQueryOrder"
  280. class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  281. >
  282. 已完成支付
  283. </button>
  284. </div>
  285. </div>
  286. </div>
  287. <!-- 单片购买支付等待弹窗 -->
  288. <div
  289. v-if="showSinglePaymentWaitingDialog"
  290. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  291. >
  292. <div
  293. class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
  294. >
  295. <!-- 加载动画 -->
  296. <div class="mb-6">
  297. <div
  298. class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
  299. ></div>
  300. </div>
  301. <!-- 等待文字 -->
  302. <h3 class="text-lg font-semibold text-white/90 mb-2">
  303. 正在拉起支付页面,等待支付...
  304. </h3>
  305. <p class="text-sm text-white/60 mb-6">
  306. 请在支付页面完成支付<br />
  307. 支付完成后点击已完成支付
  308. </p>
  309. <!-- 操作按钮 -->
  310. <div class="flex gap-3">
  311. <button
  312. @click="showSinglePaymentWaitingDialog = false"
  313. class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
  314. >
  315. 取消支付
  316. </button>
  317. <button
  318. @click="handleSingleQueryOrder"
  319. class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  320. >
  321. 已完成支付
  322. </button>
  323. </div>
  324. </div>
  325. </div>
  326. <!-- 错误提示弹窗 -->
  327. <div
  328. v-if="showErrorDialog"
  329. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  330. @click.self="showErrorDialog = false"
  331. >
  332. <div
  333. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
  334. >
  335. <!-- 错误图标 -->
  336. <div class="mb-4">
  337. <div
  338. class="w-12 h-12 mx-auto bg-red-500/20 rounded-full flex items-center justify-center"
  339. >
  340. <svg
  341. class="w-6 h-6 text-red-400"
  342. fill="none"
  343. stroke="currentColor"
  344. viewBox="0 0 24 24"
  345. >
  346. <path
  347. stroke-linecap="round"
  348. stroke-linejoin="round"
  349. stroke-width="2"
  350. 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"
  351. />
  352. </svg>
  353. </div>
  354. </div>
  355. <!-- 错误信息 -->
  356. <h3 class="text-lg font-semibold text-white/90 mb-2">操作失败</h3>
  357. <p class="text-sm text-white/70 mb-6">
  358. {{ errorMessage }}
  359. </p>
  360. <!-- 确认按钮 -->
  361. <button
  362. @click="showErrorDialog = false"
  363. class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  364. >
  365. 确定
  366. </button>
  367. </div>
  368. </div>
  369. <!-- 成功提示弹窗 -->
  370. <div
  371. v-if="showSuccessDialog"
  372. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  373. @click.self="showSuccessDialog = false"
  374. >
  375. <div
  376. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
  377. >
  378. <!-- 成功图标 -->
  379. <div class="mb-4">
  380. <div
  381. class="w-12 h-12 mx-auto bg-green-500/20 rounded-full flex items-center justify-center"
  382. >
  383. <svg
  384. class="w-6 h-6 text-green-400"
  385. fill="none"
  386. stroke="currentColor"
  387. viewBox="0 0 24 24"
  388. >
  389. <path
  390. stroke-linecap="round"
  391. stroke-linejoin="round"
  392. stroke-width="2"
  393. d="M5 13l4 4L19 7"
  394. />
  395. </svg>
  396. </div>
  397. </div>
  398. <!-- 成功信息 -->
  399. <h3 class="text-lg font-semibold text-white/90 mb-2">操作成功</h3>
  400. <p class="text-sm text-white/70 mb-6">
  401. {{ successMessage }}
  402. </p>
  403. <!-- 确认按钮 -->
  404. <button
  405. @click="showSuccessDialog = false"
  406. class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  407. >
  408. 确定
  409. </button>
  410. </div>
  411. </div>
  412. <!-- 视频信息区域 -->
  413. <div class="space-y-6">
  414. <!-- 视频标题和基本信息 -->
  415. <div class="space-y-3">
  416. <h1 class="text-xl font-semibold text-white leading-tight">
  417. {{ videoInfo.name || "视频标题" }}
  418. </h1>
  419. <div class="flex items-center gap-4 text-sm text-white/60">
  420. <div class="flex items-center gap-1">
  421. <svg
  422. class="w-4 h-4"
  423. fill="none"
  424. stroke="currentColor"
  425. viewBox="0 0 24 24"
  426. >
  427. <path
  428. stroke-linecap="round"
  429. stroke-linejoin="round"
  430. stroke-width="2"
  431. d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
  432. />
  433. </svg>
  434. <span>{{ formatDuration(videoInfo.duration) }}</span>
  435. </div>
  436. <div class="flex items-center gap-1">
  437. <svg
  438. class="w-4 h-4"
  439. fill="none"
  440. stroke="currentColor"
  441. viewBox="0 0 24 24"
  442. >
  443. <path
  444. stroke-linecap="round"
  445. stroke-linejoin="round"
  446. stroke-width="2"
  447. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  448. />
  449. <path
  450. stroke-linecap="round"
  451. stroke-linejoin="round"
  452. stroke-width="2"
  453. 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"
  454. />
  455. </svg>
  456. <span>{{ formatNumber(videoInfo.view) }} 次观看</span>
  457. </div>
  458. <div class="flex items-center gap-1">
  459. <svg
  460. class="w-4 h-4"
  461. fill="none"
  462. stroke="currentColor"
  463. viewBox="0 0 24 24"
  464. >
  465. <path
  466. stroke-linecap="round"
  467. stroke-linejoin="round"
  468. stroke-width="2"
  469. 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"
  470. />
  471. </svg>
  472. <span>{{ formatNumber(videoInfo.like) }} 点赞</span>
  473. </div>
  474. </div>
  475. </div>
  476. <!-- 标签信息 -->
  477. <!-- <div
  478. v-if="videoInfo.taginfo && videoInfo.taginfo.length > 0"
  479. class="space-y-3"
  480. >
  481. <h3 class="text-sm font-medium text-white/80">标签</h3>
  482. <div class="flex flex-wrap gap-2">
  483. <span
  484. v-for="tag in videoInfo.taginfo"
  485. :key="tag.hash"
  486. 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"
  487. >
  488. {{ tag.name }}
  489. </span>
  490. </div>
  491. </div> -->
  492. <!-- 相关推荐 -->
  493. <div v-if="relatedVideos.length > 0" class="space-y-4">
  494. <h3 class="text-sm font-medium text-white/80">相关推荐</h3>
  495. <div class="grid grid-cols-2 gap-3">
  496. <article
  497. v-for="video in relatedVideos.slice(0, 12)"
  498. :key="video.id"
  499. @click="playVideo(video)"
  500. class="group rounded-xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
  501. >
  502. <!-- 横屏封面 -->
  503. <div class="aspect-[16/9] relative">
  504. <VideoProcessor
  505. :cover-url="video.cover"
  506. :alt="video.name"
  507. cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
  508. />
  509. <div
  510. class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded"
  511. >
  512. {{ formatDuration(video.duration) }}
  513. </div>
  514. </div>
  515. <!-- 视频信息 -->
  516. <div class="p-3">
  517. <h4 class="text-sm font-medium text-white/90 line-clamp-2 mb-1">
  518. {{ video.name }}
  519. </h4>
  520. <p class="text-xs text-white/50 truncate">
  521. {{ formatNumber(video.view) }} 次观看 ·
  522. {{ formatNumber(video.like) }} 点赞
  523. </p>
  524. <!-- 标签信息(移动端隐藏) -->
  525. <div
  526. v-if="video.taginfo && video.taginfo.length > 0"
  527. class="hidden sm:flex flex-wrap gap-1 mt-2"
  528. >
  529. <span
  530. v-for="tag in video.taginfo.slice(0, 2)"
  531. :key="tag.hash"
  532. class="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/70"
  533. >
  534. {{ tag.name }}
  535. </span>
  536. <span
  537. v-if="video.taginfo.length > 2"
  538. class="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/50"
  539. >
  540. +{{ video.taginfo.length - 2 }}
  541. </span>
  542. </div>
  543. </div>
  544. </article>
  545. </div>
  546. </div>
  547. </div>
  548. <!-- 登录弹窗 -->
  549. <LoginDialog v-model="showLoginDialog" @login-success="onLoginSuccess" />
  550. </section>
  551. </template>
  552. <script setup lang="ts">
  553. import { ref, onMounted, onUnmounted, computed, watch } from "vue";
  554. import { useRoute, useRouter } from "vue-router";
  555. import {
  556. searchVideoByTags,
  557. getVideoDetail,
  558. purchaseSingle,
  559. purchaseMember,
  560. userQueryOrder,
  561. checkSinglePurchase,
  562. } from "@/services/api";
  563. import VideoProcessor from "@/components/VideoProcessor.vue";
  564. import LoginDialog from "@/components/LoginDialog.vue";
  565. import { useUserStore } from "@/store/user";
  566. import { usePriceStore } from "@/store/price";
  567. import { VipLevel, canWatchFullVideo, getTrialDuration } from "@/types/vip";
  568. // 路由相关
  569. const route = useRoute();
  570. const router = useRouter();
  571. // 用户状态
  572. const userStore = useUserStore();
  573. const priceStore = usePriceStore();
  574. // 视频播放器引用(现在由 VideoProcessor 组件管理)
  575. const videoPlayer = ref<HTMLVideoElement>();
  576. const videoProcessorRef = ref<any>();
  577. // 视频信息
  578. const videoInfo = ref<any>({
  579. id: "",
  580. name: "",
  581. cover: "",
  582. m3u8: "",
  583. duration: 0,
  584. view: 0,
  585. like: 0,
  586. time: 0,
  587. taginfo: [],
  588. });
  589. // 相关视频
  590. const relatedVideos = ref<any[]>([]);
  591. // 状态管理
  592. const showTrialEndModal = ref(false);
  593. const showMembershipPurchaseModal = ref(false);
  594. const showSinglePurchaseModal = ref(false);
  595. const showLoginDialog = ref(false);
  596. const isTrialMode = ref(false);
  597. // 单片购买状态
  598. const isSinglePurchased = ref(false);
  599. // 使用动态价格配置
  600. const membershipPlans = computed(() => priceStore.getMembershipPlans);
  601. const selectedPlan = ref("");
  602. const selectedPayment = ref("alipay");
  603. const isPaymentLoading = ref(false);
  604. // 支付等待相关
  605. const showPaymentWaitingDialog = ref(false);
  606. const currentOrderNo = ref("");
  607. // 提示弹窗相关
  608. const showSuccessDialog = ref(false);
  609. const showErrorDialog = ref(false);
  610. const successMessage = ref("");
  611. const errorMessage = ref("");
  612. // 单片购买支付等待相关
  613. const showSinglePaymentWaitingDialog = ref(false);
  614. const singleCurrentOrderNo = ref("");
  615. // 检测是否为夸克浏览器
  616. const isQuarkBrowser = () => {
  617. const ua = navigator.userAgent.toLowerCase();
  618. return ua.includes("quark");
  619. };
  620. // 显示成功/错误提示的函数
  621. const showSuccess = (message: string) => {
  622. successMessage.value = message;
  623. showSuccessDialog.value = true;
  624. // 夸克浏览器自动滚动到底部
  625. if (isQuarkBrowser()) {
  626. setTimeout(() => {
  627. window.scrollTo({
  628. top: document.documentElement.scrollHeight,
  629. behavior: "smooth",
  630. });
  631. }, 100);
  632. }
  633. };
  634. const showError = (message: string) => {
  635. errorMessage.value = message;
  636. showErrorDialog.value = true;
  637. // 夸克浏览器自动滚动到底部
  638. if (isQuarkBrowser()) {
  639. setTimeout(() => {
  640. window.scrollTo({
  641. top: document.documentElement.scrollHeight,
  642. behavior: "smooth",
  643. });
  644. }, 100);
  645. }
  646. };
  647. // 生成设备标识
  648. const generateMacAddress = (): string => {
  649. const hex = "0123456789ABCDEF";
  650. let mac = "";
  651. for (let i = 0; i < 6; i++) {
  652. if (i > 0) mac += ":";
  653. mac += hex[Math.floor(Math.random() * 16)];
  654. mac += hex[Math.floor(Math.random() * 16)];
  655. }
  656. return mac;
  657. };
  658. const device = generateMacAddress();
  659. // Safari兼容性处理:打开支付页面
  660. const openPaymentPage = (url: string) => {
  661. // 检测是否为Safari浏览器
  662. const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  663. if (isSafari) {
  664. // Safari浏览器
  665. setTimeout(() => window.open(url, "_blank"));
  666. // window.location.href = url;
  667. } else {
  668. // 其他浏览器:正常使用window.open
  669. window.open(url, "_blank");
  670. }
  671. };
  672. // 计算属性
  673. const currentVipLevel = computed(() => userStore.getVipLevel());
  674. const isGuestOrFree = computed(() => !canWatchFullVideo(currentVipLevel.value));
  675. const trialDuration = computed(() => getTrialDuration(priceStore));
  676. // 参考Account.vue的用户类型判断
  677. const isGuest = computed(() => {
  678. return userStore.userInfo?.vipLevel === "guest";
  679. });
  680. const isVip = computed(() => {
  681. const level = userStore.userInfo?.vipLevel;
  682. return level && level !== VipLevel.GUEST && level !== VipLevel.FREE;
  683. });
  684. // VideoProcessor 事件处理
  685. const onCoverLoaded = (url: string) => {
  686. // 封面加载完成
  687. };
  688. const onVideoLoaded = (url: string) => {
  689. // 视频加载完成
  690. };
  691. const onVideoProcessorError = (error: string) => {
  692. console.error("VideoProcessor 错误:", error);
  693. };
  694. const onVideoProcessorRetry = () => {
  695. // VideoProcessor 重试
  696. };
  697. // 处理登录成功
  698. const onLoginSuccess = async () => {
  699. // 登录成功后刷新用户信息
  700. await userStore.sync();
  701. // 检查购买状态
  702. if (videoInfo.value.id && videoInfo.value.id !== "unknown") {
  703. await checkVideoPurchaseStatus();
  704. }
  705. };
  706. const onVideoPlay = () => {
  707. // 如果是guest或free用户且还没有开始试看,且未购买当前视频,则开始试看
  708. if (isGuestOrFree.value && !isTrialMode.value && !isSinglePurchased.value) {
  709. startTrial();
  710. }
  711. };
  712. // 监听视频播放进度
  713. const onVideoTimeUpdate = () => {
  714. if (isGuestOrFree.value && isTrialMode.value) {
  715. const video = document.querySelector("video");
  716. if (video && video.currentTime > trialDuration.value) {
  717. // 超过试看时间,停止播放并显示购买弹窗
  718. video.pause();
  719. // 将视频时间重置到试看结束时间
  720. video.currentTime = trialDuration.value;
  721. // 停止HLS加载
  722. if (videoProcessorRef.value) {
  723. videoProcessorRef.value.stopHlsLoading();
  724. }
  725. showTrialEndModal.value = true;
  726. }
  727. }
  728. };
  729. // 监听视频时间变化(包括拖拽进度条)
  730. const onVideoSeeking = () => {
  731. if (isGuestOrFree.value && isTrialMode.value) {
  732. const video = document.querySelector("video");
  733. if (video && video.currentTime > trialDuration.value) {
  734. // 如果拖拽到超过试看时间,强制回到试看结束时间
  735. video.currentTime = trialDuration.value;
  736. }
  737. }
  738. };
  739. // 格式化时长
  740. const formatDuration = (duration: string | number): string => {
  741. const seconds = parseInt(String(duration));
  742. const minutes = Math.floor(seconds / 60);
  743. const remainingSeconds = seconds % 60;
  744. return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
  745. };
  746. // 格式化数字
  747. const formatNumber = (num: string | number): string => {
  748. const n = parseInt(String(num));
  749. if (n >= 10000) {
  750. return `${(n / 10000).toFixed(1)}万`;
  751. }
  752. return n.toString();
  753. };
  754. // 返回上一页
  755. const goBack = () => {
  756. // 使用router.back()返回上一页,而不是直接导航到主页
  757. // 这样可以确保按照浏览历史顺序返回
  758. router.back();
  759. };
  760. // 播放视频
  761. const playVideo = (video: any) => {
  762. const vipLevel = userStore.getVipLevel();
  763. if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
  764. // guest和free用户通过URL参数传递cover和m3u8,同时包含视频ID
  765. router.push({
  766. name: "VideoPlayer",
  767. params: { id: video.id },
  768. query: {
  769. cover: video.cover,
  770. m3u8: video.m3u8,
  771. name: video.name,
  772. duration: video.duration,
  773. view: video.view,
  774. like: video.like,
  775. },
  776. });
  777. } else {
  778. // 其他VIP用户正常调用详情接口
  779. router.push({
  780. name: "VideoPlayer",
  781. params: { id: video.id },
  782. });
  783. }
  784. // 滚动到页面顶端
  785. window.scrollTo({
  786. top: 0,
  787. behavior: "smooth",
  788. });
  789. };
  790. // 开始试看
  791. const startTrial = () => {
  792. isTrialMode.value = true;
  793. };
  794. // 处理顶部会员购买按钮点击
  795. const handleMembershipClick = () => {
  796. // 检查用户是否已登录
  797. if (!userStore.token) {
  798. // 未登录,显示登录弹窗
  799. showLoginDialog.value = true;
  800. return;
  801. }
  802. // 已登录,显示购买弹窗
  803. showMembershipPurchaseModal.value = true;
  804. // 夸克浏览器自动滚动到底部
  805. if (isQuarkBrowser()) {
  806. setTimeout(() => {
  807. window.scrollTo({
  808. top: document.documentElement.scrollHeight,
  809. behavior: "smooth",
  810. });
  811. }, 100);
  812. }
  813. };
  814. // 处理顶部单片购买按钮点击
  815. const handleSinglePurchaseClick = () => {
  816. // 检查用户是否已登录
  817. if (!userStore.token) {
  818. // 未登录,显示登录弹窗
  819. showLoginDialog.value = true;
  820. return;
  821. }
  822. // 已登录,显示购买弹窗
  823. showSinglePurchaseModal.value = true;
  824. // 夸克浏览器自动滚动到底部
  825. if (isQuarkBrowser()) {
  826. setTimeout(() => {
  827. window.scrollTo({
  828. top: document.documentElement.scrollHeight,
  829. behavior: "smooth",
  830. });
  831. }, 100);
  832. }
  833. };
  834. // 购买会员
  835. const purchaseMembership = () => {
  836. // 检查用户是否已登录
  837. if (!userStore.token) {
  838. // 未登录,显示登录弹窗
  839. showTrialEndModal.value = false;
  840. showLoginDialog.value = true;
  841. return;
  842. }
  843. // 关闭试看结束弹窗
  844. showTrialEndModal.value = false;
  845. // 打开会员购买弹窗
  846. showMembershipPurchaseModal.value = true;
  847. // 夸克浏览器自动滚动到底部
  848. if (isQuarkBrowser()) {
  849. setTimeout(() => {
  850. window.scrollTo({
  851. top: document.documentElement.scrollHeight,
  852. behavior: "smooth",
  853. });
  854. }, 100);
  855. }
  856. };
  857. // 处理会员购买
  858. const handleMembershipPurchase = async () => {
  859. if (!selectedPlan.value) {
  860. showError("请选择会员套餐");
  861. return;
  862. }
  863. isPaymentLoading.value = true;
  864. try {
  865. const response = await purchaseMember(
  866. userStore.userInfo.id,
  867. selectedPlan.value
  868. );
  869. if (response.code === 1) {
  870. // 保存订单号
  871. currentOrderNo.value = response.out_trade_no;
  872. // 关闭购买弹窗
  873. showMembershipPurchaseModal.value = false;
  874. // Safari兼容性处理:使用多种方式打开支付页面
  875. // openPaymentPage(response.code_url);
  876. setTimeout(() => window.open(response.code_url, "_blank"));
  877. // 显示支付等待弹窗
  878. showPaymentWaitingDialog.value = true;
  879. // 重置选择
  880. selectedPlan.value = "";
  881. } else {
  882. showError(`支付失败: ${response.msg || "未知错误"}`);
  883. }
  884. } catch (error) {
  885. console.error("购买会员失败", error);
  886. showError("购买失败,请重试");
  887. } finally {
  888. isPaymentLoading.value = false;
  889. }
  890. };
  891. // 查询订单状态
  892. const handleQueryOrder = async () => {
  893. if (!currentOrderNo.value) {
  894. showError("没有找到订单信息");
  895. return;
  896. }
  897. try {
  898. const response = await userQueryOrder(currentOrderNo.value);
  899. if (response.status === 1) {
  900. // 支付成功
  901. await userStore.sync();
  902. showPaymentWaitingDialog.value = false;
  903. showSuccess("会员购买成功!");
  904. currentOrderNo.value = "";
  905. } else {
  906. showError(response.msg || "会员购买失败");
  907. }
  908. } catch (error) {
  909. console.error("查询订单状态失败:", error);
  910. showError("会员购买失败,请重试");
  911. }
  912. };
  913. // 查询单片购买订单状态
  914. const handleSingleQueryOrder = async () => {
  915. if (!singleCurrentOrderNo.value) {
  916. showError("没有找到订单信息");
  917. return;
  918. }
  919. try {
  920. // 使用视频信息的真实ID查询订单
  921. const response = await userQueryOrder(
  922. singleCurrentOrderNo.value,
  923. videoInfo.value.id
  924. );
  925. if (response.status === 1) {
  926. // 支付成功
  927. await userStore.sync();
  928. showSinglePaymentWaitingDialog.value = false;
  929. showSuccess("视频购买成功!");
  930. singleCurrentOrderNo.value = "";
  931. // 重新检查购买状态(已完成支付时的检查)
  932. await checkVideoPurchaseStatus();
  933. } else {
  934. showError(response.msg || "视频购买失败");
  935. }
  936. } catch (error) {
  937. console.error("查询单片购买订单状态失败:", error);
  938. showError("视频购买失败,请重试");
  939. }
  940. };
  941. // 单独购买本片
  942. const purchaseVideo = async () => {
  943. // 检查用户是否已登录
  944. if (!userStore.token) {
  945. // 未登录,显示登录弹窗
  946. showTrialEndModal.value = false;
  947. showSinglePurchaseModal.value = false;
  948. showLoginDialog.value = true;
  949. return;
  950. }
  951. // 夸克浏览器自动滚动到底部
  952. if (isQuarkBrowser()) {
  953. setTimeout(() => {
  954. window.scrollTo({
  955. top: document.documentElement.scrollHeight,
  956. behavior: "smooth",
  957. });
  958. }, 100);
  959. }
  960. try {
  961. // 使用视频信息的真实ID
  962. if (!videoInfo.value.id || videoInfo.value.id === "unknown") {
  963. showError("未找到视频资源ID");
  964. return;
  965. }
  966. const response = await purchaseSingle(
  967. userStore.userInfo.id,
  968. videoInfo.value.id
  969. );
  970. if (response.code === 1) {
  971. // 保存订单号
  972. singleCurrentOrderNo.value = response.out_trade_no;
  973. // 关闭弹窗
  974. showTrialEndModal.value = false;
  975. showSinglePurchaseModal.value = false;
  976. // Safari兼容性处理:使用多种方式打开支付页面
  977. // openPaymentPage(response.code_url);
  978. setTimeout(() => window.open(response.code_url, "_blank"));
  979. // 显示支付等待弹窗
  980. showSinglePaymentWaitingDialog.value = true;
  981. console.log("单片购买订单创建成功:", response.out_trade_no);
  982. } else {
  983. console.error("单片购买失败:", response.msg);
  984. showError(`购买失败: ${response.msg || "未知错误"}`);
  985. }
  986. } catch (error) {
  987. console.error("单片购买异常:", error);
  988. showError("购买失败,请重试");
  989. }
  990. };
  991. // 检查用户是否已购买当前视频
  992. const checkVideoPurchaseStatus = async () => {
  993. if (!videoInfo.value.id || videoInfo.value.id === "unknown") {
  994. return;
  995. }
  996. try {
  997. const response = await checkSinglePurchase(videoInfo.value.id);
  998. isSinglePurchased.value = response === true;
  999. } catch (error) {
  1000. console.error("检查视频购买状态失败:", error);
  1001. isSinglePurchased.value = false;
  1002. }
  1003. };
  1004. // 加载视频信息
  1005. const loadVideoInfo = async () => {
  1006. const vipLevel = userStore.getVipLevel();
  1007. // 获取视频ID(无论什么用户类型都使用相同的获取方式)
  1008. const videoId = route.params.id || route.query.id;
  1009. // 如果是guest或free用户,从query参数获取视频信息
  1010. if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
  1011. const { cover, m3u8, name, duration, view, like } = route.query;
  1012. if (cover && m3u8) {
  1013. videoInfo.value = {
  1014. id: videoId || "unknown",
  1015. name: name || "试看视频",
  1016. cover: cover as string,
  1017. m3u8: m3u8 as string,
  1018. duration: parseInt(duration as string) || 0,
  1019. view: parseInt(view as string) || 0,
  1020. like: parseInt(like as string) || 0,
  1021. time: 0,
  1022. taginfo: [],
  1023. };
  1024. // 设置页面标题
  1025. document.title = `${videoInfo.value.name} - 试看`;
  1026. return;
  1027. }
  1028. }
  1029. // 其他情况,通过API获取视频详情
  1030. if (videoId) {
  1031. try {
  1032. // 通过API获取视频详情
  1033. const response = await getVideoDetail(device, String(videoId));
  1034. if (response.status === 0 && response.data) {
  1035. const data = response.data;
  1036. videoInfo.value = {
  1037. id: data.id || videoId,
  1038. name: data.name || `视频 ${videoId}`,
  1039. cover: data.cover || "",
  1040. m3u8: data.m3u8 || "",
  1041. duration: data.duration || 0,
  1042. view: data.view || 0,
  1043. like: data.like || 0,
  1044. time: data.time || 0,
  1045. taginfo: data.tags
  1046. ? data.tags
  1047. .split(",")
  1048. .map((tag: string) => ({ name: tag.trim(), hash: tag.trim() }))
  1049. : [],
  1050. };
  1051. // 设置页面标题
  1052. document.title = `${videoInfo.value.name} - 视频播放`;
  1053. } else {
  1054. console.error("获取视频详情失败-1:", response.msg);
  1055. // 设置默认值
  1056. videoInfo.value = {
  1057. id: videoId,
  1058. name: `视频 ${videoId}`,
  1059. cover: "",
  1060. m3u8: "",
  1061. duration: 0,
  1062. view: 0,
  1063. like: 0,
  1064. time: 0,
  1065. taginfo: [],
  1066. };
  1067. }
  1068. } catch (error) {
  1069. console.error("获取视频详情失败-2:", error);
  1070. // 设置默认值
  1071. videoInfo.value = {
  1072. id: videoId,
  1073. name: `视频 ${videoId}`,
  1074. cover: "",
  1075. m3u8: "",
  1076. duration: 0,
  1077. view: 0,
  1078. like: 0,
  1079. time: 0,
  1080. taginfo: [],
  1081. };
  1082. }
  1083. } else {
  1084. console.error("未找到视频ID");
  1085. }
  1086. };
  1087. // 加载相关视频
  1088. const loadRelatedVideos = async () => {
  1089. try {
  1090. // 获取当前视频的标签
  1091. const currentVideoTags = videoInfo.value.taginfo || [];
  1092. if (currentVideoTags.length > 0) {
  1093. // 随机选择一个标签
  1094. const randomTag =
  1095. currentVideoTags[Math.floor(Math.random() * currentVideoTags.length)];
  1096. // 生成1-10之间的随机页码
  1097. const randomPage = Math.floor(Math.random() * 10) + 1;
  1098. // 获取12个该标签下的最热视频
  1099. const response = await searchVideoByTags(
  1100. device,
  1101. randomPage,
  1102. 12,
  1103. randomTag.hash,
  1104. "long",
  1105. "view"
  1106. );
  1107. if (response.status === 0 && response.data?.list) {
  1108. // 过滤掉当前视频
  1109. relatedVideos.value = response.data.list
  1110. .filter((video: any) => video.id !== videoInfo.value.id)
  1111. .slice(0, 12);
  1112. }
  1113. } else {
  1114. // 如果没有标签,则获取全局最热视频
  1115. const randomPage = Math.floor(Math.random() * 10) + 1;
  1116. const response = await searchVideoByTags(
  1117. device,
  1118. randomPage,
  1119. 12,
  1120. undefined,
  1121. "long",
  1122. "view"
  1123. );
  1124. if (response.status === 0 && response.data?.list) {
  1125. relatedVideos.value = response.data.list
  1126. .filter((video: any) => video.id !== videoInfo.value.id)
  1127. .slice(0, 12);
  1128. }
  1129. }
  1130. } catch (error) {
  1131. console.error("加载相关视频失败:", error);
  1132. }
  1133. };
  1134. // 监听路由参数变化
  1135. watch(
  1136. () => route.params.id,
  1137. async (newId, oldId) => {
  1138. if (newId && newId !== oldId) {
  1139. // 停止当前视频播放
  1140. if (videoProcessorRef.value) {
  1141. videoProcessorRef.value.stopVideo();
  1142. }
  1143. // 重置购买状态
  1144. isSinglePurchased.value = false;
  1145. await loadVideoInfo();
  1146. await loadRelatedVideos();
  1147. // 检查购买状态(首页点击进入视频时的检查)
  1148. const vipLevel = userStore.getVipLevel();
  1149. if (
  1150. vipLevel !== VipLevel.GUEST &&
  1151. videoInfo.value.id &&
  1152. videoInfo.value.id !== "unknown"
  1153. ) {
  1154. await checkVideoPurchaseStatus();
  1155. }
  1156. } else if (oldId === undefined && newId) {
  1157. await loadVideoInfo();
  1158. await loadRelatedVideos();
  1159. const vipLevel = userStore.getVipLevel();
  1160. if (
  1161. vipLevel !== VipLevel.GUEST &&
  1162. videoInfo.value.id &&
  1163. videoInfo.value.id !== "unknown"
  1164. ) {
  1165. await checkVideoPurchaseStatus();
  1166. }
  1167. }
  1168. },
  1169. { immediate: true }
  1170. );
  1171. onMounted(async () => {
  1172. // 加载价格配置
  1173. if (!priceStore.isPriceConfigLoaded) {
  1174. await priceStore.fetchPriceConfig();
  1175. }
  1176. });
  1177. onUnmounted(() => {
  1178. // 组件卸载时的清理工作
  1179. });
  1180. </script>
  1181. <style scoped>
  1182. /* 视频容器样式 */
  1183. .video-container {
  1184. position: relative;
  1185. }
  1186. /* 自定义闪烁动画 */
  1187. @keyframes flash {
  1188. 0%,
  1189. 50%,
  1190. 100% {
  1191. opacity: 1;
  1192. transform: scale(1);
  1193. }
  1194. 25%,
  1195. 75% {
  1196. opacity: 0.8;
  1197. transform: scale(1.05);
  1198. }
  1199. }
  1200. @keyframes glow {
  1201. 0%,
  1202. 100% {
  1203. box-shadow: 0 0 5px rgba(59, 130, 246, 0.5),
  1204. 0 0 10px rgba(59, 130, 246, 0.3);
  1205. }
  1206. 50% {
  1207. box-shadow: 0 0 20px rgba(59, 130, 246, 0.8),
  1208. 0 0 30px rgba(59, 130, 246, 0.6);
  1209. }
  1210. }
  1211. .flash-animation {
  1212. animation: flash 2s ease-in-out infinite;
  1213. }
  1214. .glow-animation {
  1215. animation: glow 2s ease-in-out infinite;
  1216. }
  1217. /* 移动端全屏样式 */
  1218. @media screen and (max-width: 768px) {
  1219. video {
  1220. /* 强制横屏全屏 */
  1221. object-fit: contain;
  1222. }
  1223. /* 全屏时的样式 */
  1224. video:fullscreen {
  1225. width: 100vw;
  1226. height: 100vh;
  1227. object-fit: contain;
  1228. background: black;
  1229. }
  1230. /* WebKit全屏样式 */
  1231. video:-webkit-full-screen {
  1232. width: 100vw;
  1233. height: 100vh;
  1234. object-fit: contain;
  1235. background: black;
  1236. }
  1237. /* Mozilla全屏样式 */
  1238. video:-moz-full-screen {
  1239. width: 100vw;
  1240. height: 100vh;
  1241. object-fit: contain;
  1242. background: black;
  1243. }
  1244. /* MS全屏样式 */
  1245. video:-ms-fullscreen {
  1246. width: 100vw;
  1247. height: 100vh;
  1248. object-fit: contain;
  1249. background: black;
  1250. }
  1251. }
  1252. </style>