first commit

This commit is contained in:
z9130 2024-07-23 15:37:23 +08:00
commit 4685718dbc
505 changed files with 69494 additions and 0 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

48
.env Normal file
View File

@ -0,0 +1,48 @@
VITE_BASE_URL=/
VITE_APP_TITLE=ERP 综合办公系统
VITE_APP_DESC=
# the prefix of the icon name
VITE_ICON_PREFIX=icon
# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
# format {VITE_ICON_PREFIX}-{local icon name}
VITE_ICON_LOCAL_PREFIX=icon-local
# 路由模式: static 静态 dynamic 动态
VITE_AUTH_ROUTE_MODE=static
# static 静态路由首页地址
VITE_ROUTE_HOME=home
# 默认菜单图标
VITE_MENU_ICON=mdi:menu
# 当在开发模式下,是否使用代理
VITE_HTTP_PROXY=Y
# vue-router 路由模式: hash | history | memory
VITE_ROUTER_HISTORY_MODE=history
# success code of backend service, when the code is received, the request is successful
VITE_SERVICE_SUCCESS_CODE=200
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
VITE_SERVICE_LOGOUT_CODES=8888,8889
# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998
# when the route mode is static, the defined super role
VITE_STATIC_SUPER_ROLE=R_SUPER
# sourcemap
VITE_SOURCE_MAP=N
# Used to differentiate storage across different domains
VITE_STORAGE_PREFIX=app_

7
.env.prod Normal file
View File

@ -0,0 +1,7 @@
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9529"
}`

7
.env.test Normal file
View File

@ -0,0 +1,7 @@
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=http://192.168.147.238:8083
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9529"
}`

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['antfu'],
};

13
.gitattributes vendored Normal file
View File

@ -0,0 +1,13 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.mjs" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.scss" eol=lf
"*.md" eol=lf
"*.yaml" eol=lf
"*.yml" eol=lf

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
yarn.lock
.history
.VSCodeCounter
vite.config.ts.**

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
registry=https://registry.npmmirror.com/
shamefully-hoist=true
ignore-workspace-root-check=true
link-workspace-packages=true

34
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
"recommendations": [
// my extensions, ofc :P
"antfu.browse-lite",
"antfu.iconify",
"antfu.slidev",
"antfu.unocss",
"antfu.vite",
"antfu.where-am-i",
"antfu.open-in-github-button",
"lokalise.i18n-ally",
// themes & icons
"antfu.icons-carbon",
"antfu.theme-vitesse",
"file-icons.file-icons",
"sainnhe.gruvbox-material",
// life savers!
"dbaeumer.vscode-eslint",
"Vue.volar",
"GitHub.copilot",
"usernamehw.errorlens",
"streetsidesoftware.code-spell-checker",
// up to you
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"github.vscode-github-actions",
"GitHub.vscode-pull-request-github",
"johnsoncodehk.vscode-tsconfig-helper",
"mpontus.tab-cycle",
"naumovs.color-highlight",
"WakaTime.vscode-wakatime",
"znck.grammarly"
]
}

304
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,304 @@
{
// ========== Visuals ==========
"editor.cursorSmoothCaretAnimation": "on",
// "editor.fontFamily": "Input Mono, monospace",
"editor.guides.bracketPairs": "active",
"editor.lineNumbers": "interval",
"editor.renderWhitespace": "boundary",
"window.autoDetectColorScheme": true,
"workbench.colorTheme": "Vitesse Dark Soft",
"workbench.editor.tabActionLocation": "left",
"workbench.fontAliasing": "antialiased",
"workbench.iconTheme": "file-icons-colourless",
"workbench.list.smoothScrolling": true,
"workbench.preferredDarkColorTheme": "Vitesse Dark",
"workbench.preferredLightColorTheme": "Vitesse Dark Soft",
"workbench.productIconTheme": "icons-carbon",
"workbench.sideBar.location": "right",
"workbench.startupEditor": "newUntitledFile",
"workbench.tree.expandMode": "singleClick",
"workbench.tree.indent": 10,
// ========== Editor ==========
"debug.onTaskErrors": "debugAnyway",
"diffEditor.ignoreTrimWhitespace": true,
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.find.addExtraSpaceOnTop": false,
"editor.inlineSuggest.enabled": true,
"editor.multiCursorModifier": "ctrlCmd",
"editor.suggestSelection": "first",
"editor.tabSize": 2,
"editor.unicodeHighlight.invisibleCharacters": false,
"editor.stickyScroll.enabled": true,
"editor.hover.sticky": true,
"editor.codeActionsOnSave": {
"source.fixAll": "never",
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"explorer.confirmDelete": false,
"explorer.confirmDragAndDrop": false,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"git.autofetch": true,
"git.confirmSync": false,
"git.enableSmartCommit": true,
"git.untrackedChanges": "separate",
"scm.diffDecorationsGutterWidth": 2,
"terminal.integrated.cursorBlinking": true,
"terminal.integrated.cursorStyle": "line",
"terminal.integrated.fontWeight": "300",
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.tabs.enabled": true,
"workbench.editor.closeOnFileDelete": true,
"workbench.editor.highlightModifiedTabs": true,
"workbench.editor.limit.enabled": true,
"workbench.editor.limit.perEditorGroup": true,
"workbench.editor.limit.value": 5,
"search.exclude": {
"**/*.snap": true,
"**/*.svg": true,
"**/.git": true,
"**/.github": false,
"**/.nuxt": true,
"**/.output": true,
"**/.pnpm": true,
"**/.vscode": true,
"**/.yarn": true,
"**/assets": true,
"**/bower_components": true,
"**/dist/**": true,
"**/logs": true,
"**/node_modules": true,
"**/out/**": true,
"**/package-lock.json": true,
"**/pnpm-lock.yaml": true,
"**/public": true,
"**/temp": true,
"**/yarn.lock": true,
"**/CHANGELOG*": true,
"**/LICENSE*": true,
},
// ========== Global Level Config, needs to put in User Settings ==========
"window.dialogStyle": "custom",
"window.nativeTabs": true, // this is great, macOS only
"window.title": "${rootName}", // this make tabs more readable
"window.titleBarStyle": "custom",
"extensions.autoUpdate": "onlyEnabledExtensions",
// ========== Extension configs ==========
"emmet.showSuggestionsAsSnippets": true,
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": [
"warning",
"error"
],
"errorLens.excludeBySource": [
"cSpell",
"Grammarly",
"eslint"
],
// ESLint config: https://github.com/antfu/eslint-config
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.quiet": true,
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{
"rule": "style/*",
"severity": "off"
},
{
"rule": "format/*",
"severity": "off"
},
{
"rule": "*-indent",
"severity": "off"
},
{
"rule": "*-spacing",
"severity": "off"
},
{
"rule": "*-spaces",
"severity": "off"
},
{
"rule": "*-order",
"severity": "off"
},
{
"rule": "*-dangle",
"severity": "off"
},
{
"rule": "*-newline",
"severity": "off"
},
{
"rule": "*quotes",
"severity": "off"
},
{
"rule": "*semi",
"severity": "off"
}
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml"
],
"github.copilot.enable": {
"*": true,
"markdown": true,
"plaintext": false,
},
"cSpell.allowCompoundWords": true,
"cSpell.language": "en,en-US",
"css.lint.hexColorLength": "ignore",
"githubIssues.workingIssueFormatScm": "#${issueNumberLabel}",
"githubPullRequests.fileListLayout": "tree",
"gitlens.codeLens.authors.enabled": false,
"gitlens.codeLens.enabled": false,
"gitlens.codeLens.recentChange.enabled": false,
"gitlens.menus": {
"editor": {
"blame": false,
"clipboard": true,
"compare": true,
"history": false,
"remote": false
},
"editorGroup": {
"blame": true,
"compare": false
},
"editorTab": {
"clipboard": true,
"compare": true,
"history": true,
"remote": true
},
"explorer": {
"clipboard": true,
"compare": true,
"history": true,
"remote": true
},
"scm": {
"authors": true
},
"scmGroup": {
"compare": true,
"openClose": true,
"stash": true
},
"scmGroupInline": {
"stash": true
},
"scmItem": {
"clipboard": true,
"compare": true,
"history": true,
"remote": false,
"stash": true
}
},
"i18n-ally.autoDetection": false,
"i18n-ally.displayLanguage": "en",
"i18n-ally.ignoredLocales": [],
"iconify.annotations": true,
"iconify.inplace": true,
"svg.preview.mode": "svg",
// I only use Prettier for manually formatting
"prettier.enable": false,
"prettier.printWidth": 200,
"prettier.semi": false,
"prettier.singleQuote": true,
// ========== File Nesting ==========
// this might not be up to date with the repo, please check yourself
// https://github.com/antfu/vscode-file-nesting-config
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.asax": "$(capture).*.cs, $(capture).*.vb",
"*.ascx": "$(capture).*.cs, $(capture).*.vb",
"*.ashx": "$(capture).*.cs, $(capture).*.vb",
"*.aspx": "$(capture).*.cs, $(capture).*.vb",
"*.bloc.dart": "$(capture).event.dart, $(capture).state.dart",
"*.c": "$(capture).h",
"*.cc": "$(capture).hpp, $(capture).h, $(capture).hxx",
"*.cjs": "$(capture).cjs.map, $(capture).*.cjs, $(capture)_*.cjs",
"*.component.ts": "$(capture).component.html, $(capture).component.spec.ts, $(capture).component.css, $(capture).component.scss, $(capture).component.sass, $(capture).component.less",
"*.cpp": "$(capture).hpp, $(capture).h, $(capture).hxx",
"*.cs": "$(capture).*.cs",
"*.cshtml": "$(capture).cshtml.cs",
"*.csproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
"*.css": "$(capture).css.map, $(capture).*.css",
"*.cxx": "$(capture).hpp, $(capture).h, $(capture).hxx",
"*.dart": "$(capture).freezed.dart, $(capture).g.dart",
"*.ex": "$(capture).html.eex, $(capture).html.heex, $(capture).html.leex",
"*.go": "$(capture)_test.go",
"*.java": "$(capture).class",
"*.js": "$(capture).js.map, $(capture).*.js, $(capture)_*.js",
"*.jsx": "$(capture).js, $(capture).*.jsx, $(capture)_*.js, $(capture)_*.jsx",
"*.master": "$(capture).*.cs, $(capture).*.vb",
"*.mjs": "$(capture).mjs.map, $(capture).*.mjs, $(capture)_*.mjs",
"*.module.ts": "$(capture).resolver.ts, $(capture).controller.ts, $(capture).service.ts",
"*.pubxml": "$(capture).pubxml.user",
"*.resx": "$(capture).*.resx, $(capture).designer.cs, $(capture).designer.vb",
"*.tex": "$(capture).acn, $(capture).acr, $(capture).alg, $(capture).aux, $(capture).bbl, $(capture).blg, $(capture).fdb_latexmk, $(capture).fls, $(capture).glg, $(capture).glo, $(capture).gls, $(capture).idx, $(capture).ind, $(capture).ist, $(capture).lof, $(capture).log, $(capture).lot, $(capture).out, $(capture).pdf, $(capture).synctex.gz, $(capture).toc, $(capture).xdv",
"*.ts": "$(capture).js, $(capture).d.ts.map, $(capture).*.ts, $(capture)_*.js, $(capture)_*.ts",
"*.tsx": "$(capture).ts, $(capture).*.tsx, $(capture)_*.ts, $(capture)_*.tsx",
"*.vbproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
"*.vue": "$(capture).*.ts, $(capture).*.js, $(capture).story.vue",
"*.xaml": "$(capture).xaml.cs",
"+layout.svelte": "+layout.ts,+layout.ts,+layout.js,+layout.server.ts,+layout.server.js,+layout.gql",
"+page.svelte": "+page.server.ts,+page.server.js,+page.ts,+page.js,+page.gql",
".clang-tidy": ".clang-format, .clangd, compile_commands.json",
".env": "*.env, .env.*, .envrc, env.d.ts",
".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*",
".project": ".classpath",
"//": "Last update at 4/29/2023, 2:04:58 PM",
"BUILD.bazel": "*.bzl, *.bazel, *.bazelrc, bazel.rc, .bazelignore, .bazelproject, WORKSPACE",
"CMakeLists.txt": "*.cmake, *.cmake.in, .cmake-format.yaml, CMakePresets.json",
"I*.cs": "$(capture).cs",
"artisan": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, server.php, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, webpack.mix.js, windi.config.*",
"astro.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"cargo.toml": ".clippy.toml, .rustfmt.toml, cargo.lock, clippy.toml, cross.toml, rust-toolchain.toml, rustfmt.toml",
"composer.json": ".php*.cache, composer.lock, phpunit.xml*, psalm*.xml",
"default.nix": "shell.nix",
"deno.json*": "*.env, .env.*, .envrc, api-extractor.json, deno.lock, env.d.ts, import-map.json, import_map.json, jsconfig.*, tsconfig.*, tsdoc.*",
"dockerfile": ".dockerignore, docker-compose.*, dockerfile*",
"flake.nix": "flake.lock",
"gatsby-config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, gatsby-browser.*, gatsby-node.*, gatsby-ssr.*, gatsby-transformer.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"gemfile": ".ruby-version, gemfile.lock",
"go.mod": ".air*, go.sum",
"go.work": "go.work.sum",
"mix.exs": ".credo.exs, .dialyzer_ignore.exs, .formatter.exs, .iex.exs, .tool-versions, mix.lock",
"next.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, next-env.d.ts, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"nuxt.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"package.json": ".browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json, eslint*, firebase.json, grunt*, gulp*, jenkins*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, simple-git-hooks*, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, xo.config.*, yarn*",
"pubspec.yaml": ".metadata, .packages, all_lint_rules.yaml, analysis_options.yaml, build.yaml, pubspec.lock, pubspec_overrides.yaml",
"pyproject.toml": ".pdm.toml, pdm.lock, pyproject.toml",
"quasar.conf.js": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, quasar.extensions.json, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"readme*": "authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors*",
"remix.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, remix.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"rush.json": ".browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json, eslint*, firebase.json, grunt*, gulp*, jenkins*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, simple-git-hooks*, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, xo.config.*, yarn*",
"shims.d.ts": "*.d.ts",
"svelte.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, houdini.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, mdsvex.config.js, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vite.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"vite.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*",
"vue.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*"
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
}

0
CHANGELOG.md Normal file
View File

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Soybean
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

0
README.md Normal file
View File

79
alova.config.ts Normal file
View File

@ -0,0 +1,79 @@
/**
* Alova
* swagger生成对应的api调用函数
*/
export default {
// api生成设置数组每项代表一个自动生成的规则包含生成的输入输出目录、规范文件地址等等
generator: [
// 服务器1
{
// input参数1openapi的json文件url地址
input: 'http://127.0.0.1:4523/export/openapi/8?version=3.0',
// input参数2以当前项目为相对目录的本地地址
// input: 'openapi/api.json'
// input参数3没有直接指向openapi文件时是一个文档地址必须配合platform参数指定文档类型
// input: 'http://192.168.5.123:8080'
// 可选platform为支持openapi的平台目前只支持swagger默认为空
// 当指定了此参数后input字段只需要指定文档的地址而不需要指定到openapi文件
platform: 'swagger',
// 接口文件和类型文件的输出路径多个generator不能重复的地址否则生成的代码会相互覆盖
output: 'src/api',
// 可选指定生成的响应数据的mediaType以此数据类型来生成200状态码的响应ts格式默认application/json
responseMediaType: 'application/json',
// 可选指定生成的请求体数据的bodyMediaType以此数据类型来生成请求体的ts格式默认application/json
bodyMediaType: 'application/json',
// 可选指定生成的api版本默认为auto会通过当前项目安装的alova版本判断当前项目的版本如果生成不正确你也可以自定义指定版本
version: 'auto',
/**
* auto/ts/typescript/module/commonjsauto
* ts/typescriptts类型文件
* moduleesModule规范文件
* commonjscommonjs规范文件
*/
type: 'typescript',
/**
* api名称访api`Apis`generator时为必填
*/
global: 'Apis',
/**
* api接口函数apiDescriptor来生成api调用函数apiDescripor对象
*/
handleApi: apiDescriptor => {
// 返回falsy值表示过滤此api
// if (!apiDescriptor.path.startWith('/user')) {
// return;
// }
// apiDescriptor.parameter = apiDescriptor.parameter.filter(
// param => param.in === 'header' && param.name === 'token'
// );
// delete apiDescriptor.requestBody.id;
// apiDescriptor.url = '/app/rl' + apiDescriptor.url;
apiDescriptor.url = '/app/rl' + apiDescriptor.url;
return apiDescriptor;
}
},
],
// 可选是否自动更新接口默认开启每5分钟检查一次false时关闭
autoUpdate: false
/*
autoUpdate: {
// 编辑器开启时更新默认false
launchEditor: true,
// 自动更新间隔,单位毫秒
interval: 5 * 60 * 1000
}
*/
};

1
build/config/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './proxy';

36
build/config/proxy.ts Normal file
View File

