Files
tyapi-frontend/src/pages/admin/ui-components/index.vue
2025-12-23 17:17:51 +08:00

1231 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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"
>
<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' : ''">
重置筛选
</el-button>
<el-button :size="isMobile ? 'small' : 'default'" type="primary" @click="handleSearch" :class="isMobile ? 'flex-1' : ''">
应用筛选
</el-button>
<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>
</el-button>
</div>
</template>
</FilterSection>
</template>
<!-- 表格区域 -->
<template #table>
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-12">
<el-loading size="large" />
</div>
<!-- 移动端卡片布局 -->
<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>
<!-- 创建/编辑对话框 -->
<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>
<!-- 编辑时显示当前文件状态 -->
<el-form-item label="当前文件状态" v-if="isEdit && currentComponent">
<div class="file-status-display">
<el-tag v-if="currentComponent.is_extracted" type="success" size="small">已解压</el-tag>
<el-tag v-else-if="currentComponent.file_path" type="warning" size="small">已上传</el-tag>
<el-tag v-else type="info" size="small">未上传</el-tag>
<div v-if="currentComponent.file_path" class="file-path">
<span class="file-label">文件路径:</span>
<span class="file-value">{{ currentComponent.file_path }}</span>
</div>
<div v-if="currentComponent.is_extracted && currentComponent.folder_path" class="folder-path">
<span class="file-label">文件夹路径:</span>
<span class="file-value">{{ currentComponent.folder_path }}</span>
</div>
</div>
</el-form-item>
<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="editUploadRef"
:auto-upload="false"
:multiple="true"
:on-change="handleEditFileChange"
:on-remove="handleEditFileRemove"
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">
可以上传多个文件替换当前文件ZIP文件可以自动解压其他文件类型仅保存
</div>
</template>
</el-upload>
</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'">
可以上传多个文件每个文件不超过100MBZIP文件可以自动解压其他文件类型仅保存
</div>
<div class="el-upload__tip" v-else>
ZIP文件可以自动解压每个文件不超过100MB
</div>
</template>
</el-upload>
</el-form-item>
</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">
文件夹使用压缩为zip格式可以批量上传文件
</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'
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'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, reactive, ref } from 'vue'
// 移动端检测
const { isMobile, isTablet } = useMobileTable()
// 响应式数据
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)
const selectedCreateFile = ref(null)
const selectedCreateFiles = ref([])
const uploadMode = ref('files') // 'files' 或 'folder'
const formRef = ref(null)
const uploadRef = ref(null)
const createUploadRef = ref(null)
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
}
}
// 格式化日期
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()
}
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 {
// 记录删除操作的详细信息
console.log('正在删除UI组件:', {
id: row.id,
name: row.component_name,
code: row.component_code,
file_path: row.file_path,
is_extracted: row.is_extracted,
folder_path: row.folder_path
})
await uiComponentApi.deleteUIComponent(row.id)
ElMessage.success('删除成功')
fetchComponentList()
} catch (error) {
console.error('删除UI组件失败:', error)
// 显示更详细的错误信息
const errorMsg = error.response?.data?.message || error.message || '删除失败'
ElMessage.error(`删除失败: ${errorMsg}`)
}
}).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 {
// 检查是否上传了文件
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('创建成功')
}
}
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
})
selectedCreateFile.value = null
selectedCreateFiles.value = []
uploadMode.value = 'files' // 重置为文件上传模式
if (formRef.value) {
formRef.value.resetFields()
}
if (createUploadRef.value) {
createUploadRef.value.clearFiles()
}
}
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
}
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 // 保留兼容性
}
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
// 根据文件类型确定下载文件名
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)
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 {
// 记录删除操作的详细信息
console.log('正在删除UI组件文件夹:', {
id: row.id,
name: row.component_name,
code: row.component_code,
file_path: row.file_path,
is_extracted: row.is_extracted,
folder_path: row.folder_path
})
await uiComponentApi.deleteUIComponentFolder(row.id)
ElMessage.success('文件夹删除成功')
fetchComponentList()
} catch (error) {
console.error('删除UI组件文件夹失败:', error)
// 显示更详细的错误信息
const errorMsg = error.response?.data?.message || error.message || '文件夹删除失败'
ElMessage.error(`文件夹删除失败: ${errorMsg}`)
}
}).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')
}
// 判断文件是否为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')
}
// 生命周期
onMounted(() => {
fetchComponentList()
})
</script>
<style scoped>
/* 对话框样式 */
.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;
}
/* 移动端卡片布局 */
.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;
}
}
</style>