release(project): initial release

- 发布初始版本,文件位置记录查询工具,可以记录文件存放位置,提供多样查询方式,便于查找
This commit is contained in:
ApplePine 2025-07-01 23:25:40 +08:00
commit 9b459a92c6
32 changed files with 2988 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.vscode/
.cursor/
.idea/
.git/
release/
node_modules/
dist/
build/
*.lock

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
.vscode/
.cursor/
.idea/
.git/
public/
release/
node_modules/
dist/
build/
*.lock

60
.prettierrc.json Normal file
View File

@ -0,0 +1,60 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": true,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"proseWrap": "always",
"htmlWhitespaceSensitivity": "css",
"bracketSameLine": false,
"vueIndentScriptAndStyle": false,
"embeddedLanguageFormatting": "auto",
"jsxSingleQuote": true,
"overrides": [
{
"files": "*.{json,json5}",
"options": {
"printWidth": 100,
"singleQuote": false,
"trailingComma": "none"
}
},
{
"files": "*.{yaml,yml}",
"options": {
"singleQuote": false,
"tabWidth": 2
}
},
{
"files": "*.html",
"options": {
"printWidth": 120,
"htmlWhitespaceSensitivity": "ignore"
}
},
{
"files": "*.md",
"options": {
"proseWrap": "always",
"printWidth": 100
}
},
{
"files": "*.sql",
"options": {
"printWidth": 100,
"keywordCase": "upper",
"identifierCase": "lower",
"linesBetweenQueries": 1,
"useTabs": true,
"tabWidth": 4
}
}
]
}

40
electron-builder.js Normal file
View File

