config_tools: Add vue-json-schema-form and advanced custom component for IVSHMEM field

1. add Vue devtools support
2. update project dependencies
3. refactor configurator source code tree for private library hook
4. fix build issue
5. dynamic load scenario JSON schema(fix cache issue)
6. add vjsf 1.12.2 (latest) for private package dependencies
7. remove vjsf unnecessary files
8. use private vjsf as configurator dependencies
9. Add custom IVSHMEM_REGION widget
10. add a script to populate default values
11. get default values before export scenario xml
12. specify widgets in XML schema
13. add missing vjsf license file
14. populate default values to empty nodes
15. when user clicks save button, update formData with each field default value
16. fix when the user clicks the save button will collapse configFom
17. add success message for saving scenario XML

vue-json-schema-form 1.12.2 (latest)link: b30ea7c2d6/packages/lib

Tracked-On: #6691
Signed-off-by: Weiyi Feng <weiyix.feng@intel.com>
This commit is contained in:
Weiyi-Feng 2022-04-28 23:49:23 +08:00 committed by acrnsi-robot
parent e36b615fe1
commit 06b942f5eb
154 changed files with 7977 additions and 490 deletions

View File

@ -4,6 +4,6 @@ python scenario_config/jsonschema/converter.py
xmllint --xinclude schema/datachecks.xsd > schema/allchecks.xsd
python -m build
rem pip install .\dist\acrn_config_tools-3.0-py3-none-any.whl --force-reinstall
del .\configurator\thirdLib\acrn_config_tools-3.0-py3-none-any.whl
python .\configurator\thirdLib\manager.py install
del .\configurator\packages\configurator\thirdLib\acrn_config_tools-3.0-py3-none-any.whl
python .\configurator\packages\configurator\thirdLib\manager.py install
echo build and install success

View File

