release(project): initial release
- 发布初始版本,文件位置记录查询工具,可以记录文件存放位置,提供多样查询方式,便于查找
This commit is contained in:
commit
9b459a92c6
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.vscode/
|
||||
.cursor/
|
||||
.idea/
|
||||
.git/
|
||||
release/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.lock
|
||||
10
.prettierignore
Normal file
10
.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
.vscode/
|
||||
.cursor/
|
||||
.idea/
|
||||
.git/
|
||||
public/
|
||||
release/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.lock
|
||||
60
.prettierrc.json
Normal file
60
.prettierrc.json
Normal 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
40
electron-builder.js
Normal 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}/',
|
||||
},
|
||||
}
|
||||
322
electron/database/sqlite.cjs
Normal file
322
electron/database/sqlite.cjs
Normal 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
179
electron/database/sqlite.js
Normal 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
280
electron/main.cjs
Normal 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
28
electron/preload.cjs
Normal 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
34
package.json
Normal 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
11
public/index.html
Normal 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
23
rsbuild.config.js
Normal 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
42
src/App.vue
Normal 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>
|
||||
94
src/components/Charts/FileTypeChart.vue
Normal file
94
src/components/Charts/FileTypeChart.vue
Normal 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>
|
||||
69
src/components/Charts/StatisticsCard.vue
Normal file
69
src/components/Charts/StatisticsCard.vue
Normal 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
125
src/components/FileCard.vue
Normal 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>
|
||||
145
src/components/FileSelector.vue
Normal file
145
src/components/FileSelector.vue
Normal 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>
|
||||
194
src/components/FileTable.vue
Normal file
194
src/components/FileTable.vue
Normal 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>
|
||||
44
src/components/Layout/Header.vue
Normal file
44
src/components/Layout/Header.vue
Normal 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>
|
||||
64
src/components/Layout/Sidebar.vue
Normal file
64
src/components/Layout/Sidebar.vue
Normal 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
11
src/index.html
Normal 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
19
src/index.js
Normal 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
17
src/main.js
Normal 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
30
src/router/index.js
Normal 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
32
src/stores/dashboard.js
Normal 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
109
src/stores/files.js
Normal 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
3
src/stores/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
||||
65
src/stores/metadata.js
Normal file
65
src/stores/metadata.js
Normal 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
47
src/styles/index.css
Normal 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
25
src/styles/variables.css
Normal 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
288
src/views/AddFile.vue
Normal 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) // 使用warning样式而不是error
|
||||
} 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
70
src/views/Dashboard.vue
Normal 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
499
src/views/SearchFiles.vue
Normal 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
|
||||
}
|
||||
|
||||
// 当FileTable组件触发refresh事件时,重新加载数据
|
||||
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>
|
||||
Loading…
x
Reference in New Issue
Block a user