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

494 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { 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>