@ -86,7 +86,7 @@ xmllint --xinclude schema/datachecks.xsd > schema/allchecks.xsd
python -m build
cd configurator
python thirdLib/manager.py install
python packages/configurator/thirdLib/manager.py install
yarn build
```

View File

@ -0,0 +1,3 @@
# Do Not Delete
# This file be used in configurator's wasm python env
# See: https://stackoverflow.com/questions/42791179/why-does-pip-install-not-include-my-package-data-files

View File

@ -1,5 +1,4 @@
{
"name": "acrn-configurator",
"private": true,
"version": "0.3.0",
"author": {
@ -8,34 +7,13 @@
"url": "https://github.com/Weiyi-Feng"
},
"description": "ACRN Configurator",
"workspaces": [
"packages/configurator",
"packages/vue-json-schema-form/**"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
"dev": "yarn workspace acrn-configurator dev",
"build": "yarn workspace acrn-configurator build"
},
"dependencies": {
"@lljj/vue3-form-naive": "^1.12.2",
"@popperjs/core": "^2.11.5",
"@rollup/plugin-replace": "^4.0.0",
"@tauri-apps/api": "^1.0.0-rc.3",
"@vicons/fa": "^0.12.0",
"@vicons/utils": "^0.1.4",
"ajv-i18n": "^4.2.0",
"bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"naive-ui": "^2.28.1",
"node-fetch": "2",
"sass": "^1.50.0",
"vfonts": "^0.0.3",
"vue": "^3.2.25",
"vue-router": "4"
},
"devDependencies": {
"@tauri-apps/cli": "^1.0.0-rc.8",
"@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.2"
}
}
"dependencies": {}
}

View File

@ -0,0 +1,42 @@
{
"name": "acrn-configurator",
"private": true,
"version": "0.3.0",
"author": {
"name": "Feng, Weiyi",
"email": "weiyix.feng@intel.com",
"url": "https://github.com/Weiyi-Feng"
},
"description": "ACRN Configurator",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@lljj/vue3-form-naive": "^1.12.2",
"@popperjs/core": "^2.11.5",
"@rollup/plugin-replace": "^4.0.0",
"@tauri-apps/api": "^1.0.0-rc.4",
"@vicons/fa": "^0.12.0",
"@vicons/utils": "^0.1.4",
"ajv-i18n": "^4.2.0",
"bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"naive-ui": "^2.28.1",
"node-fetch": "2",
"sass": "^1.50.0",
"vfonts": "^0.0.3",
"vue": "^3.2.25",
"vue-router": "4"
},
"devDependencies": {
"@tauri-apps/cli": "^1.0.0-rc.9",
"@types/node": "^16.11.33",
"@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.2"
}
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -185,9 +185,9 @@ dependencies = [
[[package]]
name = "attohttpc"
version = "0.18.0"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e"
dependencies = [
"flate2",
"http",
@ -351,7 +351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f89d248799e3f15f91b70917f65381062a01bb8e222700ea0e5a7ff9785f9c"
dependencies = [
"byteorder",
"uuid",
"uuid 0.8.2",
]
[[package]]
@ -387,12 +387,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cocoa"
version = "0.24.0"
@ -1572,9 +1566,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.16"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
@ -2605,18 +2599,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.136"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
dependencies = [
"proc-macro2",
"quote",
@ -2625,9 +2619,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"itoa 1.0.1",
"ryu",
@ -2954,9 +2948,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.7.0"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b6a3359088d4c4735a13f933202f4ecd91f5991b41a8eb757f2449c044ce925"
checksum = "3765f329d831aa461cd3f0f94b065a9fe37560fd7f8099d5bcf3e95c923071f0"
dependencies = [
"bitflags",
"cairo-rs",
@ -3018,16 +3012,14 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.0.0-rc.6"
version = "1.0.0-rc.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d514a34b3f9a07e2002d95e1371b42a446636e3d571a59e974b21d6acf3007"
checksum = "537978045ca229b9c1bb51ea85bc807b9d109a119721134fc5da24f94fd3074a"
dependencies = [
"anyhow",
"attohttpc",
"bincode",
"cfg_aliases",
"dirs-next",
"either",
"embed_plist",
"flate2",
"futures",
@ -3035,9 +3027,9 @@ dependencies = [
"glib",
"glob",
"gtk",
"heck 0.4.0",
"http",
"ignore",
"memchr",
"notify-rust",
"once_cell",
"open",
@ -3064,15 +3056,15 @@ dependencies = [
"thiserror",
"tokio",
"url",
"uuid",
"uuid 1.0.0",
"windows 0.30.0",
]
[[package]]
name = "tauri-build"
version = "1.0.0-rc.5"
version = "1.0.0-rc.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede6462a4692e2fd5030497ad576264dc90eea5fa337182492e77291d45fc78b"
checksum = "7e6448e80778032b4f9dd86b5efc8214d5bfc81a11efa502bb5211b05d422b14"
dependencies = [
"anyhow",
"cargo_toml",
@ -3083,9 +3075,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.0.0-rc.4"
version = "1.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54193ebdb010e85824301ce5f0940742b680d66376203f6425d549d2f32ad499"
checksum = "e4c2e553c2ceaf30f1feabc76abebbd5f9eddb99b643de0078e38037e43e3c2f"
dependencies = [
"base64",
"brotli",
@ -3099,15 +3091,15 @@ dependencies = [
"sha2",
"tauri-utils",
"thiserror",
"uuid",
"uuid 1.0.0",
"walkdir",
]
[[package]]
name = "tauri-macros"
version = "1.0.0-rc.4"
version = "1.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8b867ef4703cb8e50f128ee3c941895d94c01e0ebd9007a7b45ecca52516dbf"
checksum = "d3e8af1367b1e1224edfa4117c88fe19717970fabfbc2555e957e077f0469248"
dependencies = [
"heck 0.4.0",
"proc-macro2",
@ -3119,9 +3111,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.3.4"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b289ac8eafc52a36425fcaf3de23febd0b2606d3cce2b39ac412a1817fae537"
checksum = "27653d24a0d7e2c8e04838e975acbf7a5628746d8d60a916d33a9ccf8a06c4ea"
dependencies = [
"gtk",
"http",
@ -3131,22 +3123,22 @@ dependencies = [
"serde_json",
"tauri-utils",
"thiserror",
"uuid",
"uuid 1.0.0",
"webview2-com",
"windows 0.30.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "0.3.5"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8bf16e0476a8249aa2c75e7b49ec4c059be5fb27d9f6514e30ed327e8e9fa2"
checksum = "53d9b0922c27ea8a1430a2bbf666fe7789645dffb9d317f8d2dfca1a5dff2271"
dependencies = [
"gtk",
"rand 0.8.5",
"tauri-runtime",
"tauri-utils",
"uuid",
"uuid 1.0.0",
"webview2-com",
"windows 0.30.0",
"wry",
@ -3154,9 +3146,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.0.0-rc.4"
version = "1.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a67fcf8fdd1340de4e75c01966fceab03057a8b0e97864eb39a21e420deed503"
checksum = "a485f9fc0f381d3da0818c4260b3a04be86dc1844a12edaff68afb07bc55d735"
dependencies = [
"brotli",
"ctor",
@ -3165,13 +3157,13 @@ dependencies = [
"html5ever",
"json-patch",
"kuchiki",
"memchr",
"phf 0.10.1",
"proc-macro2",
"quote",
"serde",
"serde_json",
"serde_with",
"serialize-to-javascript",
"thiserror",
"url",
"walkdir",
@ -3417,6 +3409,12 @@ name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0"
dependencies = [
"getrandom 0.2.6",
]
@ -3916,9 +3914,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.14.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd09ffc86ecea0a0d5f50cc8e4a8121a1bfc0b0825a160f86ac39e86979344c"
checksum = "20b69cff9f50bab10b42e51bac9c2cf695484059f1b19e911754477ae703ef42"
dependencies = [
"block",
"cocoa",

View File

@ -12,16 +12,16 @@ rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.0.0-rc.5", features = [] }
tauri-build = { version = "1.0.0-rc.7", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-rc.6", features = ["api-all", "devtools"] }
log = "0.4"
glob = "0.3"
dirs = "4.0"
itertools = "0.10"
serde_json = "1.0.81"
serde = { version = "1.0.137", features = ["derive"] }
tauri = { version = "1.0.0-rc.8", features = ["api-all", "devtools"] }
log = "0.4.17"
glob = "0.3.0"
dirs = "4.0.0"
itertools = "0.10.3"
[features]
# by default Tauri runs in production mode

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1 +1 @@
{"package":{"productName":"acrn-configurator","version":"../package.json"},"build":{"distDir":"../build","devPath":"http://localhost:3000","beforeDevCommand":"","beforeBuildCommand":""},"tauri":{"bundle":{"active":true,"targets":"all","identifier":"com.projectacrn.configurator","icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"resources":[],"externalBin":[],"copyright":"","category":"DeveloperTool","shortDescription":"","longDescription":"","deb":{"depends":[],"useBootstrapper":false},"macOS":{"frameworks":[],"useBootstrapper":false,"exceptionDomain":"","signingIdentity":null,"providerShortName":null,"entitlements":null},"windows":{"certificateThumbprint":null,"digestAlgorithm":"sha256","timestampUrl":""}},"updater":{"active":false},"allowlist":{"all":true},"windows":[{"title":"ACRN Configurator","width":1366,"height":768,"resizable":true,"fullscreen":false,"decorations":false}],"security":{"csp":null}}}
{"package":{"productName":"acrn-configurator","version":"../package.json"},"build":{"distDir":"../build","devPath":"http://localhost:3000","beforeDevCommand":"","beforeBuildCommand":""},"tauri":{"bundle":{"active":true,"targets":"all","identifier":"com.projectacrn.configurator","icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"resources":[],"externalBin":[],"copyright":"","category":"DeveloperTool","shortDescription":"","longDescription":"","deb":{"depends":[]},"macOS":{"frameworks":[],"exceptionDomain":"","signingIdentity":null,"providerShortName":null,"entitlements":null},"windows":{"certificateThumbprint":null,"digestAlgorithm":"sha256","timestampUrl":""}},"updater":{"active":false},"allowlist":{"all":true},"windows":[{"title":"ACRN Configurator","width":1366,"height":768,"resizable":true,"fullscreen":false,"decorations":false}],"security":{"csp":null}}}

View File

@ -28,12 +28,10 @@
"shortDescription": "",
"longDescription": "",
"deb": {
"depends": [],
"useBootstrapper": false
"depends": []
},
"macOS": {
"frameworks": [],
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": null,
"providerShortName": null,

View File

@ -16,7 +16,12 @@
"enum": [
"y",
"n"
]
],
"ui:widget": "b-form-checkbox",
"ui:options": {
"value": "y",
"unchecked-value": "n"
}
},
"EnablementType": {
"type": "string",
@ -599,6 +604,7 @@
"properties": {
"size": {
"type": "integer",
"minimum": 0,
"default": 256,
"title": "Memory size (MB)",
"description": "<div class=\"document\">\n <p>\n Specify the physical memory size allocated to this VM in megabytes.\n </p>\n</div>\n"
@ -739,18 +745,18 @@
"properties": {
"pci_dev": {
"items": {
"type": "string"
"type": "string",
"enum": {
"type": "dynamicEnum",
"function": "get_enum",
"source": "board_xml",
"selector": "//device[class]/@description",
"sorted": "lambda s: (s.split(' ', maxsplit=1)[-1].split(':')[0], s.split(' ')[0])"
}
},
"type": "array",
"title": "PCI device assignment",
"description": "<div class=\"document\">\n <p>\n Select the PCI devices you want to assign to this virtual machine.\n </p>\n</div>\n",
"enum": {
"type": "dynamicEnum",
"function": "get_enum",
"source": "board_xml",
"selector": "//device[class]/@description",
"sorted": "lambda s: (s.split(' ', maxsplit=1)[-1].split(':')[0], s.split(' ')[0])"
}
"description": "<div class=\"document\">\n <p>\n Select the PCI devices you want to assign to this virtual machine.\n </p>\n</div>\n"
}
}
},
@ -1050,6 +1056,8 @@
},
"MAX_PT_IRQ_ENTRIES": {
"type": "integer",
"minimum": 1,
"maximum": 1024,
"default": 256,
"title": "Max passthrough IRQ entries",
"description": "<div class=\"document\">\n <p>\n Specify the maximum number of interrupt request (IRQ) entries from all passthrough devices.\n </p>\n</div>\n"
@ -1339,7 +1347,7 @@
"gpu": {
"type": "string",
"title": "Virtio GPU device",
"description": "<div class=\"document\">\n <dl class=\"simple\">\n <dt>\n The virtio GPU device presents a GPU device to the VM.\n </dt>\n <dd>\n <p>\n This feature enables you to view the VM's GPU output in the Service VM.\n </p>\n </dd>\n </dl>\n</div>\n"
"description": "<div class=\"document\">\n <p>\n The virtio GPU device presents a GPU device to the VM.\nThis feature enables you to view the VM's GPU output in the Service VM.\n </p>\n</div>\n"
}
},
"title": "Virt-IO devices",
@ -1630,6 +1638,7 @@
"properties": {
"size": {
"type": "integer",
"minimum": 0,
"default": 256,
"title": "Memory size (MB)",
"description": "<div class=\"document\">\n <p>\n Specify the physical memory size allocated to this VM in megabytes.\n </p>\n</div>\n"
@ -1811,7 +1820,7 @@
"gpu": {
"type": "string",
"title": "Virtio GPU device",
"description": "<div class=\"document\">\n <dl class=\"simple\">\n <dt>\n The virtio GPU device presents a GPU device to the VM.\n </dt>\n <dd>\n <p>\n This feature enables you to view the VM's GPU output in the Service VM.\n </p>\n </dd>\n </dl>\n</div>\n"
"description": "<div class=\"document\">\n <p>\n The virtio GPU device presents a GPU device to the VM.\nThis feature enables you to view the VM's GPU output in the Service VM.\n </p>\n</div>\n"
}
},
"title": "Virt-IO devices",
@ -1824,6 +1833,7 @@
"properties": {
"size": {
"type": "integer",
"minimum": 0,
"default": 256,
"title": "Memory size (MB)",
"description": "<div class=\"document\">\n <p>\n Specify the physical memory size allocated to this VM in megabytes.\n </p>\n</div>\n"
@ -1975,7 +1985,7 @@
"gpu": {
"type": "string",
"title": "Virtio GPU device",
"description": "<div class=\"document\">\n <dl class=\"simple\">\n <dt>\n The virtio GPU device presents a GPU device to the VM.\n </dt>\n <dd>\n <p>\n This feature enables you to view the VM's GPU output in the Service VM.\n </p>\n </dd>\n </dl>\n</div>\n"
"description": "<div class=\"document\">\n <p>\n The virtio GPU device presents a GPU device to the VM.\nThis feature enables you to view the VM's GPU output in the Service VM.\n </p>\n</div>\n"
}
},
"title": "Virt-IO devices",
@ -2181,7 +2191,7 @@
"gpu": {
"type": "string",
"title": "Virtio GPU device",
"description": "<div class=\"document\">\n <dl class=\"simple\">\n <dt>\n The virtio GPU device presents a GPU device to the VM.\n </dt>\n <dd>\n <p>\n This feature enables you to view the VM's GPU output in the Service VM.\n </p>\n </dd>\n </dl>\n</div>\n"
"description": "<div class=\"document\">\n <p>\n The virtio GPU device presents a GPU device to the VM.\nThis feature enables you to view the VM's GPU output in the Service VM.\n </p>\n</div>\n"
}
},
"title": "Virt-IO devices",
@ -2501,6 +2511,8 @@
},
"MAX_PT_IRQ_ENTRIES": {
"type": "integer",
"minimum": 1,
"maximum": 1024,
"default": 256,
"title": "Max passthrough IRQ entries",
"description": "<div class=\"document\">\n <p>\n Specify the maximum number of interrupt request (IRQ) entries from all passthrough devices.\n </p>\n</div>\n"

View File

@ -1,6 +1,5 @@
import {dialog, invoke} from "@tauri-apps/api";
import JSON2XML from "./json2xml"
import scenarioSchema from "../assets/schema/scenario.json";
import {OpenDialogOptions} from "@tauri-apps/api/dialog";
enum HistoryType {
@ -34,6 +33,10 @@ class PythonObject {
generateLaunchScript(boardXMLText, scenarioXMLText) {
return this.api('generateLaunchScript', boardXMLText, scenarioXMLText)
}
populateDefaultValues(scenarioXMLText) {
return this.api('populateDefaultValues', scenarioXMLText)
}
}
class Configurator {
@ -93,13 +96,8 @@ class Configurator {
loadBoard(path: String) {
return this.readFile(path)
.then((fileContent) => {
let params = JSON.stringify({
boardXML: fileContent,
scenarioSchema: scenarioSchema
})
return this.pythonObject.loadBoard(fileContent)
})
}
loadScenario(path: String): Object {

View File

@ -0,0 +1,59 @@
const isTauri = !!window.__TAURI_IPC__;
if (isTauri) {
let openCount = 0
function openDevTools() {
openCount++;
console.log(`openCount ${openCount} of 5`)
if (openCount >= 5) {
invoke('open_devtools', {})
}
}
window.openDevTools = openDevTools;
} else {
(async () => {
// Patch Browser function to mock Tauri env
let mockJS = await import('../tests/mock');
mockJS.default();
})()
}
import {createApp} from 'vue';
import App from './App.vue';
import router from "./router";
import {invoke} from "@tauri-apps/api/tauri";
import BootstrapVue3 from 'bootstrap-vue-3'
import naive from 'naive-ui';
const app = createApp(App);
app.use(BootstrapVue3);
app.use(naive);
app.use(router);
app.config.unwrapInjectedRef = true
async function main() {
console.log("Pyodide Load Begin")
let t1 = Date.now();
let WASMPythonLoader = await import('./pyodide');
await WASMPythonLoader.default()
let t2 = Date.now();
console.log("Pyodide Load Time: " + (t2 - t1) + "ms")
async function setWindowSystemInfo() {
let homeDir = await invoke("get_home");
let pathSplit = homeDir.indexOf("\\") > 0 ? "\\" : "/";
window.systemInfo = {
homeDir, pathSplit
}
}
await setWindowSystemInfo();
app.mount('#app')
}
main();

View File

@ -32,15 +32,18 @@
</template>
<Scenario :scenario="scenario" @scenarioUpdate="scenarioUpdate"/>
</b-accordion-item>
<Banner/>
<Banner>
<div style="position: relative">
<button type="button" class="btn btn-primary btn-lg SaveButton" @click="saveScenario">
Save Scenario and Launch Scripts
</button>
</div>
</Banner>
<b-accordion-item visible>
<template #title>
<div class="p-1 ps-3 d-flex w-100 justify-content-between align-items-center">
<div class="fs-4">3. Configure settings for scenario and launch scripts</div>
<button type="button" class="btn btn-primary btn-lg" @click="saveScenario">
Save Scenario and Launch Scripts
</button>
</div>
</template>
@ -80,7 +83,7 @@ import Banner from '../components/common/Banner.vue';
import Board from "./Config/Board.vue";
import Scenario from "./Config/Scenario.vue";
import TabBox from "./Config/ConfigForm/TabBox.vue";
import ConfigForm from "./Config/ConfigForm/ConfigForm.vue";
import ConfigForm from "./Config/ConfigForm.vue";
import configurator from "../lib/acrn";
@ -90,6 +93,7 @@ export default {
props: ['WorkingFolder'],
mounted() {
this.updateCurrentFormSchema()
window.getCurrentScenarioData = this.getCurrentScenarioData
},
data() {
return {
@ -119,7 +123,6 @@ export default {
this.updateCurrentFormSchema()
},
updateCurrentFormSchema() {
console.log(this.schemas)
if (this.activeVMID === -1) {
this.currentFormSchema = this.schemas.HV
} else {
@ -140,8 +143,10 @@ export default {
this.updateCurrentFormSchema()
this.updateCurrentFormData()
},
getCurrentScenarioData() {
return this.scenario
},
updateCurrentFormData() {
console.log(this.scenario)
if (this.activeVMID === -1) {
this.currentFormData = this.scenario.hv;
}
@ -249,6 +254,17 @@ export default {
"acrn-config": JSON.parse(JSON.stringify(this.scenario))
}
);
console.log(scenarioXMLData)
// get scenario Defaults
let scenarioWithDefault = configurator.pythonObject.populateDefaultValues(scenarioXMLData)
console.log(scenarioWithDefault)
// write defaults to frontend
this.scenario = scenarioWithDefault.json['acrn-config']
this.updateCurrentFormData()
// get scenario XML with defaults
scenarioXMLData = scenarioWithDefault.xml
debugger
// begin write down and verify
configurator.writeFile(this.WorkingFolder + 'scenario.xml', scenarioXMLData)
.then(() => {
step = 1
@ -279,5 +295,11 @@ export default {
</script>
<style scoped>
.SaveButton {
position: absolute;
right: 1rem;
top: 64px;
z-index: 3;
}
</style>

View File

@ -77,7 +77,7 @@ export default {
default: {}
}
},
emits:['boardUpdate'],
emits: ['boardUpdate'],
data() {
return {
boardHistory: [],
@ -88,7 +88,10 @@ export default {
},
mounted() {
this.getBoardHistory()
//Todo: auto load board
.then(() => {
this.importBoard()
})
// Todo: auto load board
},
computed: {
imported() {
@ -123,7 +126,7 @@ export default {
if (filepath.length > 0) {
configurator.loadBoard(filepath)
.then(({scenarioJSONSchema, boardInfo}) => {
this.$emit('boardUpdate', boardInfo,scenarioJSONSchema);
this.$emit('boardUpdate', boardInfo, scenarioJSONSchema);
let boardFileNewPath = this.WorkingFolder + boardInfo.name;
// Todo: use rust command writeBoard to fix bugs.
configurator.writeFile(boardFileNewPath, boardInfo.content)

View File

@ -56,7 +56,7 @@ import VueForm, {i18n} from "@lljj/vue3-form-naive"
import {Icon} from "@vicons/utils";
import {Minus} from "@vicons/fa"
import localizeEn from 'ajv-i18n/localize/en';
import IVSHMEM_REGION from "./ConfigForm/CustomWidget/IVSHMEM_REGION.vue";
i18n.useLocal(localizeEn);
export default {
@ -77,7 +77,28 @@ export default {
"labelWidth": "300px",
"labelSuffix": ""
},
uiSchema: {}
uiSchema: {
"FEATURES": {
"IVSHMEM": {
"ui:title": "InterVM shared memory",
"IVSHMEM_REGION": {
"ui:title": "",
"ui:sortable": false,
"ui:field": IVSHMEM_REGION,
}
}
},
"vuart_connections": {
"vuart_connection": {
"ui:sortable": false,
"items": {
"endpoint": {
"ui:sortable": false
}
}
}
}
}
};
},
methods: {

View File

@ -0,0 +1,240 @@
<template>
<div class="IVSH_REGIONS" v-if="defaultVal && defaultVal.length>0">
<div class="IVSH_REGION" v-for="(IVSHMEM_VMO, index) in defaultVal">
<div class="IVSH_REGION_CONTENT">
<b style="margin-bottom: 2rem">InterVM shared memory region {{ index + 1 }}</b>
<b-row class="align-items-center my-2 mt-4">
<b-col md="2">
<label>Region name: </label>
</b-col>
<b-col md="4">
<b-form-input v-model="IVSHMEM_VMO.NAME"/>
</b-col>
</b-row>
<b-row class="align-items-center my-2">
<b-col md="2">
<label>Emulated by: </label>
</b-col>
<b-col md="4">
<b-form-select v-model="IVSHMEM_VMO.PROVIDED_BY" :options="providerType"></b-form-select>
</b-col>
</b-row>
<b-row class="align-items-center my-2">
<b-col md="2">
<label>Size (MB): </label>
</b-col>
<b-col md="4">
<b-form-select v-model="IVSHMEM_VMO.IVSHMEM_SIZE" :options="IVSHMEMSize"></b-form-select>
</b-col>
</b-row>
<div class="m-3 mt-4 d-flex flex-column gap-2">
<b>Shared VMs</b>
<p>Select all VMs that will use this shared memory region</p>
<b-row>
<b-col sm="2" offset-sm="6">
Virtual BDF:
</b-col>
</b-row>
<b-row class="justify-content-between align-items-center"
v-for="(IVSHMEM_VM,index) in IVSHMEM_VMO.IVSHMEM_VMS.IVSHMEM_VM">
<b-col sm="1">
<label>VM name:</label>
</b-col>
<b-col sm="3">
<b-form-select v-model="IVSHMEM_VM.VM_NAME" :options="vmNames"></b-form-select>
</b-col>
<b-col sm="3">
<b-form-input v-model="IVSHMEM_VM.VBDF"/>
</b-col>
<b-col sm="3">
<div class="ToolSet">
<div @click="removeSharedVM(IVSHMEM_VMO.IVSHMEM_VMS.IVSHMEM_VM,index)">
<Icon size="18px">
<Minus/>
</Icon>
</div>
<div @click="addSharedVM(IVSHMEM_VMO.IVSHMEM_VMS.IVSHMEM_VM,index)">
<Icon size="18px">
<Plus/>
</Icon>
</div>
</div>
</b-col>
</b-row>
</div>
</div>
<div class="ToolSet">
<div @click="removeIVSHMEM_VMO(index)">
<Icon size="18px">
<Minus/>
</Icon>
</div>
<div @click="addIVSHMEM_VMO(index)">
<Icon size="18px">
<Plus/>
</Icon>
</div>
</div>
</div>
</div>
<div v-else>
<div class="ToolSet">
<div @click="addIVSHMEM_VMO">
<Icon size="18px">
<Plus/>
</Icon>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash';
import {Icon} from "@vicons/utils";
import {Plus, Minus} from '@vicons/fa'
import {fieldProps, vueUtils} from '@lljj/vue3-form-naive';
export default {
name: 'IVSHMEM_REGION',
components: {Icon, Plus, Minus},
props: {
...fieldProps,
// Todo: use ui:fieldProps to pass getScenarioData function
fieldProps: {
type: null,
default: null
}
},
computed: {
vmNames() {
let currentScenarioData = window.getCurrentScenarioData()
let vmNames = []
for (let i = 0; i < currentScenarioData.vm.length; i++) {
vmNames.push(currentScenarioData.vm[i].name)
}
return vmNames
}
},
data() {
return {
providerType: this.rootSchema.definitions['ProviderType']['enum'],
IVSHMEMSize: this.rootSchema.definitions['IVSHMEMSize']['enum'],
defaultVal: vueUtils.getPathVal(this.rootFormData, this.curNodePath)
};
},
watch: {
rootFormData: {
handler(newValue, oldValue) {
this.defaultVal = vueUtils.getPathVal(newValue, this.curNodePath)
},
deep: true
},
defaultVal: {
handler(newValue, oldValue) {
// Note: `newValue` will be equal to `oldValue` here
// on nested mutations as long as the object itself
// hasn't been replaced.
vueUtils.setPathVal(this.rootFormData, this.curNodePath, newValue);
},
deep: true
}
},
methods: {
addSharedVM(vms, index) {
// add new item after current item
vms.splice(index + 1, 0, {
"VM_NAME": "",
"VBDF": ""
})
},
removeSharedVM(vms, index) {
if (vms.length <= 2) {
return
}
vms.splice(index, 1)
},
removeIVSHMEM_VMO(index) {
this.defaultVal.splice(index, 1);
},
addIVSHMEM_VMO(index) {
if (!_.isArray(this.defaultVal)) {
this.defaultVal = []
}
this.defaultVal.splice(index + 1, 0, {
"NAME": "shm_region_" + this.defaultVal.length,
"PROVIDED_BY": "Hypervisor",
"IVSHMEM_SIZE": "2",
"IVSHMEM_VMS": {
"IVSHMEM_VM": [
{
"VM_NAME": "PRE_RT_VM0",
"VBDF": ""
},
{
"VM_NAME": "POST_STD_VM1",
"VBDF": ""
}
]
}
})
}
}
};
</script>
<style scoped>
label:before {
content: '*';
color: red;
margin-right: 3px;
}
.form-control {
line-height: 2;
}
.IVSH_REGIONS {
display: flex;
flex-direction: column;
width: 100%;
gap: 2rem;
}
.IVSH_REGION {
display: flex;
align-items: start;
gap: 1rem;
width: 100%;
}
.IVSH_REGION_CONTENT {
border: 1px solid gray;
border-radius: 5px;
padding: 25px;
width: 100%;
}
.ToolSet {
display: flex;
justify-content: space-around;
gap: 0.5rem;
max-width: 5rem;
width: 100%;
}
.ToolSet div {
cursor: pointer;
border: 1px solid gray;
border-radius: 3px;
background: #f9f9f9;
padding: 5px 5px 3px;
}
</style>

View File

@ -27,7 +27,7 @@
<tr>
<td>
<div class="py-4 text-right">
<button type="button" class="wel-btn btn btn-primary btn-lg" @click="loadScenario">
<button type="button" class="wel-btn btn btn-primary btn-lg" @click="loadScenario(false)">
Import Scenario
</button>
</div>
@ -62,20 +62,27 @@ export default {
}
},
mounted() {
this.getScenarioHistory()
this.getScenarioHistory().then(() => {
// delay 2s for board loading
setTimeout(() => {
this.loadScenario(true)
}, 2000);
})
// Todo: auto load scenario
},
methods: {
newScenario(data) {
this.$emit('scenarioUpdate', data)
},
loadScenario() {
loadScenario(auto = false) {
if (this.currentSelectedScenario.length > 0) {
configurator.loadScenario(this.currentSelectedScenario)
.then((scenarioConfig) => {
console.log(scenarioConfig)
this.$emit('scenarioUpdate', scenarioConfig['acrn-config'])
alert(`Scenario ${this.currentSelectedScenario} loaded success!`)
if (!auto) {
alert(`Scenario ${this.currentSelectedScenario} loaded success!`)
}
}).catch((err) => {
console.log(err)
alert(`Failed to open ${this.currentSelectedScenario}, file may not exist`)
@ -96,7 +103,7 @@ export default {
})
},
getScenarioHistory() {
configurator.getHistory("Scenario")
return configurator.getHistory("Scenario")
.then((scenarioHistory) => {
this.scenarioHistory = scenarioHistory
if (this.scenarioHistory.length > 0) {

View File

@ -0,0 +1,37 @@
import {loadPyodide} from "/thirdLib/pyodide/pyodide";
import scenarioJSONSchema from './assets/schema/scenario.json';
window.__dynamic__load__scenario__from__pyodide__ = () => {
return JSON.stringify(scenarioJSONSchema)
}
export default async function () {
let pyodide = await loadPyodide({
indexURL: '/thirdLib/pyodide/'
});
await pyodide.loadPackage(['micropip', 'lxml', 'beautifulsoup4'])
await pyodide.runPythonAsync(`
import micropip
await micropip.install([
'./thirdLib/xmltodict-0.12.0-py2.py3-none-any.whl',
'./thirdLib/elementpath-2.4.0-py3-none-any.whl',
'./thirdLib/defusedxml-0.7.1-py2.py3-none-any.whl',
'./thirdLib/xmlschema-1.9.2-py3-none-any.whl',
'./thirdLib/acrn_config_tools-3.0-py3-none-any.whl'
])
`)
function test() {
let result = pyodide.runPython(`
import sys
sys.version
`)
console.log(result);
}
test()
// pyodide load success
window.pyodide = pyodide;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
import {mockIPC} from "@tauri-apps/api/mocks";
import mockData from './data/data.json';
export default function mock() {
const origin_confirm = window.confirm;
window.confirm = async (message) => origin_confirm(message);
// mock custom tauri command
mockIPC(async (cmd, args) => {
const packageInfo = await import('../package.json');
function handle() {
switch (cmd) {
case 'get_home':
return 'C:\\Users\\Axel'
case 'get_history':
return JSON.stringify(mockData.history[args.historyType])
case 'acrn_read':
return mockData.files[args.filePath]
default:
if (args?.message?.cmd === "getAppVersion") {
return packageInfo.version;
}
console.log(cmd, args)
return {}
}
}
return handle()
})
}

View File

@ -13,7 +13,7 @@
"install": [
{
"type": "copy",
"from": "../../dist/acrn_config_tools-3.0-py3-none-any.whl",
"from": "../../../../dist/acrn_config_tools-3.0-py3-none-any.whl",
"to": "acrn_config_tools-3.0-py3-none-any.whl"
}
]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
misc/config_tools/configurator/thirdLib/manager.py
depend on misc/config_tools/configurator/thirdLib/library.json
misc/config_tools/configurator/packages/configurator/thirdLib/manager.py
depend on misc/config_tools/configurator/packages/configurator/thirdLib/library.json
"""
import argparse
import os

View File

@ -1,21 +1,12 @@
const fs = require('fs')
const path = require('path')
import type {ConfigEnv, Plugin, ResolvedConfig} from "vite";
// @ts-ignore
import type {Plugin, ResolvedConfig} from "vite";
import replace from "@rollup/plugin-replace";
// @ts-ignore
import cli from "@tauri-apps/cli"
import Config from "../src-tauri/types/config"
// @ts-ignore
import tauriConf from "../src-tauri/tauri.json";
interface Options {
config?: (c: Config, e: ConfigEnv) => Config;
}
function copyFolder(copiedPath, resultPath, direct) {
if (!direct) {
@ -68,17 +59,11 @@ function copyFolder(copiedPath, resultPath, direct) {
}
export default (options?: Options): Plugin => {
let tauriConfig: Config = {...tauriConf};
export default (): Plugin => {
let viteConfig: ResolvedConfig;
const tauri = (mode: "dev" | "build"): Promise<any> => {
// Generate `tauri.conf.json` by `tauri.json`.
console.log("Generate `tauri.conf.json` by `tauri.json`.")
let filePath = path.resolve(__dirname, '..', 'src-tauri', 'tauri.conf.json')
let config = JSON.stringify(tauriConfig)
try {
fs.writeFileSync(filePath, config)
return cli.run([mode], 'tauri')
} catch (err) {
console.error(err)
@ -96,7 +81,6 @@ export default (options?: Options): Plugin => {
server?.httpServer?.on("listening", () => {
if (!process.env.TAURI_SERVE) {
process.env.TAURI_SERVE = "true";
delete tauriConfig["$schema"]
tauri('dev').finally()
}
});
@ -104,19 +88,12 @@ export default (options?: Options): Plugin => {
closeBundle() {
if (!process.env.TAURI_BUILD) {
process.env.TAURI_BUILD = "true";
delete tauriConfig["$schema"]
copyFolder('../thirdLib', '../build/thirdLib', false)
tauri('build').finally()
}
},
config(viteConfig, env) {
process.env.IS_TAURI = "true";
if (options && options.config) {
options.config(tauriConfig, env);
}
if (env.command === "build") {
viteConfig.base = "/";
}
},
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;

View File

@ -8,6 +8,9 @@ import tauri from "./thirdLib/tauri-plugin";
export default defineConfig({
base: './',
plugins: [vue(), tauri()],
resolve: {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
build: {
outDir: path.resolve(__dirname, 'build')
}

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,223 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.12.1...v1.12.2) (2022-04-11)
### Bug Fixes
* **lib:** ui:hidden 透传到widget组件 ([f63094e](https://github.com/lljj-x/vue-json-schema-form/commit/f63094ee85659d1fea45bd789321817c08664ffa)), closes [#170](https://github.com/lljj-x/vue-json-schema-form/issues/170)
# [1.12.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.11.0...v1.12.0) (2022-03-08)
### Bug Fixes
* **lib:** 添加严格模式配置更精准计算anyOf 默认值 ([10cdc08](https://github.com/lljj-x/vue-json-schema-form/commit/10cdc089087d83d8fe08e1fd379b7a1aaad0cd5d)), closes [#152](https://github.com/lljj-x/vue-json-schema-form/issues/152)
### Features
* **lib:** 优化样式 ([e53291b](https://github.com/lljj-x/vue-json-schema-form/commit/e53291b8395fdceb971f15f72c9e809cdee8ec7e))
# [1.11.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.10.0...v1.11.0) (2022-02-19)
### Bug Fixes
* **lib:** 添加严格模式配置更精准计算anyOf 默认值 ([2cd65bb](https://github.com/lljj-x/vue-json-schema-form/commit/2cd65bb5f275a021f1cc368e4c63387163c94d57)), closes [#157](https://github.com/lljj-x/vue-json-schema-form/issues/157)
## [1.9.5](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.4...v1.9.5) (2021-11-21)
### Bug Fixes
* **lib:** 修复inline 布局样式问题 ([65a7143](https://github.com/lljj-x/vue-json-schema-form/commit/65a7143fc19105f9096afc24a25107c0ef27ac5f)), closes [#122](https://github.com/lljj-x/vue-json-schema-form/issues/122)
## [1.9.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.2...v1.9.3) (2021-10-10)
### Bug Fixes
* **lib:** allOf merge 相同类型直接使用左边 ([c0bd0cd](https://github.com/lljj-x/vue-json-schema-form/commit/c0bd0cde9f15b4ca928fec84b4831a5cb459aa43)), closes [#116](https://github.com/lljj-x/vue-json-schema-form/issues/116)
## [1.9.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.1...v1.9.2) (2021-09-25)
### Bug Fixes
* **lib:** 修复anyof 默认值计算可能丢失属性 ([44bcb44](https://github.com/lljj-x/vue-json-schema-form/commit/44bcb44af63a37847cd3df5614fad2f26bdf307d)), closes [#108](https://github.com/lljj-x/vue-json-schema-form/issues/108)
* **lib:** 修复anyOf嵌套object 可能丢失部分校验规则的问题 ([5c06294](https://github.com/lljj-x/vue-json-schema-form/commit/5c06294d9a9c978bda1c3724710cfd4ba478af5b))
## [1.9.1](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.0...v1.9.1) (2021-09-22)
**Note:** Version bump only for package @lljj/vjsf-utils
# [1.9.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.7.0...v1.9.0) (2021-09-06)
### Features
* **vue2:** vue2 添加 widgetListeners 配置 ([50348c2](https://github.com/lljj-x/vue-json-schema-form/commit/50348c27e72813ea16fdcfcea46e6450ccf06018)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
# [1.8.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.7.0...v1.8.0) (2021-09-06)
### Features
* **vue2:** vue2 添加 widgetListeners 配置 ([50348c2](https://github.com/lljj-x/vue-json-schema-form/commit/50348c27e72813ea16fdcfcea46e6450ccf06018)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
# [1.7.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.4...v1.7.0) (2021-08-29)
### Features
* **lib:** 支持配置 slots ([27f1501](https://github.com/lljj-x/vue-json-schema-form/commit/27f1501eda01eabd4a723656be56904e9cb0f069)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
## [1.6.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.2...v1.6.3) (2021-07-12)
**Note:** Version bump only for package @lljj/vjsf-utils
## [1.6.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.1...v1.6.2) (2021-05-31)
**Note:** Version bump only for package @lljj/vjsf-utils
# [1.3.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.2.1...v1.3.0) (2021-04-15)
### Bug Fixes
* **core:** 修复 anyof 后代存在 $ref可能无法回填的问题 ([4dd046b](https://github.com/lljj-x/vue-json-schema-form/commit/4dd046bee0e5c3589f2bfa64ba0d90ed7869067a)), closes [#59](https://github.com/lljj-x/vue-json-schema-form/issues/59)
### Features
* **core:** widget 节点直接配置onChange ([2d2264b](https://github.com/lljj-x/vue-json-schema-form/commit/2d2264b004c3b6586e225c563bf03ca52fc5e53a))
## [1.2.1](https://github.com/lljj-x/vue-json-schema-form/compare/v1.2.0...v1.2.1) (2021-04-11)
### Bug Fixes
* **lib:** 修复anyOf下多级对象初始值计算错误问题 ([6dd9780](https://github.com/lljj-x/vue-json-schema-form/commit/6dd97804573aa55001c2715da4a6ffcc5ee897b9)), closes [#57](https://github.com/lljj-x/vue-json-schema-form/issues/57)
# [1.2.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.1.3...v1.2.0) (2021-03-30)
### Features
* **lib:** 添加 fallback-label 参数 ([cd2d8c3](https://github.com/lljj-x/vue-json-schema-form/commit/cd2d8c3ed72b9bc03e44eb5b86eb1b18fe67c34c)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
## [1.1.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.1.2...v1.1.3) (2021-03-18)
**Note:** Version bump only for package @lljj/vjsf-utils
# [1.1.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.0.2...v1.1.0) (2021-03-06)
### Features
* **vue3-ant:** 更新初始化 ([71a2810](https://github.com/lljj-x/vue-json-schema-form/commit/71a281045af11f215333050396aa546dd5e78b88)), closes [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#40](https://github.com/lljj-x/vue-json-schema-form/issues/40)
## [1.0.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.0.1...v1.0.2) (2021-01-31)
### Bug Fixes
* **style:** 修复p标签等自带边距导致的样式问题  ([7b7e43e](https://github.com/lljj-x/vue-json-schema-form/commit/7b7e43eaa06c14a436b34c38d6d69aad27d67512))
## [0.6.1](https://github.com/lljj-x/vue-json-schema-form/compare/v0.6.0...v0.6.1) (2021-01-19)
**Note:** Version bump only for package @lljj/vjsf-utils
# [0.6.0](https://github.com/lljj-x/vue-json-schema-form/compare/v0.5.0...v0.6.0) (2021-01-19)
### Bug Fixes
* **lib:** 修复 anyOf 类型,编辑时不能匹配正确选项 ([d747722](https://github.com/lljj-x/vue-json-schema-form/commit/d7477227d004e47c2b186c3eb956e4c83d7077ad)), closes [#31](https://github.com/lljj-x/vue-json-schema-form/issues/31)
* **lib:** 解决打包后包含es6代码问题 ([f03352e](https://github.com/lljj-x/vue-json-schema-form/commit/f03352eb129c45963ad41e3e91eebe102c303913)), closes [#29](https://github.com/lljj-x/vue-json-schema-form/issues/29)
### Features
* **iview:** 添加iview i-switch 组件转换 ([8fae70c](https://github.com/lljj-x/vue-json-schema-form/commit/8fae70cb28f7fd02073d6d4318861b7f08f6199b))
* **vue3:** 完成 vue3 form组件 ([1c5deba](https://github.com/lljj-x/vue-json-schema-form/commit/1c5debae4cb92f3f54de64d8f38c98396022a344))

View File

@ -0,0 +1,40 @@
# @lljj/vjsf-utils
表单基础通用工具类,具体的参数可参见源码
## @lljj/vjsf-utils/i18n
管理当前多语言
## @lljj/vjsf-utils/schema/getDefaultFormState
根据 jsonSchema 和 formData计算当前schema value
## @lljj/vjsf-utils/schema/validate
```js
import {
ajvValidateFormData,
validateFormDataAndTransformMsg,
isValid
} from '@lljj/vjsf-utils/schema/validate';
// 直接调用 ajv 验证schema返回格式化后的结果
ajvValidateFormData(...args);
// 校验数据并处理多语言(只处理当前节点)
validateFormDataAndTransformMsg(...args);
// 返回数据是否校验成功
isValid(...args);
// 返回数据是否校验成功
isValid(...args);
```
## @lljj/vjsf-utils/arrayUtils
数组相关的工具类
## @lljj/vjsf-utils/formUtils
表单相关的工具类
## @lljj/vjsf-utils/vueUtils
Vue相关的工具类

View File

@ -0,0 +1,53 @@
/**
* Created by Liu.Jun on 2020/4/25 10:53.
*/
// 通过 index 上移
export function moveUpAt(target, index) {
if (index === 0) return false;
const item = target[index];
const newItems = [item, target[index - 1]];
return target.splice(index - 1, 2, ...newItems);
}
// 通过 index 下移动
export function moveDownAt(target, index) {
if (index === target.length - 1) return false;
const item = target[index];
const newItems = [target[index + 1], item];
return target.splice(index, 2, ...newItems);
}
// 移除
export function removeAt(target, index) {
// 移除数组中指定位置的元素,返回布尔表示成功与否
return !!target.splice(index, 1).length;
}
// 数组填充对象
export function fillObj(target, data) {
// 简单复制 异常直接抛错
try {
if (typeof data === 'object') {
return target.fill(null).map(() => JSON.parse(JSON.stringify(data)));
}
} catch (e) {
// nothing ...
}
// 默认返回一个 undefined
return undefined;
}
// 切割分为多个数组
export function cutOff(target, cutOffPointIndex) {
return target.reduce((preVal, curVal, curIndex) => {
preVal[curIndex > cutOffPointIndex ? 1 : 0].push(curVal);
return preVal;
}, [[], []]);
}
// 数组交集
export function intersection(arr1, arr2) {
return arr1.filter(item => arr2.includes(item));
}

View File

@ -0,0 +1,64 @@
<template>
<div class="fieldGroupWrap">
<h3
v-if="showTitle && trueTitle"
class="fieldGroupWrap_title"
>
{{ trueTitle }}
</h3>
<p
v-if="showDescription && description"
class="fieldGroupWrap_des"
v-html="description"
>
</p>
<div class="fieldGroupWrap_box">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FieldGroupWrap',
inject: ['genFormProvide'],
props: {
//
curNodePath: {
type: String,
default: ''
},
showTitle: {
type: Boolean,
default: true
},
showDescription: {
type: Boolean,
default: true
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
},
computed: {
trueTitle() {
const title = this.title;
if (title) {
return title;
}
const genFormProvide = this.genFormProvide.value || this.genFormProvide;
const backTitle = genFormProvide.fallbackLabel && this.curNodePath.split('.').pop();
if (backTitle !== `${Number(backTitle)}`) return backTitle;
return '';
}
}
};
</script>

View File

@ -0,0 +1,430 @@
import retrieveSchema from './schema/retriev';
import { getPathVal } from './vueUtils';
import { getSchemaType, isObject } from './utils';
// 通用的处理表达式方法
// 这里打破 JSON Schema 规范
const regExpression = /{{(.*)}}/;
function handleExpression(rootFormData, curNodePath, expression, fallBack) {
// 未配置
if (undefined === expression) {
return undefined;
}
// 配置了 mustache 表达式
const matchExpression = regExpression.exec(expression);
regExpression.lastIndex = 0; // 重置索引
if (matchExpression) {
const code = matchExpression[1].trim();
// eslint-disable-next-line no-new-func
const fn = new Function('parentFormData', 'rootFormData', `return ${code}`);
return fn(getPathVal(rootFormData, curNodePath, 1), rootFormData);
}
// 回退
return fallBack();
}
export function replaceArrayIndex({ schema, uiSchema } = {}, index) {
const itemUiOptions = getUiOptions({
schema,
uiSchema,
containsSpec: false
});
return ['title', 'description'].reduce((preVal, curItem) => {
if (itemUiOptions[curItem]) {
preVal[`ui:${curItem}`] = String(itemUiOptions[curItem]).replace(/\$index/g, index + 1);
}
return preVal;
}, {});
}
// 是否为 hidden Widget
export function isHiddenWidget({
schema = {},
uiSchema = {},
curNodePath = '',
rootFormData = {}
}) {
const widget = uiSchema['ui:widget'] || schema['ui:widget'];
const hiddenExpression = uiSchema['ui:hidden'] || schema['ui:hidden'];
// 支持配置 ui:hidden 表达式
return widget === 'HiddenWidget'
|| widget === 'hidden'
|| !!handleExpression(rootFormData, curNodePath, hiddenExpression, () => {
// 配置了函数 function
if (typeof hiddenExpression === 'function') {
return hiddenExpression(getPathVal(rootFormData, curNodePath, 1), rootFormData);
}
// 配置了常量
return hiddenExpression;
});
}
// 解析当前节点 ui field
export function getUiField(FIELDS_MAP, {
schema = {},
uiSchema = {},
}) {
const field = schema['ui:field'] || uiSchema['ui:field'];
// vue 组件,或者已注册的组件名
if (typeof field === 'function' || typeof field === 'object' || typeof field === 'string') {
return {
field,
fieldProps: uiSchema['ui:fieldProps'] || schema['ui:fieldProps'], // 自定义field ,支持传入额外的 props
};
}
// 类型默认 field
const fieldCtor = FIELDS_MAP[getSchemaType(schema)];
if (fieldCtor) {
return {
field: fieldCtor
};
}
// 如果包含 oneOf anyOf 返回空不异常
// SchemaField 会附加onyOf anyOf信息
if (!fieldCtor && (schema.anyOf || schema.oneOf)) {
return {
field: null
};
}
// 不支持的类型
throw new Error(`不支持的field类型 ${schema.type}`);
}
// 解析用户配置的 uiSchema options
export function getUserUiOptions({
schema = {},
uiSchema = {},
curNodePath, // undefined 不处理 表达式
rootFormData = {}
}) {
// 支持 uiSchema配置在 schema文件中
return Object.assign({}, ...[schema, uiSchema].map(itemSchema => Object.keys(itemSchema)
.reduce((options, key) => {
const value = itemSchema[key];
// options 内外合并
if (key === 'ui:options' && isObject(value)) {
return { ...options, ...value };
}
// https://github.com/lljj-x/vue-json-schema-form/issues/170
// ui:hidden需要作为内置属性使用不能直接透传给widget组件如果组件需要只能在ui:options 中使用hidden传递
if (key !== 'ui:hidden' && key.indexOf('ui:') === 0) {
// 只对 ui:xxx 配置形式支持表达式
return {
...options,
[key.substring(3)]: curNodePath === undefined ? value : handleExpression(rootFormData, curNodePath, value, () => value)
};
}
return options;
}, {})));
}
// 解析当前节点的ui options参数
export function getUiOptions({
schema = {},
uiSchema = {},
containsSpec = true,
curNodePath,
rootFormData,
}) {
const spec = {};
if (containsSpec) {
spec.readonly = !!schema.readOnly;
if (undefined !== schema.multipleOf) {
// 组件计数器步长
spec.step = schema.multipleOf;
}
if (schema.minimum || schema.minimum === 0) {
spec.min = schema.minimum;
}
if (schema.maximum || schema.maximum === 0) {
spec.max = schema.maximum;
}
if (schema.minLength || schema.minLength === 0) {
spec.minlength = schema.minLength;
}
if (schema.maxLength || schema.maxLength === 0) {
spec.maxlength = schema.maxLength;
}
if (schema.format === 'date-time' || schema.format === 'date') {
// 数组类型 时间区间
// 打破了schema的规范type array 配置了 format
if (schema.type === 'array') {
spec.isRange = true;
spec.isNumberValue = !(schema.items && schema.items.type === 'string');
} else {
// 字符串 ISO 时间
spec.isNumberValue = !(schema.type === 'string');
}
}
}
if (schema.title) spec.title = schema.title;
if (schema.description) spec.description = schema.description;
// 计算ui配置
return {
...spec,
// 用户配置最高优先级
...getUserUiOptions({
schema,
uiSchema,
curNodePath,
rootFormData
})
};
}
// 获取当前节点的ui 配置 options + widget
// 处理成 Widget 组件需要的格式
export function getWidgetConfig({
schema = {},
uiSchema = {},
curNodePath,
rootFormData,
}, fallback = null) {
const uiOptions = getUiOptions({
schema,
uiSchema,
curNodePath,
rootFormData,
});
// 没有配置 Widget 各个Field组件根据类型判断
if (!uiOptions.widget && fallback) {
Object.assign(uiOptions, fallback({
schema,
uiSchema
}));
}
const {
widget,
title: label,
labelWidth,
description,
attrs: widgetAttrs,
class: widgetClass,
style: widgetStyle,
widgetListeners,
fieldAttrs,
fieldStyle,
fieldClass,
emptyValue,
width,
getWidget,
renderScopedSlots,
renderChildren,
onChange,
...uiProps
} = uiOptions;
return {
widget,
label,
labelWidth,
description,
widgetAttrs,
widgetClass,
widgetStyle,
fieldAttrs,
width,
fieldStyle,
fieldClass,
emptyValue,
getWidget,
renderScopedSlots,
renderChildren,
onChange,
widgetListeners,
uiProps
};
}
// 解析用户配置的 errorSchema options
export function getUserErrOptions({
schema = {},
uiSchema = {},
errorSchema = {}
}) {
return Object.assign({}, ...[schema, uiSchema, errorSchema].map(itemSchema => Object.keys(itemSchema)
.reduce((options, key) => {
const value = itemSchema[key];
// options 内外合并
if (key === 'err:options' && isObject(value)) {
return { ...options, ...value };
}
if (key.indexOf('err:') === 0) {
return { ...options, [key.substring(4)]: value };
}
return options;
}, {})));
}
// ui:order object-> properties 排序
export function orderProperties(properties, order) {
if (!Array.isArray(order)) {
return properties;
}
const arrayToHash = arr => arr.reduce((prev, curr) => {
prev[curr] = true;
return prev;
}, {});
const errorPropList = arr => (arr.length > 1
? `properties '${arr.join("', '")}'`
: `property '${arr[0]}'`);
const propertyHash = arrayToHash(properties);
const orderFiltered = order.filter(
prop => prop === '*' || propertyHash[prop]
);
const orderHash = arrayToHash(orderFiltered);
const rest = properties.filter(prop => !orderHash[prop]);
const restIndex = orderFiltered.indexOf('*');
if (restIndex === -1) {
if (rest.length) {
throw new Error(
`uiSchema order list does not contain ${errorPropList(rest)}`
);
}
return orderFiltered;
}
if (restIndex !== orderFiltered.lastIndexOf('*')) {
throw new Error('uiSchema order list contains more than one wildcard item');
}
const complete = [...orderFiltered];
complete.splice(restIndex, 1, ...rest);
return complete;
}
/**
* 单个匹配
* 常量或者只有一个枚举
*/
export function isConstant(schema) {
return (
(Array.isArray(schema.enum) && schema.enum.length === 1)
|| schema.hasOwnProperty('const')
);
}
export function toConstant(schema) {
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
return schema.enum[0];
} if (schema.hasOwnProperty('const')) {
return schema.const;
}
throw new Error('schema cannot be inferred as a constant');
}
/**
* 是否为选择列表
* 枚举 或者 oneOf anyOf 每项都只有一个固定常量值
* @param _schema
* @param rootSchema
* @returns {boolean|*}
*/
export function isSelect(_schema, rootSchema = {}) {
const schema = retrieveSchema(_schema, rootSchema);
const altSchemas = schema.oneOf || schema.anyOf;
if (Array.isArray(schema.enum)) {
return true;
} if (Array.isArray(altSchemas)) {
return altSchemas.every(altSchemasItem => isConstant(altSchemasItem));
}
return false;
}
// items 都为一个对象
export function isFixedItems(schema) {
return (
Array.isArray(schema.items)
&& schema.items.length > 0
&& schema.items.every(item => isObject(item))
);
}
// 是否为多选
export function isMultiSelect(schema, rootSchema = {}) {
if (!schema.uniqueItems || !schema.items) {
return false;
}
return isSelect(schema.items, rootSchema);
}
// array additionalItems
// https://json-schema.org/understanding-json-schema/reference/array.html#tuple-validation
export function allowAdditionalItems(schema) {
if (schema.additionalItems === true) {
console.warn('additionalItems=true is currently not supported');
}
return isObject(schema.additionalItems);
}
// 下拉选项
export function optionsList(schema, uiSchema, curNodePath, rootFormData) {
// enum
if (schema.enum) {
const uiOptions = getUserUiOptions({
schema,
uiSchema,
curNodePath,
rootFormData
});
// ui配置 enumNames 优先
const enumNames = uiOptions.enumNames || schema.enumNames;
return schema.enum.map((value, i) => {
const label = (enumNames && enumNames[i]) || String(value);
return { label, value };
});
}
// oneOf | anyOf
const altSchemas = schema.oneOf || schema.anyOf;
const altUiSchemas = uiSchema.oneOf || uiSchema.anyOf;
return altSchemas.map((curSchema, i) => {
const uiOptions = (altUiSchemas && altUiSchemas[i]) ? getUserUiOptions({
schema: curSchema,
uiSchema: altUiSchemas[i],
curNodePath,
rootFormData
}) : {};
const value = toConstant(curSchema);
const label = uiOptions.title || curSchema.title || String(value);
return { label, value };
});
}
export function fallbackLabel(oriLabel, isFallback, curNodePath) {
if (oriLabel) return oriLabel;
if (isFallback) {
const backLabel = curNodePath.split('.').pop();
// 过滤纯数字字符串
if (backLabel && (backLabel !== `${Number(backLabel)}`)) return backLabel;
}
return '';
}

View File

@ -0,0 +1,19 @@
/**
* Created by Liu.Jun on 2020/4/30 11:22.
*/
// 使用 ajv-i18n 这里只为初始化默认可以设置语言
// 也可以自己使用官方的语言包
// https://github.com/epoberezkin/ajv-i18n/tree/master/localize
import localizeZh from './localize/zh';
export default {
$$currentLocalizeFn: localizeZh,
getCurrentLocalize() {
return this.$$currentLocalizeFn;
},
useLocal(fn) {
this.$$currentLocalizeFn = fn;
}
};

View File

@ -0,0 +1,152 @@
// https://github.com/epoberezkin/ajv-i18n
export default function localizeZh(errors) {
if (!(errors && errors.length)) return;
for (let i = 0; i < errors.length; i += 1) {
const e = errors[i];
let out;
let n;
let cond;
switch (e.keyword) {
case '$ref':
out = `无法找到引用${e.params.ref}`;
break;
case 'additionalItems':
out = '';
n = e.params.limit;
out += `不允许超过${n}个元素`;
break;
case 'additionalProperties':
out = '不允许有额外的属性';
break;
case 'anyOf':
out = '数据应为 anyOf 所指定的其中一个';
break;
case 'const':
out = '应当等于常量';
break;
case 'contains':
out = '应当包含一个有效项';
break;
case 'custom':
out = `应当通过 "${e.keyword} 关键词校验"`;
break;
case 'dependencies':
out = '';
n = e.params.depsCount;
out += `应当拥有属性${e.params.property}的依赖属性${e.params.deps}`;
break;
case 'enum':
out = '应当是预设定的枚举值之一';
break;
case 'exclusiveMaximum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当为 ${cond}`;
break;
case 'exclusiveMinimum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当为 ${cond}`;
break;
case 'false schema':
out = '布尔模式出错';
break;
case 'format':
out = `应当匹配格式 "${e.params.format}"`;
break;
case 'formatExclusiveMaximum':
out = 'formatExclusiveMaximum 应当是布尔值';
break;
case 'formatExclusiveMinimum':
out = 'formatExclusiveMinimum 应当是布尔值';
break;
case 'formatMaximum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当是 ${cond}`;
break;
case 'formatMinimum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当是 ${cond}`;
break;
case 'if':
out = `应当匹配模式 "${e.params.failingKeyword}" `;
break;
case 'maximum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当为 ${cond}`;
break;
case 'maxItems':
out = '';
n = e.params.limit;
out += `不应多于 ${n} 个项`;
break;
case 'maxLength':
out = '';
n = e.params.limit;
out += `不应多于 ${n} 个字符`;
break;
case 'maxProperties':
out = '';
n = e.params.limit;
out += `不应有多于 ${n} 个属性`;
break;
case 'minimum':
out = '';
cond = `${e.params.comparison} ${e.params.limit}`;
out += `应当为 ${cond}`;
break;
case 'minItems':
out = '';
n = e.params.limit;
out += `不应少于 ${n} 个项`;
break;
case 'minLength':
out = '';
n = e.params.limit;
out += `不应少于 ${n} 个字符`;
break;
case 'minProperties':
out = '';
n = e.params.limit;
out += `不应有少于 ${n} 个属性`;
break;
case 'multipleOf':
out = `应当是 ${e.params.multipleOf} 的整数倍`;
break;
case 'not':
out = '不应当匹配 "not" schema';
break;
case 'oneOf':
out = '只能匹配一个 "oneOf" 中的 schema';
break;
case 'pattern':
out = `应当匹配模式 "${e.params.pattern}"`;
break;
case 'patternRequired':
out = `应当有属性匹配模式 ${e.params.missingPattern}`;
break;
case 'propertyNames':
out = `属性名 '${e.params.propertyName}' 无效`;
break;
case 'required':
out = `应当有必需属性 ${e.params.missingProperty}`;
break;
case 'switch':
out = `由于 ${e.params.caseIndex} 失败,未通过 "switch" 校验, `;
break;
case 'type':
out = `应当是 ${e.params.type} 类型`;
break;
case 'uniqueItems':
out = `不应当含有重复项 (第 ${e.params.j} 项与第 ${e.params.i} 项是重复的)`;
break;
default:
// eslint-disable-next-line no-continue
continue;
}
e.message = out;
}
}

