新增多种生成方式
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
477
apps/web-antd/src/views/code-generation/components/ai-prompt.vue
Normal file
477
apps/web-antd/src/views/code-generation/components/ai-prompt.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Button, Card, Input, message, Select, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { getMenuTreeOption } from '#/views/system/menu/api';
|
||||
|
||||
import type { CodeGenData } from '../utils/data-transformer';
|
||||
import { transformAiToStandard } from '../utils/data-transformer';
|
||||
import {
|
||||
deleteDraft,
|
||||
listDrafts,
|
||||
loadDraft,
|
||||
saveDraft,
|
||||
type GenerationMode,
|
||||
} from '../utils/draft-manager';
|
||||
|
||||
const mode: GenerationMode = 'ai';
|
||||
|
||||
const emit = defineEmits<{
|
||||
generate: [data: CodeGenData];
|
||||
back: [];
|
||||
}>();
|
||||
|
||||
// 表单数据
|
||||
const moduleName = ref('');
|
||||
const description = ref('');
|
||||
const pid = ref<number>(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 菜单树数据
|
||||
const menuTreeData = ref<any[]>([]);
|
||||
const menuTreeLoading = ref(false);
|
||||
|
||||
// 草稿相关
|
||||
const draftId = ref<string | null>(null);
|
||||
const drafts = ref<ReturnType<typeof listDrafts>>([]);
|
||||
const selectedDraft = ref<string>('');
|
||||
|
||||
// 加载草稿列表
|
||||
const loadDraftList = () => {
|
||||
drafts.value = listDrafts(mode);
|
||||
};
|
||||
|
||||
// 加载草稿
|
||||
const handleLoadDraft = (id: string) => {
|
||||
const draft = loadDraft(mode, id);
|
||||
if (draft && draft.data) {
|
||||
draftId.value = id;
|
||||
selectedDraft.value = id;
|
||||
moduleName.value = draft.data.moduleName || '';
|
||||
description.value = draft.data.description || '';
|
||||
pid.value = draft.data.pid ?? 0;
|
||||
message.success('草稿已加载');
|
||||
}
|
||||
};
|
||||
|
||||
// 手动保存草稿
|
||||
const handleSaveDraft = () => {
|
||||
if (!moduleName.value.trim() && !description.value.trim()) {
|
||||
message.warning('请先填写内容');
|
||||
return;
|
||||
}
|
||||
|
||||
const draftData = {
|
||||
moduleName: moduleName.value,
|
||||
description: description.value,
|
||||
pid: pid.value,
|
||||
};
|
||||
|
||||
try {
|
||||
const id = saveDraft(mode, draftData, undefined, draftId.value || undefined);
|
||||
draftId.value = id;
|
||||
loadDraftList();
|
||||
message.success('草稿已保存');
|
||||
} catch (error) {
|
||||
console.error('保存草稿失败:', error);
|
||||
message.error('保存草稿失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 发送AI请求
|
||||
const handleSend = async () => {
|
||||
if (!moduleName.value.trim()) {
|
||||
message.warning('请输入模块名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description.value.trim()) {
|
||||
message.warning('请输入功能描述');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 模拟AI请求(实际开发中替换为真实API)
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
message.info('正在开发中~');
|
||||
// TODO: 实际开发时,这里应该调用AI API
|
||||
// const aiData = await aiGenerationApi({
|
||||
// moduleName: moduleName.value,
|
||||
// description: description.value,
|
||||
// pid: pid.value,
|
||||
// });
|
||||
// 处理AI返回的数据
|
||||
// const standardData = transformAiToStandard(aiData);
|
||||
// emit('generate', standardData);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 删除草稿
|
||||
const handleDeleteDraft = (id: string) => {
|
||||
if (deleteDraft(mode, id)) {
|
||||
message.success('草稿已删除');
|
||||
loadDraftList();
|
||||
if (draftId.value === id) {
|
||||
draftId.value = null;
|
||||
selectedDraft.value = '';
|
||||
moduleName.value = '';
|
||||
description.value = '';
|
||||
pid.value = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenuTree = async () => {
|
||||
menuTreeLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuTreeOption();
|
||||
menuTreeData.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('加载菜单树失败:', error);
|
||||
message.error('加载菜单树失败');
|
||||
} finally {
|
||||
menuTreeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDraftList();
|
||||
loadMenuTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-prompt">
|
||||
<!-- 顶部操作栏 -->
|
||||
<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')">
|
||||
<i class="fa-solid fa-arrow-left mr-2"></i>返回
|
||||
</Button>
|
||||
<span class="text-lg font-semibold">AI提示词模式</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>
|
||||
</Card>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Card class="content-area mb-24 shadow-sm">
|
||||
<div class="mb-6 rounded-lg bg-gradient-to-r from-blue-50 to-purple-50 p-4 dark:from-blue-900/20 dark:to-purple-900/20">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<i class="fa-solid fa-sparkles text-primary"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-1 font-semibold text-gray-900 dark:text-gray-100">AI智能生成</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
只需简单描述模块信息,AI将自动为您生成完整的代码结构
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fa-solid fa-cube text-primary"></i>
|
||||
<span>模块名</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
v-model:value="moduleName"
|
||||
placeholder="请输入模块名称,例如:用户管理"
|
||||
size="large"
|
||||
class="rounded-lg"
|
||||
/>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
建议使用简洁明了的名称,便于理解模块功能
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fa-solid fa-file-lines text-primary"></i>
|
||||
<span>功能描述</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Input.TextArea
|
||||
v-model:value="description"
|
||||
:rows="8"
|
||||
placeholder="请简单描述模块的功能,例如:管理用户的基本信息,包括用户的增删改查功能"
|
||||
show-count
|
||||
:maxlength="500"
|
||||
class="rounded-lg"
|
||||
/>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
详细描述有助于AI更准确地生成代码结构
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fa-solid fa-sitemap text-primary"></i>
|
||||
<span>父级菜单</span>
|
||||
</label>
|
||||
<TreeSelect
|
||||
v-model:value="pid"
|
||||
:tree-data="menuTreeData"
|
||||
:field-names="{ label: 'title', value: 'id', children: 'children' }"
|
||||
:loading="menuTreeLoading"
|
||||
placeholder="请选择父级菜单(可选)"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
class="rounded-lg"
|
||||
tree-default-expand-all
|
||||
allow-clear
|
||||
/>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
选择后,生成的菜单将作为该菜单的子菜单
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 固定底部操作栏 -->
|
||||
<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="
|
||||
moduleName && description
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600'
|
||||
"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
moduleName && description
|
||||
? 'fa-solid fa-sparkles'
|
||||
: 'fa-solid fa-circle-info'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-sm font-medium"
|
||||
:class="
|
||||
moduleName && description
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
"
|
||||
>
|
||||
<span v-if="moduleName && description">
|
||||
信息已完整,可以发送AI请求
|
||||
</span>
|
||||
<span v-else>请填写模块名和功能描述</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{
|
||||
moduleName && description
|
||||
? 'AI将根据您的描述生成代码结构'
|
||||
: '完整填写后即可使用AI生成功能'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Button size="large" class="action-btn-secondary" @click="emit('back')">
|
||||
<i class="fa-solid fa-times mr-2"></i>取消
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
class="action-btn-secondary"
|
||||
:disabled="!moduleName && !description"
|
||||
@click="handleSaveDraft"
|
||||
>
|
||||
<i class="fa-solid fa-save mr-2"></i>保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn-primary"
|
||||
:loading="loading"
|
||||
:disabled="!moduleName.trim() || !description.trim()"
|
||||
@click="handleSend"
|
||||
>
|
||||
<i class="fa-solid fa-paper-plane mr-2"></i>发送AI请求
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-prompt {
|
||||
animation: fade-slide-enter 0.3s ease-out;
|
||||
padding-bottom: 120px; /* 为固定底部栏留出空间 */
|
||||
}
|
||||
|
||||
.content-area {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* 优化卡片样式 */
|
||||
: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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@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>
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
<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')">
|
||||
<i class="fa-solid fa-arrow-left mr-2"></i>返回
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Button, Card, message, Select, Steps } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { codeGenerationForm } from '#/views/code-generation/config/form';
|
||||
|
||||
import CodeGenerationFiledTable from './fileds.vue';
|
||||
import type { CodeGenData } from '../utils/data-transformer';
|
||||
import { transformManualToStandard, validateData } from '../utils/data-transformer';
|
||||
import {
|
||||
deleteDraft,
|
||||
listDrafts,
|
||||
loadDraft,
|
||||
saveDraft,
|
||||
type GenerationMode,
|
||||
} from '../utils/draft-manager';
|
||||
|
||||
const mode: GenerationMode = 'manual';
|
||||
|
||||
const emit = defineEmits<{
|
||||
generate: [data: CodeGenData];
|
||||
back: [];
|
||||
}>();
|
||||
|
||||
// 表单相关
|
||||
const [Form, formApi] = useVbenForm(codeGenerationForm);
|
||||
const tableRef = ref<InstanceType<typeof CodeGenerationFiledTable> | null>(null);
|
||||
|
||||
// 步骤
|
||||
const stepsCurrent = ref(0);
|
||||
const stepsItems = [
|
||||
{
|
||||
title: '基础信息',
|
||||
description: '类和菜单的基础信息',
|
||||
},
|
||||
{
|
||||
title: '字段信息',
|
||||
description: '设计数据库字段信息',
|
||||
},
|
||||
];
|
||||
|
||||
// 草稿相关
|
||||
const draftId = ref<string | null>(null);
|
||||
const drafts = ref<ReturnType<typeof listDrafts>>([]);
|
||||
const selectedDraft = ref<string>('');
|
||||
|
||||
// 加载草稿列表
|
||||
const loadDraftList = () => {
|
||||
drafts.value = listDrafts(mode);
|
||||
};
|
||||
|
||||
// 加载草稿
|
||||
const handleLoadDraft = (id: string) => {
|
||||
const draft = loadDraft(mode, id);
|
||||
if (draft && draft.data) {
|
||||
draftId.value = id;
|
||||
selectedDraft.value = id;
|
||||
|
||||
// 恢复表单数据
|
||||
if (draft.data.formData) {
|
||||
formApi.setValues(draft.data.formData);
|
||||
}
|
||||
|
||||
// 恢复字段数据
|
||||
if (draft.data.fields && tableRef.value) {
|
||||
tableRef.value.tableData = draft.data.fields;
|
||||
}
|
||||
|
||||
message.success('草稿已加载');
|
||||
}
|
||||
};
|
||||
|
||||
// 手动保存草稿
|
||||
const handleSaveDraft = () => {
|
||||
formApi.getValues().then((formData) => {
|
||||
const fields = tableRef.value?.tableData || [];
|
||||
const draftData = {
|
||||
formData,
|
||||
fields,
|
||||
};
|
||||
|
||||
try {
|
||||
const id = saveDraft(mode, draftData, undefined, draftId.value || undefined);
|
||||
draftId.value = id;
|
||||
loadDraftList();
|
||||
message.success('草稿已保存');
|
||||
} catch (error) {
|
||||
console.error('保存草稿失败:', error);
|
||||
message.error('保存草稿失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 下一步
|
||||
const handleNext = async () => {
|
||||
if (stepsCurrent.value === 0) {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
stepsCurrent.value++;
|
||||
saveCurrentDraft();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 上一步
|
||||
const handlePrev = () => {
|
||||
if (stepsCurrent.value > 0) {
|
||||
stepsCurrent.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成代码
|
||||
const handleGenerate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
message.warning('请先完成基础信息填写');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = await formApi.getValues();
|
||||
const fields = tableRef.value?.tableData || [];
|
||||
|
||||
if (!fields || fields.length === 0) {
|
||||
message.warning('请至少添加一个字段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换为标准格式
|
||||
const standardData = transformManualToStandard(formData, fields);
|
||||
|
||||
// 验证数据
|
||||
const validation = validateData(standardData);
|
||||
if (!validation.valid) {
|
||||
message.error(validation.message);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('generate', standardData);
|
||||
};
|
||||
|
||||
// 删除草稿
|
||||
const handleDeleteDraft = (id: string) => {
|
||||
if (deleteDraft(mode, id)) {
|
||||
message.success('草稿已删除');
|
||||
loadDraftList();
|
||||
if (draftId.value === id) {
|
||||
draftId.value = null;
|
||||
selectedDraft.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDraftList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="manual-form">
|
||||
<!-- 顶部操作栏 -->
|
||||
<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')">
|
||||
<i class="fa-solid fa-arrow-left mr-2"></i>返回
|
||||
</Button>
|
||||
<span class="text-lg font-semibold">手动填写模式</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>
|
||||
</Card>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<Card class="content-area mb-24 shadow-sm">
|
||||
<Steps :current="stepsCurrent" :items="stepsItems" class="mb-8" />
|
||||
|
||||
<div class="min-h-[500px]">
|
||||
<div v-show="stepsCurrent === 0" class="fade-in">
|
||||
<Form />
|
||||
</div>
|
||||
<div v-show="stepsCurrent === 1" class="fade-in">
|
||||
<CodeGenerationFiledTable ref="tableRef" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 固定底部操作栏 -->
|
||||
<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 bg-primary/10 text-primary">
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
步骤
|
||||
<span class="text-primary font-semibold">{{ stepsCurrent + 1 }}</span>
|
||||
/
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ stepsItems.length }}</span>
|
||||
<span class="ml-2">{{ stepsItems[stepsCurrent].title }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ stepsItems[stepsCurrent].description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="stepsCurrent > 0"
|
||||
size="large"
|
||||
class="action-btn-secondary"
|
||||
@click="handlePrev"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-left mr-2"></i>上一步
|
||||
</Button>
|
||||
<Button size="large" class="action-btn-secondary" @click="handleSaveDraft">
|
||||
<i class="fa-solid fa-save mr-2"></i>保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
v-if="stepsCurrent < stepsItems.length - 1"
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn-primary"
|
||||
@click="handleNext"
|
||||
>
|
||||
下一步
|
||||
<i class="fa-solid fa-arrow-right ml-2"></i>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="stepsCurrent === stepsItems.length - 1"
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn-primary"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
<i class="fa-solid fa-code mr-2"></i>生成代码
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.manual-form {
|
||||
animation: fade-slide-enter 0.3s ease-out;
|
||||
padding-bottom: 120px; /* 为固定底部栏留出空间 */
|
||||
}
|
||||
|
||||
.content-area {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* 优化卡片样式 */
|
||||
: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);
|
||||
}
|
||||
|
||||
/* 优化步骤条样式 */
|
||||
:deep(.ant-steps) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 固定底部操作栏 - 现代化设计 */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@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>
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
<script setup lang="ts">
|
||||
export type GenerationMode = 'json' | 'manual' | 'ai';
|
||||
|
||||
interface ModeOption {
|
||||
mode: GenerationMode;
|
||||
title: string;
|
||||
description: string;
|
||||
iconColor: string;
|
||||
borderGradient: string;
|
||||
}
|
||||
|
||||
const modes: ModeOption[] = [
|
||||
{
|
||||
mode: 'json',
|
||||
title: 'JSON导入',
|
||||
description: '直接提供JSON数据,快速生成代码',
|
||||
iconColor: '#6366f1',
|
||||
borderGradient:
|
||||
'linear-gradient(90deg, #6366f1, #8b5cf6, #a855f7, #8b5cf6, #6366f1)',
|
||||
},
|
||||
{
|
||||
mode: 'manual',
|
||||
title: '手动填写',
|
||||
description: '逐步填写表单,精确控制每个字段',
|
||||
iconColor: '#f59e0b',
|
||||
borderGradient:
|
||||
'linear-gradient(90deg, #f59e0b, #f97316, #fb923c, #f97316, #f59e0b)',
|
||||
},
|
||||
{
|
||||
mode: 'ai',
|
||||
title: 'AI提示词',
|
||||
description: '使用AI智能生成代码结构(开发中)',
|
||||
iconColor: '#10b981',
|
||||
borderGradient:
|
||||
'linear-gradient(90deg, #10b981, #059669, #34d399, #059669, #10b981)',
|
||||
},
|
||||
];
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [mode: GenerationMode];
|
||||
}>();
|
||||
|
||||
const handleSelect = (mode: GenerationMode) => {
|
||||
emit('select', mode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mode-selector">
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
<div
|
||||
v-for="(item, index) in modes"
|
||||
:key="item.mode"
|
||||
class="mode-card enter-y group relative overflow-hidden"
|
||||
:style="{ animationDelay: `${index * 0.15}s` }"
|
||||
:data-mode="item.mode"
|
||||
@click="handleSelect(item.mode)"
|
||||
>
|
||||
|
||||
<!-- 装饰背景 -->
|
||||
<div class="mode-card-decoration">
|
||||
<!-- JSON模式装饰:网格 -->
|
||||
<div v-if="item.mode === 'json'" class="json-decoration">
|
||||
<svg
|
||||
class="absolute inset-0 h-full w-full opacity-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="grid-json"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path
|
||||
d="M 20 0 L 0 0 0 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid-json)" />
|
||||
</svg>
|
||||
<!-- 浮动圆点 -->
|
||||
<div class="absolute right-4 top-4 h-2 w-2 rounded-full bg-current opacity-20"></div>
|
||||
<div class="absolute bottom-6 left-6 h-1.5 w-1.5 rounded-full bg-current opacity-30"></div>
|
||||
<div class="absolute right-8 top-1/2 h-1 w-1 rounded-full bg-current opacity-25"></div>
|
||||
</div>
|
||||
|
||||
<!-- 手动模式装饰:线条 -->
|
||||
<div v-else-if="item.mode === 'manual'" class="manual-decoration">
|
||||
<svg
|
||||
class="absolute inset-0 h-full w-full opacity-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="lines-manual"
|
||||
width="30"
|
||||
height="30"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="30"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<line
|
||||
x1="30"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#lines-manual)" />
|
||||
</svg>
|
||||
<!-- 几何图形 -->
|
||||
<div class="absolute right-6 top-6 h-8 w-8 rotate-45 border border-current opacity-10"></div>
|
||||
<div class="absolute bottom-8 left-8 h-6 w-6 rounded-full border border-current opacity-15"></div>
|
||||
</div>
|
||||
|
||||
<!-- AI模式装饰:点阵 -->
|
||||
<div v-else-if="item.mode === 'ai'" class="ai-decoration">
|
||||
<svg
|
||||
class="absolute inset-0 h-full w-full opacity-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="dots-ai"
|
||||
width="25"
|
||||
height="25"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle cx="12.5" cy="12.5" r="1.5" fill="currentColor" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#dots-ai)" />
|
||||
</svg>
|
||||
<!-- 星形装饰 -->
|
||||
<div class="absolute right-5 top-5">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
class="opacity-20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M8 0L9.8 5.5L15.5 5.5L10.9 8.9L12.7 14.4L8 11L3.3 14.4L5.1 8.9L0.5 5.5L6.2 5.5L8 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute bottom-7 left-7">
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 16 16"
|
||||
class="opacity-15"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M8 0L9.8 5.5L15.5 5.5L10.9 8.9L12.7 14.4L8 11L3.3 14.4L5.1 8.9L0.5 5.5L6.2 5.5L8 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="relative z-10 flex h-full flex-col items-center justify-center text-center">
|
||||
<!-- 图标容器 -->
|
||||
<div
|
||||
class="mb-6 flex h-20 w-20 items-center justify-center rounded-2xl transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg"
|
||||
:style="{
|
||||
backgroundColor: item.iconColor + '15',
|
||||
border: `2px solid ${item.iconColor}40`,
|
||||
}"
|
||||
>
|
||||
<!-- JSON图标 -->
|
||||
<svg
|
||||
v-if="item.mode === 'json'"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ color: item.iconColor }"
|
||||
>
|
||||
<path
|
||||
d="M12 8C10.8954 8 10 8.89543 10 10V38C10 39.1046 10.8954 40 12 40H36C37.1046 40 38 39.1046 38 38V10C38 8.89543 37.1046 8 36 8H12Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M14 14H34M14 20H34M14 26H28M14 32H24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M18 14V20M24 20V26M18 26V32"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="32" cy="28" r="3" fill="currentColor" />
|
||||
<circle cx="36" cy="32" r="2" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
<!-- 手动填写图标 -->
|
||||
<svg
|
||||
v-else-if="item.mode === 'manual'"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ color: item.iconColor }"
|
||||
>
|
||||
<path
|
||||
d="M24 8L30 14H36C37.1046 14 38 14.8954 38 16V36C38 37.1046 37.1046 38 36 38H12C10.8954 38 10 37.1046 10 36V12C10 10.8954 10.8954 10 12 10H18L24 8Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M24 8V14H30"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16 22H32M16 28H28M16 34H24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M20 18C20 19.1046 19.1046 20 18 20C16.8954 20 16 19.1046 16 18C16 16.8954 16.8954 16 18 16C19.1046 16 20 16.8954 20 18Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- AI图标 -->
|
||||
<svg
|
||||
v-else-if="item.mode === 'ai'"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ color: item.iconColor }"
|
||||
>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="16"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M20 18C20 19.1046 19.1046 20 18 20C16.8954 20 16 19.1046 16 18C16 16.8954 16.8954 16 18 16C19.1046 16 20 16.8954 20 18Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M32 18C32 19.1046 31.1046 20 30 20C28.8954 20 28 19.1046 28 18C28 16.8954 28.8954 16 30 16C31.1046 16 32 16.8954 32 18Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M24 28C26.2091 28 28 26.2091 28 24C28 21.7909 26.2091 20 24 20C21.7909 20 20 21.7909 20 24C20 26.2091 21.7909 28 24 28Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18 32C18 32 20 30 24 30C28 30 30 32 30 32"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M16 14L18 12M30 12L32 14M32 34L30 36M18 36L16 34"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h3 class="mb-3 text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<div
|
||||
class="mt-4 flex translate-y-2 items-center gap-2 text-sm font-medium opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100"
|
||||
:style="{ color: item.iconColor }"
|
||||
>
|
||||
<span>点击开始</span>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 12L10 8L6 4"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mode-selector {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
min-height: 320px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* 暗色模式卡片背景 */
|
||||
.dark .mode-card {
|
||||
background: #1f2937;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mode-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mode-card:active {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* 流光边框 - 使用圆锥渐变旋转效果 */
|
||||
.mode-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 18px;
|
||||
padding: 3px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mode-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
background: #ffffff;
|
||||
border-radius: 15px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 暗色模式遮挡层背景 */
|
||||
.dark .mode-card::after {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
/* 防止闪烁的关键属性 */
|
||||
transform: translateZ(0);
|
||||
isolation: isolate;
|
||||
/* 添加边框防止内容溢出 */
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* 修复白色闪烁的关键 - 完全重写 ::before */
|
||||
.mode-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -5px; /* 增大边框宽度 */
|
||||
opacity: 0;
|
||||
border-radius: 16px; /* 增大圆角匹配边框 */
|
||||
z-index: -1;
|
||||
/* 只对opacity进行过渡,避免其他属性变化 */
|
||||
transition: opacity 0.3s ease;
|
||||
/* 硬件加速 */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
/* 初始状态 - 完全透明但保持渐变结构 */
|
||||
background:
|
||||
conic-gradient(
|
||||
from 0deg at 50% 50%,
|
||||
transparent 0%,
|
||||
transparent 20%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
/* 移除所有可能引起闪烁的滤镜 */
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.mode-card:hover::before {
|
||||
opacity: 1;
|
||||
animation:
|
||||
rotate-glow 2.5s linear infinite,
|
||||
pulse-glow 4s ease-in-out infinite;
|
||||
/* 悬停时才应用渐变和滤镜 */
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 70%, rgba(255,255,255,0.6) 0%, transparent 50%),
|
||||
conic-gradient(
|
||||
from 0deg at 50% 50%,
|
||||
var(--color-start, #6366f1) 0%,
|
||||
var(--color-mid, #8b5cf6) 20%,
|
||||
var(--color-end, #a855f7) 40%,
|
||||
var(--color-mid, #8b5cf6) 60%,
|
||||
var(--color-start, #6366f1) 80%,
|
||||
var(--color-start, #6366f1) 100%
|
||||
);
|
||||
filter: blur(12px) brightness(1.3);
|
||||
}
|
||||
|
||||
/* 颜色定义 */
|
||||
.mode-card[data-mode='json'] {
|
||||
--color-start: #6366f1;
|
||||
--color-mid: #8b5cf6;
|
||||
--color-end: #a855f7;
|
||||
}
|
||||
|
||||
.mode-card[data-mode='manual'] {
|
||||
--color-start: #f59e0b;
|
||||
--color-mid: #f97316;
|
||||
--color-end: #fb923c;
|
||||
}
|
||||
|
||||
.mode-card[data-mode='ai'] {
|
||||
--color-start: #10b981;
|
||||
--color-mid: #059669;
|
||||
--color-end: #34d399;
|
||||
}
|
||||
|
||||
/* 旋转动画 - 优化性能 */
|
||||
@keyframes rotate-glow {
|
||||
0% {
|
||||
transform: translateZ(0) rotate(0deg);
|
||||
background-position: 0% 0%, 0% 0%, 0% 50%;
|
||||
}
|
||||
25% {
|
||||
background-position: 100% 0%, 0% 100%, 25% 75%;
|
||||
}
|
||||
50% {
|
||||
transform: translateZ(0) rotate(180deg);
|
||||
background-position: 100% 100%, 100% 100%, 50% 100%;
|
||||
}
|
||||
75% {
|
||||
background-position: 0% 100%, 100% 0%, 75% 75%;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0) rotate(360deg);
|
||||
background-position: 0% 0%, 0% 0%, 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 脉动光晕 - 简化避免闪烁 */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
filter: blur(12px) brightness(1.3) saturate(1.2);
|
||||
}
|
||||
50% {
|
||||
filter: blur(14px) brightness(1.5) saturate(1.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 内部光效 - 完全重写避免闪烁 */
|
||||
.mode-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px; /* 调整内部间距 */
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease 0.1s;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
/* 使用box-shadow替代背景渐变 */
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 30px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mode-card:hover::after {
|
||||
opacity: 1;
|
||||
/* 简化动画,只做微妙的脉动 */
|
||||
animation: inner-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes inner-pulse {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 30px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 0 25px rgba(255, 255, 255, 0.15),
|
||||
inset 0 0 35px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加一个额外的容器层确保内容在光效之上 */
|
||||
.mode-card > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 性能优化 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mode-card:hover::before {
|
||||
animation:
|
||||
rotate-glow 6s linear infinite,
|
||||
pulse-glow 8s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* 针对某些浏览器的额外修复 */
|
||||
@supports not (backdrop-filter: blur(10px)) {
|
||||
.mode-card {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* 装饰元素 */
|
||||
.mode-card-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.json-decoration,
|
||||
.manual-decoration,
|
||||
.ai-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.enter-y {
|
||||
opacity: 0;
|
||||
animation: enter-y-animation 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes enter-y-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dark .mode-card:hover {
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .mode-card-border-inner {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.mode-card {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.mode-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
|
||||
import { confirm, Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Steps } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { downloadByData } from '#/util/tool';
|
||||
import {
|
||||
codeGenerationDownloadApi,
|
||||
generationApi,
|
||||
} from '#/views/code-generation/api';
|
||||
import { codeGenerationForm } from '#/views/code-generation/config/form';
|
||||
|
||||
import CodeGenerationFiledTable from './components/fileds.vue';
|
||||
import type { CodeGenData } from './utils/data-transformer';
|
||||
import type { GenerationMode } from './utils/draft-manager';
|
||||
|
||||
const [Form, formApi] = useVbenForm(codeGenerationForm);
|
||||
import AiPrompt from './components/ai-prompt.vue';
|
||||
import JsonImport from './components/json-import.vue';
|
||||
import ManualForm from './components/manual-form.vue';
|
||||
import ModeSelector from './components/mode-selector.vue';
|
||||
|
||||
const formValue = ref();
|
||||
const tableRef = ref();
|
||||
const generation = () => {
|
||||
formApi.getValues().then((data) => {
|
||||
formValue.value = data;
|
||||
formValue.value.field = tableRef.value?.tableData;
|
||||
generationApi(formValue.value).then((res) => {
|
||||
confirm({
|
||||
cancelText: '迟点下载',
|
||||
confirmText: '是的,马上下载',
|
||||
content:
|
||||
'您的模块已经生成成功,是否立即下载?\r\n' +
|
||||
'后端代码已经自动生成:\r\n' +
|
||||
'(Controller、Service、Model、Route、Mysql数据表),无需您操作。\r\n' +
|
||||
'您只需要把代码黏贴至 ”您的根目录/apps/web-antd/src/views/my-gen“文件夹下即可使用',
|
||||
icon: 'success',
|
||||
title: '代码生成成功!',
|
||||
}).then(() => {
|
||||
codeGenerationDownloadApi(res.id).then((blob) => {
|
||||
downloadByData(blob, res.file_name, 'application/zip');
|
||||
});
|
||||
// 列表中下载代码方式
|
||||
// downloadInfoApi(1).then((result) => {
|
||||
// codeGenerationDownloadApi(1).then((blob) => {
|
||||
// downloadByData(blob, result.file_name, 'application/zip');
|
||||
// });
|
||||
// });
|
||||
});
|
||||
});
|
||||
});
|
||||
// 懒加载组件
|
||||
const ModeSelectorComponent = defineAsyncComponent(() =>
|
||||
Promise.resolve(ModeSelector),
|
||||
);
|
||||
const JsonImportComponent = defineAsyncComponent(() =>
|
||||
Promise.resolve(JsonImport),
|
||||
);
|
||||
const ManualFormComponent = defineAsyncComponent(() =>
|
||||
Promise.resolve(ManualForm),
|
||||
);
|
||||
const AiPromptComponent = defineAsyncComponent(() =>
|
||||
Promise.resolve(AiPrompt),
|
||||
);
|
||||
|
||||
// 当前模式
|
||||
const currentMode = ref<GenerationMode | null>(null);
|
||||
|
||||
// 模式标题映射
|
||||
const modeTitles: Record<GenerationMode, string> = {
|
||||
json: 'JSON导入模式',
|
||||
manual: '手动填写模式',
|
||||
ai: 'AI提示词模式',
|
||||
};
|
||||
|
||||
const stepsCurrent = ref(0);
|
||||
const stepsItems = [
|
||||
{
|
||||
title: '基础信息',
|
||||
description: '类和菜单的基础信息',
|
||||
},
|
||||
{
|
||||
title: '字段信息',
|
||||
description: '设计数据库字段信息',
|
||||
subTitle: 'Left 00:00:08',
|
||||
},
|
||||
{
|
||||
title: '代码生成成功',
|
||||
description: '下载压缩包',
|
||||
},
|
||||
];
|
||||
// 选择模式
|
||||
const handleSelectMode = (mode: GenerationMode) => {
|
||||
currentMode.value = mode;
|
||||
};
|
||||
|
||||
// 返回模式选择
|
||||
const handleBack = () => {
|
||||
currentMode.value = null;
|
||||
};
|
||||
|
||||
// 生成代码
|
||||
const handleGenerate = async (data: CodeGenData) => {
|
||||
try {
|
||||
const res = await generationApi(data);
|
||||
confirm({
|
||||
cancelText: '迟点下载',
|
||||
confirmText: '是的,马上下载',
|
||||
content:
|
||||
'您的模块已经生成成功,是否立即下载?\r\n' +
|
||||
'后端代码已经自动生成:\r\n' +
|
||||
'(Controller、Service、Model、Route、Mysql数据表),无需您操作。\r\n' +
|
||||
'您只需要把代码黏贴至 "您的根目录/apps/web-antd/src/views/my-gen"文件夹下即可使用',
|
||||
icon: 'success',
|
||||
title: '代码生成成功!',
|
||||
}).then(() => {
|
||||
codeGenerationDownloadApi(res.id).then((blob) => {
|
||||
downloadByData(blob, res.file_name, 'application/zip');
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '代码生成失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="代码生成器" auto-content-height>
|
||||
<Steps :current="stepsCurrent" :items="stepsItems" />
|
||||
<Card class="mt-6" :title="stepsItems[stepsCurrent]?.title">
|
||||
<Form v-show="stepsCurrent === 0" />
|
||||
<CodeGenerationFiledTable ref="tableRef" v-show="stepsCurrent === 1" />
|
||||
</Card>
|
||||
<template #extra>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<Button
|
||||
v-if="stepsCurrent >= 0 && stepsCurrent < 1"
|
||||
@click="stepsCurrent++"
|
||||
type="primary"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
<Button v-if="stepsCurrent > 0" @click="stepsCurrent--" type="primary">
|
||||
上一步
|
||||
</Button>
|
||||
<Button v-if="stepsCurrent === 1" type="primary" @click="generation">
|
||||
<i class="fa-solid fa-code mr-2"></i>生成代码
|
||||
</Button>
|
||||
<Page
|
||||
:title="currentMode ? modeTitles[currentMode] : '代码生成器'"
|
||||
auto-content-height
|
||||
>
|
||||
<Transition name="fade-slide" mode="out-in">
|
||||
<!-- 模式选择界面 -->
|
||||
<div v-if="!currentMode" key="selector" class="mode-selector-container">
|
||||
<div class="flex min-h-[calc(100vh-200px)] flex-col items-center">
|
||||
<div class="mb-12 w-full max-w-2xl">
|
||||
<div class="mb-6 flex items-center justify-center gap-3">
|
||||
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-gray-300 to-gray-300 dark:via-gray-600 dark:to-gray-600"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-2 w-2 rounded-full bg-primary"></div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
选择代码生成方式
|
||||
</h2>
|
||||
<div class="flex h-2 w-2 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-gradient-to-l from-transparent via-gray-300 to-gray-300 dark:via-gray-600 dark:to-gray-600"></div>
|
||||
</div>
|
||||
<p class="text-center text-base text-gray-500 dark:text-gray-400">
|
||||
请选择一种适合您的代码生成方式
|
||||
</p>
|
||||
</div>
|
||||
<ModeSelectorComponent @select="handleSelectMode" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- JSON导入模式 -->
|
||||
<JsonImportComponent
|
||||
v-else-if="currentMode === 'json'"
|
||||
key="json"
|
||||
@generate="handleGenerate"
|
||||
@back="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 手动填写模式 -->
|
||||
<ManualFormComponent
|
||||
v-else-if="currentMode === 'manual'"
|
||||
key="manual"
|
||||
@generate="handleGenerate"
|
||||
@back="handleBack"
|
||||
/>
|
||||
|
||||
<!-- AI提示词模式 -->
|
||||
<AiPromptComponent
|
||||
v-else-if="currentMode === 'ai'"
|
||||
key="ai"
|
||||
@generate="handleGenerate"
|
||||
@back="handleBack"
|
||||
/>
|
||||
</Transition>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.mode-selector-container {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 代码生成数据转换工具
|
||||
* 确保三种模式输出统一的数据格式
|
||||
*/
|
||||
|
||||
export interface FieldType {
|
||||
name: string;
|
||||
type: string;
|
||||
type_length: string;
|
||||
default: string;
|
||||
comment: string;
|
||||
not_null: boolean;
|
||||
formShow: boolean;
|
||||
tableShow: boolean;
|
||||
formType: string;
|
||||
search: boolean;
|
||||
searchValue: string;
|
||||
}
|
||||
|
||||
export interface CodeGenData {
|
||||
class_name: string;
|
||||
class_comment: string;
|
||||
icon: string;
|
||||
sort: number;
|
||||
pid: number;
|
||||
field: FieldType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认字段结构
|
||||
*/
|
||||
const createDefaultField = (): FieldType => ({
|
||||
name: '',
|
||||
type: 'varchar',
|
||||
type_length: '',
|
||||
default: '',
|
||||
comment: '',
|
||||
not_null: true,
|
||||
formShow: true,
|
||||
tableShow: true,
|
||||
formType: 'VbenInput',
|
||||
search: true,
|
||||
searchValue: '=',
|
||||
});
|
||||
|
||||
/**
|
||||
* JSON模式数据转换
|
||||
*/
|
||||
export function transformJsonToStandard(json: any): CodeGenData {
|
||||
// 如果JSON包含完整结构,直接使用
|
||||
if (json.class_name && json.field) {
|
||||
return {
|
||||
class_name: json.class_name || '',
|
||||
class_comment: json.class_comment || '',
|
||||
icon: json.icon || '',
|
||||
sort: json.sort ?? 9999,
|
||||
pid: json.pid ?? 0,
|
||||
field: Array.isArray(json.field)
|
||||
? json.field.map((f: any) => ({
|
||||
name: f.name || '',
|
||||
type: f.type || 'varchar',
|
||||
type_length: f.type_length || '',
|
||||
default: f.default || '',
|
||||
comment: f.comment || '',
|
||||
not_null: f.not_null !== undefined ? f.not_null : true,
|
||||
formShow: f.formShow !== undefined ? f.formShow : true,
|
||||
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
||||
formType: f.formType || 'VbenInput',
|
||||
search: f.search !== undefined ? f.search : true,
|
||||
searchValue: f.searchValue || '=',
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 如果只有字段数组,需要用户补充基础信息
|
||||
if (Array.isArray(json)) {
|
||||
return {
|
||||
class_name: '',
|
||||
class_comment: '',
|
||||
icon: '',
|
||||
sort: 9999,
|
||||
pid: 0,
|
||||
field: json.map((f: any) => ({
|
||||
name: f.name || '',
|
||||
type: f.type || 'varchar',
|
||||
type_length: f.type_length || '',
|
||||
default: f.default || '',
|
||||
comment: f.comment || '',
|
||||
not_null: f.not_null !== undefined ? f.not_null : true,
|
||||
formShow: f.formShow !== undefined ? f.formShow : true,
|
||||
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
||||
formType: f.formType || 'VbenInput',
|
||||
search: f.search !== undefined ? f.search : true,
|
||||
searchValue: f.searchValue || '=',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// 默认返回空结构
|
||||
return {
|
||||
class_name: '',
|
||||
class_comment: '',
|
||||
icon: '',
|
||||
sort: 9999,
|
||||
pid: 0,
|
||||
field: [createDefaultField()],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动模式数据转换
|
||||
*/
|
||||
export function transformManualToStandard(
|
||||
formData: Record<string, any>,
|
||||
fields: FieldType[],
|
||||
): CodeGenData {
|
||||
return {
|
||||
class_name: formData.class_name || '',
|
||||
class_comment: formData.class_comment || '',
|
||||
icon: formData.icon || '',
|
||||
sort: formData.sort ?? 9999,
|
||||
pid: formData.pid ?? 0,
|
||||
field: Array.isArray(fields) ? fields : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI模式数据转换
|
||||
*/
|
||||
export function transformAiToStandard(aiData: any): CodeGenData {
|
||||
// AI返回完整表单数据
|
||||
if (aiData && aiData.class_name) {
|
||||
return {
|
||||
class_name: aiData.class_name || '',
|
||||
class_comment: aiData.class_comment || '',
|
||||
icon: aiData.icon || '',
|
||||
sort: aiData.sort ?? 9999,
|
||||
pid: aiData.pid ?? 0,
|
||||
field: Array.isArray(aiData.field)
|
||||
? aiData.field.map((f: any) => ({
|
||||
name: f.name || '',
|
||||
type: f.type || 'varchar',
|
||||
type_length: f.type_length || '',
|
||||
default: f.default || '',
|
||||
comment: f.comment || '',
|
||||
not_null: f.not_null !== undefined ? f.not_null : true,
|
||||
formShow: f.formShow !== undefined ? f.formShow : true,
|
||||
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
||||
formType: f.formType || 'VbenInput',
|
||||
search: f.search !== undefined ? f.search : true,
|
||||
searchValue: f.searchValue || '=',
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 默认返回空结构
|
||||
return {
|
||||
class_name: '',
|
||||
class_comment: '',
|
||||
icon: '',
|
||||
sort: 9999,
|
||||
pid: 0,
|
||||
field: [createDefaultField()],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据验证
|
||||
*/
|
||||
export function validateData(data: CodeGenData): {
|
||||
valid: boolean;
|
||||
message?: string;
|
||||
} {
|
||||
if (!data.class_name || !data.class_name.trim()) {
|
||||
return { valid: false, message: '类名不能为空' };
|
||||
}
|
||||
|
||||
if (!data.class_comment || !data.class_comment.trim()) {
|
||||
return { valid: false, message: '中文名称不能为空' };
|
||||
}
|
||||
|
||||
if (!data.icon || !data.icon.trim()) {
|
||||
return { valid: false, message: '菜单图标不能为空' };
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.field) || data.field.length === 0) {
|
||||
return { valid: false, message: '至少需要一个字段' };
|
||||
}
|
||||
|
||||
// 验证字段
|
||||
for (const field of data.field) {
|
||||
if (!field.name || !field.name.trim()) {
|
||||
return { valid: false, message: '字段名不能为空' };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
254
apps/web-antd/src/views/code-generation/utils/draft-manager.ts
Normal file
254
apps/web-antd/src/views/code-generation/utils/draft-manager.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 草稿管理工具
|
||||
* 使用 localStorage 存储草稿,每种模式独立存储
|
||||
*/
|
||||
|
||||
export type GenerationMode = 'json' | 'manual' | 'ai';
|
||||
|
||||
export interface DraftItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mode: GenerationMode;
|
||||
data: any;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
const STORAGE_PREFIX = 'code-gen-draft';
|
||||
const MAX_DRAFTS_PER_MODE = 10; // 每种模式最多保存10个草稿
|
||||
|
||||
/**
|
||||
* 获取存储key
|
||||
*/
|
||||
function getStorageKey(mode: GenerationMode, id?: string): string {
|
||||
if (id) {
|
||||
return `${STORAGE_PREFIX}-${mode}-${id}`;
|
||||
}
|
||||
return `${STORAGE_PREFIX}-${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有草稿的索引key
|
||||
*/
|
||||
function getIndexKey(mode: GenerationMode): string {
|
||||
return `${STORAGE_PREFIX}-index-${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成草稿ID
|
||||
*/
|
||||
function generateDraftId(): string {
|
||||
return `draft-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取草稿索引列表
|
||||
*/
|
||||
function getDraftIndex(mode: GenerationMode): string[] {
|
||||
try {
|
||||
const key = getIndexKey(mode);
|
||||
const indexStr = localStorage.getItem(key);
|
||||
if (indexStr) {
|
||||
return JSON.parse(indexStr);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取草稿索引失败:', error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存草稿索引
|
||||
*/
|
||||
function saveDraftIndex(mode: GenerationMode, ids: string[]): void {
|
||||
try {
|
||||
const key = getIndexKey(mode);
|
||||
localStorage.setItem(key, JSON.stringify(ids));
|
||||
} catch (error) {
|
||||
console.error('保存草稿索引失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认草稿名称
|
||||
*/
|
||||
function getDefaultDraftName(mode: GenerationMode, index: number): string {
|
||||
const modeNames = {
|
||||
json: 'JSON导入',
|
||||
manual: '手动填写',
|
||||
ai: 'AI提示词',
|
||||
};
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
||||
const number = numbers[index] || `${index + 1}`;
|
||||
return `${modeNames[mode]} - 草稿${number}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存草稿
|
||||
*/
|
||||
export function saveDraft(
|
||||
mode: GenerationMode,
|
||||
data: any,
|
||||
name?: string,
|
||||
draftId?: string,
|
||||
): string {
|
||||
try {
|
||||
const id = draftId || generateDraftId();
|
||||
const now = Date.now();
|
||||
const index = getDraftIndex(mode);
|
||||
|
||||
// 如果指定了draftId,更新现有草稿
|
||||
if (draftId && index.includes(draftId)) {
|
||||
const existingDraft = loadDraft(mode, draftId);
|
||||
const draftName = name || existingDraft?.name || getDefaultDraftName(mode, 0);
|
||||
const draft: DraftItem = {
|
||||
id,
|
||||
name: draftName,
|
||||
mode,
|
||||
data,
|
||||
createdAt: existingDraft?.createdAt || now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const key = getStorageKey(mode, id);
|
||||
localStorage.setItem(key, JSON.stringify(draft));
|
||||
return id;
|
||||
}
|
||||
|
||||
// 创建新草稿
|
||||
const draftName =
|
||||
name || getDefaultDraftName(mode, index.length);
|
||||
const draft: DraftItem = {
|
||||
id,
|
||||
name: draftName,
|
||||
mode,
|
||||
data,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// 保存草稿
|
||||
const key = getStorageKey(mode, id);
|
||||
localStorage.setItem(key, JSON.stringify(draft));
|
||||
|
||||
// 更新索引(限制数量)
|
||||
const newIndex = [id, ...index].slice(0, MAX_DRAFTS_PER_MODE);
|
||||
saveDraftIndex(mode, newIndex);
|
||||
|
||||
// 如果超出限制,删除最旧的草稿
|
||||
if (index.length >= MAX_DRAFTS_PER_MODE) {
|
||||
const oldIds = index.slice(MAX_DRAFTS_PER_MODE - 1);
|
||||
oldIds.forEach((oldId) => {
|
||||
try {
|
||||
localStorage.removeItem(getStorageKey(mode, oldId));
|
||||
} catch (error) {
|
||||
console.error('删除旧草稿失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error('保存草稿失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载草稿
|
||||
*/
|
||||
export function loadDraft(
|
||||
mode: GenerationMode,
|
||||
id: string,
|
||||
): DraftItem | null {
|
||||
try {
|
||||
const key = getStorageKey(mode, id);
|
||||
const draftStr = localStorage.getItem(key);
|
||||
if (draftStr) {
|
||||
const draft = JSON.parse(draftStr) as DraftItem;
|
||||
// 验证模式是否匹配
|
||||
if (draft.mode === mode) {
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载草稿失败:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有草稿
|
||||
*/
|
||||
export function listDrafts(mode: GenerationMode): DraftItem[] {
|
||||
try {
|
||||
const index = getDraftIndex(mode);
|
||||
const drafts: DraftItem[] = [];
|
||||
|
||||
for (const id of index) {
|
||||
const draft = loadDraft(mode, id);
|
||||
if (draft) {
|
||||
drafts.push(draft);
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间倒序排列
|
||||
return drafts.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
} catch (error) {
|
||||
console.error('列出草稿失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除草稿
|
||||
*/
|
||||
export function deleteDraft(mode: GenerationMode, id: string): boolean {
|
||||
try {
|
||||
const key = getStorageKey(mode, id);
|
||||
localStorage.removeItem(key);
|
||||
|
||||
// 更新索引
|
||||
const index = getDraftIndex(mode);
|
||||
const newIndex = index.filter((draftId) => draftId !== id);
|
||||
saveDraftIndex(mode, newIndex);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除草稿失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取草稿名称
|
||||
*/
|
||||
export function getDraftName(mode: GenerationMode, id: string): string {
|
||||
const draft = loadDraft(mode, id);
|
||||
return draft?.name || getDefaultDraftName(mode, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有草稿(用于测试或重置)
|
||||
*/
|
||||
export function clearAllDrafts(mode?: GenerationMode): void {
|
||||
try {
|
||||
if (mode) {
|
||||
// 清空指定模式的草稿
|
||||
const index = getDraftIndex(mode);
|
||||
index.forEach((id) => {
|
||||
localStorage.removeItem(getStorageKey(mode, id));
|
||||
});
|
||||
localStorage.removeItem(getIndexKey(mode));
|
||||
} else {
|
||||
// 清空所有模式的草稿
|
||||
const modes: GenerationMode[] = ['json', 'manual', 'ai'];
|
||||
modes.forEach((m) => {
|
||||
clearAllDrafts(m);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空草稿失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user