init, electron+vite+better-sqlite3

This commit is contained in:
YILS
2025-07-10 17:43:46 +08:00
commit b56eb0038a
43 changed files with 6303 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dist-electron
dist-native
release
*.local
*.db
*.db-journal
# Editor directories and files
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

23
.npmrc Normal file
View File

@@ -0,0 +1,23 @@
# npm源地址
registry=https://registry.npmmirror.com/
# node-sass依赖
sass_binary_site=https://npmmirror.com/mirrors/node-sass/
# weex项目依赖phantomjs-prebuilt
phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs/
# electron依赖
# 阿里云采用双份拷贝策略,即x.x版本的electron存储时既有 vx.x也有x.x
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_custom_dir={{ version }}
# sqlite3预构建二进制
sqlite3_binary_host_mirror=http://npmmirror.com/mirrors/
# better-sqlite3预构建二进制
better_sqlite3_binary_host=https://registry.npmmirror.com/-/binary/better-sqlite3
# node-inspector依赖
profiler_binary_host_mirror=http://npmmirror.com/mirrors/node-inspector/
# chromedriver安装失败
chromedriver_cdnurl=https://npmmirror.com/mirrors/chromedriver
# sentry-cli依赖
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
# 平铺依赖以便electron-builder依赖分析与打包
shamefully-hoist=true
engine-strict = true

1
.nvmdrc Normal file
View File

@@ -0,0 +1 @@
22.17.0

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"scss.validate": false,
"css.validate": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.quickSuggestions": {
"strings": "on"
}
}

48
.vscode/vue3.code-snippets vendored Normal file
View File

@@ -0,0 +1,48 @@
{
// Place your 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Print Vue3 SFC": {
"scope": "vue",
"prefix": "v3",
"body": [
"<template>",
" <div class=\"\">$1</div>",
"</template>\n",
"<script lang=\"ts\" setup>",
"//$2",
"</script>\n",
"<style lang=\"scss\" scoped>",
"//$3",
"</style>\n",
],
},
"Print style": {
"scope": "vue",
"prefix": "st",
"body": ["<style lang=\"scss\" scoped>", "//$1", "</style>\n"],
},
"Print script": {
"scope": "vue",
"prefix": "sc",
"body": ["<script lang=\"ts\" setup>", "//$1", "</script>\n"],
},
"Print template": {
"scope": "vue",
"prefix": "te",
"body": ["<template>", " <view class=\"\">$1</view>", "</template>\n"],
},
}

39
electron-builder.json5 Normal file
View File

@@ -0,0 +1,39 @@
// @see - https://www.electron.build/configuration/configuration
{
$schema: 'https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json',
productName: '短视频工厂',
appId: 'com.yils.short-video-factory',
asar: true,
directories: {
output: 'release/${version}',
},
files: ['dist', 'dist-electron', 'dist-native'],
npmRebuild: false, // disable rebuild node_modules 使用包内自带预构建二进制,而不重新构建
beforePack: './scripts/before-pack.js',
mac: {
target: ['dmg'],
artifactName: '${productName}-Mac-${arch}-${version}-Installer.${ext}',
icon: './public/icon.png',
},
win: {
target: [
{
target: 'nsis',
},
],
artifactName: '${productName}-Windows-${arch}-${version}-Setup.${ext}',
icon: './public/icon.png',
},
nsis: {
language: '0x0804',
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
deleteAppDataOnUninstall: false,
},
linux: {
target: ['AppImage'],
artifactName: '${productName}-Linux-${arch}-${version}.${ext}',
icon: './public/icon.png',
},
}

