feat: 增加openinula模板转换脚本 (#1567)

This commit is contained in:
Jack Liu 2024-04-29 17:30:14 +08:00 committed by GitHub
parent f834ca7e05
commit 61aa2cd82c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 2358 additions and 51 deletions

View File

@ -51,7 +51,11 @@
"build:entry-react": "esno src/index.ts build:entry-react",
"create:mapping-react": "esno src/commands/create/create-mapping-react.ts",
"build:react": "esno src/index.ts build:react",
"build:chartTheme": "esno src/index.ts build:chartTheme"
"// ----------------------OpenInula脚本---------------------- ": "",
"build:entry-openinula": "esno src/index.ts build:entry-openinula",
"create:mapping-openinula": "esno src/commands/create/create-mapping-openinula.ts",
"build:openinula": "esno src/index.ts build:openinula",
"create:ui-openinula": "esno src/index.ts create:ui-openinula"
},
"dependencies": {
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
@ -62,4 +66,4 @@
"vite-plugin-svgr": "^3.2.0",
"vite-svg-loader": "^3.6.0"
}
}
}

View File

@ -0,0 +1,10 @@
import [[UNAME]] from './src/index[[SUFFIX]]'
import '@opentiny/vue-theme/[[NAME]]/index.less'
import { version } from './package.json'
[[UNAME]].version = version
export default [[UNAME]]

View File

@ -0,0 +1,25 @@
{
"name": "@opentiny/openinula-[[NAME]]",
"version": "3.[[MINOR]].0",
"description": "",
"main": "lib/index.js",
"module": "index.ts",
"sideEffects": false,
"type": "module",
"scripts": {
"build": "pnpm -w build:ui-openinula $npm_package_name",
"//postversion": "pnpm build"
},
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "^0.31.0"
},
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/openinula-common": "workspace:~",
"@opentiny/openinula-icon": "workspace:~",
"@opentiny/vue-theme": "workspace:~",
"@opentiny/vue-theme-mobile": "workspace:~"
},
"license": "MIT"
}

View File

@ -0,0 +1,15 @@
import pc from './pc'
import mobile from './mobile'
import mobileFirst from './mobile-first'
export default function (props) {
const { tiny_mode = 'pc' } = props
const S = {
pc,
mobile,
'mobile-first': mobileFirst
}[tiny_mode]
return S(props)
}

View File

@ -0,0 +1,38 @@
import { renderless, api } from '@opentiny/vue-renderless/[[NAME]]/vue'
import { vc, If, Component, Slot, Transition, useSetup, useVm } from '@opentiny/openinula-common'
import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/openinula-icon'
const $constants = {
}
export default function [[UNAME]](props) {
const {
_constants,
} = props
const defaultProps = Object.assign(
{
},
props
)
const { ref, current: vm, parent } = useVm()
const { [[API]] } = useSetup({
props: defaultProps,
renderless,
api,
constants: _constants,
vm,
parent
})
return (
[[TEMPLATE]]
)
}

View File

@ -0,0 +1,39 @@
import { renderless, api } from '@opentiny/vue-renderless/[[NAME]]/vue'
import { vc, If, Component, Slot, Transition, useSetup, useVm } from '@opentiny/openinula-common'
import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/openinula-icon'
import '@opentiny/vue-theme-mobile/[[NAME]]/index.less'
const $constants = {
}
export default function [[UNAME]](props) {
const {
_constants,
} = props
const defaultProps = Object.assign(
{
},
props
)
const { ref, current: vm, parent } = useVm()
const { [[API]] } = useSetup({
props: defaultProps,
renderless,
api,
constants: _constants,
vm,
parent
})
return (
[[TEMPLATE]]
)
}

View File

@ -0,0 +1,39 @@
import { renderless, api } from '@opentiny/vue-renderless/[[NAME]]/vue'
import { vc, If, Component, Slot, Transition, useSetup, useVm } from '@opentiny/openinula-common'
import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/openinula-icon'
import '@opentiny/vue-theme/[[NAME]]/index.less'
const $constants = {
}
export default function [[UNAME]](props) {
const {
_constants,
} = props
const defaultProps = Object.assign(
{
},
props
)
const { ref, current: vm, parent } = useVm()
const { [[API]] } = useSetup({
props: defaultProps,
renderless,
api,
constants: _constants,
vm,
parent
})
return (
[[TEMPLATE]]
)
}

View File

@ -0,0 +1,98 @@
/**
* pc.js / mobile.js / mobile-first.js / index.js
*/
import fs from 'fs-extra'
import { EOL as endOfLine } from 'node:os'
import { pathFromWorkspaceRoot, capitalizeKebabCase, prettierFormat, logGreen } from '../../shared/utils'
import { getAllModules2 } from './build-ui-openinula'
import handlebarsRender from './handlebars.render'
const version = (({ key }) => {
const packageJSON = fs.readJSONSync(pathFromWorkspaceRoot('packages/openinula/package.json'))
const packageJsonOption = packageJSON[key] || packageJSON
return packageJsonOption
})({ key: 'version' })
const outputDir = 'packages/openinula'
const fileNames = {
all: 'index.ts',
pc: 'pc.ts',
mobile: 'mobile.ts',
'mobile-first': 'mobile-first.ts'
}
function getMainTemplate() {
return `{{{include}}}
import { $prefix } from '@opentiny/openinula-common'
const components = [{{{components}}}]
export const version = '${version}'
export {
{{{components}}}
}
export default {
{{{components}}}
} as any
`
}
function getComponents(mode) {
const modules = getAllModules2()
const components = modules
.filter((item) => item.type === 'component')
.filter((item) => mode === 'all' || !item.mode || item.mode.includes(mode))
return components
}
function createEntry(mode) {
const OUTPUT_PATH = pathFromWorkspaceRoot(outputDir, fileNames[mode])
const MAIN_TEMPLATE = getMainTemplate({ mode })
const includeTemplate: string[] = []
const componentsTemplate: string[] = []
const components = getComponents(mode)
const PKG_PATH = pathFromWorkspaceRoot(outputDir, 'package.json')
const PKGContent = fs.readJSONSync(PKG_PATH)
const PKGDeps = {
'@opentiny/openinula-common': 'workspace:~'
}
components.forEach((item) => {
const component = capitalizeKebabCase(item.name)
PKGDeps[item.importName] = 'workspace:~'
componentsTemplate.push(` ${component}`)
const importName = mode === 'all' ? item.importName : `${item.importName}/src/${mode}`
includeTemplate.push(`import ${item.name} from '${importName}'`)
})
if (mode === 'all') {
PKGContent.dependencies = PKGDeps
fs.writeFileSync(PKG_PATH, JSON.stringify(PKGContent, null, 2))
}
const template = handlebarsRender({
template: MAIN_TEMPLATE,
data: {
include: includeTemplate.join(endOfLine),
components: componentsTemplate.join(',' + endOfLine)
}
})
const output = prettierFormat({ str: template })
fs.writeFileSync(OUTPUT_PATH, output)
}
export function buildEntryOpenInula() {
;['all', 'pc', 'mobile', 'mobile-first'].forEach(createEntry)
logGreen(
`npm run build:entry done. [${outputDir}/index.ts,${outputDir}/pc.ts,${outputDir}/mobile.ts,${outputDir}/mobile-first.ts]`
)
}

View File