View File

@ -0,0 +1,9 @@
<template>
<svg
class="genFormIcon genFormIcon-down"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
>
<path d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z" />
</svg>
</template>

View File

@ -0,0 +1,11 @@
<template>
<svg
class="genFormIcon genFormIcon-up"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</template>

View File

@ -0,0 +1,13 @@
<template>
<svg
class="genFormIcon genFormIcon-close"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1
191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0
0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</template>

View File

@ -0,0 +1,25 @@
<template>
<Icon size="16px" class="labelInfoIcon">
<InfoCircle/>
</Icon>
</template>
<script>
import {Icon} from "@vicons/utils";
import {InfoCircle} from "@vicons/fa"
export default {
name: "IconInfo",
components: {Icon,InfoCircle}
}
</script>
<style>
.genFormLabel{
display: flex;
align-items: center;
}
.labelInfoIcon{
margin: 0 6px;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<svg
class="genFormIcon genFormIcon-plus"
t="1551322312294"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="10297"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="200"
height="200"
>
<path
d="M474 152m8 0l60 0q8 0 8 8l0 704q0 8-8 8l-60 0q-8 0-8-8l0-704q0-8 8-8Z"
p-id="10298"
/>
<path
d="M168 474m8 0l672 0q8 0 8 8l0 60q0 8-8 8l-672 0q-8 0-8-8l0-60q0-8 8-8Z"
p-id="10299"
/>
</svg>
</template>

View File

@ -0,0 +1,16 @@
<template>
<svg
class="genFormIcon genFormIcon-qs"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 708c-22.1
0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm62.9-219.5a48.3 48.3 0 0
0-30.9 44.8V620c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8v-21.5c0-23.1 6.7-45.9 19.9-64.9 12.9-18.6 30.9-32.8
52.1-40.9 34-13.1 56-41.6 56-72.7 0-44.1-43.1-80-96-80s-96 35.9-96 80v7.6c0 4.4-3.6
8-8 8h-48c-4.4 0-8-3.6-8-8V420c0-39.3 17.2-76 48.4-103.3C430.4 290.4 470 276 512 276s81.6 14.5 111.6
40.7C654.8 344 672 380.7 672 420c0 57.8-38.1 109.8-97.1 132.5z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
/**
* Created by Liu.Jun on 2021/3/6 2:58 下午.
*/
import IconCaretDown from './IconCaretDown.vue';
import IconCaretUp from './IconCaretUp.vue';
import IconClose from './IconClose.vue';
import IconPlus from './IconPlus.vue';
import IconQuestion from './IconQuestion.vue';
import IconInfo from "./IconInfo.vue";
export {
IconCaretDown,
IconCaretUp,
IconClose,
IconPlus,
IconQuestion,
IconInfo
};

View File

@ -0,0 +1,29 @@
{
"name": "@lljj/vjsf-utils",
"version": "1.12.2",
"description": "vue json schema form 使用的基础utils工具类",
"private": false,
"publishConfig": {
"access": "public"
},
"keywords": [
"@lljj/vjsf-utils",
"vue",
"vuejs",
"form",
"jsonSchema"
],
"dependencies": {
"ajv": "^6.10.2"
},
"repository": "https://github.com/lljj-x/vue-json-schema-form",
"homepage": "https://github.com/lljj-x/vue-json-schema-form",
"license": "Apache-2.0",
"author": "Liu.Jun",
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"gitHead": "92795075169c879e1c1fabfe26f1d3c10b861060"
}

View File

@ -0,0 +1,29 @@
// $ref 引用
function getPathVal(obj, pathStr) {
const pathArr = pathStr.split('/');
for (let i = 0; i < pathArr.length; i += 1) {
if (obj === undefined) return undefined;
obj = pathArr[i] === '' ? obj : obj[pathArr[i]];
}
return obj;
}
// 找到ref引用的schema
export default function findSchemaDefinition($ref, rootSchema = {}) {
const origRef = $ref;
if ($ref.startsWith('#')) {
// Decode URI fragment representation.
$ref = decodeURIComponent($ref.substring(1));
} else {
throw new Error(`Could not find a definition for ${origRef}.`);
}
const current = getPathVal(rootSchema, $ref);
if (current === undefined) {
throw new Error(`Could not find a definition for ${origRef}.`);
}
if (current.hasOwnProperty('$ref')) {
return findSchemaDefinition(current.$ref, rootSchema);
}
return current;
}

View File

@ -0,0 +1,256 @@
/**
* 根据schema计算出formData的初始值
* 源码来自react-jsonschema-form 做了细节调整重写了allOf实现逻辑
* https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/utils.js#L283
*/
import { getSchemaType, isObject, mergeObjects } from '../utils';
import findSchemaDefinition from './findSchemaDefinition';
import { getMatchingOption } from './validate';
import { fillObj } from '../arrayUtils';
import { isFixedItems, isMultiSelect } from '../formUtils';
import retrieveSchema, { /* resolveDependencies, */ resolveAllOf } from './retriev';
/**
* When merging defaults and form data, we want to merge in this specific way:
* - objects are deeply merged
* - arrays are merged in such a way that:
* - when the array is set in form data, only array entries set in form data
* are deeply merged; additional entries from the defaults are ignored
* - when the array is not set in form data, the default is copied over
* - scalars are overwritten/set by form data
*/
function mergeDefaultsWithFormData(defaults, formData) {
if (Array.isArray(formData)) {
if (!Array.isArray(defaults)) {
defaults = [];
}
return formData.map((value, idx) => {
if (defaults[idx]) {
return mergeDefaultsWithFormData(defaults[idx], value);
}
return value;
});
} if (isObject(formData)) {
const acc = Object.assign({}, defaults); // Prevent mutation of source object.
return Object.keys(formData).reduce((preAcc, key) => {
preAcc[key] = mergeDefaultsWithFormData(
defaults ? defaults[key] : {},
formData[key]
);
return preAcc;
}, acc);
}
return formData;
}
function computeDefaults(
_schema,
parentDefaults,
rootSchema,
rawFormData = {},
includeUndefinedValues = false,
haveAllFields = false
) {
let schema = isObject(_schema) ? _schema : {};
const formData = isObject(rawFormData) ? rawFormData : {};
// allOf 处理合并数据
if ('allOf' in schema) {
schema = resolveAllOf(schema, rootSchema, formData);
}
// Compute the defaults recursively: give highest priority to deepest nodes.
let defaults = parentDefaults;
if (isObject(defaults) && isObject(schema.default)) {
// For object defaults, only override parent defaults that are defined in
// schema.default.
defaults = mergeObjects(defaults, schema.default);
} else if ('default' in schema) {
// Use schema defaults for this node.
defaults = schema.default;
} else if ('$ref' in schema) {
// Use referenced schema defaults for this node.
const refSchema = findSchemaDefinition(schema.$ref, rootSchema);
return computeDefaults(
refSchema,
defaults,
rootSchema,
formData,
includeUndefinedValues,
haveAllFields
);
} else if /* ('dependencies' in schema) {
const resolvedSchema = resolveDependencies(schema, rootSchema, formData);
return computeDefaults(
resolvedSchema,
defaults,
rootSchema,
formData,
includeUndefinedValues,
haveAllFields
);
} else if */ (isFixedItems(schema)) {
defaults = schema.items.map((itemSchema, idx) => computeDefaults(
itemSchema,
Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
rootSchema,
formData,
includeUndefinedValues,
haveAllFields
));
} else if ('oneOf' in schema) {
const matchSchema = retrieveSchema(
schema.oneOf[getMatchingOption(formData, schema.oneOf, rootSchema, haveAllFields)],
rootSchema,
formData
);
schema = mergeObjects(schema, matchSchema);
delete schema.oneOf;
// if (schema.properties && matchSchema.properties) {
// // 对象 oneOf 需要合并原属性和 oneOf 属性
// const mergeSchema = mergeObjects(schema, matchSchema);
// delete mergeSchema.oneOf;
// schema = mergeSchema;
// } else {
// schema = matchSchema;
// }
} else if ('anyOf' in schema) {
const matchSchema = retrieveSchema(
schema.anyOf[getMatchingOption(formData, schema.anyOf, rootSchema, haveAllFields)],
rootSchema,
formData
);
schema = mergeObjects(schema, matchSchema);
delete schema.anyOf;
// if (schema.properties && matchSchema.properties) {
// // 对象 anyOf 需要合并原属性和 anyOf 属性
// const mergeSchema = mergeObjects(schema, matchSchema);
// delete mergeSchema.anyOf;
// schema = mergeSchema;
// } else {
// schema = matchSchema;
// }
}
// Not defaults defined for this node, fallback to generic typed ones.
if (typeof defaults === 'undefined') {
defaults = schema.default;
}
// eslint-disable-next-line default-case
switch (getSchemaType(schema)) {
case 'null':
return null;
// We need to recur for object schema inner default values.
case 'object':
return Object.keys(schema.properties || {}).reduce((acc, key) => {
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults(
schema.properties[key],
(defaults || {})[key],
rootSchema,
(formData || {})[key],
includeUndefinedValues,
haveAllFields
);
if (includeUndefinedValues || computedDefault !== undefined) {
acc[key] = computedDefault;
}
return acc;
}, {});
case 'array':
// Inject defaults into existing array defaults
if (Array.isArray(defaults)) {
defaults = defaults.map((item, idx) => computeDefaults(
schema.items[idx] || schema.additionalItems || {},
item,
rootSchema,
{},
includeUndefinedValues,
haveAllFields
));
}
// Deeply inject defaults into already existing form data
if (Array.isArray(rawFormData)) {
defaults = rawFormData.map((item, idx) => computeDefaults(
schema.items,
(defaults || {})[idx],
rootSchema,
item,
{},
includeUndefinedValues,
haveAllFields
));
}
if (schema.minItems) {
if (!isMultiSelect(schema, rootSchema)) {
const defaultsLength = defaults ? defaults.length : 0;
if (schema.minItems > defaultsLength) {
const defaultEntries = defaults || [];
// populate the array with the defaults
const fillerSchema = Array.isArray(schema.items)
? schema.additionalItems
: schema.items;
const fillerEntries = fillObj(
new Array(schema.minItems - defaultsLength), computeDefaults(
fillerSchema, fillerSchema.defaults, rootSchema, {}, includeUndefinedValues, haveAllFields
)
);
return defaultEntries.concat(fillerEntries);
}
} else {
return defaults || [];
}
}
// undefined 默认一个空数组
defaults = defaults === undefined ? [] : defaults;
}
return defaults;
}
// 获取默认form data
export default function getDefaultFormState(
_schema,
formData,
rootSchema = {},
includeUndefinedValues = true,
haveAllFields = false
) {
if (!isObject(_schema)) {
throw new Error(`Invalid schema: ${_schema}`);
}
const schema = retrieveSchema(_schema, rootSchema, formData);
const defaults = computeDefaults(
schema,
_schema.default,
rootSchema,
formData,
includeUndefinedValues,
haveAllFields
);
if (typeof formData === 'undefined') {
// No form data? Use schema defaults.
return defaults;
}
// 传入formData时合并传入数据
if (isObject(formData) || Array.isArray(formData)) {
return mergeDefaultsWithFormData(defaults, formData);
}
if (formData === 0 || formData === false || formData === '') {
return formData;
}
return formData || defaults;
}

View File

@ -0,0 +1,424 @@
/**
* @param schema
* @param rootSchema
* @param formData
* @returns {{properties: *}|{}|{properties: *}|{}|{properties: *}|{additionalProperties}|*|{}|{allOf}}
* 源码来自react-jsonschema-form
* 做了细节和模块调整
* 重写了allOf实现逻辑解决使用allOf必须根节点同时存在以及对json-schema-merge-allof依赖包过大
* 移除对lodash json-schema-merge-allofjsonpointer 等依赖重新实现
* https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/utils.js#L621
*/
import findSchemaDefinition from './findSchemaDefinition';
import { intersection } from '../arrayUtils';
import {
/* guessType, mergeSchemas, */ isObject, scm
} from '../utils';
// import { getMatchingOption, isValid } from './validate';
// 自动添加分割线
// export const ADDITIONAL_PROPERTY_FLAG = '__additional_property';
// resolve Schema - dependencies
// https://json-schema.org/understanding-json-schema/reference/object.html#dependencies
/*
export function resolveDependencies(schema, rootSchema, formData) {
// 从源模式中删除依赖项。
const { dependencies = {} } = schema;
let { ...resolvedSchema } = schema;
if ('oneOf' in resolvedSchema) {
resolvedSchema = resolvedSchema.oneOf[
getMatchingOption(formData, resolvedSchema.oneOf, rootSchema)
];
} else if ('anyOf' in resolvedSchema) {
resolvedSchema = resolvedSchema.anyOf[
getMatchingOption(formData, resolvedSchema.anyOf, rootSchema)
];
}
return processDependencies(
dependencies,
resolvedSchema,
rootSchema,
formData
);
}
*/
// 处理依赖关系 dependencies
// https://json-schema.org/understanding-json-schema/reference/object.html#dependencies
/*
function processDependencies(
dependencies,
resolvedSchema,
rootSchema,
formData
) {
// Process dependencies updating the local schema properties as appropriate.
for (const dependencyKey in dependencies) {
// Skip this dependency if its trigger property is not present.
if (formData[dependencyKey] === undefined) {
// eslint-disable-next-line no-continue
continue;
}
// Skip this dependency if it is not included in the schema (such as when dependencyKey is itself a hidden dependency.)
if (
resolvedSchema.properties
&& !(dependencyKey in resolvedSchema.properties)
) {
// eslint-disable-next-line no-continue
continue;
}
const {
[dependencyKey]: dependencyValue,
...remainingDependencies
} = dependencies;
if (Array.isArray(dependencyValue)) {
resolvedSchema = withDependentProperties(resolvedSchema, dependencyValue);
} else if (isObject(dependencyValue)) {
resolvedSchema = withDependentSchema(
resolvedSchema,
rootSchema,
formData,
dependencyKey,
dependencyValue
);
}
return processDependencies(
remainingDependencies,
resolvedSchema,
rootSchema,
formData
);
}
return resolvedSchema;
}
*/
// 属性依赖
// https://json-schema.org/understanding-json-schema/reference/object.html#property-dependencies
/*
function withDependentProperties(schema, additionallyRequired) {
if (!additionallyRequired) {
return schema;
}
const required = Array.isArray(schema.required)
? Array.from(new Set([...schema.required, ...additionallyRequired]))
: additionallyRequired;
return { ...schema, required };
}
*/
// schema 依赖
// https://json-schema.org/understanding-json-schema/reference/object.html#schema-dependencies
/*
function withDependentSchema(
schema,
rootSchema,
formData,
dependencyKey,
dependencyValue
) {
const { oneOf, ...dependentSchema } = retrieveSchema(
dependencyValue,
rootSchema,
formData
);
schema = mergeSchemas(schema, dependentSchema);
// Since it does not contain oneOf, we return the original schema.
if (oneOf === undefined) {
return schema;
} if (!Array.isArray(oneOf)) {
throw new Error(`invalid: it is some ${typeof oneOf} instead of an array`);
}
// Resolve $refs inside oneOf.
const resolvedOneOf = oneOf.map(subschema => (subschema.hasOwnProperty('$ref')
? resolveReference(subschema, rootSchema, formData)
: subschema));
return withExactlyOneSubschema(
schema,
rootSchema,
formData,
dependencyKey,
resolvedOneOf
);
}
function withExactlyOneSubschema(
schema,
rootSchema,
formData,
dependencyKey,
oneOf
) {
// eslint-disable-next-line array-callback-return,consistent-return
const validSubschemas = oneOf.filter((subschema) => {
if (!subschema.properties) {
return false;
}
const { [dependencyKey]: conditionPropertySchema } = subschema.properties;
if (conditionPropertySchema) {
const conditionSchema = {
type: 'object',
properties: {
[dependencyKey]: conditionPropertySchema,
},
};
return isValid(conditionSchema, formData);
}
});
if (validSubschemas.length !== 1) {
console.warn(
"ignoring oneOf in dependencies because there isn't exactly one subschema that is valid"
);
return schema;
}
const subschema = validSubschemas[0];
const {
// eslint-disable-next-line no-unused-vars
[dependencyKey]: conditionPropertySchema,
...dependentSubschema
} = subschema.properties;
const dependentSchema = { ...subschema, properties: dependentSubschema };
return mergeSchemas(
schema,
retrieveSchema(dependentSchema, rootSchema, formData)
);
}
*/
// resolve Schema - $ref
// https://json-schema.org/understanding-json-schema/structuring.html#using-id-with-ref
function resolveReference(schema, rootSchema, formData) {
// Retrieve the referenced schema definition.
const $refSchema = findSchemaDefinition(schema.$ref, rootSchema);
// Drop the $ref property of the source schema.
// eslint-disable-next-line no-unused-vars
const { $ref, ...localSchema } = schema;
// Update referenced schema definition with local schema properties.
return retrieveSchema(
{ ...$refSchema, ...localSchema },
rootSchema,
formData
);
}
// 深度递归合并 合并allOf的每2项
function mergeSchemaAllOf(...args) {
if (args.length < 2) return args[0];
let preVal = {};
const copyArgs = [...args];
while (copyArgs.length >= 2) {
const obj1 = isObject(copyArgs[0]) ? copyArgs[0] : {};
const obj2 = isObject(copyArgs[1]) ? copyArgs[1] : {};
preVal = Object.assign({}, obj1);
Object.keys(obj2).reduce((acc, key) => {
const left = obj1[key];
const right = obj2[key];
// 左右一边为object
if (isObject(left) || isObject(right)) {
// 两边同时为object
if (isObject(left) && isObject(right)) {
acc[key] = mergeSchemaAllOf(left, right);
} else {
// 其中一边为 object
const [objTypeData, baseTypeData] = isObject(left) ? [left, right] : [right, left];
if (key === 'additionalProperties') {
// 适配类型: 一边配置了对象一边没配置或者true false
// {
// additionalProperties: {
// type: 'string',
// },
// additionalProperties: false
// }
acc[key] = baseTypeData === true ? objTypeData : false; // default false
} else {
acc[key] = objTypeData;
}
}
// 一边为array
} else if (Array.isArray(left) || Array.isArray(right)) {
// 同为数组取交集
if (Array.isArray(left) && Array.isArray(right)) {
// 数组里面嵌套对象不支持 因为我不知道该怎么合并
if (isObject(left[0]) || isObject(right[0])) {
throw new Error('暂不支持如上数组对象元素合并');
}
// 交集
const intersectionArray = intersection([].concat(left), [].concat(right));
// 没有交集
if (intersectionArray.length <= 0) {
throw new Error('无法合并如上数据');
}
if (intersectionArray.length === 0 && key === 'type') {
// 自己取出值
acc[key] = intersectionArray[0];
} else {
acc[key] = intersectionArray;
}
} else {
// 其中一边为 Array
// 查找包含关系
const [arrayTypeData, baseTypeData] = Array.isArray(left) ? [left, right] : [right, left];
// 空值直接合并另一边
if (baseTypeData === undefined) {
acc[key] = arrayTypeData;
} else {
if (!arrayTypeData.includes(baseTypeData)) {
throw new Error('无法合并如下数据');
}
acc[key] = baseTypeData;
}
}
} else if (left !== undefined && right !== undefined) {
// 两边都不是 undefined - 基础数据类型 string number boolean...
if (key === 'maxLength' || key === 'maximum' || key === 'maxItems' || key === 'exclusiveMaximum' || key === 'maxProperties') {
acc[key] = Math.min(left, right);
} else if (key === 'minLength' || key === 'minimum' || key === 'minItems' || key === 'exclusiveMinimum' || key === 'minProperties') {
acc[key] = Math.max(left, right);
} else if (key === 'multipleOf') {
// 获取最小公倍数
acc[key] = scm(left, right);
} else {
// if (left !== right) {
// throw new Error('无法合并如下数据');
// }
acc[key] = left;
}
} else {
// 一边为undefined
acc[key] = left === undefined ? right : left;
}
return acc;
}, preVal);
// 先进先出
copyArgs.splice(0, 2, preVal);
}
return preVal;
}
// resolve Schema - allOf
export function resolveAllOf(schema, rootSchema, formData) {
// allOf item中可能存在 $ref
const resolvedAllOfRefSchema = {
...schema,
allOf: schema.allOf.map(allOfItem => retrieveSchema(allOfItem, rootSchema, formData)),
};
try {
const { allOf, ...originProperties } = resolvedAllOfRefSchema;
return mergeSchemaAllOf(originProperties, ...allOf);
} catch (e) {
console.error(`无法合并allOf丢弃allOf配置继续渲染: \n${e}`);
// eslint-disable-next-line no-unused-vars
const { allOf: errAllOf, ...resolvedSchemaWithoutAllOf } = resolvedAllOfRefSchema;
return resolvedSchemaWithoutAllOf;
}
}
// resolve Schema
function resolveSchema(schema, rootSchema = {}, formData = {}) {
// allOf 、$ref、dependencies 可能被同时配置
// allOf
if (schema.hasOwnProperty('allOf')) {
schema = resolveAllOf(schema, rootSchema, formData);
}
// $ref
if (schema.hasOwnProperty('$ref')) {
schema = resolveReference(schema, rootSchema, formData);
}
// dependencies
/*
if (schema.hasOwnProperty('dependencies')) {
const resolvedSchema = resolveDependencies(schema, rootSchema, formData);
schema = retrieveSchema(resolvedSchema, rootSchema, formData);
}
*/
// additionalProperties
/*
const hasAdditionalProperties = schema.hasOwnProperty('additionalProperties') && schema.additionalProperties !== false;
if (hasAdditionalProperties) {
return stubExistingAdditionalProperties(
schema,
rootSchema,
formData
);
}
*/
return schema;
}
// 这个函数将为formData中的每个键创建新的“属性”项
// 查找到附加属性统一到properties[key]格式 并且打上标准
/* function stubExistingAdditionalProperties(
schema,
rootSchema = {},
formData = {}
) {
// clone the schema so we don't ruin the consumer's original
schema = {
...schema,
properties: { ...schema.properties },
};
Object.keys(formData).forEach((key) => {
if (schema.properties.hasOwnProperty(key)) {
// No need to stub, our schema already has the property
return;
}
let additionalProperties;
if (schema.additionalProperties.hasOwnProperty('$ref')) {
additionalProperties = retrieveSchema(
{ $ref: schema.additionalProperties.$ref },
rootSchema,
formData
);
} else if (schema.additionalProperties.hasOwnProperty('type')) {
additionalProperties = { ...schema.additionalProperties };
} else {
additionalProperties = { type: guessType(formData[key]) };
}
// The type of our new key should match the additionalProperties value;
// 把追加进去的属性设置为标准 schema格式同时打上标志
schema.properties[key] = additionalProperties;
// Set our additional property flag so we know it was dynamically added
schema.properties[key][ADDITIONAL_PROPERTY_FLAG] = true;
});
return schema;
} */
// 索引当前节点
export default function retrieveSchema(schema, rootSchema = {}, formData = {}) {
if (!isObject(schema)) {
return {};
}
return resolveSchema(schema, rootSchema, formData);
}

View File

@ -0,0 +1,335 @@
import Ajv from 'ajv';
import i18n from '../i18n';
import retrieveSchema from './retriev';
import {
isObject, deepEquals
} from '../utils';
import { getUserErrOptions } from '../formUtils';
let ajv = createAjvInstance();
let formerCustomFormats = null;
let formerMetaSchema = null;
// 创建实例
function createAjvInstance() {
const ajvInstance = new Ajv({
errorDataPath: 'property',
allErrors: true,
multipleOfPrecision: 8,
schemaId: 'auto',
unknownFormats: 'ignore',
});
// 添加base-64 format
ajvInstance.addFormat(
'data-url',
/^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/
);
// 添加color format
ajvInstance.addFormat(
'color',
// eslint-disable-next-line max-len
/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/
);
return ajvInstance;
}
/**
* 将错误输出从ajv转换为jsonschema使用的格式
* At some point, components should be updated to support ajv.
*/
function transformAjvErrors(errors = []) {
if (errors === null) {
return [];
}
return errors.map((e) => {
const {
dataPath, keyword, message, params, schemaPath
} = e;
const property = `${dataPath}`;
// put data in expected format
return {
name: keyword,
property,
message,
params, // specific to ajv
stack: `${property} ${message}`.trim(),
schemaPath,
};
});
}
/**
* 通过 schema校验formData并返回错误信息
* @param formData 校验的数据
* @param schema
* @param transformErrors function - 转换错误, 如个性化的配置
* @param additionalMetaSchemas 数组 添加 ajv metaSchema
* @param customFormats 添加 ajv 自定义 formats
* @returns {{errors: ([]|{stack: string, schemaPath: *, name: *, property: string, message: *, params: *}[])}}
*/
export function ajvValidateFormData({
formData,
schema,
transformErrors,
additionalMetaSchemas = [],
customFormats = {}
} = {}) {
const hasNewMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas);
const hasNewFormats = !deepEquals(formerCustomFormats, customFormats);
// 变更了 Meta或者调整了format配置重置新的实例
if (hasNewMetaSchemas || hasNewFormats) {
ajv = createAjvInstance();
}
// 添加更多要验证的模式
if (
additionalMetaSchemas
&& hasNewMetaSchemas
&& Array.isArray(additionalMetaSchemas)
) {
ajv.addMetaSchema(additionalMetaSchemas);
formerMetaSchema = additionalMetaSchemas;
}
// 注册自定义的 formats - 没有变更只会注册一次 - 否则重新创建实例
if (customFormats && hasNewFormats && isObject(customFormats)) {
Object.keys(customFormats).forEach((formatName) => {
ajv.addFormat(formatName, customFormats[formatName]);
});
formerCustomFormats = customFormats;
}
let validationError = null;
try {
ajv.validate(schema, formData);
} catch (err) {
validationError = err;
}
// ajv 默认多语言处理
i18n.getCurrentLocalize()(ajv.errors);
let errors = transformAjvErrors(ajv.errors);
// 清除错误
ajv.errors = null;
// 处理异常
const noProperMetaSchema = validationError
&& validationError.message
&& typeof validationError.message === 'string'
&& validationError.message.includes('no schema with key or ref ');
if (noProperMetaSchema) {
errors = [
...errors,
{
stack: validationError.message,
},
];
}
// 转换错误, 如传入自定义的错误
if (typeof transformErrors === 'function') {
errors = transformErrors(errors);
}
return {
errors
};
}
// 校验formData 并转换错误信息
export function validateFormDataAndTransformMsg({
formData,
schema,
uiSchema,
transformErrors,
additionalMetaSchemas = [],
customFormats = {},
errorSchema = {},
required = false,
propPath = '',
isOnlyFirstError = true, // 只取第一条错误信息
} = {}) {
// 是否过滤根节点错误 固定只能根
const filterRootNodeError = true;
// 校验required信息 isEmpty 校验
// 如果数组类型针对配置了 format 的特殊处理
const emptyArray = (schema.type === 'array' && Array.isArray(formData) && formData.length === 0);
const isEmpty = formData === undefined || emptyArray;
if (required) {
if (isEmpty) {
const requireErrObj = {
keyword: 'required',
params: {
missingProperty: propPath
}
};
// 用户设置校验信息
const errSchemaMsg = getUserErrOptions({
schema,
uiSchema,
errorSchema
}).required;
if (errSchemaMsg) {
requireErrObj.message = errSchemaMsg;
} else {
// 处理多语言require提示信息 ajv 修改原引用)
i18n.getCurrentLocalize()([requireErrObj]);
}
return [requireErrObj];
}
} else if (isEmpty && !emptyArray) {
// 非required 为空 校验通过
return [];
}
// 校验ajv错误信息
let ajvErrors = ajvValidateFormData({
formData,
schema,
transformErrors,
additionalMetaSchemas,
customFormats,
}).errors;
// 过滤顶级错误
if (filterRootNodeError) {
ajvErrors = ajvErrors.filter(
item => (item.property === ''
&& (!item.schemaPath.includes('#/anyOf/') && !item.schemaPath.includes('#/oneOf/')))
|| item.name === 'additionalProperties'
);
}
const userErrOptions = getUserErrOptions({
schema,
uiSchema,
errorSchema
});
return (isOnlyFirstError && ajvErrors.length > 0 ? [ajvErrors[0]] : ajvErrors).reduce((preErrors, errorItem) => {
// 优先获取 errorSchema 配置
errorItem.message = userErrOptions[errorItem.name] !== undefined ? userErrOptions[errorItem.name] : errorItem.message;
preErrors.push(errorItem);
return preErrors;
}, []);
}
/**
* 根据模式验证数据如果数据有效则返回true否则返回* false如果模式无效那么这个函数将返回* false
* @param schema
* @param data
* @returns {boolean|PromiseLike<any>}
*/
export function isValid(schema, data) {
try {
return ajv.validate(schema, data);
} catch (e) {
return false;
}
}
// ajv valida
export function ajvValid(schema, data) {
return ajv.validate(schema, data);
}
// 如果查找不到
// return -1
export function getMatchingIndex(formData, options, rootSchema, haveAllFields = false) {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < options.length; i++) {
const option = retrieveSchema(options[i], rootSchema, formData);
// If the schema describes an object then we need to add slightly more
// strict matching to the schema, because unless the schema uses the
// "requires" keyword, an object will match the schema as long as it
// doesn't have matching keys with a conflicting type. To do this we use an
// "anyOf" with an array of requires. This augmentation expresses that the
// schema should match if any of the keys in the schema are present on the
// object and pass validation.
if (option.properties) {
// Create an "anyOf" schema that requires at least one of the keys in the
// "properties" object
const requiresAnyOf = {
// 如果后代节点存在 $ref 需要正常引用
...(rootSchema.definitions ? {
definitions: rootSchema.definitions
} : {}),
anyOf: Object.keys(option.properties).map(key => ({
required: [key],
})),
};
let augmentedSchema;
// If the "anyOf" keyword already exists, wrap the augmentation in an "allOf"
if (option.anyOf) {
// Create a shallow clone of the option
const { ...shallowClone } = option;
if (!shallowClone.allOf) {
shallowClone.allOf = [];
} else {
// If "allOf" already exists, shallow clone the array
shallowClone.allOf = shallowClone.allOf.slice();
}
shallowClone.allOf.push(requiresAnyOf);
augmentedSchema = shallowClone;
} else {
augmentedSchema = Object.assign({}, option, requiresAnyOf);
}
// Remove the "required" field as it's likely that not all fields have
// been filled in yet, which will mean that the schema is not valid
// 如果编辑回填数据的场景 可直接使用 required 判断
if (!haveAllFields) delete augmentedSchema.required;
if (isValid(augmentedSchema, formData)) {
return i;
}
} else if (isValid(options[i], formData)) {
return i;
}
}
// 尝试查找const 配置
if (options[0] && options[0].properties) {
const constProperty = Object.keys(options[0].properties).find(k => options[0].properties[k].const);
if (constProperty) {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < options.length; i++) {
if (
options[i].properties
&& options[i].properties[constProperty]
&& options[i].properties[constProperty].const === formData[constProperty]) {
return i;
}
}
}
}
return -1;
}
// oneOf anyOf 通过formData的值来找到当前匹配项索引
export function getMatchingOption(formData, options, rootSchema, haveAllFields = false) {
const index = getMatchingIndex(formData, options, rootSchema, haveAllFields);
return index === -1 ? 0 : index;
}

