From 9b459a92c6ed4e8de1312fea90cd942db1066105 Mon Sep 17 00:00:00 2001 From: App1ePine Date: Tue, 1 Jul 2025 23:25:40 +0800 Subject: [PATCH] release(project): initial release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 发布初始版本,文件位置记录查询工具,可以记录文件存放位置,提供多样查询方式,便于查找 --- .gitignore | 9 + .prettierignore | 10 + .prettierrc.json | 60 +++ electron-builder.js | 40 ++ electron/database/sqlite.cjs | 322 +++++++++++++++ electron/database/sqlite.js | 179 ++++++++ electron/main.cjs | 280 +++++++++++++ electron/preload.cjs | 28 ++ package.json | 34 ++ public/index.html | 11 + rsbuild.config.js | 23 ++ src/App.vue | 42 ++ src/components/Charts/FileTypeChart.vue | 94 +++++ src/components/Charts/StatisticsCard.vue | 69 ++++ src/components/FileCard.vue | 125 ++++++ src/components/FileSelector.vue | 145 +++++++ src/components/FileTable.vue | 194 +++++++++ src/components/Layout/Header.vue | 44 ++ src/components/Layout/Sidebar.vue | 64 +++ src/index.html | 11 + src/index.js | 19 + src/main.js | 17 + src/router/index.js | 30 ++ src/stores/dashboard.js | 32 ++ src/stores/files.js | 109 +++++ src/stores/index.js | 3 + src/stores/metadata.js | 65 +++ src/styles/index.css | 47 +++ src/styles/variables.css | 25 ++ src/views/AddFile.vue | 288 +++++++++++++ src/views/Dashboard.vue | 70 ++++ src/views/SearchFiles.vue | 499 +++++++++++++++++++++++ 32 files changed, 2988 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 electron-builder.js create mode 100644 electron/database/sqlite.cjs create mode 100644 electron/database/sqlite.js create mode 100644 electron/main.cjs create mode 100644 electron/preload.cjs create mode 100644 package.json create mode 100644 public/index.html create mode 100644 rsbuild.config.js create mode 100644 src/App.vue create mode 100644 src/components/Charts/FileTypeChart.vue create mode 100644 src/components/Charts/StatisticsCard.vue create mode 100644 src/components/FileCard.vue create mode 100644 src/components/FileSelector.vue create mode 100644 src/components/FileTable.vue create mode 100644 src/components/Layout/Header.vue create mode 100644 src/components/Layout/Sidebar.vue create mode 100644 src/index.html create mode 100644 src/index.js create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/stores/dashboard.js create mode 100644 src/stores/files.js create mode 100644 src/stores/index.js create mode 100644 src/stores/metadata.js create mode 100644 src/styles/index.css create mode 100644 src/styles/variables.css create mode 100644 src/views/AddFile.vue create mode 100644 src/views/Dashboard.vue create mode 100644 src/views/SearchFiles.vue diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87bd99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +.cursor/ +.idea/ +.git/ +release/ +node_modules/ +dist/ +build/ +*.lock \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9f8c055 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +.vscode/ +.cursor/ +.idea/ +.git/ +public/ +release/ +node_modules/ +dist/ +build/ +*.lock \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..063e288 --- /dev/null +++ b/.prettierrc.json @@ -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 + } + } + ] +} diff --git a/electron-builder.js b/electron-builder.js new file mode 100644 index 0000000..808f4c3 --- /dev/null +++ b/electron-builder.js @@ -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}/', + }, +} diff --git a/electron/database/sqlite.cjs b/electron/database/sqlite.cjs new file mode 100644 index 0000000..bcb763c --- /dev/null +++ b/electron/database/sqlite.cjs @@ -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, +} diff --git a/electron/database/sqlite.js b/electron/database/sqlite.js new file mode 100644 index 0000000..b4e2bcc --- /dev/null +++ b/electron/database/sqlite.js @@ -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, +} diff --git a/electron/main.cjs b/electron/main.cjs new file mode 100644 index 0000000..a482189 --- /dev/null +++ b/electron/main.cjs @@ -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) + } + }) + }) +} diff --git a/electron/preload.cjs b/electron/preload.cjs new file mode 100644 index 0000000..729ad8d --- /dev/null +++ b/electron/preload.cjs @@ -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'), +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a12134 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f9fbb4e --- /dev/null +++ b/public/index.html @@ -0,0 +1,11 @@ + + + + + + 文件位置查询 + + +
+ + \ No newline at end of file diff --git a/rsbuild.config.js b/rsbuild.config.js new file mode 100644 index 0000000..76bc124 --- /dev/null +++ b/rsbuild.config.js @@ -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', + }, +}) diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..7ac129a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/Charts/FileTypeChart.vue b/src/components/Charts/FileTypeChart.vue new file mode 100644 index 0000000..613609e --- /dev/null +++ b/src/components/Charts/FileTypeChart.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/components/Charts/StatisticsCard.vue b/src/components/Charts/StatisticsCard.vue new file mode 100644 index 0000000..e83eb47 --- /dev/null +++ b/src/components/Charts/StatisticsCard.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/FileCard.vue b/src/components/FileCard.vue new file mode 100644 index 0000000..5736fca --- /dev/null +++ b/src/components/FileCard.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/components/FileSelector.vue b/src/components/FileSelector.vue new file mode 100644 index 0000000..c768bbb --- /dev/null +++ b/src/components/FileSelector.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/components/FileTable.vue b/src/components/FileTable.vue new file mode 100644 index 0000000..54607fc --- /dev/null +++ b/src/components/FileTable.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/src/components/Layout/Header.vue b/src/components/Layout/Header.vue new file mode 100644 index 0000000..7478536 --- /dev/null +++ b/src/components/Layout/Header.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/components/Layout/Sidebar.vue b/src/components/Layout/Sidebar.vue new file mode 100644 index 0000000..8efe011 --- /dev/null +++ b/src/components/Layout/Sidebar.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..4f37c41 --- /dev/null +++ b/src/index.html @@ -0,0 +1,11 @@ + + + + + + 文件位置查询 + + +
+ + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f85f4bf --- /dev/null +++ b/src/index.js @@ -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') +}) diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..81258ff --- /dev/null +++ b/src/main.js @@ -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') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..a9b82db --- /dev/null +++ b/src/router/index.js @@ -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 diff --git a/src/stores/dashboard.js b/src/stores/dashboard.js new file mode 100644 index 0000000..f26d076 --- /dev/null +++ b/src/stores/dashboard.js @@ -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 + } + }, + }, +}) diff --git a/src/stores/files.js b/src/stores/files.js new file mode 100644 index 0000000..75a08d9 --- /dev/null +++ b/src/stores/files.js @@ -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 + } + }, + }, +}) diff --git a/src/stores/index.js b/src/stores/index.js new file mode 100644 index 0000000..f00b209 --- /dev/null +++ b/src/stores/index.js @@ -0,0 +1,3 @@ +import { createPinia } from 'pinia' + +export default createPinia() diff --git a/src/stores/metadata.js b/src/stores/metadata.js new file mode 100644 index 0000000..99ddbd4 --- /dev/null +++ b/src/stores/metadata.js @@ -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) + } + }, + }, +}) diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..8852b1a --- /dev/null +++ b/src/styles/index.css @@ -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; +} diff --git a/src/styles/variables.css b/src/styles/variables.css new file mode 100644 index 0000000..51172a5 --- /dev/null +++ b/src/styles/variables.css @@ -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; +} diff --git a/src/views/AddFile.vue b/src/views/AddFile.vue new file mode 100644 index 0000000..d7c67df --- /dev/null +++ b/src/views/AddFile.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue new file mode 100644 index 0000000..f27f9cd --- /dev/null +++ b/src/views/Dashboard.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/views/SearchFiles.vue b/src/views/SearchFiles.vue new file mode 100644 index 0000000..3ceecbd --- /dev/null +++ b/src/views/SearchFiles.vue @@ -0,0 +1,499 @@ + + + + +