@ -0,0 +1,40 @@
export default {
appId: 'com.djl.findyourfile.app',
productName: '文件位置查询软件',
directories: {
output: 'release',
},
files: ['dist/**/*', 'electron/**/*'],
asar: true,
mac: {
target: [
{ target: 'zip', arch: ['arm64', 'x64'] },
{ target: 'dmg', arch: ['arm64', 'x64'] },
],
icon: 'src/assets/icons/icon.icns',
artifactName: 'findyourfile-${version}-mac-${arch}.${ext}',
category: 'public.app-category.productivity',
hardenedRuntime: false,
notarize: false,
},
win: {
target: [
{ target: 'zip', arch: ['x64'] },
{ target: 'nsis', arch: ['x64'] },
],
icon: 'src/assets/icons/icon.ico',
artifactName: 'findyourfile-${version}-win-x64.${ext}',
},
nsis: {
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
forceCodeSigning: false,
publish: {
provider: 'generic',
url: 'https://updates.djldjl.cn/findyourfile/${os}-${arch}/',
},
}

View File

@ -0,0 +1,322 @@
const sqlite3 = require('sqlite3').verbose()
const path = require('path')
const fs = require('fs')
const { app } = require('electron')
// 数据库路径
const dbPath = path.join(app.getPath('userData'), 'database.sqlite')
let db
// 初始化数据库
function initDatabase() {
const dbDir = path.dirname(dbPath)
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true })
}
db = new sqlite3.Database(dbPath)
// 创建表
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS files (
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
file_name TEXT NOT NULL,
file_ext TEXT,
file_cat TEXT,
file_tags TEXT,
file_desc TEXT,
file_created_time DATETIME,
file_updated_time DATETIME,
file_path TEXT NOT NULL UNIQUE,
is_directory BOOLEAN DEFAULT 0,
added_time DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
})
console.log('Database initialized at:', dbPath)
return db
}
// 检查文件是否已存在于数据库
function isFileExists(filePath) {
return new Promise((resolve, reject) => {
db.get('SELECT file_id FROM files WHERE file_path = ?', [filePath], (err, row) => {
if (err) {
reject(err)
} else {
resolve(!!row) // 如果找到记录则返回true
}
})
})
}
// 添加文件记录
function addFile(fileData) {
return new Promise(async (resolve, reject) => {
try {
// 先检查文件是否已存在
const exists = await isFileExists(fileData.filePath)
if (exists) {
// 返回自定义错误,指明是文件重复
reject(new Error('FILE_DUPLICATE'))
return
}
console.log('收到文件数据:', fileData)
// 处理标签 - 确保它是一个字符串
let tagsString
if (typeof fileData.fileTags === 'string') {
// 如果已经是字符串检查是否是有效的JSON
try {
JSON.parse(fileData.fileTags)
tagsString = fileData.fileTags
} catch (e) {
tagsString = JSON.stringify([fileData.fileTags])
}
} else if (Array.isArray(fileData.fileTags)) {
tagsString = JSON.stringify(fileData.fileTags)
} else {
tagsString = JSON.stringify([])
}
const stmt = db.prepare(`
INSERT INTO files (
file_name, file_ext, file_cat, file_tags, file_desc,
file_created_time, file_updated_time, file_path, is_directory
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
stmt.run(
fileData.fileName,
fileData.fileExt,
fileData.fileCat,
tagsString,
fileData.fileDesc,
fileData.fileCreatedTime,
fileData.fileUpdatedTime,
fileData.filePath,
fileData.isDirectory ? 1 : 0,
function (err) {
if (err) {
console.error('数据库插入错误:', err)
// 如果是唯一约束错误,返回自定义错误
if (err.message.includes('UNIQUE constraint failed')) {
reject(new Error('FILE_DUPLICATE'))
} else {
reject(err)
}
} else {
resolve(this.lastID)
}
},
)
stmt.finalize()
} catch (error) {
console.error('addFile函数执行错误:', error)
reject(error)
}
})
}
// 搜索文件
function searchFiles(criteria) {
return new Promise((resolve, reject) => {
try {
// 构建基本查询
let query = `SELECT * FROM files WHERE 1=1`
const params = []
// 关键字查询 - 简化逻辑,确保正确性
if (criteria.keyword) {
// 使用OR逻辑匹配文件名或描述
query += ` AND (file_name LIKE ? OR file_desc LIKE ?)`
params.push(`%${criteria.keyword}%`)
params.push(`%${criteria.keyword}%`)
// 输出调试信息
console.log(`关键字查询: "${criteria.keyword}"`)
}
// 其他查询条件保持不变
if (criteria.fileName) {
query += ` AND file_name LIKE ?`
params.push(`%${criteria.fileName}%`)
}
if (criteria.fileCat) {
query += ` AND file_cat = ?`
params.push(criteria.fileCat)
}
if (criteria.fileTags && Array.isArray(criteria.fileTags) && criteria.fileTags.length > 0) {
const tagConditions = []
criteria.fileTags.forEach((tag) => {
tagConditions.push(`file_tags LIKE ?`)
params.push(`%"${tag}"%`)
})
query += ` AND (${tagConditions.join(' OR ')})`
}
if (criteria.startDate) {
query += ` AND added_time >= ?`
params.push(`${criteria.startDate} 00:00:00`)
}
if (criteria.endDate) {
query += ` AND added_time <= ?`
params.push(`${criteria.endDate} 23:59:59`)
}
console.log('执行SQL:', query)
console.log('参数:', params)
// 执行查询
db.all(query, params, (err, rows) => {
if (err) {
console.error('查询错误:', err)
reject(err)
return
}
console.log(`查询结果: ${rows.length}`)
// 如果是关键字查询且没有结果,检查数据库中是否有描述字段有数据
if (rows.length === 0 && criteria.keyword) {
db.get(
"SELECT COUNT(*) as count FROM files WHERE file_desc IS NOT NULL AND file_desc != ''",
[],
(err, result) => {
if (err) {
console.error('检查描述字段错误:', err)
} else {
console.log(`数据库中有 ${result.count} 条记录含有描述`)
}
resolve(rows)
},
)
} else {
resolve(rows)
}
})
} catch (error) {
console.error('查询异常:', error)
reject(error)
}
})
}
// 获取统计数据
function getStats() {
return new Promise((resolve, reject) => {
const stats = {}
// 获取文件总数
db.get('SELECT COUNT(*) as count FROM files WHERE is_directory = 0', (err, row) => {
if (err) {
reject(err)
return
}
stats.fileCount = row.count
// 获取文件夹总数
db.get('SELECT COUNT(*) as count FROM files WHERE is_directory = 1', (err, row) => {
if (err) {
reject(err)
return
}
stats.dirCount = row.count
// 获取文件类型分布
db.all(
'SELECT file_ext, COUNT(*) as count FROM files WHERE is_directory = 0 GROUP BY file_ext',
(err, rows) => {
if (err) {
reject(err)
return
}
stats.fileTypes = rows
// 获取最近添加的文件
db.all('SELECT * FROM files ORDER BY added_time DESC LIMIT 10', (err, rows) => {
if (err) {
reject(err)
} else {
stats.recentFiles = rows.map((row) => ({
...row,
fileTags: JSON.parse(row.file_tags || '[]'),
}))
resolve(stats)
}
})
},
)
})
})
})
}
// 获取所有分类
function getAllCategories() {
return new Promise((resolve, reject) => {
db.all(
'SELECT DISTINCT file_cat FROM files WHERE file_cat IS NOT NULL AND file_cat != ""',
(err, rows) => {
if (err) {
reject(err)
return
}
const categories = rows.map((row) => row.file_cat)
resolve(categories)
},
)
})
}
// 获取所有标签
function getAllTags() {
return new Promise((resolve, reject) => {
db.all(
'SELECT file_tags FROM files WHERE file_tags IS NOT NULL AND file_tags != ""',
(err, rows) => {
if (err) {
reject(err)
return
}
// 从所有记录中提取唯一标签
const allTags = new Set()
rows.forEach((row) => {
if (row.file_tags) {
try {
const tags = JSON.parse(row.file_tags)
if (Array.isArray(tags)) {
tags.forEach((tag) => {
if (tag) allTags.add(tag)
})
}
} catch (e) {
console.error('解析标签失败:', e)
}
}
})
resolve(Array.from(allTags))
},
)
})
}
module.exports = {
initDatabase,
addFile,
searchFiles,
getStats,
getAllTags,
getAllCategories,
isFileExists,
}

179
electron/database/sqlite.js Normal file
View File

@ -0,0 +1,179 @@
const sqlite3 = require('sqlite3').verbose()
const path = require('path')
const fs = require('fs')
const { app } = require('electron')
// 数据库路径
const dbPath = path.join(app.getPath('userData'), 'database.sqlite')
let db
// 初始化数据库
function initDatabase() {
const dbDir = path.dirname(dbPath)
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true })
}
db = new sqlite3.Database(dbPath)
// 创建表
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS files (
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
file_name TEXT NOT NULL,
file_ext TEXT,
file_cat TEXT,
file_tags TEXT,
file_desc TEXT,
file_created_time DATETIME,
file_updated_time DATETIME,
file_path TEXT NOT NULL UNIQUE,
is_directory BOOLEAN DEFAULT 0,
added_time DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
})
console.log('Database initialized at:', dbPath)
return db
}
// 添加文件记录
function addFile(fileData) {
return new Promise((resolve, reject) => {
const stmt = db.prepare(`
INSERT INTO files (
file_name, file_ext, file_cat, file_tags, file_desc,
file_created_time, file_updated_time, file_path, is_directory
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
stmt.run(
fileData.fileName,
fileData.fileExt,
fileData.fileCat,
JSON.stringify(fileData.fileTags || []),
fileData.fileDesc,
fileData.fileCreatedTime,
fileData.fileUpdatedTime,
fileData.filePath,
fileData.isDirectory ? 1 : 0,
function (err) {
if (err) {
reject(err)
} else {
resolve(this.lastID)
}
},
)
stmt.finalize()
})
}
// 搜索文件
function searchFiles(criteria) {
return new Promise((resolve, reject) => {
let query = `SELECT * FROM files WHERE 1=1`
const params = []
if (criteria.fileName) {
query += ` AND file_name LIKE ?`
params.push(`%${criteria.fileName}%`)
}
if (criteria.fileCat) {
query += ` AND file_cat = ?`
params.push(criteria.fileCat)
}
if (criteria.fileTags && criteria.fileTags.length > 0) {
const tagConditions = criteria.fileTags.map(() => `file_tags LIKE ?`).join(' OR ')
query += ` AND (${tagConditions})`
criteria.fileTags.forEach((tag) => {
params.push(`%${tag}%`)
})
}
if (criteria.startDate) {
query += ` AND added_time >= ?`
params.push(criteria.startDate)
}
if (criteria.endDate) {
query += ` AND added_time <= ?`
params.push(criteria.endDate)
}
db.all(query, params, (err, rows) => {
if (err) {
reject(err)
} else {
// 解析JSON格式的标签
const results = rows.map((row) => ({
...row,
fileTags: JSON.parse(row.file_tags || '[]'),
}))
resolve(results)
}
})
})
}
// 获取统计数据
function getStats() {
return new Promise((resolve, reject) => {
const stats = {}
// 获取文件总数
db.get('SELECT COUNT(*) as count FROM files WHERE is_directory = 0', (err, row) => {
if (err) {
reject(err)
return
}
stats.fileCount = row.count
// 获取文件夹总数
db.get('SELECT COUNT(*) as count FROM files WHERE is_directory = 1', (err, row) => {
if (err) {
reject(err)
return
}
stats.dirCount = row.count
// 获取文件类型分布
db.all(
'SELECT file_ext, COUNT(*) as count FROM files WHERE is_directory = 0 GROUP BY file_ext',
(err, rows) => {
if (err) {
reject(err)
return
}
stats.fileTypes = rows
// 获取最近添加的文件
db.all('SELECT * FROM files ORDER BY added_time DESC LIMIT 10', (err, rows) => {
if (err) {
reject(err)
} else {
stats.recentFiles = rows.map((row) => ({
...row,
fileTags: JSON.parse(row.file_tags || '[]'),
}))
resolve(stats)
}
})
},
)
})
})
})
}
module.exports = {
initDatabase,
addFile,
searchFiles,
getStats,
}

280
electron/main.cjs Normal file
View File

@ -0,0 +1,280 @@
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron')
const path = require('path')
const fs = require('fs')
const {
initDatabase,
addFile,
searchFiles,
getStats,
getAllTags,
getAllCategories,
} = require('./database/sqlite.cjs')
let mainWindow
let db
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
},
})
// 设置CSP策略
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
// 开发环境使用较宽松的CSP
const isDev = process.env.NODE_ENV === 'development'
const cspValue = isDev
? "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:;"
: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;"
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [cspValue],
},
})
})
// 打印路径,帮助调试
const htmlPath = path.join(__dirname, '../dist/index.html')
console.log('Loading HTML from:', htmlPath)
console.log('File exists:', fs.existsSync(htmlPath))
// 在开发模式下直接加载开发服务器
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
mainWindow.webContents.openDevTools()
} else {
// 生产模式下加载构建后的文件
mainWindow.loadURL(`file://${htmlPath}`)
}
}
app.whenReady().then(() => {
db = initDatabase()
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// 文件选择器处理
ipcMain.handle('select-file', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
})
if (canceled || filePaths.length === 0) {
return null
}
const filePath = filePaths[0]
return getFileInfo(filePath)
})
ipcMain.handle('select-directory', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory'],
})
if (canceled || filePaths.length === 0) {
return null
}
const dirPath = filePaths[0]
return getFileInfo(dirPath)
})
// 文件信息获取
ipcMain.handle('get-file-info', async (event, filePath) => {
return getFileInfo(filePath)
})
// 文件操作
ipcMain.handle('open-file', async (event, filePath) => {
return shell.openPath(filePath)
})
ipcMain.handle('show-item-in-folder', async (event, filePath) => {
return shell.showItemInFolder(filePath)
})
// 数据库操作
ipcMain.handle('add-file', async (event, fileData) => {
try {
console.log('主进程接收到添加文件请求:', JSON.stringify(fileData))
// 确保文件仍然存在
if (!fs.existsSync(fileData.filePath)) {
throw new Error('FILE_NOT_EXIST')
}
// 添加到数据库
const fileId = await addFile(fileData)
return fileId
} catch (error) {
console.error('添加文件失败:', error)
// 根据错误类型返回不同的错误信息
if (error.message === 'FILE_DUPLICATE') {
throw new Error('FILE_DUPLICATE')
} else if (error.message === 'FILE_NOT_EXIST') {
throw new Error('FILE_NOT_EXIST')
} else {
throw new Error(error.message || '未知错误')
}
}
})
// 测试关键字查询功能
ipcMain.handle('search-files', async (event, criteria) => {
console.log('main: 收到search-files请求, 参数:', JSON.stringify(criteria))
// 如果是关键字查询,添加特殊处理
if (criteria.keyword) {
console.log('检测到关键字查询:', criteria.keyword)
// 执行最简单的SQL查询测试
const testQuery = `SELECT * FROM files WHERE file_name LIKE '%${criteria.keyword}%' OR file_desc LIKE '%${criteria.keyword}%' LIMIT 10`
console.log('执行测试查询:', testQuery)
return new Promise((resolve, reject) => {
db.all(testQuery, [], (err, rows) => {
if (err) {
console.error('测试查询错误:', err)
reject(err)
} else {
console.log(`测试查询结果: ${rows.length} 条记录`)
resolve(rows)
}
})
})
}
// 其他查询逻辑...
try {
const results = await searchFiles(criteria)
return results
} catch (error) {
console.error('搜索失败:', error)
throw error
}
})
ipcMain.handle('get-stats', async () => {
try {
const stats = await getStats()
return stats
} catch (error) {
console.error('获取统计数据失败:', error)
throw error
}
})
ipcMain.handle('check-file-exists', async (event, filePath) => {
return fs.existsSync(filePath)
})
// 获取所有分类
ipcMain.handle('get-all-categories', async () => {
try {
const categories = await getAllCategories()
return categories
} catch (error) {
console.error('获取所有分类失败:', error)
throw error
}
})
// 获取所有标签
ipcMain.handle('get-all-tags', async () => {
try {
const tags = await getAllTags()
return tags
} catch (error) {
console.error('获取所有标签失败:', error)
throw error
}
})
// 删除文件记录
ipcMain.handle('delete-file', async (event, fileId) => {
try {
await deleteFile(fileId)
return true
} catch (error) {
console.error('删除文件记录失败:', error)
throw error
}
})
// 检查数据库中是否有数据
db.get('SELECT COUNT(*) as count FROM files', [], (err, row) => {
if (err) {
console.error('数据库连接测试失败:', err)
} else {
console.log(`数据库中有 ${row.count} 条记录`)
// 查看几条示例数据
if (row.count > 0) {
db.all('SELECT * FROM files LIMIT 3', [], (err, rows) => {
if (err) {
console.error('获取示例数据失败:', err)
} else {
console.log('示例数据:')
rows.forEach((row) => {
console.log(`ID: ${row.file_id}, 名称: ${row.file_name}, 描述: ${row.file_desc}`)
})
}
})
}
}
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// 获取文件信息的辅助函数
function getFileInfo(filePath) {
try {
const stats = fs.statSync(filePath)
const isDirectory = stats.isDirectory()
const fileName = path.basename(filePath)
const fileExt = isDirectory ? '' : path.extname(filePath).slice(1)
return {
path: filePath,
name: fileName,
isDirectory,
fileExt,
createdTime: stats.birthtime.toISOString(),
updatedTime: stats.mtime.toISOString(),
size: stats.size,
}
} catch (error) {
console.error('获取文件信息失败:', error)
return null
}
}
function deleteFile(fileId) {
return new Promise((resolve, reject) => {
db.run('DELETE FROM files WHERE file_id = ?', [fileId], function (err) {
if (err) {
reject(err)
} else {
resolve(this.changes)
}
})
})
}

28
electron/preload.cjs Normal file
View File

@ -0,0 +1,28 @@
const { contextBridge, ipcRenderer } = require('electron')
// 暴露API到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
addFile: (fileData) => ipcRenderer.invoke('add-file', fileData),
searchFiles: (criteria) => {
console.log('preload: searchFiles 被调用,参数:', JSON.stringify(criteria))
return ipcRenderer.invoke('search-files', criteria)
},
getStats: () => ipcRenderer.invoke('get-stats'),
checkFileExists: (filePath) => ipcRenderer.invoke('check-file-exists', filePath),
deleteFile: (fileId) => ipcRenderer.invoke('delete-file', fileId),
updateFile: (fileId, fileData) => ipcRenderer.invoke('update-file', fileId, fileData),
// 文件选择器
selectFile: () => ipcRenderer.invoke('select-file'),
selectDirectory: () => ipcRenderer.invoke('select-directory'),
// 文件操作
openFile: (filePath) => ipcRenderer.invoke('open-file', filePath),
showItemInFolder: (filePath) => ipcRenderer.invoke('show-item-in-folder', filePath),
getFileInfo: (filePath) => ipcRenderer.invoke('get-file-info', filePath),
// 添加新方法
getAllCategories: () => ipcRenderer.invoke('get-all-categories'),
getAllTags: () => ipcRenderer.invoke('get-all-tags'),
})

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "findyourfile",
"version": "1.0.0",
"description": "文件存放位置查询软件。",
"author": "Jianlong Deng",
"main": "electron/main.cjs",
"type": "module",
"scripts": {
"dev": "rsbuild dev",
"bb": "rsbuild build",
"pp": "rsbuild build && rsbuild preview",
"edev": "rsbuild dev --host localhost && electron .",
"ebuild": "rsbuild build && bun run ebuild:mac && bun run ebuild:win",
"ebuild:mac": "electron-builder --mac --arm64",
"ebuild:win": "electron-builder --win --x64"
},
"devDependencies": {
"@rsbuild/core": "^1.0.0",
"@rsbuild/plugin-vue": "^1.0.0",
"electron": "^37.1.0",
"electron-builder": "^26.0.12"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.0",
"axios": "^1.7.0",
"echarts": "^5.5.0",
"element-plus": "^2.8.0",
"pinia": "^2.0.0",
"sqlite3": "^5.1.0",
"vue": "^3.4.0",
"vue-echarts": "^7.0.0",
"vue-router": "^4.0.0"
}
}

