2025-12-19 11:28:59 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="ui-components-page">
|
|
|
|
|
|
<el-card class="filter-card">
|
|
|
|
|
|
<el-form :model="filterForm" inline>
|
|
|
|
|
|
<el-form-item label="关键词">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="filterForm.keyword"
|
|
|
|
|
|
placeholder="请输入关键词搜索"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@keyup.enter="handleSearch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="filterForm.is_active" placeholder="请选择状态" clearable>
|
|
|
|
|
|
<el-option label="启用" :value="true" />
|
|
|
|
|
|
<el-option label="禁用" :value="false" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
|
|
|
|
|
<el-button @click="handleReset">重置</el-button>
|
|
|
|
|
|
<el-button type="success" @click="handleCreate">新增UI组件</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-card class="table-card">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
|
:data="componentList"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
border
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="component_code" label="组件编码" width="150" />
|
|
|
|
|
|
<el-table-column prop="component_name" label="组件名称" width="200" />
|
|
|
|
|
|
<el-table-column prop="description" label="描述" 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">已解压</el-tag>
|
|
|
|
|
|
<el-tag v-else-if="row.file_path" type="warning">已上传</el-tag>
|
|
|
|
|
|
<el-tag v-else type="info">未上传</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>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" width="100">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag :type="row.is_active ? 'success' : 'danger'">
|
|
|
|
|
|
{{ 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 }">
|
|
|
|
|
|
{{ formatDateTime(row.created_at) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="320" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<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
|
2025-12-19 16:57:49 +08:00
|
|
|
|
v-if="row.file_path && !row.is_extracted && isZipFileFromPath(row.file_path)"
|
2025-12-19 11:28:59 +08:00
|
|
|
|
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-button
|
|
|
|
|
|
v-if="row.file_path"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
@click="handleDownload(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
下载文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="row.is_extracted"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
@click="handleDeleteFolder(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除文件夹
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-model:current-page="pagination.page"
|
|
|
|
|
|
v-model:page-size="pagination.pageSize"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
:total="pagination.total"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 创建/编辑对话框 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
可以上传整个文件夹,保持原有目录结构。ZIP文件可以自动解压,每个文件不超过100MB。
|
|
|
|
|
|
</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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { uiComponentApi } from '@/api/ui-component'
|
|
|
|
|
|
import { Document, Folder, UploadFilled } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
.ui-components-page {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-card {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-card {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|