mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-28 07:42:41 +08:00
init
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
16
.env
Normal file
16
.env
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
NODE_ENV = production
|
||||||
|
# 程序配置
|
||||||
|
## 程序名称
|
||||||
|
MAIN_VITE_TITLE = "ChatLens"
|
||||||
|
# 全局 API 配置
|
||||||
|
MAIN_VITE_SERVER_API = 127.0.0.1
|
||||||
|
|
||||||
|
# 浏览器环境
|
||||||
|
RENDERER_VITE_SERVER_URL =
|
||||||
|
|
||||||
|
# 程序信息
|
||||||
|
RENDERER_VITE_SITE_TITLE = "聊天记录分析"
|
||||||
|
RENDERER_VITE_SITE_KEYWORDS = ""
|
||||||
|
RENDERER_VITE_SITE_DES = ""
|
||||||
|
RENDERER_VITE_SITE_URL = ""
|
||||||
|
RENDERER_VITE_SITE_LOGO = "/assets/images/favicon.ico"
|
||||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.gitignore
|
||||||
19
.eslintrc.cjs
Normal file
19
.eslintrc.cjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'@electron-toolkit',
|
||||||
|
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||||
|
'@vue/eslint-config-typescript/recommended',
|
||||||
|
'@vue/eslint-config-prettier',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/snippets.code-snippets
|
||||||
|
|
||||||
|
# Project
|
||||||
|
dist/
|
||||||
|
.history/
|
||||||
|
out
|
||||||
|
.obskey
|
||||||
|
release-config.json
|
||||||
|
src/auto-imports.d.ts
|
||||||
|
|
||||||
|
# 使用npm作为包管理器
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.cursor
|
||||||
|
.vscode
|
||||||
39
.npmrc
Normal file
39
.npmrc
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 参考 https://docs.npmjs.com/files/npmrc
|
||||||
|
|
||||||
|
# 设置使用淘宝镜像地址
|
||||||
|
registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 设置一些二进制文件镜像地址
|
||||||
|
disturl=https://npmmirror.com/dist
|
||||||
|
chromedriver-cdnurl=https://npmmirror.com/mirrors/chromedriver
|
||||||
|
couchbase-binary-host-mirror=https://npmmirror.com/mirrors/couchbase/v{version}
|
||||||
|
debug-binary-host-mirror=https://npmmirror.com/mirrors/node-inspector
|
||||||
|
electron-mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||||
|
flow-bin-binary-host-mirror=https://npmmirror.com/mirrors/flow/v
|
||||||
|
fse-binary-host-mirror=https://npmmirror.com/mirrors/fsevents
|
||||||
|
fuse-bindings-binary-host-mirror=https://npmmirror.com/mirrors/fuse-bindings/v{version}
|
||||||
|
git4win-mirror=https://npmmirror.com/mirrors/git-for-windows
|
||||||
|
gl-binary-host-mirror=https://npmmirror.com/mirrors/gl/v{version}
|
||||||
|
grpc-node-binary-host-mirror=https://npmmirror.com/mirrors
|
||||||
|
hackrf-binary-host-mirror=https://npmmirror.com/mirrors/hackrf/v{version}
|
||||||
|
leveldown-binary-host-mirror=https://npmmirror.com/mirrors/leveldown/v{version}
|
||||||
|
leveldown-hyper-binary-host-mirror=https://npmmirror.com/mirrors/leveldown-hyper/v{version}
|
||||||
|
mknod-binary-host-mirror=https://npmmirror.com/mirrors/mknod/v{version}
|
||||||
|
node-sqlite3-binary-host-mirror=https://npmmirror.com/mirrors
|
||||||
|
node-tk5-binary-host-mirror=https://npmmirror.com/mirrors/node-tk5/v{version}
|
||||||
|
nodegit-binary-host-mirror=https://npmmirror.com/mirrors/nodegit/v{version}/
|
||||||
|
operadriver-cdnurl=https://npmmirror.com/mirrors/operadriver
|
||||||
|
phantomjs-cdnurl=https://npmmirror.com/mirrors/phantomjs
|
||||||
|
profiler-binary-host-mirror=https://npmmirror.com/mirrors/node-inspector/
|
||||||
|
puppeteer-download-host=https://npmmirror.com/mirrors
|
||||||
|
python-mirror=https://npmmirror.com/mirrors/python
|
||||||
|
rabin-binary-host-mirror=https://npmmirror.com/mirrors/rabin/v{version}
|
||||||
|
sass-binary-site=https://npmmirror.com/mirrors/node-sass
|
||||||
|
sodium-prebuilt-binary-host-mirror=https://npmmirror.com/mirrors/sodium-prebuilt/v{version}
|
||||||
|
sqlite3-binary-site=https://npmmirror.com/mirrors/sqlite3
|
||||||
|
utf-8-validate-binary-host-mirror=https://npmmirror.com/mirrors/utf-8-validate/v{version}
|
||||||
|
utp-native-binary-host-mirror=https://npmmirror.com/mirrors/utp-native/v{version}
|
||||||
|
zmq-prebuilt-binary-host-mirror=https://npmmirror.com/mirrors/zmq-prebuilt/v{version}
|
||||||
|
phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs/
|
||||||
|
shamefully-hoist=true
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
9
.prettierrc.yaml
Normal file
9
.prettierrc.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
singleQuote: true
|
||||||
|
semi: false
|
||||||
|
printWidth: 120
|
||||||
|
trailingComma: 'es5'
|
||||||
|
tabWidth: 2
|
||||||
|
useTabs: false
|
||||||
|
endOfLine: 'lf'
|
||||||
|
htmlWhitespaceSensitivity: 'ignore'
|
||||||
|
arrowParens: 'always'
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ChatLens
|
||||||
|
|
||||||
|
聊天记录分析工具
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **框架**: Electron + Vue 3 + TypeScript
|
||||||
|
- **构建工具**: electron-vite
|
||||||
|
- **UI 框架**: Nuxt UI + Tailwind CSS
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 启动开发环境
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 构建应用
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建 macOS 版本
|
||||||
|
pnpm build:mac
|
||||||
|
|
||||||
|
# 构建 Windows 版本
|
||||||
|
pnpm build:win
|
||||||
|
|
||||||
|
# 构建 Linux 版本
|
||||||
|
pnpm build:linux
|
||||||
|
|
||||||
|
# 构建所有平台
|
||||||
|
pnpm build:all
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
64
electron-builder.yml
Normal file
64
electron-builder.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 应用程序的唯一标识符
|
||||||
|
appId: com.chatlens.app
|
||||||
|
# 应用程序的产品名称
|
||||||
|
productName: ChatLens
|
||||||
|
# 构建资源所在的目录
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
# 包含在最终应用程序构建中的文件列表
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
# 哪些文件将不会被压缩,而是解压到构建目录
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
|
||||||
|
# Windows 平台配置
|
||||||
|
win:
|
||||||
|
executableName: ChatLens
|
||||||
|
target: nsis
|
||||||
|
|
||||||
|
# NSIS 安装器配置
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
artifactName: ChatLens-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
allowElevation: true
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
installerIcon: build/icon.ico
|
||||||
|
uninstallerIcon: build/icon.ico
|
||||||
|
|
||||||
|
# macOS 平台配置
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
|
||||||
|
# macOS 平台的 DMG 配置
|
||||||
|
dmg:
|
||||||
|
artifactName: ChatLens-${version}.${ext}
|
||||||
|
|
||||||
|
# Linux 平台配置
|
||||||
|
linux:
|
||||||
|
executableName: chatlens
|
||||||
|
icon: build/icon.png
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
- tar.gz
|
||||||
|
category: Utility
|
||||||
|
|
||||||
|
# AppImage 配置
|
||||||
|
appImage:
|
||||||
|
artifactName: ChatLens-${version}.${ext}
|
||||||
|
|
||||||
|
# 是否在构建之前重新编译原生模块
|
||||||
|
npmRebuild: false
|
||||||
59
electron.vite.config.ts
Normal file
59
electron.vite.config.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import ui from '@nuxt/ui/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'electron/main/index.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'electron/preload/index.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src/'),
|
||||||
|
'~': resolve('src/'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
ui({
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'green',
|
||||||
|
neutral: 'slate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
root: 'src/',
|
||||||
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'src/index.html'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
193
electron/main/index.ts
Normal file
193
electron/main/index.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { app, shell, BrowserWindow, protocol, nativeTheme } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { optimizer, is, platform } from '@electron-toolkit/utils'
|
||||||
|
import * as fs from 'fs/promises'
|
||||||
|
import { checkUpdate } from './update'
|
||||||
|
import mainIpcMain from './ipcMain'
|
||||||
|
|
||||||
|
class MainProcess {
|
||||||
|
mainWindow: BrowserWindow | null
|
||||||
|
constructor() {
|
||||||
|
// 主窗口
|
||||||
|
this.mainWindow = null
|
||||||
|
|
||||||
|
// 设置应用程序名称
|
||||||
|
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
||||||
|
// 初始化
|
||||||
|
this.checkApp().then(async (lockObtained) => {
|
||||||
|
if (lockObtained) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单例锁
|
||||||
|
async checkApp() {
|
||||||
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
app.quit()
|
||||||
|
// 未获得锁
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 聚焦到当前程序
|
||||||
|
else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.show()
|
||||||
|
if (this.mainWindow.isMinimized()) this.mainWindow.restore()
|
||||||
|
this.mainWindow.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 获得锁
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化程序
|
||||||
|
async init() {
|
||||||
|
// 注册应用协议
|
||||||
|
app.setAsDefaultProtocolClient('chatlens')
|
||||||
|
|
||||||
|
// 应用程序准备好之前注册
|
||||||
|
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
|
||||||
|
|
||||||
|
// 主应用程序事件
|
||||||
|
this.mainAppEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建主窗口
|
||||||
|
async createWindow() {
|
||||||
|
this.mainWindow = new BrowserWindow({
|
||||||
|
width: 1180,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 1180,
|
||||||
|
minHeight: 720,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
devTools: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置默认日间模式
|
||||||
|
nativeTheme.themeSource = 'light'
|
||||||
|
|
||||||
|
this.mainWindow.once('ready-to-show', () => {
|
||||||
|
this.mainWindow?.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主窗口事件
|
||||||
|
this.mainWindowEvents()
|
||||||
|
|
||||||
|
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
this.mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
this.mainWindow.loadFile(join(__dirname, '../../out/renderer/index.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主应用程序事件
|
||||||
|
mainAppEvents() {
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
// 设置Windows应用程序用户模型id
|
||||||
|
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
||||||
|
|
||||||
|
// 创建主窗口
|
||||||
|
this.createWindow()
|
||||||
|
// 检查更新逻辑
|
||||||
|
checkUpdate(this.mainWindow)
|
||||||
|
// 引入主进程ipcMain
|
||||||
|
mainIpcMain(this.mainWindow)
|
||||||
|
|
||||||
|
// 开发环境下 F12 打开控制台
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
this.createWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.isMacOS) {
|
||||||
|
this.mainWindow?.show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听渲染进程崩溃
|
||||||
|
app.on('render-process-gone', (e, w, d) => {
|
||||||
|
if (d.reason == 'crashed') {
|
||||||
|
w.reload()
|
||||||
|
}
|
||||||
|
fs.appendFile(`./error-log-${+new Date()}.txt`, `${new Date()}渲染进程被杀死${d.reason}\n`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 自定义协议
|
||||||
|
app.on('open-url', (_, url) => {
|
||||||
|
console.log('Received custom protocol URL:', url)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (!platform.isMacOS) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 只有显式调用quit才退出系统,区分MAC系统程序坞退出和点击X隐藏
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
app.isQuiting = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主窗口事件
|
||||||
|
mainWindowEvents() {
|
||||||
|
if (!this.mainWindow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.mainWindow.webContents.on('did-finish-load', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.mainWindow && this.mainWindow.webContents.send('app-started')
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainWindow.on('maximize', () => {
|
||||||
|
this.mainWindow?.webContents.send('windowState', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainWindow.on('unmaximize', () => {
|
||||||
|
this.mainWindow?.webContents.send('windowState', false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 窗口关闭
|
||||||
|
this.mainWindow.on('close', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
// @ts-ignore
|
||||||
|
if (!app.isQuiting) {
|
||||||
|
this.mainWindow?.hide()
|
||||||
|
} else {
|
||||||
|
app.exit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 捕获未捕获的异常
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught Exception:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
new MainProcess()
|
||||||
110
electron/main/ipcMain.ts
Normal file
110
electron/main/ipcMain.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { ipcMain, app, dialog, clipboard, shell } from 'electron'
|
||||||
|
import { autoUpdater } from 'electron-updater'
|
||||||
|
import * as fs from 'fs/promises'
|
||||||
|
|
||||||
|
const mainIpcMain = (win) => {
|
||||||
|
// ==================== 窗口操作 ====================
|
||||||
|
ipcMain.on('window-min', (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
win.minimize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-maxOrRestore', (ev) => {
|
||||||
|
const winSizeState = win.isMaximized()
|
||||||
|
winSizeState ? win.restore() : win.maximize()
|
||||||
|
ev.reply('windowState', win.isMaximized())
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-restore', () => {
|
||||||
|
win.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-hide', () => {
|
||||||
|
win.hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-close', () => {
|
||||||
|
win.close()
|
||||||
|
// @ts-ignore
|
||||||
|
app.isQuitting = true
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-resize', (_, data) => {
|
||||||
|
if (data.resize) {
|
||||||
|
win.setResizable(true)
|
||||||
|
} else {
|
||||||
|
win.setSize(1180, 720)
|
||||||
|
win.setResizable(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('open-devtools', () => {
|
||||||
|
win.webContents.openDevTools()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 更新检查 ====================
|
||||||
|
ipcMain.on('check-update', () => {
|
||||||
|
autoUpdater.checkForUpdates()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 通用工具 ====================
|
||||||
|
ipcMain.handle('show-message', (event, args) => {
|
||||||
|
event.sender.send('show-message', args)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
ipcMain.handle('copyData', async (_, data) => {
|
||||||
|
try {
|
||||||
|
clipboard.writeText(data)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制操作出错:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 文件系统操作 ====================
|
||||||
|
// 选择文件夹
|
||||||
|
ipcMain.handle('selectDir', async (_, defaultPath = '') => {
|
||||||
|
try {
|
||||||
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
|
title: '选择目录',
|
||||||
|
defaultPath: defaultPath || app.getPath('documents'),
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: '选择文件夹',
|
||||||
|
})
|
||||||
|
if (!canceled) {
|
||||||
|
return filePaths[0]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (err) {
|
||||||
|
console.error('选择文件夹时发生错误:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
ipcMain.handle('checkFileExist', async (_, filePath) => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在文件管理器中打开
|
||||||
|
ipcMain.handle('openInFolder', async (_, path) => {
|
||||||
|
try {
|
||||||
|
await fs.access(path)
|
||||||
|
await shell.showItemInFolder(path)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开目录时出错:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mainIpcMain
|
||||||
105
electron/main/update.ts
Normal file
105
electron/main/update.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { dialog, app } from 'electron'
|
||||||
|
import { autoUpdater } from 'electron-updater'
|
||||||
|
import { platform } from '@electron-toolkit/utils'
|
||||||
|
|
||||||
|
let isFirstShow = true
|
||||||
|
const checkUpdate = (win) => {
|
||||||
|
autoUpdater.autoDownload = false // 自动下载
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装
|
||||||
|
|
||||||
|
// 绕过开发模式更新检测,模拟线上更新(Skip checkForUpdates because application is not packed and dev update config is not forced)
|
||||||
|
// Object.defineProperty(app, 'isPackaged', {
|
||||||
|
// get() {
|
||||||
|
// return true
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
let showUpdateMessageBox = false
|
||||||
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
// win.webContents.send('show-message', 'electron:发现新版本')
|
||||||
|
if (showUpdateMessageBox) return
|
||||||
|
showUpdateMessageBox = true
|
||||||
|
dialog
|
||||||
|
.showMessageBox({
|
||||||
|
title: '发现新版本 v' + info.version,
|
||||||
|
message: '发现新版本 v' + info.version,
|
||||||
|
detail: '是否立即下载并安装新版本?',
|
||||||
|
buttons: ['立即下载', '取消'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 2,
|
||||||
|
type: 'question',
|
||||||
|
noLink: true,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
showUpdateMessageBox = false
|
||||||
|
if (result.response === 0) {
|
||||||
|
autoUpdater
|
||||||
|
.downloadUpdate()
|
||||||
|
.then(() => {
|
||||||
|
console.log('wait for post download operation')
|
||||||
|
})
|
||||||
|
.catch((downloadError) => {
|
||||||
|
dialog.showErrorBox('客户端下载失败', `err:${downloadError}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听下载进度事件
|
||||||
|
autoUpdater.on('download-progress', (progressObj) => {
|
||||||
|
console.log(`更新下载进度: ${progressObj.percent}%`)
|
||||||
|
win.webContents.send('update-download-progress', progressObj.percent)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下载完成
|
||||||
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
dialog
|
||||||
|
.showMessageBox({
|
||||||
|
title: '下载完成',
|
||||||
|
message: '新版本已准备就绪,是否现在安装?',
|
||||||
|
buttons: ['安装', platform.isMacOS ? '之后提醒' : '稍后(应用退出后自动安装)'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 2,
|
||||||
|
type: 'question',
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (result.response === 0) {
|
||||||
|
win.webContents.send('begin-install')
|
||||||
|
// @ts-ignore
|
||||||
|
app.isQuiting = true
|
||||||
|
setTimeout(() => {
|
||||||
|
setImmediate(() => {
|
||||||
|
autoUpdater.quitAndInstall()
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 不需要更新
|
||||||
|
autoUpdater.on('update-not-available', (info) => {
|
||||||
|
// 客户端打开会默认弹一次,用isFirstShow来控制不弹
|
||||||
|
if (isFirstShow) {
|
||||||
|
isFirstShow = false
|
||||||
|
} else {
|
||||||
|
win.webContents.send('show-message', {
|
||||||
|
type: 'success',
|
||||||
|
message: '已是最新版本',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
autoUpdater.on('error', (err, ev) => {
|
||||||
|
// 更新出错,其中一步错误都会emit
|
||||||
|
console.log('error事件:', err, ev)
|
||||||
|
dialog.showErrorBox('遇到错误', `err:${err}, ev:${ev}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待 3 秒再检查更新,确保窗口准备完成,用户进入系统
|
||||||
|
setTimeout(() => {
|
||||||
|
autoUpdater.checkForUpdatesAndNotify().catch()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkUpdate }
|
||||||
8
electron/preload/index.d.ts
vendored
Normal file
8
electron/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI
|
||||||
|
api: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
43
electron/preload/index.ts
Normal file
43
electron/preload/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
send: (channel, data) => {
|
||||||
|
// whitelist channels
|
||||||
|
const validChannels = [
|
||||||
|
'show-message',
|
||||||
|
'check-update',
|
||||||
|
'get-gpu-acceleration',
|
||||||
|
'set-gpu-acceleration',
|
||||||
|
'save-gpu-acceleration',
|
||||||
|
]
|
||||||
|
if (validChannels.includes(channel)) {
|
||||||
|
ipcRenderer.send(channel, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
receive: (channel, func) => {
|
||||||
|
const validChannels = ['show-message']
|
||||||
|
if (validChannels.includes(channel)) {
|
||||||
|
// Deliberately strip event as it includes `sender`
|
||||||
|
ipcRenderer.on(channel, (event, ...args) => func(...args))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.api = api
|
||||||
|
}
|
||||||
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "ChatLens",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "获取你的聊天记录年度分析报告",
|
||||||
|
"author": "",
|
||||||
|
"main": "./out/main/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"preview": "electron-vite preview",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"build:mac": "electron-vite build && electron-builder --mac",
|
||||||
|
"build:win": "electron-vite build && electron-builder --win",
|
||||||
|
"build:linux": "electron-vite build && electron-builder --linux",
|
||||||
|
"build:all": "electron-vite build && electron-builder --mac --win --linux"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"@nuxt/ui": "^4.2.1",
|
||||||
|
"@vueuse/core": "^14.0.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"vue-router": "^4.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@rushstack/eslint-patch": "^1.15.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"electron": "^35.0.0",
|
||||||
|
"electron-builder": "^26.0.12",
|
||||||
|
"electron-vite": "^3.0.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7379
pnpm-lock.yaml
generated
Normal file
7379
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
src/App.vue
Normal file
8
src/App.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<router-view />
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
8
src/assets/styles/main.css
Normal file
8
src/assets/styles/main.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
|
|
||||||
|
/* 配置主题颜色 */
|
||||||
|
:root {
|
||||||
|
--ui-primary: var(--color-green-500);
|
||||||
|
--ui-neutral: var(--color-slate-500);
|
||||||
|
}
|
||||||
32
src/components.d.ts
vendored
Normal file
32
src/components.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
|
||||||
|
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||||
|
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||||
|
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
|
||||||
|
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
|
||||||
|
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||||
|
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||||
|
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||||
|
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
|
||||||
|
USelect: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||||
|
USkeleton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Skeleton.vue')['default']
|
||||||
|
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||||
|
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||||
|
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||||
|
UToaster: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.2__26fe4596361b2ba7cfd995b4ae348088/node_modules/@nuxt/ui/dist/runtime/components/Toaster.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/env.d.ts
vendored
Normal file
7
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
14
src/index.html
Normal file
14
src/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>ChatLens</title>
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<link rel="icon" href="/assets/imgs/favicon.ico" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
src/main.ts
Normal file
16
src/main.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { router } from './routes/'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
import './assets/styles/main.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
pinia.use(piniaPluginPersistedstate)
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
55
src/pages/index.vue
Normal file
55
src/pages/index.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col items-center justify-center p-8">
|
||||||
|
<div class="text-center max-w-2xl">
|
||||||
|
<!-- Logo / 标题 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="w-20 h-20 mx-auto mb-6 bg-primary-500 rounded-2xl flex items-center justify-center">
|
||||||
|
<span class="text-4xl">💬</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
ChatLens
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
聊天记录分析工具 - 让你更好地了解你的聊天数据
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能入口 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||||
|
<UCard class="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<div class="text-center p-4">
|
||||||
|
<div class="text-3xl mb-3">📁</div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">导入聊天记录</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">支持多种格式的聊天记录导入</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard class="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<div class="text-center p-4">
|
||||||
|
<div class="text-3xl mb-3">📊</div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">数据分析</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">可视化展示聊天数据统计</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速开始按钮 -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton size="lg" color="primary">
|
||||||
|
开始使用
|
||||||
|
</UButton>
|
||||||
|
<UButton size="lg" variant="outline" to="/ui">
|
||||||
|
组件演示
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 版本信息 -->
|
||||||
|
<p class="mt-12 text-sm text-gray-400">
|
||||||
|
v0.1.0 · Built with Vue 3 + Electron + Nuxt UI
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
262
src/pages/ui.vue
Normal file
262
src/pages/ui.vue
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
message: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 开关状态
|
||||||
|
const isDarkMode = ref(false)
|
||||||
|
const isEnabled = ref(true)
|
||||||
|
|
||||||
|
// 下拉选择
|
||||||
|
const selectedOption = ref('')
|
||||||
|
const options = [
|
||||||
|
{ label: '选项一', value: 'option1' },
|
||||||
|
{ label: '选项二', value: 'option2' },
|
||||||
|
{ label: '选项三', value: 'option3' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 标签页
|
||||||
|
const activeTab = ref('tab1')
|
||||||
|
const tabs = [
|
||||||
|
{ label: '概览', value: 'tab1' },
|
||||||
|
{ label: '分析', value: 'tab2' },
|
||||||
|
{ label: '设置', value: 'tab3' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Toast 通知
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const showToast = (type: 'success' | 'error' | 'warning' | 'info') => {
|
||||||
|
const messages = {
|
||||||
|
success: '操作成功!',
|
||||||
|
error: '操作失败,请重试',
|
||||||
|
warning: '请注意此操作',
|
||||||
|
info: '这是一条提示信息',
|
||||||
|
}
|
||||||
|
toast.add({
|
||||||
|
title: messages[type],
|
||||||
|
color: type === 'error' ? 'red' : type === 'warning' ? 'yellow' : type === 'success' ? 'green' : 'blue',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模态框
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
isLoading.value = false
|
||||||
|
toast.add({ title: '表单提交成功!', color: 'green' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
🎉 Nuxt UI 组件演示
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
ChatLens - 聊天记录分析工具
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">按钮 Buttons</h2>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<UButton>默认按钮</UButton>
|
||||||
|
<UButton color="primary">主要按钮</UButton>
|
||||||
|
<UButton color="green">成功按钮</UButton>
|
||||||
|
<UButton color="red">危险按钮</UButton>
|
||||||
|
<UButton color="yellow">警告按钮</UButton>
|
||||||
|
<UButton variant="outline">描边按钮</UButton>
|
||||||
|
<UButton variant="ghost">幽灵按钮</UButton>
|
||||||
|
<UButton variant="soft">柔和按钮</UButton>
|
||||||
|
<UButton :loading="isLoading" @click="handleSubmit">
|
||||||
|
{{ isLoading ? '加载中...' : '带加载状态' }}
|
||||||
|
</UButton>
|
||||||
|
<UButton icon="i-heroicons-arrow-path" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 表单输入 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">表单 Forms</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-2xl">
|
||||||
|
<UFormField label="用户名">
|
||||||
|
<UInput v-model="formData.name" placeholder="请输入用户名" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="邮箱">
|
||||||
|
<UInput v-model="formData.email" type="email" placeholder="请输入邮箱" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="选择选项" class="md:col-span-2">
|
||||||
|
<USelect v-model="selectedOption" :items="options" placeholder="请选择" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="留言" class="md:col-span-2">
|
||||||
|
<UTextarea v-model="formData.message" placeholder="请输入留言内容" :rows="4" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 开关与复选框 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">开关 Toggles</h2>
|
||||||
|
<div class="flex flex-wrap items-center gap-8">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<USwitch v-model="isDarkMode" />
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">深色模式: {{ isDarkMode ? '开' : '关' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<USwitch v-model="isEnabled" color="green" />
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">启用功能: {{ isEnabled ? '是' : '否' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UCheckbox label="记住我" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">标签页 Tabs</h2>
|
||||||
|
<UTabs v-model="activeTab" :items="tabs" class="w-full max-w-xl">
|
||||||
|
<template #tab1>
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg">
|
||||||
|
<h3 class="font-medium text-gray-900 dark:text-white mb-2">概览内容</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">这里是概览页面的内容区域。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #tab2>
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg">
|
||||||
|
<h3 class="font-medium text-gray-900 dark:text-white mb-2">分析内容</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">这里是分析页面的内容区域。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #tab3>
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg">
|
||||||
|
<h3 class="font-medium text-gray-900 dark:text-white mb-2">设置内容</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">这里是设置页面的内容区域。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Toast 通知 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">通知 Toast</h2>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<UButton color="green" @click="showToast('success')">成功通知</UButton>
|
||||||
|
<UButton color="red" @click="showToast('error')">错误通知</UButton>
|
||||||
|
<UButton color="yellow" @click="showToast('warning')">警告通知</UButton>
|
||||||
|
<UButton color="blue" @click="showToast('info')">信息通知</UButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 模态框 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">模态框 Modal</h2>
|
||||||
|
<UButton @click="isModalOpen = true">打开模态框</UButton>
|
||||||
|
<UModal v-model:open="isModalOpen">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">模态框标题</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
这是模态框的内容区域,可以放置任何内容。
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<UButton variant="ghost" @click="isModalOpen = false">取消</UButton>
|
||||||
|
<UButton color="primary" @click="isModalOpen = false">确认</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 徽章 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">徽章 Badge</h2>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<UBadge>默认</UBadge>
|
||||||
|
<UBadge color="green">成功</UBadge>
|
||||||
|
<UBadge color="red">错误</UBadge>
|
||||||
|
<UBadge color="yellow">警告</UBadge>
|
||||||
|
<UBadge color="blue">信息</UBadge>
|
||||||
|
<UBadge variant="outline">描边</UBadge>
|
||||||
|
<UBadge variant="soft">柔和</UBadge>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 卡片 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">卡片 Card</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="font-semibold">卡片标题</h3>
|
||||||
|
</template>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">这是卡片的内容区域。</p>
|
||||||
|
<template #footer>
|
||||||
|
<UButton size="sm">查看详情</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="font-semibold">功能卡片</h3>
|
||||||
|
</template>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">支持自定义头部和底部。</p>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton size="sm" variant="ghost">取消</UButton>
|
||||||
|
<UButton size="sm">确认</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="font-semibold">简洁卡片</h3>
|
||||||
|
</template>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">简洁的卡片展示样式。</p>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">进度 Progress</h2>
|
||||||
|
<div class="space-y-4 max-w-md">
|
||||||
|
<UProgress :value="30" />
|
||||||
|
<UProgress :value="60" color="green" />
|
||||||
|
<UProgress :value="90" color="red" />
|
||||||
|
<UProgress :value="100" color="blue" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 骨架屏 -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">骨架屏 Skeleton</h2>
|
||||||
|
<div class="flex items-center gap-4 max-w-md">
|
||||||
|
<USkeleton class="w-12 h-12 rounded-full" />
|
||||||
|
<div class="space-y-2 flex-1">
|
||||||
|
<USkeleton class="h-4 w-3/4" />
|
||||||
|
<USkeleton class="h-4 w-1/2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast 容器 -->
|
||||||
|
<UToaster />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
src/routes/index.ts
Normal file
25
src/routes/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'index',
|
||||||
|
component: () => import('@/pages/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ui',
|
||||||
|
name: 'ui',
|
||||||
|
component: () => import('@/pages/ui.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
document.body.id = `page-${to.name as string}`
|
||||||
|
})
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
|
||||||
|
}
|
||||||
8
tsconfig.node.json
Normal file
8
tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
|
"include": ["electron.vite.config.*", "vite.config.*", "src/main/*", "src/preload/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": ["electron-vite/node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tsconfig.web.json
Normal file
29
tsconfig.web.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||||
|
"include": ["src/env.d.ts", "src/**/*", "src/**/*.vue", "electron/preload/*.d.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@/utils/*": ["src/utils/*"],
|
||||||
|
"@/plugins/*": ["src/plugins/*"],
|
||||||
|
"@/types": ["src/types"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user