11
public/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件位置查询</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

23
rsbuild.config.js Normal file
View File

@ -0,0 +1,23 @@
import { defineConfig } from '@rsbuild/core'
import { pluginVue } from '@rsbuild/plugin-vue'
export default defineConfig({
plugins: [pluginVue()],
server: {
port: 3000,
},
output: {
distPath: {
root: 'dist',
},
assetPrefix: './',
},
source: {
entry: {
index: './src/index.js',
},
},
html: {
template: './public/index.html',
},
})

42
src/App.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div class="app-container">
<el-container>
<el-aside width="200px">
<Sidebar />
</el-aside>
<el-container>
<el-header>
<Header />
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { ElContainer, ElAside, ElHeader, ElMain } from 'element-plus'
import Sidebar from './components/Layout/Sidebar.vue'
import Header from './components/Layout/Header.vue'
export default {
name: 'App',
components: {
ElContainer,
ElAside,
ElHeader,
ElMain,
Sidebar,
Header,
},
}
</script>
<style>
.app-container {
height: 100vh;
width: 100%;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<el-card class="chart-card">
<div class="chart-title">文件类型分布</div>
<v-chart class="chart" :option="chartOption" autoresize />
</el-card>
</template>
<script>
import { computed, ref, watch } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, LegendComponent])
export default {
name: 'FileTypeChart',
components: {
VChart,
},
props: {
data: {
type: Array,
default: () => [],
},
},
setup(props) {
const chartOption = computed(() => {
const chartData = props.data.map((item) => ({
name: item.file_ext || '无扩展名',
value: item.count,
}))
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'horizontal',
bottom: 'bottom',
data: chartData.map((item) => item.name),
},
series: [
{
name: '文件类型',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: chartData,
},
],
}
})
return {
chartOption,
}
},
}
</script>
<style scoped>
.chart-card {
width: 100%;
height: 400px;
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
}
.chart {
height: 320px;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<el-card class="stats-card">
<div class="stats-card-content">
<div class="stats-icon">
<el-icon :size="36">
<component :is="icon" />
</el-icon>
</div>
<div class="stats-info">
<div class="stats-title">{{ title }}</div>
<div class="stats-value">{{ value }}</div>
</div>
</div>
</el-card>
</template>
<script>
export default {
name: 'StatisticsCard',
props: {
title: {
type: String,
required: true,
},
value: {
type: [Number, String],
required: true,
},
icon: {
type: String,
default: 'DataLine',
},
},
}
</script>
<style scoped>
.stats-card {
height: 120px;
margin-bottom: 20px;
}
.stats-card-content {
display: flex;
align-items: center;
height: 100%;
}
.stats-icon {
padding: 0 20px;
color: #409eff;
}
.stats-info {
flex-grow: 1;
}
.stats-title {
font-size: 16px;
color: #909399;
margin-bottom: 10px;
}
.stats-value {
font-size: 28px;
font-weight: bold;
color: #303133;
}
</style>

125
src/components/FileCard.vue Normal file
View File

@ -0,0 +1,125 @@
<template>
<el-card class="file-card" shadow="hover">
<div class="file-icon">
<el-icon v-if="file.is_directory">
<Folder />
</el-icon>
<el-icon v-else>
<Document />
</el-icon>
</div>
<div class="file-info">
<h3 class="file-name">{{ file.file_name }}</h3>
<p class="file-path">{{ file.file_path }}</p>
<div class="file-meta">
<el-tag v-if="file.file_cat" size="small" type="info">{{ file.file_cat }}</el-tag>
<el-tag v-for="tag in parsedTags" :key="tag" size="small" class="file-tag">
{{ tag }}
</el-tag>
</div>
<p v-if="file.file_desc" class="file-desc">{{ file.file_desc }}</p>
<div class="file-actions">
<el-button size="small" @click="openFile">打开文件</el-button>
<el-button size="small" @click="openLocation">打开位置</el-button>
</div>
</div>
</el-card>
</template>
<script>
import { computed } from 'vue'
import { Folder, Document } from '@element-plus/icons-vue'
export default {
name: 'FileCard',
components: {
Folder,
Document,
},
props: {
file: {
type: Object,
required: true,
},
},
setup(props) {
const parsedTags = computed(() => {
if (!props.file.file_tags) return []
try {
return JSON.parse(props.file.file_tags)
} catch (e) {
return []
}
})
const openFile = async () => {
try {
await window.electronAPI.openFile(props.file.file_path)
} catch (error) {
console.error('打开文件失败:', error)
}
}
const openLocation = async () => {
try {
await window.electronAPI.showItemInFolder(props.file.file_path)
} catch (error) {
console.error('打开文件位置失败:', error)
}
}
return {
parsedTags,
openFile,
openLocation,
}
},
}
</script>
<style scoped>
.file-card {
margin-bottom: 15px;
}
.file-icon {
font-size: 24px;
margin-bottom: 10px;
}
.file-name {
margin: 0 0 5px 0;
font-size: 16px;
font-weight: bold;
}
.file-path {
margin: 5px 0;
color: #909399;
font-size: 12px;
word-break: break-all;
}
.file-meta {
margin: 10px 0;
}
.file-tag {
margin-right: 5px;
margin-bottom: 5px;
}
.file-desc {
margin: 10px 0;
color: #606266;
font-size: 14px;
}
.file-actions {
margin-top: 15px;
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="file-selector">
<div
class="drop-zone"
@click="selectFile"
@dragover.prevent
@dragenter.prevent
@drop.prevent="handleDrop"
:class="{ active: isDragging }"
@dragenter="isDragging = true"
@dragleave="isDragging = false"
>
<el-icon class="drop-icon"><Upload /></el-icon>
<div class="drop-text">
<p>点击选择文件或拖放到此处</p>
<el-button type="primary" size="small" @click.stop="selectFile">选择文件</el-button>
<el-button size="small" @click.stop="selectDirectory">选择文件夹</el-button>
</div>
</div>
<div v-if="selectedFile" class="file-info">
<el-alert title="文件已选择" type="success" :closable="false" show-icon>
<template #default>
<p><strong>名称:</strong> {{ selectedFile.name }}</p>
<p><strong>路径:</strong> {{ selectedFile.path }}</p>
<p><strong>类型:</strong> {{ selectedFile.isDirectory ? '文件夹' : '文件' }}</p>
</template>
</el-alert>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { Upload } from '@element-plus/icons-vue'
export default {
name: 'FileSelector',
components: {
Upload,
},
emits: ['file-selected'],
setup(props, { emit }) {
const selectedFile = ref(null)
const isDragging = ref(false)
const selectFile = async () => {
try {
const file = await window.electronAPI.selectFile()
if (file) {
handleFileSelected(file)
}
} catch (error) {
console.error('选择文件失败:', error)
}
}
const selectDirectory = async () => {
try {
const dir = await window.electronAPI.selectDirectory()
if (dir) {
handleFileSelected(dir)
}
} catch (error) {
console.error('选择文件夹失败:', error)
}
}
const handleDrop = async (event) => {
isDragging.value = false
const file = event.dataTransfer.files[0]
if (file) {
const filePath = file.path
// Electron API
try {
const fileInfo = await window.electronAPI.getFileInfo(filePath)
handleFileSelected(fileInfo)
} catch (error) {
console.error('获取文件信息失败:', error)
}
}
}
const handleFileSelected = (file) => {
//
const safeFile = {
path: file.path,
name: file.name,
isDirectory: file.isDirectory,
fileExt: file.fileExt || '',
// ISO
createdTime: file.createdTime ? new Date(file.createdTime).toISOString() : null,
updatedTime: file.updatedTime ? new Date(file.updatedTime).toISOString() : null,
size: file.size,
}
selectedFile.value = safeFile
emit('file-selected', safeFile)
}
return {
selectedFile,
isDragging,
selectFile,
selectDirectory,
handleDrop,
}
},
}
</script>
<style scoped>
.file-selector {
width: 100%;
}
.drop-zone {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.drop-zone:hover,
.drop-zone.active {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
.drop-icon {
font-size: 48px;
color: #909399;
margin-bottom: 16px;
}
.drop-text {
color: #606266;
}
.file-info {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<el-table :data="files" stripe style="width: 100%" v-loading="loading">
<el-table-column label="文件名" min-width="150">
<template #default="scope">
<div class="file-name-cell">
<el-icon v-if="scope.row.is_directory === 1"><Folder /></el-icon>
<el-icon v-else><Document /></el-icon>
<span>{{ scope.row.file_name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="file_cat" label="分类" width="100" />
<el-table-column label="标签" width="180">
<template #default="scope">
<el-tag v-for="tag in getTags(scope.row)" :key="tag" size="small" class="file-tag">
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="file_desc" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="file_path" label="路径" min-width="200" show-overflow-tooltip />
<el-table-column prop="added_time" label="添加时间" width="180" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="scope">
<el-button size="small" @click="openFile(scope.row)" type="primary" plain>
打开文件
</el-button>
<el-button size="small" @click="openLocation(scope.row)" type="info" plain>
打开位置
</el-button>
<el-button size="small" @click="deleteFile(scope.row)" type="danger" plain>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import { ref } from 'vue'
import { Folder, Document } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'FileTable',
components: {
Folder,
Document,
},
props: {
files: {
type: Array,
required: true,
},
},
emits: ['refresh'],
setup(props, { emit }) {
const loading = ref(false)
const getTags = (file) => {
if (!file.file_tags) return []
try {
return JSON.parse(file.file_tags)
} catch (e) {
console.error('解析标签失败:', e)
return []
}
}
const openFile = async (file) => {
loading.value = true
try {
//
const exists = await window.electronAPI.checkFileExists(file.file_path)
if (!exists) {
// 使
ElMessageBox.confirm('此文件不存在或已被移动,是否从数据库中删除此记录?', '文件丢失', {
confirmButtonText: '删除记录',
cancelButtonText: '保留记录',
type: 'warning',
})
.then(async () => {
//
await window.electronAPI.deleteFile(file.file_id)
ElMessage.success('记录已删除')
emit('refresh') //
})
.catch(() => {
//
ElMessage.info('记录已保留')
})
return
}
await window.electronAPI.openFile(file.file_path)
} catch (error) {
console.error('打开文件失败:', error)
ElMessage.error(`打开文件失败: ${error.message || '未知错误'}`)
} finally {
loading.value = false
}
}
const openLocation = async (file) => {
// openFile
loading.value = true
try {
const exists = await window.electronAPI.checkFileExists(file.file_path)
if (!exists) {
ElMessageBox.confirm('此文件不存在或已被移动,是否从数据库中删除此记录?', '文件丢失', {
confirmButtonText: '删除记录',
cancelButtonText: '保留记录',
type: 'warning',
})
.then(async () => {
await window.electronAPI.deleteFile(file.file_id)
ElMessage.success('记录已删除')
emit('refresh')
})
.catch(() => {
ElMessage.info('记录已保留')
})
return
}
await window.electronAPI.showItemInFolder(file.file_path)
} catch (error) {
console.error('打开文件位置失败:', error)
ElMessage.error(`打开文件位置失败: ${error.message || '未知错误'}`)
} finally {
loading.value = false
}
}
//
const deleteFile = async (file) => {
try {
await ElMessageBox.confirm(
'确认从数据库中删除此记录?此操作不会删除实际文件。',
'删除确认',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
},
)
await window.electronAPI.deleteFile(file.file_id)
ElMessage.success('记录已删除')
// ""
setTimeout(() => {
emit('refresh')
}, 500)
} catch (error) {
if (error !== 'cancel') {
console.error('删除文件记录失败:', error)
ElMessage.error(`删除失败: ${error.message || '未知错误'}`)
}
}
}
return {
loading,
getTags,
openFile,
openLocation,
deleteFile,
}
},
}
</script>
<style scoped>
.file-name-cell {
display: flex;
align-items: center;
}
.file-name-cell i {
margin-right: 5px;
}
.file-tag {
margin-right: 5px;
margin-bottom: 3px;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="header">
<div class="left">
<h2>{{ currentPageTitle }}</h2>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
export default {
name: 'Header',
setup() {
const route = useRoute()
const currentPageTitle = computed(() => {
const pathMap = {
'/dashboard': 'DashBoard',
'/add': '添加文件',
'/search': '查询文件',
}
return pathMap[route.path] || ''
})
return {
currentPageTitle,
}
},
}
</script>
<style scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 60px;
border-bottom: 1px solid #e6e6e6;
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div class="sidebar">
<div class="logo-container">
<h2>FindYourFile</h2>
</div>
<el-menu :default-active="activeIndex" class="sidebar-menu" router>
<el-menu-item index="/dashboard">
<el-icon><DataLine /></el-icon>
<span>Dashboard</span>
</el-menu-item>
<el-menu-item index="/add">
<el-icon><Plus /></el-icon>
<span>添加文件</span>
</el-menu-item>
<el-menu-item index="/search">
<el-icon><Search /></el-icon>
<span>查询文件</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { DataLine, Plus, Search } from '@element-plus/icons-vue'
export default {
name: 'Sidebar',
components: {
DataLine,
Plus,
Search,
},
setup() {
const route = useRoute()
const activeIndex = computed(() => route.path)
return {
activeIndex,
}
},
}
</script>
<style scoped>
.sidebar {
height: 100%;
background-color: #fff;
border-right: 1px solid #e6e6e6;
}
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #e6e6e6;
}
.sidebar-menu {
height: calc(100% - 60px);
}
</style>

11
src/index.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文件位置查询</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

19
src/index.js Normal file
View File

@ -0,0 +1,19 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './styles/index.css'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia()).use(router).use(ElementPlus).mount('#app')
})

17
src/main.js Normal file
View File

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './styles/index.css'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia()).use(router).use(ElementPlus).mount('#app')

30
src/router/index.js Normal file
View File

@ -0,0 +1,30 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
},
{
path: '/add',
name: 'AddFile',
component: () => import('../views/AddFile.vue'),
},
{
path: '/search',
name: 'SearchFiles',
component: () => import('../views/SearchFiles.vue'),
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router

32
src/stores/dashboard.js Normal file
View File

@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
stats: {
fileCount: 0,
dirCount: 0,
fileTypes: [],
recentFiles: [],
},
isLoading: false,
error: null,
}),
actions: {
async getStats() {
this.isLoading = true
this.error = null
try {
const stats = await window.electronAPI.getStats()
this.stats = stats
return stats
} catch (err) {
this.error = err.message
return this.stats
} finally {
this.isLoading = false
}
},
},
})

