├── vite.config.ts ├── examples ├── demo-with-fes │ ├── src │ │ ├── models │ │ │ └── user.js │ │ ├── common │ │ │ ├── utils.js │ │ │ └── service.js │ │ ├── .fes │ │ │ ├── plugin-layout │ │ │ │ ├── icons.js │ │ │ │ ├── index.js │ │ │ │ ├── assets │ │ │ │ │ ├── 403.png │ │ │ │ │ ├── 404.png │ │ │ │ │ └── logo.png │ │ │ │ ├── helpers │ │ │ │ │ ├── utils.js │ │ │ │ │ ├── getConfig.js │ │ │ │ │ ├── pluginLocale.js │ │ │ │ │ ├── pluginAccess.js │ │ │ │ │ ├── svg.js │ │ │ │ │ └── fillMenu.js │ │ │ │ ├── views │ │ │ │ │ ├── 403.vue │ │ │ │ │ ├── 404.vue │ │ │ │ │ ├── components │ │ │ │ │ │ └── Wrapper.vue │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── MenuIcon.vue │ │ │ │ │ └── page.vue │ │ │ │ ├── useTitle.js │ │ │ │ └── runtime.js │ │ │ ├── plugin-model │ │ │ │ ├── models │ │ │ │ │ └── initialState.js │ │ │ │ └── core.js │ │ │ ├── core │ │ │ │ ├── routes │ │ │ │ │ ├── runtime.js │ │ │ │ │ └── routes.js │ │ │ │ ├── pluginExports.js │ │ │ │ ├── plugin.js │ │ │ │ ├── coreExports.js │ │ │ │ └── pluginRegister.js │ │ │ ├── initialState.js │ │ │ ├── plugin-access │ │ │ │ ├── createComponent.js │ │ │ │ ├── createDirective.js │ │ │ │ └── runtime.js │ │ │ ├── defaultContainer.jsx │ │ │ ├── configType.d.ts │ │ │ ├── plugin-request │ │ │ │ ├── paramsProcess.js │ │ │ │ ├── genRequestKey.js │ │ │ │ ├── scheduler.js │ │ │ │ ├── preventRepeatReq.js │ │ │ │ └── helpers.js │ │ │ └── fes.js │ │ ├── images │ │ │ └── icon.png │ │ ├── global.less │ │ ├── components │ │ │ ├── userCenter.vue │ │ │ └── pageLoading.vue │ │ └── app.jsx │ ├── README.md │ ├── public │ │ ├── error.json │ │ ├── success.json │ │ ├── logo.png │ │ └── user.json │ ├── .prettierrc.js │ ├── .fes.prod.js │ ├── .eslintrc.js │ ├── .editorconfig │ ├── index.html │ ├── .fes.js │ ├── package.json │ └── tsconfig.json └── demo-with-vue │ ├── src │ ├── assets │ │ ├── main.css │ │ ├── logo.svg │ │ └── base.css │ ├── views │ │ └── AboutView.vue │ ├── App.vue │ ├── components │ │ ├── icons │ │ │ ├── IconSupport.vue │ │ │ ├── IconTooling.vue │ │ │ ├── IconCommunity.vue │ │ │ ├── IconDocumentation.vue │ │ │ └── IconEcosystem.vue │ │ ├── HelloWorld.vue │ │ └── WelcomeItem.vue │ ├── router │ │ └── index.ts │ └── main.ts │ ├── env.d.ts │ ├── public │ ├── error.json │ ├── success.json │ ├── favicon.ico │ └── user.json │ ├── .vscode │ └── extensions.json │ ├── .prettierrc.json │ ├── tsconfig.json │ ├── tsconfig.app.json │ ├── tsconfig.node.json │ ├── index.html │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── vite.config.ts │ ├── package.json │ └── README.md ├── docs ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── package.json │ │ │ ├── vue.js.map │ │ │ ├── chunk-DFKQJ226.js.map │ │ │ ├── mitt.js │ │ │ ├── _metadata.json │ │ │ └── chunk-DFKQJ226.js │ ├── styles │ │ └── index.less │ └── theme │ │ ├── index.js │ │ └── components │ │ ├── DemoDoc.vue │ │ └── ExampleDoc.vue ├── public │ ├── success.json │ ├── error.json │ ├── design.png │ ├── logo.png │ ├── page.png │ ├── KoalaForm.png │ ├── delete-form.png │ ├── insert-form.png │ ├── update-form.png │ ├── view-form.png │ └── user.json ├── desigin │ ├── scene.png │ └── plugins.png ├── examples │ ├── demos │ │ ├── index.js │ │ ├── editTable.vue │ │ └── login.vue │ ├── const.js │ ├── Test.vue │ ├── started │ │ ├── useScene.js │ │ ├── useScene.vue │ │ ├── usePager.js │ │ ├── useModal.jsx │ │ ├── useTable.js │ │ └── useForm.js │ ├── main.js │ ├── base │ │ ├── slotRender.vue │ │ ├── comp.js │ │ └── form.js │ ├── compose │ │ ├── useTableWithPager.js │ │ └── useTableWithPager2.js │ ├── useForm │ │ ├── query.js │ │ ├── edit.js │ │ ├── relation.js │ │ └── validate.js │ ├── labelPlugin.js │ └── index.js ├── zh │ ├── demos │ │ ├── login.md │ │ ├── editTable.md │ │ └── index.md │ ├── guide │ │ ├── plugin │ │ │ ├── hooks.md │ │ │ ├── design.md │ │ │ ├── api.md │ │ │ └── how.md │ │ ├── scene │ │ │ ├── usePager.md │ │ │ ├── useModal.md │ │ │ ├── useScene.md │ │ │ ├── useTable.md │ │ │ └── useForm.md │ │ ├── base │ │ │ ├── event.md │ │ │ ├── slots.md │ │ │ ├── form.md │ │ │ └── plugin.md │ │ ├── index.md │ │ └── upgrade.md │ └── preset │ │ └── index.md └── index.md ├── pnpm-workspace.yaml ├── .gitignore ├── .prettierrc.js ├── packages ├── core │ ├── .prettierrc.js │ ├── src │ │ ├── plugins │ │ │ ├── index.ts │ │ │ ├── eventsPlugin.ts │ │ │ ├── vModelsPlugin.ts │ │ │ ├── vIfPlugin.ts │ │ │ ├── vShowPlugin.ts │ │ │ ├── disabledPlugin.ts │ │ │ ├── formRule.ts │ │ │ └── slotPlugin.ts │ │ ├── index.ts │ │ ├── koalaRender.tsx │ │ ├── when │ │ │ └── index.ts │ │ ├── handles │ │ │ └── index.ts │ │ ├── useTable │ │ │ └── index.ts │ │ └── useModal │ │ │ └── index.ts │ ├── .eslintrc.js │ ├── tsconfig.json │ └── package.json ├── antd-plugin │ ├── .prettierrc.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ └── readme.md ├── fes-plugin │ ├── .prettierrc.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── readme.md └── element-plugin │ ├── .prettierrc.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ ├── src │ └── slots.ts │ └── readme.md ├── cypress ├── fixtures │ └── example.json ├── e2e │ ├── spec.cy.ts │ └── base │ │ └── scene.cy.ts ├── support │ ├── component-index.html │ ├── e2e.ts │ ├── commands.ts │ └── component.ts └── component │ ├── usePager.cy.tsx │ ├── useModal.cy.tsx │ ├── useTable.cy.tsx │ ├── Utils.cy.tsx │ └── useCurd.cy.tsx ├── tsconfig.json ├── .eslintrc.js ├── lerna.json ├── cypress.config.ts ├── LICENSE └── package.json /vite.config.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/models/user.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/demo-with-fes/README.md: -------------------------------------------------------------------------------- 1 | # fes 模版 2 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/common/utils.js: -------------------------------------------------------------------------------- 1 | // 放工具函数 2 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/common/service.js: -------------------------------------------------------------------------------- 1 | // 服务端接口管理 2 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; -------------------------------------------------------------------------------- /examples/demo-with-vue/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "message": "操作成功" 4 | } -------------------------------------------------------------------------------- /docs/public/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 100000, 3 | "message": "系统错误" 4 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/public/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 100000, 3 | "message": "系统错误" 4 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/public/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "message": "操作成功" 4 | } -------------------------------------------------------------------------------- /examples/demo-with-vue/public/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 100000, 3 | "message": "系统错误" 4 | } -------------------------------------------------------------------------------- /examples/demo-with-vue/public/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "message": "操作成功" 4 | } -------------------------------------------------------------------------------- /docs/desigin/scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/desigin/scene.png -------------------------------------------------------------------------------- /docs/public/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/design.png -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/page.png -------------------------------------------------------------------------------- /docs/desigin/plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/desigin/plugins.png -------------------------------------------------------------------------------- /docs/public/KoalaForm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/KoalaForm.png -------------------------------------------------------------------------------- /docs/public/delete-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/delete-form.png -------------------------------------------------------------------------------- /docs/public/insert-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/insert-form.png -------------------------------------------------------------------------------- /docs/public/update-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/update-form.png -------------------------------------------------------------------------------- /docs/public/view-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/docs/public/view-form.png -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/icons.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default { 4 | 5 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("@webank/eslint-config-webank/.prettierrc.js"), 3 | }; -------------------------------------------------------------------------------- /examples/demo-with-vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: # 所有在 packages/ 子目录下的 package 2 | - 'packages/**' 3 | # 不包括在 test 文件夹下的 package 4 | - '!**/test/**' 5 | -------------------------------------------------------------------------------- /examples/demo-with-fes/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-fes/public/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/**/dist 3 | .temp 4 | .cache 5 | .DS_Store 6 | dist 7 | yarn-error.log 8 | packages/**/.fes 9 | ai-dist -------------------------------------------------------------------------------- /examples/demo-with-fes/src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-fes/src/images/icon.png -------------------------------------------------------------------------------- /examples/demo-with-vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-vue/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/index.js: -------------------------------------------------------------------------------- 1 | export { default as Page } from './views/page.vue'; 2 | export { useTabTitle } from './useTitle'; 3 | -------------------------------------------------------------------------------- /examples/demo-with-fes/.fes.prod.js: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from '@fesjs/fes'; 2 | 3 | export default defineBuildConfig({ 4 | publicPath: './', 5 | }); 6 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/chunk-DFKQJ226.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/examples/demos/index.js: -------------------------------------------------------------------------------- 1 | import Login from './login.vue'; 2 | import EditTable from './editTable.vue'; 3 | export default { 4 | Login, 5 | EditTable, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/assets/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-fes/src/.fes/plugin-layout/assets/403.png -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/assets/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-fes/src/.fes/plugin-layout/assets/404.png -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankFinTech/KoalaForm/HEAD/examples/demo-with-fes/src/.fes/plugin-layout/assets/logo.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: false, 7 | printWidth: 160 8 | }; -------------------------------------------------------------------------------- /docs/zh/demos/login.md: -------------------------------------------------------------------------------- 1 | # 登录示例 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /packages/core/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: false, 7 | printWidth: 180 8 | }; -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/global.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | } 4 | 5 | .page { 6 | padding: 12px; 7 | margin: 12px; 8 | background: #FFF; 9 | border-radius: 8px; 10 | } -------------------------------------------------------------------------------- /packages/antd-plugin/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: false, 7 | printWidth: 180 8 | }; -------------------------------------------------------------------------------- /packages/fes-plugin/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: false, 7 | printWidth: 180 8 | }; -------------------------------------------------------------------------------- /docs/zh/demos/editTable.md: -------------------------------------------------------------------------------- 1 | # 编辑表格 2 | 3 | 4 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-model/models/initialState.js: -------------------------------------------------------------------------------- 1 | import { initialState } from '@@/initialState'; 2 | 3 | export default function initialStateModel() { 4 | return initialState; 5 | } 6 | -------------------------------------------------------------------------------- /packages/element-plugin/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: false, 7 | printWidth: 180 8 | }; -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe('My First Test', () => { 2 | it('koala form docs', () => { 3 | cy.visit('http://localhost:3000/'); 4 | cy.contains('a', 'Get Started').click(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/demo-with-vue/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /examples/demo-with-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/routes/runtime.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from "./routeExports"; 2 | 3 | export function onAppCreated({ app, routes }) { 4 | const router = createRouter(routes); 5 | app.use(router); 6 | } 7 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/initialState.js: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | 3 | export const initialState = reactive({}); 4 | 5 | export const updateInitialState = (obj) => { 6 | Object.assign(initialState, obj); 7 | }; 8 | -------------------------------------------------------------------------------- /docs/examples/const.js: -------------------------------------------------------------------------------- 1 | // export const BASE_URL = process.env.NODE_ENV === 'production' ? '/s/koala-form/v2' : '/'; 2 | export const BASE_URL = '/'; 3 | 4 | export const LIST_API = BASE_URL + 'user.json'; 5 | export const SUCCESS_API = BASE_URL + 'success.json'; 6 | export const FAIL_API = BASE_URL + 'error.json'; 7 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-access/createComponent.js: -------------------------------------------------------------------------------- 1 | export default function createComponent(useAccess) { 2 | return (props, { slots }) => { 3 | const access = useAccess(props.id); 4 | if (!access.value || !slots.default) return null; 5 | return slots.default(); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/utils.js: -------------------------------------------------------------------------------- 1 | export const flatNodes = (nodes = []) => 2 | nodes.reduce((res, node) => { 3 | res.push(node); 4 | if (node.children) { 5 | res = res.concat(flatNodes(node.children)); 6 | } 7 | return res; 8 | }, []); 9 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/demo-with-fes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-webank/vue.js'], 3 | overrides: [ 4 | { 5 | files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], 6 | }, 7 | ], 8 | env: { 9 | jest: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/defaultContainer.jsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { RouterView } from '/Users/aring/code/koala-form/packages/demo-with-fes/node_modules/.pnpm/@fesjs+runtime@3.0.0_vue@3.3.4/node_modules/@fesjs/runtime'; 3 | 4 | export default defineComponent(() => () => ()); 5 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | // 这样就可以对 `this` 上的数据属性进行更严格的推断 6 | "strict": true, 7 | "allowJs": true, 8 | "jsx": "preserve", 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "Node" 11 | }, 12 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/pluginExports.js: -------------------------------------------------------------------------------- 1 | export { access, useAccess } from '../plugin-access/core.js'; 2 | export { Page, useTabTitle } from '../plugin-layout/index.js'; 3 | export { useModel } from '../plugin-model/core.js'; 4 | export { enums } from '../plugin-enums/core.js'; 5 | export * from '../plugin-request/request.js'; 6 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/configType.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from '@fesjs/preset-built-in'; 3 | export * from '@fesjs/builder-webpack'; 4 | export * from '@fesjs/plugin-access'; 5 | export * from '@fesjs/plugin-layout'; 6 | export * from '@fesjs/plugin-model'; 7 | export * from '@fesjs/plugin-enums'; 8 | export * from '@fesjs/plugin-request'; 9 | -------------------------------------------------------------------------------- /examples/demo-with-vue/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo-with-fes/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | lib 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-request/paramsProcess.js: -------------------------------------------------------------------------------- 1 | import { checkHttpRequestHasBody, trimObj } from './helpers'; 2 | 3 | export default async (ctx, next) => { 4 | const config = ctx.config; 5 | if (checkHttpRequestHasBody(config.method)) { 6 | trimObj(config.data); 7 | } else { 8 | trimObj(config.params); 9 | } 10 | await next(); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/demo-with-vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "types": ["node"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/demo-with-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './define'; 2 | export * from './renderPlugin'; 3 | export * from './vIfPlugin'; 4 | export * from './vShowPlugin'; 5 | export * from './disabledPlugin'; 6 | export * from './eventsPlugin'; 7 | export * from './slotPlugin'; 8 | export * from './formRule'; 9 | export * from './optionsPlugin'; 10 | export * from './formatPlugin'; 11 | export * from './vModelsPlugin'; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-ts/vue'], 3 | globals: { 4 | // 这里填入你的项目需要的全局变量 5 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: 6 | // 7 | // Vue: false 8 | __DEV__: false, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "command": { 4 | "publish": { 5 | "ignoreChanges": [ 6 | "*.md", 7 | "**/test/**" 8 | ], 9 | "message": "chore(release): publish" 10 | } 11 | }, 12 | "packages": [ 13 | "packages/*" 14 | ], 15 | "useWorkspaces": true, 16 | "npmClient": "yarn", 17 | "ignoreChanges": [ 18 | "**/test/**", 19 | "**/*.md" 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/demo-with-vue/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/plugin.js: -------------------------------------------------------------------------------- 1 | import { Plugin } from '/Users/aring/code/koala-form/packages/demo-with-fes/node_modules/.pnpm/@fesjs+runtime@3.0.0_vue@3.3.4/node_modules/@fesjs/runtime'; 2 | 3 | const plugin = new Plugin({ 4 | validKeys: ['modifyClientRenderOpts','rootContainer','onAppCreated','render','patchRoutes','modifyCreateHistory','modifyRoute','beforeRender','onRouterCreated','access','layout','request',], 5 | }); 6 | 7 | export { plugin }; 8 | -------------------------------------------------------------------------------- /examples/demo-with-vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /examples/demo-with-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | vueJsx(), 12 | ], 13 | resolve: { 14 | alias: { 15 | '@': fileURLToPath(new URL('./src', import.meta.url)) 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import KoalaRender from './koalaRender'; 2 | export * from './scheme'; 3 | export * from './base'; 4 | export * from './when'; 5 | export * from './handles'; 6 | export * from './plugins'; 7 | export * from './preset'; 8 | export * from './useForm'; 9 | export * from './useTable'; 10 | export * from './usePager'; 11 | export * from './useModal'; 12 | export { mergeRefProps, mergeWithStrategy, travelTree, turnArray } from './helper'; 13 | export { KoalaRender }; 14 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/components/userCenter.vue: -------------------------------------------------------------------------------- 1 | 4 | 16 | 22 | -------------------------------------------------------------------------------- /docs/.vitepress/styles/index.less: -------------------------------------------------------------------------------- 1 | .example-doc, 2 | .ant-picker-panel-container, .el-picker-panel { 3 | table { 4 | border-collapse: initial; 5 | margin: initial; 6 | display: table; 7 | overflow-x: initial; 8 | } 9 | tr { 10 | border-top: initial; 11 | } 12 | 13 | tr:nth-child(2n) { 14 | background-color: initial; 15 | } 16 | 17 | th, 18 | td { 19 | border: initial; 20 | padding: initial; 21 | } 22 | } 23 | 24 | 25 | .container { 26 | max-width: none !important; 27 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/403.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-ts/vue'], 3 | globals: { 4 | // 这里填入你的项目需要的全局变量 5 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: 6 | // 7 | // Vue: false 8 | __DEV__: false, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | '@typescript-eslint/ban-types': 0, 14 | }, 15 | env: { 16 | jest: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import examples from '../../examples'; 3 | import ExampleDoc from './components/ExampleDoc.vue' 4 | import '../styles/index.less'; 5 | import fesd from '@fesjs/fes-design' 6 | 7 | export default { 8 | ...DefaultTheme, 9 | enhanceApp({app}) { 10 | Object.keys(examples).forEach(key => { 11 | app.component(key, examples[key]) 12 | }) 13 | app.use(fesd); 14 | app.component('ExampleDoc', ExampleDoc) 15 | } 16 | } -------------------------------------------------------------------------------- /packages/antd-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-ts/vue'], 3 | globals: { 4 | // 这里填入你的项目需要的全局变量 5 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: 6 | // 7 | // Vue: false 8 | __DEV__: false, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | '@typescript-eslint/ban-types': 0, 14 | }, 15 | env: { 16 | jest: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/fes-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-ts/vue'], 3 | globals: { 4 | // 这里填入你的项目需要的全局变量 5 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: 6 | // 7 | // Vue: false 8 | __DEV__: false, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | '@typescript-eslint/ban-types': 0, 14 | }, 15 | env: { 16 | jest: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/element-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@webank/eslint-config-ts/vue'], 3 | globals: { 4 | // 这里填入你的项目需要的全局变量 5 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: 6 | // 7 | // Vue: false 8 | __DEV__: false, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | '@typescript-eslint/ban-types': 0, 14 | }, 15 | env: { 16 | jest: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/useTitle.js: -------------------------------------------------------------------------------- 1 | import { reactive, ref } from 'vue'; 2 | import { useRoute } from '@@/core/coreExports'; 3 | 4 | const cache = reactive(new Map()); 5 | 6 | export const getTitle = (path) => cache.get(path); 7 | 8 | export const deleteTitle = (patch) => cache.delete(patch); 9 | 10 | export const useTabTitle = (title) => { 11 | const route = useRoute(); 12 | const titleRef = ref(title); 13 | const path = route.path; 14 | 15 | cache.set(path, titleRef); 16 | 17 | return titleRef; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/getConfig.js: -------------------------------------------------------------------------------- 1 | import { plugin, ApplyPluginsType } from '@@/core/coreExports'; 2 | import { initialState } from '@@/initialState'; 3 | 4 | export default () => { 5 | const initConfig = {"title":"Fes.js","footer":"Created by MumbleFE","navigation":"mixin","multiTabs":false,"menus":[{"name":"index"}]} 6 | const runtimeConfig = plugin.applyPlugins({ 7 | key: 'layout', 8 | type: ApplyPluginsType.modify, 9 | initialValue: initConfig, 10 | args: { 11 | initialState 12 | } 13 | }); 14 | return runtimeConfig; 15 | }; 16 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/components/pageLoading.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | 30 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import vueJsx from '@vitejs/plugin-vue-jsx'; 4 | 5 | export default defineConfig({ 6 | e2e: { 7 | baseUrl: 'http://localhost:3000', 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | }, 11 | }, 12 | 13 | component: { 14 | devServer: { 15 | framework: 'vue', 16 | bundler: 'vite', 17 | viteConfig: { 18 | plugins: [vue(), vueJsx({})], 19 | }, 20 | }, 21 | }, 22 | viewportWidth: 1200, 23 | }); 24 | -------------------------------------------------------------------------------- /examples/demo-with-fes/.fes.js: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from '@fesjs/fes'; 2 | 3 | export default defineBuildConfig({ 4 | access: { 5 | roles: { 6 | admin: ['*'], 7 | manager: ['/'], 8 | }, 9 | }, 10 | layout: { 11 | title: 'Fes.js', 12 | footer: 'Created by MumbleFE', 13 | navigation: 'mixin', 14 | multiTabs: false, 15 | menus: [ 16 | { 17 | name: 'index', 18 | }, 19 | ], 20 | }, 21 | enums: { 22 | status: [ 23 | ['0', '无效的'], 24 | ['1', '有效的'], 25 | ], 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/coreExports.js: -------------------------------------------------------------------------------- 1 | export { 2 | useRoute, 3 | useRouter, 4 | onBeforeRouteUpdate, 5 | onBeforeRouteLeave, 6 | RouterLink, 7 | RouterView, 8 | useLink, 9 | createWebHashHistory, 10 | createWebHistory, 11 | createMemoryHistory, 12 | createRouter, 13 | Plugin, 14 | ApplyPluginsType 15 | } from '/Users/aring/code/koala-form/packages/demo-with-fes/node_modules/.pnpm/@fesjs+runtime@3.0.0_vue@3.3.4/node_modules/@fesjs/runtime'; 16 | 17 | export { plugin } from '../core/plugin.js'; 18 | export { getRouter, getHistory, destroyRouter, defineRouteMeta } from '../core/routes/routeExports.js'; 19 | 20 | -------------------------------------------------------------------------------- /docs/examples/Test.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | navbar: false 4 | heroImage: /logo.png 5 | heroAlt: Koala Form 6 | heroText: Koala Form 7 | tagline: 低代码表单解决方案,让你跟考拉一样“懒” 8 | actionText: Get Started 9 | actionLink: /zh/guide/ 10 | head: 11 | - - link 12 | - rel: shortcut icon 13 | type: image/png 14 | href: /logo.png 15 | features: 16 | - title: Low Code 17 | details: 减少你80%重复的工作量,提升你的生产效率 18 | - title: Easier 19 | details: 快速上手,提供常见的基础的场景,只要简单的配置即可完成CURD的表单页面 20 | - title: Flexible 21 | details: 提供插件扩展功能,如扩展UI库支持。 22 | footer: MIT Licensed | Copyright © 2019-present aring lai 23 | --- 24 | -------------------------------------------------------------------------------- /packages/core/src/koalaRender.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, PropType } from 'vue'; 2 | import { SceneContext } from './base'; 3 | import { turnArray } from './helper'; 4 | import { composeRender } from './plugins'; 5 | 6 | const KoalaRender = defineComponent({ 7 | props: { 8 | render: { 9 | type: Function as unknown as PropType, 10 | required: true, 11 | }, 12 | }, 13 | setup(props, ctx) { 14 | return () => { 15 | const render = composeRender(turnArray(props.render)); 16 | return render(ctx.slots); 17 | }; 18 | }, 19 | }); 20 | 21 | export default KoalaRender; 22 | -------------------------------------------------------------------------------- /packages/core/src/plugins/eventsPlugin.ts: -------------------------------------------------------------------------------- 1 | import { SceneContext, SceneConfig } from '../base'; 2 | import { mergeRefProps, travelTree } from '../helper'; 3 | import { ComponentDesc } from '../scheme'; 4 | import { PluginFunction } from './define'; 5 | 6 | export const eventsPlugin: PluginFunction = (api) => { 7 | api.describe('events-plugin'); 8 | api.on('schemeLoaded', ({ ctx }) => { 9 | travelTree(ctx.schemes, (scheme) => { 10 | const _events = (scheme?.__node as ComponentDesc)?.events; 11 | if (!_events) return; 12 | mergeRefProps(scheme, 'events', _events); 13 | }); 14 | api.emit('started'); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/mitt.js: -------------------------------------------------------------------------------- 1 | import "./chunk-DFKQJ226.js"; 2 | 3 | // node_modules/mitt/dist/mitt.mjs 4 | function mitt_default(n) { 5 | return { all: n = n || /* @__PURE__ */ new Map(), on: function(t, e) { 6 | var i = n.get(t); 7 | i ? i.push(e) : n.set(t, [e]); 8 | }, off: function(t, e) { 9 | var i = n.get(t); 10 | i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, [])); 11 | }, emit: function(t, e) { 12 | var i = n.get(t); 13 | i && i.slice().map(function(n2) { 14 | n2(e); 15 | }), (i = n.get("*")) && i.slice().map(function(n2) { 16 | n2(t, e); 17 | }); 18 | } }; 19 | } 20 | export { 21 | mitt_default as default 22 | }; 23 | //# sourceMappingURL=mitt.js.map 24 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-request/genRequestKey.js: -------------------------------------------------------------------------------- 1 | import { isURLSearchParams } from './helpers'; 2 | /** 3 | * 唯一定位一个请求(url, data | params, method) 4 | * 其中请求参数(data, params)根据请求方法,只使用其中一个 5 | * 一个请求同时包含 data | params 参数的设计本身不合理 6 | * 不对这种情况进行兼容 7 | */ 8 | 9 | const getQueryString = (data) => { 10 | if (isURLSearchParams(data)) { 11 | return data.toString(); 12 | } 13 | return data ? JSON.stringify(data) : ''; 14 | }; 15 | 16 | export default async function genRequestKey(ctx, next) { 17 | const { url, data, params, method } = ctx.config; 18 | 19 | ctx.key = `${url}${getQueryString(data)}${getQueryString(params)}${method}`; 20 | 21 | await next(); 22 | } 23 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: HomeView 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | // route level code-splitting 16 | // this generates a separate chunk (About.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('../views/AboutView.vue') 19 | } 20 | ] 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /docs/zh/guide/plugin/hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | 5 | # 事件汇总 6 | 7 | 为了更好的编写插件,在此汇总所有内置插件的事件。 8 | 9 | | 事件 | 触发插件 | 说明 | 10 | | ------------ | ----------------------- | ----- | 11 | | onStart | 所有插件 | 插件开始执行 | 12 | | onSelfStart | 所有插件 | 当前插件开始执行 | 13 | | on('started') | 所有插件 | 插件执行完成 | 14 | | on('baseSchemeLoaded') | `base-comp-plugin` | 基础场景解析组件描述完成 | 15 | | on('schemeLoaded') | `base-comp-plugin` `form-plugin` `table-plugin` `pager-plugin` `modal-plugin` | scheme已经加载 | 16 | | on('formSchemeLoaded') | `form-plugin` | 表单场景字段已解析到scheme上 | 17 | | on('tableSchemeLoaded') | `table-plugin` | 列表场景字段已解析到scheme上 | 18 | | on('pagerSchemeLoaded') | `pager-plugin` | 分页场景已解析到scheme上 | 19 | | on('modalSchemeLoaded') | `modal-plugin` | 弹框场景已解析到scheme上 | 20 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/pluginLocale.js: -------------------------------------------------------------------------------- 1 | import { plugin } from '@@/core/coreExports'; 2 | 3 | export const transTitle = (name) => { 4 | if (!/^\$\S+$/.test(name)) { 5 | return name; 6 | } 7 | const sharedLocale = plugin.getShared('locale'); 8 | if (sharedLocale) { 9 | const { t } = sharedLocale.locale; 10 | return t(name.slice(1)); 11 | } 12 | return name; 13 | }; 14 | 15 | export const transform = (menus) => 16 | menus.map((menu) => { 17 | const copy = { 18 | ...menu, 19 | label: transTitle(menu.label), 20 | }; 21 | if (menu.children) { 22 | copy.children = transform(menu.children); 23 | } 24 | return copy; 25 | }); 26 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-model/core.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import initialState from '/Users/aring/code/koala-form/packages/demo-with-fes/src/.fes/plugin-model/models/initialState'; 4 | 5 | export const models = { 6 | '@@initialState': initialState, 7 | }; 8 | 9 | 10 | const cache = new Map(); 11 | 12 | export const useModel = (name) => { 13 | const modelFunc = models[name]; 14 | if (modelFunc === undefined) { 15 | throw new Error('[plugin-model]: useModel, name is undefined.'); 16 | } 17 | if (typeof modelFunc !== 'function') { 18 | throw new Error('[plugin-model]: useModel is not a function.'); 19 | } 20 | if (!cache.has(name)) { 21 | cache.set(name, modelFunc()); 22 | } 23 | return cache.get(name); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/antd-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "noImplicitAny": true, 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "preserveConstEnums": false, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false, 13 | "downlevelIteration": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "removeComments": false, 18 | "declaration": true, 19 | "declarationMap": false, 20 | "outDir": "./dist", 21 | "moduleResolution": "Node" 22 | //支持set等的迭代 23 | }, 24 | "include": [ 25 | "./src/**/*.ts", 26 | "./src/**/*.tsx" 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/core/src/plugins/vModelsPlugin.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from 'lodash-es'; 2 | import { SceneConfig, SceneContext } from '../base'; 3 | import { mergeRefProps, travelTree } from '../helper'; 4 | import { ComponentDesc } from '../scheme'; 5 | import { PluginFunction } from './define'; 6 | 7 | export const vModelsPlugin: PluginFunction = (api) => { 8 | api.describe('v-models-plugin'); 9 | 10 | api.on('schemeLoaded', ({ ctx }) => { 11 | travelTree(ctx.schemes, (scheme) => { 12 | const node = scheme?.__node as ComponentDesc; 13 | if (!node || isUndefined(node.vModels)) return; 14 | mergeRefProps(scheme, 'vModels', node.vModels); 15 | }); 16 | api.emit('started'); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/element-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "noImplicitAny": true, 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "preserveConstEnums": false, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false, 13 | "downlevelIteration": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "removeComments": false, 18 | "declaration": true, 19 | "declarationMap": false, 20 | "outDir": "./dist", 21 | "moduleResolution": "Node" 22 | //支持set等的迭代 23 | }, 24 | "include": [ 25 | "./src/**/*.ts", 26 | "./src/**/*.tsx" 27 | ] 28 | } -------------------------------------------------------------------------------- /docs/examples/started/useScene.js: -------------------------------------------------------------------------------- 1 | import { useScene } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const { render } = useScene({ 7 | components: [ 8 | { 9 | name: 'div', 10 | props: { id: 'baseScene' }, 11 | children: [ 12 | { name: 'h3', children: ['基础场景'] }, 13 | { 14 | name: 'p', 15 | children: ['我是基础场景的内容', { name: 'br' }, '我是基础场景的内容2'], 16 | }, 17 | ], 18 | }, 19 | ], 20 | }); 21 | return render; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "noImplicitAny": true, 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "preserveConstEnums": false, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false, 13 | "downlevelIteration": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "ignoreDeprecations": "5.0", 18 | "removeComments": false, 19 | "declaration": true, 20 | "declarationMap": false, 21 | "outDir": "./dist", 22 | "moduleResolution": "Node" 23 | //支持set等的迭代 24 | }, 25 | "include": [ 26 | "./src/**/*.ts", 27 | "./src/**/*.tsx" 28 | ] 29 | } -------------------------------------------------------------------------------- /packages/fes-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "noImplicitAny": true, 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "preserveConstEnums": false, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false, 13 | "downlevelIteration": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "ignoreDeprecations": "5.0", 18 | "removeComments": false, 19 | "declaration": true, 20 | "declarationMap": false, 21 | "outDir": "./dist", 22 | "moduleResolution": "Node" 23 | //支持set等的迭代 24 | }, 25 | "include": [ 26 | "./src/**/*.ts", 27 | "./src/**/*.tsx" 28 | ] 29 | } -------------------------------------------------------------------------------- /packages/fes-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koala-form/fes-plugin", 3 | "version": "2.0.3", 4 | "description": "KoalaForm fes design ui插件", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "author": "aringlai", 9 | "license": "MIT", 10 | "files": [ 11 | "dist/" 12 | ], 13 | "peerDependencies": { 14 | "vue": "^3.0.7", 15 | "@koala-form/core": "^2.0.0-rc.8", 16 | "@fesjs/fes-design": "^0.7.15" 17 | }, 18 | "keywords": [ 19 | "koala-form" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/WeBankFinTech/KoalaForm.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/WeBankFinTech/KoalaForm/issues" 27 | }, 28 | "homepage": "https://github.com/WeBankFinTech/KoalaForm#readme" 29 | } 30 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/DemoDoc.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /packages/antd-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koala-form/antd-plugin", 3 | "version": "2.0.5", 4 | "description": "KoalaForm Ant Design Vue ui插件", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "author": "aringlai", 9 | "license": "MIT", 10 | "files": [ 11 | "dist/" 12 | ], 13 | "peerDependencies": { 14 | "vue": "^3.0.7", 15 | "@koala-form/core": "^2.0.1", 16 | "ant-design-vue": "^3.2.20" 17 | }, 18 | "dependencies": { 19 | }, 20 | "keywords": [ 21 | "koala-form" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/WeBankFinTech/KoalaForm.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/WeBankFinTech/KoalaForm/issues" 29 | }, 30 | "homepage": "https://github.com/WeBankFinTech/KoalaForm#readme" 31 | } 32 | -------------------------------------------------------------------------------- /packages/element-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koala-form/element-plugin", 3 | "version": "2.0.9", 4 | "description": "KoalaForm element plus ui插件", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "author": "aringlai", 9 | "license": "MIT", 10 | "files": [ 11 | "dist/" 12 | ], 13 | "peerDependencies": { 14 | "vue": "^3.0.7", 15 | "@koala-form/core": "^2.0.1" 16 | }, 17 | "dependencies": { 18 | "element-plus": "^2.3.7" 19 | }, 20 | "keywords": [ 21 | "koala-form" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/WeBankFinTech/KoalaForm.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/WeBankFinTech/KoalaForm/issues" 29 | }, 30 | "homepage": "https://github.com/WeBankFinTech/KoalaForm#readme" 31 | } 32 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /packages/core/src/when/index.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isString } from 'lodash-es'; 2 | import { watch } from 'vue'; 3 | import { WhenPlugin } from '../base'; 4 | 5 | export const when: WhenPlugin) => unknown)> = (expression) => { 6 | if (!expression) return; 7 | return (ctx, invoke) => { 8 | let code: any; 9 | if (isString(expression)) { 10 | if (!ctx.modelRef) { 11 | console.warn('When: ctx.model not found!'); 12 | return; 13 | } 14 | code = Function('state', `with(state){ return ${expression}}`); 15 | } else if (isFunction(expression)) { 16 | code = expression; 17 | } 18 | if (!code) return; 19 | watch( 20 | () => code(ctx.modelRef.value), 21 | (value) => invoke(value), 22 | { immediate: true }, 23 | ); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /docs/zh/guide/scene/usePager.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # usePager 5 | 6 | 封装分页器场景 7 | 8 | ```js 9 | const ctx = usePager({}) 10 | 11 | // 设置数据 12 | ctx.modelRef.value.currentPage = 100; 13 | 14 | // 或者通过handle调用 15 | doSetPager(ctx, { 16 | currentPage: 12, 17 | totalCount: 1000 18 | }) 19 | 20 | ctx.render // 渲染函数 21 | 22 | 23 | ``` 24 | ## API 25 | 26 | ### 参数 27 | 28 | - ctx:指定上下文 29 | - pager:table组件 30 | 31 | ```js 32 | export interface PagerSceneConfig extends SceneConfig { 33 | ctx: PagerSceneContext; 34 | pager: ComponentDesc; 35 | } 36 | ``` 37 | 38 | ### 返回 39 | 40 | - ref:分页组件实例引用 41 | - modelRef:分页数据,双向绑定 42 | - isPager:上下文标识是分页 43 | 44 | ```js 45 | export interface PagerSceneContext extends SceneContext { 46 | modelRef: Ref<{ 47 | pageSize: number; 48 | currentPage: number; 49 | totalCount: number; 50 | }>; 51 | ref: Ref; 52 | isPager: boolean; 53 | } 54 | ``` -------------------------------------------------------------------------------- /docs/examples/main.js: -------------------------------------------------------------------------------- 1 | import '@koala-form/fes-plugin'; 2 | import { setupGlobalConfig, installPluginPreset } from '@koala-form/core'; 3 | import { FMessage } from '@fesjs/fes-design'; 4 | import { BASE_URL } from './const'; 5 | // 将依赖的插件安装到全局 6 | installPluginPreset(); 7 | 8 | setupGlobalConfig({ 9 | // 实现网络请求的实现 10 | request(api, params, config) { 11 | console.log('request.params => ', params); 12 | return fetch(location.origin + BASE_URL + api) 13 | .then((res) => { 14 | return res.json(); 15 | }) 16 | .then((data) => { 17 | console.log('request.data => ', data); 18 | if (data.code !== 0) { 19 | const msg = `${data.message}(${data.code})`; 20 | FMessage.error(msg); 21 | throw new Error(msg); 22 | } 23 | return data?.result; 24 | }); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koala-form/core", 3 | "version": "2.0.9", 4 | "description": "基于Vue3的中后台表单解决方案", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "author": "aringlai", 9 | "license": "MIT", 10 | "files": [ 11 | "dist/" 12 | ], 13 | "peerDependencies": { 14 | "vue": "^3.0.7" 15 | }, 16 | "dependencies": { 17 | "lodash-es": "^4.17.21", 18 | "dayjs": "^1.11.5", 19 | "mitt": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash-es": "^4.17.5", 23 | "@types/node": "^22.5.1" 24 | }, 25 | "keywords": [ 26 | "koala-form" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/WeBankFinTech/KoalaForm.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/WeBankFinTech/KoalaForm/issues" 34 | }, 35 | "homepage": "https://github.com/WeBankFinTech/KoalaForm#readme" 36 | } 37 | -------------------------------------------------------------------------------- /docs/examples/base/slotRender.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | -------------------------------------------------------------------------------- /packages/fes-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { installInGlobal, isComponent, PluginFunction, SceneConfig, SceneContext, setupGlobalConfig } from '@koala-form/core'; 2 | import * as fesD from '@fesjs/fes-design'; 3 | export * from './preset'; 4 | export * from './useCurd'; 5 | 6 | export const componentPlugin: PluginFunction = (api) => { 7 | setupGlobalConfig({ 8 | modelValueName: 'modelValue', 9 | }); 10 | 11 | api.describe('fes-plugin'); 12 | 13 | api.onSelfStart(({ ctx }) => { 14 | ctx.getComponent = (name) => { 15 | if (typeof name === 'string') { 16 | const comp = (fesD as any)[`F${name}`]; 17 | if (isComponent(comp)) return comp; 18 | else return name; 19 | } else { 20 | return name; 21 | } 22 | }; 23 | api.emit('componentLoaded'); 24 | }); 25 | }; 26 | 27 | installInGlobal(componentPlugin); 28 | -------------------------------------------------------------------------------- /docs/examples/started/useScene.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /docs/public/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "result": { 4 | "list": [ 5 | { 6 | "id": "1", 7 | "name": "蒙奇·D·路飞", 8 | "age": 16, 9 | "sex": "1", 10 | "hobby": "2,3", 11 | "birthday": 1115251200000, 12 | "idCard": "440223198310130033", 13 | "address": "上海市普陀区金沙江路 1518 弄", 14 | "education": "1" 15 | 16 | }, 17 | { 18 | "id": "2", 19 | "name": "罗罗诺亚·索隆", 20 | "age": 18, 21 | "sex": "1", 22 | "birthday": 1115251200000, 23 | "idCard": "440223193110130024", 24 | "address": "上海市普陀区金沙江路 1518 弄", 25 | "education": "2" 26 | } 27 | ], 28 | "page": { 29 | "currentPage": 1, 30 | "totalCount": 23 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fesjs/template", 3 | "version": "3.0.0", 4 | "description": "fes项目模版", 5 | "scripts": { 6 | "build": "fes build", 7 | "prod": "FES_ENV=prod fes build", 8 | "analyze": "ANALYZE=1 fes build", 9 | "dev": "fes dev", 10 | "test:unit": "fes test:unit" 11 | }, 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "devDependencies": { 16 | "@webank/eslint-config-webank": "1.2.7" 17 | }, 18 | "dependencies": { 19 | "@fesjs/fes": "^3.0.0", 20 | "@fesjs/plugin-access": "^3.0.0", 21 | "@fesjs/plugin-layout": "^5.0.0", 22 | "@fesjs/plugin-model": "^3.0.0", 23 | "@fesjs/plugin-enums": "^3.0.0", 24 | "@fesjs/plugin-request": "3.0.0", 25 | "@fesjs/fes-design": "^0.7.23", 26 | "@fesjs/builder-webpack": "^3.0.0", 27 | "@koala-form/core": "^2.0.0", 28 | "@koala-form/fes-plugin": "2.0.0", 29 | "vue": "^3.2.47", 30 | "core-js": "^3.29.1" 31 | }, 32 | "private": true 33 | } -------------------------------------------------------------------------------- /examples/demo-with-fes/public/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "result": { 4 | "list": [ 5 | { 6 | "id": "1", 7 | "name": "蒙奇·D·路飞", 8 | "age": 16, 9 | "sex": "1", 10 | "hobby": "2,3", 11 | "birthday": 1115251200000, 12 | "idCard": "440223198310130033", 13 | "address": "上海市普陀区金沙江路 1518 弄", 14 | "education": "1" 15 | 16 | }, 17 | { 18 | "id": "2", 19 | "name": "罗罗诺亚·索隆", 20 | "age": 18, 21 | "sex": "1", 22 | "birthday": 1115251200000, 23 | "idCard": "440223193110130024", 24 | "address": "上海市普陀区金沙江路 1518 弄", 25 | "education": "2" 26 | } 27 | ], 28 | "page": { 29 | "currentPage": 1, 30 | "totalCount": 23 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /examples/demo-with-vue/public/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "result": { 4 | "list": [ 5 | { 6 | "id": "1", 7 | "name": "蒙奇·D·路飞", 8 | "age": 16, 9 | "sex": "1", 10 | "hobby": "2,3", 11 | "birthday": 1115251200000, 12 | "idCard": "440223198310130033", 13 | "address": "上海市普陀区金沙江路 1518 弄", 14 | "education": "1" 15 | 16 | }, 17 | { 18 | "id": "2", 19 | "name": "罗罗诺亚·索隆", 20 | "age": 18, 21 | "sex": "1", 22 | "birthday": 1115251200000, 23 | "idCard": "440223193110130024", 24 | "address": "上海市普陀区金沙江路 1518 弄", 25 | "education": "2" 26 | } 27 | ], 28 | "page": { 29 | "currentPage": 1, 30 | "totalCount": 23 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /cypress/e2e/base/scene.cy.ts: -------------------------------------------------------------------------------- 1 | describe('快速上手', () => { 2 | // it('基础场景', () => { 3 | // cy.visit('/zh/guide/getting-started.html'); 4 | // cy.get('.example-case').eq(0).as('baseScene'); 5 | // cy.get('@baseScene').find('h3').should('contain.text', '基础场景'); 6 | // cy.get('@baseScene').find('p').should('contain.text', '我是基础场景的内容'); 7 | // }); 8 | 9 | it('表单这么写', () => { 10 | cy.visit('/zh/guide/getting-started.html'); 11 | cy.get('.example-case').eq(1).find('.fes-form-item').as('items'); 12 | 13 | cy.get('@items').eq(0).get('input').should('contain.value', '蒙奇·D·路飞'); 14 | cy.get('@items').eq(1).get('.fes-select-trigger-label').should('contain.text', '男'); 15 | cy.get('@items').eq(2).find('input').type('18{enter}'); 16 | 17 | cy.get('@items').eq(3).contains('保存').click(); 18 | cy.get('@items').eq(3).contains('重置').click(); 19 | cy.get('@items').eq(2).find('input').should('contain.value', ''); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /examples/demo-with-fes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": [ 7 | "esnext", 8 | "dom" 9 | ], 10 | "sourceMap": true, 11 | "baseUrl": ".", 12 | "jsx": "preserve", 13 | "allowSyntheticDefaultImports": true, 14 | "moduleResolution": "node", 15 | "forceConsistentCasingInFileNames": true, 16 | "noImplicitReturns": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": true, 19 | "allowJs": true, 20 | "experimentalDecorators": true, 21 | "strict": true, 22 | "paths": { 23 | "@/*": [ 24 | "./src/*" 25 | ], 26 | "@@/*": [ 27 | "./src/.fes/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "*.js", 33 | ".fes*.js", 34 | "src/**/*", 35 | "typings/**/*", 36 | "config/**/*" 37 | ], 38 | "exclude": [ 39 | "build", 40 | "dist", 41 | "scripts", 42 | "webpack", 43 | "jest", 44 | "node_modules" 45 | ] 46 | } -------------------------------------------------------------------------------- /docs/zh/guide/base/event.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 事件基础 6 | 可以通过`ComponentDesc`的属性events绑定事件。 7 | 8 | ## 绑定事件 9 | events是对象,属性名为事件名,约定事件名为`on[事件名]` 如: 10 | - onClick 11 | - onChange 12 | - onInput 13 | - onSelect 14 | 15 | ::: tip 事件回调参数 16 | 事件的回调参数和组件的事件回调参数一致,依赖于组件库。 17 | ::: 18 | 19 | ```js 20 | 21 | const btn = { name: ComponentType.Button, events: { 22 | onClick: (event) => {} 23 | } } 24 | 25 | const select = { name: ComponentType.Select, events: { 26 | onChange: (value) => {}, 27 | onBlur: (event) => {} 28 | } } 29 | 30 | 31 | ``` 32 | 33 | ## 增强列表事件 34 | 在列表里面响应事件,一般需要获取当前事件触发的行数据,比如点击编辑/删除按钮,需要取到行的ID等数据,因此对于列表里面的事件回调,参数的顺序是: 35 | - 列表 Column Slot的参数 36 | - 绑定组件的事件参数 37 | 38 | ```js 39 | 40 | useTable({ fields: [ 41 | { label: 'ID', name: 'id' }, 42 | { label: '操作', components: { 43 | name: ComponentType.Button, 44 | children: ['删除'], 45 | events: { 46 | // // 第一个参数就是列插槽的参数,第一个参数是按钮组件的事件参数 47 | onClick: (record, event) => {} 48 | } 49 | }} 50 | ] }) 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/zh/guide/base/slots.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 自定义渲染 6 | 自定义渲染可通过slotName和format实现。 7 | 8 | slotName使用可参考[组件描述/插槽](./component.md#插槽) 9 | 10 | format使用可参考[字段描述描述/format](./field.md#format) 11 | 12 | ## 渲染优先级 13 | 如果同时存在format、slotName、children、components,那么优先级低的将不会渲染。 14 | 15 | 优先级时 format > slotName > (children = components) 16 | 17 | ```js 18 | const name = { 19 | label: '姓名', 20 | name: 'name', 21 | format: () => 'test', 22 | slotName: 'name', 23 | components: { name: 'input' } 24 | } 25 | // 只会渲染format的内容 26 | 27 | const name2 = { 28 | name: 'div', 29 | slotName: 'name', 30 | children: [ 31 | { name: 'div', 'test' } 32 | ] 33 | } 34 | const { render } = useScene({ components: [name2] }) 35 | render({ 36 | name: () => 'slot test' 37 | }) 38 | // 只会渲染slotName的内容 39 | 40 | ``` 41 | 42 | ## KoalaRender 43 | KoalaRender组件用于渲染场景上下文,方便在template中编写slot。 44 | 45 | 46 | 47 | 48 | 53 | -------------------------------------------------------------------------------- /docs/zh/guide/scene/useModal.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # useModal 5 | 6 | 模态框场景可以用于展示新增和修改表单。 7 | 8 | ```js 9 | const form = useForm({}) 10 | const ctx = useModel({ 11 | modal: { 12 | children: form, // 嵌套form场景 13 | } 14 | }) 15 | 16 | // 设置数据 17 | ctx.modelRef.value.show = true; 18 | 19 | // 或者通过handle调用 20 | doOpen(ctx) 21 | doClose(ctx) 22 | 23 | ctx.render // 渲染函数 24 | 25 | ``` 26 | ## API 27 | 28 | ### 参数 29 | 30 | - ctx:指定上下文 31 | - modal:modal组件 32 | - title: 标题 33 | 34 | ```js 35 | export interface ModalSceneConfig extends SceneConfig { 36 | ctx?: ModalSceneContext; 37 | title?: string; 38 | modal?: ComponentDesc; 39 | } 40 | ``` 41 | 42 | ### 返回 43 | 44 | - ref:模态框组件实例引用 45 | - modelRef:模态框数据,双向绑定 46 | 47 | ```js 48 | export interface ModalSceneContext extends SceneContext { 49 | modelRef: Ref<{ 50 | show: boolean; 51 | title: string; 52 | }>; 53 | ref: Ref; 54 | } 55 | ``` 56 | 57 | ### 抽屉模式 58 | 59 | ```js 60 | const ctx = useModel({ 61 | modal: { 62 | name: ComponentType.Drawer 63 | } 64 | }) 65 | ``` -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /cypress/component/usePager.cy.tsx: -------------------------------------------------------------------------------- 1 | import UsePager from '../../docs/examples/started/usePager'; 2 | 3 | describe('Pager component', () => { 4 | it('Should display correct page number and total count', () => { 5 | const app = cy.mount(UsePager); 6 | app.get('.fes-pagination').get('.is-active').should('have.text', '2'); 7 | }); 8 | 9 | it('Should trigger onChange event when the page is changed', () => { 10 | const app = cy.mount(UsePager); 11 | app.get('.fes-pagination-pager-item').eq(3).click(); // Assuming that the class "pager__page-link" represents the clickable link element for each page. Selecting the fourth link to simulate a page change event. 12 | app.get('.fes-alert-info').should('have.text', 'onChange 3'); // Assuming that the class "f-message--info" represents the message element displayed when the onChange event is triggered. 13 | app.get('.fes-alert-success').should('have.text', 'watch 3'); // Assuming that the class "f-message--success" represents the message element displayed when the watched currentPage value is changed to 3. 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/src/plugins/vIfPlugin.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isUndefined } from 'lodash-es'; 2 | import { Ref, computed, ref, unref } from 'vue'; 3 | import { SceneConfig, SceneContext } from '../base'; 4 | import { travelTree } from '../helper'; 5 | import { ComponentDesc } from '../scheme'; 6 | import { PluginFunction } from './define'; 7 | 8 | export const vIfPlugin: PluginFunction = (api) => { 9 | api.describe('v-if-plugin'); 10 | 11 | api.on('schemeLoaded', ({ ctx }) => { 12 | travelTree(ctx.schemes, (scheme) => { 13 | const node = scheme.__node as ComponentDesc; 14 | if (!node || isUndefined(node.vIf)) return; 15 | if (isFunction(node.vIf)) { 16 | const vIf = ref(true); 17 | node.vIf(ctx, (value: any) => { 18 | vIf.value = !!value; 19 | }); 20 | scheme.vIf = vIf; 21 | } else { 22 | scheme.vIf = computed(() => unref(node.vIf)) as Ref; 23 | } 24 | }); 25 | api.emit('started'); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /docs/zh/demos/index.md: -------------------------------------------------------------------------------- 1 | # 如何贡献Demo 2 | 3 | 通过提PR的形式贡献,步骤如下: 4 | 5 | ### 1. 启动项目 6 | 7 | 通过git clone KoalaForm的github地址,执行以下命令 8 | 9 | ```bash 10 | pnpm i 11 | pnpm run docs:dev 12 | ``` 13 | 启动后可访问文档地址:`http://localhost:3000/` 14 | 15 | ### 2. 新建demo的md文件 16 | 17 | 在`docs/zh/demos`目录新建md文件,比如login.md 18 | 19 | ### 3. 配置md文件 20 | 21 | 在`docs/.vitepress/config.js`中,找到getDemosSidebar方法,配置md文件的路由,比如 22 | ```js 23 | function getDemosSidebar() { 24 | return [ 25 | { text: '如何贡献Demo', link: '/zh/demos/' }, 26 | { text: '登录', link: '/zh/demos/login' }, 27 | ] 28 | } 29 | ``` 30 | 31 | 保存后,访问:`http://localhost:3000/zh/demos/login` 可以看到配置的md文件 32 | 33 | ### 4. 开发demo示例 34 | 35 | 在`docs/examples/demos`目录下,新建demo文件,比如login.vue 36 | 37 | 开发完demo后,将文件导出到`docs/examples/demos/index.js`中 38 | 39 | ### 5. 配置demo到md文件 40 | 41 | 回到md文件上,使用`ExampleDoc`组件引用示例和代码,如: 42 | ```html 43 | 44 | 45 | 46 | 47 | 52 | 53 | 54 | ``` 55 | 56 | 配置保存之后,可以在浏览器看到效果。 57 | 58 | ### 6. 提交PR 59 | 60 | 提交PR到gitHub上。 -------------------------------------------------------------------------------- /docs/examples/started/usePager.js: -------------------------------------------------------------------------------- 1 | import { FMessage } from '@fesjs/fes-design'; 2 | import { usePager } from '@koala-form/core'; 3 | import { defineComponent, watch } from 'vue'; 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const { render, modelRef } = usePager({ 8 | pager: { 9 | props: { 10 | style: { 11 | justifyContent: 'center', 12 | }, 13 | }, 14 | events: { 15 | onChange: (value) => { 16 | FMessage.info('onChange ' + value); 17 | }, 18 | }, 19 | }, 20 | }); 21 | modelRef.value.currentPage = 2; 22 | modelRef.value.totalCount = 100; 23 | // doSetPager(ctx, { currentPage: 2, totalCount: 100 }); 24 | 25 | watch( 26 | () => modelRef.value.currentPage, 27 | () => { 28 | FMessage.success('watch ' + modelRef.value.currentPage); 29 | }, 30 | ); 31 | 32 | return render; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-request/scheduler.js: -------------------------------------------------------------------------------- 1 | class Scheduler { 2 | constructor() { 3 | this.middlewares = []; 4 | } 5 | 6 | use(fn) { 7 | if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); 8 | this.middlewares.push(fn); 9 | return this; 10 | } 11 | 12 | compose() { 13 | return (context, next) => { 14 | let index = -1; 15 | const dispatch = (i) => { 16 | if (i <= index) return Promise.reject(new Error('next() called multiple times')); 17 | index = i; 18 | let fn = this.middlewares[i]; 19 | if (index === this.middlewares.length) fn = next; 20 | if (!fn) return Promise.resolve(); 21 | try { 22 | return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); 23 | } catch (e) { 24 | return Promise.reject(e); 25 | } 26 | }; 27 | return dispatch(0); 28 | }; 29 | } 30 | } 31 | 32 | export default new Scheduler(); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 WeBankFinTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/src/plugins/vShowPlugin.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isUndefined } from 'lodash-es'; 2 | import { Ref, computed, ref, unref } from 'vue'; 3 | import { SceneConfig, SceneContext } from '../base'; 4 | import { travelTree } from '../helper'; 5 | import { ComponentDesc } from '../scheme'; 6 | import { PluginFunction } from './define'; 7 | 8 | export const vShowPlugin: PluginFunction = (api) => { 9 | api.describe('v-show-plugin'); 10 | 11 | api.on('schemeLoaded', ({ ctx }) => { 12 | travelTree(ctx.schemes, (scheme) => { 13 | const node = scheme?.__node as ComponentDesc; 14 | if (!node || isUndefined(node.vShow)) return; 15 | if (isFunction(node.vShow)) { 16 | const vShow = ref(true); 17 | node.vShow(ctx, (value: any) => { 18 | vShow.value = !!value; 19 | }); 20 | scheme.vShow = vShow; 21 | } else { 22 | scheme.vShow = computed(() => unref(node.vShow)) as Ref; 23 | } 24 | }); 25 | api.emit('started'); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/pluginAccess.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import { hasAccessSync } from '../../plugin-access/core'; 3 | 4 | if (!hasAccessSync) { 5 | throw new Error('[plugin-layout]: pLugin-layout depends on plugin-access,please install plugin-access first!'); 6 | } 7 | 8 | export const hasAccessByMenuItem = (item) => { 9 | const hasChild = item.children && item.children.length; 10 | if (item.path && !hasChild) { 11 | return hasAccessSync(item.path); 12 | } 13 | if (hasChild) { 14 | return item.children.some((child) => { 15 | const rst = hasAccessByMenuItem(child); 16 | return rst; 17 | }); 18 | } 19 | return true; 20 | }; 21 | 22 | export const transform = (menus) => 23 | menus 24 | .map((menu) => { 25 | const hasAccess = hasAccessByMenuItem(menu); 26 | if (!hasAccess) { 27 | return false; 28 | } 29 | if (menu.children) { 30 | menu.children = transform(menu.children); 31 | } 32 | return menu; 33 | }) 34 | .filter(Boolean); 35 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/pluginRegister.js: -------------------------------------------------------------------------------- 1 | import { plugin } from './plugin'; 2 | import * as Plugin_0 from '/Users/aring/code/koala-form/packages/demo-with-fes/src/app.jsx'; 3 | import * as Plugin_1 from '@@/core/routes/runtime.js'; 4 | import * as Plugin_2 from '@@/plugin-access/runtime.js'; 5 | import * as Plugin_3 from '@@/plugin-layout/runtime.js'; 6 | 7 | function handleDefaultExport(pluginExports) { 8 | // 避免编译警告 9 | const defaultKey = 'default'; 10 | if (pluginExports[defaultKey]) { 11 | const {default: defaultExport, ...otherExports} = pluginExports; 12 | return { 13 | ...defaultExport, 14 | ...otherExports 15 | } 16 | } 17 | return pluginExports; 18 | } 19 | 20 | plugin.register({ 21 | apply: handleDefaultExport(Plugin_0), 22 | path: '/Users/aring/code/koala-form/packages/demo-with-fes/src/app.jsx', 23 | }); 24 | plugin.register({ 25 | apply: handleDefaultExport(Plugin_1), 26 | path: '@@/core/routes/runtime.js', 27 | }); 28 | plugin.register({ 29 | apply: handleDefaultExport(Plugin_2), 30 | path: '@@/plugin-access/runtime.js', 31 | }); 32 | plugin.register({ 33 | apply: handleDefaultExport(Plugin_3), 34 | path: '@@/plugin-layout/runtime.js', 35 | }); 36 | -------------------------------------------------------------------------------- /docs/examples/compose/useTableWithPager.js: -------------------------------------------------------------------------------- 1 | import { useSceneContext, useTableWithPager } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const { 7 | ctxs: [table, pager], 8 | } = useSceneContext(['table', 'pager']); 9 | const { render, dataSource } = useTableWithPager( 10 | { 11 | ctx: table, 12 | fields: [ 13 | { 14 | name: 'id', 15 | label: 'ID', 16 | }, 17 | { 18 | name: 'name', 19 | label: '姓名', 20 | }, 21 | ], 22 | }, 23 | { 24 | ctx: pager, 25 | }, 26 | ); 27 | 28 | pager.modelRef.value.pageSize = 5; 29 | const names = ['蒙奇·D·路飞', '罗罗诺亚·索隆', '山治']; 30 | for (let index = 1; index <= 22; index++) { 31 | dataSource.value.push({ 32 | id: index, 33 | name: names[parseInt(Math.random() * 10) % 3], 34 | }); 35 | } 36 | return render; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import App from './App.vue' 5 | import router from './router' 6 | import { installPluginPreset, setupGlobalConfig, installInGlobal } from '@koala-form/core'; 7 | import { componentPlugin } from '@koala-form/element-plugin'; 8 | import { FMessage } from '@fesjs/fes-design'; 9 | 10 | installInGlobal(componentPlugin); 11 | const BASE_URL = '/' 12 | 13 | installPluginPreset(); 14 | 15 | setupGlobalConfig({ 16 | // 实现网络请求的实现 17 | modelValueName: 'modelValue', 18 | request(api, params, config) { 19 | console.log('request.params => ', params); 20 | return fetch(location.origin + BASE_URL + api) 21 | .then((res) => { 22 | return res.json(); 23 | }) 24 | .then((data) => { 25 | console.log('request.data => ', data); 26 | if (data.code !== 0) { 27 | const msg = `${data.message}(${data.code})`; 28 | FMessage.error(msg); 29 | throw new Error(msg); 30 | } 31 | return data?.result; 32 | }); 33 | }, 34 | }); 35 | 36 | const app = createApp(App) 37 | 38 | app.use(router) 39 | 40 | app.mount('#app') 41 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/examples/useForm/query.js: -------------------------------------------------------------------------------- 1 | import { FMessage } from '@fesjs/fes-design'; 2 | import { ComponentType, useForm, useSceneContext, doResetFields } from '@koala-form/core'; 3 | import { genForm, genQueryAction } from '@koala-form/fes-plugin'; 4 | import { defineComponent } from 'vue'; 5 | 6 | export default defineComponent({ 7 | setup() { 8 | const { ctx } = useSceneContext('form'); 9 | const { render } = useForm({ 10 | ctx, 11 | form: genForm('inline'), 12 | fields: [ 13 | { 14 | name: 'name', 15 | label: '姓名', 16 | defaultValue: '蒙奇·D·路飞', 17 | components: { 18 | name: ComponentType.Input, 19 | }, 20 | }, 21 | { 22 | name: 'age', 23 | label: '年龄', 24 | components: { 25 | name: ComponentType.InputNumber, 26 | }, 27 | }, 28 | genQueryAction({ 29 | query: () => FMessage.success('点击查询'), 30 | reset: () => doResetFields(ctx), 31 | }), 32 | ], 33 | }); 34 | return render; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/plugins/disabledPlugin.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isUndefined } from 'lodash-es'; 2 | import { Ref, computed, ref, unref } from 'vue'; 3 | import { SceneConfig, SceneContext } from '../base'; 4 | import { mergeRefProps, travelTree } from '../helper'; 5 | import { ComponentDesc } from '../scheme'; 6 | import { PluginFunction } from './define'; 7 | 8 | export const disabledPlugin: PluginFunction = (api) => { 9 | api.describe('disabled-plugin'); 10 | 11 | api.on('schemeLoaded', ({ ctx }) => { 12 | travelTree(ctx.schemes, (scheme) => { 13 | const node = scheme.__node as ComponentDesc; 14 | if (!node || isUndefined(node.disabled)) return; 15 | if (isFunction(node.disabled)) { 16 | const _disabled = ref(true); 17 | node.disabled(ctx, (value: any) => { 18 | _disabled.value = !!value; 19 | }); 20 | mergeRefProps(scheme, 'props', { disabled: _disabled }); 21 | } else { 22 | const props: any = scheme.props || {}; 23 | props.disabled = computed(() => unref(node.disabled as Ref)); 24 | scheme.props = props; 25 | } 26 | }); 27 | api.emit('started'); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "1d52eabc", 3 | "browserHash": "4a7a1aa0", 4 | "optimized": { 5 | "vue": { 6 | "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", 7 | "file": "vue.js", 8 | "fileHash": "819cf8ff", 9 | "needsInterop": false 10 | }, 11 | "@fesjs/fes-design": { 12 | "src": "../../../../node_modules/@fesjs/fes-design/es/index.js", 13 | "file": "@fesjs_fes-design.js", 14 | "fileHash": "d6c9fced", 15 | "needsInterop": false 16 | }, 17 | "lodash-es": { 18 | "src": "../../../../packages/core/node_modules/lodash-es/lodash.js", 19 | "file": "lodash-es.js", 20 | "fileHash": "7a5cd7c5", 21 | "needsInterop": false 22 | }, 23 | "dayjs": { 24 | "src": "../../../../node_modules/dayjs/dayjs.min.js", 25 | "file": "dayjs.js", 26 | "fileHash": "4c2f0edd", 27 | "needsInterop": true 28 | }, 29 | "mitt": { 30 | "src": "../../../../node_modules/mitt/dist/mitt.mjs", 31 | "file": "mitt.js", 32 | "fileHash": "70ccbc39", 33 | "needsInterop": false 34 | } 35 | }, 36 | "chunks": { 37 | "chunk-265XQW5X": { 38 | "file": "chunk-265XQW5X.js" 39 | }, 40 | "chunk-DFKQJ226": { 41 | "file": "chunk-DFKQJ226.js" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /docs/examples/useForm/edit.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useForm, useSceneContext, doResetFields, doGetFormData } from '@koala-form/core'; 2 | import { genSubmitAction } from '@koala-form/fes-plugin'; 3 | import { defineComponent } from 'vue'; 4 | export default defineComponent({ 5 | setup() { 6 | const { ctx } = useSceneContext('form'); 7 | const { render } = useForm({ 8 | ctx, 9 | fields: [ 10 | { 11 | name: 'name', 12 | label: '姓名', 13 | defaultValue: '蒙奇·D·路飞', 14 | components: { 15 | name: ComponentType.Input, 16 | }, 17 | }, 18 | { 19 | name: 'age', 20 | label: '年龄', 21 | components: { 22 | name: ComponentType.InputNumber, 23 | }, 24 | }, 25 | genSubmitAction({ 26 | save: () => console.log(doGetFormData(ctx)), 27 | clear: () => Object.assign(ctx.modelRef.value, { name: null, age: null }), 28 | reset: () => doResetFields(ctx), 29 | }), 30 | ], 31 | }); 32 | return render; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /packages/element-plugin/src/slots.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType, Reactive, SceneContext, Scheme } from '@koala-form/core'; 2 | import { ElButton, ElCheckbox, ElRadio, ElSpace } from 'element-plus'; 3 | import { unref, h } from 'vue'; 4 | 5 | const optComps = { [ComponentType.CheckboxGroup]: ElCheckbox, [ComponentType.RadioGroup]: ElRadio }; 6 | export const genOptions = (name: string, props?: Reactive) => { 7 | const Comp = optComps[name]; 8 | return () => { 9 | return unref(unref(props || {}).options)?.map((item: any) => h(Comp, { ...item, label: item.value }, () => item.label)); 10 | }; 11 | }; 12 | 13 | export const genModalFooter = (scheme: Scheme, ctx: SceneContext) => { 14 | const onOk = () => { 15 | if (scheme.events?.onOk) { 16 | scheme.events.onOk(); 17 | } else { 18 | ctx.modelRef.value.show = false; 19 | } 20 | }; 21 | 22 | const onCancel = () => { 23 | if (scheme.events?.onCancel) { 24 | scheme.events.onCancel(); 25 | } else { 26 | ctx.modelRef.value.show = false; 27 | } 28 | }; 29 | 30 | return () => 31 | h(ElSpace, {}, () => [ 32 | h(ElButton, { onClick: onCancel }, () => unref(scheme.props || {}).cancelText || '取消'), 33 | h(ElButton, { onClick: onOk, type: 'primary' }, () => unref(scheme.props || {}).okText || '确定'), 34 | ]); 35 | }; 36 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExampleDoc.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | 29 | -------------------------------------------------------------------------------- /examples/demo-with-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-with-vue", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 12 | "format": "prettier --write src/" 13 | }, 14 | "dependencies": { 15 | "vue": "^3.3.4", 16 | "vue-router": "^4.2.2", 17 | "@fesjs/fes-design": "^0.7.23", 18 | "@koala-form/core": "^2.0.0", 19 | "@koala-form/fes-plugin": "2.0.0", 20 | "@koala-form/element-plugin": "2.0.1", 21 | "element-plus": "2.3.8" 22 | }, 23 | "devDependencies": { 24 | "@rushstack/eslint-patch": "^1.2.0", 25 | "@tsconfig/node18": "^2.0.1", 26 | "@types/node": "^18.16.17", 27 | "@vitejs/plugin-vue": "^4.2.3", 28 | "@vitejs/plugin-vue-jsx": "^3.0.1", 29 | "@vue/eslint-config-prettier": "^7.1.0", 30 | "@vue/eslint-config-typescript": "^11.0.3", 31 | "@vue/tsconfig": "^0.4.0", 32 | "eslint": "^8.39.0", 33 | "eslint-plugin-vue": "^9.11.0", 34 | "npm-run-all": "^4.1.5", 35 | "prettier": "^2.8.8", 36 | "typescript": "~5.0.4", 37 | "vite": "^4.3.9", 38 | "vue-tsc": "^1.6.5", 39 | "less": "4.1.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/zh/guide/scene/useScene.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # 基础场景 5 | 基础场景使用`useScene`。用ComponentDesc描述一段组件树,通过场景上下文返回的render方法,即可将组件树渲染成VNode树。 6 | 7 | 插件是扩展useScene功能的途径,通过插件可以修改场景的上下文(SceneContext),可以将一些功能模块进行场景化复用,比如:useForm、useTable、usePager、useModal。 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | ## 场景配置 19 | - ctx:传入的场景上下文 20 | - components:场景的组件树 21 | ```ts 22 | export interface SceneConfig { 23 | ctx?: SceneContext; 24 | components?: ComponentDesc[] | ComponentDesc; 25 | } 26 | ``` 27 | ## 场景上下文 28 | 场景提供上下文进行场景渲染和操作场景的API。 29 | 30 | - name 场景名 31 | - modelRef 场景的响应式的对象,用于取值和更新值。 32 | - schemes 组件树的解析态,用于插件的修改和访问。 33 | - getComponent 解析组件,用于扩展UI库 34 | - render 渲染场景 35 | 36 | ```ts 37 | export interface SceneContext { 38 | name: string; 39 | modelRef?: Ref; 40 | schemes: Array; 41 | getComponent: (name: keyof typeof ComponentType | String | Component) => Component | string; 42 | render: (slots?: Slots) => VNodeChild; 43 | } 44 | ``` 45 | 46 | ## useSceneContext 47 | 多个场景相互依赖上下文,可以使用`useSceneContext`先去生成场景的上下文引用,再传入场景中实例化。 48 | 49 | ```js 50 | // 单场景上下文 51 | const { ctx } = useSceneContext('form'); 52 | useForm({ ctx }) 53 | 54 | // 多场景上下文 55 | const { ctxs: [form, table] } = useSceneContext(['form', 'table']); 56 | useForm({ ctx: form }) 57 | useTable({ ctx: table }) 58 | 59 | ``` -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /docs/examples/labelPlugin.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useForm, useSceneContext, travelTree, mergeRefProps } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | const formLabelColonPlugin = (api) => { 5 | api.describe('form-label-colon-plugin'); 6 | 7 | api.on('formSchemeLoaded', ({ ctx }) => { 8 | // 遍历scheme 9 | travelTree(ctx.schemes, (scheme) => { 10 | if (scheme.props?.label?.trim()) { 11 | // 合并属性 12 | mergeRefProps(scheme, 'props', { label: scheme.props.label + ':' }); 13 | } 14 | }); 15 | }); 16 | }; 17 | 18 | export default defineComponent({ 19 | setup() { 20 | const { ctx } = useSceneContext('form'); 21 | ctx.use(formLabelColonPlugin); 22 | const { render } = useForm({ 23 | ctx, 24 | fields: [ 25 | { 26 | name: 'name', 27 | label: '姓名', 28 | defaultValue: '蒙奇·D·路飞', 29 | components: { 30 | name: ComponentType.Input, 31 | }, 32 | }, 33 | { 34 | name: 'age', 35 | label: '年龄', 36 | components: { 37 | name: ComponentType.InputNumber, 38 | }, 39 | }, 40 | ], 41 | }); 42 | return render; 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-access/createDirective.js: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue'; 2 | 3 | const cache = new WeakMap(); 4 | const setDisplay = (el, access) => { 5 | if (access.value) { 6 | el.style.display = el._display; 7 | } else { 8 | el.style.display = 'none'; 9 | } 10 | }; 11 | export default function createDirective(useAccess) { 12 | return { 13 | beforeMount(el) { 14 | const ctx = {}; 15 | ctx.watch = (path) => { 16 | el._display = el._display || el.style.display; 17 | const access = useAccess(path); 18 | setDisplay(el, access); 19 | return watch(access, () => { 20 | setDisplay(el, access); 21 | }); 22 | }; 23 | cache.set(el, ctx); 24 | }, 25 | mounted(el, binding) { 26 | const ctx = cache.get(el); 27 | if (ctx.unwatch) { 28 | ctx.unwatch(); 29 | } 30 | ctx.unwatch = ctx.watch(binding.value); 31 | }, 32 | updated(el, binding) { 33 | const ctx = cache.get(el); 34 | if (ctx.unwatch) { 35 | ctx.unwatch(); 36 | } 37 | ctx.unwatch = ctx.watch(binding.value); 38 | }, 39 | beforeUnmount(el) { 40 | const ctx = cache.get(el); 41 | if (ctx.unwatch) { 42 | ctx.unwatch(); 43 | } 44 | }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/chunk-DFKQJ226.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __commonJS = (cb, mod) => function __require() { 8 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 9 | }; 10 | var __copyProps = (to, from, except, desc) => { 11 | if (from && typeof from === "object" || typeof from === "function") { 12 | for (let key of __getOwnPropNames(from)) 13 | if (!__hasOwnProp.call(to, key) && key !== except) 14 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 15 | } 16 | return to; 17 | }; 18 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( 19 | // If the importer is in node compatibility mode or this is not an ESM 20 | // file that has been converted to a CommonJS file using a Babel- 21 | // compatible transform (i.e. "__esModule" has not been set), then set 22 | // "default" to the CommonJS "module.exports" for node compatibility. 23 | isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, 24 | mod 25 | )); 26 | 27 | export { 28 | __commonJS, 29 | __toESM 30 | }; 31 | //# sourceMappingURL=chunk-DFKQJ226.js.map 32 | -------------------------------------------------------------------------------- /docs/examples/base/comp.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useScene, useForm } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const form = useForm({ 7 | form: { props: { labelWidth: '40px' } }, 8 | fields: [ 9 | { 10 | name: 'name', // modelRef.value.name可以访问到值 11 | label: '姓名', // 表单项的名称 12 | defaultValue: '蒙奇·D·路飞', // 默认值 13 | components: { 14 | name: ComponentType.Input, // 表单组件是输入框 15 | }, 16 | }, 17 | ], 18 | }); 19 | 20 | const { render } = useScene({ 21 | components: [ 22 | { name: 'h3', children: '用户信息' }, 23 | { 24 | name: 'div', 25 | props: { style: { padding: '20px' } }, 26 | children: [ 27 | form, // 嵌套 28 | { 29 | name: ComponentType.Button, 30 | children: '保存', 31 | events: { 32 | onClick: () => { 33 | console.log(form.modelRef); 34 | }, 35 | }, 36 | }, 37 | ], 38 | }, 39 | ], 40 | }); 41 | 42 | return render; 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /docs/zh/guide/plugin/design.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 设计原理 6 | 7 | 在插件基础中讲过,可以使用插件扩展场景或者场景的功能,比如vIf、vShow等。 8 | 9 | 那么插件是如何实现扩展功能的呢? 10 | 11 | ## 场景上下文 12 | 所有的场景都可以继承SceneContext,扩展自定义场景的上下文。 13 | ```js 14 | export interface SceneContext { 15 | name: string; 16 | modelRef: Ref; 17 | schemes: Array; 18 | getComponent: (name: keyof typeof ComponentType | String | Component) => Component | string; 19 | render: (slots?: Slots) => VNodeChild; 20 | __config?: SceneConfig; 21 | __pluginStarted?: boolean; 22 | readonly __scopedId: string | number; 23 | readonly __plugins: PluginFunction[]; 24 | /** 插件 */ 25 | readonly use: (define: PluginFunction) => SceneContext; 26 | } 27 | ``` 28 | 29 | 其中上下文的核心就是`schemes`和`render`,插件也就是主要围绕着schemes来扩展功能。 30 | 31 | ## scheme是什么 32 | scheme实际上是一个中间态的节点,schemes就是中间态的节点树。 33 | 34 | ```js 35 | export interface Scheme { 36 | component: string | ComponentDesc; 37 | props?: Reactive; 38 | vModels?: Record; 39 | vShow?: Ref; 40 | vIf?: Ref; 41 | events?: Record void>; 42 | children?: SchemeChildren; 43 | slots?: Slots; 44 | __ref?: Ref; 45 | __node: unknown; 46 | } 47 | 48 | export type SchemeChildren = Array | string | ModelRef | Slot; 49 | ``` 50 | 51 | 我们可以称开发者定义的Field、ComponentDesc为初始态。 52 | 53 | 在场景中运行一系列插件把初始态转换为中间态。 54 | 55 | 最终render方法会根据中间态渲染成vNode终态。 56 | 57 | 如下图: 58 |
59 | 60 |
61 | -------------------------------------------------------------------------------- /docs/zh/preset/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # Preset 5 | Preset是负责渲染koala-form,比如根据字段定义的type,决定如何渲染对应的form控件。 6 | 7 | Preset就像是koala-form和UI组件库的一个连接桥梁。 8 | 9 | 比如@koala-form/fesd-preset,是通过preset接口将UI组件库fes-design和koala-form进行连接; 10 | 11 | @koala-form/antd-preset,是通过preset接口将UI组件库ant-design和koala-form进行连接。 12 | 13 | ## 上手开发 14 | 这里选用fes-design作为ui框架 15 | 16 | 1. 安装依赖 17 | ``` 18 | npm i @fesjs/fes-design 19 | npm i @koala-form/core 20 | ``` 21 | 22 | 2. 实现preset 23 | 通过preset提供的definePreset方法,实现部分方法 24 | ```jsx 25 | export default definePreset({ 26 | // 实现模态框渲染 27 | modalRender(defaultSlot, { modalModel, onOk, onCancel, modalProps }) { 28 | return ( 29 | onOk()} 36 | onCancel={() => onCancel()} 37 | {...modalProps} 38 | > 39 | {defaultSlot()} 40 | 41 | ); 42 | }, 43 | // 实现表单页面渲染 44 | pageRender(defaultSlot) { 45 | return ( 46 | 47 |
{defaultSlot()}
48 |
49 | ); 50 | }, 51 | // message组件渲染 52 | message: FMessage, 53 | // confirm渲染 54 | confirm(params) { 55 | FModal.confirm(params); 56 | }, 57 | }) 58 | ``` 59 | 60 | 3. 发布到npm 61 | 62 | ## 使用技巧 -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/svg.js: -------------------------------------------------------------------------------- 1 | const isStr = function (str) { 2 | return typeof str === 'string'; 3 | }; 4 | 5 | export const isValid = (elm) => { 6 | if (elm.nodeType === 1) { 7 | if (elm.nodeName.toLowerCase() === 'script') { 8 | return false; 9 | } 10 | 11 | for (let i = 0; i < elm.attributes.length; i++) { 12 | const val = elm.attributes[i].value; 13 | if (isStr(val) && val.toLowerCase().indexOf('on') === 0) { 14 | return false; 15 | } 16 | } 17 | 18 | for (let i = 0; i < elm.childNodes.length; i++) { 19 | if (!isValid(elm.childNodes[i])) { 20 | return false; 21 | } 22 | } 23 | } 24 | return true; 25 | }; 26 | 27 | export const validateContent = (svgContent) => { 28 | const div = document.createElement('div'); 29 | div.innerHTML = svgContent; 30 | 31 | // setup this way to ensure it works on our buddy IE 32 | for (let i = div.childNodes.length - 1; i >= 0; i--) { 33 | if (div.childNodes[i].nodeName.toLowerCase() !== 'svg') { 34 | div.removeChild(div.childNodes[i]); 35 | } 36 | } 37 | 38 | // must only have 1 root element 39 | const svgElm = div.firstElementChild; 40 | if (svgElm && svgElm.nodeName.toLowerCase() === 'svg') { 41 | // root element must be an svg 42 | // lets double check we've got valid elements 43 | // do not allow scripts 44 | if (isValid(svgElm)) { 45 | return div.innerHTML; 46 | } 47 | } 48 | return ''; 49 | }; 50 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/core/routes/routes.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViewsIndexJsx from '/Users/aring/code/koala-form/packages/demo-with-fes/src/.fes/plugin-layout/views/index.jsx' 4 | import index from '/Users/aring/code/koala-form/packages/demo-with-fes/src/pages/index.vue' 5 | import usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViews403 from '/Users/aring/code/koala-form/packages/demo-with-fes/src/.fes/plugin-layout/views/403.vue' 6 | import usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViews404 from '/Users/aring/code/koala-form/packages/demo-with-fes/src/.fes/plugin-layout/views/404.vue' 7 | 8 | export function getRoutes() { 9 | const routes = [ 10 | { 11 | "path": "/", 12 | "component": usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViewsIndexJsx, 13 | "children": [ 14 | { 15 | "path": "/", 16 | "component": index, 17 | "name": "index", 18 | "meta": { 19 | "name": "index", 20 | "title": "首页" 21 | }, 22 | "count": 5 23 | }, 24 | { 25 | "path": "/403", 26 | "name": "Exception403", 27 | "component": usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViews403, 28 | "meta": { 29 | "title": "403" 30 | } 31 | }, 32 | { 33 | "path": "/404", 34 | "name": "Exception404", 35 | "component": usersAringCodeKoalaFormPackagesDemoWithFesSrcFesPluginLayoutViews404, 36 | "meta": { 37 | "title": "404" 38 | } 39 | } 40 | ] 41 | } 42 | ]; 43 | return routes; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/runtime.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | import { access as accessApi } from '../plugin-access/core'; 3 | // eslint-disable-next-line import/extensions 4 | import getConfig from './helpers/getConfig'; 5 | 6 | if (!accessApi) { 7 | throw new Error('[plugin-layout]: plugin-layout depends on plugin-access,please install plugin-access first!'); 8 | } 9 | 10 | export const access = (memo) => { 11 | const runtimeConfig = getConfig(); 12 | const accessIds = accessApi.getAccess(); 13 | if (!accessIds.includes('/403')) { 14 | accessApi.setAccess(accessIds.concat('/403')); 15 | } 16 | if (!accessIds.includes('/404')) { 17 | accessApi.setAccess(accessIds.concat('/404')); 18 | } 19 | return { 20 | unAccessHandler({ router, to, from, next }) { 21 | if (runtimeConfig.unAccessHandler && typeof runtimeConfig.unAccessHandler === 'function') { 22 | return runtimeConfig.unAccessHandler({ 23 | router, 24 | to, 25 | from, 26 | next, 27 | }); 28 | } 29 | next('/403'); 30 | }, 31 | noFoundHandler({ router, to, from, next }) { 32 | if (runtimeConfig.noFoundHandler && typeof runtimeConfig.noFoundHandler === 'function') { 33 | return runtimeConfig.noFoundHandler({ 34 | router, 35 | to, 36 | from, 37 | next, 38 | }); 39 | } 40 | next('/404'); 41 | }, 42 | ...memo, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /docs/zh/guide/plugin/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 插件API 6 | 7 | 8 | ## describe 9 | 描述插件名 10 | 11 | - 类型 12 | 13 | ```ts 14 | describe(name: string): void 15 | ``` 16 | 17 | ## onStart 18 | 插件可以通过on/emit来进行通信,改方法是插件开始执行的钩子 19 | 20 | - 类型 21 | 22 | ```ts 23 | onStart(handler: PluginHook): void; 24 | ``` 25 | 26 | 其中 27 | ```ts 28 | interface PluginHookData { 29 | id: number; 30 | /** 插件名 */ 31 | name: string; 32 | scopeId: string; 33 | ctx: T; 34 | config: K; 35 | [key: string]: any; 36 | } 37 | declare type PluginHook = (data: PluginHookData) => void; 38 | ``` 39 | 40 | - 说明 41 | 42 | PluginHook函数回传的参数包括当前执行的场景上下文和配置等属性,因此可以在钩子里根据配置处理上下文。 43 | 44 | - 示例 45 | ```js 46 | api.onStart(({ ctx, config }) => { 47 | // do something 48 | }) 49 | ``` 50 | 51 | ## onSelfStart 52 | 仅插件自己的开始执行的钩子 53 | 54 | - 类型 55 | 56 | ```ts 57 | onSelfStart(handler: PluginHook): void; 58 | ``` 59 | 60 | ## onSelf 61 | 仅监听插件自己的事件钩子 62 | 63 | - 类型 64 | 65 | ```ts 66 | onSelf(type: string, handler: PluginHook): void; 67 | ``` 68 | 69 | ## on 70 | 监听插件事件钩子 71 | 72 | - 类型 73 | 74 | ```ts 75 | on(type: string, handler: PluginHook): void; 76 | ``` 77 | 78 | ## emit 79 | 出发事件钩子 80 | 81 | - 类型 82 | 83 | ```ts 84 | emit(type: string, event?: PluginHookData | any): void; 85 | ``` 86 | 87 | - 说明 88 | 89 | 插件直接通信,可以通过emit事件,通知钩子执行,event是事件的参数。 90 | 91 | ## start 92 | 93 | 开始执行插件,并触发start事件。 94 | 95 | ```ts 96 | start(config: K): void; 97 | ``` 98 | 99 | ## destroy 100 | 101 | 插件销毁 102 | 103 | ```ts 104 | destroy(): void; 105 | ``` -------------------------------------------------------------------------------- /cypress/component/useModal.cy.tsx: -------------------------------------------------------------------------------- 1 | import UseModal from '../../docs/examples/started/useModal'; 2 | 3 | describe('UseModal', () => { 4 | it('should open the modal when click the button `Open Modal`', () => { 5 | cy.mount(UseModal); 6 | cy.contains('Open Modal').click(); // 点击‘Open Modal’按钮 7 | cy.get('.fes-modal-container').should('be.visible'); // 验证‘Modal’是否可见 8 | }); 9 | 10 | it('should open the drawer when click the button `Open Drawer`', () => { 11 | cy.mount(UseModal); 12 | cy.contains('Open Drawer').click(); // 点击‘Open Drawer’按钮 13 | cy.get('.fes-drawer-container').should('be.visible'); // 验证‘Drawer’是否可见 14 | }); 15 | 16 | it('should open the modal with correct form fields', () => { 17 | cy.mount(UseModal); 18 | cy.contains('Open Modal').click(); // 点击‘Open Modal’按钮 19 | cy.contains('名字').should('be.visible'); // 验证名字输入框是否出现 20 | cy.contains('年龄').should('be.visible'); // 验证年龄输入框是否出现 21 | }); 22 | 23 | it('should close the modal when click the `Cancel` button', () => { 24 | cy.mount(UseModal); 25 | cy.contains('Open Modal').click(); // 点击‘Open Modal’按钮 26 | cy.get('.fes-modal-container').find('button').contains('取消').click(); // 点击‘Cancel’按钮 27 | cy.get('.fes-modal-container').should('not.be.visible'); // 验证‘Modal’是否不可见 28 | }); 29 | 30 | it('should close the drawer when click the `Cancel` button', () => { 31 | cy.mount(UseModal); 32 | cy.contains('Open Drawer').click(); // 点击‘Open Drawer’按钮 33 | cy.get('.fes-drawer-container').find('button').contains('取消').click(); // 点击‘Cancel’按钮 34 | cy.get('.fes-drawer-container').should('not.be.visible'); // 验证‘Drawer’是否不可见 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /docs/examples/demos/editTable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 57 | -------------------------------------------------------------------------------- /cypress/component/useTable.cy.tsx: -------------------------------------------------------------------------------- 1 | import UseTable from '../../docs/examples/started/useTable'; 2 | describe('UseTable', () => { 3 | it('should display table correctly', () => { 4 | // Visit UseTable component page 5 | const app = cy.mount(UseTable); 6 | 7 | // Check table headers 8 | app.get('thead tr th').eq(0).should('have.text', '姓名'); 9 | app.get('thead tr th').eq(1).should('have.text', '性别'); 10 | app.get('thead tr th').eq(2).should('have.text', '年龄'); 11 | app.get('thead tr th').eq(3).should('have.text', '生日'); 12 | app.get('thead tr th').eq(4).should('have.text', '操作'); 13 | 14 | // Check if there are 2 table rows 15 | app.get('tbody tr').should('have.length', 2); 16 | 17 | // Check the values of the first table row 18 | app.get('tbody tr').eq(0).children().eq(0).should('contain.text', '蒙奇·D·路飞'); 19 | app.get('tbody tr').eq(0).children().eq(1).should('contain.text', '男'); 20 | app.get('tbody tr').eq(0).children().eq(2).should('contain.text', '16'); 21 | app.get('tbody tr').eq(0).children().eq(3).should('contain.text', '2022-02-12'); 22 | app.get('tbody tr').eq(0).children().eq(4).click(); // Click the "详情" button and log to console 23 | 24 | // Check the values of the second table row 25 | app.get('tbody tr').eq(1).children().eq(0).should('contain.text', '罗罗诺亚·索隆'); 26 | app.get('tbody tr').eq(1).children().eq(1).should('contain.text', '男'); 27 | app.get('tbody tr').eq(1).children().eq(2).should('contain.text', '18'); 28 | app.get('tbody tr').eq(1).children().eq(3).should('not.have.text', ''); 29 | app.get('tbody tr').eq(1).children().eq(4).click(); // Click the "详情" button and log to console 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/helpers/fillMenu.js: -------------------------------------------------------------------------------- 1 | const getMetaByName = (config, name) => { 2 | let res = {}; 3 | if (Array.isArray(config)) { 4 | for (let i = 0; i < config.length; i++) { 5 | const item = config[i]; 6 | if (item.meta && item.meta.name === name) { 7 | res = item.meta; 8 | res.path = item.path; 9 | break; 10 | } 11 | if (item.children && item.children.length > 0) { 12 | res = getMetaByName(item.children, name); 13 | if (res.path) { 14 | break; 15 | } 16 | } 17 | } 18 | } 19 | return res; 20 | }; 21 | 22 | const fillMenuByRoute = (menuConfig, routeConfig, dep = 0) => { 23 | dep += 1; 24 | if (dep > 3) { 25 | console.warn('[plugin-layout]: 菜单层级最好不要超出三层!'); 26 | } 27 | const arr = []; 28 | if (Array.isArray(menuConfig) && Array.isArray(routeConfig)) { 29 | menuConfig.forEach((menu) => { 30 | const pageConfig = {}; 31 | if (menu.name) { 32 | Object.assign(pageConfig, getMetaByName(routeConfig, menu.name)); 33 | } 34 | // menu的配置优先级高,当menu存在配置时,忽略页面的配置 35 | Object.keys(pageConfig).forEach((prop) => { 36 | if (menu[prop] === undefined || menu[prop] === null || menu[prop] === '') { 37 | menu[prop] = pageConfig[prop]; 38 | } 39 | }); 40 | if (menu.children && menu.children.length > 0) { 41 | menu.children = fillMenuByRoute(menu.children, routeConfig, dep); 42 | } 43 | arr.push(menu); 44 | }); 45 | } 46 | return arr; 47 | }; 48 | 49 | export default fillMenuByRoute; 50 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/fes.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | createApp, 4 | } from 'vue'; 5 | import { plugin } from './core/plugin'; 6 | import './core/pluginRegister'; 7 | import { ApplyPluginsType } from '/Users/aring/code/koala-form/packages/demo-with-fes/node_modules/.pnpm/@fesjs+runtime@3.0.0_vue@3.3.4/node_modules/@fesjs/runtime'; 8 | import { getRoutes } from './core/routes/routes'; 9 | import DefaultContainer from './defaultContainer.jsx'; 10 | 11 | 12 | 13 | import '../global.less'; 14 | 15 | const renderClient = (opts = {}) => { 16 | const { plugin, routes, rootElement } = opts; 17 | const rootContainer = plugin.applyPlugins({ 18 | type: ApplyPluginsType.modify, 19 | key: 'rootContainer', 20 | initialValue: DefaultContainer, 21 | args: { 22 | routes: routes, 23 | plugin: plugin 24 | } 25 | }); 26 | 27 | const app = createApp(rootContainer); 28 | 29 | plugin.applyPlugins({ 30 | key: 'onAppCreated', 31 | type: ApplyPluginsType.event, 32 | args: { app, routes }, 33 | }); 34 | 35 | if (rootElement) { 36 | app.mount(rootElement); 37 | } 38 | return app; 39 | } 40 | 41 | const getClientRender = (args = {}) => plugin.applyPlugins({ 42 | key: 'render', 43 | type: ApplyPluginsType.compose, 44 | initialValue: () => { 45 | const opts = plugin.applyPlugins({ 46 | key: 'modifyClientRenderOpts', 47 | type: ApplyPluginsType.modify, 48 | initialValue: { 49 | routes: args.routes || getRoutes(), 50 | plugin, 51 | rootElement: '#app', 52 | defaultTitle: `fes.js`, 53 | }, 54 | }); 55 | return renderClient(opts); 56 | }, 57 | args, 58 | }); 59 | 60 | const clientRender = getClientRender(); 61 | 62 | const app = clientRender(); 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 88 | -------------------------------------------------------------------------------- /docs/examples/compose/useTableWithPager2.js: -------------------------------------------------------------------------------- 1 | import { useSceneContext, useTableWithPager } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const { 7 | ctxs: [table, pager], 8 | } = useSceneContext(['table', 'pager']); 9 | 10 | const names = ['蒙奇·D·路飞', '罗罗诺亚·索隆', '山治']; 11 | const doQuery = () => { 12 | dataSource.value = []; 13 | const { pageSize, totalCount, currentPage } = pager.modelRef.value; 14 | const len = Math.ceil(totalCount / pageSize) === currentPage ? totalCount - (currentPage - 1) * pageSize : pageSize; 15 | for (let index = 1; index <= len; index++) { 16 | dataSource.value.push({ 17 | id: index, 18 | name: names[parseInt(Math.random() * 10) % 3], 19 | }); 20 | } 21 | }; 22 | 23 | const { render, dataSource } = useTableWithPager( 24 | { 25 | ctx: table, 26 | fields: [ 27 | { 28 | name: 'id', 29 | label: 'ID', 30 | }, 31 | { 32 | name: 'name', 33 | label: '姓名', 34 | }, 35 | ], 36 | }, 37 | { 38 | ctx: pager, 39 | pager: { 40 | events: { 41 | onChange: doQuery, 42 | }, 43 | }, 44 | }, 45 | ); 46 | 47 | pager.modelRef.value = { 48 | pageSize: 5, 49 | currentPage: 1, 50 | totalCount: 32, 51 | }; 52 | 53 | doQuery(); 54 | 55 | return render; 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /docs/examples/index.js: -------------------------------------------------------------------------------- 1 | import './main'; 2 | import StartedScene from './started/useScene'; 3 | import StartedSceneVue from './started/useScene.vue'; 4 | import StartedUseForm from './started/useForm'; 5 | import StartedUseTable from './started/useTable'; 6 | import StartedUsePager from './started/usePager'; 7 | import StartedUseModal from './started/useModal'; 8 | import StartedUseCurd from './started/useCurd'; 9 | // import ComposeUseTableWithPager from './compose/useTableWithPager'; 10 | // import ComposeUseTableWithPager2 from './compose/useTableWithPager2'; 11 | 12 | import UseFormQuery from './useForm/query'; 13 | import UseFormEdit from './useForm/edit'; 14 | import UseFormRelation from './useForm/relation'; 15 | import UseFormValidate from './useForm/validate'; 16 | 17 | import BaseForm from './base/form'; 18 | import BaseComp from './base/comp'; 19 | import BaseField from './base/field'; 20 | import BaseSlotRender from './base/slotRender.vue'; 21 | 22 | import FesdCurd from './fesdCurd'; 23 | import FesdUseCurd from './fesdUseCurd.vue'; 24 | 25 | import LabelPlugin from './labelPlugin'; 26 | 27 | import ElementCurd from './elementCurd'; 28 | import ElementUseCurd from './elementUseCurd.vue'; 29 | 30 | import AntdCurd from './antdCurd'; 31 | import antdUseCurd from './antdUseCurd.vue'; 32 | 33 | import Demos from './demos/index'; 34 | 35 | export default { 36 | StartedSceneVue, 37 | StartedScene, 38 | StartedUseForm, 39 | StartedUseTable, 40 | StartedUsePager, 41 | StartedUseModal, 42 | StartedUseCurd, 43 | UseFormQuery, 44 | UseFormEdit, 45 | UseFormRelation, 46 | UseFormValidate, 47 | BaseForm, 48 | BaseComp, 49 | BaseField, 50 | BaseSlotRender, 51 | FesdCurd, 52 | FesdUseCurd, 53 | LabelPlugin, 54 | ElementCurd, 55 | ElementUseCurd, 56 | AntdCurd, 57 | antdUseCurd, 58 | ...Demos, 59 | }; 60 | -------------------------------------------------------------------------------- /examples/demo-with-vue/README.md: -------------------------------------------------------------------------------- 1 | # demo-with-vue 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Lint with [ESLint](https://eslint.org/) 43 | 44 | ```sh 45 | npm run lint 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/core/src/plugins/formRule.ts: -------------------------------------------------------------------------------- 1 | import { FormSceneConfig, FormSceneContext } from '../useForm'; 2 | import { mergeRefProps } from '../helper'; 3 | import { Field, findScheme, ValidateRule } from '../scheme'; 4 | import { PluginFunction } from './define'; 5 | import { computed, unref } from 'vue'; 6 | 7 | const parseFieldRule = (field: Field): Array => { 8 | const rules = unref(field.rules) || []; 9 | const required = unref(field.required); 10 | if (rules.some((rule) => rule.required)) { 11 | return rules; 12 | } else if (required) { 13 | return [{ required: true, message: '必填项', type: field.type }, ...rules]; 14 | } else { 15 | return rules || []; 16 | } 17 | }; 18 | 19 | export const formRulePlugin: PluginFunction = (api) => { 20 | api.describe('form-rule-plugin'); 21 | 22 | api.on('formSchemeLoaded', ({ ctx, config: { fields } }) => { 23 | if (!fields) return; 24 | fields.forEach((field) => { 25 | if (!field.name) return; 26 | const scheme = findScheme(ctx.schemes, field); 27 | if (!scheme || !(field.required || field.rules)) return; 28 | const rules = computed(() => parseFieldRule(field)); 29 | mergeRefProps(scheme, 'props', { rules, prop: field.name }); 30 | }); 31 | 32 | ctx.validate = async (names) => { 33 | await ctx.formRef.value?.validate(names); 34 | }; 35 | 36 | ctx.clearValidate = () => { 37 | ctx.formRef.value?.clearValidate(); 38 | }; 39 | 40 | api.emit('started'); 41 | }); 42 | }; 43 | 44 | export const doValidate = async (ctx: FormSceneContext, names?: string[]) => { 45 | await ctx?.validate(names); 46 | }; 47 | 48 | export const doClearValidate = (ctx: FormSceneContext) => { 49 | ctx?.clearValidate(); 50 | }; 51 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-access/runtime.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { plugin, ApplyPluginsType } from '@@/core/coreExports'; 3 | // eslint-disable-next-line import/extensions, import/no-unresolved 4 | import { access, install } from './core'; 5 | 6 | export function onRouterCreated({ router }) { 7 | router.beforeEach(async (to, from, next) => { 8 | const runtimeConfig = plugin.applyPlugins({ 9 | key: 'access', 10 | type: ApplyPluginsType.modify, 11 | initialValue: {}, 12 | }); 13 | if (to.matched.length === 0) { 14 | if (runtimeConfig.noFoundHandler && typeof runtimeConfig.noFoundHandler === 'function') { 15 | return runtimeConfig.noFoundHandler({ 16 | router, 17 | to, 18 | from, 19 | next, 20 | }); 21 | } 22 | return next(false); 23 | } 24 | if (Array.isArray(runtimeConfig.ignoreAccess)) { 25 | const isIgnored = await access.match(to.matched[to.matched.length - 1].path, runtimeConfig.ignoreAccess); 26 | if (isIgnored) { 27 | return next(); 28 | } 29 | } 30 | // path是匹配路由的path,不是页面hash 31 | const canRoute = await access.hasAccess(to.matched[to.matched.length - 1].path); 32 | if (canRoute) { 33 | return next(); 34 | } 35 | if (runtimeConfig.unAccessHandler && typeof runtimeConfig.unAccessHandler === 'function') { 36 | return runtimeConfig.unAccessHandler({ 37 | router, 38 | to, 39 | from, 40 | next, 41 | }); 42 | } 43 | next(false); 44 | }); 45 | } 46 | 47 | export function onAppCreated({ app }) { 48 | install(app); 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/handles/index.ts: -------------------------------------------------------------------------------- 1 | import { getGlobalConfig } from '../base'; 2 | import { FormSceneContext, doGetFormData } from '../useForm'; 3 | import { PagerSceneContext } from '../usePager'; 4 | import { TableSceneContext } from '../useTable'; 5 | /** 6 | * 网络接口请求 7 | */ 8 | export const doRequest = async (api: string, params?: Record, opt?: Record) => { 9 | const globalConfig = getGlobalConfig(); 10 | if (!globalConfig.request) throw new Error('doRequest: globalConfig.request not found, please config request by setupGlobalConfig'); 11 | if (!api) throw new Error('doRequest: config.api not found!'); 12 | return await globalConfig.request(api, params, opt); 13 | }; 14 | 15 | /** 16 | * 查询之前参数整理,handle的结果格式如下: 17 | * ``` 18 | * { 19 | * ...formModel, // 表单字段 20 | * page?: { // 分页数据 21 | * pageSize, 22 | * currentPage 23 | * } 24 | * } 25 | * ``` 26 | * 如接口参数格式不是适配,可以后面新增一个handle进行处理 27 | * @param pager 分页上下文 28 | * @param form 表单上下文 29 | * @returns 30 | */ 31 | export const doBeforeQuery = ( 32 | form: FormSceneContext, 33 | pager?: PagerSceneContext, 34 | ): { 35 | [key: string]: any; 36 | page?: { 37 | pageSize: number; 38 | currentPage: number; 39 | }; 40 | } => { 41 | const { currentPage, pageSize } = pager?.modelRef.value || ({} as PagerSceneContext['modelRef']['value']); 42 | return doGetFormData(form, { page: { currentPage, pageSize } }); 43 | }; 44 | 45 | /** 46 | * 解析查询结果解析到分页和列表上 47 | * @param pager 分页上下文 48 | * @param table 列表上下文 49 | * @returns 50 | */ 51 | export const doAfterQuery = (table: TableSceneContext, pager?: PagerSceneContext, data?: { list: any[]; page: PagerSceneContext['modelRef']['value'] }) => { 52 | if (pager?.modelRef) { 53 | pager.modelRef.value.totalCount = data?.page?.totalCount || 0; 54 | } 55 | if (table?.modelRef?.value) { 56 | table.modelRef.value = data?.list || []; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-request/preventRepeatReq.js: -------------------------------------------------------------------------------- 1 | const requestMap = new Map(); 2 | 3 | const mergeRequestMap = new Map(); 4 | const requestQueue = new Map(); 5 | 6 | function handleCachingStart(ctx) { 7 | const isRequesting = mergeRequestMap.get(ctx.key); 8 | if (isRequesting) { 9 | return new Promise((resolve) => { 10 | const queue = requestQueue.get(ctx.key) || []; 11 | requestQueue.set(ctx.key, queue.concat(resolve)); 12 | }); 13 | } 14 | mergeRequestMap.set(ctx.key, true); 15 | } 16 | 17 | function handleRepeatRequest(ctx) { 18 | const queue = requestQueue.get(ctx.key); 19 | if (queue && queue.length > 0) { 20 | queue.forEach((resolve) => { 21 | if (ctx.error) { 22 | resolve({ 23 | error: ctx.error, 24 | }); 25 | } else { 26 | resolve({ 27 | response: ctx.response, 28 | }); 29 | } 30 | }); 31 | } 32 | requestQueue.delete(ctx.key); 33 | mergeRequestMap.delete(ctx.key); 34 | } 35 | 36 | export default async (ctx, next) => { 37 | if (ctx.config.mergeRequest) { 38 | const result = await handleCachingStart(ctx); 39 | if (result) { 40 | Object.keys(result).forEach((key) => { 41 | ctx[key] = result[key]; 42 | }); 43 | return; 44 | } 45 | } else { 46 | if (requestMap.get(ctx.key) && !ctx.config.mergeRequest) { 47 | ctx.error = { 48 | type: 'REPEAT', 49 | msg: '重复请求', 50 | config: ctx.config, 51 | }; 52 | return; 53 | } 54 | requestMap.set(ctx.key, true); 55 | } 56 | 57 | await next(); 58 | 59 | if (ctx.config.mergeRequest) { 60 | handleRepeatRequest(ctx); 61 | } else { 62 | requestMap.delete(ctx.key); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/components/Wrapper.vue: -------------------------------------------------------------------------------- 1 | 11 | 43 | 77 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/core/src/plugins/slotPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Slot, Slots } from 'vue'; 2 | import { SceneContext, SceneConfig, getGlobalConfig } from '../base'; 3 | import { mergeRefProps, travelTree } from '../helper'; 4 | import { ComponentDesc, Field, Scheme } from '../scheme'; 5 | import { PluginFunction } from './define'; 6 | 7 | const parseSlotName = (_slots: Slots, scheme: Scheme, node: ComponentDesc | Field, ctx: SceneContext) => { 8 | const slots: Record = {}; 9 | const slotNameRule = `${node.slotName}__`; 10 | Object.keys(_slots).forEach((key) => { 11 | let name = ''; 12 | if (key === node.slotName || `${slotNameRule}default` === key) { 13 | name = 'default'; 14 | } else if (key.startsWith(slotNameRule)) { 15 | name = key.slice(slotNameRule.length); 16 | } 17 | if (name) { 18 | slots[name] = _slots[key] as Slot; 19 | getGlobalConfig().debug && console.log(`【${ctx.name}】通过slotName(${node.slotName})解析出${scheme.component}组件${name}的插槽`); 20 | } 21 | }); 22 | mergeRefProps(scheme, 'slots', slots); 23 | }; 24 | 25 | export const slotPlugin: PluginFunction = (api) => { 26 | api.describe('slot-plugin'); 27 | 28 | api.on('started', ({ name, ctx }) => { 29 | if (name === 'render-plugin') { 30 | const render = ctx.render; 31 | ctx.render = (slots) => { 32 | travelTree(ctx.schemes, (scheme) => { 33 | if ((scheme?.__node as ComponentDesc)?.slots) { 34 | mergeRefProps(scheme, 'slots', (scheme?.__node as ComponentDesc)?.slots); 35 | } 36 | if (slots && (scheme?.__node as ComponentDesc)?.slotName) { 37 | parseSlotName(slots, scheme, scheme.__node as ComponentDesc, ctx); 38 | } 39 | }); 40 | return render(slots); 41 | }; 42 | api.emit('started'); 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/index.jsx: -------------------------------------------------------------------------------- 1 | import { unref, defineComponent, computed } from 'vue'; 2 | import { plugin } from '@@/core/coreExports'; 3 | import { getRoutes } from '@@/core/routes/routes'; 4 | // eslint-disable-next-line import/extensions 5 | import getConfig from '../helpers/getConfig'; 6 | import fillMenu from '../helpers/fillMenu'; 7 | import BaseLayout from './BaseLayout.vue'; 8 | 9 | const Layout = defineComponent({ 10 | name: 'Layout', 11 | setup() { 12 | const config = getConfig(); 13 | 14 | const menus = typeof config.menus === 'function' ? config.menus() : config.menus; 15 | 16 | const routes = getRoutes(); 17 | 18 | // 把路由的 meta 合并到 menu 配置中 19 | const filledMenuRef = computed(() => fillMenu(unref(menus) ?? [], routes)); 20 | 21 | const localeShared = plugin.getShared('locale'); 22 | 23 | return () => { 24 | const slots = { 25 | renderCustom: config.renderCustom, 26 | locale: () => { 27 | if (localeShared) { 28 | return ; 29 | } 30 | return null; 31 | }, 32 | }; 33 | return ( 34 | 49 | ); 50 | }; 51 | }, 52 | }); 53 | 54 | export default Layout; 55 | -------------------------------------------------------------------------------- /packages/fes-plugin/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | koala 5 | 6 |