@ -0,0 +1,479 @@
import { logGreen, kebabCase, capitalizeKebabCase } from '../../shared/utils'
import { pathFromWorkspaceRoot } from '../../shared/utils'
import fg from 'fast-glob'
import path from 'node:path'
import { build, defineConfig } from 'vite'
import { getBabelOutputPlugin } from '@rollup/plugin-babel'
import { external } from '../../shared/config'
import type { Plugin, NormalizedOutputOptions, OutputBundle } from 'rollup'
import fs from 'fs-extra'
import { sync as findUpSync } from 'find-up'
import replace from 'rollup-plugin-replace'
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const moduleMap = require(pathFromWorkspaceRoot('packages/openinula/modules.json'))
type mode = 'pc' | 'mobile' | 'mobile-first'
const pathFromPackages = (...args) => pathFromWorkspaceRoot('packages', ...args)
let scopeName = '@opentiny'
let buildVersion = '1.0.0'
export interface Module2 {
/** 源码路径,如 vue/src/button/index.ts */
path: string
/** 模块类型,可选 component, template, module */
type: 'component' | 'template' | 'module'
/** 是否排除构建,例如组件尚未开发完,设置 true */
exclude?: boolean
/** 组件类型支持的模式 */
mode?: mode[]
/** 模块名称,如 Button */
name: string
/** 模块构建物路径,如 vue/button/lib/index */
libPath: string
/** 模块名,如 @opentiny/vue/vue/lib/button/src@deprecated */
libName?: string
/** 模块的npm包名如 @opentiny/vue-button */
importName: string
/** 构建物文件名,@deprecated */
tmpName?: string
/** 全局变量名,如 TinyButton */
global?: string
/** 组件名的大写形态,如 Button */
UpperName?: string
/** 组件名的小写形态,如 button */
LowerName?: string
/** 模块的路径 */
parentDir?: string[]
}
function getEntryTasks(): Module2[] {
// 读取TinyVue组件库入口文件
return ['index', 'pc', 'mobile'].map((mode) => ({
path: `openinula/${mode}.ts`,
dtsRoot: true,
libPath: `openinula/${mode}`,
type: 'module',
name: kebabCase({ str: `${scopeName}/openinula` }),
global: capitalizeKebabCase('opentinyOpenInula'),
importName: `${scopeName}/openinula`
}))
}
export function getAllModules2(): Module2[] {
return getSortModules({ filterIntercept: () => true })
}
const getSortModules = ({ filterIntercept }: { filterIntercept: Function }) => {
let modules: Module2[] = []
let componentCount = 0
const importName = `${scopeName}/openinula`
Object.entries(moduleMap).forEach(([key, module]) => {
let component = module as Module2
component.name = key
// filterIntercept过滤筛选命令行传过来的组件名称只输出命令行传递过来的组件
if (filterIntercept(component) === true && component.exclude !== true) {
const dirs = component.path.split('/')
// 这段逻辑暂时没有用到
const componentName = dirs.slice(1, dirs.indexOf('src'))
// UpperName: Todo
component.UpperName = capitalizeKebabCase(componentName.pop() ?? '')
// LowerName: todo
component.LowerName = kebabCase({ str: component.UpperName })
// 工程的父文件夹
component.parentDir = componentName
// libPath: 'packages/todo/dist/pc.ts' 组件输出路径
component.libPath = component.path
.replace('openinula/src/', 'packages/')
.replace('openinula-common/src/', 'packages/common/')
.replace('openinula-locale/src/', 'packages/locale/')
.replace('openinula-icon/src/', 'packages/icon/')
.replace('/index.ts', '/src/index.js')
.replace('/src/', '/dist/lib/')
.replace('.jsx', '.js')
.replace('.tsx', '.js')
// libName: '@opentiny/vue/todo/pc'
component.libName = component.libPath
.replace('packages/', '')
.replace('/index', '')
.replace('.js', '')
.replace('/dist/', '/')
.replace(/\/lib$/, '')
// 处理子目录
if (componentName.length) {
component.libName = component.libName.replace(componentName.join('/'), '').replace(/^\//, '')
}
// importName: '@opentiny/vue-tag/pc'
component.importName = `${scopeName}/openinula` + '-' + component.libName
// libName: '@opentiny/vue/todo/pc'
component.libName = importName + '/' + component.libName
// tmpName: 'pc'
component.tmpName = component.libPath.replace('.ts', '').split('/').slice(-1)[0]
// global: 'TinyTodoPc'
component.global = 'Tiny' + key
component.importName = `${scopeName}/openinula-${kebabCase({ str: key })}`
// "vue-common/src/index.ts" ==> "vue-common/lib/index"
if (component.type === 'module') {
component.libPath = component.path.replace('/src/', '/lib/').replace('index.ts', 'index')
}
// "vue/src/button/index.ts" ==> "button/lib/index"
if (component.type === 'component') {
component.libPath = component.path.replace('openinula/src/', '').replace('/index.ts', '/lib/index')
}
// "vue/src/button/src/mobile-first.vue" ==> "button/lib/mobile-first"
if (component.type === 'template') {
component.libPath = component.path.replace('openinula/src/', '').replace('/src/', '/lib/').replace(/\..+$/, '')
}
modules.push(component)
}
component.type === 'component' && componentCount++
})
return modules
}
/**
*
* @private
* @param {Function} filterIntercept
*/
const getModules = (filterIntercept: Function) => {
let modules = {}
if (typeof filterIntercept === 'function') {
for (const key in moduleMap) {
const component = moduleMap[key]
if (filterIntercept(component) === true && component.exclude !== true) {
modules[key] = component
}
}
} else {
modules = moduleMap
}
return modules
}
const getByName = ({
name,
inversion = false,
isOriginal = false
}: {
name: string
inversion?: boolean
isOriginal?: boolean
}) => {
const callback = (item) => {
const result = new RegExp(`/${name}/|^openinula-${name}/`).test(item.path)
return inversion ? !result : result
}
return isOriginal ? getModules(callback) : getSortModules({ filterIntercept: callback })
}
function getTasks(names: string[]) {
// 没有指定组件,则全量构建
if (names.length === 0) {
return [...getAllModules2(), ...getEntryTasks()]
}
return names
.map((name) =>
getByName({
name: kebabCase({ str: name.replace('@opentiny/openinula-', '') })
// isSort: false
})
)
.flat()
}
const getAllIcons = () => {
const entries = fg.sync('openinula-icon*/src/*', { cwd: pathFromWorkspaceRoot('packages'), onlyDirectories: true })
return entries.map((item) => {
const name = path.basename(item)
return {
path: item + '/index.ts',
libPath: item.replace('/src/', '/lib/'),
type: 'component',
componentType: 'icon',
name: kebabCase({ str: name }),
global: capitalizeKebabCase(name),
importName: '@opentiny/openinula-' + item
} as Module2
})
}
function toEntry(libs) {
return libs.reduce((result, { libPath, path: file }) => {
const tLibPath = libPath.replace('-lib/', '/lib/')
result[tLibPath] = pathFromPackages(file)
return result
}, {})
}
function toTsInclued(libs) {
return new Set(
libs
.filter((item) => ['module', 'component'].includes(item.type))
.map((lib) => `packages/${lib.dtsRoot ? lib.path : path.dirname(lib.path)}`)
)
}
function generatePackageJson({ beforeWriteFile }): Plugin {
return {
name: 'opentiny-vue:generate-package-json',
generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
const cache = {}
Object.entries(bundle).forEach(([, item]) => {
// 主入口文件, button/index.ts, common/src/index.ts
if (item.type === 'chunk' && /\/index\.(js|ts)/.test(item.facadeModuleId!)) {
// 从源文件中往上查找最近的 package.json 文件
const packageJsonFile = findUpSync('package.json', { cwd: item.facadeModuleId! })
if (!packageJsonFile) return
if (cache[packageJsonFile]) return
let packageJson
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonFile, { encoding: 'utf-8' }))
} catch {}
const { filePath, content } = beforeWriteFile(path.dirname(item.fileName), packageJson)
if (content) {
this.emitFile({
type: 'asset',
fileName: `${filePath}/package.json`,
source: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
})
}
const changelogFile = path.join(path.dirname(packageJsonFile), 'CHANGELOG.md')
if (fs.existsSync(changelogFile)) {
this.emitFile({
type: 'asset',
fileName: `${filePath}/CHANGELOG.md`,
source: fs.readFileSync(changelogFile, { encoding: 'utf-8' })
})
}
cache[packageJsonFile] = true
}
})
}
}
}
// const getOpenInulaPlugins = (openinulaVersion: string) => {
// const pluginMap = {
// '18': () => {
// // const openinula18Plugin = requireModules('examples/openinula-docs/node_modules/@vitejs/plugin-openinula')
// const openinula18Plugin = requireModules('examples/openinula-docs/node_modules/@vitejs/plugin-react')
// const openinula18SvgPlugin = svgr
// return [openinula18Plugin(), openinula18SvgPlugin()]
// }
// }
// return pluginMap[openinulaVersion]()
// }
function getBaseConfig() {
return defineConfig({
publicDir: false,
resolve: {
extensions: ['.js', '.ts', '.tsx', '.jsx']
},
define: {
'process.env.BUILD_TARGET': JSON.stringify('component')
},
plugins: [
// ...getOpenInulaPlugins('18'),
generatePackageJson({
beforeWriteFile: (filePath, content) => {
const dependencies: any = {}
Object.entries(content.dependencies).forEach(([key, value]) => {
// 只替换 openinula 系的依赖,方便调试
// 其他公用的依赖vue 之前可能发过包
const newKey = key.replace('@opentiny/openinula', `${scopeName}/openinula`)
if ((value as string).includes('workspace:~')) {
dependencies[newKey] = '*'
} else {
dependencies[newKey] = value
}
})
if (filePath.includes('openinula-common')) {
dependencies.openinula = '18.2.0'
}
// 如果是主入口或者svg图标则直接指向相同路径
if (filePath === 'openinula' || filePath === 'openinula-icon') {
content.main = './index.js'
content.module = './index.js'
} else {
content.main = './lib/index.js'
content.module = './lib/index.js'
}
content.version = buildVersion
content.dependencies = dependencies
content.name = content.name.replace('@opentiny/openinula', `${scopeName}/openinula`)
delete content.devDependencies
delete content.private
delete content.exports
return {
filePath: filePath.replace(/[\\/]lib$/, ''),
content
}
}
})
]
})
}
async function batchBuild({ tasks, formats, message, emptyOutDir, dts, outDir }) {
if (tasks.length === 0) return
logGreen(`====== 开始构建 ${message} ======`)
const entry = toEntry(tasks)
const dtsInclude = toTsInclued(tasks)
await build({
...getBaseConfig({ dts, dtsInclude }),
configFile: false,
build: {
outDir,
emptyOutDir,
minify: false,
rollupOptions: {
plugins: [
getBabelOutputPlugin({
presets: [['@babel/preset-env', { loose: true, modules: false }]]
}) as any,
replace({
'.less': '.css'
}),
{
name: 'replace-scope',
transform(code) {
if (scopeName === '@opentiny') return code
let modifiedCode = code
while (modifiedCode.match(/@opentiny\/openinula/g)) {
modifiedCode = modifiedCode.replace('@opentiny/openinula', `${scopeName}/openinula`)
}
return {
code: modifiedCode
}
}
}
],
output: {
strict: false,
manualChunks: {}
},
external: (source, importer, isResolved) => {
// vite打包入口文件或者没有解析过得包不能排除依赖
if (isResolved || !importer) {
return false
}
// 子图标排除周边引用, 这里注意不要排除svg图标
if (/openinula-icon\/.+\/index/.test(importer)) {
return !/\.svg/.test(source)
}
// @opentiny/vue 总入口,需要排除所有依赖
if (/openinula\/(index|pc|mobile|mobile-first)\.ts$/.test(importer)) {
return true
}
if (['openinula', 'openinula/jsx-runtime'].includes(source)) {
return true
}
if (source.indexOf(scopeName) === 0) {
return true
}
return external(source)
}
},
lib: {
entry,
formats,
fileName: (format, entryName) => `${entryName}.js`
}
}
})
}
async function batchBuildAll({ tasks, formats, message, emptyOutDir, dts, npmScope }) {
const rootDir = pathFromPackages('')
const outDir = path.resolve(rootDir, `dist-openinula/${npmScope}`)
await batchBuild({
tasks,
formats,
message,
emptyOutDir,
dts,
outDir
})
}
export async function buildOpenInula(
names: string[] = [],
{ buildTarget = '1.0.0', formats = ['es'], clean = false, dts = true, scope = '@opentiny' }
) {
scopeName = scope
buildVersion = buildTarget
// 要构建的模块
let tasks = getTasks(names)
// 如果指定了打包icon或者没有传入任何组件
if (names.some((name) => name.includes('icon')) || !names.length) {
tasks.push(...getAllIcons())
}
// 构建 @opentiny/openinula
if (names.some((name) => [`${scopeName}/openinula`, 'openinula'].includes(name))) {
tasks.push(...getEntryTasks())
}
const message = `TINY for openinula: ${JSON.stringify(names.length ? names : '全量')}`
await batchBuildAll({
tasks,
formats,
message,
emptyOutDir: clean,
dts,
npmScope: scope
})
}

