u-short-video.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <template>
  2. <view class="u-short-video">
  3. <!-- 顶部导航区域 -->
  4. <view class="u-short-video__header">
  5. <slot name="menu">
  6. <view class="u-short-video__header__menu">
  7. <up-icon name="grid" size="24"></up-icon>
  8. </view>
  9. </slot>
  10. <up-tabs
  11. :list="tabsList"
  12. :current="currentTab"
  13. lineColor="#ddd"
  14. :activeStyle="{
  15. color: '#ddd',
  16. fontWeight: 400,
  17. transform: 'scale(1)'
  18. }"
  19. :inactiveStyle="{
  20. color: '#bbb',
  21. transform: 'scale(1)'
  22. }"
  23. @change="handleTabChange"
  24. class="u-short-video__header__tabs"
  25. ></up-tabs>
  26. <slot name="search">
  27. <view class="u-short-video__header__search">
  28. <up-icon name="search" size="24"></up-icon>
  29. </view>
  30. </slot>
  31. </view>
  32. <!-- 视频内容区域 -->
  33. <swiper
  34. :vertical="true"
  35. :autoplay="false"
  36. @change="handleSwiperChange"
  37. :current="currentVideo"
  38. class="u-short-video__content"
  39. >
  40. <swiper-item v-for="(item, index) in videoList" :key="index">
  41. <view class="u-short-video__content__item">
  42. <!-- 视频播放区域 -->
  43. <view class="u-short-video__content__video">
  44. <video
  45. :id="'video-' + index"
  46. :src="item.videoUrl"
  47. :autoplay="index === currentVideo"
  48. :controls="false"
  49. :show-fullscreen-btn="false"
  50. :show-play-btn="false"
  51. :show-center-play-btn="false"
  52. :enable-progress-gesture="true"
  53. :loop="true"
  54. :playback-rate="item.playbackRate || 1.0"
  55. @play="onVideoPlay"
  56. @pause="onVideoPause"
  57. @ended="onVideoEnded"
  58. @timeupdate="onTimeUpdate"
  59. @loadedmetadata="onLoadedMetadata"
  60. style="width: 100%; height: 100%;"
  61. ></video>
  62. <!-- 倍速设置按钮 -->
  63. <!-- <view class="u-short-video__content__video__speed" @click="showSpeedOptions(index)">
  64. <text class="speed-text">{{ item.playbackRate || 1.0 }}x</text>
  65. <up-icon name="arrow-down" size="12" color="#fff"></up-icon>
  66. </view> -->
  67. </view>
  68. <!-- 作者信息 -->
  69. <view class="u-short-video__content__author">
  70. <view class="u-short-video__content__author__avatar">
  71. <u-avatar :src="item.author.avatar" size="50px"></u-avatar>
  72. </view>
  73. <view class="u-short-video__content__author__info">
  74. <text class="u-short-video__content__author__name">{{ item.author.name }}</text>
  75. <text class="u-short-video__content__author__desc">{{ item.author.desc }}</text>
  76. </view>
  77. <view class="u-short-video__content__author__follow">
  78. <up-button type="primary" size="mini">关注</up-button>
  79. </view>
  80. </view>
  81. <!-- 右侧操作区域 -->
  82. <view class="u-short-video__content__actions">
  83. <slot name="actions" :item="item" :index="index">
  84. <view class="u-short-video__content__actions__item" @click="handleLike(item, index)">
  85. <up-icon color="#eee" :name="item.isLiked ? 'thumb-up-fill' : 'thumb-up'" size="32px"></up-icon>
  86. <text class="u-short-video__content__actions__text">{{ item.likeCount }}</text>
  87. </view>
  88. <view class="u-short-video__content__actions__item" @click="handleComment(item, index)">
  89. <up-icon color="#eee" name="chat" size="32px"></up-icon>
  90. <text class="u-short-video__content__actions__text">{{ item.commentCount }}</text>
  91. </view>
  92. <view class="u-short-video__content__actions__item" @click="handleShare(item, index)">
  93. <up-icon color="#eee" name="share" size="32px"></up-icon>
  94. <text class="u-short-video__content__actions__text">{{ item.shareCount }}</text>
  95. </view>
  96. <view class="u-short-video__content__actions__item" @click="handleCollect(item, index)">
  97. <up-icon color="#eee" :name="item.isCollected ? 'bookmark-fill' : 'bookmark'" size="32px"></up-icon>
  98. <text class="u-short-video__content__actions__text">{{ item.collectCount }}</text>
  99. </view>
  100. </slot>
  101. </view>
  102. </view>
  103. </swiper-item>
  104. </swiper>
  105. <!-- 倍速选择弹窗 -->
  106. <up-action-sheet
  107. :show="showSpeedSheet"
  108. :actions="speedOptions"
  109. title="播放速度"
  110. @close="showSpeedSheet = false"
  111. @select="selectSpeed"
  112. ></up-action-sheet>
  113. <!-- 底部导航栏 -->
  114. <view class="u-short-video__footer">
  115. <!-- 进度条 -->
  116. <view class="u-short-video__progress" style="z-index: 999;">
  117. <up-slider
  118. :value="videoList[currentVideo]?.progress"
  119. :min="0"
  120. :max="100"
  121. :step="1"
  122. :show-value="false"
  123. :innerStyle="{padding: 0}"
  124. activeColor="rgba(255,255,255,0.32)"
  125. inactive-color="rgba(255,255,255,0.3)"
  126. block-size="6px"
  127. block-color="rgba(255,255,255,0.5)"
  128. height="1px"
  129. @changing="onProgressChanging"
  130. @change="onProgressChange"
  131. ></up-slider>
  132. </view>
  133. <slot name="tabbar">
  134. <up-tabbar
  135. :fixed="true"
  136. :placeholder="true"
  137. :safeAreaInsetBottom="true"
  138. borderColor="rgba(255,255,255,0.25) !important"
  139. backgroundColor="rgba(255,255,255,0.05)"
  140. >
  141. <up-tabbar-item
  142. @click="goNext"
  143. text="首页"
  144. icon="home"
  145. >
  146. </up-tabbar-item>
  147. <up-tabbar-item
  148. text="放映厅"
  149. icon="photo"
  150. ></up-tabbar-item>
  151. <up-tabbar-item
  152. text="直播"
  153. icon="play-right"
  154. ></up-tabbar-item>
  155. <up-tabbar-item
  156. text="我的"
  157. icon="account"
  158. ></up-tabbar-item>
  159. </up-tabbar>
  160. </slot>
  161. </view>
  162. </view>
  163. </template>
  164. <script>
  165. export default {
  166. name: 'u-short-video',
  167. props: {
  168. // tabs标签列表
  169. tabsList: {
  170. type: Array,
  171. default: () => [
  172. { name: '推荐' },
  173. { name: '关注' },
  174. { name: '朋友' },
  175. { name: '本地' }
  176. ]
  177. },
  178. // 视频列表数据
  179. videoList: {
  180. type: Array,
  181. default: () => []
  182. },
  183. // 当前选中的tab索引
  184. currentTab: {
  185. type: Number,
  186. default: 0
  187. },
  188. // 当前播放的视频索引
  189. currentVideo: {
  190. type: Number,
  191. default: 0
  192. }
  193. },
  194. data() {
  195. return {
  196. progressValue: 0,
  197. showSpeedSheet: false,
  198. currentSpeedVideoIndex: 0,
  199. speedOptions: [
  200. { name: '0.5x', value: 0.5 },
  201. { name: '0.75x', value: 0.75 },
  202. { name: '1.0x', value: 1.0 },
  203. { name: '1.25x', value: 1.25 },
  204. { name: '1.5x', value: 1.5 },
  205. { name: '2.0x', value: 2.0 }
  206. ]
  207. }
  208. },
  209. methods: {
  210. // 处理tab切换
  211. handleTabChange(index) {
  212. this.$emit('tabChange', index);
  213. },
  214. // 处理swiper切换
  215. handleSwiperChange(e) {
  216. const currentIndex = e.detail.current;
  217. // 暂停当前播放的视频
  218. this.pauseCurrentVideo();
  219. // 播放新切换到的视频
  220. this.$nextTick(() => {
  221. this.playVideo(currentIndex);
  222. });
  223. this.$emit('videoChange', currentIndex);
  224. },
  225. // 处理点赞
  226. handleLike(item, index) {
  227. this.$emit('like', { item, index });
  228. },
  229. // 处理评论
  230. handleComment(item, index) {
  231. this.$emit('comment', { item, index });
  232. },
  233. // 处理分享
  234. handleShare(item, index) {
  235. this.$emit('share', { item, index });
  236. },
  237. // 处理收藏
  238. handleCollect(item, index) {
  239. this.$emit('collect', { item, index });
  240. },
  241. // 进度条拖动中
  242. onProgressChanging(value) {
  243. // 更新当前视频的进度值
  244. if (this.videoList[this.currentVideo]) {
  245. this.videoList[this.currentVideo]['progressValue'] = value.detail.value
  246. }
  247. this.$emit('progressChanging', {
  248. progress: value.detail.value,
  249. index: this.currentVideo
  250. });
  251. },
  252. // 进度条值改变
  253. onProgressChange(value) {
  254. // 更新当前视频的进度值
  255. if (this.videoList[this.currentVideo]) {
  256. this.$set(this.videoList[this.currentVideo], 'progressValue', value.detail.value);
  257. }
  258. this.$emit('progressChange', {
  259. progress: value.detail.value,
  260. index: this.currentVideo
  261. });
  262. },
  263. // 显示倍速选项
  264. showSpeedOptions(index) {
  265. this.currentSpeedVideoIndex = index;
  266. this.showSpeedSheet = true;
  267. },
  268. // 选择倍速
  269. selectSpeed(action) {
  270. const videoContext = uni.createVideoContext('video-' + this.currentSpeedVideoIndex, this);
  271. videoContext.playbackRate(action.value);
  272. // 更新视频倍速数据
  273. this.$set(this.videoList[this.currentSpeedVideoIndex], 'playbackRate', action.value);
  274. this.showSpeedSheet = false;
  275. },
  276. // 播放指定索引的视频
  277. playVideo(index) {
  278. const videoContext = uni.createVideoContext('video-' + index, this);
  279. videoContext.play();
  280. },
  281. // 暂停当前视频
  282. pauseCurrentVideo() {
  283. const videoContext = uni.createVideoContext('video-' + this.currentVideo, this);
  284. videoContext.pause();
  285. },
  286. // 视频播放事件
  287. onVideoPlay(e) {
  288. this.$emit('videoPlay', { index: this.currentVideo, event: e });
  289. },
  290. // 视频暂停事件
  291. onVideoPause(e) {
  292. this.$emit('videoPause', { index: this.currentVideo, event: e });
  293. },
  294. // 视频结束事件
  295. onVideoEnded(e) {
  296. this.$emit('videoEnded', { index: this.currentVideo, event: e });
  297. },
  298. // 视频时间更新事件
  299. onTimeUpdate(e) {
  300. const progress = (e.detail.currentTime / e.detail.duration) * 100;
  301. if (this.videoList[this.currentVideo]) {
  302. this.$set(this.videoList[this.currentVideo], 'progress', progress);
  303. }
  304. this.$emit('timeUpdate', { index: this.currentVideo, event: e });
  305. },
  306. // 视频元数据加载完成事件
  307. onLoadedMetadata(e) {
  308. this.$emit('loadedMetadata', { index: this.currentVideo, event: e });
  309. }
  310. }
  311. }
  312. </script>
  313. <style lang="scss" scoped>
  314. .u-short-video {
  315. width: 100%;
  316. height: 100vh;
  317. position: relative;
  318. &__header {
  319. position: absolute;
  320. top: 0;
  321. left: 0;
  322. right: 0;
  323. z-index: 10;
  324. display: flex;
  325. flex-direction: row;
  326. align-items: center;
  327. justify-content: space-between;
  328. padding: 10px 15px;
  329. background-color: rgba(255, 255, 255, 0.05);
  330. opacity: 1;
  331. &__menu, &__search {
  332. width: 40px;
  333. height: 40px;
  334. display: flex;
  335. align-items: center;
  336. justify-content: center;
  337. color: #fff;
  338. }
  339. &__tabs {
  340. flex: 1;
  341. margin: 0 10px;
  342. }
  343. }
  344. &__content {
  345. width: 100%;
  346. height: 100%;
  347. &__item {
  348. width: 100%;
  349. height: 100%;
  350. position: relative;
  351. }
  352. &__video {
  353. width: 100%;
  354. height: 100%;
  355. position: relative;
  356. &__speed {
  357. position: absolute;
  358. top: 15px;
  359. right: 15px;
  360. z-index: 10;
  361. background-color: rgba(0, 0, 0, 0.3);
  362. border-radius: 20px;
  363. padding: 5px 10px;
  364. display: flex;
  365. align-items: center;
  366. .speed-text {
  367. color: #fff;
  368. font-size: 12px;
  369. margin-right: 4px;
  370. }
  371. }
  372. }
  373. &__author {
  374. position: absolute;
  375. left: 15px;
  376. bottom: 100px;
  377. display: flex;
  378. flex-direction: row;
  379. align-items: center;
  380. z-index: 10;
  381. &__info {
  382. margin-left: 10px;
  383. display: flex;
  384. flex-direction: column;
  385. justify-content: center;
  386. }
  387. &__name {
  388. color: #eee;
  389. font-size: 16px;
  390. font-weight: bold;
  391. margin-bottom: 5px;
  392. }
  393. &__desc {
  394. color: rgba(255, 255, 255, 0.8);
  395. font-size: 14px;
  396. }
  397. &__follow {
  398. margin-left: 15px;
  399. }
  400. }
  401. &__actions {
  402. position: absolute;
  403. right: 15px;
  404. bottom: 100px;
  405. display: flex;
  406. flex-direction: column;
  407. align-items: center;
  408. z-index: 10;
  409. &__item {
  410. display: flex;
  411. flex-direction: column;
  412. align-items: center;
  413. margin-bottom: 20px;
  414. color: #fff;
  415. }
  416. &__text {
  417. color: #fff;
  418. font-size: 12px;
  419. margin-top: 5px;
  420. }
  421. }
  422. }
  423. &__footer {
  424. position: absolute;
  425. bottom: 0;
  426. left: 0;
  427. right: 0;
  428. z-index: 10;
  429. }
  430. &__progress {
  431. }
  432. }
  433. </style>