mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
feat: 文件搜索
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type FileList struct {
|
||||
Path string `json:"path" form:"path" validate:"required"`
|
||||
Sort string `json:"sort" form:"sort"`
|
||||
@@ -50,5 +56,11 @@ type FileUnCompress struct {
|
||||
|
||||
type FileSearch struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
KeyWord string `form:"keyword" json:"keyword" validate:"required"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func Http(r chi.Router) {
|
||||
r.Post("/permission", file.Permission)
|
||||
r.Post("/compress", file.Compress)
|
||||
r.Post("/unCompress", file.UnCompress)
|
||||
r.Post("/search", file.Search)
|
||||
r.Get("/search", file.Search)
|
||||
r.Get("/list", file.List)
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
@@ -327,22 +328,18 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
paths := make(map[string]stdos.FileInfo)
|
||||
err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(info.Name(), req.KeyWord) {
|
||||
paths[path] = info
|
||||
}
|
||||
return nil
|
||||
})
|
||||
results, err := io.SearchX(req.Path, req.Keyword, req.Sub)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, paths)
|
||||
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) {
|
||||
@@ -378,14 +375,27 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
var paths []any
|
||||
for _, file := range list {
|
||||
info, _ := file.Info()
|
||||
stat := info.Sys().(*syscall.Stat_t)
|
||||
paged, total := Paginate(r, s.formatDir(req.Path, list))
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// formatDir 格式化目录信息
|
||||
func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
|
||||
var paths []any
|
||||
for _, file := range entries {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stat := info.Sys().(*syscall.Stat_t)
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": filepath.Join(req.Path, info.Name()),
|
||||
"full": filepath.Join(base, info.Name()),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
@@ -395,21 +405,52 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
"gid": stat.Gid,
|
||||
"hidden": io.IsHidden(info.Name()),
|
||||
"symlink": io.IsSymlink(info.Mode()),
|
||||
"link": io.GetSymlink(filepath.Join(req.Path, info.Name())),
|
||||
"link": io.GetSymlink(filepath.Join(base, info.Name())),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, paths)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
return paths
|
||||
}
|
||||
|
||||
// setPermission
|
||||
// formatInfo 格式化文件信息
|
||||
func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any {
|
||||
var paths []map[string]any
|
||||
for path, info := range infos {
|
||||
stat := info.Sys().(*syscall.Stat_t)
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": path,
|
||||
"size": str.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(path),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(paths, func(a, b map[string]any) int {
|
||||
if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) {
|
||||
return -1
|
||||
}
|
||||
if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"])))
|
||||
})
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// setPermission 设置权限
|
||||
func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) {
|
||||
_ = io.Chmod(path, mode)
|
||||
_ = io.Chown(path, owner, group)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
@@ -314,22 +315,18 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
paths := make(map[string]stdos.FileInfo)
|
||||
err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(info.Name(), req.KeyWord) {
|
||||
paths[path] = info
|
||||
}
|
||||
return nil
|
||||
})
|
||||
results, err := io.SearchX(req.Path, req.Keyword, req.Sub)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, paths)
|
||||
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) {
|
||||
@@ -365,13 +362,23 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, s.formatDir(req.Path, list))
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// formatDir 格式化目录信息
|
||||
func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
|
||||
var paths []any
|
||||
for _, file := range list {
|
||||
for _, file := range entries {
|
||||
info, _ := file.Info()
|
||||
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": filepath.Join(req.Path, info.Name()),
|
||||
"full": filepath.Join(base, info.Name()),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
@@ -381,18 +388,48 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
"gid": 0,
|
||||
"hidden": io.IsHidden(info.Name()),
|
||||
"symlink": io.IsSymlink(info.Mode()),
|
||||
"link": io.GetSymlink(filepath.Join(req.Path, info.Name())),
|
||||
"link": io.GetSymlink(filepath.Join(base, info.Name())),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
// formatInfo 格式化文件信息
|
||||
func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any {
|
||||
var paths []map[string]any
|
||||
for path, info := range infos {
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": path,
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": "",
|
||||
"group": "",
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"hidden": io.IsHidden(info.Name()),
|
||||
"symlink": io.IsSymlink(info.Mode()),
|
||||
"link": io.GetSymlink(path),
|
||||
"dir": info.IsDir(),
|
||||
"modify": info.ModTime().Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(paths, func(a, b map[string]any) int {
|
||||
if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) {
|
||||
return -1
|
||||
}
|
||||
if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"])))
|
||||
})
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// setPermission
|
||||
|
||||
@@ -211,6 +211,11 @@ func GetSymlink(path string) string {
|
||||
return linkPath
|
||||
}
|
||||
|
||||
// TempFile 创建临时文件
|
||||
func TempFile(dir, prefix string) (*os.File, error) {
|
||||
return os.CreateTemp(dir, prefix)
|
||||
}
|
||||
|
||||
func getFormat(f FormatArchive) archiver.CompressedArchive {
|
||||
format := archiver.CompressedArchive{}
|
||||
switch f {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -146,11 +147,6 @@ func TempDir(prefix string) (string, error) {
|
||||
return os.MkdirTemp("", prefix)
|
||||
}
|
||||
|
||||
// TempFile 创建临时文件
|
||||
func TempFile(dir, prefix string) (*os.File, error) {
|
||||
return os.CreateTemp(dir, prefix)
|
||||
}
|
||||
|
||||
// ReadDir 读取目录
|
||||
func ReadDir(path string) ([]os.DirEntry, error) {
|
||||
return os.ReadDir(path)
|
||||
@@ -190,3 +186,57 @@ func CountX(path string) (int64, error) {
|
||||
count := len(string(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 cmd *exec.Cmd
|
||||
if sub {
|
||||
cmd = exec.Command("find", path, "-name", "*"+keyword+"*")
|
||||
} else {
|
||||
cmd = exec.Command("find", path, "-maxdepth", "1", "-name", "*"+keyword+"*")
|
||||
}
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\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
|
||||
}
|
||||
|
||||
@@ -50,8 +50,14 @@ export default {
|
||||
unCompress: (file: string, path: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/file/unCompress', { file, path }),
|
||||
// 搜索文件
|
||||
search: (keyword: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/file/search', { keyword }),
|
||||
search: (
|
||||
path: string,
|
||||
keyword: string,
|
||||
sub: boolean,
|
||||
page: number,
|
||||
limit: number
|
||||
): Promise<AxiosResponse<any>> =>
|
||||
request.get('/file/search', { params: { path, keyword, sub, page, limit } }),
|
||||
// 获取文件列表
|
||||
list: (path: string, page: number, limit: number, sort: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/file/list', { params: { path, page, limit, sort } })
|
||||
|
||||
@@ -71,13 +71,8 @@ const columns: DataTableColumns<RowData> = [
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
minWidth: 180,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
defaultSortOrder: false,
|
||||
sorter(row1, row2) {
|
||||
return row1.name - row2.name
|
||||
},
|
||||
sorter: 'default',
|
||||
render(row) {
|
||||
let icon = 'bi:file-earmark'
|
||||
if (row.dir) {
|
||||
@@ -388,7 +383,6 @@ const handleRename = () => {
|
||||
const target = path.value + '/' + renameModel.value.target
|
||||
if (!checkName(renameModel.value.source) || !checkName(renameModel.value.target)) {
|
||||
window.$message.error('名称不合法')
|
||||
console.log(source, target)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -500,8 +494,6 @@ const handleSorterChange = (sorter: {
|
||||
}) => {
|
||||
if (!sorter || sorter.columnKey === 'name') {
|
||||
if (!loading.value) {
|
||||
console.log(sorter)
|
||||
console.log(sorter.order)
|
||||
switch (sorter.order) {
|
||||
case 'ascend':
|
||||
sort.value = 'asc'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { onUnmounted } from 'vue'
|
||||
|
||||
import EventBus from '@/utils/event'
|
||||
import { checkPath } from '@/utils/file'
|
||||
import SearchModal from '@/views/file/SearchModal.vue'
|
||||
|
||||
const path = defineModel<string>('path', { type: String, required: true })
|
||||
const isInput = ref(false)
|
||||
@@ -13,6 +14,12 @@ const input = ref('www')
|
||||
const history: string[] = []
|
||||
let current = -1
|
||||
|
||||
const searchModal = ref(false)
|
||||
const search = ref({
|
||||
keyword: '',
|
||||
sub: false
|
||||
})
|
||||
|
||||
const handleInput = () => {
|
||||
isInput.value = true
|
||||
nextTick(() => {
|
||||
@@ -84,7 +91,7 @@ const handlePushHistory = (path: string) => {
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
window.$message.info('搜索功能暂未实现')
|
||||
searchModal.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -141,9 +148,9 @@ onUnmounted(() => {
|
||||
/>
|
||||
</n-input-group>
|
||||
<n-input-group w-400>
|
||||
<n-input placeholder="请输入搜索内容">
|
||||
<n-input v-model:value="search.keyword" placeholder="请输入搜索内容">
|
||||
<template #suffix>
|
||||
<n-checkbox> 包含子目录 </n-checkbox>
|
||||
<n-checkbox v-model:checked="search.sub"> 包含子目录 </n-checkbox>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
@@ -151,6 +158,12 @@ onUnmounted(() => {
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<search-modal
|
||||
v-model:show="searchModal"
|
||||
v-model:path="path"
|
||||
v-model:keyword="search.keyword"
|
||||
v-model:sub="search.sub"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
178
web/src/views/file/SearchModal.vue
Normal file
178
web/src/views/file/SearchModal.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import file from '@/api/panel/file'
|
||||
import EventBus from '@/utils/event'
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import type { RowData } from 'naive-ui/es/data-table/src/interface'
|
||||
|
||||
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: '名称',
|
||||
key: 'full',
|
||||
minWidth: 300,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
key: 'size',
|
||||
width: 80,
|
||||
render(row: any): any {
|
||||
return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
key: 'modify',
|
||||
width: 200,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: 'warning', size: 'small', bordered: false },
|
||||
{ default: () => row.modify }
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render(row) {
|
||||
return h(
|
||||
NSpace,
|
||||
{},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'success',
|
||||
tertiary: true,
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(row.full)
|
||||
window.$message.success('复制成功')
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '复制路径'
|
||||
}
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => {
|
||||
file.delete(row.full).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
EventBus.emit('file:refresh')
|
||||
})
|
||||
},
|
||||
onNegativeClick: () => {}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定删除吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
tertiary: true
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
await file
|
||||
.search(path.value, keyword.value, sub.value, page, pagination.pageSize!)
|
||||
.then((res) => {
|
||||
data.value = res.data.items
|
||||
pagination.itemCount = res.data.total
|
||||
pagination.pageCount = res.data.total / pagination.pageSize! + 1
|
||||
})
|
||||
.catch(() => {
|
||||
window.$message.error('搜索失败')
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
search(1)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="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>
|
||||
Reference in New Issue
Block a user