From 654f169ed96148247fda48765b47a75ccb2143ab Mon Sep 17 00:00:00 2001
From: YM <479443481@qq.com>
Date: 星期四, 11 四月 2024 18:33:36 +0800
Subject: [PATCH] 初始化

---
 src/views/setting.vue        |   62 
 tsconfig.app.json            |   13 
 src/views/exportTask.vue     |  363 +++++
 .gitignore                   |   46 
 src/plugin/axios/index.ts    |   51 
 vite.config.ts               |   38 
 electron/preload.ts          |  131 +
 electron/toolClass.ts        |   30 
 index.html                   |   14 
 src/views/transmission.vue   |  495 ++++++
 electron/main.ts             |  176 ++
 electron/update.ts           |   28 
 public/favicon.ico           |    0 
 src/router/index.ts          |   53 
 src/assets/style/global.less |    1 
 .eslintrc.cjs                |   15 
 build/installer.nsh          |    9 
 src/assets/main.css          |   11 
 electron/downloadTask.ts     |  738 ++++++++++
 src/assets/js/toolClass.ts   |   17 
 README.md                    |   18 
 src/main.ts                  |   49 
 src/store/index.ts           |    8 
 tsconfig.electron.json       |   45 
 src/layout/layout.vue        |  115 +
 tsconfig.node.json           |   17 
 src/views/login.vue          |  111 +
 electron/config.ts           |    4 
 src/assets/base.css          |   92 +
 tsconfig.json                |   11 
 package.json                 |  107 +
 .prettierrc.json             |    8 
 electron/exportTask.ts       |  930 +++++++++++++
 env.d.ts                     |    1 
 src/views/home.vue           |  281 +++
 src/App.vue                  |  124 +
 src/store/downloadTask.ts    |   38 
 37 files changed, 4,228 insertions(+), 22 deletions(-)

diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..6f40582
--- /dev/null
+++ b/.eslintrc.cjs
@@ -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'
+  }
+}
diff --git a/.gitignore b/.gitignore
index 4d40434..af0e3e0 100644
--- a/.gitignore
+++ b/.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
+
+
+
+
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..66e2335
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 442387a..72aea48 100644
--- a/README.md
+++ b/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
 