109
src/stores/files.js Normal file
View File

@ -0,0 +1,109 @@
import { defineStore } from 'pinia'
import { ElMessage } from 'element-plus'
export const useFilesStore = defineStore('files', {
state: () => ({
files: [],
isLoading: false,
error: null,
totalFiles: 0,
currentPage: 1,
pageSize: 10,
}),
actions: {
async searchFiles(criteria) {
this.isLoading = true
this.error = null
try {
console.log('filesStore.searchFiles 收到条件:', JSON.stringify(criteria))
// 检查是否有关键字查询
if (criteria.keyword) {
console.log('检测到关键字查询:', criteria.keyword)
}
// 创建一个简单的可序列化对象
const simpleCriteria = {}
// 只添加有值的属性
if (criteria.fileName) simpleCriteria.fileName = criteria.fileName
if (criteria.fileCat) simpleCriteria.fileCat = criteria.fileCat
// 特别处理标签数组,确保它们是简单字符串
if (Array.isArray(criteria.fileTags) && criteria.fileTags.length > 0) {
// 转换为简单字符串数组
simpleCriteria.fileTags = criteria.fileTags.map((tag) => String(tag))
}
if (criteria.startDate) simpleCriteria.startDate = criteria.startDate
if (criteria.endDate) simpleCriteria.endDate = criteria.endDate
console.log('发送查询条件:', JSON.stringify(simpleCriteria))
// 如果没有任何条件,返回空数组
if (Object.keys(simpleCriteria).length === 0) {
return []
}
const result = await window.electronAPI.searchFiles(simpleCriteria)
console.log('searchFiles 结果数量:', result.length)
// 确保结果是可序列化的
const safeResult = JSON.parse(JSON.stringify(result))
this.files = safeResult
this.totalFiles = safeResult.length
return safeResult
} catch (err) {
this.error = err.message
console.error('搜索错误:', err)
throw new Error(`查询失败: ${err.message}`)
} finally {
this.isLoading = false
}
},
async addFile(fileData) {
this.isLoading = true
this.error = null
try {
// 确保所有对象都是可序列化的
const serializable = {
fileName: fileData.fileName || '',
fileExt: fileData.fileExt || '',
fileCat: fileData.fileCat || '',
fileTags: JSON.stringify(fileData.fileTags || []), // 直接在这里序列化标签
fileDesc: fileData.fileDesc || '',
filePath: fileData.filePath || '',
fileCreatedTime:
typeof fileData.fileCreatedTime === 'string' ? fileData.fileCreatedTime : null,
fileUpdatedTime:
typeof fileData.fileUpdatedTime === 'string' ? fileData.fileUpdatedTime : null,
isDirectory: Boolean(fileData.isDirectory),
}
console.log('正在添加文件,序列化数据:', serializable)
const fileId = await window.electronAPI.addFile(serializable)
return fileId
} catch (err) {
this.error = err.message
console.error('添加文件错误:', err)
// 翻译错误信息为用户友好的提示
if (err.message === 'FILE_DUPLICATE') {
throw new Error('该文件已存在于数据库中,请勿重复添加')
} else if (err.message === 'FILE_NOT_EXIST') {
throw new Error('文件不存在或已被移动')
} else {
throw err
}
} finally {
this.isLoading = false
}
},
},
})

