home.vue 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483
  1. <template>
  2. <div>
  3. <el-container style="height: 100vh;">
  4. <el-header height="45px" style="background-color: #fafafa; padding: 0 10px;">
  5. <soft-header ref="headerRef" @update-soft="updateSoft()" @login-url="loginUrl" @clear-cache="clearCache"></soft-header>
  6. </el-header>
  7. <el-main ref="el-main" style="background-color: #fafafa;">
  8. <template>
  9. <div style="padding: 10px 20px 0 20px; height: 100%;">
  10. <div style="text-align: center;">
  11. <p class="c-titles">星优-在线视频下载助手</p>
  12. <div style="padding-top: 8px; font-weight: 600;">
  13. 支持
  14. <img src="../assets/image/douyin.png" class="soft-icon" title="抖音" />
  15. <img src="../assets/image/weibo.png" class="soft-icon" title="微博" />
  16. <img src="../assets/image/kuaishou.png" class="soft-icon" title="快手" />
  17. <img src="../assets/image/xiaohongshu.png" class="soft-icon" title="小红书" />
  18. <img src="../assets/image/haokan.png" class="soft-icon" title="好看视频" />
  19. <img src="../assets/image/bilibili.png" class="soft-icon" title="b站" />
  20. <img src="../assets/image/cctv.png" class="soft-icon" title="cctv" />
  21. <img src="../assets/image/56.png" class="soft-icon" title="56视频" />
  22. <img src="../assets/image/acfun.png" class="soft-icon" title="acfun" />
  23. 等多个平台,<span style="color: #F56C6C;">(请使用win7以上系统)</span>
  24. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="会员/付费/版权视频不支持下载">
  25. <i class="el-icon-info" slot="reference" style="margin-right: 10px; color: #F56C6C;"></i>
  26. </el-popover>
  27. </div>
  28. </div>
  29. <div style="margin-top: 20px;">
  30. <el-input type="textarea" :rows="5" placeholder="请输入需要解析的视频地址" v-model="formatUrl"></el-input>
  31. <div class="content-top" style="padding: 10px 0; text-align: center; display: inherit;">
  32. <el-button type="danger" @click="startParsing()" :loading="parseLoading">开始解析</el-button>
  33. </div>
  34. </div>
  35. <div class="table-scroll" style="height: calc(100% - 290px); overflow: hidden;">
  36. <el-row type="flex" justify="space-between">
  37. <div>
  38. <h4 style="display: inline-block;">
  39. 视频信息:
  40. </h4>
  41. <el-button @click="mergeClick()" size="small" :disabled="bUrl" title="解析B站网址后启用">B站音视频合并</el-button>
  42. </div>
  43. <el-row type="flex" style="align-items: center;">
  44. <div class="set-item">
  45. <span class="set-title">视频保存目录:</span>
  46. <el-input :title="downloadDir" ref="upload-input" size="mini" @focus="pickPath" placeholder="请选择输出目录" v-model="downloadDir" readonly style="width:200px;" prefix-icon="el-icon-folder"></el-input>
  47. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="打开保存目录">
  48. <i class="el-icon-folder-opened" slot="reference" style="padding-left: 5px; cursor: pointer; font-size: 22px; vertical-align: middle;" @click="openFolder()"></i>
  49. </el-popover>
  50. </div>
  51. </el-row>
  52. </el-row>
  53. <vxe-table ref="xTable" show-overflow class="img-table" max-height="100%" empty-text="没有更多数据了!" :loading="tabLoading" :row-config="{isHover: true}"
  54. :loading-config="{icon: 'vxe-icon-indicator roll', text: '加载中...'}" :data="videoList" :scroll-y="{enabled: true}">
  55. <vxe-column field="title" title="标题">
  56. <template #default="{ row }">
  57. {{row.title}}[{{row.format_id}}]
  58. </template>
  59. </vxe-column>
  60. <vxe-column field="ext" title="扩展名" width="70"></vxe-column>
  61. <vxe-column field="resolution" title="分辨率" width="100"></vxe-column>
  62. <vxe-column field="fps" title="帧率" width="60"></vxe-column>
  63. <vxe-column field="filesize" title="大小" width="90">
  64. <template #default="{ row }">
  65. <span v-if="row.filesize">{{$utils.handleSize(row.filesize)}}</span>
  66. <span v-else-if="row.filesize_approx">{{$utils.handleSize(row.filesize_approx)}}</span>
  67. </template>
  68. </vxe-column>
  69. <vxe-column field="vcodec" title="视频编码" width="120"></vxe-column>
  70. <vxe-column field="acodec" title="音频编码" width="120"></vxe-column>
  71. <vxe-column field="status" title="状态" width="140">
  72. <template #default="{ row }">
  73. <template v-if="row.status == '1'">
  74. <i class="el-icon-info" style="font-size: 16px; color: #999;"></i>
  75. <span>待操作</span>
  76. </template>
  77. <template v-if="row.status == '2'">
  78. <i class="el-icon-loading" style="font-size: 16px; color: #999;"></i>
  79. <span>下载中.. <span v-if="row.percent">{{row.percent}}%</span></span>
  80. </template>
  81. <template v-if="row.status == '3'">
  82. <i class="el-icon-success" style="font-size: 16px; color: #19be6b;"></i>
  83. <span>下载完成</span>
  84. </template>
  85. <template v-if="row.status == '4'">
  86. <i class="el-icon-error" style="font-size: 16px; color: #ed4014;"></i>
  87. <span>下载出错,请重试!</span>
  88. </template>
  89. </template>
  90. </vxe-column>
  91. <vxe-column title="操作" width="100">
  92. <template #default="{ row, rowIndex }">
  93. <!-- <el-button v-if="row.urlList && row.urlList.length > 0" type="danger" size="mini" plain @click="copy(rowIndex)">复制</el-button> -->
  94. <el-button :loading="row.loading" type="danger" size="mini" plain @click="downloadVideo(rowIndex)">下载</el-button>
  95. </template>
  96. </vxe-column>
  97. </vxe-table>
  98. </div>
  99. </div>
  100. </template>
  101. <!-- B站音视频合并提示框 -->
  102. <el-dialog title="B站音视频合并" :visible.sync="mergeModal" width="420px" :close-on-click-modal="false" :close-on-press-escape="false">
  103. <div class="member-model">
  104. <div class="handle-item">
  105. <label class="handle-label">视频文件:</label>
  106. <el-upload :style="{display: 'inline-block'}" action="/" :before-upload="getVideoPath">
  107. <el-input type="text" readonly size="small" v-model="bVideoInfo.videoPath" style="width:220px;" placeholder="点击选择文件"></el-input>
  108. <i class="el-icon-folder-opened open-folder" style="font-size:22px;"></i>
  109. </el-upload>
  110. </div>
  111. <div class="handle-item" v-if="bVideoInfo.videoDuration">
  112. <label class="handle-label">视频时长:</label>
  113. <span style="color: #999;">{{formatSeconds(bVideoInfo.videoDuration)}}</span>
  114. </div>
  115. <div class="handle-item">
  116. <label class="handle-label">音频文件:</label>
  117. <el-upload :style="{display: 'inline-block'}" action="/" :before-upload="getAudioPath">
  118. <el-input type="text" readonly size="small" v-model="bVideoInfo.audioPath" style="width:220px;" placeholder="点击选择文件"></el-input>
  119. <i class="el-icon-folder-opened open-folder" style="font-size:22px;"></i>
  120. </el-upload>
  121. </div>
  122. <div class="handle-item" v-if="bVideoInfo.audioDuration">
  123. <label class="handle-label">音频时长:</label>
  124. <span style="color: #999;">{{formatSeconds(bVideoInfo.audioDuration)}}</span>
  125. </div>
  126. <div class="handle-item">
  127. <label class="handle-label">文件名称:</label>
  128. <el-input type="text" size="small" placeholder="请输入文字水印内容" style="width:220px;" v-model="bVideoInfo.title"></el-input>
  129. </div>
  130. <div class="handle-item">
  131. <label class="handle-label">文件类型:</label>
  132. <el-select size="small" style="width:220px;" v-model="bVideoInfo.suffix">
  133. <el-option value="mp4" label="mp4"></el-option>
  134. <el-option value="mkv" label="mkv"></el-option>
  135. </el-select>
  136. </div>
  137. <div class="member-btn">
  138. <el-button size="small" type="danger" @click="mergeVideo()" :loading="mergeLoading">开始合并</el-button>
  139. </div>
  140. </div>
  141. </el-dialog>
  142. <!-- 非会员转换提示框 -->
  143. <el-dialog title="非会员提示" :visible.sync="tipsModal" width="400px">
  144. <div class="member-model">
  145. <div class="tips-flex">
  146. <i class="el-icon-s-opportunity"></i>
  147. <p class="m-title">{{tipsDesc}}</p>
  148. </div>
  149. <div class="member-btn">
  150. <el-button size="small" round type="primary" @click="openVip()">开通会员</el-button>
  151. </div>
  152. </div>
  153. </el-dialog>
  154. <!-- 抖音下载 -->
  155. <el-dialog title="视频下载" :visible.sync="dyModal" width="300px" :close-on-click-modal="false" :close-on-press-escape="false">
  156. <el-divider content-position="left" v-if="commonUrl">
  157. 浏览器下载
  158. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="点击复制地址到浏览器中打开">
  159. <i class="el-icon-info" slot="reference" style="margin-left: 10px; color: #F56C6C;"></i>
  160. </el-popover>
  161. </el-divider>
  162. <div style="text-align: center;">
  163. <el-button v-if="commonUrl" size="small" round type="primary" @click="copyText(commonUrl)">复制下载地址</el-button>
  164. </div>
  165. <el-divider content-position="left">
  166. 迅雷下载
  167. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="点击复制下载地址到迅雷中下载">
  168. <i class="el-icon-info" slot="reference" style="margin-left: 10px; color: #F56C6C;"></i>
  169. </el-popover>
  170. </el-divider>
  171. <div style="text-align: center;">
  172. <div v-for="(item, index) in thunderUrl" :key="index">
  173. <el-button size="small" round type="primary" @click="copyText(item)" style="margin-bottom: 15px;">复制下载地址{{index+1}}</el-button>
  174. </br>
  175. </div>
  176. </div>
  177. <el-divider content-position="left">
  178. 普通下载
  179. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="使用软件下载可能会失败,建议使用浏览器/迅雷下载">
  180. <i class="el-icon-info" slot="reference" style="margin-left: 10px; color: #F56C6C;"></i>
  181. </el-popover>
  182. </el-divider>
  183. <div style="text-align: center;">
  184. <el-button v-if="selectIndex > -1" :loading="videoList[selectIndex].loading" size="small" round type="primary" @click="downloadVideo(selectIndex, true)" style="margin-bottom: 15px;">点击下载</el-button>
  185. </div>
  186. </el-dialog>
  187. </el-main>
  188. <el-footer height="48px">
  189. <!-- 更新 -->
  190. <soft-update ref="updateRef" :showDowload="dowloadModel" :dowloadFinish="finishModel"></soft-update>
  191. </el-footer>
  192. </el-container>
  193. </div>
  194. </template>
  195. <script>
  196. import os from 'os'
  197. import fs from 'fs'
  198. import request from 'request'
  199. import path from 'path';
  200. import xlsx from 'node-xlsx';
  201. import softUpdate from './update.vue';
  202. import softHeader from './header.vue';
  203. import electronApi from '@/utils/electronApi';
  204. import pjson from '/package.json'
  205. // import puppeteer from 'puppeteer'
  206. import puppeteer from 'puppeteer-extra'
  207. const axios = require('axios');
  208. const StealthPlugin = require('puppeteer-extra-plugin-stealth');
  209. const listNameArr = ['douyin','kuaishou','weibo','','','','','', '' ,'common'];
  210. import { VxeTooltip } from 'vxe-table';
  211. let separator = '';
  212. if (os.platform == 'linux') {
  213. separator = '/'
  214. } else {
  215. separator = '\\'
  216. }
  217. export default {
  218. name: 'landing-page',
  219. components: {
  220. softUpdate,
  221. softHeader
  222. },
  223. data() {
  224. return {
  225. selectIndex: -1,
  226. usageTimes: 3,
  227. tipsModal: false,
  228. tipsDesc: "暂无下载权限,请开通VIP会员使用",
  229. mergeModal: false,
  230. bVideoInfo: {
  231. videoPath: '',
  232. audioPath: '',
  233. title: '',
  234. suffix: 'mp4',
  235. videoDuration: '',
  236. audioDuration: '',
  237. },
  238. formatUrl: '',
  239. downloadUrl: '', //格式化之后的dtp下载地址
  240. tabLoading: false,
  241. videoInfo: {},
  242. videoList: [],
  243. productName: pjson.softInfo.softName,
  244. imgUrl: this.$api.imgUrl,
  245. imgSrc: '',
  246. fileList: [],
  247. downloadDir: os.userInfo().homedir + separator + "Downloads",
  248. dowloadModel: false,
  249. finishModel: false,
  250. loading: false,
  251. /** 浏览器名称 **/
  252. videoBrowser: null,
  253. loginBrowser: null, // 登录用的浏览器实例
  254. parseLoading: false,
  255. mergeLoading: false,
  256. bUrl: true,
  257. dyModal: false,
  258. dyIndex: 0,
  259. thunderUrl: [],
  260. commonUrl: '',
  261. };
  262. },
  263. async mounted() {
  264. this.$refs.updateRef.updateSoft(true);
  265. let homedir = os.userInfo().homedir;
  266. if (fs.existsSync(homedir + separator + "Desktop")) {
  267. this.downloadDir = homedir + separator + "Desktop"
  268. } else {
  269. this.downloadDir = homedir + separator + "Downloads"
  270. }
  271. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  272. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  273. }
  274. // 打开浏览器
  275. const {
  276. shell
  277. } = require('electron');
  278. const links = document.querySelectorAll('a[href]');
  279. links.forEach(link => {
  280. link.addEventListener('click', e => {
  281. const url = link.getAttribute('href');
  282. e.preventDefault();
  283. shell.openExternal(url);
  284. });
  285. });
  286. // 初始化开发者设置
  287. this.$utils.setStorage('headless', 1);
  288. this.$utils.setStorage('waitUntil', 'networkidle2');
  289. },
  290. methods: {
  291. // 实时获取开发者设置
  292. initDevelop(){
  293. let develop = {
  294. headless: true,
  295. waitUntil: 'networkidle2'
  296. };
  297. let n1 = this.$utils.getStorage('headless');
  298. let n2 = this.$utils.getStorage('waitUntil');
  299. if(n1){
  300. if(n1 == 1){
  301. develop.headless = true;
  302. }else if(n1 == 2){
  303. develop.headless = false;
  304. }
  305. }
  306. if(n2){
  307. develop.waitUntil = n2;
  308. }
  309. return develop;
  310. },
  311. // 实时获取浏览器路径
  312. initPath(){
  313. let chromePath = puppeteer.executablePath().replace('win32-1', 'win64-1');
  314. return chromePath;
  315. },
  316. checkAuthority(){
  317. let authority = this.$refs.headerRef.authority;
  318. this.$refs.imgRef.authority = authority;
  319. },
  320. // 选择目录
  321. pickPath() {
  322. this.$refs['upload-input'].blur();
  323. electronApi.call('pickDir', []).then((path) => {
  324. if (path) {
  325. this.downloadDir = path;
  326. }
  327. });
  328. },
  329. // 打开自定义下载目录
  330. openFolder() {
  331. let path = this.downloadDir;
  332. if (fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName)) {
  333. path = this.downloadDir + separator + pjson.softInfo.softName;
  334. } else {
  335. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName);
  336. path = this.downloadDir + separator + pjson.softInfo.softName;
  337. }
  338. electronApi.call('showItemInfolder', [path + '\\tty.tty'])
  339. },
  340. openVip() {
  341. this.tipsModal = false;
  342. this.$refs.headerRef.openVip();
  343. },
  344. updateSoft() {
  345. this.$refs.updateRef.updateSoft();
  346. },
  347. // 清除缓存的后续操作
  348. async clearCache(){
  349. if(this.loginBrowser){
  350. await this.loginBrowser.close();
  351. this.loginBrowser = null;
  352. }
  353. if(this.videoBrowser){
  354. await this.videoBrowser.close();
  355. this.videoBrowser = null;
  356. }
  357. },
  358. // 去登录
  359. async loginUrl(url){
  360. if(this.loginBrowser){
  361. await this.loginBrowser.close();
  362. this.loginBrowser = null;
  363. }
  364. if(this.videoBrowser){
  365. await this.videoBrowser.close();
  366. this.videoBrowser = null;
  367. }
  368. (async () => {
  369. try{
  370. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  371. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  372. }
  373. let userDataDir = os.tmpdir() + separator + 'chrome-data-capture-video';
  374. puppeteer.use(StealthPlugin());
  375. this.loginBrowser = await puppeteer.launch({
  376. headless: false,
  377. executablePath: this.initPath(),
  378. args: ['--window-size=1280,800'],
  379. userDataDir: userDataDir,
  380. });
  381. const page = await this.loginBrowser.newPage();
  382. await page.setViewport({ width: 1280, height: 800 });
  383. await page.evaluateOnNewDocument(() => {
  384. const newProto = navigator.__proto__;
  385. delete newProto.webdriver;
  386. navigator.__proto__ = newProto;
  387. });
  388. await page.goto(url, {waitUntil : 'networkidle2'});
  389. }catch(e){
  390. this.showError(e);
  391. }
  392. })();
  393. },
  394. getVideoPath(e){
  395. if (e) {
  396. this.bVideoInfo.videoPath = e.path;
  397. electronApi.spawnExec(['ffprobe.exe','-i', e.path,'-v','quiet','-print_format','json','-show_format', '-show_streams', '-hide_banner']).then((result) => {
  398. result = result.stdout.toString();
  399. result = JSON.parse(result);
  400. this.bVideoInfo.videoDuration = result.format.duration;
  401. });
  402. }
  403. },
  404. getAudioPath(e){
  405. if (e) {
  406. this.bVideoInfo.audioPath = e.path;
  407. electronApi.spawnExec(['ffprobe.exe','-i', e.path,'-v','quiet','-print_format','json','-show_format', '-show_streams', '-hide_banner']).then((result) => {
  408. result = result.stdout.toString();
  409. result = JSON.parse(result);
  410. this.bVideoInfo.audioDuration = result.format.duration;
  411. });
  412. }
  413. },
  414. mergeClick(){
  415. this.mergeModal = true;
  416. this.bVideoInfo.title = this.videoList[0].title;
  417. },
  418. // b站视频合并
  419. mergeVideo(){
  420. let newPath = this.downloadDir + separator + pjson.softInfo.softName;
  421. let params = [
  422. 'ffmpeg.exe',
  423. '-i',
  424. this.bVideoInfo.videoPath,
  425. '-i',
  426. this.bVideoInfo.audioPath,
  427. '-c',
  428. 'copy',
  429. newPath + separator + this.bVideoInfo.title + '.' + this.bVideoInfo.suffix,
  430. '-hide_banner',
  431. '-y'
  432. ];
  433. const regexTime = /time=(.*?) bitrate/;
  434. this.mergeLoading = true;
  435. electronApi.spawnExec(params,{
  436. stderr:(data) =>{
  437. // let timeStr = regexTime.exec(data.toString());
  438. // if(timeStr && timeStr[1]){
  439. // let percent = Math.ceil((this.getSs(timeStr[1])/this.bVideoInfo.videoDuration).toFixed(2) * 100);
  440. // }
  441. }
  442. }).then(res => {
  443. this.mergeLoading = false;
  444. this.$message({message: '恭喜你,合并已完成!', type: 'success'});
  445. electronApi.call('showItemInfolder',[this.downloadDir + separator + pjson.softInfo.softName +'\\tty.tty']);
  446. }).catch(err =>{
  447. this.mergeLoading = false;
  448. this.$notify.error({
  449. title: '合并失败',
  450. message: '视频参数有误,请重试! ',
  451. });
  452. })
  453. },
  454. // 开始解析
  455. async startParsing(){
  456. if(this.formatUrl.trim()){
  457. let formatUrl = this.formatUrl.trim();
  458. if(formatUrl.indexOf('https://') == -1){
  459. this.$message.error('错了哦,请输入正确的视频地址(https://开头)');
  460. return false;
  461. }
  462. let arr = formatUrl.split('https://');
  463. formatUrl = 'https://' + arr[1];
  464. this.downloadUrl = formatUrl;
  465. const regex = /https:\/\/.*?.douyin.com/;
  466. const res = regex.exec(formatUrl);
  467. if(res && res.length > 0){ //抖音视频解析,使用puputter
  468. this.videoList = [];
  469. let reg2 = /[?&]modal_id=(\w+)/;
  470. let res2 = reg2.exec(formatUrl);
  471. if(res2){
  472. let modal_id = res2[1];
  473. formatUrl = res[0] + '/video/'+modal_id;
  474. this.downloadUrl = formatUrl;
  475. }
  476. this.douyinParsing(formatUrl);
  477. return false;
  478. }
  479. this.parseLoading = true;
  480. this.tabLoading = true;
  481. this.videoList = [];
  482. this.selectIndex = -1;
  483. let params = [
  484. '--dump-json',
  485. formatUrl
  486. ];
  487. electronApi.spawnExec(['dlp.exe', ...params]).then(res => {
  488. this.parseLoading = false;
  489. this.tabLoading = false;
  490. let info = res.stdout ? res.stdout.toString() : '{formats: []}';
  491. this.videoInfo = JSON.parse(info);
  492. this.videoList = this.videoInfo.formats || [];
  493. this.videoList.map(item => {
  494. item.title = this.videoInfo.title,
  495. item.status = '1';
  496. })
  497. const bregex = /https:\/\/.*?.bilibili.com/;
  498. const bres = bregex.exec(formatUrl);
  499. if(bres && bres.length > 0){ //b站的视频
  500. this.bUrl = false;
  501. }else{
  502. this.bUrl = true;
  503. }
  504. }).catch(err =>{
  505. this.parseLoading = false;
  506. this.tabLoading = false;
  507. // console.log('err1',err.stderr.toString());
  508. let errStr = err.stderr.toString();
  509. this.bUrl = true;
  510. if(errStr.indexOf('Unsupported URL') > -1){
  511. this.videoParsing(formatUrl);
  512. return false;
  513. }else{
  514. this.$notify.error({
  515. title: '网址解析失败!',
  516. message: errStr
  517. });
  518. }
  519. })
  520. }
  521. },
  522. // 复制链接地址
  523. copyText(text){
  524. navigator.clipboard.writeText(text).then(() => {
  525. this.$message({message: '视频下载链接已成功复制到剪贴板', type: 'success'});
  526. }).catch(err => {
  527. this.$message.error('无法复制文本', err.toString());
  528. });
  529. },
  530. // 点击复制
  531. copy(index){
  532. // let authority = this.$refs.headerRef.authority.isAuthority;
  533. // if (!authority) {
  534. // this.tipsModal = true;
  535. // this.tipsDesc = '暂无下载权限,请开通VIP会员使用';
  536. // return false;
  537. // }
  538. this.commonUrl = '';
  539. this.thunderUrl = [];
  540. let tag = this.videoList[index].tag;
  541. let urlList = this.videoList[index].urlList;
  542. let title = this.videoList[index].title;
  543. let text = urlList[0];
  544. if(urlList.length <= 0){
  545. this.$message.error('无法复制下载地址');
  546. return false;
  547. }
  548. if(tag == 'douyin'){
  549. this.dyModal = true;
  550. this.dyIndex = index;
  551. urlList.map(uitem => {
  552. if(uitem.startsWith('https://www.douyin.com/')){
  553. this.commonUrl = uitem;
  554. }
  555. if(!title){
  556. title = '抖音视频';
  557. }
  558. // console.log(`AA`+uitem +`&filename=`+title+`.mp4ZZ`);
  559. let urlBuffer = Buffer.from(`AA`+uitem +`&filename=`+title+`.mp4ZZ`).toString('base64');
  560. this.thunderUrl.push('thunder://'+urlBuffer);
  561. })
  562. }else{
  563. this.copyText(text);
  564. }
  565. },
  566. // 点击下载视频
  567. async downloadVideo(index, flag){
  568. this.selectIndex = index;
  569. let authority = this.$refs.headerRef.authority.isAuthority;
  570. if (!authority) {
  571. this.tipsModal = true;
  572. this.tipsDesc = '暂无下载权限,请开通VIP会员使用';
  573. return false;
  574. }
  575. this.tipsModal = false;
  576. if(this.videoList[index].urlList && this.videoList[index].urlList.length > 0 && !flag && this.videoList[index].tag == 'douyin'){ // 浏览器解析出来的下载地址
  577. this.copy(index);
  578. }else{
  579. let item = this.videoList[index];
  580. if (!fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName)) {
  581. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName);
  582. }
  583. let newPath = this.downloadDir + separator + pjson.softInfo.softName;
  584. if(item.tag){ //抖音下载
  585. if(item.title){
  586. item.title = item.title.substring(0, 50);
  587. if(this.containsAnyChar(item.title)){ //判断是否含有特殊字符
  588. item.title = item.title.replace(/[\\/:*?"<>|#%&\s]/g, "");
  589. }
  590. }
  591. let outputPath = newPath + separator + item.title + '[' + item.format_id + ']' + '.mp4';
  592. let url = item.urlList.length > 0 ? item.urlList[0] : '';
  593. if(item.tag == 'douyin'){
  594. item.urlList.map(uitem => {
  595. if(uitem.startsWith('https://www.douyin.com/')){
  596. url = uitem;
  597. }
  598. })
  599. }
  600. item.status = '2';
  601. item.loading = true;
  602. await this.downloadImage(url, outputPath, item);
  603. item.loading = false;
  604. this.$forceUpdate();
  605. this.$message({message: '恭喜你,下载已完成!', type: 'success'});
  606. electronApi.call('showItemInfolder',[this.downloadDir + separator + pjson.softInfo.softName +'\\tty.tty']);
  607. return false;
  608. }else{
  609. // console.log(this.downloadUrl);
  610. let params = [
  611. '-f',
  612. item.format_id,
  613. '--force-overwrites',
  614. '-o',
  615. newPath + separator + '%(title).50s['+item.format_id+'].%(ext)s',
  616. this.downloadUrl
  617. ];
  618. item.status = '2';
  619. item.loading = true;
  620. this.$forceUpdate();
  621. electronApi.spawnExec(['dlp.exe', ...params],{
  622. stdout:(data) =>{
  623. let str = data.toString();
  624. const regexDuration = /[download].*? (.*?)%/;
  625. const res = regexDuration.exec(str);
  626. if(res && res[1]){
  627. item.percent = res[1];
  628. this.$forceUpdate();
  629. }
  630. }
  631. }).then(res => {
  632. let outData = res.stdout ? res.stdout.toString() : '{}';
  633. item.status = '3';
  634. item.loading = false;
  635. this.$forceUpdate();
  636. this.$message({message: '恭喜你,下载已完成!', type: 'success'});
  637. electronApi.call('showItemInfolder',[this.downloadDir + separator + pjson.softInfo.softName +'\\tty.tty']);
  638. }).catch(err =>{
  639. item.status = '4';
  640. item.loading = false;
  641. this.$forceUpdate();
  642. // console.log(err);
  643. })
  644. }
  645. }
  646. },
  647. // 解析抖音视频
  648. async douyinParsing(url){
  649. if(this.loginBrowser){
  650. await this.loginBrowser.close();
  651. this.loginBrowser = null;
  652. }
  653. if(this.videoBrowser){
  654. await this.videoBrowser.close();
  655. this.videoBrowser = null;
  656. }
  657. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  658. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  659. }
  660. this.parseLoading = true;
  661. this.tabLoading = true;
  662. setTimeout(()=> {
  663. this.parseLoading = false;
  664. this.tabLoading = false;
  665. }, 20000)
  666. let userDataDir = os.tmpdir() + separator + 'chrome-data-capture-video';
  667. // 运行不同平台的浏览器
  668. puppeteer.use(StealthPlugin());
  669. let headless = true;
  670. headless = this.initDevelop().headless;
  671. this.videoBrowser = await puppeteer.launch({
  672. headless: headless,
  673. executablePath: this.initPath(),
  674. userDataDir: userDataDir,
  675. args: [
  676. '--start-maximized',
  677. '--no-sandbox',
  678. '--disable-setuid-sandbox',
  679. '--disable-blink-features=AutomationControlled',
  680. ]
  681. });
  682. await new Promise((resolve,reject) =>{
  683. (async () => {
  684. try{
  685. let authority = this.$refs.headerRef.authority.isAuthority;
  686. const page = await this.videoBrowser.newPage();
  687. let responseVideo = [];
  688. let responseUrl = [];
  689. let responseObj = {};
  690. let vtitle = '视频';
  691. page.on('response', async(response) => {
  692. // 检查响应的 MIME 类型是否以 'video/' 开头
  693. if (response.headers()['content-type'] && response.headers()['content-type'].startsWith('video/')) {
  694. if(responseVideo.indexOf(response.url()) < 0 && !response.url().startsWith('blob:https://')){
  695. responseVideo.push(response.url());
  696. vtitle = await page.title();
  697. if(vtitle){
  698. vtitle = vtitle.substring(0, 50);
  699. if(this.containsAnyChar(vtitle)){ //判断是否含有特殊字符
  700. vtitle = vtitle.replace(/[\\/:*?"<>|#%&\s]/g, "");
  701. }
  702. }
  703. }
  704. }
  705. if (response.headers()['content-type'] && response.headers()['content-type'].startsWith('application/json')) {
  706. if(response.url().indexOf('/aweme/detail/') > -1){
  707. if(responseUrl.indexOf(response.url()) < 0){
  708. responseUrl.push(response.url());
  709. }else{
  710. return false;
  711. }
  712. let jsonText = await response.text();
  713. if(jsonText && typeof jsonText == 'string'){
  714. responseObj = JSON.parse(jsonText);
  715. }
  716. }
  717. }
  718. });
  719. let waitUntil = 'networkidle2';
  720. waitUntil = this.initDevelop().waitUntil;
  721. await page.goto(url, {waitUntil : waitUntil});
  722. await page.waitForTimeout(1000);
  723. await this.videoBrowser.close();
  724. if(responseObj['aweme_detail']){ // 返回的接口数据中有aweme_detail参数 才会解析接口数据
  725. let arr = ['aweme_detail']; //'video' ,'play_addr', 'url_list', '2'];
  726. for(let i = 0; i < arr.length; i++){
  727. responseObj = responseObj[arr[i]];
  728. }
  729. if(responseObj && responseObj.preview_title){
  730. responseObj.preview_title = responseObj.preview_title.substring(0, 50);
  731. if(this.containsAnyChar(responseObj.preview_title)){ //判断是否含有特殊字符
  732. responseObj.preview_title = responseObj.preview_title.replace(/[\\/:*?"<>|#%&\s]/g, "");
  733. }
  734. }
  735. if(responseObj && responseObj.video && responseObj.video.play_addr){
  736. let vinfo = {
  737. title: responseObj.preview_title,
  738. tag: 'douyin',
  739. format_id: 'default',
  740. ext: 'mp4',
  741. resolution: responseObj.video.play_addr.width + 'x' + responseObj.video.play_addr.height,
  742. fps: '-',
  743. filesize: responseObj.video.play_addr.data_size,
  744. vcodec: '-',
  745. acodec: '-',
  746. urlList: responseObj.video.play_addr.url_list,
  747. status: '1',
  748. loading: false
  749. }
  750. this.videoList.push(vinfo);
  751. }
  752. if(responseObj && responseObj.video && responseObj.video.play_addr_265){
  753. let vinfo = {
  754. title: responseObj.preview_title,
  755. tag: 'douyin',
  756. format_id: 'play_addr_265',
  757. ext: 'mp4',
  758. resolution: responseObj.video.play_addr_265.width + 'x' + responseObj.video.play_addr_265.height,
  759. fps: '-',
  760. filesize: responseObj.video.play_addr_265.data_size,
  761. vcodec: '-',
  762. acodec: '-',
  763. urlList: responseObj.video.play_addr_265.url_list,
  764. status: '1',
  765. loading: false
  766. }
  767. this.videoList.push(vinfo);
  768. }
  769. if(responseObj && responseObj.video && responseObj.video.play_addr_h264){
  770. let vinfo = {
  771. title: responseObj.preview_title,
  772. tag: 'douyin',
  773. format_id: 'play_addr_h264',
  774. ext: 'mp4',
  775. resolution: responseObj.video.play_addr_h264.width + 'x' + responseObj.video.play_addr_h264.height,
  776. fps: '-',
  777. filesize: responseObj.video.play_addr_h264.data_size,
  778. vcodec: '-',
  779. acodec: '-',
  780. urlList: responseObj.video.play_addr_h264.url_list,
  781. status: '1',
  782. loading: false
  783. }
  784. this.videoList.push(vinfo);
  785. }
  786. }else if(responseVideo.length > 0){ //网页解析到视频地址
  787. let vinfo = {
  788. title: vtitle,
  789. tag: 'douyin',
  790. format_id: 'video',
  791. ext: 'mp4',
  792. resolution: '-',
  793. fps: '-',
  794. filesize: '',
  795. vcodec: '-',
  796. acodec: '-',
  797. urlList: responseVideo,
  798. status: '1',
  799. loading: false
  800. }
  801. this.videoList.push(vinfo);
  802. }
  803. this.parseLoading = false;
  804. this.tabLoading = false;
  805. }catch(e){
  806. this.parseLoading = false;
  807. this.tabLoading = false;
  808. reject(e);
  809. this.showError(e);
  810. }
  811. })();
  812. });
  813. },
  814. // 解析视频-快手-其他
  815. async videoParsing(url){
  816. if(this.loginBrowser){
  817. await this.loginBrowser.close();
  818. this.loginBrowser = null;
  819. }
  820. if(this.videoBrowser){
  821. await this.videoBrowser.close();
  822. this.videoBrowser = null;
  823. }
  824. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  825. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  826. }
  827. this.parseLoading = true;
  828. this.tabLoading = true;
  829. this.videoList = [];
  830. setTimeout(()=> {
  831. this.parseLoading = false;
  832. this.tabLoading = false;
  833. }, 30000)
  834. let userDataDir = os.tmpdir() + separator + 'chrome-data-capture-video';
  835. // 运行不同平台的浏览器
  836. puppeteer.use(StealthPlugin());
  837. let headless = true;
  838. headless = this.initDevelop().headless;
  839. this.videoBrowser = await puppeteer.launch({
  840. headless: headless,
  841. executablePath: this.initPath(),
  842. userDataDir: userDataDir,
  843. args: [
  844. '--start-maximized',
  845. '--no-sandbox',
  846. '--disable-setuid-sandbox',
  847. '--disable-blink-features=AutomationControlled',
  848. ]
  849. });
  850. await new Promise((resolve,reject) =>{
  851. (async () => {
  852. try{
  853. let authority = this.$refs.headerRef.authority.isAuthority;
  854. const page = await this.videoBrowser.newPage();
  855. let responseVideo = [];
  856. page.on('response', async(response) => {
  857. // 检查响应的 MIME 类型是否以 'video/' 开头
  858. if (response.headers()['content-type'] && response.headers()['content-type'].startsWith('video/')) {
  859. if(responseVideo.indexOf(response.url()) < 0 && !response.url().startsWith('blob:https://')){
  860. let title = await page.title();
  861. if(title){
  862. title = title.substring(0, 50);
  863. if(this.containsAnyChar(title)){ //判断是否含有特殊字符
  864. title = title.replace(/[\\/:*?"<>|#%&\s]/g, "");
  865. }
  866. }
  867. let vinfo = {
  868. title: title,
  869. tag: 'other',
  870. format_id: 'default',
  871. ext: 'mp4',
  872. resolution: '-',
  873. fps: '-',
  874. filesize: response.headers()['content-length'],
  875. vcodec: '-',
  876. acodec: '-',
  877. urlList: [response.url()],
  878. status: '1',
  879. loading: false
  880. }
  881. responseVideo.push(response.url());
  882. this.videoList.push(vinfo);
  883. }
  884. }
  885. });
  886. let waitUntil = 'networkidle2';
  887. waitUntil = this.initDevelop().waitUntil;
  888. await page.goto(url, {waitUntil : waitUntil});
  889. await page.waitForTimeout(1000);
  890. let pageInfo = await page.evaluate(() => {
  891. let cHeight = document.documentElement.clientHeight;
  892. let scrollHeight = document.body.scrollHeight;
  893. return {'scrollHeight': scrollHeight, 'cHeight': cHeight}
  894. });
  895. let scrollHeight = pageInfo.scrollHeight;
  896. let cHeight = pageInfo.cHeight;
  897. let num = Math.ceil(scrollHeight / cHeight);
  898. let start = -1;
  899. let scrollInt = setInterval(async() => {
  900. start ++;
  901. if(start > num || start > 20){ // 防止页面过长,滚动50次自动停止
  902. clearInterval(scrollInt);
  903. const videoArr = await page.evaluate(() => {
  904. let video = [];
  905. // 视频
  906. let arr5 = document.querySelectorAll('video');
  907. if(arr5.length == 0){
  908. arr5 = document.querySelectorAll('video source');
  909. }
  910. for(let i=0; i< arr5.length; i++){
  911. if(video.indexOf(arr5[i].src) < 0 && !arr5[i].src.startsWith('blob:https://')){
  912. if(arr5[i].src){
  913. video.push(arr5[i].src);
  914. }
  915. }
  916. }
  917. return video;
  918. });
  919. if(videoArr.length > 0){
  920. let title = await page.title();
  921. if(title){
  922. title = title.substring(0, 50);
  923. if(this.containsAnyChar(title)){ //判断是否含有特殊字符
  924. title = title.replace(/[\\/:*?"<>|#%&\s]/g, "");
  925. }
  926. }
  927. videoArr.map((item, index) => {
  928. let vinfo = {
  929. title: title,
  930. tag: 'other',
  931. format_id: index,
  932. ext: 'mp4',
  933. resolution: '-',
  934. fps: '-',
  935. filesize: '-',
  936. vcodec: '-',
  937. acodec: '-',
  938. urlList: [item],
  939. status: '1',
  940. loading: false
  941. }
  942. let skipFlag = true;
  943. for(let j = 0; j < this.videoList.length; j++){
  944. if(this.videoList[j].urlList[0] == item){
  945. skipFlag = false;
  946. }
  947. }
  948. if(skipFlag){
  949. this.videoList.push(vinfo);
  950. }
  951. });
  952. }
  953. await this.videoBrowser.close();
  954. this.parseLoading = false;
  955. this.tabLoading = false;
  956. }
  957. }, 600);
  958. this.parseLoading = false;
  959. this.tabLoading = false;
  960. }catch(e){
  961. this.parseLoading = false;
  962. this.tabLoading = false;
  963. reject(e);
  964. this.showError(e);
  965. }
  966. })();
  967. });
  968. },
  969. // 下载网址链接的图片
  970. async downloadImage(imageUrl, outputPath, urlInfo) {
  971. let _this = this;
  972. let received_bytes = 0;
  973. let total_bytes = 0;
  974. try {
  975. let req = request({
  976. method: 'GET', uri: imageUrl, strictSSL: false
  977. });
  978. let out = fs.createWriteStream(outputPath);
  979. req.pipe(out);
  980. return new Promise((resolve, reject) => {
  981. req.on('response', (data) => {
  982. total_bytes = parseInt(data.headers['content-length']);
  983. const status = data.statusCode;
  984. if(this.menuIndex != '1'){
  985. if (status < 200 || status >= 300) {
  986. this.$notify.error({
  987. title: '网络资源访问异常!- 1',
  988. message: imageUrl.slice(0,50)
  989. });
  990. }else if(isNaN(total_bytes)){
  991. this.$notify.error({
  992. title: '网络资源访问异常!- 2',
  993. message: imageUrl.slice(0,50)
  994. });
  995. }else{
  996. // console.log('下载中...')
  997. }
  998. }
  999. if(status == 403 || isNaN(total_bytes)){
  1000. fs.unlinkSync(outputPath);
  1001. urlInfo.status = '4';
  1002. }
  1003. });
  1004. req.on('data', (chunk) => {
  1005. received_bytes += chunk.length;
  1006. if(urlInfo.filesize){
  1007. let size = Number(urlInfo.filesize);
  1008. let percent = Number(received_bytes / size * 100);
  1009. urlInfo.percent = percent.toFixed(2);
  1010. if(percent > 100){
  1011. urlInfo.percent = 100;
  1012. }
  1013. this.$forceUpdate();
  1014. }
  1015. });
  1016. req.on('end', ()=> {
  1017. urlInfo.status = '3';
  1018. //console.log('下载完成', outputPath)
  1019. resolve(true);
  1020. });
  1021. });
  1022. } catch (error) {
  1023. urlInfo.status = '4';
  1024. console.error(imageUrl, `Failed to download image: ${error.message}`);
  1025. throw error;
  1026. }
  1027. },
  1028. // 获取页面标题 - 生成对应的文件夹
  1029. async getTitle(page, urlInfo){
  1030. // 已页面标题作为新建文件夹,保留前50个字
  1031. let title = await page.title();
  1032. if(title){
  1033. title = title.substring(0, 50);
  1034. if(this.containsAnyChar(title)){ //判断是否含有特殊字符
  1035. title = title.replace(/[\\/:*?"<>|#%&\s]/g, "");
  1036. }
  1037. if (fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName + separator + title)) {
  1038. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + title;
  1039. } else {
  1040. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName + separator + title);
  1041. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + title;
  1042. }
  1043. }
  1044. },
  1045. // 下载base64位的图片
  1046. async downloadBaseImage(base64String, outputPath, urlInfo) {
  1047. const buffer = Buffer.from(base64String, 'base64');
  1048. const writeStream = fs.createWriteStream(outputPath);
  1049. writeStream.write(buffer);
  1050. writeStream.end();
  1051. writeStream.on('finish', () => {
  1052. urlInfo.num += 1;
  1053. });
  1054. // 监听错误事件
  1055. writeStream.on('error', (err) => {
  1056. console.error('base64位写入文件时出错:', err);
  1057. });
  1058. },
  1059. // 错误提示
  1060. showError(e){
  1061. let str = '';
  1062. if(e.toString().indexOf('ERR_NAME_NOT_RESOLVE') > -1){
  1063. str = '无法解析该网址,请查看网址是否正确!-1';
  1064. }else if(e.toString().indexOf('Cannot navigate to invalid URL') > -1){
  1065. str = '无效的网址,请查看网址格式是否正确!-2';
  1066. }else if(e.toString().indexOf('ERR_CONNECTION_TIMED_OUT') > -1){
  1067. str = '链接请求超时,请查看网络状态!-3';
  1068. }else if(e.toString().indexOf('TimeoutError') > -1){
  1069. str = '链接请求超时,请查看网络状态!-4';
  1070. }else if(e.toString().indexOf('operation not permitted') > -1){
  1071. str = '权限受限,请以管理员权限运行软件!-5';
  1072. }else if(e.toString().indexOf('browser has disconnected') > -1){
  1073. str = '内置浏览器已关闭!-6';
  1074. }else if(e.toString().indexOf('Failed to launch the browser') > -1){
  1075. str = '请先关闭内置浏览器!-7';
  1076. }else{
  1077. str = e.toString();
  1078. //console.log(e);
  1079. }
  1080. this.loading = false;
  1081. this.$notify.error({
  1082. title: '提示',
  1083. message: str
  1084. });
  1085. },
  1086. // 是否包含特殊字符
  1087. containsAnyChar(str) {
  1088. let charsArray = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '#', '%' ,'&', ' ', '\t', '\n'];
  1089. for (let i = 0; i < charsArray.length; i++) {
  1090. if (str.includes(charsArray[i])) {
  1091. return true;
  1092. }
  1093. }
  1094. return false;
  1095. },
  1096. // 随机生成字符
  1097. randomString(length) {
  1098. let result = '';
  1099. const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  1100. const charactersLength = characters.length;
  1101. for (let i = 0; i < length; i++ ) {
  1102. result += characters.charAt(Math.floor(Math.random() * charactersLength));
  1103. }
  1104. return result;
  1105. },
  1106. //
  1107. formatSeconds(value) {
  1108. var secondTime = parseInt(value); // 秒
  1109. var minuteTime = 0; // 分
  1110. var hourTime = 0; // 小时
  1111. if (secondTime >= 60) {
  1112. minuteTime = parseInt(secondTime / 60);
  1113. secondTime = parseInt(secondTime % 60);
  1114. if (minuteTime >= 60) {
  1115. hourTime = parseInt(minuteTime / 60);
  1116. minuteTime = parseInt(minuteTime % 60);
  1117. }
  1118. }
  1119. var result ="" +(parseInt(secondTime) < 10? "0" + parseInt(secondTime): parseInt(secondTime));
  1120. result ="" + (parseInt(minuteTime) < 10? "0" + parseInt(minuteTime) : parseInt(minuteTime)) + ":" + result;
  1121. result ="" + (parseInt(hourTime) < 10 ? "0" + parseInt(hourTime): parseInt(hourTime)) +":" + result;
  1122. return result;
  1123. },
  1124. // 视频时间转为秒
  1125. getSs(rawDuration){
  1126. const $ar = rawDuration.split(":");
  1127. let $duration = parseFloat($ar[2]);
  1128. if ($ar[1]) $duration += parseInt($ar[1]) * 60;
  1129. if ($ar[0]) $duration += parseInt($ar[0]) * 60 * 60;
  1130. return $duration;
  1131. },
  1132. }
  1133. };
  1134. </script>
  1135. <style lang="scss">
  1136. @import "../assets/css/fontx/iconfont.css";
  1137. @import "../assets/css/home.scss";
  1138. .ivu-input-number-controls-outside-btn i {
  1139. font-weight: 800;
  1140. }
  1141. .update-point {
  1142. display: inline-block;
  1143. width: 8px;
  1144. height: 8px;
  1145. border-radius: 8px;
  1146. background: #ff0000;
  1147. top: 14px;
  1148. position: absolute;
  1149. left: -13px;
  1150. }
  1151. .menu-item {
  1152. padding: 8px 0;
  1153. font-size: 14px;
  1154. .iconfont {
  1155. font-size: 32px;
  1156. }
  1157. &:hover,
  1158. &.active {
  1159. color: #ed4014;
  1160. }
  1161. }
  1162. .ivu-progress-show-info .ivu-progress-outer {
  1163. padding-right: 40px !important;
  1164. margin-right: -40px !important;
  1165. }
  1166. .tips {
  1167. text-align: center;
  1168. padding: 10px 0;
  1169. color: #ed4014;
  1170. font-size: 12px;
  1171. }
  1172. .handle-desc {
  1173. display: inline-block;
  1174. width: calc(100% - 100px);
  1175. overflow: hidden;
  1176. }
  1177. .ivu-menu-submenu-title {
  1178. font-weight: 600;
  1179. }
  1180. .ivu-menu .ivu-menu-item {
  1181. line-height: 1;
  1182. }
  1183. // new-el
  1184. .el-menu {
  1185. border-right: none !important;
  1186. }
  1187. .cmenu-item {
  1188. padding: 0 20px 20px;
  1189. margin-bottom: 15px;
  1190. .cmenu-title {
  1191. font-size: 18px;
  1192. font-weight: 600;
  1193. padding-bottom: 20px;
  1194. }
  1195. .citem-nav {
  1196. text-align: center;
  1197. border-radius: 10px;
  1198. min-height: 110px;
  1199. padding: 15px 0;
  1200. background-color: #fff;
  1201. font-size: 15px;
  1202. cursor: pointer;
  1203. &.bg-linear1 {
  1204. color: #fff;
  1205. font-size: 20px;
  1206. background: linear-gradient(to right bottom, #2A56CA, #5795F4);
  1207. }
  1208. &.bg-linear2 {
  1209. color: #fff;
  1210. font-size: 20px;
  1211. background: linear-gradient(to right top, #147FBB, #5EB3E3);
  1212. }
  1213. &.bg-linear3 {
  1214. color: #fff;
  1215. font-size: 20px;
  1216. background: linear-gradient(to right bottom, #2F9E8A, #56CDB1);
  1217. }
  1218. &:hover {
  1219. margin-top: -5px;
  1220. box-shadow: 3px 3px 6px #ccc, -3px -3px 6px #ccc;
  1221. }
  1222. .citem-img {
  1223. width: 50px;
  1224. margin-bottom: 10px;
  1225. }
  1226. }
  1227. }
  1228. .popper-open{
  1229. text-align: center !important;
  1230. padding: 10px !important;
  1231. background: #303133 !important;
  1232. color: #fff !important;
  1233. min-width: 120px !important;
  1234. opacity: 0.8;
  1235. }
  1236. .popper-open[x-placement^=bottom] .popper__arrow::after{
  1237. border-bottom-color: #303133 !important;
  1238. }
  1239. textarea.el-textarea__inner{
  1240. height: 100% !important;
  1241. font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
  1242. }
  1243. .set-item{
  1244. margin-right: 15px;
  1245. .set-title{
  1246. font-size: 13px;
  1247. }
  1248. }
  1249. .outarea{
  1250. height: 100%;
  1251. border: 1px solid #DCDFE6;
  1252. background-color: #4851a415;
  1253. padding: 15px;
  1254. font-size: 20px;
  1255. overflow: hidden auto;
  1256. }
  1257. .outtext{
  1258. height: 100%;
  1259. font-size: 20px;
  1260. overflow: hidden auto;
  1261. textarea{
  1262. background-color: #4851a415;
  1263. }
  1264. }
  1265. .pin-tips{
  1266. font-size: 14px;
  1267. position: absolute;
  1268. bottom: 10px;
  1269. color: #ff0000;
  1270. left: 0;
  1271. right: 0;
  1272. margin: auto;
  1273. text-align: center;
  1274. }
  1275. .outarea.red-border{
  1276. border: 1px solid #F56C6C;
  1277. }
  1278. .outtext.red-border textarea{
  1279. border: 1px solid #F56C6C;
  1280. }
  1281. h3{
  1282. margin: 0;
  1283. }
  1284. .m-image{
  1285. width: 20px;
  1286. margin-right: 5px;
  1287. }
  1288. .dialog-footer-center{
  1289. text-align: center;
  1290. }
  1291. .visible-tips-style{
  1292. font-size: 18px;
  1293. color: #f73131;
  1294. font-weight: 600;
  1295. padding: 30px 40px 0;
  1296. }
  1297. .no-select{
  1298. user-select: none;
  1299. }
  1300. .tips-flex{
  1301. display: flex;
  1302. flex-wrap: nowrap;
  1303. justify-content: space-around;
  1304. align-items: center;
  1305. .el-icon-s-opportunity{
  1306. font-size: 50px;
  1307. color: #f73131;
  1308. margin-right: 10px;
  1309. }
  1310. .m-title{
  1311. flex: 1;
  1312. color: #f73131;
  1313. }
  1314. }
  1315. .c-titles{
  1316. background: -webkit-linear-gradient(left, #FF993F, #FFCE7C 25%, #FF9682 50%, #8e430d 75%, #FF993F);
  1317. color: transparent;
  1318. -webkit-background-clip: text;
  1319. background-size: 200% 100%;
  1320. animation: mask-ani 6s infinite linear;
  1321. font-size: 30px;
  1322. font-weight: 600;
  1323. line-height: 1.5;
  1324. }
  1325. @-webkit-keyframes mask-ani {
  1326. 0% {
  1327. background-position: 0 0;
  1328. }
  1329. 50% {
  1330. background-position: 100% 0;
  1331. }
  1332. 100% {
  1333. background-position: 200% 0;
  1334. }
  1335. }
  1336. .soft-icon{
  1337. margin: 0 3px;
  1338. }
  1339. .el-upload-list{
  1340. display: none;
  1341. }
  1342. </style>