@xiping/react-components 1.0.52 → 1.0.54
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/cjs/components/image-compare/ImageCompare.d.ts +6 -0
- package/dist/cjs/components/image-compare/ImageCompare.js +1 -1
- package/dist/cjs/components/subtitle-player/CurrentMode.d.ts +2 -0
- package/dist/cjs/components/subtitle-player/CurrentMode.js +2 -2
- package/dist/cjs/components/subtitle-player/LyricsMode.d.ts +2 -0
- package/dist/cjs/components/subtitle-player/LyricsMode.js +2 -2
- package/dist/cjs/components/subtitle-player/SubtitlePlayer.d.ts +34 -0
- package/dist/cjs/components/subtitle-player/SubtitlePlayer.js +1 -1
- package/dist/cjs/components/subtitle-player/utils.d.ts +1 -0
- package/dist/cjs/components/subtitle-player/utils.js +1 -1
- package/dist/cjs/react-components.css +1 -1
- package/dist/es/components/image-compare/ImageCompare.d.ts +6 -0
- package/dist/es/components/image-compare/ImageCompare.js +86 -67
- package/dist/es/components/subtitle-player/CurrentMode.d.ts +2 -0
- package/dist/es/components/subtitle-player/CurrentMode.js +25 -22
- package/dist/es/components/subtitle-player/LyricsMode.d.ts +2 -0
- package/dist/es/components/subtitle-player/LyricsMode.js +46 -43
- package/dist/es/components/subtitle-player/SubtitlePlayer.d.ts +34 -0
- package/dist/es/components/subtitle-player/SubtitlePlayer.js +146 -98
- package/dist/es/components/subtitle-player/utils.d.ts +1 -0
- package/dist/es/components/subtitle-player/utils.js +7 -6
- package/dist/es/react-components.css +1 -1
- package/package.json +16 -16
- package/dist/cjs/components/toast/index.d.ts +0 -4
- package/dist/cjs/components/translate/Translate.d.ts +0 -5
- package/dist/es/components/toast/index.d.ts +0 -4
- package/dist/es/components/translate/Translate.d.ts +0 -5
|
@@ -1,85 +1,104 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import
|
|
3
|
-
import { useState as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}, [
|
|
23
|
-
|
|
24
|
-
}, [
|
|
25
|
-
|
|
26
|
-
}, []),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { jsxs as j, jsx as t } from "react/jsx-runtime";
|
|
2
|
+
import F from "clsx";
|
|
3
|
+
import { useState as E, useRef as i, useCallback as a, useEffect as y, useMemo as w } from "react";
|
|
4
|
+
/* empty css */
|
|
5
|
+
const q = (N) => {
|
|
6
|
+
const {
|
|
7
|
+
originalImage: c,
|
|
8
|
+
modifiedImage: b,
|
|
9
|
+
className: S,
|
|
10
|
+
dividerWidth: l = 1,
|
|
11
|
+
originalLabel: I = "原图",
|
|
12
|
+
modifiedLabel: P = "处理过的图"
|
|
13
|
+
} = N, [s, $] = E(50), [x, A] = E(16 / 9), h = i(null), u = i(!1), o = i(null), d = i(null), n = i(null), k = a((e) => {
|
|
14
|
+
if (!h.current) return;
|
|
15
|
+
(!d.current || !u.current) && (d.current = h.current.getBoundingClientRect());
|
|
16
|
+
const r = d.current, g = (e - r.left) / r.width * 100, p = Math.min(100, Math.max(0, g));
|
|
17
|
+
$(p);
|
|
18
|
+
}, []), m = a((e) => {
|
|
19
|
+
u.current && (o.current !== null && cancelAnimationFrame(o.current), o.current = requestAnimationFrame(() => {
|
|
20
|
+
k(e);
|
|
21
|
+
}));
|
|
22
|
+
}, [k]), M = a((e) => {
|
|
23
|
+
m(e.clientX);
|
|
24
|
+
}, [m]), R = a((e) => {
|
|
25
|
+
e.touches.length > 0 && m(e.touches[0].clientX);
|
|
26
|
+
}, [m]), L = a(() => {
|
|
27
|
+
u.current = !0, d.current = null;
|
|
28
|
+
const e = (f) => M(f), r = (f) => R(f), v = () => {
|
|
29
|
+
u.current = !1, o.current !== null && (cancelAnimationFrame(o.current), o.current = null), n.current && (n.current(), n.current = null);
|
|
30
|
+
}, g = () => v(), p = () => v();
|
|
31
|
+
window.addEventListener("mousemove", e), window.addEventListener("touchmove", r, { passive: !1 }), window.addEventListener("mouseup", g), window.addEventListener("touchend", p);
|
|
32
|
+
const T = () => {
|
|
33
|
+
window.removeEventListener("mousemove", e), window.removeEventListener("touchmove", r), window.removeEventListener("mouseup", g), window.removeEventListener("touchend", p);
|
|
34
|
+
};
|
|
35
|
+
n.current = T;
|
|
36
|
+
}, [M, R]);
|
|
37
|
+
y(() => () => {
|
|
38
|
+
n.current && (n.current(), n.current = null);
|
|
39
|
+
}, []), y(() => {
|
|
30
40
|
const e = new Image();
|
|
31
41
|
e.onload = () => {
|
|
32
|
-
|
|
33
|
-
}, e.src =
|
|
34
|
-
}, [
|
|
42
|
+
A(e.width / e.height);
|
|
43
|
+
}, e.src = c;
|
|
44
|
+
}, [c]);
|
|
45
|
+
const C = w(
|
|
46
|
+
() => ({
|
|
47
|
+
backgroundRepeat: "no-repeat",
|
|
48
|
+
backgroundPosition: "top left",
|
|
49
|
+
backgroundImage: `url(${c})`,
|
|
50
|
+
backgroundSize: "contain",
|
|
51
|
+
aspectRatio: x
|
|
52
|
+
}),
|
|
53
|
+
[c, x]
|
|
54
|
+
), D = w(
|
|
55
|
+
() => ({
|
|
56
|
+
width: `${s}%`,
|
|
57
|
+
backgroundImage: `url(${b})`,
|
|
58
|
+
backgroundRepeat: "no-repeat",
|
|
59
|
+
backgroundPosition: "top left",
|
|
60
|
+
backgroundSize: "auto 100%"
|
|
61
|
+
}),
|
|
62
|
+
[s, b]
|
|
63
|
+
), G = w(
|
|
64
|
+
() => ({
|
|
65
|
+
left: `${s}%`,
|
|
66
|
+
transform: "translateX(-50%)",
|
|
67
|
+
width: typeof l == "number" ? `${l}px` : l
|
|
68
|
+
}),
|
|
69
|
+
[s, l]
|
|
70
|
+
);
|
|
71
|
+
return /* @__PURE__ */ j(
|
|
35
72
|
"div",
|
|
36
73
|
{
|
|
37
|
-
ref:
|
|
38
|
-
className:
|
|
39
|
-
style:
|
|
40
|
-
backgroundRepeat: "no-repeat",
|
|
41
|
-
backgroundPosition: "top left",
|
|
42
|
-
backgroundImage: `url(${o})`,
|
|
43
|
-
backgroundSize: "contain",
|
|
44
|
-
aspectRatio: w
|
|
45
|
-
},
|
|
46
|
-
onMouseMove: a ? M : void 0,
|
|
47
|
-
onTouchMove: a ? k : void 0,
|
|
48
|
-
onMouseUp: u,
|
|
49
|
-
onMouseLeave: u,
|
|
50
|
-
onTouchEnd: u,
|
|
74
|
+
ref: h,
|
|
75
|
+
className: F("xiping-image-compare-container", S),
|
|
76
|
+
style: C,
|
|
51
77
|
children: [
|
|
52
|
-
/* @__PURE__ */
|
|
78
|
+
/* @__PURE__ */ t("div", { className: "xiping-image-compare-label xiping-image-compare-label-original", children: I }),
|
|
79
|
+
/* @__PURE__ */ t(
|
|
53
80
|
"div",
|
|
54
81
|
{
|
|
55
|
-
className: "
|
|
56
|
-
style:
|
|
57
|
-
|
|
58
|
-
backgroundImage: `url(${n})`,
|
|
59
|
-
backgroundRepeat: "no-repeat",
|
|
60
|
-
backgroundPosition: "top left",
|
|
61
|
-
backgroundSize: "auto 100%"
|
|
62
|
-
}
|
|
82
|
+
className: "xiping-image-compare-overlay",
|
|
83
|
+
style: D,
|
|
84
|
+
children: /* @__PURE__ */ t("div", { className: "xiping-image-compare-label xiping-image-compare-label-modified", children: P })
|
|
63
85
|
}
|
|
64
86
|
),
|
|
65
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ t(
|
|
66
88
|
"div",
|
|
67
89
|
{
|
|
68
|
-
className: "
|
|
69
|
-
style:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
onMouseDown: m,
|
|
74
|
-
onTouchStart: m,
|
|
75
|
-
children: /* @__PURE__ */ s("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-white shadow-lg flex items-center justify-center", children: /* @__PURE__ */ s(
|
|
90
|
+
className: "xiping-image-compare-divider",
|
|
91
|
+
style: G,
|
|
92
|
+
onMouseDown: L,
|
|
93
|
+
onTouchStart: L,
|
|
94
|
+
children: /* @__PURE__ */ t("div", { className: "xiping-image-compare-divider-button", children: /* @__PURE__ */ t(
|
|
76
95
|
"svg",
|
|
77
96
|
{
|
|
78
|
-
className: "
|
|
97
|
+
className: "xiping-image-compare-divider-icon",
|
|
79
98
|
fill: "none",
|
|
80
99
|
stroke: "currentColor",
|
|
81
100
|
viewBox: "0 0 24 24",
|
|
82
|
-
children: /* @__PURE__ */
|
|
101
|
+
children: /* @__PURE__ */ t(
|
|
83
102
|
"path",
|
|
84
103
|
{
|
|
85
104
|
strokeLinecap: "round",
|
|
@@ -97,5 +116,5 @@ const T = (i, o) => {
|
|
|
97
116
|
);
|
|
98
117
|
};
|
|
99
118
|
export {
|
|
100
|
-
|
|
119
|
+
q as default
|
|
101
120
|
};
|
|
@@ -13,6 +13,8 @@ export interface CurrentModeProps {
|
|
|
13
13
|
enableWordHover: boolean;
|
|
14
14
|
/** 悬停覆盖层节点 */
|
|
15
15
|
overlayNode: React.ReactNode;
|
|
16
|
+
/** 整条字幕点击处理器 */
|
|
17
|
+
onSubtitleClick?: (event: React.MouseEvent<HTMLDivElement>, entry: SubtitleEntry, label?: string) => void;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* 当前模式组件:只显示当前时间对应的字幕
|
|
@@ -1,40 +1,43 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { renderTextWithWords as
|
|
3
|
-
const
|
|
4
|
-
currentEntries:
|
|
5
|
-
wordHoverFactory:
|
|
6
|
-
enableWordHover:
|
|
7
|
-
overlayNode:
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { jsxs as p, Fragment as h, jsx as r } from "react/jsx-runtime";
|
|
2
|
+
import { renderTextWithWords as x } from "./utils.js";
|
|
3
|
+
const N = ({
|
|
4
|
+
currentEntries: a,
|
|
5
|
+
wordHoverFactory: u,
|
|
6
|
+
enableWordHover: d,
|
|
7
|
+
overlayNode: c,
|
|
8
|
+
onSubtitleClick: t
|
|
9
|
+
}) => a.some(({ entry: i }) => i !== null) ? /* @__PURE__ */ p(h, { children: [
|
|
10
|
+
a.map(({ entry: i, label: s }, m) => i ? /* @__PURE__ */ p(
|
|
10
11
|
"div",
|
|
11
12
|
{
|
|
12
13
|
className: "xiping-subtitle-player__item",
|
|
13
14
|
"data-label": s || void 0,
|
|
15
|
+
onClick: t ? (e) => t(e, i, s) : void 0,
|
|
16
|
+
style: t ? { cursor: "pointer" } : void 0,
|
|
14
17
|
children: [
|
|
15
|
-
s && /* @__PURE__ */
|
|
16
|
-
/* @__PURE__ */
|
|
17
|
-
let
|
|
18
|
+
s && /* @__PURE__ */ r("span", { className: "xiping-subtitle-player__label", children: s }),
|
|
19
|
+
/* @__PURE__ */ r("div", { className: "xiping-subtitle-player__text", children: (() => {
|
|
20
|
+
let e = 0;
|
|
18
21
|
return i.text.split(`
|
|
19
|
-
`).map((n,
|
|
22
|
+
`).map((n, l) => {
|
|
20
23
|
if (!i.words || i.words.length === 0)
|
|
21
|
-
return /* @__PURE__ */
|
|
22
|
-
const
|
|
24
|
+
return /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__line", children: n }, l);
|
|
25
|
+
const o = x(
|
|
23
26
|
n,
|
|
24
27
|
i.words,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
e,
|
|
29
|
+
l,
|
|
30
|
+
d ? u(i, s) : void 0
|
|
28
31
|
);
|
|
29
|
-
return
|
|
32
|
+
return e = o.nextWordIndex, /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__line", children: o.nodes }, l);
|
|
30
33
|
});
|
|
31
34
|
})() })
|
|
32
35
|
]
|
|
33
36
|
},
|
|
34
|
-
|
|
37
|
+
m
|
|
35
38
|
) : null),
|
|
36
|
-
|
|
39
|
+
d && /* @__PURE__ */ r("div", { className: "xiping-subtitle-player__hover-layer", children: c })
|
|
37
40
|
] }) : null;
|
|
38
41
|
export {
|
|
39
|
-
|
|
42
|
+
N as CurrentMode
|
|
40
43
|
};
|
|
@@ -19,6 +19,8 @@ export interface LyricsModeProps {
|
|
|
19
19
|
overlayNode: React.ReactNode;
|
|
20
20
|
/** 容器引用,用于滚动 */
|
|
21
21
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
22
|
+
/** 整条字幕点击处理器 */
|
|
23
|
+
onSubtitleClick?: (event: React.MouseEvent<HTMLDivElement>, entry: SubtitleEntry, label?: string) => void;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* 歌词模式组件:显示全部字幕并高亮当前字幕
|
|
@@ -1,67 +1,70 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { useRef as
|
|
3
|
-
import
|
|
4
|
-
import { secondsToTimeString as
|
|
5
|
-
const
|
|
6
|
-
groupedEntriesByTime:
|
|
7
|
-
currentTime:
|
|
8
|
-
wordHoverFactory:
|
|
9
|
-
enableWordHover:
|
|
10
|
-
overlayNode:
|
|
11
|
-
containerRef:
|
|
1
|
+
import { jsxs as v, Fragment as j, jsx as s } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as F, useEffect as S } from "react";
|
|
3
|
+
import W from "clsx";
|
|
4
|
+
import { secondsToTimeString as $, renderTextWithWords as A } from "./utils.js";
|
|
5
|
+
const P = ({
|
|
6
|
+
groupedEntriesByTime: y,
|
|
7
|
+
currentTime: o,
|
|
8
|
+
wordHoverFactory: N,
|
|
9
|
+
enableWordHover: u,
|
|
10
|
+
overlayNode: b,
|
|
11
|
+
containerRef: n,
|
|
12
|
+
onSubtitleClick: c
|
|
12
13
|
}) => {
|
|
13
|
-
const
|
|
14
|
-
return
|
|
15
|
-
if (
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
top:
|
|
14
|
+
const a = F(null);
|
|
15
|
+
return S(() => {
|
|
16
|
+
if (a.current && n.current) {
|
|
17
|
+
const e = n.current, r = a.current, p = r.offsetTop, m = e.clientHeight, t = r.clientHeight, i = p - m / 2 + t / 2;
|
|
18
|
+
e.scrollTo({
|
|
19
|
+
top: i,
|
|
19
20
|
behavior: "smooth"
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
}, [
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
return /* @__PURE__ */
|
|
26
|
-
const
|
|
27
|
-
let
|
|
28
|
-
return /* @__PURE__ */
|
|
23
|
+
}, [o, n]), /* @__PURE__ */ v(j, { children: [
|
|
24
|
+
y.map((e, r) => {
|
|
25
|
+
const p = e[0]?.startTime ?? 0, m = $(p);
|
|
26
|
+
return /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__group", children: e.map(({ entry: t, label: i, startTime: T, endTime: _ }, g) => {
|
|
27
|
+
const h = o >= T && o <= _, w = o > _, H = g === 0;
|
|
28
|
+
let x = 0;
|
|
29
|
+
return /* @__PURE__ */ v(
|
|
29
30
|
"div",
|
|
30
31
|
{
|
|
31
|
-
ref:
|
|
32
|
-
className:
|
|
32
|
+
ref: h ? a : null,
|
|
33
|
+
className: W(
|
|
33
34
|
"xiping-subtitle-player__item",
|
|
34
35
|
{
|
|
35
|
-
"xiping-subtitle-player__item--active":
|
|
36
|
-
"xiping-subtitle-player__item--past":
|
|
36
|
+
"xiping-subtitle-player__item--active": h,
|
|
37
|
+
"xiping-subtitle-player__item--past": w
|
|
37
38
|
}
|
|
38
39
|
),
|
|
39
|
-
"data-label":
|
|
40
|
+
"data-label": i || void 0,
|
|
41
|
+
onClick: c ? (l) => c(l, t, i) : void 0,
|
|
42
|
+
style: c ? { cursor: "pointer" } : void 0,
|
|
40
43
|
children: [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/* @__PURE__ */
|
|
44
|
-
`).map((
|
|
44
|
+
H && /* @__PURE__ */ s("span", { className: "xiping-subtitle-player__time", children: m }),
|
|
45
|
+
i && /* @__PURE__ */ s("span", { className: "xiping-subtitle-player__label", children: i }),
|
|
46
|
+
/* @__PURE__ */ s("div", { className: "xiping-subtitle-player__text", children: t.text.split(`
|
|
47
|
+
`).map((l, d) => {
|
|
45
48
|
if (!t.words || t.words.length === 0)
|
|
46
|
-
return /* @__PURE__ */
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
+
return /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__line", children: l }, d);
|
|
50
|
+
const f = A(
|
|
51
|
+
l,
|
|
49
52
|
t.words,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
x,
|
|
54
|
+
d,
|
|
55
|
+
u ? N(t, i) : void 0
|
|
53
56
|
);
|
|
54
|
-
return
|
|
57
|
+
return x = f.nextWordIndex, /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__line", children: f.nodes }, d);
|
|
55
58
|
}) })
|
|
56
59
|
]
|
|
57
60
|
},
|
|
58
|
-
`${r}-${
|
|
61
|
+
`${r}-${g}`
|
|
59
62
|
);
|
|
60
63
|
}) }, r);
|
|
61
64
|
}),
|
|
62
|
-
|
|
65
|
+
u && /* @__PURE__ */ s("div", { className: "xiping-subtitle-player__hover-layer", children: b })
|
|
63
66
|
] });
|
|
64
67
|
};
|
|
65
68
|
export {
|
|
66
|
-
|
|
69
|
+
P as LyricsMode
|
|
67
70
|
};
|
|
@@ -26,6 +26,7 @@ export interface SubtitlePlayerProps {
|
|
|
26
26
|
/**
|
|
27
27
|
* 分词 hover 状态回调,由外部决定如何处理(比如请求第三方接口、展示弹窗等)
|
|
28
28
|
* 传入 null 表示 hover 结束
|
|
29
|
+
* target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
|
|
29
30
|
*/
|
|
30
31
|
onWordHoverChange?: (target: HoverTarget | null) => void;
|
|
31
32
|
/**
|
|
@@ -33,16 +34,49 @@ export interface SubtitlePlayerProps {
|
|
|
33
34
|
* 建议将数据请求放到父组件,通过该函数返回需要展示的内容
|
|
34
35
|
*/
|
|
35
36
|
renderWordOverlay?: (target: HoverTarget) => React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* 单词点击回调
|
|
39
|
+
* target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
|
|
40
|
+
*/
|
|
41
|
+
onWordClick?: (target: WordClickTarget) => void;
|
|
42
|
+
/**
|
|
43
|
+
* 整条字幕点击回调
|
|
44
|
+
* target.entry 包含完整的字幕条目数据(JSON 格式的数据项)
|
|
45
|
+
*/
|
|
46
|
+
onSubtitleClick?: (target: SubtitleClickTarget) => void;
|
|
36
47
|
}
|
|
37
48
|
export interface HoverTarget {
|
|
38
49
|
word: string;
|
|
39
50
|
wordIndex: number;
|
|
40
51
|
lineIndex: number;
|
|
52
|
+
/** 完整的字幕条目数据(JSON 格式的数据项) */
|
|
53
|
+
entry: SubtitleEntry;
|
|
54
|
+
label?: string;
|
|
55
|
+
rect: DOMRect;
|
|
56
|
+
element: HTMLElement;
|
|
57
|
+
}
|
|
58
|
+
/** 点击目标类型:单词或整条字幕 */
|
|
59
|
+
export type ClickTargetType = "word" | "subtitle";
|
|
60
|
+
export interface WordClickTarget {
|
|
61
|
+
type: "word";
|
|
62
|
+
word: string;
|
|
63
|
+
wordIndex: number;
|
|
64
|
+
lineIndex: number;
|
|
65
|
+
/** 完整的字幕条目数据(JSON 格式的数据项) */
|
|
66
|
+
entry: SubtitleEntry;
|
|
67
|
+
label?: string;
|
|
68
|
+
rect: DOMRect;
|
|
69
|
+
element: HTMLElement;
|
|
70
|
+
}
|
|
71
|
+
export interface SubtitleClickTarget {
|
|
72
|
+
type: "subtitle";
|
|
73
|
+
/** 完整的字幕条目数据(JSON 格式的数据项) */
|
|
41
74
|
entry: SubtitleEntry;
|
|
42
75
|
label?: string;
|
|
43
76
|
rect: DOMRect;
|
|
44
77
|
element: HTMLElement;
|
|
45
78
|
}
|
|
79
|
+
export type ClickTarget = WordClickTarget | SubtitleClickTarget;
|
|
46
80
|
/**
|
|
47
81
|
* 字幕播放组件
|
|
48
82
|
* 支持 SRT 和 WebVTT 格式,可以同时显示多个字幕进行对照
|