feat: 增加AI生成页面功能。

This commit is contained in:
pixelmaxQM
2025-04-07 17:03:18 +08:00
parent e111b01f16
commit 5fe5caaf2b
6 changed files with 130 additions and 532 deletions

View File

@@ -118,7 +118,7 @@ func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) {
return return
} }
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"])) path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("chat/%s", llm["mode"]))
res, err := request.HttpRequest( res, err := request.HttpRequest(
path, path,
"POST", "POST",

View File

@@ -173,6 +173,17 @@ export const eye = (data) => {
} }
export const createWeb = (data) => {
return service({
url: '/autoCode/llmAuto',
method: 'post',
data: { ...data, mode: 'painter' },
timeout: 1000 * 60 * 10
})
}
export const addFunc = (data) => { export const addFunc = (data) => {
return service({ return service({
url: '/autoCode/addFunc', url: '/autoCode/addFunc',

View File

@@ -16,6 +16,7 @@
"/src/view/example/breakpoint/breakpoint.vue": "BreakPoint", "/src/view/example/breakpoint/breakpoint.vue": "BreakPoint",
"/src/view/example/customer/customer.vue": "Customer", "/src/view/example/customer/customer.vue": "Customer",
"/src/view/example/index.vue": "Example", "/src/view/example/index.vue": "Example",
"/src/view/example/upload/scanUpload.vue": "scanUpload",
"/src/view/example/upload/upload.vue": "Upload", "/src/view/example/upload/upload.vue": "Upload",
"/src/view/init/index.vue": "Init", "/src/view/init/index.vue": "Init",
"/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",
@@ -53,6 +54,7 @@
"/src/view/superAdmin/user/user.vue": "User", "/src/view/superAdmin/user/user.vue": "User",
"/src/view/system/state.vue": "State", "/src/view/system/state.vue": "State",
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
"/src/view/systemTools/autoCode/component/iframeRenderer.vue": "IframeRenderer",
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
"/src/view/systemTools/autoCode/index.vue": "AutoCode", "/src/view/systemTools/autoCode/index.vue": "AutoCode",
"/src/view/systemTools/autoCode/picture.vue": "Picture", "/src/view/systemTools/autoCode/picture.vue": "Picture",

View File

@@ -0,0 +1,99 @@
<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>

View File

@@ -4,7 +4,7 @@
href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=3" href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=3"
title="此功能为开发环境使用,不建议发布到生产,具体使用效果请点我观看。" title="此功能为开发环境使用,不建议发布到生产,具体使用效果请点我观看。"
/> />
<div v-if="!isAdd" class="gva-search-box"> <div class="gva-search-box">
<div class="text-lg mb-2 text-gray-600"> <div class="text-lg mb-2 text-gray-600">
使用AI创建<a 使用AI创建<a
class="text-blue-600 text-sm ml-4" class="text-blue-600 text-sm ml-4"
@@ -17,39 +17,13 @@
<el-input <el-input
v-model="prompt" v-model="prompt"
:maxlength="2000" :maxlength="2000"
:placeholder="`现已完全免费\n试试复制一张图片然后按下ctrl+v或者commend+v\n试试描述你的表让AI帮你完成。\n此功能需要到插件市场个人中心获取自己的AI-Path把AI-Path填入config.yaml下的autocode-->ai-path重启项目即可使用。\n按下 Ctrl+Enter 或 Cmd+Enter 直接生成`" :placeholder="`请大体描述您希望生成的页面`"
:rows="5" :rows="5"
resize="none" resize="none"
type="textarea" type="textarea"
@blur="handleBlur" @blur="handleBlur"
@focus="handleFocus" @focus="handleFocus"
/> />
<div class="flex absolute right-28 bottom-2">
<el-tooltip effect="light">
<template #content>
<div>
完全免费前往<a
class="text-blue-600"
href="https://plugin.gin-vue-admin.com/#/layout/userInfo/center"
target="_blank"
>插件市场个人中心</a
>申请AIPath填入config.yaml的ai-path属性即可使用
</div>
</template>
<el-button
:disabled="form.onlyTemplate"
type="primary"
@click="eyeFunc()"
>
<el-icon size="18">
<ai-gva/>
</el-icon>
识图
</el-button>
</el-tooltip>
</div>
<div class="flex absolute right-2 bottom-2"> <div class="flex absolute right-2 bottom-2">
<el-tooltip effect="light"> <el-tooltip effect="light">
<template #content> <template #content>
@@ -63,7 +37,6 @@
</div> </div>
</template> </template>
<el-button <el-button
:disabled="form.onlyTemplate"
type="primary" type="primary"
@click="llmAutoFunc()" @click="llmAutoFunc()"
> >
@@ -81,14 +54,11 @@
<el-empty :image-size="200"/> <el-empty :image-size="200"/>
</div> </div>
<div v-if="outPut"> <div v-if="outPut">
<div <div v-for="(snippet, index) in htmlFromLLM" :key="index" class="mb-6 p-4 border">
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> <el-button type="primary" :icon="Upload" class="px-2 py-1" @click="copySnippet(snippet)" plain>复制</el-button>
<!-- v-html 渲染该片段 --> <div class="mt-2">
<div class="mt-2" v-html="snippet"></div> <iframe-renderer :html="snippet" />
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -97,25 +67,23 @@
<script setup> <script setup>
import { import {
getDB, eye, createWeb
getMeta,
getPackageApi,
llmAuto, eye
} from '@/api/autoCode' } from '@/api/autoCode'
import {getDict} from '@/utils/dictionary' import {ref} from 'vue'
import {ref, watch, nextTick} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {ElMessage, ElMessageBox} from 'element-plus'
import WarningBar from '@/components/warningBar/warningBar.vue' import WarningBar from '@/components/warningBar/warningBar.vue'
defineOptions({
name: 'Picture'
})
import IframeRenderer from '@/view/systemTools/autoCode/component/iframeRenderer.vue'
const handleFocus = () => { const handleFocus = () => {
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
document.addEventListener('paste', handlePaste);
} }
const handleBlur = () => { const handleBlur = () => {
document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('paste', handlePaste);
} }
const handleKeydown = (event) => { const handleKeydown = (event) => {
@@ -124,402 +92,22 @@ const handleKeydown = (event) => {
} }
} }
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
const reader = new FileReader();
reader.onload = async (e) => {
const base64String = e.target.result;
const res = await eye({picture: base64String, command: 'eye'})
if (res.code === 0) {
prompt.value = res.data
llmAutoFunc()
}
};
reader.readAsDataURL(file);
}
}
};
const prompt = ref('') const prompt = ref('')
const eyeFunc = async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const base64String = e.target.result;
const res = await eye({picture: base64String, command: 'eye'})
if (res.code === 0) {
prompt.value = res.data
llmAutoFunc()
}
};
reader.readAsDataURL(file);
}
};
input.click();
}
// 判断是否返回的标志 // 判断是否返回的标志
const outPut = ref(false) const outPut = ref(false)
// 容纳llm返回的html // 容纳llm返回的html
const htmlFromLLM = ref([]) const htmlFromLLM = ref([])
const llmAutoFunc = async (flag) => { const llmAutoFunc = async () => {
const res = await llmAuto({ const res = await createWeb({web: prompt.value, command: 'createWeb'})
prompt: flag ? '结构体名称为' + form.value.structName : prompt.value
})
if (res.code === 0) { if (res.code === 0) {
outPut.value = true outPut.value = true
// 使用mock数据模拟大模型返回值 // 使用mock数据模拟大模型返回值
const mock1 = `<div class="flex flex-col min-h-screen bg-white"> htmlFromLLM.value.push(res.data)
<!-- 顶部导航栏 -->
<header class="bg-white shadow-md">
<div class="container mx-auto px-6 py-4 flex items-center justify-between">
<div class="text-2xl font-bold text-blue-600">LOGO</div>
<nav class="flex space-x-6">
<a href="#" class="text-black hover:text-blue-600 transition duration-300">首页</a>
<a href="#" class="text-black hover:text-blue-600 transition duration-300">关于我们</a>
<a href="#" class="text-black hover:text-blue-600 transition duration-300">服务</a>
<a href="#" class="text-black hover:text-blue-600 transition duration-300">联系我们</a>
</nav>
</div>
</header>
<!-- 中间内容区域 -->
<main class="flex-grow container mx-auto px-6 py-8 flex">
<!-- 左侧边栏 -->
<aside class="w-1/4 bg-gray-50 p-6 rounded-lg shadow-sm">
<nav class="space-y-3">
<a href="#" class="block text-black hover:text-blue-600 transition duration-300">导航1</a>
<a href="#" class="block text-black hover:text-blue-600 transition duration-300">导航2</a>
<a href="#" class="block text-black hover:text-blue-600 transition duration-300">导航3</a>
<a href="#" class="block text-black hover:text-blue-600 transition duration-300">导航4</a>
</nav>
</aside>
<!-- 右侧主要内容区 -->
<div class="w-3/4 ml-8">
<!-- 轮播图 -->
<div class="bg-gray-200 h-64 mb-8 rounded-lg shadow-sm flex items-center justify-center text-gray-600">
轮播图
</div>
<!-- 新闻列表 -->
<div class="mb-8">
<h2 class="text-xl font-bold text-black mb-4">最新新闻</h2>
<ul class="space-y-4">
<li class="p-4 bg-gray-50 rounded-lg shadow-sm hover:shadow-md transition duration-300">
<a href="#" class="text-black hover:text-blue-600 transition duration-300">新闻标题1</a>
<p class="text-sm text-gray-600 mt-1">新闻简介内容...</p>
</li>
<li class="p-4 bg-gray-50 rounded-lg shadow-sm hover:shadow-md transition duration-300">
<a href="#" class="text-black hover:text-blue-600 transition duration-300">新闻标题2</a>
<p class="text-sm text-gray-600 mt-1">新闻简介内容...</p>
</li>
<li class="p-4 bg-gray-50 rounded-lg shadow-sm hover:shadow-md transition duration-300">
<a href="#" class="text-black hover:text-blue-600 transition duration-300">新闻标题3</a>
<p class="text-sm text-gray-600 mt-1">新闻简介内容...</p>
</li>
</ul>
</div>
<!-- 广告位 -->
<div class="bg-gray-200 h-32 rounded-lg shadow-sm flex items-center justify-center text-gray-600">
广告位
</div>
</div>
</main>
<!-- 底部 -->
<footer class="bg-gray-800 text-white py-6">
<div class="container mx-auto px-6 text-center">
<p class="text-sm">© 2023 公司名称. 保留所有权利.</p>
<p class="text-sm mt-2">联系我们: info@example.com</p>
</div>
</footer>
</div>`
const mock2 = ` <div class="flex flex-col min-h-screen bg-[#e9f0fc]">
<!-- 上布局 -->
<div class="flex flex-1">
<!-- 左侧登录表单区域 -->
<div class="w-1/2 p-12 flex items-center justify-center">
<div class="w-full max-w-md bg-white p-8 rounded-xl shadow-2xl">
<h2 class="text-3xl font-bold text-[#2c3e50] mb-8 text-center">登录</h2>
<form class="space-y-6">
<div>
<label class="block text-sm font-medium text-[#2c3e50] mb-2">用户名</label>
<input type="text" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#20a0ff] transition duration-300" placeholder="请输入用户名" />
</div>
<div>
<label class="block text-sm font-medium text-[#2c3e50] mb-2">密码</label>
<input type="password" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#20a0ff] transition duration-300" placeholder="请输入密码" />
</div>
<div>
<label class="block text-sm font-medium text-[#2c3e50] mb-2">验证码</label>
<div class="flex space-x-4">
<input type="text" class="w-3/4 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#20a0ff] transition duration-300" placeholder="请输入验证码" />
<button class="w-1/4 px-4 py-3 bg-[#20a0ff] text-white rounded-lg hover:bg-[#1d8fe0] transition duration-300">获取验证码</button>
</div>
</div>
<button type="submit" class="w-full px-4 py-3 bg-[#20a0ff] text-white rounded-lg hover:bg-[#1d8fe0] transition duration-300">登录</button>
</form>
</div>
</div>
<!-- 右侧功能介绍区 -->
<div class="w-1/2 p-12 flex items-center justify-center">
<div class="relative w-full max-w-2xl">
<!-- 复古电视机样式 -->
<div class="relative bg-gray-800 rounded-2xl p-8 shadow-2xl">
<div class="absolute inset-0 bg-black rounded-2xl opacity-50"></div>
<div class="relative z-10">
<!-- 电视屏幕 -->
<div class="bg-gray-700 rounded-lg p-6">
<div class="bg-gray-900 rounded-lg p-6">
<!-- SVG图形和动画内容 -->
<svg class="w-full h-64" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#20a0ff" />
<text x="50%" y="50%" text-anchor="middle" fill="#ffffff" font-size="24" dy=".3em">TV</text>
<!-- 模拟电视内容 -->
<animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="10s" repeatCount="indefinite" />
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 中部空白区域 -->
<div class="flex-1 bg-[#e9f0fc] p-12">
<!-- 广告或其他内容 -->
<div class="bg-white p-8 rounded-xl shadow-2xl text-center">
<h3 class="text-2xl font-bold text-[#2c3e50] mb-4">广告位</h3>
<p class="text-gray-600">这里可以放置广告或其他内容。</p>
</div>
</div>
<!-- 底部 -->
<footer class="bg-[#2c3e50] text-white py-6">
<div class="container mx-auto px-6 text-center">
<p class="text-sm">© 2023 公司名称. 保留所有权利.</p>
<p class="text-sm mt-2">联系我们: info@example.com</p>
</div>
</footer>
</div>`
htmlFromLLM.value.push(mock1, mock2)
console.log(htmlFromLLM.value);
}
}
// 复制方法:把某个字符串写进剪贴板
const copySnippet = (htmlString) => {
navigator.clipboard.writeText(htmlString)
.then(() => {
ElMessage({
message: '复制成功',
type: 'success',
})
})
.catch(err => {
ElMessage({
message: '复制失败',
type: 'warning',
})
})
}
const isAdd = ref(false)
defineOptions({
name: 'AutoCode'
})
const gormModelList = ['id', 'created_at', 'updated_at', 'deleted_at']
const dataModelList = ['created_by', 'updated_by', 'deleted_by']
const route = useRoute()
const router = useRouter()
const dbform = ref({
businessDB: '',
dbName: '',
tableName: ''
})
const fdMap = ref({})
const form = ref({
structName: '',
tableName: '',
packageName: '',
package: '',
abbreviation: '',
description: '',
businessDB: '',
autoCreateApiToSql: true,
autoCreateMenuToSql: true,
autoCreateBtnAuth: false,
autoMigrate: true,
gvaModel: true,
autoCreateResource: false,
onlyTemplate: false,
isTree: false,
treeJson: "",
fields: []
})
const rules = ref({
structName: [
{required: true, message: '请输入结构体名称', trigger: 'blur'}
],
abbreviation: [
{required: true, message: '请输入结构体简称', trigger: 'blur'}
],
description: [
{required: true, message: '请输入结构体描述', trigger: 'blur'}
],
packageName: [
{
required: true,
message: '文件名称sysXxxxXxxx',
trigger: 'blur'
}
],
package: [{required: true, message: '请选择package', trigger: 'blur'}]
})
const dbList = ref([])
const dbOptions = ref([])
const getDbFunc = async () => {
dbform.value.dbName = ''
dbform.value.tableName = ''
const res = await getDB({businessDB: dbform.value.businessDB})
if (res.code === 0) {
dbOptions.value = res.data.dbs
dbList.value = res.data.dbList
}
}
const setFdMap = async () => {
const fdTypes = ['string', 'int', 'bool', 'float64', 'time.Time']
fdTypes.forEach(async (fdtype) => {
const res = await getDict(fdtype)
res &&
res.forEach((item) => {
fdMap.value[item.label] = fdtype
})
})
}
const getAutoCodeJson = async (id) => {
const res = await getMeta({id: Number(id)})
if (res.code === 0) {
const add = route.query.isAdd
isAdd.value = add
form.value = JSON.parse(res.data.meta)
if (isAdd.value) {
form.value.fields.forEach((item) => {
item.disabled = true
})
}
}
}
const pkgs = ref([])
const getPkgs = async () => {
const res = await getPackageApi()
if (res.code === 0) {
pkgs.value = res.data.pkgs
} }
} }
const init = () => {
getDbFunc()
setFdMap()
getPkgs()
const id = route.params.id
if (id) {
getAutoCodeJson(id)
}
}
init()
watch(
() => route.params.id,
() => {
if (route.name === 'autoCodeEdit') {
init()
}
}
)
const getCatch = () => {
const data = window.sessionStorage.getItem('autoCode')
if (data) {
form.value = JSON.parse(data)
}
}
const clearCatch = async () => {
form.value = {
structName: '',
tableName: '',
packageName: '',
package: '',
abbreviation: '',
description: '',
businessDB: '',
autoCreateApiToSql: true,
autoCreateMenuToSql: true,
autoCreateBtnAuth: false,
autoMigrate: true,
gvaModel: true,
autoCreateResource: false,
onlyTemplate: false,
isTree: false,
treeJson: "",
fields: []
}
await nextTick()
window.sessionStorage.removeItem('autoCode')
}
getCatch()
watch(
() => form.value.onlyTemplate,
(val) => {
if (val) {
ElMessageBox.confirm(
'使用基础模板将不会生成任何结构体和CURD,仅仅配置enter等属性方便自行开发非CURD逻辑',
'注意',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
form.value.fields = []
})
.catch(() => {
form.value.onlyTemplate = false
})
}
}
)
</script> </script>