34
electron/electron-env.d.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
/**
* 已构建的目录结构
*
* ```tree
* ├─┬─┬ dist
* │ │ └── index.html
* │ │
* │ ├─┬ dist-electron
* │ │ ├── main.js
* │ │ └── preload.js
* │
* ```
*/
APP_ROOT: string
/** /dist/ or /public/ */
VITE_PUBLIC: string
}
}
// 在渲染器进程中使用,在 `preload.ts` 中暴露方法
interface Window {
ipcRenderer: import('electron').IpcRenderer
sqlite: {
query: (param: import('./sqlite/types').queryParam) => Promise<any>
insert: (param: import('./sqlite/types').insertParam) => Promise<any>
update: (param: import('./sqlite/types').updateParam) => Promise<any>
delete: (param: import('./sqlite/types').deleteParam) => Promise<any>
bulkInsertOrUpdate: (param: import('./sqlite/types').bulkInsertOrUpdateParam) => Promise<any>
}
}

62
electron/ipc.ts Normal file
View File

@@ -0,0 +1,62 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { BrowserWindow, ipcMain } from 'electron'
import { queryParam, insertParam, updateParam, deleteParam } from './sqlite/types'
import { sqBulkInsertOrUpdate, sqDelete, sqInsert, sqQuery, sqUpdate } from './sqlite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
process.env.APP_ROOT = path.join(__dirname, '..')
// 🚧 使用['ENV_NAME'] 避免 vite:define plugin - Vite@2.x
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: RENDERER_DIST
export default function initIPC(win: BrowserWindow) {
// 是否最大化
ipcMain.handle('win-maxed', () => {
return win?.isMaximized()
})
//最小化
ipcMain.on('win-min', () => {
win?.minimize()
})
//最大化
ipcMain.on('win-max', () => {
if (win?.isMaximized()) {
win?.restore()
} else {
win?.maximize()
}
})
//关闭程序
ipcMain.on('win-close', () => {
win?.close()
})
// sqlite 查询
ipcMain.handle('sqlite-query', (_event, params: queryParam) => {
return sqQuery(params)
})
// sqlite 插入
ipcMain.handle('sqlite-insert', async (_event, params: insertParam) => {
return await sqInsert(params)
})
// sqlite 更新
ipcMain.handle('sqlite-update', async (_event, params: updateParam) => {
return await sqUpdate(params)
})
// sqlite 删除
ipcMain.handle('sqlite-delete', async (_event, params: deleteParam) => {
return await sqDelete(params)
})
// sqlite 批量插入或更新
ipcMain.handle('sqlite-bulk-insert-or-update', async (_event, params: any) => {
return await sqBulkInsertOrUpdate(params)
})
}

View File

@@ -0,0 +1,18 @@
import { app, session } from 'electron'
/**
* 解决 electron15 后跨域cookie无法携带问题
*/
export default function useCookieAllowCrossSite() {
app.whenReady().then(() => {
const filter = { urls: ['https://*/*'] }
session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => {
if (details.responseHeaders && details.responseHeaders['Set-Cookie']) {
for (let i = 0; i < details.responseHeaders['Set-Cookie'].length; i++) {
details.responseHeaders['Set-Cookie'][i] += ';SameSite=None;Secure'
}
}
callback({ responseHeaders: details.responseHeaders })
})
})
}

104
electron/main.ts Normal file
View File