View File

@ -0,0 +1,322 @@
.genFromComponent {
font-size: 14px;
line-height: 1;
word-wrap: break-word;
word-break: break-word;
padding: 0;
margin: 0;
a,
p,
li,
ul,
h1,
h2,
h3,
p {
font-size: 14px;
}
.genFormIcon {
width: 12px;
height: 12px;
vertical-align: top;
}
.genFormBtn {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: .1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
&.is-plain{
&:focus, &:hover {
background: #fff;
border-color: #409eff;
color: #409eff;
}
}
}
.hiddenWidget {
display: none;
}
.fieldGroupWrap+.fieldGroupWrap {
.fieldGroupWrap_title {
margin-top: 20px;
}
}
.fieldGroupWrap_title {
position: relative;
display: block;
width: 100%;
line-height: 26px;
margin-bottom: 8px;
font-size: 15px;
font-weight: bold;
border: 0;
}
.fieldGroupWrap_des {
font-size: 12px;
line-height: 20px;
margin-bottom: 10px;
color: rgb(153, 153, 153);
}
.genFromWidget_des {
padding: 0;
margin-top: 0;
margin-bottom: 2px;
font-size: 12px;
line-height: 20px;
color: #999;
text-align: left;
width: 100%;
flex-shrink: 0;
}
.formItemErrorBox {
margin: 0 auto;
color: #ff5757;
padding-top: 2px;
position: absolute;
top: 100%;
left: 0;
display: -webkit-box !important;
line-height: 16px;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
white-space: normal;
font-size: 12px;
text-align: left;
}
.genFormIcon-qs {
fill: #606266;
vertical-align: middle;
display: inline-block;
width: 16px;
height: 16px;
margin-left: 2px;
margin-top: -2px;
cursor: pointer;
}
.genFormItemRequired {
&:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
}
/* oneOf anyOf - appendCombining_box*/
.appendCombining_box {
margin-bottom: 22px;
.appendCombining_box {
margin-bottom: 10px;
}
padding: 10px;
background: rgba(242,242,242,0.8);
box-shadow: 0 3px 1px -2px rgba(0,0,0,0.2), 0 0 3px 1px rgba(0,0,0,0.1);
}
/* validateWidget 单独的校验不属于输入框的*/
.validateWidget {
margin-bottom: 0 !important;
width: 100% !important;
flex-basis: 100% !important;
padding: 0 !important;
.formItemErrorBox {
padding: 5px 0;
position: relative;
}
}
/* type array */
.arrayField:not(.genFormItem){
margin-bottom: 22px;
.arrayField {
margin-bottom: 10px;
}
}
.arrayOrderList {
background: rgba(242,242,242,0.8);
box-shadow: 0 3px 1px -2px rgba(0,0,0,0.2), 0 0 3px 1px rgba(0,0,0,0.1);
}
.arrayOrderList_item {
position: relative;
padding: 25px 10px 12px;
border-radius: 2px;
margin-bottom: 6px;
display: flex;
align-items: center;
}
.arrayOrderList_bottomAddBtn {
text-align: right;
padding: 15px 10px;
margin-bottom: 10px;
}
.bottomAddBtn {
width: 40%;
min-width: 10px;
max-width: 180px;
/*box-shadow: 0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 1px rgba(0,0,0,0.1);*/
}
.arrayListItem_content {
padding-top: 15px;
flex: 1;
margin: 0 auto;
box-shadow: 0 -1px 0 0 rgba(0,0,0,0.05);
}
.arrayListItem_index, .arrayListItem_operateTool{
position: absolute;
height: 25px;
}
.arrayListItem_index {
top: 6px;
line-height: 18px;
height: 18px;
padding: 0 6px;
background-color: rgba(0,0,0,.28);
color: #fff;
font-size: 12px;
border-radius: 2px;
}
.arrayListItem_operateTool {
width: 75px;
right: 9px;
top: -1px;
text-align: right;
font-size: 0;
}
.arrayListItem_btn {
vertical-align: top;
display: inline-block;
padding: 6px;
margin: 0;
font-size: 0;
-webkit-appearance: none;
appearance: none;
outline: none;
border: none;
cursor: pointer;
text-align: center;
background: transparent;
color: #666;
&:hover {
opacity: 0.6;
}
&[disabled] {
color: #999999;
opacity: 0.3 !important;
cursor: not-allowed;
}
}
.arrayListItem_orderBtn-top {
background-color: #f0f9eb;
}
.arrayListItem_orderBtn-bottom {
background-color: #f0f9eb;
}
.arrayListItem_btn-delete {
background-color: #fef0f0;
}
.formFooter_item {
text-align: right;
border-top: 1px solid rgba(0, 0, 0, 0.08);
padding-top: 10px;
}
&.formInlineFooter {
&>.fieldGroupWrap{
display: inline-block;
margin-right: 10px;
}
}
/*.arrayListItem_content .genFormItem {
&:last-child {
margin-bottom: 0;
}
}*/
&.formInline {
/*.genFormItem {
display: inline-block;
margin-right: 10px;
vertical-align: top;
}*/
.validateWidget {
margin-right: 0;
}
.formFooter_item {
border-top: none;
padding-top: 0;
}
}
}
/*popover 弹出可能appendtobody*/
.genFromWidget_des_mini {
font-size: 14px;
line-height: 1.5715;
}
/* 适配多列布局 */
:root {
--width-column-gutter : 10px;
}
.layoutColumn {
.layoutColumn_w100 {
width: 100% !important;
flex-basis: 100% !important;;
}
.fieldGroupWrap_box {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
align-content: flex-start;
&>div {
width: 100%;
flex-basis: 100%;
}
&>.genFormItem{
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
padding-right: var(--width-column-gutter);
}
}
&.layoutColumn-1 {
.fieldGroupWrap_box>.genFormItem {
padding-right: 0;
}
}
&.layoutColumn-2 {
.fieldGroupWrap_box>.genFormItem {
width: 50%;
flex-basis: 50%;
}
}
&.layoutColumn-3 {
.fieldGroupWrap_box>.genFormItem{
width: 33.333%;
flex-basis: 33.333%;
}
}
}