View File

@ -3,4 +3,5 @@ export * from './build-entry'
export * from './build-runtime'
export * from './build-ui-react'
export * from './build-entry-react'
export * from './build-chart-theme'
export * from './build-ui-openinula'
export * from './build-entry-openinula'

View File

@ -0,0 +1,12 @@
{
"Icon": {
"path": "openinula/icon/index.ts",
"type": "module",
"exclude": true
},
"Common": {
"path": "openinula/common/src/index.ts",
"type": "module",
"exclude": false
}
}

View File

@ -0,0 +1,111 @@
import { capitalize, walkFileTree, pathFromWorkspaceRoot, logGreen, prettierFormat } from '../../shared/utils'
import { quickSort } from '../../shared/module-utils'
import path from 'node:path'
import fs from 'fs-extra'
import commonMappingOpenInula from './common-mapping-openinula.json'
const getBuildEntryFile = (file, dirs, subPath) => {
// 模板文件pc|mobile|mobile-first需要同级目录有index.ts文件才能成为打包入口
const isTemplatePath = dirs.includes('index.ts')
const isMainEntry = file.includes('index') && dirs.includes('package.json')
const isPcEntry = file.includes('pc.') && subPath.includes(`src${path.sep}pc.`) && isTemplatePath
const isMobileEntry = file.includes('mobile.') && subPath.includes(`src${path.sep}mobile.`) && isTemplatePath
const isMobileFirstEntry =
file.includes('mobile-first.') && subPath.includes(`src${path.sep}mobile-first.`) && isTemplatePath
return {
isBuildEntryFile: isMainEntry || isPcEntry || isMobileEntry || isMobileFirstEntry,
isMainEntry,
isPcEntry,
isMobileEntry,
isMobileFirstEntry
}
}
const tempMap = {
'pc.jsx': 'pc',
'mobile.jsx': 'mobile',
'mobile-first.jsx': 'mobile-first',
'pc.tsx': 'pc',
'mobile.tsx': 'mobile',
'mobile-first.tsx': 'mobile-first'
}
const getTemplateName = (currentPaths, entryObj) => {
const entryMaps = {
isPcEntry: 'Pc',
isMobileEntry: 'Mobile',
isMobileFirstEntry: 'MobileFirst',
isMainEntry: ''
}
const mapKey = Object.keys(entryObj).filter((item) => entryObj[item] && item !== 'isBuildEntryFile')[0]
const subFix = entryMaps[mapKey]
return `${currentPaths.split('-').map(capitalize).join('')}${subFix}`
}
export const writeModuleMap = (moduleMap) => {
fs.writeFileSync(
pathFromWorkspaceRoot('packages/openinula/modules.json'),
prettierFormat({
str: typeof moduleMap === 'string' ? moduleMap : JSON.stringify(moduleMap),
options: {
parser: 'json',
printWidth: 10
}
})
)
}
function makeOpenInulaModules() {
const templates = { ...commonMappingOpenInula }
walkFileTree({
isDeep: true,
dirPath: pathFromWorkspaceRoot('packages/openinula/src'),
fileFilter({ file }) {
return !/node_modules/.test(file)
},
callback({ file, subPath, dirs }) {
const entryObj = getBuildEntryFile(file, dirs, subPath)
const mode: string[] = []
if (entryObj.isMainEntry && dirs.includes('src')) {
const srcPath = subPath.replace(file, 'src')
const srcFiles = fs.readdirSync(srcPath) || []
srcFiles.forEach((item) => {
if (tempMap[item]) {
mode.push(tempMap[item])
}
})
}
if (entryObj.isBuildEntryFile) {
const modulePath = subPath.slice(subPath.lastIndexOf(`openinula${path.sep}src`)).replaceAll(path.sep, '/')
const matchArr = modulePath.match(/.+\/(.+?)\/(index\.ts|src\/pc\.|src\/mobile\.|src\/mobile-first\.)/)
if (matchArr?.[1]) {
const compName = getTemplateName(matchArr[1], entryObj)
templates[compName] = {
path: modulePath,
type: entryObj.isMainEntry ? 'component' : 'template',
exclude: false
}
if (mode.length > 0) {
templates[compName].mode = mode
}
}
}
}
})
const modulesJson = quickSort({ sortData: templates, returnType: 'object' })
writeModuleMap(modulesJson)
}
try {
makeOpenInulaModules()
logGreen('npm run create:mapping-openinula done.')
} catch (e) {
// eslint-disable-next-line no-console
console.log(e)
}

View File

