diff --git a/examples/openinula-docs/index.html b/examples/openinula-docs/index.html
new file mode 100644
index 000000000..d2994c193
--- /dev/null
+++ b/examples/openinula-docs/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Opentiny openinula 组件调试
+
+
+
+
+
+
diff --git a/examples/openinula-docs/package.json b/examples/openinula-docs/package.json
new file mode 100644
index 000000000..678530030
--- /dev/null
+++ b/examples/openinula-docs/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@opentiny/openinula-docs",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@opentiny/openinula": "workspace:~",
+ "openinula": "^0.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.14",
+ "@types/react-dom": "^18.2.6",
+ "@typescript-eslint/eslint-plugin": "^6.12.0",
+ "@typescript-eslint/parser": "^6.12.0",
+ "@vitejs/plugin-react": "^4.0.1",
+ "eslint": "^8.44.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "postcss": "^8.4.16",
+ "typescript": "^5.0.2",
+ "vite": "^4.3.8",
+ "vite-plugin-svgr": "^3.2.0"
+ }
+}
diff --git a/examples/openinula-docs/public/vite.svg b/examples/openinula-docs/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/examples/openinula-docs/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/openinula-docs/src/App.tsx b/examples/openinula-docs/src/App.tsx
new file mode 100644
index 000000000..b059ad1f5
--- /dev/null
+++ b/examples/openinula-docs/src/App.tsx
@@ -0,0 +1,12 @@
+import { Alert } from '@opentiny/openinula'
+
+// 在这里导入组件,进行 api 调试
+function App() {
+ return (
+
+ )
+}
+
+export default App
diff --git a/examples/openinula-docs/src/main.css b/examples/openinula-docs/src/main.css
new file mode 100644
index 000000000..50154555f
--- /dev/null
+++ b/examples/openinula-docs/src/main.css
@@ -0,0 +1,4 @@
+.app {
+ margin: 10px;
+ width: 500px;
+}
diff --git a/examples/openinula-docs/src/main.tsx b/examples/openinula-docs/src/main.tsx
new file mode 100644
index 000000000..753782ad8
--- /dev/null
+++ b/examples/openinula-docs/src/main.tsx
@@ -0,0 +1,5 @@
+import Inula from 'openinula'
+import App from './App.tsx'
+import './main.css'
+
+Inula.render(, document.getElementById('root'))
diff --git a/examples/openinula-docs/tsconfig.json b/examples/openinula-docs/tsconfig.json
new file mode 100644
index 000000000..a7fc6fbf2
--- /dev/null
+++ b/examples/openinula-docs/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/openinula-docs/tsconfig.node.json b/examples/openinula-docs/tsconfig.node.json
new file mode 100644
index 000000000..42872c59f
--- /dev/null
+++ b/examples/openinula-docs/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/openinula-docs/vite.config.ts b/examples/openinula-docs/vite.config.ts
new file mode 100644
index 000000000..eb2d5f5df
--- /dev/null
+++ b/examples/openinula-docs/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import svgr from 'vite-plugin-svgr'
+
+const alias = {
+ react: 'openinula',
+ 'react-dom': 'openinula',
+ 'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime'
+}
+
+export default defineConfig({
+ plugins: [svgr(), react({ include: /\.(mdx|js|jsx|ts|tsx)$/ })],
+ resolve: {
+ alias
+ }
+})
diff --git a/package.json b/package.json
index 6bcfbf16b..5f16d7c0f 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,6 @@
"dev2:saas": "pnpm create:icon-saas && pnpm build:entry && pnpm -C examples/vue2 dev:saas",
"dev2.7": "pnpm build:entry && gulp themeConcat -w & pnpm -C examples/vue2.7 dev",
"dev2.7:saas": "pnpm create:icon-saas && pnpm build:entry && pnpm -C examples/vue2.7 dev:saas",
- "dev:react": "pnpm create:mapping-react && pnpm build:entry-react && pnpm -C examples/react-docs run dev",
"// ---------- 启动官网文档 ----------": "",
"site": "pnpm build:entry && gulp themeConcat -w & pnpm -C examples/sites start",
"site:open": "pnpm build:entry && gulp themeConcat -w & pnpm -C examples/sites start:open",
@@ -114,6 +113,8 @@
"ci:deployBeta": "pnpm build:ui",
"postci:deployBeta": "lerna publish from-package --yes --dist-tag beta",
"analyse:depends": "pnpm --filter @opentiny/analyse_depends start",
+ "// ---------- react 相关脚本命令 ----------": "",
+ "dev:react": "pnpm create:mapping-react && pnpm build:entry-react && pnpm -C examples/react-docs run dev",
"build:entry-react": "pnpm -C internals/cli build:entry-react",
"create:mapping-react": "pnpm -C internals/cli create:mapping-react",
"build:react": "pnpm -C internals/cli build:react",
@@ -121,7 +122,9 @@
"pub:react": "pnpm --filter=\"./packages/dist-react/**\" publish --no-git-checks --access=public",
"dev:react-site": "pnpm --filter @opentiny/react-site start",
"build:react-site": "pnpm --filter @opentiny/react-site build",
- "prettier": "prettier --config .prettierrc --write ."
+ "prettier": "prettier --config .prettierrc --write .",
+ "// ---------- openinula 相关脚本命令 ----------": "",
+ "dev:openinula": "pnpm -C examples/openinula-docs run dev"
},
"dependencies": {
"@vue/composition-api": "1.2.2",
diff --git a/packages/openinula/.depcheckrc.yaml b/packages/openinula/.depcheckrc.yaml
new file mode 100644
index 000000000..89d202940
--- /dev/null
+++ b/packages/openinula/.depcheckrc.yaml
@@ -0,0 +1,2 @@
+ignores:
+ - '@opentiny/openinula*'
diff --git a/packages/openinula/index.ts b/packages/openinula/index.ts
new file mode 100644
index 000000000..1e3f1a329
--- /dev/null
+++ b/packages/openinula/index.ts
@@ -0,0 +1,9 @@
+import Alert from '@opentiny/openinula-alert'
+
+export const version = '1.0.0'
+
+export { Alert }
+
+export default {
+ Alert
+} as any
diff --git a/packages/openinula/package.json b/packages/openinula/package.json
new file mode 100644
index 000000000..d1762dab9
--- /dev/null
+++ b/packages/openinula/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@opentiny/openinula",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.ts",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@opentiny/openinula-common": "workspace:~",
+ "@opentiny/openinula-alert": "workspace:~"
+ }
+}
diff --git a/packages/openinula/src/alert/index.ts b/packages/openinula/src/alert/index.ts
new file mode 100644
index 000000000..9bc345130
--- /dev/null
+++ b/packages/openinula/src/alert/index.ts
@@ -0,0 +1,3 @@
+import Alert from './src'
+
+export default Alert
diff --git a/packages/openinula/src/alert/package.json b/packages/openinula/src/alert/package.json
new file mode 100644
index 000000000..addfaa2e3
--- /dev/null
+++ b/packages/openinula/src/alert/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@opentiny/openinula-alert",
+ "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:~"
+ }
+}
diff --git a/packages/openinula/src/alert/src/index.ts b/packages/openinula/src/alert/src/index.ts
new file mode 100644
index 000000000..e79744199
--- /dev/null
+++ b/packages/openinula/src/alert/src/index.ts
@@ -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)
+}
diff --git a/packages/openinula/src/alert/src/mobile-first.jsx b/packages/openinula/src/alert/src/mobile-first.jsx
new file mode 100644
index 000000000..78a524520
--- /dev/null
+++ b/packages/openinula/src/alert/src/mobile-first.jsx
@@ -0,0 +1,185 @@
+import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
+import { IconClose, IconSuccess, IconError, IconHelp, IconWarning, IconChevronDown } from '@opentiny/openinula-icon'
+import { vc, If, Component, Slot, useSetup, useVm } from '@opentiny/openinula-common'
+
+const $constants = {
+ ICON_MAP: {
+ success: IconSuccess,
+ error: IconError,
+ info: IconHelp,
+ warning: IconWarning
+ },
+ TITLE_MAP: {
+ success: 'ui.alert.success',
+ error: 'ui.alert.error',
+ info: 'ui.alert.info',
+ warning: 'ui.alert.warning'
+ },
+ CONTENT_MAXHEUGHT: 252
+}
+
+export default function Alert(props) {
+ const {
+ type = 'success',
+ size = 'normal',
+ center = false,
+ showIcon = true,
+ description = '',
+ slots = {},
+ _constants = $constants,
+ closable = true,
+ closeText,
+ title,
+ showFoldable = true,
+ singleLine,
+ scrolling
+ } = props
+
+ const defaultProps = {
+ type,
+ size,
+ center,
+ showIcon,
+ description,
+ slots,
+ _constants,
+ closable,
+ closeText,
+ title,
+ showFoldable
+ }
+
+ const { ref, current: vm, parent } = useVm()
+
+ const { state, handleHeaderClick } = useSetup({
+ props: defaultProps,
+ renderless,
+ api,
+ constants: _constants,
+ vm,
+ parent
+ })
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {state.getTitle}
+
+
+
+
+
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {state.getTitle}
+
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {closeText}
+
+
+
+
+
+ )
+}
diff --git a/packages/openinula/src/alert/src/mobile.jsx b/packages/openinula/src/alert/src/mobile.jsx
new file mode 100644
index 000000000..7245d81b7
--- /dev/null
+++ b/packages/openinula/src/alert/src/mobile.jsx
@@ -0,0 +1,81 @@
+import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
+import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/openinula-icon'
+import { vc, If, Component, Slot, useSetup, useVm } from '@opentiny/openinula-common'
+import '@opentiny/vue-theme-mobile/alert/index.less'
+
+const $constants = {
+ ICON_MAP: {
+ success: IconSuccess,
+ error: IconError,
+ info: IconHelp,
+ warning: IconWarning
+ },
+ TITLE_MAP: {
+ success: 'ui.alert.success',
+ error: 'ui.alert.error',
+ info: 'ui.alert.info',
+ warning: 'ui.alert.warning'
+ },
+ CONTENT_MAXHEUGHT: 252
+}
+
+export default function Alert(props) {
+ const {
+ type = 'success',
+ size = 'normal',
+ showIcon = true,
+ closable = true,
+ closeText,
+ _constants = $constants,
+ description = ''
+ } = props
+
+ const defaultProps = Object.assign(
+ {
+ type,
+ size,
+ showIcon,
+ closable,
+ _constants
+ },
+ props
+ )
+
+ const { ref, current: vm, parent } = useVm()
+
+ const { state, handleClose } = useSetup({
+ props: defaultProps,
+ renderless,
+ api,
+ vm,
+ parent,
+ constants: _constants
+ })
+
+ return (
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+ {closeText}
+
+
+
+
+
+ )
+}
diff --git a/packages/openinula/src/alert/src/pc.jsx b/packages/openinula/src/alert/src/pc.jsx
new file mode 100644
index 000000000..a80ad1503
--- /dev/null
+++ b/packages/openinula/src/alert/src/pc.jsx
@@ -0,0 +1,103 @@
+import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
+import { IconClose, IconSuccess, IconError, IconHelp, IconWarning } from '@opentiny/openinula-icon'
+import '@opentiny/vue-theme/alert/index.less'
+import { vc, If, Component, Slot, useSetup, useVm } from '@opentiny/openinula-common'
+
+const $constants = {
+ ICON_MAP: {
+ success: IconSuccess,
+ error: IconError,
+ info: IconHelp,
+ warning: IconWarning
+ },
+ TITLE_MAP: {
+ success: 'ui.alert.success',
+ error: 'ui.alert.error',
+ info: 'ui.alert.info',
+ warning: 'ui.alert.warning'
+ },
+ CONTENT_MAXHEUGHT: 252
+}
+
+export default function Alert(props) {
+ const {
+ type = 'success',
+ size = 'normal',
+ center = false,
+ showIcon = true,
+ description = '',
+ slots = {},
+ _constants = $constants,
+ closable = true,
+ closeText,
+ title
+ } = props
+
+ const defaultProps = {
+ type,
+ size,
+ center,
+ showIcon,
+ description,
+ slots,
+ _constants,
+ closable,
+ closeText,
+ title
+ }
+
+ const { ref, current: vm, parent } = useVm()
+
+ const { state, handleClose } = useSetup({
+ props: defaultProps,
+ renderless,
+ api,
+ constants: _constants,
+ vm,
+ parent
+ })
+
+ return (
+
+
+
+
+
+
+
+
+ {state.getTitle}
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {closeText}
+
+
+
+
+
+ )
+}
diff --git a/packages/openinula/src/common/package.json b/packages/openinula/src/common/package.json
new file mode 100644
index 000000000..134fd1166
--- /dev/null
+++ b/packages/openinula/src/common/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@opentiny/openinula-common",
+ "version": "1.0.0",
+ "description": "",
+ "main": "src/index.ts",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@opentiny/vue-renderless": "workspace:~",
+ "@opentiny/vue-theme": "workspace:~",
+ "classnames": "^2.3.2",
+ "openinula": "0.1.1",
+ "tailwind-merge": "^1.8.0",
+ "@vue/runtime-core": "^3.3.7"
+ }
+}
diff --git a/packages/openinula/src/common/src/csscls.ts b/packages/openinula/src/common/src/csscls.ts
new file mode 100644
index 000000000..27f274a6e
--- /dev/null
+++ b/packages/openinula/src/common/src/csscls.ts
@@ -0,0 +1,65 @@
+interface CssClassObject {
+ [k: string]: any
+}
+type CssClassArray = Array
+export type CssClass = string | CssClassObject | CssClassArray
+
+/**
+ * 简单合并 tailwind 类对象为字符串值
+ *
+ * @param cssClassObject tailwind 类对象
+ * @returns string
+ */
+const stringifyCssClassObject = (cssClassObject: CssClassObject): string => {
+ const allCssClass: Array = []
+
+ Object.keys(cssClassObject).forEach((cssClass) => cssClassObject[cssClass] && allCssClass.push(cssClass))
+
+ return allCssClass.join('\u{20}')
+}
+
+/**
+ * 简单合并 tailwind 类数组为字符串值
+ *
+ * @param cssClassArray tailwind 类数组
+ * @returns string
+ */
+const stringifyCssClassArray = (cssClassArray: CssClassArray): string => {
+ const allCssClass: Array = []
+
+ cssClassArray.forEach((cssClass) => {
+ if (typeof cssClass === 'string') {
+ allCssClass.push(cssClass)
+ } else if (typeof cssClass === 'object') {
+ allCssClass.push(stringifyCssClassObject(cssClass))
+ }
+ })
+
+ return allCssClass.join('\u{20}')
+}
+
+/**
+ * 简单合并 tailwind 类对象为字符串值,去重处理留给 tailwind-merge 处理
+ *
+ * @param {*} cssClasses tailwind 类集合
+ * @returns string
+ */
+export const stringifyCssClass = (cssClasses: Array): string => {
+ if (!cssClasses || (Array.isArray(cssClasses) && !cssClasses.length)) return ''
+
+ const allCssClass: Array = []
+
+ cssClasses.forEach((cssClass) => {
+ if (cssClass) {
+ if (typeof cssClass === 'string') {
+ allCssClass.push(cssClass)
+ } else if (Array.isArray(cssClass)) {
+ allCssClass.push(stringifyCssClassArray(cssClass))
+ } else if (typeof cssClass === 'object') {
+ allCssClass.push(stringifyCssClassObject(cssClass))
+ }
+ }
+ })
+
+ return allCssClass.join('\u{20}')
+}
diff --git a/packages/openinula/src/common/src/event.ts b/packages/openinula/src/common/src/event.ts
new file mode 100644
index 000000000..57df646a9
--- /dev/null
+++ b/packages/openinula/src/common/src/event.ts
@@ -0,0 +1,90 @@
+import { eventBus } from './utils'
+
+const $busMap = new Map()
+
+export const emit =
+ (props) =>
+ (evName, ...args) => {
+ const openinulaEvName = 'on' + evName.substr(0, 1).toUpperCase() + evName.substr(1)
+
+ if (props[openinulaEvName] && typeof props[openinulaEvName] === 'function') {
+ props[openinulaEvName](...args)
+ } else {
+ const $bus = $busMap.get(props)
+ if ($bus) {
+ $bus.emit(evName, ...args)
+ }
+ }
+ }
+export const on = (props) => (evName, callback) => {
+ if ($busMap.get(props)) {
+ const $bus = $busMap.get(props)
+ $bus.on(evName, callback)
+ } else {
+ const $bus = eventBus()
+ $bus.on(evName, callback)
+ $busMap.set(props, $bus)
+ }
+}
+export const off = (props) => (evName, callback) => {
+ const $bus = $busMap.get(props)
+ if (!$bus) return
+ $bus.off(evName, callback)
+}
+export const once = (props) => (evName, callback) => {
+ let $bus = null
+ const onceCallback = (...args) => {
+ callback(...args)
+ $bus && $bus.off(evName, onceCallback)
+ }
+
+ if ($busMap.get(props)) {
+ $bus = $busMap.get(props)
+ $bus.on(evName, onceCallback)
+ } else {
+ $bus = eventBus()
+ $bus.on(evName, onceCallback)
+ $busMap.set(props, $bus)
+ }
+}
+export const emitEvent = (vm) => {
+ const broadcast = (vm, componentName, eventName, ...args) => {
+ const children = vm.$children
+
+ Array.isArray(children) &&
+ children.forEach((child) => {
+ const name = child.$options && child.$options.componentName
+ const component = child
+
+ if (name === componentName) {
+ component.emit(eventName, ...args)
+ // todo: 调研 component.$emitter
+ // component.$emitter && component.$emitter.emit(eventName, params)
+ } else {
+ broadcast(child, componentName, eventName, ...args)
+ }
+ })
+ }
+
+ return {
+ dispatch(componentName, eventName, ...args) {
+ let parent = vm.$parent
+ if (parent.type === null) return
+ let name = parent.$options && parent.$options.componentName
+ while (parent && parent.type && (!name || name !== componentName)) {
+ parent = parent.$parent
+
+ if (parent) name = parent.$options && parent.$options.componentName
+ }
+
+ if (parent) {
+ parent.emit(eventName, ...args)
+ // fix: VUE3下事件参数为数组,VUE2下事件参数不是数组,这里修改为和VUE2兼容
+ // parent.$emitter && parent.$emitter.emit(...[eventName].concat(params))
+ }
+ },
+ broadcast(componentName, eventName, ...args) {
+ broadcast(vm, componentName, eventName, ...args)
+ }
+ }
+}
diff --git a/packages/openinula/src/common/src/fiber.ts b/packages/openinula/src/common/src/fiber.ts
new file mode 100644
index 000000000..8734c805d
--- /dev/null
+++ b/packages/openinula/src/common/src/fiber.ts
@@ -0,0 +1,87 @@
+import { useRef, useEffect, useState } from 'openinula'
+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 dom[key]
+}
+
+function defaultBreaker({ type }) {
+ if (type && typeof type !== 'string') {
+ return !compWhiteList.includes(type.name)
+ }
+}
+
+export function traverseFiber(fiber, handler, breaker = defaultBreaker) {
+ if (!fiber) return
+ typeof handler === 'function' && handler(fiber)
+ Array.isArray(handler) &&
+ handler.forEach((task) => {
+ typeof task === 'function' && task(fiber)
+ })
+ traverseFiber(fiber.sibling, handler, breaker)
+ breaker(fiber) || traverseFiber(fiber.child, handler, breaker)
+}
+
+const parentMap = new WeakMap()
+export function getParentFiber(fiber, isFirst = true, child = fiber) {
+ if (!fiber || !fiber.return) return null
+ if (parentMap.has(child)) return parentMap.get(child)
+ if (fiber.type && typeof fiber.type !== 'string' && !isFirst) {
+ parentMap.set(child, fiber)
+ return fiber
+ }
+ return getParentFiber(fiber.return, false, fiber)
+}
+
+export function creatFiberCombine(fiber) {
+ if (!fiber) return
+ const refs = {}
+ 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 (fiber.type && typeof fiber.type !== 'string') {
+ children.push(fiber)
+ }
+ }
+ ])
+
+ return {
+ fiber,
+ refs,
+ children
+ }
+}
+
+export function useFiber() {
+ const ref = useRef()
+ const [parent, setParent] = useState()
+ const [current, setCurrent] = useState()
+ useEffect(() => {
+ if (ref.current) {
+ const current_fiber = getFiberByDom(ref.current)
+ setParent(getParentFiber(current_fiber.return))
+ setCurrent(current_fiber.return)
+ }
+ }, [])
+
+ return {
+ ref,
+ parent: creatFiberCombine(parent),
+ current: creatFiberCombine(current)
+ }
+}
diff --git a/packages/openinula/src/common/src/hooks.ts b/packages/openinula/src/common/src/hooks.ts
new file mode 100644
index 000000000..d139a2861
--- /dev/null
+++ b/packages/openinula/src/common/src/hooks.ts
@@ -0,0 +1,24 @@
+import { useState, useRef } from 'openinula'
+
+export function useExcuteOnce(cb, ...args) {
+ const isExcuted = useRef(false)
+ const result = useRef()
+ if (!isExcuted.current) {
+ isExcuted.current = true
+ result.current = cb(...args)
+ }
+ return result.current
+}
+
+export function useReload() {
+ const [_, reload] = useState(0)
+ return () => reload((pre) => pre + 1)
+}
+
+export function useOnceResult(func, ...args) {
+ const result = useRef()
+ if (!result.current) {
+ result.current = func(...args)
+ }
+ return result.current
+}
diff --git a/packages/openinula/src/common/src/index.ts b/packages/openinula/src/common/src/index.ts
new file mode 100644
index 000000000..d598ac23e
--- /dev/null
+++ b/packages/openinula/src/common/src/index.ts
@@ -0,0 +1,149 @@
+import { Svg } from './svg-render'
+import { generateVueHooks, useVueLifeHooks } from './vue-hooks.js'
+import { emitEvent } from './event.js'
+import { If, Component, Slot, For, Transition } from './virtual-comp'
+import { filterAttrs, vc, getElementCssClass, eventBus } from './utils.js'
+import { useFiber } from './fiber.js'
+import { useVm } from './vm.js'
+import { twMerge } from 'tailwind-merge'
+import { stringifyCssClass } from './csscls.js'
+import { useExcuteOnce, useReload, useOnceResult } from './hooks.js'
+
+// 导入 vue 响应式系统
+import { effectScope, nextTick, reactive } from '@vue/runtime-core'
+import { useCreateVueInstance } from './vue-instance'
+
+import '@opentiny/vue-theme/base/index.less'
+
+// emitEvent, dispath, broadcast
+export const $prefix = 'Tiny'
+
+export const $props = {
+ 'tiny_mode': String,
+ 'tiny_mode_root': Boolean,
+ 'tiny_template': [Function, Object],
+ 'tiny_renderless': Function,
+ 'tiny_theme': String,
+ 'tiny_chart_theme': Object
+}
+
+export const mergeClass = (...cssClasses) => twMerge(stringifyCssClass(cssClasses))
+
+const setup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants, vm, parent, $bus }) => {
+ const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless
+ const { dispatch, broadcast } = emitEvent(vm)
+
+ const utils = {
+ vm,
+ parent,
+ emit: vm.$emit,
+ constants,
+ nextTick,
+ dispatch,
+ broadcast,
+ t() {},
+ mergeClass,
+ mode: props.tiny_mode
+ }
+
+ const sdk = render(
+ props,
+ {
+ ...generateVueHooks({
+ $bus
+ })
+ },
+ utils,
+ extendOptions
+ )
+
+ const attrs = {
+ a: filterAttrs,
+ m: mergeClass,
+ vm: utils.vm,
+ gcls: (key) => getElementCssClass(classes, key)
+ }
+
+ if (Array.isArray(api)) {
+ api.forEach((name) => {
+ const value = sdk[name]
+
+ if (typeof value !== 'undefined') {
+ attrs[name] = value
+ }
+ })
+ }
+
+ return attrs
+}
+
+export const useSetup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants }) => {
+ const $bus = useOnceResult(() => eventBus())
+
+ // 刷新逻辑
+ const reload = useReload()
+ useExcuteOnce(() => {
+ // 1. 响应式触发 $bus 的事件
+ // 2. 事件响应触发组件更新
+ $bus.on('event:reload', reload)
+ })
+
+ // 收集副作用,组件卸载自动清除副作用
+ const scope = useOnceResult(() => effectScope())
+ useExcuteOnce(() => {
+ $bus.on('hook:onBeforeUnmount', () => scope.stop())
+ })
+
+ // 创建响应式 props,每次刷新更新响应式 props
+ const reactiveProps = useOnceResult(() => reactive(props))
+ Object.assign(reactiveProps, props)
+
+ const { ref, vm } = useCreateVueInstance({
+ $bus,
+ props
+ })
+
+ // 执行一次 renderless
+ // renderless 作为 setup 的结果,最后要将结果挂在 vm 上
+ let setupResult = useExcuteOnce(() => {
+ let result
+ // 在 effectScope 里运行 renderless
+ scope.run(() => {
+ result = setup({
+ props: reactiveProps,
+ renderless,
+ api,
+ constants,
+ extendOptions,
+ classes,
+ vm,
+ parent,
+ $bus
+ })
+ })
+ return result
+ })
+
+ // 触发生命周期
+ useVueLifeHooks($bus)
+
+ Object.keys(setupResult).forEach((key) => {
+ vm[key] = setupResult[key]
+ })
+
+ return {
+ ...setupResult,
+ _: {
+ ref,
+ vm
+ }
+ }
+}
+
+export { Svg, If, Component, Slot, For, Transition, vc, emitEvent, useVm, useFiber }
+
+export * from './vue-hooks.js'
+export * from './vue-props.js'
+export * from './render-stack.js'
+export * from './vue-instance.js'
+export * from './hooks'
diff --git a/packages/openinula/src/common/src/reactive.ts b/packages/openinula/src/common/src/reactive.ts
new file mode 100644
index 000000000..7654993f4
--- /dev/null
+++ b/packages/openinula/src/common/src/reactive.ts
@@ -0,0 +1,94 @@
+import { useState, useRef } from 'openinula'
+import { computed } from './vue-hooks'
+
+// 响应式核心
+const reactiveMap = new WeakMap()
+const reactive = (staticObject, handler = {}, path = [], rootStaticObject = staticObject) => {
+ reactiveMap.has(staticObject) ||
+ reactiveMap.set(
+ staticObject,
+ new Proxy(staticObject, {
+ get(target, property, receiver) {
+ const targetVal = target[property]
+ if (targetVal && targetVal['v-hooks-type'] === computed) {
+ return targetVal.value
+ }
+
+ const _path = [...path, property]
+ const res = typeof targetVal === 'object' ? reactive(targetVal, handler, _path, rootStaticObject) : targetVal
+
+ // 监听访问
+ handler.get &&
+ handler.get({
+ result: res,
+ root: rootStaticObject,
+ path: _path,
+ target,
+ property,
+ receiver
+ })
+
+ return res
+ },
+ set(target, property, value, receiver) {
+ const targetVal = target[property]
+ if (targetVal && targetVal['v-hooks-type'] === computed) {
+ targetVal.value = value
+ return true
+ }
+
+ const _path = [...path, property]
+
+ // 监听修改
+ handler.set &&
+ handler.set({
+ target,
+ property,
+ receiver,
+ root: rootStaticObject,
+ path: _path,
+ newVal: value,
+ oldVal: target[property]
+ })
+
+ target[property] = value
+ return true
+ }
+ })
+ )
+
+ return reactiveMap.get(staticObject)
+}
+
+export const useReload = () => {
+ const setReload = useState(0)[1]
+ return () => setReload((pre) => pre + 1)
+}
+
+const isObject = (val) => val !== null && typeof val === 'object'
+
+// 用于从 hooks 链表中查找 reactive 生成的 state
+export function Reactive(obj) {
+ Object.keys(obj).forEach((key) => {
+ this[key] = obj[key]
+ })
+}
+
+export const useReactive = (initalObject) => {
+ if (!isObject(initalObject)) {
+ return initalObject
+ }
+ const memoried = useRef()
+ const proxy = useRef()
+ const reload = useReload()
+
+ if (!memoried.current && !proxy.current) {
+ memoried.current = new Reactive(initalObject)
+ proxy.current = reactive(memoried.current, {
+ set() {
+ reload()
+ }
+ })
+ }
+ return proxy.current
+}
diff --git a/packages/openinula/src/common/src/render-stack.ts b/packages/openinula/src/common/src/render-stack.ts
new file mode 100644
index 000000000..905454469
--- /dev/null
+++ b/packages/openinula/src/common/src/render-stack.ts
@@ -0,0 +1,15 @@
+const renderStack = []
+
+export const getParent = () => renderStack[renderStack.length - 1] || {}
+
+export const getRoot = () => renderStack[0] || {}
+
+export const EnterStack = (props) => {
+ renderStack.push(props)
+ return ''
+}
+
+export const LeaveStack = () => {
+ renderStack.pop()
+ return ''
+}
diff --git a/packages/openinula/src/common/src/resolve-props.ts b/packages/openinula/src/common/src/resolve-props.ts
new file mode 100644
index 000000000..408de719b
--- /dev/null
+++ b/packages/openinula/src/common/src/resolve-props.ts
@@ -0,0 +1,32 @@
+// todo: 一个方法去拿到 props 身上的事件,以 on 为前缀
+const openinulaEventPrefix = /^on[A-Z]/
+export function getEventByopeninulaProps(props) {
+ const $listeners = {}
+ Object.keys(props)
+ .filter((propName) => {
+ return openinulaEventPrefix.test(propName) && typeof props[propName] === 'function'
+ })
+ .map((openinulaEvName) => {
+ return {
+ openinulaEvName,
+ vueEvName: openinulaEvName.substr(2).toLowerCase()
+ }
+ })
+ .forEach(({ openinulaEvName, vueEvName }) => {
+ Object.assign($listeners, {
+ [vueEvName]: props[openinulaEvName]
+ })
+ })
+ return $listeners
+}
+export function getAttrsByopeninulaProps(props) {
+ const $attrs = {}
+ Object.keys(props)
+ .filter((propName) => {
+ return !openinulaEventPrefix.test(propName) && !['children'].includes(propName)
+ })
+ .forEach((attr) => {
+ $attrs[attr] = props[attr]
+ })
+ return $attrs
+}
diff --git a/packages/openinula/src/common/src/svg-render.jsx b/packages/openinula/src/common/src/svg-render.jsx
new file mode 100644
index 000000000..2651e53da
--- /dev/null
+++ b/packages/openinula/src/common/src/svg-render.jsx
@@ -0,0 +1,19 @@
+import classNames from 'classnames'
+import { If } from './virtual-comp'
+
+export const Svg = ({ name = 'Icon', component: Icon }) => {
+ const funcObj = {
+ [name](props) {
+ const className = classNames('icon', 'tiny-svg', props.className)
+ const v_if = typeof props['v-if'] === 'boolean' ? props['v-if'] : true
+ const defaultProps = { ...props }
+ delete defaultProps['v-if']
+ return (
+
+
+
+ )
+ }
+ }
+ return funcObj[name]
+}
diff --git a/packages/openinula/src/common/src/utils.ts b/packages/openinula/src/common/src/utils.ts
new file mode 100644
index 000000000..6ec0aa33d
--- /dev/null
+++ b/packages/openinula/src/common/src/utils.ts
@@ -0,0 +1,132 @@
+/**
+ * filterAttrs 属性过滤函数
+ * @param {object} attrs 由父组件传入,且没有被子组件声明为 props 的一些属性
+ * @param {Array} filters 过滤数组,元素可以为字符串,也可以为正则表达式
+ * @param {boolean} include 是否返回为被过滤的属性集合,如果为 false,filters 是过滤不要的属性
+ * @returns {object} 过滤后的属性对象
+ */
+export const filterAttrs = (attrs, filters, include) => {
+ const props = {}
+
+ for (let name in attrs) {
+ const find = filters.some((r) => new RegExp(r).test(name))
+
+ if ((include && find) || (!include && !find)) {
+ props[name] = attrs[name]
+ }
+ }
+
+ return props
+}
+
+/**
+ * event bus
+ * $bus.on
+ * $bus.off
+ * $bus.emit
+ */
+
+export const eventBus = () => {
+ const $bus = {}
+
+ const on = (eventName, callback) => {
+ if (!$bus[eventName]) {
+ $bus[eventName] = []
+ }
+
+ $bus[eventName].push(callback)
+ }
+
+ const off = (eventName, callback) => {
+ if (!$bus[eventName]) {
+ return
+ }
+
+ $bus[eventName] = $bus[eventName].filter((subscriber) => subscriber !== callback)
+ }
+
+ const emit = (eventName, ...args) => {
+ if (!$bus[eventName]) {
+ return
+ }
+
+ $bus[eventName].forEach((subscriber) => subscriber(...args))
+ }
+
+ const once = (eventName, callback) => {
+ const onceCallBack = (...args) => {
+ callback(...args)
+ off(eventName, onceCallBack)
+ }
+ on(eventName, onceCallBack)
+ }
+
+ return {
+ on,
+ emit,
+ off,
+ once
+ }
+}
+
+/**
+ * 实现 vue 中 :class 的用法
+ */
+
+export function VueClassName(className) {
+ if (typeof className === 'string') {
+ return className
+ } else if (Array.isArray(className)) {
+ return className.reduce((pre, cur, index) => {
+ if (typeof cur === 'string') {
+ return `${pre}${index === 0 ? '' : ' '}${cur}`
+ } else {
+ return `${pre}${index === 0 ? '' : ' '}${VueClassName(cur)}`
+ }
+ }, '')
+ } else if (typeof className === 'object') {
+ return Object.keys(className).reduce((pre, key, index) => {
+ if (className[key]) {
+ return `${pre}${index === 0 ? '' : ' '}${key}`
+ } else {
+ return pre
+ }
+ }, '')
+ }
+}
+
+export const vc = VueClassName
+
+export const getElementCssClass = (classes = {}, key) => {
+ if (typeof key === 'object') {
+ const keys = Object.keys(key)
+ let cls = ''
+ keys.forEach((k) => {
+ if (key[k] && classes[k]) cls += `${classes[k]} `
+ })
+ return cls
+ } else {
+ return classes[key] || ''
+ }
+}
+
+export function getPropByPath(obj, path) {
+ let tempObj = obj
+ // 将a[b].c转换为a.b.c
+ path = path.replace(/\[(\w+)\]/g, '.$1')
+ // 将.a.b转换为a.b
+ path = path.replace(/^\./, '')
+
+ let keyArr = path.split('.')
+ let len = keyArr.length
+
+ for (let i = 0; i < len - 1; i++) {
+ let key = keyArr[i]
+ if (key in tempObj) {
+ tempObj = tempObj[key]
+ } else {
+ return
+ }
+ }
+ return tempObj[keyArr[keyArr.length - 1]]
+}
diff --git a/packages/openinula/src/common/src/virtual-comp.jsx b/packages/openinula/src/common/src/virtual-comp.jsx
new file mode 100644
index 000000000..24beb99b2
--- /dev/null
+++ b/packages/openinula/src/common/src/virtual-comp.jsx
@@ -0,0 +1,61 @@
+export function If(props) {
+ if (props['v-if']) {
+ return props.children
+ } else {
+ return ''
+ }
+}
+
+function defaultVIfAsTrue(props) {
+ if (typeof props === 'object' && Object.hasOwnProperty.call(props, 'v-if')) {
+ return props['v-if']
+ } else {
+ return true
+ }
+}
+
+export function Component(props) {
+ const Is = props.is || (() => '')
+ return (
+
+
+
+ )
+}
+
+export function Slot(props) {
+ const { name = 'default', slots = {}, parent_children } = props
+
+ const EmptySlot = () => ''
+
+ const S = slots[name] || EmptySlot
+
+ return (
+
+ {parent_children || props.children}
+
+
+
+
+ {props.children}
+
+
+ )
+}
+
+export function For(props) {
+ const { item: Item, list = [] } = props
+
+ const listItems = list.map((item, index, list) => {
+ return
+ })
+
+ return {listItems}
+}
+
+export function Transition(props) {
+ // todo: improve tarnsiton comp
+ return {props.children}
+}
+
+export const compWhiteList = ['If', 'Component', 'Slot', 'For', 'Transition']
diff --git a/packages/openinula/src/common/src/vm.ts b/packages/openinula/src/common/src/vm.ts
new file mode 100644
index 000000000..038b79b7b
--- /dev/null
+++ b/packages/openinula/src/common/src/vm.ts
@@ -0,0 +1,106 @@
+import { useFiber, getParentFiber, creatFiberCombine } from './fiber.js'
+import { getEventByopeninulaProps, getAttrsByopeninulaProps } from './resolve-props.js'
+import { Reactive } from './reactive'
+import { emit, on, off, once } from './event'
+
+const vmProxy = {
+ $parent: ({ fiber }) => {
+ const parentFiber = getParentFiber(fiber)
+ if (!parentFiber) return null
+ return createVmProxy(creatFiberCombine(parentFiber))
+ },
+ $el: ({ fiber }) => fiber.child?.stateNode,
+ $refs: ({ refs, fiber }) => createRefsProxy(refs, fiber.constructor),
+ $children: ({ children }) => children.map((fiber) => createVmProxy(creatFiberCombine(getParentFiber(fiber)))),
+ $listeners: ({ fiber }) => getEventByopeninulaProps(fiber.memoizedProps),
+ $attrs: ({ fiber }) => getAttrsByopeninulaProps(fiber.memoizedProps),
+ $slots: ({ fiber }) => fiber.memoizedProps.slots,
+ $scopedSlots: ({ fiber }) => fiber.memoizedProps.slots,
+ $options: ({ fiber }) => ({ componentName: fiber.type.name }),
+ $constants: ({ fiber }) => fiber.memoizedProps._constants,
+ $template: ({ fiber }) => fiber.memoizedProps.tiny_template,
+ $renderless: ({ fiber }) => fiber.memoizedProps.tiny_renderless,
+ $mode: () => 'pc',
+ state: ({ fiber }) => findStateInHooks(fiber.memoizedState),
+ $type: ({ fiber }) => fiber.type,
+ $service: (_, vm) => vm.state?.$service,
+ $emit: ({ fiber }) => emit(fiber.memoizedProps),
+ $on: ({ fiber }) => on(fiber.memoizedProps),
+ $once: ({ fiber }) => once(fiber.memoizedProps),
+ $off: ({ fiber }) => off(fiber.memoizedProps),
+ $set: () => (target, propName, value) => (target[propName] = value)
+}
+
+const vmProxyMap = new WeakMap()
+function createVmProxy(fiberCombine) {
+ if (!vmProxyMap.has(fiberCombine)) {
+ vmProxyMap.set(
+ fiberCombine,
+ new Proxy(fiberCombine, {
+ get(target, property, receiver) {
+ if (!vmProxy[property]) {
+ return target.fiber.memoizedProps[property]
+ }
+ return vmProxy[property](target, receiver)
+ },
+ set() {
+ return true
+ }
+ })
+ )
+ }
+ return vmProxyMap.get(fiberCombine)
+}
+
+function createEmptyProxy() {
+ return new Proxy(
+ {},
+ {
+ get() {
+ return undefined
+ },
+ set() {
+ return true
+ }
+ }
+ )
+}
+
+function createRefsProxy(refs, FiberNode) {
+ return new Proxy(refs, {
+ get(target, property) {
+ if (target[property] instanceof FiberNode) {
+ return createVmProxy(creatFiberCombine(target[property]))
+ } else {
+ return target[property]
+ }
+ }
+ })
+}
+
+function findStateInHooks(hookStart) {
+ let curHook = hookStart
+ // find state from hooks chain by Constructor reactive
+ while (curHook) {
+ const refCurrent = curHook.memoizedState && curHook.memoizedState.current
+ if (refCurrent instanceof Reactive) break
+ curHook = curHook.next
+ }
+ return curHook && curHook.memoizedState && curHook.memoizedState.current
+}
+
+export function useVm() {
+ const { ref, current, parent } = useFiber()
+ if (!ref.current) {
+ return {
+ ref,
+ current: createEmptyProxy(),
+ parent: createEmptyProxy()
+ }
+ }
+ return {
+ ref,
+ current: current.fiber && createVmProxy(current),
+ parent: parent.fiber && createVmProxy(parent)
+ }
+}
diff --git a/packages/openinula/src/common/src/vue-hooks.ts b/packages/openinula/src/common/src/vue-hooks.ts
new file mode 100644
index 000000000..e64e07912
--- /dev/null
+++ b/packages/openinula/src/common/src/vue-hooks.ts
@@ -0,0 +1,159 @@
+import {
+ // 响应式:核心
+ ref,
+ computed,
+ reactive,
+ readonly,
+ watch,
+ watchEffect,
+ watchPostEffect,
+ watchSyncEffect,
+ // 响应式:工具
+ isRef,
+ unref,
+ toRef,
+ toValue,
+ toRefs,
+ isProxy,
+ isReactive,
+ isReadonly,
+ // 响应式:进阶
+ shallowRef,
+ triggerRef,
+ customRef,
+ shallowReactive,
+ shallowReadonly,
+ toRaw,
+ markRaw,
+ effectScope,
+ getCurrentScope,
+ onScopeDispose,
+ // 通用
+ nextTick
+} from '@vue/runtime-core'
+import { useExcuteOnce } from './hooks'
+import { useEffect } from 'openinula'
+
+// 通用
+const inject = () => {}
+const provide = () => {}
+
+export function generateVueHooks({ $bus }) {
+ const reload = () => $bus.emit('event:reload')
+
+ function toPageLoad(reactiveHook, reload) {
+ return function (...args) {
+ const result = reactiveHook(...args)
+ nextTick(() => {
+ watch(
+ result,
+ () => {
+ typeof reload === 'function' && reload()
+ },
+ {
+ flush: 'sync'
+ }
+ )
+ })
+ return result
+ }
+ }
+
+ return {
+ // 响应式:核心
+ ref: toPageLoad(ref, reload),
+ computed: toPageLoad(computed, reload),
+ reactive: toPageLoad(reactive, reload),
+ readonly,
+ watchEffect,
+ watchPostEffect,
+ watchSyncEffect,
+ watch,
+ // 响应式:工具
+ isRef,
+ unref,
+ toRef: toPageLoad(toRef, reload),
+ toValue,
+ toRefs,
+ isProxy,
+ isReactive,
+ isReadonly,
+ // 响应式:进阶
+ shallowRef: toPageLoad(shallowRef, reload),
+ triggerRef,
+ customRef: toPageLoad(customRef, reload),
+ shallowReactive: toPageLoad(shallowReactive, reload),
+ shallowReadonly,
+ toRaw,
+ markRaw,
+ effectScope,
+ getCurrentScope,
+ onScopeDispose,
+ // 依赖注入
+ inject,
+ provide,
+ // 生命周期函数
+ onBeforeUnmount() {
+ $bus.on('hook:onBeforeUnmount')
+ },
+ onMounted() {
+ $bus.on('hook:onMounted')
+ },
+ onUpdated() {
+ $bus.on('hook:onUpdated')
+ },
+ onUnmounted() {
+ $bus.on('hook:onUnmounted')
+ },
+ onBeforeMount() {
+ $bus.on('hook:onBeforeMount')
+ },
+ onBeforeUpdate() {
+ $bus.on('hook:onBeforeUpdate')
+ },
+ onErrorCaptured() {
+ $bus.on('hook:onErrorCaptured')
+ },
+ onRenderTracked() {
+ $bus.on('hook:onRenderTracked')
+ },
+ onRenderTriggered() {
+ $bus.on('hook:onRenderTriggered')
+ },
+ onActivated() {
+ $bus.on('hook:onActivated')
+ },
+ onDeactivated() {
+ $bus.on('hook:onDeactivated')
+ },
+ onServerPrefetch() {
+ $bus.on('hook:onServerPrefetch')
+ }
+ }
+}
+
+// 在这里出发生命周期钩子
+export function useVueLifeHooks($bus) {
+ $bus.emit('hook:onBeforeUpdate')
+ nextTick(() => {
+ $bus.emit('hook:onUpdated')
+ })
+
+ useExcuteOnce(() => {
+ $bus.emit('hook:onBeforeMount')
+ })
+
+ useEffect(() => {
+ $bus.emit('hook:onMounted')
+
+ return () => {
+ // 卸载
+ $bus.emit('hook:onBeforeUnmount')
+ nextTick(() => {
+ $bus.emit('hook:onUnmounted')
+ })
+ }
+ }, [])
+}
+
+export * from '@vue/runtime-core'
diff --git a/packages/openinula/src/common/src/vue-instance.ts b/packages/openinula/src/common/src/vue-instance.ts
new file mode 100644
index 000000000..db7d093e4
--- /dev/null
+++ b/packages/openinula/src/common/src/vue-instance.ts
@@ -0,0 +1,88 @@
+import { useRef } from 'openinula'
+import { useExcuteOnce, useOnceResult } from './hooks'
+import { reactive, nextTick, watch, computed } from '@vue/runtime-core'
+import { getPropByPath } from './utils'
+import { getParent, getRoot } from './render-stack'
+import { getFiberByDom, traverseFiber } from './fiber'
+
+const collectRefs = (rootEl, $children) => {
+ const refs = {}
+ if (!rootEl) return refs
+ 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
+ }
+ })
+ // 收集组件元素 ref
+ $children.forEach((child) => {
+ if (child.$props['v-ref']) {
+ refs[child.$props['v-ref']] = child
+ }
+ })
+ return refs
+}
+
+export function useCreateVueInstance({ $bus, props }) {
+ const ref = useRef()
+ const vm = useOnceResult(() =>
+ reactive({
+ $el: undefined,
+ $options: props.$options || {},
+ $props: props,
+ $parent: getParent().vm || {},
+ $root: getRoot().vm || {},
+ $slots: props.slots,
+ $scopedSlots: props.slots,
+ $listeners: props.$listeners,
+ $attrs: props.$attrs,
+ // 通过 fiber 计算
+ $children: [],
+ $refs: computed(() => collectRefs(vm.$el, vm.$children)),
+ // 方法
+ $set: (target, property, value) => (target[property] = value),
+ $delete: (target, property) => delete target[property],
+ $watch: (expression, callback, options) => {
+ if (typeof expression === 'string') {
+ watch(() => getPropByPath(vm, expression), callback, options)
+ } else if (typeof expression === 'function') {
+ watch(expression, callback, options)
+ }
+ },
+ $on: (event, callback) => $bus.on(event, callback),
+ $once: (event, callback) => $bus.once(event, callback),
+ $off: (event, callback) => $bus.off(event, callback),
+ $emit: (event, ...args) => $bus.emit(event, ...args),
+ $forceUpdate: () => $bus.emit('event:reload'),
+ $nextTick: nextTick,
+ $destroy: () => {},
+ $mount: () => {}
+ })
+ )
+
+ useExcuteOnce(() => {
+ const { $listeners } = props
+
+ if ($listeners) {
+ Object.keys($listeners).forEach((eventName) => {
+ $bus.on(eventName, $listeners[eventName])
+ })
+ }
+
+ // 给父的 $children 里 push 当前的 vm
+ const parent = vm.$parent
+ if (Array.isArray(parent.$children)) {
+ parent.$children.push(vm)
+ }
+
+ nextTick(() => {
+ vm.$el = ref.current
+ })
+ })
+
+ return {
+ ref,
+ vm
+ }
+}
diff --git a/packages/openinula/src/common/src/vue-props.ts b/packages/openinula/src/common/src/vue-props.ts
new file mode 100644
index 000000000..622540576
--- /dev/null
+++ b/packages/openinula/src/common/src/vue-props.ts
@@ -0,0 +1,35 @@
+export function defineVueProps(propsOptions, props) {
+ const $props = {}
+ const $attrs = {}
+ const $listeners = {}
+ const openinulaEventPrefix = /^on[A-Z]/
+
+ const propsArray = Array.isArray(propsOptions) ? propsOptions : Object.keys(propsOptions)
+ Object.keys(props).forEach((key) => {
+ if (propsArray.includes(key)) {
+ $props[key] = props[key]
+ } else {
+ if (openinulaEventPrefix.test(key)) {
+ $listeners[key.substr(2).toLowerCase()] = props[key]
+ } else {
+ $attrs[key] = props[key]
+ }
+ }
+ })
+
+ if (typeof propsOptions === 'object') {
+ Object.keys(propsOptions)
+ .filter((key) => !$props[key])
+ .forEach((key) => {
+ const options = propsOptions[key]
+ const defaultValue = typeof options.default === 'function' ? options.default() : options.default
+ defaultValue !== undefined && ($props[key] = defaultValue)
+ })
+ }
+
+ return {
+ $props,
+ $attrs,
+ $listeners
+ }
+}
diff --git a/packages/openinula/src/icon/index.ts b/packages/openinula/src/icon/index.ts
new file mode 100644
index 000000000..a31cf4e73
--- /dev/null
+++ b/packages/openinula/src/icon/index.ts
@@ -0,0 +1,39 @@
+import IconLoading from './src/loading'
+import IconAdd from './src/add'
+import IconCheck from './src/check'
+import IconCheckedSur from './src/check'
+import IconChevronDown from './src/chevron-down'
+import IconClose from './src/close'
+import IconError from './src/error'
+import IconHalfSelect from './src/half-select'
+import IconHelp from './src/help'
+import IconSuccess from './src/success'
+import IconWarning from './src/warning'
+
+export {
+ IconLoading,
+ IconAdd,
+ IconCheck,
+ IconCheckedSur,
+ IconChevronDown,
+ IconClose,
+ IconError,
+ IconHalfSelect,
+ IconHelp,
+ IconSuccess,
+ IconWarning
+}
+
+export default {
+ IconLoading,
+ IconAdd,
+ IconCheck,
+ IconCheckedSur,
+ IconChevronDown,
+ IconClose,
+ IconError,
+ IconHalfSelect,
+ IconHelp,
+ IconSuccess,
+ IconWarning
+}
diff --git a/packages/openinula/src/icon/package.json b/packages/openinula/src/icon/package.json
new file mode 100644
index 000000000..5e997f79f
--- /dev/null
+++ b/packages/openinula/src/icon/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@opentiny/openinula-icon",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.ts",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@opentiny/openinula-common": "workspace:~",
+ "@opentiny/vue-theme": "workspace:~"
+ }
+}
diff --git a/packages/openinula/src/icon/src/add/index.ts b/packages/openinula/src/icon/src/add/index.ts
new file mode 100644
index 000000000..5f490a265
--- /dev/null
+++ b/packages/openinula/src/icon/src/add/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as AddLoading } from '@opentiny/vue-theme/svgs/add.svg'
+
+export default Svg({ name: 'AddLoading', component: AddLoading })
diff --git a/packages/openinula/src/icon/src/check/index.ts b/packages/openinula/src/icon/src/check/index.ts
new file mode 100644
index 000000000..9c761cee0
--- /dev/null
+++ b/packages/openinula/src/icon/src/check/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Check } from '@opentiny/vue-theme/svgs/check.svg'
+
+export default Svg({ name: 'Check', component: Check })
diff --git a/packages/openinula/src/icon/src/checked-sur/index.ts b/packages/openinula/src/icon/src/checked-sur/index.ts
new file mode 100644
index 000000000..1712224d4
--- /dev/null
+++ b/packages/openinula/src/icon/src/checked-sur/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as CheckedSur } from '@opentiny/vue-theme/svgs/checked-sur.svg'
+
+export default Svg({ name: 'CheckedSur', component: CheckedSur })
diff --git a/packages/openinula/src/icon/src/chevron-down/index.ts b/packages/openinula/src/icon/src/chevron-down/index.ts
new file mode 100644
index 000000000..d823acaa7
--- /dev/null
+++ b/packages/openinula/src/icon/src/chevron-down/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as ChevronDown } from '@opentiny/vue-theme/svgs/chevron-down.svg'
+
+export default Svg({ name: 'ChevronDown', component: ChevronDown })
diff --git a/packages/openinula/src/icon/src/close/index.ts b/packages/openinula/src/icon/src/close/index.ts
new file mode 100644
index 000000000..8eead40b2
--- /dev/null
+++ b/packages/openinula/src/icon/src/close/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Close } from '@opentiny/vue-theme/svgs/close.svg'
+
+export default Svg({ name: 'Close', component: Close })
diff --git a/packages/openinula/src/icon/src/error/index.ts b/packages/openinula/src/icon/src/error/index.ts
new file mode 100644
index 000000000..4fabd547c
--- /dev/null
+++ b/packages/openinula/src/icon/src/error/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Error } from '@opentiny/vue-theme/svgs/error.svg'
+
+export default Svg({ name: 'Error', component: Error })
diff --git a/packages/openinula/src/icon/src/half-select/index.ts b/packages/openinula/src/icon/src/half-select/index.ts
new file mode 100644
index 000000000..5c1db223a
--- /dev/null
+++ b/packages/openinula/src/icon/src/half-select/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as HalfSelect } from '@opentiny/vue-theme/svgs/halfselect.svg'
+
+export default Svg({ name: 'HalfSelect', component: HalfSelect })
diff --git a/packages/openinula/src/icon/src/help/index.ts b/packages/openinula/src/icon/src/help/index.ts
new file mode 100644
index 000000000..aff9a89ca
--- /dev/null
+++ b/packages/openinula/src/icon/src/help/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Help } from '@opentiny/vue-theme/svgs/help.svg'
+
+export default Svg({ name: 'Help', component: Help })
diff --git a/packages/openinula/src/icon/src/loading/index.ts b/packages/openinula/src/icon/src/loading/index.ts
new file mode 100644
index 000000000..bd3680982
--- /dev/null
+++ b/packages/openinula/src/icon/src/loading/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as IconLoading } from '@opentiny/vue-theme/svgs/loading.svg'
+
+export default Svg({ name: 'IconLoading', component: IconLoading })
diff --git a/packages/openinula/src/icon/src/success/index.ts b/packages/openinula/src/icon/src/success/index.ts
new file mode 100644
index 000000000..6fb4bfbd6
--- /dev/null
+++ b/packages/openinula/src/icon/src/success/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Success } from '@opentiny/vue-theme/svgs/success.svg'
+
+export default Svg({ name: 'Success', component: Success })
diff --git a/packages/openinula/src/icon/src/warning/index.ts b/packages/openinula/src/icon/src/warning/index.ts
new file mode 100644
index 000000000..8b2a126ed
--- /dev/null
+++ b/packages/openinula/src/icon/src/warning/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - 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 { Svg } from '@opentiny/openinula-common'
+import { ReactComponent as Warning } from '@opentiny/vue-theme/svgs/warning.svg'
+
+export default Svg({ name: 'Warning', component: Warning })