From 89037170c08f5910009d198b348f515ce0c70e5e Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Mon, 11 Dec 2023 18:58:51 +0800 Subject: [PATCH] feat(openinula): add openinula base structure (#1093) * feat(openinula): add openinula base structure * feat(openinula): add openinula base structure --- examples/openinula-docs/index.html | 13 ++ examples/openinula-docs/package.json | 29 +++ examples/openinula-docs/public/vite.svg | 1 + examples/openinula-docs/src/App.tsx | 12 ++ examples/openinula-docs/src/main.css | 4 + examples/openinula-docs/src/main.tsx | 5 + examples/openinula-docs/tsconfig.json | 25 +++ examples/openinula-docs/tsconfig.node.json | 10 + examples/openinula-docs/vite.config.ts | 16 ++ package.json | 7 +- packages/openinula/.depcheckrc.yaml | 2 + packages/openinula/index.ts | 9 + packages/openinula/package.json | 16 ++ packages/openinula/src/alert/index.ts | 3 + packages/openinula/src/alert/package.json | 19 ++ packages/openinula/src/alert/src/index.ts | 15 ++ .../openinula/src/alert/src/mobile-first.jsx | 185 ++++++++++++++++++ packages/openinula/src/alert/src/mobile.jsx | 81 ++++++++ packages/openinula/src/alert/src/pc.jsx | 103 ++++++++++ packages/openinula/src/common/package.json | 20 ++ packages/openinula/src/common/src/csscls.ts | 65 ++++++ packages/openinula/src/common/src/event.ts | 90 +++++++++ packages/openinula/src/common/src/fiber.ts | 87 ++++++++ packages/openinula/src/common/src/hooks.ts | 24 +++ packages/openinula/src/common/src/index.ts | 149 ++++++++++++++ packages/openinula/src/common/src/reactive.ts | 94 +++++++++ .../openinula/src/common/src/render-stack.ts | 15 ++ .../openinula/src/common/src/resolve-props.ts | 32 +++ .../openinula/src/common/src/svg-render.jsx | 19 ++ packages/openinula/src/common/src/utils.ts | 132 +++++++++++++ .../openinula/src/common/src/virtual-comp.jsx | 61 ++++++ packages/openinula/src/common/src/vm.ts | 106 ++++++++++ .../openinula/src/common/src/vue-hooks.ts | 159 +++++++++++++++ .../openinula/src/common/src/vue-instance.ts | 88 +++++++++ .../openinula/src/common/src/vue-props.ts | 35 ++++ packages/openinula/src/icon/index.ts | 39 ++++ packages/openinula/src/icon/package.json | 16 ++ packages/openinula/src/icon/src/add/index.ts | 15 ++ .../openinula/src/icon/src/check/index.ts | 15 ++ .../src/icon/src/checked-sur/index.ts | 15 ++ .../src/icon/src/chevron-down/index.ts | 15 ++ .../openinula/src/icon/src/close/index.ts | 15 ++ .../openinula/src/icon/src/error/index.ts | 15 ++ .../src/icon/src/half-select/index.ts | 15 ++ packages/openinula/src/icon/src/help/index.ts | 15 ++ .../openinula/src/icon/src/loading/index.ts | 15 ++ .../openinula/src/icon/src/success/index.ts | 15 ++ .../openinula/src/icon/src/warning/index.ts | 15 ++ 48 files changed, 1949 insertions(+), 2 deletions(-) create mode 100644 examples/openinula-docs/index.html create mode 100644 examples/openinula-docs/package.json create mode 100644 examples/openinula-docs/public/vite.svg create mode 100644 examples/openinula-docs/src/App.tsx create mode 100644 examples/openinula-docs/src/main.css create mode 100644 examples/openinula-docs/src/main.tsx create mode 100644 examples/openinula-docs/tsconfig.json create mode 100644 examples/openinula-docs/tsconfig.node.json create mode 100644 examples/openinula-docs/vite.config.ts create mode 100644 packages/openinula/.depcheckrc.yaml create mode 100644 packages/openinula/index.ts create mode 100644 packages/openinula/package.json create mode 100644 packages/openinula/src/alert/index.ts create mode 100644 packages/openinula/src/alert/package.json create mode 100644 packages/openinula/src/alert/src/index.ts create mode 100644 packages/openinula/src/alert/src/mobile-first.jsx create mode 100644 packages/openinula/src/alert/src/mobile.jsx create mode 100644 packages/openinula/src/alert/src/pc.jsx create mode 100644 packages/openinula/src/common/package.json create mode 100644 packages/openinula/src/common/src/csscls.ts create mode 100644 packages/openinula/src/common/src/event.ts create mode 100644 packages/openinula/src/common/src/fiber.ts create mode 100644 packages/openinula/src/common/src/hooks.ts create mode 100644 packages/openinula/src/common/src/index.ts create mode 100644 packages/openinula/src/common/src/reactive.ts create mode 100644 packages/openinula/src/common/src/render-stack.ts create mode 100644 packages/openinula/src/common/src/resolve-props.ts create mode 100644 packages/openinula/src/common/src/svg-render.jsx create mode 100644 packages/openinula/src/common/src/utils.ts create mode 100644 packages/openinula/src/common/src/virtual-comp.jsx create mode 100644 packages/openinula/src/common/src/vm.ts create mode 100644 packages/openinula/src/common/src/vue-hooks.ts create mode 100644 packages/openinula/src/common/src/vue-instance.ts create mode 100644 packages/openinula/src/common/src/vue-props.ts create mode 100644 packages/openinula/src/icon/index.ts create mode 100644 packages/openinula/src/icon/package.json create mode 100644 packages/openinula/src/icon/src/add/index.ts create mode 100644 packages/openinula/src/icon/src/check/index.ts create mode 100644 packages/openinula/src/icon/src/checked-sur/index.ts create mode 100644 packages/openinula/src/icon/src/chevron-down/index.ts create mode 100644 packages/openinula/src/icon/src/close/index.ts create mode 100644 packages/openinula/src/icon/src/error/index.ts create mode 100644 packages/openinula/src/icon/src/half-select/index.ts create mode 100644 packages/openinula/src/icon/src/help/index.ts create mode 100644 packages/openinula/src/icon/src/loading/index.ts create mode 100644 packages/openinula/src/icon/src/success/index.ts create mode 100644 packages/openinula/src/icon/src/warning/index.ts 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 })