@ -0,0 +1,236 @@
/*
* pnpm create:ui-openinula
*
* pnpm create:ui-openinula alert
*/
import path from 'node:path'
import fs from 'fs-extra'
import semver from 'semver'
import {
walkFileTree,
capitalizeKebabCase,
logGreen,
logYellow,
templatePath,
pathJoinFromCLI
} from '../../shared/utils'
import handlebarsRender from '../build/handlebars.render'
import { parse } from '@vue/compiler-sfc'
import { requireModules } from '../build/build-ui'
// 从renderless中获取组件API
function getApi(name) {
const { api } = requireModules(`packages/renderless/src/${name}/vue`)
let apiStr = JSON.stringify(api)
apiStr = apiStr.replaceAll('"', '').replace('[', '').replace(']', '')
return apiStr
}
// vue index中获取组件Props,该部分待完成
// function getProps(name) {
// let path = pathJoinFromCLI('../../packages/vue/src/alert/src/index.ts')
// let fileContent = fs.readFileSync(path, { encoding: 'utf8' })
// // return props
// }
// 解析vue template部分
function vueToJSX(templateSource) {
const { descriptor } = parse(templateSource)
if (descriptor.template) {
const ast = descriptor.template.ast
const jsx = JSXFromAST(ast)
return jsx
} else {
throw new Error('No template found in the source.')
}
}
// 遍历Vue AST解析成符合openInula规范的组件模板 E:\tinyvue\tsconfig.vue3.json
function JSXFromAST(node) {
let jsx = ''
// If和For标记
let tagName = ''
let tagAttr = ''
switch (node.type) {
case 1: // Element
if (node.props) {
node.props.forEach((attr) => {
if (attr.name === 'if') {
tagName = 'If'
tagAttr = ' v-if ={' + attr.exp.content + '}'
}
if (attr.name === 'for') {
tagName = 'For'
let list = attr.exp.content
list = list.slice(list.indexOf('in ') + 3)
tagAttr = ' list ={' + list + '}'
}
// class转换成className
if (attr.name === 'class') {
jsx += ' className="' + attr.value.content + '"'
}
// 含有:的动态属性添加大括号{},其中:class需要转换成className
if (attr.name === 'bind') {
if (attr.arg) {
if (attr.arg.content === 'class') {
jsx += ' className={vc(' + attr.exp.content + ')}'
} else {
jsx += ' ' + attr.arg.content + '={' + attr.exp.content + '}'
}
}
}
// 含有@的动态方法转添加on前缀
if (attr.name === 'on') {
// 有修饰符的exp可能为undefiend修饰符待处理
if (attr.exp) {
jsx += ' on' + capitalizeKebabCase(attr.arg.content) + '={' + attr.exp.content + '}'
}
}
// 含有model的处理
// if (attr.name === 'model') {
// }
})
}
jsx = '<' + node.tag + jsx + '>'
// 添加If和For外层包裹,前后添加换行符更好看
if (tagName) {
jsx = '\n<' + tagName + tagAttr + '>\n' + jsx
}
if (node.children) {
node.children.forEach((child) => {
jsx += JSXFromAST(child)
})
}
jsx += '</' + node.tag + '>'
// 添加If和For外层包裹
if (tagName) {
jsx += '\n</' + tagName + '>'
}
break
case 2: // Text此处多数是换行符
jsx += node.content
break
case 3: // 注释
jsx += node.loc.source
break
case 5: // 插值或组件内变量
jsx += `{${node.content.content}}`
break
// 其他节点类型...
default:
// 处理其他节点类型或抛出错误
logYellow(`======================node.type=${node.type}====================`)
logYellow(node)
// throw new Error('Unsupported node type: ' + node.type);
}
// 单独处理slot
jsx = jsx.replace('<slot', '<Slot slots={props.slots}').replace('</slot>', '</Slot>')
// 单独处理transition
jsx = jsx.replace('transition', 'Transition')
// 单独处理component组件
jsx = jsx.replace('component', 'Component')
// 最外层template标签替换成div并添加ref
jsx = jsx.replace('<template>', '<div ref={ref}>').replace('</template>', '</div>')
// windows下调整换行符
jsx = jsx.replace('\n', '\r\n')
return jsx
}
export async function createUi(names = []) {
const templateDir = path.join(templatePath, './openinula')
const componetDir = pathJoinFromCLI('../../packages/openinula/src')
const vueComponetDir = pathJoinFromCLI('../../packages/vue/src')
const { version } = fs.readJSONSync(pathJoinFromCLI('../../packages/openinula/package.json'))
names.forEach((componentName) => {
let componentPath = path.join(componetDir, componentName)
let vueComponentPath = path.join(vueComponetDir, componentName, 'src')
if (fs.existsSync(componentPath)) {
logYellow(`The component name : ${componentName} is exist , please enter other name.`)
return
}
// 获取API
let api = getApi(componentName)
// 定义不同模板内容
let pcTemplate = ``
let mobileTemplate = ``
let mobileFirstTemplate = ``
walkFileTree({
isDeep: true,
dirPath: vueComponentPath,
callback({ file, subPath }) {
// 判断是否是.vue文件否则会出错
if (file.split('.')[1] === 'vue') {
let fileContent = fs.readFileSync(subPath, { encoding: 'utf8' })
let content = vueToJSX(fileContent)
if (file === 'pc.vue') {
pcTemplate = content
}
if (file === 'mobile.vue') {
mobileTemplate = content
}
if (file === 'mobile-first.vue') {
mobileFirstTemplate = content
}
}
}
})
walkFileTree({
isDeep: true,
dirPath: templateDir,
callback({ file, subPath }) {
let fileName = file
// 每次遍历需要重置组件目录,否则会报错
componentPath = path.join(componetDir, componentName)
const isSrcDir = path.basename(path.dirname(subPath)) === 'src'
if (isSrcDir) {
componentPath = path.join(componentPath, 'src')
}
if (!fs.existsSync(componentPath)) {
fs.mkdirSync(componentPath)
}
let template = ''
if (file === 'pc.jsx') {
template = pcTemplate
}
if (file === 'mobile.jsx') {
template = mobileTemplate
}
if (file === 'mobile-first.jsx') {
template = mobileFirstTemplate
}
let fileContent = fs.readFileSync(subPath, { encoding: 'utf8' })
const upperComponentName = capitalizeKebabCase(componentName)
// 编译模板
fileContent = handlebarsRender({
template: fileContent,
data: {
NAME: componentName,
UNAME: upperComponentName,
MINOR: semver.minor(version),
SUFFIX: '',
THEME: 'theme',
TEMPLATE: template,
API: api
},
delimiter: ['\\[\\[', '\\]\\]'],
options: { noEscape: true }
})
componentPath = path.join(componentPath, fileName)
fs.writeFileSync(componentPath, fileContent)
}
})
logGreen('Create ui openinula done for ' + componentName)
})
}

View File

@ -1 +1,2 @@
export * from './create-icon-saas'
export * from './create-ui-openinula'

View File

@ -1,9 +1,19 @@
#!/usr/bin/env node
import { Command, Option } from 'commander'
import { createIconSaas } from './commands/create/index.js'
import { buildUi, buildEntry, buildRuntime, buildReact, buildEntryReact, chartTheme } from './commands/build'
import {
buildUi,
buildEntry,
buildRuntime,
buildReact,
buildEntryReact,
buildOpenInula,
buildEntryOpenInula
} from './commands/build'
import { releaseAurora } from './commands/release/releaseAurora'
import { createUi } from './commands/create'
const program = new Command()
program.command('release:aurora').description('转换为aurora的包').action(releaseAurora)
@ -12,9 +22,9 @@ program.command('create:icon-saas').description('同步生成 icon-saas').action
program.command('build:entry-react').description('生成 react 组件库入口').action(buildEntryReact)
program.command('build:entry').description('生成组件库入口').action(buildEntry)
program.command('build:entry-openinula').description('生成 openinula 组件库入口').action(buildEntryOpenInula)
program.command('build:chartTheme').description('切换chart主题').action(chartTheme)
program.command('build:entry').description('生成组件库入口').action(buildEntry)
program
.command('build:ui')
@ -23,7 +33,6 @@ program
.addOption(new Option('-v --vue-versions <vueVersions...>', '目标框架,默认所有').choices(['2', '2.7', '3']))
.addOption(new Option('-f --formats <formats...>', '目标格式,默认 ["es"]').choices(['es', 'cjs']))
.addOption(new Option('-t --build-target <buildTarget>', '组件的目标版本'))
.addOption(new Option('-d --design <design>', '构建的目标设计规范'))
.option('-s, --scope <scope>', 'npm scope默认是 opentiny会以 @opentiny 发布到 npm')
.option('-c, --clean', '清空构建目录')
.option('--no-dts', '不生成 dts')
@ -48,4 +57,21 @@ program
.option('--no-dts', '不生成 dts')
.action(buildReact)
program
.command('build:openinula')
.description('打包 openinula 组件库')
.argument('[names...]', '构建指定组件,如 button alert不指定则构建全量组件')
.addOption(new Option('-f --formats <formats...>', '目标格式,默认 ["es"]').choices(['es', 'cjs']))
.addOption(new Option('-t --build-target <buildTarget>', '组件的目标版本'))
.option('-s, --scope <scope>', 'npm scope默认是 opentiny会以 @opentiny 发布到 npm')
.option('-c, --clean', '清空构建目录')
.option('--no-dts', '不生成 dts')
.action(buildOpenInula)
program
.command('create:ui-openinula')
.description(' openinula 组件库代码生成')
.argument('[names...]', '构建指定组件,如 button alert不指定则构建全量组件')
.action(createUi)
program.parse()

View File

