feat: Modify the theme switching logic and add a new theme template to CLI. (#1143)

1.修改主题切换逻辑,默认值改成从注册表中的themes获取。
只有亮色和暗色两种默认主题的情况下,保持原来的逻辑,点击直接切换主题
用户添加了自定义主题,出现选择列表,在列表中切换主题
2.添加新主题模板
在终端中,用命令创建新主题,选择theme类型,添加主题模板,输入主题名称:demo-theme
在demo-theme下可以看到新创建的主题,cd demo-theme && pnpm i
将主题接入设计器
This commit is contained in:
xuanlid 2025-03-12 20:26:47 -07:00 committed by GitHub
parent f53c455eba
commit ee01af8ecb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 303 additions and 52 deletions

View File

@ -70,11 +70,17 @@ export default {
themes: [
{
id: 'engine.theme.light',
title: '亮色主题'
text: '浅色主题',
type: 'light',
icon: 'light',
oppositeTheme: 'dark'
},
{
id: 'engine.theme.dark',
title: '暗色主题'
text: '深色主题',
type: 'dark',
icon: 'dark',
oppositeTheme: 'light'
}
]
}

View File

@ -30,3 +30,10 @@ npx @opentiny/tiny-engine-cli@latest create-platform my-designer
```sh
npx @opentiny/tiny-engine-cli@latest create-plugin my-plugin
```
### create a tiny-engine theme
```sh
npx @opentiny/tiny-engine-cli@latest create-theme my-theme
```

View File

@ -33,6 +33,13 @@ npx @opentiny/tiny-engine-cli@latest create-platform my-designer
npx @opentiny/tiny-engine-cli@latest create-plugin my-tiny-engine-plugin
```
### 创建主题
```sh
# 创建名为 my-tiny-engine-theme 的 tiny-engine 主题
npx @opentiny/tiny-engine-cli@latest create-theme my-tiny-engine-theme
```
TODO:
- [ ] 插件开发文档&平台二次开发文档

View File

@ -14,7 +14,7 @@ import { cwd } from 'node:process'
import path from 'node:path'
import fs from 'fs-extra'
import chalk from 'chalk'
import { generateConfig, generatePackageJson } from './generateConfig'
import { generateConfig, generatePackageJson, generateThemeMeta } from './generateConfig'
const logger = console
@ -43,7 +43,7 @@ export function createPlatform(name, options = {}) {
fs.copySync(templatePath, destPath)
const configContent = generateConfig(mergedOptions)
const pkgContent = generatePackageJson(name, mergedOptions, templatePath)
const pkgContent = generatePackageJson(name, templatePath)
fs.outputFileSync(path.resolve(destPath, 'engine.config.js'), configContent)
fs.outputJSONSync(path.resolve(destPath, 'package.json'), pkgContent, { spaces: 2 })
@ -62,3 +62,23 @@ export function createPlugin(name) {
chalk.green(`create finish, run the follow command to start project: \ncd ${name} && npm install && npm run dev`)
)
}
export function createTheme(name, themeName) {
const sourcePath = path.join(__dirname, '../template/theme/')
const destPath = path.join(cwd(), name)
fs.copySync(sourcePath, destPath)
const configContent = generateThemeMeta(themeName)
const themePath = path.resolve(destPath, 'src/common.less')
fs.outputFileSync(path.resolve(destPath, 'meta.js'), configContent)
const content = fs.readFileSync(themePath, 'utf-8')
const outputContent = content.replace(/data-theme='custom'/g, `data-theme='${themeName}'`)
fs.writeFileSync(themePath, outputContent, 'utf-8')
const pkgContent = generatePackageJson(name, sourcePath)
fs.outputJSONSync(path.resolve(destPath, 'package.json'), pkgContent, { spaces: 2 })
logger.log(
chalk.green(`create finish, run the follow command to start project: \ncd ${name} && npm install && npm run dev`)
)
}

View File

@ -20,10 +20,24 @@ export default {
}
// 根据参数修改 package.json
export const generatePackageJson = (name, options, templatePath) => {
export const generatePackageJson = (name, templatePath) => {
const templatePackageJson = fs.readJSONSync(path.resolve(templatePath, 'package.json'))
templatePackageJson.name = name
return templatePackageJson
}
// 根据参数生成 config 文件内容
export const generateThemeMeta = (themeName = 'custom') => {
const metaContent = `
export default {
id: 'engine.theme.${themeName}',
text: '自定义主题',
type: '${themeName}',
icon: 'dark'
}
`
return metaContent
}

View File

@ -11,10 +11,35 @@
*/
import { Command, Option } from 'commander'
import { input, select } from '@inquirer/prompts'
import { createPlatform, createPlugin } from './commands/create.js'
import { createPlatform, createPlugin, createTheme } from './commands/create.js'
const program = new Command()
const messageMap = {
theme: {
message:
'Please enter the theme ID (used to uniquely identify the theme in code or configuration, such as "custom"). 请输入主题ID用于代码或配置中唯一标识该主题如“custom”',
validateMessage: 'theme ID can not be empty. 主题ID不允许为空。'
},
project: {
message: 'please enter the project name. 请输入项目名称',
validateMessage: 'project name can not be empty. 项目名称不允许为空。'
}
}
const getName = async (type) => {
return await input({
message: type ? messageMap.theme.message : messageMap.project.message,
validate: (inputName) => {
if (!inputName) {
return type ? messageMap.theme.validateMessage : messageMap.project.validateMessage
}
return true
}
})
}
program
.command('create-platform <name>')
.description('create a new tiny-engine platform 创建一个新的tiny-engine低代码平台')
@ -34,6 +59,14 @@ program
createPlugin(name)
})
program
.command('create-theme <name>')
.description('create a new tiny-engine theme 创建一个新的 tiny-engine 主题')
.action(async (name) => {
const themeName = await getName('theme')
createTheme(name, themeName)
})
program
.command('create')
.description('create a new tiny-engine platform or plugin by prompt 根据提示创建一个新的 tiny-engine 插件')
@ -50,27 +83,26 @@ program
name: 'plugin',
value: 'plugin',
description: 'create a new tiny-engine plugin 创建一个新的 tiny-engine 插件'
},
{
name: 'theme',
value: 'theme',
description: 'create a new tiny-engine theme 创建一个新的 tiny-engine 主题'
}
]
})
const projectName = await input({
message: 'please enter the project name. 请输入项目名称',
validate: (inputName) => {
if (!inputName) {
return 'project name can not be empty. 项目名称不允许为空。'
}
return true
}
})
const projectName = await getName()
const typeMapper = {
platform: createPlatform,
plugin: createPlugin
plugin: createPlugin,
theme: createTheme
}
typeMapper[type](projectName)
const themeName = type === 'theme' ? await getName(type) : ''
typeMapper[type](projectName, themeName)
})
program.parse(process.argv)

View File

@ -0,0 +1,3 @@
# tiny-engine Theme demo

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import './src/common.less'
import './src/styles/vars.less'
import metaData from './meta.js'
export default {
...metaData,
// 插件暴露的 api可以提供其他 api 进行调用,如果无需暴露,可为空
apis: {}
}

View File

@ -0,0 +1,7 @@
// 描述该插件的相关元信息
export default {
id: 'engine.theme.custom',
text: '自定义主题',
type: 'custom',
icon: 'dark'
}

View File

@ -0,0 +1,19 @@
{
"name": "theme-custom",
"version": "2.0.0",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "vite build"
},
"type": "module",
"main": "dist/index.js",
"files": [
"dist"
],
"license": "MIT",
"devDependencies": {
"vite": "^5.4.2"
}
}

View File

@ -0,0 +1,4 @@
// 通过修改common的变量控制主题颜色
:root[data-theme='custom'] {
--te-common-text-primary: var(--te-base-red-80);
}

View File

@ -0,0 +1,9 @@
// 自定义某个模块的变量
:root {
--te-styles-common-text-color-primary: var(--te-base-blue-80);
}
// 自定义区块管理分组删除气泡
.block-category-option-popper-wrapper {
--te-block-popper-content-text-color: var(--te-base-blue-80);
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { defineConfig } from 'vite'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
publicDir: false,
build: {
lib: {
entry: path.resolve(__dirname, './index.js'),
fileName: () => 'index.js',
formats: ['es']
},
rollupOptions: {
output: {
banner: 'import "./style.css"'
}
}
}
})

View File

@ -1,27 +1,54 @@
<template>
<div class="toolbar-theme-switch">
<toolbar-base :content="baseContent" :icon="baseIcon" :options="optionsData" @click-api="toChangeTheme">
<template v-if="position === 'collapse'">
<div class="toolbar-theme-switch-radio">
<div class="toolbar-theme-switch-radio-title">主题</div>
<tiny-radio-group v-model="state.theme" :options="THEME_DATA" class="theme-radio-group" @change="themeChange">
</tiny-radio-group>
<tiny-popover
width="130"
trigger="manual"
v-model="showpopover"
:visible-arrow="false"
popper-class="theme-popover"
>
<div class="theme-list">
<div
v-for="item in THEME_DATA"
:key="item.type"
:class="['theme-item', { active: state.theme === item.type }]"
@click="themeItemChange(item.type)"
>
{{ item.text }}
</div>
</div>
<template #reference>
<toolbar-base :content="baseContent" :icon="baseIcon" :options="optionsData" @click-api="changeThemeType">
<template v-if="position === 'collapse'">
<div class="toolbar-theme-switch-radio">
<div class="toolbar-theme-switch-radio-title">主题</div>
<tiny-radio-group
v-model="state.theme"
:options="radioThemeList"
:vertical="themeShowType ? false : true"
class="theme-radio-group"
@change="themeChange"
>
</tiny-radio-group>
</div>
</template>
</toolbar-base>
</template>
</toolbar-base>
</tiny-popover>
</div>
</template>
<script>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { ToolbarBase } from '@opentiny/tiny-engine-common'
import { RadioGroup } from '@opentiny/vue'
import { TinyRadioGroup, TinyPopover } from '@opentiny/vue'
import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
export default {
components: {
ToolbarBase,
TinyRadioGroup: RadioGroup
TinyRadioGroup,
TinyPopover
},
props: {
options: {
@ -46,27 +73,67 @@ export default {
return options
})
const radioThemeList = computed(() => {
return THEME_DATA.value.map((item) => ({ ...item, label: item.type }))
})
const baseContent = computed(() => (props.position === COLLAPSE ? '' : state.themeLabel))
const baseIcon = computed(() => (props.position === COLLAPSE ? '' : state.theme))
const baseIcon = computed(() => (props.position === COLLAPSE ? '' : state.themeIcon))
const showpopover = ref(false)
const themeShowType = computed(() => {
let filterList = THEME_DATA.value.filter((item) => ['light', 'dark'].includes(item.type)) || []
return THEME_DATA.value.length === filterList.length
})
const toChangeTheme = () => {
const theme = getTheme(state.theme).oppositeTheme
themeChange(theme)
}
const changeThemeType = () => {
if (props.position === COLLAPSE) {
return
}
if (themeShowType.value) {
toChangeTheme()
} else {
showpopover.value = true
}
}
const theme = getTheme(state.theme).oppositeTheme
const themeItemChange = (theme) => {
themeChange(theme)
showpopover.value = false
}
return {
THEME_DATA,
state,
optionsData,
radioThemeList,
baseContent,
baseIcon,
toChangeTheme,
themeChange
themeChange,
showpopover,
themeShowType,
themeItemChange,
changeThemeType
}
}
}
</script>
<style lang="less" scoped>
.theme-list {
.theme-item {
padding: 4px 16px;
margin: 0 -16px;
&:hover {
background-color: var(--te-toolbar-theme-popover-list-item-bg-color-hover);
}
}
.active {
background-color: var(--te-toolbar-theme-popover-list-item-bg-color-active);
}
}
</style>

View File

@ -1,37 +1,34 @@
import { reactive } from 'vue'
import { defineService, getMetaApi, getMergeMeta, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import { reactive, ref } from 'vue'
import {
defineService,
getMetaApi,
getMergeMeta,
META_SERVICE,
getMergeRegistry
} from '@opentiny/tiny-engine-meta-register'
import { setGlobalMonacoEditorTheme } from '@opentiny/tiny-engine-common'
const THEME_DATA = [
{
text: '浅色模式',
label: 'light',
oppositeTheme: 'dark'
},
{
text: '深色模式',
label: 'dark',
oppositeTheme: 'light'
}
]
let THEME_DATA = ref([])
const DEFAULT_THEME = THEME_DATA[0]
let DEFAULT_THEME = null
const themeState = reactive({
theme: DEFAULT_THEME.label,
themeLabel: DEFAULT_THEME.text
theme: '',
themeLabel: '',
themeIcon: ''
})
const getThemeData = () => THEME_DATA
const getThemeState = () => themeState
const getTheme = (theme) => {
return THEME_DATA.find((item) => theme === item.label) || DEFAULT_THEME
return THEME_DATA.value.find((item) => theme === item.type) || DEFAULT_THEME
}
const themeChange = (theme) => {
themeState.theme = getTheme(theme).label
themeState.theme = getTheme(theme).type
themeState.themeLabel = getTheme(themeState.theme).text
themeState.themeIcon = getTheme(themeState.theme).icon
document.documentElement.setAttribute('data-theme', themeState.theme)
const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
@ -45,9 +42,10 @@ export default defineService({
type: 'MetaService',
init: () => {
const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
THEME_DATA.value = getMergeRegistry('themes')
DEFAULT_THEME = THEME_DATA.value[0]
const theme =
localStorage.getItem(`tiny-engine-theme-${appId}`) || getMergeMeta('engine.config').theme || DEFAULT_THEME.label
localStorage.getItem(`tiny-engine-theme-${appId}`) || getMergeMeta('engine.config').theme || DEFAULT_THEME.type
themeChange(theme)
},
apis: () => ({

View File

@ -1,3 +1,7 @@
.toolbar-theme-switch {
--te-toolbar-theme-switch-radio-title: var(--te-common-text-secondary);
}
.theme-list {
--te-toolbar-theme-popover-list-item-bg-color-hover: var(--te-common-bg-container);
--te-toolbar-theme-popover-list-item-bg-color-active: var(--te-common-bg-container);
}

View File

@ -29,6 +29,9 @@ export default defineConfig({
formats: ['es']
},
rollupOptions: {
output: {
banner: 'import "./style.css"'
},
external: ['vue', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/]
}
}