@@ -0,0 +1,104 @@
import { app, BrowserWindow, screen } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import GlobalSetting from '../setting.global'
import initIPC from './ipc'
import { initSqlite } from './sqlite'
import useCookieAllowCrossSite from './lib/cookieAllowCrossSite'
// 用于引入 CommonJS 模块的方法
// import { createRequire } from 'node:module'
// const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// 已构建的目录结构
//
// ├─┬─┬ dist
// │ │ └── index.html
// │ │
// │ ├─┬ dist-electron
// │ │ ├── main.js
// │ │ └── preload.mjs
// │
process.env.APP_ROOT = path.join(__dirname, '..')
// 🚧 使用['ENV_NAME'] 避免 vite:define plugin - Vite@2.x
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: RENDERER_DIST
let win: BrowserWindow | null
function createWindow() {
const { width, height } = screen.getPrimaryDisplay().workAreaSize
win = new BrowserWindow({
icon: path.join(process.env.VITE_PUBLIC, 'icon.png'),
title: GlobalSetting.appName,
width: Math.ceil(width * 0.7),
height: Math.ceil(height * 0.7),
minWidth: 800,
minHeight: 600,
backgroundColor: '#F3F3F3',
show: false,
frame: false,
webPreferences: {
webSecurity: false,
preload: path.join(__dirname, 'preload.js'),
},
})
// 优化应用进入体验
win.once('ready-to-show', () => {
win?.show()
})
//测试向渲染器进程发送的活动推送消息。
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString())
})
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL)
} else {
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
}
}
//关闭所有窗口后退出macOS除外。在那里这很常见
//让应用程序及其菜单栏保持活动状态,直到用户退出
//显式使用Cmd+Q。
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
win = null
}
})
app.on('activate', () => {
//在OS X上当出现以下情况时通常会在应用程序中重新创建一个窗口
//单击dock图标后没有其他打开的窗口。
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// 禁用硬件加速
// app.disableHardwareAcceleration();
app.whenReady().then(() => {
createWindow()
initSqlite()
initIPC(win as BrowserWindow)
// 允许跨站请求携带cookie
useCookieAllowCrossSite()
// 禁用 CORS
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
// 允许本地网络请求
app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')
})

45
electron/preload.ts Normal file
View File

@@ -0,0 +1,45 @@
import { ipcRenderer, contextBridge } from 'electron'
import { queryParam, insertParam, updateParam, deleteParam } from './sqlite/types'
// --------- 向界面渲染进程暴露某些API ---------
contextBridge.exposeInMainWorld('ipcRenderer', {
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
},
once(...args: Parameters<typeof ipcRenderer.once>) {
const [channel, listener] = args
return ipcRenderer.once(channel, (event, ...args) => listener(event, ...args))
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args
return ipcRenderer.off(channel, ...omit)
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args
return ipcRenderer.send(channel, ...omit)
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args
return ipcRenderer.invoke(channel, ...omit)
},
})
contextBridge.exposeInMainWorld('sqlite', {
query: async (param: queryParam) => {
return await ipcRenderer.invoke('sqlite-query', param)
},
insert: async (param: insertParam) => {
return await ipcRenderer.invoke('sqlite-insert', param)
},
update: async (param: updateParam) => {
return await ipcRenderer.invoke('sqlite-update', param)
},
delete: async (param: deleteParam) => {
return await ipcRenderer.invoke('sqlite-delete', param)
},
bulkInsertOrUpdate: async (param: any) => {
return await ipcRenderer.invoke('sqlite-bulk-insert-or-update', param)
},
})

146
electron/sqlite/index.ts Normal file
View File

@@ -0,0 +1,146 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import BetterSqlite3 from 'better-sqlite3'
import { queryParam, insertParam, updateParam, deleteParam, bulkInsertOrUpdateParam } from './types'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
process.env.APP_ROOT = path.join(__dirname, '..')
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const NATIVE_DIST = path.join(process.env.APP_ROOT, 'dist-native')
// 根据当前系统和架构判断使用哪个native模块
const platform = process.platform
const arch = process.arch
const nativeDir = `${platform}-${arch}`
// 设置native模块路径
const nativePath = VITE_DEV_SERVER_URL
? path.join(
process.env.APP_ROOT,
'native/better-sqlite3',
`better-sqlite3-v9.6.0-electron-v110-${nativeDir}.node`,
)
: path.join(NATIVE_DIST, 'better-sqlite3.node')
const dbPath = path.join(process.cwd(), 'data.db')
console.log('BetterSqlite3 path:', nativePath)
console.log('Database path:', dbPath)
class Database {
private db
constructor() {
this.db = new BetterSqlite3(dbPath, {
nativeBinding: nativePath,
})
}
open(): Promise<void> {
return new Promise<void>((resolve, _reject) => {
this.db.pragma('foreign_keys = ON')
console.log('Connected to the database.')
resolve()
})
}
close(): Promise<void> {
return new Promise<void>((resolve, _reject) => {
this.db.close()
console.log('Database closed.')
resolve()
})
}
query(param: queryParam): Promise<any[]> {
return new Promise<any[]>((resolve, _reject) => {
const stmt = this.db.prepare(param.sql)
const rows = param.params ? stmt.all(...param.params) : stmt.all()
resolve(rows)
})
}
insert(param: insertParam): Promise<number> {
return new Promise<number>((resolve, _reject) => {
const keys = Object.keys(param.data)
const values = Object.values(param.data)
const placeholders = keys.map(() => '?').join(',')
const sql = `INSERT INTO ${param.table} (${keys.join(',')}) VALUES (${placeholders})`
const stmt = this.db.prepare(sql)
const info = stmt.run(...values)
resolve(info.lastInsertRowid as number)
})
}
async checkForeignKey(table: string, id: string): Promise<boolean> {
const result = await this.query({ sql: `SELECT 1 FROM ${table} WHERE id = ?`, params: [id] })
return result.length > 0
}
update(param: updateParam): Promise<number> {
return new Promise<number>((resolve, _reject) => {
const entries = Object.entries(param.data)
.map(([key, _value]) => `${key} = ?`)
.join(',')
const params = Object.values(param.data)
const sql = `UPDATE ${param.table} SET ${entries} WHERE ${param.condition}`
const stmt = this.db.prepare(sql)
const info = stmt.run(...params)
resolve(info.changes)
})
}
delete(param: deleteParam): Promise<void> {
return new Promise<void>((resolve, _reject) => {
const sql = `DELETE FROM ${param.table} WHERE ${param.condition}`
const stmt = this.db.prepare(sql)
stmt.run()
resolve()
})
}
async bulkInsertOrUpdate(param: bulkInsertOrUpdateParam): Promise<void> {
return new Promise<void>((resolve, _reject) => {
const keys = Object.keys(param.data[0])
const placeholders = keys.map(() => '?').join(',')
const updatePlaceholders = keys.map((key) => `${key} = excluded.${key}`).join(',')
const sql = `
INSERT INTO ${param.table} (${keys.join(',')})
VALUES (${placeholders})
ON CONFLICT(id) DO UPDATE SET ${updatePlaceholders}
`
const stmt = this.db.prepare(sql)
// 开始事务
const transaction = this.db.transaction((records) => {
for (const record of records) {
stmt.run(...Object.values(record))
}
})
transaction(param.data)
resolve()
})
}
}
const db = new Database()
export const initSqlite = async () => {
try {
await db.open()
console.log('Database initialized.')
} catch (err) {
console.error('Error opening database:', err)
}
}
export const sqQuery = db.query.bind(db)
export const sqInsert = db.insert.bind(db)
export const sqUpdate = db.update.bind(db)
export const sqDelete = db.delete.bind(db)
export const sqBulkInsertOrUpdate = db.bulkInsertOrUpdate.bind(db)

25
electron/sqlite/types.ts Normal file
View File

@@ -0,0 +1,25 @@
export interface queryParam {
sql: string
params?: any[]
}
export interface insertParam {
table: string
data: { [key: string]: any }
}
export interface updateParam {
table: string
data: { [key: string]: any }
condition: string
}
export interface deleteParam {
table: string
condition: string
}
export interface bulkInsertOrUpdateParam {
table: string
data: any[]
}

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "short-video-factory",
"description": "短视频工厂一键生成产品营销与泛内容短视频AI批量自动剪辑",
"version": "0.0.1",
"author": {
"name": "YILS",
"developer": "YILS",
"email": "yils_lin@163.com",
"url": "https://yils.blog/"
},
"main": "dist-electron/main.js",
"scripts": {
"dev": "cross-env VITE_CJS_IGNORE_WARNING=true vite",
"build": "vue-tsc && cross-env VITE_CJS_IGNORE_WARNING=true vite build && electron-builder",
"preview": "vite preview",
"format": "prettier --write .",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@vueuse/core": "^13.5.0",
"better-sqlite3": "9.6.0",
"mitt": "^3.0.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vuetify": "^3.9.0"
},
"devDependencies": {
"@mdi/font": "^7.4.47",
"@types/better-sqlite3": "^7.6.13",
"@vitejs/plugin-vue": "^6.0.0",
"cross-env": "^7.0.3",
"electron": "^22.3.27",
"electron-builder": "^24.13.3",
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "5.6.2",
"unocss": "^66.3.3",
"vite": "^7.0.3",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "3.0.1"
},
"packageManager": "pnpm@10.12.4",
"engines": {
"node": ">=22.17.0",
"pnpm": ">=10.12.4"
}
}

