2
0
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:
Copilot
2026-01-11 23:36:44 +08:00
committed by GitHub
parent 9cbef88984
commit 2f63523540
11 changed files with 1999 additions and 899 deletions

View File

@@ -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,