@ -0,0 +1,36 @@
import type { ProxyOptions } from 'vite';
import { createServiceConfig } from '../../src/utils/service';
/**
* Set http proxy
*
* @param env - The current env
* @param isDev - Is development environment
*/
export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
if (!isEnableHttpProxy) return undefined;
const { baseURL, proxyPattern, other } = createServiceConfig(env);
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern });
other.forEach(item => {
Object.assign(proxy, createProxyItem(item));
});
return proxy;
}
function createProxyItem(item: App.Service.ServiceConfigItem) {
const proxy: Record<string, ProxyOptions> = {};
proxy[item.proxyPattern] = {
target: item.baseURL,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
};
return proxy;
}

39
build/plugins/index.ts Normal file
View File

@ -0,0 +1,39 @@
import type { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import VueDevtools from 'vite-plugin-vue-devtools';
import progress from 'vite-plugin-progress';
import { setupElegantRouter } from './router';
import { setupUnocss } from './unocss';
import { setupUnplugin } from './unplugin';
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import'
export function setupVitePlugins(viteEnv: Env.ImportMeta) {
const plugins: PluginOption = [
vue({
script: {
defineModel: true
}
}),
vueJsx(),
VueDevtools(),
setupElegantRouter(),
setupUnocss(viteEnv),
...setupUnplugin(viteEnv),
progress(),
// lazyImport({
// resolvers: [
// VxeResolver({
// libraryName: 'vxe-table'
// }),
// VxeResolver({
// libraryName: 'vxe-pc-ui'
// })
// ]
// })
];
return plugins;
}

63
build/plugins/router.ts Normal file
View File

@ -0,0 +1,63 @@
import type { RouteMeta } from 'vue-router';
import ElegantVueRouter from '@elegant-router/vue/vite';
import type { RouteKey } from '@elegant-router/types';
export function setupElegantRouter() {
return ElegantVueRouter({
layouts: {
base: 'src/layouts/base-layout/index.vue',
blank: 'src/layouts/blank-layout/index.vue'
},
customRoutes: {
names: [
'exception_403',
'exception_404',
'exception_500',
'document_project',
'document_project-link',
'document_vue',
'document_vite',
'document_unocss',
'document_naive',
'document_antd'
]
},
routePathTransformer(routeName, routePath) {
const key = routeName as RouteKey;
if (key === 'login') {
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
const moduleReg = modules.join('|');
return `/login/:module(${moduleReg})?`;
}
if (key === "meeting_edit") {
return "/meeting/edit/:id?";
}
if (key === 'contract_approval_edit') {
return "/contract/approval/edit/:id?";
}
return routePath;
},
onRouteMetaGen(routeName) {
const key = routeName as RouteKey;
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
const meta: Partial<RouteMeta> = {
title: key,
// i18nKey: `route.${key}` as App.I18n.I18nKey
};
if (constantRoutes.includes(key)) {
meta.constant = true;
}
return meta;
}
});
}

32
build/plugins/unocss.ts Normal file
View File

@ -0,0 +1,32 @@
import process from 'node:process';
import path from 'node:path';
import unocss from '@unocss/vite';
import presetIcons from '@unocss/preset-icons';
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
export function setupUnocss(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
return unocss({
presets: [
presetIcons({
// prefix: `${VITE_ICON_PREFIX}-`,
scale: 1.2,
extraProperties: {
display: 'inline-block'
},
collections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
warn: true
})
]
});
}

52
build/plugins/unplugin.ts Normal file
View File

@ -0,0 +1,52 @@
import process from 'node:process';
import path from 'node:path';
import type { PluginOption } from 'vite';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export function setupUnplugin(viteEnv: Env.ImportMeta) {
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
/** The name of the local icon collection */
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
const plugins: PluginOption[] = [
Icons({
compiler: 'vue3',
customCollections: {
[collectionName]: FileSystemIconLoader(localIconPath, svg =>
svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dirs: ['src/components'],
extensions: ['vue'],
dts: 'src/types/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [
AntDesignVueResolver({
importStyle: false
}),
NaiveUiResolver(),
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
]
}),
createSvgIconsPlugin({
iconDirs: [localIconPath],
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
inject: 'body-last',
customDomId: '__SVG_ICON_LOCAL__'
})
];
return plugins;
}

48
eslint.config.js Normal file
View File

@ -0,0 +1,48 @@
// eslint.config.js
import antfu from '@antfu/eslint-config'
export default antfu(
{
// Configures for antfu's config
// Enable stylistic formatting rules
stylistic: true,
// TypeScript and Vue are auto-detected, you can also explicitly enable them:
typescript: true,
vue: true,
// Disable jsonc and yaml support
jsonc: false,
yaml: false,
formatters: {
/**
* Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
* By default uses Prettier
*/
css: true,
/**
* Format HTML files
* By default uses Prettier
*/
html: true,
/**
* Format Markdown files
* Supports Prettier and dprint
* By default uses Prettier
*/
markdown: 'prettier',
},
},
// From the second arguments they are ESLint Flat Configs
// you can have multiple configs
{
files: ['**/*.ts'],
rules: {},
},
{
rules: {
'no-console': 'off',
'style/semi': ['error', 'never'],
},
},
)

29
index.html Normal file
View File

@ -0,0 +1,29 @@
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="text/javascript">
(function () {
var ua = navigator.userAgent.toLocaleLowerCase();
var browserType = "", browserVersion = "";
if (ua.match(/msie/) != null || ua.match(/trident/) != null) {
browserType = "IE";
browserVersion = ua.match(/msie ([\d.]+)/) != null ? ua.match(/msie ([\d.]+)/)[1] : ua.match(/rv:([\d.]+)/)[1];
if (1 * browserVersion < 12) {
alert("请在ie11版本浏览器上使用系统");
}
}
})();
</script>
</body>
</html>

14
mock/common.ts Normal file
View File

@ -0,0 +1,14 @@
export function ok(data?: any) {
return {
code: 0,
data: data || {},
msg: ''
}
}
export function err(data?: any) {
return {
code: 0,
data: data || {},
msg: ''
}

0
mock/index.ts Normal file
View File

12
mock/system/auth.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineMock } from '@alova/mock';
import { ok, err } from '../common'
export default defineMock(
{
// 捕获get请求
'/todo': ok({
nickname: ''
}),
},
true
);

108
package.json Normal file
View File

@ -0,0 +1,108 @@
{
"name": "pl-webbase-src",
"version": "naive-ui-admin",
"author": {
"name": "author",
"email": "aa@qq.com",
"url": "https://xx.git"
},
"private": true,
"type": "module",
"engines": {
"node": ">=18.12.0",
"pnpm": ">=8.7.0"
},
"scripts": {
"build": "vite build --mode prod",
"build:test": "vite build --mode test",
"cleanup": "sa cleanup",
"commit": "sa git-commit",
"dev": "vite --mode test",
"dev:prod": "vite --mode prod",
"gen-route": "sa gen-route",
"preview": "vite preview",
"release": "sa release",
"typecheck": "vue-tsc --noEmit --skipLibCheck",
"update-pkg": "sa update-pkg",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@alova/mock": "2.0.0-beta.11",
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@sa/color": "workspace:*",
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@vicons/antd": "^0.12.0",
"@vueuse/core": "10.9.0",
"@vxe-ui/plugin-render-naive": "workspace:*",
"alova": "3.0.0-beta.14",
"big.js": "^6.2.1",
"clipboard": "2.0.11",
"dayjs": "1.11.11",
"echarts": "5.5.0",
"exceljs": "^4.4.0",
"lodash-es": "4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"naive-ui": "2.38.2",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"sortablejs": "^1.15.2",
"tyme4ts": "^1.0.7",
"vfonts": "^0.0.3",
"vue": "3.4.27",
"vue-draggable-plus": "0.4.1",
"vue-i18n": "9.13.1",
"vue-router": "4.3.2",
"vue-types": "^5.1.2",
"vxe-pc-ui": "^4.0.44",
"vxe-table": "^4.7.40",
"vxe-table-plugin-export-xlsx": "^4.0.5",
"xe-utils": "^3.5.26"
},
"devDependencies": {
"@antfu/eslint-config": "^2.21.1",
"@elegant-router/vue": "0.3.7",
"@iconify-json/icon-park-outline": "^1.1.15",
"@iconify/json": "2.2.211",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@types/big.js": "^6.2.2",
"@types/lodash-es": "4.17.12",
"@types/node": "20.12.12",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.60.2",
"@unocss/preset-icons": "0.60.2",
"@unocss/preset-uno": "0.60.2",
"@unocss/transformer-directives": "0.60.2",
"@unocss/transformer-variant-group": "0.60.2",
"@unocss/vite": "0.60.2",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0",
"eslint": "9.3.0",
"eslint-plugin-format": "^0.1.2",
"eslint-plugin-vue": "9.26.0",
"lint-staged": "15.2.2",
"sass": "1.77.2",
"seemly": "^0.3.8",
"tsx": "4.10.5",
"typescript": "5.4.5",
"unplugin-icons": "0.19.0",
"unplugin-vue-components": "0.27.0",
"vite": "5.2.11",
"vite-plugin-lazy-import": "^1.0.7",
"vite-plugin-mock": "^2.9.8",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.2.0",
"vue-eslint-parser": "9.4.2",
"vue-tsc": "2.0.19"
},
"lint-staged": {
"*": "eslint --fix"
}
}

View File

@ -0,0 +1,21 @@
{
"name": "@sa/axios",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.6.8",
"axios-retry": "4.2.0",
"qs": "6.12.1"
},
"devDependencies": {
"@types/qs": "6.9.15"
}
}

View File

@ -0,0 +1,5 @@
/** request id key */
export const REQUEST_ID_KEY = 'X-Request-Id';
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

181
packages/axios/src/index.ts Normal file
View File

@ -0,0 +1,181 @@
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
FlatRequestInstance,
MappedType,
RequestInstance,
RequestOption,
ResponseType
} from './type';
function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
axiosRetry(instance, retryOptions);
instance.interceptors.request.use(conf => {
const config: InternalAxiosRequestConfig = { ...conf };
// set request id
const requestId = nanoid();
config.headers.set(REQUEST_ID_KEY, requestId);
// config cancel token
const cancelTokenSource = axios.CancelToken.source();
config.cancelToken = cancelTokenSource.token;
cancelTokenSourceMap.set(requestId, cancelTokenSource);
// handle config by hook
const handledConfig = opts.onRequest?.(config) || config;
return handledConfig;
});
instance.interceptors.response.use(
async response => {
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}
const fail = await opts.onBackendFail(response, instance);
if (fail) {
return fail;
}
const backendError = new AxiosError<ResponseData>(
'the backend request error',
BACKEND_ERROR_CODE,
response.config,
response.request,
response
);
await opts.onError(backendError);
return Promise.reject(backendError);
},
async (error: AxiosError<ResponseData>) => {
await opts.onError(error);
return Promise.reject(error);
}
);
function cancelRequest(requestId: string) {
const cancelTokenSource = cancelTokenSourceMap.get(requestId);
if (cancelTokenSource) {
cancelTokenSource.cancel();
cancelTokenSourceMap.delete(requestId);
}
}
function cancelAllRequest() {
cancelTokenSourceMap.forEach(cancelTokenSource => {
cancelTokenSource.cancel();
});
cancelTokenSourceMap.clear();
}
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}
/**
* create a request instance
*
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
return opts.transformBackendResponse(response);
}
return response.data as MappedType<R, T>;
} as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;
request.state = {} as State;
return request;
}
/**
* create a flat request instance
*
* The response data is a flat object: { data: any, error: AxiosError }
*
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
T = any,
R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
try {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
const data = opts.transformBackendResponse(response);
return { data, error: null };
}
return { data: response.data as MappedType<R, T>, error: null };
} catch (error) {
return { data: null, error };
}
} as FlatRequestInstance<State, ResponseData>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State;
return flatRequest;
}
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
export type * from './type';
export type { CreateAxiosDefaults, AxiosError };

View File

@ -0,0 +1,48 @@
import type { CreateAxiosDefaults } from 'axios';
import type { IAxiosRetryConfig } from 'axios-retry';
import { stringify } from 'qs';
import { isHttpSuccess } from './shared';
import type { RequestOption } from './type';
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
const opts: RequestOption<ResponseData> = {
onRequest: async config => config,
isBackendSuccess: _response => true,
onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {}
};
Object.assign(opts, options);
return opts;
}
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
const retryConfig: IAxiosRetryConfig = {
retries: 3
};
Object.assign(retryConfig, config);
return retryConfig;
}
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
const TEN_SECONDS = 10 * 1000;
const axiosConfig: CreateAxiosDefaults = {
timeout: TEN_SECONDS,
headers: {
'Content-Type': 'application/json'
},
validateStatus: isHttpSuccess,
paramsSerializer: params => {
return stringify(params);
}
};
Object.assign(axiosConfig, config);
return axiosConfig;
}

View File

@ -0,0 +1,28 @@
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export function getContentType(config: InternalAxiosRequestConfig) {
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
return contentType;
}
/**
* check if http status is success
*
* @param status
*/
export function isHttpSuccess(status: number) {
const isSuccessCode = status >= 200 && status < 300;
return isSuccessCode || status === 304;
}
/**
* is response json
*
* @param response axios response
*/
export function isResponseJson(response: AxiosResponse) {
const { responseType } = response.config;
return responseType === 'json' || responseType === undefined;
}

101
packages/axios/src/type.ts Normal file
View File