5223
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- electron
- esbuild
- vue-demi

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

35
scripts/before-pack.js Normal file
View File

@@ -0,0 +1,35 @@
const path = require('path')
const fs = require('fs')
const Arch = {
0: 'ia32',
1: 'x64',
2: 'armv7l',
3: 'arm64',
4: 'universal',
}
function copyNativeFileSync(sourceDir, targetDir) {
const sourcePath = path.join(__dirname, `../native/${sourceDir}`)
const targetPath = path.join(__dirname, `../dist-native/${targetDir}`)
if (!fs.existsSync(sourcePath)) {
throw new Error(`Native binary not found at: ${sourcePath}`)
}
if (!fs.existsSync(path.join(__dirname, `../dist-native`))) {
fs.mkdirSync(path.dirname(targetPath))
}
fs.copyFileSync(sourcePath, targetPath)
}
module.exports = function beforePack(context) {
// console.log('[beforePack] context:', context)
const platform = context.packager.platform.nodeName
const arch = Arch[context.arch]
// better-sqlite3
copyNativeFileSync(
`better-sqlite3/better-sqlite3-v9.6.0-electron-v110-${platform}-${arch}.node`,
`better-sqlite3.node`,
)
}

3
setting.global.ts Normal file
View File

@@ -0,0 +1,3 @@
export default {
appName: '短视频工厂',
}

