发布2.8.1 beta版本 (#2014)
AI: 增加了AI前端绘制功能,可以根据描述生成客户端页面【授权用户专属】 自动化: 自动化模板采用了function模式,更加方便用户二次开发和自定义改动 自动化: 默认携带ID和CreatedAt排序 自动化: 所有自动化Select模板默认支持select搜索 优化:http交互报错信息增加防止多次弹出错误遮罩机制 ICON: 优化ICON逻辑,防止多次加载svg 布局:增加侧边分栏模式 布局: 顶栏模式样式优化和高亮逻辑调整 优化: 个人配置不再需要手动点击保存,会根据变化自动保存 BUG: 修复了菜单点击设为主页勾选被取消的bug 安全: 更新了jwt版本,修复CVE-2025-30204 导出: 默认支持软删除过滤 代码: 优化了部分代码逻辑 本次更新需要重新执行 npm i --------- Signed-off-by: joohwan <zhouhuan.chen@yunqutech.com > Co-authored-by: huiyifyj <jxfengyijie@gmail.com> Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com> Co-authored-by: okppop <okppop@protonmail.com> Co-authored-by: joohwan <zhouhuan.chen@yunqutech.com > Co-authored-by: xuedinge <781408517@qq.com>
This commit is contained in:
@@ -21,15 +21,17 @@
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"ace-builds": "^1.36.4",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "1.8.2",
|
||||
"chokidar": "^4.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
"echarts": "5.5.1",
|
||||
"element-plus": "^2.8.5",
|
||||
"highlight.js": "^11.10.0",
|
||||
"install": "^0.13.0",
|
||||
"marked": "14.1.1",
|
||||
"marked-highlight": "^2.1.4",
|
||||
"mitt": "^3.0.1",
|
||||
"npm": "^11.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"pinia": "^2.2.2",
|
||||
@@ -40,13 +42,14 @@
|
||||
"tailwindcss": "^3.4.10",
|
||||
"universal-cookie": "^7",
|
||||
"vform3-builds": "^3.0.10",
|
||||
"vite-auto-import-svg": "1.1.0",
|
||||
"vite-auto-import-svg": "^1.5.0",
|
||||
"vue": "^3.5.7",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-qr": "^4.0.9",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-sfc-loader": "^0.9.5",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,6 +63,7 @@
|
||||
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/compiler-sfc": "^3.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.4.5",
|
||||
|
@@ -9,6 +9,10 @@
|
||||
import { exportExcel } from '@/api/exportTemplate'
|
||||
|
||||
const props = defineProps({
|
||||
filterDeleted: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
templateId: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -43,6 +47,11 @@ import { exportExcel } from '@/api/exportTemplate'
|
||||
baseUrl = ""
|
||||
}
|
||||
const paramsCopy = JSON.parse(JSON.stringify(props.condition))
|
||||
|
||||
if (props.filterDeleted) {
|
||||
paramsCopy.filterDeleted = 'true'
|
||||
}
|
||||
|
||||
if (props.limit) {
|
||||
paramsCopy.limit = props.limit
|
||||
}
|
||||
|
@@ -71,7 +71,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
const res = await login(loginInfo)
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.message || '登录失败')
|
||||
return false
|
||||
}
|
||||
// 登陆成功,设置用户信息和权限相关信息
|
||||
|
@@ -99,7 +99,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt" width="180">
|
||||
<el-table-column align="left" label="日期" prop="CreatedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.CreatedAt) }}
|
||||
</template>
|
||||
|
@@ -118,6 +118,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active{
|
||||
color: var(--el-color-primary)!important;
|
||||
}
|
||||
|
||||
.el-sub-menu__title.el-tooltip__trigger,
|
||||
.el-menu-item .el-menu-tooltip__trigger {
|
||||
justify-content: center;
|
||||
|
@@ -4,6 +4,9 @@ import { useUserStore } from '@/pinia/modules/user'
|
||||
import router from '@/router/index'
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
// 添加一个状态变量,用于跟踪是否已有错误弹窗显示
|
||||
let errorBoxVisible = false
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
timeout: 99999
|
||||
@@ -93,7 +96,13 @@ service.interceptors.response.use(
|
||||
closeLoading()
|
||||
}
|
||||
|
||||
// 如果已经有错误弹窗显示,则不再显示新的弹窗
|
||||
if (errorBoxVisible) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到请求错误</p>
|
||||
@@ -106,12 +115,16 @@ service.interceptors.response.use(
|
||||
confirmButtonText: '稍后重试',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
)
|
||||
).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
switch (error.response.status) {
|
||||
case 500:
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到接口错误${error}</p>
|
||||
@@ -128,9 +141,13 @@ service.interceptors.response.use(
|
||||
const userStore = useUserStore()
|
||||
userStore.ClearStorage()
|
||||
router.push({ name: 'Login', replace: true })
|
||||
}).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
case 404:
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到接口错误${error}</p>
|
||||
@@ -143,9 +160,13 @@ service.interceptors.response.use(
|
||||
confirmButtonText: '我知道了',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
)
|
||||
).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
case 401:
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>无效的令牌</p>
|
||||
@@ -162,6 +183,9 @@ service.interceptors.response.use(
|
||||
const userStore = useUserStore()
|
||||
userStore.ClearStorage()
|
||||
router.push({ name: 'Login', replace: true })
|
||||
}).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@@ -1,16 +1,29 @@
|
||||
<template>
|
||||
<el-menu-item
|
||||
:index="routerInfo.name"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
{{ routerInfo.meta.title }}
|
||||
<template #title>
|
||||
<div
|
||||
v-if="!isCollapse"
|
||||
class="flex items-center"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ routerInfo.meta.title }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ routerInfo.meta.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
@@ -88,12 +88,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.el-menu--horizontal.el-menu,
|
||||
.el-menu--horizontal > .el-menu-item.is-active {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.el-menu--horizontal>.el-sub-menu.is-active .el-sub-menu__title {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
(device === 'mobile' && config.side_mode == 'combination') ||
|
||||
(device === 'mobile' && config.side_mode == 'sidebar')
|
||||
"
|
||||
/>
|
||||
<head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" />
|
||||
@@ -12,6 +13,9 @@
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
:mode="mode"
|
||||
/>
|
||||
<sidebar-mode
|
||||
v-if="config.side_mode === 'sidebar' && device !== 'mobile'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +23,7 @@
|
||||
import NormalMode from './normalMode.vue'
|
||||
import HeadMode from './headMode.vue'
|
||||
import CombinationMode from './combinationMode.vue'
|
||||
import SidebarMode from './sidebarMode.vue'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
|
300
web/src/view/layout/aside/sidebarMode.vue
Normal file
300
web/src/view/layout/aside/sidebarMode.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<!-- 一级菜单常驻侧边栏 -->
|
||||
<div
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
|
||||
:style="{
|
||||
width: config.layout_side_collapsed_width + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="true"
|
||||
:collapse-transition="false"
|
||||
:default-active="topActive"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="selectTopMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0]?.children || []">
|
||||
<el-menu-item
|
||||
v-if="!item.hidden && (!item.children || item.children.length === 0)"
|
||||
:key="item.name"
|
||||
:index="item.name"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: config.layout_side_item_height + 'px'
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="item.meta.icon">
|
||||
<component :is="item.meta.icon" />
|
||||
</el-icon>
|
||||
<template v-else>
|
||||
{{ item.meta.title[0] }}
|
||||
</template>
|
||||
<template #title>
|
||||
{{ item.meta.title }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<template v-else-if="!item.hidden" >
|
||||
<el-menu-item
|
||||
:key="item.name"
|
||||
:index="item.name"
|
||||
:class="{'is-active': topActive === item.name}"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: config.layout_side_item_height + 'px'
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="item.meta.icon">
|
||||
<component :is="item.meta.icon" />
|
||||
</el-icon>
|
||||
<template v-else>
|
||||
{{ item.meta.title[0] }}
|
||||
</template>
|
||||
<template #title>
|
||||
{{ item.meta.title }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 二级菜单并列显示 -->
|
||||
<div
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700 px-2"
|
||||
:style="{
|
||||
width: layoutSideWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in secondLevelMenus">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<div
|
||||
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
|
||||
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<el-icon v-if="!isCollapse">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'SidebarMode'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
const topActive = ref('')
|
||||
const secondLevelMenus = ref([])
|
||||
|
||||
const layoutSideWidth = computed(() => {
|
||||
if (!isCollapse.value) {
|
||||
return config.value.layout_side_width
|
||||
} else {
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
// 更新二级菜单
|
||||
const updateSecondLevelMenus = (menuName) => {
|
||||
const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === menuName)
|
||||
if (menu && menu.children && menu.children.length > 0) {
|
||||
secondLevelMenus.value = menu.children
|
||||
}
|
||||
}
|
||||
|
||||
// 选择一级菜单
|
||||
const selectTopMenuItem = (index) => {
|
||||
topActive.value = index
|
||||
|
||||
// 获取选中的菜单项
|
||||
const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === index)
|
||||
|
||||
// 只有当选中的菜单有子菜单时,才更新二级菜单区域
|
||||
if (menu && menu.children && menu.children.length > 0) {
|
||||
updateSecondLevelMenus(index)
|
||||
|
||||
// 导航到第一个可见的子菜单
|
||||
const firstVisibleChild = menu.children.find(child => !child.hidden)
|
||||
if (firstVisibleChild) {
|
||||
navigateToMenuItem(firstVisibleChild.name)
|
||||
}
|
||||
} else {
|
||||
// 如果没有子菜单,直接导航到该菜单,但不更新二级菜单区域
|
||||
navigateToMenuItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择二级或更深层级的菜单
|
||||
const selectMenuItem = (index) => {
|
||||
navigateToMenuItem(index)
|
||||
}
|
||||
|
||||
// 导航到指定菜单
|
||||
const navigateToMenuItem = (index) => {
|
||||
const query = {}
|
||||
const params = {}
|
||||
routerStore.routeMap[index]?.parameters &&
|
||||
routerStore.routeMap[index]?.parameters.forEach((item) => {
|
||||
if (item.type === 'query') {
|
||||
query[item.key] = item.value
|
||||
} else {
|
||||
params[item.key] = item.value
|
||||
}
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
|
||||
|
||||
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
|
||||
// 找到当前路由所属的一级菜单
|
||||
const findParentMenu = () => {
|
||||
// 首先检查当前路由是否就是一级菜单
|
||||
const isTopMenu = routerStore.asyncRouters[0]?.children.some(
|
||||
item => !item.hidden && item.name === route.name
|
||||
)
|
||||
|
||||
if (isTopMenu) {
|
||||
return route.name
|
||||
}
|
||||
|
||||
for (const topMenu of routerStore.asyncRouters[0]?.children || []) {
|
||||
if (topMenu.hidden) continue
|
||||
|
||||
// 检查当前路由是否是这个一级菜单的子菜单
|
||||
if (topMenu.children && topMenu.children.some(child => child.name === route.name)) {
|
||||
return topMenu.name
|
||||
}
|
||||
|
||||
// 递归检查更深层级
|
||||
const checkChildren = (items) => {
|
||||
for (const item of items || []) {
|
||||
if (item.name === route.name) {
|
||||
return true
|
||||
}
|
||||
if (item.children && checkChildren(item.children)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (topMenu.children && checkChildren(topMenu.children)) {
|
||||
return topMenu.name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const parentMenu = findParentMenu()
|
||||
if (parentMenu) {
|
||||
topActive.value = parentMenu
|
||||
|
||||
// 只有当父菜单有子菜单时,才更新二级菜单区域
|
||||
const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === parentMenu)
|
||||
if (menu && menu.children && menu.children.length > 0) {
|
||||
updateSecondLevelMenus(parentMenu)
|
||||
} else {
|
||||
// 如果找到的父菜单没有子菜单,保持当前一级菜单高亮,但需要显示一些二级菜单
|
||||
// 寻找第一个有子菜单的一级菜单来显示其子菜单
|
||||
const firstMenuWithChildren = routerStore.asyncRouters[0].children.find(
|
||||
item => !item.hidden && item.children && item.children.length > 0
|
||||
)
|
||||
|
||||
if (firstMenuWithChildren) {
|
||||
// 只更新二级菜单区域,但保持当前一级菜单的高亮状态
|
||||
updateSecondLevelMenus(firstMenuWithChildren.name)
|
||||
}
|
||||
}
|
||||
} else if (routerStore.asyncRouters[0]?.children?.length > 0) {
|
||||
// 如果没有找到父菜单,保持当前路由名称作为高亮,但需要显示一些二级菜单
|
||||
// 寻找第一个有子菜单的一级菜单来显示其子菜单
|
||||
const firstMenuWithChildren = routerStore.asyncRouters[0].children.find(
|
||||
item => !item.hidden && item.children && item.children.length > 0
|
||||
)
|
||||
|
||||
if (firstMenuWithChildren) {
|
||||
// 只更新二级菜单区域,高亮状态保持为当前路由
|
||||
topActive.value = route.name
|
||||
secondLevelMenus.value = firstMenuWithChildren.children
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
@@ -14,7 +14,7 @@
|
||||
<div class="flex flex-row w-full gva-container pt-16 box-border h-full">
|
||||
<gva-aside
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
config.side_mode === 'normal' || config.side_mode === 'sidebar' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
|
@@ -183,6 +183,10 @@
|
||||
{
|
||||
label: '组合模式',
|
||||
value: 'combination'
|
||||
},
|
||||
{
|
||||
label: '侧边栏常驻',
|
||||
value: 'sidebar'
|
||||
}
|
||||
]
|
||||
|
||||
|
@@ -143,6 +143,38 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 重置密码对话框 -->
|
||||
<el-dialog
|
||||
v-model="resetPwdDialog"
|
||||
title="重置密码"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form :model="resetPwdInfo" ref="resetPwdForm" label-width="100px">
|
||||
<el-form-item label="用户账号">
|
||||
<el-input v-model="resetPwdInfo.userName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称">
|
||||
<el-input v-model="resetPwdInfo.nickName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<div class="flex w-full">
|
||||
<el-input class="flex-1" v-model="resetPwdInfo.password" placeholder="请输入新密码" show-password />
|
||||
<el-button type="primary" @click="generateRandomPassword" style="margin-left: 10px">
|
||||
生成随机密码
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeResetPwdDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="confirmResetPassword">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
v-model="addUserDialog"
|
||||
:size="appStore.drawerSize"
|
||||
@@ -332,28 +364,81 @@
|
||||
|
||||
initPage()
|
||||
|
||||
const resetPasswordFunc = (row) => {
|
||||
ElMessageBox.confirm('是否将此用户密码重置为123456?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await resetPassword({
|
||||
ID: row.ID
|
||||
// 重置密码对话框相关
|
||||
const resetPwdDialog = ref(false)
|
||||
const resetPwdForm = ref(null)
|
||||
const resetPwdInfo = ref({
|
||||
ID: '',
|
||||
userName: '',
|
||||
nickName: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 生成随机密码
|
||||
const generateRandomPassword = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
|
||||
let password = ''
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
resetPwdInfo.value.password = password
|
||||
// 复制到剪贴板
|
||||
navigator.clipboard.writeText(password).then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '密码已复制到剪贴板'
|
||||
})
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '复制失败,请手动复制'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.msg
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 打开重置密码对话框
|
||||
const resetPasswordFunc = (row) => {
|
||||
resetPwdInfo.value.ID = row.ID
|
||||
resetPwdInfo.value.userName = row.userName
|
||||
resetPwdInfo.value.nickName = row.nickName
|
||||
resetPwdInfo.value.password = ''
|
||||
resetPwdDialog.value = true
|
||||
}
|
||||
|
||||
// 确认重置密码
|
||||
const confirmResetPassword = async () => {
|
||||
if (!resetPwdInfo.value.password) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请输入或生成密码'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const res = await resetPassword({
|
||||
ID: resetPwdInfo.value.ID,
|
||||
password: resetPwdInfo.value.password
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg || '密码重置成功'
|
||||
})
|
||||
resetPwdDialog.value = false
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.msg || '密码重置失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭重置密码对话框
|
||||
const closeResetPwdDialog = () => {
|
||||
resetPwdInfo.value.password = ''
|
||||
resetPwdDialog.value = false
|
||||
}
|
||||
const setAuthorityIds = () => {
|
||||
tableData.value &&
|
||||
tableData.value.forEach((user) => {
|
||||
|
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<iframe
|
||||
ref="iframe"
|
||||
class="w-full border-0"
|
||||
:style="{ height: iframeHeight + 'px' }"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
html: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const iframe = ref(null)
|
||||
const iframeHeight = ref(400) // Default height
|
||||
|
||||
const renderContent = () => {
|
||||
if (!iframe.value) return
|
||||
|
||||
const doc = iframe.value.contentDocument || iframe.value.contentWindow.document
|
||||
|
||||
const htmlTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Production version of Vue -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"><\/script>
|
||||
<!-- Element Plus -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
|
||||
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"><\/script>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"><\/script>
|
||||
<style>
|
||||
body { margin: 0; padding: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Initialize Vue app
|
||||
const app = Vue.createApp({
|
||||
template: \`${props.html.replace(/`/g, '\\`')}\`,
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
});
|
||||
|
||||
// Use Element Plus
|
||||
app.use(ElementPlus);
|
||||
|
||||
// Mount the application
|
||||
app.mount('#app');
|
||||
|
||||
// Adjust iframe height
|
||||
setTimeout(() => {
|
||||
const height = document.body.scrollHeight;
|
||||
window.parent.postMessage({
|
||||
type: 'resize',
|
||||
height: height
|
||||
}, '*');
|
||||
}, 100);
|
||||
<\/script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
doc.open()
|
||||
doc.write(htmlTemplate)
|
||||
doc.close()
|
||||
}
|
||||
|
||||
// Listen for height updates from iframe
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'resize') {
|
||||
iframeHeight.value = event.data.height + 20 // Add padding
|
||||
}
|
||||
})
|
||||
|
||||
// Render with slight delay to ensure iframe is ready
|
||||
setTimeout(() => {
|
||||
renderContent()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
// Re-render when HTML changes
|
||||
watch(() => props.html, () => {
|
||||
setTimeout(() => {
|
||||
renderContent()
|
||||
}, 50)
|
||||
})
|
||||
</script>
|
@@ -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"
|
||||
@@ -534,7 +534,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
|
||||
@@ -1619,6 +1619,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 文件')
|
||||
|
@@ -1,24 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=3"
|
||||
title="此功能为开发环境使用,不建议发布到生产,具体使用效果请点我观看。"
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
title="此功能只针对授权用户开放,点我【购买授权】"
|
||||
/>
|
||||
<div class="gva-search-box">
|
||||
<div class="text-lg mb-2 text-gray-600">
|
||||
使用AI创建<a
|
||||
<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="8"
|
||||
:rows="5"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
@blur="handleBlur"
|
||||
@@ -28,12 +124,12 @@
|
||||
<el-tooltip effect="light">
|
||||
<template #content>
|
||||
<div>
|
||||
【完全免费】前往<a
|
||||
此功能仅针对授权用户开放,前往<a
|
||||
class="text-blue-600"
|
||||
href="https://plugin.gin-vue-admin.com/#/layout/userInfo/center"
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
target="_blank"
|
||||
>插件市场个人中心</a
|
||||
>申请AIPath,填入config.yaml的ai-path属性即可使用。
|
||||
>购买授权</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-button
|
||||
@@ -53,32 +149,54 @@
|
||||
<div v-if="!outPut">
|
||||
<el-empty :image-size="200"/>
|
||||
</div>
|
||||
<div v-if="outPut">
|
||||
<div v-for="(snippet, index) in htmlFromLLM" :key="index" class="mb-6 p-4 border">
|
||||
<el-button type="primary" :icon="Upload" class="px-2 py-1" @click="copySnippet(snippet)" plain>复制</el-button>
|
||||
<div class="mt-2">
|
||||
<iframe-renderer :html="snippet" />
|
||||
</div>
|
||||
</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} from 'vue'
|
||||
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'
|
||||
})
|
||||
|
||||
import IframeRenderer from '@/view/systemTools/autoCode/component/iframeRenderer.vue'
|
||||
|
||||
const handleFocus = () => {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
@@ -94,8 +212,8 @@ const handleKeydown = (event) => {
|
||||
}
|
||||
|
||||
// 复制方法:把某个字符串写进剪贴板
|
||||
const copySnippet = (htmlString) => {
|
||||
navigator.clipboard.writeText(htmlString)
|
||||
const copySnippet = (vueString) => {
|
||||
navigator.clipboard.writeText(vueString)
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
message: '复制成功',
|
||||
@@ -110,32 +228,199 @@ const copySnippet = (htmlString) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 选项模式相关变量
|
||||
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返回的html
|
||||
const htmlFromLLM = ref([])
|
||||
// 容纳llm返回的vue组件代码
|
||||
const htmlFromLLM = ref("")
|
||||
|
||||
const llmAutoFunc = async () => {
|
||||
const res = await createWeb({web: prompt.value, command: 'createWeb'})
|
||||
if (res.code === 0) {
|
||||
outPut.value = true
|
||||
// 使用mock数据模拟大模型返回值
|
||||
htmlFromLLM.value.push(res.data)
|
||||
// 存储加载的组件
|
||||
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 placeholder = ref(`"✨ 请详细描述您想要的页面,例如:
|
||||
• 页面用途(企业官网/电商页面/个人博客等)
|
||||
• 需要包含的主要内容板块
|
||||
• 偏好的风格(简约/科技感/温馨/专业等)
|
||||
• 需要特别强调的元素
|
||||
• 参考网站或配色建议
|
||||
|
||||
示例:'需要一个科技公司的产品介绍页,包含banner轮播图、三栏功能特点展示、客户案例模块,喜欢深蓝色调,参考苹果官网的简洁风格'"`)
|
||||
|
||||
// 当页面用途改变时,更新内容板块的选择
|
||||
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>
|
||||
|
@@ -123,6 +123,8 @@
|
||||
callback(new Error('不能为中文'))
|
||||
} else if (/^\d+$/.test(value[0])) {
|
||||
callback(new Error('不能够以数字开头'))
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
callback(new Error('只能包含英文字母、数字和下划线'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
|
@@ -42,9 +42,13 @@ export default ({ mode }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const base = "/"
|
||||
const root = "./"
|
||||
const outDir = "dist"
|
||||
|
||||
const config = {
|
||||
base: '/', // 编译后js导入的资源路径
|
||||
root: './', // index.html文件所在位置
|
||||
base: base, // 编译后js导入的资源路径
|
||||
root: root, // index.html文件所在位置
|
||||
publicDir: 'public', // 静态资源文件夹
|
||||
resolve: {
|
||||
alias
|
||||
@@ -79,7 +83,7 @@ export default ({ mode }) => {
|
||||
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
|
||||
manifest: false, // 是否产出manifest.json
|
||||
sourcemap: false, // 是否产出sourcemap.json
|
||||
outDir: 'dist', // 产出目录
|
||||
outDir: outDir, // 产出目录
|
||||
terserOptions: {
|
||||
compress: {
|
||||
//生产环境时移除console
|
||||
@@ -105,8 +109,7 @@ export default ({ mode }) => {
|
||||
]
|
||||
}),
|
||||
vuePlugin(),
|
||||
svgBuilder('./src/assets/icons/'),
|
||||
svgBuilder('./src/plugin/'),
|
||||
svgBuilder(['./src/plugin/','./src/assets/icons/'],base, outDir,'assets', NODE_ENV),
|
||||
[Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)],
|
||||
VueFilePathPlugin('./src/pathInfo.json')
|
||||
]
|
||||
|
Reference in New Issue
Block a user