@ -0,0 +1,101 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export type ContentType =
| 'text/html'
| 'text/plain'
| 'multipart/form-data'
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param config Axios config
*/
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* The hook to check backend response is success or not
*
* @param response Axios response
*/
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
/**
* The hook after backend request fail
*
* For example: You can handle the expired token in this hook
*
* @param response Axios response
* @param instance Axios instance
*/
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}
interface ResponseMap {
blob: Blob;
text: string;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
? ResponseMap[R]
: JsonType;
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
responseType?: R;
};
export interface RequestInstanceCommon<T> {
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;
}
/** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<T = any> = {
data: T;
error: null;
};
export type FlatResponseFailData<ResponseData = any> = {
data: null;
error: AxiosError<ResponseData>;
};
export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<T>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,16 @@
{
"name": "@sa/color",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"colord": "2.9.3"
}
}

View File

@ -0,0 +1,2 @@
export * from './name';
export * from './palette';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,356 @@
import type { ColorPaletteFamily } from '../types';
export const colorPalettes: ColorPaletteFamily[] = [
{
name: 'Slate',
palettes: [
{ hex: '#f8fafc', number: 50 },
{ hex: '#f1f5f9', number: 100 },
{ hex: '#e2e8f0', number: 200 },
{ hex: '#cbd5e1', number: 300 },
{ hex: '#94a3b8', number: 400 },
{ hex: '#64748b', number: 500 },
{ hex: '#475569', number: 600 },
{ hex: '#334155', number: 700 },
{ hex: '#1e293b', number: 800 },
{ hex: '#0f172a', number: 900 },
{ hex: '#020617', number: 950 }
]
},
{
name: 'Gray',
palettes: [
{ hex: '#f9fafb', number: 50 },
{ hex: '#f3f4f6', number: 100 },
{ hex: '#e5e7eb', number: 200 },
{ hex: '#d1d5db', number: 300 },
{ hex: '#9ca3af', number: 400 },
{ hex: '#6b7280', number: 500 },
{ hex: '#4b5563', number: 600 },
{ hex: '#374151', number: 700 },
{ hex: '#1f2937', number: 800 },
{ hex: '#111827', number: 900 },
{ hex: '#030712', number: 950 }
]
},
{
name: 'Zinc',
palettes: [
{ hex: '#fafafa', number: 50 },
{ hex: '#f4f4f5', number: 100 },
{ hex: '#e4e4e7', number: 200 },
{ hex: '#d4d4d8', number: 300 },
{ hex: '#a1a1aa', number: 400 },
{ hex: '#71717a', number: 500 },
{ hex: '#52525b', number: 600 },
{ hex: '#3f3f46', number: 700 },
{ hex: '#27272a', number: 800 },
{ hex: '#18181b', number: 900 },
{ hex: '#09090b', number: 950 }
]
},
{
name: 'Neutral',
palettes: [
{ hex: '#fafafa', number: 50 },
{ hex: '#f5f5f5', number: 100 },
{ hex: '#e5e5e5', number: 200 },
{ hex: '#d4d4d4', number: 300 },
{ hex: '#a3a3a3', number: 400 },
{ hex: '#737373', number: 500 },
{ hex: '#525252', number: 600 },
{ hex: '#404040', number: 700 },
{ hex: '#262626', number: 800 },
{ hex: '#171717', number: 900 },
{ hex: '#0a0a0a', number: 950 }
]
},
{
name: 'Stone',
palettes: [
{ hex: '#fafaf9', number: 50 },
{ hex: '#f5f5f4', number: 100 },
{ hex: '#e7e5e4', number: 200 },
{ hex: '#d6d3d1', number: 300 },
{ hex: '#a8a29e', number: 400 },
{ hex: '#78716c', number: 500 },
{ hex: '#57534e', number: 600 },
{ hex: '#44403c', number: 700 },
{ hex: '#292524', number: 800 },
{ hex: '#1c1917', number: 900 },
{ hex: '#0c0a09', number: 950 }
]
},
{
name: 'Red',
palettes: [
{ hex: '#fef2f2', number: 50 },
{ hex: '#fee2e2', number: 100 },
{ hex: '#fecaca', number: 200 },
{ hex: '#fca5a5', number: 300 },
{ hex: '#f87171', number: 400 },
{ hex: '#ef4444', number: 500 },
{ hex: '#dc2626', number: 600 },
{ hex: '#b91c1c', number: 700 },
{ hex: '#991b1b', number: 800 },
{ hex: '#7f1d1d', number: 900 },
{ hex: '#450a0a', number: 950 }
]
},
{
name: 'Orange',
palettes: [
{ hex: '#fff7ed', number: 50 },
{ hex: '#ffedd5', number: 100 },
{ hex: '#fed7aa', number: 200 },
{ hex: '#fdba74', number: 300 },
{ hex: '#fb923c', number: 400 },
{ hex: '#f97316', number: 500 },
{ hex: '#ea580c', number: 600 },
{ hex: '#c2410c', number: 700 },
{ hex: '#9a3412', number: 800 },
{ hex: '#7c2d12', number: 900 },
{ hex: '#431407', number: 950 }
]
},
{
name: 'Amber',
palettes: [
{ hex: '#fffbeb', number: 50 },
{ hex: '#fef3c7', number: 100 },
{ hex: '#fde68a', number: 200 },
{ hex: '#fcd34d', number: 300 },
{ hex: '#fbbf24', number: 400 },
{ hex: '#f59e0b', number: 500 },
{ hex: '#d97706', number: 600 },
{ hex: '#b45309', number: 700 },
{ hex: '#92400e', number: 800 },
{ hex: '#78350f', number: 900 },
{ hex: '#451a03', number: 950 }
]
},
{
name: 'Yellow',
palettes: [
{ hex: '#fefce8', number: 50 },
{ hex: '#fef9c3', number: 100 },
{ hex: '#fef08a', number: 200 },
{ hex: '#fde047', number: 300 },
{ hex: '#facc15', number: 400 },
{ hex: '#eab308', number: 500 },
{ hex: '#ca8a04', number: 600 },
{ hex: '#a16207', number: 700 },
{ hex: '#854d0e', number: 800 },
{ hex: '#713f12', number: 900 },
{ hex: '#422006', number: 950 }
]
},
{
name: 'Lime',
palettes: [
{ hex: '#f7fee7', number: 50 },
{ hex: '#ecfccb', number: 100 },
{ hex: '#d9f99d', number: 200 },
{ hex: '#bef264', number: 300 },
{ hex: '#a3e635', number: 400 },
{ hex: '#84cc16', number: 500 },
{ hex: '#65a30d', number: 600 },
{ hex: '#4d7c0f', number: 700 },
{ hex: '#3f6212', number: 800 },
{ hex: '#365314', number: 900 },
{ hex: '#1a2e05', number: 950 }
]
},
{
name: 'Green',
palettes: [
{ hex: '#f0fdf4', number: 50 },
{ hex: '#dcfce7', number: 100 },
{ hex: '#bbf7d0', number: 200 },
{ hex: '#86efac', number: 300 },
{ hex: '#4ade80', number: 400 },
{ hex: '#22c55e', number: 500 },
{ hex: '#16a34a', number: 600 },
{ hex: '#15803d', number: 700 },
{ hex: '#166534', number: 800 },
{ hex: '#14532d', number: 900 },
{ hex: '#052e16', number: 950 }
]
},
{
name: 'Emerald',
palettes: [
{ hex: '#ecfdf5', number: 50 },
{ hex: '#d1fae5', number: 100 },
{ hex: '#a7f3d0', number: 200 },
{ hex: '#6ee7b7', number: 300 },
{ hex: '#34d399', number: 400 },
{ hex: '#10b981', number: 500 },
{ hex: '#059669', number: 600 },
{ hex: '#047857', number: 700 },
{ hex: '#065f46', number: 800 },
{ hex: '#064e3b', number: 900 },
{ hex: '#022c22', number: 950 }
]
},
{
name: 'Teal',
palettes: [
{ hex: '#f0fdfa', number: 50 },
{ hex: '#ccfbf1', number: 100 },
{ hex: '#99f6e4', number: 200 },
{ hex: '#5eead4', number: 300 },
{ hex: '#2dd4bf', number: 400 },
{ hex: '#14b8a6', number: 500 },
{ hex: '#0d9488', number: 600 },
{ hex: '#0f766e', number: 700 },
{ hex: '#115e59', number: 800 },
{ hex: '#134e4a', number: 900 },
{ hex: '#042f2e', number: 950 }
]
},
{
name: 'Cyan',
palettes: [
{ hex: '#ecfeff', number: 50 },
{ hex: '#cffafe', number: 100 },
{ hex: '#a5f3fc', number: 200 },
{ hex: '#67e8f9', number: 300 },
{ hex: '#22d3ee', number: 400 },
{ hex: '#06b6d4', number: 500 },
{ hex: '#0891b2', number: 600 },
{ hex: '#0e7490', number: 700 },
{ hex: '#155e75', number: 800 },
{ hex: '#164e63', number: 900 },
{ hex: '#083344', number: 950 }
]
},
{
name: 'Sky',
palettes: [
{ hex: '#f0f9ff', number: 50 },
{ hex: '#e0f2fe', number: 100 },
{ hex: '#bae6fd', number: 200 },
{ hex: '#7dd3fc', number: 300 },
{ hex: '#38bdf8', number: 400 },
{ hex: '#0ea5e9', number: 500 },
{ hex: '#0284c7', number: 600 },
{ hex: '#0369a1', number: 700 },
{ hex: '#075985', number: 800 },
{ hex: '#0c4a6e', number: 900 },
{ hex: '#082f49', number: 950 }
]
},
{
name: 'Blue',
palettes: [
{ hex: '#eff6ff', number: 50 },
{ hex: '#dbeafe', number: 100 },
{ hex: '#bfdbfe', number: 200 },
{ hex: '#93c5fd', number: 300 },
{ hex: '#60a5fa', number: 400 },
{ hex: '#3b82f6', number: 500 },
{ hex: '#2563eb', number: 600 },
{ hex: '#1d4ed8', number: 700 },
{ hex: '#1e40af', number: 800 },
{ hex: '#1e3a8a', number: 900 },
{ hex: '#172554', number: 950 }
]
},
{
name: 'Indigo',
palettes: [
{ hex: '#eef2ff', number: 50 },
{ hex: '#e0e7ff', number: 100 },
{ hex: '#c7d2fe', number: 200 },
{ hex: '#a5b4fc', number: 300 },
{ hex: '#818cf8', number: 400 },
{ hex: '#6366f1', number: 500 },
{ hex: '#4f46e5', number: 600 },
{ hex: '#4338ca', number: 700 },
{ hex: '#3730a3', number: 800 },
{ hex: '#312e81', number: 900 },
{ hex: '#1e1b4b', number: 950 }
]
},
{
name: 'Violet',
palettes: [
{ hex: '#f5f3ff', number: 50 },
{ hex: '#ede9fe', number: 100 },
{ hex: '#ddd6fe', number: 200 },
{ hex: '#c4b5fd', number: 300 },
{ hex: '#a78bfa', number: 400 },
{ hex: '#8b5cf6', number: 500 },
{ hex: '#7c3aed', number: 600 },
{ hex: '#6d28d9', number: 700 },
{ hex: '#5b21b6', number: 800 },
{ hex: '#4c1d95', number: 900 },
{ hex: '#2e1065', number: 950 }
]
},
{
name: 'Purple',
palettes: [
{ hex: '#faf5ff', number: 50 },
{ hex: '#f3e8ff', number: 100 },
{ hex: '#e9d5ff', number: 200 },
{ hex: '#d8b4fe', number: 300 },
{ hex: '#c084fc', number: 400 },
{ hex: '#a855f7', number: 500 },
{ hex: '#9333ea', number: 600 },
{ hex: '#7e22ce', number: 700 },
{ hex: '#6b21a8', number: 800 },
{ hex: '#581c87', number: 900 },
{ hex: '#3b0764', number: 950 }
]
},
{
name: 'Fuchsia',
palettes: [
{ hex: '#fdf4ff', number: 50 },
{ hex: '#fae8ff', number: 100 },
{ hex: '#f5d0fe', number: 200 },
{ hex: '#f0abfc', number: 300 },
{ hex: '#e879f9', number: 400 },
{ hex: '#d946ef', number: 500 },
{ hex: '#c026d3', number: 600 },
{ hex: '#a21caf', number: 700 },
{ hex: '#86198f', number: 800 },
{ hex: '#701a75', number: 900 },
{ hex: '#4a044e', number: 950 }
]
},
{
name: 'Pink',
palettes: [
{ hex: '#fdf2f8', number: 50 },
{ hex: '#fce7f3', number: 100 },
{ hex: '#fbcfe8', number: 200 },
{ hex: '#f9a8d4', number: 300 },
{ hex: '#f472b6', number: 400 },
{ hex: '#ec4899', number: 500 },
{ hex: '#db2777', number: 600 },
{ hex: '#be185d', number: 700 },
{ hex: '#9d174d', number: 800 },
{ hex: '#831843', number: 900 },
{ hex: '#500724', number: 950 }
]
},
{
name: 'Rose',
palettes: [
{ hex: '#fff1f2', number: 50 },
{ hex: '#ffe4e6', number: 100 },
{ hex: '#fecdd3', number: 200 },
{ hex: '#fda4af', number: 300 },
{ hex: '#fb7185', number: 400 },
{ hex: '#f43f5e', number: 500 },
{ hex: '#e11d48', number: 600 },
{ hex: '#be123c', number: 700 },
{ hex: '#9f1239', number: 800 },
{ hex: '#881337', number: 900 },
{ hex: '#4c0519', number: 950 }
]
}
];

View File

@ -0,0 +1,7 @@
import { colorPalettes } from './constant';
export * from './palette';
export * from './shared';
export { colorPalettes };
export * from './types';

View File

@ -0,0 +1,176 @@
import type { AnyColor, HsvColor } from 'colord';
import { getHex, getHsv, isValidColor, mixColor } from '../shared';
import type { ColorIndex } from '../types';
/** Hue step */
const hueStep = 2;
/** Saturation step, light color part */
const saturationStep = 16;
/** Saturation step, dark color part */
const saturationStep2 = 5;
/** Brightness step, light color part */
const brightnessStep1 = 5;
/** Brightness step, dark color part */
const brightnessStep2 = 15;
/** Light color count, main color up */
const lightColorCount = 5;
/** Dark color count, main color down */
const darkColorCount = 4;
/**
* Get AntD palette color by index
*
* @param color - Color
* @param index - The color index of color palette (the main color index is 6)
* @returns Hex color
*/
export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string {
if (!isValidColor(color)) {
throw new Error('invalid input color value');
}
if (index === 6) {
return getHex(color);
}
const isLight = index < 6;
const hsv = getHsv(color);
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
const newHsv: HsvColor = {
h: getHue(hsv, i, isLight),
s: getSaturation(hsv, i, isLight),
v: getValue(hsv, i, isLight)
};
return getHex(newHsv);
}
/** Map of dark color index and opacity */
const darkColorMap = [
{ index: 7, opacity: 0.15 },
{ index: 6, opacity: 0.25 },
{ index: 5, opacity: 0.3 },
{ index: 5, opacity: 0.45 },
{ index: 5, opacity: 0.65 },
{ index: 5, opacity: 0.85 },
{ index: 5, opacity: 0.9 },
{ index: 4, opacity: 0.93 },
{ index: 3, opacity: 0.95 },
{ index: 2, opacity: 0.97 },
{ index: 1, opacity: 0.98 }
];
/**
* Get AntD color palette
*
* @param color - Color
* @param darkTheme - Dark theme
* @param darkThemeMixColor - Dark theme mix color (default: #141414)
*/
export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index));
if (darkTheme) {
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity);
return darkColor;
});
return darkPatterns.map(item => getHex(item));
}
return patterns;
}
/**
* Get hue
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
let hue: number;
const hsvH = Math.round(hsv.h);
if (hsvH >= 60 && hsvH <= 240) {
hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
} else {
hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
}
if (hue < 0) {
hue += 360;
}
if (hue >= 360) {
hue -= 360;
}
return hue;
}
/**
* Get saturation
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
let saturation: number;
if (isLight) {
saturation = hsv.s - saturationStep * i;
} else if (i === darkColorCount) {
saturation = hsv.s + saturationStep;
} else {
saturation = hsv.s + saturationStep2 * i;
}
if (saturation > 100) {
saturation = 100;
}
if (isLight && i === lightColorCount && saturation > 10) {
saturation = 10;
}
if (saturation < 6) {
saturation = 6;
}
return saturation;
}
/**
* Get value of hsv
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
let value: number;
if (isLight) {
value = hsv.v + brightnessStep1 * i;
} else {
value = hsv.v - brightnessStep2 * i;
}
if (value > 100) {
value = 100;
}
return value;
}

View File

@ -0,0 +1,45 @@
import type { AnyColor } from 'colord';
import { getHex } from '../shared';
import type { ColorPaletteNumber } from '../types';
import { getRecommendedColorPalette } from './recommend';
import { getAntDColorPalette } from './antd';
/**
* get color palette by provided color
*
* @param color
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
*/
export function getColorPalette(color: AnyColor, recommended = false) {
const colorMap = new Map<ColorPaletteNumber, string>();
if (recommended) {
const colorPalette = getRecommendedColorPalette(getHex(color));
colorPalette.palettes.forEach(palette => {
colorMap.set(palette.number, palette.hex);
});
} else {
const colors = getAntDColorPalette(color);
const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
colorNumbers.forEach((number, index) => {
colorMap.set(number, colors[index]);
});
}
return colorMap;
}
/**
* get color palette color by number
*
* @param color the provided color
* @param number the color palette number
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
*/
export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) {
const colorMap = getColorPalette(color, recommended);
return colorMap.get(number as ColorPaletteNumber)!;
}

View File

@ -0,0 +1,152 @@
import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared';
import { colorPalettes } from '../constant';
import type {
ColorPalette,
ColorPaletteFamily,
ColorPaletteFamilyWithNearestPalette,
ColorPaletteMatch,
ColorPaletteNumber
} from '../types';
/**
* get recommended color palette by provided color
*
* @param color the provided color
*/
export function getRecommendedColorPalette(color: string) {
const colorPaletteFamily = getRecommendedColorPaletteFamily(color);
const colorMap = new Map<ColorPaletteNumber, ColorPalette>();
colorPaletteFamily.palettes.forEach(palette => {
colorMap.set(palette.number, palette);
});
const mainColor = colorMap.get(500)!;
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!;
const colorPalette: ColorPaletteMatch = {
...colorPaletteFamily,
colorMap,
main: mainColor,
match: matchColor
};
return colorPalette;
}
/**
* get recommended palette color by provided color
*
* @param color the provided color
* @param number the color palette number
*/
export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) {
const colorPalette = getRecommendedColorPalette(color);
const { hex } = colorPalette.colorMap.get(number)!;
return hex;
}
/**
* get color palette family by provided color and color name
*
* @param color the provided color
*/
export function getRecommendedColorPaletteFamily(color: string) {
if (!isValidColor(color)) {
throw new Error('Invalid color, please check color value!');
}
let colorName = getColorName(color);
colorName = colorName.toLowerCase().replace(/\s/g, '-');
const { h: h1, s: s1 } = getHsl(color);
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes);
const { number, hex } = nearestLightnessPalette;
const { h: h2, s: s2 } = getHsl(hex);
const deltaH = h1 - h2;
const sRatio = s1 / s2;
const colorPaletteFamily: ColorPaletteFamily = {
name: colorName,
palettes: palettes.map(palette => {
let hexValue = color;
const isSame = number === palette.number;
if (!isSame) {
const { h: h3, s: s3, l } = getHsl(palette.hex);
const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH;
const newS = s3 * sRatio;
hexValue = transformHslToHex({
h: newH,
s: newS,
l
});
}
return {
hex: hexValue,
number: palette.number
};
})
};
return colorPaletteFamily;
}
/**
* get nearest color palette family
*
* @param color color
* @param families color palette families
*/
function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
const familyWithConfig = families.map(family => {
const palettes = family.palettes.map(palette => {
return {
...palette,
delta: getDeltaE(color, palette.hex)
};
});
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
return {
...family,
palettes,
nearestPalette
};
});
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
);
const { l } = getHsl(color);
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
...nearestPaletteFamily,
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
const { l: prevLightness } = getHsl(prev.hex);
const { l: currLightness } = getHsl(curr.hex);
const deltaPrev = Math.abs(prevLightness - l);
const deltaCurr = Math.abs(currLightness - l);
return deltaPrev < deltaCurr ? prev : curr;
})
};
return paletteFamily;
}

View File

