fix(admin): 补回被根 .gitignore 误吞的 game-admin 源码/构建配置(克隆后构建缺失修复)

根 .gitignore 的笼统 build/ 与 .env/.env.* 规则(本为 Java 产物/密钥设计)
误吞 yudao 前端约定提交的源码与配置,导致 fd373db 克隆到构建机后:
  vite.config.ts 解析 ./build/vite、./build/vite/optimize 失败 → 构建崩。

补回(git add -f)9 个文件并加 game-admin 作用域豁免:
  - build/vite/{index,optimize}.ts:vite 插件/optimize 配置源码
  - src/views/infra/build/index.vue:yudao infra 视图(被 build/ 误吞)
  - .env / .env.dev / .env.local / .env.prod / .env.stage / .env.test:前端构建配置
    (.env=yudao 默认;.env.local=我方接线 baseURL=localhost:48080 + 关加解密 + mock 兜底)
  - 注:env 为前端公开构建配置非密钥,内网项目按约定入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-08 18:00:52 +00:00
parent fd373db41a
commit 4ba6cbda6b
10 changed files with 641 additions and 0 deletions

16
.gitignore vendored
View File

@ -16,3 +16,19 @@ coverage/
*.log
.DS_Store
.gstack/
# ── game-adminyudao 前端 fork作用域例外 ──
# yudao 约定将 build/vite 插件配置源码)与 .env*(前端构建配置,非密钥)纳入版本库;
# 上面为 Java 产物/密钥设计的笼统 build/、.env、.env.* 规则会误吞它们,
# 导致克隆后缺源码vite.config 解析 ./build/vite 失败)无法构建。故在 game-admin 子目录内豁免,
# 不影响后端模块的 build/ 产物忽略。
!game-admin/build/
!game-admin/build/**
!game-admin/src/views/infra/build/
!game-admin/src/views/infra/build/**
!game-admin/.env
!game-admin/.env.dev
!game-admin/.env.local
!game-admin/.env.prod
!game-admin/.env.stage
!game-admin/.env.test

37
game-admin/.env Normal file
View File

@ -0,0 +1,37 @@
# 标题造梦AI 品牌文案;技术框架名 yudao 保留不改)
VITE_APP_TITLE=造梦AI 运营后台
# 项目本地运行端口号
VITE_PORT=80
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN=true
# 租户开关
VITE_APP_TENANT_ENABLE=true
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true
# 文档地址的开关
VITE_APP_DOCALERT_ENABLE=true
# 百度统计
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
# API 加解密
VITE_APP_API_ENCRYPT_ENABLE = true
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
VITE_APP_API_ENCRYPT_ALGORITHM = AES
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
# 百度地图
VITE_BAIDU_MAP_KEY = 'Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN'

37
game-admin/.env.dev Normal file
View File

@ -0,0 +1,37 @@
# 开发环境本地只启动前端项目依赖开发环境后端、APP
NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 打包路径
VITE_BASE_PATH=/
# 输出路径
VITE_OUT_DIR=dist
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

42
game-admin/.env.local Normal file
View File

@ -0,0 +1,42 @@
# 本地开发环境本地启动所有项目前端、后端、APP时使用不依赖外部环境
NODE_ENV=development
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
# API 加解密开关本地对接造梦AI MVP 后端 http://localhost:48080后端默认未配置 yudao 加解密密钥,
# 故本地关闭,覆盖 .env 中的默认 true避免请求被加密导致 401/解析失败。真实环境如启用加解密,需与后端统一密钥后再开启)
VITE_APP_API_ENCRYPT_ENABLE = false
# 造梦AI 运营页 mock 兜底开关true=各 wanxiang api 返回本地 mock 数据MVP 后端未起时也可预览/独立构建);
# 后端联调就绪后改为 false 走真实 /admin-api。严格贴 admin-api 契约,发现缺口记 contractGaps 不私造。
VITE_APP_WANXIANG_MOCK = true

34
game-admin/.env.prod Normal file
View File

@ -0,0 +1,34 @@
# 生产环境:只在打包时使用
NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/
# 输出路径
VITE_OUT_DIR=dist-prod
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

34
game-admin/.env.stage Normal file
View File

@ -0,0 +1,34 @@
# 预发布环境:只在打包时使用
NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH='http://static-vue3.yudao.iocoder.cn/'
# 输出路径
VITE_OUT_DIR=dist-stage
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

34
game-admin/.env.test Normal file
View File

@ -0,0 +1,34 @@
# 测试环境:只在打包时使用
NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/admin-ui-vue3/
# 输出路径
VITE_OUT_DIR=dist-test
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

View File

@ -0,0 +1,99 @@
import { resolve } from 'path'
import Vue from '@vitejs/plugin-vue'
import VueJsx from '@vitejs/plugin-vue-jsx'
import progress from 'vite-plugin-progress'
import EslintPlugin from 'vite-plugin-eslint'
import PurgeIcons from 'vite-plugin-purge-icons'
import { ViteEjsPlugin } from 'vite-plugin-ejs'
// @ts-ignore
import ElementPlus from 'unplugin-element-plus/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
import topLevelAwait from 'vite-plugin-top-level-await'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
import UnoCSS from 'unocss/vite'
export function createVitePlugins() {
const root = process.cwd()
// 路径查找
function pathResolve(dir: string) {
return resolve(root, '.', dir)
}
return [
Vue(),
VueJsx(),
UnoCSS(),
progress(),
PurgeIcons(),
ElementPlus({}),
AutoImport({
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/ // .md
],
imports: [
'vue',
'vue-router',
// 可额外添加需要 autoImport 的组件
{
'@/hooks/web/useI18n': ['useI18n'],
'@/hooks/web/useMessage': ['useMessage'],
'@/hooks/web/useTable': ['useTable'],
'@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
'@/utils/formRules': ['required'],
'@/utils/dict': ['DICT_TYPE']
}
],
dts: 'src/types/auto-imports.d.ts',
resolvers: [ElementPlusResolver()],
eslintrc: {
enabled: false, // Default `false`
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
}
}),
Components({
// 生成自定义 `auto-components.d.ts` 全局声明
dts: 'src/types/auto-components.d.ts',
// 自定义组件的解析器
resolvers: [ElementPlusResolver()],
globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
}),
EslintPlugin({
cache: false,
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
}),
VueI18nPlugin({
runtimeOnly: true,
compositionOnly: true,
include: [resolve(__dirname, 'src/locales/**')]
}),
createSvgIconsPlugin({
iconDirs: [pathResolve('src/assets/svgs')],
symbolId: 'icon-[dir]-[name]',
}),
viteCompression({
verbose: true, // 是否在控制台输出压缩结果
disable: false, // 是否禁用
threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b
algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
ext: '.gz', // 生成的压缩包后缀
deleteOriginFile: false //压缩后是否删除源文件
}),
ViteEjsPlugin(),
topLevelAwait({
// https://juejin.cn/post/7152191742513512485
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`
})
]
}

View File

@ -0,0 +1,124 @@
const include = [
'qs',
'url',
'vue',
'sass',
'mitt',
'axios',
'pinia',
'dayjs',
'qrcode',
'unocss',
'vue-router',
'vue-types',
'vue-i18n',
'crypto-js',
'cropperjs',
'lodash-es',
'nprogress',
'web-storage-cache',
'@iconify/iconify',
'@vueuse/core',
'@zxcvbn-ts/core',
'echarts/core',
'echarts/charts',
'echarts/components',
'echarts/renderers',
'echarts-wordcloud',
'@wangeditor-next/editor',
'@wangeditor-next/editor-for-vue',
'@microsoft/fetch-event-source',
'markdown-it',
'markmap-view',
'markmap-lib',
'markmap-toolbar',
'highlight.js',
'element-plus',
'element-plus/es',
'element-plus/es/locale/lang/zh-cn',
'element-plus/es/locale/lang/en',
'element-plus/es/components/avatar/style/css',
'element-plus/es/components/space/style/css',
'element-plus/es/components/backtop/style/css',
'element-plus/es/components/form/style/css',
'element-plus/es/components/radio-group/style/css',
'element-plus/es/components/radio/style/css',
'element-plus/es/components/checkbox/style/css',
'element-plus/es/components/checkbox-group/style/css',
'element-plus/es/components/switch/style/css',
'element-plus/es/components/time-picker/style/css',
'element-plus/es/components/date-picker/style/css',
'element-plus/es/components/descriptions/style/css',
'element-plus/es/components/descriptions-item/style/css',
'element-plus/es/components/link/style/css',
'element-plus/es/components/tooltip/style/css',
'element-plus/es/components/drawer/style/css',
'element-plus/es/components/dialog/style/css',
'element-plus/es/components/checkbox-button/style/css',
'element-plus/es/components/option-group/style/css',
'element-plus/es/components/radio-button/style/css',
'element-plus/es/components/cascader/style/css',
'element-plus/es/components/color-picker/style/css',
'element-plus/es/components/input-number/style/css',
'element-plus/es/components/rate/style/css',
'element-plus/es/components/select-v2/style/css',
'element-plus/es/components/tree-select/style/css',
'element-plus/es/components/slider/style/css',
'element-plus/es/components/time-select/style/css',
'element-plus/es/components/autocomplete/style/css',
'element-plus/es/components/image-viewer/style/css',
'element-plus/es/components/upload/style/css',
'element-plus/es/components/col/style/css',
'element-plus/es/components/form-item/style/css',
'element-plus/es/components/alert/style/css',
'element-plus/es/components/breadcrumb/style/css',
'element-plus/es/components/select/style/css',
'element-plus/es/components/input/style/css',
'element-plus/es/components/breadcrumb-item/style/css',
'element-plus/es/components/tag/style/css',
'element-plus/es/components/pagination/style/css',
'element-plus/es/components/table/style/css',
'element-plus/es/components/table-v2/style/css',
'element-plus/es/components/table-column/style/css',
'element-plus/es/components/card/style/css',
'element-plus/es/components/row/style/css',
'element-plus/es/components/button/style/css',
'element-plus/es/components/menu/style/css',
'element-plus/es/components/sub-menu/style/css',
'element-plus/es/components/menu-item/style/css',
'element-plus/es/components/option/style/css',
'element-plus/es/components/dropdown/style/css',
'element-plus/es/components/dropdown-menu/style/css',
'element-plus/es/components/dropdown-item/style/css',
'element-plus/es/components/skeleton/style/css',
'element-plus/es/components/skeleton/style/css',
'element-plus/es/components/backtop/style/css',
'element-plus/es/components/menu/style/css',
'element-plus/es/components/sub-menu/style/css',
'element-plus/es/components/menu-item/style/css',
'element-plus/es/components/dropdown/style/css',
'element-plus/es/components/tree/style/css',
'element-plus/es/components/dropdown-menu/style/css',
'element-plus/es/components/dropdown-item/style/css',
'element-plus/es/components/badge/style/css',
'element-plus/es/components/breadcrumb/style/css',
'element-plus/es/components/breadcrumb-item/style/css',
'element-plus/es/components/image/style/css',
'element-plus/es/components/collapse-transition/style/css',
'element-plus/es/components/timeline/style/css',
'element-plus/es/components/timeline-item/style/css',
'element-plus/es/components/collapse/style/css',
'element-plus/es/components/collapse-item/style/css',
'element-plus/es/components/button-group/style/css',
'element-plus/es/components/text/style/css',
'element-plus/es/components/segmented/style/css',
'@element-plus/icons-vue',
'element-plus/es/components/footer/style/css',
'element-plus/es/components/empty/style/css',
'element-plus/es/components/mention/style/css',
'element-plus/es/components/progress/style/css'
]
const exclude = ['@iconify/json']
export { include, exclude }

View File

@ -0,0 +1,184 @@
<template>
<ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<!-- 表单设计器 -->
<div
class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
>
<fc-designer class="my-designer" ref="designer" :config="designerConfig">
<template #handle>
<el-button size="small" type="primary" plain @click="showJson">生成JSON</el-button>
<el-button size="small" type="success" plain @click="showOption">生成Options</el-button>
<el-button size="small" type="danger" plain @click="showTemplate">生成组件</el-button>
</template>
</fc-designer>
</div>
</ContentWrap>
<!-- 弹窗表单预览 -->
<Dialog v-model="dialogVisible" :title="dialogTitle" max-height="600">
<div v-if="dialogVisible" ref="editor">
<el-button style="float: right" @click="copy(formData)">
{{ t('common.copy') }}
</el-button>
<el-scrollbar height="580">
<div>
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div>
</el-scrollbar>
</div>
</Dialog>
</template>
<script lang="ts" setup>
import { useFormCreateDesigner } from '@/components/FormCreate'
import { useClipboard } from '@vueuse/core'
import { isString } from '@/utils/is'
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import xml from 'highlight.js/lib/languages/java'
import json from 'highlight.js/lib/languages/json'
import formCreate from '@form-create/element-ui'
defineOptions({ name: 'InfraBuild' })
const { t } = useI18n() //
const message = useMessage() //
//
const designerConfig = ref({
switchType: [], // ,
autoActive: true, //
useTemplate: false, // vue2
formOptions: {
form: {
labelWidth: '100px' // label 100px
}
}, //
fieldReadonly: false, // field
hiddenDragMenu: false, //
hiddenDragBtn: false, //
hiddenMenu: [], //
hiddenItem: [], //
hiddenItemConfig: {}, //
disabledItemConfig: {}, //
showSaveBtn: false, //
showConfig: true, //
showBaseForm: true, //
showControl: true, //
showPropsForm: true, //
showEventForm: true, //
showValidateForm: true, //
showFormConfig: true, //
showInputData: true, //
showDevice: true, //
appendConfigData: [] // formData
})
const designer = ref() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formType = ref(-1) // 0 - JSON1 - Options2 -
const formData = ref('') //
useFormCreateDesigner(designer) //
/** 打开弹窗 */
const openModel = (title: string) => {
dialogVisible.value = true
dialogTitle.value = title
}
/** 生成 JSON */
const showJson = () => {
openModel('生成 JSON')
formType.value = 0
formData.value = designer.value.getRule()
}
/** 生成 Options */
const showOption = () => {
openModel('生成 Options')
formType.value = 1
formData.value = designer.value.getOption()
}
/** 生成组件 */
const showTemplate = () => {
openModel('生成组件')
formType.value = 2
formData.value = makeTemplate()
}
const makeTemplate = () => {
const rule = designer.value.getRule()
const opt = designer.value.getOption()
return `<template>
<form-create
v-model:api="fApi"
:rule="rule"
:option="option"
@submit="onSubmit"
></form-create>
</template>
<script setup lang=ts>
const faps = ref(null)
const rule = ref('')
const option = ref('')
const init = () => {
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
option.value = formCreate.parseJson('${JSON.stringify(opt)}')
}
const onSubmit = (formData) => {
//todo
}
init()
<\/script>`
}
/** 复制 **/
const copy = async (text: string) => {
const textToCopy = JSON.stringify(text, null, 2)
const { copy, copied, isSupported } = useClipboard({ legacy: true, source: textToCopy })
if (!isSupported) {
message.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
}
/**
* 代码高亮
*/
const highlightedCode = (code: string) => {
//
let language = 'json'
if (formType.value === 2) {
language = 'xml'
}
// debugger
if (!isString(code)) {
code = JSON.stringify(code, null, 2)
}
//
const result = hljs.highlight(code, { language: language, ignoreIllegals: true })
return result.value || '&nbsp;'
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('json', json)
})
</script>
<style>
.my-designer {
._fc-l,
._fc-m,
._fc-r {
border-top: none;
}
}
</style>