@robot-admin/naive-ui-components 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/README.md +257 -0
  2. package/dist/C_ActionBar-DWN-woTc.css.map +1 -0
  3. package/dist/C_ActionBar.cjs +5 -0
  4. package/dist/C_ActionBar.d.cts +2 -0
  5. package/dist/C_ActionBar.d.ts +2 -0
  6. package/dist/C_ActionBar.js +4 -0
  7. package/dist/C_ActionBar2.js +196 -0
  8. package/dist/C_ActionBar2.js.map +1 -0
  9. package/dist/C_AntV-AFKyK6hH.css.map +1 -0
  10. package/dist/C_AntV.cjs +8 -0
  11. package/dist/C_AntV.d.cts +2 -0
  12. package/dist/C_AntV.d.ts +2 -0
  13. package/dist/C_AntV.js +4 -0
  14. package/dist/C_AntV2.js +3150 -0
  15. package/dist/C_AntV2.js.map +1 -0
  16. package/dist/C_Barcode-P_EFj8dC.css.map +1 -0
  17. package/dist/C_Barcode.cjs +4 -0
  18. package/dist/C_Barcode.d.cts +2 -0
  19. package/dist/C_Barcode.d.ts +2 -0
  20. package/dist/C_Barcode.js +3 -0
  21. package/dist/C_Barcode2.js +68 -0
  22. package/dist/C_Barcode2.js.map +1 -0
  23. package/dist/C_Captcha-C-ef41xw.css.map +1 -0
  24. package/dist/C_Captcha.cjs +4 -0
  25. package/dist/C_Captcha.d.cts +2 -0
  26. package/dist/C_Captcha.d.ts +2 -0
  27. package/dist/C_Captcha.js +3 -0
  28. package/dist/C_Captcha2.js +155 -0
  29. package/dist/C_Captcha2.js.map +1 -0
  30. package/dist/C_Cascade-D9kNsjsV.css.map +1 -0
  31. package/dist/C_Cascade.cjs +4 -0
  32. package/dist/C_Cascade.d.cts +2 -0
  33. package/dist/C_Cascade.d.ts +2 -0
  34. package/dist/C_Cascade.js +3 -0
  35. package/dist/C_Cascade2.js +103 -0
  36. package/dist/C_Cascade2.js.map +1 -0
  37. package/dist/C_City-BCQ4ipiK.css.map +1 -0
  38. package/dist/C_City.cjs +4 -0
  39. package/dist/C_City.d.cts +2 -0
  40. package/dist/C_City.d.ts +2 -0
  41. package/dist/C_City.js +3 -0
  42. package/dist/C_City2.js +841 -0
  43. package/dist/C_City2.js.map +1 -0
  44. package/dist/C_Code-C9kvvEmO.css.map +1 -0
  45. package/dist/C_Code.cjs +5 -0
  46. package/dist/C_Code.d.cts +2 -0
  47. package/dist/C_Code.d.ts +2 -0
  48. package/dist/C_Code.js +4 -0
  49. package/dist/C_Code2.js +346 -0
  50. package/dist/C_Code2.js.map +1 -0
  51. package/dist/C_CollapsePanel-BUJHuYcU.css.map +1 -0
  52. package/dist/C_CollapsePanel.cjs +6 -0
  53. package/dist/C_CollapsePanel.d.cts +2 -0
  54. package/dist/C_CollapsePanel.d.ts +2 -0
  55. package/dist/C_CollapsePanel.js +4 -0
  56. package/dist/C_CollapsePanel2.js +319 -0
  57. package/dist/C_CollapsePanel2.js.map +1 -0
  58. package/dist/C_Cron-yx2Ob4Jl.css.map +1 -0
  59. package/dist/C_Cron.cjs +15 -0
  60. package/dist/C_Cron.d.cts +2 -0
  61. package/dist/C_Cron.d.ts +2 -0
  62. package/dist/C_Cron.js +4 -0
  63. package/dist/C_Cron2.js +1209 -0
  64. package/dist/C_Cron2.js.map +1 -0
  65. package/dist/C_Date.cjs +4 -0
  66. package/dist/C_Date.d.cts +2 -0
  67. package/dist/C_Date.d.ts +2 -0
  68. package/dist/C_Date.js +3 -0
  69. package/dist/C_Date2.js +219 -0
  70. package/dist/C_Date2.js.map +1 -0
  71. package/dist/C_Draggable-C483syRC.css.map +1 -0
  72. package/dist/C_Draggable.cjs +5 -0
  73. package/dist/C_Draggable.d.cts +2 -0
  74. package/dist/C_Draggable.d.ts +2 -0
  75. package/dist/C_Draggable.js +3 -0
  76. package/dist/C_Draggable2.js +295 -0
  77. package/dist/C_Draggable2.js.map +1 -0
  78. package/dist/C_Editor-Bp0SyIEw.css.map +1 -0
  79. package/dist/C_Editor.cjs +4 -0
  80. package/dist/C_Editor.d.cts +2 -0
  81. package/dist/C_Editor.d.ts +2 -0
  82. package/dist/C_Editor.js +3 -0
  83. package/dist/C_Editor2.js +160 -0
  84. package/dist/C_Editor2.js.map +1 -0
  85. package/dist/C_FilePreview-CPqvhoCy.css.map +1 -0
  86. package/dist/C_FilePreview.cjs +6 -0
  87. package/dist/C_FilePreview.d.cts +2 -0
  88. package/dist/C_FilePreview.d.ts +2 -0
  89. package/dist/C_FilePreview.js +3 -0
  90. package/dist/C_FilePreview2.js +1031 -0
  91. package/dist/C_FilePreview2.js.map +1 -0
  92. package/dist/C_Form-Jx7PY3sT.css.map +1 -0
  93. package/dist/C_Form.cjs +15 -0
  94. package/dist/C_Form.d.cts +2 -0
  95. package/dist/C_Form.d.ts +2 -0
  96. package/dist/C_Form.js +4 -0
  97. package/dist/C_Form2.js +2510 -0
  98. package/dist/C_Form2.js.map +1 -0
  99. package/dist/C_FormSearch-DvRgxlRn.css.map +1 -0
  100. package/dist/C_FormSearch.cjs +6 -0
  101. package/dist/C_FormSearch.d.cts +2 -0
  102. package/dist/C_FormSearch.d.ts +2 -0
  103. package/dist/C_FormSearch.js +3 -0
  104. package/dist/C_FormSearch2.js +356 -0
  105. package/dist/C_FormSearch2.js.map +1 -0
  106. package/dist/C_FormulaEditor-DtGkt4T_.css.map +1 -0
  107. package/dist/C_FormulaEditor.cjs +13 -0
  108. package/dist/C_FormulaEditor.d.cts +2 -0
  109. package/dist/C_FormulaEditor.d.ts +2 -0
  110. package/dist/C_FormulaEditor.js +4 -0
  111. package/dist/C_FormulaEditor2.js +1433 -0
  112. package/dist/C_FormulaEditor2.js.map +1 -0
  113. package/dist/C_FullCalendar-BF7H0YIx.css.map +1 -0
  114. package/dist/C_FullCalendar.cjs +9 -0
  115. package/dist/C_FullCalendar.d.cts +2 -0
  116. package/dist/C_FullCalendar.d.ts +2 -0
  117. package/dist/C_FullCalendar.js +3 -0
  118. package/dist/C_FullCalendar2.js +377 -0
  119. package/dist/C_FullCalendar2.js.map +1 -0
  120. package/dist/C_Guide.cjs +4 -0
  121. package/dist/C_Guide.d.cts +2 -0
  122. package/dist/C_Guide.d.ts +2 -0
  123. package/dist/C_Guide.js +3 -0
  124. package/dist/C_Guide2.js +58 -0
  125. package/dist/C_Guide2.js.map +1 -0
  126. package/dist/C_Icon.cjs +4 -0
  127. package/dist/C_Icon.d.cts +2 -0
  128. package/dist/C_Icon.d.ts +2 -0
  129. package/dist/C_Icon.js +3 -0
  130. package/dist/C_Icon2.js +286 -0
  131. package/dist/C_Icon2.js.map +1 -0
  132. package/dist/C_ImageCropper-BVJfUufl.css.map +1 -0
  133. package/dist/C_ImageCropper.cjs +6 -0
  134. package/dist/C_ImageCropper.d.cts +2 -0
  135. package/dist/C_ImageCropper.d.ts +2 -0
  136. package/dist/C_ImageCropper.js +4 -0
  137. package/dist/C_ImageCropper2.js +723 -0
  138. package/dist/C_ImageCropper2.js.map +1 -0
  139. package/dist/C_Language.cjs +4 -0
  140. package/dist/C_Language.d.cts +2 -0
  141. package/dist/C_Language.d.ts +2 -0
  142. package/dist/C_Language.js +3 -0
  143. package/dist/C_Language2.js +72 -0
  144. package/dist/C_Language2.js.map +1 -0
  145. package/dist/C_Map-DpzeuWdX.css.map +1 -0
  146. package/dist/C_Map.cjs +7 -0
  147. package/dist/C_Map.d.cts +2 -0
  148. package/dist/C_Map.d.ts +2 -0
  149. package/dist/C_Map.js +3 -0
  150. package/dist/C_Map2.js +199 -0
  151. package/dist/C_Map2.js.map +1 -0
  152. package/dist/C_Markdown-BEjxknqd.css.map +1 -0
  153. package/dist/C_Markdown.cjs +4 -0
  154. package/dist/C_Markdown.d.cts +2 -0
  155. package/dist/C_Markdown.d.ts +2 -0
  156. package/dist/C_Markdown.js +3 -0
  157. package/dist/C_Markdown2.js +186 -0
  158. package/dist/C_Markdown2.js.map +1 -0
  159. package/dist/C_NotificationCenter-0l3TY2Gn.css.map +1 -0
  160. package/dist/C_NotificationCenter.cjs +20 -0
  161. package/dist/C_NotificationCenter.d.cts +2 -0
  162. package/dist/C_NotificationCenter.d.ts +2 -0
  163. package/dist/C_NotificationCenter.js +4 -0
  164. package/dist/C_NotificationCenter2.js +1383 -0
  165. package/dist/C_NotificationCenter2.js.map +1 -0
  166. package/dist/C_Progress.cjs +4 -0
  167. package/dist/C_Progress.d.cts +2 -0
  168. package/dist/C_Progress.d.ts +2 -0
  169. package/dist/C_Progress.js +3 -0
  170. package/dist/C_Progress2.js +103 -0
  171. package/dist/C_Progress2.js.map +1 -0
  172. package/dist/C_QRCode-DbdiAIPg.css.map +1 -0
  173. package/dist/C_QRCode.cjs +5 -0
  174. package/dist/C_QRCode.d.cts +2 -0
  175. package/dist/C_QRCode.d.ts +2 -0
  176. package/dist/C_QRCode.js +3 -0
  177. package/dist/C_QRCode2.js +218 -0
  178. package/dist/C_QRCode2.js.map +1 -0
  179. package/dist/C_Signature-zhHCbra9.css.map +1 -0
  180. package/dist/C_Signature.cjs +8 -0
  181. package/dist/C_Signature.d.cts +2 -0
  182. package/dist/C_Signature.d.ts +2 -0
  183. package/dist/C_Signature.js +4 -0
  184. package/dist/C_Signature2.js +618 -0
  185. package/dist/C_Signature2.js.map +1 -0
  186. package/dist/C_SplitPane-C6sBsfKY.css.map +1 -0
  187. package/dist/C_SplitPane.cjs +6 -0
  188. package/dist/C_SplitPane.d.cts +2 -0
  189. package/dist/C_SplitPane.d.ts +2 -0
  190. package/dist/C_SplitPane.js +4 -0
  191. package/dist/C_SplitPane2.js +356 -0
  192. package/dist/C_SplitPane2.js.map +1 -0
  193. package/dist/C_Steps-CODHN5Hs.css.map +1 -0
  194. package/dist/C_Steps.cjs +4 -0
  195. package/dist/C_Steps.d.cts +2 -0
  196. package/dist/C_Steps.d.ts +2 -0
  197. package/dist/C_Steps.js +3 -0
  198. package/dist/C_Steps2.js +82 -0
  199. package/dist/C_Steps2.js.map +1 -0
  200. package/dist/C_Table-DSNsntmT.css.map +1 -0
  201. package/dist/C_Table.cjs +19 -0
  202. package/dist/C_Table.d.cts +2 -0
  203. package/dist/C_Table.d.ts +2 -0
  204. package/dist/C_Table.js +5 -0
  205. package/dist/C_Table2.js +3009 -0
  206. package/dist/C_Table2.js.map +1 -0
  207. package/dist/C_Theme.cjs +4 -0
  208. package/dist/C_Theme.d.cts +2 -0
  209. package/dist/C_Theme.d.ts +2 -0
  210. package/dist/C_Theme.js +3 -0
  211. package/dist/C_Theme2.js +60 -0
  212. package/dist/C_Theme2.js.map +1 -0
  213. package/dist/C_Time-BvZLYraL.css.map +1 -0
  214. package/dist/C_Time.cjs +5 -0
  215. package/dist/C_Time.d.cts +2 -0
  216. package/dist/C_Time.d.ts +2 -0
  217. package/dist/C_Time.js +3 -0
  218. package/dist/C_Time2.js +199 -0
  219. package/dist/C_Time2.js.map +1 -0
  220. package/dist/C_Tree-0GDv--jX.css.map +1 -0
  221. package/dist/C_Tree.cjs +7 -0
  222. package/dist/C_Tree.d.cts +2 -0
  223. package/dist/C_Tree.d.ts +2 -0
  224. package/dist/C_Tree.js +4 -0
  225. package/dist/C_Tree2.js +441 -0
  226. package/dist/C_Tree2.js.map +1 -0
  227. package/dist/C_Upload-BXd3YYLx.css.map +1 -0
  228. package/dist/C_Upload.cjs +12 -0
  229. package/dist/C_Upload.d.cts +2 -0
  230. package/dist/C_Upload.d.ts +2 -0
  231. package/dist/C_Upload.js +4 -0
  232. package/dist/C_Upload2.js +1388 -0
  233. package/dist/C_Upload2.js.map +1 -0
  234. package/dist/C_VideoPlayer-DYG3RL0Q.css.map +1 -0
  235. package/dist/C_VideoPlayer.cjs +23 -0
  236. package/dist/C_VideoPlayer.d.cts +2 -0
  237. package/dist/C_VideoPlayer.d.ts +2 -0
  238. package/dist/C_VideoPlayer.js +3 -0
  239. package/dist/C_VideoPlayer2.js +1932 -0
  240. package/dist/C_VideoPlayer2.js.map +1 -0
  241. package/dist/C_VtableGantt-fhItIiHE.css.map +1 -0
  242. package/dist/C_VtableGantt.cjs +6 -0
  243. package/dist/C_VtableGantt.d.cts +2 -0
  244. package/dist/C_VtableGantt.d.ts +2 -0
  245. package/dist/C_VtableGantt.js +4 -0
  246. package/dist/C_VtableGantt2.js +873 -0
  247. package/dist/C_VtableGantt2.js.map +1 -0
  248. package/dist/C_WaterFall-8sQDFXKg.css.map +1 -0
  249. package/dist/C_WaterFall.cjs +13 -0
  250. package/dist/C_WaterFall.d.cts +2 -0
  251. package/dist/C_WaterFall.d.ts +2 -0
  252. package/dist/C_WaterFall.js +3 -0
  253. package/dist/C_WaterFall2.js +365 -0
  254. package/dist/C_WaterFall2.js.map +1 -0
  255. package/dist/C_WorkFlow-J-dyIuh9.css.map +1 -0
  256. package/dist/C_WorkFlow.cjs +8 -0
  257. package/dist/C_WorkFlow.d.cts +2 -0
  258. package/dist/C_WorkFlow.d.ts +2 -0
  259. package/dist/C_WorkFlow.js +4 -0
  260. package/dist/C_WorkFlow2.js +1984 -0
  261. package/dist/C_WorkFlow2.js.map +1 -0
  262. package/dist/chunk.js +22 -0
  263. package/dist/city.js +4817 -0
  264. package/dist/city.js.map +1 -0
  265. package/dist/constants.d.ts +273 -0
  266. package/dist/constants.d.ts.map +1 -0
  267. package/dist/constants2.d.ts +178 -0
  268. package/dist/constants2.d.ts.map +1 -0
  269. package/dist/constants3.d.ts +475 -0
  270. package/dist/constants3.d.ts.map +1 -0
  271. package/dist/constants4.d.ts +430 -0
  272. package/dist/constants4.d.ts.map +1 -0
  273. package/dist/constants5.d.ts +4283 -0
  274. package/dist/constants5.d.ts.map +1 -0
  275. package/dist/data.d.ts +67 -0
  276. package/dist/data.d.ts.map +1 -0
  277. package/dist/export-helper.js +9 -0
  278. package/dist/index.cjs +409 -0
  279. package/dist/index.d.cts +96 -0
  280. package/dist/index.d.cts.map +1 -0
  281. package/dist/index.d.ts +103 -0
  282. package/dist/index.d.ts.map +1 -0
  283. package/dist/index.js +230 -0
  284. package/dist/index.js.map +1 -0
  285. package/dist/index.vue.d.ts +80 -0
  286. package/dist/index.vue.d.ts.map +1 -0
  287. package/dist/index10.vue.d.ts +72 -0
  288. package/dist/index10.vue.d.ts.map +1 -0
  289. package/dist/index11.vue.d.ts +26 -0
  290. package/dist/index11.vue.d.ts.map +1 -0
  291. package/dist/index12.vue.d.ts +81 -0
  292. package/dist/index12.vue.d.ts.map +1 -0
  293. package/dist/index13.vue.d.ts +55 -0
  294. package/dist/index13.vue.d.ts.map +1 -0
  295. package/dist/index14.vue.d.ts +33 -0
  296. package/dist/index14.vue.d.ts.map +1 -0
  297. package/dist/index15.vue.d.ts +18 -0
  298. package/dist/index15.vue.d.ts.map +1 -0
  299. package/dist/index16.vue.d.ts +662 -0
  300. package/dist/index16.vue.d.ts.map +1 -0
  301. package/dist/index2.vue.d.ts +38 -0
  302. package/dist/index2.vue.d.ts.map +1 -0
  303. package/dist/index3.vue.d.ts +45 -0
  304. package/dist/index3.vue.d.ts.map +1 -0
  305. package/dist/index4.vue.d.ts +31 -0
  306. package/dist/index4.vue.d.ts.map +1 -0
  307. package/dist/index5.vue.d.ts +35 -0
  308. package/dist/index5.vue.d.ts.map +1 -0
  309. package/dist/index6.vue.d.ts +48 -0
  310. package/dist/index6.vue.d.ts.map +1 -0
  311. package/dist/index7.vue.d.ts +56 -0
  312. package/dist/index7.vue.d.ts.map +1 -0
  313. package/dist/index8.vue.d.ts +41 -0
  314. package/dist/index8.vue.d.ts.map +1 -0
  315. package/dist/index9.vue.d.ts +30 -0
  316. package/dist/index9.vue.d.ts.map +1 -0
  317. package/dist/storage.js +31 -0
  318. package/dist/storage.js.map +1 -0
  319. package/dist/style.css +7725 -0
  320. package/dist/useCalendarEvents.d.ts +148 -0
  321. package/dist/useCalendarEvents.d.ts.map +1 -0
  322. package/dist/useCollapsePanel.d.ts +132 -0
  323. package/dist/useCollapsePanel.d.ts.map +1 -0
  324. package/dist/useCropperCore.d.ts +102 -0
  325. package/dist/useCropperCore.d.ts.map +1 -0
  326. package/dist/useDraggableLayout.d.ts +194 -0
  327. package/dist/useDraggableLayout.d.ts.map +1 -0
  328. package/dist/useDynamicFormState.d.ts +4248 -0
  329. package/dist/useDynamicFormState.d.ts.map +1 -0
  330. package/dist/useEdgeInteraction.d.ts +7614 -0
  331. package/dist/useEdgeInteraction.d.ts.map +1 -0
  332. package/dist/useFullscreen.d.ts +166 -0
  333. package/dist/useFullscreen.d.ts.map +1 -0
  334. package/dist/useInfiniteScroll.d.ts +169 -0
  335. package/dist/useInfiniteScroll.d.ts.map +1 -0
  336. package/dist/useModalEdit.d.ts +960 -0
  337. package/dist/useModalEdit.d.ts.map +1 -0
  338. package/dist/useQRCode.d.ts +87 -0
  339. package/dist/useQRCode.d.ts.map +1 -0
  340. package/dist/useSearchState.d.ts +180 -0
  341. package/dist/useSearchState.d.ts.map +1 -0
  342. package/dist/useSignatureHistory.d.ts +189 -0
  343. package/dist/useSignatureHistory.d.ts.map +1 -0
  344. package/dist/useSplitResize.d.ts +158 -0
  345. package/dist/useSplitResize.d.ts.map +1 -0
  346. package/dist/useTimeSelection.d.ts +105 -0
  347. package/dist/useTimeSelection.d.ts.map +1 -0
  348. package/dist/useTreeOperations.d.ts +183 -0
  349. package/dist/useTreeOperations.d.ts.map +1 -0
  350. package/dist/useWorkflowValidation.d.ts +1052 -0
  351. package/dist/useWorkflowValidation.d.ts.map +1 -0
  352. package/package.json +342 -0