@ -0,0 +1,93 @@
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import mixPlugin from 'colord/plugins/mix';
import labPlugin from 'colord/plugins/lab';
import type { AnyColor, HslColor, RgbColor } from 'colord';
extend([namesPlugin, mixPlugin, labPlugin]);
export function isValidColor(color: AnyColor) {
return colord(color).isValid();
}
export function getHex(color: AnyColor) {
return colord(color).toHex();
}
export function getRgb(color: AnyColor) {
return colord(color).toRgb();
}
export function getHsl(color: AnyColor) {
return colord(color).toHsl();
}
export function getHsv(color: AnyColor) {
return colord(color).toHsv();
}
export function getDeltaE(color1: AnyColor, color2: AnyColor) {
return colord(color1).delta(color2);
}
export function transformHslToHex(color: HslColor) {
return colord(color).toHex();
}
/**
* Add color alpha
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
*/
export function addColorAlpha(color: AnyColor, alpha: number) {
return colord(color).alpha(alpha).toHex();
}
/**
* Mix color
*
* @param firstColor - First color
* @param secondColor - Second color
* @param ratio - The ratio of the second color (0 - 1)
*/
export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) {
return colord(firstColor).mix(secondColor, ratio).toHex();
}
/**
* Transform color with opacity to similar color without opacity
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
* @param bgColor Background color (usually white or black)
*/
export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') {
const originColor = addColorAlpha(color, alpha);
const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
function calRgb(or: number, bg: number, al: number) {
return bg + (or - bg) * al;
}
const resultRgb: RgbColor = {
r: calRgb(oR, bgR, alpha),
g: calRgb(oG, bgG, alpha),
b: calRgb(oB, bgB, alpha)
};
return colord(resultRgb).toHex();
}
/**
* Is white color
*
* @param color - Color
*/
export function isWhiteColor(color: AnyColor) {
return colord(color).isEqual('#ffffff');
}
export { colord };

View File

@ -0,0 +1,2 @@
export * from './colord';
export * from './name';

View File

@ -0,0 +1,49 @@
import { colorNames } from '../constant';
import { getHex, getHsl, getRgb } from './colord';
/**
* Get color name
*
* @param color
*/
export function getColorName(color: string) {
const hex = getHex(color);
const rgb = getRgb(color);
const hsl = getHsl(color);
let ndf = 0;
let ndf1 = 0;
let ndf2 = 0;
let cl = -1;
let df = -1;
let name = '';
colorNames.some((item, index) => {
const [hexValue, colorName] = item;
const match = hex === hexValue;
if (match) {
name = colorName;
} else {
const { r, g, b } = getRgb(hexValue);
const { h, s, l } = getHsl(hexValue);
ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
ndf = ndf1 + ndf2 * 2;
if (df < 0 || df > ndf) {
df = ndf;
cl = index;
}
}
return match;
});
name = colorNames[cl][1];
return name;
}

View File

@ -0,0 +1,58 @@
/**
* the color palette number
*
* the main color number is 500
*/
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
/** the color palette */
export type ColorPalette = {
/** the color hex value */
hex: string;
/**
* the color number
*
* - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
*/
number: ColorPaletteNumber;
};
/** the color palette family */
export type ColorPaletteFamily = {
/** the color palette family name */
name: string;
/** the color palettes */
palettes: ColorPalette[];
};
/** the color palette with delta */
export type ColorPaletteWithDelta = ColorPalette & {
delta: number;
};
/** the color palette family with nearest palette */
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
nearestPalette: ColorPaletteWithDelta;
nearestLightnessPalette: ColorPaletteWithDelta;
};
/** the color palette match */
export type ColorPaletteMatch = ColorPaletteFamily & {
/** the color map of the palette */
colorMap: Map<ColorPaletteNumber, ColorPalette>;
/**
* the main color of the palette
*
* which number is 500
*/
main: ColorPalette;
/** the match color of the palette */
match: ColorPalette;
};
/**
* The color index of color palette
*
* From left to right, the color is from light to dark, 6 is main color
*/
export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,15 @@
{
"name": "@sa/hooks",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*"
}
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,15 @@
{
"name": "@sa/hooks",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*"
}
}

View File

@ -0,0 +1,11 @@
import useBoolean from './use-boolean';
import useLoading from './use-loading';
import useCountDown from './use-count-down';
import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export * from './use-signal';
export * from './use-table';

View File

@ -0,0 +1,31 @@
import { ref } from 'vue';
/**
* Boolean
*
* @param initValue Init value
*/
export default function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle
};
}

View File

@ -0,0 +1,96 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/**
* Use context
*
* @example
* ```ts
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
*
* // context.ts
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
* count.value++;
* }
*
* function decrement() {
* count.value--;
* }
*
* return {
* count,
* increment,
* decrement
* };
* })
* ``` // A.vue
* ```vue
* <template>
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
*
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
* <template>
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
*
* const { count, increment } = useStore();
* </script>
* ```;
*
* // C.vue is same as B.vue
*
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
return {
/** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
};
}
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvide(context: T) {
provide(injectKey, context);
return context;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
};
}

View File

@ -0,0 +1,49 @@
import { computed, onScopeDispose, ref } from 'vue';
import { useRafFn } from '@vueuse/core';
/**
* count down
*
* @param seconds - count down seconds
*/
export default function useCountDown(seconds: number) {
const FPS_PER_SECOND = 60;
const fps = ref(0);
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
const isCounting = computed(() => fps.value > 0);
const { pause, resume } = useRafFn(
() => {
if (fps.value > 0) {
fps.value -= 1;
} else {
pause();
}
},
{ immediate: false }
);
function start(updateSeconds: number = seconds) {
fps.value = FPS_PER_SECOND * updateSeconds;
resume();
}
function stop() {
fps.value = 0;
pause();
}
onScopeDispose(() => {
pause();
});
return {
count,
isCounting,
start,
stop
};
}

View File

@ -0,0 +1,16 @@
import useBoolean from './use-boolean';
/**
* Loading
*
* @param initValue Init value
*/
export default function useLoading(initValue = false) {
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
return {
loading,
startLoading,
endLoading
};
}

View File

@ -0,0 +1,144 @@
import { computed, ref, shallowRef, triggerRef } from 'vue';
import type {
ComputedGetter,
DebuggerOptions,
Ref,
ShallowRef,
WritableComputedOptions,
WritableComputedRef
} from 'vue';
type Updater<T> = (value: T) => T;
type Mutator<T> = (value: T) => void;
/**
* Signal is a reactive value that can be set, updated or mutated
*
* @example
* ```ts
* const count = useSignal(0);
*
* // `watchEffect`
* watchEffect(() => {
* console.log(count());
* });
*
* // watch
* watch(count, value => {
* console.log(value);
* });
*
* // useComputed
* const double = useComputed(() => count() * 2);
* const writeableDouble = useComputed({
* get: () => count() * 2,
* set: value => count.set(value / 2)
* });
* ```
*/
export interface Signal<T> {
(): Readonly<T>;
/**
* Set the value of the signal
*
* It recommend use `set` for primitive values
*
* @param value
*/
set(value: T): void;
/**
* Update the value of the signal using an updater function
*
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
*
* @param updater
*/
update(updater: Updater<T>): void;
/**
* Mutate the value of the signal using a mutator function
*
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
*
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
*
* @param mutator
*/
mutate(mutator: Mutator<T>): void;
/**
* Get the reference of the signal
*
* Sometimes it can be useful to make `v-model` work with the signal
*
* ```vue
* <template>
* <input v-model="model.count" />
* </template>;
*
* <script setup lang="ts">
* const state = useSignal({ count: 0 }, { useRef: true });
*
* const model = state.getRef();
* </script>
* ```
*/
getRef(): Readonly<ShallowRef<Readonly<T>>>;
}
export interface ReadonlySignal<T> {
(): Readonly<T>;
}
export interface SignalOptions {
/**
* Whether to use `ref` to store the value
*
* @default false use `sharedRef` to store the value
*/
useRef?: boolean;
}
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
const { useRef } = options || {};
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
return createSignal(state);
}
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
export function useComputed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
const isGetter = typeof getterOrOptions === 'function';
const computedValue = computed(getterOrOptions as any, debugOptions);
if (isGetter) {
return () => computedValue.value as ReadonlySignal<T>;
}
return createSignal(computedValue);
}
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
const signal = () => state.value;
signal.set = (value: T) => {
state.value = value;
};
signal.update = (updater: Updater<T>) => {
state.value = updater(state.value);
};
signal.mutate = (mutator: Mutator<T>) => {
mutator(state.value);
triggerRef(state);
};
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
return signal;
}

View File

@ -0,0 +1,50 @@
import { h } from 'vue';
import type { Component } from 'vue';
/**
* Svg icon render hook
*
* @param SvgIcon Svg icon component
*/
export default function useSvgIconRender(SvgIcon: Component) {
interface IconConfig {
/** Iconify icon name */
icon?: string;
/** Local icon name */
localIcon?: string;
/** Icon color */
color?: string;
/** Icon size */
fontSize?: number;
}
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
/**
* Svg icon VNode
*
* @param config
*/
const SvgIconVNode = (config: IconConfig) => {
const { color, fontSize, icon, localIcon } = config;
const style: IconStyle = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
if (!icon && !localIcon) {
return undefined;
}
return () => h(SvgIcon, { icon, localIcon, style });
};
return {
SvgIconVNode
};
}

View File

@ -0,0 +1,151 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheck = {
key: string;
title: string;
checked: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
data: TableDataWithIndex<T>[];
pageNum: number;
pageSize: number;
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* callback when response fetched
*
* @param transformed transformed data
*/
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
...col,
checked: checkMap.get(col.key) ?? col.checked
}));
}
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.data;
setEmpty(transformed.data.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
}
if (immediate) {
getData();
}
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,19 @@
{
"name": "@sa/materials",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"simplebar-vue": "2.3.4"
},
"devDependencies": {
"typed-css-modules": "0.9.1"
}
}

View File

@ -0,0 +1,6 @@
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
import PageTab from './libs/page-tab';
import SimpleScrollbar from './libs/simple-scrollbar';
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
export * from './types';

View File

@ -0,0 +1,63 @@
/* @type */
.layout-header,
.layout-header-placement {
height: var(--soy-header-height);
}
.layout-header {
z-index: var(--soy-header-z-index);
}
.layout-tab {
top: var(--soy-header-height);
height: var(--soy-tab-height);
z-index: var(--soy-tab-z-index);
}
.layout-tab-placement {
height: var(--soy-tab-height);
}
.layout-sider {
width: var(--soy-sider-width);
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider {
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider-mask {
z-index: var(--soy-mobile-sider-z-index);
}
.layout-sider_collapsed {
width: var(--soy-sider-collapsed-width);
z-index: var(--soy-sider-z-index);
}
.layout-footer,
.layout-footer-placement {
height: var(--soy-footer-height);
}
.layout-footer {
z-index: var(--soy-footer-z-index);
}
.left-gap {
padding-left: var(--soy-sider-width);
}
.left-gap_collapsed {
padding-left: var(--soy-sider-collapsed-width);
}
.sider-padding-top {
padding-top: var(--soy-header-height);
}
.sider-padding-bottom {
padding-bottom: var(--soy-footer-height);
}

View File

@ -0,0 +1,18 @@
declare const styles: {
readonly 'layout-header': string;
readonly 'layout-header-placement': string;
readonly 'layout-tab': string;
readonly 'layout-tab-placement': string;
readonly 'layout-sider': string;
readonly 'layout-mobile-sider': string;
readonly 'layout-mobile-sider-mask': string;
readonly 'layout-sider_collapsed': string;
readonly 'layout-footer': string;
readonly 'layout-footer-placement': string;
readonly 'left-gap': string;
readonly 'left-gap_collapsed': string;
readonly 'sider-padding-top': string;
readonly 'sider-padding-bottom': string;
};
export default styles;

View File

@ -0,0 +1,5 @@
import AdminLayout from './index.vue';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
export default AdminLayout;
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };

View File

@ -0,0 +1,237 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { AdminLayoutProps } from '../../types';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
import style from './index.module.css';
defineOptions({
name: 'AdminLayout'
});
const props = withDefaults(defineProps<AdminLayoutProps>(), {
mode: 'vertical',
scrollMode: 'content',
scrollElId: LAYOUT_SCROLL_EL_ID,
commonClass: 'transition-all-300',
fixedTop: true,
maxZIndex: LAYOUT_MAX_Z_INDEX,
headerVisible: true,
headerHeight: 56,
tabVisible: true,
tabHeight: 48,
siderVisible: true,
siderCollapse: false,
siderWidth: 220,
siderCollapsedWidth: 64,
footerVisible: true,
footerHeight: 48,
rightFooter: false
});
interface Emits {
/** Update siderCollapse */
(e: 'update:siderCollapse', collapse: boolean): void;
}
const emit = defineEmits<Emits>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/** Main */
default?: SlotFn;
/** Header */
header?: SlotFn;
/** Tab */
tab?: SlotFn;
/** Sider */
sider?: SlotFn;
/** Footer */
footer?: SlotFn;
};
const slots = defineSlots<Slots>();
const cssVars = computed(() => createLayoutCssVars(props));
// config visible
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
// scroll mode
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
const isContentScroll = computed(() => props.scrollMode === 'content');
// layout direction
const isVertical = computed(() => props.mode === 'vertical');
const isHorizontal = computed(() => props.mode === 'horizontal');
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
// css
const leftGapClass = computed(() => {
if (!props.fullContent && showSider.value) {
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
}
return '';
});
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
const footerLeftGapClass = computed(() => {
const condition1 = isVertical.value;
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
if (condition1 || condition2 || condition3) {
return leftGapClass.value;
}
return '';
});
const siderPaddingClass = computed(() => {
let cls = '';
if (showHeader.value && !headerLeftGapClass.value) {
cls += style['sider-padding-top'];
}
if (showFooter.value && !footerLeftGapClass.value) {
cls += ` ${style['sider-padding-bottom']}`;
}
return cls;
});
function handleClickMask() {
emit('update:siderCollapse', true);
}
</script>
<template>
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
class="h-full flex flex-col"
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-header'],
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="header"></slot>
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-header-placement']]"
></div>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
class="flex-shrink-0"
:class="[
style['layout-tab'],
commonClass,
tabClass,
{ 'top-0!': fullContent || !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="tab"></slot>
</div>
<div
v-show="fullContent || fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-tab-placement']]"
></div>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
class="absolute left-0 top-0 h-full"
:class="[
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
class="absolute left-0 top-0 h-full w-0 bg-white"
:class="[
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
<div
v-show="!siderCollapse"
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
:class="[style['layout-mobile-sider-mask']]"
@click="handleClickMask"
></div>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
class="flex flex-col flex-grow"
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
>
<slot></slot>
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-footer'],
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
]"
>
<slot name="footer"></slot>
</footer>
<div
v-show="!fullContent && fixedFooter"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-footer-placement']]"
></div>
</template>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
/** The id of the scroll element of the layout */
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
/** The max z-index of the layout */
export const LAYOUT_MAX_Z_INDEX = 100;
/**
* Create layout css vars by css vars props
*
* @param props Css vars props
*/
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
const cssVars: LayoutCssVars = {
'--soy-header-height': `${props.headerHeight}px`,
'--soy-header-z-index': props.headerZIndex,
'--soy-tab-height': `${props.tabHeight}px`,
'--soy-tab-z-index': props.tabZIndex,
'--soy-sider-width': `${props.siderWidth}px`,
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
'--soy-sider-z-index': props.siderZIndex,
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
'--soy-footer-height': `${props.footerHeight}px`,
'--soy-footer-z-index': props.footerZIndex
};
return cssVars;
}
/**
* Create layout css vars
*
* @param props
*/
export function createLayoutCssVars(props: AdminLayoutProps) {
const {
mode,
isMobile,
maxZIndex = LAYOUT_MAX_Z_INDEX,
headerHeight,
tabHeight,
siderWidth,
siderCollapsedWidth,
footerHeight
} = props;
const headerZIndex = maxZIndex - 3;
const tabZIndex = maxZIndex - 5;
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
const footerZIndex = maxZIndex - 5;
const cssProps: LayoutCssVarsProps = {
headerHeight,
headerZIndex,
tabHeight,
tabZIndex,
siderWidth,
siderZIndex,
mobileSiderZIndex,
siderCollapsedWidth,
footerHeight,
footerZIndex
};
return createLayoutCssVarsByCssVarsProps(cssProps);
}

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import style from './index.module.css';
defineOptions({
name: 'ButtonTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-1px rounded-4px border-solid px-12px py-4px"
:class="[
style['button-tab'],
{ [style['button-tab_dark']]: darkMode },
{ [style['button-tab_active']]: active },
{ [style['button-tab_active_dark']]: active && darkMode }
]"
>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'ChromeTabBg'
});
</script>
<template>
<svg class="size-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
</svg>
</g>
</svg>
</template>
<style scoped></style>

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import ChromeTabBg from './chrome-tab-bg.vue';
import style from './index.module.css';
defineOptions({
name: 'ChromeTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
:class="[
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode }
]"
>
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,97 @@
/* @type */
.button-tab {
border-color: #e5e7eb;
}
.button-tab_dark {
border-color: #ffffff3d;
}
.button-tab:hover {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
}
.button-tab_active {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
background-color: var(--soy-primary-color-opacity1);
}
.button-tab_active_dark {
background-color: var(--soy-primary-color-opacity2);
}
.button-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: var(--soy-primary-color);
}
.button-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab:hover {
z-index: 9;
}
.chrome-tab_active {
z-index: 10;
color: var(--soy-primary-color);
}
.chrome-tab__bg {
color: transparent;
}
.chrome-tab_active .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_active_dark .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab:hover .chrome-tab__bg {
color: #dee1e6;
}
.chrome-tab_active:hover .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_dark:hover .chrome-tab__bg {
color: #333333;
}
.chrome-tab_active_dark:hover .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: #9ca3af;
}
.chrome-tab_active .svg-close:hover {
background-color: var(--soy-primary-color);
}
.chrome-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab_active .chrome-tab-divider {
opacity: 0;
}
.chrome-tab:hover .chrome-tab-divider {
opacity: 0;
}
.chrome-tab_dark .chrome-tab-divider {
background-color: rgba(255, 255, 255, 0.9);
}