@ -1,6 +1,6 @@
{
"name": "opentiny-vue",
"version": "3.14.0",
"version": "3.7.12",
"private": true,
"packageManager": "pnpm@8.3.1",
"description": "An enterprise-class UI component library, support both Vue.js 2 and Vue.js 3, as well as PC and mobile.",
@ -28,7 +28,7 @@
"main": "packages/index.js",
"engines": {
"node": ">=16",
"pnpm": ">=6.35"
"pnpm": ">=7"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
@ -59,28 +59,19 @@
"build:runtime": "pnpm -C internals/cli build:runtime",
"// ---------- 构建相关脚本 ----------": "",
"build:ui": "pnpm create:icon-saas && pnpm create:mapping && pnpm build:entry && gulp themeConcat && pnpm -C internals/cli build:ui",
"build:chartTheme": "pnpm -C internals/cli build:chartTheme",
"build:renderless": "pnpm -C packages/renderless build:fast",
"build:theme": "gulp themeConcat && pnpm -C packages/theme build:fast",
"build:themeSaas": "pnpm -C packages/theme-saas build:fast",
"build:themeMobile": "pnpm -C packages/theme-mobile build:fast",
"build:themejson": "gulp themeJson",
"build:internals": "pnpm \"--filter=./internals/*\" build",
"build:vite-import": "pnpm --filter @opentiny/vue-vite-import build",
"build:virtual-template": "pnpm --filter @opentiny-internal/unplugin-virtual-template build",
"build:site": "gulp themeConcat && pnpm i -g pnpm && pnpm build:vite-import && pnpm build:virtual-template && pnpm -C examples/sites build",
"build:site": "gulp themeConcat && pnpm -C examples/sites build",
"release:aurora": "pnpm -C internals/cli release:aurora",
"// ---------- 使用pnpm批量发布npm包 ----------": "",
"pub2": "pnpm --filter=\"./packages/dist2/**\" publish --tag v2-latest --no-git-checks --access=public",
"pub3": "pnpm --filter=\"./packages/dist3/**\" publish --no-git-checks --access=public",
"pub2.7": "pnpm --filter=\"./packages/dist2.7/**\" publish --tag v2.7-latest --no-git-checks --access=public",
"pub:aurora": "pnpm --filter=\"./packages/dist2/@aurora/**\" publish --no-git-checks --access=public",
"pub:theme": "pnpm --filter=\"./packages/theme/dist\" publish --no-git-checks --access=public",
"pub:themeMobile": "pnpm --filter=\"./packages/theme-mobile/dist/\" publish --no-git-checks --access=public",
"pub:themeSaas": "pnpm --filter=\"./packages/theme-saas/dist\" publish --no-git-checks --access=public",
"pub:renderless": "pnpm --filter=\"./packages/renderless/dist\" publish --no-git-checks --access=public",
"pub:all": "pnpm pub2 && pnpm pub3 && pnpm pub:theme && pnpm pub:themeMobile && pnpm pub:themeSaas && pnpm pub:renderless",
"pub:site": "pnpm -C examples/sites pub",
"// ---------- unit单元测试 ----------": "",
"test:unit2": "pnpm -C examples/vue2 test:unit",
"test:unit2.7": "pnpm -C examples/vue2.7 test:unit",
@ -89,18 +80,14 @@
"test:e2e2": "pnpm -C examples/vue2 test:e2e --project=chromium",
"test:e2e2.7": "pnpm -C examples/vue2.7 test:e2e --project=chromium",
"test:e2e3": "pnpm -C examples/vue3 test:e2e --project=chromium",
"test:e2e2:mobile": "pnpm -C examples/vue2 test:e2e --project=android",
"test:e2e2.7:mobile": "pnpm -C examples/vue2.7 test:e2e --project=android",
"test:e2e3:mobile": "pnpm -C examples/vue3 test:e2e --project=android",
"// ---------- playwright下载chromium、firefox等浏览器内核 ----------": "",
"install:browser": "pnpm -C examples/vue3 install:browser",
"// ---------- e2e测试代码生成器 ----------": "",
"codegen": "pnpm -C examples/vue3 codegen",
"codegen:mobile": "pnpm -C examples/vue3 codegen --device=\"Pixel 5\" ",
"format": "prettier --write --cache packages/**/{*.vue,*.js,*.ts,*.jsx,*.tsx,*.less} examples/**/{*.vue,*.js,*.ts,*.jsx,*.tsx} internals/**/{*.js,*.ts}",
"lint": "eslint \"packages/**/{*.vue,*.js,*.ts}\" --quiet --fix",
"lint:doc": "eslint \"examples/**/{*.vue,*.js,*.ts}\" --quiet --fix",
"clean:build": "rimraf packages/dist2 packages/dist3 packages/dist2.7 packages/renderless/dist packages/theme/dist packages/theme-saas/dist packages/theme-mobile/dist",
"clean:build": "rimraf packages/dist2 packages/dist3 packages/renderless/dist packages/dist2.7",
"clean:dependencies": "rm -rf node_modules /**/node_modules",
"// ---------- 构建【mf】版本 ----------": "",
"preci:deployMfPatch": "pnpm clean:build && lerna version prepatch --conventional-prerelease --include-merged-tags --preid mf --no-push --yes",
@ -138,17 +125,22 @@
"build:react-site": "pnpm --filter @opentiny/react-site build",
"prettier": "prettier --config .prettierrc --write .",
"// ---------- openinula 相关脚本命令 ----------": "",
"dev:openinula": "pnpm -C examples/openinula-docs run dev",
"dev:openinula": "pnpm create:mapping-openinula && pnpm build:entry-openinula && pnpm -C examples/openinula-docs run dev",
"build:entry-openinula": "pnpm -C internals/cli build:entry-openinula",
"create:mapping-openinula": "pnpm -C internals/cli create:mapping-openinula",
"build:openinula": "pnpm -C internals/cli build:openinula",
"build:ui-openinula": "pnpm create:mapping-openinula && pnpm build:entry-openinula && pnpm build:openinula",
"create:ui-openinula": "pnpm -C internals/cli create:ui-openinula",
"// ---------- solid 相关脚本命令 ----------": "",
"dev:solid": "pnpm -C examples/solid-docs run dev"
},
"dependencies": {
"@vue/composition-api": "1.7.2",
"@vue/composition-api": "1.2.2",
"color": "^4.2.3",
"cropperjs": "1.5.12",
"crypto-js": "4.2.0",
"echarts": "5.4.1",
"echarts-liquidfill": "3.1.0",
"echarts-liquidfill": "3.0.0",
"echarts-wordcloud": "2.0.0",
"fastdom": "1.0.11",
"shepherd.js": "11.0.1",

View File

@ -0,0 +1,239 @@
# 开发文档
## 更新日志
#### 2024-04-07
1. 修复pnpm build:openinula构建命令
2. 增加pnpm create:ui-openinula创建openInula组件命令
该命令主要使用了/internals/cli/src/create/create-ui-openinula的转换脚本通过解析vue模板遍历ast,生成inula组件。目前该脚本已经实现了api和template的转换能减少95%的人工转换工作量。
3. 修复事件点击报错。该错误来自适配层dom和vnode(对应react的fiber)的关联。
## 如何将已经开发好的Vue组件快速转换为openInula组件
执行 `pnpm create:ui-openinula +组件名称` 可在`/packages/openinula/src/`目录下生成对应组件,然后进行测试,根据需要进行少量修改即可。
## 开发成果
1. 基本工具的开发
- create-ui-openinula(已完成通过该转换工具可以将Vue模板转换成openInula组件模板)
- create-mapping-openinula已完成,通过该工具可以生成openinula组件的map文件。新增组件后需要调用
- build-entry-openinula已完成通过该工具可以生成openinula组件的入口文件。新增组件后需要调用
- build-ui-openinula已完成,通过该工具可以打包openInula组件
- common-mapping-openinula.json(map文件的公共部分)
调用顺序为create-ui-openinula(转换组件)=>create-mapping-openinula(创建map)=>build-ui-openinula(创建入口文件)
实际开发过程中通过命令行,首先执行`pnpm create:ui-openinula+组件名称`,然后执行`pnpm dev:openinula`
其中,`pnpm dev:openinula`命令已经包含了map文件生成和入口文件生成
2. openInula-common 适配层的开发已完成全部API的转换
## 下载依赖
```bash
pnpm i
```
## 将Vue组件转换位 openinula 组件
转换后还需检查转换的代码将缺少的props和constants补进代码,然后进行测试
```bash
pnpm create:ui-openinula +组件名称
for example:
pnpm create:ui-openinula button
```
## 生成 openinula 组件入口,每次生成新组件后需更新组件入口
```bash
pnpm build:entry-openinula
```
## 本地启动 openinula 调试项目
```bash
pnpm dev:openinula
```
## 本地启动 openinula 文档项目
```bash
pnpm dev:openinula-site
```
## 打包 openinula 组件
```bash
pnpm build:ui-openinula
```
运行此命令后,会在 pacakges-openinula 产生打包产物
一般是
packages/dist-openinula/@opention/button
... 单个组件产物
packages/dist-openinula/@opention/openinula-common
packages/dist-openinula/@opention/openinula
packages/dist-openinula/@opention/openinula-icon
命令参数:传入字符串参数列表可以指定只打包单个组件或多个特定组件,比如
```bash
pnpm build:ui-openinula button
```
默认不传的话,会打包所有组件,以及公共任务,比如 openinula-common、openinula-icon
可以通过 -f 指定目标格式,默认 es可选 es、cjs
可以通过 -t 指定目标版本,默认 18现在 openinula 只支持 18
可以通过 -s 指定发布 npm scope默认是 opentiny
可以通过 -c 指定是否清空构建目录
可以通过 --no-dts 指定不生成类型定义文件
## 发包 openinula 组件
```bash
pnpm pub:openinula
```
# 目录结构
## 打包 openinula 相关
```b
internals/cli
/build
/build-entry-openinula.ts (packages/openinula 目录下生成入口)
/build-ui-openinula.ts (packages/dist-openinula 下生成打包产物)
/create
/create-mapping-openinula.ts (packages 下生成构建任务列表 modules.json)
/common-mapping-openinula.json (定义一些公共的打包任务,如 openinula-common)
/create-ui-openinula.ts (vue到openInula的转换脚本)
internals/cli
/public/template/openinula(转换脚本使用的模板文件)
```
## 开发 openinula 模版文件相关
packages/openinula/src/[compName] 目录
一个组件模版的目录结构如下
```b
alert
/node_modules
/src
/index.ts
/pc.tsx
/mobile.tsx
/mobile-first.ts
/index.ts
/package.json
```
alert/index 是组件入口
pc、mobile、mobile-first 是三套模版
## 开发 openinula-icon 相关
packages/openinula-icon/src/[svgName] 目录
一个 svg 直接用一个 index.ts 创建
packages/openinula-icon/src/add/index.ts
```ts
import { Svg } from '@opentiny/openinula-common'
import { openinulaComponent as AddLoading } from '@opentiny/vue-theme/svgs/add.svg'
export default Svg({ name: 'AddLoading', component: AddLoading })
```
## 开发 openinula-common openinula 适配层相关
openinula-common 的目录如下,主要是适配层的文件
```b
packages/openinula-common
/src
/csscls.ts 操作样式类名的一些方法
/event.ts 模拟 vue 事件系统
/fiber.ts 对 fiber 的一些读取操作
/openinulaive.ts 实现数据响应式
/resolveProps.js 从 openinula 的 props 上解析事件或属性
/svg-render.jsx 渲染 svg 组件的公共函数
/utils.ts 工具函数
/virtual-comp.jsx 虚拟组件,用于实现 vue 的指令系统
/vm.js 用户模拟 vue 的 vm 对象
/vue-hooks.js 用户模拟 vue 的钩子函数
```
# 用户使用文档
## 在项目中使用所有组件
- 1.下载整个组件库
```bash
npm i @pe-3/openinula
```
- 2. 导入组件
```js
import { Button as TinyButton, openinula as Tiny openinula } from '@pe-3/openinula'
```
- 3. 使用组件(查看 api 文档)
```js
function App() {
return (
<div>
<TinyButton type="primary">主要按钮</TinyButton>
<TinyAlert description="提示组件" closeable={false} />
</div>
)
}
```
## 在项目中使用单个组件
- 1. 下载单个组件
```bash
npm i @pe-3/openinula-button
npm i @pe-3/openinula-alert
```
- 2. 导入单个组件
```js
import TintButton from '@pe-3/openinula-button'
import TintAlert from '@pe-3/openinula-alert'
```
- 3. 使用单个组件
```js
function App() {
return (
<div>
<TinyButton type="primary">主要按钮</TinyButton>
<TinyAlert description="提示组件" closeable={false} />
</div>
)
}
```
## 组件 api 文档地址:
https://opentiny.design/
## codesandbox
https://codesandbox.io/s/hungry-bash-tlch6l?file=/src/App.js

View File

@ -1,9 +1,19 @@
import Alert from '@opentiny/openinula-alert'
import Badge from '@opentiny/openinula-badge'
import Button from '@opentiny/openinula-button'
import Icon from '@opentiny/openinula-icon'
import Switch from '@opentiny/openinula-switch'
const components = [Alert, Badge, Button, Icon, Switch]
export const version = '1.0.0'
export { Alert }
export { Alert, Badge, Button, Icon, Switch }
export default {
Alert
Alert,
Badge,
Button,
Icon,
Switch
} as any

View File

@ -0,0 +1,17 @@
import Alert from '@opentiny/openinula-alert/src/mobile-first'
import Button from '@opentiny/openinula-button/src/mobile-first'
import Icon from '@opentiny/openinula-icon/src/mobile-first'
import Switch from '@opentiny/openinula-switch/src/mobile-first'
const components = [Alert, Button, Icon, Switch]
export const version = '1.0.0'
export { Alert, Button, Icon, Switch }
export default {
Alert,
Button,
Icon,
Switch
} as any

View File

@ -0,0 +1,19 @@
import Alert from '@opentiny/openinula-alert/src/mobile'
import Badge from '@opentiny/openinula-badge/src/mobile'
import Button from '@opentiny/openinula-button/src/mobile'
import Icon from '@opentiny/openinula-icon/src/mobile'
import Switch from '@opentiny/openinula-switch/src/mobile'
const components = [Alert, Badge, Button, Icon, Switch]
export const version = '1.0.0'
export { Alert, Badge, Button, Icon, Switch }
export default {
Alert,
Badge,
Button,
Icon,
Switch
} as any

View File

@ -0,0 +1,106 @@
{
"Alert": {
"path": "openinula/src/alert/index.ts",
"type": "component",
"exclude": false,
"mode": [
"mobile-first",
"mobile",
"pc"
]
},
"AlertMobile": {
"path": "openinula/src/alert/src/mobile.jsx",
"type": "template",
"exclude": false
},
"AlertMobileFirst": {
"path": "openinula/src/alert/src/mobile-first.jsx",
"type": "template",
"exclude": false
},
"AlertPc": {
"path": "openinula/src/alert/src/pc.jsx",
"type": "template",
"exclude": false
},
"Badge": {
"path": "openinula/src/badge/index.ts",
"type": "component",
"exclude": false,
"mode": [
"mobile",
"pc"
]
},
"BadgeMobile": {
"path": "openinula/src/badge/src/mobile.jsx",
"type": "template",
"exclude": false
},
"BadgePc": {
"path": "openinula/src/badge/src/pc.jsx",
"type": "template",
"exclude": false
},
"Button": {
"path": "openinula/src/button/index.ts",
"type": "component",
"exclude": false,
"mode": [
"mobile-first",
"mobile",
"pc"
]
},
"ButtonMobile": {
"path": "openinula/src/button/src/mobile.jsx",
"type": "template",
"exclude": false
},
"ButtonMobileFirst": {
"path": "openinula/src/button/src/mobile-first.jsx",
"type": "template",
"exclude": false
},
"ButtonPc": {
"path": "openinula/src/button/src/pc.jsx",
"type": "template",
"exclude": false
},
"Common": {
"path": "openinula/common/src/index.ts",
"type": "module",
"exclude": false
},
"Icon": {
"path": "openinula/src/icon/index.ts",
"type": "component",
"exclude": false
},
"Switch": {
"path": "openinula/src/switch/index.ts",
"type": "component",
"exclude": false,
"mode": [
"mobile-first",
"mobile",
"pc"
]
},
"SwitchMobile": {
"path": "openinula/src/switch/src/mobile.jsx",
"type": "template",
"exclude": false
},
"SwitchMobileFirst": {
"path": "openinula/src/switch/src/mobile-first.jsx",
"type": "template",
"exclude": false
},
"SwitchPc": {
"path": "openinula/src/switch/src/pc.jsx",
"type": "template",
"exclude": false
}
}

View File

@ -11,6 +11,10 @@
"license": "ISC",
"dependencies": {
"@opentiny/openinula-common": "workspace:~",
"@opentiny/openinula-alert": "workspace:~"
"@opentiny/openinula-alert": "workspace:~",
"@opentiny/openinula-badge": "workspace:~",
"@opentiny/openinula-button": "workspace:~",
"@opentiny/openinula-icon": "workspace:~",
"@opentiny/openinula-switch": "workspace:~"
}
}
}