View File

@@ -18,108 +18,6 @@ module.exports = {
} }
} }
}, },
safelist: [
/*
1) 常见的自定义颜色写法,如 bg-[#xxxxxx]、text-[#xxxxxx]、border-[#xxxxxx]
如果 LLM/接口频繁返回各种 [#RRGGBB] 形式,这个 pattern 可以保留它们的CSS。
*/
{ pattern: /^bg-\[.*\]$/ },
{ pattern: /^text-\[.*\]$/ },
{ pattern: /^border-\[.*\]$/ },
/*
2) Tailwind 默认调色板里的常见前缀
下面以 (red|green|blue|gray|indigo|...) 为例,你可根据自己项目加减。
同时包括不同深度 (50,100,200,...,900),也可再详细拆分。
*/
{
pattern: /^(bg|text|border)-(red|green|blue|gray|indigo|yellow|purple|pink|cyan|teal|orange|amber|lime|emerald|fuchsia|rose|sky|violet|stone|neutral)-(50|100|200|300|400|500|600|700|800|900)$/
},
/*
3) 大小相关padding、margin等
m-*, p-* 以及更精细的 mt-*, mb-*...
匹配数字、1/2、px、等常见写法也包括 m-auto
你可以根据自己需求加减。
*/
{ pattern: /^m-[0-9]+$/ },
{ pattern: /^mx-[0-9]+$/ },
{ pattern: /^my-[0-9]+$/ },
{ pattern: /^mt-[0-9]+$/ },
{ pattern: /^mr-[0-9]+$/ },
{ pattern: /^mb-[0-9]+$/ },
{ pattern: /^ml-[0-9]+$/ },
{ pattern: /^m-(auto|px)$/ },
{ pattern: /^p-[0-9]+$/ },
{ pattern: /^px-[0-9]+$/ },
{ pattern: /^py-[0-9]+$/ },
{ pattern: /^pt-[0-9]+$/ },
{ pattern: /^pr-[0-9]+$/ },
{ pattern: /^pb-[0-9]+$/ },
{ pattern: /^pl-[0-9]+$/ },
{ pattern: /^p-(auto|px)$/ },
/*
4) 宽高相关: w-*, h-*,以及自定义的 w-[300px]、h-[50vh] 等。
*/
{ pattern: /^w-.*$/ },
{ pattern: /^h-.*$/ },
/*
5) 文本尺寸/排版
常见如 text-sm, text-lg, text-xl, text-2xl... 也可加正则覆盖 text-[数字]xl
*/
{ pattern: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)$/ },
{ pattern: /^text-\d+xl$/ },
/*
6) Flex 相关
*/
"flex",
{ pattern: /^flex-(row|col|wrap|nowrap|row-reverse|col-reverse)$/ },
{ pattern: /^items-(start|end|center|baseline|stretch)$/ },
{ pattern: /^justify-(start|end|center|between|around|evenly)$/ },
{ pattern: /^content-(start|end|center|between|around|evenly)$/ },
/*
7) 边框 & 圆角 & 阴影
*/
{ pattern: /^rounded(-(none|sm|md|lg|xl|2xl|3xl|full))?$/ },
{ pattern: /^rounded-[trbl]{1,2}(-(none|sm|md|lg|xl|2xl|3xl|full))?$/ }, // 形如 rounded-t-lg、rounded-bl-md
{ pattern: /^shadow(-(sm|md|lg|xl|2xl|inner|none))?$/ },
{ pattern: /^border(-(0|2|4|8))?$/ },
{ pattern: /^border-[trbl]{1,2}(-(0|2|4|8))?$/ },
/*
8) 文本对齐 & 显示模式
*/
{ pattern: /^text-(left|center|right|justify)$/ },
{ pattern: /^(block|inline|inline-block|inline-flex|hidden)$/ },
/*
9) 状态变体(如 hover:, focus:, active: 等)
允许 hover:bg-red-500、focus:border-blue-500 等
这里用 .+ 去捕获前缀后所有东西不过要小心可能会保留过多无用CSS
*/
{ pattern: /^hover:.+$/ },
{ pattern: /^focus:.+$/ },
{ pattern: /^active:.+$/ },
/*
10) 你自己项目中最常出现的其他 patterns (可自行添加)
- z-[数字]
- absolute / relative / fixed / sticky
- top-[数字] / left-[数字]
- grid / gap-[数字]
- ...
*/
// { pattern: /^z-\d+$/ },
// "absolute", "relative", "fixed", "sticky",
// { pattern: /^top-\[.*\]$/ },
// { pattern: /^left-\[.*\]$/ },
// "grid", { pattern: /^gap-\d+$/ },
],
darkMode: 'class', darkMode: 'class',
plugins: [] plugins: []
} }