View File

@ -0,0 +1,15 @@
declare const styles: {
readonly 'button-tab': string;
readonly 'button-tab_dark': string;
readonly 'button-tab_active': string;
readonly 'button-tab_active_dark': string;
readonly 'chrome-tab': string;
readonly 'chrome-tab_active': string;
readonly 'chrome-tab__bg': string;
readonly 'chrome-tab_active_dark': string;
readonly 'chrome-tab_dark': string;
readonly 'chrome-tab-divider': string;
readonly 'svg-close': string;
};
export default styles;

View File

@ -0,0 +1,3 @@
import PageTab from './index.vue';
export default PageTab;

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import type { PageTabMode, PageTabProps } from '../../types';
import { ACTIVE_COLOR, createTabCssVars } from './shared';
import ChromeTab from './chrome-tab.vue';
import ButtonTab from './button-tab.vue';
import SvgClose from './svg-close.vue';
import style from './index.module.css';
defineOptions({
name: 'PageTab'
});
const props = withDefaults(defineProps<PageTabProps>(), {
mode: 'chrome',
commonClass: 'transition-all-300',
activeColor: ACTIVE_COLOR,
closable: true
});
interface Emits {
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass } = props;
const tabComponentMap = {
chrome: {
component: ChromeTab,
class: chromeClass
},
button: {
component: ButtonTab,
class: buttonClass
}
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
return tabComponentMap[mode];
});
const cssVars = computed(() => createTabCssVars(props.activeColor));
const bindProps = computed(() => {
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
return rest;
});
function handleClose() {
emit('close');
}
function handleMouseup(e: MouseEvent) {
// close tab by mouse wheel button click
if (e.button === 1) {
handleClose();
}
}
</script>
<template>
<component
:is="activeTabComponent.component"
:class="activeTabComponent.class"
:style="cssVars"
v-bind="bindProps"
@mouseup="handleMouseup"
>
<template #prefix>
<slot name="prefix"></slot>
</template>
<slot></slot>
<template #suffix>
<slot name="suffix">
<SvgClose v-if="closable" :class="[style['svg-close']]" @click.stop="handleClose" />
</slot>
</template>
</component>
</template>
<style scoped></style>

View File

@ -0,0 +1,31 @@
import { addColorAlpha, transformColorWithOpacity } from '@sa/color';
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
/** The active color of the tab */
export const ACTIVE_COLOR = '#1890ff';
function createCssVars(props: PageTabCssVarsProps) {
const cssVars: PageTabCssVars = {
'--soy-primary-color': props.primaryColor,
'--soy-primary-color1': props.primaryColor1,
'--soy-primary-color2': props.primaryColor2,
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
'--soy-primary-color-opacity3': props.primaryColorOpacity3
};
return cssVars;
}
export function createTabCssVars(primaryColor: string) {
const cssProps: PageTabCssVarsProps = {
primaryColor,
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
};
return createCssVars(cssProps);
}

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
defineOptions({
name: 'SvgClose'
});
</script>
<template>
<div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
<path
fill="currentColor"
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.8L295.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 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.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>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import SimpleScrollbar from './index.vue';
export default SimpleScrollbar;

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import Simplebar from 'simplebar-vue';
import 'simplebar-vue/dist/simplebar.min.css';
defineOptions({
name: 'SimpleScrollbar'
});
</script>
<template>
<div class="h-full flex-1-hidden">
<Simplebar class="h-full">
<slot />
</Simplebar>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,294 @@
/** Header config */
interface AdminLayoutHeaderConfig {
/**
* Whether header is visible
*
* @default true
*/
headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/**
* Header height
*
* @default 56px
*/
headerHeight?: number;
}
/** Tab config */
interface AdminLayoutTabConfig {
/**
* Whether tab is visible
*
* @default true
*/
tabVisible?: boolean;
/**
* Tab class
*
* @default ''
*/
tabClass?: string;
/**
* Tab height
*
* @default 48px
*/
tabHeight?: number;
}
/** Sider config */
interface AdminLayoutSiderConfig {
/**
* Whether sider is visible
*
* @default true
*/
siderVisible?: boolean;
/**
* Sider class
*
* @default ''
*/
siderClass?: string;
/**
* Mobile sider class
*
* @default ''
*/
mobileSiderClass?: string;
/**
* Sider collapse status
*
* @default false
*/
siderCollapse?: boolean;
/**
* Sider width when collapse is false
*
* @default '220px'
*/
siderWidth?: number;
/**
* Sider width when collapse is true
*
* @default '64px'
*/
siderCollapsedWidth?: number;
}
/** Content config */
export interface AdminLayoutContentConfig {
/**
* Content class
*
* @default ''
*/
contentClass?: string;
/**
* Whether content is full the page
*
* If true, other elements will be hidden by `display: none`
*/
fullContent?: boolean;
}
/** Footer config */
export interface AdminLayoutFooterConfig {
/**
* Whether footer is visible
*
* @default true
*/
footerVisible?: boolean;
/**
* Whether footer is fixed
*
* @default true
*/
fixedFooter?: boolean;
/**
* Footer class
*
* @default ''
*/
footerClass?: string;
/**
* Footer height
*
* @default 48px
*/
footerHeight?: number;
/**
* Whether footer is on the right side
*
* When the layout is vertical, the footer is on the right side
*/
rightFooter?: boolean;
}
/**
* Layout mode
*
* - Horizontal
* - Vertical
*/
export type LayoutMode = 'horizontal' | 'vertical';
/**
* The scroll mode when content overflow
*
* - Wrapper: the layout component's wrapper element has a scrollbar
* - Content: the layout component's content element has a scrollbar
*
* @default 'wrapper'
*/
export type LayoutScrollMode = 'wrapper' | 'content';
/** Admin layout props */
export interface AdminLayoutProps
extends AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
AdminLayoutSiderConfig,
AdminLayoutContentConfig,
AdminLayoutFooterConfig {
/**
* Layout mode
*
* - {@link LayoutMode}
*/
mode?: LayoutMode;
/** Is mobile layout */
isMobile?: boolean;
/**
* Scroll mode
*
* - {@link ScrollMode}
*/
scrollMode?: LayoutScrollMode;
/**
* The id of the scroll element of the layout
*
* It can be used to get the corresponding Dom and scroll it
*
* @example
* use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*
* @default
* ```ts
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
* ```
*/
scrollElId?: string;
/** The class of the scroll element */
scrollElClass?: string;
/** The class of the scroll wrapper element */
scrollWrapperClass?: string;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/**
* Whether fix the header and tab
*
* @default true
*/
fixedTop?: boolean;
/**
* The max z-index of the layout
*
* The z-index of Header,Tab,Sider and Footer will not exceed this value
*/
maxZIndex?: number;
}
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
: S;
type Prefix = '--soy-';
export type LayoutCssVarsProps = Pick<
AdminLayoutProps,
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
> & {
headerZIndex?: number;
tabZIndex?: number;
siderZIndex?: number;
mobileSiderZIndex?: number;
footerZIndex?: number;
};
export type LayoutCssVars = {
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};
/**
* The mode of the tab
*
* - Button: button style
* - Chrome: chrome style
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome';
export interface PageTabProps {
/** Whether is dark mode */
darkMode?: boolean;
/**
* The mode of the tab
*
* - {@link TabMode}
*/
mode?: PageTabMode;
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/** The class of the button tab */
buttonClass?: string;
/** The class of the chrome tab */
chromeClass?: string;
/** Whether the tab is active */
active?: boolean;
/** The color of the active tab */
activeColor?: string;
/**
* Whether the tab is closable
*
* Show the close icon when true
*/
closable?: boolean;
}
export type PageTabCssVarsProps = {
primaryColor: string;
primaryColor1: string;
primaryColor2: string;
primaryColorOpacity1: string;
primaryColorOpacity2: string;
primaryColorOpacity3: string;
};
export type PageTabCssVars = {
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
};

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,15 @@
{
"name": "@sa/fetch",
"version": "1.1.1",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.3.4"
}
}

View File

@ -0,0 +1,10 @@
import { ofetch } from 'ofetch';
import type { FetchOptions } from 'ofetch';
export function createRequest(options: FetchOptions) {
const request = ofetch.create(options);
return request;
}
export default createRequest;

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,31 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
objectLiteralDuplicateProperties: false
}
},
env: {
amd: true,
browser: true,
node: true
},
plugins: [
'@typescript-eslint'
],
extends: [
'prettier/@typescript-eslint',
'standard'
],
rules: {
'array-bracket-spacing': ['error', 'never'],
'no-debugger': ['error'],
'keyword-spacing': ['error']
}
}