19
packages/openinula/pc.ts Normal file
View File

@ -0,0 +1,19 @@
import Alert from '@opentiny/openinula-alert/src/pc'
import Badge from '@opentiny/openinula-badge/src/pc'
import Button from '@opentiny/openinula-button/src/pc'
import Icon from '@opentiny/openinula-icon/src/pc'
import Switch from '@opentiny/openinula-switch/src/pc'
const components = [Alert, Badge, Button, Icon, Switch]
export const version = '1.0.0'
export { Alert, Badge, Button, Icon, Switch }
export default {
Alert,
Badge,
Button,
Icon,
Switch
} as any

View File

@ -0,0 +1,3 @@
import Badge from './src'
export default Badge

View File

@ -0,0 +1,19 @@
{
"name": "@opentiny/openinula-badge",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/openinula-common": "workspace:~",
"@opentiny/openinula-icon": "workspace:~",
"@opentiny/vue-theme": "workspace:~",
"@opentiny/vue-theme-mobile": "workspace:~"
}
}

View File

@ -0,0 +1,13 @@
import pc from './pc'
import mobile from './mobile'
export default function (props) {
const { tiny_mode = 'pc' } = props
const S = {
pc,
mobile
}[tiny_mode]
return S(props)
}

View File

@ -0,0 +1,70 @@
import { useSetup, useVm, vc, If, Slot } from '@opentiny/openinula-common'
import { api, renderless } from '@opentiny/vue-renderless/badge/vue'
import '@opentiny/vue-theme-mobile/badge/index.less'
export default function (props) {
const {
isDot = false,
isFixed = true,
isMini = false,
max,
value,
modelValue,
href,
target,
hidden = false,
type,
badgeClass,
offset = [0, 0]
} = props
const defaultProps = Object.assign(
{
isDot,
isFixed,
isMini,
hidden,
offset
},
props
)
const { ref, parent, current: vm } = useVm()
const { state } = useSetup({
props: defaultProps,
api,
renderless,
vm,
parent
})
return (
<div ref={ref} className="tiny-mobile-badge">
<Slot parent_children={props.children}></Slot>
<If v-if={!hidden && (value > 0 || isDot)}>
<div
className={vc([
'tiny-mobile-badge__content',
{
'is-dot': isDot,
'is-fixed': isFixed,
'is-mini': isMini
},
value < 10 ? 'is-circle' : '',
type ? 'tiny-mobile-badge--' + type : ''
])}>
<If v-if={!isDot}>
<span>
<Slot name="content" slots={props.slots}>
<a href={state.href} target={target} rel="noopener noreferrer" className="tiny-mobile-badge__link">
{state.content}
</a>
</Slot>
</span>
</If>
</div>
</If>
</div>
)
}

View File

@ -0,0 +1,78 @@
import { useSetup, useVm, vc, If, Slot } from '@opentiny/openinula-common'
import { api, renderless } from '@opentiny/vue-renderless/badge/vue'
import '@opentiny/vue-theme/badge/index.less'
export default function (props) {
const {
isDot = false,
isFixed = true,
isMini = false,
max,
value,
modelValue,
href,
target,
hidden = false,
type,
badgeClass,
offset = [0, 0],
data
} = props
const defaultProps = Object.assign(
{
isDot,
isFixed,
isMini,
hidden,
offset
},
props
)
const { ref, parent, current: vm } = useVm()
const { state } = useSetup({
props: defaultProps,
api,
renderless,
vm,
parent
})
return (
<div ref={ref} className="tiny-badge__wrapper">
<If v-if={data}>
<span>{data}</span>
</If>
<Slot v-if={!data} parent_children={props.children}></Slot>
<If v-if={!hidden && (state.content || state.content === 0 || isDot)}>
<div
className={vc([
'tiny-badge',
isDot ? 'tiny-badge--default' : '',
state.isOverstep ? 'tiny-badge--max' : '',
type ? 'tiny-badge--' + type : '',
badgeClass || ''
])}
style={{
transform: `translate(
${offset[0]}${typeof offset[0] === 'number' ? 'px' : ''},
${offset[1]}${typeof offset[1] === 'number' ? 'px' : ''}
)`
}}>
<Slot name="content" slots={props.slots}>
<If v-if={state.href}>
<a href={state.href} target={target} rel="noopener noreferrer">
{state.content}
</a>
</If>
<If v-if={!state.href}>
<span className="tiny-badge__content-text">{state.content}</span>
</If>
</Slot>
</div>
</If>
</div>
)
}

View File

@ -0,0 +1,3 @@
import Button from './src'
export default Button