3
src/stores/index.js Normal file
View File

@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export default createPinia()

65
src/stores/metadata.js Normal file
View File

@ -0,0 +1,65 @@
import { defineStore } from 'pinia'
import { ElMessage } from 'element-plus'
export const useMetadataStore = defineStore('metadata', {
state: () => ({
categories: [],
tags: [],
isLoading: false,
error: null,
}),
actions: {
async fetchCategories() {
if (this.categories.length > 0) return this.categories
this.isLoading = true
try {
const categories = await window.electronAPI.getAllCategories()
this.categories = categories
return categories
} catch (error) {
console.error('获取分类失败:', error)
this.error = error.message
return []
} finally {
this.isLoading = false
}
},
async fetchTags() {
if (this.tags.length > 0) return this.tags
this.isLoading = true
try {
const tags = await window.electronAPI.getAllTags()
this.tags = tags
return tags
} catch (error) {
console.error('获取标签失败:', error)
this.error = error.message
return []
} finally {
this.isLoading = false
}
},
async refreshMetadata() {
this.categories = []
this.tags = []
await Promise.all([this.fetchCategories(), this.fetchTags()])
},
addCategory(category) {
if (category && !this.categories.includes(category)) {
this.categories.push(category)
}
},
addTag(tag) {
if (tag && !this.tags.includes(tag)) {
this.tags.push(tag)
}
},
},
})

