🎨 更新用户版本

This commit is contained in:
2025-09-02 22:56:30 +08:00
parent 8276b3e87f
commit a1906d2a36
71 changed files with 4790 additions and 962 deletions

View File

@@ -206,7 +206,7 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="TableName" class="w-full">
<el-form-item label="abbreviation" prop="abbreviation" class="w-full">
<template #label>
<el-tooltip
content="简称会作为入参对象名和路由group"
@@ -268,7 +268,7 @@
prop="package"
class="w-full relative"
>
<el-select v-model="form.package" class="w-full pr-12">
<el-select v-model="form.package" class="w-full pr-12" filterable>
<el-option
v-for="item in pkgs"
:key="item.ID"
@@ -307,6 +307,7 @@
</template>
<el-select
v-model="form.businessDB"
clearable
placeholder="选择业务库"
class="w-full"
>
@@ -534,7 +535,7 @@
width="160"
>
<template #default="{ row }">
<el-input :disabled="row.disabled" v-model="row.fieldName" />
<el-input disabled v-model="row.fieldName" />
</template>
</el-table-column>
<el-table-column
@@ -696,7 +697,7 @@
style="width: 100%"
placeholder="请选择字段查询条件"
clearable
:disabled="row.fieldType !== 'json' || row.disabled"
:disabled="row.fieldType === 'json' || row.disabled"
>
<el-option
v-for="item in typeSearchOptions"
@@ -1541,15 +1542,6 @@
}
init()
watch(
() => route.params.id,
() => {
if (route.name === 'autoCodeEdit') {
init()
}
}
)
watch(()=>form.value.generateServer,()=>{
if(!form.value.generateServer){
form.value.autoCreateApiToSql = false
@@ -1566,6 +1558,7 @@
const catchData = () => {
window.sessionStorage.setItem('autoCode', JSON.stringify(form.value))
ElMessage.success('暂存成功')
}
const getCatch = () => {
@@ -1619,6 +1612,8 @@
reader.onload = (e) => {
try {
form.value = JSON.parse(e.target.result)
form.value.generateServer = true
form.value.generateWeb = true
ElMessage.success('JSON 文件导入成功')
} catch (_) {
ElMessage.error('无效的 JSON 文件')

View File

@@ -0,0 +1,151 @@
<template>
<div class="gva-form-box">
<el-form :model="form" ref="formRef" label-width="100px" :rules="rules">
<el-form-item label="工具名称" prop="name">
<el-input v-model="form.name" placeholder="例:CurrentTime" />
</el-form-item>
<el-form-item label="工具描述" prop="description">
<el-input type="textarea" v-model="form.description" placeholder="请输入工具描述" />
</el-form-item>
<el-form-item label="参数列表">
<el-table :data="form.params" style="width: 100%">
<el-table-column prop="name" label="参数名" width="120">
<template #default="scope">
<el-input v-model="scope.row.name" placeholder="参数名" />
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="180">
<template #default="scope">
<el-input v-model="scope.row.description" placeholder="描述" />
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="120">
<template #default="scope">
<el-select v-model="scope.row.type" placeholder="类型">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
<el-option label="object" value="object" />
<el-option label="array" value="array" />
</el-select>
</template>
</el-table-column>
<el-table-column label="默认值" width="300">
<template #default="scope">
<el-input :disabled="scope.row.type === 'object'" v-model="scope.row.default" />
</template>
</el-table-column>
<el-table-column prop="required" label="必填" width="80">
<template #default="scope">
<el-checkbox v-model="scope.row.required" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scope">
<el-button type="text" @click="removeParam(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" icon="plus" @click="addParam" style="margin-top: 10px;">添加参数</el-button>
</div>
<el-form-item label="返回参数">
<el-table :data="form.response" style="width: 100%">
<el-table-column prop="type" label="类型" min-width="120">
<template #default="scope">
<el-select v-model="scope.row.type" placeholder="类型">
<el-option label="text" value="text" />
<el-option label="image" value="image" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scope">
<el-button type="text" @click="removeResponse(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" icon="plus" @click="addResponse" style="margin-top: 10px;">添加返回参数</el-button>
</div>
<div class="flex justify-end mt-8">
<el-button type="primary" @click="submit">生成</el-button>
</div>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { mcp } from '@/api/autoCode'
defineOptions({
name: 'MCP'
})
const formRef = ref(null)
const form = reactive({
name: '',
description: '',
type: '',
params: [],
response: []
})
const rules = {
name: [{ required: true, message: '请输入工具名称', trigger: 'blur' }],
description: [{ required: true, message: '请输入工具描述', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
function addParam() {
form.params.push({
name: '',
description: '',
type: '',
required: false
})
}
function removeParam(index) {
form.params.splice(index, 1)
}
function addResponse() {
form.response.push({
type: ''
})
}
function removeResponse(index) {
form.response.splice(index, 1)
}
function submit() {
formRef.value.validate(async (valid) => {
if (!valid) return
// 简单校验参数
for (const p of form.params) {
if (!p.name || !p.description || !p.type) {
ElMessage.error('请完善所有参数信息')
return
}
}
// 校验返回参数
for (const r of form.response) {
if (!r.type) {
ElMessage.error('请完善所有返回参数类型')
return
}
}
const res = await mcp(form)
if (res.code === 0) {
ElMessage.success(res.msg)
}
})
}
</script>

View File

@@ -0,0 +1,261 @@
<template>
<div class="p-2">
<el-card class="mb-2">
<template #header>
<div class="flex justify-between items-center font-bold">
<span>MCP 服务器配置示例</span>
<el-tooltip content="复制配置" placement="top">
<el-button :icon="DocumentCopy" circle @click="copyMcpConfig" />
</el-tooltip>
</div>
</template>
<pre class="font-mono whitespace-pre-wrap break-words bg-gray-100 p-2.5 rounded text-gray-700">{{ mcpServerConfig }}</pre>
</el-card>
<el-row :gutter="8">
<el-col v-for="tool in mcpTools" :key="tool.name" :xs="24" :sm="12" :md="12" :lg="8">
<el-card class="mb-5 min-h-[150px] flex flex-col overflow-hidden">
<template #header>
<div class="flex justify-between items-center font-bold">
<span>{{ tool.name }}</span>
<el-tooltip content="测试工具" placement="top">
<el-button :icon="VideoPlay" circle @click="openTestDialog(tool)" />
</el-tooltip>
</div>
</template>
<div class="text-sm mb-1">{{ tool.description }}</div>
<div v-if="tool.inputSchema && tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0" class="mt-1 text-xs overflow-y-auto max-h-[100px] p-2 border-t border-gray-200 bg-gray-50 rounded-b">
<p class="font-semibold mb-1 text-gray-700 flex items-center">
<span class="mr-1 my-2">参数列表</span>
<span class="text-xs text-gray-500">({{ Object.keys(tool.inputSchema.properties).length }})</span>
</p>
<div class="space-y-2">
<div v-for="(propDetails, propName) in tool.inputSchema.properties" :key="propName" class="flex flex-col p-1.5 bg-white rounded border border-gray-100 hover:border-gray-300 transition-colors">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="font-medium text-gray-800">{{ propName }}</span>
<span v-if="tool.inputSchema.required && tool.inputSchema.required.includes(propName)" class="ml-1 text-red-500 text-xs">*</span>
</div>
<span class="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded">{{ propDetails.type }}</span>
</div>
<div class="text-gray-500 mt-0.5 text-xs line-clamp-2" :title="propDetails.description || '无描述'">
{{ propDetails.description || '无描述' }}
</div>
</div>
</div>
</div>
<div v-else class="mt-1 text-xs p-2 border-t border-gray-200 bg-gray-50 rounded-b flex items-center justify-center">
<span class="text-gray-500 italic py-3">无输入参数</span>
</div>
</el-card>
</el-col>
</el-row>
<el-dialog
v-model="testDialogVisible"
:title="currentTestingTool ? `${currentTestingTool.name} - 参数测试` : '参数测试'"
width="60%"
:before-close="handleCloseDialog"
>
<el-form v-if="currentTestingTool" :model="testParamsForm" ref="testParamsFormRef" label-width="120px" label-position="top" class="max-h-[calc(60vh-120px)] overflow-y-auto">
<el-form-item
v-for="(propDetails, propName) in currentTestingTool.inputSchema.properties"
:key="propName"
:label="propDetails.description || propName"
:prop="propName"
:rules="currentTestingTool.inputSchema.required && currentTestingTool.inputSchema.required.includes(propName) ? [{ required: true, message: '请输入 ' + (propDetails.description || propName), trigger: 'blur' }] : []"
>
<el-input
v-if="propDetails.type === 'string' && !propDetails.enum"
v-model="testParamsForm[propName]"
:placeholder="propDetails.description || '请输入' + propName"
/>
<el-input
v-else-if="propDetails.type === 'number'"
v-model.number="testParamsForm[propName]"
type="number"
:placeholder="propDetails.description || '请输入数字' + propName"
/>
<el-select
v-else-if="propDetails.type === 'boolean'"
v-model="testParamsForm[propName]"
:placeholder="propDetails.description || '请选择'"
>
<el-option label="True" :value="true" />
<el-option label="False" :value="false" />
</el-select>
<el-select
v-else-if="propDetails.type === 'string' && propDetails.enum"
v-model="testParamsForm[propName]"
:placeholder="propDetails.description || '请选择' + propName"
>
<el-option v-for="enumValue in propDetails.enum" :key="enumValue" :label="enumValue" :value="enumValue" />
</el-select>
<el-input
v-else
type="textarea"
v-model="testParamsForm[propName]"
:placeholder="(propDetails.description || propName) + ' (请输入JSON格式)'"
:autosize="{ minRows: 2, maxRows: 6 }"
/>
</el-form-item>
</el-form>
<div v-if="apiDialogResponse" class="mt-5 p-[15px] border border-gray-200 rounded bg-gray-50">
<h4 class="mt-0 mb-2.5 text-base">API 返回结果:</h4>
<div v-if="typeof apiDialogResponse === 'string'">
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse }}</pre>
</div>
<div v-else-if="apiDialogResponse.type === 'image' && apiDialogResponse.content">
<el-image
class="max-w-full max-h-[300px]"
:src="apiDialogResponse.content"
:preview-src-list="[apiDialogResponse.content]"
fit="contain"
/>
</div>
<div v-else-if="apiDialogResponse.type === 'text' && apiDialogResponse.content">
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse.content }}</pre>
</div>
<div v-else>
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ JSON.stringify(apiDialogResponse, null, 2) }}</pre>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="testDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleDialogTestTool">测试</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { VideoPlay, DocumentCopy } from '@element-plus/icons-vue' // Added DocumentCopy
import { mcpList, mcpTest } from '@/api/autoCode'
defineOptions({
name: 'MCPTest'
})
const mcpTools = ref([])
const testDialogVisible = ref(false)
const currentTestingTool = ref(null)
const testParamsForm = reactive({})
const testParamsFormRef = ref(null)
const apiDialogResponse = ref(null)
const mcpServerConfig = ref(JSON.stringify({
"mcpServers": {
"gva": {
"url": "https://127.0.0.1/sse"
}
}
}, null, 2))
const fetchMcpTools = async () => {
const res = await mcpList()
if (res.code === 0 && res.data && res.data.list.tools) {
mcpTools.value = res.data.list.tools
mcpServerConfig.value = JSON.stringify(res.data.mcpServerConfig, null, 2)
} else {
ElMessage.error(res.msg || '获取工具列表失败或数据格式不正确')
}
}
onMounted(() => {
fetchMcpTools()
})
const copyMcpConfig = async () => {
try {
await navigator.clipboard.writeText(mcpServerConfig.value)
ElMessage.success('配置已复制到剪贴板')
} catch (err) {
ElMessage.error('复制失败: ' + err)
}
}
const openTestDialog = (tool) => {
currentTestingTool.value = tool
apiDialogResponse.value = null // 清空之前的API响应
// 重置并初始化表单数据
for (const key in testParamsForm) {
delete testParamsForm[key]
}
if (tool.inputSchema && tool.inputSchema.properties) {
Object.keys(tool.inputSchema.properties).forEach(propName => {
const propDetails = tool.inputSchema.properties[propName]
// 设置默认值,优先使用 schema 中的 default否则根据类型给初始值
if (propDetails.default !== undefined) {
testParamsForm[propName] = propDetails.default
} else if (propDetails.type === 'boolean') {
testParamsForm[propName] = false
} else if (propDetails.type === 'number') {
testParamsForm[propName] = null // 或者 0
} else if (propDetails.type === 'object' || propDetails.type === 'array') {
testParamsForm[propName] = '' // 对象和数组类型默认为空字符串提示用户输入JSON
} else {
testParamsForm[propName] = ''
}
})
}
testDialogVisible.value = true
// 清除表单校验状态
if (testParamsFormRef.value) {
testParamsFormRef.value.clearValidate()
}
}
const handleCloseDialog = (done) => {
apiDialogResponse.value = null
done()
}
const handleDialogTestTool = async () => {
if (!currentTestingTool.value) {
ElMessage.warning('没有选中的测试工具')
return
}
if (testParamsFormRef.value) {
testParamsFormRef.value.validate(async (valid) => {
if (valid) {
const toolName = currentTestingTool.value.name
const payload = { ...testParamsForm }
if (currentTestingTool.value.inputSchema && currentTestingTool.value.inputSchema.properties) {
Object.keys(currentTestingTool.value.inputSchema.properties).forEach(propName => {
const propDetails = currentTestingTool.value.inputSchema.properties[propName]
if ((propDetails.type === 'object' || propDetails.type === 'array') && payload[propName] && typeof payload[propName] === 'string') {
try {
payload[propName] = JSON.parse(payload[propName])
} catch (e) {
ElMessage.error(`参数 ${propName} 的JSON格式无效: ${e.message}`)
throw new Error(`参数 ${propName} JSON无效`);
}
}
})
}
const res = await mcpTest({
name:toolName,
arguments:payload
})
apiDialogResponse.value = res.data
if (res.code === 0) {
ElMessage.success('API调用成功')
}
}
})
}
}
</script>

View File

@@ -0,0 +1,426 @@
<template>
<div>
<warning-bar
href="https://www.gin-vue-admin.com/empower/"
title="此功能只针对授权用户开放,点我【购买授权】"
/>
<div class="gva-search-box">
<div class="text-xl mb-2 text-gray-600">
AI前端工程师<a
class="text-blue-600 text-sm ml-4"
href="https://plugin.gin-vue-admin.com/#/layout/userInfo/center"
target="_blank"
>获取AiPath</a
>
</div>
<!-- 选项模式 -->
<div class="mb-4">
<div class="mb-3">
<div class="text-base font-medium mb-2">页面用途</div>
<el-radio-group v-model="pageType" class="mb-2" @change="handlePageTypeChange">
<el-radio label="企业官网">企业官网</el-radio>
<el-radio label="电商页面">电商页面</el-radio>
<el-radio label="个人博客">个人博客</el-radio>
<el-radio label="产品介绍">产品介绍</el-radio>
<el-radio label="活动落地页">活动落地页</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
<el-input v-if="pageType === '其他'" v-model="pageTypeCustom" placeholder="请输入页面用途" class="w-full" />
</div>
<div class="mb-3">
<div class="text-base font-medium mb-2">主要内容板块</div>
<el-checkbox-group v-model="contentBlocks" class="flex flex-wrap gap-2 mb-2">
<el-checkbox label="Banner轮播图">Banner轮播图</el-checkbox>
<el-checkbox label="产品/服务介绍">产品/服务介绍</el-checkbox>
<el-checkbox label="功能特点展示">功能特点展示</el-checkbox>
<el-checkbox label="客户案例">客户案例</el-checkbox>
<el-checkbox label="团队介绍">团队介绍</el-checkbox>
<el-checkbox label="联系表单">联系表单</el-checkbox>
<el-checkbox label="新闻/博客列表">新闻/博客列表</el-checkbox>
<el-checkbox label="价格表">价格表</el-checkbox>
<el-checkbox label="FAQ/常见问题">FAQ/常见问题</el-checkbox>
<el-checkbox label="用户评价">用户评价</el-checkbox>
<el-checkbox label="数据统计">数据统计</el-checkbox>
<el-checkbox label="商品列表">商品列表</el-checkbox>
<el-checkbox label="商品卡片">商品卡片</el-checkbox>
<el-checkbox label="购物车">购物车</el-checkbox>
<el-checkbox label="结算页面">结算页面</el-checkbox>
<el-checkbox label="订单跟踪">订单跟踪</el-checkbox>
<el-checkbox label="商品分类">商品分类</el-checkbox>
<el-checkbox label="热门推荐">热门推荐</el-checkbox>
<el-checkbox label="限时特惠">限时特惠</el-checkbox>
<el-checkbox label="其他">其他</el-checkbox>
</el-checkbox-group>
<el-input v-if="contentBlocks.includes('其他')" v-model="contentBlocksCustom" placeholder="请输入其他内容板块" class="w-full" />
</div>
<div class="mb-3">
<div class="text-base font-medium mb-2">风格偏好</div>
<el-radio-group v-model="stylePreference" class="mb-2">
<el-radio label="简约">简约</el-radio>
<el-radio label="科技感">科技感</el-radio>
<el-radio label="温馨">温馨</el-radio>
<el-radio label="专业">专业</el-radio>
<el-radio label="创意">创意</el-radio>
<el-radio label="复古">复古</el-radio>
<el-radio label="奢华">奢华</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
<el-input v-if="stylePreference === '其他'" v-model="stylePreferenceCustom" placeholder="请输入风格偏好" class="w-full" />
</div>
<div class="mb-3">
<div class="text-base font-medium mb-2">设计布局</div>
<el-radio-group v-model="layoutDesign" class="mb-2">
<el-radio label="单栏布局">单栏布局</el-radio>
<el-radio label="双栏布局">双栏布局</el-radio>
<el-radio label="三栏布局">三栏布局</el-radio>
<el-radio label="网格布局">网格布局</el-radio>
<el-radio label="画廊布局">画廊布局</el-radio>
<el-radio label="瀑布流">瀑布流</el-radio>
<el-radio label="卡片式">卡片式</el-radio>
<el-radio label="侧边栏+内容布局">侧边栏+内容布局</el-radio>
<el-radio label="分屏布局">分屏布局</el-radio>
<el-radio label="全屏滚动布局">全屏滚动布局</el-radio>
<el-radio label="混合布局">混合布局</el-radio>
<el-radio label="响应式">响应式</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
<el-input v-if="layoutDesign === '其他'" v-model="layoutDesignCustom" placeholder="请输入设计布局" class="w-full" />
</div>
<div class="mb-3">
<div class="text-base font-medium mb-2">配色方案</div>
<el-radio-group v-model="colorScheme" class="mb-2">
<el-radio label="蓝色系">蓝色系</el-radio>
<el-radio label="绿色系">绿色系</el-radio>
<el-radio label="红色系">红色系</el-radio>
<el-radio label="黑白灰">黑白灰</el-radio>
<el-radio label="纯黑白">纯黑白</el-radio>
<el-radio label="暖色调">暖色调</el-radio>
<el-radio label="冷色调">冷色调</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
<el-input v-if="colorScheme === '其他'" v-model="colorSchemeCustom" placeholder="请输入配色方案" class="w-full" />
</div>
</div>
<!-- 详细描述输入框 -->
<div class="relative">
<div class="text-base font-medium mb-2">详细描述可选</div>
<el-input
v-model="prompt"
:maxlength="2000"
:placeholder="placeholder"
:rows="5"
resize="none"
type="textarea"
@blur="handleBlur"
@focus="handleFocus"
/>
<div class="flex absolute right-2 bottom-2">
<el-tooltip effect="light">
<template #content>
<div>
此功能仅针对授权用户开放前往<a
class="text-blue-600"
href="https://www.gin-vue-admin.com/empower/"
target="_blank"
>购买授权</a
>
</div>
</template>
<el-button
type="primary"
@click="llmAutoFunc()"
>
<el-icon size="18">
<ai-gva/>
</el-icon>
生成
</el-button>
</el-tooltip>
</div>
</div>
</div>
<div>
<div v-if="!outPut">
<el-empty :image-size="200"/>
</div>
<div v-if="outPut && htmlFromLLM">
<el-tabs type="border-card">
<el-tab-pane label="页面预览">
<div class="h-[500px] overflow-auto bg-gray-50 p-4 rounded">
<div v-if="!loadedComponents" class="text-gray-500 text-center py-4">
组件加载中...
</div>
<component
v-else
:is="loadedComponents"
class="vue-component-container w-full"
/>
</div>
</el-tab-pane>
<el-tab-pane label="源代码">
<div class="relative h-[500px] overflow-auto bg-gray-50 p-4 rounded">
<el-button
type="primary"
:icon="DocumentCopy"
class="absolute top-2 right-2 px-2 py-1"
@click="copySnippet(htmlFromLLM)"
plain
>
复制
</el-button>
<pre class="mt-10 whitespace-pre-wrap">{{ htmlFromLLM }}</pre>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</template>
<script setup>
import { createWeb } from '@/api/autoCode'
import { ref, reactive, markRaw } from 'vue'
import * as Vue from "vue";
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ElMessage } from 'element-plus'
import { defineAsyncComponent } from 'vue'
import { DocumentCopy } from '@element-plus/icons-vue'
import { loadModule } from "vue3-sfc-loader";
defineOptions({
name: 'Picture'
})
const handleFocus = () => {
document.addEventListener('keydown', handleKeydown);
}
const handleBlur = () => {
document.removeEventListener('keydown', handleKeydown);
}
const handleKeydown = (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
llmAutoFunc()
}
}
// 复制方法:把某个字符串写进剪贴板
const copySnippet = (vueString) => {
navigator.clipboard.writeText(vueString)
.then(() => {
ElMessage({
message: '复制成功',
type: 'success',
})
})
.catch(err => {
ElMessage({
message: '复制失败',
type: 'warning',
})
})
}
// 选项模式相关变量
const pageType = ref('企业官网')
const pageTypeCustom = ref('')
const contentBlocks = ref(['Banner轮播图', '产品/服务介绍'])
const contentBlocksCustom = ref('')
const stylePreference = ref('简约')
const stylePreferenceCustom = ref('')
const layoutDesign = ref('响应式')
const layoutDesignCustom = ref('')
const colorScheme = ref('蓝色系')
const colorSchemeCustom = ref('')
// 页面用途与内容板块的推荐映射关系
const pageTypeContentMap = {
'企业官网': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '客户案例', '联系表单'],
'电商页面': ['Banner轮播图', '商品列表', '商品卡片', '购物车', '商品分类', '热门推荐', '限时特惠', '结算页面', '用户评价'],
'个人博客': ['Banner轮播图', '新闻/博客列表', '用户评价', '联系表单'],
'产品介绍': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '价格表', 'FAQ/常见问题'],
'活动落地页': ['Banner轮播图', '功能特点展示', '联系表单', '数据统计']
}
const prompt = ref('')
// 判断是否返回的标志
const outPut = ref(false)
// 容纳llm返回的vue组件代码
const htmlFromLLM = ref("")
// 存储加载的组件
const loadedComponents = ref(null)
const loadVueComponent = async (vueCode) => {
try {
// 使用内存中的虚拟路径
const fakePath = `virtual:component-0.vue`
const component = defineAsyncComponent({
loader: async () => {
try {
const options = {
moduleCache: {
vue: Vue,
},
getFile(url) {
// 处理所有可能的URL格式包括相对路径、绝对路径等
// 提取路径的最后部分,忽略查询参数
const fileName = url.split('/').pop().split('?')[0]
const componentFileName = fakePath.split('/').pop()
// 如果文件名包含我们的组件名称或者url完全匹配fakePath
if (fileName === componentFileName || url === fakePath ||
url === `./component/0.vue`) {
return Promise.resolve({
type: '.vue',
getContentData: () => vueCode
})
}
console.warn('请求未知文件:', url)
return Promise.reject(new Error(`找不到文件: ${url}`))
},
addStyle(textContent) {
// 不再将样式添加到document.head而是返回样式内容
// 稍后会将样式添加到Shadow DOM中
return textContent
},
handleModule(type, source, path, options) {
// 默认处理器
return undefined
},
log(type, ...args) {
console.log(`[vue3-sfc-loader] [${type}]`, ...args)
}
}
// 尝试加载组件
const comp = await loadModule(fakePath, options)
return comp.default || comp
} catch (error) {
console.error('组件加载详细错误:', error)
throw error
}
},
loadingComponent: {
template: '<div>加载中...</div>'
},
errorComponent: {
props: ['error'],
template: '<div>组件加载失败: {{ error && error.message }}</div>',
setup(props) {
console.error('错误组件收到的错误:', props.error)
return {}
}
},
// 添加超时和重试选项
timeout: 30000,
delay: 200,
suspensible: false,
onError(error, retry, fail) {
console.error('加载错误,细节:', error)
fail()
}
})
// 创建一个包装组件使用Shadow DOM隔离样式
const ShadowWrapper = {
name: 'ShadowWrapper',
setup() {
return {}
},
render() {
return Vue.h('div', { class: 'shadow-wrapper' })
},
mounted() {
// 创建Shadow DOM
const shadowRoot = this.$el.attachShadow({ mode: 'open' })
// 创建一个容器元素
const container = document.createElement('div')
container.className = 'shadow-container'
shadowRoot.appendChild(container)
// 提取组件中的样式
const styleContent = vueCode.match(/<style[^>]*>([\s\S]*?)<\/style>/i)?.[1] || ''
// 创建样式元素并添加到Shadow DOM
if (styleContent) {
const style = document.createElement('style')
style.textContent = styleContent
shadowRoot.appendChild(style)
}
// 创建Vue应用并挂载到Shadow DOM容器中
const app = Vue.createApp({
render: () => Vue.h(component)
})
app.mount(container)
}
}
loadedComponents.value = markRaw(ShadowWrapper)
return ShadowWrapper
} catch (error) {
console.error('组件创建总错误:', error)
return null
}
}
// 当页面用途改变时,更新内容板块的选择
const handlePageTypeChange = (value) => {
if (value !== '其他' && pageTypeContentMap[value]) {
contentBlocks.value = [...pageTypeContentMap[value]]
}
}
const llmAutoFunc = async () => {
// 构建完整的描述,包含选项模式的选择
let fullPrompt = ''
// 添加页面用途
fullPrompt += `页面用途: ${pageType.value === '其他' ? pageTypeCustom.value : pageType.value}\n`
// 添加内容板块
fullPrompt += '主要内容板块: '
const blocks = contentBlocks.value.filter(block => block !== '其他')
if (contentBlocksCustom.value) {
blocks.push(contentBlocksCustom.value)
}
fullPrompt += blocks.join(', ') + '\n'
// 添加风格偏好
fullPrompt += `风格偏好: ${stylePreference.value === '其他' ? stylePreferenceCustom.value : stylePreference.value}\n`
// 添加设计布局
fullPrompt += `设计布局: ${layoutDesign.value === '其他' ? layoutDesignCustom.value : layoutDesign.value}\n`
// 添加配色方案
fullPrompt += `配色方案: ${colorScheme.value === '其他' ? colorSchemeCustom.value : colorScheme.value}\n`
// 添加用户的详细描述
if (prompt.value) {
fullPrompt += `\n详细描述: ${prompt.value}`
}
const res = await createWeb({web: fullPrompt, command: 'createWeb'})
if (res.code === 0) {
outPut.value = true
// 添加返回的Vue组件代码到数组
htmlFromLLM.value = res.data
// 加载新生成的组件
await loadVueComponent(res.data)
}
}
const placeholder = ref(`补充您对页面的其他要求或特殊需求,例如:特别强调的元素、参考网站、交互效果等。`)
</script>