YM
2024-04-11 654f169ed96148247fda48765b47a75ccb2143ab
初始化
2个文件已修改
35个文件已添加
4250 ■■■■■ 已修改文件
.eslintrc.cjs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierrc.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
build/installer.nsh 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/config.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/downloadTask.ts 738 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/exportTask.ts 930 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/main.ts 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/preload.ts 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/toolClass.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
electron/update.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
env.d.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/base.css 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/toolClass.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/main.css 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/style/global.less 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/layout.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugin/axios/index.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/downloadTask.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/exportTask.vue 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/home.vue 281 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/setting.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transmission.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.app.json 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.electron.json 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.node.json 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintrc.cjs
New file
@@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
  root: true,
  'extends': [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  }
}
.gitignore
@@ -1,23 +1,29 @@
# Object files
*.o
*.ko
*.obj
*.elf
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Libraries
*.lib
*.a
# 依赖
node_modules
*.lock
package-lock.json
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# 编译文件
dist
dist-electron
electron-commonJS
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# 打包文件
release
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.prettierrc.json
New file
@@ -0,0 +1,8 @@
{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none"
}
README.md
@@ -1,4 +1,18 @@
## TextbookReader
# 数字教材阅读器
电子教材阅读器
# 环境
Node.js 要求 >= 18.0.0
# 安装依赖
yarn
# 启动项目
npm run dev
# 编译打包
windows: npm run build
mac: npm run build_mac
# 技术栈
Vue3  Vite  Electron  NodeJs  ElementUI  Qiankun  TypeScript  Less
build/installer.nsh
New file
@@ -0,0 +1,9 @@
!macro customInstall
  DetailPrint "Register DigitalTextbookReader URI Handler"
  DeleteRegKey HKCR "DigitalTextbookReader"
  WriteRegStr HKCR "DigitalTextbookReader" "" "URL:DigitalTextbookReader"
  WriteRegStr HKCR "DigitalTextbookReader" "URL Protocol" ""
  WriteRegStr HKCR "DigitalTextbookReader\shell" "" ""
  WriteRegStr HKCR "DigitalTextbookReader\shell\Open" "" ""
  WriteRegStr HKCR "DigitalTextbookReader\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
