uview-plus 3.8.127 → 3.8.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +13 -0
  2. package/changelog.md +38 -0
  3. package/components/u-barcode/u-barcode.vue +1 -1
  4. package/components/u-calendar/month.vue +71 -35
  5. package/components/u-calendar/u-calendar.vue +67 -31
  6. package/components/u-car-keyboard/u-car-keyboard.vue +21 -8
  7. package/components/u-cate-tab/u-cate-tab.vue +56 -11
  8. package/components/u-cell/u-cell.vue +20 -16
  9. package/components/u-collapse-item/u-collapse-item.vue +34 -11
  10. package/components/u-datetime-picker/u-datetime-picker.vue +16 -0
  11. package/components/u-dropdown/u-dropdown.vue +15 -0
  12. package/components/u-grid/u-grid.vue +9 -0
  13. package/components/u-grid-item/u-grid-item.vue +8 -0
  14. package/components/u-icon/util.js +16 -1
  15. package/components/u-list/u-list.vue +59 -8
  16. package/components/u-list-item/u-list-item.vue +9 -3
  17. package/components/u-navbar/navbar.js +2 -1
  18. package/components/u-navbar/props.js +6 -1
  19. package/components/u-navbar/u-navbar.vue +20 -3
  20. package/components/u-number-keyboard/u-number-keyboard.vue +18 -6
  21. package/components/u-parse/parser.js +4 -3
  22. package/components/u-picker/u-picker.vue +6 -3
  23. package/components/u-search/u-search.vue +15 -5
  24. package/components/u-select/u-select.vue +206 -31
  25. package/components/u-slider/u-slider.vue +21 -12
  26. package/components/u-tabs/u-tabs.vue +5 -1
  27. package/components/u-text/u-text.vue +34 -32
  28. package/components/u-textarea/props.js +5 -0
  29. package/components/u-textarea/textarea.js +4 -1
  30. package/components/u-textarea/u-textarea.vue +3 -1
  31. package/components/u-upload/u-upload.vue +15 -4
  32. package/components/u-upload/utils.js +4 -1
  33. package/components/u-video/danmaku.js +97 -0
  34. package/components/u-video/props.js +205 -0
  35. package/components/u-video/u-video.vue +1427 -0
  36. package/components/u-video/video-danmaku.vue +273 -0
  37. package/components/u-video/video-slider.vue +179 -0
  38. package/components/u-video/video-utils.js +98 -0
  39. package/components/u-video/video.js +72 -0
  40. package/libs/config/props.js +2 -1
  41. package/libs/function/test.js +2 -0
  42. package/libs/i18n/locales/de.js +14 -1
  43. package/libs/i18n/locales/en.js +14 -1
  44. package/libs/i18n/locales/es.js +14 -1
  45. package/libs/i18n/locales/fr.js +14 -1
  46. package/libs/i18n/locales/ja.js +14 -1
  47. package/libs/i18n/locales/ko.js +14 -1
  48. package/libs/i18n/locales/ru.js +14 -1
  49. package/libs/i18n/locales/th.js +14 -1
  50. package/libs/i18n/locales/zh-Hans.js +14 -1
  51. package/libs/i18n/locales/zh-Hant.js +14 -1
  52. package/libs/mixin/mixin.js +11 -6
  53. package/package.json +1 -1
  54. package/skills/README.md +66 -0
  55. package/skills/uview-plus/SKILL.md +95 -0
  56. package/skills/uview-plus/references/api.md +105 -0
  57. package/skills/uview-plus/references/components.md +156 -0
  58. package/skills/uview-plus/references/icons.md +223 -0
  59. package/skills/uview-plus/references/pitfalls.md +77 -0
  60. package/skills/uview-plus/references/setup.md +168 -0
  61. package/types/comps/form.d.ts +44 -8
  62. package/types/comps/list.d.ts +1 -1
  63. package/types/comps/listItem.d.ts +1 -1
  64. package/types/comps/navbar.d.ts +6 -0
  65. package/types/comps/numberBox.d.ts +85 -21
  66. package/types/comps/pagination.d.ts +96 -0
  67. package/types/comps/swipeActionItem.d.ts +3 -4
  68. package/types/comps/textarea.d.ts +9 -3
  69. package/types/comps.d.ts +1 -0
  70. package/types/index.d.ts +5 -0
