mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 03:15:03 +08:00
init, electron+vite+better-sqlite3
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal 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
23
.npmrc
Normal 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
|
||||
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal 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
48
.vscode/vue3.code-snippets
vendored
Normal 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
39
electron-builder.json5
Normal 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
34
electron/electron-env.d.ts
vendored
Normal 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
62
electron/ipc.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
18
electron/lib/cookieAllowCrossSite.ts
Normal file
18
electron/lib/cookieAllowCrossSite.ts
Normal 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
104
electron/main.ts
Normal 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
45
electron/preload.ts
Normal 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
146
electron/sqlite/index.ts
Normal 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
25
electron/sqlite/types.ts
Normal 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
14
index.html
Normal 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>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
51
package.json
Normal file
51
package.json
Normal 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
5223
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- electron
|
||||
- esbuild
|
||||
- vue-demi
|
||||
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
35
scripts/before-pack.js
Normal file
35
scripts/before-pack.js
Normal 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
3
setting.global.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
appName: '短视频工厂',
|
||||
}
|
||||
9
src/App.vue
Normal file
9
src/App.vue
Normal 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
8
src/assets/base.scss
Normal 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
107
src/layout/default.vue
Normal 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
42
src/main.ts
Normal 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
21
src/router/index.ts
Normal 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
9
src/router/router.d.ts
vendored
Normal 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
11
src/store/app.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'app',
|
||||
() => {
|
||||
return {}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
8
src/store/index.ts
Normal file
8
src/store/index.ts
Normal 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
15
src/views/Home/index.vue
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
12
stylelint.config.js
Normal file
12
stylelint.config.js
Normal 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
31
tsconfig.json
Normal 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
15
tsconfig.node.json
Normal 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
44
uno.config.ts
Normal 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
51
vite.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user