mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-01-24 09:23:07 +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