├── 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 |
7 |
8 | <<< @/examples/demos/login.vue
9 |
10 |
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 |
7 |
8 | <<< @/examples/demos/editTable.vue
9 |
10 |
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 |
2 |
3 |
This is an about page
4 |
5 |
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 |
5 |
6 |
7 |
8 |
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 |
2 |
7 |
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 |
2 | {{ initialState.userName }}
3 |
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 |
2 |
3 |
4 |
20 |
--------------------------------------------------------------------------------
/examples/demo-with-fes/src/.fes/plugin-layout/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
8 |
9 |
{{ msg }}
10 |
11 | You’ve successfully created a project with
12 | Vite +
13 | Vue 3. What's next?
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 | +86
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
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 |
3 |
19 |
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 |
49 |
50 | <<< @/examples/base/slotRender.vue
51 |
52 |
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 |
2 |
7 |
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 |
48 |
49 | <<< @/examples/demos/login.vue
50 |
51 |
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 |
2 |
7 |
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 |
2 |
11 |
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 |
13 |
14 | <<< @/examples/started/useScene.js
15 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
![]()
4 |
{{ title }}
5 |
{{ subTitle }}
6 |
7 | 返回上一页
8 |
9 |
10 |
11 |
43 |
77 |
--------------------------------------------------------------------------------
/examples/demo-with-vue/src/components/icons/IconEcosystem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
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 |
5 |
6 |
7 | Koala-Form
8 |
9 |
10 |
11 | 低代码表单解决方案,让你跟考拉一样“懒”
12 |
13 | [](../../issues)
14 | [](http://opensource.org/licenses/MIT)
15 | [](../../pulls)
16 | [](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 |
41 |
42 |
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 |
96 |
97 | <<< @/examples/base/form.js
98 |
99 |
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 |
5 |
6 |
7 | Koala-Form
8 |
9 |
10 | 低代码表单解决方案,让你跟考拉一样“懒”
11 |
12 | [](../../issues)
13 | [](http://opensource.org/licenses/MIT)
14 | [](../../pulls)
15 | [](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 |
42 |
43 |
44 |
45 |
69 | ```
70 |
71 |
72 | ## 反馈
--------------------------------------------------------------------------------
/packages/antd-plugin/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 | Koala-Form Ant Design Vue插件
8 |
9 |
10 | 低代码表单解决方案,让你跟考拉一样“懒”
11 |
12 | [](../../issues)
13 | [](http://opensource.org/licenses/MIT)
14 | [](../../pulls)
15 | [](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 |
42 |
43 |
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 |
98 |
99 |
100 | 自定义操作
101 |
102 |
103 |
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 |
2 |
3 |
欢迎使用Koala Form
4 |
5 |
登录
6 |
7 | 没有账号,去注册
8 | 忘记密码?
9 |
10 |
11 |
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 |
96 |
97 | <<< @/examples/labelPlugin.js
98 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
71 |
72 | <<< @/examples/useForm/query.js
73 |
74 |
75 |
76 |
77 | ## 新增/更新表单
78 |
79 | 新增/更新表单一般是垂直排列,场景的操作是保存、清空和重置
80 |
81 |
82 |
83 |
84 |
85 |
86 | <<< @/examples/useForm/edit.js
87 |
88 |
89 |
90 |
91 | ## 表单校验
92 |
93 |
94 |
95 |
96 |
97 |
98 | <<< @/examples/useForm/validate.js
99 |
100 |
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 |
123 |
124 | <<< @/examples/useForm/relation.js
125 |
126 |
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 |
--------------------------------------------------------------------------------