24
packages/plugin-render-naive/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
.DS_Store
.history
node_modules
/dist
package-lock.json
yarn.lock
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Xu Liangzhan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,151 @@
# @vxe-ui/plugin-render-antd
[Vxe UI](https://vxeui.com/) plug-in for compatibility with the [ant-design-vue](https://www.npmjs.com/package/ant-design-vue) component.
## Compatibility
It corresponds to [vxe-table v4](https://www.npmjs.com/package/vxe-table) or [vxe-pc-ui v4](https://www.npmjs.com/package/vxe-pc-ui)
## Installing
```shell
npm install @vxe-ui/plugin-render-antd
```
```javascript
// ...
// Use vxe-pc-ui
import { VxeUI } from 'vxe-pc-ui'
// Use vxe-table
// import { VxeUI } from 'vxe-table'
import VxeUIPluginRenderNaive from '@vxe-ui/plugin-render-antd'
import '@vxe-ui/plugin-render-antd/dist/style.css'
// ...
VxeUI.use(VxeUIPluginRenderNaive)
```
## API
### cell-render 默认的渲染器配置项说明
| 属性 | 描述 | 类型 | 可选值 | 默认值 |
|------|------|-----|-----|-----|
| name | 支持的渲染组件 | String | AInput, AAutocomplete, AInputNumber, ASwitch, ARate, AButton, AButtons | — |
| props | 渲染组件附加属性,参数请查看被渲染的 Component props | Object | — | {} |
| options | 只对 name=ASelect 有效,下拉组件选项列表 | Array | — | [] |
| optionProps | 只对 name=ASelect 有效,下拉组件选项属性参数配置 | Object | — | { value: 'value', label: 'label' } |
| optionGroups | 只对 name=ASelect 有效,下拉组件分组选项列表 | Array | — | [] |
| optionGroupProps | 只对 name=ASelect 有效,下拉组件分组选项属性参数配置 | Object | — | { options: 'options', label: 'label' } |
| events | 渲染组件附加事件,参数为 ( {row,rowIndex,column,columnIndex}, ...Component arguments ) | Object | — | — |
### edit-render 可编辑渲染器配置项说明
| 属性 | 描述 | 类型 | 可选值 | 默认值 |
|------|------|-----|-----|-----|
| name | 支持的渲染组件 | String | AInput, AAutocomplete, AInputNumber, ASelect, ACascader, ADatePicker, AMonthPicker, ARangePicker, AWeekPicker, ATimePicker, ATreeSelect, ASwitch, ARate, AButton, AButtons | — |
| props | 渲染组件附加属性,参数请查看被渲染的 Component props | Object | — | {} |
| options | 只对 name=ASelect 有效,下拉组件选项列表 | Array | — | [] |
| optionProps | 只对 name=ASelect 有效,下拉组件选项属性参数配置 | Object | — | { value: 'value', label: 'label' } |
| optionGroups | 只对 name=ASelect 有效,下拉组件分组选项列表 | Array | — | [] |
| optionGroupProps | 只对 name=ASelect 有效,下拉组件分组选项属性参数配置 | Object | — | { options: 'options', label: 'label' } |
| events | 渲染组件附加事件,参数为 ( {row,rowIndex,column,columnIndex}, ...Component arguments ) | Object | — | — |
### filter-render 筛选渲染器配置项说明
| 属性 | 描述 | 类型 | 可选值 | 默认值 |
|------|------|-----|-----|-----|
| name | 支持的渲染组件 | String | AInput, AAutocomplete, AInputNumber, ASelect, ASwitch, ARate | — |
| props | 渲染组件附加属性,参数请查看被渲染的 Component props | Object | — | {} |
| options | 只对 name=ASelect 有效,下拉组件选项列表 | Array | — | [] |
| optionProps | 只对 name=ASelect 有效,下拉组件选项属性参数配置 | Object | — | { value: 'value', label: 'label' } |
| optionGroups | 只对 name=ASelect 有效,下拉组件分组选项列表 | Array | — | [] |
| optionGroupProps | 只对 name=ASelect 有效,下拉组件分组选项属性参数配置 | Object | — | { options: 'options', label: 'label' } |
| events | 渲染组件附加事件,参数为 ( {}, ...Component arguments ) | Object | — | — |
### item-render 表单-项选渲染器配置项说明
| 属性 | 描述 | 类型 | 可选值 | 默认值 |
|------|------|-----|-----|-----|
| name | 支持的渲染组件 | String | AInput, AAutocomplete, AInputNumber, ASelect, ASwitch, ARate, ARadio, ACheckbox, AButton, AButtons | — |
| props | 渲染组件附加属性,参数请查看被渲染的 Component props | Object | — | {} |
| options | 只对 name=ASelect 有效,下拉组件选项列表 | Array | — | [] |
| optionProps | 只对 name=ASelect 有效,下拉组件选项属性参数配置 | Object | — | { value: 'value', label: 'label' } |
| optionGroups | 只对 name=ASelect 有效,下拉组件分组选项列表 | Array | — | [] |
| optionGroupProps | 只对 name=ASelect 有效,下拉组件分组选项属性参数配置 | Object | — | { options: 'options', label: 'label' } |
| events | 渲染组件附加事件,参数为 ( {}, ...Component arguments ) | Object | — | — |
### 表单设计器配置项
| 描述 |
|------|
| 'AInputWidget', 'ATextareaWidget', 'ANumberInputWidget', 'ADatePickerWidget', 'ASelectWidget', 'ARadioWidget', 'ACheckboxWidget', 'ASwitchWidget' |
## Cell demo
```html
<vxe-table
height="600"
:data="tableData"
:edit-config="{trigger: 'click', mode: 'cell'}">
<vxe-column field="name" title="Name" :edit-render="{name: 'AInput'}"></vxe-column>
<vxe-column field="age" title="Age" :edit-render="{name: 'AInputNumber'}"></vxe-column>
<vxe-column field="date" title="Date" width="200" :edit-render="{name: 'ADatePicker'}"></vxe-column>
</vxe-table>
```
```javascript
export default {
data () {
return {
tableData: [
{ id: 100, name: 'test0', age: 28, sex: '1', date: '' },
{ id: 101, name: 'test1', age: 32, sex: '0', date: '' },
{ id: 102, name: 'test2', age: 36, sex: '1', date: '' }
]
}
}
}
```
## Filter demo
```html
<vxe-table
height="600"
:data="tableData">
<vxe-column field="name" title="Name":filters="nameOptions" :filter-render="{name: 'AInput'}"></vxe-column>
<vxe-column field="age" title="Age"></vxe-column>
<vxe-column field="date" title="Date" ></vxe-column>
</vxe-table>
```
```javascript
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
return {
nameOptions: [
{ data: '' }
],
tableData: [
{ id: 100, name: 'test0', age: 28, date: '' },
{ id: 101, name: 'test1', age: 32, date: '' },
{ id: 102, name: 'test2', age: 36, date: '' }
]
}
}
})
```
## Contributors
Thank you to everyone who contributed to this project.
[![vxe-ui-plugins](https://contrib.rocks/image?repo=x-extends/vxe-ui-plugins)](https://github.com/x-extends/vxe-ui-plugins/graphs/contributors)
## License
[MIT](LICENSE) © 2019-present, Xu Liangzhan

View File

@ -0,0 +1,125 @@
const gulp = require('gulp')
const del = require('del')
const uglify = require('gulp-uglify')
const babel = require('gulp-babel')
const rename = require('gulp-rename')
const replace = require('gulp-replace')
const dartSass = require('sass')
const gulpSass = require('gulp-sass')
const sass = gulpSass(dartSass)
const cleanCSS = require('gulp-clean-css')
const prefixer = require('gulp-autoprefixer')
const sourcemaps = require('gulp-sourcemaps')
const ts = require('gulp-typescript')
const browserify = require('browserify')
const source = require('vinyl-source-stream')
const pack = require('./package.json')
const tsconfig = require('./tsconfig.json')
const exportModuleName = 'VxeUIPluginRenderNaive'
gulp.task('build_style', function () {
return gulp.src('style.scss')
.pipe(sass())
.pipe(prefixer({
borwsers: ['last 1 version', '> 1%', 'not ie <= 8'],
cascade: true,
remove: true
}))
.pipe(gulp.dest('dist'))
.pipe(cleanCSS())
.pipe(rename({
extname: '.min.css'
}))
.pipe(gulp.dest('dist'))
})
gulp.task('build_ts', function () {
return gulp.src(['src/**/*.ts'])
.pipe(ts(tsconfig.compilerOptions))
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(gulp.dest('dist'))
})
gulp.task('build_commonjs', function () {
return gulp.src(['dist/index.js'])
.pipe(rename({
basename: 'index',
extname: '.common.js'
}))
.pipe(gulp.dest('dist'))
})
gulp.task('browserify_common', function () {
return browserify({
entries: 'dist/index.js'
}).external([
'vue',
'xe-utils',
'dayjs'
])
.bundle()
.pipe(source('all.common.js'))
.pipe(gulp.dest('dist'))
})
gulp.task('build_umd', gulp.series('browserify_common', function () {
return gulp.src(['dist/all.common.js'])
.pipe(babel({
moduleId: pack.name,
presets: [
'@babel/env'
],
plugins: [
['@babel/transform-modules-umd', {
globals: {
[pack.name]: exportModuleName,
vue: 'Vue',
'xe-utils': 'XEUtils',
dayjs: 'dayjs'
},
exactGlobals: true
}]
]
}))
.pipe(rename({
basename: 'index',
suffix: '.umd',
extname: '.js'
}))
.pipe(gulp.dest('dist'))
.pipe(uglify())
.pipe(rename({
basename: 'index',
suffix: '.umd.min',
extname: '.js'
}))
.pipe(gulp.dest('dist'))
}))
gulp.task('build_esm', function () {
return gulp.src(['src/**/*.ts'])
.pipe(ts({
...tsconfig.compilerOptions,
module: 'ESNext'
}))
.pipe(babel({
presets: [['@babel/env', { modules: false }]],
plugins: ['@babel/plugin-transform-runtime']
}))
.pipe(rename({
basename: 'index',
extname: '.esm.js'
}))
.pipe(gulp.dest('dist'));
});
gulp.task('clear', () => {
return del([
'dist'
])
})
gulp.task('build', gulp.series('clear', 'build_ts', gulp.parallel('build_esm', 'build_style')))

View File

@ -0,0 +1,73 @@
{
"name": "@vxe-ui/plugin-render-naive",
"version": "4.0.5",
"description": "Vxe UI plug-in for compatibility with the ant-design-vue component.",
"scripts": {
"lib": "gulp build"
},
"files": [
"types",
"dist",
"src"
],
"main": "dist/index.common.js",
"unpkg": "dist/index.umd.js",
"jsdelivr": "dist/index.umd.js",
"style": "dist/style.css",
"typings": "types/index.d.ts",
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/runtime": "^7.12.5",
"@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1",
"browserify": "^17.0.0",
"del": "^6.0.0",
"eslint": "^7.13.0",
"eslint-config-prettier": "^6.15.0",
"eslint-config-standard": "^16.0.1",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.2",
"eslint-plugin-typescript": "^0.14.0",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^8.0.0",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.3",
"gulp-sass": "^5.1.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-typescript": "^5.0.1",
"gulp-uglify": "^3.0.2",
"markdown-doctest": "^1.1.0",
"prettier": "^2.1.2",
"sass": "^1.55.0",
"typescript": "^4.6.4",
"vinyl-source-stream": "^2.0.0",
"vue": "^3.4.27",
"vxe-pc-ui": "^4.0.35",
"vxe-table": "^4.7.36"
},
"repository": {
"type": "git",
"url": "git+https://github.com/x-extends/vxe-ui-plugins.git"
},
"keywords": [
"vxe-ui-plugins"
],
"author": {
"name": "Xu Liangzhan",
"email": "xu_liangzhan@163.com"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/x-extends/vxe-ui-plugins/issues"
},
"homepage": "https://github.com/x-extends/vxe-ui-plugins#readme"
}

View File

@ -0,0 +1,164 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions, computed } from 'vue'
import { useWidgetPropDataSource, WidgetDataSourceOptionObjVO } from './use'
import XEUtils from 'xe-utils'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetACheckboxFormObjVO {
options: WidgetDataSourceOptionObjVO[]
}
export function createWidgetACheckbox (VxeUI: VxeUIExport) {
const getWidgetACheckboxConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetACheckboxFormObjVO> => {
return {
title: '复选框',
icon: 'vxe-icon-checkbox-checked',
options: {
options: XEUtils.range(0, 3).map((v, i) => {
return {
value: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.defValue', [i + 1])
}
})
}
}
}
const WidgetACheckboxFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetACheckboxFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
const { renderDataSourceFormItem } = useWidgetPropDataSource(VxeUI, props, false)
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default () {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default () {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue' (val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default () {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue' (val: any) {
widget.required = val
}
})
}
}),
renderDataSourceFormItem()
]
}
})
}
}
})
const WidgetACheckboxViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetACheckboxFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const checkboxOptions = computed(() => {
const { renderParams } = props
const { widget } = renderParams
const { options } = widget
return options.options.map(item => {
return {
label: item.value,
value: item.value
}
})
})
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default () {
return h(resolveComponent('a-checkbox-group') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
options: checkboxOptions.value,
onChange: changeEvent,
'onUpdate:value' (val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetACheckboxConfig,
WidgetACheckboxFormComponent,
WidgetACheckboxViewComponent
}
}

View File

@ -0,0 +1,144 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetADatePickerFormObjVO {
placeholder: string
}
export function createWidgetADatePicker (VxeUI: VxeUIExport) {
const getWidgetADatePickerConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetADatePickerFormObjVO> => {
return {
title: '日期',
icon: 'vxe-icon-input',
options: {
placeholder: ''
}
}
}
const WidgetADatePickerFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetADatePickerFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default () {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default () {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue' (val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'VxeInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default () {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue' (val: any) {
widget.required = val
}
})
}
})
]
}
})
}
}
})
const WidgetADatePickerViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetADatePickerFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
const { options } = widget
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default () {
return h(resolveComponent('a-date-picker') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
placeholder: options.placeholder,
onChange: changeEvent,
'onUpdate:value' (val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetADatePickerConfig,
WidgetADatePickerFormComponent,
WidgetADatePickerViewComponent
}
}

View File

@ -0,0 +1,101 @@
import { h } from 'vue'
import { createWidgetAInput } from './input-widget'
import { createWidgetATextarea } from './textarea-widget'
import { createWidgetAInputNumber } from './number-input-widget'
import { createWidgetADatePicker } from './date-picker-widget'
import { createWidgetASelect } from './select-widget'
import { createWidgetARadio } from './radio-widget'
import { createWidgetACheckbox } from './checkbox-widget'
import { createWidgetASwitch } from './switch-widget'
import type { VxeUIExport } from 'vxe-pc-ui'
/**
* -
*/
export function defineFormDesignRender (VxeUI: VxeUIExport) {
const { getWidgetAInputConfig, WidgetAInputViewComponent, WidgetAInputFormComponent } = createWidgetAInput(VxeUI)
const { getWidgetATextareaConfig, WidgetATextareaViewComponent, WidgetATextareaFormComponent } = createWidgetATextarea(VxeUI)
const { getWidgetAInputNumberConfig, WidgetAInputNumberViewComponent, WidgetAInputNumberFormComponent } = createWidgetAInputNumber(VxeUI)
const { getWidgetADatePickerConfig, WidgetADatePickerViewComponent, WidgetADatePickerFormComponent } = createWidgetADatePicker(VxeUI)
const { getWidgetASelectConfig, WidgetASelectViewComponent, WidgetASelectFormComponent } = createWidgetASelect(VxeUI)
const { getWidgetARadioConfig, WidgetARadioViewComponent, WidgetARadioFormComponent } = createWidgetARadio(VxeUI)
const { getWidgetACheckboxConfig, WidgetACheckboxViewComponent, WidgetACheckboxFormComponent } = createWidgetACheckbox(VxeUI)
const { getWidgetASwitchConfig, WidgetASwitchViewComponent, WidgetASwitchFormComponent } = createWidgetASwitch(VxeUI)
VxeUI.renderer.mixin({
AInputWidget: {
createFormDesignWidgetConfig: getWidgetAInputConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetAInputViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetAInputFormComponent, { renderOpts, renderParams })
}
},
ATextareaWidget: {
createFormDesignWidgetConfig: getWidgetATextareaConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetATextareaViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetATextareaFormComponent, { renderOpts, renderParams })
}
},
ANumberInputWidget: {
createFormDesignWidgetConfig: getWidgetAInputNumberConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetAInputNumberViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetAInputNumberFormComponent, { renderOpts, renderParams })
}
},
ADatePickerWidget: {
createFormDesignWidgetConfig: getWidgetADatePickerConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetADatePickerViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetADatePickerFormComponent, { renderOpts, renderParams })
}
},
ASelectWidget: {
createFormDesignWidgetConfig: getWidgetASelectConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetASelectViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetASelectFormComponent, { renderOpts, renderParams })
}
},
ARadioWidget: {
createFormDesignWidgetConfig: getWidgetARadioConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetARadioViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetARadioFormComponent, { renderOpts, renderParams })
}
},
ACheckboxWidget: {
createFormDesignWidgetConfig: getWidgetACheckboxConfig,
createFormDesignWidgetFieldValue: () => [],
renderFormDesignWidgetView (renderOpts: any, renderParams: any) {
return h(WidgetACheckboxViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts: any, renderParams: any) {
return h(WidgetACheckboxFormComponent, { renderOpts, renderParams })
}
},
ASwitchWidget: {
createFormDesignWidgetConfig: getWidgetASwitchConfig,
renderFormDesignWidgetView (renderOpts, renderParams) {
return h(WidgetASwitchViewComponent, { renderOpts, renderParams })
},
renderFormDesignWidgetFormView (renderOpts, renderParams) {
return h(WidgetASwitchFormComponent, { renderOpts, renderParams })
}
}
})
}

View File

@ -0,0 +1,144 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetAInputFormObjVO {
placeholder: string
}
export function createWidgetAInput(VxeUI: VxeUIExport) {
const getWidgetAInputConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetAInputFormObjVO> => {
return {
title: '输入框',
icon: 'vxe-icon-input',
options: {
placeholder: ''
}
}
}
const WidgetAInputFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetAInputFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
return () => {
const { renderParams } = props
const { widget } = renderParams
const { options } = widget
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: options
}, {
default() {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default() {
return h(VxeUIInputComponent, {
modelValue: widget.title,
placeholder: options.placeholder,
'onUpdate:modelValue'(val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'VxeInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default() {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue'(val: any) {
widget.required = val
}
})
}
})
]
}
})
}
}
})
const WidgetAInputViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetAInputFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default() {
return h(resolveComponent('n-input') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
onChange: changeEvent,
'onUpdate:value'(val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetAInputConfig,
WidgetAInputFormComponent,
WidgetAInputViewComponent
}
}

View File

@ -0,0 +1,144 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetAInputNumberFormObjVO {
placeholder: string
}
export function createWidgetAInputNumber(VxeUI: VxeUIExport) {
const getWidgetAInputNumberConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetAInputNumberFormObjVO> => {
return {
title: '数字',
icon: 'vxe-icon-number',
options: {
placeholder: '请输入'
}
}
}
const WidgetAInputNumberFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetAInputNumberFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default() {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default() {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue'(val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default() {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue'(val: any) {
widget.required = val
}
})
}
})
]
}
})
}
}
})
const WidgetAInputNumberViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetAInputNumberFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
const { options } = widget
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default() {
return h(resolveComponent('n-input-number') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
placeholder: options.placeholder,
onChange: changeEvent,
'onUpdate:value'(val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetAInputNumberConfig,
WidgetAInputNumberFormComponent,
WidgetAInputNumberViewComponent
}
}

View File

@ -0,0 +1,164 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions, computed } from 'vue'
import { useWidgetPropDataSource, WidgetDataSourceOptionObjVO } from './use'
import XEUtils from 'xe-utils'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetARadioFormObjVO {
options: WidgetDataSourceOptionObjVO[]
}
export function createWidgetARadio (VxeUI: VxeUIExport) {
const getWidgetARadioConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetARadioFormObjVO> => {
return {
title: '单选框',
icon: 'vxe-icon-radio-checked',
options: {
options: XEUtils.range(0, 3).map((v, i) => {
return {
value: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.defValue', [i + 1])
}
})
}
}
}
const WidgetARadioFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetARadioFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
const { renderDataSourceFormItem } = useWidgetPropDataSource(VxeUI, props, false)
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default () {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default () {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue' (val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default () {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue' (val: any) {
widget.required = val
}
})
}
}),
renderDataSourceFormItem()
]
}
})
}
}
})
const WidgetARadioViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetARadioFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const radioOptions = computed(() => {
const { renderParams } = props
const { widget } = renderParams
const { options } = widget
return options.options.map(item => {
return {
label: item.value,
value: item.value
}
})
})
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default () {
return h(resolveComponent('a-radio-group') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
options: radioOptions.value,
onChange: changeEvent,
'onUpdate:value' (val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetARadioConfig,
WidgetARadioFormComponent,
WidgetARadioViewComponent
}
}

View File

@ -0,0 +1,156 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import { useWidgetPropDataSource, WidgetDataSourceOptionObjVO } from './use'
import XEUtils from 'xe-utils'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetASelectFormObjVO {
placeholder: string
options: WidgetDataSourceOptionObjVO[]
}
export function createWidgetASelect(VxeUI: VxeUIExport) {
const getWidgetASelectConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetASelectFormObjVO> => {
return {
title: '下拉框',
icon: 'vxe-icon-select',
options: {
placeholder: '请选择',
options: XEUtils.range(0, 3).map((v, i) => {
return {
value: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.defValue', [i + 1])
}
})
}
}
}
const WidgetASelectFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetASelectFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
const { renderDataSourceFormItem } = useWidgetPropDataSource(VxeUI, props, false)
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default() {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default() {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue'(val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default() {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue'(val: any) {
widget.required = val
}
})
}
}),
renderDataSourceFormItem()
]
}
})
}
}
})
const WidgetASelectViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetASelectFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup(props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
const { options } = widget
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default() {
return h(resolveComponent('n-select') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
options: options.options,
placeholder: options.placeholder,
onChange: changeEvent,
'onUpdate:value'(val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetASelectConfig,
WidgetASelectFormComponent,
WidgetASelectViewComponent
}
}

View File

@ -0,0 +1,140 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetASwitchFormObjVO {
}
export function createWidgetASwitch (VxeUI: VxeUIExport) {
const getWidgetASwitchConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetASwitchFormObjVO> => {
return {
title: '是/否',
icon: 'vxe-icon-switch',
options: {
}
}
}
const WidgetASwitchFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetASwitchFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default () {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default () {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue' (val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default () {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue' (val: any) {
widget.required = val
}
})
}
})
]
}
})
}
}
})
const WidgetASwitchViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetASwitchFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default () {
return h(resolveComponent('a-switch') as ComponentOptions, {
checked: $formView ? $formView.getItemValue(widget) : null,
onChange: changeEvent,
'onUpdate:checked' (val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetASwitchConfig,
WidgetASwitchFormComponent,
WidgetASwitchViewComponent
}
}

View File

@ -0,0 +1,148 @@
import { defineComponent, h, PropType, resolveComponent, ComponentOptions } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormComponent, VxeFormItemComponent, VxeSwitchComponent, VxeInputComponent } from 'vxe-pc-ui'
interface WidgetATextareaFormObjVO {
placeholder: string
}
export function createWidgetATextarea (VxeUI: VxeUIExport) {
const getWidgetATextareaConfig = (params: VxeGlobalRendererHandles.CreateFormDesignWidgetConfigParams): VxeGlobalRendererHandles.CreateFormDesignWidgetConfigObj<WidgetATextareaFormObjVO> => {
return {
title: '文本域',
icon: 'vxe-icon-textarea',
options: {
placeholder: '请输入'
}
}
}
const WidgetATextareaFormComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<WidgetATextareaFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormComponent = VxeUI.getComponent<VxeFormComponent>('VxeForm')
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUISwitchComponent = VxeUI.getComponent<VxeSwitchComponent>('VxeSwitch')
const VxeUIInputComponent = VxeUI.getComponent<VxeInputComponent>('VxeInput')
return () => {
const { renderParams } = props
const { widget } = renderParams
return h(VxeUIFormComponent, {
class: 'vxe-form-design--widget-render-form-wrapper',
vertical: true,
span: 24,
titleBold: true,
titleOverflow: true,
data: widget.options
}, {
default () {
return [
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.name')
}, {
default () {
return h(VxeUIInputComponent, {
modelValue: widget.title,
'onUpdate:modelValue' (val: any) {
widget.title = val
}
})
}
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.placeholder'),
field: 'placeholder',
itemRender: { name: 'ElInput' }
}),
h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.required')
}, {
default () {
return h(VxeUISwitchComponent, {
modelValue: widget.required,
'onUpdate:modelValue' (val: any) {
widget.required = val
}
})
}
})
]
}
})
}
}
})
const WidgetATextareaViewComponent = defineComponent({
props: {
renderOpts: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewOptions>,
default: () => ({})
},
renderParams: {
type: Object as PropType<VxeGlobalRendererHandles.RenderFormDesignWidgetViewParams<WidgetATextareaFormObjVO>>,
default: () => ({})
}
},
emits: [],
setup (props) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const changeEvent = () => {
const { renderParams } = props
const { widget, $formView } = renderParams
if ($formView) {
const itemValue = $formView ? $formView.getItemValue(widget) : null
$formView.updateItemStatus(widget, itemValue)
}
}
return () => {
const { renderParams } = props
const { widget, $formView } = renderParams
const { options } = widget
return h(VxeUIFormItemComponent, {
class: ['vxe-form-design--widget-render-form-item'],
field: widget.field,
title: widget.title
}, {
default () {
return h(resolveComponent('a-textarea') as ComponentOptions, {
value: $formView ? $formView.getItemValue(widget) : null,
placeholder: options.placeholder,
autoSize: {
minRows: 2,
maxRows: 4
},
onChange: changeEvent,
'onUpdate:value' (val: any) {
if ($formView) {
$formView.setItemValue(widget, val)
}
}
})
}
})
}
}
})
return {
getWidgetATextareaConfig,
WidgetATextareaFormComponent,
WidgetATextareaViewComponent
}
}

