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