View File

@ -0,0 +1,346 @@
/**
* Created by Liu.Jun on 2020/4/17 17:05.
*/
// is object
export function isObject(object) {
return Object.prototype.toString.call(object) === '[object Object]';
}
// is arguments
function isArguments(object) {
return Object.prototype.toString.call(object) === '[object Arguments]';
}
// 定义的数据推导出schema 类型
export const guessType = function guessType(value) {
if (Array.isArray(value)) {
return 'array';
} if (typeof value === 'string') {
return 'string';
} if (value == null) {
return 'null';
} if (typeof value === 'boolean') {
return 'boolean';
// eslint-disable-next-line no-restricted-globals
} if (!isNaN(value)) {
return 'number';
} if (typeof value === 'object') {
return 'object';
}
// Default to string if we can't figure it out
return 'string';
};
export function union(arr1, arr2) {
return [...new Set([...arr1, ...arr2])];
}
// Recursively merge deeply nested schemas.
// The difference between mergeSchemas and mergeObjects
// is that mergeSchemas only concats arrays for
// values under the "required" keyword, and when it does,
// it doesn't include duplicate values.
export function mergeSchemas(obj1, obj2) {
const acc = Object.assign({}, obj1); // Prevent mutation of source object.
// eslint-disable-next-line no-shadow
return Object.keys(obj2).reduce((acc, key) => {
const left = obj1 ? obj1[key] : {};
const right = obj2[key];
if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
acc[key] = mergeSchemas(left, right);
} else if (
obj1
&& obj2
&& (getSchemaType(obj1) === 'object' || getSchemaType(obj2) === 'object')
&& key === 'required'
&& Array.isArray(left)
&& Array.isArray(right)
) {
// Don't include duplicate values when merging
// "required" fields.
acc[key] = union(left, right);
} else {
acc[key] = right;
}
return acc;
}, acc);
}
// 合并对象数据
export function mergeObjects(obj1, obj2, concatArrays = false) {
// Recursively merge deeply nested objects.
const preAcc = Object.assign({}, obj1); // Prevent mutation of source object.
if (!isObject(obj2)) return preAcc;
return Object.keys(obj2).reduce((acc, key) => {
const left = obj1 ? obj1[key] : {};
const right = obj2[key];
if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
acc[key] = mergeObjects(left, right, concatArrays);
} else if (concatArrays && Array.isArray(left) && Array.isArray(right)) {
acc[key] = left.concat(right);
} else {
acc[key] = right;
}
return acc;
}, preAcc);
}
// 获取给定 schema 类型。
export function getSchemaType(schema) {
const { type } = schema;
// 通过const 申明的常量 做类型推断
if (!type && schema.const) {
return guessType(schema.const);
}
// 枚举默认字符串
if (!type && schema.enum) {
return 'string';
}
// items 推断为 array 类型
if (!type && (schema.items)) {
return 'array';
}
// anyOf oneOf 不申明 type 字段
if (!type && (schema.properties || schema.additionalProperties)) {
return 'object';
}
if (type instanceof Array && type.length === 2 && type.includes('null')) {
return type.find(curType => curType !== 'null');
}
return type;
}
// 深度相等对比
export function deepEquals(a, b, ca = [], cb = []) {
// Partially extracted from node-deeper and adapted to exclude comparison
// checks for functions.
// https://github.com/othiym23/node-deeper
if (a === b) {
return true;
} if (typeof a === 'function' || typeof b === 'function') {
// Assume all functions are equivalent
// see https://github.com/mozilla-services/react-jsonschema-form/issues/255
return true;
} if (typeof a !== 'object' || typeof b !== 'object') {
return false;
} if (a === null || b === null) {
return false;
} if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
} if (a instanceof RegExp && b instanceof RegExp) {
return (
a.source === b.source
&& a.global === b.global
&& a.multiline === b.multiline
&& a.lastIndex === b.lastIndex
&& a.ignoreCase === b.ignoreCase
);
} if (isArguments(a) || isArguments(b)) {
if (!(isArguments(a) && isArguments(b))) {
return false;
}
const slice = Array.prototype.slice;
return deepEquals(slice.call(a), slice.call(b), ca, cb);
}
if (a.constructor !== b.constructor) {
return false;
}
const ka = Object.keys(a);
const kb = Object.keys(b);
// don't bother with stack acrobatics if there's nothing there
if (ka.length === 0 && kb.length === 0) {
return true;
}
if (ka.length !== kb.length) {
return false;
}
let cal = ca.length;
// eslint-disable-next-line no-plusplus
while (cal--) {
if (ca[cal] === a) {
return cb[cal] === b;
}
}
ca.push(a);
cb.push(b);
ka.sort();
kb.sort();
// eslint-disable-next-line no-plusplus
for (let j = ka.length - 1; j >= 0; j--) {
if (ka[j] !== kb[j]) {
return false;
}
}
let key;
// eslint-disable-next-line no-plusplus
for (let k = ka.length - 1; k >= 0; k--) {
key = ka[k];
if (!deepEquals(a[key], b[key], ca, cb)) {
return false;
}
}
ca.pop();
cb.pop();
return true;
}
// 只保证同时生成不重复
export const genId = (function genIdFn() {
let preKey = `${+new Date()}`;
let key = 0;
return () => {
const curTimestamp = `${+new Date()}`;
if (curTimestamp === preKey) {
key += 1;
} else {
// 重置 key
key = 0;
}
preKey = curTimestamp;
return `${preKey}x${key}`;
};
}());
// 空对象
export function isEmptyObject(obj) {
if (!obj) return true;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
// 过滤和转换对象的key
export function filterObject(obj, filterFn) {
return Object.entries(obj).reduce((preVal, [key, value]) => {
const newKey = filterFn(key, value);
if (undefined !== newKey) {
preVal[newKey] = value;
}
return preVal;
}, {});
}
const f = s => `0${s}`.substr(-2);
export function parseDateString(dateString, includeTime = true) {
if (!dateString) {
return {
year: -1,
month: -1,
day: -1,
hour: includeTime ? -1 : 0,
minute: includeTime ? -1 : 0,
second: includeTime ? -1 : 0,
};
}
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
throw new Error(`Unable to parse date ${dateString}`);
}
return {
year: date.getFullYear(),
month: f(date.getMonth() + 1), // oh you, javascript.
day: f(date.getDate()),
hour: f(includeTime ? date.getHours() : 0),
minute: f(includeTime ? date.getMinutes() : 0),
second: f(includeTime ? date.getSeconds() : 0),
};
}
export function toDateString(
{
year, month, day, hour = 0, minute = 0, second = 0
},
time = true
) {
const utcTime = Date.UTC(year, month - 1, day, hour, minute, second);
const datetime = new Date(utcTime).toJSON();
return time ? datetime : datetime.slice(0, 10);
}
export function pad(num, size) {
let s = String(num);
while (s.length < size) {
s = `0${s}`;
}
return s;
}
// dataUrl 转 Blob文件对象
export function dataURItoBlob(dataURI) {
// Split metadata from data
const splitted = dataURI.split(',');
// Split params
const params = splitted[0].split(';');
// Get mime-type from params
const type = params[0].replace('data:', '');
// Filter the name property from params
const properties = params.filter(param => param.split('=')[0] === 'name');
// Look for the name and use unknown if no name property.
let name;
if (properties.length !== 1) {
name = 'unknown';
} else {
// Because we filtered out the other property,
// we only have the name case here.
name = properties[0].split('=')[1];
}
// Built the Uint8Array Blob parameter from the base64 string.
const binary = atob(splitted[1]);
const array = [];
// eslint-disable-next-line no-plusplus
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
// Create the blob object
const blob = new window.Blob([new Uint8Array(array)], { type });
return { blob, name };
}
// 字符串首字母小写
export function lowerCase(str) {
if (undefined === str) return str;
return String(str).replace(/^./, s => s.toLocaleLowerCase());
}
// 最大公约数
export function gcd(a, b) {
if (b === 0) return a;
return gcd(b, a % b);
}
// 最小公倍数
export function scm(a, b) {
return (a * b) / gcd(a, b);
}
// 打开新页面
export function openNewPage(url, target = '_blank') {
const a = document.createElement('a');
a.style.display = 'none';
a.target = target;
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}