7 |

Koala-Form

8 | 9 |
10 | 11 | 低代码表单解决方案,让你跟考拉一样“懒” 12 | 13 | [![GitHub issues](https://img.shields.io/github/issues/WeBankFinTech/KoalaForm.svg?style=flat-square)](../../issues) 14 | [![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT) 15 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](../../pulls) 16 | [![Page Views Count](https://badges.toozhao.com/badges/01H51S4REBN596ZZ2BTNVV6566/green.svg)](https://badges.toozhao.com/stats/01H51S4REBN596ZZ2BTNVV6566 "Get your own page views count badge on badges.toozhao.com") 17 | 18 |
19 | 20 | - 使用文档 - [https://koala-form.mumblefe.cn/zh/ui/fes.html](https://koala-form.mumblefe.cn/zh/ui/fes.html) 21 | 22 | ## Install 23 | 24 | ```bash 25 | npm i @koala-form/core 26 | npm i @koala-form/fes-plugin 27 | ``` 28 | 29 | ## Usage 30 | 注册全局插件 31 | ```js 32 | import '@koala-form/fes-plugin'; 33 | import { installPluginPreset } from '@koala-form/core'; 34 | 35 | // 将依赖的插件安装到全局 36 | installPluginPreset(); 37 | ``` 38 | 写一个简单的表单 39 | ```html 40 | 43 | 44 | 68 | ``` 69 | 70 | 71 | ## 反馈 -------------------------------------------------------------------------------- /docs/zh/guide/base/form.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | ## 全局配置 6 | 在入口文件中,配置全局应用,比如插件、网络请求等。 7 | 8 | <<< @/examples/main.js 9 | 10 | ## 使用表单场景 11 | 表单抽象为一个场景,使用useForm,可以快速的建立一个表单。返回上下文对象 12 | ```js 13 | const ctx = useForm({ fields: [] }) 14 | // ctx.render // 渲染方法 15 | // ctx.modelRef // form响应对象 16 | // ...更多操作表单的方法 17 | ``` 18 | 19 | ## 添加字段 20 | 每个表单都包含多个字段,用于搜集用户输入,字段的表现有各种形式,比如输入框、下拉选择框、单选、多选等,使用Field描述表单的字段,定义字段的标签描述、属性名、默认值、组件等。 21 | ```js 22 | const name = { 23 | name: 'name', // modelRef.value.name可以访问到值 24 | label: '姓名', // 表单项的名称 25 | defaultValue: '蒙奇·D·路飞', // 默认值 26 | components: { 27 | name: ComponentType.Input, // 表单组件是输入框 28 | }, 29 | } 30 | 31 | useForm({ fields: [name] }) 32 | 33 | ``` 34 | 35 | ## 添加操作 36 | 用户填写完表单后,会把数据提交给服务,所以我们需要给表单添加提交操作,需要用到ComponentDesc定义按钮的内容、属性和响应事件等。 37 | ```js 38 | 39 | // ... 40 | const submit = { 41 | name: ComponentType.Button, // 按钮组件 42 | children: ['提交'], // 按钮内容 43 | props: { type: 'primary' }, // 组件的属性 44 | events: { 45 | // 按钮的事件 46 | onClick: (event) => { 47 | console.log(event); 48 | }, 49 | }, 50 | } 51 | 52 | useForm({ 53 | fields: [ 54 | name, 55 | { // 添加一行,放置表单操作 56 | label: ' ', 57 | components: [submit] 58 | } 59 | ] 60 | }) 61 | 62 | ``` 63 | 64 | ## 上下文 65 | useForm返回场景上下文,可以进行表单重置、校验、渲染等。 66 | ```js 67 | // ... 68 | 69 | const ctx = useForm({}); 70 | 71 | ctx.initFields({name: 'aring'}) // 初始默认值 72 | 73 | ctx.resetFields() // 重置 74 | 75 | ctx.validate() // 校验 76 | 77 | ctx.render() // 渲染 78 | 79 | ``` 80 | 81 | ## 多个表单 82 | useSceneContext可以预创建上下文,同时有多个表单时,可以先创建上下文。 83 | ```js 84 | const [form1, form2] = useSceneContext(['form1', 'form2']); 85 | useForm({ ctx: form1 }); 86 | useForm({ ctx: form2 }); 87 | ``` 88 | 89 | ## 完整表单 90 | 场景上下文render方法可以渲染出表单。 91 | 92 | 93 | 94 | 95 | 100 | -------------------------------------------------------------------------------- /docs/examples/started/useModal.jsx: -------------------------------------------------------------------------------- 1 | import { FButton, FMessage, FSpace } from '@fesjs/fes-design'; 2 | import { useModal, ComponentType, doOpen, useForm } from '@koala-form/core'; 3 | import { defineComponent } from 'vue'; 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const form = useForm({ 8 | fields: [ 9 | { 10 | label: '名字', 11 | name: 'name', 12 | components: { name: ComponentType.Input }, 13 | }, 14 | { 15 | label: '年龄', 16 | name: 'age', 17 | components: { name: ComponentType.InputNumber }, 18 | }, 19 | ], 20 | }); 21 | 22 | const modal1 = useModal({ 23 | title: 'Modal', 24 | modal: { 25 | events: { 26 | onOk() { 27 | FMessage.success('点击了OK'); 28 | }, 29 | onCancel() { 30 | FMessage.success('点击了Cancel'); 31 | }, 32 | }, 33 | children: form, // 嵌套表单场景 34 | }, 35 | }); 36 | 37 | const modal2 = useModal({ 38 | title: 'Drawer', 39 | modal: { 40 | name: ComponentType.Drawer, 41 | props: { footer: true }, 42 | events: { 43 | onOk() { 44 | FMessage.success('点击了OK'); 45 | }, 46 | onCancel() { 47 | FMessage.success('点击了Cancel'); 48 | }, 49 | }, 50 | children: 'Drawer内容', 51 | }, 52 | }); 53 | 54 | return () => ( 55 | 56 | doOpen(modal1)}>Open Modal 57 | doOpen(modal2)}>Open Drawer 58 | {modal1.render()} 59 | {modal2.render()} 60 | 61 | ); 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/MenuIcon.vue: -------------------------------------------------------------------------------- 1 | 49 | 64 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/app.jsx: -------------------------------------------------------------------------------- 1 | import { access, defineRuntimeConfig, request } from '@fesjs/fes'; 2 | 3 | import PageLoading from '@/components/pageLoading.vue'; 4 | import UserCenter from '@/components/userCenter.vue'; 5 | import { installInGlobal, installPluginPreset, setupGlobalConfig } from '@koala-form/core'; 6 | 7 | installPluginPreset(); 8 | setupGlobalConfig({ 9 | request: request 10 | }) 11 | 12 | export default defineRuntimeConfig({ 13 | beforeRender: { 14 | loading: , 15 | action() { 16 | const { setRole } = access; 17 | return new Promise((resolve) => { 18 | setTimeout(() => { 19 | setRole('admin'); 20 | // 初始化应用的全局状态,可以通过 useModel('@@initialState') 获取,具体用法看@/components/UserCenter 文件 21 | resolve({ 22 | userName: '李雷', 23 | }); 24 | }, 1000); 25 | }); 26 | }, 27 | }, 28 | layout: { 29 | renderCustom: () => , 30 | }, 31 | request: { 32 | baseURL: '/', 33 | timeout: 10000, // 默认 10s 34 | method: 'get', // 默认 post 35 | mergeRequest: false, // 是否合并请求 36 | cacheData: false, // 是否缓存 37 | dataHandler(data, response) { 38 | // 处理响应内容异常 39 | if (data.code === '10000') { 40 | return Promise.reject(data); 41 | } 42 | return data?.result ? data.result : data; 43 | }, 44 | // http 异常,和插件异常 45 | errorHandler(error) { 46 | if (error.response) { 47 | // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围 48 | console.log(error.response.data); 49 | console.log(error.response.status); 50 | console.log(error.response.headers); 51 | } else if (error.msg) { 52 | console.log(error.msg); 53 | } else { 54 | // 发送请求时出了点问题 55 | console.log('Error', error.message); 56 | } 57 | console.log(error.config); 58 | }, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /docs/zh/guide/index.md: -------------------------------------------------------------------------------- 1 | # Koala Form是什么 2 | 3 | 对于中后台产品的前端开发来说,最常见的场景无非是开发一个表的CURD操作: 4 | - **Create:** 创建一条新的记录 5 | 6 | - **Update:** 更新一条新的记录 7 | 8 | - **Retrieve:** 读取一条记录 9 | 10 | - **Delete:** 删除一条记录 11 | 12 | ### 举个🌰 13 | 需要实现一个用户管理页面,包含`条件查询`、`用户新增`、`用户更新`、`用户删除`、`用户详情`功能。开发步骤如下: 14 | - **Step1:** 编写一个查询`Form`用于填写查询条件,点击查询按钮后,执行接口调用。 15 | 16 | - **Step2:** 编写一个`Table`用于展示数据,需要定义哪些字段展示,并把格式化展示查询结果。 17 | 18 | - **Step3:** 编写一个用户新增`Form`用于录入用户信息,点击新增后执行接口调用。 19 | 20 | - **Step4:** 编写一个用户更新`Form`用于更改用户信息,点击更新后执行接口调用。 21 | 22 | - **Step5:** 编写一个用户详情信息展示的`Form`,执行查询详情接口或从Table中获取用户数据 23 | 24 | - **Step6:** 编写一个删除用户确认提示,点击确定后执行接口调用。 25 | 26 | - **Step7:** 编写CURD的操作按钮,并编写按钮打开对应的的表单。 27 | 28 | 接下来又要新增一个角色管理页面,一样是CURD的功能,重复上面的步骤就会发现,两个页面除了字段和接口不同,大概有80%的其他逻辑基本一样,但还是少不了那些胶水代码。 29 | 30 | **而 Koala Form 可以帮你减少这80%的胶水代码。** 31 | 32 | **Koala Form** 是一个表单页面的低代码解决方案。以Vue3为基础,围绕后台产品的表单场景进行封装,使得开发者仅需关注表单页面的字段和接口。 33 | 34 | 它主要具备以下特点: 35 | - **高效的:** 从零开发一个完整的表单页面也许需要你花一天或者几个小时,而Koala From也许仅需几分钟,你需要做的就配置字段的展示规则。 36 | 37 | - **简单的:** 内置基础的表单场景,`useScene`, `useFrom`、`useTable`、`useModal`、`usePager`, 根据传入的字段规则解析,返回场景上下文用于操作场景内容,render函数更是减少了你对UI的关注。 38 | 39 | - **灵活的:** 丰富的场景可以自由组合;所有的字段也支持vue`slot`; 可扩展自己的插件,render自己的UI。 40 | 41 | 42 | ### 基础概念 43 | - **字段描述** 44 | 45 | 字段常见于表单和列表中,因此用`Field`对字段单独描述,关键属性: 46 | - 字段名 47 | - 字段描述 48 | - 字段规则 49 | - 字段组件 50 | 51 | - **组件描述** 52 | 53 | 组件用`ComponentDesc`描述,关键属性: 54 | - 组件名 55 | - 组件事件 56 | - 组件属性 57 | - children 58 | 59 | children是用于实现多个组件的并列和嵌套的组件树。 60 | 61 | - **场景** 62 | 63 | 场景是对重复功能的聚合描述,将重复的逻辑隐藏在场景里,提供一些便利的api进行功能操作。 64 | 65 | 比如useForm是负责表单的功能,提供validate、resetFields等api; 66 | 67 | 场景定义配置(即SceneConfig),场景执行之后返回场景上下文(即SceneContext)。 68 | 69 | - **插件** 70 | 71 | 插件是运行在场景里面,插件可扩展场景的配置和上下文,定制场景的功能。 72 | 73 | 插件之间可以通过emit/on发布订阅的模式进行通信。 74 | 75 | - **handler函数** 76 | 77 | handler函数是将一系列操作分解成N个单项操作,handler就是单项操作。 78 | 79 | 使用时选择合适的handler,并灵活插入一些自定义的操作,组合成一些列完整的逻辑。 80 | 81 | 比如按查询列表数据可以分解成: 取表单数据 -> 取pager数据 -> 合并处理(自定义) -> 提交获取数据 -> 设置列表数据 -> 设置分页数据 82 | 83 | - **预设函数** 84 | 85 | 预设函数是获取一些固定的handler组合和场景描述。能快速的辅助场景的配置。 86 | 87 | 比如查询表单,提供预设函数返回固定的查询按钮、重置按钮。 -------------------------------------------------------------------------------- /packages/element-plugin/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | koala 5 | 6 |

7 |

Koala-Form

8 |
9 | 10 | 低代码表单解决方案,让你跟考拉一样“懒” 11 | 12 | [![GitHub issues](https://img.shields.io/github/issues/WeBankFinTech/KoalaForm.svg?style=flat-square)](../../issues) 13 | [![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT) 14 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](../../pulls) 15 | [![Page Views Count](https://badges.toozhao.com/badges/01H51S4REBN596ZZ2BTNVV6566/green.svg)](https://badges.toozhao.com/stats/01H51S4REBN596ZZ2BTNVV6566 "Get your own page views count badge on badges.toozhao.com") 16 | 17 |
18 | 19 | - 使用文档 - [https://koala-form.mumblefe.cn/zh/ui/element.html](https://koala-form.mumblefe.cn/zh/ui/element.html) 20 | 21 | ## Install 22 | 23 | ```bash 24 | npm i @koala-form/core 25 | npm i @koala-form/element-plugin 26 | ``` 27 | 28 | ## Usage 29 | 注册全局插件 30 | ```js 31 | import { componentPlugin } '@koala-form/element-plugin'; 32 | import { installPluginPreset, installInGlobal } from '@koala-form/core'; 33 | 34 | // 将依赖的插件安装到全局 35 | installPluginPreset(); 36 | 37 | installInGlobal(componentPlugin) 38 | ``` 39 | 写一个简单的表单 40 | ```html 41 | 44 | 45 | 69 | ``` 70 | 71 | 72 | ## 反馈 -------------------------------------------------------------------------------- /packages/antd-plugin/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | koala 5 | 6 |

7 |

Koala-Form Ant Design Vue插件

8 |
9 | 10 | 低代码表单解决方案,让你跟考拉一样“懒” 11 | 12 | [![GitHub issues](https://img.shields.io/github/issues/WeBankFinTech/KoalaForm.svg?style=flat-square)](../../issues) 13 | [![MIT](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](http://opensource.org/licenses/MIT) 14 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](../../pulls) 15 | [![Page Views Count](https://badges.toozhao.com/badges/01H51S4REBN596ZZ2BTNVV6566/green.svg)](https://badges.toozhao.com/stats/01H51S4REBN596ZZ2BTNVV6566 "Get your own page views count badge on badges.toozhao.com") 16 | 17 |
18 | 19 | - 使用文档 - [https://koala-form.mumblefe.cn/zh/ui/antd.html](https://koala-form.mumblefe.cn/zh/ui/antd.html) 20 | 21 | ## Install 22 | 23 | ```bash 24 | npm i @koala-form/core 25 | npm i @koala-form/antd-plugin 26 | ``` 27 | 28 | ## Usage 29 | 注册全局插件 30 | ```js 31 | import { componentPlugin } from '@koala-form/antd-plugin'; 32 | import { installPluginPreset, installInGlobal } from '@koala-form/core'; 33 | 34 | // 将依赖的插件安装到全局 35 | installPluginPreset(); 36 | 37 | installInGlobal(componentPlugin) 38 | ``` 39 | 写一个简单的表单 40 | ```html 41 | 44 | 45 | 69 | ``` 70 | 71 | 72 | ## 反馈 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koala-form", 3 | "version": "2.0.5", 4 | "description": "vue form helper", 5 | "author": "aringlai", 6 | "license": "MIT", 7 | "scripts": { 8 | "docs:dev": "vitepress dev docs", 9 | "docs:build": "vitepress build docs", 10 | "docs:serve": "vitepress serve docs", 11 | "cypress:open": "cypress open", 12 | "build": "lerna exec --scope @koala-form/$PKG tsc", 13 | "publish": "cd packages/$PKG && npm publish", 14 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" 15 | }, 16 | "dependencies": { 17 | "@fesjs/fes-design": "0.7.15", 18 | "less": "4.1.2", 19 | "lodash-es": "4.17.15", 20 | "vue": "^3.2.27", 21 | "ant-design-vue": "^4.0.0", 22 | "element-plus": "^2.3.7", 23 | "dayjs": "^1.11.9", 24 | "@koala-form/core": "^2.0.0-rc.8", 25 | "@koala-form/fes-plugin": "^2.0.0", 26 | "@koala-form/element-plugin": "^2.0.0", 27 | "@koala-form/antd-plugin": "^2.0.0" 28 | }, 29 | "workspaces": [ 30 | "packages/*" 31 | ], 32 | "private": true, 33 | "devDependencies": { 34 | "@babel/core": "7.22.1", 35 | "@babel/plugin-transform-runtime": "^7.17.0", 36 | "@babel/preset-env": "7.15.0", 37 | "@babel/preset-typescript": "^7.16.7", 38 | "@betit/rollup-plugin-rename-extensions": "^0.1.0", 39 | "@rollup/plugin-babel": "^5.3.0", 40 | "@rollup/plugin-commonjs": "^21.0.1", 41 | "@rollup/plugin-json": "^4.1.0", 42 | "@rollup/plugin-node-resolve": "^13.1.3", 43 | "@types/lodash-es": "4.17.5", 44 | "@vitejs/plugin-vue-jsx": "1.3.0", 45 | "@vue/repl": "0.4.4", 46 | "@vue/theme": "0.1.16", 47 | "@webank/eslint-config-ts": "1.0.0", 48 | "@webank/eslint-config-webank": "1.1.1", 49 | "eslint-plugin-react": "7.27.1", 50 | "fs-extra": "^10.0.0", 51 | "lerna": "6.6.2", 52 | "rollup": "^2.68.0", 53 | "rollup-plugin-postcss": "^4.0.2", 54 | "rollup-plugin-vue": "^6.0.0", 55 | "typescript": "5.6.2", 56 | "vitepress": "0.21.6", 57 | "cypress": "12.13.0", 58 | "@vitejs/plugin-vue": "^4.2.3", 59 | "npm-run-all": "4.1.5", 60 | "commitizen": "^4.2.1", 61 | "vite": "^4.4.7", 62 | "cz-conventional-changelog": "^3.3.0", 63 | "conventional-changelog-cli": "^2.2.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/examples/started/useTable.js: -------------------------------------------------------------------------------- 1 | import { FMessage } from '@fesjs/fes-design'; 2 | import { ComponentType, formatByOptions, useTable, genFormatByDate } from '@koala-form/core'; 3 | import { defineComponent } from 'vue'; 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const { render, modelRef } = useTable({ 8 | fields: [ 9 | { 10 | name: 'name', // 每行数据取值的key 11 | label: '姓名', // 行标题 12 | }, 13 | { 14 | name: 'sex', 15 | label: '性别', 16 | options: [ 17 | // 枚举映射 18 | { value: '0', label: '女' }, 19 | { value: '1', label: '男' }, 20 | ], 21 | format: formatByOptions, // 格式化展示值 22 | }, 23 | { 24 | name: 'age', 25 | label: '年龄', 26 | }, 27 | { 28 | name: 'birthday', 29 | label: '生日', 30 | format: genFormatByDate(), 31 | }, 32 | { 33 | label: '操作', 34 | props: { width: 100 }, 35 | components: { 36 | name: ComponentType.Button, 37 | children: ['详情'], 38 | props: { type: 'link' }, 39 | events: { 40 | onClick: (record, event) => { 41 | // record只有在useTable下才会存在 42 | FMessage.success(record.row.name); 43 | console.log(record, event); 44 | }, 45 | }, 46 | }, 47 | }, 48 | ], 49 | }); 50 | 51 | modelRef.value = [ 52 | { name: '蒙奇·D·路飞', sex: '1', age: 16, birthday: '2022-02-12' }, 53 | { 54 | name: '罗罗诺亚·索隆', 55 | sex: '1', 56 | age: 18, 57 | birthday: Date.now() + '', 58 | }, 59 | ]; 60 | return render; 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /cypress/component/Utils.cy.tsx: -------------------------------------------------------------------------------- 1 | import { mergeWithStrategy, travelTree, turnArray } from '@koala-form/core'; 2 | import { reactive, ref } from 'vue'; 3 | 4 | describe('工具方法', () => { 5 | it('turnArray', () => { 6 | expect(turnArray(1)[0]).eq(1); 7 | expect(turnArray([1, 2])[1]).eq(2); 8 | expect(turnArray()[0]).eq(undefined); 9 | }); 10 | 11 | it('travelTree', () => { 12 | const root = [ 13 | { 14 | name: '1', 15 | children: [ 16 | { 17 | name: '2-1', 18 | }, 19 | { 20 | name: '2-2', 21 | children: [{ name: '3-1' }], 22 | }, 23 | ], 24 | }, 25 | ]; 26 | const path = ['1', '2-1', '2-2', '3-1']; 27 | const tPath: typeof path = []; 28 | travelTree(root, (node) => { 29 | tPath.push(node.name); 30 | }); 31 | 32 | expect(tPath.join(',')).eq(path.join(',')); 33 | }); 34 | 35 | it('mergeWithStrategy', () => { 36 | const obj: Record = { 37 | id: 1, 38 | children: [1, 2, 3], 39 | address: { 40 | province: '湖南', 41 | city: '长沙', 42 | }, 43 | labelRef: 1, 44 | }; 45 | const newAddress = reactive({ province: '广东' }); 46 | mergeWithStrategy(obj, { id: 2, name: 'aring', children: [11, 22] }); 47 | expect(obj.id).eq(2); 48 | expect(obj.name).eq('aring'); 49 | expect(obj.children.join()).eq('11,22,3'); 50 | 51 | mergeWithStrategy(obj, { children: [4] }, { array: 'concat' }); 52 | expect(obj.children.join()).eq('11,22,3,4'); 53 | 54 | mergeWithStrategy(obj, { children: [5, 6] }, { array: 'override' }); 55 | expect(obj.children.join()).eq('5,6'); 56 | 57 | mergeWithStrategy(obj, { address: newAddress, labelRef: ref(2) }); 58 | 59 | expect(obj.address.province).eq('广东'); 60 | expect(obj.address.city).eq(undefined); 61 | expect(obj.labelRef.value).eq(2); 62 | 63 | newAddress.province = '广西'; 64 | expect(obj.address.province).eq('广西'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-request/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | *判断类型 3 | * @param {*} obj 需要判断的对象 4 | */ 5 | export function typeOf(obj) { 6 | const map = { 7 | '[object Boolean]': 'boolean', 8 | '[object Number]': 'number', 9 | '[object String]': 'string', 10 | '[object Function]': 'function', 11 | '[object Array]': 'array', 12 | '[object Date]': 'date', 13 | '[object RegExp]': 'regExp', 14 | '[object Undefined]': 'undefined', 15 | '[object Null]': 'null', 16 | '[object Object]': 'object', 17 | '[object URLSearchParams]': 'URLSearchParams', 18 | }; 19 | return map[Object.prototype.toString.call(obj)]; 20 | } 21 | 22 | export function isFunction(obj) { 23 | return typeOf(obj) === 'function'; 24 | } 25 | 26 | export function isDate(obj) { 27 | return typeOf(obj) === 'date'; 28 | } 29 | 30 | export function isString(obj) { 31 | return typeOf(obj) === 'string'; 32 | } 33 | 34 | export function isArray(obj) { 35 | return typeOf(obj) === 'array'; 36 | } 37 | 38 | export function isObject(obj) { 39 | return typeOf(obj) === 'object'; 40 | } 41 | 42 | export function isURLSearchParams(obj) { 43 | return typeOf(obj) === 'URLSearchParams'; 44 | } 45 | 46 | export function checkHttpRequestHasBody(method) { 47 | method = method.toUpperCase(); 48 | const HTTP_METHOD = { 49 | GET: { 50 | request_body: false, 51 | }, 52 | POST: { 53 | request_body: true, 54 | }, 55 | PUT: { 56 | request_body: true, 57 | }, 58 | DELETE: { 59 | request_body: true, 60 | }, 61 | HEAD: { 62 | request_body: false, 63 | }, 64 | OPTIONS: { 65 | request_body: false, 66 | }, 67 | PATCH: { 68 | request_body: true, 69 | }, 70 | }; 71 | return HTTP_METHOD[method].request_body; 72 | } 73 | 74 | export function trimObj(obj) { 75 | if (isObject(obj)) { 76 | Object.entries(obj).forEach(([key, value]) => { 77 | if (isString(value)) { 78 | obj[key] = value.trim(); 79 | } else if (isObject(value)) { 80 | trimObj(value); 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/demo-with-vue/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #EEE; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | /* @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } */ 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: color 0.5s, background-color 0.5s; 66 | line-height: 1.6; 67 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 68 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 69 | font-size: 15px; 70 | text-rendering: optimizeLegibility; 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | } 74 | -------------------------------------------------------------------------------- /docs/zh/guide/scene/useTable.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # useTable 5 | 6 | 表格展示数据,最主要的就是字段定义、行数据操作和数据格式转换 7 | 8 | useTable提供了一些能力,更好的使用表格。 9 | 10 | ```js 11 | const ctx = useTable({ 12 | fields: [ // 设置列表头 13 | { name: 'user', label: '用户' }, 14 | { name: 'date', label: '时间', format: gen }, 15 | { label: '操作', slotName: 'options' } 16 | ] 17 | }) 18 | 19 | // 设置数据 20 | ctx.modelRef.value = [ 21 | { user: '蒙奇·D·路飞', date: Date.now() } 22 | ]; 23 | 24 | ctx.render // 渲染函数 25 | 26 | ``` 27 | ## API 28 | 29 | ### 参数 30 | 31 | - ctx:指定上下文 32 | - table:table组件 33 | - fields:table字段定义 34 | 35 | ```js 36 | export interface TableSceneConfig extends SceneConfig { 37 | ctx: TableSceneContext; 38 | table: ComponentDesc; 39 | fields: Field[]; 40 | } 41 | ``` 42 | 43 | ### 返回 44 | 45 | - ref:列表组件实例引用 46 | - modelRef:列表数据,双向绑定 47 | 48 | ```js 49 | export interface TableSceneContext extends SceneContext { 50 | ref: Ref; 51 | modelRef: Ref>; 52 | } 53 | ``` 54 | 55 | ## 更新数据 56 | 57 | ```js 58 | const { modelRef } = useTable({ fields: [] }) 59 | modelRef.value = [{user: '111'}, { user: '2222' }] 60 | ``` 61 | 62 | ## 修改table属性 63 | ```js 64 | useTable({ 65 | table: { props: { rowKey: 'id' } } 66 | fields: [] 67 | }) 68 | 69 | ``` 70 | 71 | ## 修改列属性 72 | 通过Field.props修改列属性,比如固定位置、宽度等 73 | ```js 74 | useTable({ 75 | fields: [ 76 | { name: 'user', label: '用户', props: { fixed: 'left', width: 150 } }, 77 | ] 78 | }) 79 | ``` 80 | 81 | ## 自定义渲染数据 82 | 通过Field.format 83 | ```js 84 | 85 | useTable({ 86 | fields: [ 87 | { name: 'user', label: '用户', format: (model, value) => value || '--' }, 88 | { name: 'date', label: '时间', format: genFormatByDate() }, 89 | { name: 'status', label: '状态', options: [] , format: formatByOptions }, 90 | ] 91 | }) 92 | 93 | ``` 94 | 通过slotName 95 | 96 | ```html 97 | 104 | 105 | 112 | ``` -------------------------------------------------------------------------------- /cypress/component/useCurd.cy.tsx: -------------------------------------------------------------------------------- 1 | import FesdUseCurd from '../../docs/examples/fesdUseCurd.vue'; 2 | 3 | describe('FesdUseCurd', () => { 4 | it('渲染成功', () => { 5 | const app = cy.mount(FesdUseCurd); 6 | app.get('.fes-form').should('exist'); 7 | }); 8 | 9 | it('扩展查询操作:导出、批量按钮存在', () => { 10 | const app = cy.mount(FesdUseCurd); 11 | app.get('.fes-space').contains('导出').should('exist'); 12 | app.get('.fes-space').contains('批量').should('exist'); 13 | }); 14 | 15 | it('表格操作:审核、更新按钮存在', () => { 16 | const app = cy.mount(FesdUseCurd); 17 | app.get('.fes-table').find('.fes-space').contains('审核').should('exist'); 18 | app.get('.fes-table').find('.fes-space').contains('更新').should('exist'); 19 | }); 20 | 21 | it('点击导出按钮', () => { 22 | const app = cy.mount(FesdUseCurd); 23 | app.get('.fes-space').contains('导出').click(); 24 | app.log('执行导出操作'); 25 | // 检查导出操作成功弹窗 26 | app.get('.fes-message-wrapper').should('have.text', '导出'); 27 | }); 28 | 29 | it('点击批量按钮-未选择任何记录', () => { 30 | const app = cy.mount(FesdUseCurd); 31 | app.get('.fes-space').contains('批量').click(); 32 | app.log('执行批量操作-未选择记录'); 33 | // 检查警告弹窗 34 | app.get('.fes-message-wrapper').should('have.text', '至少选择一条记录'); 35 | }); 36 | 37 | it('点击批量按钮-选择记录', () => { 38 | const app = cy.mount(FesdUseCurd); 39 | app.get('tbody').find('.fes-checkbox').first().click(); 40 | app.get('.fes-space').contains('批量').click(); 41 | app.log('执行批量操作-选择记录'); 42 | // 检查批量操作成功弹窗 43 | app.get('.fes-message-wrapper').should('have.text', '批量操作 ==> ids: 1'); 44 | }); 45 | 46 | it('点击审核按钮', () => { 47 | const app = cy.mount(FesdUseCurd); 48 | app.get('tbody').contains('审核').first().click(); 49 | app.log('执行审核操作'); 50 | // 检查审核操作成功弹窗 51 | app.get('.fes-message-wrapper').should('have.text', '审核 ===> 蒙奇·D·路飞'); 52 | }); 53 | 54 | it('点击更新按钮', () => { 55 | const app = cy.mount(FesdUseCurd); 56 | app.get('.fes-table-row td').eq(3).click(); 57 | app.get('.fes-space').contains('更新').should('not.be.disabled').click(); 58 | app.log('执行更新操作'); 59 | // 检查更新操作成功弹窗 60 | app.get('.fes-modal-header').should('have.text', '用户更新'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/examples/demos/login.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 71 | -------------------------------------------------------------------------------- /packages/core/src/useTable/index.ts: -------------------------------------------------------------------------------- 1 | import { SceneConfig, SceneContext, useScene, useSceneContext } from '../base'; 2 | import { ref, Ref } from 'vue'; 3 | import { compileComponents, ComponentDesc, ComponentType, createScheme, Field, Scheme, SchemeChildren } from '../scheme'; 4 | import { PluginFunction } from '../plugins'; 5 | import { mergeRefProps } from '../helper'; 6 | export interface TableSceneContext extends SceneContext { 7 | ref: Ref; 8 | modelRef: Ref>; 9 | } 10 | 11 | export interface TableSceneConfig extends SceneConfig { 12 | ctx?: TableSceneContext; 13 | table?: ComponentDesc; 14 | fields: Field[]; 15 | } 16 | 17 | export const tableSchemePlugin: PluginFunction = (api) => { 18 | api.describe('table-plugin'); 19 | 20 | api.onSelfStart(({ ctx, config: { fields, table } }) => { 21 | if (!fields) return; 22 | const { modelRef } = ctx; 23 | modelRef.value = []; 24 | const schemeChildren: SchemeChildren = []; 25 | const scheme = createScheme(table || { name: ComponentType.Table }); 26 | scheme.__ref = ref(null); 27 | if (!scheme.component) { 28 | scheme.component = ComponentType.Table; 29 | } 30 | 31 | fields.forEach((field) => { 32 | const scheme: Scheme = createScheme(field); 33 | scheme.component = ComponentType.TableColumn; 34 | scheme.children = compileComponents(ctx.schemes, field.components); 35 | mergeRefProps(scheme, 'props', { label: field.label, prop: field.name }); 36 | schemeChildren.push(scheme); 37 | }); 38 | scheme.children = schemeChildren; 39 | mergeRefProps(scheme, 'props', { data: modelRef }); 40 | 41 | ctx.ref = scheme.__ref; 42 | if (ctx.schemes) { 43 | ctx.schemes.push(scheme); 44 | } else { 45 | ctx.schemes = [scheme]; 46 | } 47 | 48 | api.emit('tableSchemeLoaded'); 49 | api.emit('schemeLoaded'); 50 | api.emit('started'); 51 | }); 52 | }; 53 | 54 | export function useTable(config: TableSceneConfig): TableSceneContext { 55 | if (!config.ctx) { 56 | const { ctx } = useSceneContext('table'); 57 | config.ctx = ctx as TableSceneContext; 58 | } 59 | config.ctx.use(tableSchemePlugin as PluginFunction); 60 | return useScene(config); 61 | } 62 | -------------------------------------------------------------------------------- /docs/zh/guide/upgrade.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # V1到V2迁移指南 6 | 7 | ## 全局配置 8 | 9 | 1. setGlobalConfig删除、增删改查模块移除。 10 | 2. 删除usePreset 11 | 3. 改用v2的插件安装和配置 12 | 4. uniqueKey移动到useCurd中的table配置 13 | 5. 模块的请求不再统一处理,请自行处理数据格式。 14 | 15 | ```js 16 | import '@koala-form/fes-plugin'; 17 | import { setupGlobalConfig, installPluginPreset } from '@koala-form/core'; 18 | import { request } from '@fesjs/fes'; 19 | 20 | // 将依赖的插件安装到全局 21 | installPluginPreset(); 22 | setupGlobalConfig({ request }) // 配置请求 23 | ``` 24 | 25 | ## 字段 26 | 27 | 1. type不再指定组件,改为components 28 | 2. status和模块属性移除,改为由传入的场景决定。 29 | 3. enumsName移除,不支持从fes中读取options,统一为options配置,可以加如下方法获取: 30 | ```js 31 | import { enums } from '@fesjs/fes'; 32 | 33 | export const getEnumOptions = (name) => enums.get(name, commEnumExt); 34 | ``` 35 | 36 | 4. span移除,由用户自己在场景中控制表单组件属性。 37 | 38 | ## 场景 39 | 大多数场景使用的是usePage,新版中我们用[useCurd](../ui//fes.md),配置和使用方法不一样 40 | 41 | 1. usePage的字段通过status解析到模块中,useCurd则提供模块自由传入 42 | ```js 43 | useCurd({ 44 | query: {}, 45 | table: {}, 46 | edit: {}, 47 | modal: {}, 48 | pager: {}, 49 | actions: {} 50 | }) 51 | ``` 52 | 2. 处理请求,useCurd中,使用actions配置请求处理 53 | ```js 54 | useCurd({ 55 | actions: { 56 | query: { 57 | before(params) { 58 | const { page, ...newParams } = params 59 | if (page) { 60 | newParams.pager = { 61 | pageSize: page.pageSize 62 | current: page.currentPage 63 | } 64 | } 65 | newParams.addProps = '123' 66 | return newParams 67 | }, 68 | after(data) { 69 | const { pager, dataList } = data 70 | return { 71 | list: dataList, 72 | page: { 73 | totalCount: pager.total 74 | } 75 | } 76 | } 77 | } 78 | } 79 | }) 80 | ``` 81 | 82 | 3. 日期、日期范围格式化 83 | 84 | 列表中可以通过genFormatByDate指定格式化 85 | 86 | 4. 枚举值转化 87 | 88 | 通过formatByOptions转 89 | 90 | 5. slots 91 | useCurd提供了 92 | 93 | queryActionsExtend 在重置按钮前扩展查询域按钮 94 | 95 | tableActionsExtend 在最后扩展列表操作按钮 96 | 97 | 其他可以通过指定字段或者组件的slotName 98 | 99 | 6. KoalaForm组件改为KoalaRender只接受render函数 100 | 101 | 7. 方法、属性访问 102 | 103 | 有场景上下文提供,或者使用handler函数,一般以do开头。 104 | 105 | -------------------------------------------------------------------------------- /docs/examples/useForm/relation.js: -------------------------------------------------------------------------------- 1 | import { FMessage } from '@fesjs/fes-design'; 2 | import { ComponentType, useForm, useSceneContext, when } from '@koala-form/core'; 3 | import { genButton, genForm } from '@koala-form/fes-plugin'; 4 | import { computed, defineComponent } from 'vue'; 5 | 6 | export default defineComponent({ 7 | setup() { 8 | const { ctx } = useSceneContext('form'); 9 | const showCheck = computed(() => { 10 | return ctx.modelRef.value.age < 18; 11 | }); 12 | const { render } = useForm({ 13 | ctx, 14 | form: genForm('horizontal', { labelWidth: 50 }), 15 | fields: [ 16 | { 17 | name: 'name', 18 | label: '姓名', 19 | defaultValue: '蒙奇·D·路飞', 20 | components: { 21 | name: ComponentType.Input, 22 | }, 23 | }, 24 | { 25 | name: 'sex', 26 | label: '性别', 27 | defaultValue: '1', 28 | vIf: when('!!name'), 29 | options: [ 30 | { value: '0', label: '女' }, 31 | { value: '1', label: '男' }, 32 | ], 33 | components: { 34 | name: ComponentType.Select, 35 | }, 36 | }, 37 | { 38 | name: 'age', 39 | label: '年龄', 40 | components: { 41 | name: ComponentType.InputNumber, 42 | }, 43 | }, 44 | { 45 | label: ' ', 46 | components: { 47 | name: ComponentType.Space, 48 | children: [ 49 | { 50 | ...genButton('保存', () => FMessage.success('--保存--')), 51 | disabled: when(() => ctx.modelRef.value.age <= 0), 52 | }, 53 | { 54 | ...genButton('审核', () => FMessage.success('--审核--')), 55 | vShow: showCheck, 56 | }, 57 | ], 58 | }, 59 | }, 60 | ], 61 | }); 62 | return render; 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /docs/zh/guide/plugin/how.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 自定义插件 6 | 7 | 在插件基础中,可以通过一个函数来定义一个插件, 如 8 | 9 | ```ts 10 | export const plugin: PluginFunction = (api, config) => { 11 | // do something 12 | } 13 | ``` 14 | 其中PluginFunction的类型如下 15 | ```ts 16 | export type PluginFunction = (api: Plugin, config?: K) => void; 17 | ``` 18 | 19 | 插件的执行是通过事件驱动的,所以我们是在插件函数内使用api的on/emit定义插件的任务和依赖。 20 | 21 | ## 编写一个插件 22 | 23 | 为了更好的了解插件,我们可以简单写一个给form label自动加上`:`的插件 24 | 25 | 首先我们给插件加一个描述 26 | ```ts 27 | const formLabelColonPlugin: PluginFunction = (api) => { 28 | api.describe('form-label-colon-plugin'); 29 | } 30 | ``` 31 | 32 | 接下我们要知道,useForm场景执行之后,form字段会解析到Scheme上,此时会触发插件formSchemeLoaded的事件 33 | 34 | 因此在插件中,添加事件 35 | 36 | ```ts 37 | const formLabelColonPlugin: PluginFunction = (api) => { 38 | api.describe('form-label-colon-plugin'); 39 | 40 | api.on('formSchemeLoaded', ({ ctx }) => { 41 | // 添加处理 42 | }) 43 | } 44 | ``` 45 | 46 | 在场景上下文中,可以访问到Scheme,此钩子还可以访问到已解析的字段,因此可以加上这样处理 47 | 48 | ```ts 49 | import { travelTree, mergeRefProps } from '@koala-form/core'; 50 | 51 | const formLabelColonPlugin: PluginFunction = (api) => { 52 | api.describe('form-label-colon-plugin'); 53 | 54 | api.on('formSchemeLoaded', ({ ctx }) => { 55 | // 遍历scheme 56 | travelTree(ctx.schemes, (scheme) => { 57 | if (scheme.props?.label?.trim()) { 58 | // 合并属性 59 | mergeRefProps(scheme, 'props', { label: scheme.props.label + ':' }); 60 | } 61 | }); 62 | }) 63 | } 64 | ``` 65 | 最后安装使用 66 | 67 | 安装到全局使用 68 | 69 | ```js 70 | import { installPluginPreset } from '@koala-form/core'; 71 | 72 | installInGlobal(formLabelColonPlugin); 73 | ``` 74 | 75 | 或者场景安装 76 | 77 | ```js 78 | import { useSceneContext } from '@koala-form/core'; 79 | 80 | const { ctx } = useSceneContext(['form']) 81 | 82 | ctx.use(formLabelColonPlugin) 83 | 84 | 85 | // 多场景安装 version >= 2.0.1 86 | useSceneContext(['form', 'table'], [formLabelColonPlugin]) 87 | 88 | ``` 89 | 90 | 运行示例效果 91 | 92 | 93 | 94 | 95 | 99 | 100 | 101 | 关于插件的更多细节,可以参考[设计原理](./design.md)和[API](./api.md)。 -------------------------------------------------------------------------------- /docs/examples/useForm/validate.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useForm, doValidate, doClearValidate } from '@koala-form/core'; 2 | import { genForm } from '@koala-form/fes-plugin'; 3 | import { defineComponent } from 'vue'; 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const form = useForm({ 8 | form: genForm(), 9 | fields: [ 10 | { 11 | name: 'name', 12 | label: '姓名', 13 | required: true, 14 | defaultValue: '蒙奇·D·路飞', 15 | components: { 16 | name: ComponentType.Input, 17 | }, 18 | }, 19 | { 20 | name: 'age', 21 | label: '年龄', 22 | rules: [ 23 | { required: true, message: '年龄不能为空' }, 24 | { 25 | type: 'number', 26 | min: 0, 27 | max: 200, 28 | message: '年龄范围0-200', 29 | }, 30 | ], 31 | components: { 32 | name: ComponentType.InputNumber, 33 | }, 34 | }, 35 | { 36 | label: ' ', 37 | components: { 38 | name: ComponentType.Space, 39 | children: [ 40 | { 41 | name: ComponentType.Button, 42 | props: { type: 'primary' }, 43 | children: '校验', 44 | events: { 45 | onClick() { 46 | doValidate(form); 47 | }, 48 | }, 49 | }, 50 | { 51 | name: ComponentType.Button, 52 | children: '清空校验', 53 | events: { 54 | onClick() { 55 | doClearValidate(form); 56 | }, 57 | }, 58 | }, 59 | ], 60 | }, 61 | }, 62 | ], 63 | }); 64 | return form.render; 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /docs/zh/guide/base/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | 5 | # 插件基础 6 | 使用插件可以扩展场景或者场景的功能,比如可以通过插件来适配UI库、扩展ComponentDesc和Field的配置。 7 | 8 | 内置了这些插件 9 | 10 | | 插件 | 插件说明 | 11 | | ------------ | ----------------------- | 12 | | disabledPlugin | 组件disbaled支持 | 13 | | eventsPlugin | 事件支持 | 14 | | formatPlugin | 格式化解析支持 | 15 | | formRule | 表单校验规则支持 | 16 | | optionsPlugin | 枚举选项支持 | 17 | | renderPlugin | 场景渲染 | 18 | | slotPlugin | 插槽支持 | 19 | | vIfPlugin | vIf支持 | 20 | | vShowPlugin | vShow支持 | 21 | | vModelsPlugin | 组件响应式属性绑定 | 22 | 23 | ## 安装到全局 24 | 25 | - installInGlobal 将插件安装到全局,意味着所有的场景都生效。 26 | 27 | - installPluginPreset 将所有内置插件安装到全局 28 | 29 | ```js 30 | import { componentPlugin } from '@koala-form/fes-plugin'; 31 | import { installPluginPreset } from '@koala-form/core'; 32 | 33 | installInGlobal(componentPlugin); // 安装fes design组件插件 34 | 35 | installPluginPreset() // 安装所有预设内置插件 36 | 37 | ``` 38 | ## 局部安装 39 | 全局安装的插件将在所有的场景中生效,当场景需要特定的插件时,可以使用上下文进行局部安装。 40 | 41 | ```js 42 | const plugin = (api) => { 43 | // do something 44 | } 45 | 46 | const ctx = useSceneContext('name'); 47 | ctx.use(plugin); // 安装插件到局部场景中 48 | ``` 49 | 50 | ## disabledPlugin 51 | 为disabled提供响应式支持 52 | 53 | - 作用于组件描述属性:disabled 54 | - 作用于字段描述属性:无 55 | - 提供行为函数:无 56 | - 提供帮助函数:无 57 | 58 | ## eventsPlugin 59 | 为组件提供事件绑定 60 | 61 | - 作用于组件描述属性:events 62 | - 作用于字段描述属性:无 63 | - 提供行为函数:无 64 | - 提供帮助函数:无 65 | 66 | ## formatPlugin 67 | 格式化解析支持 68 | 69 | - 作用于组件描述属性:format 70 | - 作用于字段描述属性:format 71 | - 提供行为函数:无 72 | - 提供帮助函数:formatByOptions、genFormatByDate 73 | ## formRule 74 | 表单校验规则支持 75 | 76 | - 作用于组件描述属性:无 77 | - 作用于字段描述属性:required、rules 78 | - 提供行为函数:doValidate、doClearValidate 79 | - 提供帮助函数:无 80 | ## optionsPlugin 81 | 枚举选项支持 82 | 83 | - 作用于组件描述属性:无 84 | - 作用于字段描述属性:options 85 | - 提供行为函数:doTransferOptions、doRemoteOptions、doComputedOptions、doComputedLabels 86 | - 提供帮助函数:无 87 | 88 | ## renderPlugin 89 | 场景渲染 90 | 91 | - 作用于组件描述属性:无 92 | - 作用于字段描述属性:无 93 | - 提供行为函数:无 94 | - 提供帮助函数:无 95 | 96 | ## slotPlugin 97 | 插槽支持 98 | 99 | - 作用于组件描述属性:slotName 100 | - 作用于字段描述属性:slotName 101 | - 提供行为函数:无 102 | - 提供帮助函数:无 103 | 104 | ## vIfPlugin 105 | vIf支持 106 | 107 | - 作用于组件描述属性:vIf 108 | - 作用于字段描述属性:vIf 109 | - 提供行为函数:无 110 | - 提供帮助函数:无 111 | ## vShowPlugin 112 | vShow支持 113 | 114 | - 作用于组件描述属性:vShow 115 | - 作用于字段描述属性:vShow 116 | - 提供行为函数:无 117 | - 提供帮助函数:无 118 | 119 | ## vModelsPlugin 120 | 组件响应式属性绑定 121 | 122 | - 作用于组件描述属性:vModels 123 | - 作用于字段描述属性:vModels 124 | - 提供行为函数:无 125 | - 提供帮助函数:无 -------------------------------------------------------------------------------- /docs/examples/base/form.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useForm } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const form = useForm({ 7 | form: { props: { labelWidth: '40px' } }, 8 | fields: [ 9 | { 10 | name: 'name', // modelRef.value.name可以访问到值 11 | label: '姓名', // 表单项的名称 12 | defaultValue: '蒙奇·D·路飞', // 默认值 13 | components: { 14 | name: ComponentType.Input, // 表单组件是输入框 15 | }, 16 | }, 17 | { 18 | name: 'sex', 19 | label: '性别', 20 | defaultValue: '1', 21 | options: [ 22 | // 设置下拉框选项 23 | { value: '0', label: '女' }, 24 | { value: '1', label: '男' }, 25 | ], 26 | components: { 27 | name: ComponentType.Select, 28 | }, 29 | }, 30 | { 31 | name: 'age', 32 | label: '年龄', 33 | components: { 34 | name: ComponentType.InputNumber, 35 | }, 36 | }, 37 | { 38 | label: ' ', 39 | components: { 40 | name: ComponentType.Space, 41 | children: [ 42 | { 43 | name: ComponentType.Button, // 按钮组件 44 | children: ['保存'], // 按钮内容 45 | props: { type: 'primary' }, // 组件的属性 46 | events: { 47 | // 按钮的事件 48 | onClick: (event) => { 49 | console.log(event, form.modelRef); 50 | }, 51 | }, 52 | }, 53 | { 54 | name: ComponentType.Button, 55 | children: ['重置'], 56 | events: { onClick: () => form.resetFields() }, // 重置按钮点击 57 | }, 58 | ], 59 | }, 60 | }, 61 | ], 62 | }); 63 | return form.render; 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /docs/examples/started/useForm.js: -------------------------------------------------------------------------------- 1 | import { ComponentType, useForm, doResetFields } from '@koala-form/core'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | setup() { 6 | const form = useForm({ 7 | form: { props: { labelWidth: '40px' } }, 8 | fields: [ 9 | { 10 | name: 'name', // modelRef.value.name可以访问到值 11 | label: '姓名', // 表单项的名称 12 | defaultValue: '蒙奇·D·路飞', // 默认值 13 | components: { 14 | name: ComponentType.Input, // 表单组件是输入框 15 | }, 16 | }, 17 | { 18 | name: 'sex', 19 | label: '性别', 20 | defaultValue: '1', 21 | options: [ 22 | // 设置下拉框选项 23 | { value: '0', label: '女' }, 24 | { value: '1', label: '男' }, 25 | ], 26 | components: { 27 | name: ComponentType.Select, 28 | }, 29 | }, 30 | { 31 | name: 'age', 32 | label: '年龄', 33 | components: { 34 | name: ComponentType.InputNumber, 35 | }, 36 | }, 37 | { 38 | label: ' ', 39 | components: { 40 | name: ComponentType.Space, 41 | children: [ 42 | { 43 | name: ComponentType.Button, // 按钮组件 44 | children: ['保存'], // 按钮内容 45 | props: { type: 'primary' }, // 组件的属性 46 | events: { 47 | // 按钮的事件 48 | onClick: (event) => { 49 | console.log(event, form.modelRef); 50 | }, 51 | }, 52 | }, 53 | { 54 | name: ComponentType.Button, 55 | children: ['重置'], 56 | events: { onClick: () => doResetFields(form) }, // 重置按钮点击调用handler函数 57 | }, 58 | ], 59 | }, 60 | }, 61 | ], 62 | }); 63 | return form.render; 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /packages/core/src/useModal/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, Ref } from 'vue'; 2 | import { getGlobalConfig, SceneConfig, SceneContext, useScene, useSceneContext } from '../base'; 3 | import { mergeRefProps } from '../helper'; 4 | import { PluginFunction } from '../plugins'; 5 | import { compileComponents, ComponentDesc, ComponentType, createScheme } from '../scheme'; 6 | 7 | export interface ModalSceneContext extends SceneContext { 8 | modelRef: Ref<{ 9 | show: boolean; 10 | title: string; 11 | }>; 12 | ref: Ref; 13 | } 14 | 15 | export interface ModalSceneConfig extends SceneConfig { 16 | ctx?: ModalSceneContext; 17 | title?: string; 18 | modal?: ComponentDesc; 19 | } 20 | 21 | export const modalPlugin: PluginFunction = (api) => { 22 | api.describe('modal-plugin'); 23 | 24 | api.onSelfStart(({ ctx, config: { modal, title } }) => { 25 | const { modelRef } = ctx; 26 | modelRef.value = { 27 | show: false, 28 | title: title || '', 29 | }; 30 | const scheme = createScheme(modal || { name: ComponentType.Modal }); 31 | scheme.__ref = ref(null); 32 | if (!scheme.component) { 33 | scheme.component = ComponentType.Modal; 34 | } 35 | mergeRefProps(scheme, 'vModels', { 36 | show: { ref: modelRef, name: 'show' }, 37 | }); 38 | 39 | mergeRefProps(scheme, 'props', { title: computed(() => modelRef.value.title) }); 40 | 41 | ctx.ref = scheme.__ref; 42 | if (ctx.schemes) { 43 | ctx.schemes.push(scheme); 44 | } else { 45 | ctx.schemes = [scheme]; 46 | } 47 | scheme.children = compileComponents(ctx.schemes, modal?.children as ComponentDesc[]); 48 | 49 | api.emit('modalSchemeLoaded'); 50 | api.emit('schemeLoaded'); 51 | api.emit('started'); 52 | }); 53 | }; 54 | 55 | export const doOpen = (ctx: ModalSceneContext) => { 56 | if (ctx.modelRef.value) { 57 | ctx.modelRef.value.show = true; 58 | } 59 | }; 60 | 61 | export const doClose = (ctx: ModalSceneContext) => { 62 | if (ctx.modelRef.value) { 63 | ctx.modelRef.value.show = false; 64 | } 65 | }; 66 | 67 | export function useModal(config: ModalSceneConfig): ModalSceneContext { 68 | if (!config.ctx) { 69 | const { ctx } = useSceneContext('modal'); 70 | config.ctx = ctx as ModalSceneContext; 71 | } 72 | const modal = config?.modal || { name: ComponentType.Modal }; 73 | config.ctx.use(modalPlugin as PluginFunction); 74 | const mergeConfig = { ...(config || {}), modal }; 75 | return useScene(mergeConfig); 76 | } 77 | -------------------------------------------------------------------------------- /examples/demo-with-fes/src/.fes/plugin-layout/views/page.vue: -------------------------------------------------------------------------------- 1 | 8 | 78 | -------------------------------------------------------------------------------- /docs/zh/guide/scene/useForm.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 3 3 | --- 4 | # useForm 5 | 在curd中出现表单场景是查询表单、新增表单、更新表单。 6 | 7 | useForm根据传入的字段定义创建表单场景 8 | 9 | ```js 10 | const ctx = useForm({ fields: [ { name: 'user', label: '用户' } ] }) 11 | ctx.initFields({ user: 'test' }) 12 | 13 | ctx.modelRef.value.user; // test 14 | 15 | ctx.render // 渲染函数 16 | 17 | ``` 18 | 19 | 对表单的常见操作也提供了支持,比如: 20 | 21 | - 表单布局 22 | - 表单校验 23 | - 表单重置 24 | - 表单提交 25 | - 表单初始化 26 | - 表单联动 27 | 28 | ## API 29 | 30 | ### 参数 31 | 32 | - ctx:指定上下文 33 | - form:form组件 34 | - fields:表单字段定义 35 | 36 | ```js 37 | export interface FormSceneConfig extends SceneConfig { 38 | ctx: FormSceneContext; 39 | form?: ComponentDesc; 40 | fields: Field[]; 41 | } 42 | ``` 43 | 44 | ### 返回 45 | 46 | - formRef:表单组件实例引用 47 | - initFields:初始化字段值 48 | - resetFields:重置为最近一次初始化的值 49 | - setFields: 设置表单值 50 | - clearValidate:清空校验 51 | - validate:校验 52 | 53 | ```js 54 | export interface FormSceneContext extends SceneContext { 55 | formRef: Ref; 56 | initFields: (values: Record, name?: string) => void; 57 | resetFields: () => void; 58 | setFields: (values: Record, name?: string) => Record; 59 | clearValidate: () => void; 60 | validate: (names?: string[]) => Promise; 61 | } 62 | ``` 63 | 64 | ## 查询表单 65 | 查询表单一般是横向排列的,常见的操作是查询和重置 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | ## 新增/更新表单 78 | 79 | 新增/更新表单一般是垂直排列,场景的操作是保存、清空和重置 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | ## 表单校验 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 | 104 | ## 表单联动 105 | 106 | 表单联动可使用下面三个属性,可以作用于ComponentDesc组件描述和Fields字段上: 107 | 108 | - vIf:是否渲染 109 | - vShow:是否显示 110 | - disabled:是否可用 111 | 112 | 类型是:`Ref | boolean | When`,如下面例子所示: 113 | 1. 字段性别上的`vIf: when('!!name')`, 表示当字段name不为空时,渲染性别字段。 114 | 2. 保存按钮的`disabled: when(() => ctx.modelRef.value.age <= 0)`,表示年龄age>0时,可保存按钮可点击。 115 | 3. 审核按钮的`vShow: showCheck`,其中showCheck时一个Ref,根据Ref的值,判断审核按钮是否显示,判断age大于18岁时,不需要提交审核 116 | 117 | 其中when接受字符串表达式和函数,更多了解请参考[When] 118 | 119 | 120 | 121 | 122 | 127 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/vue'; 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount; 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount); 37 | 38 | import '@koala-form/fes-plugin'; 39 | import { setupGlobalConfig, installPluginPreset } from '@koala-form/core'; 40 | import { FMessage } from '@fesjs/fes-design'; 41 | // import { FMessage } from '@fesjs/fes-design'; 42 | // 将依赖的插件安装到全局 43 | installPluginPreset(); 44 | setupGlobalConfig({ 45 | debug: true, 46 | modelValueName: 'modelValue', 47 | // 实现网络请求的实现 48 | request: async (api, params) => { 49 | console.log('request.params => ', api, params); 50 | return { 51 | list: [ 52 | { 53 | id: '1', 54 | name: '蒙奇·D·路飞', 55 | age: 16, 56 | sex: '1', 57 | hobby: '2,3', 58 | birthday: 1115251200000, 59 | idCard: '440223198310130033', 60 | address: '上海市普陀区金沙江路 1518 弄', 61 | education: '1', 62 | }, 63 | { 64 | id: '2', 65 | name: '罗罗诺亚·索隆', 66 | age: 18, 67 | sex: '1', 68 | birthday: 1115251200000, 69 | idCard: '440223193110130024', 70 | address: '上海市普陀区金沙江路 1518 弄', 71 | education: '2', 72 | }, 73 | ], 74 | page: { 75 | currentPage: 1, 76 | totalCount: 23, 77 | }, 78 | }; 79 | }, 80 | }); 81 | 82 | // Example use: 83 | // cy.mount(MyComponent) 84 | --------------------------------------------------------------------------------