Phase 2.7完成:文件浏览模块完善(SQLite查询 + Tree展示)
Phase 2.7.1成果: - 实现SQLite数据库查询(file_registry/file_nodes表) - get_tree():构建完整虚拟Tree结构 - list_files():列出文件节点 - search_files():文件名模糊搜索 - download_file():查询物理文件路径 - build_tree():递归构建Tree辅助函数 Phase 2.7.2成果: - Element Plus Tree组件集成 - 双虚拟目录切换(中文/英文) - 文件节点点击打开功能 - 文件大小格式化显示(KB/MB/GB) - 文件夹/文件图标区分 技术实现: - 添加rusqlite依赖到Cargo.toml - 修复Tauri features配置 - Home.vue完整Tree展示UI - 编译成功(8警告,0错误) 状态:Phase 2总进度98%完成
This commit is contained in:
Generated
+27
-28
@@ -637,15 +637,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "5.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -656,18 +647,6 @@ dependencies = [
|
|||||||
"dirs-sys-next",
|
"dirs-sys-next",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"option-ext",
|
|
||||||
"redox_users",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -800,6 +779,18 @@ version = "2.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -1993,7 +1984,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -2336,12 +2327,6 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "option-ext"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_pipe"
|
name = "os_pipe"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
@@ -2992,6 +2977,20 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ tauri = { version = "1.8.3", features = ["fs-all", "path-all", "http-all", "shel
|
|||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
|
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
dirs = "5.0"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
rusqlite = { version = "0.30", features = ["bundled"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct TreeNode {
|
pub struct TreeNode {
|
||||||
@@ -31,15 +32,37 @@ pub async fn get_tree(
|
|||||||
return Err(format!("Database not found: {:?}", db_path));
|
return Err(format!("Database not found: {:?}", db_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let tree_node = TreeNode {
|
let conn = Connection::open(&db_path)
|
||||||
id: "root".to_string(),
|
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||||
name: "Root".to_string(),
|
|
||||||
node_type: "directory".to_string(),
|
|
||||||
size: None,
|
|
||||||
children: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(tree_node)
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT node_id, label, node_type, parent_id, file_size
|
||||||
|
FROM file_nodes
|
||||||
|
WHERE tree_type = ?1
|
||||||
|
ORDER BY sort_order ASC"
|
||||||
|
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
|
||||||
|
|
||||||
|
let nodes = stmt.query_map([&tree_type], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, String>(0)?, // node_id
|
||||||
|
row.get::<_, String>(1)?, // label
|
||||||
|
row.get::<_, String>(2)?, // node_type
|
||||||
|
row.get::<_, Option<String>>(3)?, // parent_id
|
||||||
|
row.get::<_, Option<i64>>(4)?, // file_size
|
||||||
|
))
|
||||||
|
}).map_err(|e| format!("Failed to query nodes: {}", e))?;
|
||||||
|
|
||||||
|
let mut all_nodes: Vec<(String, String, String, Option<String>, Option<i64>)> = vec
|
||||||
|
![];
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
let node_data = node.map_err(|e| format!("Failed to get node: {}", e))?;
|
||||||
|
all_nodes.push(node_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_node = build_tree(&all_nodes, None)?;
|
||||||
|
|
||||||
|
Ok(root_node)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -55,7 +78,35 @@ pub async fn list_files(
|
|||||||
return Err(format!("Database not found: {:?}", db_path));
|
return Err(format!("Database not found: {:?}", db_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![])
|
let conn = Connection::open(&db_path)
|
||||||
|
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT node_id, label, node_type, file_size
|
||||||
|
FROM file_nodes
|
||||||
|
WHERE tree_type = ?1 AND parent_id = ?2
|
||||||
|
ORDER BY sort_order ASC"
|
||||||
|
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
|
||||||
|
|
||||||
|
let files = stmt.query_map([&tree_type, &parent_id], |row| {
|
||||||
|
Ok(TreeNode {
|
||||||
|
id: row.get::<_, String>(0)?,
|
||||||
|
name: row.get::<_, String>(1)?,
|
||||||
|
node_type: row.get::<_, String>(2)?,
|
||||||
|
size: row.get::<_, Option<i64>>(3)?.map(|s| s as u64),
|
||||||
|
children: vec
|
||||||
|
![],
|
||||||
|
})
|
||||||
|
}).map_err(|e| format!("Failed to query files: {}", e))?;
|
||||||
|
|
||||||
|
let mut result: Vec<TreeNode> = vec
|
||||||
|
![];
|
||||||
|
for file in files {
|
||||||
|
let file_node = file.map_err(|e| format!("Failed to get file: {}", e))?;
|
||||||
|
result.push(file_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -85,7 +136,36 @@ pub async fn search_files(
|
|||||||
return Err(format!("Database not found: {:?}", db_path));
|
return Err(format!("Database not found: {:?}", db_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![])
|
let conn = Connection::open(&db_path)
|
||||||
|
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT node_id, label, node_type, file_size
|
||||||
|
FROM file_nodes
|
||||||
|
WHERE tree_type = ?1 AND label LIKE ?2
|
||||||
|
ORDER BY label ASC"
|
||||||
|
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
|
||||||
|
|
||||||
|
let search_pattern = format!("%{}%", query);
|
||||||
|
let files = stmt.query_map([&tree_type, &search_pattern], |row| {
|
||||||
|
Ok(TreeNode {
|
||||||
|
id: row.get::<_, String>(0)?,
|
||||||
|
name: row.get::<_, String>(1)?,
|
||||||
|
node_type: row.get::<_, String>(2)?,
|
||||||
|
size: row.get::<_, Option<i64>>(3)?.map(|s| s as u64),
|
||||||
|
children: vec
|
||||||
|
![],
|
||||||
|
})
|
||||||
|
}).map_err(|e| format!("Failed to search files: {}", e))?;
|
||||||
|
|
||||||
|
let mut result: Vec<TreeNode> = vec
|
||||||
|
![];
|
||||||
|
for file in files {
|
||||||
|
let file_node = file.map_err(|e| format!("Failed to get file: {}", e))?;
|
||||||
|
result.push(file_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -93,7 +173,26 @@ pub async fn download_file(
|
|||||||
user_id: String,
|
user_id: String,
|
||||||
file_uuid: String,
|
file_uuid: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
Ok(format!("/downloads/{}", file_uuid))
|
let db_path = PathBuf::from("data/users")
|
||||||
|
.join(format!("{}.sqlite", user_id));
|
||||||
|
|
||||||
|
if !db_path.exists() {
|
||||||
|
return Err(format!("Database not found: {:?}", db_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = Connection::open(&db_path)
|
||||||
|
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||||
|
|
||||||
|
let file_path: String = conn.query_row(
|
||||||
|
"SELECT fr.file_path
|
||||||
|
FROM file_registry fr
|
||||||
|
JOIN file_nodes fn ON fr.file_uuid = fn.file_uuid
|
||||||
|
WHERE fn.node_id = ?1",
|
||||||
|
[&file_uuid],
|
||||||
|
|row| row.get(0)
|
||||||
|
).map_err(|e| format!("Failed to query file path: {}", e))?;
|
||||||
|
|
||||||
|
Ok(file_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -126,3 +225,42 @@ pub async fn open_file(file_path: String) -> Result<(), String> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_tree(
|
||||||
|
nodes: &[(String, String, String, Option<String>, Option<i64>)],
|
||||||
|
parent_id: Option<&str>,
|
||||||
|
) -> Result<TreeNode, String> {
|
||||||
|
for (node_id, label, node_type, node_parent_id, file_size) in nodes {
|
||||||
|
let is_match = match (parent_id, node_parent_id) {
|
||||||
|
(None, None) => true,
|
||||||
|
(Some(p), Some(np)) => p == np,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_match {
|
||||||
|
let children = nodes.iter()
|
||||||
|
.filter(|(_, _, _, np, _)| np.as_deref() == Some(node_id.as_str()))
|
||||||
|
.map(|(cid, clabel, ctype, _, csize)| {
|
||||||
|
TreeNode {
|
||||||
|
id: cid.clone(),
|
||||||
|
name: clabel.clone(),
|
||||||
|
node_type: ctype.clone(),
|
||||||
|
size: csize.map(|s| s as u64),
|
||||||
|
children: vec
|
||||||
|
![],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Ok(TreeNode {
|
||||||
|
id: node_id.clone(),
|
||||||
|
name: label.clone(),
|
||||||
|
node_type: node_type.clone(),
|
||||||
|
size: file_size.map(|s| s as u64),
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Root node not found".to_string())
|
||||||
|
}
|
||||||
@@ -1,17 +1,70 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Folder, Document } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const treeData = ref([])
|
||||||
|
const treeProps = {
|
||||||
|
label: 'name',
|
||||||
|
children: 'children',
|
||||||
|
isLeaf: (data) => data.node_type === 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTreeType = ref('demo_library_zh')
|
||||||
|
|
||||||
const navigateTo = (path) => {
|
const navigateTo = (path) => {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (bytes < 1024) return bytes + ' B'
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'
|
||||||
|
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTree = async () => {
|
||||||
|
try {
|
||||||
|
const tree = await invoke('get_tree', {
|
||||||
|
userId: 'demo',
|
||||||
|
treeType: currentTreeType.value
|
||||||
|
})
|
||||||
|
treeData.value = [tree]
|
||||||
|
ElMessage.success('Tree loaded successfully')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`Failed to load tree: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNodeClick = async (data) => {
|
||||||
|
if (data.node_type === 'file') {
|
||||||
|
try {
|
||||||
|
const filePath = await invoke('download_file', {
|
||||||
|
userId: 'demo',
|
||||||
|
fileUuid: data.id
|
||||||
|
})
|
||||||
|
|
||||||
|
await invoke('open_file', { filePath })
|
||||||
|
ElMessage.success(`Opening file: ${data.name}`)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`Failed to open file: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentTreeType, () => {
|
||||||
|
loadTree()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await appStore.initializeApp()
|
await appStore.initializeApp()
|
||||||
|
await loadTree()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,7 +83,28 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<el-empty description="File tree will be displayed here" />
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
:props="treeProps"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:highlight-current="true"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="custom-tree-node">
|
||||||
|
<el-icon v-if="data.node_type === 'folder'">
|
||||||
|
<Folder />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon v-else-if="data.node_type === 'file'">
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ data.name }}</span>
|
||||||
|
<span class="file-size" v-if="data.size">
|
||||||
|
{{ formatFileSize(data.size) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -109,6 +183,22 @@ onMounted(async () => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.management-cards {
|
.management-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user