mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
feat(file): 文件管理多项改进 - 权限显示、双击交互、终端集成、图标视图 (#1220)
* Initial plan * feat(file): 实现文件管理多项改进功能 1. 修复权限弹窗中所有者和组固定显示 www 的问题 2. 目录单击进入,文件双击打开(编辑/预览) 3. 点击权限/所有组列直接打开编辑弹窗 4. 压缩包双击自动弹出解压窗口 5. 预留视图切换按钮(列表/图标视图) 6. 工具栏新增终端按钮 7. 右键菜单新增终端选项(仅目录) 8. 右键菜单新增属性选项 9. 新增显示/隐藏隐藏文件按钮(刷新按钮旁边) Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 完善后端 Info 接口返回完整文件信息并修复代码审查问题 1. 后端 /file/info 接口现在返回 owner、group、uid、gid 等完整信息 2. 修复权限弹窗 mode 处理逻辑,确保正确处理边界情况 3. 优化视图切换逻辑,避免不必要的状态变更 4. 提取 openFile 函数复用代码,减少重复逻辑 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 添加类型断言检查和提取 normalizeMode 辅助函数 1. 后端 Info 方法添加 syscall.Stat_t 类型断言安全检查 2. 前端 PermissionModal 提取 normalizeMode 函数消除代码重复 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat(file): 实现图标视图和鼠标框选功能 1. 新增 GridView.vue 组件实现图标视图 2. 支持鼠标拖动框选批量选择文件/目录 3. 支持 Ctrl/Cmd+点击多选和 Shift+点击范围选择 4. 更新 store 添加 viewType 状态持久化 5. 更新 ToolBar 和 IndexView 支持视图切换 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint * feat: 完善切换逻辑 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -319,13 +319,35 @@ func (s *FileService) Info(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get file system info"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有 immutable 属性
|
||||
immutable := false
|
||||
if f, err := stdos.OpenFile(req.Path, stdos.O_RDONLY, 0); err == nil {
|
||||
immutable, _ = chattr.IsAttr(f, chattr.FS_IMMUTABLE_FL)
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"name": info.Name(),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
"name": info.Name(),
|
||||
"full": req.Path,
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": os.GetUser(stat.Uid),
|
||||
"group": os.GetGroup(stat.Gid),
|
||||
"uid": stat.Uid,
|
||||
"gid": stat.Gid,
|
||||
"hidden": io.IsHidden(info.Name()),
|
||||
"symlink": io.IsSymlink(info.Mode()),
|
||||
"link": io.GetSymlink(req.Path),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
"immutable": immutable,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -449,28 +471,81 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
switch req.Sort {
|
||||
case "asc":
|
||||
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
|
||||
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
|
||||
})
|
||||
case "desc":
|
||||
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
|
||||
return strings.Compare(strings.ToLower(b.Name()), strings.ToLower(a.Name()))
|
||||
})
|
||||
default:
|
||||
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
|
||||
if a.IsDir() && !b.IsDir() {
|
||||
return -1
|
||||
}
|
||||
if !a.IsDir() && b.IsDir() {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
|
||||
})
|
||||
// 前缀 - 表示降序
|
||||
sortKey := req.Sort
|
||||
sortDesc := false
|
||||
if strings.HasPrefix(sortKey, "-") {
|
||||
sortDesc = true
|
||||
sortKey = strings.TrimPrefix(sortKey, "-")
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, s.formatDir(req.Path, list))
|
||||
// 获取文件信息用于排序
|
||||
type entryWithInfo struct {
|
||||
entry stdos.DirEntry
|
||||
info stdos.FileInfo
|
||||
}
|
||||
entriesWithInfo := make([]entryWithInfo, 0, len(list))
|
||||
for _, entry := range list {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entriesWithInfo = append(entriesWithInfo, entryWithInfo{entry: entry, info: info})
|
||||
}
|
||||
|
||||
// 排序
|
||||
slices.SortFunc(entriesWithInfo, func(a, b entryWithInfo) int {
|
||||
// 文件夹始终排在前面(除非按特定字段排序)
|
||||
if sortKey == "" {
|
||||
if a.info.IsDir() && !b.info.IsDir() {
|
||||
return -1
|
||||
}
|
||||
if !a.info.IsDir() && b.info.IsDir() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var cmp int
|
||||
switch sortKey {
|
||||
case "size":
|
||||
// 按大小排序
|
||||
if a.info.Size() < b.info.Size() {
|
||||
cmp = -1
|
||||
} else if a.info.Size() > b.info.Size() {
|
||||
cmp = 1
|
||||
} else {
|
||||
cmp = 0
|
||||
}
|
||||
case "modify":
|
||||
// 按修改时间排序
|
||||
if a.info.ModTime().Before(b.info.ModTime()) {
|
||||
cmp = -1
|
||||
} else if a.info.ModTime().After(b.info.ModTime()) {
|
||||
cmp = 1
|
||||
} else {
|
||||
cmp = 0
|
||||
}
|
||||
case "name":
|
||||
// 按名称排序
|
||||
cmp = strings.Compare(strings.ToLower(a.info.Name()), strings.ToLower(b.info.Name()))
|
||||
default:
|
||||
// 默认按名称排序
|
||||
cmp = strings.Compare(strings.ToLower(a.info.Name()), strings.ToLower(b.info.Name()))
|
||||
}
|
||||
|
||||
if sortDesc {
|
||||
cmp = -cmp
|
||||
}
|
||||
return cmp
|
||||
})
|
||||
|
||||
// 转换回 DirEntry 列表
|
||||
sortedList := make([]stdos.DirEntry, len(entriesWithInfo))
|
||||
for i, e := range entriesWithInfo {
|
||||
sortedList[i] = e.entry
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, s.formatDir(req.Path, sortedList))
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
|
||||
Reference in New Issue
Block a user