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