9
src/App.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<RouterView />
</template>
<script lang="ts" setup>
import { RouterView } from 'vue-router'
</script>
<style lang="scss" scoped></style>

8
src/assets/base.scss Normal file
View File

@@ -0,0 +1,8 @@
html,
body {
overflow-y: hidden;
overflow-x: hidden;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
'Source Han Sans', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}

107
src/layout/default.vue Normal file
View File

@@ -0,0 +1,107 @@
<template>
<div class="layout-container">
<div class="logo" v-if="!route.meta.hideAppIcon">
<img src="/icon.png" alt="" />
<span>{{ GlobalSetting.appName }}</span>
</div>
<div class="window-control-bar">
<div class="control-btn control-btn-min" @click="handleMin">
<v-icon icon="mdi-window-minimize" size="small" />
</div>
<div class="control-btn control-btn-max" @click="handleMax">
<v-icon icon="mdi-window-maximize" size="small" v-if="!windowIsMaxed" />
<v-icon icon="mdi-window-restore" size="small" v-else />
</div>
<div class="control-btn control-btn-close" @click="handleClose">
<v-icon icon="mdi-window-close" size="small" />
</div>
</div>
<RouterView />
</div>
</template>
<script lang="ts" setup>
import GlobalSetting from '../../setting.global'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const windowIsMaxed = ref(false)
window.addEventListener('resize', async () => {
windowIsMaxed.value = await window.ipcRenderer.invoke('win-maxed')
})
const handleMin = () => {
window.ipcRenderer.send('win-min')
}
const handleMax = () => {
window.ipcRenderer.send('win-max')
}
const handleClose = () => {
window.ipcRenderer.send('win-close')
}
</script>
<style lang="scss" scoped>
.layout-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
--title-bar-height: 40px;
.logo {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
height: var(--title-bar-height);
padding-left: 15px;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
user-select: none;
-webkit-app-region: drag;
img {
width: 24px;
height: 24px;
}
}
.window-control-bar {
position: absolute;
z-index: 9999;
top: 0;
right: 0;
display: flex;
align-items: center;
font-size: 14px;
user-select: none;
.control-btn {
transition: all 0.1s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
width: 42px;
height: var(--title-bar-height);
box-sizing: border-box;
&:hover {
@apply bg-gray-200;
}
&-close {
&:hover {
@apply text-white bg-red-600;
}
}
}
}
}
</style>

