home.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  1. <template>
  2. <div>
  3. <el-container style="height: 100vh;">
  4. <!-- #333744 -->
  5. <el-aside width="180px" style="user-select: none; border-right: 1px solid #fafafa; background-color: #333744; overflow-x: hidden;">
  6. <p class="soft-name" style="-webkit-app-region: drag; color: #fff;">
  7. <img src="../assets/image/icon.png" class="soft-icon" />
  8. <span style="letter-spacing: 1px;">{{productName}}</span>
  9. </p>
  10. <el-menu :default-openeds="['a']" :default-active="menuIndex" @select="setMenuIndex" active-text-color="#409EFF" background-color="#333744" text-color="#fff" style="margin-top: 10px;">
  11. <el-submenu index="a">
  12. <template slot="title"><img src="../assets/image/m-download.png" class="m-image"/>图片下载</template>
  13. <el-menu-item index="1">
  14. <img src="../assets/image/m-douyin.png" class="m-image"/><span slot="title">抖音</span>
  15. </el-menu-item>
  16. <el-menu-item index="2">
  17. <img src="../assets/image/m-kuaishou.png" class="m-image"/><span slot="title">快手</span>
  18. </el-menu-item>
  19. <el-menu-item index="3">
  20. <img src="../assets/image/m-weibo.png" class="m-image"/><span slot="title">微博</span>
  21. </el-menu-item>
  22. <el-menu-item index="10">
  23. <img src="../assets/image/m-chrome.png" class="m-image"/><span slot="title">其他(Beta)</span>
  24. </el-menu-item>
  25. </el-submenu>
  26. </el-menu>
  27. </el-aside>
  28. <el-container>
  29. <el-header height="45px" style="background-color: #fafafa; padding: 0 10px;">
  30. <soft-header ref="headerRef" @update-soft="updateSoft()" @export-file="exportFile" @login-url="loginUrl" @clear-cache="clearCache"></soft-header>
  31. </el-header>
  32. <el-main ref="el-main" style="background-color: #fafafa;">
  33. <template>
  34. <div class="content-top">
  35. <div>
  36. <el-button-group>
  37. <el-button type="primary" size="mini" icon="el-icon-circle-plus-outline"
  38. @click="addVisible = true;">添加链接</el-button>
  39. <el-button type="primary" size="mini" icon="el-icon-upload"
  40. @click="pickLink()">导入链接</el-button>
  41. <el-button type="primary" size="mini" icon="el-icon-delete"
  42. @click="clearList()">清空链接</el-button>
  43. </el-button-group>
  44. <el-link type="info" style="margin-left: 20px; vertical-align:baseline; font-size: 12px;" @click="downloadExample()">导入模板下载<i class="el-icon-download"></i></el-link>
  45. </div>
  46. <el-row type="flex" style="align-items: center;">
  47. <div class="set-item">
  48. <span class="set-title">保存目录:</span>
  49. <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>
  50. <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="打开保存目录">
  51. <i class="el-icon-folder-opened" slot="reference" style="padding-left: 5px; cursor: pointer; font-size: 22px; vertical-align: middle;" @click="openFolder()"></i>
  52. </el-popover>
  53. </div>
  54. <el-button type="danger" @click="exportFile()" :loading="loading">开始下载</el-button>
  55. </el-row>
  56. </div>
  57. <div style="padding: 20px 20px 0 20px; height: calc(100% - 62px);">
  58. <el-row type="flex" justify="space-between">
  59. <div>
  60. <h3 style="display: inline-block;">
  61. <span v-if="menuIndex == '1'">抖音 - </span>
  62. <span v-if="menuIndex == '2'">快手 - </span>
  63. <span v-if="menuIndex == '3'">微博 - </span>
  64. <span v-if="menuIndex == '10'">网页 - </span>
  65. 视频下载
  66. </h3>
  67. <!-- <el-popover placement="bottom" popper-class="popper-open" trigger="hover" content="下载类型至少选一个,评论图默认只下载商品首页展示的评论内容">
  68. <i class="el-icon-info" slot="reference" style="margin-right: 10px; color: #F56C6C;"></i>
  69. </el-popover> -->
  70. <el-link v-if="menuIndex != '10'" :underline="false" type="danger" style="text-align: center; font-size: 12px;">
  71. 不支持win7及以下系统
  72. </el-link>
  73. <el-link v-if="menuIndex == '10'" :underline="false" type="danger" style="text-align: center; font-size: 12px;">
  74. 非会员功能,仅提供测试试用
  75. </el-link>
  76. </div>
  77. </el-row>
  78. <div class="table-scroll" style="padding-top: 30px; height: calc(100% - 31px);">
  79. <!-- 1、 -->
  80. <vxe-table ref="xTable" show-overflow class="img-table" max-height="100%" empty-text="没有更多数据了!" :loading="tabLoading" :row-config="{isHover: true}"
  81. :loading-config="{icon: 'vxe-icon-indicator roll', text: '列表加载中...'}" :data="this[listStr+'List']" :scroll-y="{enabled: true}">
  82. <vxe-column type="seq" width="60"></vxe-column>
  83. <vxe-column field="title" title="目录名称" width="200">
  84. <template #default="{ row, rowIndex }">
  85. <span v-if="row.title">{{row.title}}</span>
  86. <el-tag size="mini" v-else>默认使用网页标题</el-tag>
  87. </template>
  88. </vxe-column>
  89. <vxe-column field="url" title="网页链接"></vxe-column>
  90. <vxe-column field="status" title="下载状态" width="200">
  91. <template #default="{ row }">
  92. <template v-if="row.status == '1'">
  93. <i class="el-icon-info" style="font-size: 16px; color: #999;"></i>
  94. <span>待操作</span>
  95. </template>
  96. <template v-if="row.status == '2'">
  97. <i class="el-icon-loading" style="font-size: 16px; color: #999;"></i>
  98. <span>任务处理中...</span>
  99. </template>
  100. <template v-if="row.status == '3'">
  101. <i class="el-icon-loading" style="font-size: 16px; color: #999;"></i>
  102. <span>视频下载中..<span v-if="row.num > 0">第{{row.num}}个</span></span>
  103. </template>
  104. <template v-if="row.status == '4'">
  105. <i class="el-icon-success" style="font-size: 16px; color: #19be6b;"></i>
  106. <span>下载完成</span>
  107. </template>
  108. <template v-if="row.status == '5'">
  109. <i class="el-icon-error" style="font-size: 16px; color: #ed4014;"></i>
  110. <span>网络异常,请重试!</span>
  111. </template>
  112. <template v-if="row.status == '6'">
  113. <i class="el-icon-error" style="font-size: 16px; color: #ed4014;"></i>
  114. <span>验证码拦截,请手动验证!</span>
  115. </template>
  116. </template>
  117. </vxe-column>
  118. <vxe-column title="操作" width="80">
  119. <template #default="{ row, rowIndex }">
  120. <i class="el-icon-delete cur-pointer" style="font-size: 20px;" @click="delFile(rowIndex)"></i>
  121. </template>
  122. </vxe-column>
  123. </vxe-table>
  124. </div>
  125. </div>
  126. </template>
  127. <el-dialog title="添加链接" :visible.sync="addVisible" width="500px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
  128. <div>
  129. <el-form label-position="right" label-width="80px" :rules="rules" :model="formData" ref="formData">
  130. <el-form-item label="目录名称" prop="title">
  131. <el-input v-model="formData.title" placeholder="为空则默认使用网页标题前50个字符"></el-input>
  132. </el-form-item>
  133. <el-form-item label="网页链接" prop="url">
  134. <el-input type="textarea" :rows="10" v-if="menuIndex < 10" :placeholder="'请输入网址链接(例:' + exampleUrl[menuIndex-1] + ')'" v-model="formData.url"></el-input>
  135. <el-input type="textarea" :rows="10" v-else :placeholder="'请输入网址链接'" v-model="formData.url"></el-input>
  136. </el-form-item>
  137. </el-form>
  138. </div>
  139. <span slot="footer" class="dialog-footer">
  140. <el-button @click="addVisible = false; $refs['formData'].resetFields();">取 消</el-button>
  141. <el-button type="primary" @click="onSubmit">确 定</el-button>
  142. </span>
  143. </el-dialog>
  144. <el-dialog title="提示" :visible.sync="loginVisible" width="400px" :close-on-click-modal="false" :close-on-press-escape="false">
  145. <div style="text-align: center; color: #999; font-size: 14px;">
  146. <template v-if="menuIndex == '1'">
  147. <p>阿里巴巴渠道建议登录后下载</p>
  148. <p class="visible-tips-style">目前检测还未登录阿里巴巴账号,未登录可能会导致图片下载不全</p>
  149. </template>
  150. <template v-else-if="menuIndex == '2'">
  151. <p>京东渠道需要登录后才能下载</p>
  152. <p class="visible-tips-style">目前检测还未登录京东账号,需立即登录</p>
  153. </template>
  154. <template v-else-if="menuIndex == '5'">
  155. <p>小红书渠道需要登录后才能下载</p>
  156. <p class="visible-tips-style">目前检测还未登录小红书账号,需立即登录</p>
  157. </template>
  158. <template v-else>
  159. <p>天猫/淘宝渠道需要登录后才能下载</p>
  160. <p class="visible-tips-style">目前检测还未登录天猫/淘宝账号,需立即登录</p>
  161. </template>
  162. </div>
  163. <div slot="footer" class="dialog-footer-center">
  164. <el-button v-if="menuIndex == '2'" @click="loginVisible = false; loginUrl('https://passport.jd.com/new/login.aspx')">点击登录京东账号</el-button>
  165. <template v-else-if="menuIndex == '1'">
  166. <el-button @click="loginVisible = false; loginUrl('https://www.1688.com')">点击登录阿里巴巴账号</el-button>
  167. <el-button type="primary" @click="loginVisible = false; skipLogin = true; exportFile();">继续下载</el-button>
  168. </template>
  169. <el-button v-else-if="menuIndex == '5'" @click="loginVisible = false; loginUrl('https://www.xiaohongshu.com')">点击登录小红书账号</el-button>
  170. <el-button v-else @click="loginVisible = false; loginUrl('https://login.taobao.com')">点击登录天猫/淘宝账号</el-button>
  171. </div>
  172. </el-dialog>
  173. <!-- 非会员转换提示框 -->
  174. <el-dialog title="非会员提示" :visible.sync="tipsModal" width="400px">
  175. <div class="member-model">
  176. <div class="tips-flex">
  177. <i class="el-icon-s-opportunity"></i>
  178. <p class="m-title">{{tipsDesc}}</p>
  179. </div>
  180. <div class="member-btn">
  181. <el-button size="small" round type="primary" @click="openVip()">开通会员</el-button>
  182. </div>
  183. </div>
  184. </el-dialog>
  185. </el-main>
  186. <el-footer height="48px">
  187. <!-- 更新 -->
  188. <soft-update ref="updateRef" :showDowload="dowloadModel" :dowloadFinish="finishModel"></soft-update>
  189. </el-footer>
  190. </el-container>
  191. </el-container>
  192. </div>
  193. </template>
  194. <script>
  195. import os from 'os'
  196. import fs from 'fs'
  197. import request from 'request'
  198. import path from 'path';
  199. import xlsx from 'node-xlsx';
  200. import softUpdate from './update.vue';
  201. import softHeader from './header.vue';
  202. import electronApi from '@/utils/electronApi';
  203. import pjson from '/package.json'
  204. // import puppeteer from 'puppeteer'
  205. import puppeteer from 'puppeteer-extra'
  206. const StealthPlugin = require('puppeteer-extra-plugin-stealth');
  207. const listNameArr = ['douyin','kuaishou','weibo','','','','','', '' ,'common'];
  208. let separator = '';
  209. if (os.platform == 'linux') {
  210. separator = '/'
  211. } else {
  212. separator = '\\'
  213. }
  214. // let chromePath = process.cwd() + '\\resources\\app\\node_modules\\puppeteer\\.local-chromium\\win64-1045629\\chrome-win\\chrome.exe';
  215. // if (process.env.NODE_ENV == 'development') {
  216. // chromePath = process.cwd() + '\\node_modules\\puppeteer\\.local-chromium\\win64-1045629\\chrome-win\\chrome.exe';
  217. // }
  218. export default {
  219. name: 'landing-page',
  220. components: {
  221. softUpdate,
  222. softHeader
  223. },
  224. data() {
  225. return {
  226. tipsModal: false,
  227. tipsDesc: "非VIP用户不能下载视频,如需完整功能请开通VIP会员。",
  228. settingArr: ['mainImg'],
  229. loginVisible: false,
  230. addVisible: false,
  231. formData: {
  232. title: '',
  233. url: ''
  234. },
  235. rules:{ //
  236. title: [
  237. { min: 0, max: 50, message: '不能超过 50 个字符', trigger: 'blur' },
  238. { pattern: /^[^\\/:*?"<>|]*$/, message: '标题中不能包含 \\ / : * ? " < > |', trigger: 'blur' }
  239. ],
  240. url: [
  241. { required: true, message: '请输入网页链接', trigger: 'blur' },
  242. { pattern: /^(http|https):\/\/.+$/, message: '请输入正确的网址链接(http://或https://开头)', trigger: 'blur' }
  243. ],
  244. },
  245. tabLoading: false,
  246. douyinList: [],
  247. kuaishouList: [],
  248. weiboList: [],
  249. commonList: [],
  250. productName: pjson.softInfo.softName,
  251. imgUrl: this.$api.imgUrl,
  252. imgSrc: '',
  253. menuIndex: '1',
  254. settingData: {
  255. detailImg: true,
  256. skuImg: true,
  257. commentImg: false,
  258. video: false,
  259. },
  260. exampleUrl: ['https://www.douyin.com', 'https://www.kuaishou.com', 'https://www.weibo.com'],
  261. fileList: [],
  262. downloadDir: os.userInfo().homedir + separator + "Downloads",
  263. dowloadModel: false,
  264. finishModel: false,
  265. loading: false,
  266. checkLoading: false, //点击检测登录状态
  267. execLimit: 1,
  268. execNum: 3, // 限制5张
  269. /** 浏览器名称 **/
  270. videoBrowser: null,
  271. loginBrowser: null, // 登录用的浏览器实例
  272. skipLogin: false,
  273. };
  274. },
  275. computed: {
  276. listStr: function(){
  277. let index = Number(this.menuIndex) - 1;
  278. return listNameArr[index];
  279. }
  280. },
  281. async mounted() {
  282. this.$refs.updateRef.updateSoft(true);
  283. let homedir = os.userInfo().homedir;
  284. if (fs.existsSync(homedir + separator + "Desktop")) {
  285. this.downloadDir = homedir + separator + "Desktop"
  286. } else {
  287. this.downloadDir = homedir + separator + "Downloads"
  288. }
  289. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  290. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  291. }
  292. // 打开浏览器
  293. const {
  294. shell
  295. } = require('electron');
  296. const links = document.querySelectorAll('a[href]');
  297. links.forEach(link => {
  298. link.addEventListener('click', e => {
  299. const url = link.getAttribute('href');
  300. e.preventDefault();
  301. shell.openExternal(url);
  302. });
  303. });
  304. },
  305. methods: {
  306. // 实时获取浏览器路径
  307. initPath(){
  308. let chromePath = puppeteer.executablePath().replace('win32-1', 'win64-1');
  309. return chromePath;
  310. },
  311. // 删除文件夹内容
  312. deleteAll(folderPath, flag) {
  313. if (fs.existsSync(folderPath)) {
  314. fs.readdirSync(folderPath).forEach((file, index) => {
  315. var curPath = path.join(folderPath, file);
  316. if (fs.lstatSync(curPath).isDirectory()) {
  317. this.deleteAll(curPath, true);
  318. } else {
  319. fs.unlinkSync(curPath);
  320. }
  321. });
  322. if(flag){
  323. fs.rmdirSync(folderPath);
  324. }
  325. }
  326. },
  327. checkAuthority(){
  328. let authority = this.$refs.headerRef.authority;
  329. this.$refs.imgRef.authority = authority;
  330. },
  331. setMenuIndex(index){
  332. this.menuIndex = index;
  333. if(index.indexOf('2-') > -1){
  334. let num = Number(index.split('-')[1]);
  335. this.$refs.imgRef.setMenuIndex(num);
  336. }
  337. },
  338. // 选择目录
  339. pickPath() {
  340. this.$refs['upload-input'].blur();
  341. electronApi.call('pickDir', []).then((path) => {
  342. if (path) {
  343. this.downloadDir = path;
  344. }
  345. });
  346. },
  347. // 打开自定义下载目录
  348. openFolder() {
  349. let path = this.downloadDir;
  350. if (fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName)) {
  351. path = this.downloadDir + separator + pjson.softInfo.softName;
  352. } else {
  353. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName);
  354. path = this.downloadDir + separator + pjson.softInfo.softName;
  355. }
  356. electronApi.call('showItemInfolder', [path + '\\tty.tty'])
  357. },
  358. openVip() {
  359. this.$refs.headerRef.openVip();
  360. },
  361. updateSoft() {
  362. this.$refs.updateRef.updateSoft();
  363. },
  364. // 删除文件
  365. delFile(rowIndex){
  366. let index = Number(this.menuIndex) - 1;
  367. let type = listNameArr[index];
  368. this.$confirm('确认删除此行数据吗?', '提示', {
  369. confirmButtonText: '确定',
  370. cancelButtonText: '取消',
  371. type: 'warning'
  372. }).then(() => {
  373. this[type+'List'].splice(rowIndex, 1);
  374. if(this[type+'List'].length == 0) {
  375. this.clearList();
  376. }
  377. }).catch(() => {
  378. });
  379. },
  380. // 清除列表
  381. clearList() {
  382. let index = Number(this.menuIndex) - 1;
  383. this[listNameArr[index]+'List'] = [];
  384. },
  385. // 提交表单
  386. onSubmit(){
  387. this.$refs['formData'].validate((valid) => {
  388. if (valid) {
  389. let info = {
  390. url: this.formData.url,
  391. title: this.formData.title,
  392. status: '1',
  393. num: 0,
  394. newPath: ''
  395. }
  396. let index = Number(this.menuIndex) - 1;
  397. this[listNameArr[index]+'List'].push(info);
  398. this.$refs['formData'].resetFields();
  399. this.addVisible = false;
  400. } else {
  401. return false;
  402. }
  403. });
  404. },
  405. async pickLink() { // 导入链接
  406. const spinLoad = this.$loading();
  407. await electronApi.call('pickFile', ['xlsx,xls', true]).then(async (path) => {
  408. spinLoad.close();
  409. if(path.length > 0){
  410. this.$notify({
  411. title: '提示',
  412. message: '导入成功',
  413. type: 'success'
  414. });
  415. let workSheetsFromBuffer = xlsx.parse(fs.readFileSync(path[0]));
  416. if(workSheetsFromBuffer && workSheetsFromBuffer.length > 0){
  417. let errMsg = "";
  418. workSheetsFromBuffer[0].data.map((item, index) => {
  419. if(item.length > 0){
  420. let ititle = '';
  421. if(item[0]){
  422. ititle = item[0].toString().trim();
  423. }
  424. if(item[0] == '目录名称' && item[1] == '网页链接'){
  425. return false;
  426. }
  427. if(this.containsAnyChar(ititle.toString(), ['\\', '/', ':', '*', '?', '"', '<', '>', '|'])){ //判断是否含有特殊字符
  428. errMsg += "第" + (index+1) + '行-名称不能包以下字符 \\ / : * ? " < > |';
  429. ititle = ititle.replace(/[\\|/|:|*|?|"|<|>||]/g, "");
  430. }
  431. if(item[1] == undefined){
  432. errMsg += "第" + (index+1) + "行格式有误</br>";
  433. item[1] = '';
  434. }else if(!/^(http|https):\/\/.+$/.exec(item[1])){
  435. errMsg += "第" + (index+1) + "行网址格式有误</br>";
  436. }else{
  437. let info = {
  438. url: item[1],
  439. title: ititle,
  440. status: '1',
  441. num: 0,
  442. newPath: ''
  443. }
  444. let key = Number(this.menuIndex) - 1;
  445. this[listNameArr[key]+'List'].push(info);
  446. }
  447. }
  448. });
  449. if(errMsg){
  450. this.$notify({
  451. title: '请检查文件以下问题',
  452. dangerouslyUseHTMLString: true,
  453. message: errMsg,
  454. type: 'warning'
  455. });
  456. }
  457. }else{
  458. this.$notify({
  459. title: '提示',
  460. message: '格式有误,请重新导入',
  461. type: 'warning'
  462. });
  463. }
  464. }
  465. });
  466. },
  467. settingGroup(name){
  468. let authority = this.$refs.headerRef.authority.isAuthority;
  469. let index = name.indexOf('video');
  470. if(index > -1 && !authority){ // 非会员选中下载视频选项-提示
  471. this.tipsModal = true;
  472. name.splice(index, 1);
  473. }
  474. },
  475. // 清除缓存的后续操作
  476. async clearCache(){
  477. this.jdStatus = 3;
  478. this.tbStatus = 3;
  479. this.redStatus = 3;
  480. this.alibabaStatus = 3;
  481. this.aliguojiStatus = 3;
  482. if(this.loginBrowser){
  483. await this.loginBrowser.close();
  484. this.loginBrowser = null;
  485. }
  486. if(this.videoBrowser){
  487. await this.videoBrowser.close();
  488. this.videoBrowser = null;
  489. }
  490. },
  491. // 去登录
  492. loginUrl(url){
  493. (async () => {
  494. try{
  495. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  496. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  497. }
  498. let userDataDir = os.tmpdir() + separator + 'chrome-data-capture-video';
  499. puppeteer.use(StealthPlugin());
  500. this.loginBrowser = await puppeteer.launch({
  501. headless: false,
  502. executablePath: this.initPath(),
  503. args: ['--window-size=1280,800'],
  504. userDataDir: userDataDir,
  505. });
  506. const page = await this.loginBrowser.newPage();
  507. await page.setViewport({ width: 1280, height: 800 });
  508. await page.evaluateOnNewDocument(() => {
  509. const newProto = navigator.__proto__;
  510. delete newProto.webdriver;
  511. navigator.__proto__ = newProto;
  512. });
  513. await page.goto(url, {waitUntil : 'networkidle2'});
  514. }catch(e){
  515. this.showError(e);
  516. }
  517. })();
  518. },
  519. // 开始下载
  520. async exportFile(flag) {
  521. if(this.loginBrowser){
  522. await this.loginBrowser.close();
  523. this.loginBrowser = null;
  524. }
  525. let authority = this.$refs.headerRef.authority.isAuthority;
  526. if (!fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName)) {
  527. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName);
  528. }
  529. let index = Number(this.menuIndex) - 1;
  530. let fileList = this[listNameArr[index]+'List'];
  531. if(fileList.length > 0){
  532. if (!authority && !flag) { // 非会员点击转换弹出提示框
  533. this.$refs.headerRef.memberModel = true;
  534. this.$refs.headerRef.isClick = true;
  535. return false;
  536. } else {
  537. this.$refs.headerRef.memberModel = false;
  538. this.loading = true;
  539. setTimeout(() => {
  540. this.loading = false;
  541. }, 60000);
  542. }
  543. if (!fs.existsSync(os.tmpdir() + separator + 'chrome-data-capture-video')) {
  544. fs.mkdirSync(os.tmpdir() + separator + 'chrome-data-capture-video');
  545. }
  546. let taskArr = [];
  547. let task = "";
  548. let userDataDir = os.tmpdir() + separator + 'chrome-data-capture-video';
  549. // 运行不同平台的浏览器
  550. puppeteer.use(StealthPlugin());
  551. this.videoBrowser = await puppeteer.launch({
  552. headless: false,
  553. executablePath: this.initPath(),
  554. userDataDir: userDataDir,
  555. args: [
  556. '--start-maximized',
  557. '--no-sandbox',
  558. '--disable-setuid-sandbox',
  559. '--disable-blink-features=AutomationControlled',
  560. ]
  561. });
  562. for(let i = 0; i < fileList.length; i++){
  563. let item = fileList[i];
  564. task = this.videoDownload(item, this.videoBrowser);
  565. if(task){
  566. taskArr.push(task);
  567. }
  568. if((i+1) % this.execLimit == 0){
  569. await Promise.all(taskArr).then(result => {
  570. taskArr = [];
  571. }).catch(err => {
  572. console.log('err'+i, err)
  573. })
  574. }
  575. }
  576. if(taskArr.length > 0){
  577. await Promise.all(taskArr).then(result => {
  578. taskArr = [];
  579. }).catch(err => {
  580. // 错误文件添加到服务中
  581. console.log('err',err)
  582. })
  583. }
  584. if(this.videoBrowser){
  585. this.videoBrowser.close();
  586. this.videoBrowser = null;
  587. }
  588. this.loading = false;
  589. // 打开文件夹
  590. if(fileList.length > 0){
  591. this.$message({message: '恭喜你,任务已完成!', type: 'success'});
  592. electronApi.call('showItemInfolder',[this.downloadDir + separator + pjson.softInfo.softName +'\\tty.tty'])
  593. }
  594. }
  595. },
  596. // 1 - 抖音视频下载
  597. async videoDownload(urlInfo, browser){
  598. let task = await new Promise((resolve,reject) =>{
  599. (async () => {
  600. try{
  601. let authority = this.$refs.headerRef.authority.isAuthority;
  602. urlInfo.status = '2';
  603. urlInfo.num = 0;
  604. const page = await browser.newPage();
  605. let responseVideo = [];
  606. page.on('response', async(response) => {
  607. // 检查响应的 MIME 类型是否以 'video/' 开头
  608. if (response.headers()['content-type'] && response.headers()['content-type'].startsWith('video/')) {
  609. if(responseVideo.indexOf(response.url()) < 0 && !response.url().startsWith('blob:https://')){
  610. responseVideo.push(response.url());
  611. }
  612. }
  613. });
  614. await page.goto(urlInfo.url, {waitUntil : 'networkidle2'});
  615. if(urlInfo.title){
  616. if (fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName + separator + urlInfo.title)) {
  617. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + urlInfo.title;
  618. } else {
  619. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName + separator + urlInfo.title);
  620. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + urlInfo.title;
  621. }
  622. }else{
  623. await this.getTitle(page, urlInfo); // 生成页面标题对应的文件夹
  624. }
  625. urlInfo.status = '3';
  626. console.log(responseVideo);
  627. //视频下载
  628. for(let j = 0; j < responseVideo.length; j++){
  629. let num = Number(j) + 1;
  630. let suffix = '.mp4';
  631. let outputPath = urlInfo.newPath + '\\视频' + num + suffix;
  632. await this.downloadImage(responseVideo[j], outputPath, urlInfo);
  633. }
  634. this.loading = false;
  635. urlInfo.status = '4';
  636. resolve(true);
  637. }catch(e){
  638. urlInfo.status = '5';
  639. reject(e);
  640. this.showError(e);
  641. }
  642. })();
  643. });
  644. },
  645. //
  646. downloadExample(){
  647. let url = 'https://www.xingyousoft.com/soft/XYCapture/example.xlsx';
  648. if (!fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName)) {
  649. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName);
  650. }
  651. let path = this.downloadDir + separator + pjson.softInfo.softName + '\\链接模板下载.xlsx';
  652. this.downloadImage(url, path, 'example');
  653. },
  654. // 下载网址链接的图片
  655. async downloadImage(imageUrl, outputPath, urlInfo) {
  656. let _this = this;
  657. let received_bytes = 0;
  658. let total_bytes = 0;
  659. try {
  660. let req = request({
  661. method: 'GET', uri: imageUrl, strictSSL: false
  662. });
  663. let out = fs.createWriteStream(outputPath);
  664. req.pipe(out);
  665. return new Promise((resolve, reject) => {
  666. req.on('response', (data) => {
  667. total_bytes = parseInt(data.headers['content-length']);
  668. const status = data.statusCode;
  669. if (status < 200 || status >= 300) {
  670. //reject(false);
  671. this.$notify.error({
  672. title: '网络资源访问异常!- 1',
  673. message: imageUrl.slice(0,50)
  674. });
  675. }else if(isNaN(total_bytes)){
  676. //reject(false);
  677. this.$notify.error({
  678. title: '网络资源访问异常!- 2',
  679. message: imageUrl.slice(0,50)
  680. });
  681. }else{
  682. // console.log('下载中...')
  683. }
  684. });
  685. req.on('data', (chunk) => {
  686. received_bytes += chunk.length;
  687. });
  688. req.on('end', ()=> {
  689. if(urlInfo != 'example'){
  690. urlInfo.num += 1;
  691. }else{
  692. this.$msgbox({
  693. title: '消息',
  694. message: '模板下载成功,保存位置:' + this.downloadDir + separator + pjson.softInfo.softName,
  695. showCancelButton: true,
  696. confirmButtonText: '去查看',
  697. cancelButtonText: '取消',
  698. }).then(action => {
  699. this.openFolder();
  700. }).catch(action => {
  701. });
  702. }
  703. //console.log('下载完成', outputPath)
  704. resolve(true);
  705. });
  706. });
  707. } catch (error) {
  708. console.error(imageUrl, `Failed to download image: ${error.message}`);
  709. throw error;
  710. }
  711. },
  712. // 获取页面标题 - 生成对应的文件夹
  713. async getTitle(page, urlInfo){
  714. // 已页面标题作为新建文件夹,保留前50个字
  715. let title = await page.title();
  716. if(title){
  717. title = title.substring(0, 50);
  718. if(this.containsAnyChar(title, ['\\', '/', ':', '*', '?', '"', '<', '>', '|'])){ //判断是否含有特殊字符
  719. title = title.replace(/[\\|/|:|*|?|"|<|>||]/g, "");
  720. }
  721. if (fs.existsSync(this.downloadDir + separator + pjson.softInfo.softName + separator + title)) {
  722. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + title;
  723. } else {
  724. fs.mkdirSync(this.downloadDir + separator + pjson.softInfo.softName + separator + title);
  725. urlInfo.newPath = this.downloadDir + separator + pjson.softInfo.softName + separator + title;
  726. }
  727. }
  728. },
  729. // 下载base64位的图片
  730. async downloadBaseImage(base64String, outputPath, urlInfo) {
  731. const buffer = Buffer.from(base64String, 'base64');
  732. const writeStream = fs.createWriteStream(outputPath);
  733. writeStream.write(buffer);
  734. writeStream.end();
  735. writeStream.on('finish', () => {
  736. urlInfo.num += 1;
  737. });
  738. // 监听错误事件
  739. writeStream.on('error', (err) => {
  740. console.error('base64位写入文件时出错:', err);
  741. });
  742. },
  743. // 错误提示
  744. showError(e){
  745. let str = '';
  746. if(e.toString().indexOf('ERR_NAME_NOT_RESOLVE') > -1){
  747. str = '无法解析该网址,请查看网址是否正确!-1';
  748. }else if(e.toString().indexOf('Cannot navigate to invalid URL') > -1){
  749. str = '无效的网址,请查看网址格式是否正确!-2';
  750. }else if(e.toString().indexOf('ERR_CONNECTION_TIMED_OUT') > -1){
  751. str = '链接请求超时,请查看网络状态!-3';
  752. }else if(e.toString().indexOf('TimeoutError') > -1){
  753. str = '链接请求超时,请查看网络状态!-4';
  754. }else if(e.toString().indexOf('operation not permitted') > -1){
  755. str = '权限受限,请以管理员权限运行软件!-5';
  756. }else if(e.toString().indexOf('browser has disconnected') > -1){
  757. str = '内置浏览器已关闭!-6';
  758. }else if(e.toString().indexOf('Failed to launch the browser') > -1){
  759. str = '请先关闭内置浏览器!-7';
  760. }else{
  761. str = e.toString();
  762. //console.log(e);
  763. }
  764. this.loading = false;
  765. this.$notify.error({
  766. title: '提示',
  767. message: str
  768. });
  769. },
  770. // 是否包含特殊字符
  771. containsAnyChar(str, charsArray) {
  772. for (let i = 0; i < charsArray.length; i++) {
  773. if (str.includes(charsArray[i])) {
  774. return true;
  775. }
  776. }
  777. return false;
  778. },
  779. // 随机生成字符
  780. randomString(length) {
  781. let result = '';
  782. const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  783. const charactersLength = characters.length;
  784. for (let i = 0; i < length; i++ ) {
  785. result += characters.charAt(Math.floor(Math.random() * charactersLength));
  786. }
  787. return result;
  788. }
  789. }
  790. };
  791. </script>
  792. <style lang="scss">
  793. @import "../assets/css/fontx/iconfont.css";
  794. @import "../assets/css/home.scss";
  795. .ivu-input-number-controls-outside-btn i {
  796. font-weight: 800;
  797. }
  798. .update-point {
  799. display: inline-block;
  800. width: 8px;
  801. height: 8px;
  802. border-radius: 8px;
  803. background: #ff0000;
  804. top: 14px;
  805. position: absolute;
  806. left: -13px;
  807. }
  808. .menu-item {
  809. padding: 8px 0;
  810. font-size: 14px;
  811. .iconfont {
  812. font-size: 32px;
  813. }
  814. &:hover,
  815. &.active {
  816. color: #ed4014;
  817. }
  818. }
  819. .ivu-progress-show-info .ivu-progress-outer {
  820. padding-right: 40px !important;
  821. margin-right: -40px !important;
  822. }
  823. .tips {
  824. text-align: center;
  825. padding: 10px 0;
  826. color: #ed4014;
  827. font-size: 12px;
  828. }
  829. .handle-desc {
  830. display: inline-block;
  831. width: calc(100% - 100px);
  832. overflow: hidden;
  833. }
  834. .ivu-menu-submenu-title {
  835. font-weight: 600;
  836. }
  837. .ivu-menu .ivu-menu-item {
  838. line-height: 1;
  839. }
  840. // new-el
  841. .el-menu {
  842. border-right: none !important;
  843. }
  844. .cmenu-item {
  845. padding: 0 20px 20px;
  846. margin-bottom: 15px;
  847. .cmenu-title {
  848. font-size: 18px;
  849. font-weight: 600;
  850. padding-bottom: 20px;
  851. }
  852. .citem-nav {
  853. text-align: center;
  854. border-radius: 10px;
  855. min-height: 110px;
  856. padding: 15px 0;
  857. background-color: #fff;
  858. font-size: 15px;
  859. cursor: pointer;
  860. &.bg-linear1 {
  861. color: #fff;
  862. font-size: 20px;
  863. background: linear-gradient(to right bottom, #2A56CA, #5795F4);
  864. }
  865. &.bg-linear2 {
  866. color: #fff;
  867. font-size: 20px;
  868. background: linear-gradient(to right top, #147FBB, #5EB3E3);
  869. }
  870. &.bg-linear3 {
  871. color: #fff;
  872. font-size: 20px;
  873. background: linear-gradient(to right bottom, #2F9E8A, #56CDB1);
  874. }
  875. &:hover {
  876. margin-top: -5px;
  877. box-shadow: 3px 3px 6px #ccc, -3px -3px 6px #ccc;
  878. }
  879. .citem-img {
  880. width: 50px;
  881. margin-bottom: 10px;
  882. }
  883. }
  884. }
  885. .popper-open{
  886. text-align: center !important;
  887. padding: 10px !important;
  888. background: #303133 !important;
  889. color: #fff !important;
  890. min-width: 120px !important;
  891. opacity: 0.8;
  892. }
  893. .popper-open[x-placement^=bottom] .popper__arrow::after{
  894. border-bottom-color: #303133 !important;
  895. }
  896. textarea.el-textarea__inner{
  897. height: 100% !important;
  898. font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
  899. }
  900. .set-item{
  901. margin-right: 15px;
  902. .set-title{
  903. font-size: 13px;
  904. }
  905. }
  906. .outarea{
  907. height: 100%;
  908. border: 1px solid #DCDFE6;
  909. background-color: #4851a415;
  910. padding: 15px;
  911. font-size: 20px;
  912. overflow: hidden auto;
  913. }
  914. .outtext{
  915. height: 100%;
  916. font-size: 20px;
  917. overflow: hidden auto;
  918. textarea{
  919. background-color: #4851a415;
  920. }
  921. }
  922. .pin-tips{
  923. font-size: 14px;
  924. position: absolute;
  925. bottom: 10px;
  926. color: #ff0000;
  927. left: 0;
  928. right: 0;
  929. margin: auto;
  930. text-align: center;
  931. }
  932. .outarea.red-border{
  933. border: 1px solid #F56C6C;
  934. }
  935. .outtext.red-border textarea{
  936. border: 1px solid #F56C6C;
  937. }
  938. h3{
  939. margin: 0;
  940. }
  941. .m-image{
  942. width: 20px;
  943. margin-right: 5px;
  944. }
  945. .dialog-footer-center{
  946. text-align: center;
  947. }
  948. .visible-tips-style{
  949. font-size: 18px;
  950. color: #f73131;
  951. font-weight: 600;
  952. padding: 30px 40px 0;
  953. }
  954. .no-select{
  955. user-select: none;
  956. }
  957. .tips-flex{
  958. display: flex;
  959. flex-wrap: nowrap;
  960. justify-content: space-around;
  961. align-items: center;
  962. .el-icon-s-opportunity{
  963. font-size: 50px;
  964. color: #f73131;
  965. margin-right: 10px;
  966. }
  967. .m-title{
  968. flex: 1;
  969. color: #f73131;
  970. }
  971. }
  972. </style>