!macroend
electron/config.ts
New file
@@ -0,0 +1,4 @@
// 测试
export const ctx = "http://182.92.203.7:5001";
export const downloaderFileCtx = "http://182.92.203.7:3007/DigitalTextbookReader";
electron/downloadTask.ts
New file
@@ -0,0 +1,738 @@
import { ipcMain, shell } from 'electron'
import fs from 'fs'
import os from 'os'
import axios from 'axios'
import path from 'path'
import moment from 'moment'
import { uuid } from './toolClass'
const Store = require('electron-store')
const FileDownloadTasks = new Store('FileDownloadTasks')
const DownloadConfig = new Store('DownloadConfig')
const DOWNLOAD_SAVE_FOLDER = 'FileDownloadSaveFolder'
const DOWNLOAD_TASKS = 'FileDownloadTasks'
// 下载列表
let downloadTasks = []
// 默认路径
const homeDir = os.homedir()
let DOWNLOAD_PATH = ''
const FileDownloadSaveFolder = DownloadConfig.get(DOWNLOAD_SAVE_FOLDER)
if (FileDownloadSaveFolder) {
  DOWNLOAD_PATH = FileDownloadSaveFolder
} else {
  if (os.platform() === 'win32') {
    // Windows 系统下的 "Downloads" 文件夹路径
    DOWNLOAD_PATH = path.join(homeDir, 'Downloads')
  } else {
    // macOS 和 Linux 系统下的 "Downloads" 文件夹路径
    DOWNLOAD_PATH = path.join(homeDir, 'Downloads')
  }
}
let mainWindow: any = null
let request: any = null
let token = ''
export const init = (win, req) => {
  mainWindow = win
  request = req
  loadTasks()
}
// 下载任务实例
class DownloadTask {
  constructor(data) {
    this.initData(data)
    updateTask(this.data)
  }
  public data
  // 初始化数据
  initData(data) {
    if (data.id) {
      // id:唯一标识
      // state:状态
      // savePath:储存路径
      // fileInfoMap:所有下载文件信息map
      // name:任务名称
      // size:任务大小
      // offset:任务已完成偏移量
      // msg:任务消息
      // md5:下载文件md5
      // taskDesc:任务描述,与md5二选一,任务描述会以文件夹形式下载并储存
      // 例如:
      // [
      //   {
      //     name: '文件夹1',
      //     type: 'folder',
      //     childer: [
      //       {
      //         name: '文件夹2',
      //         type: 'folder',
      //         childer: [
      //           {
      //             name: '文件1',
      //             type: 'file',
      //             md5: "a654f654das6f5465ds4f65sa4f"
      //           }
      //         ]
      //       },
      //       {
      //         name: '文件2',
      //         type: 'file',
      //         md5: "asdfsadf564s644544er4g65f4h"
      //       }
      //     ]
      //   }
      // ]
      this.data = { ...data }
    } else {
      this.data = {
        id: uuid(8),
        state: 'init',
        fileState: 'init',
        savePath: '',
        fileInfoMap: {},
        name: data.name ? data.name : data.taskDesc ? data.taskDesc[0].name : '文件',
        size: 0,
        offset: 0,
        msg: '',
        md5: data.md5 ? data.md5 : null,
        taskDesc: data.taskDesc ? data.taskDesc : null,
        oldTaskDesc: data.taskDesc ? JSON.stringify(data.taskDesc) : null,
        createDate: moment().format('YYYY-MM-DD HH:mm:ss'),
        completeDate: ''
      }
    }
  }
  async start() {
    if (this.data.md5) {
      // 单个下载
      if (!this.data.fileInfoMap[this.data.md5]) {
        // 没有文件信息时获取
        this.data.state = 'getFileInfo'
        const info: any = await this.getFileInfo(this.data.md5)
        if (info) {
          this.data.fileInfoMap[this.data.md5] = info
          this.data.size = info.size
          this.data.name = info.name + '.' + info.extension
        }
      }
      if (this.data.name) {
        if (fs.existsSync(path.join(DOWNLOAD_PATH, this.data.name))) {
          // 文件已存在
          const stats = fs.statSync(path.join(DOWNLOAD_PATH, this.data.name))
          if (stats.size == this.data.size) {
            // 文件已下载完成
            mainWindow.webContents.send(
              'showMessage',
              JSON.stringify({
                type: 'warning',
                msg: '文件已下载完成,请在已完成或下载目录中查看。'
              })
            )
            // 删除当前下载任务
            this.data = false
            return false
          } else {
            // 存在旧的文件未完成或正在下载的任务
            const oldTask = downloadTasks.find(
              (item) =>
                item.md5 == this.data.md5 &&
                item.id != this.data.id &&
                (item.state == 'download' || item.state == 'pause')
            )
            if (oldTask) {
              // 存在任务
              if (oldTask.state == 'download') {
                mainWindow.webContents.send(
                  'showMessage',
                  JSON.stringify({
                    type: 'warning',
                    msg: '已存在相同的下载任务'
                  })
                )
                // 删除当前下载任务
                this.data = false
                return false
              }
              if (oldTask.state == 'pause') {
                const task = new DownloadTask(oldTask)
                task.start()
                mainWindow.webContents.send(
                  'showMessage',
                  JSON.stringify({
                    type: 'warning',
                    msg: '已存在相同的下载任务,已自动开始下载'
                  })
                )
                // 删除当前下载任务
                this.data = false
                return false
              }
            }
          }
        } else {
          // 文件不存在
          const oldTasks = downloadTasks.filter(
            (item) =>
              item.md5 == this.data.md5 &&
              item.id != this.data.id &&
              (item.state == 'download' || item.state == 'pause')
          )
          if (oldTasks.length) {
            // 存在相同文件的任务,分析这些任务状态,并标记这些任务的文件已不存在
            for (let i = 0; i < oldTasks.length; i++) {
              const oldTask = oldTasks[i]
              if (oldTask.state == 'pause') {
                const task = new DownloadTask(oldTask)
                task.start()
                mainWindow.webContents.send(
                  'showMessage',
                  JSON.stringify({
                    type: 'warning',
                    msg: '已存在相同的下载任务,已自动开始下载'
                  })
                )
                // 删除当前下载任务
                this.data = false
                return false
              }
              if (oldTask.state == 'success') {
                oldTask.fileState = 'fileMissing'
              }
            }
          } else {
            this.data.offset = 0
          }
        }
        this.data.savePath = path.join(DOWNLOAD_PATH, this.data.name)
        this.data.state = 'download'
        this.data.fileState = 'download'
        this.data.fileInfoMap[this.data.md5].downloadState = 'download'
        updateTask(this.data)
        this.download(this.data, (type) => {
          this.data.state = type
          this.data.fileInfoMap[this.data.md5].downloadState = type
          if (type == 'success') {
            mainWindow.webContents.send(
              'showMessage',
              JSON.stringify({
                type: 'success',
                msg: `${this.data.name}已下载完成,请在已完成或下载目录中查看。`
              })
            )
            this.data.completeDate = moment().format('YYYY-MM-DD HH:mm:ss')
          }
        })
      }
    } else if (this.data.taskDesc && this.data.taskDesc.length) {
      // 文件夹下载
      this.data.state = 'download'
      this.data.offset = 0
      this.data.name = this.data.taskDesc[0].name
      this.data.savePath = path.join(DOWNLOAD_PATH, this.data.name)
      // 存在旧的文件未完成或正在下载的任务
      const oldTask = downloadTasks.find(
        (item) =>
          item.taskDesc &&
          item.oldTaskDesc == this.data.oldTaskDesc &&
          item.id != this.data.id &&
          (item.state == 'download' || item.state == 'pause')
      )
      if (oldTask) {
        // 存在任务
        if (oldTask.state == 'download') {
          mainWindow.webContents.send(
            'showMessage',
            JSON.stringify({
              type: 'warning',
              msg: '已存在相同的下载任务'
            })
          )
          // 删除当前下载任务
          this.data = false
          return false
        }
        if (oldTask.state == 'pause') {
          const task = new DownloadTask(oldTask)
          task.start()
          mainWindow.webContents.send(
            'showMessage',
            JSON.stringify({
              type: 'warning',
              msg: '已存在相同的下载任务,已自动开始下载'
            })
          )
          // 删除当前下载任务
          this.data = false
          return false
        }
      }
      await this.handleFolderData(this.data.taskDesc, DOWNLOAD_PATH)
      await this.handleFolderDownload(this.data.taskDesc, DOWNLOAD_PATH)
    }
    return false
  }
  // 先处理总体大小,不然会导致下载过程中判断为已完成
  async handleFolderData(dataList, savePath) {
    for (let i = 0; i < dataList.length; i++) {
      const dataItem = dataList[i]
      if (!dataItem.md5) {
        // 文件夹
        dataItem.type = 'folder'
        // 判断文件夹是否存在,创建文件夹
        const folderPath = path.join(savePath, dataItem.name)
        if (!fs.existsSync(folderPath)) {
          fs.mkdirSync(folderPath)
        }
        // 处理子
        if (dataItem.childer && dataItem.childer.length) {
          await this.handleFolderData(dataItem.childer, folderPath)
        }
      } else {
        // 文件
        dataItem.type = 'file'
        if (!this.data.fileInfoMap[dataItem.md5]) {
          // 没有文件信息时获取
          dataItem.state = 'getFileInfo'
          const info: any = await this.getFileInfo(dataItem.md5)
          if (info) {
            this.data.fileInfoMap[dataItem.md5] = info
            dataItem.size = info.size
            dataItem.name = info.name + '.' + info.extension
            this.data.size += dataItem.size
          }
        }
        if (dataItem.name) {
          dataItem.offset = 0
          dataItem.savePath = path.join(savePath, dataItem.name)
          if (fs.existsSync(dataItem.savePath)) {
            // 文件已存在
            const stats = fs.statSync(dataItem.savePath)
            if (stats.size == this.data.size) {
              // 已完成
              dataItem.state = 'success'
              this.data.fileInfoMap[dataItem.md5].downloadState = 'success'
            } else {
              // 未完成
              dataItem.state = 'download'
              this.data.fileInfoMap[dataItem.md5].downloadState = 'download'
              dataItem.offset = stats.size
              this.data.offset += dataItem.offset
            }
          } else {
            // 文件不存在
            dataItem.state = 'download'
            this.data.fileInfoMap[dataItem.md5].downloadState = 'download'
          }
          updateTask(this.data)
        }
      }
    }
  }
  // 处理folder数据
  async handleFolderDownload(dataList, savePath) {
    for (let i = 0; i < dataList.length; i++) {
      const dataItem = dataList[i]
      if (!dataItem.md5) {
        // 文件夹
        if (dataItem.childer && dataItem.childer.length) {
          await this.handleFolderDownload(dataItem.childer, path.join(savePath, dataItem.name))
        }
      } else {
        // 文件
        this.download(
          dataItem,
          (type) => {
            this.data.fileInfoMap[dataItem.md5].downloadState = type
            dataItem.state = type
            if (type == 'success' && this.data.state != 'success') {
              if (this.data.offset == this.data.size) {
                this.data.state = type
                this.data.completeDate = moment().format('YYYY-MM-DD HH:mm:ss')
                mainWindow.webContents.send(
                  'showMessage',
                  JSON.stringify({
                    type: 'success',
                    msg: `${this.data.name}已下载完成,请在已完成或下载目录中查看。`
                  })
                )
              }
            } else {
              this.data.state = type
            }
          },
          (length) => {
            this.data.offset += length
          }
        )
      }
    }
  }
  pause() {
    if (this.data.md5) {
      // 暂停单个文件下载
      if (this.data.cancelToken && this.data.cancelToken.cancel) this.data.cancelToken.cancel()
      if (this.data.cancelPipe) this.data.cancelPipe()
      this.data.status = 'pause'
      if (this.data.fileInfoMap[this.data.md5])
        this.data.fileInfoMap[this.data.md5].downloadState = 'pause'
      updateTask(this.data)
    } else {
      // 暂停整本下载
      this.pauseAll(this.data.taskDesc)
    }
  }
  pauseAll(data) {
    for (let i = 0; i < data.length; i++) {
      const item = data[i]
      if (item.md5) {
        if (item.cancelToken && item.cancelToken.cancel) item.cancelToken.cancel()
        if (item.cancelPipe) item.cancelPipe()
        item.status = 'pause'
        if (this.data.fileInfoMap[item.md5]) {
          this.data.fileInfoMap[item.md5].downloadState = 'pause'
        }
      }
      if (item.childer && item.childer.length) {
        this.pauseAll(item.childer)
      }
    }
  }
  // 获取文件信息
  async getFileInfo(md5) {
    try {
      const resp = await request({
        url: `/file/FileUpload/GetFileInfo`,
        method: 'post',
        headers: {
          Authorization: 'bearer ' + token
        },
        data: {
          md5: md5
        }
      })
      return resp.data
    } catch (error) {
      this.data.state = 'error'
      updateTask(this.data)
      return false
    }
  }
  async download(fileItem, endCallback, ongoingCallback?) {
    try {
      if (fileItem.offset === 0) fs.unlink(fileItem.savePath, () => {})
      fileItem.cancelToken = axios.CancelToken.source()
      const resp = await request({
        url: `http://182.92.203.7:5004/file/FileDownload/Download?md5=${fileItem.md5}&offset=${fileItem.offset}`,
        method: 'get',
        cancelToken: fileItem.cancelToken.token,
        responseType: 'stream',
        headers: {
          Authorization: 'bearer ' + token
        }
      })
      const writer = fs.createWriteStream(fileItem.savePath, {
        start: fileItem.offset,
        flags: 'a+',
        autoClose: true
      })
      resp.pipe(writer)
      fileItem.cancelPipe = () => {
        resp.unpipe(writer)
        writer.end('停止写入!')
      }
      resp.on('data', (data) => {
        fileItem.offset += data.length
        // 调用渲染进程
        updateTask(this.data)
        if (ongoingCallback) ongoingCallback(data.length)
      })
      resp.on('end', () => {
        if (fileItem.offset == fileItem.size) {
          endCallback('success')
        } else {
          // 完成后没有达到实际大小
          endCallback('error')
        }
      })
      resp.on('error', (err) => {
        if (err.name == 'CanceledError') {
          // 暂停,取消请求
          endCallback('pause')
        } else {
          // 下载错误
          endCallback('error')
        }
        updateTask(this.data)
      })
      resp.on('close', () => {
        updateTask(this.data)
      })
    } catch (error) {
      updateTask(this.data)
      // 下载错误
      endCallback('error')
    }
  }
}
// 本地缓存加载任务列表
const loadTasks = () => {
  const txt = FileDownloadTasks.get(DOWNLOAD_TASKS, '[]')
  try {
    downloadTasks = JSON.parse(txt)
  } catch (error) {
    downloadTasks = []
  }
  for (let i = 0; i < downloadTasks.length; i++) {
    const item = downloadTasks[i]
    if (item.state == 'download') item.state = 'pause'
  }
}
// 本地缓存记录任务列表
const saveTasks = () => {
  FileDownloadTasks.set(DOWNLOAD_TASKS, JSON.stringify(downloadTasks))
}
const updateTask = (data?) => {
  // 调用渲染进程
  if (data) {
    mainWindow.webContents.send('downloadTaskChange', JSON.stringify(data))
    const index = downloadTasks.findIndex((item) => item.id == data.id)
    downloadTasks[index] = data
  } else {
    mainWindow.webContents.send('downloadTaskChange')
  }
  saveTasks()
}
// 获取任务列表
ipcMain.on('getDownloadTasks', (ev, data) => {
  // 页面会先获取任务列表,在获取列表时将token挂载
  mainWindow.webContents
    .executeJavaScript('localStorage.getItem("token");', true)
    .then((result) => {
      token = result
    })
  const returnData = JSON.parse(JSON.stringify(downloadTasks))
  ev.returnValue = returnData.map((item: any) => {
    if (item.md5) {
      return {
        ...item,
        cancelToken: '', // 去掉方法,通信避免报错
        cancelPipe: '' // 去掉方法,通信避免报错
      }
    } else {
      return {
        ...item,
        taskDesc: handleTasksInfo(item.taskDesc)
      }
    }
  })
})
const handleTasksInfo = (dataList) => {
  for (let i = 0; i < dataList.length; i++) {
    const item = dataList[i]
    if (item.cancelToken) item.cancelToken = ''
    if (item.cancelPipe) item.cancelPipe = ''
    if (item.childer) {
      item.childer = handleTasksInfo(item.childer)
    }
  }
  return dataList
}
// 新建任务
ipcMain.on('newDownloadTask', async (ev, data) => {
  // 判断下载目录是否存在
  if (!fs.existsSync(DOWNLOAD_PATH)) {
    // 未获取到默认下载目录
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        type: 'notDownloadFolder',
        msg: JSON.stringify(data)
      })
    )
  } else {
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        type: 'showState',
        msg: '检测到下载任务,正在创建...'
      })
    )
    const task = new DownloadTask(data)
    if (!data.id && data.autoPlay) {
      await task.start()
    }
    if (task.data) {
      downloadTasks.push(task.data)
      updateTask(task)
    }
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        type: 'showState',
        msg: ''
      })
    )
  }
})
// 暂停任务
ipcMain.on('pauseTask', (ev, id) => {
  const index = downloadTasks.findIndex((item) => item.id == id)
  if (index === -1) {
    ev.returnValue = false
  } else {
    const task = new DownloadTask(downloadTasks[index])
    task.pause()
    ev.returnValue = true
  }
})
ipcMain.on('pauseAllTask', (ev) => {
  for (let i = 0; i < downloadTasks.length; i++) {
    const item = downloadTasks[i]
    if (item.state == 'download') {
      const task = new DownloadTask(item)
      task.pause()
    }
  }
  ev.returnValue = true
})
// 开始任务
ipcMain.on('startTask', (ev, id) => {
  const index = downloadTasks.findIndex((item) => item.id == id)
  if (index === -1) {
    ev.returnValue = false
  } else {
    const task = new DownloadTask(downloadTasks[index])
    task.start()
    ev.returnValue = true
  }
})
ipcMain.on('startAllTask', (ev) => {
  for (let i = 0; i < downloadTasks.length; i++) {
    const item = downloadTasks[i]
    if (item.state != 'success' && item.state != 'download') {
      const task = new DownloadTask(item)
      task.start()
    }
  }
  ev.returnValue = true
})
ipcMain.on('cleanTask', (ev, id) => {
  const item = downloadTasks.find((item) => item.id == id)
  if (item.state == 'download') {
    const task = new DownloadTask(item)
    task.pause()
  }
  downloadTasks = downloadTasks.filter((item) => item.id != id)
  updateTask()
  ev.returnValue = true
})
ipcMain.on('cleanAll', (ev) => {
  for (let i = 0; i < downloadTasks.length; i++) {
    const item = downloadTasks[i]
    if (item.state == 'download') {
      const task = new DownloadTask(item)
      task.pause()
    }
  }
  downloadTasks = downloadTasks.filter((item) => item.state == 'success')
  updateTask()
  ev.returnValue = true
})
ipcMain.on('openPath', (ev, path) => {
  if (fs.existsSync(path)) {
    shell.openPath(path)
    ev.returnValue = { state: true }
  } else {
    ev.returnValue = { state: false, msg: '目录不存在!' }
  }
})
ipcMain.on('openPathByTaskId', (ev, id) => {
  const item = downloadTasks.find((item) => item.id == id)
  if (fs.existsSync(item.savePath)) {
    if (item.md5) {
      shell.showItemInFolder(item.savePath)
    } else {
      shell.openPath(item.savePath)
    }
  } else {
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        type: 'warning',
        msg: '本地文件目录不存在或者已被删除'
      })
    )
  }
  ev.returnValue = true
})
ipcMain.on('getDownloadPath', async (ev) => {
  ev.returnValue = DOWNLOAD_PATH
})
ipcMain.on('setDownloadPath', async (ev, data) => {
  if (fs.existsSync(data.path)) {
    // 设置下载目录
    DOWNLOAD_PATH = data.path
    DownloadConfig.set(DOWNLOAD_SAVE_FOLDER, data.path)
    if (data.dataStr) {
      // 如果有任务,开始下载任务
      mainWindow.webContents.send(
        'showMessage',
        JSON.stringify({
          type: 'showState',
          msg: '检测到下载任务,正在创建...'
        })
      )
      const taskData = JSON.parse(data.dataStr)
      const task = new DownloadTask(taskData)
      if (!taskData.id && taskData.autoPlay) {
        await task.start()
      }
      if (task.data) {
        downloadTasks.push(task.data)
        updateTask(task)
      }
      mainWindow.webContents.send(
        'showMessage',
        JSON.stringify({
          type: 'showState',
          msg: ''
        })
      )
    }
    ev.returnValue = { state: true }
  } else {
    ev.returnValue = { state: false, msg: '目录不存在!' }
  }
})
electron/exportTask.ts
New file
@@ -0,0 +1,930 @@
import { ipcMain } from 'electron'
import fs from 'fs'
import os from 'os'
import moment from 'moment'
import path from 'path'
import { uuid } from './toolClass'
const xlsx = require('node-xlsx')
const Store = require('electron-store')
const ExportTasks = new Store('ExportTasks')
const DownloadConfig = new Store('DownloadConfig')
const EXPORT_TASKS = 'ExportTasks'
const DOWNLOAD_SAVE_FOLDER = 'FileDownloadSaveFolder'
// 默认路径
const homeDir = os.homedir()
let DOWNLOAD_PATH = ''
const FileDownloadSaveFolder = DownloadConfig.get(DOWNLOAD_SAVE_FOLDER)
if (FileDownloadSaveFolder) {
  DOWNLOAD_PATH = FileDownloadSaveFolder
} else {
  if (os.platform() === 'win32') {
    // Windows 系统下的 "Downloads" 文件夹路径
    DOWNLOAD_PATH = path.join(homeDir, 'Downloads')
  } else {
    // macOS 和 Linux 系统下的 "Downloads" 文件夹路径
    DOWNLOAD_PATH = path.join(homeDir, 'Downloads')
  }
}
let mainWindow: any = null
let request: any = null
let token = ''
let taskList: any = []
export const init = (win, req) => {
  mainWindow = win
  request = req
  loadTasks()
}
type CmsItemProps = {
  path: string
  storeId?: number
  repositoryId?: number
  type?: string
  paging: {
    Start: number
    Size: number
  }
  sort?: Record<string, any>
  linkTypes?: string[]
  fields?: {
    [key: string]: []
  }
  filters?: {
    [key: string]: string[]
  }
  subQuery?: {}
  keyword?: string
  itemId?: number
  resType?: any
}
class ExportTask {
  // 初始状态
  static INIT = 0
  // 处理中
  static LOADING = 1
  // 暂停中
  static PAUSE = 2
  // 完成
  static FINISHED = 3
  // 失败
  static FAILED = 4
  public data
  constructor(data) {
    this.initData(data)
    this.start()
  }
  // 新建任务
  initData = (data) => {
    if (data.id) {
      this.data = { ...data }
    } else {
      this.data = {
        id: uuid(8),
        startDate: moment().format('YYYY-MM-DD HH:mm:ss'),
        endDate: '',
        bookTpl: null, // 图书模板(Excel模板)
        fileTpl: null, // 文件导出模板
        tplInfo: data.tplInfo,
        bookInfo: data.bookInfo, // 导出的图书
        typeKeyMap: null,
        state: ExportTask.INIT, // 任务状态
        fileTypeList: [], // 需下载文件类型
        tabHeader: [], // Excel表头数据
        tabData: [], // Excel表头数据
        temporaryFolderPath: '', // 临时文件夹目录
        bookFolderPath: null, // 图书文件夹path
        fileMd5List: {}, // 图书文件
        taskPath: '',
        progressInfo: {
          // 进程信息
          createTask: ExportTask.INIT,
          temporaryFolder: ExportTask.INIT,
          handleFile: {},
          handleBook: {
            state: ExportTask.INIT,
            info: {
              success: 0,
              failed: 0,
              total: 0
            }
          },
          handleFolder: {},
          handleExcel: ExportTask.INIT
        }
      }
    }
    this.preloadData(this.data)
  }
  // 开始任务
  start = () => {
    const task = this.data
    task.state = ExportTask.LOADING
    // 请求数据
    if (
      !task.fileTypeList ||
      !task.fileTypeList.length ||
      !task.tabHeader ||
      !task.tabHeader.length
    ) {
      this.preloadData(task)
    } else {
      // 临时文件夹
      if (!task.temporaryFolderPath) {
        this.createTemporaryFolder(task)
      }
    }
  }
  // 预处理数据
  preloadData = (task) => {
    // 先请求相关数据,并预处理
    task.progressInfo.createTask = ExportTask.LOADING
    this.getTplInfo(task.tplInfo).then((data) => {
      console.log(data)
      if (data) {
        let fileTypeList = []
        for (let i = 0; i < task.fileTpl.fileData.length; i++) {
          const folderItem = task.fileTpl.fileData[i]
          fileTypeList = fileTypeList.concat(folderItem.fileType)
        }
        task.fileTypeList = fileTypeList
        // 处理图书模板创建excel表头
        const tabHeader: any = []
        for (let j = 0; j < task.bookTpl.fieldKeys.length; j++) {
          const field = task.bookTpl.fieldKeys[j]
          tabHeader.push({
            key: field.typeField.refCode,
            label: field.title
          })
        }
        task.tabHeader = tabHeader
        task.progressInfo.handleBook.info.total = task.bookInfo.length
        this.sendProgressInfo()
        // 临时文件夹
        if (!task.temporaryFolderPath) {
          this.createTemporaryFolder(task)
        }
      }
    })
  }
  // 获取模板信息
  getTplInfo = (data) => {
    if (data.type == 'user') {
      return request({
        url: '/identity/User/GetUserKey',
        method: 'post',
        headers: {
          Authorization: 'bearer ' + token
        },
        data: {
          userKeyList: [{ storeKey: 'personImportTemplate', key: data.key }]
        }
      })
        .then((res) => {
          if (res && res.length > 0) {
            try {
              return JSON.parse(res[0].value)
            } catch (error) {
              return null
            }
          }
        })
        .catch((error) => {
          console.log(error)
        })
    }
    if (data.type == 'sys') {
      return request({
        url: '/identity/Sys/GetSysKey',
        method: 'post',
        headers: {
          Authorization: 'bearer ' + token
        },
        data: {
          userKeyList: [{ storeKey: 'sysImportTemplate', key: data.key }]
        }
      }).then((res) => {
        if (res && res.length > 0) {
          try {
            return JSON.parse(res[0].value)
          } catch (error) {
            return null
          }
        }
      })
    }
  }
  // 创建临时文件夹
  createTemporaryFolder = (task) => {
    task.progressInfo.temporaryFolder = ExportTask.LOADING
    this.sendProgressInfo()
    // 临时文件夹名称
    const folderName = 'TF' + moment().valueOf()
    // 创建临时文件夹
    const folderPath = path.join(DOWNLOAD_PATH, folderName)
    fs.mkdirSync(folderPath, { recursive: true })
    task.temporaryFolderPath = folderPath
    // 更新状态
    task.progressInfo.temporaryFolder = ExportTask.FINISHED
    this.sendProgressInfo()
    this.createBookFolder(task)
  }
  // 创建图书文件夹
  createBookFolder = (task) => {
    if (!task.bookFolderPath) task.bookFolderPath = {}
    // 在临时文件夹中创建不同书籍文件夹
    for (let i = 0; i < task.bookInfo.length; i++) {
      const book = task.bookInfo[i]
      if (book.ISBN) book.ISBN = book.ISBN.replace(new RegExp('-', 'gm'), '')
      const folderPath = path.join(
        task.temporaryFolderPath,
        book.name + (book.ISBN ? '(' + book.ISBN + ')' : '')
      )
      fs.mkdirSync(folderPath, { recursive: true })
      task.bookFolderPath[book.id] = folderPath
      task.fileMd5List[book.id] = []
      task.progressInfo.handleFile[book.id] = {
        state: ExportTask.INIT,
        info: {
          success: 0,
          failed: 0,
          total: 0
        }
      }
      task.progressInfo.handleFolder[book.id] = ExportTask.INIT
      this.sendProgressInfo()
      // 获取图书信息
      this.getBookData(book, task)
    }
  }
  // 获取图书信息
  getBookData(book, task) {
    task.progressInfo.handleBook.state = ExportTask.LOADING
    this.sendProgressInfo()
    // const idPath = book.idPath.slice(0, book.idPath.lastIndexOf('\\'))
    // this.getCmsItem(book.storeId, book.repoId, idPath, book.id, task.typeKeyMap)
    //   .then((result) => {
    //     const bookInfo = result.data.data[0].datas[0].datas
    //     for (const key in bookInfo) {
    //       if (bookInfo[key]) {
    //         try {
    //           bookInfo[key] = JSON.parse(bookInfo[key])[0].Value
    //         } catch (error) {
    //           console.log(error)
    //         }
    //       }
    //     }
    //     task.tabData.push(bookInfo)
    //     task.progressInfo.handleBook.info.success++
    //     // 获取文件信息
    //     this.getFileData(book, task)
    //     this.sendProgressInfo()
    //     if (task.progressInfo.handleBook.info.success == task.progressInfo.handleBook.info.total) {
    //       task.progressInfo.handleBook.state = ExportTask.FINISHED
    //       this.sendProgressInfo()
    //       this.exportExcel(task)
    //     }
    //   })
    //   .catch((e) => {
    //     console.error(e)
    //   })
  }
  // 获取文件信息
  getFileData(book, task) {
    task.progressInfo.handleFile[book.id].state = ExportTask.LOADING
    this.sendProgressInfo()
    // let requestIndex = 0
    // let bookFile = []
    for (let i = 0; i < book.getFilePaths.length; i++) {
      // const filePath = book.getFilePaths[i]
      // this.getCmsItem({
      //   repo: {
      //     storeId: book.storeId,
      //     repoId: book.repoId
      //   },
      //   parent: { idPath: filePath },
      //   paging: { Start: 0, Size: 10 },
      //   sysTypes: ['CmsFile'],
      //   recursion: true
      // })
      //   .then((result) => {
      //     bookFile = bookFile.concat(result.datas)
      //     requestIndex++
      //     if (requestIndex == book.getFilePaths.length) {
      //       this.handleBookFile(book, task, bookFile)
      //     }
      //   })
      //   .catch((e) => {
      //     console.error(e)
      //   })
    }
  }
  // 处理图书文件
  handleBookFile = (book, task, bookFile) => {
    // 获取到图书下所有文件信息
    const bookFileList = bookFile.filter((item) => {
      return item.datas.LinkFile && task.fileTypeList.indexOf(item.type) > -1
    })
    // 需判断去重,同样的文件只下载一个(如果同样的文件移动两次,第二次会报错;如果是要移动至多个文件夹,移动时能兼容)
    const newBookFileList: any = []
    for (let i = 0; i < bookFileList.length; i++) {
      const exists = newBookFileList.filter((item) => {
        return bookFileList[i].linkFile[0].File.Md5 == item.linkFile[0].File.Md5
      })
      if (!exists.length) {
        newBookFileList.push(bookFileList[i])
      }
    }
    task.progressInfo.handleFile[book.id].info.total = newBookFileList.length
    this.sendProgressInfo()
    if (newBookFileList && newBookFileList.length) {
      for (let i = 0; i < newBookFileList.length; i++) {
        const item = newBookFileList[i]
        const linkInfo = JSON.parse(item.datas.LinkFile)[0]
        linkInfo.File.MetaData = JSON.parse(linkInfo.File.MetaData)
        linkInfo.File.itemType = item.type
        task.fileMd5List[book.id].push(linkInfo.File)
        // 调用下载队列进行下载(异步)
        // const downloadPath = path.join(
        //   task.bookFolderPath[book.id],
        //   linkInfo.File.MetaData.fileName
        // )
        // const info = new DownloadInfo(
        //   apps.resAppId,
        //   linkInfo.File.Md5,
        //   0,
        //   linkInfo.File.MetaData.size,
        //   downloadPath
        // )
        // const downloadTask = new DownloadTask(info)
        // downloadTask.start((downloadInfo) => {
        //   task.progressInfo.handleFile[book.id].info.success++
        //   this.sendProgressInfo()
        //   // let isbn = path.basename(path.dirname(downloadInfo.path));
        //   for (let i = 0; i < task.fileMd5List[book.id].length; i++) {
        //     const item = task.fileMd5List[book.id][i]
        //     if (item.Md5 == downloadInfo.md5 && !item.path) {
        //       item.path = downloadInfo.path
        //       break
        //     }
        //   }
        //   if (
        //     task.progressInfo.handleFile[book.id].info.success ==
        //     task.progressInfo.handleFile[book.id].info.total
        //   ) {
        //     task.progressInfo.handleFile[book.id].state = ExportTask.FINISHED
        //     this.sendProgressInfo()
        //     this.handleBookFileMerge(task, book.id)
        //   }
        // })
      }
    } else {
      task.progressInfo.handleFile[book.id].state = ExportTask.FINISHED
      task.progressInfo.handleFolder[book.id] = ExportTask.FINISHED
      this.sendProgressInfo()
      this.taskSucess(task)
    }
  }
  // 按文件模板合并图书文件
  handleBookFileMerge = (task, key) => {
    task.progressInfo.handleFolder[key] = ExportTask.LOADING
    this.sendProgressInfo()
    const fileTpl = task.fileTpl
    const typeToFolderList = {}
    if (task.bookTpl.tplType == 1 || !task.bookTpl.tplType) {
      // 按图书导出
      // 创建模板对应的文件夹
      for (let i = 0; i < fileTpl.fileData.length; i++) {
        const item = fileTpl.fileData[i]
        // 创建文件夹
        const folderPath = path.join(task.bookFolderPath[key], item.fileName)
        fs.mkdirSync(folderPath, { recursive: true })
        for (let z = 0; z < item.fileType.length; z++) {
          const typeItem = item.fileType[z]
          if (!typeToFolderList[typeItem]) {
            typeToFolderList[typeItem] = []
          }
          typeToFolderList[typeItem].push({
            folderName: item.fileName,
            folderPath: folderPath
          })
        }
      }
    } else if (task.bookTpl.tplType == 2) {
      // 按文件夹导出
      // 创建模板对应的文件夹
      for (let i = 0; i < fileTpl.fileData.length; i++) {
        const item = fileTpl.fileData[i]
        // 创建文件夹
        const folderPath = path.join(task.temporaryFolderPath, item.fileName)
        const stat = fs.existsSync(folderPath)
        if (!stat) {
          fs.mkdirSync(folderPath, { recursive: true })
        }
        for (let z = 0; z < item.fileType.length; z++) {
          const typeItem = item.fileType[z]
          if (!typeToFolderList[typeItem]) {
            typeToFolderList[typeItem] = []
          }
          typeToFolderList[typeItem].push({
            folderName: item.fileName,
            folderPath: folderPath
          })
        }
      }
    }
    // 移动文件对应类型文件至文件夹
    for (let j = 0; j < task.fileMd5List[key].length; j++) {
      let file = task.fileMd5List[key][j]
      if (typeToFolderList[file.itemType] && typeToFolderList[file.itemType].length > 0) {
        // 处理重命名
        file = this.handleRename(task, file, key)
        if (typeToFolderList[file.itemType].length > 1) {
          // 一个类型对应多个文件夹时,先复制后移动
          for (let n = 0; n < typeToFolderList[file.itemType].length; n++) {
            const folder = typeToFolderList[file.itemType][n]
            if (n < typeToFolderList[file.itemType].length - 1) {
              // 复制
              fs.copyFileSync(file.path, path.join(folder.folderPath, file.MetaData.fileName))
            } else {
              // 移动
              fs.renameSync(file.path, path.join(folder.folderPath, file.MetaData.fileName))
            }
          }
        } else {
          // 直接移动
          fs.renameSync(
            file.path,
            path.join(typeToFolderList[file.itemType][0].folderPath, file.MetaData.fileName)
          )
        }
      }
    }
    // 如果按文件夹导出,需删除图书文件夹
    if (task.bookTpl.tplType == 2) {
      try {
        fs.rmdirSync(task.bookFolderPath[key])
      } catch (error) {
        console.log(error)
      }
    }
    task.progressInfo.handleFolder[key] = ExportTask.FINISHED
    this.sendProgressInfo()
    this.taskSucess(task)
  }
  // 处理重命名
  handleRename = (task, file, bookId) => {
    // 获取图书信息
    const bookInfo = task.tabData.filter((item) => {
      return item.Id == bookId
    })
    const renameData = task.fileTpl.renameData
    if (!renameData) {
      return file
    }
    for (let i = 0; i < renameData.length; i++) {
      const renameItem = renameData[i]
      if (renameItem.fileType == file.itemType) {
        let newFileName = ''
        const oldFileName = file.MetaData.fileName
        const suffix = oldFileName.substring(oldFileName.lastIndexOf('.') + 1, oldFileName.length)
        // 拼接前置字符
        if (renameItem.firstTxt) {
          newFileName += renameItem.firstTxt
        }
        // 拼接替换名称
        const field = renameItem.renameField.split('/')[0]
        if (bookInfo.length && bookInfo[0][field]) {
          let fieldTxt = bookInfo[0][field]
          // 处理特殊字符
          const reg = new RegExp('[\\/:*?"<>]', 'g')
          fieldTxt = fieldTxt.replace(reg, '')
          for (let z = 0; z < renameItem.formatTxt.length; z++) {
            const formatItem = renameItem.formatTxt[z]
            if (formatItem == 'nbsp') {
              fieldTxt = fieldTxt.replace(new RegExp(' ', 'gm'), '')
            } else {
              fieldTxt = fieldTxt.replace(new RegExp(formatItem, 'gm'), '')
            }
          }
          newFileName += fieldTxt
        } else {
          const oldName = oldFileName.substring(0, oldFileName.lastIndexOf('.'))
          newFileName += oldName
        }
        // 拼接后置字符
        if (renameItem.lastTxt) {
          newFileName += renameItem.lastTxt
        }
        // 拼接后缀
        newFileName = newFileName + '.' + suffix
        // 返回数据
        file.MetaData.fileName = newFileName
        return file
      }
    }
    return file
  }
  // 导出excel
  exportExcel = (task) => {
    task.progressInfo.handleExcel = ExportTask.LOADING
    this.sendProgressInfo()
    const excelPath = path.join(task.temporaryFolderPath, '图书信息.xls')
    console.log(task)
    const excelData: any = []
    const headerData: any = []
    for (let i = 0; i < task.tabHeader.length; i++) {
      const header = task.tabHeader[i]
      headerData.push(header.label)
    }
    const tableData: any = []
    for (let z = 0; z < task.tabData.length; z++) {
      const tabItem = task.tabData[z]
      const itemData: any = []
      for (let j = 0; j < task.tabHeader.length; j++) {
        const headerItem = task.tabHeader[j]
        itemData.push(tabItem[headerItem.key])
      }
      tableData.push(itemData)
    }
    excelData.push(headerData)
    excelData.push(...tableData)
    console.log(excelData)
    const buffer = xlsx.build([{ name: 'Sheet', data: excelData }])
    fs.writeFileSync(excelPath, buffer)
    task.progressInfo.handleExcel = ExportTask.FINISHED
    this.sendProgressInfo()
    this.taskSucess(task)
  }
  // 完成
  taskSucess = (task) => {
    let bookFileState = true
    for (const key in task.progressInfo.handleFolder) {
      if (task.progressInfo.handleFolder[key] !== ExportTask.FINISHED) {
        bookFileState = false
        break
      }
    }
    if (bookFileState && task.progressInfo.handleExcel === ExportTask.FINISHED) {
      // const obj = path.parse(task.temporaryFolderPath)
      // const creactTime = parseInt(obj.base.replace('TF', ''))
      const newFolderName = task.bookTpl.name + '_' + task.key
      const newPath = path.join(path.dirname(task.temporaryFolderPath), newFolderName)
      fs.renameSync(task.temporaryFolderPath, newPath)
      task.taskPath = newPath
      task.endDate = moment().format('YYYY-MM-DD HH:mm:ss')
      task.state = ExportTask.FINISHED
      this.sendProgressInfo()
    }
  }
  // 发送进度信息
  sendProgressInfo = () => {}
  // 获取用于显示的数据
  // getShowData = () => {
  //   const showDataList = []
  //   for (let i = 0; i < this.taskList.length; i++) {
  //     const taskItem = this.taskList[i]
  //     const obj = {
  //       id: taskItem.id,
  //       bookTplName: taskItem.bookTpl.name,
  //       fileTplName: taskItem.fileTpl.name,
  //       bookInfo: {
  //         name: taskItem.bookInfo.name,
  //         ISBN: taskItem.bookInfo.ISBN
  //       },
  //       state: taskItem.state,
  //       progressInfo: taskItem.progressInfo,
  //       startDate: taskItem.startDate,
  //       endDate: taskItem.endDate,
  //       taskPath: taskItem.taskPath
  //     }
  //     showDataList.unshift(obj)
  //   }
  //   return showDataList
  // }
  getCmsItem = ({
    path,
    storeId,
    repositoryId,
    type,
    paging,
    sort,
    linkTypes,
    fields,
    filters,
    subQuery,
    keyword,
    itemId,
    resType
  }: CmsItemProps) => {
    const query = {
      AccessControl: {
        Path: path,
        StoreId: storeId + '',
        RepositoryId: repositoryId + '',
        Type: type ? type : '\\'
      },
      PageQuery: paging,
      SortQuery: sort ? [sort] : [],
      CreateDate: [],
      Description: [],
      Name: [],
      RefCode: [],
      Type: [],
      TypeId: [],
      State: [],
      Tag: [],
      LinkDepartment: [],
      LinkOrg: [],
      LinkInfo: [],
      LinkId: [],
      LinkOrder: [],
      LinkParentId: [],
      LinkFile: [],
      LinkType: linkTypes ?? [],
      LinkStore: [],
      LinkRepository: [],
      LinkPath: [],
      LinkAppId: [],
      Creator: [],
      ...fields,
      ...filters,
      ...subQuery
    }
    if (keyword) {
      delete query.Name
      query['Name*'] = [keyword]
    }
    if (itemId) query['Id='] = [`${itemId}`]
    const body = { query: JSON.stringify({ Query: [{ Q1: query }] }) }
    return request({
      url: '/resource/ResourceItem/QueryCmsItem',
      method: 'post',
      headers: {
        Authorization: 'bearer ' + token
      },
      data: body
    }).then((res) => {
      if (res && res.length > 0) {
        const data = res[0]
        const datas = this.handleCmsItemListRequestData(
          data.datas,
          fields,
          path,
          storeId,
          repositoryId
        )
        return { datas, total: data.totalCount }
      } else {
        return { datas: [], total: 0 }
      }
    })
  }
  handleCmsItemListRequestData = (datas, fields, path?, storeId?, repositoryId?) => {
    const dataList = []
    for (let i = 0; i < datas.length; i++) {
      const item = datas[i]
      const _fields = {}
      const _datas = []
      if (fields != null) {
        for (let fieldKey in fields) {
          // 兼容筛选条件的字段值获取,因为后台筛选和取值只能传一个,都会返回值
          fieldKey = fieldKey.replace(/[!=<>*]/g, '')
          if (item.datas[fieldKey]) {
            let values = []
            if (typeof item.datas[fieldKey] == 'string') {
              values = JSON.parse(item.datas[fieldKey])
            } else {
              values = item.datas[fieldKey]
            }
            if (values?.length > 0) {
              // 用字段名处理返回的字段值
              if (values[0].Value) {
                _fields[fieldKey] = values[0].Value
                values[0].sequenceNum = values[0].SequenceNum
              }
              // 兼容处理数据返回的key是CmsItemData
              // if (values[0].CmsItemData) {
              //   _fields[fieldKey] = values[0].CmsItemData.Value;
              //   values[0].sequenceNum = values[0].CmsItemData.SequenceNum;
              // }
              item.datas[fieldKey] = values[0]
              if (values?.length > 1) {
                const isFile = values.find((citem) => citem.FileList?.length > 0)
                const dataItems = this.deduplicateArray(values, 'FieldId')
                if (!isFile) {
                  _datas.push(dataItems[0])
                } else {
                  const customFile = {
                    customFileList: values,
                    name: fieldKey,
                    md5: _fields[fieldKey]
                  }
                  _datas.push(customFile)
                }
              } else {
                _datas.push(values[0])
              }
            }
          }
        }
      }
      if (item.datas.LogQuery) {
        item.datas.LogQuery = JSON.parse(item.datas.LogQuery)
      }
      const subDatas = {}
      if (item.subDatas) {
        for (let subData of item.subDatas) {
          const tag = subData.queryTag.replace('Query', '')
          subDatas[tag] = subData.datas
        }
      }
      dataList.push({
        ...item,
        id: item.id,
        name: item.datas.Name,
        icon: item.datas.Icon,
        storeId: storeId,
        repositoryId: repositoryId,
        refCode: item.datas.RefCode === '[]' ? null : item.datas.RefCode,
        state: item.datas.State,
        type: item.datas.Type,
        tag: item.datas.Tag,
        creator: item.datas.Creator ? JSON.parse(item.datas.Creator) : undefined,
        linkType: item.datas.LinkType,
        childrenCount: parseInt(item.datas.ChildrenCount ?? '0'),
        childrenFolderCount: parseInt(item.datas.ChildrenFolderCount ?? '0'),
        childrenChannelCount: parseInt(item.datas.ChildrenChannelCount ?? '0'),
        childrenCmsItemCount: parseInt(item.datas.ChildrenCmsItemCount ?? '0'),
        childrenFileCount: parseInt(item.datas.ChildrenFileCount ?? '0'),
        createDate: moment(item.datas.CreateDate).format('YYYY-MM-DD HH:mm:ss'),
        description: item.datas.Description,
        sysType: item.datas.SysType,
        idPath: path + '\\' + item.id,
        typeId: parseInt(item.datas.TypeId),
        linkAppId: item.datas.linkAppId,
        linkFile: JSON.parse(item.datas.LinkFile ?? '[]'),
        linkInfo: item.datas.LinkInfo ? JSON.parse(item.datas.LinkInfo) : [],
        linkPath: item.datas.LinkPath ?? null,
        linkOrg: item.datas.linkOrg ? JSON.parse(item.datas.linkOrg) : [],
        linkDepartment: item.datas.linkDepartment ? JSON.parse(item.datas.linkDepartment) : [],
        ..._fields,
        datas: item.datas,
        fieldList: _datas,
        subDatas
      })
    }
    return dataList
  }
  //数组去重
  deduplicateArray = (arr, idKey) => {
    const seen = {}
    const deduplicatedArray = arr.filter((item) => {
      const id = item[idKey]
      if (!seen[id]) {
        seen[id] = true
        return true
      }
      return false
    })
    return deduplicatedArray
  }
}
// 本地缓存加载任务列表
const loadTasks = () => {
  const txt = ExportTasks.get(EXPORT_TASKS, '[]')
  try {
    taskList = JSON.parse(txt)
  } catch (error) {
    taskList = []
  }
}
// 本地缓存记录任务列表
const saveTasks = () => {
  ExportTasks.set(EXPORT_TASKS, JSON.stringify(taskList))
}
const updateTask = (data?) => {
  // 调用渲染进程
  if (data) {
    mainWindow.webContents.send('exportTaskChange', JSON.stringify(data))
    const index = taskList.findIndex((item) => item.id == data.id)
    taskList[index] = data
  } else {
    mainWindow.webContents.send('exportTaskChange')
  }
  saveTasks()
}
// 获取任务列表
ipcMain.on('getExportTasks', (ev, data) => {
  // 页面会先获取任务列表,在获取列表时将token挂载
  mainWindow.webContents
    .executeJavaScript('localStorage.getItem("token");', true)
    .then((result) => {
      token = result
    })
  const returnData = JSON.parse(JSON.stringify(taskList))
  ev.returnValue = returnData
})
// 新建任务
ipcMain.on('newExportTask', async (ev, data) => {
  // 判断下载目录是否存在
  if (!fs.existsSync(DOWNLOAD_PATH)) {
    // 未获取到默认下载目录
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        showType: 'ExportTask',
        type: 'notDownloadFolder',
        msg: JSON.stringify(data)
      })
    )
  } else {
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        showType: 'ExportTask',
        type: 'showState',
        msg: '检测到导出任务,正在创建...'
      })
    )
    const task = new ExportTask(data)
    if (!data.id && data.autoPlay) {
      await task.start()
    }
    if (task.data) {
      taskList.push(task.data)
      updateTask(task)
    }
    mainWindow.webContents.send(
      'showMessage',
      JSON.stringify({
        showType: 'ExportTask',
        type: 'showState',
        msg: ''
      })
    )
  }
})
// 删除任务
ipcMain.on('cleanExportTask', (ev, id) => {
  // const item = taskList.find((item) => item.id == id)
  // if (item.state == 'download') {
  //   const task = new DownloadTask(item)
  //   task.pause()
  // }
  taskList = taskList.filter((item) => item.id != id)
  updateTask()
  ev.returnValue = true
})
// 清空任务
ipcMain.on('cleanAllExportTask', (ev) => {
  // for (let i = 0; i < taskList.length; i++) {
  //   const item = taskList[i]
  //   if (item.state == 'download') {
  //     const task = new DownloadTask(item)
  //     task.pause()
  //   }
  // }
  taskList = []
  updateTask()
  ev.returnValue = true
})
electron/main.ts
New file
@@ -0,0 +1,176 @@
import { app, BrowserWindow, globalShortcut, dialog, ipcMain } from 'electron'
import path from 'path'
// import fs from 'fs'
import { init as initDownloadTask } from './downloadTask'
// import { init as initExportTask } from './exportTask'
import { checkUpdate } from './update'
import axios from 'axios'
import { ctx } from './config'
// 用于本地测试更新
// 手动设置为已打包
Object.defineProperty(app, 'isPackaged', {
  get() {
    return true
  }
})
let myWindow: any = null
const request = axios.create()
const development = process.env.NODE_ENV == 'development' ? true : false
const createWindow = () => {
  const win = new BrowserWindow({
    minWidth: development ? 1800 : 1300,
    minHeight: development ? 1000 : 800,
    width: development ? 1400 : 1300,
    height: development ? 1000 : 800,
    //窗口是否在屏幕居中. 默认值为 false
    center: true,
    //窗口是否在创建时显示。 默认值为 true。
    show: true,
    title: '数字教材阅读器',
    icon: 'public/favicon.ico',
    webPreferences: {
      // nodeIntegration: true,
      // nodeIntegrationInWorker: true,
      // contextIsolation: true,
      // sandbox: false,
      preload: path.join(__dirname, 'preload.js')
    }
  })
  win.setMenu(null)
  if (development) {
    const elePath = path.join(__dirname, '../node_modules/electron')
    require('electron-reload')('../', {
      electron: require(elePath)
    })
    win.loadURL('http://localhost:8005')
    win.webContents.openDevTools()
  } else {
    win.loadURL(path.join(__dirname, 'index.html'))
  }
  globalShortcut.register('CommandOrControl+Shift+i', function () {
    win.webContents.openDevTools()
  })
  // 解决应用启动白屏问题
  win.on('ready-to-show', () => {
    win.show()
    win.focus()
  })
  request.interceptors.request.use((config) => {
    if (config.url && config.url.indexOf('http') == -1) {
      config.url = ctx + config.url
    }
    return config
  })
  // 响应拦截器
  request.interceptors.response.use(
    (response) => {
      return response.data
    },
    (error) => {
      if (error.response && error.response.status == 401) {
        win.webContents.send('logout')
      } else {
        if (error.response && error.response.data && error.response.data.error) {
          console.error(error.response.data.error.msg)
        } else {
          console.error('请求发生错误')
        }
        // dialog.showMessageBox({
        //   type: 'error',
        //   message: '错误',
        //   detail: JSON.stringify(error.response)
        // })
      }
      return Promise.reject(error)
    }
  )
  // 业务主体
  // 下载器
  initDownloadTask(win, request)
  // initExportTask(win, request)
  checkUpdate(win, ipcMain)
  ipcMain.on('openSelectFileOrFolderDialog', (ev, opt) => {
    const path = dialog.showOpenDialogSync(win, opt)
    ev.returnValue = path
  })
  return win
}
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  // 如果获取失败,说明已经有实例在运行了,直接退出
  app.quit()
} else {
  app.whenReady().then(() => {
    myWindow = createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
    // 注册自定义协议
    const agreement = 'DigitalTextbookReader' // 开发环境
    app.removeAsDefaultProtocolClient(agreement) // 每次运行都删除自定义协议 然后再重新注册
    // 开发模式下在window运行需要做兼容
    if (development) {
      // 设置electron.exe 和 app的路径
      app.setAsDefaultProtocolClient(agreement, process.execPath, [
        path.resolve(process.argv[1]),
        '--'
      ])
    }
    // 验证是否为自定义协议的链接
    const AGREEMENT_REGEXP = new RegExp(`^${agreement}://`)
    // mac唤醒应用 会激活open-url事件 在open-url中判断是否为自定义协议打开事件
    app.on('open-url', (event, url) => {
      const isProtocol = AGREEMENT_REGEXP.test(url)
      if (isProtocol) {
        dialog.showMessageBox({
          type: 'info',
          message: 'Mac protocol 自定义协议打开',
          detail: `自定义协议链接:${url}`
        })
      }
    })
    // window系统下唤醒应用会激活second-instance事件 它在ready执行之后才能被监听
    app.on('second-instance', (event, argv) => {
      if (myWindow) {
        if (myWindow.isMinimized()) myWindow.restore()
        myWindow.focus()
      }
      if (process.platform === 'win32') {
        // 开发阶段,跳过前两个参数(`electron.exe .`)
        // 打包后,跳过第一个参数(`myapp.exe`)
        const offset = app.isPackaged ? 1 : 2
        let url: any = argv.find((arg, i) => i >= offset && arg.startsWith(agreement))
        if (url) {
          url = url.replace(`${agreement}://`, '')
          url = url.substr(0, url.length - 1)
        }
        myWindow.webContents.send('openUrl', url)
      }
    })
  })
  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })
}
electron/preload.ts
New file
@@ -0,0 +1,131 @@
import { contextBridge, ipcRenderer } from 'electron'
//桥接渲染进程与主进程
const newDownloadTask = (data) => {
  ipcRenderer.send('newDownloadTask', data)
}
const getDownloadTasks = () => {
  return ipcRenderer.sendSync('getDownloadTasks')
}
const onDownloadTaskChange = (callback) => {
  ipcRenderer.on('downloadTaskChange', (ev, data) => {
    if (data) {
      callback(JSON.parse(data))
    } else {
      callback()
    }
  })
}
const onShowMessage = (callback) => {
  ipcRenderer.on('showMessage', (ev, data) => {
    callback(JSON.parse(data))
  })
}
const onOpenUrl = (callback) => {
  ipcRenderer.on('openUrl', (ev, data) => {
    callback(data)
  })
}
const onLogout = (callback) => {
  ipcRenderer.on('logout', (ev, data) => {
    callback(data)
  })
}
const cleanTask = (data) => {
  ipcRenderer.send('cleanTask', data)
}
const cleanAll = () => {
  ipcRenderer.send('cleanAll')
}
const pauseTask = (data) => {
  ipcRenderer.send('pauseTask', data)
}
const pauseAllTask = () => {
  ipcRenderer.send('pauseAllTask')
}
const startTask = (data) => {
  ipcRenderer.send('startTask', data)
}
const startAllTask = () => {
  ipcRenderer.send('startAllTask')
}
const openPath = (data) => {
  return ipcRenderer.sendSync('openPath', data)
}
const openPathByTaskId = (data) => {
  ipcRenderer.send('openPathByTaskId', data)
}
const openSelectFileOrFolderDialog = (data) => {
  return ipcRenderer.sendSync('openSelectFileOrFolderDialog', data)
}
const getDownloadPath = () => {
  return ipcRenderer.sendSync('getDownloadPath')
}
const setDownloadPath = (path, dataStr) => {
  return ipcRenderer.sendSync('setDownloadPath', { path, dataStr })
}
const onUpdateDownloadProgress = (callback) => {
  ipcRenderer.on('updateDownloadProgress', (ev, data) => {
    callback(data)
  })
}
const onUpdateDownloadSuccess = (callback) => {
  ipcRenderer.on('updateDownloadSuccess', (ev) => {
    callback()
  })
}
const updateApp = () => {
  ipcRenderer.send('updateApp')
}
// 发行导出
const onExportTaskChange = (callback) => {
  ipcRenderer.on('exportTaskChange', (ev, data) => {
    if (data) {
      callback(JSON.parse(data))
    } else {
      callback()
    }
  })
}
const newExportTask = (data) => {
  ipcRenderer.send('newExportTask', data)
}
const getExportTasks = () => {
  return ipcRenderer.sendSync('getExportTasks')
}
const cleanExportTask = (data) => {
  ipcRenderer.send('cleanExportTask', data)
}
const cleanAllExportTask = () => {
  ipcRenderer.send('cleanAllExportTask')
}
contextBridge.exposeInMainWorld('electronAPI', {
  newDownloadTask,
  getDownloadTasks,
  onDownloadTaskChange,
  onOpenUrl,
  onShowMessage,
  onLogout,
  cleanTask,
  cleanAll,
  pauseTask,
  pauseAllTask,
  startTask,
  startAllTask,
  openPath,
  openPathByTaskId,
  openSelectFileOrFolderDialog,
  getDownloadPath,
  setDownloadPath,
  onUpdateDownloadProgress,
  onUpdateDownloadSuccess,
  updateApp,
  onExportTaskChange,
  newExportTask,
  getExportTasks,
  cleanExportTask,
  cleanAllExportTask,
})
electron/toolClass.ts
New file
@@ -0,0 +1,30 @@
export function uuid(len = 32, radix = 16) {
  const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
  let uuid = [],
    i;
  radix = radix || chars.length;
  if (len) {
    // Compact form
    for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
  } else {
    // rfc4122, version 4 form
    let r;
    // rfc4122 requires these characters
    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
    uuid[14] = '4';
    // Fill in random data.  At i==19 set the high bits of clock sequence as
    // per rfc4122, sec. 4.1.5
    for (i = 0; i < 36; i++) {
      if (!uuid[i]) {
        r = 0 | (Math.random() * 16);
        uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r];
      }
    }
  }
  return uuid.join('');
}
electron/update.ts
New file
@@ -0,0 +1,28 @@
import { autoUpdater } from 'electron-updater'
import { downloaderFileCtx } from './config'
let mainWin = null
export const checkUpdate = (win, ipcMain) => {
  mainWin = win
  autoUpdater.autoDownload = true // 自动下载
  autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装
  // 检测是否有更新包并通知
  // 测试下载更新
  autoUpdater.setFeedURL(downloaderFileCtx)
  autoUpdater.checkForUpdates()
  autoUpdater.on('download-progress', (prog) => {
    mainWin.webContents.send('updateDownloadProgress', {
      speed: Math.ceil(prog.bytesPerSecond / 1000), // 网速
      percent: Math.ceil(prog.percent) // 百分比
    })
  })
  autoUpdater.on('update-downloaded', (info) => {
    mainWin.webContents.send('updateDownloadSuccess')
    // 下载完成后强制用户安装,不推荐
    // autoUpdater.quitAndInstall();
  })
  ipcMain.on('updateApp', () => {
    autoUpdater.quitAndInstall()
  })
}
env.d.ts
New file
@@ -0,0 +1 @@
/// <reference types="vite/client" />
index.html
New file
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';"> -->
    <title>数字教材阅读器</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