47
src/styles/index.css Normal file
View File

@ -0,0 +1,47 @@
/* 全局样式 */
html,
body {
margin: 0;
padding: 0;
font-family:
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun,
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f6f6f6;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 公共样式 */
.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
color: #303133;
}
.text-center {
text-align: center;
}
.mb-20 {
margin-bottom: 20px;
}

25
src/styles/variables.css Normal file
View File

@ -0,0 +1,25 @@
:root {
/* 主题颜色 */
--primary-color: #409eff;
--success-color: #67c23a;
--warning-color: #e6a23c;
--danger-color: #f56c6c;
--info-color: #909399;
/* 文本颜色 */
--text-primary: #303133;
--text-regular: #606266;
--text-secondary: #909399;
--text-placeholder: #c0c4cc;
/* 边框颜色 */
--border-color: #dcdfe6;
--border-light: #e4e7ed;
/* 背景颜色 */
--bg-color: #f5f7fa;
/* 尺寸变量 */
--header-height: 60px;
--sidebar-width: 200px;
}

288
src/views/AddFile.vue Normal file
View File

@ -0,0 +1,288 @@
<template>
<div class="add-file-container">
<h1>添加新文件</h1>
<div class="file-selector-wrapper">
<FileSelector @file-selected="handleFileSelected" />
</div>
<el-form
v-if="selectedFile"
:model="fileForm"
label-width="120px"
:rules="rules"
ref="fileFormRef"
>
<el-form-item label="文件路径" prop="filePath">
<el-input v-model="fileForm.filePath" disabled />
</el-form-item>
<el-form-item label="文件名" prop="fileName">
<el-input v-model="fileForm.fileName" disabled />
</el-form-item>
<el-form-item label="分类" prop="fileCat">
<el-select
v-model="fileForm.fileCat"
filterable
allow-create
default-first-option
placeholder="请选择或输入分类"
style="width: 100%"
clearable
>
<el-option
v-for="category in categories"
:key="category"
:label="category"
:value="category"
/>
</el-select>
</el-form-item>
<el-form-item label="标签" prop="fileTags">
<div class="tags-container">
<el-select
v-model="selectedTag"
filterable
allow-create
default-first-option
placeholder="请选择或输入标签"
style="width: 100%"
clearable
@change="handleTagChange"
>
<el-option v-for="tag in availableTags" :key="tag" :label="tag" :value="tag" />
</el-select>
<div class="selected-tags" v-if="fileForm.fileTags.length > 0">
<el-tag
v-for="tag in fileForm.fileTags"
:key="tag"
closable
@close="removeTag(tag)"
class="tag-item"
>
{{ tag }}
</el-tag>
</div>
</div>
</el-form-item>
<el-form-item label="描述" prop="fileDesc">
<el-input
v-model="fileForm.fileDesc"
type="textarea"
rows="4"
placeholder="请输入文件描述"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="isSubmitting">保存</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useFilesStore } from '../stores/files'
import { useMetadataStore } from '../stores/metadata'
import FileSelector from '../components/FileSelector.vue'
export default {
name: 'AddFile',
components: {
FileSelector,
},
setup() {
const filesStore = useFilesStore()
const metadataStore = useMetadataStore()
const fileFormRef = ref(null)
const selectedFile = ref(null)
const isSubmitting = ref(false)
//
const categories = ref([])
const availableTags = ref([])
const selectedTag = ref('')
const fileForm = reactive({
filePath: '',
fileName: '',
fileExt: '',
fileCat: '',
fileTags: [],
fileDesc: '',
fileCreatedTime: '',
fileUpdatedTime: '',
isDirectory: false,
})
const rules = {
filePath: [{ required: true, message: '文件路径不能为空', trigger: 'blur' }],
fileName: [{ required: true, message: '文件名不能为空', trigger: 'blur' }],
}
//
const loadMetadata = async () => {
try {
categories.value = await metadataStore.fetchCategories()
availableTags.value = await metadataStore.fetchTags()
} catch (error) {
console.error('加载元数据失败:', error)
ElMessage.error('加载分类和标签数据失败')
}
}
const handleFileSelected = (file) => {
selectedFile.value = file
fileForm.filePath = file.path
fileForm.fileName = file.name
fileForm.fileExt = file.fileExt || ''
fileForm.fileCreatedTime = file.createdTime
fileForm.fileUpdatedTime = file.updatedTime
fileForm.isDirectory = file.isDirectory
}
const handleTagChange = (value) => {
if (value && !fileForm.fileTags.includes(value)) {
fileForm.fileTags.push(value)
selectedTag.value = '' //
}
}
const removeTag = (tag) => {
fileForm.fileTags = fileForm.fileTags.filter((t) => t !== tag)
}
const submitForm = async () => {
if (!fileFormRef.value) return
await fileFormRef.value.validate(async (valid) => {
if (valid) {
isSubmitting.value = true
try {
//
const fileExists = await window.electronAPI.checkFileExists(fileForm.filePath)
if (!fileExists) {
ElMessage.error('文件不存在或已被移动')
return
}
//
const serializableData = {
fileName: fileForm.fileName,
filePath: fileForm.filePath,
fileExt: fileForm.fileExt || '',
fileCat: fileForm.fileCat || '',
fileTags: fileForm.fileTags || [],
fileDesc: fileForm.fileDesc || '',
fileCreatedTime:
typeof fileForm.fileCreatedTime === 'string' ? fileForm.fileCreatedTime : null,
fileUpdatedTime:
typeof fileForm.fileUpdatedTime === 'string' ? fileForm.fileUpdatedTime : null,
isDirectory: Boolean(fileForm.isDirectory),
}
//
const fileId = await filesStore.addFile(serializableData)
//
if (fileForm.fileCat && !categories.value.includes(fileForm.fileCat)) {
metadataStore.addCategory(fileForm.fileCat)
}
fileForm.fileTags.forEach((tag) => {
if (!availableTags.value.includes(tag)) {
metadataStore.addTag(tag)
}
})
ElMessage.success('文件添加成功')
resetForm()
} catch (error) {
console.error('提交表单错误:', error)
//
if (error.message.includes('已存在于数据库中')) {
ElMessage.warning(error.message) // 使warningerror
} else {
ElMessage.error(`添加失败: ${error.message || '未知错误'}`)
}
} finally {
isSubmitting.value = false
}
}
})
}
const resetForm = () => {
selectedFile.value = null
fileForm.filePath = ''
fileForm.fileName = ''
fileForm.fileExt = ''
fileForm.fileCat = ''
fileForm.fileTags = []
fileForm.fileDesc = ''
fileForm.fileCreatedTime = ''
fileForm.fileUpdatedTime = ''
fileForm.isDirectory = false
selectedTag.value = ''
if (fileFormRef.value) {
fileFormRef.value.resetFields()
}
}
onMounted(() => {
loadMetadata()
})
return {
fileFormRef,
selectedFile,
fileForm,
rules,
categories,
availableTags,
selectedTag,
isSubmitting,
handleFileSelected,
handleTagChange,
removeTag,
submitForm,
resetForm,
}
},
}
</script>
<style scoped>
.add-file-container {
padding: 20px;
}
.file-selector-wrapper {
margin-bottom: 30px;
}
.tags-container {
width: 100%;
}
.selected-tags {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
}
.tag-item {
margin-right: 8px;
margin-bottom: 8px;
}
</style>

