2025-12-19 11:28:59 +08:00
|
|
|
|
<template>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
<ListPageLayout
|
|
|
|
|
|
title="UI组件管理"
|
|
|
|
|
|
subtitle="管理系统中的UI组件和文件资源"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 筛选区域 -->
|
|
|
|
|
|
<template #filters>
|
|
|
|
|
|
<FilterSection>
|
|
|
|
|
|
<div :class="['grid gap-4', isMobile ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4']">
|
|
|
|
|
|
<FilterItem label="关键词">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="filterForm.keyword"
|
|
|
|
|
|
placeholder="请输入关键词搜索"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@input="handleFilterChange"
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FilterItem label="状态">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="filterForm.is_active"
|
|
|
|
|
|
placeholder="请选择状态"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@change="handleFilterChange"
|
|
|
|
|
|
class="w-full"
|
2025-12-19 11:28:59 +08:00
|
|
|
|
>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
<el-option label="启用" :value="true" />
|
|
|
|
|
|
<el-option label="禁用" :value="false" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #stats>
|
|
|
|
|
|
共找到 {{ pagination.total }} 个UI组件
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #buttons>
|
|
|
|
|
|
<div :class="['flex gap-2', isMobile ? 'flex-wrap w-full' : '']">
|
|
|
|
|
|
<el-button :size="isMobile ? 'small' : 'default'" @click="handleReset" :class="isMobile ? 'flex-1' : ''">
|
|
|
|
|
|
重置筛选
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</el-button>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
<el-button :size="isMobile ? 'small' : 'default'" type="primary" @click="handleSearch" :class="isMobile ? 'flex-1' : ''">
|
|
|
|
|
|
应用筛选
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</el-button>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
<el-button :size="isMobile ? 'small' : 'default'" type="success" @click="handleCreate" :class="isMobile ? 'w-full' : ''">
|
|
|
|
|
|
<Plus class="w-4 h-4 mr-1" />
|
|
|
|
|
|
<span :class="isMobile ? 'hidden sm:inline' : ''">新增UI组件</span>
|
|
|
|
|
|
<span :class="isMobile ? 'sm:hidden' : 'hidden'">新增</span>
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</el-button>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</FilterSection>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 表格区域 -->
|
|
|
|
|
|
<template #table>
|
|
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
|
|
|
|
<el-loading size="large" />
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</div>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 移动端卡片布局 -->
|
|
|
|
|
|
<div v-else-if="isMobile && componentList.length > 0" class="component-cards">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="component in componentList"
|
|
|
|
|
|
:key="component.id"
|
|
|
|
|
|
class="component-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<span class="font-semibold text-base text-blue-600">{{ component.component_name || '未知组件' }}</span>
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
:type="component.is_active ? 'success' : 'danger'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="light"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ component.is_active ? '启用' : '禁用' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-gray-500 font-mono">编码: {{ component.component_code }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">描述</span>
|
|
|
|
|
|
<span class="card-value">{{ component.description || '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">版本</span>
|
|
|
|
|
|
<span class="card-value">{{ component.version || '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">文件状态</span>
|
|
|
|
|
|
<span class="card-value">
|
|
|
|
|
|
<el-tag v-if="component.is_extracted" type="success" size="small">已解压</el-tag>
|
|
|
|
|
|
<el-tag v-else-if="component.file_path" type="warning" size="small">已上传</el-tag>
|
|
|
|
|
|
<el-tag v-else type="info" size="small">未上传</el-tag>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">文件大小</span>
|
|
|
|
|
|
<span class="card-value">{{ component.file_size ? formatFileSize(component.file_size) : '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">排序</span>
|
|
|
|
|
|
<span class="card-value">{{ component.sort_order || '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">创建时间</span>
|
|
|
|
|
|
<span class="card-value text-sm">{{ formatDateTime(component.created_at) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-footer">
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="handleEdit(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
编辑
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="!component.file_path"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="handleUpload(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
上传文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="component.file_path && !component.is_extracted && isZipFileFromPath(component.file_path)"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
@click="handleUploadExtract(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
上传并解压
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="component.is_extracted"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
@click="handleViewFolder(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看文件夹
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="component.file_path"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
@click="handleDownload(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
下载文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="component.is_extracted"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
@click="handleDeleteFolder(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除文件夹
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
@click="handleDelete(component)"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 桌面端表格布局 -->
|
|
|
|
|
|
<div v-else-if="!isMobile" class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
|
:data="componentList"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
background: '#f8fafc',
|
|
|
|
|
|
color: '#475569',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
fontSize: '14px'
|
|
|
|
|
|
}"
|
|
|
|
|
|
:cell-style="{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
color: '#1e293b'
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="component_code" label="组件编码" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="font-mono text-sm text-gray-600">{{ row.component_code }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="component_name" label="组件名称" min-width="200">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="font-medium text-blue-600">{{ row.component_name || '未知组件' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="version" label="版本" width="100" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="文件状态" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag v-if="row.is_extracted" type="success" size="small">已解压</el-tag>
|
|
|
|
|
|
<el-tag v-else-if="row.file_path" type="warning" size="small">已上传</el-tag>
|
|
|
|
|
|
<el-tag v-else type="info" size="small">未上传</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="文件大小" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span v-if="row.file_size">{{ formatFileSize(row.file_size) }}</span>
|
|
|
|
|
|
<span v-else class="text-gray-400">-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="状态" width="100">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">
|
|
|
|
|
|
{{ row.is_active ? '启用' : '禁用' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="sort_order" label="排序" width="80" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="text-sm">
|
|
|
|
|
|
<div class="text-gray-900">{{ formatDate(row.created_at) }}</div>
|
|
|
|
|
|
<div class="text-gray-500">{{ formatTime(row.created_at) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="操作" width="340" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="flex items-center space-x-2">
|
|
|
|
|
|
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="!row.file_path"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="handleUpload(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
上传文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="row.file_path && !row.is_extracted && isZipFileFromPath(row.file_path)"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
@click="handleUploadExtract(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
上传并解压
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="row.is_extracted"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
@click="handleViewFolder(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看文件夹
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-dropdown v-if="row.file_path || row.is_extracted">
|
|
|
|
|
|
<el-button size="small" type="info">
|
|
|
|
|
|
更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item v-if="row.file_path" @click="handleDownload(row)">
|
|
|
|
|
|
下载文件
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item v-if="row.is_extracted" @click="handleDeleteFolder(row)">
|
|
|
|
|
|
删除文件夹
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item @click="handleDelete(row)" divided>
|
|
|
|
|
|
删除组件
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
<el-button v-else size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div v-if="!loading && componentList.length === 0" class="text-center py-12">
|
|
|
|
|
|
<el-empty description="暂无UI组件" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
|
<template #pagination>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-if="pagination.total > 0"
|
|
|
|
|
|
v-model:current-page="pagination.page"
|
|
|
|
|
|
v-model:page-size="pagination.pageSize"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
:total="pagination.total"
|
|
|
|
|
|
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
|
|
|
|
|
:small="isMobile"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ListPageLayout>
|
2025-12-19 11:28:59 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 创建/编辑对话框 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="dialogVisible"
|
|
|
|
|
|
:title="dialogTitle"
|
|
|
|
|
|
width="600px"
|
|
|
|
|
|
@close="handleDialogClose"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="formRef"
|
|
|
|
|
|
:model="form"
|
|
|
|
|
|
:rules="formRules"
|
|
|
|
|
|
label-width="100px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form-item label="组件编码" prop="component_code">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.component_code"
|
|
|
|
|
|
:disabled="isEdit"
|
|
|
|
|
|
placeholder="请输入组件编码"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="组件名称" prop="component_name">
|
|
|
|
|
|
<el-input v-model="form.component_name" placeholder="请输入组件名称" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="描述">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.description"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
placeholder="请输入组件描述"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="版本">
|
|
|
|
|
|
<el-input v-model="form.version" placeholder="请输入版本号" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="排序">
|
|
|
|
|
|
<el-input-number v-model="form.sort_order" :min="0" />
|
|
|
|
|
|
</el-form-item>
|
2025-12-19 16:57:49 +08:00
|
|
|
|
<el-form-item label="上传模式" v-if="!isEdit">
|
|
|
|
|
|
<el-radio-group v-model="uploadMode">
|
|
|
|
|
|
<el-radio label="files">文件上传</el-radio>
|
|
|
|
|
|
<el-radio label="folder">文件夹上传</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="组件文件" v-if="!isEdit">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="createUploadRef"
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
:multiple="uploadMode === 'files'"
|
|
|
|
|
|
:webkitdirectory="uploadMode === 'folder'"
|
|
|
|
|
|
:on-change="handleCreateFileChange"
|
|
|
|
|
|
:on-remove="handleCreateFileRemove"
|
|
|
|
|
|
drag
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
|
|
|
|
<div class="el-upload__text">
|
|
|
|
|
|
将文件拖到此处,或<em>点击上传</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #tip>
|
|
|
|
|
|
<div class="el-upload__tip" v-if="uploadMode === 'files'">
|
|
|
|
|
|
可以上传多个文件,每个文件不超过100MB。ZIP文件可以自动解压,其他文件类型仅保存。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="el-upload__tip" v-else>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
。ZIP文件可以自动解压,每个文件不超过100MB。
|
2025-12-19 16:57:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
</el-form-item>
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
|
|
|
|
|
确定
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文件上传对话框 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="uploadDialogVisible"
|
|
|
|
|
|
:title="uploadDialogTitle"
|
|
|
|
|
|
width="500px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="uploadRef"
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
:limit="1"
|
|
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
|
:on-remove="handleFileRemove"
|
|
|
|
|
|
drag
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
|
|
|
|
<div class="el-upload__text">
|
|
|
|
|
|
将文件拖到此处,或<em>点击上传</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #tip>
|
|
|
|
|
|
<div class="el-upload__tip">
|
2025-12-19 16:57:49 +08:00
|
|
|
|
文件夹使用压缩为zip格式,可以批量上传文件。
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="handleFileSubmit"
|
|
|
|
|
|
:loading="uploading"
|
|
|
|
|
|
:disabled="!selectedFile"
|
|
|
|
|
|
>
|
|
|
|
|
|
上传
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文件夹内容预览对话框 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="folderDialogVisible"
|
|
|
|
|
|
:title="folderDialogTitle"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-tree
|
|
|
|
|
|
:data="folderTree"
|
|
|
|
|
|
:props="defaultProps"
|
|
|
|
|
|
default-expand-all
|
|
|
|
|
|
show-checkbox
|
|
|
|
|
|
node-key="path"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ node, data }">
|
|
|
|
|
|
<span class="custom-tree-node">
|
|
|
|
|
|
<el-icon>
|
|
|
|
|
|
<folder v-if="data.type === 'folder'" />
|
|
|
|
|
|
<document v-else />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span>{{ data.name }}</span>
|
|
|
|
|
|
<span class="file-size">({{ formatSize(data.size) }})</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-tree>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
|
<el-button @click="folderDialogVisible = false">关闭</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { uiComponentApi } from '@/api/ui-component'
|
2025-12-22 18:32:34 +08:00
|
|
|
|
import FilterItem from '@/components/common/FilterItem.vue'
|
|
|
|
|
|
import FilterSection from '@/components/common/FilterSection.vue'
|
|
|
|
|
|
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
|
|
|
|
|
import { ArrowDown, Document, Folder, Plus, UploadFilled } from '@element-plus/icons-vue'
|
2025-12-19 11:28:59 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 移动端检测
|
|
|
|
|
|
const { isMobile, isTablet } = useMobileTable()
|
|
|
|
|
|
|
2025-12-19 11:28:59 +08:00
|
|
|
|
// 响应式数据
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const componentList = ref([])
|
|
|
|
|
|
const dialogVisible = ref(false)
|
|
|
|
|
|
const uploadDialogVisible = ref(false)
|
|
|
|
|
|
const folderDialogVisible = ref(false)
|
|
|
|
|
|
const submitting = ref(false)
|
|
|
|
|
|
const uploading = ref(false)
|
|
|
|
|
|
const isEdit = ref(false)
|
|
|
|
|
|
const currentComponent = ref(null)
|
|
|
|
|
|
const selectedFile = ref(null)
|
2025-12-19 16:57:49 +08:00
|
|
|
|
const selectedCreateFile = ref(null)
|
|
|
|
|
|
const selectedCreateFiles = ref([])
|
|
|
|
|
|
const uploadMode = ref('files') // 'files' 或 'folder'
|
2025-12-19 11:28:59 +08:00
|
|
|
|
const formRef = ref(null)
|
|
|
|
|
|
const uploadRef = ref(null)
|
2025-12-19 16:57:49 +08:00
|
|
|
|
const createUploadRef = ref(null)
|
2025-12-19 11:28:59 +08:00
|
|
|
|
const folderTree = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选表单
|
|
|
|
|
|
const filterForm = reactive({
|
|
|
|
|
|
keyword: '',
|
|
|
|
|
|
is_active: null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 分页数据
|
|
|
|
|
|
const pagination = reactive({
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
total: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
component_code: '',
|
|
|
|
|
|
component_name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
version: '',
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
sort_order: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 表单验证规则
|
|
|
|
|
|
const formRules = {
|
|
|
|
|
|
component_code: [
|
|
|
|
|
|
{ required: true, message: '请输入组件编码', trigger: 'blur' }
|
|
|
|
|
|
],
|
|
|
|
|
|
component_name: [
|
|
|
|
|
|
{ required: true, message: '请输入组件名称', trigger: 'blur' }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 树形组件默认属性
|
|
|
|
|
|
const defaultProps = {
|
|
|
|
|
|
children: 'children',
|
|
|
|
|
|
label: 'name'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
|
const dialogTitle = computed(() => isEdit.value ? '编辑UI组件' : '新增UI组件')
|
|
|
|
|
|
const uploadDialogTitle = computed(() => `上传文件 - ${currentComponent.value?.component_name || ''}`)
|
|
|
|
|
|
const folderDialogTitle = computed(() => `文件夹内容 - ${currentComponent.value?.component_name || ''}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 方法
|
|
|
|
|
|
const fetchComponentList = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
page: pagination.page,
|
|
|
|
|
|
page_size: pagination.pageSize,
|
|
|
|
|
|
keyword: filterForm.keyword,
|
|
|
|
|
|
is_active: filterForm.is_active
|
|
|
|
|
|
}
|
|
|
|
|
|
const response = await uiComponentApi.getUIComponentList(params)
|
|
|
|
|
|
componentList.value = response.data.components
|
|
|
|
|
|
pagination.total = response.data.total
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('获取UI组件列表失败')
|
|
|
|
|
|
console.error('获取UI组件列表失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 18:32:34 +08:00
|
|
|
|
// 格式化日期
|
|
|
|
|
|
const formatDate = (date) => {
|
|
|
|
|
|
if (!date) return '-'
|
|
|
|
|
|
return new Date(date).toLocaleDateString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化时间
|
|
|
|
|
|
const formatTime = (date) => {
|
|
|
|
|
|
if (!date) return '-'
|
|
|
|
|
|
return new Date(date).toLocaleTimeString('zh-CN', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理筛选变化
|
|
|
|
|
|
const handleFilterChange = () => {
|
|
|
|
|
|
pagination.page = 1
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 11:28:59 +08:00
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
pagination.page = 1
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
|
filterForm.keyword = ''
|
|
|
|
|
|
filterForm.is_active = null
|
|
|
|
|
|
pagination.page = 1
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSizeChange = (val) => {
|
|
|
|
|
|
pagination.pageSize = val
|
|
|
|
|
|
pagination.page = 1
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCurrentChange = (val) => {
|
|
|
|
|
|
pagination.page = val
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreate = () => {
|
|
|
|
|
|
isEdit.value = false
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = (row) => {
|
|
|
|
|
|
isEdit.value = true
|
|
|
|
|
|
currentComponent.value = row
|
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
id: row.id,
|
|
|
|
|
|
component_code: row.component_code,
|
|
|
|
|
|
component_name: row.component_name,
|
|
|
|
|
|
description: row.description,
|
|
|
|
|
|
version: row.version,
|
|
|
|
|
|
is_active: row.is_active,
|
|
|
|
|
|
sort_order: row.sort_order
|
|
|
|
|
|
})
|
|
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = (row) => {
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
|
`确定要删除UI组件"${row.component_name}"吗?`,
|
|
|
|
|
|
'删除确认',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
).then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await uiComponentApi.deleteUIComponent(row.id)
|
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('删除失败')
|
|
|
|
|
|
console.error('删除UI组件失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
// 用户取消删除
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
if (!formRef.value) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await formRef.value.validate()
|
|
|
|
|
|
submitting.value = true
|
|
|
|
|
|
|
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
|
|
await uiComponentApi.updateUIComponent(form.id, form)
|
|
|
|
|
|
ElMessage.success('更新成功')
|
|
|
|
|
|
} else {
|
2025-12-19 16:57:49 +08:00
|
|
|
|
// 检查是否上传了文件
|
|
|
|
|
|
if (selectedCreateFiles.value && selectedCreateFiles.value.length > 0) {
|
|
|
|
|
|
// 使用合并接口,同时创建组件和上传文件
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
|
formData.append('component_code', form.component_code)
|
|
|
|
|
|
formData.append('component_name', form.component_name)
|
|
|
|
|
|
formData.append('description', form.description || '')
|
|
|
|
|
|
formData.append('version', form.version || '')
|
|
|
|
|
|
formData.append('is_active', form.is_active ? 'true' : 'false')
|
|
|
|
|
|
formData.append('sort_order', form.sort_order.toString())
|
|
|
|
|
|
|
|
|
|
|
|
// 添加所有文件
|
|
|
|
|
|
selectedCreateFiles.value.forEach(file => {
|
|
|
|
|
|
formData.append('files', file.raw)
|
|
|
|
|
|
if (uploadMode.value === 'folder') {
|
|
|
|
|
|
formData.append('paths', file.path)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await uiComponentApi.createUIComponentWithFile(formData)
|
|
|
|
|
|
ElMessage.success('创建并上传成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 只创建组件,不上传文件
|
|
|
|
|
|
await uiComponentApi.createUIComponent(form)
|
|
|
|
|
|
ElMessage.success('创建成功')
|
|
|
|
|
|
}
|
2025-12-19 11:28:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error !== false) { // 不是表单验证错误
|
|
|
|
|
|
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
|
|
|
|
|
console.error('提交UI组件失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDialogClose = () => {
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
component_code: '',
|
|
|
|
|
|
component_name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
version: '',
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
sort_order: 0
|
|
|
|
|
|
})
|
2025-12-19 16:57:49 +08:00
|
|
|
|
selectedCreateFile.value = null
|
|
|
|
|
|
selectedCreateFiles.value = []
|
|
|
|
|
|
uploadMode.value = 'files' // 重置为文件上传模式
|
2025-12-19 11:28:59 +08:00
|
|
|
|
if (formRef.value) {
|
|
|
|
|
|
formRef.value.resetFields()
|
|
|
|
|
|
}
|
2025-12-19 16:57:49 +08:00
|
|
|
|
if (createUploadRef.value) {
|
|
|
|
|
|
createUploadRef.value.clearFiles()
|
|
|
|
|
|
}
|
2025-12-19 11:28:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpload = (row) => {
|
|
|
|
|
|
currentComponent.value = row
|
|
|
|
|
|
selectedFile.value = null
|
|
|
|
|
|
if (uploadRef.value) {
|
|
|
|
|
|
uploadRef.value.clearFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
uploadDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleUploadExtract = (row) => {
|
|
|
|
|
|
currentComponent.value = row
|
|
|
|
|
|
selectedFile.value = null
|
|
|
|
|
|
if (uploadRef.value) {
|
|
|
|
|
|
uploadRef.value.clearFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
uploadDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFileChange = (file) => {
|
|
|
|
|
|
selectedFile.value = file.raw
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFileRemove = () => {
|
|
|
|
|
|
selectedFile.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 16:57:49 +08:00
|
|
|
|
const handleCreateFileChange = (file, fileList) => {
|
|
|
|
|
|
selectedCreateFiles.value = fileList.map(f => ({
|
|
|
|
|
|
raw: f.raw,
|
|
|
|
|
|
name: f.name,
|
|
|
|
|
|
path: uploadMode.value === 'folder' ? (f.raw.webkitRelativePath || f.name) : f.name
|
|
|
|
|
|
}))
|
|
|
|
|
|
selectedCreateFile.value = fileList.length > 0 ? fileList[0].raw : null // 保留兼容性
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreateFileRemove = () => {
|
|
|
|
|
|
selectedCreateFiles.value = []
|
|
|
|
|
|
selectedCreateFile.value = null // 保留兼容性
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 11:28:59 +08:00
|
|
|
|
const handleFileSubmit = async () => {
|
|
|
|
|
|
if (!selectedFile.value) {
|
|
|
|
|
|
ElMessage.warning('请选择要上传的文件')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uploading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
|
formData.append('file', selectedFile.value)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据当前组件是否已解压决定使用哪个接口
|
|
|
|
|
|
if (currentComponent.value.is_extracted) {
|
|
|
|
|
|
// 已解压,使用普通上传接口
|
|
|
|
|
|
await uiComponentApi.uploadUIComponentFile(currentComponent.value.id, formData)
|
|
|
|
|
|
ElMessage.success('文件上传成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 未解压,使用上传并解压接口
|
|
|
|
|
|
await uiComponentApi.uploadAndExtractUIComponentFile(currentComponent.value.id, formData)
|
|
|
|
|
|
ElMessage.success('文件上传并解压成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uploadDialogVisible.value = false
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('文件上传失败')
|
|
|
|
|
|
console.error('上传UI组件文件失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
uploading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDownload = async (row) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await uiComponentApi.downloadUIComponentFile(row.id)
|
|
|
|
|
|
|
|
|
|
|
|
// 创建下载链接
|
|
|
|
|
|
const url = window.URL.createObjectURL(new Blob([response.data]))
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = url
|
2025-12-19 16:57:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据文件类型确定下载文件名
|
|
|
|
|
|
let fileName = row.component_name
|
|
|
|
|
|
if (row.file_path) {
|
|
|
|
|
|
const fileExtension = row.file_path.substring(row.file_path.lastIndexOf('.'))
|
|
|
|
|
|
fileName += fileExtension
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fileName += '.zip' // 默认扩展名
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
link.setAttribute('download', fileName)
|
2025-12-19 11:28:59 +08:00
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('文件下载失败')
|
|
|
|
|
|
console.error('下载UI组件文件失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleViewFolder = async (row) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await uiComponentApi.getUIComponentFolderContent(row.id)
|
|
|
|
|
|
folderTree.value = buildTree(response.data)
|
|
|
|
|
|
currentComponent.value = row
|
|
|
|
|
|
folderDialogVisible.value = true
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('获取文件夹内容失败')
|
|
|
|
|
|
console.error('获取UI组件文件夹内容失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteFolder = (row) => {
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
|
`确定要删除UI组件"${row.component_name}"的文件夹吗?`,
|
|
|
|
|
|
'删除确认',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
).then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await uiComponentApi.deleteUIComponentFolder(row.id)
|
|
|
|
|
|
ElMessage.success('文件夹删除成功')
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('文件夹删除失败')
|
|
|
|
|
|
console.error('删除UI组件文件夹失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
// 用户取消删除
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建树形结构
|
|
|
|
|
|
const buildTree = (files) => {
|
|
|
|
|
|
const tree = []
|
|
|
|
|
|
const pathMap = {}
|
|
|
|
|
|
|
|
|
|
|
|
// 先创建所有节点
|
|
|
|
|
|
files.forEach(file => {
|
|
|
|
|
|
const parts = file.path.split('/')
|
|
|
|
|
|
let currentPath = ''
|
|
|
|
|
|
|
|
|
|
|
|
parts.forEach((part, index) => {
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
|
currentPath = part
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentPath = currentPath + '/' + part
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!pathMap[currentPath]) {
|
|
|
|
|
|
pathMap[currentPath] = {
|
|
|
|
|
|
name: part,
|
|
|
|
|
|
path: file.path,
|
|
|
|
|
|
type: index === parts.length - 1 ? file.type : 'folder',
|
|
|
|
|
|
size: index === parts.length - 1 ? file.size : 0,
|
|
|
|
|
|
children: []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 构建树形结构
|
|
|
|
|
|
Object.keys(pathMap).forEach(path => {
|
|
|
|
|
|
const node = pathMap[path]
|
|
|
|
|
|
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
|
|
|
|
|
|
|
|
|
|
|
if (parentPath && pathMap[parentPath]) {
|
|
|
|
|
|
pathMap[parentPath].children.push(node)
|
|
|
|
|
|
} else if (!parentPath) {
|
|
|
|
|
|
tree.push(node)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return tree
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatFileSize = (bytes) => {
|
|
|
|
|
|
if (bytes === 0) return '0 B'
|
|
|
|
|
|
const k = 1024
|
|
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatSize = (size) => {
|
|
|
|
|
|
return formatFileSize(size)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatDateTime = (dateTime) => {
|
|
|
|
|
|
if (!dateTime) return ''
|
|
|
|
|
|
const date = new Date(dateTime)
|
|
|
|
|
|
return date.toLocaleString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 16:57:49 +08:00
|
|
|
|
// 判断文件是否为ZIP类型
|
|
|
|
|
|
const isZipFile = (file) => {
|
|
|
|
|
|
if (!file) return false
|
|
|
|
|
|
return file.toLowerCase().endsWith('.zip')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断文件路径是否为ZIP类型
|
|
|
|
|
|
const isZipFileFromPath = (path) => {
|
|
|
|
|
|
if (!path) return false
|
|
|
|
|
|
return path.toLowerCase().endsWith('.zip')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 11:28:59 +08:00
|
|
|
|
// 生命周期
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchComponentList()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-12-22 18:32:34 +08:00
|
|
|
|
/* 对话框样式 */
|
2025-12-19 11:28:59 +08:00
|
|
|
|
.dialog-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-tree-node {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-size {
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
2025-12-22 18:32:34 +08:00
|
|
|
|
|
|
|
|
|
|
/* 移动端卡片布局 */
|
|
|
|
|
|
.component-cards {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.component-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-footer {
|
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
|
border-top: 1px solid #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格容器 */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格样式优化 */
|
|
|
|
|
|
:deep(.el-table) {
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table th) {
|
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table td) {
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table tr:hover > td) {
|
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 对话框样式优化 */
|
|
|
|
|
|
:deep(.el-dialog) {
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-dialog__header) {
|
|
|
|
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
|
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-dialog__title) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
max-height: 70vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.card-row {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格在移动端优化 */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table) {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
min-width: 1000px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table th),
|
|
|
|
|
|
:deep(.el-table td) {
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table .cell) {
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 分页组件在移动端优化 */
|
|
|
|
|
|
:deep(.el-pagination) {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__sizes) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__total) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__jump) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 对话框在移动端优化 */
|
|
|
|
|
|
:deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 超小屏幕进一步优化 */
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.component-card {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
min-width: 70px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-19 11:28:59 +08:00
|
|
|
|
</style>
|