vueVideoClip.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. <template>
  2. <div class="custom-video" v-if="videoShow">
  3. <div class="custom-video_container" ref="custom-video_container">
  4. <div class="custom-video_video">
  5. <video ref="custom-video" controls :src="url">
  6. <p>设备不支持</p>
  7. </video>
  8. </div>
  9. </div>
  10. <!-- 缩略图,裁剪区域 -->
  11. <div class="video-controls" @mousedown="handleClick">
  12. <div class="thumbs" ref="thumbs">
  13. <div class="inner" v-if="thumbCount" ref="thumbs-inner">
  14. <div class="inner-item" :style="`width:${videoUnitWidth}px;`" v-for="(item, index) in thumbArr"
  15. :key="index">
  16. <video width="100%" preload="metadata" :src="item.url" @canplay="item.loading = false;"></video>
  17. <div class="inner-loading" v-if="item.loading">
  18. <div class="loading">
  19. <div class="loading-con"></div>
  20. </div>
  21. </div>
  22. </div>
  23. </div>
  24. </div>
  25. <div class="control-bars" ref="control-bars">
  26. <div class="control-bars-mask left" data-direction="left" :style="`width:${leftMovePercentage}%;`">
  27. </div>
  28. <div class="control-bars-mask right" data-direction="right" :style="`width:${rightMovePercentage}%;`">
  29. </div>
  30. <div class="control-bars-progress" :style="`left:${videoEdit.currentPosition}%;`"
  31. :class="{ animate: videoEdit.play }">
  32. <svg width="54px" height="24px" viewBox="0 0 54 24" version="1.1" xmlns="http://www.w3.org/2000/svg"
  33. xmlns:xlink="http://www.w3.org/1999/xlink">
  34. <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  35. <path
  36. d="M27,23 L27,24 L26,24 L26,23 C26,20.2385763 23.7614237,18 21,18 L9,18 C4.02943725,18 0,13.9705627 0,9 C0,4.02943725 4.02943725,0 9,0 L45,0 C49.9705627,0 54,4.02943725 54,9 C54,13.9705627 49.9705627,18 45,18 L32,18 L32,18 C29.2385763,18 27,20.2385763 27,23 Z"
  37. id="time_bg" fill="#FFFFFF"></path>
  38. </g>
  39. </svg>
  40. <span class="text">{{ timeTranslate(currentTime) }}</span>
  41. </div>
  42. <div class="control-bars-wrapper" :style="`left:${leftMovePercentage}%;right:${rightMovePercentage}%;`">
  43. <div ref="move-left-icon" class="control-bars-move left"
  44. @mousedown="handleMoveDown($event, 'left')"></div>
  45. <div ref="move-right-icon" class="control-bars-move right"
  46. @mousedown="handleMoveDown($event, 'right')"></div>
  47. </div>
  48. </div>
  49. </div>
  50. <!-- 按钮文字 -->
  51. <div class="video-btn">
  52. <div class="crop-range" style="font-size: 12px; margin-bottom: 4px; text-align: center;">
  53. <div style="width: 150px;">输入开始时间</div>
  54. <span></span>
  55. <div style="width: 150px;">输入截止时间</div>
  56. </div>
  57. </div>
  58. <!-- 按钮区域 -->
  59. <div class="video-btn">
  60. <!-- <div class="toggle" @click="togglePlayStatus">
  61. <div class="toggle-icon" :class="{ playing: videoEdit.play }"></div>
  62. </div> -->
  63. <div class="crop-range">
  64. <div class="crop-input crop-start">
  65. <input class="text-right" type="text" placeholder="00" maxlength="3" id="range-0"
  66. v-model.number="inputStartLeftTime" @blur="onBlur('inputStartLeftTime')" />
  67. <span>:</span>
  68. <input class="text-left" type="text" placeholder="00" maxlength="2" id="range-1"
  69. v-model.number="inputStartRightTime" @blur="onBlur('inputStartRightTime')" />
  70. </div>
  71. <span style="color:#fff;margin:0 10px;"></span>
  72. <div class="crop-input crop-end">
  73. <input class="text-right" type="text" placeholder="00" maxlength="3" id="range-2" v-model.number="inputEndLeftTime" @blur="onBlur('inputEndLeftTime')" />
  74. <span>:</span>
  75. <input class="text-left" type="text" placeholder="00" maxlength="2" id="range-3" v-model.number="inputEndRightTime" @blur="onBlur('inputEndRightTime')" />
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. </template>
  81. <script>
  82. export default {
  83. name: 'vueVideoClip',
  84. props: {
  85. url: {
  86. type: String,
  87. required: true
  88. }
  89. },
  90. data() {
  91. return {
  92. blobURL: '',
  93. videoState: {
  94. play: false, // 播放状态
  95. currentPosition: 0 // 当前播放点距离左边的百分比
  96. },
  97. videoEdit: {
  98. start: 0,
  99. end: 0,
  100. duration: 0,
  101. play: false, // 播放状态
  102. currentPosition: 0 // 当前播放点距离左边的百分比
  103. },
  104. videoDom: null, // video
  105. duration: 0, // 视频总时长
  106. currentTime: 0, // 视频当前播放时长 = this.videoDom.currentTime
  107. objectURL: '',
  108. videoUnit: 0,
  109. videoUnitWidth: 0,
  110. videoRatio: 0,
  111. isPortraitVideo: false,
  112. thumbCount: 0,
  113. thumbArr: [], // 缩略图数组
  114. leftMovePercentage: 0,
  115. leftMoveInit: 0,
  116. rightMovePercentage: 0,
  117. rightMoveInit: 0,
  118. videoShow: true,
  119. loadingTap: '',
  120. }
  121. },
  122. mounted() {
  123. this.loadingTap = this.$loading({
  124. text: '加载中',
  125. spinner: 'el-icon-loading',
  126. });
  127. this.init();
  128. window.onresize = () => {
  129. this.throttle(this.init(), 300)
  130. this.handleClick(null, this.handleMoveDirection);
  131. }
  132. },
  133. computed: {
  134. inputStartLeftTime: {
  135. set(val) {
  136. val = val * 60 + this.toInt(document.getElementById('range-1').value)
  137. // if (val >= this.videoEdit.end || val < 0) {
  138. // val = 0
  139. // }
  140. this.videoEdit.start = val
  141. },
  142. get() {
  143. return this.timeTranslate(this.videoEdit.start).split(':')[0]
  144. }
  145. },
  146. inputStartRightTime: {
  147. set(val) {
  148. val = this.toInt(document.getElementById('range-0').value) * 60 + val
  149. // if (val >= this.videoEdit.end || val < 0) {
  150. // val = 0
  151. // }
  152. this.videoEdit.start = val
  153. },
  154. get() {
  155. return this.timeTranslate(this.videoEdit.start).split(':')[1]
  156. }
  157. },
  158. inputEndLeftTime: {
  159. set(val) {
  160. val = val * 60 + this.toInt(document.getElementById('range-3').value)
  161. // if (val <= this.videoEdit.start || val > this.videoEdit.duration) {
  162. // val = this.videoEdit.duration
  163. // }
  164. this.videoEdit.end = val
  165. },
  166. get() {
  167. return this.timeTranslate(this.videoEdit.end).split(':')[0]
  168. }
  169. },
  170. inputEndRightTime: {
  171. set(val) {
  172. val = this.toInt(document.getElementById('range-2').value) * 60 + val
  173. // if (val <= this.videoEdit.start || val > this.videoEdit.duration) {
  174. // val = this.videoEdit.duration
  175. // }
  176. this.videoEdit.end = val
  177. },
  178. get() {
  179. return this.timeTranslate(this.videoEdit.end).split(':')[1]
  180. }
  181. }
  182. },
  183. watch: {
  184. 'videoEdit.start': {
  185. handler(val) {
  186. this.currentTime = val
  187. this.videoEdit.currentPosition =
  188. (this.currentTime / this.videoEdit.duration) * 100
  189. this.leftMovePercentage = this.videoEdit.currentPosition
  190. this.$emit('getTime', {
  191. start: val,
  192. end: this.videoEdit.end
  193. })
  194. },
  195. deep: true
  196. },
  197. 'videoEdit.end': {
  198. handler(val) {
  199. this.currentTime = val
  200. this.videoEdit.currentPosition =
  201. (this.currentTime / this.videoEdit.duration) * 100
  202. this.rightMovePercentage = 100 - this.videoEdit.currentPosition
  203. this.$emit('getTime', {
  204. start: this.videoEdit.start,
  205. end: val
  206. })
  207. },
  208. deep: true
  209. }
  210. },
  211. methods: {
  212. onBlur(str){
  213. let val = Number(this[str]);
  214. switch(str){
  215. case 'inputStartLeftTime':
  216. val = val * 60 + this.toInt(document.getElementById('range-1').value)
  217. if (val >= this.videoEdit.end || val < 0) {
  218. val = 0
  219. }
  220. this.videoEdit.start = val
  221. break;
  222. case 'inputStartRightTime':
  223. val = this.toInt(document.getElementById('range-0').value) * 60 + val
  224. if (val >= this.videoEdit.end || val < 0) {
  225. val = 0
  226. }
  227. this.videoEdit.start = val
  228. break;
  229. case 'inputEndLeftTime':
  230. val = val * 60 + this.toInt(document.getElementById('range-3').value)
  231. if (val <= this.videoEdit.start || val > this.videoEdit.duration) {
  232. val = this.videoEdit.duration
  233. }
  234. this.videoEdit.end = val
  235. break;
  236. case 'inputEndRightTime':
  237. val = this.toInt(document.getElementById('range-2').value) * 60 + val
  238. if (val <= this.videoEdit.start || val > this.videoEdit.duration) {
  239. val = this.videoEdit.duration
  240. }
  241. this.videoEdit.end = val
  242. break;
  243. }
  244. // console.log(this.videoEdit.end, this.videoEdit.start);
  245. },
  246. throttle(fn, threshold, scope) {
  247. let timer
  248. return function() {
  249. const context = scope || this
  250. const args = arguments
  251. if (!timer) {
  252. timer = setTimeout(function() {
  253. fn.apply(context, args)
  254. timer = null
  255. }, threshold)
  256. }
  257. }
  258. },
  259. init() {
  260. // 初始化相关元数据
  261. this.videoDom = this.$refs['custom-video']
  262. this.leftIconDom = this.$refs['move-left-icon']
  263. this.rightIconDom = this.$refs['move-right-icon']
  264. this.thumbsWidth = this.$refs.thumbs.clientWidth
  265. this.leftMoveInit = this.getOffset(this.leftIconDom).left + 5
  266. this.rightMoveInit = this.getOffset(this.rightIconDom).left + 5
  267. this.minWidthPercentage = ((10 / this.thumbsWidth) * 100).toFixed(4) // 最小裁剪区域所占百分比
  268. this.thumbArr = [];
  269. this.initMedaData()
  270. // this.transformBlob()
  271. document.addEventListener('mouseup', ev => {
  272. this.handleMoveStatus = false
  273. })
  274. document.addEventListener('mousemove', ev => {
  275. if (!this.handleMoveStatus) return
  276. if (this.handleMoveDirection === 'left') {
  277. const distanceMoveXLeft = ev.clientX - this.leftMoveInit
  278. this.leftMovePercentage =
  279. distanceMoveXLeft > 0 ?
  280. ((distanceMoveXLeft / (this.thumbsWidth - 20)) * 100).toFixed(4) :
  281. 0
  282. // 控制裁剪百分比最小值
  283. if (
  284. this.leftMovePercentage >
  285. 100 - this.rightMovePercentage - this.minWidthPercentage
  286. ) {
  287. this.leftMovePercentage =
  288. 100 - this.rightMovePercentage - this.minWidthPercentage
  289. }
  290. this.videoEdit.start = (
  291. (this.videoEdit.duration * this.leftMovePercentage) /
  292. 100
  293. ).toFixed(4)
  294. }
  295. if (this.handleMoveDirection === 'right') {
  296. const distanceMoveXRight = this.rightMoveInit - ev.clientX
  297. this.rightMovePercentage =
  298. distanceMoveXRight > 0 ?
  299. ((distanceMoveXRight / (this.thumbsWidth - 20)) * 100).toFixed(
  300. 4
  301. ) :
  302. 0
  303. // 控制裁剪百分比最小值
  304. if (
  305. this.rightMovePercentage >
  306. 100 - this.leftMovePercentage - this.minWidthPercentage
  307. ) {
  308. this.rightMovePercentage =
  309. 100 - this.leftMovePercentage - this.minWidthPercentage
  310. }
  311. this.videoEdit.end = (
  312. this.videoEdit.duration *
  313. (1 - this.rightMovePercentage / 100)
  314. ).toFixed(4)
  315. }
  316. this.handleClick(ev, this.handleMoveDirection)
  317. })
  318. },
  319. toInt(val) {
  320. return parseInt(val) || 0
  321. },
  322. togglePlayStatus() {
  323. // 播放暂停按钮事件
  324. this.videoEdit.play ? this.toggleVideoPause() : this.toggleVideoPlay()
  325. },
  326. toggleVideoPlay() {
  327. // 处理当前位置在末尾的时候先初始化开始播放时间
  328. if (this.videoEdit.end - this.currentTime < 0.01) {
  329. this.videoDom.currentTime = this.videoEdit.start
  330. }
  331. // 为了取消当前点平滑移动到开始点的过渡
  332. setTimeout(() => {
  333. this.videoDom.play()
  334. this.videoEdit.play = true
  335. }, 50)
  336. },
  337. toggleVideoPause() {
  338. this.videoDom.pause()
  339. this.videoEdit.play = false
  340. },
  341. playEnd() {
  342. this.videoDom.currentTime = this.videoEdit.start
  343. this.videoDom.pause()
  344. this.videoEdit.play = false
  345. },
  346. transformBlob() {
  347. const self = this
  348. const xhr = new XMLHttpRequest()
  349. xhr.open('GET', this.url, true)
  350. xhr.responseType = 'blob'
  351. xhr.onload = function(e) {
  352. if (this.status === 200) {
  353. // 获取blob对象
  354. const myBlob = this.response
  355. self.blobURL = URL.createObjectURL(myBlob)
  356. } else {
  357. this.$Notice.error({
  358. title: '读取失败',
  359. desc: '参数有误,读取视频失败!',
  360. duration: 5
  361. })
  362. }
  363. }
  364. xhr.send()
  365. },
  366. initMedaData() {
  367. // 初始化video相关事件
  368. this.videoDom.addEventListener('loadedmetadata', () => {
  369. // 获取视频总时长
  370. this.videoEdit.duration = this.videoDom.duration // 视频总时长
  371. this.videoEdit.end = this.videoEdit.duration
  372. })
  373. const self = this
  374. this.videoDom.addEventListener('canplay', () => {
  375. // 监听视频可播放时的状态
  376. self.videoRatio = this.videoHeight / this.videoWidth
  377. self.isPortraitVideo = self.videoRatio > 1.5 // 是否是竖向视频
  378. self.videoUnitWidth = self.isPortraitVideo ? 28 : 88 // 单个缩略图宽度
  379. self.thumbCount = Math.ceil(self.thumbsWidth / self.videoUnitWidth) // 缩略图个数
  380. self.videoUnit = self.videoEdit.duration / self.thumbCount
  381. // 缩略图
  382. if (self.thumbArr.length !== self.thumbCount) {
  383. self.thumbArr = []
  384. for (let i = 0; i < self.thumbCount; i++) {
  385. self.thumbArr.push({
  386. url: `${self.url}#t=${self.videoUnit * i}`,
  387. loading: true
  388. })
  389. }
  390. }
  391. const innerMoveLeft = Math.round(
  392. (self.thumbCount * self.videoUnitWidth - self.thumbsWidth) / 2
  393. )
  394. self.$nextTick(() => {
  395. self.$refs['thumbs-inner'].style.marginLeft = `${-innerMoveLeft}px`
  396. })
  397. })
  398. this.videoDom.addEventListener('timeupdate', () => {
  399. // 监听视频播放过程中的时间
  400. this.videoEdit.currentPosition =
  401. (this.videoDom.currentTime / this.videoEdit.duration) * 100
  402. this.currentTime = this.videoDom.currentTime
  403. if (
  404. this.videoEdit.end - this.currentTime < 0.01 &&
  405. this.videoEdit.play
  406. ) {
  407. this.playEnd()
  408. }
  409. })
  410. this.loadingTap.close();
  411. },
  412. handleMoveDown(ev, direction) {
  413. this.handleMoveStatus = true
  414. this.handleMoveDirection = direction
  415. this.toggleVideoPause()
  416. this.handleClick(ev, direction)
  417. },
  418. handleClick(ev, direction) {
  419. // 区分各种情况是为了获取各种情况的当前播放点更准确
  420. if (direction === 'left') {
  421. this.videoEdit.currentPosition = this.leftMovePercentage
  422. } else if (direction === 'right') {
  423. this.videoEdit.currentPosition = 100 - this.rightMovePercentage
  424. } else {
  425. if(ev){
  426. // 点击中间剪辑区域
  427. this.videoEdit.currentPosition =
  428. ((ev.clientX - this.leftMoveInit) / this.thumbsWidth) * 100
  429. }else{
  430. this.videoEdit.currentPosition = 0;
  431. }
  432. }
  433. this.currentTime = (
  434. (this.videoEdit.currentPosition * this.videoEdit.duration) /
  435. 100
  436. ).toFixed(4)
  437. this.videoDom.currentTime = this.currentTime
  438. // 处理两边 mask 点击而非滑动时,拖拽区域的处理
  439. if(ev){
  440. if (ev.target.dataset.direction === 'left' && !this.handleMoveStatus) {
  441. this.leftMovePercentage = this.videoEdit.currentPosition
  442. this.videoEdit.start = this.currentTime
  443. }
  444. if (ev.target.dataset.direction === 'right' && !this.handleMoveStatus) {
  445. this.rightMovePercentage = 100 - this.videoEdit.currentPosition
  446. this.videoEdit.end = this.currentTime
  447. }
  448. }
  449. // 百分比取最大值:100 最小值:0
  450. this.videoEdit.currentPosition =
  451. this.videoEdit.currentPosition > 100 ?
  452. 100 :
  453. this.videoEdit.currentPosition
  454. this.videoEdit.currentPosition =
  455. this.videoEdit.currentPosition < 0 ? 0 : this.videoEdit.currentPosition
  456. },
  457. getOffset(node, offset) {
  458. // 获取当前屏幕下进度条的左偏移量和又偏移量
  459. if (!offset) {
  460. offset = {}
  461. offset.left = 0
  462. offset.top = 0
  463. }
  464. if (node === document.body || node === null) {
  465. return offset
  466. }
  467. offset.top += node.offsetTop
  468. offset.left += node.offsetLeft
  469. return this.getOffset(node.offsetParent, offset)
  470. },
  471. timeTranslate(t) {
  472. // 时间转化
  473. let m = Math.floor(t / 60)
  474. m < 10 && (m = '0' + m)
  475. return m + ':' + ((t % 60) / 100).toFixed(2).slice(-2)
  476. }
  477. }
  478. }
  479. </script>
  480. <style scoped lang="scss">
  481. .custom-video {
  482. user-select: none;
  483. margin: 0 auto;
  484. // min-height: 100%;
  485. padding: 20px;
  486. border-radius: 20px;
  487. min-width: 500px;
  488. .custom-video_video{
  489. text-align: center;
  490. }
  491. &_container {
  492. height: 290px;
  493. margin: 0 auto;
  494. position: relative;
  495. overflow: hidden;
  496. }
  497. &_video {
  498. height: 100%;
  499. video {
  500. height: 100%;
  501. }
  502. }
  503. }
  504. .thumbs {
  505. white-space: nowrap;
  506. overflow: hidden;
  507. height: 50px;
  508. background: #ffc7d1;
  509. .inner {
  510. height: 50px;
  511. display: flex;
  512. &-item {
  513. position: relative;
  514. }
  515. &-loading {
  516. position: absolute;
  517. top: 0;
  518. left: 0;
  519. height: 100%;
  520. width: 100%;
  521. }
  522. }
  523. video {
  524. object-fit: cover;
  525. }
  526. }
  527. .video-controls {
  528. position: relative;
  529. margin: 50px 0 15px;
  530. overflow: hidden;
  531. }
  532. .control-bars {
  533. position: absolute;
  534. right: 0;
  535. bottom: 0;
  536. top: 0;
  537. left: 0;
  538. &-wrapper {
  539. position: absolute;
  540. right: 0;
  541. bottom: 0;
  542. top: 0;
  543. left: 0;
  544. border: 1px solid #04f0fb;
  545. z-index: 10;
  546. min-width: 10px;
  547. }
  548. &-mask {
  549. position: absolute;
  550. top: 0;
  551. bottom: 0;
  552. background-color: rgba(25, 30, 64, 0.8);
  553. &.left {
  554. left: 0;
  555. }
  556. &.right {
  557. right: 0;
  558. }
  559. }
  560. &-progress {
  561. position: absolute;
  562. height: 80px;
  563. top: -15px;
  564. width: 1px;
  565. background: #ffffff;
  566. border-radius: 100px;
  567. &.animate {
  568. transition: all 0.3s;
  569. }
  570. svg {
  571. position: absolute;
  572. top: -10px;
  573. left: -26px;
  574. }
  575. .text {
  576. position: absolute;
  577. width: 54px;
  578. height: 18px;
  579. top: -10px;
  580. left: -26px;
  581. font-size: 12px;
  582. line-height: 18px;
  583. }
  584. }
  585. &-move {
  586. position: absolute;
  587. width: 10px;
  588. height: 10px;
  589. background: #04f0fb;
  590. top: 50%;
  591. transform: translateY(-50%);
  592. border-radius: 50%;
  593. cursor: ew-resize;
  594. z-index: 2;
  595. &:active {
  596. background: #fff;
  597. }
  598. &.left {
  599. left: -5px;
  600. }
  601. &.right {
  602. right: -5px;
  603. }
  604. }
  605. }
  606. .video-btn {
  607. display: flex;
  608. align-items: center;
  609. justify-content: space-between;
  610. .toggle {
  611. width: 70px;
  612. height: 30px;
  613. line-height: 30px;
  614. border-radius: 30px;
  615. background-color: rgba(0, 0, 0, 0.3);
  616. position: relative;
  617. transition: background-color 0.2s;
  618. cursor: pointer;
  619. &-icon {
  620. position: absolute;
  621. display: inline-block;
  622. top: 50%;
  623. left: 50%;
  624. transform: translate(-50%, -50%);
  625. width: 0;
  626. height: 0;
  627. border-top: 8px solid transparent;
  628. border-bottom: 8px solid transparent;
  629. border-left: 12px solid #fff;
  630. opacity: 1;
  631. transition: all 0.2s;
  632. &.playing {
  633. width: 12px;
  634. height: 15px;
  635. border-left: solid 4px #fff;
  636. border-right: solid 4px #fff;
  637. border-top: solid 0 transparent;
  638. border-bottom: solid 0 transparent;
  639. box-sizing: border-box;
  640. }
  641. }
  642. }
  643. .crop {
  644. &-range {
  645. display: flex;
  646. align-items: center;
  647. font-size: 14px;
  648. }
  649. &-input {
  650. height: 30px;
  651. border-radius: 15px;
  652. overflow: hidden;
  653. padding: 0 14px;
  654. display: flex;
  655. align-items: center;
  656. input {
  657. float: left;
  658. width: 50px;
  659. border: 1px solid #ddd;
  660. font-size: 14px;
  661. border-radius: 20px;
  662. text-align: center;
  663. }
  664. span {
  665. width: 8px;
  666. line-height: 28px;
  667. height: 28px;
  668. float: left;
  669. margin: 0;
  670. padding: 0;
  671. }
  672. }
  673. }
  674. }
  675. .loading {
  676. width: 100%;
  677. height: 100%;
  678. position: relative;
  679. display: flex;
  680. align-items: center;
  681. justify-content: center;
  682. &-con {
  683. position: absolute;
  684. animation: loading 1s linear infinite;
  685. width: 25px;
  686. height: 25px;
  687. border-radius: 50%;
  688. box-shadow: 0 2px 0 0 #fff; // ffc7d1
  689. transform-origin: 50% 50%;
  690. }
  691. }
  692. @keyframes loading {
  693. 0% {
  694. transform: rotate(0deg);
  695. }
  696. 50% {
  697. transform: rotate(180deg);
  698. }
  699. 100% {
  700. transform: rotate(360deg);
  701. }
  702. }
  703. .text-right{
  704. text-align: right;
  705. }
  706. .text-left{
  707. text-align: left;
  708. }
  709. </style>