mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-14 05:00:10 +00:00
feat: 新增权限指令并集成到多个页面组件
refactor(backend): 重构数据库模型和初始化逻辑 fix(frontend): 修复权限指令在多个组件中的使用问题 perf(backend): 优化数据库连接和初始化性能 docs: 更新低代码生成器的README文档 style: 清理无用代码和注释 chore: 移除不再需要的依赖项 test: 更新用户权限相关测试用例 ci: 更新CI配置以支持新的权限检查 build: 更新依赖项版本
This commit is contained in:
+7
-7
@@ -124,7 +124,7 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link icon="RefreshRight" @click="refreshCacheNames" />
|
||||
<el-button type="primary" link icon="RefreshRight" @click="refreshCacheNames" v-hasPerm="['monitor:cache:refresh']" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -134,7 +134,7 @@
|
||||
</template>
|
||||
<el-table-column prop="cache_name" label="缓存名称" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="getCacheKeyList(row)">{{ row.cache_name }}</el-button>
|
||||
<el-button type="primary" link @click="getCacheKeyList(row)" v-hasPerm="['monitor:cache:view']">{{ row.cache_name }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
||||
@@ -142,7 +142,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm class="box-item" :title="`确认删除缓存 ${row.cache_name} 吗?`" placement="top" @confirm="handleClearCacheName(row)">
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" link icon="delete" />
|
||||
<el-button type="danger" size="small" link icon="delete" v-hasPerm="['monitor:cache:delete']" />
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
@@ -168,7 +168,7 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link icon="RefreshRight" @click="refreshCacheKeys" />
|
||||
<el-button type="primary" link icon="RefreshRight" @click="refreshCacheKeys" v-hasPerm="['monitor:cache:refresh']" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -178,7 +178,7 @@
|
||||
</template>
|
||||
<el-table-column prop="cacheKey" label="缓存键名" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleCacheValue(row.cacheKey)">{{ row.cacheKey
|
||||
<el-button type="primary" link @click="handleCacheValue(row.cacheKey)" v-hasPerm="['monitor:cache:detail']">{{ row.cacheKey
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -186,7 +186,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm class="box-item" :title="`确认删除键 ${row.cacheKey} 吗?`" placement="top" @confirm="handleClearCacheKey(row.cacheKey)">
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" link icon="delete" />
|
||||
<el-button type="danger" size="small" link icon="delete" v-hasPerm="['monitor:cache:delete']" />
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
@@ -212,7 +212,7 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="danger" link icon="delete" @click="handleClearCacheAll">清理全部</el-button>
|
||||
<el-button type="danger" link icon="delete" @click="handleClearCacheAll" v-hasPerm="['monitor:cache:delete_all']">清理全部</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -151,7 +151,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/monitor/job";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery"
|
||||
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['monitor:job:query']"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['monitor:job:query']">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link
|
||||
@@ -83,6 +83,7 @@
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('create')"
|
||||
v-hasPerm="['monitor:job:create']"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
@@ -90,6 +91,7 @@
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@click="handleDelete(selectIds)"
|
||||
v-hasPerm="['monitor:job:delete']"
|
||||
>批量删除</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -100,10 +102,11 @@
|
||||
icon="download"
|
||||
circle
|
||||
@click="handleExport"
|
||||
v-hasPerm="['monitor:job:export']"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="清除">
|
||||
<el-button type="danger" icon="delete" circle @click="handleClear" />
|
||||
<el-button type="danger" icon="delete" circle @click="handleClear" v-hasPerm="['monitor:job:clear']" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button
|
||||
@@ -111,6 +114,7 @@
|
||||
icon="refresh"
|
||||
circle
|
||||
@click="handleRefresh"
|
||||
v-hasPerm="['monitor:job:refresh']"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -213,6 +217,7 @@
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog('update', scope.row.id)"
|
||||
v-hasPerm="['monitor:job:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
@@ -222,10 +227,11 @@
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-hasPerm="['monitor:job:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-dropdown trigger="click">
|
||||
<el-dropdown trigger="click" v-hasPerm="['monitor:job:status']">
|
||||
<el-button type="warning" size="small" link icon="ArrowDown"
|
||||
>更多</el-button
|
||||
>
|
||||
@@ -560,7 +566,7 @@
|
||||
@click="handleSubmit"
|
||||
>确定</el-button
|
||||
>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog"
|
||||
<el-button v-else type="primary" @click="handleCloseDialog" v-hasPerm="['monitor:job:detail']"
|
||||
>确定</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['monitor:online:query']">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['monitor:online:query']">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -37,15 +37,15 @@
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--actions">
|
||||
<el-button type="danger" icon="delete" @click="handleClear">强退所有</el-button>
|
||||
<el-button type="danger" icon="delete" @click="handleClear" v-hasPerm="['monitor:online:force_logout']">强退所有</el-button>
|
||||
</div>
|
||||
<div class="data-table__toolbar--tools">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh" v-hasPerm="['monitor:online:refresh']" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="列表筛选">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="default" icon="operation" circle />
|
||||
<el-dropdown trigger="click" v-hasPerm="['monitor:online:filter']">
|
||||
<el-button type="default" icon="operation" circle />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
|
||||
@@ -81,7 +81,7 @@
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'login_time')?.show" key="login_time" label="登录时间" prop="login_time" min-width="180" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" key="operation" fixed="right" label="操作" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleSubmit(scope.row.session_id)">强退
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleSubmit(scope.row.session_id)" v-hasPerm="['monitor:online:force_logout']">强退
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -104,7 +104,6 @@ defineOptions({
|
||||
});
|
||||
|
||||
import OnlineAPI, { type OnlineUserPageQuery, type OnlineUserTable } from "@/api/monitor/online";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const total = ref(0);
|
||||
|
||||
@@ -3,38 +3,12 @@
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":">
|
||||
<el-form-item prop="keyword" label="关键词">
|
||||
<el-input v-model="queryFormData.keyword" placeholder="请输入文件名或关键词" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="file_type" label="文件类型">
|
||||
<el-select v-model="queryFormData.file_type" placeholder="请选择" style="width: 167.5px" clearable>
|
||||
<el-option label="图片" value="image" />
|
||||
<el-option label="文档" value="document" />
|
||||
<el-option label="视频" value="video" />
|
||||
<el-option label="音频" value="audio" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="min_size" label="文件大小">
|
||||
<el-input-number
|
||||
v-model="queryFormData.min_size"
|
||||
placeholder="最小"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span style="margin: 0 8px">-</span>
|
||||
<el-input-number
|
||||
v-model="queryFormData.max_size"
|
||||
placeholder="最大"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<el-form-item prop="name" label="关键词">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入文件名或目录名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<el-button type="primary" icon="search" @click="handleQuery" v-hasPerm="['monitor:resource:query']">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery" v-hasPerm="['monitor:resource:query']">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -55,30 +29,32 @@
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--actions">
|
||||
<el-button type="success" icon="plus" @click="handleUpload">上传文件</el-button>
|
||||
<el-button type="primary" icon="folder-add" @click="handleCreateDir">新建文件夹</el-button>
|
||||
<el-button type="danger" icon="delete" :disabled="selectedItems.length === 0" @click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button type="success" icon="plus" @click="handleUpload" v-hasPerm="['monitor:resource:upload']">上传文件</el-button>
|
||||
<el-button type="primary" icon="folder-add" @click="handleCreateDir" v-hasPerm="['monitor:resource:create_dir']">新建文件夹</el-button>
|
||||
<el-button type="danger" icon="delete" :disabled="selectedItems.length === 0" @click="handleBatchDelete" v-hasPerm="['monitor:resource:delete']">批量删除</el-button>
|
||||
</div>
|
||||
<div class="data-table__toolbar--tools">
|
||||
<el-checkbox v-model="showHiddenFiles" @change="handleShowHiddenChange">
|
||||
<el-checkbox v-model="showHiddenFiles" @change="handleShowHiddenChange" v-hasPerm="['monitor:resource:show_hidden']">
|
||||
显示隐藏文件
|
||||
</el-checkbox>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
:type="viewMode === 'list' ? 'primary' : ''"
|
||||
@click="viewMode = 'list'"
|
||||
v-hasPerm="['monitor:resource:view_list']"
|
||||
>
|
||||
<el-icon><List /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="viewMode === 'grid' ? 'primary' : ''"
|
||||
@click="viewMode = 'grid'"
|
||||
v-hasPerm="['monitor:resource:view_grid']"
|
||||
>
|
||||
<el-icon><Grid /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh" v-hasPerm="['monitor:resource:refresh']" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,7 +73,7 @@
|
||||
v-for="(item, index) in breadcrumbList"
|
||||
:key="index"
|
||||
:class="{ 'is-link': index < breadcrumbList.length - 1 }"
|
||||
@click="handleBreadcrumbClick(item, index)"
|
||||
@click="handleBreadcrumbClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-breadcrumb-item>
|
||||
@@ -111,7 +87,7 @@
|
||||
ref="dataTableRef"
|
||||
v-loading="loading"
|
||||
:data="fileList"
|
||||
row-key="path"
|
||||
row-key="file_url"
|
||||
class="data-table__content"
|
||||
height="540"
|
||||
border
|
||||
@@ -133,6 +109,7 @@
|
||||
<span
|
||||
:class="{ 'file-name-clickable': true }"
|
||||
@click="handleFileNameClick(row)"
|
||||
v-hasPerm="['monitor:resource:preview']"
|
||||
>
|
||||
{{ row.name }}
|
||||
</span>
|
||||
@@ -159,13 +136,14 @@
|
||||
link
|
||||
icon="download"
|
||||
@click="handleDownload(row)"
|
||||
v-hasPerm="['monitor:resource:download']"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" link icon="edit" @click="handleRename(row)">
|
||||
<el-button type="primary" size="small" link icon="edit" @click="handleRename(row)" v-hasPerm="['monitor:resource:rename']">
|
||||
重命名
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete(row)">
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete(row)" v-hasPerm="['monitor:resource:delete']">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -176,7 +154,7 @@
|
||||
<div v-else class="grid-view">
|
||||
<div
|
||||
v-for="item in fileList"
|
||||
:key="item.path"
|
||||
:key="item.file_url"
|
||||
class="grid-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
@@ -233,7 +211,7 @@
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="uploading" @click="handleUploadConfirm">
|
||||
<el-button type="primary" :loading="uploading" @click="handleUploadConfirm" v-hasPerm="['monitor:resource:upload']">
|
||||
确定上传
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -256,7 +234,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDirDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleCreateDirConfirm">确定</el-button>
|
||||
<el-button type="primary" @click="handleCreateDirConfirm" v-hasPerm="['monitor:resource:create_dir']">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -277,13 +255,18 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="renameDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleRenameConfirm">确定</el-button>
|
||||
<el-button type="primary" @click="handleRenameConfirm" v-hasPerm="['monitor:resource:rename']">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "ResourceMonitor",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
@@ -294,32 +277,35 @@ import {
|
||||
UploadFilled,
|
||||
QuestionFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ResourceAPI, type ResourceItem, type ResourceSearchQuery } from '@/api/monitor/resource'
|
||||
import { ResourceAPI, type ResourceItem, type ResourcePageQuery } from '@/api/monitor/resource'
|
||||
|
||||
// 分页表单
|
||||
const fileList = ref<ResourceItem[]>([])
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const fileList = ref<ResourceItem[]>([])
|
||||
const selectedItems = ref<ResourceItem[]>([])
|
||||
const breadcrumbList = ref([{ name: '资源根目录', path: '/' }])
|
||||
const showHiddenFiles = ref(false)
|
||||
const viewMode = ref<'list' | 'grid'>('list')
|
||||
const total = ref(0)
|
||||
const isSearchMode = ref(false) // 用于标记是否处于搜索模式
|
||||
const queryFormRef = ref()
|
||||
|
||||
// 路径相关数据
|
||||
const currentPath = ref('/') // 添加当前路径的响应式变量
|
||||
const currentPath = ref('/') // 当前路径的响应式变量
|
||||
|
||||
// 分页
|
||||
// 分页查询参数
|
||||
const pagination = reactive({
|
||||
page_no: 1,
|
||||
page_size: 20
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const queryFormData = reactive<ResourceSearchQuery>({
|
||||
keyword: '',
|
||||
file_type: '',
|
||||
min_size: undefined,
|
||||
max_size: undefined
|
||||
// 搜索表单数据
|
||||
const queryFormData = reactive<ResourcePageQuery>({
|
||||
name: undefined,
|
||||
page_no: 1,
|
||||
page_size: 20
|
||||
})
|
||||
|
||||
// 对话框状态
|
||||
@@ -346,7 +332,9 @@ const renameForm = reactive({
|
||||
const currentQuery = computed(() => {
|
||||
// 构建查询参数
|
||||
const query: any = {
|
||||
include_hidden: showHiddenFiles.value
|
||||
include_hidden: showHiddenFiles.value,
|
||||
page_no: pagination.page_no,
|
||||
page_size: pagination.page_size
|
||||
}
|
||||
|
||||
// 如果当前路径不是根路径,则添加路径参数
|
||||
@@ -355,25 +343,35 @@ const currentQuery = computed(() => {
|
||||
query.path = currentPath.value
|
||||
}
|
||||
|
||||
// 添加搜索关键词
|
||||
if (queryFormData.name) {
|
||||
query.name = queryFormData.name
|
||||
}
|
||||
|
||||
return query
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadFileList = async () => {
|
||||
// 加载文件列表
|
||||
async function loadFileList() {
|
||||
loading.value = true
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await ResourceAPI.getResourceList(currentQuery.value)
|
||||
|
||||
// 根据新的 API 响应结构获取数据
|
||||
const data = response.data?.data?.items || response.data?.data
|
||||
// 正确处理后端返回的分页数据结构
|
||||
const pageResult = response.data?.data
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
// 直接使用后端返回的数据结构,无需转换
|
||||
fileList.value = data.map(item => ({
|
||||
if (pageResult && Array.isArray(pageResult.items)) {
|
||||
fileList.value = pageResult.items.map(item => ({
|
||||
...item,
|
||||
is_hidden: item.name.startsWith('.')
|
||||
}))
|
||||
total.value = response.data?.data?.total_files + response.data?.data?.total_dirs || fileList.value.length
|
||||
total.value = pageResult.total
|
||||
if (pageResult.page_no !== undefined) {
|
||||
pagination.page_no = pageResult.page_no
|
||||
}
|
||||
if (pageResult.page_size !== undefined) {
|
||||
pagination.page_size = pageResult.page_size
|
||||
}
|
||||
} else {
|
||||
fileList.value = []
|
||||
total.value = 0
|
||||
@@ -388,14 +386,16 @@ const loadFileList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBreadcrumbClick = (item: any) => {
|
||||
// 面包屑点击处理
|
||||
function handleBreadcrumbClick(item: any) {
|
||||
// 更新当前路径为点击的面包屑项路径
|
||||
currentPath.value = item.path
|
||||
updateBreadcrumb()
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const updateBreadcrumb = () => {
|
||||
// 更新面包屑路径
|
||||
function updateBreadcrumb() {
|
||||
// 对于根路径,直接显示根目录
|
||||
if (currentPath.value === '/') {
|
||||
breadcrumbList.value = [{ name: '资源根目录', path: '/' }]
|
||||
@@ -414,7 +414,8 @@ const updateBreadcrumb = () => {
|
||||
]
|
||||
}
|
||||
|
||||
const handleFileNameClick = (row: ResourceItem) => {
|
||||
// 文件名点击处理
|
||||
function handleFileNameClick(row: ResourceItem) {
|
||||
if (row.is_dir) {
|
||||
// 如果当前在根路径,则直接使用文件夹名称
|
||||
// 如果当前已在某个文件夹中,则拼接路径
|
||||
@@ -431,7 +432,8 @@ const handleFileNameClick = (row: ResourceItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemClick = (item: ResourceItem) => {
|
||||
// 网格视图项目点击处理
|
||||
function handleItemClick(item: ResourceItem) {
|
||||
if (item.is_dir) {
|
||||
// 如果当前在根路径,则直接使用文件夹名称
|
||||
// 如果当前已在某个文件夹中,则拼接路径
|
||||
@@ -449,48 +451,37 @@ const handleItemClick = (item: ResourceItem) => {
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
const handleFilePreview = (file: ResourceItem) => {
|
||||
// 使用后端返回的完整URL路径进行预览
|
||||
let previewUrl = file.path
|
||||
function handleFilePreview(file: ResourceItem) {
|
||||
// 直接使用file_url字段进行预览
|
||||
let previewUrl = file.file_url
|
||||
|
||||
// 如果是完整URL,直接使用
|
||||
if (!previewUrl.startsWith('http')) {
|
||||
// 对于相对路径,需要构建完整URL
|
||||
// 如果是相对路径,构建完整URL
|
||||
if (previewUrl && !previewUrl.startsWith('http')) {
|
||||
previewUrl = `${window.location.origin}${previewUrl}`
|
||||
}
|
||||
|
||||
// 根据文件类型决定预览方式
|
||||
const fileExtension = file.file_extension?.toLowerCase() || ''
|
||||
|
||||
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'].includes(fileExtension)) {
|
||||
// 图片预览
|
||||
window.open(previewUrl, '_blank')
|
||||
} else if (['.pdf'].includes(fileExtension)) {
|
||||
// PDF预览
|
||||
window.open(previewUrl, '_blank')
|
||||
} else if (['.txt', '.md', '.json', '.xml', '.html', '.css', '.js', '.ts', '.vue'].includes(fileExtension)) {
|
||||
// 文本文件预览
|
||||
window.open(previewUrl, '_blank')
|
||||
} else {
|
||||
// 其他文件直接下载
|
||||
window.open(previewUrl, '_blank')
|
||||
}
|
||||
// 所有文件类型统一直接打开预览
|
||||
window.open(previewUrl, '_blank')
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: ResourceItem[]) => {
|
||||
// 选择项变化处理
|
||||
function handleSelectionChange(selection: ResourceItem[]) {
|
||||
selectedItems.value = selection
|
||||
}
|
||||
|
||||
const handleUpload = () => {
|
||||
// 打开上传对话框
|
||||
function handleUpload() {
|
||||
uploadDialogVisible.value = true
|
||||
uploadFileList.value = []
|
||||
}
|
||||
|
||||
const handleUploadChange = (file: any, fileList: any[]) => {
|
||||
// 上传文件变化处理
|
||||
function handleUploadChange(file: any, fileList: any[]) {
|
||||
uploadFileList.value = fileList
|
||||
}
|
||||
|
||||
const handleUploadConfirm = async () => {
|
||||
// 确认上传文件
|
||||
async function handleUploadConfirm() {
|
||||
if (uploadFileList.value.length === 0) {
|
||||
ElMessage.warning('请选择要上传的文件')
|
||||
return
|
||||
@@ -503,17 +494,8 @@ const handleUploadConfirm = async () => {
|
||||
formData.append('file', file.raw)
|
||||
})
|
||||
|
||||
// 处理目标路径
|
||||
let targetPath = currentPath.value
|
||||
if (targetPath.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(targetPath)
|
||||
targetPath = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', targetPath, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 在根目录时传递空字符串作为target_path,与后端relative_path格式保持一致
|
||||
const targetPath = currentPath.value === '/' ? '' : currentPath.value
|
||||
formData.append('target_path', targetPath)
|
||||
|
||||
await ResourceAPI.uploadFile(formData)
|
||||
@@ -528,34 +510,28 @@ const handleUploadConfirm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadClose = () => {
|
||||
// 关闭上传对话框
|
||||
function handleUploadClose() {
|
||||
uploadDialogVisible.value = false
|
||||
uploadFileList.value = []
|
||||
}
|
||||
|
||||
const handleCreateDir = () => {
|
||||
// 打开新建目录对话框
|
||||
function handleCreateDir() {
|
||||
createDirForm.dir_name = ''
|
||||
createDirDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCreateDirConfirm = async () => {
|
||||
// 确认创建目录
|
||||
async function handleCreateDirConfirm() {
|
||||
if (!createDirForm.dir_name.trim()) {
|
||||
ElMessage.warning('请输入文件夹名称')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理父路径
|
||||
let parentPath = currentPath.value
|
||||
if (parentPath.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(parentPath)
|
||||
parentPath = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', parentPath, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 在根目录时传递空字符串作为parent_path,与后端relative_path格式保持一致
|
||||
const parentPath = currentPath.value === '/' ? '' : currentPath.value
|
||||
await ResourceAPI.createDirectory({
|
||||
parent_path: parentPath,
|
||||
dir_name: createDirForm.dir_name.trim()
|
||||
@@ -569,67 +545,35 @@ const handleCreateDirConfirm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadFileList()
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1
|
||||
await loadFileList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
queryFormData.page_no = 1
|
||||
isSearchMode.value = false // 退出搜索模式
|
||||
await loadFileList()
|
||||
}
|
||||
|
||||
// 显示隐藏文件切换
|
||||
function handleShowHiddenChange() {
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const handleQuery = async () => {
|
||||
// 文件下载
|
||||
async function handleDownload(item: ResourceItem) {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await ResourceAPI.searchResource(queryFormData)
|
||||
|
||||
// 根据新的 API 响应结构获取数据
|
||||
const data = response.data?.data?.items || response.data?.data
|
||||
if (Array.isArray(data)) {
|
||||
// 直接使用后端返回的数据结构,无需转换
|
||||
fileList.value = data.map(item => ({
|
||||
...item,
|
||||
is_hidden: item.name.startsWith('.')
|
||||
}))
|
||||
total.value = response.data?.data?.total_files + response.data?.data?.total_dirs || fileList.value.length
|
||||
} else {
|
||||
console.warn('搜索 API 返回的数据不是数组类型:', data)
|
||||
fileList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('搜索失败')
|
||||
console.error('Search error:', error)
|
||||
fileList.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetQuery = () => {
|
||||
Object.assign(queryFormData, {
|
||||
keyword: '',
|
||||
file_type: '',
|
||||
min_size: undefined,
|
||||
max_size: undefined
|
||||
})
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const handleShowHiddenChange = () => {
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const handleDownload = async (item: ResourceItem) => {
|
||||
try {
|
||||
// 处理完整URL路径
|
||||
let filePath = item.path
|
||||
if (filePath.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(filePath)
|
||||
filePath = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', filePath, error)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await ResourceAPI.downloadFile(filePath)
|
||||
// 使用file_url字段
|
||||
const response = await ResourceAPI.downloadFile(item.file_url)
|
||||
const blob = response.data
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
@@ -645,24 +589,16 @@ const handleDownload = async (item: ResourceItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = (item: ResourceItem) => {
|
||||
// 处理完整URL路径
|
||||
let oldPath = item.path
|
||||
if (oldPath.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(oldPath)
|
||||
oldPath = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', oldPath, error)
|
||||
}
|
||||
}
|
||||
|
||||
renameForm.old_path = oldPath
|
||||
// 打开重命名对话框
|
||||
function handleRename(item: ResourceItem) {
|
||||
// 使用file_url字段
|
||||
renameForm.old_path = item.file_url
|
||||
renameForm.new_name = item.name
|
||||
renameDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleRenameConfirm = async () => {
|
||||
// 确认重命名
|
||||
async function handleRenameConfirm() {
|
||||
if (!renameForm.new_name.trim()) {
|
||||
ElMessage.warning('请输入新名称')
|
||||
return
|
||||
@@ -682,7 +618,8 @@ const handleRenameConfirm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (item: ResourceItem) => {
|
||||
// 文件删除
|
||||
async function handleDelete(item: ResourceItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除 ${item.name} 吗?`,
|
||||
@@ -694,35 +631,27 @@ const handleDelete = async (item: ResourceItem) => {
|
||||
}
|
||||
)
|
||||
|
||||
// 处理完整URL路径
|
||||
let filePath = item.path
|
||||
if (filePath.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(filePath)
|
||||
filePath = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', filePath, error)
|
||||
}
|
||||
}
|
||||
|
||||
await ResourceAPI.deleteResource([filePath])
|
||||
// 使用file_url字段
|
||||
await ResourceAPI.deleteResource([item.file_url])
|
||||
ElMessage.success('删除成功')
|
||||
loadFileList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
console.error('Delete error:', error)
|
||||
}
|
||||
loadFileList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
console.error('Delete error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePagination = (params: { page: number; limit: number }) => {
|
||||
// 分页处理
|
||||
function handlePagination(params: { page: number; limit: number }) {
|
||||
pagination.page_no = params.page
|
||||
pagination.page_size = params.limit
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
// 批量删除
|
||||
async function handleBatchDelete() {
|
||||
if (selectedItems.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的文件')
|
||||
return
|
||||
@@ -739,19 +668,8 @@ const handleBatchDelete = async () => {
|
||||
}
|
||||
)
|
||||
|
||||
const paths = selectedItems.value.map(item => {
|
||||
// 处理完整URL路径
|
||||
let path = item.path
|
||||
if (path.startsWith('http')) {
|
||||
try {
|
||||
const url = new URL(path)
|
||||
path = url.pathname.replace('/api/v1/static', '') || '/'
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse URL:', path, error)
|
||||
}
|
||||
}
|
||||
return path
|
||||
})
|
||||
// 使用file_url字段
|
||||
const paths = selectedItems.value.map(item => item.file_url)
|
||||
|
||||
await ResourceAPI.deleteResource(paths)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -764,9 +682,9 @@ const handleBatchDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const formatFileSize = (size?: number | null) => {
|
||||
if (!size || size === null) return '-'
|
||||
// 格式化文件大小
|
||||
function formatFileSize(size?: number | null) {
|
||||
if (!size || size === null) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let unitIndex = 0
|
||||
let fileSize = size
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 存储空间信息 -->
|
||||
<el-row :gutter="16" class="mb-4">
|
||||
<el-col :span="24">
|
||||
<el-card :loading="statsLoading" shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span class="font-medium">存储空间信息</span>
|
||||
<el-tooltip content="存储空间使用详情">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="挂载点">{{ stats?.mount_point || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总空间">{{ formatFileSize(stats?.total_space) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用">{{ formatFileSize(stats?.used_space) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="可用空间">{{ formatFileSize(stats?.free_space) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="mt-4">
|
||||
<div class="mb-2">
|
||||
<span>使用率</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="Math.round(((stats?.used_space || 0) / (stats?.total_space || 1)) * 100)"
|
||||
:status="getStorageStatus(Math.round(((stats?.used_space || 0) / (stats?.total_space || 1)) * 100))"
|
||||
:text-inside="true"
|
||||
:stroke-width="16"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 基础统计信息表格 -->
|
||||
<el-row :gutter="16" class="mb-4">
|
||||
<el-col :span="24">
|
||||
<el-card :loading="statsLoading" shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span class="font-medium">基础统计信息</span>
|
||||
<el-tooltip content="文件系统基础统计信息">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="getBasicStatsData()" border>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column label="文件总数" prop="total_files" align="center" />
|
||||
<el-table-column label="文件夹数" prop="total_dirs" align="center" />
|
||||
<el-table-column label="总大小" prop="total_size" align="center" />
|
||||
<el-table-column label="已使用空间" prop="used_space" align="center" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 文件类型和扩展名统计 -->
|
||||
<el-row :gutter="16" class="mb-4">
|
||||
<!-- 文件类型统计 -->
|
||||
<el-col :span="12">
|
||||
<el-card :loading="statsLoading" shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span class="font-medium">文件类型统计</span>
|
||||
<el-tooltip content="按文件类型统计文件数量">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="getTypeStatsData()" border>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column label="文件类型" prop="type" />
|
||||
<el-table-column label="数量" prop="count" align="center" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 文件扩展名统计 -->
|
||||
<el-col :span="12">
|
||||
<el-card :loading="statsLoading" shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><PieChart /></el-icon>
|
||||
<span class="font-medium">文件扩展名统计</span>
|
||||
<el-tooltip content="按文件扩展名统计文件数量">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="getExtensionStatsData()" border>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column label="扩展名" prop="extension" />
|
||||
<el-table-column label="数量" prop="count" align="center" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Document,
|
||||
Folder,
|
||||
DataAnalysis,
|
||||
PieChart,
|
||||
Monitor,
|
||||
QuestionFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ResourceAPI, type ResourceStats } from '@/api/monitor/resource'
|
||||
|
||||
// 响应式数据
|
||||
const stats = ref<ResourceStats | null>(null)
|
||||
const statsLoading = ref(false)
|
||||
|
||||
// 方法
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
statsLoading.value = true
|
||||
const response = await ResourceAPI.getResourceStats()
|
||||
stats.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取统计信息失败')
|
||||
console.error('Load stats error:', error)
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数据处理函数
|
||||
const getBasicStatsData = () => {
|
||||
if (!stats.value) return []
|
||||
return [
|
||||
{
|
||||
total_files: stats.value.total_files || 0,
|
||||
total_dirs: stats.value.total_dirs || 0,
|
||||
total_size: formatFileSize(stats.value.total_size),
|
||||
used_space: formatFileSize(stats.value.used_space)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getTypeStatsData = () => {
|
||||
if (!stats.value?.type_stats) return []
|
||||
return Object.entries(stats.value.type_stats).map(([type, count]) => ({
|
||||
type,
|
||||
count
|
||||
}))
|
||||
}
|
||||
|
||||
const getExtensionStatsData = () => {
|
||||
if (!stats.value?.extension_stats) return []
|
||||
return Object.entries(stats.value.extension_stats).map(([extension, count]) => ({
|
||||
extension,
|
||||
count
|
||||
}))
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const formatFileSize = (size?: number | null) => {
|
||||
if (!size || size === null) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let unitIndex = 0
|
||||
let fileSize = size
|
||||
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 存储空间使用率状态计算
|
||||
const getStorageStatus = (percentage: number) => {
|
||||
if (percentage > 80) return 'exception'
|
||||
if (percentage > 60) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Reference in New Issue
Block a user