View File

@ -0,0 +1,255 @@
import { VNode, h, onMounted, ref, watch } from 'vue'
import type { VxeUIExport, VxeGlobalRendererHandles, VxeFormItemComponent, VxeButtonComponent, VxeTextareaComponent, VxeTipComponent } from 'vxe-pc-ui'
export interface WidgetDataSourceOptionSubObjVO {
value: string,
}
export interface WidgetDataSourceOptionObjVO {
value: string,
options?: WidgetDataSourceOptionSubObjVO[]
}
export function useWidgetPropDataSource (VxeUI: VxeUIExport, props: {
readonly renderOpts: VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewOptions;
readonly renderParams: VxeGlobalRendererHandles.RenderFormDesignWidgetFormViewParams<{
options: WidgetDataSourceOptionObjVO[]
}>;
}, isSubOption: boolean) {
const VxeUIFormItemComponent = VxeUI.getComponent<VxeFormItemComponent>('VxeFormItem')
const VxeUIButtonComponent = VxeUI.getComponent<VxeButtonComponent>('VxeButton')
const VxeUITextareaComponent = VxeUI.getComponent<VxeTextareaComponent>('VxeTextarea')
const VxeUITipComponent = VxeUI.getComponent<VxeTipComponent>('VxeTip')
const optionsContent = ref('')
const expandIndexList = ref<number[]>([])
const addOptionEvent = () => {
const { renderParams } = props
const { widget } = renderParams
const options = widget.options.options || []
options.push({
value: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.defValue', [options.length + 1])
})
widget.options.options = [...options]
}
const subRE = /^(\s|\t)+/
const hasSubOption = (str: string) => {
return subRE.test(str)
}
const expandAllOption = () => {
const { renderParams } = props
const { widget } = renderParams
const options = widget.options.options || []
const indexList: number[] = []
options.forEach((group, gIndex) => {
const { options } = group
if (options && options.length) {
indexList.push(gIndex)
}
})
expandIndexList.value = indexList
}
const toggleExpandOption = (item: WidgetDataSourceOptionSubObjVO, gIndex: number) => {
if (expandIndexList.value.includes(gIndex)) {
expandIndexList.value = expandIndexList.value.filter(num => num !== gIndex)
} else {
expandIndexList.value.push(gIndex)
}
}
const confirmBatchAddOptionEvent = () => {
const { renderParams } = props
const { widget } = renderParams
const optList: WidgetDataSourceOptionSubObjVO[] = []
const rowList = optionsContent.value.split('\n')
let prevGroup: Required<WidgetDataSourceOptionObjVO> | null = null
if (isSubOption) {
rowList.forEach((str, index) => {
const nextStr = rowList[index + 1]
const value = str.trim()
if (!value) {
return
}
const item: WidgetDataSourceOptionSubObjVO = {
value
}
if (prevGroup) {
if (hasSubOption(str)) {
prevGroup.options.push(item)
return
}
prevGroup = null
optList.push(item)
} else {
optList.push(item)
}
if (nextStr) {
if (hasSubOption(nextStr)) {
prevGroup = Object.assign(item, { options: [] })
}
}
})
} else {
rowList.forEach((str) => {
optList.push({
value: str.trim()
})
})
}
widget.options.options = optList
expandAllOption()
}
const openPopupEditEvent = () => {
const { renderParams } = props
const { widget } = renderParams
const contList: string[] = []
widget.options.options?.forEach(group => {
contList.push(group.value)
group.options?.forEach(item => {
contList.push(`\t${item.value}`)
})
})
optionsContent.value = contList.join('\n')
VxeUI.modal.open({
title: `${widget.title} - ${VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.batchEditOption')}`,
width: 500,
height: '50vh ',
resize: true,
showFooter: true,
showCancelButton: true,
showConfirmButton: true,
confirmButtonText: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.buildOption'),
onConfirm: confirmBatchAddOptionEvent,
slots: {
default () {
return h('div', {
class: 'vxe-form-design--widget-form-item-data-source-popup'
}, [
h(VxeUITipComponent, {
status: 'primary',
title: '',
content: VxeUI.getI18n(`vxe.formDesign.widgetProp.dataSource.${isSubOption ? 'batchEditSubTip' : 'batchEditTip'}`)
}),
h(VxeUITextareaComponent, {
resize: 'none',
modelValue: optionsContent.value,
'onUpdate:modelValue' (val: any) {
optionsContent.value = val
}
})
])
}
}
})
}
const renderOption = (item: WidgetDataSourceOptionSubObjVO, hasFirstLevel: boolean, isExpand: boolean, gIndex: number, hasSub: boolean, isFirst: boolean, isLast: boolean) => {
return h('div', {
class: ['vxe-form-design--widget-form-item-data-source-option', {
'is--first': isFirst,
'is--last': isLast
}]
}, [
h('div', {
class: 'vxe-form-design--widget-expand-btn'
}, hasFirstLevel && hasSub
? [
h('i', {
class: isExpand ? VxeUI.getIcon().FORM_DESIGN_WIDGET_OPTION_EXPAND_CLOSE : VxeUI.getIcon().FORM_DESIGN_WIDGET_OPTION_EXPAND_OPEN,
onClick () {
toggleExpandOption(item, gIndex)
}
})
]
: []),
h('input', {
class: 'vxe-default-input',
value: item.value,
onInput (evnt: InputEvent & { target: HTMLInputElement }) {
item.value = evnt.target.value
}
}),
h(VxeUIButtonComponent, {
status: 'danger',
mode: 'text',
icon: VxeUI.getIcon().FORM_DESIGN_WIDGET_DELETE
})
])
}
const renderOptions = () => {
const { renderParams } = props
const { widget } = renderParams
const { options } = widget
const groups = options.options
const optVNs: VNode[] = []
if (groups) {
groups.forEach((group, gIndex) => {
const { options } = group
const isExpand = expandIndexList.value.includes(gIndex)
if (options && options.length) {
optVNs.push(renderOption(group, true, isExpand, gIndex, true, gIndex === 0, gIndex === groups.length - 1))
if (isExpand) {
optVNs.push(
h('div', {
class: 'vxe-form-design--widget-form-item-data-source-sub-option'
}, options.map(item => renderOption(item, false, isExpand, 0, false, false, false)))
)
}
} else {
optVNs.push(renderOption(group, true, isExpand, gIndex, false, gIndex === 0, gIndex === groups.length - 1))
}
})
}
return optVNs
}
watch(() => props.renderParams.widget, () => {
expandAllOption()
})
onMounted(() => {
expandAllOption()
})
return {
renderDataSourceFormItem () {
return h(VxeUIFormItemComponent, {
title: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.name'),
field: 'options'
}, {
default () {
return [
h('div', {}, [
h(VxeUIButtonComponent, {
status: 'primary',
mode: 'text',
content: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.addOption'),
onClick: addOptionEvent
}),
h(VxeUIButtonComponent, {
status: 'primary',
mode: 'text',
content: VxeUI.getI18n('vxe.formDesign.widgetProp.dataSource.batchEditOption'),
onClick: openPopupEditEvent
})
]),
h('div', {
class: 'vxe-form-design--widget-form-item-data-source'
}, renderOptions())
]
}
})
}
}
}

View File

@ -0,0 +1,244 @@
import { h, resolveComponent, ComponentOptions } from 'vue'
import XEUtils from 'xe-utils'
import type { VxeUIExport, VxeGlobalRendererHandles, FormItemContentRenderParams } from 'vxe-pc-ui'
/**
* -
*/
export function defineFormRender(VxeUI: VxeUIExport) {
function isEmptyValue(cellValue: any) {
return cellValue === null || cellValue === undefined || cellValue === ''
}
function getOnName(type: string) {
return 'on' + type.substring(0, 1).toLocaleUpperCase() + type.substring(1)
}
function getModelProp(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let prop = 'value'
switch (renderOpts.name) {
case 'ASwitch':
prop = 'checked'
break
}
return prop
}
function getModelEvent(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let type = 'update:value'
switch (renderOpts.name) {
case 'ASwitch':
type = 'update:checked'
break
}
return type
}
function getChangeEvent(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
return 'change'
}
function getItemProps(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: FormItemContentRenderParams, value: any, defaultProps?: { [prop: string]: any }) {
return XEUtils.assign({}, defaultProps, renderOpts.props, { [getModelProp(renderOpts)]: value })
}
function formatText(cellValue: any) {
return '' + (isEmptyValue(cellValue) ? '' : cellValue)
}
function getOns(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderParams, inputFunc?: Function, changeFunc?: Function) {
const { events } = renderOpts
const modelEvent = getModelEvent(renderOpts)
const changeEvent = getChangeEvent(renderOpts)
const isSameEvent = changeEvent === modelEvent
const ons: { [type: string]: Function } = {}
XEUtils.objectEach(events, (func: Function, key: string) => {
ons[getOnName(key)] = function (...args: any[]) {
func(params, ...args)
}
})
if (inputFunc) {
ons[getOnName(modelEvent)] = function (targetEvnt: any) {
inputFunc(targetEvnt)
if (events && events[modelEvent]) {
events[modelEvent](params, targetEvnt)
}
if (isSameEvent && changeFunc) {
changeFunc(targetEvnt)
}
}
}
if (!isSameEvent && changeFunc) {
ons[getOnName(changeEvent)] = function (...args: any[]) {
changeFunc(...args)
if (events && events[changeEvent]) {
events[changeEvent](params, ...args)
}
}
}
return ons
}
function getItemOns(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: FormItemContentRenderParams) {
const { $form, data, field } = params
return getOns(renderOpts, params, (value: any) => {
// 处理 model 值双向绑定
XEUtils.set(data, field, value)
}, () => {
// 处理 change 事件相关逻辑
$form.updateStatus(params)
})
}
function cellText(cellValue: any): string[] {
return [formatText(cellValue)]
}
function createFormItemRender(defaultProps?: { [key: string]: any }) {
return function (renderOpts: VxeGlobalRendererHandles.RenderItemContentOptions & { name: string }, params: FormItemContentRenderParams) {
const { data, field } = params
const { name } = renderOpts
const { attrs } = renderOpts
const itemValue = XEUtils.get(data, field)
return [
h(resolveComponent(name), {
...attrs,
...getItemProps(renderOpts, params, itemValue, defaultProps),
...getItemOns(renderOpts, params)
})
]
}
}
function defaultButtonItemRender(renderOpts: VxeGlobalRendererHandles.RenderItemContentOptions, params: FormItemContentRenderParams) {
const { attrs } = renderOpts
const props = getItemProps(renderOpts, params, null)
return [
h(resolveComponent('a-button') as ComponentOptions, {
...attrs,
...props,
...getItemOns(renderOpts, params)
}, {
default: () => cellText(renderOpts.content || props.content)
})
]
}
function defaultButtonsItemRender(renderOpts: VxeGlobalRendererHandles.RenderItemContentOptions, params: FormItemContentRenderParams) {
const { children } = renderOpts
if (children) {
return children.map((childRenderOpts: VxeGlobalRendererHandles.RenderItemContentOptions) => defaultButtonItemRender(childRenderOpts, params)[0])
}
return []
}
function createFormItemRadioAndCheckboxRender() {
return function (renderOpts: VxeGlobalRendererHandles.RenderItemContentOptions & { name: string }, params: FormItemContentRenderParams) {
const { name, options = [], optionProps = {} } = renderOpts
const { data, field } = params
const { attrs } = renderOpts
const labelProp = optionProps.label || 'label'
const valueProp = optionProps.value || 'value'
const itemValue = XEUtils.get(data, field)
return [
h(resolveComponent(`${name}Group`) as ComponentOptions, {
...attrs,
...getItemProps(renderOpts, params, itemValue),
...getItemOns(renderOpts, params)
}, {
default: () => {
return options.map((option, oIndex) => {
return h(resolveComponent(name) as ComponentOptions, {
key: oIndex,
value: option[valueProp],
disabled: option.disabled
}, {
default: () => cellText(option[labelProp])
})
})
}
})
]
}
}
VxeUI.renderer.mixin({
AAutoComplete: {
renderFormItemContent: createFormItemRender()
},
AInput: {
renderFormItemContent: createFormItemRender()
},
AInputNumber: {
renderFormItemContent: createFormItemRender()
},
ASelect: {
renderFormItemContent(renderOpts, params) {
const { options = [], optionGroups } = renderOpts
const { data, field } = params
const { attrs } = renderOpts
const itemValue = XEUtils.get(data, field)
const props = getItemProps(renderOpts, params, itemValue)
const ons = getItemOns(renderOpts, params)
if (optionGroups) {
return [
h(resolveComponent('n-select') as ComponentOptions, {
...attrs,
...props,
options: optionGroups,
...ons
})
]
}
return [
h(resolveComponent('n-select') as ComponentOptions, {
...attrs,
...props,
options: props.options || options,
...ons
})
]
}
},
ACascader: {
renderFormItemContent: createFormItemRender()
},
ADatePicker: {
renderFormItemContent: createFormItemRender()
},
AMonthPicker: {
renderFormItemContent: createFormItemRender()
},
ARangePicker: {
renderFormItemContent: createFormItemRender()
},
AWeekPicker: {
renderFormItemContent: createFormItemRender()
},
ATimePicker: {
renderFormItemContent: createFormItemRender()
},
ATreeSelect: {
renderFormItemContent: createFormItemRender()
},
ARate: {
renderFormItemContent: createFormItemRender()
},
ASwitch: {
renderFormItemContent: createFormItemRender()
},
ARadio: {
renderFormItemContent: createFormItemRadioAndCheckboxRender()
},
ACheckbox: {
renderFormItemContent: createFormItemRadioAndCheckboxRender()
},
AButton: {
renderFormItemContent: defaultButtonItemRender
},
AButtons: {
renderFormItemContent: defaultButtonsItemRender
}
})
}

View File

@ -0,0 +1,71 @@
import { defineTableRender } from './table'
import { defineFormRender } from './form'
import { defineFormDesignRender } from './form-design'
import type { VxeUIPluginObject, VxeGlobalInterceptorHandles } from 'vxe-pc-ui'
/**
*
*/
function getEventTargetNode(evnt: any, container: HTMLElement, className: string) {
let targetElem
let target = evnt.target
while (target && target.nodeType && target !== document) {
if (className && target.className && target.className.split && target.className.split(' ').indexOf(className) > -1) {
targetElem = target
} else if (target === container) {
return { flag: className ? !!targetElem : true, container, targetElem: targetElem }
}
target = target.parentNode
}
return { flag: false }
}
/**
*
*/
function handleClearEvent(params: VxeGlobalInterceptorHandles.InterceptorClearFilterParams | VxeGlobalInterceptorHandles.InterceptorClearEditParams | VxeGlobalInterceptorHandles.InterceptorClearAreasParams) {
const { $event } = params
const bodyElem = document.body
if (
// 下拉框
getEventTargetNode($event, bodyElem, 'ant-select-dropdown').flag ||
// 级联
getEventTargetNode($event, bodyElem, 'ant-cascader-menus').flag ||
// 日期
getEventTargetNode($event, bodyElem, 'ant-picker-dropdown').flag ||
getEventTargetNode($event, bodyElem, 'ant-calendar-picker-container').flag ||
// 时间选择
getEventTargetNode($event, bodyElem, 'ant-time-picker-panel').flag
) {
return false
}
}
export const VxeUIPluginRenderNaive: VxeUIPluginObject = {
install(VxeUI, options?: {
Naive?: any
}) {
// 检查版本
if (!/^(4)\./.test(VxeUI.uiVersion)) {
console.error('[plugin-render-naive 4.x] Version 4.x is required')
}
defineTableRender(VxeUI)
defineFormRender(VxeUI)
defineFormDesignRender(VxeUI)
VxeUI.interceptor.add('event.clearFilter', handleClearEvent)
VxeUI.interceptor.add('event.clearEdit', handleClearEvent)
VxeUI.interceptor.add('event.clearAreas', handleClearEvent)
// 兼容老版本
VxeUI.interceptor.add('event.clearActived', handleClearEvent)
}
}
if (typeof window !== 'undefined' && window.VxeUI && window.VxeUI.use) {
window.VxeUI.use(VxeUIPluginRenderNaive)
}
export default VxeUIPluginRenderNaive