70
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,70 @@
<template>
<div class="dashboard-container">
<h1>文件管理统计</h1>
<el-row :gutter="20" class="stats-cards">
<el-col :span="12">
<StatisticsCard title="文件总数" :value="stats.fileCount" icon="Document" />
</el-col>
<el-col :span="12">
<StatisticsCard title="文件夹总数" :value="stats.dirCount" icon="Folder" />
</el-col>
</el-row>
<el-row :gutter="20" class="charts-row">
<el-col :span="24">
<FileTypeChart :data="stats.fileTypes" />
</el-col>
</el-row>
<h2>最近添加的文件</h2>
<el-table :data="stats.recentFiles" stripe>
<el-table-column prop="file_name" label="文件名" />
<el-table-column prop="file_cat" label="分类" />
<el-table-column prop="file_path" label="路径" />
<el-table-column prop="added_time" label="添加时间" />
</el-table>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { useDashboardStore } from '../stores/dashboard'
import StatisticsCard from '../components/Charts/StatisticsCard.vue'
import FileTypeChart from '../components/Charts/FileTypeChart.vue'
export default {
name: 'Dashboard',
components: {
StatisticsCard,
FileTypeChart,
},
setup() {
const dashboardStore = useDashboardStore()
const stats = ref(dashboardStore.stats)
onMounted(async () => {
await dashboardStore.getStats()
stats.value = dashboardStore.stats
})
return {
stats,
}
},
}
</script>
<style scoped>
.dashboard-container {
padding: 20px;
}
.stats-cards {
margin-bottom: 30px;
}
.charts-row {
margin-bottom: 30px;
}
</style>

499
src/views/SearchFiles.vue Normal file
View File