View File

@ -0,0 +1,19 @@
{
"name": "@opentiny/openinula-button",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/openinula-common": "workspace:~",
"@opentiny/openinula-icon": "workspace:~",
"@opentiny/vue-theme": "workspace:~",
"@opentiny/vue-theme-mobile": "workspace:~"
}
}

View File

@ -0,0 +1,15 @@
import pc from './pc'
import mobile from './mobile'
import mobileFirst from './mobile-first'
export default function (props) {
const { tiny_mode = 'pc' } = props
const S = {
pc,
mobile,
'mobile-first': mobileFirst
}[tiny_mode]
return S(props)
}

View File

@ -0,0 +1,107 @@
import { renderless, api } from '@opentiny/vue-renderless/button/vue'
import { useSetup, vc, If, Component, Slot, useVm } from '@opentiny/openinula-common'
import { IconLoading } from '@opentiny/openinula-icon'
import { classes } from './token'
const define_props = [
'children',
'text',
'loading',
'autofocus',
'plain',
'round',
'circle',
'icon',
'size',
'type',
'nativeType',
'resetTime',
/^on/
]
export default function Button(props) {
const {
loading,
autofocus,
size,
icon,
round,
circle,
href,
buttonClass,
tabindex,
text,
type,
nativeType = 'button',
resetTime = 1000
} = props
const defaultProps = Object.assign({
nativeType,
resetTime,
}, props)
const {
ref,
parent,
current: vm
} = useVm()
const {
handleClick,
state,
a,
m,
gcls,
} = useSetup({
api,
renderless,
props: defaultProps,
classes,
ref,
parent,
vm
})
const $attrs = a(props, define_props, false)
return (
<button
data-tag="tiny-button"
onClick={handleClick}
disabled={state.buttonDisabled || loading}
autoFocus={autofocus}
type={nativeType}
className={m(
gcls('button'),
gcls(`size-${size || 'default'}`),
gcls(
`type-${type || 'default'}${icon ? '-icon' : state.plain ? '-plain' : ''}
${state.buttonDisabled ? '-disabled' : ''}`
),
gcls(round ? 'is-round' : 'no-round'),
gcls(circle ? 'is-circle' : 'no-circle'),
gcls({ 'is-border': circle || !(type === 'text' || icon) }),
gcls({ 'button-link': href }),
buttonClass
)}
tabIndex={tabindex}
{...a($attrs, ['class', 'style'], true)}
>
<If v-if={loading}>
<IconLoading className={gcls('loading-svg')} />
</If>
<Component
v-if={icon && !loading}
is={icon}
className={vc([
gcls('button-icon'),
gcls(`button-icon-${state.buttonDisabled ? 'disabled' : 'default'}`)
])}
/>
<Slot slots={props.slots} parent_children={props.children}>
<span>{text}</span>
</Slot>
</button>
)
}

View File

@ -0,0 +1,88 @@
import { renderless, api } from '@opentiny/vue-renderless/button/vue'
import { useSetup, vc, If, Component, Slot, useVm } from '@opentiny/openinula-common'
import { IconLoading } from '@opentiny/openinula-icon'
import '@opentiny/vue-theme-mobile/button/index.less'
const define_props = [
'children',
'text',
'loading',
'autofocus',
'plain',
'round',
'circle',
'icon',
'size',
'type',
'nativeType',
'resetTime',
/^on/
]
export default function Button(props) {
const {
text,
loading,
round,
icon,
size,
type = 'default',
nativeType = 'button',
resetTime = 1000
} = props
const defaultProps = Object.assign({
type,
nativeType,
resetTime
}, props)
const {
ref,
parent,
current: vm
} = useVm()
const {
handleClick,
state,
a
} = useSetup({
props: defaultProps,
renderless,
api,
parent,
vm
})
const $attrs = a(props, define_props, false)
return (
<button
ref={ref}
onClick={handleClick}
disabled={state.buttonDisabled || loading}
type={nativeType}
className={vc([
'tiny-mobile-button',
type ? 'tiny-mobile-button--' + type : '',
size ? 'tiny-mobile-button--' + size : '',
{
'is-disabled': state.buttonDisabled,
'is-loading': loading,
'is-plain': state.plain,
'is-round': round
}
])}
{...a($attrs, ['class', 'style'], true)}
>
<If v-if={loading}>
<IconLoading class='tiny-icon-loading' />
</If>
<Component v-if={icon && !loading} is={icon} class='tiny-icon is-icon' />
<Slot slots={props.slots} parent_children={props.children}>
<span style={{ marginLeft: text && (icon || loading) ? '4px' : 0 }}>{text}</span>
</Slot>
</button>
)
}

View File

@ -0,0 +1,99 @@
import { renderless, api } from '@opentiny/vue-renderless/button/vue'
import { useSetup, If, Component, vc, useVm } from '@opentiny/openinula-common'
import { IconLoading } from '@opentiny/openinula-icon'
import '@opentiny/vue-theme/button/index.less'
const define_props = [
'children',
'text',
'loading',
'autofocus',
'plain',
'round',
'circle',
'icon',
'size',
'type',
'nativeType',
'resetTime',
/^on/
]
export default function Button(props) {
const {
children,
text,
loading,
autofocus,
round,
circle,
icon,
size,
tabindex,
type = 'default',
nativeType = 'button',
resetTime = 1000
} = props
const defaultProps = Object.assign({
type,
nativeType,
resetTime
}, props)
const {
ref,
parent,
current: vm
} = useVm()
const {
handleClick,
state,
a
} = useSetup({
props: defaultProps,
renderless,
api,
vm,
parent
})
const $attrs = a(props, define_props, false)
return (
<button
ref={ref}
className={vc([
'tiny-button',
type ? 'tiny-button--' + type : '',
size ? 'tiny-button--' + size : '',
{
'is-disabled': state.buttonDisabled,
'is-loading': loading,
'is-plain': state.plain,
'is-round': round,
'is-circle': circle,
'is-icon': icon && !loading && (text || $slots.default),
'is-only-icon': icon && !loading && !(text || $slots.default)
}
])}
onClick={handleClick}
disabled={state.buttonDisabled || loading}
autoFocus={autofocus}
type={nativeType}
tabIndex={tabindex}
{...a($attrs, ['class', 'style'], true)}
>
<If v-if={loading}>
<IconLoading className="tiny-icon-loading tiny-svg-size" />
</If>
<Component
v-if={icon && !loading}
is={icon}
className={(text || children) ? 'is-text' : ''}
/>
<span>{children || text}</span>
</button>
)
}

View File

@ -0,0 +1,66 @@
export const classes = {
'button':
'inline-block sm:max-w-[9rem] text-center overflow-hidden overflow-ellipsis whitespace-nowrap transition-button duration-300 delay-[0ms]',
'size-default': 'h-10 text-sm sm:h-7 sm:text-xs',
'size-medium': 'h-10 text-sm sm:h-8 sm:text-xs',
'size-small': 'h-8 text-sm sm:h-7 sm:text-xs',
'size-mini': 'h-7 sm:h-6 sm:text-xs',
'type-default':
'text-black border-color-border hover:border-color-border-hover active:border-color-border-active sm:cursor-pointer',
'type-primary':
'text-white border-color-brand bg-color-brand hover:border-color-brand-hover hover:bg-color-brand-hover active:border-color-brand-active active:bg-color-brand-active sm:cursor-pointer',
'type-success':
'text-white border-color-success bg-color-success hover:border-color-success-hover hover:bg-color-success-hover active:border-color-success-active active:bg-color-success-active sm:cursor-pointer',
'type-info':
'text-white border-color-info-secondary bg-color-info-secondary hover:border-color-info-secondary-hover hover:bg-color-info-secondary-hover active:border-color-info-secondary-active active:bg-color-info-secondary-active sm:cursor-pointer',
'type-warning':
'text-white border-color-warning bg-color-warning hover:border-color-warning-hover hover:bg-color-warning-hover active:border-color-warning-active active:bg-color-warning-active sm:cursor-pointer',
'type-danger':
'text-white border-color-error bg-color-error hover:border-color-error-hover hover:bg-color-error-hover active:border-color-error-active active:bg-color-error-active sm:cursor-pointer',
'type-text':
'border-none bg-transparent cursor-pointer text-color-text-placeholder active:text-color-text-primary sm:hover:text-color-text-primary sm:active:!text-color-brand-active',
'type-default-disabled': 'text-color-text-disabled bg-color-bg-3 border-transparent hover:cursor-not-allowed',
'type-primary-disabled': 'text-white bg-color-brand-disabled border-transparent hover:cursor-not-allowed',
'type-success-disabled': 'text-white bg-color-success-disabled border-transparent hover:cursor-not-allowed',
'type-info-disabled': 'text-white bg-color-info-secondary-disabled border-transparent hover:cursor-not-allowed',
'type-warning-disabled': 'text-white bg-color-alert-disabled border-transparent hover:cursor-not-allowed',
'type-danger-disabled': 'text-white bg-color-error-disabled border-transparent hover:cursor-not-allowed',
'type-text-disabled': 'text-color-text-disabled hover:cursor-not-allowed',
'type-default-plain':
'text-black border-color-border hover:border-color-border-hover active:border-color-border-active sm:cursor-pointer',
'type-primary-plain':
'text-color-brand border-color-brand hover:text-color-brand-hover hover:border-color-brand-hover active:text-color-brand-active active:border-color-brand-active bg-white sm:cursor-pointer',
'type-success-plain':
'text-color-success border-color-success hover:text-color-success-hover hover:border-color-success-hover active:text-color-success-active active:border-color-success-active bg-white sm:cursor-pointer',
'type-info-plain':
'text-color-info-secondary border-color-info-secondary hover:text-color-info-secondary-hover hover:border-color-info-secondary-hover active:text-color-info-secondary-active active:border-color-info-secondary-active bg-white sm:cursor-pointer',
'type-warning-plain':
'text-color-warning border-color-warning hover:text-color-warning-hover hover:border-color-warning-hover active:text-color-warning-active active:border-color-warning-active bg-white sm:cursor-pointer',
'type-danger-plain':
'text-color-error border-color-error hover:text-color-error-hover hover:border-color-error-hover active:text-color-error-active active:border-color-error-active bg-white sm:cursor-pointer',
'type-text-plain': 'text-color-brand hover:text-color-brand-hover active:text-color-brand-active',
'type-default-plain-disabled':
'text-color-text-disabled bg-white border-color-text-disabled hover:cursor-not-allowed',
'type-primary-plain-disabled':
'text-color-brand-disabled bg-white border-color-brand-disabled hover:cursor-not-allowed',
'type-success-plain-disabled':
'text-color-success-disabled bg-white border-color-success-disabled hover:cursor-not-allowed',
'type-info-plain-disabled':
'text-color-info-secondary-disabled bg-white border-color-info-secondary-disabled hover:cursor-not-allowed',
'type-warning-plain-disabled':
'text-color-alert-disabled bg-white border-color-alert-disabled hover:cursor-not-allowed',
'type-danger-plain-disabled':
'text-color-error-disabled bg-white border-color-error-disabled hover:cursor-not-allowed',
'type-text-plain-disabled': 'text-color-text-disabled hover:cursor-not-allowed',
'no-round': 'rounded-sm',
'is-round': 'rounded-full',
'is-border': 'border-0.5 sm:border',
'no-circle': 'sm:min-w-[4.5rem] pl-3 pr-3',
'is-circle': 'sm:min-w-[0.4375rem] sm:rounded-full sm:pl-2 sm:pr-2',
'button-icon': '-mt-0.5 sm:text-base fill-current',
'button-icon-default': 'text-color-icon-primary hover:text-color-icon-hover active:text-color-icon-active',
'button-icon-disabled': 'text-color-icon-disabled hover:cursor-not-allowed',
'loading-svg': 'animate-spin-2 mr-1 fill-current -inset-0.5',
'button-link':
'text-color-link hover:text-color-link-hover active:color-link-hover active:hover:text-color-link-hover sm:hover:text-color-link-hover'
}

