2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

feat: 优化文件搜索体验,close #997

This commit is contained in:
2025-09-18 01:29:54 +08:00
parent dcaa603d28
commit 5632a5aa1d
15 changed files with 269 additions and 318 deletions

4
go.mod
View File

@@ -60,11 +60,11 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/G-Core/gcore-dns-sdk-go v0.3.2 // indirect
github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofiber/schema v1.6.0 // indirect

18
go.sum
View File

@@ -15,8 +15,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/G-Core/gcore-dns-sdk-go v0.3.2 h1:WCTKmoJ5uND69UFE318yxWuHy/h3AA28Z1OnqSYUxRk=
github.com/G-Core/gcore-dns-sdk-go v0.3.2/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -57,8 +57,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
@@ -106,8 +106,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -218,14 +216,10 @@ github.com/libtnb/acmez/v3 v3.0.0-20250707093727-dc5aedd96413 h1:q6Qttk+i7r8JWw1
github.com/libtnb/acmez/v3 v3.0.0-20250707093727-dc5aedd96413/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/libtnb/chix v1.3.0 h1:/U+CyuxI41ooeB6M/762PHOjQlfHRg6BQjHKLZarlrM=
github.com/libtnb/chix v1.3.0/go.mod h1:o8nQLEp/UrUojBKYzw8K8sltU/h0XxI2VLZ/z7AQCQg=
github.com/libtnb/gormstore v1.1.0 h1:VbX8u0hhyl2YFSGfkSlF/xSfFG/LE29DCLrY8MJ+uxM=
github.com/libtnb/gormstore v1.1.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
github.com/libtnb/gormstore v1.1.1 h1:FG/3P4PuWM6/vB4weVJ31meiSaoeXns1NQlP66quKeg=
github.com/libtnb/gormstore v1.1.1/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA=
github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA=
github.com/libtnb/sessions v1.2.0 h1:g/RMcfGTC5P2BQE1IqrgppPEQ++x/QjOuiwbN9Frke8=
github.com/libtnb/sessions v1.2.0/go.mod h1:45Bn9d6PseDINLIM1QaJrlCMbzSZ0NWpDbWkdrKJKw0=
github.com/libtnb/sessions v1.2.1 h1:O9gkEIeZuqyaxopXrUJcGxlNxmNfRBI8BOK43yLJXDI=
github.com/libtnb/sessions v1.2.1/go.mod h1:45Bn9d6PseDINLIM1QaJrlCMbzSZ0NWpDbWkdrKJKw0=
github.com/libtnb/utils v1.2.0 h1:6bTZrWn2OkNrODpCY4dhuHwbhsVRV7HICIgmZ31we98=
@@ -380,8 +374,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -454,8 +446,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=

View File

