@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 @@
1
+ {"version":3,"file":"C_VideoPlayer2.js","names":["visible","text","tracks","activeLanguage","$emit","chapters","currentChapter","currentIndex","$emit","bookmarks","$emit","quiz","showResult","isCorrect","$emit","visible","text"],"sources":["../src/components/C_VideoPlayer/constants.ts","../src/components/C_VideoPlayer/composables/usePlayerCore.ts","../src/components/C_VideoPlayer/composables/usePlaybackControl.ts","../src/components/C_VideoPlayer/composables/useProgressTracker.ts","../src/components/C_VideoPlayer/composables/useQualitySwitch.ts","../src/components/C_VideoPlayer/composables/useChapters.ts","../src/components/C_VideoPlayer/composables/useBookmarks.ts","../src/components/C_VideoPlayer/composables/useAntiCheat.ts","../src/components/C_VideoPlayer/composables/useSubtitle.ts","../src/components/C_VideoPlayer/composables/useQuiz.ts","../src/components/C_VideoPlayer/composables/useMiniPlayer.ts","../src/components/C_VideoPlayer/composables/useKeyboard.ts","../src/components/C_VideoPlayer/plugins/analytics-reporter.ts","../src/components/C_VideoPlayer/components/ControlBar.vue","../src/components/C_VideoPlayer/components/ControlBar.vue","../src/components/C_VideoPlayer/components/ControlBar.vue","../src/components/C_VideoPlayer/components/SubtitleOverlay.vue","../src/components/C_VideoPlayer/components/SubtitleOverlay.vue","../src/components/C_VideoPlayer/components/SubtitleOverlay.vue","../src/components/C_VideoPlayer/components/ChapterMarkers.vue","../src/components/C_VideoPlayer/components/ChapterMarkers.vue","../src/components/C_VideoPlayer/components/ChapterMarkers.vue","../src/components/C_VideoPlayer/components/BookmarkPanel.vue","../src/components/C_VideoPlayer/components/BookmarkPanel.vue","../src/components/C_VideoPlayer/components/BookmarkPanel.vue","../src/components/C_VideoPlayer/components/QuizOverlay.vue","../src/components/C_VideoPlayer/components/QuizOverlay.vue","../src/components/C_VideoPlayer/components/QuizOverlay.vue","../src/components/C_VideoPlayer/components/WatermarkOverlay.vue","../src/components/C_VideoPlayer/components/WatermarkOverlay.vue","../src/components/C_VideoPlayer/components/WatermarkOverlay.vue","../src/components/C_VideoPlayer/index.vue","../src/components/C_VideoPlayer/index.vue","../src/components/C_VideoPlayer/index.vue"],"sourcesContent":["/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 视频播放器组件 - 常量配置\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport type { PlaybackRate, QualityLevel, VideoSourceType } from \"./types\";\r\n\r\n/** 默认播放倍速列表 */\r\nexport const DEFAULT_PLAYBACK_RATES: PlaybackRate[] = [\r\n 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0,\r\n];\r\n\r\n/** 默认播放倍速 */\r\nexport const DEFAULT_PLAYBACK_RATE: PlaybackRate = 1.0;\r\n\r\n/** 默认音量 */\r\nexport const DEFAULT_VOLUME = 0.75;\r\n\r\n/** 心跳上报间隔(毫秒) */\r\nexport const DEFAULT_HEARTBEAT_INTERVAL = 30_000;\r\n\r\n/** 进度上报节流间隔(毫秒) */\r\nexport const PROGRESS_THROTTLE_INTERVAL = 5_000;\r\n\r\n/** 清晰度标签映射 */\r\nexport const QUALITY_LABEL_MAP: Record<QualityLevel, string> = {\r\n \"360p\": \"流畅 360P\",\r\n \"480p\": \"标清 480P\",\r\n \"720p\": \"高清 720P\",\r\n \"1080p\": \"超清 1080P\",\r\n \"1440p\": \"2K 1440P\",\r\n \"4K\": \"4K 超高清\",\r\n};\r\n\r\n/** 视频源文件扩展名与类型映射 */\r\nexport const SOURCE_TYPE_MAP: Record<string, VideoSourceType> = {\r\n \".mp4\": \"mp4\",\r\n \".m3u8\": \"hls\",\r\n \".mpd\": \"dash\",\r\n \".flv\": \"flv\",\r\n};\r\n\r\n/** 快捷键映射描述 */\r\nexport const KEYBOARD_SHORTCUTS = {\r\n SPACE: \"播放/暂停\",\r\n ARROW_LEFT: \"快退 5 秒\",\r\n ARROW_RIGHT: \"快进 5 秒\",\r\n ARROW_UP: \"音量增加 10%\",\r\n ARROW_DOWN: \"音量减少 10%\",\r\n F: \"全屏/退出全屏\",\r\n M: \"静音/取消静音\",\r\n ESC: \"退出全屏\",\r\n} as const;\r\n\r\n/** 快进/快退步长(秒) */\r\nexport const SEEK_STEP = 5;\r\n\r\n/** 音量调节步长 */\r\nexport const VOLUME_STEP = 0.1;\r\n\r\n/** localStorage 存储键前缀 */\r\nexport const STORAGE_PREFIX = \"c_video_player_\";\r\n\r\n/** 存储键 */\r\nexport const STORAGE_KEYS = {\r\n /** 音量 */\r\n VOLUME: `${STORAGE_PREFIX}volume`,\r\n /** 倍速 */\r\n PLAYBACK_RATE: `${STORAGE_PREFIX}playback_rate`,\r\n /** 播放进度前缀(后接视频标识) */\r\n PROGRESS: `${STORAGE_PREFIX}progress_`,\r\n /** 书签前缀(后接视频标识) */\r\n BOOKMARKS: `${STORAGE_PREFIX}bookmarks_`,\r\n} as const;\r\n\r\n/** 水印默认样式 */\r\nexport const WATERMARK_DEFAULT_STYLE = {\r\n fontSize: 14,\r\n color: \"rgba(150, 150, 150, 0.15)\",\r\n rotate: -25,\r\n gap: 120,\r\n} as const;\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 播放器核心逻辑 - 初始化 / 销毁 / 状态管理\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, shallowRef, onBeforeUnmount, type Ref } from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport \"xgplayer/dist/index.min.css\";\r\nimport { DEFAULT_VOLUME, SOURCE_TYPE_MAP } from \"../constants\";\r\nimport type {\r\n PlayerInstance,\r\n PlayerState,\r\n VideoPlayerProps,\r\n IPlayerOptions,\r\n VideoSourceType,\r\n} from \"../types\";\r\n\r\n/** 根据 URL 推断视频源类型 */\r\nfunction detectSourceType(url: string): VideoSourceType {\r\n try {\r\n const { pathname } = new URL(url, location.href);\r\n const ext = pathname.slice(pathname.lastIndexOf(\".\")).toLowerCase();\r\n return SOURCE_TYPE_MAP[ext] ?? \"mp4\";\r\n } catch {\r\n return \"mp4\";\r\n }\r\n}\r\n\r\n/**\r\n * 播放器核心 composable\r\n * - 管理 xgplayer 实例的创建 & 销毁\r\n * - 暴露播放器状态、引用\r\n */\r\nexport function usePlayerCore(props: VideoPlayerProps) {\r\n /** 播放器 DOM 容器 */\r\n const containerRef: Ref<HTMLElement | null> = ref(null);\r\n\r\n /** xgplayer 实例(使用 shallowRef 避免深度响应) */\r\n const playerRef = shallowRef<PlayerInstance | null>(null);\r\n\r\n /** 播放器当前状态 */\r\n const playerState = ref<PlayerState>(\"idle\");\r\n\r\n /** 当前播放时间 */\r\n const currentTime = ref(0);\r\n\r\n /** 视频总时长 */\r\n const duration = ref(0);\r\n\r\n /** 是否全屏 */\r\n const isFullscreen = ref(false);\r\n\r\n /** 构建播放器核心容器配置 */\r\n function buildCoreConfig(): IPlayerOptions {\r\n return {\r\n el: containerRef.value!,\r\n url: props.url,\r\n width: props.width ?? \"100%\",\r\n height: props.height ?? \"100%\",\r\n fluid: props.fluid !== false,\r\n fitVideoSize: \"fixWidth\" as const,\r\n poster: props.poster ?? \"\",\r\n playsinline: true,\r\n videoAttributes: { crossOrigin: \"anonymous\" },\r\n lang: props.lang ?? \"zh-cn\",\r\n inactive: 3000,\r\n };\r\n }\r\n\r\n /** 构建播放行为配置 */\r\n function buildPlaybackConfig(): Partial<IPlayerOptions> {\r\n return {\r\n autoplay: props.autoplay ?? false,\r\n autoplayMuted: props.autoplayMuted ?? false,\r\n loop: props.loop ?? false,\r\n volume: props.volume ?? DEFAULT_VOLUME,\r\n startTime: props.startTime ?? 0,\r\n defaultPlaybackRate: props.defaultPlaybackRate ?? 1,\r\n playbackRate: props.playbackRates\r\n ? { list: props.playbackRates.map((r) => r) }\r\n : true,\r\n };\r\n }\r\n\r\n /** 构建功能开关配置 */\r\n function buildFeatureConfig(): Partial<IPlayerOptions> {\r\n return {\r\n pip: props.pip !== false,\r\n screenShot: props.screenshot ?? false,\r\n mini: props.miniPlayer ?? false,\r\n fullscreen: props.fullscreen !== false,\r\n cssFullscreen: props.cssFullscreen !== false,\r\n keyShortcut: props.keyboard !== false,\r\n };\r\n }\r\n\r\n /** 构建扩展配置(缩略图、清晰度等) */\r\n function applyExtendedConfig(config: IPlayerOptions): void {\r\n if (props.thumbnail) {\r\n config.thumbnail = {\r\n urls: props.thumbnail.urls,\r\n pic_num: props.thumbnail.picNum,\r\n col: props.thumbnail.col,\r\n row: props.thumbnail.row,\r\n width: props.thumbnail.width,\r\n height: props.thumbnail.height,\r\n };\r\n }\r\n\r\n if (props.qualityList?.length) {\r\n config.definition = {\r\n list: props.qualityList.map((q) => ({\r\n url: q.url,\r\n definition: q.label,\r\n text: { zh: q.label, en: q.label },\r\n bitrate: q.bitrate,\r\n })),\r\n defaultDefinition: props.defaultQuality ?? props.qualityList[0].label,\r\n };\r\n }\r\n\r\n if (props.playerOptions) {\r\n Object.assign(config, props.playerOptions);\r\n }\r\n }\r\n\r\n /** 构建完整 xgplayer 配置 */\r\n function buildConfig(): IPlayerOptions {\r\n const sourceType = props.sourceType ?? detectSourceType(props.url);\r\n const config: IPlayerOptions = {\r\n ...buildCoreConfig(),\r\n ...buildPlaybackConfig(),\r\n ...buildFeatureConfig(),\r\n };\r\n applyExtendedConfig(config);\r\n (config as Record<string, unknown>).__sourceType = sourceType;\r\n return config;\r\n }\r\n\r\n /** 初始化播放器 */\r\n async function initPlayer() {\r\n if (!containerRef.value) return;\r\n\r\n playerState.value = \"loading\";\r\n\r\n const config = buildConfig();\r\n const sourceType = (config as Record<string, unknown>)\r\n .__sourceType as VideoSourceType;\r\n delete (config as Record<string, unknown>).__sourceType;\r\n\r\n let PlayerConstructor: typeof import(\"xgplayer\").default;\r\n\r\n /* 根据源类型动态加载对应的播放器 */\r\n if (sourceType === \"hls\") {\r\n const { default: HlsPlayer } = await import(\"xgplayer-hls\");\r\n PlayerConstructor =\r\n HlsPlayer as unknown as typeof import(\"xgplayer\").default;\r\n } else {\r\n const { default: PresetPlayer } = await import(\"xgplayer\");\r\n PlayerConstructor = PresetPlayer;\r\n }\r\n\r\n const player = new PlayerConstructor(config);\r\n playerRef.value = player;\r\n\r\n /* 绑定事件 */\r\n player.on(Events.READY, () => {\r\n playerState.value = \"ready\";\r\n });\r\n\r\n player.on(Events.PLAY, () => {\r\n playerState.value = \"playing\";\r\n });\r\n\r\n player.on(Events.PAUSE, () => {\r\n playerState.value = \"paused\";\r\n });\r\n\r\n player.on(Events.ENDED, () => {\r\n playerState.value = \"ended\";\r\n });\r\n\r\n player.on(Events.ERROR, () => {\r\n playerState.value = \"error\";\r\n });\r\n\r\n player.on(Events.TIME_UPDATE, () => {\r\n currentTime.value = player.currentTime ?? 0;\r\n duration.value = player.duration ?? 0;\r\n });\r\n\r\n player.on(Events.DURATION_CHANGE, () => {\r\n duration.value = player.duration ?? 0;\r\n });\r\n\r\n player.on(Events.FULLSCREEN_CHANGE, (isFS: boolean) => {\r\n isFullscreen.value = isFS;\r\n });\r\n }\r\n\r\n /** 销毁播放器 */\r\n function destroyPlayer() {\r\n const player = playerRef.value;\r\n if (player) {\r\n player.destroy();\r\n playerRef.value = null;\r\n }\r\n playerState.value = \"idle\";\r\n currentTime.value = 0;\r\n duration.value = 0;\r\n }\r\n\r\n onBeforeUnmount(() => {\r\n destroyPlayer();\r\n });\r\n\r\n return {\r\n containerRef,\r\n playerRef,\r\n playerState,\r\n currentTime,\r\n duration,\r\n isFullscreen,\r\n initPlayer,\r\n destroyPlayer,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 播放控制 - 播放/暂停/倍速/音量/跳转\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, watch, type ShallowRef } from \"vue\";\r\nimport { STORAGE_KEYS } from \"../constants\";\r\nimport type { PlayerInstance } from \"../types\";\r\n\r\n/**\r\n * 播放控制 composable\r\n * - 播放 / 暂停 / 跳转\r\n * - 音量调节(本地持久化)\r\n * - 倍速调节(本地持久化)\r\n */\r\nexport function usePlaybackControl(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n) {\r\n const volume = ref(_getStoredVolume());\r\n const playbackRate = ref(_getStoredRate());\r\n const isMuted = ref(false);\r\n\r\n /** 播放 */\r\n function play() {\r\n playerRef.value?.play();\r\n }\r\n\r\n /** 暂停 */\r\n function pause() {\r\n playerRef.value?.pause();\r\n }\r\n\r\n /** 切换播放/暂停 */\r\n function togglePlay() {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n if (player.paused) {\r\n player.play();\r\n } else {\r\n player.pause();\r\n }\r\n }\r\n\r\n /** 跳转到指定时间(秒) */\r\n function seek(time: number) {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n const safeTime = Math.max(0, Math.min(time, player.duration ?? 0));\r\n player.currentTime = safeTime;\r\n }\r\n\r\n /** 设置音量 0-1 */\r\n function setVolume(val: number) {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n const safeVolume = Math.max(0, Math.min(1, val));\r\n player.volume = safeVolume;\r\n volume.value = safeVolume;\r\n isMuted.value = safeVolume === 0;\r\n localStorage.setItem(STORAGE_KEYS.VOLUME, String(safeVolume));\r\n }\r\n\r\n /** 切换静音 */\r\n function toggleMute() {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n if (isMuted.value) {\r\n const restored = volume.value > 0 ? volume.value : 0.5;\r\n setVolume(restored);\r\n } else {\r\n player.volume = 0;\r\n isMuted.value = true;\r\n }\r\n }\r\n\r\n /** 设置倍速 */\r\n function setPlaybackRate(rate: number) {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n player.playbackRate = rate;\r\n playbackRate.value = rate;\r\n localStorage.setItem(STORAGE_KEYS.PLAYBACK_RATE, String(rate));\r\n }\r\n\r\n /** 同步播放器实例的初始状态 */\r\n function syncInitialState() {\r\n const player = playerRef.value;\r\n if (!player) return;\r\n player.volume = volume.value;\r\n player.playbackRate = playbackRate.value;\r\n isMuted.value = volume.value === 0;\r\n }\r\n\r\n /** 监听播放器实例变化,自动同步状态 */\r\n watch(playerRef, (player) => {\r\n if (player) syncInitialState();\r\n });\r\n\r\n return {\r\n volume,\r\n playbackRate,\r\n isMuted,\r\n play,\r\n pause,\r\n togglePlay,\r\n seek,\r\n setVolume,\r\n toggleMute,\r\n setPlaybackRate,\r\n };\r\n}\r\n\r\n/* ======================== 内部工具函数 ======================== */\r\n\r\n/** 从 localStorage 读取已存储的音量 */\r\nfunction _getStoredVolume(): number {\r\n const stored = localStorage.getItem(STORAGE_KEYS.VOLUME);\r\n if (stored !== null) {\r\n const val = parseFloat(stored);\r\n if (!isNaN(val) && val >= 0 && val <= 1) return val;\r\n }\r\n return 0.75;\r\n}\r\n\r\n/** 从 localStorage 读取已存储的倍速 */\r\nfunction _getStoredRate(): number {\r\n const stored = localStorage.getItem(STORAGE_KEYS.PLAYBACK_RATE);\r\n if (stored !== null) {\r\n const val = parseFloat(stored);\r\n if (!isNaN(val) && val > 0) return val;\r\n }\r\n return 1.0;\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 学习进度追踪 & 心跳上报\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, watch, onBeforeUnmount, type Ref, type ShallowRef } from \"vue\";\r\nimport {\r\n DEFAULT_HEARTBEAT_INTERVAL,\r\n PROGRESS_THROTTLE_INTERVAL,\r\n STORAGE_KEYS,\r\n} from \"../constants\";\r\nimport type {\r\n PlayerInstance,\r\n ProgressData,\r\n ProgressReporter,\r\n AntiCheatConfig,\r\n} from \"../types\";\r\n\r\n/**\r\n * 学习进度追踪 composable\r\n * - 精确记录已观看时长(排除暂停和拖动时间)\r\n * - 节流上报进度\r\n * - 心跳上报\r\n * - 页面关闭时使用 sendBeacon 兜底上报\r\n * - 断点续播 localStorage 存储\r\n */\r\nexport function useProgressTracker(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n currentTime: Ref<number>,\r\n duration: Ref<number>,\r\n url: Ref<string>,\r\n onProgress?: ProgressReporter,\r\n antiCheat?: AntiCheatConfig,\r\n) {\r\n /** 累计实际观看时长(秒) */\r\n const watchedDuration = ref(0);\r\n\r\n /** 完成百分比 */\r\n const completionPercent = ref(0);\r\n\r\n /** 上次记录时间 */\r\n let lastRecordTime = 0;\r\n\r\n /** 是否正在播放 */\r\n let isPlaying = false;\r\n\r\n /** 心跳定时器 */\r\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n /** 进度节流定时器 */\r\n let throttleTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n /** 视频标识(用于 localStorage key) */\r\n function getVideoKey(): string {\r\n return STORAGE_KEYS.PROGRESS + encodeURIComponent(url.value);\r\n }\r\n\r\n /** 恢复进度 */\r\n function restoreProgress(): number {\r\n try {\r\n const stored = localStorage.getItem(getVideoKey());\r\n if (stored) {\r\n const data: ProgressData = JSON.parse(stored);\r\n watchedDuration.value = data.watchedDuration ?? 0;\r\n completionPercent.value = data.completionPercent ?? 0;\r\n return data.currentTime ?? 0;\r\n }\r\n } catch {\r\n /* 忽略解析失败 */\r\n }\r\n return 0;\r\n }\r\n\r\n /** 获取当前进度数据 */\r\n function getProgressData(): ProgressData {\r\n return {\r\n currentTime: currentTime.value,\r\n duration: duration.value,\r\n watchedDuration: watchedDuration.value,\r\n completionPercent: completionPercent.value,\r\n updatedAt: Date.now(),\r\n };\r\n }\r\n\r\n /** 保存进度到 localStorage */\r\n function saveProgress() {\r\n try {\r\n localStorage.setItem(getVideoKey(), JSON.stringify(getProgressData()));\r\n } catch {\r\n /* localStorage 可能已满,忽略 */\r\n }\r\n }\r\n\r\n /** 上报进度(节流) */\r\n function reportProgress() {\r\n const data = getProgressData();\r\n onProgress?.(data);\r\n saveProgress();\r\n }\r\n\r\n /** 计算观看时长 */\r\n function updateWatchedDuration() {\r\n if (!isPlaying) return;\r\n const now = performance.now();\r\n if (lastRecordTime > 0) {\r\n const delta = (now - lastRecordTime) / 1000;\r\n /* 防止异常值(如浏览器后台导致的大跳跃) */\r\n if (delta > 0 && delta < 5) {\r\n watchedDuration.value += delta;\r\n }\r\n }\r\n lastRecordTime = now;\r\n\r\n /* 计算完成百分比 */\r\n if (duration.value > 0) {\r\n completionPercent.value = Math.min(\r\n 100,\r\n Math.round((watchedDuration.value / duration.value) * 100),\r\n );\r\n }\r\n }\r\n\r\n /** 心跳上报 */\r\n function startHeartbeat() {\r\n const interval = antiCheat?.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;\r\n heartbeatTimer = setInterval(() => {\r\n if (isPlaying) {\r\n antiCheat?.onHeartbeat?.(getProgressData());\r\n }\r\n }, interval);\r\n }\r\n\r\n /** 启动节流上报 */\r\n function startThrottleReport() {\r\n throttleTimer = setInterval(() => {\r\n if (isPlaying) {\r\n reportProgress();\r\n }\r\n }, PROGRESS_THROTTLE_INTERVAL);\r\n }\r\n\r\n /** 页面关闭兜底上报 */\r\n function handleBeforeUnload() {\r\n const data = getProgressData();\r\n saveProgress();\r\n /* 使用 sendBeacon 确保数据不丢失 */\r\n if (onProgress) {\r\n try {\r\n navigator.sendBeacon?.(\"/api/video/progress\", JSON.stringify(data));\r\n } catch {\r\n /* sendBeacon 可能不可用 */\r\n }\r\n }\r\n }\r\n\r\n /** 页面可见性变化处理 */\r\n function handleVisibilityChange() {\r\n if (document.hidden) {\r\n /* 离开页面时保存并上报 */\r\n updateWatchedDuration();\r\n reportProgress();\r\n isPlaying = false;\r\n } else {\r\n /* 回到页面时恢复计时 */\r\n const player = playerRef.value;\r\n if (player && !player.paused) {\r\n isPlaying = true;\r\n lastRecordTime = performance.now();\r\n }\r\n }\r\n }\r\n\r\n /** 初始化追踪 */\r\n function startTracking() {\r\n startHeartbeat();\r\n startThrottleReport();\r\n window.addEventListener(\"beforeunload\", handleBeforeUnload);\r\n document.addEventListener(\"visibilitychange\", handleVisibilityChange);\r\n }\r\n\r\n /** 停止追踪 */\r\n function stopTracking() {\r\n isPlaying = false;\r\n if (heartbeatTimer) {\r\n clearInterval(heartbeatTimer);\r\n heartbeatTimer = null;\r\n }\r\n if (throttleTimer) {\r\n clearInterval(throttleTimer);\r\n throttleTimer = null;\r\n }\r\n window.removeEventListener(\"beforeunload\", handleBeforeUnload);\r\n document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\r\n saveProgress();\r\n }\r\n\r\n /** 标记开始播放 */\r\n function onPlay() {\r\n isPlaying = true;\r\n lastRecordTime = performance.now();\r\n }\r\n\r\n /** 标记暂停 */\r\n function onPause() {\r\n updateWatchedDuration();\r\n isPlaying = false;\r\n lastRecordTime = 0;\r\n saveProgress();\r\n }\r\n\r\n /** timeupdate 回调 */\r\n function onTimeUpdate() {\r\n updateWatchedDuration();\r\n }\r\n\r\n /** 监听播放器就绪后恢复进度 */\r\n watch(playerRef, (player) => {\r\n if (player) {\r\n startTracking();\r\n }\r\n });\r\n\r\n onBeforeUnmount(() => {\r\n stopTracking();\r\n });\r\n\r\n return {\r\n watchedDuration,\r\n completionPercent,\r\n getProgressData,\r\n restoreProgress,\r\n saveProgress,\r\n startTracking,\r\n stopTracking,\r\n onPlay,\r\n onPause,\r\n onTimeUpdate,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 清晰度切换\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, type ShallowRef } from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport type { PlayerInstance, QualityDefinition, QualityLevel } from \"../types\";\r\n\r\n/**\r\n * 清晰度切换 composable\r\n * - 监听 xgplayer 原生清晰度切换事件\r\n * - 提供编程式清晰度切换\r\n */\r\nexport function useQualitySwitch(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n qualityList: QualityDefinition[] = [],\r\n) {\r\n const currentQuality = ref<QualityLevel | null>(null);\r\n const isSwitching = ref(false);\r\n\r\n /** 初始化:监听清晰度变化事件 */\r\n function bindEvents(player: PlayerInstance) {\r\n player.on(Events.AFTER_DEFINITION_CHANGE, (data: { to: string }) => {\r\n currentQuality.value = data.to as QualityLevel;\r\n isSwitching.value = false;\r\n });\r\n\r\n player.on(Events.BEFORE_DEFINITION_CHANGE, () => {\r\n isSwitching.value = true;\r\n });\r\n }\r\n\r\n /** 编程式切换清晰度 */\r\n function switchQuality(quality: QualityLevel) {\r\n const player = playerRef.value;\r\n if (!player || !qualityList.length) return;\r\n\r\n const target = qualityList.find((q) => q.label === quality);\r\n if (!target) {\r\n console.warn(`[C_VideoPlayer] 未找到清晰度: ${quality}`);\r\n return;\r\n }\r\n\r\n isSwitching.value = true;\r\n /* xgplayer definition 切换 */\r\n player.changeDefinition?.({\r\n url: target.url,\r\n definition: target.label,\r\n text: { zh: target.label, en: target.label },\r\n bitrate: target.bitrate,\r\n });\r\n }\r\n\r\n return {\r\n currentQuality,\r\n isSwitching,\r\n switchQuality,\r\n bindEvents,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 章节标记\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { computed, type Ref } from \"vue\";\r\nimport type { Chapter } from \"../types\";\r\n\r\n/**\r\n * 章节标记 composable\r\n * - 根据当前播放时间计算所在章节\r\n * - 提供跳转到指定章节的能力\r\n * - 计算进度条上章节标记的位置\r\n */\r\nexport function useChapters(\r\n chapters: Ref<Chapter[]>,\r\n currentTime: Ref<number>,\r\n duration: Ref<number>,\r\n seekFn: (time: number) => void,\r\n) {\r\n /** 当前所在章节 */\r\n const currentChapter = computed<Chapter | null>(() => {\r\n if (!chapters.value.length) return null;\r\n const time = currentTime.value;\r\n return (\r\n chapters.value.find((ch) => time >= ch.startTime && time < ch.endTime) ??\r\n null\r\n );\r\n });\r\n\r\n /** 当前章节索引 */\r\n const currentChapterIndex = computed(() => {\r\n if (!currentChapter.value) return -1;\r\n return chapters.value.findIndex((ch) => ch.id === currentChapter.value!.id);\r\n });\r\n\r\n /** 章节在进度条上的位置百分比 */\r\n const chapterMarkers = computed(() => {\r\n if (!duration.value || !chapters.value.length) return [];\r\n return chapters.value.map((ch) => ({\r\n ...ch,\r\n startPercent: (ch.startTime / duration.value) * 100,\r\n endPercent: (ch.endTime / duration.value) * 100,\r\n widthPercent: ((ch.endTime - ch.startTime) / duration.value) * 100,\r\n }));\r\n });\r\n\r\n /** 跳转到指定章节 */\r\n function goToChapter(chapterId: string) {\r\n const chapter = chapters.value.find((ch) => ch.id === chapterId);\r\n if (chapter) {\r\n seekFn(chapter.startTime);\r\n }\r\n }\r\n\r\n /** 跳转到上一章 */\r\n function prevChapter() {\r\n const idx = currentChapterIndex.value;\r\n if (idx > 0) {\r\n seekFn(chapters.value[idx - 1].startTime);\r\n }\r\n }\r\n\r\n /** 跳转到下一章 */\r\n function nextChapter() {\r\n const idx = currentChapterIndex.value;\r\n if (idx >= 0 && idx < chapters.value.length - 1) {\r\n seekFn(chapters.value[idx + 1].startTime);\r\n }\r\n }\r\n\r\n return {\r\n currentChapter,\r\n currentChapterIndex,\r\n chapterMarkers,\r\n goToChapter,\r\n prevChapter,\r\n nextChapter,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 书签笔记\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, type Ref } from \"vue\";\r\nimport { STORAGE_KEYS } from \"../constants\";\r\nimport type { Bookmark } from \"../types\";\r\n\r\n/**\r\n * 书签笔记 composable\r\n * - 添加 / 删除 / 更新书签\r\n * - localStorage 持久化\r\n * - 按时间排序\r\n */\r\nexport function useBookmarks(\r\n url: Ref<string>,\r\n currentTime: Ref<number>,\r\n seekFn: (time: number) => void,\r\n initialBookmarks: Bookmark[] = [],\r\n) {\r\n const bookmarks = ref<Bookmark[]>([...initialBookmarks]);\r\n\r\n /** 存储 key */\r\n function getStorageKey(): string {\r\n return STORAGE_KEYS.BOOKMARKS + encodeURIComponent(url.value);\r\n }\r\n\r\n /** 从 localStorage 恢复书签 */\r\n function restoreBookmarks() {\r\n try {\r\n const stored = localStorage.getItem(getStorageKey());\r\n if (stored) {\r\n const parsed: Bookmark[] = JSON.parse(stored);\r\n if (Array.isArray(parsed)) {\r\n bookmarks.value = parsed;\r\n }\r\n }\r\n } catch {\r\n /* 忽略解析错误 */\r\n }\r\n }\r\n\r\n /** 保存书签到 localStorage */\r\n function saveBookmarks() {\r\n try {\r\n localStorage.setItem(getStorageKey(), JSON.stringify(bookmarks.value));\r\n } catch {\r\n /* 忽略存储失败 */\r\n }\r\n }\r\n\r\n /** 排序(按时间升序) */\r\n function sortBookmarks() {\r\n bookmarks.value.sort((a, b) => a.time - b.time);\r\n }\r\n\r\n /** 添加书签 */\r\n function addBookmark(note: string = \"\"): Bookmark {\r\n const bookmark: Bookmark = {\r\n id: `bm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\r\n time: currentTime.value,\r\n note,\r\n createdAt: Date.now(),\r\n };\r\n bookmarks.value.push(bookmark);\r\n sortBookmarks();\r\n saveBookmarks();\r\n return bookmark;\r\n }\r\n\r\n /** 删除书签 */\r\n function removeBookmark(id: string) {\r\n const index = bookmarks.value.findIndex((b) => b.id === id);\r\n if (index !== -1) {\r\n bookmarks.value.splice(index, 1);\r\n saveBookmarks();\r\n }\r\n }\r\n\r\n /** 更新书签备注 */\r\n function updateBookmark(id: string, note: string) {\r\n const bookmark = bookmarks.value.find((b) => b.id === id);\r\n if (bookmark) {\r\n bookmark.note = note;\r\n saveBookmarks();\r\n }\r\n }\r\n\r\n /** 跳转到书签位置 */\r\n function goToBookmark(id: string) {\r\n const bookmark = bookmarks.value.find((b) => b.id === id);\r\n if (bookmark) {\r\n seekFn(bookmark.time);\r\n }\r\n }\r\n\r\n /** 清空所有书签 */\r\n function clearBookmarks() {\r\n bookmarks.value = [];\r\n saveBookmarks();\r\n }\r\n\r\n /* 初始化时恢复 */\r\n if (!initialBookmarks.length) {\r\n restoreBookmarks();\r\n }\r\n\r\n return {\r\n bookmarks,\r\n addBookmark,\r\n removeBookmark,\r\n updateBookmark,\r\n goToBookmark,\r\n clearBookmarks,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 防作弊 - 防跳播 / 焦点检测 / 水印\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, onBeforeUnmount, type ShallowRef, type Ref } from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport type { PlayerInstance, AntiCheatConfig } from \"../types\";\r\n\r\n/**\r\n * 防作弊 composable\r\n * - 首次观看防跳播:禁止向未观看区域拖动进度条\r\n * - 焦点检测:切出页面自动暂停\r\n * - 水印标记开关(UI 通过 WatermarkOverlay.vue 组件渲染)\r\n */\r\nexport function useAntiCheat(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n currentTime: Ref<number>,\r\n config?: AntiCheatConfig,\r\n) {\r\n /** 已观看的最远位置(秒) */\r\n const maxWatchedTime = ref(0);\r\n\r\n /** 是否为首次观看(有未完整看过的区段) */\r\n const isFirstWatch = ref(true);\r\n\r\n /** 是否处于焦点外(页面不可见) */\r\n const isBlurred = ref(false);\r\n\r\n /** 是否启用水印 */\r\n const showWatermark = ref(config?.watermark ?? false);\r\n\r\n /** 水印文本 */\r\n const watermarkText = ref(config?.watermarkText ?? \"\");\r\n\r\n /** 记录播放的最远位置 */\r\n function updateMaxWatched(time: number) {\r\n if (time > maxWatchedTime.value) {\r\n maxWatchedTime.value = time;\r\n }\r\n }\r\n\r\n /** 防跳播拦截 */\r\n function handleSeeking(player: PlayerInstance) {\r\n if (!config?.preventSeekOnFirstWatch || !isFirstWatch.value) return;\r\n\r\n const seekTarget = player.currentTime ?? 0;\r\n /* 只允许向已观看区域拖动 */\r\n if (seekTarget > maxWatchedTime.value + 2) {\r\n /* 回弹到最远观看位置 */\r\n player.currentTime = maxWatchedTime.value;\r\n }\r\n }\r\n\r\n /** 焦点检测 - 页面可见性变化 */\r\n function handleVisibilityChange() {\r\n if (!config?.focusDetection) return;\r\n\r\n const player = playerRef.value;\r\n if (!player) return;\r\n\r\n if (document.hidden) {\r\n isBlurred.value = true;\r\n if (!player.paused) {\r\n player.pause();\r\n }\r\n } else {\r\n isBlurred.value = false;\r\n }\r\n }\r\n\r\n /** 绑定事件 */\r\n function bindEvents(player: PlayerInstance) {\r\n /* 防跳播 */\r\n if (config?.preventSeekOnFirstWatch) {\r\n player.on(Events.SEEKING, () => handleSeeking(player));\r\n }\r\n\r\n /* 更新最远观看位置 */\r\n player.on(Events.TIME_UPDATE, () => {\r\n updateMaxWatched(currentTime.value);\r\n });\r\n\r\n /* 焦点检测 */\r\n if (config?.focusDetection) {\r\n document.addEventListener(\"visibilitychange\", handleVisibilityChange);\r\n }\r\n }\r\n\r\n /** 标记已完整看过(允许自由拖动) */\r\n function markAsWatched() {\r\n isFirstWatch.value = false;\r\n }\r\n\r\n /** 设置水印文本 */\r\n function setWatermarkText(text: string) {\r\n watermarkText.value = text;\r\n }\r\n\r\n onBeforeUnmount(() => {\r\n document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\r\n });\r\n\r\n return {\r\n maxWatchedTime,\r\n isFirstWatch,\r\n isBlurred,\r\n showWatermark,\r\n watermarkText,\r\n bindEvents,\r\n markAsWatched,\r\n setWatermarkText,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 字幕管理(原生 VTT 解析 + Overlay 渲染)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, computed, type Ref, type ShallowRef } from \"vue\";\r\nimport type { PlayerInstance, SubtitleTrack } from \"../types\";\r\n\r\n/** 解析后的字幕条目 */\r\nexport interface SubtitleCue {\r\n /** 起始时间(秒) */\r\n start: number;\r\n /** 结束时间(秒) */\r\n end: number;\r\n /** 字幕文本 */\r\n text: string;\r\n}\r\n\r\n/** 将 \"HH:MM:SS.mmm\" 或 \"MM:SS.mmm\" 格式转为秒 */\r\nfunction parseVttTime(raw: string): number {\r\n const parts = raw.trim().split(\":\");\r\n if (parts.length === 3) {\r\n return Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2]);\r\n }\r\n /* MM:SS.mmm */\r\n return Number(parts[0]) * 60 + Number(parts[1]);\r\n}\r\n\r\n/** 解析 WebVTT 文本为 cue 列表 */\r\nfunction parseVtt(vttText: string): SubtitleCue[] {\r\n const cues: SubtitleCue[] = [];\r\n /* 按空行分割 block */\r\n const blocks = vttText.trim().split(/\\n\\s*\\n/);\r\n\r\n for (const block of blocks) {\r\n const lines = block.trim().split(\"\\n\");\r\n /* 查找包含 \"-->\" 的行 */\r\n const timeLineIdx = lines.findIndex((l) => l.includes(\"-->\"));\r\n if (timeLineIdx === -1) continue;\r\n\r\n const [startRaw, endRaw] = lines[timeLineIdx].split(\"-->\");\r\n if (!startRaw || !endRaw) continue;\r\n\r\n const start = parseVttTime(startRaw);\r\n const end = parseVttTime(endRaw.split(/\\s/)[0]); /* 去掉 position 等后缀 */\r\n const text = lines\r\n .slice(timeLineIdx + 1)\r\n .join(\"\\n\")\r\n .replace(/<[^>]+>/g, \"\") /* 去 HTML 标签 */\r\n .trim();\r\n\r\n if (text) {\r\n cues.push({ start, end, text });\r\n }\r\n }\r\n\r\n return cues;\r\n}\r\n\r\n/**\r\n * 字幕管理 composable\r\n * - fetch VTT → 解析 → 按当前播放时间匹配 cue\r\n * - 提供切换 / 关闭能力\r\n */\r\nexport function useSubtitle(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n subtitles: SubtitleTrack[] = [],\r\n currentTime: Ref<number> = ref(0),\r\n) {\r\n /** 当前激活的字幕语言(null = 关闭) */\r\n const activeLanguage = ref<string | null>(null);\r\n\r\n /** 字幕列表 */\r\n const subtitleList = ref<SubtitleTrack[]>([...subtitles]);\r\n\r\n /** 已加载的 cue 数据:language → cues */\r\n const cueMap = ref<Record<string, SubtitleCue[]>>({});\r\n\r\n /** 是否正在加载 */\r\n const isLoading = ref(false);\r\n\r\n /** 当前应显示的字幕文本 */\r\n const currentText = computed(() => {\r\n if (!activeLanguage.value) return \"\";\r\n const cues = cueMap.value[activeLanguage.value];\r\n if (!cues?.length) return \"\";\r\n const t = currentTime.value;\r\n const cue = cues.find((c) => t >= c.start && t < c.end);\r\n return cue?.text ?? \"\";\r\n });\r\n\r\n /** 是否有字幕可用 */\r\n const hasSubtitles = computed(() => subtitleList.value.length > 0);\r\n\r\n /** 加载指定语言的 VTT 文件 */\r\n async function loadTrack(language: string): Promise<SubtitleCue[]> {\r\n /* 已缓存 */\r\n if (cueMap.value[language]) return cueMap.value[language];\r\n\r\n const track = subtitleList.value.find((s) => s.language === language);\r\n if (!track) return [];\r\n\r\n isLoading.value = true;\r\n try {\r\n const resp = await fetch(track.src);\r\n if (!resp.ok) {\r\n console.warn(\r\n `[C_VideoPlayer] 字幕加载失败: ${track.src} (${resp.status})`,\r\n );\r\n return [];\r\n }\r\n const text = await resp.text();\r\n const cues = parseVtt(text);\r\n cueMap.value[language] = cues;\r\n return cues;\r\n } catch (e) {\r\n console.warn(\"[C_VideoPlayer] 字幕加载异常:\", e);\r\n return [];\r\n } finally {\r\n isLoading.value = false;\r\n }\r\n }\r\n\r\n /** 初始化:加载默认字幕 */\r\n async function initSubtitles() {\r\n if (!subtitleList.value.length) return;\r\n const defaultTrack =\r\n subtitleList.value.find((s) => s.default) ?? subtitleList.value[0];\r\n await loadTrack(defaultTrack.language);\r\n activeLanguage.value = defaultTrack.language;\r\n }\r\n\r\n /** 切换字幕语言 */\r\n async function switchSubtitle(language: string) {\r\n const track = subtitleList.value.find((s) => s.language === language);\r\n if (!track) {\r\n console.warn(`[C_VideoPlayer] 未找到字幕轨道: ${language}`);\r\n return;\r\n }\r\n await loadTrack(language);\r\n activeLanguage.value = language;\r\n }\r\n\r\n /** 关闭字幕 */\r\n function closeSubtitle() {\r\n activeLanguage.value = null;\r\n }\r\n\r\n /** 切换字幕开/关 */\r\n function toggleSubtitle() {\r\n if (activeLanguage.value) {\r\n closeSubtitle();\r\n } else {\r\n const defaultTrack =\r\n subtitleList.value.find((s) => s.default) ?? subtitleList.value[0];\r\n if (defaultTrack) {\r\n switchSubtitle(defaultTrack.language);\r\n }\r\n }\r\n }\r\n\r\n return {\r\n activeLanguage,\r\n subtitleList,\r\n currentText,\r\n hasSubtitles,\r\n isLoading,\r\n initSubtitles,\r\n switchSubtitle,\r\n closeSubtitle,\r\n toggleSubtitle,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 视频内测验弹窗\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, watch, type Ref, type ShallowRef } from \"vue\";\r\nimport type { PlayerInstance, VideoQuiz } from \"../types\";\r\n\r\n/**\r\n * 视频内测验 composable\r\n * - 在指定时间点触发测验弹窗\r\n * - 暂停视频等待作答\r\n * - 判断答案是否正确\r\n * - 支持必须答对才能继续\r\n */\r\nexport function useQuiz(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n currentTime: Ref<number>,\r\n quizzes: Ref<VideoQuiz[]>,\r\n) {\r\n /** 当前显示的测验 */\r\n const activeQuiz = ref<VideoQuiz | null>(null);\r\n\r\n /** 已完成的测验 ID 集合 */\r\n const completedQuizIds = ref<Set<string>>(new Set());\r\n\r\n /** 当前选中的答案 */\r\n const selectedAnswer = ref<string | string[]>(\"\");\r\n\r\n /** 是否显示结果反馈 */\r\n const showResult = ref(false);\r\n\r\n /** 上次作答是否正确 */\r\n const lastAnswerCorrect = ref(false);\r\n\r\n /** 检查时间容差(秒) */\r\n const TIME_TOLERANCE = 1;\r\n\r\n /** 检查是否需要触发测验 */\r\n function checkQuizTrigger() {\r\n if (activeQuiz.value) return; /* 已有测验在显示 */\r\n if (!quizzes.value.length) return;\r\n\r\n const time = currentTime.value;\r\n const quiz = quizzes.value.find(\r\n (q) =>\r\n !completedQuizIds.value.has(q.id) &&\r\n Math.abs(time - q.triggerTime) < TIME_TOLERANCE,\r\n );\r\n\r\n if (quiz) {\r\n triggerQuiz(quiz);\r\n }\r\n }\r\n\r\n /** 触发测验 */\r\n function triggerQuiz(quiz: VideoQuiz) {\r\n activeQuiz.value = quiz;\r\n selectedAnswer.value = quiz.type === \"multiple\" ? [] : \"\";\r\n showResult.value = false;\r\n lastAnswerCorrect.value = false;\r\n\r\n /* 暂停视频 */\r\n playerRef.value?.pause();\r\n }\r\n\r\n /** 提交答案 */\r\n function submitAnswer(): boolean {\r\n if (!activeQuiz.value) return false;\r\n\r\n const quiz = activeQuiz.value;\r\n const isCorrect = checkAnswer(quiz, selectedAnswer.value);\r\n\r\n lastAnswerCorrect.value = isCorrect;\r\n showResult.value = true;\r\n\r\n if (isCorrect || !quiz.required) {\r\n /* 答对或非必须题目 -> 标记完成 */\r\n completedQuizIds.value.add(quiz.id);\r\n }\r\n\r\n return isCorrect;\r\n }\r\n\r\n /** 关闭测验弹窗并继续播放 */\r\n function closeQuiz() {\r\n if (!activeQuiz.value) return;\r\n\r\n const quiz = activeQuiz.value;\r\n\r\n /* 必须答对但未答对时,不允许关闭 */\r\n if (quiz.required && !completedQuizIds.value.has(quiz.id)) {\r\n return;\r\n }\r\n\r\n activeQuiz.value = null;\r\n showResult.value = false;\r\n selectedAnswer.value = \"\";\r\n\r\n /* 恢复播放 */\r\n playerRef.value?.play();\r\n }\r\n\r\n /** 重试当前测验 */\r\n function retryQuiz() {\r\n if (!activeQuiz.value) return;\r\n selectedAnswer.value = activeQuiz.value.type === \"multiple\" ? [] : \"\";\r\n showResult.value = false;\r\n lastAnswerCorrect.value = false;\r\n }\r\n\r\n /** 判断答案是否正确 */\r\n function checkAnswer(quiz: VideoQuiz, answer: string | string[]): boolean {\r\n if (quiz.type === \"multiple\") {\r\n const selected = Array.isArray(answer) ? [...answer].sort() : [answer];\r\n const correct = Array.isArray(quiz.answer)\r\n ? [...quiz.answer].sort()\r\n : [quiz.answer];\r\n return (\r\n selected.length === correct.length &&\r\n selected.every((v, i) => v === correct[i])\r\n );\r\n }\r\n return answer === quiz.answer;\r\n }\r\n\r\n /** 监听时间变化检查测验触发 */\r\n watch(currentTime, () => {\r\n checkQuizTrigger();\r\n });\r\n\r\n return {\r\n activeQuiz,\r\n completedQuizIds,\r\n selectedAnswer,\r\n showResult,\r\n lastAnswerCorrect,\r\n submitAnswer,\r\n closeQuiz,\r\n retryQuiz,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 小窗播放(滚动时自动浮动)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { ref, onBeforeUnmount, type Ref } from \"vue\";\r\n\r\n/**\r\n * 小窗播放 composable\r\n * - 当播放器滚出可视区域时自动切换为小窗浮动\r\n * - 小窗可拖动定位\r\n * - 点击小窗可滚回原位\r\n */\r\nexport function useMiniPlayer(\r\n containerRef: Ref<HTMLElement | null>,\r\n enabled: Ref<boolean>,\r\n) {\r\n /** 是否处于小窗模式 */\r\n const isMiniMode = ref(false);\r\n\r\n /** IntersectionObserver 实例 */\r\n let observer: IntersectionObserver | null = null;\r\n\r\n /** 初始化观察器 */\r\n function initObserver() {\r\n if (!containerRef.value || !enabled.value) return;\r\n\r\n observer = new IntersectionObserver(\r\n ([entry]) => {\r\n /* 播放器离开视口 → 开启小窗 */\r\n isMiniMode.value = !entry.isIntersecting;\r\n },\r\n {\r\n threshold: 0.3 /* 30% 可见度阈值 */,\r\n },\r\n );\r\n\r\n observer.observe(containerRef.value);\r\n }\r\n\r\n /** 销毁观察器 */\r\n function destroyObserver() {\r\n if (observer) {\r\n observer.disconnect();\r\n observer = null;\r\n }\r\n isMiniMode.value = false;\r\n }\r\n\r\n /** 滚动回原始位置 */\r\n function scrollToPlayer() {\r\n containerRef.value?.scrollIntoView({\r\n behavior: \"smooth\",\r\n block: \"center\",\r\n });\r\n }\r\n\r\n /** 关闭小窗 */\r\n function closeMiniPlayer() {\r\n isMiniMode.value = false;\r\n destroyObserver();\r\n }\r\n\r\n onBeforeUnmount(() => {\r\n destroyObserver();\r\n });\r\n\r\n return {\r\n isMiniMode,\r\n initObserver,\r\n destroyObserver,\r\n scrollToPlayer,\r\n closeMiniPlayer,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 快捷键管理\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { onBeforeUnmount, type ShallowRef, type Ref } from \"vue\";\r\nimport { SEEK_STEP, VOLUME_STEP } from \"../constants\";\r\nimport type { PlayerInstance } from \"../types\";\r\n\r\n/**\r\n * 快捷键管理 composable\r\n * - xgplayer 已内置快捷键,这里做补充增强\r\n * - 支持自定义快捷键回调\r\n */\r\nexport function useKeyboard(\r\n playerRef: ShallowRef<PlayerInstance | null>,\r\n containerRef: Ref<HTMLElement | null>,\r\n options: {\r\n enabled?: boolean;\r\n onToggleFullscreen?: () => void;\r\n } = {},\r\n) {\r\n const { enabled = true, onToggleFullscreen } = options;\r\n\r\n /** 快捷键动作映射 */\r\n const keyActions: Record<\r\n string,\r\n (e: KeyboardEvent, player: NonNullable<typeof playerRef.value>) => void\r\n > = {\r\n \" \": (e, player) => {\r\n e.preventDefault();\r\n if (player.paused) {\r\n player.play();\r\n } else {\r\n player.pause();\r\n }\r\n },\r\n ArrowLeft: (e, player) => {\r\n e.preventDefault();\r\n player.currentTime = Math.max(0, (player.currentTime ?? 0) - SEEK_STEP);\r\n },\r\n ArrowRight: (e, player) => {\r\n e.preventDefault();\r\n player.currentTime = Math.min(\r\n player.duration ?? 0,\r\n (player.currentTime ?? 0) + SEEK_STEP,\r\n );\r\n },\r\n ArrowUp: (e, player) => {\r\n e.preventDefault();\r\n player.volume = Math.min(1, (player.volume ?? 0) + VOLUME_STEP);\r\n },\r\n ArrowDown: (e, player) => {\r\n e.preventDefault();\r\n player.volume = Math.max(0, (player.volume ?? 0) - VOLUME_STEP);\r\n },\r\n f: (e) => {\r\n e.preventDefault();\r\n onToggleFullscreen?.();\r\n },\r\n F: (e) => {\r\n e.preventDefault();\r\n onToggleFullscreen?.();\r\n },\r\n m: (e, player) => {\r\n e.preventDefault();\r\n player.volume = player.volume > 0 ? 0 : 0.75;\r\n },\r\n M: (e, player) => {\r\n e.preventDefault();\r\n player.volume = player.volume > 0 ? 0 : 0.75;\r\n },\r\n };\r\n\r\n /** 快捷键处理函数 */\r\n function handleKeydown(e: KeyboardEvent) {\r\n if (!enabled) return;\r\n const player = playerRef.value;\r\n if (!player) return;\r\n\r\n /* 避免在输入框中触发 */\r\n const target = e.target as HTMLElement;\r\n if (\r\n target.tagName === \"INPUT\" ||\r\n target.tagName === \"TEXTAREA\" ||\r\n target.isContentEditable\r\n ) {\r\n return;\r\n }\r\n\r\n const action = keyActions[e.key];\r\n action?.(e, player);\r\n }\r\n\r\n /** 开始监听 */\r\n function startListening() {\r\n if (!enabled) return;\r\n /* 使用 container 级别监听,避免全局冲突 */\r\n containerRef.value?.addEventListener(\"keydown\", handleKeydown);\r\n }\r\n\r\n /** 停止监听 */\r\n function stopListening() {\r\n containerRef.value?.removeEventListener(\"keydown\", handleKeydown);\r\n }\r\n\r\n onBeforeUnmount(() => {\r\n stopListening();\r\n });\r\n\r\n return {\r\n startListening,\r\n stopListening,\r\n };\r\n}\r\n","/*\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 数据上报插件\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n */\r\n\r\nimport { Events } from \"xgplayer\";\r\nimport type {\r\n PlayerInstance,\r\n AnalyticsEvent,\r\n AnalyticsReporter,\r\n} from \"../types\";\r\n\r\n/**\r\n * 数据上报插件\r\n * - 监听播放器核心事件并通过回调上报\r\n * - 支持自定义事件过滤\r\n * - 页面关闭时使用 sendBeacon 兜底\r\n */\r\nexport function createAnalyticsPlugin(\r\n player: PlayerInstance,\r\n reporter: AnalyticsReporter,\r\n) {\r\n /** 构建事件数据 */\r\n function buildEvent(\r\n type: AnalyticsEvent[\"type\"],\r\n payload?: Record<string, unknown>,\r\n ): AnalyticsEvent {\r\n return {\r\n type,\r\n currentTime: player.currentTime ?? 0,\r\n timestamp: Date.now(),\r\n payload,\r\n };\r\n }\r\n\r\n /** 上报单个事件 */\r\n function report(\r\n type: AnalyticsEvent[\"type\"],\r\n payload?: Record<string, unknown>,\r\n ) {\r\n const event = buildEvent(type, payload);\r\n reporter(event);\r\n }\r\n\r\n /* 事件处理函数(保留引用以便销毁时移除) */\r\n const onPlay = () => report(\"play\");\r\n const onPause = () => report(\"pause\");\r\n const onEnded = () => report(\"ended\");\r\n const onSeeked = () => report(\"seek\");\r\n const onError = () => report(\"error\");\r\n const onWaiting = () => report(\"buffer\");\r\n const onFullscreen = (isFS: boolean) => {\r\n report(\"fullscreen\", { isFullscreen: isFS });\r\n };\r\n const onDefinition = (data: Record<string, unknown>) => {\r\n report(\"quality_change\", data);\r\n };\r\n const onRate = () => {\r\n report(\"speed_change\", { rate: player.playbackRate });\r\n };\r\n\r\n /* 绑定播放器事件 */\r\n player.on(Events.PLAY, onPlay);\r\n player.on(Events.PAUSE, onPause);\r\n player.on(Events.ENDED, onEnded);\r\n player.on(Events.SEEKED, onSeeked);\r\n player.on(Events.ERROR, onError);\r\n player.on(Events.WAITING, onWaiting);\r\n player.on(Events.FULLSCREEN_CHANGE, onFullscreen);\r\n player.on(Events.DEFINITION_CHANGE, onDefinition);\r\n player.on(Events.RATE_CHANGE, onRate);\r\n\r\n /** 销毁插件:移除所有事件监听 */\r\n function destroy() {\r\n player.off(Events.PLAY, onPlay);\r\n player.off(Events.PAUSE, onPause);\r\n player.off(Events.ENDED, onEnded);\r\n player.off(Events.SEEKED, onSeeked);\r\n player.off(Events.ERROR, onError);\r\n player.off(Events.WAITING, onWaiting);\r\n player.off(Events.FULLSCREEN_CHANGE, onFullscreen);\r\n player.off(Events.DEFINITION_CHANGE, onDefinition);\r\n player.off(Events.RATE_CHANGE, onRate);\r\n }\r\n\r\n return { report, destroy };\r\n}\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 自定义控制栏(扩展 xgplayer 原生控制栏的补充 UI)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible\" class=\"vp-control-bar\">\r\n <div class=\"vp-control-bar__left\">\r\n <slot name=\"left\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__center\">\r\n <slot name=\"center\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__right\">\r\n <slot name=\"right\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ninterface Props {\r\n /** 是否显示 */\r\n visible?: boolean;\r\n}\r\n\r\nwithDefaults(defineProps<Props>(), {\r\n visible: true,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-control-bar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 6px 12px;\r\n background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);\r\n pointer-events: auto;\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n &:hover,\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n\r\n &__left,\r\n &__center,\r\n &__right {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__center {\r\n flex: 1;\r\n justify-content: center;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 自定义控制栏(扩展 xgplayer 原生控制栏的补充 UI)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible\" class=\"vp-control-bar\">\r\n <div class=\"vp-control-bar__left\">\r\n <slot name=\"left\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__center\">\r\n <slot name=\"center\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__right\">\r\n <slot name=\"right\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ninterface Props {\r\n /** 是否显示 */\r\n visible?: boolean;\r\n}\r\n\r\nwithDefaults(defineProps<Props>(), {\r\n visible: true,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-control-bar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 6px 12px;\r\n background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);\r\n pointer-events: auto;\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n &:hover,\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n\r\n &__left,\r\n &__center,\r\n &__right {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__center {\r\n flex: 1;\r\n justify-content: center;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 自定义控制栏(扩展 xgplayer 原生控制栏的补充 UI)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible\" class=\"vp-control-bar\">\r\n <div class=\"vp-control-bar__left\">\r\n <slot name=\"left\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__center\">\r\n <slot name=\"center\" />\r\n </div>\r\n\r\n <div class=\"vp-control-bar__right\">\r\n <slot name=\"right\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ninterface Props {\r\n /** 是否显示 */\r\n visible?: boolean;\r\n}\r\n\r\nwithDefaults(defineProps<Props>(), {\r\n visible: true,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-control-bar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 6px 12px;\r\n background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);\r\n pointer-events: auto;\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n &:hover,\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n\r\n &__left,\r\n &__center,\r\n &__right {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n\r\n &__center {\r\n flex: 1;\r\n justify-content: center;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 字幕渲染层 + 字幕切换按钮\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <!-- 字幕文本 -->\r\n <Transition name=\"vp-subtitle-fade\">\r\n <div v-if=\"text\" class=\"vp-subtitle-overlay\">\r\n <span class=\"vp-subtitle-text\">{{ text }}</span>\r\n </div>\r\n </Transition>\r\n\r\n <!-- 字幕切换按钮(叠加在播放器控制栏上方) -->\r\n <div v-if=\"tracks.length\" class=\"vp-subtitle-toggle\">\r\n <NPopover trigger=\"click\" placement=\"top\" :show-arrow=\"false\">\r\n <template #trigger>\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-subtitle-btn\"\r\n :class=\"{ 'is-active': !!activeLanguage }\"\r\n >\r\n 字幕\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-subtitle-menu\">\r\n <div\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': !activeLanguage }\"\r\n @click=\"$emit('close')\"\r\n >\r\n 关闭字幕\r\n </div>\r\n <div\r\n v-for=\"track in tracks\"\r\n :key=\"track.language\"\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': activeLanguage === track.language }\"\r\n @click=\"$emit('switch', track.language)\"\r\n >\r\n {{ track.label }}\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { SubtitleTrack } from \"../types\";\r\n\r\ninterface Props {\r\n /** 当前要显示的字幕文本 */\r\n text: string;\r\n /** 字幕轨道列表 */\r\n tracks: SubtitleTrack[];\r\n /** 当前激活语言 */\r\n activeLanguage: string | null;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n switch: [language: string];\r\n close: [];\r\n}>();\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n/* ========== 字幕文本 ========== */\r\n.vp-subtitle-overlay {\r\n position: absolute;\r\n bottom: 60px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 50;\r\n pointer-events: none;\r\n max-width: 80%;\r\n text-align: center;\r\n}\r\n\r\n.vp-subtitle-text {\r\n display: inline-block;\r\n padding: 4px 16px;\r\n font-size: 16px;\r\n line-height: 1.6;\r\n color: #fff;\r\n background: rgba(0, 0, 0, 0.65);\r\n border-radius: 4px;\r\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n}\r\n\r\n/* ========== 字幕按钮 ========== */\r\n.vp-subtitle-toggle {\r\n position: absolute;\r\n right: 12px;\r\n bottom: 46px;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n}\r\n\r\n.vp-subtitle-btn {\r\n color: rgba(255, 255, 255, 0.7);\r\n font-size: 12px;\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n }\r\n}\r\n\r\n/* ========== 字幕菜单 ========== */\r\n.vp-subtitle-menu {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 100px;\r\n\r\n &__item {\r\n padding: 6px 14px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n border-radius: 4px;\r\n text-align: center;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.06);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n }\r\n}\r\n\r\n/* ========== 过渡 ========== */\r\n.vp-subtitle-fade-enter-active,\r\n.vp-subtitle-fade-leave-active {\r\n transition: opacity 0.25s ease;\r\n}\r\n\r\n.vp-subtitle-fade-enter-from,\r\n.vp-subtitle-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 字幕渲染层 + 字幕切换按钮\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <!-- 字幕文本 -->\r\n <Transition name=\"vp-subtitle-fade\">\r\n <div v-if=\"text\" class=\"vp-subtitle-overlay\">\r\n <span class=\"vp-subtitle-text\">{{ text }}</span>\r\n </div>\r\n </Transition>\r\n\r\n <!-- 字幕切换按钮(叠加在播放器控制栏上方) -->\r\n <div v-if=\"tracks.length\" class=\"vp-subtitle-toggle\">\r\n <NPopover trigger=\"click\" placement=\"top\" :show-arrow=\"false\">\r\n <template #trigger>\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-subtitle-btn\"\r\n :class=\"{ 'is-active': !!activeLanguage }\"\r\n >\r\n 字幕\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-subtitle-menu\">\r\n <div\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': !activeLanguage }\"\r\n @click=\"$emit('close')\"\r\n >\r\n 关闭字幕\r\n </div>\r\n <div\r\n v-for=\"track in tracks\"\r\n :key=\"track.language\"\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': activeLanguage === track.language }\"\r\n @click=\"$emit('switch', track.language)\"\r\n >\r\n {{ track.label }}\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { SubtitleTrack } from \"../types\";\r\n\r\ninterface Props {\r\n /** 当前要显示的字幕文本 */\r\n text: string;\r\n /** 字幕轨道列表 */\r\n tracks: SubtitleTrack[];\r\n /** 当前激活语言 */\r\n activeLanguage: string | null;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n switch: [language: string];\r\n close: [];\r\n}>();\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n/* ========== 字幕文本 ========== */\r\n.vp-subtitle-overlay {\r\n position: absolute;\r\n bottom: 60px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 50;\r\n pointer-events: none;\r\n max-width: 80%;\r\n text-align: center;\r\n}\r\n\r\n.vp-subtitle-text {\r\n display: inline-block;\r\n padding: 4px 16px;\r\n font-size: 16px;\r\n line-height: 1.6;\r\n color: #fff;\r\n background: rgba(0, 0, 0, 0.65);\r\n border-radius: 4px;\r\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n}\r\n\r\n/* ========== 字幕按钮 ========== */\r\n.vp-subtitle-toggle {\r\n position: absolute;\r\n right: 12px;\r\n bottom: 46px;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n}\r\n\r\n.vp-subtitle-btn {\r\n color: rgba(255, 255, 255, 0.7);\r\n font-size: 12px;\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n }\r\n}\r\n\r\n/* ========== 字幕菜单 ========== */\r\n.vp-subtitle-menu {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 100px;\r\n\r\n &__item {\r\n padding: 6px 14px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n border-radius: 4px;\r\n text-align: center;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.06);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n }\r\n}\r\n\r\n/* ========== 过渡 ========== */\r\n.vp-subtitle-fade-enter-active,\r\n.vp-subtitle-fade-leave-active {\r\n transition: opacity 0.25s ease;\r\n}\r\n\r\n.vp-subtitle-fade-enter-from,\r\n.vp-subtitle-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 字幕渲染层 + 字幕切换按钮\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <!-- 字幕文本 -->\r\n <Transition name=\"vp-subtitle-fade\">\r\n <div v-if=\"text\" class=\"vp-subtitle-overlay\">\r\n <span class=\"vp-subtitle-text\">{{ text }}</span>\r\n </div>\r\n </Transition>\r\n\r\n <!-- 字幕切换按钮(叠加在播放器控制栏上方) -->\r\n <div v-if=\"tracks.length\" class=\"vp-subtitle-toggle\">\r\n <NPopover trigger=\"click\" placement=\"top\" :show-arrow=\"false\">\r\n <template #trigger>\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-subtitle-btn\"\r\n :class=\"{ 'is-active': !!activeLanguage }\"\r\n >\r\n 字幕\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-subtitle-menu\">\r\n <div\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': !activeLanguage }\"\r\n @click=\"$emit('close')\"\r\n >\r\n 关闭字幕\r\n </div>\r\n <div\r\n v-for=\"track in tracks\"\r\n :key=\"track.language\"\r\n class=\"vp-subtitle-menu__item\"\r\n :class=\"{ 'is-active': activeLanguage === track.language }\"\r\n @click=\"$emit('switch', track.language)\"\r\n >\r\n {{ track.label }}\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { SubtitleTrack } from \"../types\";\r\n\r\ninterface Props {\r\n /** 当前要显示的字幕文本 */\r\n text: string;\r\n /** 字幕轨道列表 */\r\n tracks: SubtitleTrack[];\r\n /** 当前激活语言 */\r\n activeLanguage: string | null;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n switch: [language: string];\r\n close: [];\r\n}>();\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n/* ========== 字幕文本 ========== */\r\n.vp-subtitle-overlay {\r\n position: absolute;\r\n bottom: 60px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 50;\r\n pointer-events: none;\r\n max-width: 80%;\r\n text-align: center;\r\n}\r\n\r\n.vp-subtitle-text {\r\n display: inline-block;\r\n padding: 4px 16px;\r\n font-size: 16px;\r\n line-height: 1.6;\r\n color: #fff;\r\n background: rgba(0, 0, 0, 0.65);\r\n border-radius: 4px;\r\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n}\r\n\r\n/* ========== 字幕按钮 ========== */\r\n.vp-subtitle-toggle {\r\n position: absolute;\r\n right: 12px;\r\n bottom: 46px;\r\n z-index: 100;\r\n opacity: 0;\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n\r\n .c-video-player:hover & {\r\n opacity: 1;\r\n }\r\n}\r\n\r\n.vp-subtitle-btn {\r\n color: rgba(255, 255, 255, 0.7);\r\n font-size: 12px;\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n }\r\n}\r\n\r\n/* ========== 字幕菜单 ========== */\r\n.vp-subtitle-menu {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 100px;\r\n\r\n &__item {\r\n padding: 6px 14px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n border-radius: 4px;\r\n text-align: center;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.06);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n }\r\n}\r\n\r\n/* ========== 过渡 ========== */\r\n.vp-subtitle-fade-enter-active,\r\n.vp-subtitle-fade-leave-active {\r\n transition: opacity 0.25s ease;\r\n}\r\n\r\n.vp-subtitle-fade-enter-from,\r\n.vp-subtitle-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 章节标记 UI\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"chapters.length\" class=\"vp-chapter-markers\">\r\n <!-- 当前章节信息 -->\r\n <div v-if=\"currentChapter\" class=\"vp-chapter-current\">\r\n <span class=\"vp-chapter-index\"\r\n >{{ currentIndex + 1 }}/{{ chapters.length }}</span\r\n >\r\n <span class=\"vp-chapter-title\">{{ currentChapter.title }}</span>\r\n </div>\r\n\r\n <!-- 章节列表面板 -->\r\n <NPopover\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NButton quaternary size=\"small\" class=\"vp-chapter-trigger\">\r\n <template #icon>\r\n <span class=\"vp-chapter-icon\">☰</span>\r\n </template>\r\n 章节\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-chapter-list\">\r\n <div\r\n v-for=\"(chapter, idx) in chapters\"\r\n :key=\"chapter.id\"\r\n class=\"vp-chapter-item\"\r\n :class=\"{ 'is-active': chapter.id === currentChapter?.id }\"\r\n @click=\"$emit('goTo', chapter.id)\"\r\n >\r\n <span class=\"vp-chapter-item-index\">{{ idx + 1 }}</span>\r\n <span class=\"vp-chapter-item-title\">{{ chapter.title }}</span>\r\n <span class=\"vp-chapter-item-time\">{{\r\n formatTime(chapter.startTime)\r\n }}</span>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Chapter } from \"../types\";\r\n\r\ninterface Props {\r\n chapters: Chapter[];\r\n currentChapter: Chapter | null;\r\n currentIndex: number;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n goTo: [chapterId: string];\r\n}>();\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-chapter-markers {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.vp-chapter-current {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-index {\r\n opacity: 0.7;\r\n}\r\n\r\n.vp-chapter-trigger {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-icon {\r\n font-size: 14px;\r\n}\r\n\r\n.vp-chapter-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 240px;\r\n}\r\n\r\n.vp-chapter-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.05);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n\r\n &-index {\r\n width: 20px;\r\n text-align: center;\r\n opacity: 0.5;\r\n font-size: 12px;\r\n }\r\n\r\n &-title {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &-time {\r\n opacity: 0.5;\r\n font-size: 12px;\r\n font-variant-numeric: tabular-nums;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 章节标记 UI\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"chapters.length\" class=\"vp-chapter-markers\">\r\n <!-- 当前章节信息 -->\r\n <div v-if=\"currentChapter\" class=\"vp-chapter-current\">\r\n <span class=\"vp-chapter-index\"\r\n >{{ currentIndex + 1 }}/{{ chapters.length }}</span\r\n >\r\n <span class=\"vp-chapter-title\">{{ currentChapter.title }}</span>\r\n </div>\r\n\r\n <!-- 章节列表面板 -->\r\n <NPopover\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NButton quaternary size=\"small\" class=\"vp-chapter-trigger\">\r\n <template #icon>\r\n <span class=\"vp-chapter-icon\">☰</span>\r\n </template>\r\n 章节\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-chapter-list\">\r\n <div\r\n v-for=\"(chapter, idx) in chapters\"\r\n :key=\"chapter.id\"\r\n class=\"vp-chapter-item\"\r\n :class=\"{ 'is-active': chapter.id === currentChapter?.id }\"\r\n @click=\"$emit('goTo', chapter.id)\"\r\n >\r\n <span class=\"vp-chapter-item-index\">{{ idx + 1 }}</span>\r\n <span class=\"vp-chapter-item-title\">{{ chapter.title }}</span>\r\n <span class=\"vp-chapter-item-time\">{{\r\n formatTime(chapter.startTime)\r\n }}</span>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Chapter } from \"../types\";\r\n\r\ninterface Props {\r\n chapters: Chapter[];\r\n currentChapter: Chapter | null;\r\n currentIndex: number;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n goTo: [chapterId: string];\r\n}>();\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-chapter-markers {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.vp-chapter-current {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-index {\r\n opacity: 0.7;\r\n}\r\n\r\n.vp-chapter-trigger {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-icon {\r\n font-size: 14px;\r\n}\r\n\r\n.vp-chapter-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 240px;\r\n}\r\n\r\n.vp-chapter-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.05);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n\r\n &-index {\r\n width: 20px;\r\n text-align: center;\r\n opacity: 0.5;\r\n font-size: 12px;\r\n }\r\n\r\n &-title {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &-time {\r\n opacity: 0.5;\r\n font-size: 12px;\r\n font-variant-numeric: tabular-nums;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 章节标记 UI\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"chapters.length\" class=\"vp-chapter-markers\">\r\n <!-- 当前章节信息 -->\r\n <div v-if=\"currentChapter\" class=\"vp-chapter-current\">\r\n <span class=\"vp-chapter-index\"\r\n >{{ currentIndex + 1 }}/{{ chapters.length }}</span\r\n >\r\n <span class=\"vp-chapter-title\">{{ currentChapter.title }}</span>\r\n </div>\r\n\r\n <!-- 章节列表面板 -->\r\n <NPopover\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NButton quaternary size=\"small\" class=\"vp-chapter-trigger\">\r\n <template #icon>\r\n <span class=\"vp-chapter-icon\">☰</span>\r\n </template>\r\n 章节\r\n </NButton>\r\n </template>\r\n\r\n <div class=\"vp-chapter-list\">\r\n <div\r\n v-for=\"(chapter, idx) in chapters\"\r\n :key=\"chapter.id\"\r\n class=\"vp-chapter-item\"\r\n :class=\"{ 'is-active': chapter.id === currentChapter?.id }\"\r\n @click=\"$emit('goTo', chapter.id)\"\r\n >\r\n <span class=\"vp-chapter-item-index\">{{ idx + 1 }}</span>\r\n <span class=\"vp-chapter-item-title\">{{ chapter.title }}</span>\r\n <span class=\"vp-chapter-item-time\">{{\r\n formatTime(chapter.startTime)\r\n }}</span>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Chapter } from \"../types\";\r\n\r\ninterface Props {\r\n chapters: Chapter[];\r\n currentChapter: Chapter | null;\r\n currentIndex: number;\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\ndefineEmits<{\r\n goTo: [chapterId: string];\r\n}>();\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-chapter-markers {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.vp-chapter-current {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-index {\r\n opacity: 0.7;\r\n}\r\n\r\n.vp-chapter-trigger {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-chapter-icon {\r\n font-size: 14px;\r\n}\r\n\r\n.vp-chapter-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 240px;\r\n}\r\n\r\n.vp-chapter-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n transition: background-color 0.2s;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.05);\r\n }\r\n\r\n &.is-active {\r\n color: var(--c-primary, #18a058);\r\n font-weight: 600;\r\n }\r\n\r\n &-index {\r\n width: 20px;\r\n text-align: center;\r\n opacity: 0.5;\r\n font-size: 12px;\r\n }\r\n\r\n &-title {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n &-time {\r\n opacity: 0.5;\r\n font-size: 12px;\r\n font-variant-numeric: tabular-nums;\r\n }\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 书签面板\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"vp-bookmark-panel\">\r\n <!-- 添加书签按钮 -->\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-bookmark-add-btn\"\r\n @click=\"handleAdd\"\r\n >\r\n <template #icon>\r\n <span>🔖</span>\r\n </template>\r\n 书签\r\n </NButton>\r\n\r\n <!-- 书签列表弹窗 -->\r\n <NPopover\r\n v-if=\"bookmarks.length\"\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NBadge :value=\"bookmarks.length\" :max=\"99\" type=\"info\">\r\n <NButton quaternary size=\"small\" class=\"vp-bookmark-list-btn\">\r\n 列表\r\n </NButton>\r\n </NBadge>\r\n </template>\r\n\r\n <div class=\"vp-bookmark-list\">\r\n <div v-for=\"bm in bookmarks\" :key=\"bm.id\" class=\"vp-bookmark-item\">\r\n <span class=\"vp-bookmark-time\" @click=\"$emit('goTo', bm.id)\">\r\n {{ formatTime(bm.time) }}\r\n </span>\r\n\r\n <span class=\"vp-bookmark-note\">\r\n {{ bm.note || \"无备注\" }}\r\n </span>\r\n\r\n <NButton\r\n quaternary\r\n size=\"tiny\"\r\n type=\"error\"\r\n @click=\"$emit('remove', bm.id)\"\r\n >\r\n ✕\r\n </NButton>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Bookmark } from \"../types\";\r\n\r\ninterface Props {\r\n bookmarks: Bookmark[];\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\nconst emit = defineEmits<{\r\n add: [note: string];\r\n remove: [id: string];\r\n goTo: [id: string];\r\n}>();\r\n\r\n/** 添加书签 */\r\nfunction handleAdd() {\r\n emit(\"add\", \"\");\r\n}\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-bookmark-panel {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n}\r\n\r\n.vp-bookmark-add-btn,\r\n.vp-bookmark-list-btn {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-bookmark-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 220px;\r\n gap: 2px;\r\n}\r\n\r\n.vp-bookmark-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 6px 8px;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.04);\r\n }\r\n}\r\n\r\n.vp-bookmark-time {\r\n color: var(--c-primary, #18a058);\r\n cursor: pointer;\r\n font-variant-numeric: tabular-nums;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n text-decoration: underline;\r\n }\r\n}\r\n\r\n.vp-bookmark-note {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n opacity: 0.7;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 书签面板\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"vp-bookmark-panel\">\r\n <!-- 添加书签按钮 -->\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-bookmark-add-btn\"\r\n @click=\"handleAdd\"\r\n >\r\n <template #icon>\r\n <span>🔖</span>\r\n </template>\r\n 书签\r\n </NButton>\r\n\r\n <!-- 书签列表弹窗 -->\r\n <NPopover\r\n v-if=\"bookmarks.length\"\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NBadge :value=\"bookmarks.length\" :max=\"99\" type=\"info\">\r\n <NButton quaternary size=\"small\" class=\"vp-bookmark-list-btn\">\r\n 列表\r\n </NButton>\r\n </NBadge>\r\n </template>\r\n\r\n <div class=\"vp-bookmark-list\">\r\n <div v-for=\"bm in bookmarks\" :key=\"bm.id\" class=\"vp-bookmark-item\">\r\n <span class=\"vp-bookmark-time\" @click=\"$emit('goTo', bm.id)\">\r\n {{ formatTime(bm.time) }}\r\n </span>\r\n\r\n <span class=\"vp-bookmark-note\">\r\n {{ bm.note || \"无备注\" }}\r\n </span>\r\n\r\n <NButton\r\n quaternary\r\n size=\"tiny\"\r\n type=\"error\"\r\n @click=\"$emit('remove', bm.id)\"\r\n >\r\n ✕\r\n </NButton>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Bookmark } from \"../types\";\r\n\r\ninterface Props {\r\n bookmarks: Bookmark[];\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\nconst emit = defineEmits<{\r\n add: [note: string];\r\n remove: [id: string];\r\n goTo: [id: string];\r\n}>();\r\n\r\n/** 添加书签 */\r\nfunction handleAdd() {\r\n emit(\"add\", \"\");\r\n}\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-bookmark-panel {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n}\r\n\r\n.vp-bookmark-add-btn,\r\n.vp-bookmark-list-btn {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-bookmark-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 220px;\r\n gap: 2px;\r\n}\r\n\r\n.vp-bookmark-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 6px 8px;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.04);\r\n }\r\n}\r\n\r\n.vp-bookmark-time {\r\n color: var(--c-primary, #18a058);\r\n cursor: pointer;\r\n font-variant-numeric: tabular-nums;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n text-decoration: underline;\r\n }\r\n}\r\n\r\n.vp-bookmark-note {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n opacity: 0.7;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 书签面板\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div class=\"vp-bookmark-panel\">\r\n <!-- 添加书签按钮 -->\r\n <NButton\r\n quaternary\r\n size=\"small\"\r\n class=\"vp-bookmark-add-btn\"\r\n @click=\"handleAdd\"\r\n >\r\n <template #icon>\r\n <span>🔖</span>\r\n </template>\r\n 书签\r\n </NButton>\r\n\r\n <!-- 书签列表弹窗 -->\r\n <NPopover\r\n v-if=\"bookmarks.length\"\r\n trigger=\"click\"\r\n placement=\"top-start\"\r\n :show-arrow=\"false\"\r\n style=\"max-height: 300px; overflow-y: auto\"\r\n >\r\n <template #trigger>\r\n <NBadge :value=\"bookmarks.length\" :max=\"99\" type=\"info\">\r\n <NButton quaternary size=\"small\" class=\"vp-bookmark-list-btn\">\r\n 列表\r\n </NButton>\r\n </NBadge>\r\n </template>\r\n\r\n <div class=\"vp-bookmark-list\">\r\n <div v-for=\"bm in bookmarks\" :key=\"bm.id\" class=\"vp-bookmark-item\">\r\n <span class=\"vp-bookmark-time\" @click=\"$emit('goTo', bm.id)\">\r\n {{ formatTime(bm.time) }}\r\n </span>\r\n\r\n <span class=\"vp-bookmark-note\">\r\n {{ bm.note || \"无备注\" }}\r\n </span>\r\n\r\n <NButton\r\n quaternary\r\n size=\"tiny\"\r\n type=\"error\"\r\n @click=\"$emit('remove', bm.id)\"\r\n >\r\n ✕\r\n </NButton>\r\n </div>\r\n </div>\r\n </NPopover>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport type { Bookmark } from \"../types\";\r\n\r\ninterface Props {\r\n bookmarks: Bookmark[];\r\n}\r\n\r\ndefineProps<Props>();\r\n\r\nconst emit = defineEmits<{\r\n add: [note: string];\r\n remove: [id: string];\r\n goTo: [id: string];\r\n}>();\r\n\r\n/** 添加书签 */\r\nfunction handleAdd() {\r\n emit(\"add\", \"\");\r\n}\r\n\r\n/** 格式化时间为 mm:ss */\r\nfunction formatTime(seconds: number): string {\r\n const m = Math.floor(seconds / 60);\r\n const s = Math.floor(seconds % 60);\r\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-bookmark-panel {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n}\r\n\r\n.vp-bookmark-add-btn,\r\n.vp-bookmark-list-btn {\r\n color: #fff;\r\n font-size: 12px;\r\n}\r\n\r\n.vp-bookmark-list {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 220px;\r\n gap: 2px;\r\n}\r\n\r\n.vp-bookmark-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 6px 8px;\r\n border-radius: 4px;\r\n font-size: 13px;\r\n\r\n &:hover {\r\n background-color: rgba(0, 0, 0, 0.04);\r\n }\r\n}\r\n\r\n.vp-bookmark-time {\r\n color: var(--c-primary, #18a058);\r\n cursor: pointer;\r\n font-variant-numeric: tabular-nums;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n text-decoration: underline;\r\n }\r\n}\r\n\r\n.vp-bookmark-note {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n opacity: 0.7;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 测验弹窗\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <Transition name=\"vp-quiz-fade\">\r\n <div v-if=\"quiz\" class=\"vp-quiz-overlay\">\r\n <div class=\"vp-quiz-card\">\r\n <!-- 标题 -->\r\n <div class=\"vp-quiz-header\">\r\n <span class=\"vp-quiz-type-tag\">\r\n {{ typeLabel }}\r\n </span>\r\n <h3 class=\"vp-quiz-title\">{{ quiz.title }}</h3>\r\n </div>\r\n\r\n <!-- 选项 -->\r\n <div class=\"vp-quiz-options\">\r\n <template v-if=\"quiz.type === 'multiple'\">\r\n <NCheckboxGroup v-model:value=\"multiAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NCheckbox\r\n :value=\"opt.key\"\r\n :label=\"`${opt.key}. ${opt.label}`\"\r\n />\r\n </div>\r\n </NCheckboxGroup>\r\n </template>\r\n\r\n <template v-else>\r\n <NRadioGroup v-model:value=\"singleAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NRadio :value=\"opt.key\" :label=\"`${opt.key}. ${opt.label}`\" />\r\n </div>\r\n </NRadioGroup>\r\n </template>\r\n </div>\r\n\r\n <!-- 结果反馈 -->\r\n <div\r\n v-if=\"showResult\"\r\n class=\"vp-quiz-result\"\r\n :class=\"isCorrect ? 'is-correct' : 'is-wrong'\"\r\n >\r\n {{ isCorrect ? \"✓ 回答正确!\" : \"✗ 回答错误,请重试\" }}\r\n </div>\r\n\r\n <!-- 操作按钮 -->\r\n <div class=\"vp-quiz-actions\">\r\n <NButton\r\n v-if=\"!showResult\"\r\n type=\"primary\"\r\n :disabled=\"!hasAnswer\"\r\n @click=\"$emit('submit')\"\r\n >\r\n 提交答案\r\n </NButton>\r\n\r\n <template v-else>\r\n <NButton\r\n v-if=\"!isCorrect && quiz.required\"\r\n type=\"warning\"\r\n @click=\"$emit('retry')\"\r\n >\r\n 重新作答\r\n </NButton>\r\n <NButton\r\n v-if=\"isCorrect || !quiz.required\"\r\n type=\"primary\"\r\n @click=\"$emit('close')\"\r\n >\r\n 继续学习\r\n </NButton>\r\n </template>\r\n </div>\r\n </div>\r\n </div>\r\n </Transition>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport type { VideoQuiz } from \"../types\";\r\n\r\ninterface Props {\r\n quiz: VideoQuiz | null;\r\n showResult?: boolean;\r\n isCorrect?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n showResult: false,\r\n isCorrect: false,\r\n});\r\n\r\ndefineEmits<{\r\n submit: [];\r\n retry: [];\r\n close: [];\r\n}>();\r\n\r\n/** 单选答案 */\r\nconst singleAnswer = ref<string>(\"\");\r\n\r\n/** 多选答案 */\r\nconst multiAnswer = ref<string[]>([]);\r\n\r\n/** 当前选中的答案(对外) */\r\nconst currentAnswer = defineModel<string | string[]>(\"answer\", {\r\n default: \"\",\r\n});\r\n\r\n/** 题目类型标签 */\r\nconst typeLabel = computed(() => {\r\n const map: Record<string, string> = {\r\n single: \"单选题\",\r\n multiple: \"多选题\",\r\n judge: \"判断题\",\r\n };\r\n return map[props.quiz?.type ?? \"single\"] ?? \"单选题\";\r\n});\r\n\r\n/** 是否已选答案 */\r\nconst hasAnswer = computed(() => {\r\n if (props.quiz?.type === \"multiple\") {\r\n return multiAnswer.value.length > 0;\r\n }\r\n return singleAnswer.value !== \"\";\r\n});\r\n\r\n/* 同步答案到外部 */\r\nwatch(singleAnswer, (val) => {\r\n if (props.quiz?.type !== \"multiple\") {\r\n currentAnswer.value = val;\r\n }\r\n});\r\n\r\nwatch(multiAnswer, (val) => {\r\n if (props.quiz?.type === \"multiple\") {\r\n currentAnswer.value = [...val];\r\n }\r\n});\r\n\r\n/* 重置答案 */\r\nwatch(\r\n () => props.quiz?.id,\r\n () => {\r\n singleAnswer.value = \"\";\r\n multiAnswer.value = [];\r\n },\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-quiz-overlay {\r\n position: absolute;\r\n inset: 0;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: rgba(0, 0, 0, 0.7);\r\n z-index: 100;\r\n backdrop-filter: blur(4px);\r\n}\r\n\r\n.vp-quiz-card {\r\n background: var(--c-bg-card, #fff);\r\n border-radius: 12px;\r\n padding: 24px;\r\n max-width: 480px;\r\n width: 90%;\r\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.vp-quiz-header {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-type-tag {\r\n display: inline-block;\r\n padding: 2px 8px;\r\n font-size: 12px;\r\n background: var(--c-primary, #18a058);\r\n color: #fff;\r\n border-radius: 4px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.vp-quiz-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 1.5;\r\n color: var(--c-text-1, #333);\r\n}\r\n\r\n.vp-quiz-options {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-option {\r\n padding: 6px 0;\r\n}\r\n\r\n.vp-quiz-result {\r\n padding: 8px 12px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n margin-bottom: 16px;\r\n\r\n &.is-correct {\r\n background: #e8f5e9;\r\n color: #2e7d32;\r\n }\r\n\r\n &.is-wrong {\r\n background: #fbe9e7;\r\n color: #c62828;\r\n }\r\n}\r\n\r\n.vp-quiz-actions {\r\n display: flex;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n}\r\n\r\n.vp-quiz-fade-enter-active,\r\n.vp-quiz-fade-leave-active {\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n}\r\n\r\n.vp-quiz-fade-enter-from,\r\n.vp-quiz-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 测验弹窗\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <Transition name=\"vp-quiz-fade\">\r\n <div v-if=\"quiz\" class=\"vp-quiz-overlay\">\r\n <div class=\"vp-quiz-card\">\r\n <!-- 标题 -->\r\n <div class=\"vp-quiz-header\">\r\n <span class=\"vp-quiz-type-tag\">\r\n {{ typeLabel }}\r\n </span>\r\n <h3 class=\"vp-quiz-title\">{{ quiz.title }}</h3>\r\n </div>\r\n\r\n <!-- 选项 -->\r\n <div class=\"vp-quiz-options\">\r\n <template v-if=\"quiz.type === 'multiple'\">\r\n <NCheckboxGroup v-model:value=\"multiAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NCheckbox\r\n :value=\"opt.key\"\r\n :label=\"`${opt.key}. ${opt.label}`\"\r\n />\r\n </div>\r\n </NCheckboxGroup>\r\n </template>\r\n\r\n <template v-else>\r\n <NRadioGroup v-model:value=\"singleAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NRadio :value=\"opt.key\" :label=\"`${opt.key}. ${opt.label}`\" />\r\n </div>\r\n </NRadioGroup>\r\n </template>\r\n </div>\r\n\r\n <!-- 结果反馈 -->\r\n <div\r\n v-if=\"showResult\"\r\n class=\"vp-quiz-result\"\r\n :class=\"isCorrect ? 'is-correct' : 'is-wrong'\"\r\n >\r\n {{ isCorrect ? \"✓ 回答正确!\" : \"✗ 回答错误,请重试\" }}\r\n </div>\r\n\r\n <!-- 操作按钮 -->\r\n <div class=\"vp-quiz-actions\">\r\n <NButton\r\n v-if=\"!showResult\"\r\n type=\"primary\"\r\n :disabled=\"!hasAnswer\"\r\n @click=\"$emit('submit')\"\r\n >\r\n 提交答案\r\n </NButton>\r\n\r\n <template v-else>\r\n <NButton\r\n v-if=\"!isCorrect && quiz.required\"\r\n type=\"warning\"\r\n @click=\"$emit('retry')\"\r\n >\r\n 重新作答\r\n </NButton>\r\n <NButton\r\n v-if=\"isCorrect || !quiz.required\"\r\n type=\"primary\"\r\n @click=\"$emit('close')\"\r\n >\r\n 继续学习\r\n </NButton>\r\n </template>\r\n </div>\r\n </div>\r\n </div>\r\n </Transition>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport type { VideoQuiz } from \"../types\";\r\n\r\ninterface Props {\r\n quiz: VideoQuiz | null;\r\n showResult?: boolean;\r\n isCorrect?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n showResult: false,\r\n isCorrect: false,\r\n});\r\n\r\ndefineEmits<{\r\n submit: [];\r\n retry: [];\r\n close: [];\r\n}>();\r\n\r\n/** 单选答案 */\r\nconst singleAnswer = ref<string>(\"\");\r\n\r\n/** 多选答案 */\r\nconst multiAnswer = ref<string[]>([]);\r\n\r\n/** 当前选中的答案(对外) */\r\nconst currentAnswer = defineModel<string | string[]>(\"answer\", {\r\n default: \"\",\r\n});\r\n\r\n/** 题目类型标签 */\r\nconst typeLabel = computed(() => {\r\n const map: Record<string, string> = {\r\n single: \"单选题\",\r\n multiple: \"多选题\",\r\n judge: \"判断题\",\r\n };\r\n return map[props.quiz?.type ?? \"single\"] ?? \"单选题\";\r\n});\r\n\r\n/** 是否已选答案 */\r\nconst hasAnswer = computed(() => {\r\n if (props.quiz?.type === \"multiple\") {\r\n return multiAnswer.value.length > 0;\r\n }\r\n return singleAnswer.value !== \"\";\r\n});\r\n\r\n/* 同步答案到外部 */\r\nwatch(singleAnswer, (val) => {\r\n if (props.quiz?.type !== \"multiple\") {\r\n currentAnswer.value = val;\r\n }\r\n});\r\n\r\nwatch(multiAnswer, (val) => {\r\n if (props.quiz?.type === \"multiple\") {\r\n currentAnswer.value = [...val];\r\n }\r\n});\r\n\r\n/* 重置答案 */\r\nwatch(\r\n () => props.quiz?.id,\r\n () => {\r\n singleAnswer.value = \"\";\r\n multiAnswer.value = [];\r\n },\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-quiz-overlay {\r\n position: absolute;\r\n inset: 0;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: rgba(0, 0, 0, 0.7);\r\n z-index: 100;\r\n backdrop-filter: blur(4px);\r\n}\r\n\r\n.vp-quiz-card {\r\n background: var(--c-bg-card, #fff);\r\n border-radius: 12px;\r\n padding: 24px;\r\n max-width: 480px;\r\n width: 90%;\r\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.vp-quiz-header {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-type-tag {\r\n display: inline-block;\r\n padding: 2px 8px;\r\n font-size: 12px;\r\n background: var(--c-primary, #18a058);\r\n color: #fff;\r\n border-radius: 4px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.vp-quiz-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 1.5;\r\n color: var(--c-text-1, #333);\r\n}\r\n\r\n.vp-quiz-options {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-option {\r\n padding: 6px 0;\r\n}\r\n\r\n.vp-quiz-result {\r\n padding: 8px 12px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n margin-bottom: 16px;\r\n\r\n &.is-correct {\r\n background: #e8f5e9;\r\n color: #2e7d32;\r\n }\r\n\r\n &.is-wrong {\r\n background: #fbe9e7;\r\n color: #c62828;\r\n }\r\n}\r\n\r\n.vp-quiz-actions {\r\n display: flex;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n}\r\n\r\n.vp-quiz-fade-enter-active,\r\n.vp-quiz-fade-leave-active {\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n}\r\n\r\n.vp-quiz-fade-enter-from,\r\n.vp-quiz-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 测验弹窗\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <Transition name=\"vp-quiz-fade\">\r\n <div v-if=\"quiz\" class=\"vp-quiz-overlay\">\r\n <div class=\"vp-quiz-card\">\r\n <!-- 标题 -->\r\n <div class=\"vp-quiz-header\">\r\n <span class=\"vp-quiz-type-tag\">\r\n {{ typeLabel }}\r\n </span>\r\n <h3 class=\"vp-quiz-title\">{{ quiz.title }}</h3>\r\n </div>\r\n\r\n <!-- 选项 -->\r\n <div class=\"vp-quiz-options\">\r\n <template v-if=\"quiz.type === 'multiple'\">\r\n <NCheckboxGroup v-model:value=\"multiAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NCheckbox\r\n :value=\"opt.key\"\r\n :label=\"`${opt.key}. ${opt.label}`\"\r\n />\r\n </div>\r\n </NCheckboxGroup>\r\n </template>\r\n\r\n <template v-else>\r\n <NRadioGroup v-model:value=\"singleAnswer\" :disabled=\"showResult\">\r\n <div\r\n v-for=\"opt in quiz.options\"\r\n :key=\"opt.key\"\r\n class=\"vp-quiz-option\"\r\n >\r\n <NRadio :value=\"opt.key\" :label=\"`${opt.key}. ${opt.label}`\" />\r\n </div>\r\n </NRadioGroup>\r\n </template>\r\n </div>\r\n\r\n <!-- 结果反馈 -->\r\n <div\r\n v-if=\"showResult\"\r\n class=\"vp-quiz-result\"\r\n :class=\"isCorrect ? 'is-correct' : 'is-wrong'\"\r\n >\r\n {{ isCorrect ? \"✓ 回答正确!\" : \"✗ 回答错误,请重试\" }}\r\n </div>\r\n\r\n <!-- 操作按钮 -->\r\n <div class=\"vp-quiz-actions\">\r\n <NButton\r\n v-if=\"!showResult\"\r\n type=\"primary\"\r\n :disabled=\"!hasAnswer\"\r\n @click=\"$emit('submit')\"\r\n >\r\n 提交答案\r\n </NButton>\r\n\r\n <template v-else>\r\n <NButton\r\n v-if=\"!isCorrect && quiz.required\"\r\n type=\"warning\"\r\n @click=\"$emit('retry')\"\r\n >\r\n 重新作答\r\n </NButton>\r\n <NButton\r\n v-if=\"isCorrect || !quiz.required\"\r\n type=\"primary\"\r\n @click=\"$emit('close')\"\r\n >\r\n 继续学习\r\n </NButton>\r\n </template>\r\n </div>\r\n </div>\r\n </div>\r\n </Transition>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from \"vue\";\r\nimport type { VideoQuiz } from \"../types\";\r\n\r\ninterface Props {\r\n quiz: VideoQuiz | null;\r\n showResult?: boolean;\r\n isCorrect?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n showResult: false,\r\n isCorrect: false,\r\n});\r\n\r\ndefineEmits<{\r\n submit: [];\r\n retry: [];\r\n close: [];\r\n}>();\r\n\r\n/** 单选答案 */\r\nconst singleAnswer = ref<string>(\"\");\r\n\r\n/** 多选答案 */\r\nconst multiAnswer = ref<string[]>([]);\r\n\r\n/** 当前选中的答案(对外) */\r\nconst currentAnswer = defineModel<string | string[]>(\"answer\", {\r\n default: \"\",\r\n});\r\n\r\n/** 题目类型标签 */\r\nconst typeLabel = computed(() => {\r\n const map: Record<string, string> = {\r\n single: \"单选题\",\r\n multiple: \"多选题\",\r\n judge: \"判断题\",\r\n };\r\n return map[props.quiz?.type ?? \"single\"] ?? \"单选题\";\r\n});\r\n\r\n/** 是否已选答案 */\r\nconst hasAnswer = computed(() => {\r\n if (props.quiz?.type === \"multiple\") {\r\n return multiAnswer.value.length > 0;\r\n }\r\n return singleAnswer.value !== \"\";\r\n});\r\n\r\n/* 同步答案到外部 */\r\nwatch(singleAnswer, (val) => {\r\n if (props.quiz?.type !== \"multiple\") {\r\n currentAnswer.value = val;\r\n }\r\n});\r\n\r\nwatch(multiAnswer, (val) => {\r\n if (props.quiz?.type === \"multiple\") {\r\n currentAnswer.value = [...val];\r\n }\r\n});\r\n\r\n/* 重置答案 */\r\nwatch(\r\n () => props.quiz?.id,\r\n () => {\r\n singleAnswer.value = \"\";\r\n multiAnswer.value = [];\r\n },\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-quiz-overlay {\r\n position: absolute;\r\n inset: 0;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: rgba(0, 0, 0, 0.7);\r\n z-index: 100;\r\n backdrop-filter: blur(4px);\r\n}\r\n\r\n.vp-quiz-card {\r\n background: var(--c-bg-card, #fff);\r\n border-radius: 12px;\r\n padding: 24px;\r\n max-width: 480px;\r\n width: 90%;\r\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.vp-quiz-header {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-type-tag {\r\n display: inline-block;\r\n padding: 2px 8px;\r\n font-size: 12px;\r\n background: var(--c-primary, #18a058);\r\n color: #fff;\r\n border-radius: 4px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.vp-quiz-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 1.5;\r\n color: var(--c-text-1, #333);\r\n}\r\n\r\n.vp-quiz-options {\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vp-quiz-option {\r\n padding: 6px 0;\r\n}\r\n\r\n.vp-quiz-result {\r\n padding: 8px 12px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n margin-bottom: 16px;\r\n\r\n &.is-correct {\r\n background: #e8f5e9;\r\n color: #2e7d32;\r\n }\r\n\r\n &.is-wrong {\r\n background: #fbe9e7;\r\n color: #c62828;\r\n }\r\n}\r\n\r\n.vp-quiz-actions {\r\n display: flex;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n}\r\n\r\n.vp-quiz-fade-enter-active,\r\n.vp-quiz-fade-leave-active {\r\n transition: opacity var(--c-transition, 0.2s ease);\r\n}\r\n\r\n.vp-quiz-fade-enter-from,\r\n.vp-quiz-fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 动态水印\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible && text\" class=\"vp-watermark\" :style=\"containerStyle\">\r\n <span\r\n v-for=\"n in repeatCount\"\r\n :key=\"n\"\r\n class=\"vp-watermark-item\"\r\n :style=\"itemStyle\"\r\n >\r\n {{ text }}\r\n </span>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport { WATERMARK_DEFAULT_STYLE } from \"../constants\";\r\n\r\ninterface Props {\r\n /** 是否显示水印 */\r\n visible?: boolean;\r\n /** 水印文本 */\r\n text?: string;\r\n /** 字体大小 */\r\n fontSize?: number;\r\n /** 文字颜色 */\r\n color?: string;\r\n /** 旋转角度 */\r\n rotate?: number;\r\n /** 水印间距 */\r\n gap?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n visible: true,\r\n text: \"\",\r\n fontSize: WATERMARK_DEFAULT_STYLE.fontSize,\r\n color: WATERMARK_DEFAULT_STYLE.color,\r\n rotate: WATERMARK_DEFAULT_STYLE.rotate,\r\n gap: WATERMARK_DEFAULT_STYLE.gap,\r\n});\r\n\r\n/** 重复次数:根据间距简单估算 */\r\nconst repeatCount = computed(() => {\r\n const area = 1920 * 1080;\r\n const itemArea = props.gap * props.gap;\r\n return Math.ceil(area / itemArea);\r\n});\r\n\r\n/** 容器样式 */\r\nconst containerStyle = computed(() => ({\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n overflow: \"hidden\",\r\n pointerEvents: \"none\" as const,\r\n zIndex: 50,\r\n display: \"flex\",\r\n flexWrap: \"wrap\" as const,\r\n alignContent: \"flex-start\",\r\n gap: `${props.gap}px`,\r\n padding: `${props.gap / 2}px`,\r\n}));\r\n\r\n/** 水印文字样式 */\r\nconst itemStyle = computed(() => ({\r\n fontSize: `${props.fontSize}px`,\r\n color: props.color,\r\n transform: `rotate(${props.rotate}deg)`,\r\n userSelect: \"none\" as const,\r\n whiteSpace: \"nowrap\" as const,\r\n lineHeight: 1,\r\n}));\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-watermark {\r\n /* 通过 CSS 阻止截图(基础防护) */\r\n -webkit-user-select: none;\r\n user-select: none;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 动态水印\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible && text\" class=\"vp-watermark\" :style=\"containerStyle\">\r\n <span\r\n v-for=\"n in repeatCount\"\r\n :key=\"n\"\r\n class=\"vp-watermark-item\"\r\n :style=\"itemStyle\"\r\n >\r\n {{ text }}\r\n </span>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport { WATERMARK_DEFAULT_STYLE } from \"../constants\";\r\n\r\ninterface Props {\r\n /** 是否显示水印 */\r\n visible?: boolean;\r\n /** 水印文本 */\r\n text?: string;\r\n /** 字体大小 */\r\n fontSize?: number;\r\n /** 文字颜色 */\r\n color?: string;\r\n /** 旋转角度 */\r\n rotate?: number;\r\n /** 水印间距 */\r\n gap?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n visible: true,\r\n text: \"\",\r\n fontSize: WATERMARK_DEFAULT_STYLE.fontSize,\r\n color: WATERMARK_DEFAULT_STYLE.color,\r\n rotate: WATERMARK_DEFAULT_STYLE.rotate,\r\n gap: WATERMARK_DEFAULT_STYLE.gap,\r\n});\r\n\r\n/** 重复次数:根据间距简单估算 */\r\nconst repeatCount = computed(() => {\r\n const area = 1920 * 1080;\r\n const itemArea = props.gap * props.gap;\r\n return Math.ceil(area / itemArea);\r\n});\r\n\r\n/** 容器样式 */\r\nconst containerStyle = computed(() => ({\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n overflow: \"hidden\",\r\n pointerEvents: \"none\" as const,\r\n zIndex: 50,\r\n display: \"flex\",\r\n flexWrap: \"wrap\" as const,\r\n alignContent: \"flex-start\",\r\n gap: `${props.gap}px`,\r\n padding: `${props.gap / 2}px`,\r\n}));\r\n\r\n/** 水印文字样式 */\r\nconst itemStyle = computed(() => ({\r\n fontSize: `${props.fontSize}px`,\r\n color: props.color,\r\n transform: `rotate(${props.rotate}deg)`,\r\n userSelect: \"none\" as const,\r\n whiteSpace: \"nowrap\" as const,\r\n lineHeight: 1,\r\n}));\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-watermark {\r\n /* 通过 CSS 阻止截图(基础防护) */\r\n -webkit-user-select: none;\r\n user-select: none;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 动态水印\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div v-if=\"visible && text\" class=\"vp-watermark\" :style=\"containerStyle\">\r\n <span\r\n v-for=\"n in repeatCount\"\r\n :key=\"n\"\r\n class=\"vp-watermark-item\"\r\n :style=\"itemStyle\"\r\n >\r\n {{ text }}\r\n </span>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport { WATERMARK_DEFAULT_STYLE } from \"../constants\";\r\n\r\ninterface Props {\r\n /** 是否显示水印 */\r\n visible?: boolean;\r\n /** 水印文本 */\r\n text?: string;\r\n /** 字体大小 */\r\n fontSize?: number;\r\n /** 文字颜色 */\r\n color?: string;\r\n /** 旋转角度 */\r\n rotate?: number;\r\n /** 水印间距 */\r\n gap?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n visible: true,\r\n text: \"\",\r\n fontSize: WATERMARK_DEFAULT_STYLE.fontSize,\r\n color: WATERMARK_DEFAULT_STYLE.color,\r\n rotate: WATERMARK_DEFAULT_STYLE.rotate,\r\n gap: WATERMARK_DEFAULT_STYLE.gap,\r\n});\r\n\r\n/** 重复次数:根据间距简单估算 */\r\nconst repeatCount = computed(() => {\r\n const area = 1920 * 1080;\r\n const itemArea = props.gap * props.gap;\r\n return Math.ceil(area / itemArea);\r\n});\r\n\r\n/** 容器样式 */\r\nconst containerStyle = computed(() => ({\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n overflow: \"hidden\",\r\n pointerEvents: \"none\" as const,\r\n zIndex: 50,\r\n display: \"flex\",\r\n flexWrap: \"wrap\" as const,\r\n alignContent: \"flex-start\",\r\n gap: `${props.gap}px`,\r\n padding: `${props.gap / 2}px`,\r\n}));\r\n\r\n/** 水印文字样式 */\r\nconst itemStyle = computed(() => ({\r\n fontSize: `${props.fontSize}px`,\r\n color: props.color,\r\n transform: `rotate(${props.rotate}deg)`,\r\n userSelect: \"none\" as const,\r\n whiteSpace: \"nowrap\" as const,\r\n lineHeight: 1,\r\n}));\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.vp-watermark {\r\n /* 通过 CSS 阻止截图(基础防护) */\r\n -webkit-user-select: none;\r\n user-select: none;\r\n}\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 视频播放器组件(基于 xgplayer)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n ref=\"wrapperRef\"\r\n class=\"c-video-player\"\r\n :class=\"{\r\n 'is-mini': miniPlayer.isMiniMode.value,\r\n 'is-fullscreen': isFullscreen,\r\n }\"\r\n tabindex=\"0\"\r\n >\r\n <!-- 播放器容器 -->\r\n <div ref=\"containerRef\" class=\"c-video-player__container\" />\r\n\r\n <!-- 动态水印 -->\r\n <WatermarkOverlay\r\n :visible=\"antiCheatState.showWatermark.value\"\r\n :text=\"antiCheatState.watermarkText.value\"\r\n />\r\n\r\n <!-- 字幕渲染层 -->\r\n <SubtitleOverlay\r\n :text=\"subtitle.currentText.value\"\r\n :tracks=\"subtitle.subtitleList.value\"\r\n :active-language=\"subtitle.activeLanguage.value\"\r\n @switch=\"subtitle.switchSubtitle\"\r\n @close=\"subtitle.closeSubtitle\"\r\n />\r\n\r\n <!-- 测验弹窗 -->\r\n <QuizOverlay\r\n v-if=\"props.quizzes?.length\"\r\n v-model:answer=\"quiz.selectedAnswer.value\"\r\n :quiz=\"quiz.activeQuiz.value\"\r\n :show-result=\"quiz.showResult.value\"\r\n :is-correct=\"quiz.lastAnswerCorrect.value\"\r\n @submit=\"handleQuizSubmit\"\r\n @retry=\"quiz.retryQuiz()\"\r\n @close=\"handleQuizClose\"\r\n />\r\n\r\n <!-- 扩展控制栏(xgplayer 自带控制栏外的扩展) -->\r\n <ControlBar :visible=\"showExtendedControls\">\r\n <template #left>\r\n <!-- 章节标记 -->\r\n <ChapterMarkers\r\n v-if=\"props.chapters?.length\"\r\n :chapters=\"chaptersRef\"\r\n :current-chapter=\"chapters.currentChapter.value\"\r\n :current-index=\"chapters.currentChapterIndex.value\"\r\n @go-to=\"chapters.goToChapter\"\r\n />\r\n </template>\r\n\r\n <template #right>\r\n <!-- 书签面板 -->\r\n <BookmarkPanel\r\n :bookmarks=\"bookmarksState.bookmarks.value\"\r\n @add=\"handleAddBookmark\"\r\n @remove=\"bookmarksState.removeBookmark\"\r\n @go-to=\"bookmarksState.goToBookmark\"\r\n />\r\n </template>\r\n </ControlBar>\r\n\r\n <!-- 小窗关闭按钮 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-close\"\r\n @click=\"miniPlayer.closeMiniPlayer()\"\r\n >\r\n ✕\r\n </div>\r\n\r\n <!-- 小窗点击回原位 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-back\"\r\n @click=\"miniPlayer.scrollToPlayer()\"\r\n >\r\n 回到原位\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport {\r\n ref,\r\n computed,\r\n watch,\r\n toRef,\r\n onMounted,\r\n onBeforeUnmount,\r\n nextTick,\r\n} from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport {\r\n usePlayerCore,\r\n usePlaybackControl,\r\n useProgressTracker,\r\n useQualitySwitch,\r\n useChapters,\r\n useBookmarks,\r\n useAntiCheat,\r\n useSubtitle,\r\n useQuiz,\r\n useMiniPlayer,\r\n useKeyboard,\r\n} from \"./composables\";\r\nimport { createAnalyticsPlugin } from \"./plugins\";\r\nimport ControlBar from \"./components/ControlBar.vue\";\r\nimport SubtitleOverlay from \"./components/SubtitleOverlay.vue\";\r\nimport ChapterMarkers from \"./components/ChapterMarkers.vue\";\r\nimport BookmarkPanel from \"./components/BookmarkPanel.vue\";\r\nimport QuizOverlay from \"./components/QuizOverlay.vue\";\r\nimport WatermarkOverlay from \"./components/WatermarkOverlay.vue\";\r\nimport type {\r\n VideoPlayerProps,\r\n VideoPlayerExpose,\r\n PlayerState,\r\n QualityLevel,\r\n ProgressData,\r\n Bookmark,\r\n Chapter,\r\n} from \"./types\";\r\n\r\ndefineOptions({ name: \"C_VideoPlayer\" });\r\n\r\nconst props = defineProps<VideoPlayerProps>();\r\n\r\nconst emit = defineEmits<{\r\n ready: [];\r\n stateChange: [state: PlayerState];\r\n timeUpdate: [currentTime: number, duration: number];\r\n ended: [];\r\n error: [error: Error];\r\n qualityChange: [quality: QualityLevel];\r\n rateChange: [rate: number];\r\n fullscreenChange: [isFullscreen: boolean];\r\n bookmarkChange: [bookmarks: Bookmark[]];\r\n quizAnswer: [quizId: string, answer: string | string[], isCorrect: boolean];\r\n chapterChange: [chapter: Chapter];\r\n progressUpdate: [data: ProgressData];\r\n}>();\r\n\r\n/* ======================== 核心 ======================== */\r\n\r\nconst core = usePlayerCore(props);\r\nconst {\r\n containerRef,\r\n playerRef,\r\n playerState,\r\n currentTime,\r\n duration,\r\n isFullscreen,\r\n} = core;\r\n\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\n\r\n/* ======================== 播放控制 ======================== */\r\n\r\nconst playback = usePlaybackControl(playerRef);\r\n\r\n/* ======================== 进度追踪 ======================== */\r\n\r\nconst urlRef = toRef(props, \"url\");\r\nconst progressTracker = useProgressTracker(\r\n playerRef,\r\n currentTime,\r\n duration,\r\n urlRef,\r\n props.onProgress,\r\n props.antiCheat,\r\n);\r\n\r\n/* ======================== 清晰度切换 ======================== */\r\n\r\nconst qualitySwitch = useQualitySwitch(playerRef, props.qualityList);\r\n\r\n/* ======================== 章节标记 ======================== */\r\n\r\nconst chaptersRef = computed(() => props.chapters ?? []);\r\nconst chapters = useChapters(chaptersRef, currentTime, duration, playback.seek);\r\n\r\n/* ======================== 书签笔记 ======================== */\r\n\r\nconst bookmarksState = useBookmarks(\r\n urlRef,\r\n currentTime,\r\n playback.seek,\r\n props.bookmarks,\r\n);\r\n\r\n/* ======================== 防作弊 ======================== */\r\n\r\nconst antiCheatState = useAntiCheat(playerRef, currentTime, props.antiCheat);\r\n\r\n/* ======================== 字幕 ======================== */\r\n\r\nconst subtitle = useSubtitle(playerRef, props.subtitles, currentTime);\r\n\r\n/* ======================== 测验 ======================== */\r\n\r\nconst quizzesRef = computed(() => props.quizzes ?? []);\r\nconst quiz = useQuiz(playerRef, currentTime, quizzesRef);\r\n\r\n/* ======================== 小窗播放 ======================== */\r\n\r\nconst miniEnabled = computed(() => props.miniPlayer ?? false);\r\nconst miniPlayer = useMiniPlayer(wrapperRef, miniEnabled);\r\n\r\n/* ======================== 快捷键 ======================== */\r\n\r\nconst keyboard = useKeyboard(playerRef, wrapperRef, {\r\n enabled: props.keyboard !== false,\r\n onToggleFullscreen: () => {\r\n playerRef.value?.getFullscreen?.();\r\n },\r\n});\r\n\r\n/* ======================== 数据分析 ======================== */\r\n\r\nlet analyticsDestroy: (() => void) | null = null;\r\n\r\n/* ======================== 扩展控制栏:书签始终可用,章节/清晰度按需 ======================== */\r\n\r\nconst showExtendedControls = true;\r\n\r\n/* ======================== 事件处理 ======================== */\r\n\r\n/** 处理添加书签 */\r\nfunction handleAddBookmark(note: string) {\r\n bookmarksState.addBookmark(note);\r\n emit(\"bookmarkChange\", bookmarksState.bookmarks.value);\r\n}\r\n\r\n/** 处理测验提交 */\r\nfunction handleQuizSubmit() {\r\n const isCorrect = quiz.submitAnswer();\r\n const activeQuiz = quiz.activeQuiz.value;\r\n if (activeQuiz) {\r\n emit(\"quizAnswer\", activeQuiz.id, quiz.selectedAnswer.value, isCorrect);\r\n }\r\n}\r\n\r\n/** 处理测验关闭 */\r\nfunction handleQuizClose() {\r\n quiz.closeQuiz();\r\n}\r\n\r\n/* ======================== 监听播放器事件并向外 emit ======================== */\r\n\r\nwatch(playerState, (state) => {\r\n emit(\"stateChange\", state);\r\n});\r\n\r\nwatch(isFullscreen, (val) => {\r\n emit(\"fullscreenChange\", val);\r\n});\r\n\r\n/* 监听章节变化 */\r\nwatch(\r\n () => chapters.currentChapter.value,\r\n (chapter) => {\r\n if (chapter) {\r\n emit(\"chapterChange\", chapter);\r\n }\r\n },\r\n);\r\n\r\n/* ======================== 初始化 & 销毁 ======================== */\r\n\r\nonMounted(async () => {\r\n /* 将 wrapperRef 的 el 赋值给 core */\r\n core.containerRef.value = containerRef.value;\r\n\r\n await nextTick();\r\n await core.initPlayer();\r\n\r\n const player = playerRef.value;\r\n if (!player) return;\r\n\r\n /* 绑定各模块事件 */\r\n qualitySwitch.bindEvents(player);\r\n antiCheatState.bindEvents(player);\r\n subtitle.initSubtitles();\r\n keyboard.startListening();\r\n miniPlayer.initObserver();\r\n\r\n /* 恢复断点续播 */\r\n const restoreTime = progressTracker.restoreProgress();\r\n if (restoreTime > 0 && props.startTime === undefined) {\r\n player.currentTime = restoreTime;\r\n }\r\n\r\n /* 播放器事件 → 进度追踪 */\r\n player.on(Events.PLAY, () => {\r\n progressTracker.onPlay();\r\n });\r\n player.on(Events.PAUSE, () => {\r\n progressTracker.onPause();\r\n });\r\n player.on(Events.TIME_UPDATE, () => {\r\n progressTracker.onTimeUpdate();\r\n emit(\"timeUpdate\", currentTime.value, duration.value);\r\n emit(\"progressUpdate\", progressTracker.getProgressData());\r\n });\r\n player.on(Events.ENDED, () => {\r\n emit(\"ended\");\r\n antiCheatState.markAsWatched();\r\n });\r\n player.on(Events.ERROR, () => {\r\n emit(\"error\", new Error(\"播放器错误\"));\r\n });\r\n player.on(Events.READY, () => {\r\n emit(\"ready\");\r\n });\r\n\r\n /* xgplayer 原生清晰度切换 → 向外 emit */\r\n player.on(Events.AFTER_DEFINITION_CHANGE, (data: { to: string }) => {\r\n emit(\"qualityChange\", data.to as QualityLevel);\r\n });\r\n\r\n /* xgplayer 原生倍速切换 → 向外 emit */\r\n player.on(Events.RATE_CHANGE, (rate: number) => {\r\n emit(\"rateChange\", rate);\r\n });\r\n\r\n /* 初始化数据分析 */\r\n if (props.onAnalytics) {\r\n const analytics = createAnalyticsPlugin(player, props.onAnalytics);\r\n analyticsDestroy = analytics.destroy;\r\n }\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n analyticsDestroy?.();\r\n keyboard.stopListening();\r\n miniPlayer.destroyObserver();\r\n progressTracker.stopTracking();\r\n core.destroyPlayer();\r\n});\r\n\r\n/* ======================== 暴露方法 ======================== */\r\n\r\ndefineExpose<VideoPlayerExpose>({\r\n play: playback.play,\r\n pause: playback.pause,\r\n seek: playback.seek,\r\n setPlaybackRate: playback.setPlaybackRate,\r\n setVolume: playback.setVolume,\r\n switchQuality: qualitySwitch.switchQuality,\r\n getProgressData: progressTracker.getProgressData,\r\n destroy: core.destroyPlayer,\r\n getPlayerInstance: () => playerRef.value,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 视频播放器组件(基于 xgplayer)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n ref=\"wrapperRef\"\r\n class=\"c-video-player\"\r\n :class=\"{\r\n 'is-mini': miniPlayer.isMiniMode.value,\r\n 'is-fullscreen': isFullscreen,\r\n }\"\r\n tabindex=\"0\"\r\n >\r\n <!-- 播放器容器 -->\r\n <div ref=\"containerRef\" class=\"c-video-player__container\" />\r\n\r\n <!-- 动态水印 -->\r\n <WatermarkOverlay\r\n :visible=\"antiCheatState.showWatermark.value\"\r\n :text=\"antiCheatState.watermarkText.value\"\r\n />\r\n\r\n <!-- 字幕渲染层 -->\r\n <SubtitleOverlay\r\n :text=\"subtitle.currentText.value\"\r\n :tracks=\"subtitle.subtitleList.value\"\r\n :active-language=\"subtitle.activeLanguage.value\"\r\n @switch=\"subtitle.switchSubtitle\"\r\n @close=\"subtitle.closeSubtitle\"\r\n />\r\n\r\n <!-- 测验弹窗 -->\r\n <QuizOverlay\r\n v-if=\"props.quizzes?.length\"\r\n v-model:answer=\"quiz.selectedAnswer.value\"\r\n :quiz=\"quiz.activeQuiz.value\"\r\n :show-result=\"quiz.showResult.value\"\r\n :is-correct=\"quiz.lastAnswerCorrect.value\"\r\n @submit=\"handleQuizSubmit\"\r\n @retry=\"quiz.retryQuiz()\"\r\n @close=\"handleQuizClose\"\r\n />\r\n\r\n <!-- 扩展控制栏(xgplayer 自带控制栏外的扩展) -->\r\n <ControlBar :visible=\"showExtendedControls\">\r\n <template #left>\r\n <!-- 章节标记 -->\r\n <ChapterMarkers\r\n v-if=\"props.chapters?.length\"\r\n :chapters=\"chaptersRef\"\r\n :current-chapter=\"chapters.currentChapter.value\"\r\n :current-index=\"chapters.currentChapterIndex.value\"\r\n @go-to=\"chapters.goToChapter\"\r\n />\r\n </template>\r\n\r\n <template #right>\r\n <!-- 书签面板 -->\r\n <BookmarkPanel\r\n :bookmarks=\"bookmarksState.bookmarks.value\"\r\n @add=\"handleAddBookmark\"\r\n @remove=\"bookmarksState.removeBookmark\"\r\n @go-to=\"bookmarksState.goToBookmark\"\r\n />\r\n </template>\r\n </ControlBar>\r\n\r\n <!-- 小窗关闭按钮 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-close\"\r\n @click=\"miniPlayer.closeMiniPlayer()\"\r\n >\r\n ✕\r\n </div>\r\n\r\n <!-- 小窗点击回原位 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-back\"\r\n @click=\"miniPlayer.scrollToPlayer()\"\r\n >\r\n 回到原位\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport {\r\n ref,\r\n computed,\r\n watch,\r\n toRef,\r\n onMounted,\r\n onBeforeUnmount,\r\n nextTick,\r\n} from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport {\r\n usePlayerCore,\r\n usePlaybackControl,\r\n useProgressTracker,\r\n useQualitySwitch,\r\n useChapters,\r\n useBookmarks,\r\n useAntiCheat,\r\n useSubtitle,\r\n useQuiz,\r\n useMiniPlayer,\r\n useKeyboard,\r\n} from \"./composables\";\r\nimport { createAnalyticsPlugin } from \"./plugins\";\r\nimport ControlBar from \"./components/ControlBar.vue\";\r\nimport SubtitleOverlay from \"./components/SubtitleOverlay.vue\";\r\nimport ChapterMarkers from \"./components/ChapterMarkers.vue\";\r\nimport BookmarkPanel from \"./components/BookmarkPanel.vue\";\r\nimport QuizOverlay from \"./components/QuizOverlay.vue\";\r\nimport WatermarkOverlay from \"./components/WatermarkOverlay.vue\";\r\nimport type {\r\n VideoPlayerProps,\r\n VideoPlayerExpose,\r\n PlayerState,\r\n QualityLevel,\r\n ProgressData,\r\n Bookmark,\r\n Chapter,\r\n} from \"./types\";\r\n\r\ndefineOptions({ name: \"C_VideoPlayer\" });\r\n\r\nconst props = defineProps<VideoPlayerProps>();\r\n\r\nconst emit = defineEmits<{\r\n ready: [];\r\n stateChange: [state: PlayerState];\r\n timeUpdate: [currentTime: number, duration: number];\r\n ended: [];\r\n error: [error: Error];\r\n qualityChange: [quality: QualityLevel];\r\n rateChange: [rate: number];\r\n fullscreenChange: [isFullscreen: boolean];\r\n bookmarkChange: [bookmarks: Bookmark[]];\r\n quizAnswer: [quizId: string, answer: string | string[], isCorrect: boolean];\r\n chapterChange: [chapter: Chapter];\r\n progressUpdate: [data: ProgressData];\r\n}>();\r\n\r\n/* ======================== 核心 ======================== */\r\n\r\nconst core = usePlayerCore(props);\r\nconst {\r\n containerRef,\r\n playerRef,\r\n playerState,\r\n currentTime,\r\n duration,\r\n isFullscreen,\r\n} = core;\r\n\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\n\r\n/* ======================== 播放控制 ======================== */\r\n\r\nconst playback = usePlaybackControl(playerRef);\r\n\r\n/* ======================== 进度追踪 ======================== */\r\n\r\nconst urlRef = toRef(props, \"url\");\r\nconst progressTracker = useProgressTracker(\r\n playerRef,\r\n currentTime,\r\n duration,\r\n urlRef,\r\n props.onProgress,\r\n props.antiCheat,\r\n);\r\n\r\n/* ======================== 清晰度切换 ======================== */\r\n\r\nconst qualitySwitch = useQualitySwitch(playerRef, props.qualityList);\r\n\r\n/* ======================== 章节标记 ======================== */\r\n\r\nconst chaptersRef = computed(() => props.chapters ?? []);\r\nconst chapters = useChapters(chaptersRef, currentTime, duration, playback.seek);\r\n\r\n/* ======================== 书签笔记 ======================== */\r\n\r\nconst bookmarksState = useBookmarks(\r\n urlRef,\r\n currentTime,\r\n playback.seek,\r\n props.bookmarks,\r\n);\r\n\r\n/* ======================== 防作弊 ======================== */\r\n\r\nconst antiCheatState = useAntiCheat(playerRef, currentTime, props.antiCheat);\r\n\r\n/* ======================== 字幕 ======================== */\r\n\r\nconst subtitle = useSubtitle(playerRef, props.subtitles, currentTime);\r\n\r\n/* ======================== 测验 ======================== */\r\n\r\nconst quizzesRef = computed(() => props.quizzes ?? []);\r\nconst quiz = useQuiz(playerRef, currentTime, quizzesRef);\r\n\r\n/* ======================== 小窗播放 ======================== */\r\n\r\nconst miniEnabled = computed(() => props.miniPlayer ?? false);\r\nconst miniPlayer = useMiniPlayer(wrapperRef, miniEnabled);\r\n\r\n/* ======================== 快捷键 ======================== */\r\n\r\nconst keyboard = useKeyboard(playerRef, wrapperRef, {\r\n enabled: props.keyboard !== false,\r\n onToggleFullscreen: () => {\r\n playerRef.value?.getFullscreen?.();\r\n },\r\n});\r\n\r\n/* ======================== 数据分析 ======================== */\r\n\r\nlet analyticsDestroy: (() => void) | null = null;\r\n\r\n/* ======================== 扩展控制栏:书签始终可用,章节/清晰度按需 ======================== */\r\n\r\nconst showExtendedControls = true;\r\n\r\n/* ======================== 事件处理 ======================== */\r\n\r\n/** 处理添加书签 */\r\nfunction handleAddBookmark(note: string) {\r\n bookmarksState.addBookmark(note);\r\n emit(\"bookmarkChange\", bookmarksState.bookmarks.value);\r\n}\r\n\r\n/** 处理测验提交 */\r\nfunction handleQuizSubmit() {\r\n const isCorrect = quiz.submitAnswer();\r\n const activeQuiz = quiz.activeQuiz.value;\r\n if (activeQuiz) {\r\n emit(\"quizAnswer\", activeQuiz.id, quiz.selectedAnswer.value, isCorrect);\r\n }\r\n}\r\n\r\n/** 处理测验关闭 */\r\nfunction handleQuizClose() {\r\n quiz.closeQuiz();\r\n}\r\n\r\n/* ======================== 监听播放器事件并向外 emit ======================== */\r\n\r\nwatch(playerState, (state) => {\r\n emit(\"stateChange\", state);\r\n});\r\n\r\nwatch(isFullscreen, (val) => {\r\n emit(\"fullscreenChange\", val);\r\n});\r\n\r\n/* 监听章节变化 */\r\nwatch(\r\n () => chapters.currentChapter.value,\r\n (chapter) => {\r\n if (chapter) {\r\n emit(\"chapterChange\", chapter);\r\n }\r\n },\r\n);\r\n\r\n/* ======================== 初始化 & 销毁 ======================== */\r\n\r\nonMounted(async () => {\r\n /* 将 wrapperRef 的 el 赋值给 core */\r\n core.containerRef.value = containerRef.value;\r\n\r\n await nextTick();\r\n await core.initPlayer();\r\n\r\n const player = playerRef.value;\r\n if (!player) return;\r\n\r\n /* 绑定各模块事件 */\r\n qualitySwitch.bindEvents(player);\r\n antiCheatState.bindEvents(player);\r\n subtitle.initSubtitles();\r\n keyboard.startListening();\r\n miniPlayer.initObserver();\r\n\r\n /* 恢复断点续播 */\r\n const restoreTime = progressTracker.restoreProgress();\r\n if (restoreTime > 0 && props.startTime === undefined) {\r\n player.currentTime = restoreTime;\r\n }\r\n\r\n /* 播放器事件 → 进度追踪 */\r\n player.on(Events.PLAY, () => {\r\n progressTracker.onPlay();\r\n });\r\n player.on(Events.PAUSE, () => {\r\n progressTracker.onPause();\r\n });\r\n player.on(Events.TIME_UPDATE, () => {\r\n progressTracker.onTimeUpdate();\r\n emit(\"timeUpdate\", currentTime.value, duration.value);\r\n emit(\"progressUpdate\", progressTracker.getProgressData());\r\n });\r\n player.on(Events.ENDED, () => {\r\n emit(\"ended\");\r\n antiCheatState.markAsWatched();\r\n });\r\n player.on(Events.ERROR, () => {\r\n emit(\"error\", new Error(\"播放器错误\"));\r\n });\r\n player.on(Events.READY, () => {\r\n emit(\"ready\");\r\n });\r\n\r\n /* xgplayer 原生清晰度切换 → 向外 emit */\r\n player.on(Events.AFTER_DEFINITION_CHANGE, (data: { to: string }) => {\r\n emit(\"qualityChange\", data.to as QualityLevel);\r\n });\r\n\r\n /* xgplayer 原生倍速切换 → 向外 emit */\r\n player.on(Events.RATE_CHANGE, (rate: number) => {\r\n emit(\"rateChange\", rate);\r\n });\r\n\r\n /* 初始化数据分析 */\r\n if (props.onAnalytics) {\r\n const analytics = createAnalyticsPlugin(player, props.onAnalytics);\r\n analyticsDestroy = analytics.destroy;\r\n }\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n analyticsDestroy?.();\r\n keyboard.stopListening();\r\n miniPlayer.destroyObserver();\r\n progressTracker.stopTracking();\r\n core.destroyPlayer();\r\n});\r\n\r\n/* ======================== 暴露方法 ======================== */\r\n\r\ndefineExpose<VideoPlayerExpose>({\r\n play: playback.play,\r\n pause: playback.pause,\r\n seek: playback.seek,\r\n setPlaybackRate: playback.setPlaybackRate,\r\n setVolume: playback.setVolume,\r\n switchQuality: qualitySwitch.switchQuality,\r\n getProgressData: progressTracker.getProgressData,\r\n destroy: core.destroyPlayer,\r\n getPlayerInstance: () => playerRef.value,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n","<!--\r\n * @Author: ChenYu ycyplus@gmail.com\r\n * @Date: 2026-02-26\r\n * @Description: 视频播放器组件(基于 xgplayer)\r\n * @Migration: naive-ui-components 组件库迁移版本\r\n * Copyright (c) 2026 by CHENY, All Rights Reserved.\r\n-->\r\n\r\n<template>\r\n <div\r\n ref=\"wrapperRef\"\r\n class=\"c-video-player\"\r\n :class=\"{\r\n 'is-mini': miniPlayer.isMiniMode.value,\r\n 'is-fullscreen': isFullscreen,\r\n }\"\r\n tabindex=\"0\"\r\n >\r\n <!-- 播放器容器 -->\r\n <div ref=\"containerRef\" class=\"c-video-player__container\" />\r\n\r\n <!-- 动态水印 -->\r\n <WatermarkOverlay\r\n :visible=\"antiCheatState.showWatermark.value\"\r\n :text=\"antiCheatState.watermarkText.value\"\r\n />\r\n\r\n <!-- 字幕渲染层 -->\r\n <SubtitleOverlay\r\n :text=\"subtitle.currentText.value\"\r\n :tracks=\"subtitle.subtitleList.value\"\r\n :active-language=\"subtitle.activeLanguage.value\"\r\n @switch=\"subtitle.switchSubtitle\"\r\n @close=\"subtitle.closeSubtitle\"\r\n />\r\n\r\n <!-- 测验弹窗 -->\r\n <QuizOverlay\r\n v-if=\"props.quizzes?.length\"\r\n v-model:answer=\"quiz.selectedAnswer.value\"\r\n :quiz=\"quiz.activeQuiz.value\"\r\n :show-result=\"quiz.showResult.value\"\r\n :is-correct=\"quiz.lastAnswerCorrect.value\"\r\n @submit=\"handleQuizSubmit\"\r\n @retry=\"quiz.retryQuiz()\"\r\n @close=\"handleQuizClose\"\r\n />\r\n\r\n <!-- 扩展控制栏(xgplayer 自带控制栏外的扩展) -->\r\n <ControlBar :visible=\"showExtendedControls\">\r\n <template #left>\r\n <!-- 章节标记 -->\r\n <ChapterMarkers\r\n v-if=\"props.chapters?.length\"\r\n :chapters=\"chaptersRef\"\r\n :current-chapter=\"chapters.currentChapter.value\"\r\n :current-index=\"chapters.currentChapterIndex.value\"\r\n @go-to=\"chapters.goToChapter\"\r\n />\r\n </template>\r\n\r\n <template #right>\r\n <!-- 书签面板 -->\r\n <BookmarkPanel\r\n :bookmarks=\"bookmarksState.bookmarks.value\"\r\n @add=\"handleAddBookmark\"\r\n @remove=\"bookmarksState.removeBookmark\"\r\n @go-to=\"bookmarksState.goToBookmark\"\r\n />\r\n </template>\r\n </ControlBar>\r\n\r\n <!-- 小窗关闭按钮 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-close\"\r\n @click=\"miniPlayer.closeMiniPlayer()\"\r\n >\r\n ✕\r\n </div>\r\n\r\n <!-- 小窗点击回原位 -->\r\n <div\r\n v-if=\"miniPlayer.isMiniMode.value\"\r\n class=\"c-video-player__mini-back\"\r\n @click=\"miniPlayer.scrollToPlayer()\"\r\n >\r\n 回到原位\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport {\r\n ref,\r\n computed,\r\n watch,\r\n toRef,\r\n onMounted,\r\n onBeforeUnmount,\r\n nextTick,\r\n} from \"vue\";\r\nimport { Events } from \"xgplayer\";\r\nimport {\r\n usePlayerCore,\r\n usePlaybackControl,\r\n useProgressTracker,\r\n useQualitySwitch,\r\n useChapters,\r\n useBookmarks,\r\n useAntiCheat,\r\n useSubtitle,\r\n useQuiz,\r\n useMiniPlayer,\r\n useKeyboard,\r\n} from \"./composables\";\r\nimport { createAnalyticsPlugin } from \"./plugins\";\r\nimport ControlBar from \"./components/ControlBar.vue\";\r\nimport SubtitleOverlay from \"./components/SubtitleOverlay.vue\";\r\nimport ChapterMarkers from \"./components/ChapterMarkers.vue\";\r\nimport BookmarkPanel from \"./components/BookmarkPanel.vue\";\r\nimport QuizOverlay from \"./components/QuizOverlay.vue\";\r\nimport WatermarkOverlay from \"./components/WatermarkOverlay.vue\";\r\nimport type {\r\n VideoPlayerProps,\r\n VideoPlayerExpose,\r\n PlayerState,\r\n QualityLevel,\r\n ProgressData,\r\n Bookmark,\r\n Chapter,\r\n} from \"./types\";\r\n\r\ndefineOptions({ name: \"C_VideoPlayer\" });\r\n\r\nconst props = defineProps<VideoPlayerProps>();\r\n\r\nconst emit = defineEmits<{\r\n ready: [];\r\n stateChange: [state: PlayerState];\r\n timeUpdate: [currentTime: number, duration: number];\r\n ended: [];\r\n error: [error: Error];\r\n qualityChange: [quality: QualityLevel];\r\n rateChange: [rate: number];\r\n fullscreenChange: [isFullscreen: boolean];\r\n bookmarkChange: [bookmarks: Bookmark[]];\r\n quizAnswer: [quizId: string, answer: string | string[], isCorrect: boolean];\r\n chapterChange: [chapter: Chapter];\r\n progressUpdate: [data: ProgressData];\r\n}>();\r\n\r\n/* ======================== 核心 ======================== */\r\n\r\nconst core = usePlayerCore(props);\r\nconst {\r\n containerRef,\r\n playerRef,\r\n playerState,\r\n currentTime,\r\n duration,\r\n isFullscreen,\r\n} = core;\r\n\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\n\r\n/* ======================== 播放控制 ======================== */\r\n\r\nconst playback = usePlaybackControl(playerRef);\r\n\r\n/* ======================== 进度追踪 ======================== */\r\n\r\nconst urlRef = toRef(props, \"url\");\r\nconst progressTracker = useProgressTracker(\r\n playerRef,\r\n currentTime,\r\n duration,\r\n urlRef,\r\n props.onProgress,\r\n props.antiCheat,\r\n);\r\n\r\n/* ======================== 清晰度切换 ======================== */\r\n\r\nconst qualitySwitch = useQualitySwitch(playerRef, props.qualityList);\r\n\r\n/* ======================== 章节标记 ======================== */\r\n\r\nconst chaptersRef = computed(() => props.chapters ?? []);\r\nconst chapters = useChapters(chaptersRef, currentTime, duration, playback.seek);\r\n\r\n/* ======================== 书签笔记 ======================== */\r\n\r\nconst bookmarksState = useBookmarks(\r\n urlRef,\r\n currentTime,\r\n playback.seek,\r\n props.bookmarks,\r\n);\r\n\r\n/* ======================== 防作弊 ======================== */\r\n\r\nconst antiCheatState = useAntiCheat(playerRef, currentTime, props.antiCheat);\r\n\r\n/* ======================== 字幕 ======================== */\r\n\r\nconst subtitle = useSubtitle(playerRef, props.subtitles, currentTime);\r\n\r\n/* ======================== 测验 ======================== */\r\n\r\nconst quizzesRef = computed(() => props.quizzes ?? []);\r\nconst quiz = useQuiz(playerRef, currentTime, quizzesRef);\r\n\r\n/* ======================== 小窗播放 ======================== */\r\n\r\nconst miniEnabled = computed(() => props.miniPlayer ?? false);\r\nconst miniPlayer = useMiniPlayer(wrapperRef, miniEnabled);\r\n\r\n/* ======================== 快捷键 ======================== */\r\n\r\nconst keyboard = useKeyboard(playerRef, wrapperRef, {\r\n enabled: props.keyboard !== false,\r\n onToggleFullscreen: () => {\r\n playerRef.value?.getFullscreen?.();\r\n },\r\n});\r\n\r\n/* ======================== 数据分析 ======================== */\r\n\r\nlet analyticsDestroy: (() => void) | null = null;\r\n\r\n/* ======================== 扩展控制栏:书签始终可用,章节/清晰度按需 ======================== */\r\n\r\nconst showExtendedControls = true;\r\n\r\n/* ======================== 事件处理 ======================== */\r\n\r\n/** 处理添加书签 */\r\nfunction handleAddBookmark(note: string) {\r\n bookmarksState.addBookmark(note);\r\n emit(\"bookmarkChange\", bookmarksState.bookmarks.value);\r\n}\r\n\r\n/** 处理测验提交 */\r\nfunction handleQuizSubmit() {\r\n const isCorrect = quiz.submitAnswer();\r\n const activeQuiz = quiz.activeQuiz.value;\r\n if (activeQuiz) {\r\n emit(\"quizAnswer\", activeQuiz.id, quiz.selectedAnswer.value, isCorrect);\r\n }\r\n}\r\n\r\n/** 处理测验关闭 */\r\nfunction handleQuizClose() {\r\n quiz.closeQuiz();\r\n}\r\n\r\n/* ======================== 监听播放器事件并向外 emit ======================== */\r\n\r\nwatch(playerState, (state) => {\r\n emit(\"stateChange\", state);\r\n});\r\n\r\nwatch(isFullscreen, (val) => {\r\n emit(\"fullscreenChange\", val);\r\n});\r\n\r\n/* 监听章节变化 */\r\nwatch(\r\n () => chapters.currentChapter.value,\r\n (chapter) => {\r\n if (chapter) {\r\n emit(\"chapterChange\", chapter);\r\n }\r\n },\r\n);\r\n\r\n/* ======================== 初始化 & 销毁 ======================== */\r\n\r\nonMounted(async () => {\r\n /* 将 wrapperRef 的 el 赋值给 core */\r\n core.containerRef.value = containerRef.value;\r\n\r\n await nextTick();\r\n await core.initPlayer();\r\n\r\n const player = playerRef.value;\r\n if (!player) return;\r\n\r\n /* 绑定各模块事件 */\r\n qualitySwitch.bindEvents(player);\r\n antiCheatState.bindEvents(player);\r\n subtitle.initSubtitles();\r\n keyboard.startListening();\r\n miniPlayer.initObserver();\r\n\r\n /* 恢复断点续播 */\r\n const restoreTime = progressTracker.restoreProgress();\r\n if (restoreTime > 0 && props.startTime === undefined) {\r\n player.currentTime = restoreTime;\r\n }\r\n\r\n /* 播放器事件 → 进度追踪 */\r\n player.on(Events.PLAY, () => {\r\n progressTracker.onPlay();\r\n });\r\n player.on(Events.PAUSE, () => {\r\n progressTracker.onPause();\r\n });\r\n player.on(Events.TIME_UPDATE, () => {\r\n progressTracker.onTimeUpdate();\r\n emit(\"timeUpdate\", currentTime.value, duration.value);\r\n emit(\"progressUpdate\", progressTracker.getProgressData());\r\n });\r\n player.on(Events.ENDED, () => {\r\n emit(\"ended\");\r\n antiCheatState.markAsWatched();\r\n });\r\n player.on(Events.ERROR, () => {\r\n emit(\"error\", new Error(\"播放器错误\"));\r\n });\r\n player.on(Events.READY, () => {\r\n emit(\"ready\");\r\n });\r\n\r\n /* xgplayer 原生清晰度切换 → 向外 emit */\r\n player.on(Events.AFTER_DEFINITION_CHANGE, (data: { to: string }) => {\r\n emit(\"qualityChange\", data.to as QualityLevel);\r\n });\r\n\r\n /* xgplayer 原生倍速切换 → 向外 emit */\r\n player.on(Events.RATE_CHANGE, (rate: number) => {\r\n emit(\"rateChange\", rate);\r\n });\r\n\r\n /* 初始化数据分析 */\r\n if (props.onAnalytics) {\r\n const analytics = createAnalyticsPlugin(player, props.onAnalytics);\r\n analyticsDestroy = analytics.destroy;\r\n }\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n analyticsDestroy?.();\r\n keyboard.stopListening();\r\n miniPlayer.destroyObserver();\r\n progressTracker.stopTracking();\r\n core.destroyPlayer();\r\n});\r\n\r\n/* ======================== 暴露方法 ======================== */\r\n\r\ndefineExpose<VideoPlayerExpose>({\r\n play: playback.play,\r\n pause: playback.pause,\r\n seek: playback.seek,\r\n setPlaybackRate: playback.setPlaybackRate,\r\n setVolume: playback.setVolume,\r\n switchQuality: qualitySwitch.switchQuality,\r\n getProgressData: progressTracker.getProgressData,\r\n destroy: core.destroyPlayer,\r\n getPlayerInstance: () => playerRef.value,\r\n});\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n@use \"./index.scss\";\r\n</style>\r\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"C_VtableGantt-fhItIiHE.css","names":[],"sources":["../src/components/C_VtableGantt/index.vue?vue&type=style&index=0&scoped=bdd6834b&lang.scss"],"sourcesContent":["/* 甘特图组件样式 */\n.c-vtable-gantt-wrapper[data-v-bdd6834b] {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--c-border, #e1e4e8);\n border-radius: var(--c-radius, 8px);\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n height: 100%;\n}\n.c-vtable-gantt-wrapper .gantt-toolbar[data-v-bdd6834b] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid var(--c-border, #e1e4e8);\n min-height: 48px;\n flex-shrink: 0;\n}\n.c-vtable-gantt-wrapper .gantt-toolbar .toolbar-left .gantt-title[data-v-bdd6834b] {\n font-size: 16px;\n font-weight: 600;\n color: var(--c-text-1, #333);\n}\n.c-vtable-gantt-wrapper .gantt-toolbar .toolbar-right[data-v-bdd6834b] {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] {\n flex: 1;\n position: relative;\n min-height: 400px;\n overflow: hidden;\n /* 确保甘特图组件完全填充容器 */\n /* 确保甘特图内部组件正确填充 */\n /* 防止内部表格自动调整高度 */\n /* 防止拖动时底部色块闪烁 */\n /* 针对拖动元素的优化 */\n /* 防止拖动时底部区域的重绘闪烁 */\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] > * {\n width: 100%;\n height: 100%;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt {\n width: 100% !important;\n height: 100% !important;\n overflow: hidden;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt * {\n backface-visibility: hidden;\n transform: translateZ(0);\n -webkit-font-smoothing: subpixel-antialiased;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-table {\n height: 100% !important;\n box-sizing: border-box;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-chart {\n position: relative;\n overflow: hidden;\n will-change: transform;\n transform: translateZ(0);\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-task {\n backface-visibility: hidden;\n transform: translateZ(0);\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-task.dragging {\n z-index: 1000;\n pointer-events: none;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-chart-container {\n position: relative;\n overflow: hidden;\n}\n.c-vtable-gantt-wrapper .gantt-container[data-v-bdd6834b] .vtable-gantt-chart-container::after {\n content: \"\";\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: transparent;\n pointer-events: none;\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
@@ -0,0 +1,6 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ require('./C_Icon.js');
3
+ const require_C_VtableGantt = require('./C_VtableGantt.js');
4
+
5
+ exports.C_VtableGantt = require_C_VtableGantt.C_VtableGantt_default;
6
+ exports.ganttPresetConfigs = require_C_VtableGantt.presetConfigs;
@@ -0,0 +1,2 @@
1
+ import { a as presetConfigs, i as GanttTask, n as GanttOptions, r as GanttPreset, t as _default } from "./index16.vue.js";
2
+ export { _default as C_VtableGantt, type GanttOptions, type GanttPreset, type GanttTask, presetConfigs as ganttPresetConfigs };
@@ -0,0 +1,2 @@
1
+ import { a as presetConfigs, i as GanttTask, n as GanttOptions, r as GanttPreset, t as _default } from "./index16.vue.js";
2
+ export { _default as C_VtableGantt, type GanttOptions, type GanttPreset, type GanttTask, presetConfigs as ganttPresetConfigs };
@@ -0,0 +1,4 @@
1
+ import "./C_Icon2.js";
2
+ import { n as presetConfigs, t as C_VtableGantt_default } from "./C_VtableGantt2.js";
3
+
4
+ export { C_VtableGantt_default as C_VtableGantt, presetConfigs as ganttPresetConfigs };