From b85ca6fd70306b1bdc6e2aee92a6cd285328de0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 11 Jan 2026 01:17:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A1=B9=E7=9B=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/data/project.go | 2 +- web/src/components/common/PathSelector.vue | 128 ++++++++---- web/src/views/project/CreateModal.vue | 219 ++++++++++++++++++++- web/src/views/setting/SettingBase.vue | 46 ++++- web/src/views/website/CreateModal.vue | 47 ++++- 5 files changed, 391 insertions(+), 51 deletions(-) diff --git a/internal/data/project.go b/internal/data/project.go index 8ed40055..837f72da 100644 --- a/internal/data/project.go +++ b/internal/data/project.go @@ -37,7 +37,7 @@ func (r *projectRepo) List(typ types.ProjectType, page, limit uint) ([]*types.Pr var total int64 query := r.db.Model(&biz.Project{}) - if typ != "" { + if typ != "" && typ != "all" { query = query.Where("type = ?", typ) } diff --git a/web/src/components/common/PathSelector.vue b/web/src/components/common/PathSelector.vue index 5c07fa55..c27ebcde 100644 --- a/web/src/components/common/PathSelector.vue +++ b/web/src/components/common/PathSelector.vue @@ -3,11 +3,12 @@ import file from '@/api/panel/file' import TheIcon from '@/components/custom/TheIcon.vue' import { checkName, checkPath, getExt, getIconByExt } from '@/utils' import type { DataTableColumns, InputInst } from 'naive-ui' -import { NButton, NDataTable, NEllipsis, NFlex, NTag } from 'naive-ui' +import { NButton, NDataTable, NEllipsis, NFlex, NSpin, NTag, useThemeVars } from 'naive-ui' import type { RowData } from 'naive-ui/es/data-table/src/interface' import { useGettext } from 'vue3-gettext' const { $gettext } = useGettext() +const themeVars = useThemeVars() const show = defineModel('show', { type: Boolean, required: true }) const path = defineModel('path', { type: String, required: true }) const props = defineProps({ @@ -17,12 +18,18 @@ const props = defineProps({ } }) +const currentPath = ref('/') + +// 目录大小计算状态 +const sizeLoading = ref>(new Map()) +const sizeCache = ref>(new Map()) + const title = computed(() => (props.dir ? $gettext('Select Directory') : $gettext('Select File'))) const isInput = ref(false) const pathInput = ref(null) const input = ref('www') const sort = ref('') -const selected = defineModel('selected', { type: Array, default: () => [] }) +const selected = ref([]) const create = ref(false) const createModel = ref({ dir: false, @@ -58,9 +65,7 @@ const columns: DataTableColumns = [ class: 'cursor-pointer hover:opacity-60', onClick: () => { if (row.dir) { - path.value = row.full - } else { - selected.value = [row.full] + currentPath.value = row.full } } }, @@ -106,9 +111,41 @@ const columns: DataTableColumns = [ { title: $gettext('Size'), key: 'size', - minWidth: 80, + minWidth: 100, render(row: any): any { - return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size }) + // 文件 + if (!row.dir) { + return h( + NTag, + { type: 'info', size: 'small', bordered: false }, + { default: () => row.size } + ) + } + // 目录 + const cachedSize = sizeCache.value.get(row.full) + if (cachedSize) { + return h( + NTag, + { type: 'info', size: 'small', bordered: false }, + { default: () => cachedSize } + ) + } + const isLoading = sizeLoading.value.get(row.full) + if (isLoading) { + return h(NSpin, { size: 16, style: { paddingTop: '4px' } }) + } + return h( + 'span', + { + style: { cursor: 'pointer', fontSize: '14px', color: themeVars.value.primaryColor }, + onClick: (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() + calculateDirSize(row.full) + } + }, + $gettext('Calculate') + ) } }, { @@ -125,9 +162,9 @@ const columns: DataTableColumns = [ } ] -const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination( +const { loading, data, page, total, pageSize, pageCount, reload } = usePagination( (page, pageSize) => - file.list(encodeURIComponent(path.value), '', false, sort.value, page, pageSize), + file.list(encodeURIComponent(currentPath.value), '', false, sort.value, page, pageSize), { initialData: { total: 0, list: [] }, initialPageSize: 100, @@ -151,11 +188,11 @@ const handleBlur = () => { } isInput.value = false - path.value = '/' + input.value + currentPath.value = '/' + input.value } const handleUp = () => { - const count = splitPath(path.value, '/').length + const count = splitPath(currentPath.value, '/').length setPath(count - 2) } @@ -167,10 +204,10 @@ const splitPath = (str: string, delimiter: string) => { } const setPath = (index: number) => { - const newPath = splitPath(path.value, '/') + const newPath = splitPath(currentPath.value, '/') .slice(0, index + 1) .join('/') - path.value = '/' + newPath + currentPath.value = '/' + newPath input.value = newPath } @@ -183,15 +220,15 @@ const handleSorterChange = (sorter: { switch (sorter.order) { case 'ascend': sort.value = 'asc' - refresh() + reload() break case 'descend': sort.value = 'desc' - refresh() + reload() break default: sort.value = '' - refresh() + reload() break } } @@ -210,32 +247,51 @@ const handleCreate = () => { return } - const fullPath = path.value + '/' + createModel.value.path + const fullPath = currentPath.value + '/' + createModel.value.path useRequest(file.create(fullPath, createModel.value.dir)).onSuccess(() => { create.value = false - refresh() + reload() window.$message.success($gettext('Created successfully')) }) } -const closeWatch = watch( - path, - (value) => { - input.value = value.slice(1) - selected.value = [] - refresh() - }, - { immediate: true } -) +// 计算目录大小 +const calculateDirSize = (dirPath: string) => { + sizeLoading.value.set(dirPath, true) + useRequest(file.size(dirPath)) + .onSuccess(({ data }) => { + sizeCache.value.set(dirPath, data) + }) + .onComplete(() => { + sizeLoading.value.set(dirPath, false) + }) +} -const handleClose = () => { - closeWatch() - if (selected.value.length) { +// 打开选择器时,用外部path初始化内部currentPath +watch(show, (val) => { + if (val) { + currentPath.value = path.value || '/' + } +}) + +// 监听内部路径变化,刷新列表 +watch(currentPath, (value) => { + if (!value) return + input.value = value.slice(1) + selected.value = [] + sizeCache.value.clear() + sizeLoading.value.clear() + reload() +}) + +// 选择后更新外部path并关闭 +watch(selected, (val) => { + if (val.length > 0) { path.value = selected.value[0] selected.value = [] + show.value = false } - show.value = false -} +})