@@ -7,8 +7,15 @@ import (
)
type FileList struct {
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
Sort string `json:"sort" form:"sort"`
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
Sort string `json:"sort" form:"sort"`
Keyword string `form:"keyword" json:"keyword"`
Sub bool `form:"sub" json:"sub"`
}
func (r *FileList) Prepare(req *http.Request) error {
r.Sub = cast.ToBool(req.FormValue("sub"))
return nil
}
type FilePath struct {
@@ -53,14 +60,3 @@ type FileUnCompress struct {
File string `form:"file" json:"file" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
}
type FileSearch struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Keyword string `form:"keyword" json:"keyword" validate:"required"`
Sub bool `form:"sub" json:"sub"`
}
func (r *FileSearch) Prepare(req *http.Request) error {
r.Sub = cast.ToBool(req.FormValue("sub"))
return nil
}

View File

@@ -360,7 +360,6 @@ func (route *Http) Register(r *chi.Mux) {
r.Post("/permission", route.file.Permission)
r.Post("/compress", route.file.Compress)
r.Post("/un_compress", route.file.UnCompress)
r.Get("/search", route.file.Search)
r.Get("/list", route.file.List)
})

View File

@@ -395,27 +395,6 @@ func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FileSearch](r)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
results, err := io.SearchX(req.Path, req.Keyword, req.Sub)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
paged, total := Paginate(r, s.formatInfo(results))
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FileList](r)
if err != nil {
@@ -423,10 +402,19 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
return
}
list, err := stdos.ReadDir(req.Path)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
var list []stdos.DirEntry
if req.Keyword != "" {
list, err = io.SearchX(req.Path, req.Keyword, req.Sub)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
} else {
list, err = stdos.ReadDir(req.Path)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
switch req.Sort {
@@ -464,7 +452,10 @@ func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
for item := range slices.Values(entries) {
info, err := item.Info()
if err != nil {
continue
continue // 直接跳过,不返回错误,不然很烦人的
}
if de, ok := item.(*io.SearchEntry); ok {
base = filepath.Dir(de.Path())
}
stat := info.Sys().(*syscall.Stat_t)

View File

@@ -53,6 +53,4 @@ func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) {}
func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) {}
func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {}
func (s *FileService) List(w http.ResponseWriter, r *http.Request) {}

View File

@@ -104,55 +104,3 @@ func CountX(path string) (int64, error) {
count := len(out)
return int64(count), nil
}
// Search 查找文件/文件夹
func Search(path, keyword string, sub bool) (map[string]os.FileInfo, error) {
paths := make(map[string]os.FileInfo)
baseDepth := strings.Count(filepath.Clean(path), string(os.PathSeparator))
err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !sub && strings.Count(p, string(os.PathSeparator)) > baseDepth+1 {
return filepath.SkipDir
}
if strings.Contains(info.Name(), keyword) {
paths[p] = info
}
return nil
})
return paths, err
}
// SearchX 查找文件/文件夹find命令
func SearchX(path, keyword string, sub bool) (map[string]os.FileInfo, error) {
paths := make(map[string]os.FileInfo)
var out string
var err error
if sub {
out, err = shell.Execf("find '%s' -name '*%s*'", path, keyword)
} else {
out, err = shell.Execf("find '%s' -maxdepth 1 -name '*%s*'", path, keyword)
}
if err != nil {
return nil, err
}
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
info, err := os.Stat(line)
if err != nil {
return nil, err
}
paths[line] = info
}
return paths, nil
}

100
pkg/io/search.go Normal file
View File

@@ -0,0 +1,100 @@
package io
import (
"os"
"path/filepath"
"strings"
"github.com/tnborg/panel/pkg/shell"
)
// Search 查找文件/文件夹
func Search(path, keyword string, sub bool) (map[string]os.FileInfo, error) {
paths := make(map[string]os.FileInfo)
baseDepth := strings.Count(filepath.Clean(path), string(os.PathSeparator))
err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !sub && strings.Count(p, string(os.PathSeparator)) > baseDepth+1 {
return filepath.SkipDir
}
if strings.Contains(info.Name(), keyword) {
paths[p] = info
}
return nil
})
return paths, err
}
// SearchX 查找文件/文件夹find命令
func SearchX(path, keyword string, sub bool) ([]os.DirEntry, error) {
var out string
var err error
if sub {
out, err = shell.Execf("find '%s' -name '*%s*'", path, keyword)
} else {
out, err = shell.Execf("find '%s' -maxdepth 1 -name '*%s*'", path, keyword)
}
if err != nil {
return nil, err
}
var entries []os.DirEntry
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || line == path {
continue
}
entry, err := newDirEntryFromPath(line)
if err != nil {
continue // 直接跳过,不返回错误,不然很烦人的
}
entries = append(entries, entry)
}
return entries, nil
}
// SearchEntry 实现 os.DirEntry 接口
type SearchEntry struct {
path string
info os.FileInfo
}
// newDirEntryFromPath 根据文件路径创建 SearchEntry
func newDirEntryFromPath(path string) (*SearchEntry, error) {
info, err := os.Stat(path)
if err != nil {
return nil, err
}
return &SearchEntry{path: path, info: info}, nil
}
// Name 返回文件基本名称
func (d *SearchEntry) Name() string {
return filepath.Base(d.path)
}
// IsDir 判断是否为目录
func (d *SearchEntry) IsDir() bool {
return d.info.IsDir()
}
// Type 返回文件模式类型
func (d *SearchEntry) Type() os.FileMode {
return d.info.Mode().Type()
}
// Info 返回文件信息
func (d *SearchEntry) Info() (os.FileInfo, error) {
return d.info, nil
}
// Path 返回文件完整路径
func (d *SearchEntry) Path() string {
return d.path
}

78
pkg/io/search_test.go Normal file
View File

@@ -0,0 +1,78 @@
package io
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/suite"
)
type SearchTestSuite struct {
suite.Suite
}
func TestSearchTestSuite(t *testing.T) {
suite.Run(t, &SearchTestSuite{})
}
func (s *SearchTestSuite) SetupTest() {
if _, err := os.Stat("testdata"); os.IsNotExist(err) {
s.NoError(os.MkdirAll("testdata", 0755))
}
}
func (s *SearchTestSuite) TearDownTest() {
s.NoError(os.RemoveAll("testdata"))
}
func (s *SearchTestSuite) TestSearchX() {
testDir := "testdata/search_test"
s.NoError(os.MkdirAll(testDir, 0755))
s.NoError(os.MkdirAll(filepath.Join(testDir, "subdir"), 0755))
testFiles := map[string]string{
"test_file1.txt": "内容1",
"test_file2.log": "内容2",
"another_test.txt": "内容3",
"subdir/nested_test.txt": "嵌套内容",
"unrelated.dat": "无关内容",
}
for path, content := range testFiles {
s.NoError(Write(filepath.Join(testDir, path), content, 0644))
}
s.Run("正常搜索", func() {
entries, err := SearchX(testDir, "test", false)
s.NoError(err)
names := make(map[string]bool)
for _, entry := range entries {
names[entry.Name()] = true
s.NotEmpty(entry.Name())
info, err := entry.Info()
s.NoError(err)
s.NotNil(info)
s.Equal(entry.Type(), info.Mode().Type())
s.Equal(entry.IsDir(), info.IsDir())
}
s.True(names["test_file1.txt"])
s.True(names["test_file2.log"])
s.True(names["another_test.txt"])
s.False(names["nested_test.txt"]) // 不应该找到子目录中的文件
s.False(names["unrelated.dat"]) // 不应该找到不匹配的文件
})
s.Run("无匹配结果", func() {
entries, err := SearchX(testDir, "nonexistent", false)
s.NoError(err)
s.Empty(entries)
})
s.Run("路径不存在", func() {
_, err := SearchX("/path/does/not/exist", "test", false)
s.Error(err)
})
}

View File

@@ -34,6 +34,12 @@ export default {
search: (path: string, keyword: string, sub: boolean, page: number, limit: number): any =>
http.Get('/file/search', { params: { path, keyword, sub, page, limit } }),
// 获取文件列表
list: (path: string, page: number, limit: number, sort: string): any =>
http.Get('/file/list', { params: { path, page, limit, sort } })
list: (
path: string,
keyword: string,
sub: boolean,
sort: string,
page: number,
limit: number
): any => http.Get('/file/list', { params: { path, keyword, sub, sort, page, limit } })
}

View File

@@ -1,16 +1,22 @@
export interface File {
path: string
keyword: string
sub: boolean
}
export const useFileStore = defineStore('file', {
state: (): File => {
return {
path: '/'
path: '/opt',
keyword: '',
sub: false
}
},
actions: {
set(info: File) {
this.path = info.path
this.keyword = info.keyword
this.sub = info.sub
}
},
persist: true

View File

@@ -24,7 +24,11 @@ const permission = ref(false)
<template>
<common-page show-footer>
<n-flex vertical :size="20">
<path-input v-model:path="fileStore.path" />
<path-input
v-model:path="fileStore.path"
v-model:keyword="fileStore.keyword"
v-model:sub="fileStore.sub"
/>
<tool-bar
v-model:path="fileStore.path"
v-model:selected="selected"
@@ -35,6 +39,8 @@ const permission = ref(false)
/>
<list-table
v-model:path="fileStore.path"
v-model:keyword="fileStore.keyword"
v-model:sub="fileStore.sub"
v-model:selected="selected"
v-model:marked="marked"
v-model:markedType="markedType"

View File

@@ -31,7 +31,9 @@ import type { Marked } from '@/views/file/types'
const { $gettext } = useGettext()
const sort = ref<string>('')
const path = defineModel<string>('path', { type: String, required: true })
const path = defineModel<string>('path', { type: String, required: true }) // 当前路径
const keyword = defineModel<string>('keyword', { type: String, default: '' }) // 搜索关键词
const sub = defineModel<boolean>('sub', { type: Boolean, default: false }) // 搜索是否包括子目录
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
const markedType = defineModel<string>('markedType', { type: String, required: true })
@@ -389,7 +391,7 @@ const rowProps = (row: any) => {
}
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => file.list(path.value, page, pageSize, sort.value),
(page, pageSize) => file.list(path.value, keyword.value, sub.value, sort.value, page, pageSize),
{
initialData: { total: 0, list: [] },
initialPageSize: 100,
@@ -633,15 +635,21 @@ const handleSorterChange = (sorter: {
switch (sorter.order) {
case 'ascend':
sort.value = 'asc'
refresh()
nextTick(() => {
refresh()
})
break
case 'descend':
sort.value = 'desc'
refresh()
nextTick(() => {
refresh()
})
break
default:
sort.value = ''
refresh()
nextTick(() => {
refresh()
})
break
}
}
@@ -649,15 +657,29 @@ const handleSorterChange = (sorter: {
}
onMounted(() => {
// 监听路径变化并刷新列表
watch(
path,
() => {
selected.value = []
refresh()
window.$bus.emit('push-history', path.value)
keyword.value = ''
sub.value = false
nextTick(() => {
refresh()
})
window.$bus.emit('file:push-history', path.value)
},
{ immediate: true }
)
// 监听搜索事件
window.$bus.on('file:search', () => {
selected.value = []
nextTick(() => {
refresh()
})
window.$bus.emit('file:push-history', path.value)
})
// 监听刷新事件
window.$bus.on('file:refresh', refresh)
})

View File

@@ -3,10 +3,11 @@ import type { InputInst } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import { checkPath } from '@/utils/file'
import SearchModal from '@/views/file/SearchModal.vue'
const { $gettext } = useGettext()
const path = defineModel<string>('path', { type: String, required: true })
const path = defineModel<string>('path', { type: String, required: true }) // 当前路径
const keyword = defineModel<string>('keyword', { type: String, default: '' }) // 搜索关键词
const sub = defineModel<boolean>('sub', { type: Boolean, default: false }) // 搜索是否包括子目录
const isInput = ref(false)
const pathInput = ref<InputInst | null>(null)
const input = ref('www')
@@ -14,12 +15,6 @@ const input = ref('www')
const history: string[] = []
let current = -1
const searchShow = ref(false)
const search = ref({
keyword: '',
sub: false
})
const handleInput = () => {
isInput.value = true
nextTick(() => {
@@ -92,7 +87,7 @@ const handlePushHistory = (path: string) => {
}
const handleSearch = () => {
searchShow.value = true
window.$bus.emit('file:search')
}
watch(
@@ -104,11 +99,11 @@ watch(
)
onMounted(() => {
window.$bus.on('push-history', handlePushHistory)
window.$bus.on('file:push-history', handlePushHistory)
})
onUnmounted(() => {
window.$bus.off('push-history')
window.$bus.off('file:push-history')
})
</script>
@@ -151,9 +146,9 @@ onUnmounted(() => {
/>
</n-input-group>
<n-input-group w-400>
<n-input v-model:value="search.keyword" :placeholder="$gettext('Enter search content')">
<n-input v-model:value="keyword" :placeholder="$gettext('Enter search content')">
<template #suffix>
<n-checkbox v-model:checked="search.sub">
<n-checkbox v-model:checked="sub">
{{ $gettext('Include subdirectories') }}
</n-checkbox>
</template>
@@ -163,12 +158,6 @@ onUnmounted(() => {
</n-button>
</n-input-group>
</n-flex>
<search-modal
v-model:show="searchShow"
v-model:path="path"
v-model:keyword="search.keyword"
v-model:sub="search.sub"
/>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,178 +0,0 @@
<script setup lang="ts">
import file from '@/api/panel/file'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import copy2clipboard from '@vavt/copy2clipboard'
import type { DataTableColumns } from 'naive-ui'
import type { RowData } from 'naive-ui/es/data-table/src/interface'
const { $gettext } = useGettext()
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const path = defineModel<string>('path', { type: String, required: true })
const keyword = defineModel<string>('keyword', { type: String, required: true })
const sub = defineModel<boolean>('sub', { type: Boolean, required: true })
const loading = ref(false)
const columns: DataTableColumns<RowData> = [
{
title: $gettext('Name'),
key: 'full',
minWidth: 300,
ellipsis: {
tooltip: true
}
},
{
title: $gettext('Size'),
key: 'size',
width: 80,
render(row: any): any {
return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size })
}
},
{
title: $gettext('Modification Time'),
key: 'modify',
width: 200,
render(row: any): any {
return h(
NTag,
{ type: 'warning', size: 'small', bordered: false },
{ default: () => row.modify }
)
}
},
{
title: $gettext('Actions'),
key: 'action',
width: 200,
render(row) {
return h(
NSpace,
{},
{
default: () => [
h(
NButton,
{
size: 'small',
type: 'success',
tertiary: true,
onClick: () => {
copy2clipboard(row.full).then(() => {
window.$message.success($gettext('Copied successfully'))
})
}
},
{
default: () => {
return $gettext('Copy Path')
}
}
),
h(
NPopconfirm,
{
onPositiveClick: () => {
useRequest(file.delete(row.full)).onSuccess(() => {
window.$bus.emit('file:refresh')
window.$message.success($gettext('Deleted successfully'))
})
},
onNegativeClick: () => {}
},
{
default: () => {
return $gettext('Are you sure you want to delete %{ name }?', { name: row.name })
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error',
tertiary: true
},
{ default: () => $gettext('Delete') }
)
}
}
)
]
}
)
}
}
]
const data = ref<RowData[]>([])
const pagination = reactive({
page: 1,
pageCount: 1,
pageSize: 100,
itemCount: 0,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [100, 200, 500, 1000, 1500, 2000, 5000]
})
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
handlePageChange(1)
}
const handlePageChange = (page: number) => {
search(page)
}
const search = async (page: number) => {
loading.value = true
useRequest(
file.search(path.value, keyword.value, sub.value, page, pagination.pageSize!)
).onSuccess(({ data }) => {
data.value = data.items
pagination.itemCount = data.total
pagination.pageCount = data.total / pagination.pageSize! + 1
})
loading.value = false
}
watch(show, (value) => {
if (value) {
search(1)
}
})
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
:title="$gettext('%{ keyword } - Search Results', { keyword })"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-data-table
remote
striped
virtual-scroll
size="small"
:scroll-x="800"
:columns="columns"
:data="data"
:loading="loading"
:pagination="pagination"
:row-key="(row: any) => row.full"
max-height="60vh"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</n-modal>
</template>
<style scoped lang="scss"></style>