@ -0,0 +1,499 @@
<template>
<div class="search-files-container">
<h1>查询文件</h1>
<el-card class="search-card">
<div class="search-type-selector">
<el-radio-group v-model="searchType" @change="handleSearchTypeChange">
<el-radio-button label="fileName">按文件名查询</el-radio-button>
<el-radio-button label="keyword">按关键字查询</el-radio-button>
<el-radio-button label="fileCat">按分类查询</el-radio-button>
<el-radio-button label="fileTags">按标签查询</el-radio-button>
<el-radio-button label="dateRange">按时间查询</el-radio-button>
<el-radio-button label="advanced">高级查询</el-radio-button>
</el-radio-group>
</div>
<div class="search-content">
<!-- 文件名查询 -->
<template v-if="searchType === 'fileName'">
<el-input
v-model="searchForm.fileName"
placeholder="请输入文件名关键词"
clearable
:prefix-icon="Search"
@keyup.enter="search"
>
<template #append>
<el-button @click="search" :loading="isLoading">查询</el-button>
</template>
</el-input>
</template>
<!-- 关键字查询 -->
<template v-else-if="searchType === 'keyword'">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字(同时搜索文件名和描述)"
clearable
:prefix-icon="Search"
@keyup.enter="search"
>
<template #append>
<el-button @click="search" :loading="isLoading">查询</el-button>
</template>
</el-input>
</template>
<!-- 分类查询 -->
<template v-else-if="searchType === 'fileCat'">
<div class="category-search">
<el-select
v-model="searchForm.fileCat"
filterable
placeholder="请选择分类"
style="width: 100%"
clearable
>
<el-option
v-for="category in availableCategories"
:key="category"
:label="category"
:value="category"
/>
</el-select>
<el-button type="primary" @click="search" :loading="isLoading" class="search-button">
查询
</el-button>
</div>
</template>
<!-- 标签查询 -->
<template v-else-if="searchType === 'fileTags'">
<div class="tags-search">
<el-select
v-model="searchForm.fileTags"
multiple
filterable
allow-create
default-first-option
placeholder="请选择或输入标签"
style="width: 100%"
clearable
>
<el-option v-for="tag in availableTags" :key="tag" :label="tag" :value="tag" />
</el-select>
<el-button type="primary" @click="search" :loading="isLoading" class="search-button">
查询
</el-button>
</div>
</template>
<!-- 时间查询 -->
<template v-else-if="searchType === 'dateRange'">
<div class="date-search">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
value-format="YYYY-MM-DD"
clearable
/>
<el-button type="primary" @click="search" :loading="isLoading" class="search-button">
查询
</el-button>
</div>
</template>
<!-- 高级查询 -->
<template v-else-if="searchType === 'advanced'">
<el-form :model="searchForm" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文件名">
<el-input
v-model="searchForm.fileName"
placeholder="请输入文件名关键词"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关键字">
<el-input
v-model="searchForm.keyword"
placeholder="同时搜索文件名和描述"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="分类">
<el-select
v-model="searchForm.fileCat"
filterable
placeholder="请选择分类"
style="width: 100%"
clearable
>
<el-option
v-for="category in availableCategories"
:key="category"
:label="category"
:value="category"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="添加时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="标签">
<el-select
v-model="searchForm.fileTags"
multiple
filterable
allow-create
default-first-option
placeholder="请选择标签"
style="width: 100%"
clearable
>
<el-option v-for="tag in availableTags" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search" :loading="isLoading">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</template>
</div>
</el-card>
<div v-if="isLoading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="searchResults.length === 0 && hasSearched" class="no-results">
<el-empty description="没有找到匹配的文件" />
</div>
<div v-else-if="searchResults.length > 0" class="search-results">
<el-card class="result-card">
<div slot="header" class="result-header">
<span>查询结果</span>
<span class="result-count">共找到 {{ searchResults.length }} 个文件</span>
</div>
<FileTable :files="paginatedResults" @refresh="handleRefresh" />
<el-pagination
v-if="searchResults.length > pageSize"
:current-page="currentPage"
:page-size="pageSize"
:total="searchResults.length"
layout="prev, pager, next"
@current-change="handlePageChange"
class="pagination"
/>
</el-card>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { useFilesStore } from '../stores/files'
import { useMetadataStore } from '../stores/metadata'
import FileTable from '../components/FileTable.vue'
import { ElMessage } from 'element-plus'
import { Search, Collection } from '@element-plus/icons-vue'
export default {
name: 'SearchFiles',
components: {
FileTable,
},
setup() {
const filesStore = useFilesStore()
const metadataStore = useMetadataStore()
const searchForm = reactive({
fileName: '',
keyword: '',
fileCat: '',
fileTags: [],
dateRange: [],
})
const searchType = ref('fileName') //
const searchResults = ref([])
const availableTags = ref([])
const availableCategories = ref([])
const isLoading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const hasSearched = ref(false)
//
const handleSearchTypeChange = (type) => {
//
if (type !== 'advanced') {
resetSearch()
}
}
//
const loadMetadata = async () => {
try {
availableTags.value = await metadataStore.fetchTags()
availableCategories.value = await metadataStore.fetchCategories()
console.log('已加载分类:', availableCategories.value)
} catch (error) {
console.error('加载元数据失败:', error)
ElMessage.error('加载分类和标签数据失败')
}
}
//
const paginatedResults = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return searchResults.value.slice(start, end)
})
//
const prepareSearchCriteria = () => {
const criteria = {}
if (searchType.value === 'fileName' || searchType.value === 'advanced') {
if (searchForm.fileName.trim()) {
criteria.fileName = searchForm.fileName.trim()
}
}
if (searchType.value === 'keyword' || searchType.value === 'advanced') {
if (searchForm.keyword.trim()) {
criteria.keyword = searchForm.keyword.trim()
console.log('添加关键字查询条件:', criteria.keyword)
}
}
if (searchType.value === 'fileCat' || searchType.value === 'advanced') {
if (searchForm.fileCat.trim()) {
criteria.fileCat = searchForm.fileCat.trim()
}
}
if (searchType.value === 'fileTags' || searchType.value === 'advanced') {
if (searchForm.fileTags.length > 0) {
criteria.fileTags = searchForm.fileTags
}
}
if (searchType.value === 'dateRange' || searchType.value === 'advanced') {
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
criteria.startDate = searchForm.dateRange[0]
criteria.endDate = searchForm.dateRange[1]
}
}
console.log('最终查询条件:', JSON.stringify(criteria))
return criteria
}
//
const search = async () => {
console.log('开始搜索searchType =', searchType.value)
console.log('searchForm =', JSON.stringify(searchForm))
//
if (searchType.value === 'keyword') {
console.log('执行关键字查询:', searchForm.keyword)
// API
try {
const results = await window.electronAPI.searchFiles({
keyword: searchForm.keyword.trim(),
})
console.log('关键字查询结果:', results)
searchResults.value = results
} catch (error) {
console.error('直接关键字查询错误:', error)
}
return
}
const criteria = prepareSearchCriteria()
//
if (Object.keys(criteria).length === 0) {
ElMessage.warning('请至少输入一个查询条件')
return
}
isLoading.value = true
hasSearched.value = true
try {
console.log('开始搜索,条件:', JSON.stringify(criteria))
const results = await filesStore.searchFiles(criteria)
console.log('搜索结果:', results)
searchResults.value = results
currentPage.value = 1
console.log('搜索结果数量:', results.length)
//
if (results.length === 0) {
ElMessage.info('没有找到匹配的文件')
}
} catch (error) {
console.error('查询执行失败:', error)
ElMessage.error(`查询失败: ${error.message || '未知错误'}`)
searchResults.value = []
} finally {
isLoading.value = false
}
}
//
const resetSearch = () => {
searchForm.fileName = ''
searchForm.keyword = ''
searchForm.fileCat = ''
searchForm.fileTags = []
searchForm.dateRange = []
searchResults.value = []
hasSearched.value = false
}
//
const handlePageChange = (page) => {
currentPage.value = page
}
// FileTablerefresh
const handleRefresh = async () => {
//
if (hasSearched.value) {
await search()
}
}
//
onMounted(() => {
loadMetadata()
})
return {
searchForm,
searchType,
searchResults,
paginatedResults,
availableTags,
availableCategories,
isLoading,
currentPage,
pageSize,
hasSearched,
Search,
Collection,
handleSearchTypeChange,
search,
resetSearch,
handlePageChange,
handleRefresh,
}
},
}
</script>
<style scoped>
.search-files-container {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-type-selector {
margin-bottom: 20px;
}
.search-content {
margin-top: 20px;
}
.tags-search,
.date-search {
display: flex;
align-items: center;
}
.search-button {
margin-left: 10px;
flex-shrink: 0;
}
.loading-container {
padding: 20px;
}
.no-results {
margin-top: 40px;
text-align: center;
}
.result-card {
margin-top: 20px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.result-count {
font-size: 14px;
color: #909399;
}
.pagination {
margin-top: 20px;
text-align: center;
}
.category-search {
display: flex;
align-items: center;
}
.category-search .el-select {
flex: 1;
}
</style>