package.json
New file
@@ -0,0 +1,107 @@
{
  "name": "DigitalTextbookReader",
  "version": "1.0.1",
  "private": true,
  "main": "electron-commonJS/main.js",
  "scripts": {
    "ele-ts-build": "tsc --project tsconfig.electron.json",
    "hot-update-ts": "gulp watch:ts",
    "hot-update-electron": "gulp watch:electron",
    "dev": "SET NODE_ENV=development&& yarn ele-ts-build && vite",
    "build": "yarn ele-ts-build && vite build && electron-builder",
    "build_mac": "yarn ele-ts-build && vite build && electron-builder --mac",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "axios": "^1.6.2",
    "cross-env": "^7.0.3",
    "electron-connect": "^0.6.3",
    "electron-reload": "^2.0.0-alpha.1",
    "electron-store": "^8.1.0",
    "electron-updater": "^6.1.7",
    "element-plus": "^2.4.3",
    "less": "^4.2.0",
    "less-loader": "^11.1.3",
    "moment": "^2.29.4",
    "node-xlsx": "^0.23.0",
    "pinia": "^2.1.7",
    "style-resources-loader": "^1.5.0",
    "vite-plugin-electron": "^0.15.5",
    "vue": "^3.3.10",
    "vue-cli-plugin-style-resources-loader": "^0.1.5",
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.3.3",
    "@tsconfig/node18": "^18.2.2",
    "@types/node": "^18.19.2",
    "@vitejs/plugin-vue": "^4.5.1",
    "@vue/eslint-config-prettier": "^8.0.0",
    "@vue/eslint-config-typescript": "^12.0.0",
    "@vue/tsconfig": "^0.4.0",
    "electron": "21.4.4",
    "electron-builder": "^24.9.1",
    "electron-updater": "^6.1.7",
    "eslint": "^8.49.0",
    "eslint-plugin-vue": "^9.17.0",
    "npm-run-all2": "^6.1.1",
    "prettier": "^3.0.3",
    "typescript": "~5.2.0",
    "vite": "^5.0.5",
    "vue-tsc": "^1.8.25"
  },
  "build": {
    "appId": "DigitalTextbookReader",
    "productName": "数字教材阅读器",
    "asar": true,
    "directories": {
      "output": "release/"
    },
    "files": [
      "electron-commonJS",
      {
        "from": "dist",
        "to": "electron-commonJS"
      }
    ],
    "mac": {
      "artifactName": "${productName}_${version}.${ext}",
      "target": [
        "dmg"
      ]
    },
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ],
      "icon": "./public/favicon.ico",
      "artifactName": "${productName}_${version}.${ext}"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowToChangeInstallationDirectory": true,
      "deleteAppDataOnUninstall": true,
      "include": "./build/installer.nsh"
    },
    "publish": [
      {
        "provider": "generic",
        "url": "http://182.92.203.7:3007/"
      }
    ],
    "releaseInfo": {
      "releaseNotes": "这里填写具体的版本更新内容"
    }
  }
}
public/favicon.ico
src/App.vue
New file
@@ -0,0 +1,124 @@
<template>
  <div class="updateDownloadInfo" v-if="showUpdateInfo">
    <el-alert
      :title="`检测到新版本,正在下载安装包${
        updateDownloadInfo?.percent ? ',进度:' + updateDownloadInfo?.percent + '%' : '...'
      }`"
      type="info"
      effect="dark"
    />
  </div>
  <RouterView />
</template>
<script setup lang="ts">
import { ref, reactive, inject } from 'vue'
import { RouterView, useRouter } from 'vue-router'
import { useDownloadTask, useExportTask } from '@/store'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const request = inject('request')
const downloadTask = useDownloadTask()
const ExportTask = useExportTask()
window.electronAPI.onLogout((data) => {
  localStorage.clear()
  router.replace({
    path: '/login'
  })
})
window.electronAPI.onOpenUrl((data) => {
  let taskInfo = null
  let downloadInfo = decodeURI(data)
  console.log(downloadInfo, '接受到的taskInfo')
  try {
    taskInfo = JSON.parse(downloadInfo)
  } catch (error) {
    taskInfo = null
  }
  if (taskInfo) {
    router.replace({
      path: '/transmission'
    })
    window.electronAPI.newDownloadTask(taskInfo)
  }
})
// 绑定消息提醒
window.electronAPI.onShowMessage((data) => {
  // 获取到消息后修改全局数据,页面监听全局数据进行变化
  if (data.showType) {
    switch (data.showType) {
      case 'DownloadTask':
        downloadTask.setMsgData(data)
        break
      case 'ExportTask':
        ExportTask.setMsgData(data)
        break
    }
  } else {
    downloadTask.setMsgData(data)
  }
})
// 绑定下载任务变化更新
window.electronAPI.onDownloadTaskChange((task) => {
  downloadTask.setUpdateList()
})
// 绑定导出任务变化更新
window.electronAPI.onExportTaskChange((task) => {
  ExportTask.setUpdateList()
})
const showUpdateInfo = ref(false)
const updateDownloadInfo = ref()
// 监听程序更新下载
window.electronAPI.onUpdateDownloadProgress((data) => {
  showUpdateInfo.value = true
  console.log(data, 'updateDownloadInfo')
  updateDownloadInfo.value = data
})
// 监听程序更新下载完成
window.electronAPI.onUpdateDownloadSuccess((data) => {
  showUpdateInfo.value = false
  ElMessageBox.confirm('检测到新版本,安装包已下载完成,是否立即更新?', '检查更新', {
    confirmButtonText: '更新',
    cancelButtonText: '取消',
    type: 'warning'
  })
    .then(() => {
      window.electronAPI.updateApp()
    })
    .catch(() => {})
})
const token = localStorage.getItem('token')
if (token) {
  request({
    url: '/identity/User/GetCurrentUser',
    method: 'post'
  }).then((res) => {
    // console.log(res)
  })
} else {
  router.replace({
    path: '/login'
  })
}
</script>
<style>
.updateDownloadInfo {
  position: fixed;
  top: 2px;
  left: 82px;
  right: 2px;
  z-index: 999;
}
</style>
src/assets/base.css
New file
@@ -0,0 +1,92 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --vt-c-white: #ffffff;
  --vt-c-white-soft: #f8f8f8;
  --vt-c-white-mute: #f2f2f2;
  --vt-c-black: #181818;
  --vt-c-black-soft: #222222;
  --vt-c-black-mute: #282828;
  --vt-c-indigo: #2c3e50;
  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
  --vt-c-text-light-1: var(--vt-c-indigo);
  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
  --vt-c-text-dark-1: var(--vt-c-white);
  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
  --color-background: var(--vt-c-white);
  --color-background-soft: var(--vt-c-white-soft);
  --color-background-mute: var(--vt-c-white-mute);
  --color-border: var(--vt-c-divider-light-2);
  --color-border-hover: var(--vt-c-divider-light-1);
  --color-heading: var(--vt-c-text-light-1);
  --color-text: var(--vt-c-text-light-1);
  --section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: var(--vt-c-black);
    --color-background-soft: var(--vt-c-black-soft);
    --color-background-mute: var(--vt-c-black-mute);
    --color-border: var(--vt-c-divider-dark-2);
    --color-border-hover: var(--vt-c-divider-dark-1);
    --color-heading: var(--vt-c-text-dark-1);
    --color-text: var(--vt-c-text-dark-2);
  }
}
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  font-weight: normal;
}
html {
  width: 100%;
  height: 100%;
}
body {
  width: 100%;
  height: 100%;
  min-height: 100vh;
  color: var(--color-text);
  background: var(--color-background);
  transition:
    color 0.5s,
    background-color 0.5s;
  font-family:
    Inter,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    'Fira Sans',
    'Droid Sans',
    'Helvetica Neue',
    sans-serif;
  font-size: 14px;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
src/assets/js/toolClass.ts
New file
@@ -0,0 +1,17 @@
export const getFileSize = (fileByte: number) => {
  const fileSizeByte = fileByte
  let fileSizeMsg = ''
  if (fileSizeByte < 1048576) fileSizeMsg = (fileSizeByte / 1024).toFixed(2) + 'KB'
  else if (fileSizeByte == 1048576) fileSizeMsg = '1MB'
  else if (fileSizeByte > 1048576 && fileSizeByte < 1073741824)
    fileSizeMsg = (fileSizeByte / (1024 * 1024)).toFixed(2) + 'MB'
  else if (fileSizeByte > 1048576 && fileSizeByte == 1073741824) fileSizeMsg = '1GB'
  else if (fileSizeByte > 1073741824 && fileSizeByte < 1099511627776)
    fileSizeMsg = (fileSizeByte / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
  else fileSizeMsg = ''
  return fileSizeMsg
}
export default {
  getFileSize
}
src/assets/main.css
New file
@@ -0,0 +1,11 @@
@import './base.css';
#app {
  width: 100%;
  height: 100%;
}
page {
  display: block;
}
src/assets/style/global.less
New file
@@ -0,0 +1 @@
@theme-color: #366aec;
src/layout/layout.vue
New file
@@ -0,0 +1,115 @@
<template>
  <div class="layoutBox">
    <div class="menuBox">
      <div :class="['menuItem', activeMenu == index ? 'active' : '']" v-for="(item, index) in menuData" :key="index"
        @click="menuItemClick(index)">
        <div class="menuIcon">
          <el-icon v-if="item.icon == 'FolderOpened'" :size="24">
            <FolderOpened />
          </el-icon>
          <el-icon v-if="item.icon == 'Files'" :size="24">
            <Files />
          </el-icon>
          <el-icon v-if="item.icon == 'Switch'" :size="24">
            <Switch />
          </el-icon>
          <el-icon v-if="item.icon == 'Setting'" :size="24">
            <Setting />
          </el-icon>
        </div>
        <p>{{ item.name }}</p>
      </div>
    </div>
    <div class="pageBox">
      <RouterView />
    </div>
  </div>
</template>
<script setup lang="ts">
  import { ref, reactive, watch } from 'vue'
  import { useRouter, RouterView } from 'vue-router'
  const router = useRouter()
  // 菜单
  const menuData = reactive([
    // {
    //   name: '文件',
    //   icon: 'FolderOpened',
    //   router: '/home'
    // },
    {
      name: '传输',
      icon: 'Switch',
      router: '/transmission'
    },
    // {
    //   name: '导出',
    //   icon: 'Files',
    //   router: '/exportTask'
    // },
    {
      name: '设置',
      icon: 'Setting',
      router: '/setting'
    }
  ])
  // 选中菜单
  const activeMenu = ref(0)
  // 监听路由变化,默认选中菜单
  watch(
    () => router.currentRoute.value, (newRoute) => {
      console.log(newRoute.path)
      const index = menuData.findIndex((item) => item.router == newRoute.path)
      activeMenu.value = index > -1 ? index : 0
    }
  )
  // 菜单点击
  const menuItemClick = (index) => {
    activeMenu.value = index
    router.push(menuData[index].router)
  }
</script>
<style lang="less">
  .layoutBox {
    width: 100%;
    height: 100%;
    display: flex;
    .menuBox {
      width: 80px;
      background-color: #f5f5f6;
      border-right: 1px solid #e6e7e8;
      padding: 10px;
      box-sizing: border-box;
      .menuItem {
        padding: 8px 0;
        text-align: center;
        border-radius: 8px;
        line-height: 1;
        cursor: pointer;
        margin-bottom: 10px;
        &.active,
        &:hover {
          background-color: #e3e3e5;
        }
        .menuIcon {
          margin-bottom: 4px;
        }
      }
    }
    .pageBox {
      flex: 1;
      overflow: auto;
    }
  }
</style>
src/main.ts
New file
@@ -0,0 +1,49 @@
import './assets/main.css'
import { createApp } from 'vue'
import pinia from '@/store/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import toolClass from '@/assets/js/toolClass'
import request from "@/plugin/axios/index.ts";
const handleGetToken = () => {
  return localStorage.getItem("token");
}
// 路由执行之前的一些操作
router.beforeEach((to, from, next) => {
  // 如果有token
  if (handleGetToken()) {
    // 是否是登录页面,直接到首页
    if (to.path === "/login") {
      next({ path: "/transmission" });
    } else {
      // 如果不是登录页面,跳转到目标的页面
      next();
    }
  } else {
    // 没有token
    if (!to.meta || !to.meta.auth) {
      // 在免登录白名单,直接进入
      next();
    } else {
      next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页
    }
  }
});
const app = createApp(App)
app.provide('toolClass', toolClass)
app.provide('request', request)
app.use(router)
app.use(ElementPlus)
app.use(pinia)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.mount('#app')
src/plugin/axios/index.ts
New file
@@ -0,0 +1,51 @@
import axios from 'axios'
import { ctx } from '../../../electron/config'
import router from '@/router'
// 创建 axios 实例
const service = axios.create({
  baseURL: ctx,
  timeout: 300000 // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) config.headers['Authorization'] = `bearer ${token}`
    return config
  },
  (error) => {
    // 发送失败
    Promise.reject(error)
  }
)
// 响应拦截器
service.interceptors.response.use(
  (response) => {
    if (response.status == 200) {
      if (response.request.responseURL.indexOf('/FileDownload') > -1) {
        return response.data;
      }
      return response.data.data;
    }
  },
  (error) => {
    let msg = '请求发生错误';
    if (error.response && error.response.status == 401) {
      localStorage.clear()
      router.replace({
        path: '/login'
      })
    } else {
      if (error.response && error.response.data) {
        msg = error.response.data.msg
      }
    }
    throw msg;
  }
)
export default service
src/router/index.ts
New file
@@ -0,0 +1,53 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/layout.vue'
const Transmission = () => import('@/views/transmission.vue')
const ExportTask = () => import('@/views/exportTask.vue')
const Setting = () => import('@/views/setting.vue')
const Login = () => import('@/views/login.vue')
const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      redirect: 'transmission'
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
    {
      path: '/',
      component: Layout,
      children: [
        // {
        //   path: '/home',
        //   name: 'home',
        //   meta: { auth: true },
        //   component: Home
        // },
        {
          path: '/transmission',
          name: 'transmission',
          meta: { auth: true },
          component: Transmission
        },
        {
          path: '/exportTask',
          name: 'exportTask',
          meta: { auth: true },
          component: ExportTask
        },
        {
          path: '/setting',
          name: 'setting',
          meta: { auth: true },
          component: Setting
        }
      ]
    }
  ]
})
export default router
src/store/downloadTask.ts
New file
@@ -0,0 +1,38 @@
import { defineStore } from 'pinia'
export const useDownloadTask = defineStore('downloadTask', {
  state: () => ({
    updateList: 0,
    msgData: {},
  }),
  actions: {
    setUpdateList() {
      if (this.updateList === 0) {
        this.updateList = 1
      } else {
        this.updateList = 0
      }
    },
    setMsgData(data) {
      this.msgData = data
    }
  }
})
export const useExportTask = defineStore('exportTask', {
  state: () => ({
    updateList: 0,
    msgData: {},
  }),
  actions: {
    setUpdateList() {
      if (this.updateList === 0) {
        this.updateList = 1
      } else {
        this.updateList = 0
      }
    },
    setMsgData(data) {
      this.msgData = data
    }
  }
})
src/store/index.ts
New file
@@ -0,0 +1,8 @@
import { createPinia } from 'pinia'
// 创建pinia实例
const pinia = createPinia()
export default pinia
export * from './downloadTask'
src/views/exportTask.vue
New file
@@ -0,0 +1,363 @@
<template>
  <div class="exportTaskBox">
    <div class="taskList">
      <p class="blockTitle">任务列表</p>
      <p @click="newTask">测试创建任务</p>
      <div
        :class="'taskItem ' + selectedTaskIndex == index ? 'active' : ''"
        v-for="(task, index) in tasks"
        :key="index"
      >
        <p class="taskItemTitle">{{ task.bookTplName + '_' + task.id }}</p>
        <p class="taskItemDesc">
          <span class="time">{{ task.startDate }}</span>
          <span class="state">
            <span style="color: #909399" v-if="task.state == 0">初始化</span>
            <span style="color: #909399" v-if="task.state == 1">处理中...</span>
            <span style="color: #e6a23c" v-if="task.state == 2">暂停</span>
            <span style="color: #67c23a" v-if="task.state == 3">已完成</span>
            <span style="color: #f56c6c" v-if="task.state == 4">失败</span>
          </span>
        </p>
      </div>
    </div>
    <div class="contentBox">
      <p class="blockTitle">任务详情</p>
      <div class="taskDetailBox" v-if="selectedTask">
        <div class="baseInfo">
          <span>
            导出模板:<span>{{ selectedTask.bookTplName }}</span>
          </span>
          <span>
            文件模板:<span>{{ selectedTask.fileTplName }}</span>
          </span>
          <span>
            创建时间:<span>{{ selectedTask.startDate }}</span>
          </span>
          <span v-if="selectedTask.state == 3">
            完成时间:<span>{{ selectedTask.endDate }}</span>
          </span>
          <span style="width: 100%">
            文件路径:
            <span style="width: auto">
              {{ selectedTask.taskPath }}
              <el-icon><Search /></el-icon>
            </span>
          </span>
        </div>
        <div class="progressInfo">
          <p class="stepItem" v-if="selectedTask.progressInfo.createTask">
            <span>创建任务...</span>
            <el-icon v-if="selectedTask.progressInfo.createTask != 3"><Loading /></el-icon>
            <el-icon v-if="selectedTask.progressInfo.createTask == 3"><CircleCheck /></el-icon>
          </p>
          <p class="stepItem" v-if="selectedTask.progressInfo.temporaryFolder">
            <span>创建临时文件夹</span>
            <el-icon v-if="selectedTask.progressInfo.temporaryFolder != 3"><Loading /></el-icon>
            <el-icon v-if="selectedTask.progressInfo.temporaryFolder == 3"><CircleCheck /></el-icon>
          </p>
          <div class="bookInfoBox" v-if="selectedTask.progressInfo.handleBook.state">
            <p class="progressItem">
              <span>获取图书信息:</span>
              <span style="display: inline-block; width: 170px">
                <el-progress
                  :percentage="selectedTask.progressInfo.handleBook.progress"
                  :status="selectedTask.progressInfo.handleBook.progress < 100 ? '' : 'success'"
                />
              </span>
              <span>
                {{ selectedTask.progressInfo.handleBook.showProgress }}
              </span>
            </p>
          </div>
          <p class="stepItem" v-if="selectedTask.progressInfo.handleExcel">
            <span>生成Excel</span>
            <el-icon v-if="selectedTask.progressInfo.handleExcel != 3"><Loading /></el-icon>
            <el-icon v-if="selectedTask.progressInfo.handleExcel == 3"><CircleCheck /></el-icon>
          </p>
          <div class="fileInfoBox" v-if="selectedTask.progressInfo.showHandleFile">
            <p class="stepItem">
              <span>开始下载文件...</span>
            </p>
            <p
              class="progressItem"
              v-if="bookKey in Object.keys(selectedTask.progressInfo.handleFile)"
              :key="bookKey"
            >
              <span :title="selectedTask.progressInfo.handleFile[bookKey].bookName">
                {{
                  selectedTask.progressInfo.handleFile[bookKey].bookName +
                  (selectedTask.progressInfo.handleFile[bookKey].ISBN
                    ? '(' + selectedTask.progressInfo.handleFile[bookKey].ISBN + ')'
                    : '')
                }}
                :
              </span>
              <span style="display: inline-block; width: 170px">
                <el-progress
                  :percentage="selectedTask.progressInfo.handleFile[bookKey].progress"
                  :status="
                    selectedTask.progressInfo.handleFile[bookKey].progress < 100 ? '' : 'success'
                  "
                />
              </span>
              <span>
                {{ selectedTask.progressInfo.handleFile[bookKey].showProgress }}
              </span>
            </p>
          </div>
          <div class="fileInfoBox" v-if="selectedTask.progressInfo.showHandleFolder">
            <p class="stepItem">
              <span>处理文件夹...</span>
            </p>
            <p class="stepItem">
              <span :title="selectedTask.progressInfo.handleFolder[bookKey].bookName">
                {{
                  selectedTask.progressInfo.handleFolder[bookKey].bookName +
                  (selectedTask.progressInfo.handleFolder[bookKey].ISBN
                    ? '(' + selectedTask.progressInfo.handleFolder[bookKey].ISBN + ')'
                    : '')
                }}
              </span>
              <el-icon v-if="selectedTask.progressInfo.handleFolder[bookKey].state != 3"
                ><Loading
              /></el-icon>
              <el-icon v-if="selectedTask.progressInfo.handleFolder[bookKey].state == 3"
                ><CircleCheck
              /></el-icon>
            </p>
          </div>
          <p class="stepItem" v-if="selectedTask.state == 3">
            <span>导出成功!</span>
            <el-icon><CircleCheck /></el-icon>
          </p>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive, inject, onMounted, watch } from 'vue'
import { useRouter, RouterView } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useExportTask } from '@/store'
const ExportTask = useExportTask()
const router = useRouter()
const selectedTaskIndex = ref(0)
const tasks = ref([])
const selectedTask = ref()
onMounted(() => {
  updateListData()
})
watch(
  () => ExportTask.updateList,
  (newValue, oldValue) => {
    updateListData()
  }
)
watch(
  () => ExportTask.msgData,
  (newValue, oldValue) => {
    console.log(newValue)
    // if (newValue.type == 'showState') {
    //   stateInfo.value = newValue.msg
    // } else if (newValue.type == 'notDownloadFolder') {
    //   console.log('notDownloadFolder')
    //   // 下载目录检测不存在,设置下载目录
    //   const path = window.electronAPI.openSelectFileOrFolderDialog({
    //     title: '选择下载目录',
    //     properties: ['openDirectory']
    //   })
    //   if (path && path.length > 0) {
    //     const returnData = window.electronAPI.setDownloadPath(path[0], newValue.msg)
    //   }
    // } else {
    //   ElMessage({
    //     message: newValue.msg,
    //     type: newValue.type
    //   })
    // }
  }
)
const newTask = () => {
  window.electronAPI.newExportTask({
    bookInfo: [
      {
        id: 23627,
        idPath: '22288\\26631\\23627',
        storeId: 9,
        repoId: 16,
        typeId: 3
      }
    ],
    tplInfo: {
      type: "user", // sys & user
      key: "9102ABDCE3C5CD294E832EBDA51948FF"
    }
  })
}
const updateListData = () => {
  // 获取导出任务列表
  const taskListData = window.electronAPI.getExportTasks()
  console.log(taskListData);
}
const handleData = (data) => {
  // 文件信息
  if (Object.keys(data.progressInfo.handleFile).length) {
    let start = false
    for (const key in data.progressInfo.handleFile) {
      let bookFile = data.progressInfo.handleFile[key]
      let bookInfo = data.bookInfo.filter((item) => {
        return item.id == key
      })
      if (bookInfo.length) {
        bookFile.bookName = bookInfo[0].name
        if (bookFile.bookName) bookFile.bookName = bookFile.bookName.replace(/[/|\\]/g, '_')
        bookFile.ISBN = bookInfo[0].ISBN
        if (bookFile.ISBN) bookFile.ISBN = bookFile.ISBN.replace(/[/|\\]/g, '_')
      }
      if (bookFile.state) {
        if (bookFile.info.total) {
          bookFile.progress = parseInt((bookFile.info.success / bookFile.info.total) * 100)
        } else {
          bookFile.progress = 100
        }
        bookFile.showProgress = '(' + bookFile.info.success + ' / ' + bookFile.info.total + ')'
        start = true
      }
    }
    if (start) {
      data.progressInfo.showHandleFile = true
    }
  }
  // 处理文件夹
  if (Object.keys(data.progressInfo.handleFolder).length) {
    let start = false
    for (const key in data.progressInfo.handleFolder) {
      let folderState = data.progressInfo.handleFolder[key]
      data.progressInfo.handleFolder[key] = {
        state: folderState
      }
      let bookInfo = data.bookInfo.filter((item) => {
        return item.id == key
      })
      if (bookInfo.length) {
        data.progressInfo.handleFolder[key].bookName = bookInfo[0].name
        data.progressInfo.handleFolder[key].ISBN = bookInfo[0].ISBN
      }
      if (folderState) {
        start = true
      }
    }
    if (start) {
      data.progressInfo.showHandleFolder = true
    }
  }
  // 书籍信息
  data.progressInfo.handleBook.progress = parseInt(
    (data.progressInfo.handleBook.info.success / data.progressInfo.handleBook.info.total) * 100
  )
  data.progressInfo.handleBook.showProgress =
    '(' +
    data.progressInfo.handleBook.info.success +
    ' / ' +
    data.progressInfo.handleBook.info.total +
    ')'
  return data
}
</script>
<style lang="less">
.exportTaskBox {
  width: 100%;
  height: 100%;
  display: flex;
  .taskList {
    width: 280px;
    padding: 40px 20px;
    border-right: 1px solid #e6e7e8;
    .taskItem {
      padding: 10px;
      cursor: pointer;
      .taskItemTitle {
        color: #000;
        font-weight: 500;
        font-size: 14px;
        margin-bottom: 6px;
      }
      .taskItemDesc {
        color: #666;
        font-size: 12px;
        overflow: hidden;
        .time {
          float: left;
        }
        .state {
          float: right;
        }
      }
      &.active {
        background: #efefef;
      }
    }
  }
  .contentBox {
    flex: 1;
    padding: 50px;
    .taskDetailBox {
      width: 100%;
      height: 100%;
      overflow: auto;
      padding: 20px;
      .baseInfo {
        margin-bottom: 20px;
        padding-bottom: 20px;
        border-bottom: 1px solid #ccc;
        span {
          display: inline-block;
          width: 50%;
          color: #999;
          margin-bottom: 5px;
          span {
            color: #000;
          }
        }
      }
      .progressInfo {
        .stepItem {
          margin-bottom: 6px;
          span {
            margin-right: 8px;
          }
        }
        .fileInfoBox {
          .progressItem {
            margin-bottom: 6px;
          }
        }
        .bookInfoBox {
          .progressItem {
            margin-bottom: 6px;
          }
        }
      }
    }
  }
  .blockTitle {
    font-size: 20px;
    font-weight: bold;
    line-height: 32px;
    margin-bottom: 20px;
  }
}
</style>
src/views/home.vue
New file
@@ -0,0 +1,281 @@
<template>
  <div class="homeBox">
    <div class="herderBox">
      <p>文件</p>
      <div class="viewChangeBox">
        <el-icon :size="16" v-if="viewMode == 0" @click="setViewMode"><Calendar /></el-icon>
        <el-icon :size="16" v-if="viewMode == 1" @click="setViewMode"><Menu /></el-icon>
      </div>
      <div class="search">
        <el-input v-model="searchKey" size="small" placeholder="关键字搜索">
          <template #append>
            <el-button :icon="Search" />
          </template>
        </el-input>
      </div>
    </div>
    <div class="toolBox">
      <div class="checkBox" v-if="viewMode == 0">
        <el-checkbox
          v-model="checkBoxState.check"
          :indeterminate="checkBoxState.indeterminate"
          @change="handleCheckAllChange"
        />
        <span class="checkText">{{
          checkBoxState.selectCount > 0
            ? `已选 ${checkBoxState.selectCount} 项`
            : `共 ${checkBoxState.totalCount} 项`
        }}</span>
      </div>
      <div class="sortBox" v-if="viewMode == 0">
        <el-dropdown trigger="click" @command="sortChange">
          <span class="sortText">
            <el-icon :size="16"><Sort /></el-icon>
            <span>
              按{{ sortState.fields[sortState.selectFieldIndex].name
              }}{{ sortState.types[sortState.selectTypeIndex].name }}排序
            </span>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item
                v-for="(item, index) in sortState.fields"
                :key="item.value"
                :command="'fields.' + item.value"
              >
                <p>
                  <span>
                    <el-icon v-if="sortState.selectFieldIndex == index" :size="16" color="#409EFF">
                      <Check />
                    </el-icon>
                  </span>
                  <span>{{ item.name }}</span>
                </p>
              </el-dropdown-item>
              <el-dropdown-item
                v-for="(item, index) in sortState.types"
                :key="item.value"
                :divided="index == 0"
                :command="'types.' + item.value"
              >
                <p>
                  <span>
                    <el-icon v-if="sortState.selectTypeIndex == index" :size="16" color="#409EFF">
                      <Check />
                    </el-icon>
                  </span>
                  <span>{{ item.name }}</span>
                </p>
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
    <div class="fileList">
      <div v-if="viewMode == 0" class="blockBox">
        <div class="fileItem" v-for="item in tableData">
          <div class="iconBox">
            <img :src="item.img" alt="" />
          </div>
          <p class="name">{{ item.name }}</p>
          <p class="time">{{ item.createDate }}</p>
        </div>
      </div>
      <el-table v-if="viewMode == 1" :data="tableData" style="width: 100%">
        <el-table-column type="selection" width="55" />
        <el-table-column type="index" width="80" />
        <el-table-column prop="name" label="名称" sortable />
        <el-table-column prop="createDate" label="创建时间" width="200" sortable />
        <el-table-column prop="size" label="大小" width="200" sortable />
      </el-table>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Search, Check } from '@element-plus/icons-vue'
import { useRouter, RouterView } from 'vue-router'
const router = useRouter()
// 搜索
const searchKey = ref('')
// 选择框
const checkBoxState = reactive({
  selectCount: 0,
  totalCount: 0,
  check: false,
  indeterminate: false
})
const handleCheckAllChange = (val) => {
  if (val) {
    checkBoxState.check = true
  } else {
    checkBoxState.check = false
  }
  checkBoxState.indeterminate = false
}
// 排序
const sortState = reactive({
  fields: [
    {
      name: '名称',
      value: 'Name'
    },
    {
      name: '创建时间',
      value: 'CreateDate'
    },
    {
      name: '文件大小',
      value: 'Size'
    }
  ],
  types: [
    {
      name: '升序',
      value: 'Asc'
    },
    {
      name: '降序',
      value: 'Desc'
    }
  ],
  selectFieldIndex: 1,
  selectTypeIndex: 1
})
const sortChange = (command) => {
  const type = command.split('.')[0]
  const data = command.split('.')[1]
  if (type == 'fields') {
    sortState.selectFieldIndex = sortState.fields.findIndex((item) => item.value == data)
  } else {
    sortState.selectTypeIndex = sortState.types.findIndex((item) => item.value == data)
  }
}
// 视图模式 0:块状视图  1:表格视图
const viewMode = ref(0)
const setViewMode = () => {
  if (viewMode.value == 0) {
    viewMode.value = 1
  } else {
    viewMode.value = 0
  }
}
// 文件列表
const tableData = ref([])
</script>
<style lang="less">
.homeBox {
  width: 100%;
  height: 100%;
  padding: 50px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  .herderBox {
    overflow: hidden;
    margin-bottom: 20px;
    p {
      float: left;
      font-size: 20px;
      font-weight: bold;
      line-height: 32px;
    }
    .search {
      float: right;
      margin-right: 20px;
    }
    .viewChangeBox {
      float: right;
      line-height: 32px;
      i {
        cursor: pointer;
        vertical-align: sub;
      }
    }
  }
  .toolBox {
    overflow: hidden;
    margin-bottom: 10px;
    line-height: 32px;
    .checkBox {
      float: left;
      .checkText {
        display: inline-block;
        line-height: 32px;
        vertical-align: top;
        margin-left: 8px;
      }
    }
    .sortBox {
      float: right;
      .sortText {
        width: 150px;
        line-height: 32px;
        cursor: pointer;
        i,
        span {
          vertical-align: middle;
        }
      }
    }
  }
  .fileList {
    flex: 1;
    overflow: auto;
    .blockBox {
      overflow: hidden;
      .fileItem {
        width: 140px;
        float: left;
        margin: 20px;
        padding: 15px;
        border-radius: 10px;
        cursor: context-menu;
        &:hover {
          background-color: #f1f1f1;
        }
        .iconBox {
          width: 100%;
          height: 140px;
          margin-bottom: 10px;
          position: relative;
          img {
            width: auto;
            height: auto;
            max-width: 100%;
            max-height: 100%;
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            margin: auto;
            border-radius: 6px;
          }
        }
        .name {
          display: -webkit-box;
          -webkit-box-orient: vertical;
          -webkit-line-clamp: 2;
          overflow: hidden;
          margin-bottom: 8px;
        }
        .time {
          font-size: 12px;
          color: #999;
        }
      }
    }
  }
}
</style>
src/views/login.vue
New file
@@ -0,0 +1,111 @@
<template>
  <div class="loginPage">
    <div class="loginForm">
      <p>数字教材阅读器</p>
      <el-form ref="ruleFormRef" :model="loginData" :rules="rules" label-width="80px">
        <el-form-item label="用户名:" prop="username">
          <el-input v-model="loginData.username"></el-input>
        </el-form-item>
        <el-form-item label="密码:" prop="password">
          <el-input type="password" v-model="loginData.password"></el-input>
        </el-form-item>
        <div class="btnBox">
          <el-button
            style="width: 120px"
            type="primary"
            @click="submitForm(ruleFormRef)"
            :loading="loading"
            >登 录</el-button
          >
        </div>
      </el-form>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive, inject } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const request = inject('request')
const ruleFormRef = ref()
const loginData = ref({
  username: '',
  password: ''
})
const rules = reactive({
  username: [{ required: true, message: '请填写用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请填写密码', trigger: 'blur' }]
})
const loading = ref(false)
const submitForm = async (formEl) => {
  if (!formEl) return
  await formEl.validate((valid, fields) => {
    if (valid) {
      loading.value = true
      request({
        url: '/identity/Login/LoginByLoginNameAndPassword',
        method: 'post',
        data: {
          loginName: loginData.value.username,
          password: loginData.value.password,
          platform: 'textbookReader',
          appId: '-1'
        }
      })
        .then((res) => {
          console.log(res)
          localStorage.setItem('token', res.token)
          if (route.query.redirect) {
            router.push(route.query.redirect)
          } else {
            router.push('/')
          }
        })
        .catch((errorMsg) => {
          ElMessage.error(errorMsg)
          loading.value = false
        })
    }
  })
}
</script>
<style lang="less">
.loginPage {
  width: 100%;
  height: 100%;
  position: relative;
  .loginForm {
    width: 400px;
    height: 300px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -150px;
    margin-left: -200px;
    border: 1px solid #ccc;
    border-radius: 10px;
    padding: 20px;
    p {
      font-size: 20px;
      font-weight: bold;
      text-align: center;
      margin-bottom: 50px;
    }
    .btnBox {
      text-align: center;
      margin-top: 50px;
    }
  }
}
</style>
src/views/setting.vue
New file
@@ -0,0 +1,62 @@
<template>
  <page>
    <div class="setting">
      <el-divider content-position="left">设置下载目录</el-divider>
      <div class="setDownloadPathBox">
        <el-input style="width: 300px; margin-right: 10px" v-model="downloadPath" disabled />
        <el-button type="primary" @click="setPath">设置目录</el-button>
        <el-button type="primary" @click="openPath">打开目录</el-button>
      </div>
    </div>
  </page>
</template>
<script setup lang="ts">
import { ref, reactive, inject, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const downloadPath = ref('')
onMounted(() => {
  const path = window.electronAPI.getDownloadPath()
  downloadPath.value = path
})
const setPath = () => {
  const path = window.electronAPI.openSelectFileOrFolderDialog({
    title: '选择下载目录',
    properties: ['openDirectory']
  })
  downloadPath.value = path[0]
  const data = window.electronAPI.setDownloadPath(path[0])
  if (data.state) {
    ElMessage({
      message: '设置成功!',
      type: 'success'
    })
  } else {
    ElMessage({
      message: data.msg,
      type: 'error'
    })
  }
}
const openPath = () => {
  const data = window.electronAPI.openPath(downloadPath.value)
  if (!data.state) {
    ElMessage({
      message: data.msg,
      type: 'error'
    })
  }
}
</script>
<style lang="less">
.setting {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 10px 50px;
  .setDownloadPathBox {
    display: flex;
  }
}
</style>
src/views/transmission.vue
New file
@@ -0,0 +1,495 @@
<template>
  <div class="transmissionBox">
    <div class="subMenuBox">
      <div
        :class="['menuItem', selectMenuIndex == index ? 'active' : '']"
        v-for="(item, index) in menuData"
        @click="selectMenu(index)"
      >
        <div class="iconBox">
          <el-icon v-if="item.icon == 'download'" :size="20"><Download /></el-icon>
          <el-icon v-if="item.icon == 'success'" :size="20"><CircleCheck /></el-icon>
        </div>
        <p>{{ item.name }}</p>
        <span>{{ item.num }}</span>
      </div>
    </div>
    <div class="pageBox">
      <div class="herderBox">
        <p>{{ menuData[selectMenuIndex].name }}</p>
      </div>
      <div class="toolBox" v-if="selectMenuIndex == 0">
        <p>
          下载列表 · <span>已下载 {{ totalProgress }}%{{ allPause ? ',已全部暂停' : '' }}</span>
        </p>
        <div class="toolBtnBox">
          <el-button
            :icon="Download"
            size="small"
            type="primary"
            :disabled="allStart"
            @click="newTask"
            >新增下载</el-button
          >
          <el-button :icon="CaretRight" size="small" :disabled="allStart" @click="startAll"
            >全部开始</el-button
          >
          <el-button :icon="VideoPause" size="small" :disabled="allPause" @click="pauseAll"
            >全部暂停</el-button
          >
          <el-button :icon="CloseBold" size="small" @click="cleanAll">全部取消</el-button>
        </div>
      </div>
      <div class="fileList">
        <el-table
          :data="tableData"
          style="width: 100%"
          :height="'100%'"
          empty-text="暂无数据"
          @cell-mouse-enter="mouseEnter"
          @cell-mouse-leave="mouseLeave"
        >
          <el-table-column prop="name" label="名称">
            <template #default="scope">
              <p class="title">
                <span>{{ scope.row.name }}</span>
                <span class="tableItemBtnBox" v-show="showId == scope.row.id">
                  <el-tooltip
                    v-if="scope.row.state == 'pause'"
                    effect="dark"
                    content="开始"
                    placement="top"
                  >
                    <el-button :icon="CaretRight" circle @click="start(scope.row.id)" />
                  </el-tooltip>
                  <el-tooltip
                    v-if="scope.row.state == 'download'"
                    effect="dark"
                    content="暂停"
                    placement="top"
                  >
                    <el-button :icon="VideoPause" circle @click="pause(scope.row.id)" />
                  </el-tooltip>
                  <el-tooltip
                    effect="dark"
                    :content="scope.row.state == 'success' ? '删除记录' : '取消'"
                    placement="top"
                  >
                    <el-button :icon="CloseBold" circle @click="clean(scope.row.id)" />
                  </el-tooltip>
                  <el-tooltip effect="dark" content="在本地目录查看" placement="top">
                    <el-button :icon="Search" circle @click="openFile(scope.row.id)" />
                  </el-tooltip>
                </span>
              </p>
            </template>
          </el-table-column>
          <el-table-column prop="size" label="大小" width="180">
            <template #default="scope">
              <span v-if="selectMenuIndex == 0"
                >{{ getFileSize(scope.row.offset) }} / {{ getFileSize(scope.row.size) }}</span
              >
              <span v-if="selectMenuIndex == 1">{{ getFileSize(scope.row.size) }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="createDate" label="创建时间" width="180" sortable>
          </el-table-column>
          <el-table-column
            v-if="selectMenuIndex == 1"
            prop="completeDate"
            label="完成时间"
            width="180"
            sortable
          >
          </el-table-column>
          <el-table-column v-if="selectMenuIndex == 0" prop="createDate" label="状态" width="300">
            <template #default="scope">
              <p class="itemState" v-if="scope.row.state == 'download'" style="color: #409eff">
                正在下载
              </p>
              <p class="itemState" v-if="scope.row.state == 'pause'" style="color: #e6a23c">
                已暂停
              </p>
              <p class="itemState" v-if="scope.row.state == 'getFileInfo'" style="color: #409eff">
                获取文件信息
              </p>
              <p class="itemState" v-if="scope.row.state == 'error'" style="color: #f56c6c">
                下载错误
              </p>
              <p class="itemState" v-if="scope.row.state == 'success'" style="color: #67c23a">
                已完成
              </p>
              <el-progress
                :show-text="false"
                :striped="scope.row.state == 'download'"
                :duration="10"
                :striped-flow="scope.row.state == 'download'"
                :percentage="scope.row.progress"
                :color="
                  () => {
                    if (scope.row.state == 'download') {
                      return '#409EFF'
                    } else if (scope.row.state == 'success') {
                      return '#67C23A'
                    } else {
                      return '#E6A23C'
                    }
                  }
                "
              />
            </template>
          </el-table-column>
        </el-table>
      </div>
      <div class="stateInfoBox" v-if="stateInfo">
        {{ stateInfo }}
      </div>
    </div>
    <el-dialog
      v-model="newTaskDialogVisible"
      title="新增下载任务"
      width="400"
      align-center
      :modal="false"
    >
      <el-input
        v-model="newTaskInfo"
        :autosize="{ minRows: 4, maxRows: 8 }"
        type="textarea"
        placeholder="请填写任务字符"
      />
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="submitTask">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive, inject, onMounted, watch } from 'vue'
import { Check } from '@element-plus/icons-vue'
import { useRouter, RouterView } from 'vue-router'
import { Download, CaretRight, CloseBold, VideoPause, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useDownloadTask } from '@/store'
const router = useRouter()
const { getFileSize } = inject('toolClass')
const downloadTask = useDownloadTask()
// 菜单
const menuData = ref([
  {
    name: '下载',
    num: 0,
    icon: 'download'
  },
  {
    name: '已完成',
    num: 0,
    icon: 'success'
  }
])
const selectMenuIndex = ref(0)
const selectMenu = (index) => {
  selectMenuIndex.value = index
  updateListData()
}
// 总进度
const totalProgress = ref(0)
// 全部暂停
const allPause = ref(false)
// 全部开始
const allStart = ref(false)
// 文件列表
const tableData = ref([])
const stateInfo = ref()
onMounted(() => {
  updateListData()
  // 绑定消息提醒
  // window.electronAPI.onShowMessage((data) => {
  //   console.log(data)
  //   if (data.type == 'showState') {
  //     stateInfo.value = data.msg
  //   } else if (data.type == 'notDownloadFolder') {
  //     console.log('notDownloadFolder')
  //     // 下载目录检测不存在,设置下载目录
  //     const path = window.electronAPI.openSelectFileOrFolderDialog({
  //       title: '选择下载目录',
  //       properties: ['openDirectory']
  //     })
  //     if (path && path.length > 0) {
  //       console.log('setDownloadPath')
  //       debugger
  //       const returnData = window.electronAPI.setDownloadPath(path[0], data.msg)
  //     }
  //   } else {
  //     ElMessage({
  //       message: data.msg,
  //       type: data.type
  //     })
  //   }
  // })
})
watch(
  () => downloadTask.updateList,
  (newValue, oldValue) => {
    updateListData()
  }
)
watch(
  () => downloadTask.msgData,
  (newValue, oldValue) => {
    if (newValue.type == 'showState') {
      stateInfo.value = newValue.msg
    } else if (newValue.type == 'notDownloadFolder') {
      console.log('notDownloadFolder')
      // 下载目录检测不存在,设置下载目录
      const path = window.electronAPI.openSelectFileOrFolderDialog({
        title: '选择下载目录',
        properties: ['openDirectory']
      })
      if (path && path.length > 0) {
        const returnData = window.electronAPI.setDownloadPath(path[0], newValue.msg)
      }
    } else {
      ElMessage({
        message: newValue.msg,
        type: newValue.type
      })
    }
  }
)
const updateListData = () => {
  // 获取下载任务列表
  const taskListData = window.electronAPI.getDownloadTasks()
  if (selectMenuIndex.value == 0) {
    tableData.value = taskListData
      .filter((item) => item.state != 'success')
      .map((item) => {
        // if(item.state == "download") alert(JSON.stringify(item))
        return {
          ...item,
          progress: (item.offset / item.size) * 100
        }
      })
  } else if (selectMenuIndex.value == 1) {
    tableData.value = taskListData.filter((item) => item.state == 'success')
  }
  menuData.value[0].num = taskListData.filter((item) => item.state != 'success').length
  menuData.value[1].num = taskListData.filter((item) => item.state == 'success').length
  let totalOffset = 0
  let totalSize = 0
  taskListData.forEach((item) => {
    if (item.state != 'success') {
      totalOffset += item.offset
      totalSize += item.size
    }
  })
  totalProgress.value = totalSize ? ((totalOffset / totalSize) * 100).toFixed(2) : 0
}
const newTaskDialogVisible = ref(false)
const newTaskInfo = ref('')
const newTask = () => {
  newTaskInfo.value = ''
  newTaskDialogVisible.value = true
}
const submitTask = () => {
  console.log(newTaskInfo.value)
  if (newTaskInfo.value) {
    let infoData = null
    try {
      infoData = JSON.parse(newTaskInfo.value)
    } catch (error) {
      infoData = null
    }
    if (infoData) {
      // 新建任务
      window.electronAPI.newDownloadTask(infoData)
      newTaskDialogVisible.value = false
    } else {
      ElMessage({
        message: '无法识别您输入的内容,请确认后重新填写',
        type: 'warning'
      })
    }
  } else {
    ElMessage({
      message: '请填写新建数据',
      type: 'warning'
    })
  }
}
const clean = (id) => {
  window.electronAPI.cleanTask(id)
}
const cleanAll = () => {
  tableData.value = []
  window.electronAPI.cleanAll()
}
const start = (id) => {
  window.electronAPI.startTask(id)
}
const startAll = () => {
  window.electronAPI.startAllTask()
}
const pause = (id) => {
  window.electronAPI.pauseTask(id)
}
const pauseAll = () => {
  window.electronAPI.pauseAllTask()
}
const openFile = (id) => {
  window.electronAPI.openPathByTaskId(id)
}
const showId = ref()
const mouseEnter = (row) => {
  showId.value = row.id //赋值行id,便于页面判断
}
//鼠标移出单元格事件
const mouseLeave = (row) => {
  showId.value = ''
}
</script>
<style lang="less">
.transmissionBox {
  width: 100%;
  height: 100%;
  display: flex;
  .subMenuBox {
    width: 180px;
    height: 100%;
    overflow: auto;
    border-right: 1px solid #e6e7e8;
    padding: 40px 20px;
    .menuItem {
      display: flex;
      width: 100%;
      padding: 10px;
      border-radius: 10px;
      cursor: pointer;
      margin-bottom: 10px;
      position: relative;
      &.active {
        background-color: #ebebed;
        span {
          background-color: #409eff;
          color: #fff;
          border-color: #409eff;
        }
      }
      &:hover {
        background-color: #ebebed;
      }
      .iconBox {
        display: inline-block;
        width: 20px;
        height: 20px;
        vertical-align: middle;
        margin-right: 16px;
      }
      p {
        vertical-align: middle;
        line-height: 20px;
      }
      span {
        position: absolute;
        right: 10px;
        top: 11px;
        font-size: 12px;
        background-color: #f6f6f7;
        display: inline-block;
        width: 30px;
        padding: 1px 0;
        border-radius: 50px;
        text-align: center;
        color: #999;
        border: 1px solid #ccc;
      }
    }
  }
  .pageBox {
    flex: 1;
    height: 100%;
    padding: 50px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    overflow: auto;
    position: relative;
    .herderBox {
      overflow: hidden;
      margin-bottom: 20px;
      p {
        float: left;
        font-size: 20px;
        font-weight: bold;
        line-height: 32px;
      }
    }
    .toolBox {
      border-bottom: 1px solid #e6e7e8;
      padding: 16px 0;
      margin-bottom: 10px;
      overflow: hidden;
      p {
        float: left;
        font-size: 14px;
        color: #333;
        line-height: 24px;
        span {
          font-size: 12px;
          color: #666;
        }
      }
      .toolBtnBox {
        float: right;
      }
    }
    .fileList {
      flex: 1;
      overflow: hidden;
      .title {
        line-height: 34px;
        .tableItemBtnBox {
          float: right;
          padding: 0 10px;
        }
      }
    }
    .stateInfoBox {
      position: absolute;
      bottom: 18px;
      left: 60px;
      font-size: 12px;
      color: #409eff;
    }
    .itemState {
      font-size: 12px;
    }
  }
}
</style>
tsconfig.app.json
New file
@@ -0,0 +1,13 @@
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "composite": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
tsconfig.electron.json
New file
@@ -0,0 +1,45 @@
{
  "compilerOptions": {
    "outDir": "electron-commonJS/",
    // 指定编译后文件所在目录。
    "module": "CommonJS",
    // 指定编译后代码使用的模块化规范。
    "target": "esnext",
    // 目标语言的版本
    "lib": [
      "esnext", "dom"
    ],
    // TS需要引用的库
    "sourceMap": true,
    // 是否生成相应的Map映射的文件,默认:false。
    "baseUrl": ".",
    // 用于解析非绝对模块名的基本目录,相对模块不受影响。
    "resolveJsonModule": true,
    //是否解析 JSON 模块,默认:false。
    "allowSyntheticDefaultImports": true,
    //是否允许从没有默认导出的模块中默认导入,默认:false。
    "moduleResolution": "node",
    //指定模块解析策略,node或classic
    "forceConsistentCasingInFileNames": true,
    //是否区分文件系统大小写规则,默认:false。
    "noImplicitReturns": true,
    //检查函数是否不含有隐式返回值,默认:false。
    "noUnusedLocals": true,
    //是否检查未使用的局部变量
    "allowJs": true,
    //是否对js文件进行编译,默认:false。
    "skipLibCheck": true,
    //是否跳过声明文件的类型检查,这可以在编译期间以牺牲类型系统准确性为代价来节省时间,默认:false。
    "experimentalDecorators": true,
    //是否启用对装饰器的实验性支持,装饰器是一种语言特性,还没有完全被 JavaScript 规范批准,默认:false。
    "strict": false,
    "esModuleInterop": true,
    //是否启动所有严格检查的总开关,默认:false,启动后将开启所有的严格检查选项。
    "paths": {
    }
  },
  "include": [
    //指定被编译文件所在的目录
    "electron/*"
  ],
}
tsconfig.json
New file
@@ -0,0 +1,11 @@
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.node.json"
    },
    {
      "path": "./tsconfig.app.json"
    }
  ]
}
tsconfig.node.json
New file
@@ -0,0 +1,17 @@
{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*"
  ],
  "compilerOptions": {
    "composite": true,
    "noEmit": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}
vite.config.ts
New file
@@ -0,0 +1,38 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    electron({
      // 配置 Electron 入口文件
      entry: 'electron-commonJS/main.js'
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    host: true,
    port: 8005,
    strictPort: true,
    hmr: true
  },
  define: {
    'process.env': process.env
  },
  css: {
    preprocessorOptions: {
      less: {
        charset: false,
        additionalData: '@import "./src/assets/style/global.less";'
      }
    }
  }
})