@@ -0,0 +1,1427 @@
1
+ <template>
2
+ <view
3
+ class="up-video"
4
+ :class="[customClass, fullscreen ? 'up-video--fullscreen' : '']"
5
+ :style="[rootStyle, addStyle(customStyle)]"
6
+ >
7
+ <video
8
+ :id="innerVideoId"
9
+ class="up-video__player"
10
+ :src="playingSrc"
11
+ :poster="currentPoster"
12
+ :controls="false"
13
+ :autoplay="nativeAutoplay"
14
+ :loop="loop"
15
+ :muted="innerMuted"
16
+ :initial-time="initialTime"
17
+ :object-fit="objectFit"
18
+ :direction="nativeDirection"
19
+ :show-fullscreen-btn="false"
20
+ :show-play-btn="false"
21
+ :show-center-play-btn="false"
22
+ :show-mute-btn="false"
23
+ :enable-progress-gesture="enableProgressGesture"
24
+ :page-gesture="pageGesture"
25
+ :vslide-gesture="vslideGesture"
26
+ :auto-pause-if-navigate="autoPauseIfNavigate"
27
+ :auto-pause-if-open-native="autoPauseIfOpenNative"
28
+ @play="onPlay"
29
+ @pause="onPause"
30
+ @ended="onEnded"
31
+ @timeupdate="onTimeUpdate"
32
+ @waiting="onWaiting"
33
+ @error="onError"
34
+ @progress="onProgress"
35
+ @fullscreenchange="onFullscreenChange"
36
+ @loadedmetadata="onLoadedMetadata"
37
+ >
38
+ <!-- 弹幕层 -->
39
+ <video-danmaku
40
+ v-if="enableDanmu"
41
+ ref="danmaku"
42
+ class="up-video__layer"
43
+ :list="activeDanmuList"
44
+ :open="innerDanmuOpen"
45
+ :color="danmuColor"
46
+ :font-size="danmuFontSize"
47
+ :duration="danmuDuration"
48
+ :area="danmuArea"
49
+ :opacity="danmuOpacity"
50
+ :max="danmuMax"
51
+ ></video-danmaku>
52
+
53
+ <!-- 整块点击区,用于显示/隐藏控制层 -->
54
+ <view class="up-video__layer" @click.stop="onPlayerTap"></view>
55
+
56
+ <!-- 缓冲提示 -->
57
+ <view v-if="loading" class="up-video__layer up-video__center">
58
+ <up-loading-icon mode="circle" color="#ffffff" size="30"></up-loading-icon>
59
+ </view>
60
+
61
+ <!-- 播放失败 -->
62
+ <view v-if="errored" class="up-video__layer up-video__center up-video__mask">
63
+ <text class="up-video__hint">{{ t('up.video.error') }}</text>
64
+ <view class="up-video__retry" @click.stop="retry">
65
+ <text class="up-video__retry-text">{{ t('up.common.retry') }}</text>
66
+ </view>
67
+ </view>
68
+
69
+ <!-- 封面 -->
70
+ <view v-if="coverVisible" class="up-video__layer up-video__cover" @click.stop="onCoverTap">
71
+ <image v-if="currentPoster" class="up-video__cover-image" :src="currentPoster" mode="aspectFill"></image>
72
+ <view class="up-video__cover-mask"></view>
73
+ <view v-if="showCenterPlayBtn" class="up-video__cover-btn">
74
+ <up-icon name="play-right-fill" color="#ffffff" size="26"></up-icon>
75
+ </view>
76
+ </view>
77
+
78
+ <!-- 顶部信息栏 -->
79
+ <view v-if="topBarVisible" class="up-video__top">
80
+ <view v-if="showBack" class="up-video__icon-btn" @click.stop="onBack">
81
+ <up-icon :name="backIcon" color="#ffffff" size="20"></up-icon>
82
+ </view>
83
+ <text v-if="title" class="up-video__top-title">{{ title }}</text>
84
+ </view>
85
+
86
+ <!-- 锁屏后只留一个解锁按钮 -->
87
+ <view v-if="locked" class="up-video__unlock" @click.stop="toggleLock">
88
+ <up-icon name="lock-fill" color="#ffffff" size="18"></up-icon>
89
+ </view>
90
+
91
+ <!-- 控制条 -->
92
+ <view v-if="barVisible" class="up-video__controls">
93
+ <view class="up-video__row">
94
+ <view class="up-video__icon-btn" @click.stop="toggle">
95
+ <up-icon :name="playing ? 'pause' : 'play-right-fill'" color="#ffffff" size="18"></up-icon>
96
+ </view>
97
+ <text class="up-video__time">{{ timeText }}</text>
98
+ <video-slider
99
+ :percent="progressPercent"
100
+ :buffered="bufferedPercent"
101
+ :active-color="activeColor"
102
+ @start="onProgressStart"
103
+ @changing="onProgressChanging"
104
+ @change="onProgressChange"
105
+ ></video-slider>
106
+ <text class="up-video__time">{{ durationText }}</text>
107
+ <view v-if="showVolume" class="up-video__icon-btn" @click.stop="togglePanel('volume')">
108
+ <up-icon :name="volumeIcon" color="#ffffff" size="18"></up-icon>
109
+ </view>
110
+ <view v-if="showRate" class="up-video__text-btn" @click.stop="togglePanel('rate')">
111
+ <text class="up-video__btn-text">{{ rateText }}</text>
112
+ </view>
113
+ <view v-if="danmuEntryVisible" class="up-video__icon-btn" @click.stop="toggleDanmu">
114
+ <up-icon :name="innerDanmuOpen ? 'chat-fill' : 'chat'" color="#ffffff" size="18"></up-icon>
115
+ </view>
116
+ <view v-if="danmuEntryVisible" class="up-video__text-btn" @click.stop="togglePanel('danmu')">
117
+ <text class="up-video__btn-text">{{ t('up.video.danmu') }}</text>
118
+ </view>
119
+ <view v-if="episodeList.length > 1" class="up-video__text-btn" @click.stop="togglePanel('episode')">
120
+ <text class="up-video__btn-text">{{ t('up.video.episodes') }}</text>
121
+ </view>
122
+ <view v-if="showLock" class="up-video__icon-btn" @click.stop="toggleLock">
123
+ <up-icon name="lock" color="#ffffff" size="18"></up-icon>
124
+ </view>
125
+ <view v-if="showFullscreenBtn" class="up-video__icon-btn" @click.stop="toggleFullscreen">
126
+ <view class="up-video__fullscreen">
127
+ <view :class="[fullscreen ? 'up-video__corner-tl-in' : 'up-video__corner-tl']"></view>
128
+ <view :class="[fullscreen ? 'up-video__corner-tr-in' : 'up-video__corner-tr']"></view>
129
+ <view :class="[fullscreen ? 'up-video__corner-bl-in' : 'up-video__corner-bl']"></view>
130
+ <view :class="[fullscreen ? 'up-video__corner-br-in' : 'up-video__corner-br']"></view>
131
+ </view>
132
+ </view>
133
+ </view>
134
+ </view>
135
+
136
+ <!-- 倍速面板 -->
137
+ <view v-if="panel === 'rate'" class="up-video__layer up-video__panel" @click.stop="closePanel">
138
+ <view class="up-video__panel-body" @click.stop="noop">
139
+ <text class="up-video__panel-title">{{ t('up.video.rate') }}</text>
140
+ <view
141
+ v-for="(item, index) in rates"
142
+ :key="index"
143
+ class="up-video__panel-item"
144
+ @click.stop="setRate(item)"
145
+ >
146
+ <text
147
+ class="up-video__panel-text"
148
+ :style="{ color: item === innerRate ? activeColor : '#ffffff' }"
149
+ >{{ item }}x</text>
150
+ </view>
151
+ </view>
152
+ </view>
153
+
154
+ <!-- 选集面板 -->
155
+ <view v-if="panel === 'episode'" class="up-video__layer up-video__panel" @click.stop="closePanel">
156
+ <view class="up-video__panel-body up-video__panel-body--wide" @click.stop="noop">
157
+ <text class="up-video__panel-title">{{ t('up.video.episodes') }}</text>
158
+ <view class="up-video__episode-grid">
159
+ <view
160
+ v-for="(item, index) in episodeList"
161
+ :key="index"
162
+ class="up-video__episode-cell"
163
+ :style="{ width: episodeWidth }"
164
+ @click.stop="switchEpisode(index)"
165
+ >
166
+ <view class="up-video__episode" :style="episodeStyle(index)">
167
+ <text class="up-video__episode-text">{{ item.title }}</text>
168
+ </view>
169
+ </view>
170
+ </view>
171
+ </view>
172
+ </view>
173
+
174
+ <!-- 音量面板 -->
175
+ <view v-if="panel === 'volume'" class="up-video__volume" @click.stop="noop">
176
+ <view class="up-video__icon-btn" @click.stop="toggleMute">
177
+ <up-icon :name="volumeIcon" color="#ffffff" size="16"></up-icon>
178
+ </view>
179
+ <text class="up-video__volume-label">{{ t('up.video.volume') }}</text>
180
+ <video-slider
181
+ :percent="volumePercent"
182
+ :active-color="activeColor"
183
+ @changing="onVolumeChanging"
184
+ @change="onVolumeChange"
185
+ ></video-slider>
186
+ <text class="up-video__volume-value">{{ volumeText }}</text>
187
+ </view>
188
+ </video>
189
+
190
+ <!-- 广告层:独立 video 节点,不嵌套进主播放器 -->
191
+ <view v-if="currentAd" class="up-video__ad">
192
+ <video
193
+ v-if="currentAd.src"
194
+ :id="adVideoId"
195
+ class="up-video__ad-media"
196
+ :src="currentAd.src"
197
+ :controls="false"
198
+ :muted="innerMuted"
199
+ autoplay
200
+ object-fit="contain"
201
+ @timeupdate="onAdTimeUpdate"
202
+ @ended="finishAd"
203
+ @error="finishAd"
204
+ ></video>
205
+ <image
206
+ v-else-if="currentAd.image"
207
+ class="up-video__ad-media"
208
+ :src="currentAd.image"
209
+ mode="aspectFill"
210
+ @click="onAdClick"
211
+ ></image>
212
+ <view class="up-video__ad-bar">
213
+ <text class="up-video__ad-tag">{{ adTagText }}</text>
214
+ <view v-if="currentAd.link" class="up-video__ad-btn" @click.stop="onAdClick">
215
+ <text class="up-video__ad-btn-text">{{ t('up.video.adDetail') }}</text>
216
+ </view>
217
+ <view v-if="adCanSkip" class="up-video__ad-btn" @click.stop="skipAd">
218
+ <text class="up-video__ad-btn-text">{{ t('up.video.skipAd') }}</text>
219
+ </view>
220
+ </view>
221
+ </view>
222
+
223
+ <!-- 暂停贴片广告 -->
224
+ <view v-if="pauseAdVisible" class="up-video__pause-ad">
225
+ <image
226
+ class="up-video__pause-ad-media"
227
+ :src="pauseAd.image"
228
+ mode="aspectFit"
229
+ @click="onAdClick"
230
+ ></image>
231
+ <view class="up-video__pause-ad-close" @click.stop="closePauseAd">
232
+ <up-icon name="close" color="#ffffff" size="14"></up-icon>
233
+ </view>
234
+ </view>
235
+
236
+ <!-- 弹幕输入:原生 input 放在 video 节点外,规避小程序同层渲染限制 -->
237
+ <view v-if="panel === 'danmu'" class="up-video__danmu-input">
238
+ <input
239
+ v-model="danmuText"
240
+ class="up-video__danmu-field"
241
+ type="text"
242
+ :placeholder="t('up.video.danmuPlaceholder')"
243
+ placeholder-style="color: rgba(255, 255, 255, 0.5)"
244
+ confirm-type="send"
245
+ @confirm="sendDanmu"
246
+ />
247
+ <view class="up-video__danmu-send" :style="{ backgroundColor: activeColor }" @click.stop="sendDanmu">
248
+ <text class="up-video__danmu-send-text">{{ t('up.video.send') }}</text>
249
+ </view>
250
+ </view>
251
+ </view>
252
+ </template>
253
+
254
+ <script>
255
+ import { addStyle, addUnit, guid, toast } from '../../libs/function/index'
256
+ import { mixin } from '../../libs/mixin/mixin'
257
+ import { mpMixin } from '../../libs/mixin/mpMixin'
258
+ import { t } from '../../libs/i18n'
259
+ import props from './props'
260
+ import VideoDanmaku from './video-danmaku.vue'
261
+ import VideoSlider from './video-slider.vue'
262
+ import {
263
+ adSkippable,
264
+ clampNumber,
265
+ formatVideoTime,
266
+ normalizeAds,
267
+ normalizeEpisodes,
268
+ normalizeRateList,
269
+ pickAds,
270
+ progressToTime,
271
+ timeToProgress
272
+ } from './video-utils'
273
+
274
+ /**
275
+ * Video 视频播放器
276
+ * @description 在原生 video 之上自绘控制层的播放器,支持倍速、弹幕、选集、音量、封面与贴片广告。
277
+ * 控制层渲染在 video 节点内部,因此进入全屏后依然可用;广告层是独立的 video 节点,避免嵌套原生组件。
278
+ * @tutorial https://uview-plus.jiangruyi.com/components/video.html
279
+ * @property {String} src 视频地址,传了 episodes 时作为兜底地址
280
+ * @property {String} poster 封面图,首次播放前展示
281
+ * @property {String} title 标题,显示在顶部信息栏
282
+ * @property {String | Number} width 宽度,默认 100%
283
+ * @property {String | Number} height 高度,默认 211px
284
+ * @property {String | Number} radius 圆角
285
+ * @property {String} objectFit 视频填充模式 contain | fill | cover
286
+ * @property {Boolean} autoplay 是否自动播放,存在前置广告时先播广告
287
+ * @property {Boolean} loop 是否循环播放
288
+ * @property {Boolean} muted 是否静音
289
+ * @property {String | Number} initialTime 指定开始播放的位置,单位秒
290
+ * @property {Boolean} controls 是否显示自绘控制层,默认 true
291
+ * @property {Boolean} showCenterPlayBtn 封面上是否显示大播放按钮,默认 true
292
+ * @property {Boolean} showFullscreenBtn 是否显示全屏按钮,默认 true
293
+ * @property {Boolean} showBack 是否显示顶部返回按钮,默认 false
294
+ * @property {String} backIcon 返回按钮图标,默认 arrow-left
295
+ * @property {String | Number} autoHide 控制层自动隐藏毫秒数,0 表示不隐藏,默认 4000
296
+ * @property {Boolean} showLock 是否显示锁屏按钮,默认 true
297
+ * @property {Boolean} showRate 是否显示倍速入口,默认 true
298
+ * @property {String | Number} rate 当前倍速,默认 1
299
+ * @property {Array} rateList 可选倍速,默认 [0.5, 0.75, 1, 1.25, 1.5, 2]
300
+ * @property {Boolean} showVolume 是否显示音量入口,默认 true
301
+ * @property {String | Number} volume 音量 0-1,App 端调节的是系统音量,小程序端仅支持静音切换
302
+ * @property {Boolean} enableDanmu 是否开启弹幕,默认 false
303
+ * @property {Array} danmuList 弹幕列表,元素为 { text, color, time, type },type 支持 scroll | top | bottom
304
+ * @property {Boolean} danmuBtn 是否显示弹幕开关与输入入口,默认 true
305
+ * @property {Boolean} danmuOpen 弹幕是否可见,默认 true
306
+ * @property {String} danmuColor 默认弹幕颜色
307
+ * @property {String | Number} danmuFontSize 弹幕字号,默认 14
308
+ * @property {String | Number} danmuDuration 一条滚动弹幕走完全屏的秒数,默认 8
309
+ * @property {String} danmuArea 弹幕区域 full | half | top,默认 full
310
+ * @property {String | Number} danmuOpacity 弹幕透明度,默认 1
311
+ * @property {String | Number} danmuMax 同屏最大弹幕数,默认 30
312
+ * @property {Array} episodes 选集列表,元素为地址字符串或 { title, src, poster, danmuList }
313
+ * @property {String | Number} episodeIndex 当前集索引,支持 v-model:episodeIndex
314
+ * @property {String | Number} episodeColumns 选集面板列数,默认 5
315
+ * @property {Boolean} autoNext 播完是否自动下一集,默认 true
316
+ * @property {Array} ads 广告列表,元素为 { type, src, image, duration, skipAfter, link, text }
317
+ * @property {Boolean} enableProgressGesture 是否启用原生进度手势,默认 false
318
+ * @property {Boolean} pageGesture 是否开启亮度与音量手势,默认 false
319
+ * @property {Boolean} vslideGesture 是否开启竖向手势,默认 false
320
+ * @property {String | Number} direction 全屏方向,-1 表示由系统判断
321
+ * @property {String} videoId 自定义 video 节点 id,留空自动生成
322
+ * @property {String} activeColor 进度条与选中态颜色,默认 #2979ff
323
+ * @event {Function} play 开始或继续播放
324
+ * @event {Function} pause 暂停播放
325
+ * @event {Function} ended 播放结束
326
+ * @event {Function} timeupdate 播放进度变化
327
+ * @event {Function} waiting 缓冲中
328
+ * @event {Function} error 播放错误
329
+ * @event {Function} progress 缓冲进度变化
330
+ * @event {Function} fullscreenchange 全屏状态变化
331
+ * @event {Function} loadedmetadata 视频元数据加载完成
332
+ * @event {Function} ratechange 倍速变化
333
+ * @event {Function} volumechange 音量变化
334
+ * @event {Function} danmu 发送了一条弹幕
335
+ * @event {Function} danmu-toggle 弹幕显示开关变化
336
+ * @event {Function} episode-change 切换选集
337
+ * @event {Function} ad-start 广告开始
338
+ * @event {Function} ad-end 广告结束
339
+ * @event {Function} ad-skip 跳过广告
340
+ * @event {Function} ad-click 点击广告,由业务侧决定跳转
341
+ * @event {Function} lock 锁屏状态变化
342
+ * @event {Function} back 点击返回按钮
343
+ * @event {Function} controlstoggle 控制层显示状态变化
344
+ * @example <up-video src="https://xxx.mp4" poster="https://xxx.png" enable-danmu></up-video>
345
+ */
346
+ export default {
347
+ name: 'up-video',
348
+ components: {
349
+ VideoDanmaku,
350
+ VideoSlider
351
+ },
352
+ mixins: [mpMixin, mixin, props],
353
+ // #ifdef VUE3
354
+ emits: [
355
+ 'play', 'pause', 'ended', 'timeupdate', 'waiting', 'error', 'progress',
356
+ 'fullscreenchange', 'loadedmetadata', 'ratechange', 'volumechange',
357
+ 'danmu', 'danmu-toggle', 'episode-change', 'ad-start', 'ad-end',
358
+ 'ad-skip', 'ad-click', 'lock', 'back', 'controlstoggle',
359
+ 'update:rate', 'update:volume', 'update:danmuOpen', 'update:episodeIndex'
360
+ ],
361
+ // #endif
362
+ data() {
363
+ return {
364
+ innerVideoId: '',
365
+ adVideoId: '',
366
+ playing: false,
367
+ // 是否已经开始播放过,用于决定封面是否显示
368
+ started: false,
369
+ loading: false,
370
+ errored: false,
371
+ currentTime: 0,
372
+ videoDuration: 0,
373
+ buffered: 0,
374
+ dragging: false,
375
+ dragPercent: 0,
376
+ controlsVisible: true,
377
+ locked: false,
378
+ fullscreen: false,
379
+ // '' | rate | episode | volume | danmu
380
+ panel: '',
381
+ innerRate: 1,
382
+ innerVolume: 1,
383
+ innerMuted: false,
384
+ innerDanmuOpen: true,
385
+ innerEpisodeIndex: 0,
386
+ danmuText: '',
387
+ currentAd: null,
388
+ adRole: '',
389
+ adElapsed: 0,
390
+ adDuration: 0,
391
+ prerollCursor: 0,
392
+ postrollCursor: 0,
393
+ pauseAdVisible: false
394
+ }
395
+ },
396
+ computed: {
397
+ rootStyle() {
398
+ const style = {
399
+ width: addUnit(this.width),
400
+ height: addUnit(this.height)
401
+ }
402
+ if (this.radius) style.borderRadius = addUnit(this.radius)
403
+ return style
404
+ },
405
+ // uni 的 direction 只接受 0 / 90 / -90,-1 表示交给系统判断
406
+ nativeDirection() {
407
+ const direction = Number(this.direction)
408
+ return [0, 90, -90].includes(direction) ? direction : undefined
409
+ },
410
+ // 有前置广告时交给组件自己调度,避免原生自动播放抢在广告之前
411
+ nativeAutoplay() {
412
+ return this.autoplay && !this.prerollAds.length
413
+ },
414
+ episodeList() {
415
+ return normalizeEpisodes(this.episodes)
416
+ },
417
+ currentEpisode() {
418
+ return this.episodeList[this.innerEpisodeIndex] || null
419
+ },
420
+ playingSrc() {
421
+ return (this.currentEpisode && this.currentEpisode.src) || this.src
422
+ },
423
+ currentPoster() {
424
+ return (this.currentEpisode && this.currentEpisode.poster) || this.poster
425
+ },
426
+ activeDanmuList() {
427
+ return (this.currentEpisode && this.currentEpisode.danmuList) || this.danmuList
428
+ },
429
+ rates() {
430
+ return normalizeRateList(this.rateList)
431
+ },
432
+ normalizedAds() {
433
+ return normalizeAds(this.ads)
434
+ },
435
+ prerollAds() {
436
+ return pickAds(this.normalizedAds, 'preroll')
437
+ },
438
+ postrollAds() {
439
+ return pickAds(this.normalizedAds, 'postroll')
440
+ },
441
+ pauseAd() {
442
+ return pickAds(this.normalizedAds, 'pause')[0] || null
443
+ },
444
+ coverVisible() {
445
+ return !this.started && !this.currentAd
446
+ },
447
+ topBarVisible() {
448
+ return this.controlsVisible && !this.locked && !this.currentAd && (this.showBack || !!this.title)
449
+ },
450
+ barVisible() {
451
+ return this.controls && this.controlsVisible && !this.locked && !this.currentAd
452
+ },
453
+ danmuEntryVisible() {
454
+ return this.enableDanmu && this.danmuBtn
455
+ },
456
+ progressPercent() {
457
+ return this.dragging ? this.dragPercent : timeToProgress(this.currentTime, this.videoDuration)
458
+ },
459
+ bufferedPercent() {
460
+ return clampNumber(this.buffered, 0, 100)
461
+ },
462
+ timeText() {
463
+ const time = this.dragging
464
+ ? progressToTime(this.dragPercent, this.videoDuration)
465
+ : this.currentTime
466
+ return formatVideoTime(time)
467
+ },
468
+ durationText() {
469
+ return formatVideoTime(this.videoDuration)
470
+ },
471
+ rateText() {
472
+ return `${this.innerRate}x`
473
+ },
474
+ volumePercent() {
475
+ return this.innerMuted ? 0 : clampNumber(this.innerVolume * 100, 0, 100)
476
+ },
477
+ volumeText() {
478
+ return `${Math.round(this.volumePercent)}`
479
+ },
480
+ volumeIcon() {
481
+ return this.innerMuted || this.innerVolume <= 0 ? 'volume-off' : 'volume'
482
+ },
483
+ episodeWidth() {
484
+ const columns = Math.max(1, parseInt(this.episodeColumns, 10) || 1)
485
+ return `${(100 / columns).toFixed(4)}%`
486
+ },
487
+ adCanSkip() {
488
+ return !!this.currentAd && adSkippable(this.currentAd, this.adElapsed)
489
+ },
490
+ adTagText() {
491
+ if (!this.currentAd) return ''
492
+ const total = Number(this.adDuration) || 0
493
+ if (total > 0) {
494
+ const left = Math.max(0, Math.ceil(total - this.adElapsed))
495
+ return t('up.video.adCountdown', { seconds: left })
496
+ }
497
+ return t('up.video.ad')
498
+ }
499
+ },
500
+ watch: {
501
+ src() {
502
+ this.resetPlayback()
503
+ },
504
+ episodeIndex(value) {
505
+ const index = parseInt(value, 10) || 0
506
+ if (index !== this.innerEpisodeIndex) this.switchEpisode(index, true)
507
+ },
508
+ rate(value) {
509
+ this.setRate(value, true)
510
+ },
511
+ volume(value) {
512
+ this.innerVolume = clampNumber(value, 0, 1)
513
+ this.applyVolume()
514
+ },
515
+ muted(value) {
516
+ this.innerMuted = value
517
+ this.applyVolume()
518
+ },
519
+ danmuOpen(value) {
520
+ this.innerDanmuOpen = value
521
+ }
522
+ },
523
+ created() {
524
+ this.ctx = null
525
+ this.hideTimer = null
526
+ this.adTimer = null
527
+ this.innerVideoId = this.videoId || `up-video-${guid(10, false)}`
528
+ this.adVideoId = `${this.innerVideoId}-ad`
529
+ this.innerRate = Number(this.rate) || 1
530
+ this.innerVolume = clampNumber(this.volume, 0, 1)
531
+ this.innerMuted = this.muted
532
+ this.innerDanmuOpen = this.danmuOpen
533
+ this.innerEpisodeIndex = parseInt(this.episodeIndex, 10) || 0
534
+ },
535
+ mounted() {
536
+ this.applyVolume()
537
+ if (this.autoplay) {
538
+ // 原生 autoplay 已经被前置广告接管时,这里补一次调度
539
+ if (this.prerollAds.length) {
540
+ this.play()
541
+ } else {
542
+ this.started = true
543
+ }
544
+ }
545
+ this.scheduleHide()
546
+ },
547
+ beforeUnmount() {
548
+ this.clearHideTimer()
549
+ this.clearAdTimer()
550
+ },
551
+ // #ifdef VUE2
552
+ beforeDestroy() {
553
+ this.clearHideTimer()
554
+ this.clearAdTimer()
555
+ },
556
+ // #endif
557
+ methods: {
558
+ t,
559
+ addStyle,
560
+ noop() {},
561
+ getContext() {
562
+ if (!this.ctx) {
563
+ this.ctx = uni.createVideoContext(this.innerVideoId, this)
564
+ }
565
+ return this.ctx
566
+ },
567
+ getAdContext() {
568
+ return uni.createVideoContext(this.adVideoId, this)
569
+ },
570
+ // ---------- 播放控制 ----------
571
+ play() {
572
+ if (this.currentAd) return
573
+ // 前置贴片广告优先,播完后由 finishAd 接着播正片
574
+ if (this.playAdQueue('preroll')) return
575
+ this.playMain()
576
+ },
577
+ playMain() {
578
+ this.started = true
579
+ this.errored = false
580
+ this.closePauseAd()
581
+ const ctx = this.getContext()
582
+ if (ctx) ctx.play()
583
+ },
584
+ pause() {
585
+ const ctx = this.getContext()
586
+ if (ctx) ctx.pause()
587
+ },
588
+ toggle() {
589
+ if (this.playing) {
590
+ this.pause()
591
+ } else {
592
+ this.play()
593
+ }
594
+ },
595
+ stop() {
596
+ const ctx = this.getContext()
597
+ if (ctx) ctx.stop()
598
+ this.playing = false
599
+ this.started = false
600
+ this.currentTime = 0
601
+ },
602
+ seek(time) {
603
+ const target = clampNumber(time, 0, this.videoDuration || Number(time) || 0)
604
+ const ctx = this.getContext()
605
+ if (ctx) ctx.seek(target)
606
+ this.currentTime = target
607
+ if (this.$refs.danmaku) this.$refs.danmaku.reset(target)
608
+ return target
609
+ },
610
+ retry() {
611
+ this.errored = false
612
+ this.loading = true
613
+ this.$nextTick(() => this.playMain())
614
+ },
615
+ // 换源、换集后把播放态清干净,广告游标一起重置
616
+ resetPlayback() {
617
+ this.playing = false
618
+ this.started = false
619
+ this.loading = false
620
+ this.errored = false
621
+ this.currentTime = 0
622
+ this.videoDuration = 0
623
+ this.buffered = 0
624
+ this.dragging = false
625
+ this.prerollCursor = 0
626
+ this.postrollCursor = 0
627
+ this.closePauseAd()
628
+ if (this.$refs.danmaku) this.$refs.danmaku.reset(0)
629
+ },
630
+ // ---------- 原生事件 ----------
631
+ onPlay(event) {
632
+ this.playing = true
633
+ this.started = true
634
+ this.loading = false
635
+ this.errored = false
636
+ this.closePauseAd()
637
+ // 换源后倍速会被重置,这里补一次
638
+ this.applyRate()
639
+ this.applyVolume()
640
+ this.scheduleHide()
641
+ this.$emit('play', event)
642
+ },
643
+ onPause(event) {
644
+ this.playing = false
645
+ this.showControls()
646
+ if (this.pauseAd && this.started && !this.currentAd) {
647
+ this.pauseAdVisible = true
648
+ }
649
+ this.$emit('pause', event)
650
+ },
651
+ onEnded(event) {
652
+ this.playing = false
653
+ this.$emit('ended', event)
654
+ if (this.loop) return
655
+ if (this.autoNext && this.innerEpisodeIndex < this.episodeList.length - 1) {
656
+ this.switchEpisode(this.innerEpisodeIndex + 1)
657
+ return
658
+ }
659
+ if (this.playAdQueue('postroll')) return
660
+ this.started = false
661
+ this.currentTime = 0
662
+ this.showControls()
663
+ },
664
+ onTimeUpdate(event) {
665
+ const detail = event && event.detail ? event.detail : {}
666
+ if (!this.dragging && detail.currentTime !== undefined) {
667
+ this.currentTime = detail.currentTime
668
+ }
669
+ if (detail.duration) this.videoDuration = detail.duration
670
+ this.loading = false
671
+ if (this.$refs.danmaku) this.$refs.danmaku.sync(this.currentTime)
672
+ this.$emit('timeupdate', event)
673
+ },
674
+ onWaiting(event) {
675
+ this.loading = true
676
+ this.$emit('waiting', event)
677
+ },
678
+ onError(event) {
679
+ this.loading = false
680
+ this.playing = false
681
+ this.errored = true
682
+ this.$emit('error', event)
683
+ },
684
+ onProgress(event) {
685
+ const detail = event && event.detail ? event.detail : {}
686
+ if (detail.buffered !== undefined) this.buffered = detail.buffered
687
+ this.$emit('progress', event)
688
+ },
689
+ onFullscreenChange(event) {
690
+ const detail = event && event.detail ? event.detail : {}
691
+ this.fullscreen = !!detail.fullScreen
692
+ this.showControls()
693
+ this.$emit('fullscreenchange', event)
694
+ },
695
+ onLoadedMetadata(event) {
696
+ const detail = event && event.detail ? event.detail : {}
697
+ if (detail.duration) this.videoDuration = detail.duration
698
+ this.loading = false
699
+ this.$emit('loadedmetadata', event)
700
+ },
701
+ // ---------- 控制层显示 ----------
702
+ onPlayerTap() {
703
+ if (this.locked) return
704
+ if (this.panel) {
705
+ this.closePanel()
706
+ return
707
+ }
708
+ this.toggleControls()
709
+ },
710
+ onCoverTap() {
711
+ this.play()
712
+ },
713
+ toggleControls() {
714
+ this.controlsVisible = !this.controlsVisible
715
+ this.$emit('controlstoggle', this.controlsVisible)
716
+ if (this.controlsVisible) this.scheduleHide()
717
+ },
718
+ showControls() {
719
+ this.controlsVisible = true
720
+ this.scheduleHide()
721
+ },
722
+ scheduleHide() {
723
+ this.clearHideTimer()
724
+ const delay = Number(this.autoHide)
725
+ if (!delay || delay <= 0) return
726
+ this.hideTimer = setTimeout(() => {
727
+ // 面板打开或暂停时不收起,避免操作被打断
728
+ if (this.panel || !this.playing) return
729
+ this.controlsVisible = false
730
+ this.$emit('controlstoggle', false)
731
+ }, delay)
732
+ },
733
+ clearHideTimer() {
734
+ if (this.hideTimer) {
735
+ clearTimeout(this.hideTimer)
736
+ this.hideTimer = null
737
+ }
738
+ },
739
+ togglePanel(name) {
740
+ this.panel = this.panel === name ? '' : name
741
+ if (this.panel) this.clearHideTimer()
742
+ else this.scheduleHide()
743
+ },
744
+ closePanel() {
745
+ this.panel = ''
746
+ this.scheduleHide()
747
+ },
748
+ toggleLock() {
749
+ this.locked = !this.locked
750
+ if (this.locked) {
751
+ this.panel = ''
752
+ this.controlsVisible = false
753
+ toast(t('up.video.lock'))
754
+ } else {
755
+ this.showControls()
756
+ toast(t('up.video.unlock'))
757
+ }
758
+ this.$emit('lock', this.locked)
759
+ },
760
+ onBack() {
761
+ if (this.fullscreen) {
762
+ this.exitFullScreen()
763
+ return
764
+ }
765
+ this.$emit('back')
766
+ },
767
+ // ---------- 进度拖动 ----------
768
+ onProgressStart() {
769
+ this.dragging = true
770
+ this.dragPercent = timeToProgress(this.currentTime, this.videoDuration)
771
+ this.clearHideTimer()
772
+ },
773
+ onProgressChanging(percent) {
774
+ this.dragPercent = percent
775
+ },
776
+ onProgressChange(percent) {
777
+ this.dragging = false
778
+ this.seek(progressToTime(percent, this.videoDuration))
779
+ this.scheduleHide()
780
+ },
781
+ // ---------- 倍速 ----------
782
+ applyRate() {
783
+ const ctx = this.getContext()
784
+ if (ctx && ctx.playbackRate) ctx.playbackRate(this.innerRate)
785
+ },
786
+ setRate(rate, silent = false) {
787
+ const value = Number(rate)
788
+ if (!Number.isFinite(value) || value <= 0) return
789
+ this.innerRate = value
790
+ this.applyRate()
791
+ this.closePanel()
792
+ if (!silent) {
793
+ this.$emit('update:rate', value)
794
+ this.$emit('ratechange', value)
795
+ }
796
+ },
797
+ // ---------- 音量 ----------
798
+ applyVolume() {
799
+ const value = this.innerMuted ? 0 : clampNumber(this.innerVolume, 0, 1)
800
+ // #ifdef H5
801
+ if (typeof document !== 'undefined') {
802
+ const wrapper = document.getElementById(this.innerVideoId)
803
+ const media = wrapper ? wrapper.querySelector('video') : null
804
+ if (media) media.volume = value
805
+ }
806
+ // #endif
807
+ // #ifdef APP-PLUS
808
+ // App 端没有播放器级音量接口,只能调节系统音量
809
+ if (typeof plus !== 'undefined' && plus.device && plus.device.setVolume) {
810
+ plus.device.setVolume(value)
811
+ }
812
+ // #endif
813
+ return value
814
+ },
815
+ setVolume(value, silent = false) {
816
+ this.innerVolume = clampNumber(value, 0, 1)
817
+ if (this.innerVolume > 0) this.innerMuted = false
818
+ this.applyVolume()
819
+ if (!silent) {
820
+ this.$emit('update:volume', this.innerVolume)
821
+ this.$emit('volumechange', { volume: this.innerVolume, muted: this.innerMuted })
822
+ }
823
+ },
824
+ onVolumeChanging(percent) {
825
+ this.setVolume(percent / 100, true)
826
+ },
827
+ onVolumeChange(percent) {
828
+ this.setVolume(percent / 100)
829
+ },
830
+ toggleMute() {
831
+ this.innerMuted = !this.innerMuted
832
+ this.applyVolume()
833
+ this.$emit('volumechange', { volume: this.innerVolume, muted: this.innerMuted })
834
+ },
835
+ // ---------- 弹幕 ----------
836
+ toggleDanmu() {
837
+ this.innerDanmuOpen = !this.innerDanmuOpen
838
+ this.$emit('update:danmuOpen', this.innerDanmuOpen)
839
+ this.$emit('danmu-toggle', this.innerDanmuOpen)
840
+ },
841
+ sendDanmu(payload) {
842
+ let danmu = null
843
+ if (typeof payload === 'string') {
844
+ danmu = { text: payload }
845
+ } else if (payload && payload.text) {
846
+ danmu = payload
847
+ } else {
848
+ danmu = { text: this.danmuText }
849
+ }
850
+ if (!danmu.text) return null
851
+ const item = {
852
+ text: danmu.text,
853
+ color: danmu.color || this.danmuColor,
854
+ time: this.currentTime,
855
+ type: danmu.type || 'scroll'
856
+ }
857
+ if (this.$refs.danmaku) {
858
+ if (!this.innerDanmuOpen) {
859
+ this.innerDanmuOpen = true
860
+ this.$emit('update:danmuOpen', true)
861
+ }
862
+ this.$refs.danmaku.push(item)
863
+ }
864
+ this.danmuText = ''
865
+ this.closePanel()
866
+ this.$emit('danmu', item)
867
+ return item
868
+ },
869
+ // ---------- 选集 ----------
870
+ switchEpisode(index, silent = false) {
871
+ this.closePanel()
872
+ const target = parseInt(index, 10)
873
+ if (!Number.isFinite(target) || target < 0 || target >= this.episodeList.length) return
874
+ if (target === this.innerEpisodeIndex && this.started) return
875
+ this.innerEpisodeIndex = target
876
+ this.resetPlayback()
877
+ if (!silent) {
878
+ this.$emit('update:episodeIndex', target)
879
+ this.$emit('episode-change', { index: target, episode: this.episodeList[target] })
880
+ }
881
+ // 等 src 生效后再播,否则部分平台会播上一集
882
+ this.$nextTick(() => this.play())
883
+ },
884
+ playNext() {
885
+ this.switchEpisode(this.innerEpisodeIndex + 1)
886
+ },
887
+ episodeStyle(index) {
888
+ const active = index === this.innerEpisodeIndex
889
+ return {
890
+ backgroundColor: active ? this.activeColor : 'rgba(255, 255, 255, 0.16)'
891
+ }
892
+ },
893
+ // ---------- 全屏 ----------
894
+ requestFullScreen() {
895
+ const ctx = this.getContext()
896
+ if (!ctx || !ctx.requestFullScreen) return
897
+ const direction = this.nativeDirection
898
+ ctx.requestFullScreen(direction === undefined ? {} : { direction })
899
+ },
900
+ exitFullScreen() {
901
+ const ctx = this.getContext()
902
+ if (ctx && ctx.exitFullScreen) ctx.exitFullScreen()
903
+ },
904
+ toggleFullscreen() {
905
+ if (this.fullscreen) {
906
+ this.exitFullScreen()
907
+ } else {
908
+ this.requestFullScreen()
909
+ }
910
+ },
911
+ // ---------- 广告 ----------
912
+ playAdQueue(role) {
913
+ const list = role === 'preroll' ? this.prerollAds : this.postrollAds
914
+ const cursorKey = role === 'preroll' ? 'prerollCursor' : 'postrollCursor'
915
+ if (this[cursorKey] >= list.length) return false
916
+ const ad = list[this[cursorKey]]
917
+ this[cursorKey] += 1
918
+ this.adRole = role
919
+ this.startAd(ad)
920
+ return true
921
+ },
922
+ startAd(ad) {
923
+ // 先落 currentAd,再暂停正片,避免异步的 pause 事件把暂停贴片顶出来
924
+ this.currentAd = ad
925
+ this.adElapsed = 0
926
+ this.adDuration = Number(ad.duration) || 0
927
+ this.panel = ''
928
+ this.pause()
929
+ this.closePauseAd()
930
+ this.$emit('ad-start', ad)
931
+ // 图片广告没有 timeupdate,用定时器走倒计时
932
+ if (!ad.src) {
933
+ this.clearAdTimer()
934
+ this.adTimer = setInterval(() => {
935
+ this.adElapsed += 0.5
936
+ if (this.adDuration > 0 && this.adElapsed >= this.adDuration) this.finishAd()
937
+ }, 500)
938
+ }
939
+ },
940
+ onAdTimeUpdate(event) {
941
+ const detail = event && event.detail ? event.detail : {}
942
+ if (detail.currentTime !== undefined) this.adElapsed = detail.currentTime
943
+ if (!this.adDuration && detail.duration) this.adDuration = detail.duration
944
+ },
945
+ onAdEnded() {
946
+ this.finishAd(false)
947
+ },
948
+ skipAd() {
949
+ if (!this.adCanSkip) return
950
+ this.$emit('ad-skip', this.currentAd)
951
+ this.finishAd(true)
952
+ },
953
+ onAdClick() {
954
+ // 只抛事件,跳转交给业务侧决定
955
+ this.$emit('ad-click', this.currentAd || this.pauseAd)
956
+ },
957
+ finishAd(skipped = false) {
958
+ const ad = this.currentAd
959
+ const role = this.adRole
960
+ this.clearAdTimer()
961
+ this.currentAd = null
962
+ this.adRole = ''
963
+ this.adElapsed = 0
964
+ this.adDuration = 0
965
+ if (ad) this.$emit('ad-end', { ad, skipped })
966
+ this.$nextTick(() => {
967
+ if (role === 'preroll') {
968
+ if (!this.playAdQueue('preroll')) this.playMain()
969
+ return
970
+ }
971
+ if (role === 'postroll' && !this.playAdQueue('postroll')) {
972
+ // 后置广告播完回到封面
973
+ this.started = false
974
+ this.currentTime = 0
975
+ }
976
+ })
977
+ },
978
+ clearAdTimer() {
979
+ if (this.adTimer) {
980
+ clearInterval(this.adTimer)
981
+ this.adTimer = null
982
+ }
983
+ },
984
+ closePauseAd() {
985
+ this.pauseAdVisible = false
986
+ }
987
+ }
988
+ }
989
+ </script>
990
+
991
+ <style lang="scss" scoped>
992
+ .up-video {
993
+ position: relative;
994
+ overflow: hidden;
995
+ background-color: #000000;
996
+ }
997
+
998
+ .up-video__player {
999
+ width: 100%;
1000
+ height: 100%;
1001
+ }
1002
+
1003
+ /* 控制层统一铺满 video 节点 */
1004
+ .up-video__layer {
1005
+ position: absolute;
1006
+ top: 0;
1007
+ left: 0;
1008
+ right: 0;
1009
+ bottom: 0;
1010
+ }
1011
+
1012
+ .up-video__center {
1013
+ align-items: center;
1014
+ justify-content: center;
1015
+ }
1016
+
1017
+ .up-video__mask {
1018
+ background-color: rgba(0, 0, 0, 0.5);
1019
+ }
1020
+
1021
+ .up-video__hint {
1022
+ color: #ffffff;
1023
+ font-size: 14px;
1024
+ }
1025
+
1026
+ .up-video__retry {
1027
+ flex-direction: row;
1028
+ align-items: center;
1029
+ justify-content: center;
1030
+ margin-top: 10px;
1031
+ padding: 4px 14px;
1032
+ border-radius: 30px;
1033
+ border-width: 1px;
1034
+ border-style: solid;
1035
+ border-color: rgba(255, 255, 255, 0.7);
1036
+ }
1037
+
1038
+ .up-video__retry-text {
1039
+ color: #ffffff;
1040
+ font-size: 12px;
1041
+ }
1042
+
1043
+ .up-video__cover {
1044
+ align-items: center;
1045
+ justify-content: center;
1046
+ background-color: #000000;
1047
+ }
1048
+
1049
+ .up-video__cover-image {
1050
+ position: absolute;
1051
+ top: 0;
1052
+ left: 0;
1053
+ width: 100%;
1054
+ height: 100%;
1055
+ }
1056
+
1057
+ .up-video__cover-mask {
1058
+ position: absolute;
1059
+ top: 0;
1060
+ left: 0;
1061
+ right: 0;
1062
+ bottom: 0;
1063
+ background-color: rgba(0, 0, 0, 0.25);
1064
+ }
1065
+
1066
+ .up-video__cover-btn {
1067
+ width: 52px;
1068
+ height: 52px;
1069
+ border-radius: 26px;
1070
+ flex-direction: row;
1071
+ align-items: center;
1072
+ justify-content: center;
1073
+ background-color: rgba(0, 0, 0, 0.45);
1074
+ }
1075
+
1076
+ .up-video__top {
1077
+ position: absolute;
1078
+ top: 0;
1079
+ left: 0;
1080
+ right: 0;
1081
+ height: 40px;
1082
+ flex-direction: row;
1083
+ align-items: center;
1084
+ padding-left: 6px;
1085
+ padding-right: 6px;
1086
+ background-color: rgba(0, 0, 0, 0.3);
1087
+ }
1088
+
1089
+ .up-video__top-title {
1090
+ flex: 1;
1091
+ margin-left: 6px;
1092
+ color: #ffffff;
1093
+ font-size: 14px;
1094
+ lines: 1;
1095
+ text-overflow: ellipsis;
1096
+ overflow: hidden;
1097
+ }
1098
+
1099
+ .up-video__icon-btn {
1100
+ width: 30px;
1101
+ height: 30px;
1102
+ flex-direction: row;
1103
+ align-items: center;
1104
+ justify-content: center;
1105
+ }
1106
+
1107
+ .up-video__text-btn {
1108
+ height: 30px;
1109
+ padding-left: 5px;
1110
+ padding-right: 5px;
1111
+ flex-direction: row;
1112
+ align-items: center;
1113
+ justify-content: center;
1114
+ }
1115
+
1116
+ .up-video__btn-text {
1117
+ color: #ffffff;
1118
+ font-size: 12px;
1119
+ }
1120
+
1121
+ .up-video__controls {
1122
+ position: absolute;
1123
+ left: 0;
1124
+ right: 0;
1125
+ bottom: 0;
1126
+ padding-left: 4px;
1127
+ padding-right: 4px;
1128
+ background-color: rgba(0, 0, 0, 0.35);
1129
+ }
1130
+
1131
+ .up-video__row {
1132
+ flex-direction: row;
1133
+ align-items: center;
1134
+ }
1135
+
1136
+ .up-video__time {
1137
+ width: 44px;
1138
+ color: #ffffff;
1139
+ font-size: 11px;
1140
+ text-align: center;
1141
+ }
1142
+
1143
+ .up-video__unlock {
1144
+ position: absolute;
1145
+ left: 12px;
1146
+ top: 50%;
1147
+ margin-top: -16px;
1148
+ width: 32px;
1149
+ height: 32px;
1150
+ border-radius: 16px;
1151
+ flex-direction: row;
1152
+ align-items: center;
1153
+ justify-content: center;
1154
+ background-color: rgba(0, 0, 0, 0.5);
1155
+ }
1156
+
1157
+ /* 全屏图标用 4 个角括号拼出来,图标字体里没有对应字形 */
1158
+ .up-video__fullscreen {
1159
+ position: relative;
1160
+ width: 15px;
1161
+ height: 15px;
1162
+ }
1163
+
1164
+ .up-video__corner-tl,
1165
+ .up-video__corner-tr,
1166
+ .up-video__corner-bl,
1167
+ .up-video__corner-br,
1168
+ .up-video__corner-tl-in,
1169
+ .up-video__corner-tr-in,
1170
+ .up-video__corner-bl-in,
1171
+ .up-video__corner-br-in {
1172
+ position: absolute;
1173
+ width: 6px;
1174
+ height: 6px;
1175
+ border-style: solid;
1176
+ border-color: #ffffff;
1177
+ border-top-width: 0;
1178
+ border-right-width: 0;
1179
+ border-bottom-width: 0;
1180
+ border-left-width: 0;
1181
+ }
1182
+
1183
+ .up-video__corner-tl {
1184
+ top: 0;
1185
+ left: 0;
1186
+ border-top-width: 2px;
1187
+ border-left-width: 2px;
1188
+ }
1189
+
1190
+ .up-video__corner-tr {
1191
+ top: 0;
1192
+ right: 0;
1193
+ border-top-width: 2px;
1194
+ border-right-width: 2px;
1195
+ }
1196
+
1197
+ .up-video__corner-bl {
1198
+ bottom: 0;
1199
+ left: 0;
1200
+ border-bottom-width: 2px;
1201
+ border-left-width: 2px;
1202
+ }
1203
+
1204
+ .up-video__corner-br {
1205
+ bottom: 0;
1206
+ right: 0;
1207
+ border-bottom-width: 2px;
1208
+ border-right-width: 2px;
1209
+ }
1210
+
1211
+ /* 退出全屏时角括号朝内 */
1212
+ .up-video__corner-tl-in {
1213
+ top: 0;
1214
+ left: 0;
1215
+ border-bottom-width: 2px;
1216
+ border-right-width: 2px;
1217
+ }
1218
+
1219
+ .up-video__corner-tr-in {
1220
+ top: 0;
1221
+ right: 0;
1222
+ border-bottom-width: 2px;
1223
+ border-left-width: 2px;
1224
+ }
1225
+
1226
+ .up-video__corner-bl-in {
1227
+ bottom: 0;
1228
+ left: 0;
1229
+ border-top-width: 2px;
1230
+ border-right-width: 2px;
1231
+ }
1232
+
1233
+ .up-video__corner-br-in {
1234
+ bottom: 0;
1235
+ right: 0;
1236
+ border-top-width: 2px;
1237
+ border-left-width: 2px;
1238
+ }
1239
+
1240
+ .up-video__panel {
1241
+ flex-direction: row;
1242
+ justify-content: flex-end;
1243
+ background-color: rgba(0, 0, 0, 0.4);
1244
+ }
1245
+
1246
+ .up-video__panel-body {
1247
+ width: 104px;
1248
+ height: 100%;
1249
+ padding: 10px;
1250
+ background-color: rgba(0, 0, 0, 0.85);
1251
+ }
1252
+
1253
+ .up-video__panel-body--wide {
1254
+ width: 240px;
1255
+ }
1256
+
1257
+ .up-video__panel-title {
1258
+ color: rgba(255, 255, 255, 0.6);
1259
+ font-size: 12px;
1260
+ margin-bottom: 6px;
1261
+ }
1262
+
1263
+ .up-video__panel-item {
1264
+ height: 32px;
1265
+ justify-content: center;
1266
+ }
1267
+
1268
+ .up-video__panel-text {
1269
+ font-size: 13px;
1270
+ }
1271
+
1272
+ .up-video__episode-grid {
1273
+ flex-direction: row;
1274
+ flex-wrap: wrap;
1275
+ }
1276
+
1277
+ .up-video__episode {
1278
+ height: 32px;
1279
+ margin: 3px;
1280
+ border-radius: 4px;
1281
+ flex-direction: row;
1282
+ align-items: center;
1283
+ justify-content: center;
1284
+ }
1285
+
1286
+ .up-video__episode-text {
1287
+ color: #ffffff;
1288
+ font-size: 12px;
1289
+ }
1290
+
1291
+ .up-video__volume {
1292
+ position: absolute;
1293
+ right: 44px;
1294
+ bottom: 44px;
1295
+ width: 200px;
1296
+ flex-direction: row;
1297
+ align-items: center;
1298
+ padding-left: 4px;
1299
+ padding-right: 10px;
1300
+ border-radius: 30px;
1301
+ background-color: rgba(0, 0, 0, 0.85);
1302
+ }
1303
+
1304
+ .up-video__volume-label {
1305
+ color: rgba(255, 255, 255, 0.7);
1306
+ font-size: 11px;
1307
+ margin-right: 6px;
1308
+ }
1309
+
1310
+ .up-video__volume-value {
1311
+ width: 28px;
1312
+ margin-left: 6px;
1313
+ color: #ffffff;
1314
+ font-size: 11px;
1315
+ text-align: right;
1316
+ }
1317
+
1318
+ .up-video__ad {
1319
+ position: absolute;
1320
+ top: 0;
1321
+ left: 0;
1322
+ right: 0;
1323
+ bottom: 0;
1324
+ background-color: #000000;
1325
+ }
1326
+
1327
+ .up-video__ad-media {
1328
+ width: 100%;
1329
+ height: 100%;
1330
+ }
1331
+
1332
+ .up-video__ad-bar {
1333
+ position: absolute;
1334
+ top: 8px;
1335
+ right: 8px;
1336
+ flex-direction: row;
1337
+ align-items: center;
1338
+ }
1339
+
1340
+ .up-video__ad-tag {
1341
+ color: #ffffff;
1342
+ font-size: 11px;
1343
+ padding: 2px 6px;
1344
+ border-radius: 3px;
1345
+ background-color: rgba(0, 0, 0, 0.55);
1346
+ }
1347
+
1348
+ .up-video__ad-btn {
1349
+ margin-left: 6px;
1350
+ padding: 2px 8px;
1351
+ border-radius: 3px;
1352
+ flex-direction: row;
1353
+ align-items: center;
1354
+ border-width: 1px;
1355
+ border-style: solid;
1356
+ border-color: rgba(255, 255, 255, 0.6);
1357
+ background-color: rgba(0, 0, 0, 0.55);
1358
+ }
1359
+
1360
+ .up-video__ad-btn-text {
1361
+ color: #ffffff;
1362
+ font-size: 11px;
1363
+ }
1364
+
1365
+ .up-video__pause-ad {
1366
+ position: absolute;
1367
+ top: 0;
1368
+ left: 0;
1369
+ right: 0;
1370
+ bottom: 0;
1371
+ align-items: center;
1372
+ justify-content: center;
1373
+ background-color: rgba(0, 0, 0, 0.6);
1374
+ }
1375
+
1376
+ .up-video__pause-ad-media {
1377
+ width: 70%;
1378
+ height: 60%;
1379
+ }
1380
+
1381
+ .up-video__pause-ad-close {
1382
+ position: absolute;
1383
+ top: 8px;
1384
+ right: 8px;
1385
+ width: 24px;
1386
+ height: 24px;
1387
+ border-radius: 12px;
1388
+ flex-direction: row;
1389
+ align-items: center;
1390
+ justify-content: center;
1391
+ background-color: rgba(0, 0, 0, 0.6);
1392
+ }
1393
+
1394
+ .up-video__danmu-input {
1395
+ position: absolute;
1396
+ left: 8px;
1397
+ right: 8px;
1398
+ bottom: 8px;
1399
+ flex-direction: row;
1400
+ align-items: center;
1401
+ padding: 4px 4px 4px 12px;
1402
+ border-radius: 30px;
1403
+ background-color: rgba(0, 0, 0, 0.85);
1404
+ }
1405
+
1406
+ .up-video__danmu-field {
1407
+ flex: 1;
1408
+ height: 30px;
1409
+ color: #ffffff;
1410
+ font-size: 13px;
1411
+ }
1412
+
1413
+ .up-video__danmu-send {
1414
+ height: 28px;
1415
+ padding-left: 14px;
1416
+ padding-right: 14px;
1417
+ border-radius: 20px;
1418
+ flex-direction: row;
1419
+ align-items: center;
1420
+ justify-content: center;
1421
+ }
1422
+
1423
+ .up-video__danmu-send-text {
1424
+ color: #ffffff;
1425
+ font-size: 12px;
1426
+ }
1427
+ </style>