diff --git a/build/installer.nsh b/build/installer.nsh
new file mode 100644
index 0000000..e99d1f3
--- /dev/null
+++ b/build/installer.nsh
@@ -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
\ No newline at end of file
diff --git a/electron/config.ts b/electron/config.ts
new file mode 100644
index 0000000..8593eeb
--- /dev/null
+++ b/electron/config.ts
@@ -0,0 +1,4 @@
+// 娴嬭瘯
+export const ctx = "http://182.92.203.7:5001";
+export const downloaderFileCtx = "http://182.92.203.7:3007/DigitalTextbookReader";
+
diff --git a/electron/downloadTask.ts b/electron/downloadTask.ts
new file mode 100644
index 0000000..c66f549
--- /dev/null
+++ b/electron/downloadTask.ts
@@ -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锛氭墍鏈変笅杞芥枃浠朵俊鎭痬ap
+      // name锛氫换鍔″悕绉�
+      // size锛氫换鍔″ぇ灏�
+      // offset锛氫换鍔″凡瀹屾垚鍋忕Щ閲�
+      // msg锛氫换鍔℃秷鎭�
+      // md5锛氫笅杞芥枃浠秏d5
+      // taskDesc锛氫换鍔℃弿杩帮紝涓巑d5浜岄�変竴锛屼换鍔℃弿杩颁細浠ユ枃浠跺す褰㈠紡涓嬭浇骞跺偍瀛�
+      // 渚嬪锛�
+      // [
+      //   {
+      //     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 {
+            // 瀛樺湪鏃х殑鏂囦欢鏈畬鎴愭垨姝e湪涓嬭浇鐨勪换鍔�
+            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)
+      // 瀛樺湪鏃х殑鏂囦欢鏈畬鎴愭垨姝e湪涓嬭浇鐨勪换鍔�
+      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: '鐩綍涓嶅瓨鍦紒' }
+  }
+})
diff --git a/electron/exportTask.ts b/electron/exportTask.ts
new file mode 100644
index 0000000..3d986a0
--- /dev/null
+++ b/electron/exportTask.ts
@@ -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, // 鍥句功妯℃澘锛圗xcel妯℃澘锛�
+        fileTpl: null, // 鏂囦欢瀵煎嚭妯℃澘
+        tplInfo: data.tplInfo,
+        bookInfo: data.bookInfo, // 瀵煎嚭鐨勫浘涔�
+        typeKeyMap: null,
+        state: ExportTask.INIT, // 浠诲姟鐘舵��
+        fileTypeList: [], // 闇�涓嬭浇鏂囦欢绫诲瀷
+        tabHeader: [], // Excel琛ㄥご鏁版嵁
+        tabData: [], // Excel琛ㄥご鏁版嵁
+        temporaryFolderPath: '', // 涓存椂鏂囦欢澶圭洰褰�
+        bookFolderPath: null, // 鍥句功鏂囦欢澶筽ath
+        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
+              }
+              // 鍏煎澶勭悊鏁版嵁杩斿洖鐨刱ey鏄疌msItemData
+              // 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
+})
diff --git a/electron/main.ts b/electron/main.ts
new file mode 100644
index 0000000..e1a7f61
--- /dev/null
+++ b/electron/main.ts
@@ -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()
+  })
+
+  // 瑙e喅搴旂敤鍚姩鐧藉睆闂
+  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) // 姣忔杩愯閮藉垹闄よ嚜瀹氫箟鍗忚 鐒跺悗鍐嶉噸鏂版敞鍐�
+    // 寮�鍙戞ā寮忎笅鍦╳indow杩愯闇�瑕佸仛鍏煎
+    if (development) {
+      // 璁剧疆electron.exe 鍜� app鐨勮矾寰�
+      app.setAsDefaultProtocolClient(agreement, process.execPath, [
+        path.resolve(process.argv[1]),
+        '--'
+      ])
+    }
+    // 楠岃瘉鏄惁涓鸿嚜瀹氫箟鍗忚鐨勯摼鎺�
+    const AGREEMENT_REGEXP = new RegExp(`^${agreement}://`)
+
+    // mac鍞ら啋搴旂敤 浼氭縺娲籵pen-url浜嬩欢 鍦╫pen-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绯荤粺涓嬪敜閱掑簲鐢ㄤ細婵�娲籹econd-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()
+  })
+}
diff --git a/electron/preload.ts b/electron/preload.ts
new file mode 100644
index 0000000..c2d740d
--- /dev/null
+++ b/electron/preload.ts
@@ -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,
+})
diff --git a/electron/toolClass.ts b/electron/toolClass.ts
new file mode 100644
index 0000000..cbcbf29
--- /dev/null
+++ b/electron/toolClass.ts
@@ -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('');
+}
+
diff --git a/electron/update.ts b/electron/update.ts
new file mode 100644
index 0000000..befac86
--- /dev/null
+++ b/electron/update.ts
@@ -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()
+  })
+}
diff --git a/env.d.ts b/env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c538241
--- /dev/null
+++ b/index.html
@@ -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>
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..cd802bb
--- /dev/null
+++ b/package.json
@@ -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": "杩欓噷濉啓鍏蜂綋鐨勭増鏈洿鏂板唴瀹�"
+    }
+  }
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..b6b8f65
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..3a97f95
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,124 @@
+<template>
+  <div class="updateDownloadInfo" v-if="showUpdateInfo">
+    <el-alert
+      :title="`妫�娴嬪埌鏂扮増鏈紝姝e湪涓嬭浇瀹夎鍖�${
+        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>
diff --git a/src/assets/base.css b/src/assets/base.css
new file mode 100644
index 0000000..8f5972d
--- /dev/null
+++ b/src/assets/base.css
@@ -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;
+}
diff --git a/src/assets/js/toolClass.ts b/src/assets/js/toolClass.ts
new file mode 100644
index 0000000..7ffae70
--- /dev/null
+++ b/src/assets/js/toolClass.ts
@@ -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
+}
diff --git a/src/assets/main.css b/src/assets/main.css
new file mode 100644
index 0000000..857b72e
--- /dev/null
+++ b/src/assets/main.css
@@ -0,0 +1,11 @@
+@import './base.css';
+
+#app {
+  width: 100%;
+  height: 100%;
+}
+
+
+page {
+  display: block;
+}
\ No newline at end of file
diff --git a/src/assets/style/global.less b/src/assets/style/global.less
new file mode 100644
index 0000000..9b2b567
--- /dev/null
+++ b/src/assets/style/global.less
@@ -0,0 +1 @@
+@theme-color: #366aec;
\ No newline at end of file
diff --git a/src/layout/layout.vue b/src/layout/layout.vue
new file mode 100644
index 0000000..aff9cda
--- /dev/null
+++ b/src/layout/layout.vue
@@ -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>
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..24ed5fb
--- /dev/null
+++ b/src/main.ts
@@ -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) => {
+  // 濡傛灉鏈塼oken
+  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')
diff --git a/src/plugin/axios/index.ts b/src/plugin/axios/index.ts
new file mode 100644
index 0000000..6ff4491
--- /dev/null
+++ b/src/plugin/axios/index.ts
@@ -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
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..ca3da48
--- /dev/null
+++ b/src/router/index.ts
@@ -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
diff --git a/src/store/downloadTask.ts b/src/store/downloadTask.ts
new file mode 100644
index 0000000..c5002ae
--- /dev/null
+++ b/src/store/downloadTask.ts
@@ -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
+    }
+  }
+})
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..0e191d8
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,8 @@
+import { createPinia } from 'pinia'
+
+// 鍒涘缓pinia瀹炰緥
+const pinia = createPinia()
+
+export default pinia
+
+export * from './downloadTask'
diff --git a/src/views/exportTask.vue b/src/views/exportTask.vue
new file mode 100644
index 0000000..7feec2c
--- /dev/null
+++ b/src/views/exportTask.vue
@@ -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>
diff --git a/src/views/home.vue b/src/views/home.vue
new file mode 100644
index 0000000..4d72ddf
--- /dev/null
+++ b/src/views/home.vue
@@ -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>
diff --git a/src/views/login.vue b/src/views/login.vue
new file mode 100644
index 0000000..db63c47
--- /dev/null
+++ b/src/views/login.vue
@@ -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>
diff --git a/src/views/setting.vue b/src/views/setting.vue
new file mode 100644
index 0000000..bbcdaac
--- /dev/null
+++ b/src/views/setting.vue
@@ -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>
diff --git a/src/views/transmission.vue b/src/views/transmission.vue
new file mode 100644
index 0000000..97b45f6
--- /dev/null
+++ b/src/views/transmission.vue
@@ -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">
+                姝e湪涓嬭浇
+              </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>
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..491e093
--- /dev/null
+++ b/tsconfig.app.json
@@ -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/*"]
+    }
+  }
+}
diff --git a/tsconfig.electron.json b/tsconfig.electron.json
new file mode 100644
index 0000000..41e6581
--- /dev/null
+++ b/tsconfig.electron.json
@@ -0,0 +1,45 @@
+{
+  "compilerOptions": {
+    "outDir": "electron-commonJS/",
+    // 鎸囧畾缂栬瘧鍚庢枃浠舵墍鍦ㄧ洰褰曘��
+    "module": "CommonJS",
+    // 鎸囧畾缂栬瘧鍚庝唬鐮佷娇鐢ㄧ殑妯″潡鍖栬鑼冦��
+    "target": "esnext",
+    // 鐩爣璇█鐨勭増鏈�
+    "lib": [
+      "esnext", "dom"
+    ],
+    // TS闇�瑕佸紩鐢ㄧ殑搴�
+    "sourceMap": true,
+    // 鏄惁鐢熸垚鐩稿簲鐨凪ap鏄犲皠鐨勬枃浠讹紝榛樿锛歠alse銆�
+    "baseUrl": ".",
+    // 鐢ㄤ簬瑙f瀽闈炵粷瀵规ā鍧楀悕鐨勫熀鏈洰褰曪紝鐩稿妯″潡涓嶅彈褰卞搷銆�
+    "resolveJsonModule": true,
+    //鏄惁瑙f瀽 JSON 妯″潡锛岄粯璁わ細false銆�
+    "allowSyntheticDefaultImports": true,
+    //鏄惁鍏佽浠庢病鏈夐粯璁ゅ鍑虹殑妯″潡涓粯璁ゅ鍏ワ紝榛樿锛歠alse銆�
+    "moduleResolution": "node",
+    //鎸囧畾妯″潡瑙f瀽绛栫暐锛宯ode鎴朿lassic
+    "forceConsistentCasingInFileNames": true,
+    //鏄惁鍖哄垎鏂囦欢绯荤粺澶у皬鍐欒鍒欙紝榛樿锛歠alse銆�
+    "noImplicitReturns": true,
+    //妫�鏌ュ嚱鏁版槸鍚︿笉鍚湁闅愬紡杩斿洖鍊硷紝榛樿锛歠alse銆�
+    "noUnusedLocals": true,
+    //鏄惁妫�鏌ユ湭浣跨敤鐨勫眬閮ㄥ彉閲�
+    "allowJs": true,
+    //鏄惁瀵筳s鏂囦欢杩涜缂栬瘧锛岄粯璁わ細false銆�
+    "skipLibCheck": true,
+    //鏄惁璺宠繃澹版槑鏂囦欢鐨勭被鍨嬫鏌ワ紝杩欏彲浠ュ湪缂栬瘧鏈熼棿浠ョ壓鐗茬被鍨嬬郴缁熷噯纭�т负浠d环鏉ヨ妭鐪佹椂闂达紝榛樿锛歠alse銆�
+    "experimentalDecorators": true,
+    //鏄惁鍚敤瀵硅楗板櫒鐨勫疄楠屾�ф敮鎸侊紝瑁呴グ鍣ㄦ槸涓�绉嶈瑷�鐗规�э紝杩樻病鏈夊畬鍏ㄨ JavaScript 瑙勮寖鎵瑰噯锛岄粯璁わ細false銆�
+    "strict": false,
+    "esModuleInterop": true,
+    //鏄惁鍚姩鎵�鏈変弗鏍兼鏌ョ殑鎬诲紑鍏筹紝榛樿锛歠alse锛屽惎鍔ㄥ悗灏嗗紑鍚墍鏈夌殑涓ユ牸妫�鏌ラ�夐」銆�
+    "paths": {
+    }
+  },
+  "include": [
+    //鎸囧畾琚紪璇戞枃浠舵墍鍦ㄧ殑鐩綍
+    "electron/*"
+  ],
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..66b5e57
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,11 @@
+{
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    },
+    {
+      "path": "./tsconfig.app.json"
+    }
+  ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..46cf2e1
--- /dev/null
+++ b/tsconfig.node.json
@@ -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"]
+  }
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..1b47c8f
--- /dev/null
+++ b/vite.config.ts
@@ -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";'
+      }
+    }
+  }
+})

--
Gitblit v1.9.1