View File

@ -3,10 +3,7 @@ import { compWhiteList } from './virtual-comp'
export function getFiberByDom(dom) {
const key = Object.keys(dom).find((key) => {
return (
key.startsWith('__openinulaFiber$') || // openinula 17+
key.startsWith('__openinulaInternalInstance$')
) // openinula <17
return key.startsWith('_inula_VNode_')
})
return dom[key]
@ -46,13 +43,13 @@ export function creatFiberCombine(fiber) {
const children = []
traverseFiber(fiber.child, [
(fiber) => {
if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v_ref')) {
refs[fiber.stateNode.getAttribute('v_ref')] = fiber.stateNode
} else if (fiber.memoizedProps.v_ref) {
refs[fiber.memoizedProps.v_ref] = fiber
}
},
// (fiber) => {
// if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v_ref')) {
// refs[fiber.stateNode.getAttribute('v_ref')] = fiber.stateNode
// } else if (fiber.memoizedProps.v_ref) {
// refs[fiber.memoizedProps.v_ref] = fiber
// }
// },
(fiber) => {
if (fiber.type && typeof fiber.type !== 'string') {
children.push(fiber)
@ -74,8 +71,9 @@ export function useFiber() {
useEffect(() => {
if (ref.current) {
const current_fiber = getFiberByDom(ref.current)
setParent(getParentFiber(current_fiber.return))
setCurrent(current_fiber.return)
setParent(getParentFiber(current_fiber))
setCurrent(current_fiber)
}
}, [])

View File

@ -99,10 +99,10 @@ export const vc = VueClassName
export const getElementCssClass = (classes = {}, key) => {
if (typeof key === 'object') {
const keys = Array.isArray(key) ? key : Object.keys(key).filter((k) => key[k])
const keys = Object.keys(key)
let cls = ''
keys.forEach((k) => {
if (classes[k]) cls += `${classes[k]} `
if (key[k] && classes[k]) cls += `${classes[k]} `
})
return cls
} else {

View File

@ -100,7 +100,7 @@ export function useVm() {
}
return {
ref,
current: current.fiber && createVmProxy(current),
parent: parent.fiber && createVmProxy(parent)
current: current && createVmProxy(current),
parent: parent && createVmProxy(parent)
}
}

View File

@ -11,9 +11,9 @@ const collectRefs = (rootEl, $children) => {
const rootFiber = getFiberByDom(rootEl)
// 收集普通元素 ref
traverseFiber(rootFiber, (fiber) => {
if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v-ref')) {
refs[fiber.stateNode.getAttribute('v-ref')] = fiber.stateNode
}
// if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v-ref')) {
// refs[fiber.stateNode.getAttribute('v-ref')] = fiber.stateNode
// }
})
// 收集组件元素 ref
$children.forEach((child) => {

View File

@ -0,0 +1,7 @@
import Switch from './src/index'
import '@opentiny/vue-theme/switch/index.less'
import { version } from './package.json'
Switch.version = version
export default Switch

View File

@ -0,0 +1,25 @@
{
"name": "@opentiny/openinula-switch",
"version": "3.0.0",
"description": "",
"main": "lib/index.js",
"module": "index.ts",
"sideEffects": false,
"type": "module",
"scripts": {
"build": "pnpm -w build:ui-openinula $npm_package_name",
"//postversion": "pnpm build"
},
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "^0.31.0"
},
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/openinula-common": "workspace:~",
"@opentiny/openinula-icon": "workspace:~",
"@opentiny/vue-theme": "workspace:~",
"@opentiny/vue-theme-mobile": "workspace:~"
},
"license": "MIT"
}

View File

@ -0,0 +1,31 @@
import pc from './pc'
import mobile from './mobile'
// import mobileFirst from './mobile-first'
const $constants = {
PC_PREFIXCLS: 'tiny-switch',
MOBILE_PREFIXCLS: 'tiny-mobile-switch',
Mode: 'pc',
prefixcls(mode) {
return mode === this.Mode ? this.PC_PREFIXCLS : this.MOBILE_PREFIXCLS
}
}
export default function (props) {
const { tiny_mode = 'pc', _constants = $constants } = props
const defaultProps = Object.assign(
{
_constants,
tiny_mode
},
props
)
const S = {
pc,
mobile
}[tiny_mode]
return S && S(defaultProps)
}

View File

@ -0,0 +1,59 @@
import { renderless, api } from '@opentiny/vue-renderless/switch/vue'
import { vc, useSetup, useVm } from '@opentiny/openinula-common'
import '@opentiny/vue-theme-mobile/switch/index.less'
const $constants = {}
export default function Switch(props) {
const {
_constants,
disabled = false,
showText,
falseColor,
falseValue = false,
mini = false,
modelValue = false,
size,
tabindex = '1',
trueColor,
trueValue = true,
beforeChange,
displayOnly = false
} = props
const defaultProps = Object.assign(
{
disabled,
falseValue,
mini,
modelValue,
tabindex,
trueValue,
displayOnly
},
props
)
const { ref, current: vm, parent } = useVm()
const { toggle, state } = useSetup({
props,
renderless,
api,
constants: _constants,
vm,
parent
})
return (
<div ref={ref}>
{' '}
<span className={vc(state.wrapClasses)} disabled={disabled} onClick={toggle} onKeydown={toggle}>
<div className="tiny-mobile-switch-loading">
{' '}
<div className="tiny-mobile-switch-loading-inner"></div>
</div>
</span>
</div>
)
}

View File

@ -0,0 +1,67 @@
import { useSetup, useVm, vc, If, Slot } from '@opentiny/openinula-common'
import { api, renderless } from '@opentiny/vue-renderless/switch/vue'
import '@opentiny/vue-theme/switch/index.less'
export default function (props) {
const {
_constants,
disabled = false,
showText,
falseColor,
falseValue = false,
mini = false,
modelValue = false,
size,
tabindex = '1',
trueColor,
trueValue = true,
beforeChange,
displayOnly = false
} = props
const defaultProps = Object.assign(
{
disabled,
falseValue,
mini,
modelValue,
tabindex,
trueValue,
displayOnly
},
props
)
const { ref, parent, current: vm } = useVm()
const { state, toggle } = useSetup({
props: defaultProps,
api,
renderless,
vm,
parent,
constants: _constants
})
return (
<span
ref={ref}
className={vc([state.wrapClasses, state.showText ? 'tiny-switch__text' : ''])}
tabIndex={tabindex}
onClick={toggle}
onKeyDown={(e) => {
e.key === 'Enter' && toggle(e)
}}>
<span className={state.innerClasses}>
<If v-if={!mini && state.showText}>
<Slot v-if={state.currentValue === trueValue} name="open" slots={props.slots}>
ON
</Slot>
<Slot v-if={state.currentValue === falseValue} name="close" slots={props.slots}>
OFF
</Slot>
</If>
</span>
</span>
)
}