Compare commits

..
Author SHA1 Message Date
guoyuqi 7e95b95f50 feat: add overview more quick menu 2026-10-20 09:41:21 +08:00
30 changed files with 678 additions and 136 deletions
+84
View File
@@ -0,0 +1,84 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags Dashboard
// @Summary List quick directories
// @Success 200 {array} model.QuickDirectory
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [get]
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
items, err := dashboardService.ListQuickDirectories()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, items)
}
// @Tags Dashboard
// @Summary Create quick directory
// @Accept json
// @Param request body dto.QuickDirectoryCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [post]
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.CreateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Update quick directory
// @Accept json
// @Param request body dto.QuickDirectoryUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/update [post]
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.UpdateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Delete quick directory
// @Accept json
// @Param request body dto.QuickDirectoryDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/del [post]
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+21 -1
View File
@@ -31,7 +31,27 @@ type DashboardBase struct {
}
type ChangeQuicks struct {
Quicks []QuickJump `json:"quicks"`
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
}
type QuickVisibility struct {
ID uint `json:"id" validate:"required"`
IsShow bool `json:"isShow"`
}
type QuickDirectoryCreate struct {
Path string `json:"path" validate:"required"`
Alias string `json:"alias"`
}
type QuickDirectoryDelete struct {
ID uint `json:"id" validate:"required"`
Legacy bool `json:"legacy"`
}
type QuickDirectoryUpdate struct {
QuickDirectoryDelete
QuickDirectoryCreate
}
type QuickJump struct {
+7
View File
@@ -5,6 +5,13 @@ type AppLauncher struct {
Key string `json:"key"`
}
type QuickDirectory struct {
BaseModel
Path string `json:"path"`
Alias string `json:"alias"`
Legacy bool `json:"legacy" gorm:"-"`
}
type QuickJump struct {
BaseModel
Name string `json:"name"`
+69 -3
View File
@@ -3,6 +3,7 @@ package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type LauncherRepo struct{}
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
ListQuickJump(withAll bool) []model.QuickJump
UpdateQuicks(quicks []model.QuickJump) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CountQuickDirectories() (int64, error)
CreateQuickDirectory(item *model.QuickDirectory) error
UpdateQuickDirectory(item *model.QuickDirectory) error
DeleteQuickDirectory(id uint, legacy bool) error
}
func NewILauncherRepo() ILauncherRepo {
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
for _, item := range quicks {
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"is_show": item.IsShow,
"detail": item.Detail,
"alias": item.Alias,
}).Error; err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
return tx.Commit().Error
}
// Legacy entries remain in the overview configuration until explicitly deleted.
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
}
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
items := make([]model.QuickDirectory, 0)
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
return nil, err
}
var quicks []model.QuickJump
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
return nil, err
}
for _, quick := range quicks {
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
}
return items, nil
}
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
var count, legacyCount int64
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
return 0, err
}
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
return 0, err
}
return count + legacyCount, nil
}
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
return global.DB.Create(item).Error
}
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
var result *gorm.DB
if item.Legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
} else {
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
var result *gorm.DB
if legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
} else {
result = global.DB.Delete(&model.QuickDirectory{}, id)
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
+16 -6
View File
@@ -43,6 +43,10 @@ type IDashboardService interface {
LoadQuickOptions() []dto.QuickJump
ChangeQuick(req dto.ChangeQuicks) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
ChangeShow(req dto.SettingUpdate) error
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
}
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
showCount := 0
var quicks []model.QuickJump
quicks := make([]model.QuickJump, 0, len(req.Quicks))
for _, item := range req.Quicks {
var quick model.QuickJump
if item.IsShow {
showCount++
}
if err := copier.Copy(&quick, &item); err != nil {
return err
}
quicks = append(quicks, quick)
quicks = append(quicks, model.QuickJump{
BaseModel: model.BaseModel{ID: item.ID},
IsShow: item.IsShow,
})
}
if showCount == 0 {
return buserr.New("ErrMinQuickJump")
@@ -706,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
switch quicks[i].Name {
case "File":
count, err := launcherRepo.CountQuickDirectories()
if err != nil {
global.LOG.Errorf("load quick directory count: %v", err)
}
quicks[i].Detail = fmt.Sprintf("%d", count)
quicks[i].Alias = ""
case "Agent":
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
case "Website":
+49
View File
@@ -0,0 +1,49 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
return launcherRepo.ListQuickDirectories()
}
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
if strings.TrimSpace(item.Path) == "" {
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
}
return item, nil
}
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req)
if err != nil {
return nil, err
}
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
if err != nil {
return nil, err
}
item.ID = req.ID
item.Legacy = req.Legacy
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
}
+1
View File
@@ -115,6 +115,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval,
migrations.AddQuickDirectory,
}
}
+7
View File
@@ -1888,3 +1888,10 @@ var AddVLLMSampleInterval = &gormigrate.Migration{
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddQuickDirectory = &gormigrate.Migration{
ID: "20261016-add-quick-directory",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.QuickDirectory{})
},
}
+4
View File
@@ -14,6 +14,10 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
cmdRouter.GET("/quick/option", baseApi.LoadQuickOption)
cmdRouter.POST("/quick/change", baseApi.UpdateQuickJump)
cmdRouter.GET("/quick/directory", baseApi.ListQuickDirectories)
cmdRouter.POST("/quick/directory", baseApi.CreateQuickDirectory)
cmdRouter.POST("/quick/directory/update", baseApi.UpdateQuickDirectory)
cmdRouter.POST("/quick/directory/del", baseApi.DeleteQuickDirectory)
cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
+6
View File
@@ -8,6 +8,12 @@ export namespace Dashboard {
diskSize: number;
}
export interface QuickDirectory {
id: number;
legacy?: boolean;
path: string;
alias: string;
}
export interface QuickJump {
id: number;
name: string;
+2 -4
View File
@@ -1,5 +1,4 @@
import http from '@/api';
import type { AxiosRequestConfig } from 'axios';
import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum';
@@ -24,11 +23,10 @@ export const loadContainerUsers = (name: string) => {
export const listContainerFiles = (params: Container.ContainerFileReq) => {
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
};
export const uploadContainerFile = (params: FormData, config?: AxiosRequestConfig & { skipErrorMessage?: boolean }) => {
export const uploadContainerFile = (params: FormData) => {
return http.upload(`/containers/files/upload`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: TimeoutEnum.T_5M,
...config,
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
});
};
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
+9 -1
View File
@@ -8,7 +8,7 @@ export const loadOsInfo = () => {
export const loadQuickOption = () => {
return http.get<Array<Dashboard.QuickJump>>(`/dashboard/quick/option`);
};
export const changeQuick = (quicks: Array<Dashboard.QuickJump>) => {
export const changeQuick = (quicks: Array<Pick<Dashboard.QuickJump, 'id' | 'isShow'>>) => {
return http.post(`/dashboard/quick/change`, { quicks: quicks });
};
export const loadAppLauncher = () => {
@@ -48,3 +48,11 @@ export const loadTopMem = () => {
export const systemRestart = (operation: string) => {
return http.post(`/dashboard/system/restart/${operation}`);
};
export const loadQuickDirectories = () => http.get<Dashboard.QuickDirectory[]>('/dashboard/quick/directory');
export const createQuickDirectory = (data: Omit<Dashboard.QuickDirectory, 'id'>) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory', data);
export const updateQuickDirectory = (data: Dashboard.QuickDirectory) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory/update', data);
export const deleteQuickDirectory = (id: number, legacy = false) =>
http.post('/dashboard/quick/directory/del', { id, legacy });
+2 -1
View File
@@ -196,7 +196,7 @@ const favorites = ref<File.Favorite[]>([]);
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths);
const oldUrl = ref<string>('');
const em = defineEmits(['choose']);
const em = defineEmits(['choose', 'close']);
const form = reactive({
path: '/',
@@ -245,6 +245,7 @@ const handleClose = () => {
open.value = false;
selectRow.value = { path: '', name: '' };
selectRows.value = [];
em('close');
};
const openPage = () => {
+1
View File
@@ -494,6 +494,7 @@ const message = {
recommend: 'Recommended',
dir: 'Directory',
alias: 'Alias',
quickDirDelete: 'Delete ({0})?',
quickDir: 'Quick Access',
minQuickJump: 'Add at least one quick access entry',
maxQuickJump: 'You can add up to four quick access entries',
+1
View File
@@ -493,6 +493,7 @@ const message = {
home: {
recommend: 'Recomendado',
dir: 'Directorio',
quickDirDelete: '¿Confirmar la eliminación de ({0})?',
quickDir: 'Acceso rápido',
minQuickJump: 'Agrega al menos una entrada de acceso rápido',
maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido',
+1
View File
@@ -489,6 +489,7 @@ const message = {
recommend: 'پیشنهادی',
dir: 'دایرکتوری',
alias: 'نام مستعار',
quickDirDelete: 'حذف ({0}) را تأیید می‌کنید؟',
quickDir: 'دسترسی سریع',
minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید',
maxQuickJump: 'می‌توانید حداکثر چهار ورودی دسترسی سریع اضافه کنید',
+1
View File
@@ -492,6 +492,7 @@ const message = {
recommend: 'おすすめ',
dir: 'ディレクトリ',
alias: 'エイリアス',
quickDirDelete: '({0})を削除しますか?',
quickDir: 'クイックアクセス',
minQuickJump: 'クイックアクセス項目を1つ以上追加してください',
maxQuickJump: '最大4件のクイックアクセス項目を追加できます',
+1
View File
@@ -487,6 +487,7 @@ const message = {
recommend: '추천',
dir: '디렉토리',
alias: '별칭',
quickDirDelete: '({0})을(를) 삭제하시겠습니까?',
quickDir: '빠른 액세스',
minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요',
maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다',
+1
View File
@@ -487,6 +487,7 @@ const message = {
recommend: 'ແນະນຳ',
dir: 'ໄດເຣັກທໍຣີ',
alias: 'ຊື່ຫຼິ້ນ',
quickDirDelete: 'ຢືນຢັນການລຶບ ({0}) ຫຼືບໍ່?',
quickDir: 'ເຂົ້າເຖິງດ່ວນ',
minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ',
+1
View File
@@ -499,6 +499,7 @@ const message = {
recommend: 'Disyorkan',
dir: 'Direktori',
alias: 'Alias',
quickDirDelete: 'Sahkan pemadaman ({0})?',
quickDir: 'Akses Pantas',
minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas',
maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas',
+1
View File
@@ -492,6 +492,7 @@ const message = {
recommend: 'Recomendado',
dir: 'Diretório',
alias: 'Apelido',
quickDirDelete: 'Confirmar a exclusão de ({0})?',
quickDir: 'Acesso rápido',
minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido',
maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido',
+1
View File
@@ -491,6 +491,7 @@ const message = {
recommend: 'Рекомендуемое',
dir: 'Каталог',
alias: 'Псевдоним',
quickDirDelete: 'Подтвердить удаление ({0})?',
quickDir: 'Быстрый доступ',
minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа',
maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа',
+1
View File
@@ -495,6 +495,7 @@ const message = {
recommend: 'Önerilen',
dir: 'Dizin',
alias: 'Takma Ad',
quickDirDelete: '({0}) silinsin mi?',
quickDir: 'Hızlı Erişim',
minQuickJump: 'En az bir hızlı erişim girdisi ekleyin',
maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz',
+1
View File
@@ -473,6 +473,7 @@ const message = {
recommend: '推薦',
dir: '目錄',
alias: '別名',
quickDirDelete: '確定刪除({0})',
quickDir: '快捷目錄',
minQuickJump: '請至少設定一個快速跳轉入口',
maxQuickJump: '最多可設定四個快速跳轉入口',
+1
View File
@@ -473,6 +473,7 @@ const message = {
recommend: '推荐',
dir: '目录',
alias: '别名',
quickDirDelete: '确定删除({0})',
quickDir: '快捷目录',
minQuickJump: '请至少设置一个快速跳转入口!',
maxQuickJump: '最多可设置四个快速跳转入口!',
@@ -32,9 +32,11 @@
@blur="cancelPathEditing"
@keyup.enter.prevent="submitPathInput"
/>
<el-button class="mt-2" type="primary" plain @click="onUpload">
{{ $t('commons.button.upload') }}
</el-button>
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
<el-button class="mt-2" :loading="uploading" type="primary" plain>
{{ $t('commons.button.upload') }}
</el-button>
</el-upload>
<el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete">
{{ $t('commons.button.delete') }}
</el-button>
@@ -95,7 +97,6 @@
</el-form>
</template>
</DrawerPro>
<FileUpload ref="uploadRef" @close="loadContainerFiles" />
<DialogPro v-model="previewVisible" :title="previewTitle" size="w-70" :close-on-click-modal="true">
<el-alert
v-if="previewTruncated"
@@ -117,11 +118,11 @@ import {
getContainerFileContent,
getContainerFileSize,
listContainerFiles,
uploadContainerFile,
} from '@/api/modules/container';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import i18n from '@/lang';
import { ElInput, ElMessageBox } from 'element-plus';
import FileUpload from '@/views/host/file-management/upload/index.vue';
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
import { computeSize2 } from '@/utils/size';
const visible = ref(false);
@@ -129,8 +130,9 @@ const title = ref('');
const containerID = ref('');
const filePath = ref('/');
const containerFiles = ref<any[]>([]);
const uploadRef = ref<InstanceType<typeof FileUpload>>();
const uploadRef = ref<UploadInstance>();
const pathInputRef = ref<InstanceType<typeof ElInput>>();
const uploading = ref(false);
const previewVisible = ref(false);
const previewTitle = ref('');
const previewContent = ref('');
@@ -251,8 +253,27 @@ const submitPathInput = async () => {
}
};
const onUpload = () => {
uploadRef.value?.acceptParams({ path: filePath.value, containerID: containerID.value });
const onUploadChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) {
return;
}
const formData = new FormData();
formData.append('containerID', containerID.value);
formData.append('path', filePath.value);
formData.append('file', uploadFile.raw);
uploading.value = true;
await uploadContainerFile(formData)
.then(async () => {
MsgSuccess(i18n.global.t('file.uploadSuccess'));
await loadContainerFiles();
})
.catch((err) => {
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
})
.finally(() => {
uploading.value = false;
uploadRef.value?.clearFiles();
});
};
const onOpenFile = async (path: string) => {
+13 -18
View File
@@ -50,20 +50,12 @@
<el-col :span="6" v-for="item in baseInfo.quickJump" :key="item.name">
<span>{{ $t(item.title, 2) }}</span>
<div class="count">
<el-tooltip
v-if="item.alias || item.detail.length > 20"
:content="item.detail"
placement="bottom"
>
<el-button
link
:disabled="!checkPermission('File')"
type="primary"
@click="quickJump(item)"
>
{{ item.alias || item.detail.substring(0, 18) + '...' }}
</el-button>
</el-tooltip>
<QuickDirectories
v-if="item.name === 'File'"
:count="item.detail"
:disabled="!checkPermission('File')"
@change="updateQuickDirectoryCount"
/>
<el-button
link
:disabled="!checkPermission(item.name)"
@@ -450,6 +442,7 @@ import SystemStatus from '@/views/home/status/index.vue';
import AppLauncher from '@/views/home/app/index.vue';
import VCharts from '@/components/v-charts/index.vue';
import QuickJump from '@/views/home/quick/index.vue';
import QuickDirectories from '@/views/home/quick/directories.vue';
import CardWithHeader from '@/components/card-with-header/index.vue';
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
import i18n from '@/lang';
@@ -470,7 +463,7 @@ import {
updateMemo,
updateSetting,
} from '@/api/modules/setting';
import { routerToFileWithPath, routerToNameWithQuery, routerToPath } from '@/utils/router';
import { routerToNameWithQuery, routerToPath } from '@/utils/router';
import { getWelcomePage } from '@/api/modules/auth';
import {
clearDashboardCache,
@@ -823,10 +816,12 @@ const onLoadBaseInfo = async (isInit: boolean, range: string) => {
}
};
const updateQuickDirectoryCount = (count: number) => {
const item = baseInfo.value.quickJump.find((item) => item.name === 'File');
if (item) item.detail = String(count);
};
const quickJump = (item: any) => {
if (item.name === 'File') {
return routerToFileWithPath(item.detail);
}
return routerToPath(item.router);
};
@@ -0,0 +1,315 @@
<template>
<el-popover
trigger="click"
placement="bottom"
:width="680"
:popper-style="{ maxWidth: 'calc(100vw - 32px)' }"
:visible="visible"
@update:visible="setVisible"
:disabled="disabled"
:title="$t('home.quickDir')"
@show="load"
>
<template #reference>
<el-button link type="primary" :disabled="disabled">{{ count }}</el-button>
</template>
<div v-loading="loading" class="quick-directories">
<div class="directory-search">
<el-input
v-model="searchInput"
clearable
prefix-icon="Search"
:placeholder="`${$t('file.path')} / ${$t('home.alias')}`"
:aria-label="$t('commons.button.search')"
@keyup.enter="search"
@clear="search"
/>
<el-button :disabled="loading" @click="search">{{ $t('commons.button.confirm') }}</el-button>
</div>
<div class="directory-row directory-header">
<span>{{ $t('file.path') }}</span>
<span>{{ $t('home.alias') }}</span>
<span />
</div>
<div class="directory-list">
<div v-if="!filteredRows.length" class="directory-empty">{{ $t('commons.msg.noneData') }}</div>
<div v-for="row in filteredRows" :key="row.key" class="directory-row">
<el-input
v-model="row.path"
:readonly="!row.editing"
:disabled="row.busy"
:aria-label="$t('file.path')"
>
<template #prepend>
<el-button
icon="Folder"
:disabled="!row.editing || row.busy"
:aria-label="$t('file.list')"
@click="selectPath(row)"
/>
</template>
</el-input>
<el-input
v-model="row.alias"
:readonly="!row.editing"
:disabled="row.busy"
:aria-label="$t('home.alias')"
/>
<div class="directory-actions">
<el-button
link
:type="row.editing ? 'success' : 'primary'"
:icon="row.editing ? 'Check' : 'Edit'"
:aria-label="$t(row.editing ? 'commons.button.save' : 'commons.button.edit')"
:disabled="loading || row.busy || (row.editing && !row.path.trim())"
@click="row.editing ? save(row) : (row.editing = true)"
/>
<el-button
v-if="row.id || rows.length > 1"
link
:type="row.editing ? 'info' : 'danger'"
:icon="row.editing ? 'Close' : 'Delete'"
:aria-label="$t('commons.button.delete')"
:disabled="loading || row.busy"
@click="remove(row)"
/>
<el-button
v-if="row.id"
link
type="primary"
icon="Position"
:title="$t('firewall.quickJump')"
:aria-label="$t('firewall.quickJump')"
:disabled="loading || row.busy"
@click="routerToFileWithPath(row.savedPath)"
/>
</div>
</div>
</div>
<el-button
class="directory-add"
link
type="primary"
icon="Plus"
:disabled="loading || !loaded"
@click="add"
>
{{ $t('commons.button.add') }}
</el-button>
</div>
<div class="directory-footer">
<el-button @click="close">{{ $t('commons.button.close') }}</el-button>
</div>
</el-popover>
<FileList ref="fileRef" @choose="loadPath" @close="selectingRow = undefined" />
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { routerToFileWithPath } from '@/utils/router';
import FileList from '@/components/file-list/index.vue';
import { Dashboard } from '@/api/interface/dashboard';
import {
loadQuickDirectories,
createQuickDirectory,
updateQuickDirectory,
deleteQuickDirectory,
} from '@/api/modules/dashboard';
defineProps<{ count: string; disabled: boolean }>();
const emit = defineEmits<{ (e: 'change', count: number): void }>();
type DirectoryRow = Dashboard.QuickDirectory & {
key: number;
savedPath: string;
editing: boolean;
busy: boolean;
};
const rows = ref<DirectoryRow[]>([]);
const searchInput = ref('');
const searchTerm = ref('');
const filteredRows = computed(() =>
rows.value.filter(
(row) =>
!row.id ||
row.editing ||
row.path.toLowerCase().includes(searchTerm.value) ||
(row.alias ?? '').toLowerCase().includes(searchTerm.value),
),
);
const search = () => {
searchTerm.value = searchInput.value.trim().toLowerCase();
};
const fileRef = ref<InstanceType<typeof FileList>>();
const selectingRow = ref<DirectoryRow>();
const visible = ref(false);
const loading = ref(false);
const loaded = ref(false);
let nextKey = 0;
const setVisible = (value: boolean) => {
if (value) visible.value = true;
};
const close = () => {
visible.value = false;
};
const selectPath = (row: DirectoryRow) => {
if (!row.editing || row.busy) return;
selectingRow.value = row;
fileRef.value?.acceptParams({ path: row.path, dir: false, isAll: true });
};
const loadPath = (path: string) => {
const row = selectingRow.value;
if (row?.editing && !row.busy) row.path = path;
};
const add = () => {
rows.value.push({
key: nextKey++,
id: 0,
path: '',
alias: '',
savedPath: '',
editing: true,
busy: false,
});
};
const updateCount = () => emit('change', rows.value.filter((row) => row.id).length);
const load = async () => {
if (loading.value || rows.value.some((row) => row.busy)) return;
loading.value = true;
loaded.value = false;
searchInput.value = '';
searchTerm.value = '';
rows.value = [];
try {
const { data } = await loadQuickDirectories();
rows.value = data.map((item) => ({
...item,
alias: item.alias ?? '',
key: nextKey++,
savedPath: item.path,
editing: false,
busy: false,
}));
if (!rows.value.length) add();
loaded.value = true;
updateCount();
} finally {
loading.value = false;
}
};
const save = async (row: DirectoryRow) => {
if (row.busy || !row.path.trim()) return;
row.busy = true;
try {
const payload = { path: row.path, alias: (row.alias ?? '').trim() };
const { data } = row.id
? await updateQuickDirectory({ id: row.id, legacy: row.legacy ?? false, ...payload })
: await createQuickDirectory(payload);
Object.assign(row, data, { savedPath: data.path, editing: false });
updateCount();
} finally {
row.busy = false;
}
};
const remove = async (row: DirectoryRow) => {
if (row.busy) return;
if (row.id) {
row.busy = true;
try {
await ElMessageBox.confirm('', i18n.global.t('home.quickDirDelete', [row.savedPath]), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
customClass: 'quick-directory-delete',
type: 'warning',
center: true,
});
} catch {
row.busy = false;
return;
}
try {
await deleteQuickDirectory(row.id, row.legacy ?? false);
} catch (error) {
row.busy = false;
throw error;
}
}
rows.value = rows.value.filter((item) => item.key !== row.key);
if (!rows.value.length) add();
updateCount();
};
</script>
<style lang="scss" scoped>
.quick-directories {
padding: 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
}
.directory-search {
display: flex;
gap: 12px;
padding: 0 10px 10px;
}
.directory-empty {
padding: 20px;
text-align: center;
color: var(--el-text-color-secondary);
}
.directory-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 96px;
gap: 12px;
align-items: center;
padding: 10px;
}
.directory-header {
font-weight: 600;
}
.directory-list {
max-height: 360px;
overflow-y: auto;
}
.directory-actions {
display: flex;
align-items: center;
:deep(.el-button + .el-button) {
margin-left: 8px;
}
}
.directory-footer {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.directory-add {
width: 100%;
margin-top: 8px;
padding: 12px;
}
</style>
<style lang="scss">
.quick-directory-delete {
.el-message-box__title {
justify-content: center;
gap: 10px;
padding: 14px 16px;
border-radius: 4px;
background-color: #59472a;
color: #f5cf87;
overflow-wrap: anywhere;
}
.el-message-box__status {
flex-shrink: 0;
font-size: 24px;
color: #f2b329;
}
.el-message-box__content {
display: none;
}
}
</style>
+1 -32
View File
@@ -10,25 +10,6 @@
<el-table-column prop="isShow" :label="$t('setting.ifShow')">
<template #default="{ row }">
<el-switch v-model="row.isShow" />
<div v-if="row.name === 'File' && row.isShow">
<el-input v-model="row.detail" class="w-full">
<template #prepend>
<el-button
class="w-16"
v-if="row.name === 'File' && row.isShow"
icon="Folder"
@click="fileRef.acceptParams({ path: row.detail, isAll: true })"
/>
</template>
</el-input>
<el-input v-model="row.alias" class="mt-1">
<template #prepend>
<el-button class="w-16">
{{ $t('home.alias') }}
</el-button>
</template>
</el-input>
</div>
</template>
</el-table-column>
</ComplexTable>
@@ -39,14 +20,12 @@
{{ $t('commons.button.confirm') }}
</el-button>
</template>
<FileList ref="fileRef" @choose="loadDir" />
</DrawerPro>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import i18n from '@/lang';
import { MsgInfo, MsgSuccess } from '@/utils/message';
import FileList from '@/components/file-list/index.vue';
import { changeQuick, loadQuickOption } from '@/api/modules/dashboard';
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -57,7 +36,6 @@ const acceptParams = (): void => {
drawerVisible.value = true;
};
const quickOptions = ref([]);
const fileRef = ref();
const search = async () => {
loading.value = true;
@@ -86,26 +64,17 @@ const onChangeShow = async () => {
return;
}
loading.value = true;
await changeQuick(quickOptions.value)
await changeQuick(quickOptions.value.map(({ id, isShow }) => ({ id, isShow })))
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
drawerVisible.value = false;
search();
})
.catch(() => {
loading.value = false;
});
};
const loadDir = async (path: string) => {
for (const item of quickOptions.value) {
if (item.name === 'File') {
item.detail = path;
}
}
};
const handleClose = () => {
drawerVisible.value = false;
emit('search');
@@ -119,9 +119,8 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { uploadContainerFile } from '@/api/modules/container';
import { computed, nextTick, reactive, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files';
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
import i18n from '@/lang';
@@ -135,7 +134,6 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
interface UploadFileProps {
path: string;
isAppendOnly?: boolean;
containerID?: string;
}
const uploadRef = ref<UploadInstance>();
@@ -156,8 +154,6 @@ const uploadOverallPercent = computed(() => {
const open = ref(false);
const isAppendOnly = ref(false);
const path = ref();
const containerID = ref('');
const targetNode = ref('');
let uploadHelper = ref('');
const dialogExistFileRef = ref();
const abortController = ref<AbortController | null>(null);
@@ -217,6 +213,9 @@ const closePage = () => {
clearFiles();
em('close', false);
};
const state = reactive({
uploadEle: null,
});
const uploaderFiles = ref<UploadFiles>([]);
const hoverIndex = ref<number | null>(null);
const tmpFiles = ref<UploadFiles>([]);
@@ -226,11 +225,9 @@ const upload = (command: string) => {
if (uploadLocked.value) {
return;
}
const input = uploadRef.value?.$el.querySelector('input[type=file]') as HTMLInputElement | null;
if (!input) return;
input.webkitdirectory = command === 'dir';
input.value = '';
input.click();
state.uploadEle.webkitdirectory = command == 'dir';
uploadRef.value.$el.querySelector('input').value = '';
uploadRef.value.$el.querySelector('input').click();
};
const removeFile = (index: number) => {
@@ -270,7 +267,7 @@ const handleDrop = async (event: DragEvent) => {
};
const convertFileToUploadFile = (file: File, path: string): UploadFile => {
const uid = genFileId();
const uid = Date.now();
const uploadRawFile: UploadRawFile = new File([file], file.name, {
type: file.type,
@@ -297,15 +294,12 @@ const traverseFileTree = async (item: any, path = '') => {
return;
}
if (item.isFile) {
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
if (tmpFiles.value.length > 1000) {
breakFlag.value = true;
return;
}
await new Promise<void>((resolve) => {
item.file((file: File) => {
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
breakFlag.value = true;
}
if (!breakFlag.value) {
tmpFiles.value.push(convertFileToUploadFile(file, path));
}
@@ -371,14 +365,7 @@ const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
};
const submit = async () => {
if (uploadLocked.value) return;
const files = containerID.value
? uploaderFiles.value.filter((file) => file.status !== 'success')
: uploaderFiles.value.slice();
if (containerID.value) {
await uploadFile(files, false);
return;
}
const files = uploaderFiles.value.slice();
const fileNamesWithPath = Array.from(
new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)),
);
@@ -423,8 +410,7 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
upLoading.value = true;
uploadTotalCount.value = files.length;
const controller = new AbortController();
const uploadNode = containerID.value ? targetNode.value : currentNode.value;
const uploadContainerID = containerID.value;
const uploadNode = currentNode.value;
abortController.value = controller;
let successCount = 0;
try {
@@ -440,27 +426,10 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
let isSuccess = false;
try {
if (uploadContainerID) {
const data = new FormData();
data.append('containerID', uploadContainerID);
data.append('path', getUploadPath(file));
data.append('file', file.raw, getFilenameFromPath(file.raw.webkitRelativePath || file.name));
await uploadContainerFile(data, {
headers: { CurrentNode: uploadNode },
signal: controller.signal,
skipErrorMessage: true,
timeout: 0,
onUploadProgress: ({ loaded, total }) => {
uploadPercent.value = total ? Math.round((loaded / total) * 100) : 0;
},
});
isSuccess = true;
} else {
isSuccess =
Number(file.size) === 0
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
}
isSuccess =
Number(file.size) === 0
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
} catch (error) {
if (!controller.signal.aborted) {
MsgError(getErrorMessage(error));
@@ -604,9 +573,6 @@ const getFilenameFromPath = (path: string) => {
};
const acceptParams = (props: UploadFileProps) => {
if (uploadLocked.value) return;
containerID.value = props.containerID || '';
targetNode.value = currentNode.value;
path.value = props.path;
isAppendOnly.value = Boolean(props.isAppendOnly);
open.value = true;
@@ -615,19 +581,22 @@ const acceptParams = (props: UploadFileProps) => {
uploadCurrentIndex.value = 0;
uploadHelper.value = '';
canceling.value = false;
nextTick(() => {
state.uploadEle = document.querySelector('.el-upload__input');
});
};
const beforeUnload = (event: BeforeUnloadEvent) => {
if (upLoading.value) {
event.preventDefault();
event.returnValue = i18n.global.t('file.cancelUploadHelper');
}
};
onMounted(() => window.addEventListener('beforeunload', beforeUnload));
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnload);
abortController.value?.abort();
void cleanupActiveChunkUpload();
onMounted(() => {
window.addEventListener('beforeunload', (e) => {
if (upLoading.value) {
if (upLoading.value) {
e.preventDefault();
e.returnValue = i18n.global.t('file.cancelUploadHelper');
return i18n.global.t('file.cancelUploadHelper');
}
}
});
});
defineExpose({ acceptParams, handleDrop, open });