This commit is contained in:
digua
2025-11-26 13:57:24 +08:00
commit 49ecd8fc1d
31 changed files with 8670 additions and 0 deletions

9
.editorconfig Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
node_modules
dist
out
.gitignore

19
.eslintrc.cjs Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

9
.prettierrc.yaml Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

8
src/App.vue Normal file
View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<UApp>
<router-view />
</UApp>
</template>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
}

8
tsconfig.node.json Normal file
View 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
View 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"]
}
}
}