Files
lgp-admin-plus/apps/web-antd/src/views/code-generation/components/json-import.vue
2025-11-28 14:10:43 +08:00

680 lines
17 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.
<script setup lang="ts">
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
import { computed, onMounted, ref, watch } from 'vue';
import { Button, Card, Input, message, Select, Tabs, Upload } from 'ant-design-vue';
import { JsonViewer } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { codeGenerationForm } from '#/views/code-generation/config/form';
import type { CodeGenData, FieldType } from '../utils/data-transformer';
import { transformJsonToStandard, validateData } from '../utils/data-transformer';
import {
deleteDraft,
listDrafts,
loadDraft,
saveDraft,
type GenerationMode,
} from '../utils/draft-manager';
const mode: GenerationMode = 'json';
const emit = defineEmits<{
generate: [data: CodeGenData];
back: [];
}>();
// 表单相关
const [Form, formApi] = useVbenForm(codeGenerationForm);
const formData = ref<Record<string, any>>({});
// JSON输入
const jsonText = ref('');
const jsonError = ref('');
const parsedData = ref<CodeGenData | null>(null);
const activeTab = ref('text');
// 文件上传相关
const fileList = ref<UploadFile[]>([]);
const uploadError = ref('');
// 草稿相关
const draftId = ref<string | null>(null);
const drafts = ref<ReturnType<typeof listDrafts>>([]);
const selectedDraft = ref<string>('');
// 预览数据
const previewData = computed(() => {
if (parsedData.value) {
return {
...parsedData.value,
field: parsedData.value.field || [],
};
}
return null;
});
// 加载草稿列表
const loadDraftList = () => {
drafts.value = listDrafts(mode);
};
// 加载草稿
const handleLoadDraft = (id: string) => {
const draft = loadDraft(mode, id);
if (draft) {
draftId.value = id;
jsonText.value = JSON.stringify(draft.data, null, 2);
selectedDraft.value = id;
parseJson();
}
};
// 手动保存草稿
const handleSaveDraft = () => {
if (!parsedData.value) {
message.warning('请先导入或填写JSON数据');
return;
}
try {
const id = saveDraft(mode, parsedData.value, undefined, draftId.value || undefined);
draftId.value = id;
loadDraftList();
message.success('草稿已保存');
} catch (error) {
console.error('保存草稿失败:', error);
message.error('保存草稿失败');
}
};
// 解析JSON
const parseJson = () => {
jsonError.value = '';
if (!jsonText.value.trim()) {
parsedData.value = null;
return;
}
try {
const json = JSON.parse(jsonText.value);
const transformed = transformJsonToStandard(json);
parsedData.value = transformed;
// 更新表单数据
formData.value = {
class_name: transformed.class_name,
class_comment: transformed.class_comment,
icon: transformed.icon,
sort: transformed.sort,
pid: transformed.pid,
};
formApi.setValues(formData.value);
} catch (error: any) {
jsonError.value = error.message || 'JSON格式错误';
parsedData.value = null;
}
};
// 监听JSON文本变化
watch(jsonText, () => {
parseJson();
});
// 文件上传前验证和处理
const handleBeforeUpload = (file: File): boolean => {
uploadError.value = '';
// 验证文件类型
const isValidType = file.type === 'application/json' || file.name.endsWith('.json');
if (!isValidType) {
uploadError.value = '只能上传 JSON 格式文件!';
message.error('只能上传 JSON 格式文件!');
return false;
}
// 验证文件大小(限制 10MB)
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
uploadError.value = '文件大小不能超过 10MB!';
message.error('文件大小不能超过 10MB!');
return false;
}
// 添加到文件列表
const uploadFile: UploadFile = {
uid: `${Date.now()}-${file.name}`,
name: file.name,
status: 'uploading',
size: file.size,
type: file.type,
originFileObj: file,
};
fileList.value = [uploadFile];
// 读取文件内容
const reader = new FileReader();
reader.onload = (e) => {
try {
const text = e.target?.result as string;
// 验证是否为有效的 JSON
const json = JSON.parse(text);
// 更新 JSON 文本(格式化为美观的 JSON)
jsonText.value = JSON.stringify(json, null, 2);
// 更新文件状态为成功
uploadFile.status = 'done';
fileList.value = [uploadFile];
// 自动切换到文本输入标签页
activeTab.value = 'text';
// 清空错误信息
uploadError.value = '';
message.success('文件上传并解析成功!');
} catch (error: any) {
uploadError.value = `JSON 解析失败:${error.message || '文件格式不正确'}`;
message.error('JSON 解析失败,请检查文件格式');
uploadFile.status = 'error';
fileList.value = [uploadFile];
}
};
reader.onerror = () => {
uploadError.value = '文件读取失败,请重试';
message.error('文件读取失败');
uploadFile.status = 'error';
fileList.value = [uploadFile];
};
reader.readAsText(file, 'UTF-8');
// 返回 false 阻止自动上传(因为我们手动处理)
return false;
};
// 移除文件
const handleRemoveFile = () => {
fileList.value = [];
uploadError.value = '';
if (!jsonText.value) {
parsedData.value = null;
}
};
// 格式化JSON
const formatJson = () => {
try {
const json = JSON.parse(jsonText.value);
jsonText.value = JSON.stringify(json, null, 2);
message.success('格式化成功');
} catch (error) {
message.error('JSON格式错误,无法格式化');
}
};
// 清空
const clearJson = () => {
jsonText.value = '';
parsedData.value = null;
formData.value = {};
formApi.resetFields();
draftId.value = null;
selectedDraft.value = '';
fileList.value = [];
uploadError.value = '';
};
// 生成代码
const handleGenerate = async () => {
if (!parsedData.value) {
message.warning('请先导入或填写JSON数据');
return;
}
// 验证数据
const validation = validateData(parsedData.value);
if (!validation.valid) {
message.error(validation.message);
return;
}
// 合并表单数据(用户可能修改了基础信息)
const formValues = await formApi.getValues();
const finalData: CodeGenData = {
...parsedData.value,
class_name: formValues.class_name || parsedData.value.class_name,
class_comment: formValues.class_comment || parsedData.value.class_comment,
icon: formValues.icon || parsedData.value.icon,
sort: formValues.sort ?? parsedData.value.sort,
pid: formValues.pid ?? parsedData.value.pid,
};
emit('generate', finalData);
};
// 删除草稿
const handleDeleteDraft = (id: string) => {
if (deleteDraft(mode, id)) {
message.success('草稿已删除');
loadDraftList();
if (draftId.value === id) {
clearJson();
}
}
};
onMounted(() => {
loadDraftList();
});
</script>
<template>
<div class="json-import">
<!-- 顶部操作栏 -->
<Card class="mb-4">
<template #title>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Button type="text" @click="emit('back')">
<svg
class="mr-2 inline-block"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.75 3.5L5.25 7L8.75 10.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
返回
</Button>
<span class="text-lg font-semibold">JSON导入模式</span>
</div>
<div class="flex items-center gap-2">
<Select
v-model:value="selectedDraft"
placeholder="选择草稿"
style="width: 200px"
allow-clear
@change="handleLoadDraft"
>
<Select.Option
v-for="draft in drafts"
:key="draft.id"
:value="draft.id"
>
<div class="flex items-center justify-between">
<span>{{ draft.name }}</span>
<Button
type="text"
danger
size="small"
@click.stop="handleDeleteDraft(draft.id)"
>
<i class="fa-solid fa-trash"></i>
</Button>
</div>
</Select.Option>
</Select>
</div>
</div>
</template>
<Tabs v-model:activeKey="activeTab">
<Tabs.TabPane key="text" tab="文本输入">
<div class="mb-4">
<Input.TextArea
v-model:value="jsonText"
:rows="12"
placeholder="请粘贴JSON数据..."
:class="{ 'border-red-500': jsonError }"
class="font-mono text-sm"
/>
<div v-if="jsonError" class="mt-2 text-red-500 text-sm">
<i class="fa-solid fa-circle-exclamation mr-1"></i>{{ jsonError }}
</div>
<div class="mt-3 flex gap-2">
<Button @click="formatJson">
<i class="fa-solid fa-code mr-2"></i>格式化
</Button>
<Button @click="clearJson">
<i class="fa-solid fa-trash mr-2"></i>清空
</Button>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="file" tab="文件上传">
<div class="upload-container">
<Upload.Dragger
:file-list="fileList"
:before-upload="handleBeforeUpload"
accept=".json,application/json"
:show-upload-list="true"
@remove="handleRemoveFile"
class="upload-dragger"
>
<div class="upload-content">
<div class="upload-icon">
<i class="fa-solid fa-cloud-arrow-up text-4xl text-primary"></i>
</div>
<p class="upload-text">
点击或拖拽文件到此区域上传
</p>
<p class="upload-hint">
支持上传 JSON 格式文件
</p>
</div>
</Upload.Dragger>
<div v-if="uploadError" class="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
<i class="fa-solid fa-circle-exclamation mr-2"></i>{{ uploadError }}
</div>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
<!-- 内容区域 -->
<div v-if="parsedData" class="content-area mb-24 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card title="基础信息" class="shadow-sm">
<Form />
</Card>
<Card title="字段预览" class="shadow-sm">
<div class="max-h-[500px] overflow-auto">
<JsonViewer
:value="previewData"
:expand-depth="2"
copyable
boxed
/>
</div>
<div class="mt-4 flex items-center gap-2 text-sm text-gray-500">
<i class="fa-solid fa-list mr-1"></i>
<span>共 {{ parsedData.field?.length || 0 }} 个字段</span>
</div>
</Card>
</div>
<!-- 固定底部操作栏 -->
<div class="fixed-bottom-bar">
<div class="fixed-bottom-bar-wrapper">
<div class="fixed-bottom-bar-content">
<!-- 左侧状态信息 -->
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-full transition-all"
:class="
parsedData
? 'bg-primary/10 text-primary'
: 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600'
"
>
<i
:class="parsedData ? 'fa-solid fa-check-circle' : 'fa-solid fa-circle-info'"
></i>
</div>
<div>
<div
class="text-sm font-medium"
:class="
parsedData
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-500 dark:text-gray-400'
"
>
<span v-if="parsedData">
已解析 <span class="text-primary font-semibold">{{ parsedData.field?.length || 0 }}</span> 个字段
</span>
<span v-else>请先导入JSON数据</span>
</div>
<div class="text-xs text-gray-400 dark:text-gray-500">
{{ parsedData ? '数据已就绪,可以生成代码' : '等待数据导入...' }}
</div>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center gap-2">
<Button
size="large"
class="action-btn-secondary"
@click="clearJson"
>
<i class="fa-solid fa-trash mr-2"></i>清空
</Button>
<Button
size="large"
class="action-btn-secondary"
:disabled="!parsedData"
@click="handleSaveDraft"
>
<i class="fa-solid fa-save mr-2"></i>保存草稿
</Button>
<Button
type="primary"
size="large"
class="action-btn-primary"
:disabled="!parsedData"
@click="handleGenerate"
>
<i class="fa-solid fa-code mr-2"></i>生成代码
</Button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.json-import {
animation: fade-slide-enter 0.3s ease-out;
padding-bottom: 120px; /* 为固定底部栏留出空间 */
}
.content-area {
min-height: 400px;
}
/* 优化卡片样式 */
:deep(.ant-card) {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
:deep(.ant-card:hover) {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.dark :deep(.ant-card) {
border-color: rgba(255, 255, 255, 0.1);
}
/* 固定底部操作栏 - 现代化设计 */
.fixed-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
padding: 0 24px 24px;
pointer-events: none;
}
.fixed-bottom-bar-wrapper {
max-width: 1400px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 16px;
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: all;
}
.dark .fixed-bottom-bar-wrapper {
background: rgba(31, 41, 55, 0.98);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.fixed-bottom-bar-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
gap: 24px;
}
.action-btn-secondary {
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
}
.action-btn-secondary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.action-btn-primary {
border-radius: 8px;
font-weight: 600;
padding: 0 24px;
height: 44px;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
transition: all 0.2s ease;
}
.action-btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
}
.action-btn-primary:active:not(:disabled) {
transform: translateY(0);
}
@keyframes fade-slide-enter {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 文件上传样式优化 */
.upload-container {
padding: 8px 0;
}
.upload-dragger {
border-radius: 12px;
border: 2px dashed #d9d9d9;
background: #fafafa;
transition: all 0.3s ease;
}
.upload-dragger:hover {
border-color: #1890ff;
background: #f0f7ff;
}
.dark .upload-dragger {
background: rgba(255, 255, 255, 0.02);
border-color: rgba(255, 255, 255, 0.2);
}
.dark .upload-dragger:hover {
border-color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
.upload-content {
padding: 40px 20px;
text-align: center;
}
.upload-icon {
margin-bottom: 16px;
opacity: 0.8;
transition: all 0.3s ease;
}
.upload-dragger:hover .upload-icon {
opacity: 1;
transform: translateY(-4px);
}
.upload-text {
font-size: 16px;
font-weight: 500;
color: #262626;
margin-bottom: 8px;
}
.dark .upload-text {
color: #fff;
}
.upload-hint {
font-size: 14px;
color: #8c8c8c;
}
.dark .upload-hint {
color: #8c8c8c;
}
/* 响应式优化 */
@media (max-width: 768px) {
.fixed-bottom-bar {
padding: 0 16px 16px;
}
.fixed-bottom-bar-wrapper {
border-radius: 12px;
}
.fixed-bottom-bar-content {
flex-direction: column;
align-items: stretch;
padding: 16px;
gap: 16px;
}
.fixed-bottom-bar-content > div:first-child {
justify-content: center;
}
.fixed-bottom-bar-content > div:last-child {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.fixed-bottom-bar-content > div:last-child .action-btn-primary {
grid-column: 1 / -1;
}
}
</style>