├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .yarnrc.yml ├── README.md ├── doc ├── config.png └── home.png ├── electron-builder.json ├── icon.png ├── package.json ├── scripts ├── build.js ├── dev-server.js └── private │ └── tsc.js ├── src ├── main │ ├── db.ts │ ├── ipc.ts │ ├── main.ts │ ├── preload.ts │ ├── static │ │ ├── .gitkeep │ │ └── icon.png │ └── tsconfig.json ├── renderer │ ├── App.vue │ ├── assets │ │ └── alipay.jpg │ ├── index.html │ ├── layout │ │ └── index.vue │ ├── main.ts │ ├── public │ │ └── .gitkeep │ ├── router │ │ └── index.ts │ ├── run │ │ ├── index.ts │ │ └── utils.ts │ ├── stores │ │ ├── cronjob.ts │ │ ├── fans.ts │ │ ├── index.ts │ │ ├── log.ts │ │ └── user.ts │ ├── style.scss │ ├── tsconfig.json │ ├── typings │ │ ├── electron.d.ts │ │ └── shims-vue.d.ts │ └── views │ │ ├── about │ │ └── index.vue │ │ ├── config │ │ └── index.vue │ │ ├── jobs │ │ └── index.vue │ │ └── login │ │ └── index.vue └── shared.d.ts ├── uno.config.ts ├── vite.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "curly": [ //强制if等语句添加大括号 5 | "error", 6 | "all" 7 | ], 8 | "brace-style": [ //强制大括号不换行 9 | "error", 10 | "1tbs" 11 | ], 12 | "no-console": "off", // 允许使用console 13 | "@typescript-eslint/brace-style": ["error", "1tbs"] 14 | }, 15 | "env": { 16 | "vue/setup-compiler-macros": true 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build/release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'V*.*.*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-13, windows-2022, ubuntu-latest] 15 | 16 | steps: 17 | - name: Install Snapcraft 18 | if: matrix.os == 'ubuntu-22.04' 19 | run: | 20 | sudo snap install snapcraft --classic 21 | 22 | - name: Check out Git repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Install Node.js, NPM and Yarn 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | - name: Build/release Electron app 30 | env: 31 | GH_TOKEN: ${{ secrets.github_token }} 32 | run: | 33 | yarn install 34 | yarn build --publish always -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | 5 | .vscode 6 | .idea 7 | .yarn 8 | 9 | .DS_Store -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | [点击这里进行下载](https://github.com/Curtion/douyu-keep/releases) 4 | 5 | ![img](./doc/home.png) 6 | ![img](./doc/config.png) 7 | 8 | 斗鱼平台自动赠送荧光棒, 支持功能: 9 | 1. 模式一:开机自启, 赠送完成后自动关闭 10 | 2. 模式二:保持程序运行,定时赠送 11 | 3. 赠送数量自定义或者百分比自定义 12 | 4. 启动时为托盘模式, 点击托盘按钮显示GUI界面 13 | 5. 支持Windows和MacOS 14 | 15 | # 后续功能 16 | 17 | - [ ] 双倍亲密度检测 18 | 19 | 寻求检测方法, 欢迎提供思路 20 | 21 | # 开发 22 | 23 | 升级: `yarn up`. 禁止升级chalk, 保持V4.1.2版本 24 | 25 | 1. `yarn` 安装依赖 26 | 2. `yarn dev` 开发模式 27 | 28 | # 打包 29 | 30 | 1. `yarn build:win` 31 | 2. `yarn build:mac` 32 | 3. `yarn build:linux` -------------------------------------------------------------------------------- /doc/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/doc/config.png -------------------------------------------------------------------------------- /doc/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/doc/home.png -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.curtion.dykeep", 3 | "productName": "斗鱼续牌工具", 4 | "artifactName": "douyu-keep-${version}-${os}_${arch}.${ext}", 5 | "directories": { 6 | "output": "dist" 7 | }, 8 | "nsis": { 9 | "oneClick": false, 10 | "perMachine": false, 11 | "allowToChangeInstallationDirectory": true, 12 | "shortcutName": "斗鱼续牌工具" 13 | }, 14 | "mac": { 15 | "target": { 16 | "target": "default", 17 | "arch": [ 18 | "x64", 19 | "arm64" 20 | ] 21 | } 22 | }, 23 | "win": { 24 | "target": "nsis" 25 | }, 26 | "linux": { 27 | "target": [ 28 | "rpm", 29 | "AppImage", 30 | "deb" 31 | ] 32 | }, 33 | "files": [ 34 | "build/main/**/*", 35 | { 36 | "from": "build/renderer", 37 | "to": "renderer", 38 | "filter": [ 39 | "**/*" 40 | ] 41 | }, 42 | { 43 | "from": "src/main/static", 44 | "to": "static", 45 | "filter": [ 46 | "**/*" 47 | ] 48 | }, 49 | "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}", 50 | "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}", 51 | "!**/node_modules/*.d.ts", 52 | "!**/node_modules/.bin", 53 | "!src", 54 | "!config", 55 | "!README.md", 56 | "!scripts", 57 | "!build/renderer", 58 | "!dist" 59 | ] 60 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "douyu-keep", 3 | "version": "1.1.0", 4 | "description": "斗鱼粉丝牌续牌", 5 | "author": { 6 | "name": "Curtion", 7 | "url": "https://github.com/Curtion", 8 | "email": "curtion@126.com" 9 | }, 10 | "license": "MIT", 11 | "repository": "https://github.com/Curtion/douyu-keep.git", 12 | "main": "build/main/main.js", 13 | "scripts": { 14 | "dev": "node scripts/dev-server.js", 15 | "build": "node scripts/build.js && electron-builder", 16 | "build:win": "node scripts/build.js && electron-builder --win", 17 | "build:mac": "node scripts/build.js && electron-builder --mac", 18 | "build:linux": "node scripts/build.js && electron-builder --linux", 19 | "up": "taze major -I" 20 | }, 21 | "dependencies": { 22 | "auto-launch": "^5.0.6", 23 | "axios": "^1.4.0", 24 | "cron": "^2.4.0", 25 | "cron-parser": "^4.8.1", 26 | "dayjs": "^1.11.9", 27 | "electron-store": "^8.1.0", 28 | "nprogress": "^0.2.0", 29 | "pinia": "^2.1.6", 30 | "vue": "^3.3.4", 31 | "vue-router": "4.2.4", 32 | "vuetify": "^3.3.12" 33 | }, 34 | "devDependencies": { 35 | "@antfu/eslint-config": "^0.40.2", 36 | "@iconify-json/bx": "^1.1.7", 37 | "@iconify-json/carbon": "^1.1.19", 38 | "@mdi/font": "^7.2.96", 39 | "@types/auto-launch": "^5.0.2", 40 | "@types/nprogress": "^0.2.0", 41 | "@vitejs/plugin-vue": "^4.2.3", 42 | "chalk": "^4.1.2", 43 | "chokidar": "^3.5.3", 44 | "daisyui": "^3.5.1", 45 | "electron": "^25.4.0", 46 | "electron-builder": "^24.6.3", 47 | "eslint": "^8.46.0", 48 | "eslint-plugin-vue": "^9.17.0", 49 | "sass": "^1.64.2", 50 | "taze": "^0.11.2", 51 | "typescript": "^5.1.6", 52 | "unocss": "^0.54.3", 53 | "vite": "^4.4.9" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const Path = require('node:path') 2 | const FileSystem = require('node:fs') 3 | const Chalk = require('chalk') 4 | const Vite = require('vite') 5 | const compileTs = require('./private/tsc') 6 | 7 | function buildRenderer() { 8 | return Vite.build({ 9 | configFile: Path.join(__dirname, '..', 'vite.config.js'), 10 | base: './', 11 | mode: 'production', 12 | }) 13 | } 14 | 15 | function buildMain() { 16 | const mainPath = Path.join(__dirname, '..', 'src', 'main') 17 | FileSystem.cpSync( 18 | Path.join(__dirname, '..', 'src', 'main', 'static'), 19 | Path.join(__dirname, '..', 'build', 'main', 'static'), 20 | { recursive: true }, 21 | ) 22 | return compileTs(mainPath) 23 | } 24 | 25 | FileSystem.rmSync(Path.join(__dirname, '..', 'build'), { 26 | recursive: true, 27 | force: true, 28 | }) 29 | 30 | console.log(Chalk.blueBright('Transpiling renderer & main...')) 31 | 32 | Promise.allSettled([ 33 | buildRenderer(), 34 | buildMain(), 35 | ]).then(() => { 36 | console.log(Chalk.greenBright('Renderer & main successfully transpiled! (ready to be built with electron-builder)')) 37 | }) 38 | -------------------------------------------------------------------------------- /scripts/dev-server.js: -------------------------------------------------------------------------------- 1 | const ChildProcess = require('node:child_process') 2 | const Path = require('node:path') 3 | const FileSystem = require('node:fs') 4 | const process = require('node:process') 5 | const Vite = require('vite') 6 | const Chalk = require('chalk') 7 | const Chokidar = require('chokidar') 8 | const Electron = require('electron') 9 | const compileTs = require('./private/tsc') 10 | 11 | process.env.NODE_ENV = 'development' 12 | 13 | let viteServer = null 14 | let electronProcess = null 15 | let electronProcessLocker = false 16 | let rendererPort = 0 17 | 18 | async function startRenderer() { 19 | viteServer = await Vite.createServer({ 20 | configFile: Path.join(__dirname, '..', 'vite.config.js'), 21 | mode: 'development', 22 | }) 23 | 24 | return viteServer.listen() 25 | } 26 | 27 | async function startElectron() { 28 | if (electronProcess) { // single instance lock 29 | return 30 | } 31 | 32 | try { 33 | await compileTs(Path.join(__dirname, '..', 'src', 'main')) 34 | } catch { 35 | console.log(Chalk.redBright('Could not start Electron because of the above typescript error(s).')) 36 | electronProcessLocker = false 37 | return 38 | } 39 | 40 | const args = [ 41 | Path.join(__dirname, '..', 'build', 'main', 'main.js'), 42 | rendererPort, 43 | ] 44 | electronProcess = ChildProcess.spawn(Electron, args) 45 | electronProcessLocker = false 46 | 47 | electronProcess.stdout.on('data', data => 48 | process.stdout.write(Chalk.blueBright('[electron] ') + Chalk.white(data.toString())), 49 | ) 50 | 51 | electronProcess.stderr.on('data', data => 52 | process.stderr.write(Chalk.blueBright('[electron] ') + Chalk.white(data.toString())), 53 | ) 54 | 55 | electronProcess.on('exit', () => stop()) 56 | } 57 | 58 | function restartElectron() { 59 | if (electronProcess) { 60 | electronProcess.removeAllListeners('exit') 61 | electronProcess.kill() 62 | electronProcess = null 63 | } 64 | 65 | if (!electronProcessLocker) { 66 | electronProcessLocker = true 67 | startElectron() 68 | } 69 | } 70 | 71 | function copyStaticFiles() { 72 | copy('static') 73 | } 74 | 75 | /* 76 | The working dir of Electron is build/main instead of src/main because of TS. 77 | tsc does not copy static files, so copy them over manually for dev server. 78 | */ 79 | function copy(path) { 80 | FileSystem.cpSync( 81 | Path.join(__dirname, '..', 'src', 'main', path), 82 | Path.join(__dirname, '..', 'build', 'main', path), 83 | { recursive: true }, 84 | ) 85 | } 86 | 87 | function stop() { 88 | viteServer.close() 89 | process.exit() 90 | } 91 | 92 | async function start() { 93 | console.log(`${Chalk.greenBright('=======================================')}`) 94 | console.log(`${Chalk.greenBright('Starting Electron + Vite Dev Server...')}`) 95 | console.log(`${Chalk.greenBright('=======================================')}`) 96 | 97 | const devServer = await startRenderer() 98 | rendererPort = devServer.config.server.port 99 | 100 | copyStaticFiles() 101 | startElectron() 102 | 103 | const path = Path.join(__dirname, '..', 'src', 'main') 104 | Chokidar.watch(path, { 105 | cwd: path, 106 | }).on('change', (path) => { 107 | console.log(`${Chalk.blueBright('[electron] ')}Change in ${path}. reloading... 🚀`) 108 | 109 | if (path.startsWith(Path.join('static', '/'))) { 110 | copy(path) 111 | } 112 | 113 | restartElectron() 114 | }) 115 | } 116 | 117 | start() 118 | -------------------------------------------------------------------------------- /scripts/private/tsc.js: -------------------------------------------------------------------------------- 1 | const ChildProcess = require('node:child_process') 2 | const process = require('node:process') 3 | const Chalk = require('chalk') 4 | 5 | function compile(directory) { 6 | return new Promise((resolve, reject) => { 7 | const tscProcess = ChildProcess.exec('tsc', { 8 | cwd: directory, 9 | }) 10 | 11 | tscProcess.stdout.on('data', data => 12 | process.stdout.write(Chalk.yellowBright('[tsc] ') + Chalk.white(data.toString())), 13 | ) 14 | 15 | tscProcess.on('exit', (exitCode) => { 16 | if (exitCode > 0) { 17 | reject(exitCode) 18 | } else { 19 | resolve() 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | module.exports = compile 26 | -------------------------------------------------------------------------------- /src/main/db.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store' 2 | 3 | const store = new Store() 4 | 5 | export default { 6 | get: (key: string) => { 7 | return store.get(key) 8 | }, 9 | set: (key: string, value: any) => { 10 | store.set(key, value) 11 | }, 12 | delete: (key: string) => { 13 | store.delete(key) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/main/ipc.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { BrowserWindow, app, ipcMain, session } from 'electron' 3 | import cronParse from 'cron-parser' 4 | import { CronJob } from 'cron' 5 | import AutoLaunch from 'auto-launch' 6 | 7 | import db from './db' 8 | 9 | let job: CronJob | undefined 10 | 11 | export default function init(callback: () => void) { 12 | ipcMain.handle('login', () => { 13 | return new Promise((resolve) => { 14 | const win = new BrowserWindow({ 15 | width: 1200, 16 | height: 800, 17 | resizable: false, 18 | webPreferences: { 19 | webSecurity: false, 20 | }, 21 | }) 22 | win.loadURL('https://www.douyu.com/directory', { 23 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', 24 | }) 25 | if (process.env.NODE_ENV === 'development') { 26 | win.webContents.openDevTools({ mode: 'detach' }) 27 | } 28 | win.on('closed', () => { 29 | resolve() 30 | }) 31 | }) 32 | }) 33 | 34 | ipcMain.handle('getGift', () => { 35 | return new Promise((resolve) => { 36 | const win = new BrowserWindow({ 37 | width: 1200, 38 | height: 800, 39 | resizable: false, 40 | webPreferences: { 41 | webSecurity: false, 42 | }, 43 | show: false, 44 | }) 45 | win.loadURL('https://www.douyu.com/4120796', { 46 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', 47 | }) 48 | win.on('closed', () => { 49 | resolve() 50 | }) 51 | setTimeout(() => { 52 | win.close() 53 | }, 10000) 54 | }) 55 | }) 56 | 57 | ipcMain.handle('db', (_, { type, key, value }) => { 58 | return new Promise((resolve, reject) => { 59 | if (type === 'get') { 60 | resolve(db.get(key)) 61 | } else if (type === 'set') { 62 | db.set(key, value) 63 | resolve(undefined) 64 | } else if (type === 'delete') { 65 | db.delete(key) 66 | resolve(undefined) 67 | } else { 68 | reject(new Error('未知的消息')) 69 | } 70 | }) 71 | }) 72 | 73 | ipcMain.handle('cron', (_, cron) => { 74 | return new Promise((resolve, reject) => { 75 | try { 76 | const interval = cronParse.parseExpression(cron) 77 | const data: Date[] = [] 78 | for (let i = 0; i < 3; i++) { 79 | data.push(interval.next().toDate()) 80 | } 81 | resolve(data) 82 | } catch (error) { 83 | reject(error) 84 | } 85 | }) 86 | }) 87 | 88 | ipcMain.handle('getDyAndSid', () => { 89 | return new Promise((resolve, reject) => { 90 | try { 91 | session.defaultSession.cookies.get({ domain: 'douyu.com' }) 92 | .then((cookies) => { 93 | const sid = cookies.find(cookie => cookie.name === 'acf_uid')?.value 94 | const dy = cookies.find(cookie => cookie.name === 'dy_did')?.value 95 | if (sid && dy) { 96 | resolve({ sid, dy }) 97 | } else { 98 | reject(new Error('Cookie中没有找到sid和dy')) 99 | } 100 | }).catch((error) => { 101 | reject(error) 102 | }) 103 | } catch (error) { 104 | reject(error) 105 | } 106 | }) 107 | }) 108 | 109 | ipcMain.handle('close', () => { 110 | return new Promise((resolve, reject) => { 111 | try { 112 | app.exit() 113 | resolve() 114 | } catch (error) { 115 | reject(error) 116 | } 117 | }) 118 | }) 119 | 120 | ipcMain.handle('timer', (_, { cron, stop = false }) => { 121 | return new Promise((resolve, reject) => { 122 | try { 123 | if (stop) { 124 | if (job) { 125 | job.stop() 126 | job = undefined 127 | return resolve() 128 | } 129 | return resolve() 130 | } 131 | if (job) { 132 | job.stop() 133 | job = undefined 134 | } 135 | job = new CronJob( 136 | cron, 137 | callback, 138 | null, 139 | false, 140 | 'Asia/Shanghai', 141 | ) 142 | job.start() 143 | resolve() 144 | } catch (error) { 145 | reject(error) 146 | } 147 | }) 148 | }) 149 | 150 | ipcMain.handle('boot', (_, status) => { 151 | return new Promise((resolve, reject) => { 152 | try { 153 | const douyuAutoLauncher = new AutoLaunch({ 154 | name: 'douyu-keep', 155 | path: process.execPath, 156 | }) 157 | if (status) { 158 | douyuAutoLauncher.isEnabled() 159 | .then((isEnabled) => { 160 | if (isEnabled) { 161 | resolve() 162 | } else { 163 | douyuAutoLauncher.enable().then(resolve).catch(reject) 164 | } 165 | }).catch(reject) 166 | } else { 167 | douyuAutoLauncher.isEnabled() 168 | .then((isEnabled) => { 169 | if (isEnabled) { 170 | douyuAutoLauncher.disable().then(resolve).catch(reject) 171 | } else { 172 | resolve() 173 | } 174 | }).catch(reject) 175 | } 176 | } catch (error) { 177 | reject(error) 178 | } 179 | }) 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /src/main/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import process from 'node:process' 3 | import { BrowserWindow, Menu, Tray, app, nativeImage, session } from 'electron' 4 | import ipc from './ipc' 5 | 6 | function createWindow() { 7 | const newSession = session.fromPartition('persist:main') 8 | const mainWindow = new BrowserWindow({ 9 | width: 1000, 10 | height: 500, 11 | webPreferences: { 12 | preload: join(__dirname, 'preload.js'), 13 | contextIsolation: true, 14 | session: newSession, 15 | }, 16 | show: false, 17 | resizable: false, 18 | icon: join(__dirname, '../', '../', 'icon.png'), 19 | }) 20 | // dialog.showMessageBox({ message: app.getPath('userData').toString() }) 21 | ipc(() => { 22 | mainWindow.webContents.send('startJob') 23 | }) 24 | if (process.env.NODE_ENV === 'development') { 25 | const rendererPort = process.argv[2] 26 | mainWindow.loadURL(`http://localhost:${rendererPort}`, { 27 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', 28 | }) 29 | mainWindow.webContents.openDevTools({ mode: 'detach' }) 30 | } else { 31 | mainWindow.loadFile(join(app.getAppPath(), 'renderer', 'index.html')) 32 | mainWindow.setMenuBarVisibility(false) 33 | } 34 | mainWindow.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { 35 | session.defaultSession.cookies.get({ url: 'https://www.douyu.com' }) 36 | .then((cookies) => { 37 | const cookiesStr = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join(';') 38 | callback({ 39 | requestHeaders: { 40 | ...details.requestHeaders, 41 | Origin: '*', 42 | Referer: 'https://www.douyu.com/', 43 | Cookie: cookiesStr, 44 | }, 45 | }) 46 | }).catch((error) => { 47 | console.log(error) 48 | callback({ 49 | requestHeaders: { 50 | ...details.requestHeaders, 51 | Origin: '*', 52 | Referer: 'https://www.douyu.com/', 53 | }, 54 | }) 55 | }) 56 | }, 57 | ) 58 | mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => { 59 | callback({ 60 | responseHeaders: { 61 | ...details.responseHeaders, 62 | 'Access-Control-Allow-Origin': ['*'], 63 | }, 64 | }) 65 | }) 66 | return mainWindow 67 | } 68 | 69 | app.whenReady().then(() => { 70 | const mainWindow = createWindow() 71 | const img = nativeImage.createFromPath(join(__dirname, './static/icon.png')) 72 | if (process.platform === 'darwin') { 73 | img.setTemplateImage(true) 74 | } 75 | const tray = new Tray(img) 76 | const contextMenu = Menu.buildFromTemplate([ 77 | { 78 | label: '显示/隐藏', 79 | click: () => { 80 | mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() 81 | mainWindow.isVisible() ? mainWindow.setSkipTaskbar(false) : mainWindow.setSkipTaskbar(true) 82 | }, 83 | }, 84 | { 85 | label: '退出', 86 | click: () => mainWindow.destroy(), 87 | }, 88 | ]) 89 | tray.setToolTip('斗鱼续牌工具') 90 | tray.setContextMenu(contextMenu) 91 | tray.on('click', () => { 92 | if (!mainWindow.isVisible()) { 93 | mainWindow.show() 94 | mainWindow.setSkipTaskbar(true) 95 | } 96 | }) 97 | mainWindow.on('close', (event) => { 98 | mainWindow.hide() 99 | mainWindow.setSkipTaskbar(true) 100 | event.preventDefault() 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | contextBridge.exposeInMainWorld('electron', { 4 | ipcRenderer, 5 | }) 6 | contextBridge.exposeInMainWorld('node', { 7 | handleStartJob: (callback: () => void) => ipcRenderer.on('startJob', callback), 8 | }) 9 | -------------------------------------------------------------------------------- /src/main/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/src/main/static/.gitkeep -------------------------------------------------------------------------------- /src/main/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/src/main/static/icon.png -------------------------------------------------------------------------------- /src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "../../build/main", 10 | "allowJs": true, 11 | "noImplicitAny": false, 12 | }, 13 | "exclude": ["static"], 14 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "../shared.d.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/renderer/assets/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/src/renderer/assets/alipay.jpg -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 斗鱼续牌应用 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/layout/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { createVuetify } from 'vuetify' 4 | import * as directives from 'vuetify/directives' 5 | import * as components from 'vuetify/components' 6 | import App from './App.vue' 7 | import router from './router' 8 | import '@unocss/reset/tailwind.css' 9 | import 'virtual:uno.css' 10 | import './style.scss' 11 | import 'vuetify/styles' 12 | import '@mdi/font/css/materialdesignicons.css' 13 | 14 | const vuetify = createVuetify({ 15 | components, 16 | directives, 17 | }) 18 | const pinia = createPinia() 19 | 20 | const app = createApp(App) 21 | app.use(pinia) 22 | app.use(router) 23 | app.use(vuetify) 24 | app.mount('#app') 25 | -------------------------------------------------------------------------------- /src/renderer/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Curtion/douyu-keep/6619ee3e5fb9610c14a6117095eebfaddc5139dc/src/renderer/public/.gitkeep -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { createRouter, createWebHashHistory } from 'vue-router' 3 | import NProgress from 'nprogress' 4 | import Layout from '~/layout/index.vue' 5 | import { useLogin } from '~/stores' 6 | 7 | const routes: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | component: Layout, 11 | redirect: '/jobs', 12 | children: [ 13 | { 14 | path: 'jobs', 15 | component: () => import('~/views/jobs/index.vue'), 16 | beforeEnter: async (to, from, next) => { 17 | to.params.from = from.path 18 | next() 19 | }, 20 | }, 21 | { 22 | path: 'config', 23 | component: () => import('~/views/config/index.vue'), 24 | }, 25 | { 26 | path: 'about', 27 | component: () => import('~/views/about/index.vue'), 28 | }, 29 | ], 30 | }, 31 | { 32 | path: '/login', 33 | component: () => import('~/views/login/index.vue'), 34 | }, 35 | ] 36 | 37 | const router = createRouter({ 38 | history: createWebHashHistory(), 39 | routes, 40 | }) 41 | 42 | router.beforeEach(async (to, from, next) => { 43 | NProgress.start() 44 | if (to.path === '/login') { 45 | return next() 46 | } 47 | const { getUser } = useLogin() 48 | try { 49 | await getUser() 50 | next() 51 | } catch (err) { 52 | console.log(err) 53 | next('/login') 54 | } 55 | }) 56 | 57 | router.afterEach(() => { 58 | NProgress.done() 59 | }) 60 | 61 | export default router 62 | -------------------------------------------------------------------------------- /src/renderer/run/index.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import dayjs from 'dayjs' 3 | import { computeGiftCountOfNumber, computeGiftCountOfPercentage, getConfig, getDid, getDyAndSid, getGiftNumber, sendGift, sleep } from './utils' 4 | import type { sendConfig } from '~/stores/fans' 5 | import { useLog } from '~/stores' 6 | 7 | const log = useLog() 8 | 9 | const { text, runing } = storeToRefs(log) 10 | 11 | export default async function startJob(manual: boolean) { 12 | if (runing.value) { 13 | text.value = '任务正在执行中...' 14 | return 15 | } 16 | const { type } = await getConfig() 17 | if (type === '自动执行' || manual) { 18 | await start() 19 | } 20 | } 21 | 22 | async function start() { 23 | runing.value = true 24 | text.value = '即将开始任务' 25 | let index = 0 26 | const timer = setInterval(() => { 27 | index++ 28 | text.value = `正在领取荧光棒${index}秒...` 29 | }, 1000) 30 | try { 31 | await window.electron.ipcRenderer.invoke('getGift') 32 | } catch (error) { 33 | clearInterval(timer) 34 | text.value = `领取荧光棒失败${error}` 35 | setTimeout(() => { 36 | runing.value = false 37 | }, 10000) 38 | return 39 | } 40 | clearInterval(timer) 41 | text.value = '领取荧光棒成功' 42 | const { time, timeValue } = await getConfig() 43 | const dayOfWeek = dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6 44 | if (time === '自定义' && !timeValue.includes(dayOfWeek)) { 45 | text.value = '领取荧光棒成功, 但未满足赠送时机' 46 | setTimeout(() => { 47 | runing.value = false 48 | }, 10000) 49 | return 50 | } 51 | let number = 0 52 | try { 53 | number = await getGiftNumber() 54 | } catch (error) { 55 | text.value = `获取荧光棒数量失败${error}` 56 | setTimeout(() => { 57 | runing.value = false 58 | }, 10000) 59 | return 60 | } 61 | if (number === 0) { 62 | text.value = '荧光棒数量为0, 结束任务' 63 | setTimeout(() => { 64 | runing.value = false 65 | }, 10000) 66 | return 67 | } 68 | text.value = `荧光棒数量为${number}` 69 | sleep(2000) 70 | const { send, model, close } = await getConfig() 71 | let Jobs: sendConfig = {} 72 | if (model === 1) { 73 | // 百分比赠送 74 | try { 75 | const sendNumber = await computeGiftCountOfPercentage(number, send) 76 | Jobs = sendNumber 77 | } catch (error: any) { 78 | text.value = error.toString() 79 | setTimeout(() => { 80 | runing.value = false 81 | }, 10000) 82 | return 83 | } 84 | } else if (model === 2) { 85 | // 指定数量赠送 86 | try { 87 | const sendNumber = await computeGiftCountOfNumber(number, send) 88 | Jobs = sendNumber 89 | } catch (error: any) { 90 | text.value = error.toString() 91 | setTimeout(() => { 92 | runing.value = false 93 | }, 10000) 94 | return 95 | } 96 | } 97 | text.value = '开始获取必要参数dy和sid' 98 | let args: sendArgs = {} 99 | try { 100 | args = await getDyAndSid() 101 | } catch (error) { 102 | text.value = `结束任务:获取参数失败${error}` 103 | setTimeout(() => { 104 | runing.value = false 105 | }, 10000) 106 | return 107 | } 108 | let faildNumber = 0 109 | for (const item of Object.values(Jobs)) { 110 | try { 111 | if (item.count === 0) { 112 | continue 113 | } 114 | text.value = `即将赠送${item.roomId}房间${item.count}个荧光棒` 115 | const did = await getDid(item.roomId.toString()) 116 | args.did = did 117 | item.count = item?.count ?? 0 + faildNumber 118 | await sendGift(args, item) 119 | faildNumber = 0 120 | text.value = `赠送${item.roomId}房间${item.count}个荧光棒成功` 121 | } catch (error) { 122 | faildNumber += item?.count ?? 0 123 | text.value = `${item.roomId}房间赠送失败${error}, ${item.count}个荧光棒自动移交给下一个房间` 124 | } 125 | await sleep(2000) 126 | } 127 | if (faildNumber > 0) { 128 | text.value = `任务执行完毕, 有${faildNumber}个荧光棒未赠送成功` 129 | } else { 130 | text.value = '任务执行完毕' 131 | } 132 | setTimeout(async () => { 133 | runing.value = false 134 | if (close) { 135 | window.electron.ipcRenderer.invoke('close') 136 | } 137 | }, 2000) 138 | } 139 | -------------------------------------------------------------------------------- /src/renderer/run/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { Config, SendGift, sendConfig } from '~/stores/fans' 3 | 4 | export async function getGiftNumber() { 5 | const { data } = await axios.get('https://www.douyu.com/japi/prop/backpack/web/v1?rid=4120796') 6 | if (data.data?.list?.length > 0) { 7 | return data.data?.list.find((item: any) => item.id === 268)?.count ?? 0 8 | } else { 9 | return 0 10 | } 11 | } 12 | 13 | export function sleep(time: number) { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | resolve() 17 | }, time) 18 | }) 19 | } 20 | 21 | export async function sendGift(args: sendArgs, Job: SendGift) { 22 | const data = new FormData() 23 | data.append('rid', String(Job.roomId)) 24 | data.append('prop_id', String(Job.giftId)) 25 | data.append('num', String(Job.count)) 26 | data.append('sid', args.sid!) 27 | data.append('did', args.did!) 28 | data.append('dy', args.dy!) 29 | const res = await axios.post('https://www.douyu.com/member/prop/send', data) 30 | return JSON.stringify(res.data) 31 | } 32 | 33 | export async function getDyAndSid() { 34 | const data: sendArgs = await window.electron.ipcRenderer.invoke('getDyAndSid') 35 | return data 36 | } 37 | 38 | export async function getDid(roomid: string) { 39 | return new Promise((resolve, reject) => { 40 | axios.get(`https://www.douyu.com/${roomid}`).then((res) => { 41 | const did: string = res.data.match(/owner_uid =(.*?);/)[1].trim() 42 | if (did !== undefined) { 43 | resolve(did) 44 | } else { 45 | reject(new Error('获取did失败')) 46 | } 47 | }) 48 | }) 49 | } 50 | 51 | export async function getConfigByUser(user: string) { 52 | const cfg = await window.electron.ipcRenderer.invoke('db', { 53 | type: 'get', 54 | key: user, 55 | }) 56 | try { 57 | return JSON.parse(cfg) as Config 58 | } catch (error) { 59 | throw new Error('当前用户配置不存在!') 60 | } 61 | } 62 | 63 | export async function getConfig() { 64 | const cfg = await window.electron.ipcRenderer.invoke('db', { 65 | type: 'get', 66 | key: 'config', 67 | }) 68 | try { 69 | return JSON.parse(cfg) as Config 70 | } catch (error) { 71 | throw new Error('请先配置任务!') 72 | } 73 | } 74 | 75 | export async function computeGiftCountOfNumber(number: number, send: sendConfig) { 76 | const cfgCountNumber = Object.values(send).reduce((a, b) => a + (b.number === -1 ? 0 : b.number), 0) 77 | if (cfgCountNumber > number) { 78 | return Promise.reject(new Error(`荧光棒数量不足,请重新配置. 当前${number}个, 需求${cfgCountNumber}个`)) 79 | } 80 | const sendSort = Object.values(send).sort((a, b) => b.number - a.number) 81 | for (let i = 0; i < sendSort.length; i++) { 82 | const item = sendSort[i] 83 | if (i === sendSort.length - 1) { 84 | const count = number - sendSort.reduce((a, b) => a + (b.count || 0), 0) 85 | item.count = count 86 | } else { 87 | item.count = item.number 88 | } 89 | } 90 | const newSend = sendSort.reduce((a, b) => { 91 | return { 92 | ...a, 93 | [b.roomId]: b, 94 | } 95 | }, {} as sendConfig) 96 | return newSend 97 | } 98 | 99 | export async function computeGiftCountOfPercentage(number: number, send: sendConfig) { 100 | const sendSort = Object.values(send).sort((a, b) => a.percentage - b.percentage) 101 | for (let i = 0; i < sendSort.length; i++) { 102 | const item = sendSort[i] 103 | if (i === sendSort.length - 1) { 104 | const count = number - sendSort.reduce((a, b) => a + (b.count || 0), 0) 105 | item.count = count 106 | } else { 107 | if (item.percentage === 0) { 108 | item.count = 0 109 | continue 110 | } 111 | const count = Math.floor((item.percentage / 100) * number) 112 | item.count = count === 0 ? 1 : count 113 | } 114 | } 115 | const newSend = sendSort.reduce((a, b) => { 116 | return { 117 | ...a, 118 | [b.roomId]: b, 119 | } 120 | }, {} as sendConfig) 121 | const cfgCountNumber = Object.values(newSend).reduce((a, b) => a + (b.number <= -1 ? 1 : b.number), 0) 122 | if (cfgCountNumber > number) { 123 | return Promise.reject(new Error(`荧光棒数量不足,请重新配置. 当前${number}个, 需求${cfgCountNumber}个`)) 124 | } 125 | return newSend 126 | } 127 | -------------------------------------------------------------------------------- /src/renderer/stores/cronjob.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useCronStatus = defineStore('cronStatus', () => { 5 | const isCronRuning = ref(false) 6 | return { 7 | isCronRuning, 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /src/renderer/stores/fans.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import axios from 'axios' 4 | 5 | export interface Fans { 6 | roomId: number // 房间号 7 | name: string // 主播名称 8 | level: number // 粉丝牌等级 9 | rank: number // 粉丝牌排名 10 | intimacy: string // 亲密度 11 | today: number // 今日亲密度 12 | } 13 | 14 | export interface SendGift { 15 | roomId: number // 房间号 16 | number: number // 礼物数量 17 | giftId: number // 礼物id 18 | percentage: number // 亲密度百分比 19 | count?: number // 实际荧光棒数量 20 | } 21 | 22 | export interface Config { 23 | boot: boolean // 开机自启 24 | close: boolean // 自动关闭 25 | type: '自动执行' | '定时执行' | '手动执行' // 执行模式 '自动执行', '定时执行', '手动执行' 26 | time: '跟随执行模式' | '自定义' // 执行时机 '跟随执行模式', '自定义' 27 | timeValue: (1 | 2 | 3 | 4 | 5 | 6 | 0)[] // 执行时机(星期) 28 | cron: string // cron表达式, 执行模式为定时执行时有效 29 | model: 1 | 2 // 荧光棒分配逻辑 1: 百分比 2: 指定数量 30 | send: sendConfig 31 | } 32 | export type sendConfig = Record 33 | 34 | export const useFans = defineStore('fans', () => { 35 | const fansList = ref([]) 36 | const loading = ref(false) 37 | const getFansList = () => { 38 | return new Promise((resolve, reject) => { 39 | loading.value = true 40 | axios.get('https://www.douyu.com/member/cp/getFansBadgeList').then((res) => { 41 | const table = res.data.match(/fans-badge-list">([\S\s]*?)<\/table>/)[1] 42 | const list = table.match(//g) 43 | list?.shift() 44 | const fans: Fans[] = list?.map((item: any) => { 45 | const tds = item.match(//g) 46 | const res: Fans = { 47 | name: String(item.match(/data-anchor_name=\"([\S\s]+?)\"/)[1]), 48 | roomId: Number(item.match(/data-fans-room=\"(\d+)\"/)[1]), 49 | level: Number(item.match(/data-fans-level=\"(\d+)\"/)[1]), 50 | rank: Number(item.match(/data-fans-rank=\"(\d+)\"/)[1]), 51 | intimacy: String(tds[2].replace(/<([\s\S]*?)>/g, '').trim()), 52 | today: Number(tds[3].replace(/<([\s\S]*?)>/g, '').trim()), 53 | } 54 | return res 55 | }) 56 | fansList.value = fans.sort((a, b) => b.level - a.level) 57 | resolve(fansList.value) 58 | }).catch((err) => { 59 | reject(err) 60 | }).finally(() => { 61 | loading.value = false 62 | }) 63 | }) 64 | } 65 | return { 66 | fansList, 67 | loading, 68 | getFansList, 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /src/renderer/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user' 2 | export * from './fans' 3 | export * from './log' 4 | export * from './cronjob' 5 | -------------------------------------------------------------------------------- /src/renderer/stores/log.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { reactive, toRefs } from 'vue' 3 | 4 | export interface Log { 5 | title: string 6 | text: string 7 | runing: boolean 8 | } 9 | 10 | export const useLog = defineStore('log', () => { 11 | const log = reactive({ 12 | title: '执行日志', 13 | text: 'Loading...', 14 | runing: false, 15 | }) 16 | return toRefs(log) 17 | }) 18 | -------------------------------------------------------------------------------- /src/renderer/stores/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { defineStore } from 'pinia' 3 | import { ref } from 'vue' 4 | import { getGiftNumber } from '~/run/utils' 5 | 6 | export interface User { 7 | isLogin: boolean 8 | phone: string 9 | level: string 10 | giftNumber: number 11 | } 12 | 13 | export const useLogin = defineStore('user', () => { 14 | const user = ref({ 15 | isLogin: false, 16 | phone: '', 17 | level: '', 18 | giftNumber: -1, 19 | }) 20 | const getUser = async (force = false) => { 21 | if (user.value.isLogin && !force) { 22 | return Promise.resolve(user.value) 23 | } 24 | try { 25 | const number = await getGiftNumber() 26 | const info = await axios.get('https://www.douyu.com/member/cp/cp_rpc_ajax') 27 | if (typeof info.data === 'object') { 28 | user.value.isLogin = true 29 | user.value.phone = info.data.info?.mobile_phone?.slice(-11) 30 | user.value.level = info.data.exp_info?.current?.pic_url 31 | user.value.giftNumber = number 32 | return Promise.resolve(user.value) 33 | } else { 34 | user.value.isLogin = false 35 | return Promise.reject(new Error('当前未登录!')) 36 | } 37 | } catch (error) { 38 | user.value.isLogin = false 39 | return Promise.reject(error) 40 | } 41 | } 42 | return { 43 | user, 44 | getUser, 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /src/renderer/style.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | height: 100vh; 3 | } 4 | 5 | html { 6 | overflow: hidden !important; 7 | } 8 | 9 | ::-webkit-scrollbar { 10 | width: 5px; /* 滚动条宽度 */ 11 | } 12 | 13 | ::-webkit-scrollbar-track { 14 | background-color: #f1f1f1; /* 滚动条背景颜色 */ 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | background-color: #888; /* 滚动条滑块颜色 */ 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client"], 15 | "paths": { 16 | "~/*": ["./*"] 17 | } 18 | }, 19 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue", "../shared.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/typings/electron.d.ts: -------------------------------------------------------------------------------- 1 | import * as Electron from 'electron'; 2 | 3 | export default interface ElectronApi { 4 | ipcRenderer: Electron.IpcRenderer, 5 | } 6 | 7 | declare global { 8 | interface Window { 9 | electron: ElectronApi, 10 | node: { 11 | handleStartJob: (callback: () => void) => void, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/typings/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /src/renderer/views/config/index.vue: -------------------------------------------------------------------------------- 1 | 186 | 187 | 394 | 395 | 402 | -------------------------------------------------------------------------------- /src/renderer/views/jobs/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 205 | 206 | 212 | -------------------------------------------------------------------------------- /src/renderer/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 79 | -------------------------------------------------------------------------------- /src/shared.d.ts: -------------------------------------------------------------------------------- 1 | declare interface sendArgs { 2 | sid?: string 3 | dy?: string 4 | did?: string 5 | } 6 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss' 2 | 3 | export default defineConfig({ 4 | presets: [ 5 | presetAttributify(), 6 | presetUno(), 7 | presetIcons(), 8 | ], 9 | }) 10 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import Path from 'node:path' 2 | import UnoCSS from 'unocss/vite' 3 | import vuePlugin from '@vitejs/plugin-vue' 4 | import { defineConfig } from 'vite' 5 | 6 | const config = defineConfig({ 7 | root: Path.join(__dirname, 'src', 'renderer'), 8 | publicDir: 'public', 9 | server: { 10 | port: 8080, 11 | }, 12 | open: false, 13 | build: { 14 | outDir: Path.join(__dirname, 'build', 'renderer'), 15 | emptyOutDir: true, 16 | }, 17 | plugins: [ 18 | vuePlugin(), 19 | UnoCSS(), 20 | ], 21 | resolve: { 22 | alias: { 23 | '~': Path.resolve(__dirname, './src/renderer'), 24 | }, 25 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], 26 | }, 27 | }) 28 | 29 | export default config 30 | --------------------------------------------------------------------------------