View File

@ -0,0 +1,59 @@
/**
* Created by Liu.Jun on 2020/4/25 14:45.
*/
import { defineComponent, h, resolveComponent as _resolveComponent } from 'vue';
export {
nodePath2ClassName, isRootNodePath, computedCurPath, getPathVal, path2prop
} from './vueUtils';
// 内部使用 . 配置数据key不能出现.
const pathSeparator = '.';
// 删除当前path值
export function deletePathVal(vueData, name) {
delete vueData[name];
}
// 设置当前path值
export function setPathVal(obj, path, value) {
const pathArr = path.split(pathSeparator);
for (let i = 0; i < pathArr.length; i += 1) {
if (pathArr.length - i < 2) {
// 倒数第一个数据
obj[pathArr[pathArr.length - 1]] = value;
break;
}
obj = obj[pathArr[i]];
}
}
export function resolveComponent(component) {
if (typeof component === 'string') return _resolveComponent(component);
return component;
}
// 转换antdv、naive等非moduleValue的v-model组件
export const modelValueComponent = (component, {
model = 'value'
} = {}) => defineComponent({
inheritAttrs: false,
setup(props, { attrs, slots }) {
return () => {
const {
modelValue: value,
'onUpdate:modelValue': onUpdateValue,
...otherAttrs
} = attrs;
// eg: 'a-input'
return h(resolveComponent(component), {
[model]: value,
[`onUpdate:${model}`]: onUpdateValue,
...otherAttrs
}, slots);
};
}
});