View File

@ -0,0 +1,547 @@
import { h, resolveComponent, ComponentOptions } from 'vue'
import XEUtils from 'xe-utils'
import type { VxeUIExport, VxeTableDefines, VxeColumnPropTypes, VxeGlobalRendererHandles } from 'vxe-pc-ui'
/**
* -
*/
export function defineTableRender(VxeUI: VxeUIExport) {
function isEmptyValue(cellValue: any) {
return cellValue === null || cellValue === undefined || cellValue === ''
}
function getOnName(type: string) {
return 'on' + type.substring(0, 1).toLocaleUpperCase() + type.substring(1)
}
function getModelProp(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let prop = 'value'
switch (renderOpts.name) {
case 'ASwitch':
prop = 'checked'
break
}
return prop
}
function getModelEvent(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let type = 'update:value'
switch (renderOpts.name) {
case 'ASwitch':
type = 'update:checked'
break
}
return type
}
function dateFormatToVxeFormat(format: string) {
if (format) {
return `${format}`.replace('YYYY', 'yyyy').replace('DD', 'dd')
}
return format
}
function getChangeEvent(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
return 'change'
}
function getCellEditFilterProps(renderOpts: any, params: VxeGlobalRendererHandles.RenderEditParams | VxeGlobalRendererHandles.RenderFilterParams, value: any, defaultProps?: { [prop: string]: any }) {
return XEUtils.assign({}, defaultProps, renderOpts.props, { [getModelProp(renderOpts)]: value })
}
function formatText(cellValue: any) {
return '' + (isEmptyValue(cellValue) ? '' : cellValue)
}
function getCellLabelVNs(renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderCellParams, cellLabel: any) {
const { placeholder } = renderOpts
return [
h('span', {
class: 'vxe-cell--label'
}, placeholder && isEmptyValue(cellLabel)
? [
h('span', {
class: 'vxe-cell--placeholder'
}, formatText(placeholder))
]
: formatText(cellLabel))
]
}
function getOns(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderParams, inputFunc?: Function, changeFunc?: Function) {
const { events } = renderOpts
const modelEvent = getModelEvent(renderOpts)
const changeEvent = getChangeEvent(renderOpts)
const isSameEvent = changeEvent === modelEvent
const ons: { [type: string]: Function } = {}
XEUtils.objectEach(events, (func: Function, key: string) => {
ons[getOnName(key)] = function (...args: any[]) {
func(params, ...args)
}
})
if (inputFunc) {
ons[getOnName(modelEvent)] = function (targetEvnt: any) {
inputFunc(targetEvnt)
if (events && events[modelEvent]) {
events[modelEvent](params, targetEvnt)
}
if (isSameEvent && changeFunc) {
changeFunc(targetEvnt)
}
}
}
if (!isSameEvent && changeFunc) {
ons[getOnName(changeEvent)] = function (...args: any[]) {
changeFunc(...args)
if (events && events[changeEvent]) {
events[changeEvent](params, ...args)
}
}
}
return ons
}
function getEditOns(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderEditParams) {
const { $table, row, column } = params
return getOns(renderOpts, params, (value: any) => {
// 处理 model 值双向绑定
XEUtils.set(row, column.field, value)
}, () => {
// 处理 change 事件相关逻辑
$table.updateStatus(params)
})
}
function getFilterOns(renderOpts: any, params: VxeGlobalRendererHandles.RenderFilterParams, option: VxeTableDefines.FilterOption, changeFunc: Function) {
return getOns(renderOpts, params, (value: any) => {
// 处理 model 值双向绑定
option.data = value
}, changeFunc)
}
function matchCascaderData(index: number, list: any[], values: any[], labels: any[]) {
const val = values[index]
if (list && values.length > index) {
XEUtils.each(list, (item) => {
if (item.value === val) {
labels.push(item.label)
matchCascaderData(++index, item.children, values, labels)
}
})
}
}
function formatDatePicker(defaultFormat?: string) {
return function (renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderCellParams) {
return getCellLabelVNs(renderOpts, params, getDatePickerCellValue(renderOpts, params, defaultFormat))
}
}
function formatTimePicker(defaultFormat?: string) {
return function (renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderCellParams) {
const { props = {} } = renderOpts
const { row, column } = params
let cellValue = XEUtils.get(row, column.field)
try {
if (cellValue) {
if (!XEUtils.isString(cellValue)) {
cellValue = cellValue.format ? cellValue.format(props.format || props.valueFormat || defaultFormat) : XEUtils.toDateString(cellValue, dateFormatToVxeFormat(props.format || props.valueFormat || defaultFormat))
}
}
} catch (e) { }
return getCellLabelVNs(renderOpts, params, cellValue)
}
}
function getSelectCellValue(renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderCellParams) {
const { options = [], optionGroups, props = {}, optionProps = {}, optionGroupProps = {} } = renderOpts
const { row, column } = params
const labelProp = optionProps.label || 'label'
const valueProp = optionProps.value || 'value'
const groupOptions = optionGroupProps.options || 'options'
const cellValue = XEUtils.get(row, column.field)
if (!isEmptyValue(cellValue)) {
return XEUtils.map(props.mode === 'multiple' ? cellValue : [cellValue], optionGroups
? (value) => {
let selectItem
for (let index = 0; index < optionGroups.length; index++) {
selectItem = XEUtils.find(optionGroups[index][groupOptions], (item) => item[valueProp] === value)
if (selectItem) {
break
}
}
return selectItem ? selectItem[labelProp] : value
}
: (value) => {
const selectItem = XEUtils.find(options, (item) => item[valueProp] === value)
return selectItem ? selectItem[labelProp] : value
}).join(', ')
}
return ''
}
function getCascaderCellValue(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams) {
const { props = {} } = renderOpts
const { row, column } = params
const cellValue = XEUtils.get(row, column.field)
const values = cellValue || []
const labels: Array<any> = []
matchCascaderData(0, props.options, values, labels)
return (props.showAllLevels === false ? labels.slice(labels.length - 1, labels.length) : labels).join(` ${props.separator || '/'} `)
}
function getRangePickerCellValue(renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.RenderEditParams) {
const { props = {} } = renderOpts
const { row, column } = params
let cellValue = XEUtils.get(row, column.field)
if (cellValue) {
cellValue = XEUtils.map(cellValue, (date: any) => {
return date && date.format ? date.format(props.format || 'YYYY-MM-DD') : XEUtils.toDateString(date, dateFormatToVxeFormat(props.format || 'YYYY-MM-DD'))
}).join(' ~ ')
}
return cellValue
}
function getTreeSelectCellValue(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.RenderEditParams) {
const { props = {} } = renderOpts
const { treeData, treeCheckable } = props
const { row, column } = params
const cellValue = XEUtils.get(row, column.field)
if (!isEmptyValue(cellValue)) {
return XEUtils.map(treeCheckable ? cellValue : [cellValue], (value) => {
const matchObj = XEUtils.findTree(treeData, (item: any) => item.value === value, { children: 'children' })
return matchObj ? matchObj.item.title : value
}).join(', ')
}
return cellValue
}
function getDatePickerCellValue(renderOpts: VxeGlobalRendererHandles.RenderOptions, params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams, defaultFormat?: string) {
const { props = {} } = renderOpts
const { row, column } = params
let cellValue = XEUtils.get(row, column.field)
try {
if (cellValue) {
if (!defaultFormat) {
if (renderOpts.name === 'ADatePicker') {
switch (props.picker) {
case 'week':
defaultFormat = 'YYYY-WW周'
break
case 'month':
defaultFormat = 'YYYY-MM'
break
case 'year':
defaultFormat = 'YYYY'
break
default:
defaultFormat = props.showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'
break
}
}
}
cellValue = cellValue.format ? cellValue.format(props.format || defaultFormat) : XEUtils.toDateString(cellValue, dateFormatToVxeFormat(props.format || defaultFormat))
}
} catch (e) { }
return cellValue
}
function createEditRender(defaultProps?: { [key: string]: any }) {
return function (renderOpts: VxeColumnPropTypes.EditRender & { name: string }, params: VxeGlobalRendererHandles.RenderEditParams) {
debugger
const { row, column } = params
const { name, attrs } = renderOpts
console.log(params)
console.log(renderOpts)
const cellValue = XEUtils.get(row, column.field)
return [
h(resolveComponent(name), {
...attrs,
...getCellEditFilterProps(renderOpts, params, cellValue, defaultProps),
...getEditOns(renderOpts, params)
})
]
}
}
function defaultButtonEditRender(renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderEditParams) {
const { attrs } = renderOpts
return [
h(resolveComponent('n-button'), {
...attrs,
...getCellEditFilterProps(renderOpts, params, null),
...getOns(renderOpts, params)
}, cellText(renderOpts.content))
]
}
function defaultButtonsEditRender(renderOpts: VxeColumnPropTypes.EditRender, params: VxeGlobalRendererHandles.RenderEditParams) {
const { children } = renderOpts
if (children) {
return children.map((childRenderOpts: VxeColumnPropTypes.EditRender) => defaultButtonEditRender(childRenderOpts, params)[0])
}
return []
}
function createFilterRender(defaultProps?: { [key: string]: any }) {
return function (renderOpts: VxeColumnPropTypes.FilterRender & { name: string }, params: VxeGlobalRendererHandles.RenderFilterParams) {
const { column } = params
const { name, attrs } = renderOpts
return [
h('div', {
class: 'vxe-table--filter-naive-wrapper'
}, column.filters.map((option, oIndex) => {
const optionValue = option.data
return h(resolveComponent(name), {
key: oIndex,
...attrs,
...getCellEditFilterProps(renderOpts, params, optionValue, defaultProps),
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, !!option.data, option)
})
})
}))
]
}
}
function handleConfirmFilter(params: VxeGlobalRendererHandles.RenderFilterParams, checked: boolean, option: VxeTableDefines.FilterOption) {
const { $panel } = params
$panel.changeOption(null, checked, option)
}
/**
*
* @param params
*/
function defaultFuzzyFilterMethod(params: VxeGlobalRendererHandles.FilterMethodParams) {
const { option, row, column } = params
const { data } = option
const cellValue = XEUtils.get(row, column.field)
return XEUtils.toValueString(cellValue).indexOf(data) > -1
}
/**
*
* @param params
*/
function defaultExactFilterMethod(params: VxeGlobalRendererHandles.FilterMethodParams) {
const { option, row, column } = params
const { data } = option
const cellValue = XEUtils.get(row, column.field)
/* eslint-disable eqeqeq */
return cellValue === data
}
function cellText(cellValue: any): string[] {
return [formatText(cellValue)]
}
function createDatePickerExportMethod(defaultFormat?: string) {
return function (params: VxeGlobalRendererHandles.ExportMethodParams) {
const { row, column, options } = params
return options && options.original ? XEUtils.get(row, column.field) : getDatePickerCellValue(column.editRender || column.cellRender, params, defaultFormat)
}
}
function createExportMethod(getExportCellValue: Function) {
return function (params: VxeGlobalRendererHandles.ExportMethodParams) {
const { row, column, options } = params
return options && options.original ? XEUtils.get(row, column.field) : getExportCellValue(column.editRender || column.cellRender, params)
}
}
VxeUI.renderer.mixin({
AAutoComplete: {
tableAutoFocus: 'input',
renderTableDefault: createEditRender(),
renderTableEdit: createEditRender(),
renderTableFilter: createFilterRender(),
tableFilterDefaultMethod: defaultExactFilterMethod
},
NInput: {
tableAutoFocus: 'input',
renderTableDefault: createEditRender(),
renderTableEdit: createEditRender(),
renderTableFilter: createFilterRender(),
tableFilterDefaultMethod: defaultFuzzyFilterMethod
},
NInputNumber: {
tableAutoFocus: 'input',
renderTableDefault: createEditRender(),
renderTableEdit: createEditRender(),
renderTableFilter: createFilterRender(),
tableFilterDefaultMethod: defaultFuzzyFilterMethod
},
NSelect: {
renderTableEdit(renderOpts, params) {
const { options, optionGroups } = renderOpts
const { row, column } = params
const { attrs } = renderOpts
const cellValue = XEUtils.get(row, column.field)
const props = getCellEditFilterProps(renderOpts, params, cellValue)
const ons = getEditOns(renderOpts, params)
if (optionGroups) {
return [
h(resolveComponent('n-select') as ComponentOptions, {
...props,
...attrs,
options: optionGroups,
...ons
})
]
}
return [
h(resolveComponent('n-select') as ComponentOptions, {
...props,
...attrs,
options: props.options || options,
...ons
})
]
},
renderTableCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getSelectCellValue(renderOpts, params))
},
renderTableFilter(renderOpts, params) {
debugger
const { options = [], optionGroups, optionGroupProps = {} } = renderOpts
const groupOptions = optionGroupProps.options || 'options'
const { column } = params
const { attrs } = renderOpts
return [
h('div', {
class: 'vxe-table--filter-naive-wrapper'
}, optionGroups
? column.filters.map((option, oIndex) => {
const optionValue = option.data
const props = getCellEditFilterProps(renderOpts, params, optionValue)
return h(resolveComponent('n-select') as ComponentOptions, {
key: oIndex,
...attrs,
...props,
options: groupOptions,
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, props.mode === 'multiple' ? (option.data && option.data.length > 0) : !XEUtils.eqNull(option.data), option)
})
})
})
: column.filters.map((option, oIndex) => {
const optionValue = option.data
const props = getCellEditFilterProps(renderOpts, params, optionValue)
return h(resolveComponent('n-select') as ComponentOptions, {
key: oIndex,
...attrs,
...props,
options: props.options || options,
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, props.mode === 'multiple' ? (option.data && option.data.length > 0) : !XEUtils.eqNull(option.data), option)
})
})
}))
]
},
tableFilterDefaultMethod(params) {
const { option, row, column } = params
const { data } = option
const { field, filterRender: renderOpts } = column
const { props = {} } = renderOpts
const cellValue = XEUtils.get(row, field)
if (props.mode === 'multiple') {
if (XEUtils.isArray(cellValue)) {
return XEUtils.includeArrays(cellValue, data)
}
return data.indexOf(cellValue) > -1
}
/* eslint-disable eqeqeq */
return cellValue == data
},
tableExportMethod: createExportMethod(getSelectCellValue)
},
ACascader: {
renderTableEdit: createEditRender(),
renderTableCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getCascaderCellValue(renderOpts, params))
},
tableExportMethod: createExportMethod(getCascaderCellValue)
},
ADatePicker: {
renderTableEdit: createEditRender(),
renderTableCell: formatDatePicker(),
tableExportMethod: createDatePickerExportMethod()
},
AMonthPicker: {
renderTableEdit: createEditRender(),
renderTableCell: formatDatePicker('YYYY-MM'),
tableExportMethod: createDatePickerExportMethod('YYYY-MM')
},
ARangePicker: {
renderTableEdit: createEditRender(),
renderTableCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getRangePickerCellValue(renderOpts, params))
},
tableExportMethod: createExportMethod(getRangePickerCellValue)
},
AWeekPicker: {
renderTableEdit: createEditRender(),
renderTableCell: formatDatePicker('YYYY-WW周'),
tableExportMethod: createDatePickerExportMethod('YYYY-WW周')
},
ATimePicker: {
renderTableEdit: createEditRender(),
renderTableCell: formatTimePicker('HH:mm:ss'),
tableExportMethod: createDatePickerExportMethod('HH:mm:ss')
},
ATreeSelect: {
renderTableEdit: createEditRender(),
renderTableCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getTreeSelectCellValue(renderOpts, params))
},
tableExportMethod: createExportMethod(getTreeSelectCellValue)
},
ARate: {
renderTableDefault: createEditRender(),
renderTableEdit: createEditRender(),
renderTableFilter: createFilterRender(),
tableFilterDefaultMethod: defaultExactFilterMethod
},
ASwitch: {
renderTableDefault: createEditRender(),
renderTableEdit: createEditRender(),
renderTableFilter(renderOpts, params) {
const { column } = params
const { name, attrs } = renderOpts
return [
h('div', {
class: 'vxe-table--filter-naive-wrapper'
}, column.filters.map((option, oIndex) => {
const optionValue = option.data
return h(name as string, {
key: oIndex,
...attrs,
...getCellEditFilterProps(renderOpts, params, optionValue),
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, XEUtils.isBoolean(option.data), option)
})
})
}))
]
},
tableFilterDefaultMethod: defaultExactFilterMethod
},
AButton: {
renderTableEdit: defaultButtonEditRender,
renderTableDefault: defaultButtonEditRender
},
AButtons: {
renderTableEdit: defaultButtonsEditRender,
renderTableDefault: defaultButtonsEditRender
}
})
}

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