494 lines
14 KiB
Vue
494 lines
14 KiB
Vue
<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')">
|
||
<svg
|
||
class="mr-2 inline-block"
|
||
width="14"
|
||
height="14"
|
||
viewBox="0 0 14 14"
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
d="M8.75 3.5L5.25 7L8.75 10.5"
|
||
stroke="currentColor"
|
||
stroke-width="1.5"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
返回
|
||
</Button>
|
||
<span class="text-lg font-semibold">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>
|
||
|