42
src/main.ts Normal file
View File

@@ -0,0 +1,42 @@
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
import 'virtual:uno.css'
import './assets/base.scss'
import { createApp } from 'vue'
import GlobalSetting from '../setting.global'
import router from './router/index.ts'
import store from './store/index.ts'
import App from './App.vue'
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
})
document.title = GlobalSetting.appName
const app = createApp(App)
app.use(vuetify)
app.use(router)
app.use(store)
app.mount('#app').$nextTick(() => {
// Use contextBridge
window.ipcRenderer.on('main-process-message', (_event, message) => {
console.log(message)
})
})

21
src/router/index.ts Normal file
View File

@@ -0,0 +1,21 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LayoutDefault from '@/layout/default.vue'
import Home from '@/views/Home/index.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: LayoutDefault,
children: [
{
path: '',
component: Home,
},
],
},
],
})
export default router

9
src/router/router.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
public?: boolean
title?: string
hideAppIcon?: boolean
}
}

11
src/store/app.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineStore } from 'pinia'
export const useUserStore = defineStore(
'app',
() => {
return {}
},
{
persist: true,
},
)

8
src/store/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia().use(piniaPluginPersistedstate)
export default store
export * from './app'

15
src/views/Home/index.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div class="w-full">
<div class="w-full h-[40px] drag relative border-b">
<div class="window-control-bar-no-drag-mask"></div>
</div>
</div>
</template>
<script lang="ts" setup>
//
</script>
<style lang="scss" scoped>
//
</style>

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

12
stylelint.config.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
rules: {
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['apply', 'variants', 'responsive', 'screen'],
},
],
'declaration-block-trailing-semicolon': null,
'no-descending-specificity': null,
},
}

31
tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "electron"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

15
tsconfig.node.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}

44
uno.config.ts Normal file
View File

@@ -0,0 +1,44 @@
import {
defineConfig,
presetIcons,
presetTypography,
presetWebFonts,
presetWind3,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
theme: {
colors: {
// ...
},
},
shortcuts: [['drag-bar', 'drag after:window-control-bar-no-drag-mask']],
rules: [
['drag', { '-webkit-app-region': 'drag' }],
['no-drag', { '-webkit-app-region': 'no-drag' }],
[
'window-control-bar-no-drag-mask',
{
position: 'absolute',
top: '0',
right: '0',
width: '120px',
height: '35px',
'-webkit-app-region': 'no-drag',
},
],
],
presets: [
presetWind3(),
presetIcons(),
presetTypography(),
presetWebFonts({
fonts: {
// ...
},
}),
],
transformers: [transformerDirectives(), transformerVariantGroup()],
})

51
vite.config.ts Normal file
View File

@@ -0,0 +1,51 @@
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron/simple'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import UnoCSS from 'unocss/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
UnoCSS(),
electron({
main: {
// Shortcut of `build.lib.entry`.
entry: 'electron/main.ts',
vite: {
build: {
rollupOptions: {
external: ['better-sqlite3'],
},
},
},
},
preload: {
// Shortcut of `build.rollupOptions.input`.
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
input: path.join(__dirname, 'electron/preload.ts'),
},
// Ployfill the Electron and Node.js API for Renderer process.
// If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process.
// See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer
renderer:
process.env.NODE_ENV === 'test'
? // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808
undefined
: {},
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
build: {
rollupOptions: {},
chunkSizeWarningLimit: 2048,
},
})