View File

@ -0,0 +1,40 @@
/**
* Created by Liu.Jun on 2020/4/25 14:45.
*/
// 内部使用 . 配置数据key不能出现.
const pathSeparator = '.';
// nodePath 转css类名
export function nodePath2ClassName(path) {
const rootPathName = '__pathRoot';
return path ? `${rootPathName}.${path}`.replace(/\./g, '_') : rootPathName;
}
// 是否为根节点
export function isRootNodePath(path) {
return path === '';
}
// 计算当前节点path
export function computedCurPath(prePath, curKey) {
return prePath === '' ? curKey : [prePath, curKey].join(pathSeparator);
}
// 获取当前path值
export function getPathVal(obj, path, leftDeviation = 0) {
const pathArr = path.split(pathSeparator);
for (let i = 0; i < pathArr.length - leftDeviation; i += 1) {
// 错误路径或者undefined中断查找
if (obj === undefined) return undefined;
obj = pathArr[i] === '' ? obj : obj[pathArr[i]];
}
return obj;
}
// path 等于props
export function path2prop(path) {
return path;
}

View File

@ -0,0 +1,3 @@
/**/node_modules/*
/**/dist/*
/**/*.css

View File

@ -0,0 +1,287 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.12.1...v1.12.2) (2022-04-11)
**Note:** Version bump only for package @lljj/vue3-form-core
## [1.12.1](https://github.com/lljj-x/vue-json-schema-form/compare/v1.12.0...v1.12.1) (2022-04-05)
### Bug Fixes
* **lib:** anyOf onChange 参数 ([60cac99](https://github.com/lljj-x/vue-json-schema-form/commit/60cac995779c1eeb90b23f2cfeeb5deb8c350feb)), closes [#166](https://github.com/lljj-x/vue-json-schema-form/issues/166)
# [1.12.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.11.0...v1.12.0) (2022-03-08)
### Features
* **lib:** 优化样式 ([e53291b](https://github.com/lljj-x/vue-json-schema-form/commit/e53291b8395fdceb971f15f72c9e809cdee8ec7e))
# [1.11.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.10.0...v1.11.0) (2022-02-19)
### Bug Fixes
* **lib:** 添加严格模式配置更精准计算anyOf 默认值 ([2cd65bb](https://github.com/lljj-x/vue-json-schema-form/commit/2cd65bb5f275a021f1cc368e4c63387163c94d57)), closes [#157](https://github.com/lljj-x/vue-json-schema-form/issues/157)
### Features
* **lib:** 阻止表单默认submit事件 ([a882181](https://github.com/lljj-x/vue-json-schema-form/commit/a882181d65a9a152f8017e55367100658464aeba)), closes [#150](https://github.com/lljj-x/vue-json-schema-form/issues/150)
# [1.10.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.5...v1.10.0) (2021-11-28)
### Features
* **lib:** 添加 $$uiFormRef 属性可在mounted 之直接访问子组件实例 ([08c6c4f](https://github.com/lljj-x/vue-json-schema-form/commit/08c6c4f2d247b4881e88fa380de8980c31cc5cd7)), closes [#127](https://github.com/lljj-x/vue-json-schema-form/issues/127)
## [1.9.5](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.4...v1.9.5) (2021-11-21)
### Bug Fixes
* **lib:** 修复inline 布局样式问题 ([65a7143](https://github.com/lljj-x/vue-json-schema-form/commit/65a7143fc19105f9096afc24a25107c0ef27ac5f)), closes [#122](https://github.com/lljj-x/vue-json-schema-form/issues/122)
## [1.9.4](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.3...v1.9.4) (2021-11-02)
**Note:** Version bump only for package @lljj/vue3-form-core
## [1.9.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.2...v1.9.3) (2021-10-10)
**Note:** Version bump only for package @lljj/vue3-form-core
## [1.9.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.1...v1.9.2) (2021-09-25)
### Bug Fixes
* **lib:** 修复antd表单不实时校验 ([5452491](https://github.com/lljj-x/vue-json-schema-form/commit/5452491354acf9884cd37691e766c0007c82f88a))
* **lib:** 修复anyOf嵌套object 可能丢失部分校验规则的问题 ([5c06294](https://github.com/lljj-x/vue-json-schema-form/commit/5c06294d9a9c978bda1c3724710cfd4ba478af5b))
## [1.9.1](https://github.com/lljj-x/vue-json-schema-form/compare/v1.9.0...v1.9.1) (2021-09-22)
### Bug Fixes
* **anyof:** 新值为object类型直接覆盖 ([d2f9791](https://github.com/lljj-x/vue-json-schema-form/commit/d2f9791ce7d35228edd07257049607177a95fc84)), closes [#77](https://github.com/lljj-x/vue-json-schema-form/issues/77)
* 修复select组件无法实时校验 ([85d9545](https://github.com/lljj-x/vue-json-schema-form/commit/85d95451b56b9d985ca7094118fbfaca87342322)), closes [#105](https://github.com/lljj-x/vue-json-schema-form/issues/105)
# [1.9.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.7.0...v1.9.0) (2021-09-06)
### Features
* **vue3-core:** 允许传递props至formFooter ([60ee613](https://github.com/lljj-x/vue-json-schema-form/commit/60ee613bda30b818adccd98ad73949ff111df74c))
### Performance Improvements
* **vue3-core:** 优化 okBtnProps 的实现方式 ([adfe93e](https://github.com/lljj-x/vue-json-schema-form/commit/adfe93e58a9e8fedc2b0c26484be5691ccc3f65a))
# [1.8.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.7.0...v1.8.0) (2021-09-06)
### Features
* **vue3-core:** 允许传递props至formFooter ([60ee613](https://github.com/lljj-x/vue-json-schema-form/commit/60ee613bda30b818adccd98ad73949ff111df74c))
### Performance Improvements
* **vue3-core:** 优化 okBtnProps 的实现方式 ([adfe93e](https://github.com/lljj-x/vue-json-schema-form/commit/adfe93e58a9e8fedc2b0c26484be5691ccc3f65a))
# [1.7.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.4...v1.7.0) (2021-08-29)
### Features
* **lib:** 支持配置 slots ([27f1501](https://github.com/lljj-x/vue-json-schema-form/commit/27f1501eda01eabd4a723656be56904e9cb0f069)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
## [1.6.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.2...v1.6.3) (2021-07-12)
**Note:** Version bump only for package @lljj/vue3-form-core
## [1.6.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.6.1...v1.6.2) (2021-05-31)
**Note:** Version bump only for package @lljj/vue3-form-core
# [1.6.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.5.0...v1.6.0) (2021-05-22)
### Features
* **lib:** form-mounted event 添加formData 参数 ([c54202c](https://github.com/lljj-x/vue-json-schema-form/commit/c54202c27304add9636a7062c05c80c60fc200a6))
# [1.5.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.4.0...v1.5.0) (2021-05-09)
### Features
* **lib:** 优化anyOf切换选项值的复用修复vue3 anyOf无法切换选项 ([6159160](https://github.com/lljj-x/vue-json-schema-form/commit/6159160d1727165e706343187aca129360dc011f))
# [1.4.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.3.0...v1.4.0) (2021-04-22)
### Features
* **lib:** 调整 widget onChange prop参数格式添加 formData参数 ([4c441fc](https://github.com/lljj-x/vue-json-schema-form/commit/4c441fce239ade40b10a42bf361c3ee920a044ed)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
# [1.3.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.2.1...v1.3.0) (2021-04-15)
### Features
* **core:** widget 节点直接配置onChange ([2d2264b](https://github.com/lljj-x/vue-json-schema-form/commit/2d2264b004c3b6586e225c563bf03ca52fc5e53a))
## [1.2.1](https://github.com/lljj-x/vue-json-schema-form/compare/v1.2.0...v1.2.1) (2021-04-11)
**Note:** Version bump only for package @lljj/vue3-form-core
# [1.2.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.1.3...v1.2.0) (2021-03-30)
### Features
* **lib:** 添加 fallback-label 参数 ([cd2d8c3](https://github.com/lljj-x/vue-json-schema-form/commit/cd2d8c3ed72b9bc03e44eb5b86eb1b18fe67c34c)), closes [#45](https://github.com/lljj-x/vue-json-schema-form/issues/45)
## [1.1.3](https://github.com/lljj-x/vue-json-schema-form/compare/v1.1.2...v1.1.3) (2021-03-18)
**Note:** Version bump only for package @lljj/vue3-form-core
## [1.1.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.1.1...v1.1.2) (2021-03-07)
### Bug Fixes
* **vue3-antd:** 修复form label 双冒号问题 ([5b4f16c](https://github.com/lljj-x/vue-json-schema-form/commit/5b4f16c3c1a4f4b784c2fd5c1fbe7eec40cf8d7b)), closes [#46](https://github.com/lljj-x/vue-json-schema-form/issues/46)
# [1.1.0](https://github.com/lljj-x/vue-json-schema-form/compare/v1.0.2...v1.1.0) (2021-03-06)
### Features
* **vue3-ant:** 更新初始化 ([71a2810](https://github.com/lljj-x/vue-json-schema-form/commit/71a281045af11f215333050396aa546dd5e78b88)), closes [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#27](https://github.com/lljj-x/vue-json-schema-form/issues/27) [#40](https://github.com/lljj-x/vue-json-schema-form/issues/40)
## [1.0.2](https://github.com/lljj-x/vue-json-schema-form/compare/v1.0.1...v1.0.2) (2021-01-31)
### Bug Fixes
* **style:** 修复p标签等自带边距导致的样式问题  ([7b7e43e](https://github.com/lljj-x/vue-json-schema-form/commit/7b7e43eaa06c14a436b34c38d6d69aad27d67512))
## [0.6.1](https://github.com/lljj-x/vue-json-schema-form/compare/v0.6.0...v0.6.1) (2021-01-19)
**Note:** Version bump only for package @lljj/vue3-form-core
# [0.6.0](https://github.com/lljj-x/vue-json-schema-form/compare/v0.5.0...v0.6.0) (2021-01-19)
**Note:** Version bump only for package @lljj/vue3-form-core

View File

@ -0,0 +1,124 @@
# @lljj/vue3-form-core
vue3 版本核心,可以基于此适配不同的 vue3 ui库。
适配的核心就是对应类型为自己的组件库,且处理默认 `props` 与自己组件库 props 之间的转换
> 适配方案可参见 [@lljj/vue3-form-element](https://github.com/lljj-x/vue-json-schema-form/tree/master/packages/lib/vue3/vue3-form-element) 、[@lljj/vue3-form-ant](https://github.com/lljj-x/vue-json-schema-form/tree/master/packages/lib/vue3/vue3-form-ant)
## 兼容性
npm 包直接为 es6+ 源码,需要在构建 lib 时通过babe转义
如配置 rollup babel plugin
```js
babel({
exclude: /node_modules\/(?!(@lljj)\/).*/, // 忽略跳过 @lljj
extensions: ['.js', '.vue'],
})
```
## 安装
```ssh
## npm
npm install --save @lljj/vue3-form-core
## yarn
yarn add @lljj/vue3-form-core
```
## 使用方法
按如下格式,配置对应组件在当前组件库中的映射关系,可以直接配置全局组件名或者组件构造函数,`默认组件 props 为elementUi格式如果props格式不同需要中间组件来做转换`
```js
import createVue2Core from '@lljj/vue3-form-core';
const globalOptions = {
// widget组件和现有组件库映射关系
WIDGET_MAP: {
// 默认按schema type 映射默认widget组件
types: {
// type boolean
boolean: 'el-switch',
// type string
string: 'el-input',
// type number
number: 'el-input-number',
// type integer
integer: 'el-input-number',
},
// 按 schema format 映射默认widget组件优先级高于 types
formats: {
// format: color
color: 'el-color-picker',
// format: time
time: TimePickerWidget, // 格式 20:20:39+00:00
// format: date
date: DatePickerWidget, // 格式 2018-11-13
// format: date-time
'date-time': DateTimePickerWidget, // 格式 2018-11-13T20:20:39+00:00
},
// 一些公共常用类型
common: {
// select option
select: SelectWidget,
// radio
radioGroup: RadioWidget,
// checkout
checkboxGroup: CheckboxesWidget,
},
// 这里配置一些 为当前ui库适配过的组件会在运行时自动注册为全局组件不注册为全局也可不配置
// Vue3 只有在组件内才能获取到当前的app所以注册时机是在 form组件setup中且只会注册一次。
widgetComponents: {
CheckboxesWidget,
RadioWidget,
SelectWidget,
TimePickerWidget,
DatePickerWidget,
DateTimePickerWidget
}
},
// 其它表单相关组件映射关系
COMPONENT_MAP: {
// form组件
form: 'el-form',
// formItem 组件
formItem: 'el-form-item',
// button 组件
button: 'el-button',
// popover用在formLable 左右布局时鼠标移入显示description
popover: 'el-popover'
},
HELPERS: {
// 是否mini显示 description
isMiniDes(formProps) {
return formProps && ['left', 'right'].includes(formProps.labselPosition);
}
}
};
const mySchemaForm = createVue2Core(globalOptions);
```
适配一个新的ui框架只需要适配如上的组件即可
## License
Apache-2.0

View File

@ -0,0 +1,29 @@
{
"name": "@lljj/vue3-form-core",
"version": "1.12.2",
"description": "基于 Vue3 、JsonSchema快速构建一个带完整校验的form表单vue3版本基础框架",
"main": "src/index.js",
"module": "src/index.js",
"keywords": [
"vue",
"vuejs",
"form",
"jsonSchema"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"@lljj/vjsf-utils": "1.12.2"
},
"repository": "https://github.com/lljj-x/vue-json-schema-form",
"homepage": "https://github.com/lljj-x/vue-json-schema-form",
"license": "Apache-2.0",
"author": "Liu.Jun",
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"gitHead": "92795075169c879e1c1fabfe26f1d3c10b861060"
}

View File

@ -0,0 +1,46 @@
/**
* Created by Liu.Jun on 2020/4/20 9:55 下午.
*/
// eslint-disable-next-line import/no-cycle
import ObjectField from './fields/ObjectField';
// eslint-disable-next-line import/no-cycle
import StringField from './fields/StringField';
// eslint-disable-next-line import/no-cycle
import NumberField from './fields/NumberField';
// eslint-disable-next-line import/no-cycle
import IntegerField from './fields/IntegerField';
// eslint-disable-next-line import/no-cycle
import BooleanField from './fields/BooleanField';
// eslint-disable-next-line import/no-cycle
import ArrayField from './fields/ArrayField';
// eslint-disable-next-line import/no-cycle
import AnyOfField from './fields/combiningSchemas/AnyOfField';
// eslint-disable-next-line import/no-cycle
import OneOfField from './fields/combiningSchemas/OneOfField';
// 默认类型使用field映射关系
const FIELDS_MAPS = {
array: ArrayField,
boolean: BooleanField,
integer: IntegerField,
number: NumberField,
object: ObjectField,
string: StringField,
null: {
render() {
return null;
}
},
anyOf: AnyOfField,
oneOf: OneOfField
};
export default FIELDS_MAPS;

View File

@ -0,0 +1,67 @@
/**
* Created by Liu.Jun on 2020/12/27 9:53 下午.
*/
import { h } from 'vue';
import { resolveComponent } from '@lljj/vjsf-utils/vue3Utils';
export default {
name: 'FormFooter',
props: {
okBtn: {
type: String,
default: '保存'
},
okBtnProps: {
type: Object,
default: () => ({})
},
cancelBtn: {
type: String,
default: '取消'
},
formItemAttrs: {
type: Object,
default: () => ({})
},
globalOptions: {
type: Object,
default: () => ({})
}
},
emits: ['cancel', 'submit'],
setup(props, { emit }) {
// globalOptions 不需要响应式
const { globalOptions: { COMPONENT_MAP } } = props;
return () => h(resolveComponent(COMPONENT_MAP.formItem), {
class: {
formFooter_item: true
},
...props.formItemAttrs
}, {
default: () => [
h(resolveComponent(COMPONENT_MAP.button), {
onClick() {
emit('cancel');
}
}, {
default: () => props.cancelBtn
}),
h(resolveComponent(COMPONENT_MAP.button), {
style: {
marginLeft: '10px'
},
type: 'primary',
onClick() {
emit('submit');
},
...props.okBtnProps
}, {
default: () => props.okBtn
})
]
});
}
};

View File

@ -0,0 +1,365 @@
/**
* Created by Liu.Jun on 2020/4/23 11:24.
*/
import {
computed, h, ref, watch, inject
} from 'vue';
import { IconInfo } from '@lljj/vjsf-utils/icons';
import { validateFormDataAndTransformMsg } from '@lljj/vjsf-utils/schema/validate';
import { fallbackLabel } from '@lljj/vjsf-utils/formUtils';
import {
isRootNodePath, path2prop, getPathVal, setPathVal, resolveComponent
} from '@lljj/vjsf-utils/vue3Utils';
export default {
name: 'Widget',
props: {
// 是否同步formData的值默认表单元素都需要
// oneOf anyOf 中的select属于formData之外的数据
isFormData: {
type: Boolean,
default: true
},
// isFormData = false时需要传入当前 value 否则会通过 curNodePath 自动计算
curValue: {
type: null,
default: 0
},
schema: {
type: Object,
default: () => ({})
},
uiSchema: {
type: Object,
default: () => ({})
},
errorSchema: {
type: Object,
default: () => ({})
},
customFormats: {
type: Object,
default: () => ({})
},
// 自定义校验
customRule: {
type: Function,
default: null
},
widget: {
type: [String, Function, Object],
default: null
},
required: {
type: Boolean,
default: false
},
// 解决 JSON Schema和实际输入元素中空字符串 required 判定的差异性
// 元素输入为 '' 使用 emptyValue 的值
emptyValue: {
type: null,
default: undefined
},
rootFormData: {
type: null
},
curNodePath: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
// width -> formItem width
width: {
type: String,
default: ''
},
labelWidth: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
// Widget attrs
widgetAttrs: {
type: Object,
default: () => ({})
},
// Widget className
widgetClass: {
type: Object,
default: () => ({})
},
// Widget style
widgetStyle: {
type: Object,
default: () => ({})
},
// Field attrs
fieldAttrs: {
type: Object,
default: () => ({})
},
// Field className
fieldClass: {
type: Object,
default: () => ({})
},
// Field style
fieldStyle: {
type: Object,
default: () => ({})
},
// props
uiProps: {
type: Object,
default: () => ({})
},
formProps: null,
getWidget: null,
renderScopedSlots: null, // 作用域插槽
globalOptions: null, // 全局配置
onChange: null
},
emits: ['otherDataChange'],
inheritAttrs: true,
setup(props, { emit }) {
const genFormProvide = inject('genFormProvide');
const widgetValue = computed({
get() {
if (props.isFormData) return getPathVal(props.rootFormData, props.curNodePath);
return props.curValue;
},
set(value) {
// 大多组件删除为空值会重置为null。
const trueValue = (value === '' || value === null) ? props.emptyValue : value;
if (props.isFormData) {
setPathVal(props.rootFormData, props.curNodePath, trueValue);
} else {
emit('otherDataChange', trueValue);
}
}
});
// 枚举类型默认值为第一个选项
if (props.uiProps.enumOptions
&& props.uiProps.enumOptions.length > 0
&& widgetValue.value === undefined
&& widgetValue.value !== props.uiProps.enumOptions[0]
) {
// array 渲染为多选框时默认为空数组
if (props.schema.items) {
widgetValue.value = [];
} else if (props.required) {
widgetValue.value = props.uiProps.enumOptions[0].value;
}
}
// 获取到widget组件实例
const widgetRef = ref(null);
// 提供一种特殊的配置 允许直接访问到 widget vm
if (typeof props.getWidget === 'function') {
watch(widgetRef, () => {
props.getWidget.call(null, widgetRef.value);
});
}
return () => {
// 判断是否为根节点
const isRootNode = isRootNodePath(props.curNodePath);
const isMiniDes = props.formProps && props.formProps.isMiniDes;
const miniDesModel = isMiniDes || props.globalOptions.HELPERS.isMiniDes(props.formProps);
const descriptionVNode = (props.description) ? h(
'div',
{
innerHTML: props.description,
class: {
genFromWidget_des: true,
genFromWidget_des_mini: miniDesModel
}
},
) : null;
const { COMPONENT_MAP } = props.globalOptions;
const miniDescriptionVNode = (miniDesModel && descriptionVNode) ? h(resolveComponent(COMPONENT_MAP.popover), {
style: {
margin: '0 2px',
fontSize: '16px',
cursor: 'pointer'
},
placement: 'top',
trigger: 'hover'
}, {
default: () => descriptionVNode,
reference: () => h(IconInfo)
}) : null;
// form-item style
const formItemStyle = {
...props.fieldStyle,
...(props.width ? {
width: props.width,
flexBasis: props.width,
paddingRight: '10px'
} : {})
};
// 运行配置回退到 属性名
const label = fallbackLabel(props.label, (props.widget && genFormProvide.value.fallbackLabel), props.curNodePath);
return h(
resolveComponent(COMPONENT_MAP.formItem),
{
class: {
...props.fieldClass,
genFormItem: true
},
style: formItemStyle,
...props.fieldAttrs,
...props.labelWidth ? { labelWidth: props.labelWidth } : {},
...props.isFormData ? {
// 这里对根节点打特殊标志绕过elementUi无prop属性不校验
prop: isRootNode ? '__$$root' : path2prop(props.curNodePath),
rules: [
{
validator(rule, value, callback) {
if (isRootNode) value = props.rootFormData;
// 校验是通过对schema逐级展开校验 这里只捕获根节点错误
const errors = validateFormDataAndTransformMsg({
formData: value,
schema: props.schema,
uiSchema: props.uiSchema,
customFormats: props.customFormats,
errorSchema: props.errorSchema,
required: props.required,
propPath: path2prop(props.curNodePath)
});
// 存在校验不通过字段
if (errors.length > 0) {
if (callback) return callback(errors[0].message);
return Promise.reject(errors[0].message);
}
// customRule 如果存在自定义校验
const curCustomRule = props.customRule;
if (curCustomRule && (typeof curCustomRule === 'function')) {
return curCustomRule({
field: props.curNodePath,
value,
rootFormData: props.rootFormData,
callback
});
}
// 校验成功
if (callback) return callback();
return Promise.resolve();
},
trigger: 'change'
}
]
} : {},
},
{
// 错误只能显示一行,多余...
error: slotProps => (slotProps.error ? h('div', {
class: {
formItemErrorBox: true
},
title: slotProps.error
}, [slotProps.error]) : null),
// label
/*
TODO:这里slot如果从无到有会导致无法正常渲染出元素 怀疑是vue3 bug
如果使用 error 的形式渲染ElementPlus label labelWrap 未做判断使用 slots.default?.() 会得到 undefined
*/
...label ? {
label: () => h('span', {
class: {
genFormLabel: true,
genFormItemRequired: props.required,
},
}, [
...miniDescriptionVNode ? [miniDescriptionVNode] : [],
`${label}`,
`${(props.formProps && props.formProps.labelSuffix) || ''}`
])
} : {},
// default
default: otherAttrs => [
// description
// 非mini模式显示 description
...(!miniDesModel && descriptionVNode) ? [descriptionVNode] : [],
...props.widget ? [
h( // 关键输入组件
resolveComponent(props.widget),
{
style: props.widgetStyle,
class: props.widgetClass,
...props.widgetAttrs,
...props.uiProps,
modelValue: widgetValue.value, // v-model
ref: widgetRef,
'onUpdate:modelValue': function updateModelValue(event) {
const preVal = widgetValue.value;
if (preVal !== event) {
widgetValue.value = event;
if (props.onChange) {
props.onChange({
curVal: event,
preVal,
parentFormData: getPathVal(props.rootFormData, props.curNodePath, 1),
rootFormData: props.rootFormData
});
}
}
},
...otherAttrs ? (() => Object.keys(otherAttrs).reduce((pre, k) => {
pre[k] = otherAttrs[k];
// 保证ui配置同名方法 ui方法先执行
[
props.widgetAttrs[k],
props.uiProps[k]
].forEach((uiConfFn) => {
if (uiConfFn && typeof uiConfFn === 'function') {
pre[k] = (...args) => {
uiConfFn(...args);
pre[k](...args);
};
}
});
return pre;
}, {}))() : {}
},
{
...(props.renderScopedSlots ? (
typeof props.renderScopedSlots === 'function' ? props.renderScopedSlots() : props.renderScopedSlots
) : {})
}
)
] : []
]
}
);
};
}
};

View File

@ -0,0 +1,57 @@
/**
* Created by Liu.Jun on 2020/4/24 11:56.
*/
import { h } from 'vue';
import {
getWidgetConfig, optionsList
} from '@lljj/vjsf-utils/formUtils';
import retrieveSchema from '@lljj/vjsf-utils/schema/retriev';
import vueProps from '../../props';
import Widget from '../../../components/Widget';
export default {
name: 'ArrayFieldMultiSelect',
props: {
...vueProps
},
setup(props, { attrs }) {
return () => {
const {
schema, rootSchema, uiSchema, curNodePath, rootFormData, globalOptions
} = props;
// 这里需要索引当前节点通过到schemaField组件的会统一处理
const itemsSchema = retrieveSchema(schema.items, rootSchema);
const enumOptions = optionsList(itemsSchema, uiSchema, curNodePath, rootFormData);
const widgetConfig = getWidgetConfig({
schema,
uiSchema,
curNodePath,
rootFormData
}, () => ({
widget: globalOptions.WIDGET_MAP.common.checkboxGroup
}));
// 存在枚举数据列表 传入 enumOptions
widgetConfig.uiProps.multiple = true;
if (enumOptions && !widgetConfig.uiProps.enumOptions) {
widgetConfig.uiProps.enumOptions = enumOptions;
}
return h(
Widget,
{
...attrs,
...props,
...widgetConfig
}
);
};
}
};

Some files were not shown because too many files have changed in this diff Show More