新增多种生成方式
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:
2025-11-28 10:55:29 +08:00
parent 11030750d7
commit dd41072fc9
7 changed files with 2791 additions and 75 deletions

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 };
}

View 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);
}
}