@@ -0,0 +1,1932 @@
1
+ import { t as export_helper_default } from "./export-helper.js";
2
+ import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveComponent, shallowRef, toDisplayString, toRef, unref, useModel, watch, withCtx } from "vue";
3
+ import { Events } from "xgplayer";
4
+ import "xgplayer/dist/index.min.css";
5
+
6
+ //#region src/components/C_VideoPlayer/constants.ts
7
+ /** 默认播放倍速列表 */
8
+ const DEFAULT_PLAYBACK_RATES = [
9
+ .5,
10
+ .75,
11
+ 1,
12
+ 1.25,
13
+ 1.5,
14
+ 2,
15
+ 3
16
+ ];
17
+ /** 默认播放倍速 */
18
+ const DEFAULT_PLAYBACK_RATE = 1;
19
+ /** 默认音量 */
20
+ const DEFAULT_VOLUME = .75;
21
+ /** 心跳上报间隔(毫秒) */
22
+ const DEFAULT_HEARTBEAT_INTERVAL = 3e4;
23
+ /** 进度上报节流间隔(毫秒) */
24
+ const PROGRESS_THROTTLE_INTERVAL = 5e3;
25
+ /** 清晰度标签映射 */
26
+ const QUALITY_LABEL_MAP = {
27
+ "360p": "流畅 360P",
28
+ "480p": "标清 480P",
29
+ "720p": "高清 720P",
30
+ "1080p": "超清 1080P",
31
+ "1440p": "2K 1440P",
32
+ "4K": "4K 超高清"
33
+ };
34
+ /** 视频源文件扩展名与类型映射 */
35
+ const SOURCE_TYPE_MAP = {
36
+ ".mp4": "mp4",
37
+ ".m3u8": "hls",
38
+ ".mpd": "dash",
39
+ ".flv": "flv"
40
+ };
41
+ /** 快捷键映射描述 */
42
+ const KEYBOARD_SHORTCUTS = {
43
+ SPACE: "播放/暂停",
44
+ ARROW_LEFT: "快退 5 秒",
45
+ ARROW_RIGHT: "快进 5 秒",
46
+ ARROW_UP: "音量增加 10%",
47
+ ARROW_DOWN: "音量减少 10%",
48
+ F: "全屏/退出全屏",
49
+ M: "静音/取消静音",
50
+ ESC: "退出全屏"
51
+ };
52
+ /** 快进/快退步长(秒) */
53
+ const SEEK_STEP = 5;
54
+ /** 音量调节步长 */
55
+ const VOLUME_STEP = .1;
56
+ /** localStorage 存储键前缀 */
57
+ const STORAGE_PREFIX = "c_video_player_";
58
+ /** 存储键 */
59
+ const STORAGE_KEYS = {
60
+ VOLUME: `${STORAGE_PREFIX}volume`,
61
+ PLAYBACK_RATE: `${STORAGE_PREFIX}playback_rate`,
62
+ PROGRESS: `${STORAGE_PREFIX}progress_`,
63
+ BOOKMARKS: `${STORAGE_PREFIX}bookmarks_`
64
+ };
65
+ /** 水印默认样式 */
66
+ const WATERMARK_DEFAULT_STYLE = {
67
+ fontSize: 14,
68
+ color: "rgba(150, 150, 150, 0.15)",
69
+ rotate: -25,
70
+ gap: 120
71
+ };
72
+
73
+ //#endregion
74
+ //#region src/components/C_VideoPlayer/composables/usePlayerCore.ts
75
+ /** 根据 URL 推断视频源类型 */
76
+ function detectSourceType(url) {
77
+ try {
78
+ const { pathname } = new URL(url, location.href);
79
+ return SOURCE_TYPE_MAP[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "mp4";
80
+ } catch {
81
+ return "mp4";
82
+ }
83
+ }
84
+ /**
85
+ * 播放器核心 composable
86
+ * - 管理 xgplayer 实例的创建 & 销毁
87
+ * - 暴露播放器状态、引用
88
+ */
89
+ function usePlayerCore(props) {
90
+ /** 播放器 DOM 容器 */
91
+ const containerRef = ref(null);
92
+ /** xgplayer 实例(使用 shallowRef 避免深度响应) */
93
+ const playerRef = shallowRef(null);
94
+ /** 播放器当前状态 */
95
+ const playerState = ref("idle");
96
+ /** 当前播放时间 */
97
+ const currentTime = ref(0);
98
+ /** 视频总时长 */
99
+ const duration = ref(0);
100
+ /** 是否全屏 */
101
+ const isFullscreen = ref(false);
102
+ /** 构建播放器核心容器配置 */
103
+ function buildCoreConfig() {
104
+ return {
105
+ el: containerRef.value,
106
+ url: props.url,
107
+ width: props.width ?? "100%",
108
+ height: props.height ?? "100%",
109
+ fluid: props.fluid !== false,
110
+ fitVideoSize: "fixWidth",
111
+ poster: props.poster ?? "",
112
+ playsinline: true,
113
+ videoAttributes: { crossOrigin: "anonymous" },
114
+ lang: props.lang ?? "zh-cn",
115
+ inactive: 3e3
116
+ };
117
+ }
118
+ /** 构建播放行为配置 */
119
+ function buildPlaybackConfig() {
120
+ return {
121
+ autoplay: props.autoplay ?? false,
122
+ autoplayMuted: props.autoplayMuted ?? false,
123
+ loop: props.loop ?? false,
124
+ volume: props.volume ?? DEFAULT_VOLUME,
125
+ startTime: props.startTime ?? 0,
126
+ defaultPlaybackRate: props.defaultPlaybackRate ?? 1,
127
+ playbackRate: props.playbackRates ? { list: props.playbackRates.map((r) => r) } : true
128
+ };
129
+ }
130
+ /** 构建功能开关配置 */
131
+ function buildFeatureConfig() {
132
+ return {
133
+ pip: props.pip !== false,
134
+ screenShot: props.screenshot ?? false,
135
+ mini: props.miniPlayer ?? false,
136
+ fullscreen: props.fullscreen !== false,
137
+ cssFullscreen: props.cssFullscreen !== false,
138
+ keyShortcut: props.keyboard !== false
139
+ };
140
+ }
141
+ /** 构建扩展配置(缩略图、清晰度等) */
142
+ function applyExtendedConfig(config) {
143
+ if (props.thumbnail) config.thumbnail = {
144
+ urls: props.thumbnail.urls,
145
+ pic_num: props.thumbnail.picNum,
146
+ col: props.thumbnail.col,
147
+ row: props.thumbnail.row,
148
+ width: props.thumbnail.width,
149
+ height: props.thumbnail.height
150
+ };
151
+ if (props.qualityList?.length) config.definition = {
152
+ list: props.qualityList.map((q) => ({
153
+ url: q.url,
154
+ definition: q.label,
155
+ text: {
156
+ zh: q.label,
157
+ en: q.label
158
+ },
159
+ bitrate: q.bitrate
160
+ })),
161
+ defaultDefinition: props.defaultQuality ?? props.qualityList[0].label
162
+ };
163
+ if (props.playerOptions) Object.assign(config, props.playerOptions);
164
+ }
165
+ /** 构建完整 xgplayer 配置 */
166
+ function buildConfig() {
167
+ const sourceType = props.sourceType ?? detectSourceType(props.url);
168
+ const config = {
169
+ ...buildCoreConfig(),
170
+ ...buildPlaybackConfig(),
171
+ ...buildFeatureConfig()
172
+ };
173
+ applyExtendedConfig(config);
174
+ config.__sourceType = sourceType;
175
+ return config;
176
+ }
177
+ /** 初始化播放器 */
178
+ async function initPlayer() {
179
+ if (!containerRef.value) return;
180
+ playerState.value = "loading";
181
+ const config = buildConfig();
182
+ const sourceType = config.__sourceType;
183
+ delete config.__sourceType;
184
+ let PlayerConstructor;
185
+ if (sourceType === "hls") {
186
+ const { default: HlsPlayer } = await import("xgplayer-hls");
187
+ PlayerConstructor = HlsPlayer;
188
+ } else {
189
+ const { default: PresetPlayer } = await import("xgplayer");
190
+ PlayerConstructor = PresetPlayer;
191
+ }
192
+ const player = new PlayerConstructor(config);
193
+ playerRef.value = player;
194
+ player.on(Events.READY, () => {
195
+ playerState.value = "ready";
196
+ });
197
+ player.on(Events.PLAY, () => {
198
+ playerState.value = "playing";
199
+ });
200
+ player.on(Events.PAUSE, () => {
201
+ playerState.value = "paused";
202
+ });
203
+ player.on(Events.ENDED, () => {
204
+ playerState.value = "ended";
205
+ });
206
+ player.on(Events.ERROR, () => {
207
+ playerState.value = "error";
208
+ });
209
+ player.on(Events.TIME_UPDATE, () => {
210
+ currentTime.value = player.currentTime ?? 0;
211
+ duration.value = player.duration ?? 0;
212
+ });
213
+ player.on(Events.DURATION_CHANGE, () => {
214
+ duration.value = player.duration ?? 0;
215
+ });
216
+ player.on(Events.FULLSCREEN_CHANGE, (isFS) => {
217
+ isFullscreen.value = isFS;
218
+ });
219
+ }
220
+ /** 销毁播放器 */
221
+ function destroyPlayer() {
222
+ const player = playerRef.value;
223
+ if (player) {
224
+ player.destroy();
225
+ playerRef.value = null;
226
+ }
227
+ playerState.value = "idle";
228
+ currentTime.value = 0;
229
+ duration.value = 0;
230
+ }
231
+ onBeforeUnmount(() => {
232
+ destroyPlayer();
233
+ });
234
+ return {
235
+ containerRef,
236
+ playerRef,
237
+ playerState,
238
+ currentTime,
239
+ duration,
240
+ isFullscreen,
241
+ initPlayer,
242
+ destroyPlayer
243
+ };
244
+ }
245
+
246
+ //#endregion
247
+ //#region src/components/C_VideoPlayer/composables/usePlaybackControl.ts
248
+ /**
249
+ * 播放控制 composable
250
+ * - 播放 / 暂停 / 跳转
251
+ * - 音量调节(本地持久化)
252
+ * - 倍速调节(本地持久化)
253
+ */
254
+ function usePlaybackControl(playerRef) {
255
+ const volume = ref(_getStoredVolume());
256
+ const playbackRate = ref(_getStoredRate());
257
+ const isMuted = ref(false);
258
+ /** 播放 */
259
+ function play() {
260
+ playerRef.value?.play();
261
+ }
262
+ /** 暂停 */
263
+ function pause() {
264
+ playerRef.value?.pause();
265
+ }
266
+ /** 切换播放/暂停 */
267
+ function togglePlay() {
268
+ const player = playerRef.value;
269
+ if (!player) return;
270
+ if (player.paused) player.play();
271
+ else player.pause();
272
+ }
273
+ /** 跳转到指定时间(秒) */
274
+ function seek(time) {
275
+ const player = playerRef.value;
276
+ if (!player) return;
277
+ player.currentTime = Math.max(0, Math.min(time, player.duration ?? 0));
278
+ }
279
+ /** 设置音量 0-1 */
280
+ function setVolume(val) {
281
+ const player = playerRef.value;
282
+ if (!player) return;
283
+ const safeVolume = Math.max(0, Math.min(1, val));
284
+ player.volume = safeVolume;
285
+ volume.value = safeVolume;
286
+ isMuted.value = safeVolume === 0;
287
+ localStorage.setItem(STORAGE_KEYS.VOLUME, String(safeVolume));
288
+ }
289
+ /** 切换静音 */
290
+ function toggleMute() {
291
+ const player = playerRef.value;
292
+ if (!player) return;
293
+ if (isMuted.value) setVolume(volume.value > 0 ? volume.value : .5);
294
+ else {
295
+ player.volume = 0;
296
+ isMuted.value = true;
297
+ }
298
+ }
299
+ /** 设置倍速 */
300
+ function setPlaybackRate(rate) {
301
+ const player = playerRef.value;
302
+ if (!player) return;
303
+ player.playbackRate = rate;
304
+ playbackRate.value = rate;
305
+ localStorage.setItem(STORAGE_KEYS.PLAYBACK_RATE, String(rate));
306
+ }
307
+ /** 同步播放器实例的初始状态 */
308
+ function syncInitialState() {
309
+ const player = playerRef.value;
310
+ if (!player) return;
311
+ player.volume = volume.value;
312
+ player.playbackRate = playbackRate.value;
313
+ isMuted.value = volume.value === 0;
314
+ }
315
+ /** 监听播放器实例变化,自动同步状态 */
316
+ watch(playerRef, (player) => {
317
+ if (player) syncInitialState();
318
+ });
319
+ return {
320
+ volume,
321
+ playbackRate,
322
+ isMuted,
323
+ play,
324
+ pause,
325
+ togglePlay,
326
+ seek,
327
+ setVolume,
328
+ toggleMute,
329
+ setPlaybackRate
330
+ };
331
+ }
332
+ /** 从 localStorage 读取已存储的音量 */
333
+ function _getStoredVolume() {
334
+ const stored = localStorage.getItem(STORAGE_KEYS.VOLUME);
335
+ if (stored !== null) {
336
+ const val = parseFloat(stored);
337
+ if (!isNaN(val) && val >= 0 && val <= 1) return val;
338
+ }
339
+ return .75;
340
+ }
341
+ /** 从 localStorage 读取已存储的倍速 */
342
+ function _getStoredRate() {
343
+ const stored = localStorage.getItem(STORAGE_KEYS.PLAYBACK_RATE);
344
+ if (stored !== null) {
345
+ const val = parseFloat(stored);
346
+ if (!isNaN(val) && val > 0) return val;
347
+ }
348
+ return 1;
349
+ }
350
+
351
+ //#endregion
352
+ //#region src/components/C_VideoPlayer/composables/useProgressTracker.ts
353
+ /**
354
+ * 学习进度追踪 composable
355
+ * - 精确记录已观看时长(排除暂停和拖动时间)
356
+ * - 节流上报进度
357
+ * - 心跳上报
358
+ * - 页面关闭时使用 sendBeacon 兜底上报
359
+ * - 断点续播 localStorage 存储
360
+ */
361
+ function useProgressTracker(playerRef, currentTime, duration, url, onProgress, antiCheat) {
362
+ /** 累计实际观看时长(秒) */
363
+ const watchedDuration = ref(0);
364
+ /** 完成百分比 */
365
+ const completionPercent = ref(0);
366
+ /** 上次记录时间 */
367
+ let lastRecordTime = 0;
368
+ /** 是否正在播放 */
369
+ let isPlaying = false;
370
+ /** 心跳定时器 */
371
+ let heartbeatTimer = null;
372
+ /** 进度节流定时器 */
373
+ let throttleTimer = null;
374
+ /** 视频标识(用于 localStorage key) */
375
+ function getVideoKey() {
376
+ return STORAGE_KEYS.PROGRESS + encodeURIComponent(url.value);
377
+ }
378
+ /** 恢复进度 */
379
+ function restoreProgress() {
380
+ try {
381
+ const stored = localStorage.getItem(getVideoKey());
382
+ if (stored) {
383
+ const data = JSON.parse(stored);
384
+ watchedDuration.value = data.watchedDuration ?? 0;
385
+ completionPercent.value = data.completionPercent ?? 0;
386
+ return data.currentTime ?? 0;
387
+ }
388
+ } catch {}
389
+ return 0;
390
+ }
391
+ /** 获取当前进度数据 */
392
+ function getProgressData() {
393
+ return {
394
+ currentTime: currentTime.value,
395
+ duration: duration.value,
396
+ watchedDuration: watchedDuration.value,
397
+ completionPercent: completionPercent.value,
398
+ updatedAt: Date.now()
399
+ };
400
+ }
401
+ /** 保存进度到 localStorage */
402
+ function saveProgress() {
403
+ try {
404
+ localStorage.setItem(getVideoKey(), JSON.stringify(getProgressData()));
405
+ } catch {}
406
+ }
407
+ /** 上报进度(节流) */
408
+ function reportProgress() {
409
+ const data = getProgressData();
410
+ onProgress?.(data);
411
+ saveProgress();
412
+ }
413
+ /** 计算观看时长 */
414
+ function updateWatchedDuration() {
415
+ if (!isPlaying) return;
416
+ const now = performance.now();
417
+ if (lastRecordTime > 0) {
418
+ const delta = (now - lastRecordTime) / 1e3;
419
+ if (delta > 0 && delta < 5) watchedDuration.value += delta;
420
+ }
421
+ lastRecordTime = now;
422
+ if (duration.value > 0) completionPercent.value = Math.min(100, Math.round(watchedDuration.value / duration.value * 100));
423
+ }
424
+ /** 心跳上报 */
425
+ function startHeartbeat() {
426
+ const interval = antiCheat?.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
427
+ heartbeatTimer = setInterval(() => {
428
+ if (isPlaying) antiCheat?.onHeartbeat?.(getProgressData());
429
+ }, interval);
430
+ }
431
+ /** 启动节流上报 */
432
+ function startThrottleReport() {
433
+ throttleTimer = setInterval(() => {
434
+ if (isPlaying) reportProgress();
435
+ }, PROGRESS_THROTTLE_INTERVAL);
436
+ }
437
+ /** 页面关闭兜底上报 */
438
+ function handleBeforeUnload() {
439
+ const data = getProgressData();
440
+ saveProgress();
441
+ if (onProgress) try {
442
+ navigator.sendBeacon?.("/api/video/progress", JSON.stringify(data));
443
+ } catch {}
444
+ }
445
+ /** 页面可见性变化处理 */
446
+ function handleVisibilityChange() {
447
+ if (document.hidden) {
448
+ updateWatchedDuration();
449
+ reportProgress();
450
+ isPlaying = false;
451
+ } else {
452
+ const player = playerRef.value;
453
+ if (player && !player.paused) {
454
+ isPlaying = true;
455
+ lastRecordTime = performance.now();
456
+ }
457
+ }
458
+ }
459
+ /** 初始化追踪 */
460
+ function startTracking() {
461
+ startHeartbeat();
462
+ startThrottleReport();
463
+ window.addEventListener("beforeunload", handleBeforeUnload);
464
+ document.addEventListener("visibilitychange", handleVisibilityChange);
465
+ }
466
+ /** 停止追踪 */
467
+ function stopTracking() {
468
+ isPlaying = false;
469
+ if (heartbeatTimer) {
470
+ clearInterval(heartbeatTimer);
471
+ heartbeatTimer = null;
472
+ }
473
+ if (throttleTimer) {
474
+ clearInterval(throttleTimer);
475
+ throttleTimer = null;
476
+ }
477
+ window.removeEventListener("beforeunload", handleBeforeUnload);
478
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
479
+ saveProgress();
480
+ }
481
+ /** 标记开始播放 */
482
+ function onPlay() {
483
+ isPlaying = true;
484
+ lastRecordTime = performance.now();
485
+ }
486
+ /** 标记暂停 */
487
+ function onPause() {
488
+ updateWatchedDuration();
489
+ isPlaying = false;
490
+ lastRecordTime = 0;
491
+ saveProgress();
492
+ }
493
+ /** timeupdate 回调 */
494
+ function onTimeUpdate() {
495
+ updateWatchedDuration();
496
+ }
497
+ /** 监听播放器就绪后恢复进度 */
498
+ watch(playerRef, (player) => {
499
+ if (player) startTracking();
500
+ });
501
+ onBeforeUnmount(() => {
502
+ stopTracking();
503
+ });
504
+ return {
505
+ watchedDuration,
506
+ completionPercent,
507
+ getProgressData,
508
+ restoreProgress,
509
+ saveProgress,
510
+ startTracking,
511
+ stopTracking,
512
+ onPlay,
513
+ onPause,
514
+ onTimeUpdate
515
+ };
516
+ }
517
+
518
+ //#endregion
519
+ //#region src/components/C_VideoPlayer/composables/useQualitySwitch.ts
520
+ /**
521
+ * 清晰度切换 composable
522
+ * - 监听 xgplayer 原生清晰度切换事件
523
+ * - 提供编程式清晰度切换
524
+ */
525
+ function useQualitySwitch(playerRef, qualityList = []) {
526
+ const currentQuality = ref(null);
527
+ const isSwitching = ref(false);
528
+ /** 初始化:监听清晰度变化事件 */
529
+ function bindEvents(player) {
530
+ player.on(Events.AFTER_DEFINITION_CHANGE, (data) => {
531
+ currentQuality.value = data.to;
532
+ isSwitching.value = false;
533
+ });
534
+ player.on(Events.BEFORE_DEFINITION_CHANGE, () => {
535
+ isSwitching.value = true;
536
+ });
537
+ }
538
+ /** 编程式切换清晰度 */
539
+ function switchQuality(quality) {
540
+ const player = playerRef.value;
541
+ if (!player || !qualityList.length) return;
542
+ const target = qualityList.find((q) => q.label === quality);
543
+ if (!target) {
544
+ console.warn(`[C_VideoPlayer] 未找到清晰度: ${quality}`);
545
+ return;
546
+ }
547
+ isSwitching.value = true;
548
+ player.changeDefinition?.({
549
+ url: target.url,
550
+ definition: target.label,
551
+ text: {
552
+ zh: target.label,
553
+ en: target.label
554
+ },
555
+ bitrate: target.bitrate
556
+ });
557
+ }
558
+ return {
559
+ currentQuality,
560
+ isSwitching,
561
+ switchQuality,
562
+ bindEvents
563
+ };
564
+ }
565
+
566
+ //#endregion
567
+ //#region src/components/C_VideoPlayer/composables/useChapters.ts
568
+ /**
569
+ * 章节标记 composable
570
+ * - 根据当前播放时间计算所在章节
571
+ * - 提供跳转到指定章节的能力
572
+ * - 计算进度条上章节标记的位置
573
+ */
574
+ function useChapters(chapters, currentTime, duration, seekFn) {
575
+ /** 当前所在章节 */
576
+ const currentChapter = computed(() => {
577
+ if (!chapters.value.length) return null;
578
+ const time = currentTime.value;
579
+ return chapters.value.find((ch) => time >= ch.startTime && time < ch.endTime) ?? null;
580
+ });
581
+ /** 当前章节索引 */
582
+ const currentChapterIndex = computed(() => {
583
+ if (!currentChapter.value) return -1;
584
+ return chapters.value.findIndex((ch) => ch.id === currentChapter.value.id);
585
+ });
586
+ /** 章节在进度条上的位置百分比 */
587
+ const chapterMarkers = computed(() => {
588
+ if (!duration.value || !chapters.value.length) return [];
589
+ return chapters.value.map((ch) => ({
590
+ ...ch,
591
+ startPercent: ch.startTime / duration.value * 100,
592
+ endPercent: ch.endTime / duration.value * 100,
593
+ widthPercent: (ch.endTime - ch.startTime) / duration.value * 100
594
+ }));
595
+ });
596
+ /** 跳转到指定章节 */
597
+ function goToChapter(chapterId) {
598
+ const chapter = chapters.value.find((ch) => ch.id === chapterId);
599
+ if (chapter) seekFn(chapter.startTime);
600
+ }
601
+ /** 跳转到上一章 */
602
+ function prevChapter() {
603
+ const idx = currentChapterIndex.value;
604
+ if (idx > 0) seekFn(chapters.value[idx - 1].startTime);
605
+ }
606
+ /** 跳转到下一章 */
607
+ function nextChapter() {
608
+ const idx = currentChapterIndex.value;
609
+ if (idx >= 0 && idx < chapters.value.length - 1) seekFn(chapters.value[idx + 1].startTime);
610
+ }
611
+ return {
612
+ currentChapter,
613
+ currentChapterIndex,
614
+ chapterMarkers,
615
+ goToChapter,
616
+ prevChapter,
617
+ nextChapter
618
+ };
619
+ }
620
+
621
+ //#endregion
622
+ //#region src/components/C_VideoPlayer/composables/useBookmarks.ts
623
+ /**
624
+ * 书签笔记 composable
625
+ * - 添加 / 删除 / 更新书签
626
+ * - localStorage 持久化
627
+ * - 按时间排序
628
+ */
629
+ function useBookmarks(url, currentTime, seekFn, initialBookmarks = []) {
630
+ const bookmarks = ref([...initialBookmarks]);
631
+ /** 存储 key */
632
+ function getStorageKey() {
633
+ return STORAGE_KEYS.BOOKMARKS + encodeURIComponent(url.value);
634
+ }
635
+ /** 从 localStorage 恢复书签 */
636
+ function restoreBookmarks() {
637
+ try {
638
+ const stored = localStorage.getItem(getStorageKey());
639
+ if (stored) {
640
+ const parsed = JSON.parse(stored);
641
+ if (Array.isArray(parsed)) bookmarks.value = parsed;
642
+ }
643
+ } catch {}
644
+ }
645
+ /** 保存书签到 localStorage */
646
+ function saveBookmarks() {
647
+ try {
648
+ localStorage.setItem(getStorageKey(), JSON.stringify(bookmarks.value));
649
+ } catch {}
650
+ }
651
+ /** 排序(按时间升序) */
652
+ function sortBookmarks() {
653
+ bookmarks.value.sort((a, b) => a.time - b.time);
654
+ }
655
+ /** 添加书签 */
656
+ function addBookmark(note = "") {
657
+ const bookmark = {
658
+ id: `bm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
659
+ time: currentTime.value,
660
+ note,
661
+ createdAt: Date.now()
662
+ };
663
+ bookmarks.value.push(bookmark);
664
+ sortBookmarks();
665
+ saveBookmarks();
666
+ return bookmark;
667
+ }
668
+ /** 删除书签 */
669
+ function removeBookmark(id) {
670
+ const index = bookmarks.value.findIndex((b) => b.id === id);
671
+ if (index !== -1) {
672
+ bookmarks.value.splice(index, 1);
673
+ saveBookmarks();
674
+ }
675
+ }
676
+ /** 更新书签备注 */
677
+ function updateBookmark(id, note) {
678
+ const bookmark = bookmarks.value.find((b) => b.id === id);
679
+ if (bookmark) {
680
+ bookmark.note = note;
681
+ saveBookmarks();
682
+ }
683
+ }
684
+ /** 跳转到书签位置 */
685
+ function goToBookmark(id) {
686
+ const bookmark = bookmarks.value.find((b) => b.id === id);
687
+ if (bookmark) seekFn(bookmark.time);
688
+ }
689
+ /** 清空所有书签 */
690
+ function clearBookmarks() {
691
+ bookmarks.value = [];
692
+ saveBookmarks();
693
+ }
694
+ if (!initialBookmarks.length) restoreBookmarks();
695
+ return {
696
+ bookmarks,
697
+ addBookmark,
698
+ removeBookmark,
699
+ updateBookmark,
700
+ goToBookmark,
701
+ clearBookmarks
702
+ };
703
+ }
704
+
705
+ //#endregion
706
+ //#region src/components/C_VideoPlayer/composables/useAntiCheat.ts
707
+ /**
708
+ * 防作弊 composable
709
+ * - 首次观看防跳播:禁止向未观看区域拖动进度条
710
+ * - 焦点检测:切出页面自动暂停
711
+ * - 水印标记开关(UI 通过 WatermarkOverlay.vue 组件渲染)
712
+ */
713
+ function useAntiCheat(playerRef, currentTime, config) {
714
+ /** 已观看的最远位置(秒) */
715
+ const maxWatchedTime = ref(0);
716
+ /** 是否为首次观看(有未完整看过的区段) */
717
+ const isFirstWatch = ref(true);
718
+ /** 是否处于焦点外(页面不可见) */
719
+ const isBlurred = ref(false);
720
+ /** 是否启用水印 */
721
+ const showWatermark = ref(config?.watermark ?? false);
722
+ /** 水印文本 */
723
+ const watermarkText = ref(config?.watermarkText ?? "");
724
+ /** 记录播放的最远位置 */
725
+ function updateMaxWatched(time) {
726
+ if (time > maxWatchedTime.value) maxWatchedTime.value = time;
727
+ }
728
+ /** 防跳播拦截 */
729
+ function handleSeeking(player) {
730
+ if (!config?.preventSeekOnFirstWatch || !isFirstWatch.value) return;
731
+ if ((player.currentTime ?? 0) > maxWatchedTime.value + 2) player.currentTime = maxWatchedTime.value;
732
+ }
733
+ /** 焦点检测 - 页面可见性变化 */
734
+ function handleVisibilityChange() {
735
+ if (!config?.focusDetection) return;
736
+ const player = playerRef.value;
737
+ if (!player) return;
738
+ if (document.hidden) {
739
+ isBlurred.value = true;
740
+ if (!player.paused) player.pause();
741
+ } else isBlurred.value = false;
742
+ }
743
+ /** 绑定事件 */
744
+ function bindEvents(player) {
745
+ if (config?.preventSeekOnFirstWatch) player.on(Events.SEEKING, () => handleSeeking(player));
746
+ player.on(Events.TIME_UPDATE, () => {
747
+ updateMaxWatched(currentTime.value);
748
+ });
749
+ if (config?.focusDetection) document.addEventListener("visibilitychange", handleVisibilityChange);
750
+ }
751
+ /** 标记已完整看过(允许自由拖动) */
752
+ function markAsWatched() {
753
+ isFirstWatch.value = false;
754
+ }
755
+ /** 设置水印文本 */
756
+ function setWatermarkText(text) {
757
+ watermarkText.value = text;
758
+ }
759
+ onBeforeUnmount(() => {
760
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
761
+ });
762
+ return {
763
+ maxWatchedTime,
764
+ isFirstWatch,
765
+ isBlurred,
766
+ showWatermark,
767
+ watermarkText,
768
+ bindEvents,
769
+ markAsWatched,
770
+ setWatermarkText
771
+ };
772
+ }
773
+
774
+ //#endregion
775
+ //#region src/components/C_VideoPlayer/composables/useSubtitle.ts
776
+ /** 将 "HH:MM:SS.mmm" 或 "MM:SS.mmm" 格式转为秒 */
777
+ function parseVttTime(raw) {
778
+ const parts = raw.trim().split(":");
779
+ if (parts.length === 3) return Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2]);
780
+ return Number(parts[0]) * 60 + Number(parts[1]);
781
+ }
782
+ /** 解析 WebVTT 文本为 cue 列表 */
783
+ function parseVtt(vttText) {
784
+ const cues = [];
785
+ const blocks = vttText.trim().split(/\n\s*\n/);
786
+ for (const block of blocks) {
787
+ const lines = block.trim().split("\n");
788
+ const timeLineIdx = lines.findIndex((l) => l.includes("-->"));
789
+ if (timeLineIdx === -1) continue;
790
+ const [startRaw, endRaw] = lines[timeLineIdx].split("-->");
791
+ if (!startRaw || !endRaw) continue;
792
+ const start = parseVttTime(startRaw);
793
+ const end = parseVttTime(endRaw.split(/\s/)[0]);
794
+ const text = lines.slice(timeLineIdx + 1).join("\n").replace(/<[^>]+>/g, "").trim();
795
+ if (text) cues.push({
796
+ start,
797
+ end,
798
+ text
799
+ });
800
+ }
801
+ return cues;
802
+ }
803
+ /**
804
+ * 字幕管理 composable
805
+ * - fetch VTT → 解析 → 按当前播放时间匹配 cue
806
+ * - 提供切换 / 关闭能力
807
+ */
808
+ function useSubtitle(playerRef, subtitles = [], currentTime = ref(0)) {
809
+ /** 当前激活的字幕语言(null = 关闭) */
810
+ const activeLanguage = ref(null);
811
+ /** 字幕列表 */
812
+ const subtitleList = ref([...subtitles]);
813
+ /** 已加载的 cue 数据:language → cues */
814
+ const cueMap = ref({});
815
+ /** 是否正在加载 */
816
+ const isLoading = ref(false);
817
+ /** 当前应显示的字幕文本 */
818
+ const currentText = computed(() => {
819
+ if (!activeLanguage.value) return "";
820
+ const cues = cueMap.value[activeLanguage.value];
821
+ if (!cues?.length) return "";
822
+ const t = currentTime.value;
823
+ return cues.find((c) => t >= c.start && t < c.end)?.text ?? "";
824
+ });
825
+ /** 是否有字幕可用 */
826
+ const hasSubtitles = computed(() => subtitleList.value.length > 0);
827
+ /** 加载指定语言的 VTT 文件 */
828
+ async function loadTrack(language) {
829
+ if (cueMap.value[language]) return cueMap.value[language];
830
+ const track = subtitleList.value.find((s) => s.language === language);
831
+ if (!track) return [];
832
+ isLoading.value = true;
833
+ try {
834
+ const resp = await fetch(track.src);
835
+ if (!resp.ok) {
836
+ console.warn(`[C_VideoPlayer] 字幕加载失败: ${track.src} (${resp.status})`);
837
+ return [];
838
+ }
839
+ const cues = parseVtt(await resp.text());
840
+ cueMap.value[language] = cues;
841
+ return cues;
842
+ } catch (e) {
843
+ console.warn("[C_VideoPlayer] 字幕加载异常:", e);
844
+ return [];
845
+ } finally {
846
+ isLoading.value = false;
847
+ }
848
+ }
849
+ /** 初始化:加载默认字幕 */
850
+ async function initSubtitles() {
851
+ if (!subtitleList.value.length) return;
852
+ const defaultTrack = subtitleList.value.find((s) => s.default) ?? subtitleList.value[0];
853
+ await loadTrack(defaultTrack.language);
854
+ activeLanguage.value = defaultTrack.language;
855
+ }
856
+ /** 切换字幕语言 */
857
+ async function switchSubtitle(language) {
858
+ if (!subtitleList.value.find((s) => s.language === language)) {
859
+ console.warn(`[C_VideoPlayer] 未找到字幕轨道: ${language}`);
860
+ return;
861
+ }
862
+ await loadTrack(language);
863
+ activeLanguage.value = language;
864
+ }
865
+ /** 关闭字幕 */
866
+ function closeSubtitle() {
867
+ activeLanguage.value = null;
868
+ }
869
+ /** 切换字幕开/关 */
870
+ function toggleSubtitle() {
871
+ if (activeLanguage.value) closeSubtitle();
872
+ else {
873
+ const defaultTrack = subtitleList.value.find((s) => s.default) ?? subtitleList.value[0];
874
+ if (defaultTrack) switchSubtitle(defaultTrack.language);
875
+ }
876
+ }
877
+ return {
878
+ activeLanguage,
879
+ subtitleList,
880
+ currentText,
881
+ hasSubtitles,
882
+ isLoading,
883
+ initSubtitles,
884
+ switchSubtitle,
885
+ closeSubtitle,
886
+ toggleSubtitle
887
+ };
888
+ }
889
+
890
+ //#endregion
891
+ //#region src/components/C_VideoPlayer/composables/useQuiz.ts
892
+ /**
893
+ * 视频内测验 composable
894
+ * - 在指定时间点触发测验弹窗
895
+ * - 暂停视频等待作答
896
+ * - 判断答案是否正确
897
+ * - 支持必须答对才能继续
898
+ */
899
+ function useQuiz(playerRef, currentTime, quizzes) {
900
+ /** 当前显示的测验 */
901
+ const activeQuiz = ref(null);
902
+ /** 已完成的测验 ID 集合 */
903
+ const completedQuizIds = ref(/* @__PURE__ */ new Set());
904
+ /** 当前选中的答案 */
905
+ const selectedAnswer = ref("");
906
+ /** 是否显示结果反馈 */
907
+ const showResult = ref(false);
908
+ /** 上次作答是否正确 */
909
+ const lastAnswerCorrect = ref(false);
910
+ /** 检查时间容差(秒) */
911
+ const TIME_TOLERANCE = 1;
912
+ /** 检查是否需要触发测验 */
913
+ function checkQuizTrigger() {
914
+ if (activeQuiz.value) return;
915
+ if (!quizzes.value.length) return;
916
+ const time = currentTime.value;
917
+ const quiz = quizzes.value.find((q) => !completedQuizIds.value.has(q.id) && Math.abs(time - q.triggerTime) < TIME_TOLERANCE);
918
+ if (quiz) triggerQuiz(quiz);
919
+ }
920
+ /** 触发测验 */
921
+ function triggerQuiz(quiz) {
922
+ activeQuiz.value = quiz;
923
+ selectedAnswer.value = quiz.type === "multiple" ? [] : "";
924
+ showResult.value = false;
925
+ lastAnswerCorrect.value = false;
926
+ playerRef.value?.pause();
927
+ }
928
+ /** 提交答案 */
929
+ function submitAnswer() {
930
+ if (!activeQuiz.value) return false;
931
+ const quiz = activeQuiz.value;
932
+ const isCorrect = checkAnswer(quiz, selectedAnswer.value);
933
+ lastAnswerCorrect.value = isCorrect;
934
+ showResult.value = true;
935
+ if (isCorrect || !quiz.required) completedQuizIds.value.add(quiz.id);
936
+ return isCorrect;
937
+ }
938
+ /** 关闭测验弹窗并继续播放 */
939
+ function closeQuiz() {
940
+ if (!activeQuiz.value) return;
941
+ const quiz = activeQuiz.value;
942
+ if (quiz.required && !completedQuizIds.value.has(quiz.id)) return;
943
+ activeQuiz.value = null;
944
+ showResult.value = false;
945
+ selectedAnswer.value = "";
946
+ playerRef.value?.play();
947
+ }
948
+ /** 重试当前测验 */
949
+ function retryQuiz() {
950
+ if (!activeQuiz.value) return;
951
+ selectedAnswer.value = activeQuiz.value.type === "multiple" ? [] : "";
952
+ showResult.value = false;
953
+ lastAnswerCorrect.value = false;
954
+ }
955
+ /** 判断答案是否正确 */
956
+ function checkAnswer(quiz, answer) {
957
+ if (quiz.type === "multiple") {
958
+ const selected = Array.isArray(answer) ? [...answer].sort() : [answer];
959
+ const correct = Array.isArray(quiz.answer) ? [...quiz.answer].sort() : [quiz.answer];
960
+ return selected.length === correct.length && selected.every((v, i) => v === correct[i]);
961
+ }
962
+ return answer === quiz.answer;
963
+ }
964
+ /** 监听时间变化检查测验触发 */
965
+ watch(currentTime, () => {
966
+ checkQuizTrigger();
967
+ });
968
+ return {
969
+ activeQuiz,
970
+ completedQuizIds,
971
+ selectedAnswer,
972
+ showResult,
973
+ lastAnswerCorrect,
974
+ submitAnswer,
975
+ closeQuiz,
976
+ retryQuiz
977
+ };
978
+ }
979
+
980
+ //#endregion
981
+ //#region src/components/C_VideoPlayer/composables/useMiniPlayer.ts
982
+ /**
983
+ * 小窗播放 composable
984
+ * - 当播放器滚出可视区域时自动切换为小窗浮动
985
+ * - 小窗可拖动定位
986
+ * - 点击小窗可滚回原位
987
+ */
988
+ function useMiniPlayer(containerRef, enabled) {
989
+ /** 是否处于小窗模式 */
990
+ const isMiniMode = ref(false);
991
+ /** IntersectionObserver 实例 */
992
+ let observer = null;
993
+ /** 初始化观察器 */
994
+ function initObserver() {
995
+ if (!containerRef.value || !enabled.value) return;
996
+ observer = new IntersectionObserver(([entry]) => {
997
+ isMiniMode.value = !entry.isIntersecting;
998
+ }, { threshold: .3 });
999
+ observer.observe(containerRef.value);
1000
+ }
1001
+ /** 销毁观察器 */
1002
+ function destroyObserver() {
1003
+ if (observer) {
1004
+ observer.disconnect();
1005
+ observer = null;
1006
+ }
1007
+ isMiniMode.value = false;
1008
+ }
1009
+ /** 滚动回原始位置 */
1010
+ function scrollToPlayer() {
1011
+ containerRef.value?.scrollIntoView({
1012
+ behavior: "smooth",
1013
+ block: "center"
1014
+ });
1015
+ }
1016
+ /** 关闭小窗 */
1017
+ function closeMiniPlayer() {
1018
+ isMiniMode.value = false;
1019
+ destroyObserver();
1020
+ }
1021
+ onBeforeUnmount(() => {
1022
+ destroyObserver();
1023
+ });
1024
+ return {
1025
+ isMiniMode,
1026
+ initObserver,
1027
+ destroyObserver,
1028
+ scrollToPlayer,
1029
+ closeMiniPlayer
1030
+ };
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region src/components/C_VideoPlayer/composables/useKeyboard.ts
1035
+ /**
1036
+ * 快捷键管理 composable
1037
+ * - xgplayer 已内置快捷键,这里做补充增强
1038
+ * - 支持自定义快捷键回调
1039
+ */
1040
+ function useKeyboard(playerRef, containerRef, options = {}) {
1041
+ const { enabled = true, onToggleFullscreen } = options;
1042
+ /** 快捷键动作映射 */
1043
+ const keyActions = {
1044
+ " ": (e, player) => {
1045
+ e.preventDefault();
1046
+ if (player.paused) player.play();
1047
+ else player.pause();
1048
+ },
1049
+ ArrowLeft: (e, player) => {
1050
+ e.preventDefault();
1051
+ player.currentTime = Math.max(0, (player.currentTime ?? 0) - SEEK_STEP);
1052
+ },
1053
+ ArrowRight: (e, player) => {
1054
+ e.preventDefault();
1055
+ player.currentTime = Math.min(player.duration ?? 0, (player.currentTime ?? 0) + SEEK_STEP);
1056
+ },
1057
+ ArrowUp: (e, player) => {
1058
+ e.preventDefault();
1059
+ player.volume = Math.min(1, (player.volume ?? 0) + VOLUME_STEP);
1060
+ },
1061
+ ArrowDown: (e, player) => {
1062
+ e.preventDefault();
1063
+ player.volume = Math.max(0, (player.volume ?? 0) - VOLUME_STEP);
1064
+ },
1065
+ f: (e) => {
1066
+ e.preventDefault();
1067
+ onToggleFullscreen?.();
1068
+ },
1069
+ F: (e) => {
1070
+ e.preventDefault();
1071
+ onToggleFullscreen?.();
1072
+ },
1073
+ m: (e, player) => {
1074
+ e.preventDefault();
1075
+ player.volume = player.volume > 0 ? 0 : .75;
1076
+ },
1077
+ M: (e, player) => {
1078
+ e.preventDefault();
1079
+ player.volume = player.volume > 0 ? 0 : .75;
1080
+ }
1081
+ };
1082
+ /** 快捷键处理函数 */
1083
+ function handleKeydown(e) {
1084
+ if (!enabled) return;
1085
+ const player = playerRef.value;
1086
+ if (!player) return;
1087
+ const target = e.target;
1088
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) return;
1089
+ const action = keyActions[e.key];
1090
+ action?.(e, player);
1091
+ }
1092
+ /** 开始监听 */
1093
+ function startListening() {
1094
+ if (!enabled) return;
1095
+ containerRef.value?.addEventListener("keydown", handleKeydown);
1096
+ }
1097
+ /** 停止监听 */
1098
+ function stopListening() {
1099
+ containerRef.value?.removeEventListener("keydown", handleKeydown);
1100
+ }
1101
+ onBeforeUnmount(() => {
1102
+ stopListening();
1103
+ });
1104
+ return {
1105
+ startListening,
1106
+ stopListening
1107
+ };
1108
+ }
1109
+
1110
+ //#endregion
1111
+ //#region src/components/C_VideoPlayer/plugins/analytics-reporter.ts
1112
+ /**
1113
+ * 数据上报插件
1114
+ * - 监听播放器核心事件并通过回调上报
1115
+ * - 支持自定义事件过滤
1116
+ * - 页面关闭时使用 sendBeacon 兜底
1117
+ */
1118
+ function createAnalyticsPlugin(player, reporter) {
1119
+ /** 构建事件数据 */
1120
+ function buildEvent(type, payload) {
1121
+ return {
1122
+ type,
1123
+ currentTime: player.currentTime ?? 0,
1124
+ timestamp: Date.now(),
1125
+ payload
1126
+ };
1127
+ }
1128
+ /** 上报单个事件 */
1129
+ function report(type, payload) {
1130
+ reporter(buildEvent(type, payload));
1131
+ }
1132
+ const onPlay = () => report("play");
1133
+ const onPause = () => report("pause");
1134
+ const onEnded = () => report("ended");
1135
+ const onSeeked = () => report("seek");
1136
+ const onError = () => report("error");
1137
+ const onWaiting = () => report("buffer");
1138
+ const onFullscreen = (isFS) => {
1139
+ report("fullscreen", { isFullscreen: isFS });
1140
+ };
1141
+ const onDefinition = (data) => {
1142
+ report("quality_change", data);
1143
+ };
1144
+ const onRate = () => {
1145
+ report("speed_change", { rate: player.playbackRate });
1146
+ };
1147
+ player.on(Events.PLAY, onPlay);
1148
+ player.on(Events.PAUSE, onPause);
1149
+ player.on(Events.ENDED, onEnded);
1150
+ player.on(Events.SEEKED, onSeeked);
1151
+ player.on(Events.ERROR, onError);
1152
+ player.on(Events.WAITING, onWaiting);
1153
+ player.on(Events.FULLSCREEN_CHANGE, onFullscreen);
1154
+ player.on(Events.DEFINITION_CHANGE, onDefinition);
1155
+ player.on(Events.RATE_CHANGE, onRate);
1156
+ /** 销毁插件:移除所有事件监听 */
1157
+ function destroy() {
1158
+ player.off(Events.PLAY, onPlay);
1159
+ player.off(Events.PAUSE, onPause);
1160
+ player.off(Events.ENDED, onEnded);
1161
+ player.off(Events.SEEKED, onSeeked);
1162
+ player.off(Events.ERROR, onError);
1163
+ player.off(Events.WAITING, onWaiting);
1164
+ player.off(Events.FULLSCREEN_CHANGE, onFullscreen);
1165
+ player.off(Events.DEFINITION_CHANGE, onDefinition);
1166
+ player.off(Events.RATE_CHANGE, onRate);
1167
+ }
1168
+ return {
1169
+ report,
1170
+ destroy
1171
+ };
1172
+ }
1173
+
1174
+ //#endregion
1175
+ //#region src/components/C_VideoPlayer/components/ControlBar.vue?vue&type=script&setup=true&lang.ts
1176
+ const _hoisted_1$4 = {
1177
+ key: 0,
1178
+ class: "vp-control-bar"
1179
+ };
1180
+ const _hoisted_2$4 = { class: "vp-control-bar__left" };
1181
+ const _hoisted_3$4 = { class: "vp-control-bar__center" };
1182
+ const _hoisted_4$4 = { class: "vp-control-bar__right" };
1183
+ var ControlBar_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1184
+ __name: "ControlBar",
1185
+ props: { visible: {
1186
+ type: Boolean,
1187
+ default: true
1188
+ } },
1189
+ setup(__props) {
1190
+ return (_ctx, _cache) => {
1191
+ return _ctx.visible ? (openBlock(), createElementBlock("div", _hoisted_1$4, [
1192
+ createElementVNode("div", _hoisted_2$4, [renderSlot(_ctx.$slots, "left", {}, void 0, true)]),
1193
+ createElementVNode("div", _hoisted_3$4, [renderSlot(_ctx.$slots, "center", {}, void 0, true)]),
1194
+ createElementVNode("div", _hoisted_4$4, [renderSlot(_ctx.$slots, "right", {}, void 0, true)])
1195
+ ])) : createCommentVNode("v-if", true);
1196
+ };
1197
+ }
1198
+ });
1199
+
1200
+ //#endregion
1201
+ //#region src/components/C_VideoPlayer/components/ControlBar.vue
1202
+ var ControlBar_default = /* @__PURE__ */ export_helper_default(ControlBar_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-ea44a973"]]);
1203
+
1204
+ //#endregion
1205
+ //#region src/components/C_VideoPlayer/components/SubtitleOverlay.vue?vue&type=script&setup=true&lang.ts
1206
+ const _hoisted_1$3 = {
1207
+ key: 0,
1208
+ class: "vp-subtitle-overlay"
1209
+ };
1210
+ const _hoisted_2$3 = { class: "vp-subtitle-text" };
1211
+ const _hoisted_3$3 = {
1212
+ key: 0,
1213
+ class: "vp-subtitle-toggle"
1214
+ };
1215
+ const _hoisted_4$3 = { class: "vp-subtitle-menu" };
1216
+ const _hoisted_5$2 = ["onClick"];
1217
+ var SubtitleOverlay_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1218
+ __name: "SubtitleOverlay",
1219
+ props: {
1220
+ text: {},
1221
+ tracks: {},
1222
+ activeLanguage: {}
1223
+ },
1224
+ emits: ["switch", "close"],
1225
+ setup(__props) {
1226
+ return (_ctx, _cache) => {
1227
+ const _component_NButton = resolveComponent("NButton");
1228
+ const _component_NPopover = resolveComponent("NPopover");
1229
+ return openBlock(), createElementBlock(Fragment, null, [
1230
+ createCommentVNode(" 字幕文本 "),
1231
+ createVNode(Transition, { name: "vp-subtitle-fade" }, {
1232
+ default: withCtx(() => [_ctx.text ? (openBlock(), createElementBlock("div", _hoisted_1$3, [createElementVNode("span", _hoisted_2$3, toDisplayString(_ctx.text), 1)])) : createCommentVNode("v-if", true)]),
1233
+ _: 1
1234
+ }),
1235
+ createCommentVNode(" 字幕切换按钮(叠加在播放器控制栏上方) "),
1236
+ _ctx.tracks.length ? (openBlock(), createElementBlock("div", _hoisted_3$3, [createVNode(_component_NPopover, {
1237
+ trigger: "click",
1238
+ placement: "top",
1239
+ "show-arrow": false
1240
+ }, {
1241
+ trigger: withCtx(() => [createVNode(_component_NButton, {
1242
+ quaternary: "",
1243
+ size: "small",
1244
+ class: normalizeClass(["vp-subtitle-btn", { "is-active": !!_ctx.activeLanguage }])
1245
+ }, {
1246
+ default: withCtx(() => _cache[1] || (_cache[1] = [createTextVNode(" 字幕 ", -1)])),
1247
+ _: 1,
1248
+ __: [1]
1249
+ }, 8, ["class"])]),
1250
+ default: withCtx(() => [createElementVNode("div", _hoisted_4$3, [createElementVNode("div", {
1251
+ class: normalizeClass(["vp-subtitle-menu__item", { "is-active": !_ctx.activeLanguage }]),
1252
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("close"))
1253
+ }, " 关闭字幕 ", 2), (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.tracks, (track) => {
1254
+ return openBlock(), createElementBlock("div", {
1255
+ key: track.language,
1256
+ class: normalizeClass(["vp-subtitle-menu__item", { "is-active": _ctx.activeLanguage === track.language }]),
1257
+ onClick: ($event) => _ctx.$emit("switch", track.language)
1258
+ }, toDisplayString(track.label), 11, _hoisted_5$2);
1259
+ }), 128))])]),
1260
+ _: 1
1261
+ })])) : createCommentVNode("v-if", true)
1262
+ ], 64);
1263
+ };
1264
+ }
1265
+ });
1266
+
1267
+ //#endregion
1268
+ //#region src/components/C_VideoPlayer/components/SubtitleOverlay.vue
1269
+ var SubtitleOverlay_default = /* @__PURE__ */ export_helper_default(SubtitleOverlay_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-1069c4da"]]);
1270
+
1271
+ //#endregion
1272
+ //#region src/components/C_VideoPlayer/components/ChapterMarkers.vue?vue&type=script&setup=true&lang.ts
1273
+ const _hoisted_1$2 = {
1274
+ key: 0,
1275
+ class: "vp-chapter-markers"
1276
+ };
1277
+ const _hoisted_2$2 = {
1278
+ key: 0,
1279
+ class: "vp-chapter-current"
1280
+ };
1281
+ const _hoisted_3$2 = { class: "vp-chapter-index" };
1282
+ const _hoisted_4$2 = { class: "vp-chapter-title" };
1283
+ const _hoisted_5$1 = { class: "vp-chapter-list" };
1284
+ const _hoisted_6$1 = ["onClick"];
1285
+ const _hoisted_7$1 = { class: "vp-chapter-item-index" };
1286
+ const _hoisted_8 = { class: "vp-chapter-item-title" };
1287
+ const _hoisted_9 = { class: "vp-chapter-item-time" };
1288
+ var ChapterMarkers_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1289
+ __name: "ChapterMarkers",
1290
+ props: {
1291
+ chapters: {},
1292
+ currentChapter: {},
1293
+ currentIndex: {}
1294
+ },
1295
+ emits: ["goTo"],
1296
+ setup(__props) {
1297
+ /** 格式化时间为 mm:ss */
1298
+ function formatTime(seconds) {
1299
+ const m = Math.floor(seconds / 60);
1300
+ const s = Math.floor(seconds % 60);
1301
+ return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
1302
+ }
1303
+ return (_ctx, _cache) => {
1304
+ const _component_NButton = resolveComponent("NButton");
1305
+ const _component_NPopover = resolveComponent("NPopover");
1306
+ return _ctx.chapters.length ? (openBlock(), createElementBlock("div", _hoisted_1$2, [
1307
+ createCommentVNode(" 当前章节信息 "),
1308
+ _ctx.currentChapter ? (openBlock(), createElementBlock("div", _hoisted_2$2, [createElementVNode("span", _hoisted_3$2, toDisplayString(_ctx.currentIndex + 1) + "/" + toDisplayString(_ctx.chapters.length), 1), createElementVNode("span", _hoisted_4$2, toDisplayString(_ctx.currentChapter.title), 1)])) : createCommentVNode("v-if", true),
1309
+ createCommentVNode(" 章节列表面板 "),
1310
+ createVNode(_component_NPopover, {
1311
+ trigger: "click",
1312
+ placement: "top-start",
1313
+ "show-arrow": false,
1314
+ style: {
1315
+ "max-height": "300px",
1316
+ "overflow-y": "auto"
1317
+ }
1318
+ }, {
1319
+ trigger: withCtx(() => [createVNode(_component_NButton, {
1320
+ quaternary: "",
1321
+ size: "small",
1322
+ class: "vp-chapter-trigger"
1323
+ }, {
1324
+ icon: withCtx(() => _cache[0] || (_cache[0] = [createElementVNode("span", { class: "vp-chapter-icon" }, "☰", -1)])),
1325
+ default: withCtx(() => [_cache[1] || (_cache[1] = createTextVNode(" 章节 ", -1))]),
1326
+ _: 1,
1327
+ __: [1]
1328
+ })]),
1329
+ default: withCtx(() => [createElementVNode("div", _hoisted_5$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.chapters, (chapter, idx) => {
1330
+ return openBlock(), createElementBlock("div", {
1331
+ key: chapter.id,
1332
+ class: normalizeClass(["vp-chapter-item", { "is-active": chapter.id === _ctx.currentChapter?.id }]),
1333
+ onClick: ($event) => _ctx.$emit("goTo", chapter.id)
1334
+ }, [
1335
+ createElementVNode("span", _hoisted_7$1, toDisplayString(idx + 1), 1),
1336
+ createElementVNode("span", _hoisted_8, toDisplayString(chapter.title), 1),
1337
+ createElementVNode("span", _hoisted_9, toDisplayString(formatTime(chapter.startTime)), 1)
1338
+ ], 10, _hoisted_6$1);
1339
+ }), 128))])]),
1340
+ _: 1
1341
+ })
1342
+ ])) : createCommentVNode("v-if", true);
1343
+ };
1344
+ }
1345
+ });
1346
+
1347
+ //#endregion
1348
+ //#region src/components/C_VideoPlayer/components/ChapterMarkers.vue
1349
+ var ChapterMarkers_default = /* @__PURE__ */ export_helper_default(ChapterMarkers_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3218c0f1"]]);
1350
+
1351
+ //#endregion
1352
+ //#region src/components/C_VideoPlayer/components/BookmarkPanel.vue?vue&type=script&setup=true&lang.ts
1353
+ const _hoisted_1$1 = { class: "vp-bookmark-panel" };
1354
+ const _hoisted_2$1 = { class: "vp-bookmark-list" };
1355
+ const _hoisted_3$1 = ["onClick"];
1356
+ const _hoisted_4$1 = { class: "vp-bookmark-note" };
1357
+ var BookmarkPanel_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1358
+ __name: "BookmarkPanel",
1359
+ props: { bookmarks: {} },
1360
+ emits: [
1361
+ "add",
1362
+ "remove",
1363
+ "goTo"
1364
+ ],
1365
+ setup(__props, { emit: __emit }) {
1366
+ const emit = __emit;
1367
+ /** 添加书签 */
1368
+ function handleAdd() {
1369
+ emit("add", "");
1370
+ }
1371
+ /** 格式化时间为 mm:ss */
1372
+ function formatTime(seconds) {
1373
+ const m = Math.floor(seconds / 60);
1374
+ const s = Math.floor(seconds % 60);
1375
+ return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
1376
+ }
1377
+ return (_ctx, _cache) => {
1378
+ const _component_NButton = resolveComponent("NButton");
1379
+ const _component_NBadge = resolveComponent("NBadge");
1380
+ const _component_NPopover = resolveComponent("NPopover");
1381
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
1382
+ createCommentVNode(" 添加书签按钮 "),
1383
+ createVNode(_component_NButton, {
1384
+ quaternary: "",
1385
+ size: "small",
1386
+ class: "vp-bookmark-add-btn",
1387
+ onClick: handleAdd
1388
+ }, {
1389
+ icon: withCtx(() => _cache[0] || (_cache[0] = [createElementVNode("span", null, "🔖", -1)])),
1390
+ default: withCtx(() => [_cache[1] || (_cache[1] = createTextVNode(" 书签 ", -1))]),
1391
+ _: 1,
1392
+ __: [1]
1393
+ }),
1394
+ createCommentVNode(" 书签列表弹窗 "),
1395
+ _ctx.bookmarks.length ? (openBlock(), createBlock(_component_NPopover, {
1396
+ key: 0,
1397
+ trigger: "click",
1398
+ placement: "top-start",
1399
+ "show-arrow": false,
1400
+ style: {
1401
+ "max-height": "300px",
1402
+ "overflow-y": "auto"
1403
+ }
1404
+ }, {
1405
+ trigger: withCtx(() => [createVNode(_component_NBadge, {
1406
+ value: _ctx.bookmarks.length,
1407
+ max: 99,
1408
+ type: "info"
1409
+ }, {
1410
+ default: withCtx(() => [createVNode(_component_NButton, {
1411
+ quaternary: "",
1412
+ size: "small",
1413
+ class: "vp-bookmark-list-btn"
1414
+ }, {
1415
+ default: withCtx(() => _cache[2] || (_cache[2] = [createTextVNode(" 列表 ", -1)])),
1416
+ _: 1,
1417
+ __: [2]
1418
+ })]),
1419
+ _: 1
1420
+ }, 8, ["value"])]),
1421
+ default: withCtx(() => [createElementVNode("div", _hoisted_2$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.bookmarks, (bm) => {
1422
+ return openBlock(), createElementBlock("div", {
1423
+ key: bm.id,
1424
+ class: "vp-bookmark-item"
1425
+ }, [
1426
+ createElementVNode("span", {
1427
+ class: "vp-bookmark-time",
1428
+ onClick: ($event) => _ctx.$emit("goTo", bm.id)
1429
+ }, toDisplayString(formatTime(bm.time)), 9, _hoisted_3$1),
1430
+ createElementVNode("span", _hoisted_4$1, toDisplayString(bm.note || "无备注"), 1),
1431
+ createVNode(_component_NButton, {
1432
+ quaternary: "",
1433
+ size: "tiny",
1434
+ type: "error",
1435
+ onClick: ($event) => _ctx.$emit("remove", bm.id)
1436
+ }, {
1437
+ default: withCtx(() => _cache[3] || (_cache[3] = [createTextVNode(" ✕ ", -1)])),
1438
+ _: 2,
1439
+ __: [3]
1440
+ }, 1032, ["onClick"])
1441
+ ]);
1442
+ }), 128))])]),
1443
+ _: 1
1444
+ })) : createCommentVNode("v-if", true)
1445
+ ]);
1446
+ };
1447
+ }
1448
+ });
1449
+
1450
+ //#endregion
1451
+ //#region src/components/C_VideoPlayer/components/BookmarkPanel.vue
1452
+ var BookmarkPanel_default = /* @__PURE__ */ export_helper_default(BookmarkPanel_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-c353e3d6"]]);
1453
+
1454
+ //#endregion
1455
+ //#region src/components/C_VideoPlayer/components/QuizOverlay.vue?vue&type=script&setup=true&lang.ts
1456
+ const _hoisted_1 = {
1457
+ key: 0,
1458
+ class: "vp-quiz-overlay"
1459
+ };
1460
+ const _hoisted_2 = { class: "vp-quiz-card" };
1461
+ const _hoisted_3 = { class: "vp-quiz-header" };
1462
+ const _hoisted_4 = { class: "vp-quiz-type-tag" };
1463
+ const _hoisted_5 = { class: "vp-quiz-title" };
1464
+ const _hoisted_6 = { class: "vp-quiz-options" };
1465
+ const _hoisted_7 = { class: "vp-quiz-actions" };
1466
+ var QuizOverlay_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1467
+ __name: "QuizOverlay",
1468
+ props: /* @__PURE__ */ mergeModels({
1469
+ quiz: {},
1470
+ showResult: {
1471
+ type: Boolean,
1472
+ default: false
1473
+ },
1474
+ isCorrect: {
1475
+ type: Boolean,
1476
+ default: false
1477
+ }
1478
+ }, {
1479
+ "answer": { default: "" },
1480
+ "answerModifiers": {}
1481
+ }),
1482
+ emits: /* @__PURE__ */ mergeModels([
1483
+ "submit",
1484
+ "retry",
1485
+ "close"
1486
+ ], ["update:answer"]),
1487
+ setup(__props) {
1488
+ const props = __props;
1489
+ /** 单选答案 */
1490
+ const singleAnswer = ref("");
1491
+ /** 多选答案 */
1492
+ const multiAnswer = ref([]);
1493
+ /** 当前选中的答案(对外) */
1494
+ const currentAnswer = useModel(__props, "answer");
1495
+ /** 题目类型标签 */
1496
+ const typeLabel = computed(() => {
1497
+ return {
1498
+ single: "单选题",
1499
+ multiple: "多选题",
1500
+ judge: "判断题"
1501
+ }[props.quiz?.type ?? "single"] ?? "单选题";
1502
+ });
1503
+ /** 是否已选答案 */
1504
+ const hasAnswer = computed(() => {
1505
+ if (props.quiz?.type === "multiple") return multiAnswer.value.length > 0;
1506
+ return singleAnswer.value !== "";
1507
+ });
1508
+ watch(singleAnswer, (val) => {
1509
+ if (props.quiz?.type !== "multiple") currentAnswer.value = val;
1510
+ });
1511
+ watch(multiAnswer, (val) => {
1512
+ if (props.quiz?.type === "multiple") currentAnswer.value = [...val];
1513
+ });
1514
+ watch(() => props.quiz?.id, () => {
1515
+ singleAnswer.value = "";
1516
+ multiAnswer.value = [];
1517
+ });
1518
+ return (_ctx, _cache) => {
1519
+ const _component_NCheckbox = resolveComponent("NCheckbox");
1520
+ const _component_NCheckboxGroup = resolveComponent("NCheckboxGroup");
1521
+ const _component_NRadio = resolveComponent("NRadio");
1522
+ const _component_NRadioGroup = resolveComponent("NRadioGroup");
1523
+ const _component_NButton = resolveComponent("NButton");
1524
+ return openBlock(), createBlock(Transition, { name: "vp-quiz-fade" }, {
1525
+ default: withCtx(() => [_ctx.quiz ? (openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("div", _hoisted_2, [
1526
+ createCommentVNode(" 标题 "),
1527
+ createElementVNode("div", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(typeLabel.value), 1), createElementVNode("h3", _hoisted_5, toDisplayString(_ctx.quiz.title), 1)]),
1528
+ createCommentVNode(" 选项 "),
1529
+ createElementVNode("div", _hoisted_6, [_ctx.quiz.type === "multiple" ? (openBlock(), createBlock(_component_NCheckboxGroup, {
1530
+ key: 0,
1531
+ value: multiAnswer.value,
1532
+ "onUpdate:value": _cache[0] || (_cache[0] = ($event) => multiAnswer.value = $event),
1533
+ disabled: _ctx.showResult
1534
+ }, {
1535
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.quiz.options, (opt) => {
1536
+ return openBlock(), createElementBlock("div", {
1537
+ key: opt.key,
1538
+ class: "vp-quiz-option"
1539
+ }, [createVNode(_component_NCheckbox, {
1540
+ value: opt.key,
1541
+ label: `${opt.key}. ${opt.label}`
1542
+ }, null, 8, ["value", "label"])]);
1543
+ }), 128))]),
1544
+ _: 1
1545
+ }, 8, ["value", "disabled"])) : (openBlock(), createBlock(_component_NRadioGroup, {
1546
+ key: 1,
1547
+ value: singleAnswer.value,
1548
+ "onUpdate:value": _cache[1] || (_cache[1] = ($event) => singleAnswer.value = $event),
1549
+ disabled: _ctx.showResult
1550
+ }, {
1551
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.quiz.options, (opt) => {
1552
+ return openBlock(), createElementBlock("div", {
1553
+ key: opt.key,
1554
+ class: "vp-quiz-option"
1555
+ }, [createVNode(_component_NRadio, {
1556
+ value: opt.key,
1557
+ label: `${opt.key}. ${opt.label}`
1558
+ }, null, 8, ["value", "label"])]);
1559
+ }), 128))]),
1560
+ _: 1
1561
+ }, 8, ["value", "disabled"]))]),
1562
+ createCommentVNode(" 结果反馈 "),
1563
+ _ctx.showResult ? (openBlock(), createElementBlock("div", {
1564
+ key: 0,
1565
+ class: normalizeClass(["vp-quiz-result", _ctx.isCorrect ? "is-correct" : "is-wrong"])
1566
+ }, toDisplayString(_ctx.isCorrect ? "✓ 回答正确!" : "✗ 回答错误,请重试"), 3)) : createCommentVNode("v-if", true),
1567
+ createCommentVNode(" 操作按钮 "),
1568
+ createElementVNode("div", _hoisted_7, [!_ctx.showResult ? (openBlock(), createBlock(_component_NButton, {
1569
+ key: 0,
1570
+ type: "primary",
1571
+ disabled: !hasAnswer.value,
1572
+ onClick: _cache[2] || (_cache[2] = ($event) => _ctx.$emit("submit"))
1573
+ }, {
1574
+ default: withCtx(() => _cache[5] || (_cache[5] = [createTextVNode(" 提交答案 ", -1)])),
1575
+ _: 1,
1576
+ __: [5]
1577
+ }, 8, ["disabled"])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [!_ctx.isCorrect && _ctx.quiz.required ? (openBlock(), createBlock(_component_NButton, {
1578
+ key: 0,
1579
+ type: "warning",
1580
+ onClick: _cache[3] || (_cache[3] = ($event) => _ctx.$emit("retry"))
1581
+ }, {
1582
+ default: withCtx(() => _cache[6] || (_cache[6] = [createTextVNode(" 重新作答 ", -1)])),
1583
+ _: 1,
1584
+ __: [6]
1585
+ })) : createCommentVNode("v-if", true), _ctx.isCorrect || !_ctx.quiz.required ? (openBlock(), createBlock(_component_NButton, {
1586
+ key: 1,
1587
+ type: "primary",
1588
+ onClick: _cache[4] || (_cache[4] = ($event) => _ctx.$emit("close"))
1589
+ }, {
1590
+ default: withCtx(() => _cache[7] || (_cache[7] = [createTextVNode(" 继续学习 ", -1)])),
1591
+ _: 1,
1592
+ __: [7]
1593
+ })) : createCommentVNode("v-if", true)], 64))])
1594
+ ])])) : createCommentVNode("v-if", true)]),
1595
+ _: 1
1596
+ });
1597
+ };
1598
+ }
1599
+ });
1600
+
1601
+ //#endregion
1602
+ //#region src/components/C_VideoPlayer/components/QuizOverlay.vue
1603
+ var QuizOverlay_default = /* @__PURE__ */ export_helper_default(QuizOverlay_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-a8cd8fbf"]]);
1604
+
1605
+ //#endregion
1606
+ //#region src/components/C_VideoPlayer/components/WatermarkOverlay.vue?vue&type=script&setup=true&lang.ts
1607
+ var WatermarkOverlay_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1608
+ __name: "WatermarkOverlay",
1609
+ props: {
1610
+ visible: {
1611
+ type: Boolean,
1612
+ default: true
1613
+ },
1614
+ text: { default: "" },
1615
+ fontSize: { default: WATERMARK_DEFAULT_STYLE.fontSize },
1616
+ color: { default: WATERMARK_DEFAULT_STYLE.color },
1617
+ rotate: { default: WATERMARK_DEFAULT_STYLE.rotate },
1618
+ gap: { default: WATERMARK_DEFAULT_STYLE.gap }
1619
+ },
1620
+ setup(__props) {
1621
+ const props = __props;
1622
+ /** 重复次数:根据间距简单估算 */
1623
+ const repeatCount = computed(() => {
1624
+ const area = 1920 * 1080;
1625
+ const itemArea = props.gap * props.gap;
1626
+ return Math.ceil(area / itemArea);
1627
+ });
1628
+ /** 容器样式 */
1629
+ const containerStyle = computed(() => ({
1630
+ position: "absolute",
1631
+ inset: 0,
1632
+ overflow: "hidden",
1633
+ pointerEvents: "none",
1634
+ zIndex: 50,
1635
+ display: "flex",
1636
+ flexWrap: "wrap",
1637
+ alignContent: "flex-start",
1638
+ gap: `${props.gap}px`,
1639
+ padding: `${props.gap / 2}px`
1640
+ }));
1641
+ /** 水印文字样式 */
1642
+ const itemStyle = computed(() => ({
1643
+ fontSize: `${props.fontSize}px`,
1644
+ color: props.color,
1645
+ transform: `rotate(${props.rotate}deg)`,
1646
+ userSelect: "none",
1647
+ whiteSpace: "nowrap",
1648
+ lineHeight: 1
1649
+ }));
1650
+ return (_ctx, _cache) => {
1651
+ return _ctx.visible && _ctx.text ? (openBlock(), createElementBlock("div", {
1652
+ key: 0,
1653
+ class: "vp-watermark",
1654
+ style: normalizeStyle(containerStyle.value)
1655
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(repeatCount.value, (n) => {
1656
+ return openBlock(), createElementBlock("span", {
1657
+ key: n,
1658
+ class: "vp-watermark-item",
1659
+ style: normalizeStyle(itemStyle.value)
1660
+ }, toDisplayString(_ctx.text), 5);
1661
+ }), 128))], 4)) : createCommentVNode("v-if", true);
1662
+ };
1663
+ }
1664
+ });
1665
+
1666
+ //#endregion
1667
+ //#region src/components/C_VideoPlayer/components/WatermarkOverlay.vue
1668
+ var WatermarkOverlay_default = /* @__PURE__ */ export_helper_default(WatermarkOverlay_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-6928c31a"]]);
1669
+
1670
+ //#endregion
1671
+ //#region src/components/C_VideoPlayer/index.vue?vue&type=script&setup=true&lang.ts
1672
+ const showExtendedControls = true;
1673
+ /** 处理添加书签 */
1674
+ var index_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
1675
+ name: "C_VideoPlayer",
1676
+ __name: "index",
1677
+ props: {
1678
+ url: {},
1679
+ sourceType: {},
1680
+ width: {},
1681
+ height: {},
1682
+ poster: {},
1683
+ fluid: { type: Boolean },
1684
+ autoplay: { type: Boolean },
1685
+ autoplayMuted: { type: Boolean },
1686
+ loop: { type: Boolean },
1687
+ volume: {},
1688
+ startTime: {},
1689
+ playbackRates: {},
1690
+ defaultPlaybackRate: {},
1691
+ pip: { type: Boolean },
1692
+ fullscreen: { type: Boolean },
1693
+ cssFullscreen: { type: Boolean },
1694
+ miniPlayer: { type: Boolean },
1695
+ screenshot: { type: Boolean },
1696
+ keyboard: { type: Boolean },
1697
+ lang: {},
1698
+ qualityList: {},
1699
+ defaultQuality: {},
1700
+ chapters: {},
1701
+ subtitles: {},
1702
+ quizzes: {},
1703
+ bookmarks: {},
1704
+ thumbnail: {},
1705
+ onProgress: { type: Function },
1706
+ antiCheat: {},
1707
+ onAnalytics: { type: Function },
1708
+ playerOptions: {}
1709
+ },
1710
+ emits: [
1711
+ "ready",
1712
+ "stateChange",
1713
+ "timeUpdate",
1714
+ "ended",
1715
+ "error",
1716
+ "qualityChange",
1717
+ "rateChange",
1718
+ "fullscreenChange",
1719
+ "bookmarkChange",
1720
+ "quizAnswer",
1721
+ "chapterChange",
1722
+ "progressUpdate"
1723
+ ],
1724
+ setup(__props, { expose: __expose, emit: __emit }) {
1725
+ const props = __props;
1726
+ const emit = __emit;
1727
+ const core = usePlayerCore(props);
1728
+ const { containerRef, playerRef, playerState, currentTime, duration, isFullscreen } = core;
1729
+ const wrapperRef = ref(null);
1730
+ const playback = usePlaybackControl(playerRef);
1731
+ const urlRef = toRef(props, "url");
1732
+ const progressTracker = useProgressTracker(playerRef, currentTime, duration, urlRef, props.onProgress, props.antiCheat);
1733
+ const qualitySwitch = useQualitySwitch(playerRef, props.qualityList);
1734
+ const chaptersRef = computed(() => props.chapters ?? []);
1735
+ const chapters = useChapters(chaptersRef, currentTime, duration, playback.seek);
1736
+ const bookmarksState = useBookmarks(urlRef, currentTime, playback.seek, props.bookmarks);
1737
+ const antiCheatState = useAntiCheat(playerRef, currentTime, props.antiCheat);
1738
+ const subtitle = useSubtitle(playerRef, props.subtitles, currentTime);
1739
+ const quiz = useQuiz(playerRef, currentTime, computed(() => props.quizzes ?? []));
1740
+ const miniPlayer = useMiniPlayer(wrapperRef, computed(() => props.miniPlayer ?? false));
1741
+ const keyboard = useKeyboard(playerRef, wrapperRef, {
1742
+ enabled: props.keyboard !== false,
1743
+ onToggleFullscreen: () => {
1744
+ playerRef.value?.getFullscreen?.();
1745
+ }
1746
+ });
1747
+ let analyticsDestroy = null;
1748
+ function handleAddBookmark(note) {
1749
+ bookmarksState.addBookmark(note);
1750
+ emit("bookmarkChange", bookmarksState.bookmarks.value);
1751
+ }
1752
+ /** 处理测验提交 */
1753
+ function handleQuizSubmit() {
1754
+ const isCorrect = quiz.submitAnswer();
1755
+ const activeQuiz = quiz.activeQuiz.value;
1756
+ if (activeQuiz) emit("quizAnswer", activeQuiz.id, quiz.selectedAnswer.value, isCorrect);
1757
+ }
1758
+ /** 处理测验关闭 */
1759
+ function handleQuizClose() {
1760
+ quiz.closeQuiz();
1761
+ }
1762
+ watch(playerState, (state) => {
1763
+ emit("stateChange", state);
1764
+ });
1765
+ watch(isFullscreen, (val) => {
1766
+ emit("fullscreenChange", val);
1767
+ });
1768
+ watch(() => chapters.currentChapter.value, (chapter) => {
1769
+ if (chapter) emit("chapterChange", chapter);
1770
+ });
1771
+ onMounted(async () => {
1772
+ core.containerRef.value = containerRef.value;
1773
+ await nextTick();
1774
+ await core.initPlayer();
1775
+ const player = playerRef.value;
1776
+ if (!player) return;
1777
+ qualitySwitch.bindEvents(player);
1778
+ antiCheatState.bindEvents(player);
1779
+ subtitle.initSubtitles();
1780
+ keyboard.startListening();
1781
+ miniPlayer.initObserver();
1782
+ const restoreTime = progressTracker.restoreProgress();
1783
+ if (restoreTime > 0 && props.startTime === void 0) player.currentTime = restoreTime;
1784
+ player.on(Events.PLAY, () => {
1785
+ progressTracker.onPlay();
1786
+ });
1787
+ player.on(Events.PAUSE, () => {
1788
+ progressTracker.onPause();
1789
+ });
1790
+ player.on(Events.TIME_UPDATE, () => {
1791
+ progressTracker.onTimeUpdate();
1792
+ emit("timeUpdate", currentTime.value, duration.value);
1793
+ emit("progressUpdate", progressTracker.getProgressData());
1794
+ });
1795
+ player.on(Events.ENDED, () => {
1796
+ emit("ended");
1797
+ antiCheatState.markAsWatched();
1798
+ });
1799
+ player.on(Events.ERROR, () => {
1800
+ emit("error", /* @__PURE__ */ new Error("播放器错误"));
1801
+ });
1802
+ player.on(Events.READY, () => {
1803
+ emit("ready");
1804
+ });
1805
+ player.on(Events.AFTER_DEFINITION_CHANGE, (data) => {
1806
+ emit("qualityChange", data.to);
1807
+ });
1808
+ player.on(Events.RATE_CHANGE, (rate) => {
1809
+ emit("rateChange", rate);
1810
+ });
1811
+ if (props.onAnalytics) analyticsDestroy = createAnalyticsPlugin(player, props.onAnalytics).destroy;
1812
+ });
1813
+ onBeforeUnmount(() => {
1814
+ analyticsDestroy?.();
1815
+ keyboard.stopListening();
1816
+ miniPlayer.destroyObserver();
1817
+ progressTracker.stopTracking();
1818
+ core.destroyPlayer();
1819
+ });
1820
+ __expose({
1821
+ play: playback.play,
1822
+ pause: playback.pause,
1823
+ seek: playback.seek,
1824
+ setPlaybackRate: playback.setPlaybackRate,
1825
+ setVolume: playback.setVolume,
1826
+ switchQuality: qualitySwitch.switchQuality,
1827
+ getProgressData: progressTracker.getProgressData,
1828
+ destroy: core.destroyPlayer,
1829
+ getPlayerInstance: () => playerRef.value
1830
+ });
1831
+ return (_ctx, _cache) => {
1832
+ return openBlock(), createElementBlock("div", {
1833
+ ref_key: "wrapperRef",
1834
+ ref: wrapperRef,
1835
+ class: normalizeClass(["c-video-player", {
1836
+ "is-mini": unref(miniPlayer).isMiniMode.value,
1837
+ "is-fullscreen": unref(isFullscreen)
1838
+ }]),
1839
+ tabindex: "0"
1840
+ }, [
1841
+ createCommentVNode(" 播放器容器 "),
1842
+ createElementVNode("div", {
1843
+ ref_key: "containerRef",
1844
+ ref: containerRef,
1845
+ class: "c-video-player__container"
1846
+ }, null, 512),
1847
+ createCommentVNode(" 动态水印 "),
1848
+ createVNode(WatermarkOverlay_default, {
1849
+ visible: unref(antiCheatState).showWatermark.value,
1850
+ text: unref(antiCheatState).watermarkText.value
1851
+ }, null, 8, ["visible", "text"]),
1852
+ createCommentVNode(" 字幕渲染层 "),
1853
+ createVNode(SubtitleOverlay_default, {
1854
+ text: unref(subtitle).currentText.value,
1855
+ tracks: unref(subtitle).subtitleList.value,
1856
+ "active-language": unref(subtitle).activeLanguage.value,
1857
+ onSwitch: unref(subtitle).switchSubtitle,
1858
+ onClose: unref(subtitle).closeSubtitle
1859
+ }, null, 8, [
1860
+ "text",
1861
+ "tracks",
1862
+ "active-language",
1863
+ "onSwitch",
1864
+ "onClose"
1865
+ ]),
1866
+ createCommentVNode(" 测验弹窗 "),
1867
+ props.quizzes?.length ? (openBlock(), createBlock(QuizOverlay_default, {
1868
+ key: 0,
1869
+ answer: unref(quiz).selectedAnswer.value,
1870
+ "onUpdate:answer": _cache[0] || (_cache[0] = ($event) => unref(quiz).selectedAnswer.value = $event),
1871
+ quiz: unref(quiz).activeQuiz.value,
1872
+ "show-result": unref(quiz).showResult.value,
1873
+ "is-correct": unref(quiz).lastAnswerCorrect.value,
1874
+ onSubmit: handleQuizSubmit,
1875
+ onRetry: _cache[1] || (_cache[1] = ($event) => unref(quiz).retryQuiz()),
1876
+ onClose: handleQuizClose
1877
+ }, null, 8, [
1878
+ "answer",
1879
+ "quiz",
1880
+ "show-result",
1881
+ "is-correct"
1882
+ ])) : createCommentVNode("v-if", true),
1883
+ createCommentVNode(" 扩展控制栏(xgplayer 自带控制栏外的扩展) "),
1884
+ createVNode(ControlBar_default, { visible: showExtendedControls }, {
1885
+ left: withCtx(() => [createCommentVNode(" 章节标记 "), props.chapters?.length ? (openBlock(), createBlock(ChapterMarkers_default, {
1886
+ key: 0,
1887
+ chapters: chaptersRef.value,
1888
+ "current-chapter": unref(chapters).currentChapter.value,
1889
+ "current-index": unref(chapters).currentChapterIndex.value,
1890
+ onGoTo: unref(chapters).goToChapter
1891
+ }, null, 8, [
1892
+ "chapters",
1893
+ "current-chapter",
1894
+ "current-index",
1895
+ "onGoTo"
1896
+ ])) : createCommentVNode("v-if", true)]),
1897
+ right: withCtx(() => [createCommentVNode(" 书签面板 "), createVNode(BookmarkPanel_default, {
1898
+ bookmarks: unref(bookmarksState).bookmarks.value,
1899
+ onAdd: handleAddBookmark,
1900
+ onRemove: unref(bookmarksState).removeBookmark,
1901
+ onGoTo: unref(bookmarksState).goToBookmark
1902
+ }, null, 8, [
1903
+ "bookmarks",
1904
+ "onRemove",
1905
+ "onGoTo"
1906
+ ])]),
1907
+ _: 1
1908
+ }),
1909
+ createCommentVNode(" 小窗关闭按钮 "),
1910
+ unref(miniPlayer).isMiniMode.value ? (openBlock(), createElementBlock("div", {
1911
+ key: 1,
1912
+ class: "c-video-player__mini-close",
1913
+ onClick: _cache[2] || (_cache[2] = ($event) => unref(miniPlayer).closeMiniPlayer())
1914
+ }, " ✕ ")) : createCommentVNode("v-if", true),
1915
+ createCommentVNode(" 小窗点击回原位 "),
1916
+ unref(miniPlayer).isMiniMode.value ? (openBlock(), createElementBlock("div", {
1917
+ key: 2,
1918
+ class: "c-video-player__mini-back",
1919
+ onClick: _cache[3] || (_cache[3] = ($event) => unref(miniPlayer).scrollToPlayer())
1920
+ }, " 回到原位 ")) : createCommentVNode("v-if", true)
1921
+ ], 2);
1922
+ };
1923
+ }
1924
+ });
1925
+
1926
+ //#endregion
1927
+ //#region src/components/C_VideoPlayer/index.vue
1928
+ var C_VideoPlayer_default = /* @__PURE__ */ export_helper_default(index_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-9e9355b9"]]);
1929
+
1930
+ //#endregion
1931
+ export { STORAGE_KEYS as _, useProgressTracker as a, WATERMARK_DEFAULT_STYLE as b, DEFAULT_HEARTBEAT_INTERVAL as c, DEFAULT_VOLUME as d, KEYBOARD_SHORTCUTS as f, SOURCE_TYPE_MAP as g, SEEK_STEP as h, useChapters as i, DEFAULT_PLAYBACK_RATE as l, QUALITY_LABEL_MAP as m, useSubtitle as n, usePlaybackControl as o, PROGRESS_THROTTLE_INTERVAL as p, useBookmarks as r, usePlayerCore as s, C_VideoPlayer_default as t, DEFAULT_PLAYBACK_RATES as u, STORAGE_PREFIX as v, VOLUME_STEP as y };
1932
+ //# sourceMappingURL=C_VideoPlayer2.js.map