diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000000..f831890c41
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,138 @@
+{
+ "projectName": "vue-devui",
+ "projectOwner": "DevCloudFE",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": true,
+ "commitConvention": "gitmoji",
+ "contributors": [
+ {
+ "login": "kagol",
+ "name": "Kagol",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9566362?v=4",
+ "profile": "https://juejin.cn/user/712139267650141",
+ "contributions": [
+ "maintenance",
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "TinsFox",
+ "name": "TinsFox",
+ "avatar_url": "https://avatars.githubusercontent.com/u/33956589?v=4",
+ "profile": "https://github.com/TinsFox",
+ "contributions": [
+ "maintenance",
+ "infra"
+ ]
+ },
+ {
+ "login": "lnzhangsong",
+ "name": "nif",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15092594?v=4",
+ "profile": "https://github.com/lnzhangsong",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Zcating",
+ "name": "Zcating",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13329558?v=4",
+ "profile": "https://github.com/Zcating",
+ "contributions": [
+ "maintenance",
+ "code"
+ ]
+ },
+ {
+ "login": "sufuwang",
+ "name": "王凯",
+ "avatar_url": "https://avatars.githubusercontent.com/u/46395105?v=4",
+ "profile": "https://github.com/sufuwang",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "iel-h",
+ "name": "iel",
+ "avatar_url": "https://avatars.githubusercontent.com/u/53589602?v=4",
+ "profile": "https://github.com/iel-h",
+ "contributions": [
+ "maintenance",
+ "code"
+ ]
+ },
+ {
+ "login": "chenxi24",
+ "name": "chenxi24",
+ "avatar_url": "https://avatars.githubusercontent.com/u/40349890?v=4",
+ "profile": "https://github.com/chenxi24",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "asdlml6",
+ "name": "小九九",
+ "avatar_url": "https://avatars.githubusercontent.com/u/61737780?v=4",
+ "profile": "https://github.com/asdlml6",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "AlanLee97",
+ "name": "AlanLee",
+ "avatar_url": "https://avatars.githubusercontent.com/u/42601044?v=4",
+ "profile": "http://blog.alanlee.top",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ForeseeBear",
+ "name": "Echo",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15258339?v=4",
+ "profile": "https://github.com/ForeseeBear",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "GaoNeng-wWw",
+ "name": "GaoNeng",
+ "avatar_url": "https://avatars.githubusercontent.com/u/31283122?v=4",
+ "profile": "https://github.com/GaoNeng-wWw",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "xingyan95",
+ "name": "行言",
+ "avatar_url": "https://avatars.githubusercontent.com/u/11143986?v=4",
+ "profile": "https://github.com/xingyan95",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "devin974",
+ "name": "devin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/67035714?v=4",
+ "profile": "https://devin974.github.io/",
+ "contributions": [
+ "code"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "skipCi": true
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..af30b8a0a8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line-length = off
+trim_trailing_whitespace = false
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..8c5f27c5df
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,113 @@
+module.exports = {
+ root: true,
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 6,
+ ecmaFeatures: {
+ jsx: true,
+ tsx: true,
+ },
+ },
+ env: {
+ browser: true,
+ node: true,
+ jest: true,
+ es6: true,
+ },
+ plugins: ['@typescript-eslint'],
+ extends: [
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:vue/vue3-recommended',
+ 'plugin:import/recommended',
+ 'plugin:import/typescript',
+ ],
+ rules: {
+ 'no-useless-constructor': 'off',
+ 'no-useless-concat': 'off',
+ 'max-params': 'off',
+ '@typescript-eslint/no-useless-constructor': 'off',
+ '@typescript-eslint/no-parameter-properties': 'off',
+ '@typescript-eslint/no-require-imports': 'off',
+ '@typescript-eslint/no-var-requires': 'off',
+ complexity: [
+ 'error',
+ {
+ max: 40,
+ },
+ ],
+ curly: 'error',
+ 'eol-last': 'error',
+ eqeqeq: ['error', 'smart'],
+ 'max-len': ['error', { code: 140 }],
+ 'no-console': [
+ 'error',
+ {
+ allow: [
+ 'log',
+ 'warn',
+ 'dir',
+ 'timeLog',
+ 'assert',
+ 'clear',
+ 'count',
+ 'countReset',
+ 'group',
+ 'groupEnd',
+ 'table',
+ 'dirxml',
+ 'error',
+ 'groupCollapsed',
+ 'Console',
+ 'profile',
+ 'profileEnd',
+ 'timeStamp',
+ 'context',
+ ],
+ },
+ ],
+ 'no-multiple-empty-lines': 'error',
+ 'no-shadow': 'off',
+ 'no-trailing-spaces': 'error',
+ 'no-unused-labels': 'error',
+ 'no-use-before-define': 'error',
+ 'no-var': 'error',
+ 'prefer-const': 'error',
+ semi: 'error',
+ 'space-in-parens': ['error', 'never'],
+ 'spaced-comment': ['error', 'always'],
+ '@typescript-eslint/dot-notation': 'off',
+ '@typescript-eslint/indent': [
+ 'error',
+ 2,
+ { FunctionDeclaration: { parameters: 'first' }, FunctionExpression: { parameters: 'first' } },
+ ],
+ '@typescript-eslint/member-delimiter-style': [
+ 'error',
+ {
+ multiline: {
+ delimiter: 'semi',
+ requireLast: true,
+ },
+ singleline: {
+ delimiter: 'semi',
+ requireLast: false,
+ },
+ },
+ ],
+ '@typescript-eslint/no-misused-new': 'error',
+ '@typescript-eslint/no-non-null-assertion': 'error',
+ '@typescript-eslint/prefer-function-type': 'error',
+ '@typescript-eslint/semi': ['error', 'always'],
+ '@typescript-eslint/type-annotation-spacing': 'error',
+ '@typescript-eslint/unified-signatures': 'error',
+ '@typescript-eslint/no-shadow': 'error',
+ 'prefer-promise-reject-errors': 'off',
+ 'max-nested-callbacks': ['error', 6],
+ '@typescript-eslint/no-this-alias': 'off',
+ 'accessor-pairs': 'off',
+ 'max-depth': 'off',
+ '@typescript-eslint/member-ordering': 'off',
+ 'array-callback-return': 'off',
+ },
+};
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 0000000000..6a6f648317
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,61 @@
+name: '🐛 Bug report'
+description: Create a report to help us improve vue-devui
+title: '🐛 [Bug]: '
+labels: ['🐛 bug']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Please fill out the following carefully in order to better fix the problem.
+ - type: input
+ id: devui-version
+ attributes:
+ label: Version
+ description: |
+ ### **Check if the issue is reproducible with the latest stable version.**
+ You can use the command `npm ls vue-devui` to view it
+ placeholder: latest
+ validations:
+ required: true
+ - type: input
+ id: vue-version
+ attributes:
+ label: Vue Version
+ placeholder: latest
+ validations:
+ required: true
+ - type: textarea
+ id: minimal-repo
+ attributes:
+ label: Link to minimal reproduction
+ description: |
+ **Provide a streamlined CodePen / CodeSandbox or GitHub repository link as much as possible. Please don't fill in a link randomly, it will only close your issue directly.**
+ placeholder: Please Input
+ validations:
+ required: true
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: Step to reproduce
+ description: |
+ **After the replay is turned on, what actions do we need to perform to make the bug appear? Simple and clear steps can help us locate the problem more quickly. Please clearly describe the steps of reproducing the issue. Issues without clear reproducing steps will not be repaired. If the issue marked with 'need reproduction' does not provide relevant steps within 7 days, it will be closed directly.**
+ placeholder: Please Input
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: What is expected
+ placeholder: Please Input
+ - type: textarea
+ id: actually
+ attributes:
+ label: What is actually happening
+ placeholder: Please Input
+ - type: textarea
+ id: additional-comments
+ attributes:
+ label: Any additional comments (optional)
+ description: |
+ **Some background / context of how you ran into this bug.**
+ placeholder: Please Input
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..f53f178517
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Questions or need help
+ url: https://github.com/DevCloudFE/vue-devui/discussions
+ about: Add this WeChat(devui-official), we will invite you to the WeChat discussion group later.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 0000000000..4838fd60ed
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,23 @@
+name: ✨ Feature Request
+description: Propose new features to vue-devui to improve it.
+title: '✨ [Feature]: '
+labels: ['✨ feature']
+body:
+ - type: textarea
+ id: feature-solve
+ attributes:
+ label: What problem does this feature solve
+ description: |
+ Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature?
+ placeholder: Please Input
+ validations:
+ required: true
+ - type: textarea
+ id: feature-api
+ attributes:
+ label: What does the proposed API look like
+ description: |
+ Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks.
+ placeholder: Please Input
+ validations:
+ required: true
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000000..d8646c75e5
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,41 @@
+name: Pull Request
+
+on:
+ push:
+ branches: [ dev, main ]
+ pull_request:
+ branches: [ dev, main ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ name: "Build: node-${{ matrix.node-version }}"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: 6
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
+
+ - name: Install deps
+ run: pnpm i
+
+ - name: Build site
+ run: pnpm build
+
+ # - name: Test vue-devui
+ # run: yarn test
diff --git a/.github/workflows/sync-gitee.yml b/.github/workflows/sync-gitee.yml
new file mode 100644
index 0000000000..c08cb6030a
--- /dev/null
+++ b/.github/workflows/sync-gitee.yml
@@ -0,0 +1,20 @@
+name: Sync to Gitee
+
+on:
+ push:
+ branches: [dev, main, gh-pages]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Sync to Gitee
+ uses: wearerequired/git-mirror-action@master
+ env:
+ # 在 Settings->Secrets 配置 SSH_PRIVATE_KEY
+ SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
+ with:
+ # GitHub 源仓库地址
+ source-repo: git@github.com:DevCloudFE/vue-devui.git
+ # Gitee 目标仓库地址
+ destination-repo: git@gitee.com:devui/vue-devui.git
diff --git a/.gitignore b/.gitignore
index 399d9de0dc..19dc38196f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,8 +6,8 @@ build
lib
types
*.local
-pnpm-lock.yaml
package-lock.json
+yarn.lock
yarn-error.log
.history
.vscode
@@ -16,4 +16,3 @@ packages/devui-vue/devui/vue-devui.ts
packages/devui-vue/devui/theme/theme.scss
packages/devui-vue/docs/.vitepress/config/sidebar.ts
packages/devui-vue/docs/.vitepress/config/enSidebar.ts
-
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 0000000000..a789998f5e
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install commitlint --edit
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000000..65d5bb7c48
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx @ls-lint/ls-lint && npx lint-staged
diff --git a/packages/devui-vue/.ls-lint.yml b/.ls-lint.yml
similarity index 100%
rename from packages/devui-vue/.ls-lint.yml
rename to .ls-lint.yml
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..6c59086d86
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+enable-pre-post-scripts=true
diff --git a/.prettierrc b/.prettierrc
index 752af08930..2083fb89e3 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,12 +1,13 @@
{
- "arrowParens": "always",
"bracketSpacing": true,
- "htmlWhitespaceSensitivity": "ignore",
+ "jsxBracketSameLine": true,
"jsxSingleQuote": true,
- "printWidth": 100,
- "semi": false,
+ "printWidth": 140,
+ "semi": true,
"useTabs": false,
- "trailingComma": "none",
+ "trailingComma": "es5",
"singleQuote": true,
- "tabWidth": 2
+ "tabWidth": 2,
+ "endOfLine": "auto",
+ "proseWrap": "preserve"
}
diff --git a/packages/devui-vue/.stylelintrc.json b/.stylelintrc.json
similarity index 100%
rename from packages/devui-vue/.stylelintrc.json
rename to .stylelintrc.json
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..35b5167093
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,132 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CODE_OF_CONDUCT.zh-CN.md b/CODE_OF_CONDUCT.zh-CN.md
new file mode 100644
index 0000000000..c2b0fa0cdf
--- /dev/null
+++ b/CODE_OF_CONDUCT.zh-CN.md
@@ -0,0 +1,87 @@
+# 贡献者公约
+
+## 我们的承诺
+
+身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。
+
+我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。
+
+## 我们的准则
+
+有助于促进本社区积极环境的行为包括但不限于:
+
+* 与人为善、推己及人
+* 尊重不同的主张、观点和经历
+* 积极提出、耐心接受有益批评
+* 面对过失,承担责任、认真道歉、从中学习
+* 关注社区共同诉求,而非一己私利
+
+不当行为包括但不限于:
+
+* 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引
+* 挑衅行为、侮辱或贬损的言论、人身及政治攻击
+* 公开或私下骚扰
+* 未获明确授权擅自发布他人的资料,如地址、电子邮箱等
+* 其他有理由认定为违反职业操守的不当行为
+
+## 落实之义务
+
+社区负责人有责任诠释何谓“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。
+
+社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论(comment)、提交(commit)、代码、维基(wiki)编辑、问题(issue)等贡献。如有必要,需告知采取措施之理由。
+
+## 适用范围
+
+此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。
+
+代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。
+
+## 贯彻落实
+
+如遇滥用、骚扰等不当行为,请通过[在此输入联系方式]向纪律检查委员举报。
+纪委将迅速审议并调查全部投诉。
+
+社区全体负责人有义务保密举报者信息。
+
+## 指导方针
+
+社区负责人将依据下列方案判断并处置违纪行为:
+
+### 一、督促
+
+**社区影响**:用语不当、举止不符合职业道德或不受社区欢迎。
+
+**处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或将要求公开道歉。
+
+### 二、警告
+
+**社区影响**:一起或多起事件中的违纪行为。
+
+**处理意见**:警告继续违纪之后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。
+
+### 三、封禁
+
+**社区影响**:严重违纪行为,包括屡教不改。
+
+**处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区执法者往来。如有违反,可致开除。
+
+### 四、开除
+
+**社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。
+
+**处理意见**:无限期禁止违纪者与项目社区的一切公开往来。
+
+## 来源
+
+本行为标准改编自[参与者公约][homepage]2.0版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]
+
+指导方针借鉴自[Mozilla纪检分级][Mozilla CoC]。
+
+此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][FAQ]。
+另有诸译本:[https://www.contributor-covenant.org/translations][translations]。
+
+[homepage]:https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 120000
index 0000000000..6a6d479819
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+packages/devui-vue/docs/CONTRIBUTING.md
\ No newline at end of file
diff --git a/README.md b/README.md
index b04d6e0fb9..3c3278701f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Vue DevUI 是 Vue3 版本的 DevUI 组件库,基于 [https://github.com/devclo
DevUI 官方网站:[https://devui.design](https://devui.design)
-# 当前状态: Beta
+## 当前状态: Beta
该项目还处于孵化和演进阶段,欢迎大家参与到 Vue DevUI 项目的建设中来!🎉🎉
@@ -18,51 +18,41 @@ DevUI 官方网站:[https://devui.design](https://devui.design)
- ⭐ 参与到开源社区中来
- 🎊 结识一群热爱学习、热爱开源的朋友
-# 快速开始
+[贡献指南](https://github.com/DevCloudFE/vue-devui/wiki/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)
-## 1 安装依赖
+## 快速开始
-```
-yarn(推荐)
-
-or
+### 1 安装依赖
-npm i
```
-
-## 2 启动
-
+pnpm i
```
-yarn dev(推荐)
-or
+### 2 启动
-npm run dev
+```
+pnpm dev
```
-## 3 访问
+### 3 访问
[http://localhost:3000/](http://localhost:3000/)
-## 4 生产打包
+### 4 生产打包
```
-yarn build(推荐)
-
-or
-
-npm run build
+pnpm build
```
-# 使用 Vue DevUI
+## 使用 Vue DevUI
-## 1. 安装
+### 1. 安装
```
-yarn add vue-devui
+pnpm i vue-devui
```
-## 2. 全量引入
+### 2. 全量引入
在`main.ts`文件中编写以下代码:
```
@@ -79,7 +69,7 @@ createApp(App)
.mount('#app')
```
-## 3. 按需引入
+### 3. 按需引入
除了全量引入,我们也支持单个组件按需引入。
@@ -99,7 +89,7 @@ createApp(App)
.mount('#app')
```
-## 4. 配置自动按需引入`unplugin-vue-components`(推荐)
+### 4. 配置自动按需引入`unplugin-vue-components`(推荐)
配置`unplugin-vue-components`插件可以无需引入Vue DevUI就可以直接按需使用其中的组件,具体使用方式如下:
@@ -124,7 +114,7 @@ export default defineConfig({
配置了以上插件,就可以直接在代码中使用`Vue DevUI`的组件,而无需在`main.ts`文件中引入`Vue DevUI`。
-## 5. 使用
+### 5. 使用
```
@@ -132,23 +122,19 @@ export default defineConfig({
```
-# 图标库
+## 图标库
图标库推荐使用[DevUI图标库](https://devui.design/icon/ruleResource),也可以使用第三方图标库,比如:iconfont。
-## 使用DevUI图标库
+### 使用DevUI图标库
-### 安装
+#### 安装
```
-yarn add @devui-design/icons(推荐)
-
-or
-
-npm i @devui-design/icons
+pnpm i @devui-design/icons
```
-### 引入
+#### 引入
在`main.ts`文件中,编写以下代码:
@@ -156,12 +142,46 @@ npm i @devui-design/icons
import '@devui-design/icons/icomoon/devui-icon.css'
```
-### 使用
+#### 使用
```
```
-# License
+## Contributors ✨
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+
+## License
[MIT](https://github.com/DevCloudFE/vue-devui/blob/dev/LICENSE)
diff --git a/packages/devui-vue/commitlint.config.js b/commitlint.config.js
similarity index 100%
rename from packages/devui-vue/commitlint.config.js
rename to commitlint.config.js
diff --git a/lerna.json b/lerna.json
deleted file mode 100644
index 793042fdd1..0000000000
--- a/lerna.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "packages": [
- "packages/*"
- ],
- "version": "0.0.0",
- "npmClient": "yarn",
- "useWorkspaces": true
-}
diff --git a/package.json b/package.json
index 17d3c41413..29a4b8b794 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,34 @@
"name": "root",
"private": true,
"scripts": {
- "dev": "lerna exec --scope vue-devui yarn dev",
- "build": "lerna exec --scope vue-devui yarn build",
- "build:lib": "lerna exec --scope vue-devui yarn build:lib"
+ "dev": "pnpm dev --filter vue-devui",
+ "build": "pnpm build --filter vue-devui",
+ "build:lib": "pnpm build:lib --filter vue-devui",
+ "build:cli": "pnpm build --filter devui-cli",
+ "prepare": "husky install",
+ "precommit": "lint-staged",
+ "lint:fix": "eslint --fix \"packages/**/{*.vue,*.js,*.ts,*.jsx,*.tsx}\"",
+ "stylelint": "stylelint --fix \"packages/**/{*.scss,*.css}\"",
+ "test": "pnpm test --filter vue-devui"
},
"devDependencies": {
- "lerna": "^4.0.0"
+ "@commitlint/cli": "^11.0.0",
+ "@ls-lint/ls-lint": "^1.10.0",
+ "all-contributors-cli": "^6.20.0",
+ "esbuild-register": "^2.6.0",
+ "eslint": "^7.28.0",
+ "eslint-plugin-import": "^2.24.2",
+ "eslint-plugin-vue": "^7.11.1",
+ "husky": "^7.0.4",
+ "lint-staged": "^11.0.0",
+ "npm-run-all": "^4.1.5",
+ "stylelint": "^13.13.1",
+ "stylelint-config-recommended-scss": "^4.3.0",
+ "stylelint-config-standard": "^22.0.0",
+ "stylelint-scss": "^3.20.1"
},
- "workspaces": [
- "packages/*"
- ]
-}
\ No newline at end of file
+ "lint-staged": {
+ ".{js,ts,jsx,tsx,vue}": "eslint --fix",
+ ".{scss,css}": "stylelint --fix"
+ }
+}
diff --git a/packages/devui-cli/README.md b/packages/devui-cli/README.md
index 57f82fe97a..08441a36db 100644
--- a/packages/devui-cli/README.md
+++ b/packages/devui-cli/README.md
@@ -1,3 +1,55 @@
-# `devui-cli`
+# DevUI CLI
-> TODO: description
\ No newline at end of file
+`DevUI CLI`是一个为组件库而生的脚手架工具,它有以下特性:
+1. 创建组件库模板
+2. 创建组件库入口文件
+
+## 快速开始
+
+### 本地启动
+
+```sh
+pnpm dev --filter devui-cli
+```
+
+成功启动之后将生成`devui-cli/lib/bin.js`文件,`dev`命令是可以热加载的,修改完代码不需要重新运行该命令。
+
+运行该文件,即可进入创建组件的交互式命令行界面,按照引导创建组件模板或组件库入口文件。
+
+```sh
+pnpm cli --filter devui-cli create
+```
+
+### 构建
+
+```sh
+pnpm build --filter devui-cli
+```
+
+构建成功之后将生成`devui-cli/lib/bin.js`文件,这是最终的产物,用于发布 npm 包。
+
+## 命令
+
+目前支持以下命令:
+- create
+- build:进行中
+
+### create
+
+在当前目录下创建组件模板、组件库入口文件等。
+
+主要有以下问答选项:
+- 选择一个类型
+ - `component`: 已完成,创建组件模板
+ - `component-test`: 进行中
+ - `component-doc`: 进行中
+ - `lib-entry`: 已完成,会根据创建的组件来自动生成入口文件
+ - `doc-nav`: 进行中
+- 输入组件名称:将以该名称创建组件文件夹和组件名
+- 输入组件中文名:文档中的组件标题将显示该名称
+- 选择组件类型:`通用`、`导航`、`反馈`、`数据录入`、`数据展示`、`布局`
+- 选择组件包含的形式(多选):`component`、`service`、`directive`
+
+### build
+
+进行中
diff --git a/packages/devui-cli/package.json b/packages/devui-cli/package.json
index b0f4cb468b..60ce4304f5 100644
--- a/packages/devui-cli/package.json
+++ b/packages/devui-cli/package.json
@@ -1,19 +1,19 @@
{
- "name": "@devui/cli",
- "version": "0.0.1",
- "description": "Cli of devui",
+ "name": "devui-cli",
+ "version": "0.0.2",
+ "description": "DevUI CLI",
"keywords": [
"cli",
"devui",
"devui-cli"
],
"author": "iel",
- "homepage": "https://gitee.com/devui/vue-devui/tree/dev/packages/devui-vue/devui-cli#README.md",
+ "homepage": "https://github.com/DevCloudFE/vue-devui",
"license": "MIT",
"main": "lib/bin.js",
- "types": "types/bin.d.ts",
+ "types": "types/config.d.ts",
"bin": {
- "dc": "lib/bin.js"
+ "devui": "lib/bin.js"
},
"files": [
"lib",
@@ -21,13 +21,12 @@
],
"repository": {
"type": "git",
- "url": "https://gitee.com/RootWater/vue-devui.git"
+ "url": "git@github.com:DevCloudFE/vue-devui.git"
},
"scripts": {
"dev": "esbuild --bundle ./src/bin.ts --format=cjs --platform=node --outfile=./lib/bin.js --external:esbuild --minify-whitespace --watch",
- "build": "npm run build:lib & npm run build:dts",
+ "build": "run-p build:lib",
"build:lib": "rimraf ./lib && esbuild --bundle ./src/bin.ts --format=cjs --platform=node --outfile=./lib/bin.js --external:esbuild --minify-whitespace",
- "build:dts": "rimraf ./types && tsc -p ./tsconfig.json",
"cli": "node ./lib/bin.js"
},
"devDependencies": {
@@ -41,6 +40,7 @@
"kolorist": "^1.5.0",
"lodash-es": "^4.17.21",
"prompts": "^2.4.2",
+ "rimraf": "^3.0.2",
"typescript": "^4.4.4"
},
"dependencies": {
diff --git a/packages/devui-cli/src/bin.ts b/packages/devui-cli/src/bin.ts
index bf19b38a6e..b802d9943f 100644
--- a/packages/devui-cli/src/bin.ts
+++ b/packages/devui-cli/src/bin.ts
@@ -1,8 +1,9 @@
#!/usr/bin/env node
import { Command } from 'commander'
+import type { CliConfig } from '../types/config'
import baseAction from './commands/base'
import createAction, { validateCreateType } from './commands/create'
-import { CliConfig, detectCliConfig } from './shared/config'
+import { detectCliConfig } from './shared/config'
import { VERSION } from './shared/constant'
import {
DEFAULT_CLI_CONFIG_FILE_NAME
diff --git a/packages/devui-cli/src/commands/base.ts b/packages/devui-cli/src/commands/base.ts
index 877dc955ad..78122a6910 100644
--- a/packages/devui-cli/src/commands/base.ts
+++ b/packages/devui-cli/src/commands/base.ts
@@ -1,68 +1,68 @@
-import { existsSync, statSync } from 'fs-extra'
-import { dirname, extname, isAbsolute, resolve } from 'path'
-import prompts from 'prompts'
-import { mergeCliConfig } from '../shared/config'
-import { CWD } from '../shared/constant'
-import generateConfig from '../shared/generate-config'
-import logger from '../shared/logger'
-import { dynamicImport, onPromptsCancel } from '../shared/utils'
-import buildAction from './build'
-import createAction from './create'
+import { existsSync, statSync } from 'fs-extra';
+import { dirname, extname, isAbsolute, resolve } from 'path';
+import prompts from 'prompts';
+import { mergeCliConfig } from '../shared/config';
+import { CWD } from '../shared/constant';
+import generateConfig from '../shared/generate-config';
+import logger from '../shared/logger';
+import { dynamicImport, onPromptsCancel } from '../shared/utils';
+import buildAction from './build';
+import createAction from './create';
function getActions() {
- const actionMap = new Map()
+ const actionMap = new Map();
actionMap.set('create', {
title: 'create',
value: 'create',
selected: true,
action: createAction
- })
- actionMap.set('build', { title: 'build', value: 'build', action: buildAction })
+ });
+ actionMap.set('build', { title: 'build', value: 'build', action: buildAction });
- return actionMap
+ return actionMap;
}
export type BaseCmd = {
init?: boolean
config?: string
-}
+};
export default async function baseAction(cmd: BaseCmd) {
if (cmd.init) {
- return generateConfig()
+ return generateConfig();
}
- loadCliConfig(cmd)
+ loadCliConfig(cmd);
- selectCommand()
+ selectCommand();
}
export function loadCliConfig(cmd: Pick) {
- if (!cmd.config) return
+ if (!cmd.config) return;
- let configPath = resolve(CWD, cmd.config)
+ const configPath = resolve(CWD, cmd.config);
if (!existsSync(configPath)) {
- logger.error(`The path "${configPath}" not exist.`)
- process.exit(1)
+ logger.error(`The path "${configPath}" not exist.`);
+ process.exit(1);
}
if (statSync(configPath).isDirectory() || !['.js', '.ts'].includes(extname(configPath))) {
- logger.error(`The path "${configPath}" is not a ".js" or ".ts" file.`)
- process.exit(1)
+ logger.error(`The path "${configPath}" is not a ".js" or ".ts" file.`);
+ process.exit(1);
}
- const config = dynamicImport(configPath)
- if (!isAbsolute(config.cwd)) {
- config.cwd = resolve(dirname(configPath), config.cwd)
+ const config = dynamicImport(configPath);
+ if (config.cwd && !isAbsolute(config.cwd)) {
+ config.cwd = resolve(dirname(configPath), config.cwd);
}
- mergeCliConfig(config)
+ mergeCliConfig(config);
}
async function selectCommand() {
- const actions = getActions()
- let result: any = {}
+ const actions = getActions();
+ let result: any = {};
try {
result = await prompts(
@@ -77,11 +77,11 @@ async function selectCommand() {
{
onCancel: onPromptsCancel
}
- )
+ );
} catch (e: any) {
- logger.error(e.message)
- process.exit(1)
+ logger.error(e.message);
+ process.exit(1);
}
- actions.get(result.command)!.action()
+ actions.get(result.command)!.action();
}
diff --git a/packages/devui-cli/src/shared/config.ts b/packages/devui-cli/src/shared/config.ts
index e4818a33ec..c52a78cc7a 100644
--- a/packages/devui-cli/src/shared/config.ts
+++ b/packages/devui-cli/src/shared/config.ts
@@ -1,71 +1,11 @@
import { readdirSync, statSync } from 'fs-extra'
import { merge } from 'lodash-es'
import { resolve } from 'path'
+import type { CliConfig } from '../../types/config'
import { loadCliConfig } from '../commands/base'
import { CWD } from './constant'
import { DEFAULT_CLI_CONFIG_NAME } from './generate-config'
-export type CliConfig = {
- /**
- * current workspace directory
- *
- * ***Should be the root directory of the component library.***
- *
- * @default process.cwd()
- */
- cwd: string
- /**
- * Generate the root directory of component.
- *
- * ***Note that the path should be based on the `cwd` of configuration item.***
- *
- * @default .
- */
- componentRootDir: string
- /**
- * category of component
- *
- * @default ['通用', '导航', '反馈', '数据录入', '数据展示', '布局']
- */
- componentCategories: string[]
- /**
- * prefix of the component library
- *
- * @default ''
- */
- libPrefix: string
- /**
- * component style file suffix of the component library
- *
- * @default .css
- */
- libStyleFileSuffix: string
- /**
- * component class prefix of the component library
- */
- libClassPrefix: string
- /**
- * component library entry file name
- *
- * @default index
- */
- libEntryFileName: string
- /**
- * Generate the root directory of the lib entry file.
- *
- * ***Note that the path should be based on the `cwd` of configuration item.***
- *
- * @default .
- */
- libEntryRootDir: string
- /**
- * version of component library
- *
- * @default 0.0.0
- */
- version: string
-}
-
export const cliConfig: CliConfig = {
cwd: CWD,
componentRootDir: '.',
diff --git a/packages/devui-cli/src/shared/generate-lib-entry.ts b/packages/devui-cli/src/shared/generate-lib-entry.ts
index fa19170b03..9e7a705e53 100644
--- a/packages/devui-cli/src/shared/generate-lib-entry.ts
+++ b/packages/devui-cli/src/shared/generate-lib-entry.ts
@@ -3,7 +3,7 @@ import { getComponentsMeta } from '../templates/component/utils'
import genLibEntryTemplate from '../templates/lib-entry/lib-entry'
import logger from './logger'
-export default async function genLibEntry(filePath: string = '') {
+export default async function genLibEntry(filePath = '') {
const componentsMeta = await getComponentsMeta()
writeFileSync(filePath, genLibEntryTemplate(componentsMeta), {
diff --git a/packages/devui-cli/src/shared/utils.ts b/packages/devui-cli/src/shared/utils.ts
index 882bed6bfd..3ff20f8142 100644
--- a/packages/devui-cli/src/shared/utils.ts
+++ b/packages/devui-cli/src/shared/utils.ts
@@ -4,6 +4,7 @@ import { camelCase, upperFirst } from 'lodash-es'
import { extname, relative, resolve } from 'path'
import { coreFileName } from '../templates/component/utils'
import { cliConfig } from './config'
+import { PKG_NAME } from './constant'
export function bigCamelCase(str: string) {
return upperFirst(camelCase(str))
@@ -35,7 +36,7 @@ export function dynamicImport(path: string) {
outfile: tempPath,
platform: 'node',
format: 'cjs',
- external: ['esbuild', 'dev-cli']
+ external: ['esbuild', PKG_NAME]
})
const config = require(relativePath).default ?? {}
diff --git a/packages/devui-cli/src/templates/base/config.ts b/packages/devui-cli/src/templates/base/config.ts
index 5e2798cfaf..14b00105b5 100644
--- a/packages/devui-cli/src/templates/base/config.ts
+++ b/packages/devui-cli/src/templates/base/config.ts
@@ -1,5 +1,5 @@
-import { CliConfig } from "../../shared/config";
-import { PKG_NAME } from "../../shared/constant";
+import type { CliConfig } from '../../../types/config';
+import { PKG_NAME } from '../../shared/constant';
export default function genConfigTemplate(config: Partial = {}) {
return `\
diff --git a/packages/devui-cli/src/templates/component/doc.ts b/packages/devui-cli/src/templates/component/doc.ts
index 641d6667fb..aad6bb48b7 100644
--- a/packages/devui-cli/src/templates/component/doc.ts
+++ b/packages/devui-cli/src/templates/component/doc.ts
@@ -1,5 +1,5 @@
-import { getPartName } from "../lib-entry/lib-entry";
-import { ComponentMeta } from "./meta";
+import { getPartName } from '../lib-entry/lib-entry';
+import { ComponentMeta } from './meta';
export default function genDocTemplate(meta: ComponentMeta) {
return `\
diff --git a/packages/devui-cli/src/templates/component/style.ts b/packages/devui-cli/src/templates/component/style.ts
index 9f7f217aa2..57a00df38e 100644
--- a/packages/devui-cli/src/templates/component/style.ts
+++ b/packages/devui-cli/src/templates/component/style.ts
@@ -1,4 +1,4 @@
-import { coreClassName } from "./utils"
+import { coreClassName } from './utils'
export default function genStyleTemplate(name: string) {
return `\
diff --git a/packages/devui-cli/src/templates/component/utils.ts b/packages/devui-cli/src/templates/component/utils.ts
index a36a4f5a20..7e87005af8 100644
--- a/packages/devui-cli/src/templates/component/utils.ts
+++ b/packages/devui-cli/src/templates/component/utils.ts
@@ -24,9 +24,8 @@ export const directiveName = (name: string) => bigCamelCase(name + 'Directive')
export async function getComponentMetaFiles() {
return glob('./**/meta.json', {
- cwd: cliConfig.cwd,
- absolute: true,
- deep: 2
+ cwd: cliConfig.componentRootDir,
+ absolute: true
})
}
diff --git a/packages/devui-cli/src/templates/lib-entry/lib-entry.ts b/packages/devui-cli/src/templates/lib-entry/lib-entry.ts
index 9450fe0bd2..7ad6221b62 100644
--- a/packages/devui-cli/src/templates/lib-entry/lib-entry.ts
+++ b/packages/devui-cli/src/templates/lib-entry/lib-entry.ts
@@ -58,7 +58,7 @@ export default function genLibEntryTemplate(componentsMeta: ComponentMeta[]) {
return `\
import type { App } from 'vue'
-${imports.join('\n')}
+${imports.join('\n') || '// Not find components.'}
const installs = [
\t${installs.join(',\n\t')}
diff --git a/packages/devui-cli/tsconfig.json b/packages/devui-cli/tsconfig.json
index e799e8ac90..5a626cfed3 100644
--- a/packages/devui-cli/tsconfig.json
+++ b/packages/devui-cli/tsconfig.json
@@ -8,10 +8,8 @@
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
- "declaration": true,
- "emitDeclarationOnly": true,
"downlevelIteration": true,
- "declarationDir": "./types",
+ "typeRoots": ["./node_modules/@types", "./types"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
@@ -20,6 +18,7 @@
},
"include": [
"./src/**/*.ts",
- "./src/**/*.d.ts"
+ "./src/**/*.d.ts",
+ "./types/**/*.d.ts"
]
}
\ No newline at end of file
diff --git a/packages/devui-theme/.gitignore b/packages/devui-theme/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/packages/devui-theme/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/packages/devui-theme/package.json b/packages/devui-theme/package.json
new file mode 100644
index 0000000000..fec81f7386
--- /dev/null
+++ b/packages/devui-theme/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "devui-theme",
+ "version": "0.0.1",
+ "main": "src/index.ts",
+ "module": "src/index.ts",
+ "files": [
+ "dist",
+ "src"
+ ],
+ "scripts": {
+ "build": "vite build"
+ },
+ "dependencies": {
+ "css-vars-ponyfill": "^2.4.7",
+ "enquirejs-ssr": "^2.1.7",
+ "rxjs": "^7.5.4",
+ "vue": "^3.2.25"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^2.2.0",
+ "typescript": "^4.5.4",
+ "vite": "^2.8.0",
+ "vue-tsc": "^0.29.8"
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-theme/src/index.ts b/packages/devui-theme/src/index.ts
new file mode 100644
index 0000000000..de2843a78e
--- /dev/null
+++ b/packages/devui-theme/src/index.ts
@@ -0,0 +1,2 @@
+export * from './theme';
+export * from './theme-collection';
diff --git a/packages/devui-theme/src/styles-var/devui-var.less b/packages/devui-theme/src/styles-var/devui-var.less
new file mode 100644
index 0000000000..0f1fc7c91c
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/devui-var.less
@@ -0,0 +1 @@
+// empty file wait to fill in
diff --git a/packages/devui-theme/src/styles-var/devui-var.scss b/packages/devui-theme/src/styles-var/devui-var.scss
new file mode 100644
index 0000000000..7152d5fc56
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/devui-var.scss
@@ -0,0 +1,6 @@
+@import './private/color';
+@import './private/font';
+@import './private/shadow';
+@import './private/corner';
+@import './private/animation';
+@import './private/z-index';
diff --git a/packages/devui-theme/src/styles-var/private/_animation.scss b/packages/devui-theme/src/styles-var/private/_animation.scss
new file mode 100644
index 0000000000..0209cefaf7
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_animation.scss
@@ -0,0 +1,9 @@
+$devui-animation-duration-slow: var(--devui-animation-duration-slow, 300ms);
+$devui-animation-duration-base: var(--devui-animation-duration-base, 200ms);
+$devui-animation-duration-fast: var(--devui-animation-duration-fast, 100ms);
+
+$devui-animation-ease-in: var(--devui-animation-ease-in, cubic-bezier(0.5, 0, 0.84, 0.25));
+$devui-animation-ease-out: var(--devui-animation-ease-out, cubic-bezier(0.16, 0.75, 0.5, 1));
+$devui-animation-ease-in-out: var(--devui-animation-ease-in-out, cubic-bezier(0.5, 0.05, 0.5, 0.95));
+$devui-animation-ease-in-out-smooth: var(--devui-animation-ease-in-out-smooth, cubic-bezier(0.645, 0.045, 0.355, 1));
+$devui-animation-linear: var(--devui-animation-linear, cubic-bezier(0, 0, 1, 1));
diff --git a/packages/devui-theme/src/styles-var/private/_color.scss b/packages/devui-theme/src/styles-var/private/_color.scss
new file mode 100755
index 0000000000..84cc2926a0
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_color.scss
@@ -0,0 +1,100 @@
+// 基础变量
+$devui-global-bg: var(--devui-global-bg, #f3f6f8); // 全局带底色背景
+$devui-global-bg-normal: var(--devui-global-bg-normal, #ffffff); // 全局白色背景
+$devui-base-bg: var(--devui-base-bg, #ffffff); // 基础区块背景白色
+$devui-base-bg-dark: var(--devui-base-bg-dark, #333854); // 基础区块背景深色(固定)
+$devui-brand: var(--devui-brand, #5e7ce0); // 品牌色
+$devui-brand-foil: var(--devui-brand-foil, #859bff); // 品牌色辅助色、正衬色
+$devui-brand-hover: var(--devui-brand-hover, #7693f5); // 品牌色高亮色(加亮)
+$devui-brand-active: var(--devui-brand-active, #526ecc); // 品牌色激活色(加深)
+$devui-brand-active-focus: var(--devui-brand-active-focus, #344899); // 品牌色焦点色(重度加深)
+$devui-contrast: var(--devui-contrast, #c7000b); // 品牌色撞色、对比色、反衬色、第二品牌色
+$devui-text: var(--devui-text, #252b3a); // 正文文本
+$devui-text-weak: var(--devui-text-weak, #575d6c); // 弱化的正文信息(手风琴子项,表头)
+$devui-aide-text: var(--devui-aide-text, #8a8e99); // 辅助文本、帮助信息(面包屑)
+$devui-aide-text-stress: var(--devui-aide-text-stress, #575d6c); // 辅助文本、帮助信息里的强调色
+$devui-placeholder: var(--devui-placeholder, #8a8e99); // 占位符
+$devui-light-text: var(--devui-light-text, #ffffff); // 有色深色背景下字体颜色(固定)
+$devui-dark-text: var(--devui-dark-text, #252b3a); // 有色浅色背景下字体颜色(固定)
+$devui-link: var(--devui-link, #526ecc); // 链接文本颜色
+$devui-link-active: var(--devui-link-active, #344899); // 链接文本悬停/激活颜色
+$devui-link-light: var(--devui-link-light, #96adfa); // 深色背景下链接文本颜色
+$devui-link-light-active: var(--devui-link-light-active, #beccfa); // 深色背景下链接文本悬停/激活颜色
+$devui-line: var(--devui-line, #adb0b8); // 边框分割线,仅用于边框
+$devui-dividing-line: var(--devui-dividing-line, #dfe1e6); // 内容分割线,用于内容之间的分割
+$devui-block: var(--devui-block, #ffffff); // 大面积的不可折叠区块的背景色(例如顶部导航背景色)
+$devui-area: var(--devui-area, #f8f8f8); // 可折叠区块的背景色(展开区域颜色)
+$devui-danger: var(--devui-danger, #f66f6a); // 失败、错误、告警
+$devui-warning: var(--devui-warning, #fac20a); // 警告、需注意类型提示
+$devui-waiting: var(--devui-waiting, #9faad7); // 等待中
+$devui-success: var(--devui-success, #50d4ab); // 成功、正确
+$devui-info: var(--devui-info, #5e7ce0); // 通知、一般提示、执行中
+$devui-initial: var(--devui-initial, #e9edfa); // 初始化、未执行
+$devui-unavailable: var(--devui-unavailable, #f5f5f6); // 不可用、禁用状态
+$devui-shadow: var(--devui-shadow, rgba(0, 0, 0, 0.2)); // 阴影色
+$devui-light-shadow: var(--devui-light-shadow, rgba(0, 0, 0, 0.1)); // 弱化阴影色
+
+// 图标
+$devui-icon-text: var(--devui-icon-text, #252b3a); // 文字图标颜色,同 正文颜色
+$devui-icon-bg: var(--devui-icon-bg, #ffffff); // svg图标 背景色
+$devui-icon-fill: var(--devui-icon-fill, #d3d5d9); // svg图标 灰色填充色
+$devui-icon-fill-hover: var(--devui-icon-fill-hover, #adb5ce); // svg图标 灰色填充色悬停反馈色
+$devui-icon-fill-active: var(--devui-icon-fill-active, #5e7ce0); // svg图标 高亮填充色(激活状态)
+$devui-icon-fill-active-hover: var(--devui-icon-fill-active-hover, #526ecc); // svg图标 高亮填充色悬停反馈色
+// 表单
+$devui-form-control-line: var(--devui-form-control-line, #adb0b8); // 表单控件边框色,同 边框分割线
+$devui-form-control-line-hover: var(--devui-form-control-line-hover, #575d6c); // 表单控件边框悬停反馈色
+$devui-form-control-line-active: var(--devui-form-control-line-active, #5e7ce0); // 表单控件边框激活色,用于获得焦点
+$devui-form-control-line-active-hover: var(--devui-form-control-line-active-hover, #344899); // 表单控件边框激活色,用于获得焦点且悬停
+// 列表
+$devui-list-item-active-bg: var(--devui-list-item-active-bg, #5e7ce0); // 列表类型单选选中背景
+$devui-list-item-active-text: var(--devui-list-item-active-text, #ffffff); // 列表类型单选选中背景搭配文本,同 有色深色背景下字体颜色(固定)
+$devui-list-item-active-hover-bg: var(--devui-list-item-active-hover-bg, #526ecc); // 列表类型单选选中背景悬停反馈色(仅用于分页等)
+$devui-list-item-hover-bg: var(--devui-list-item-hover-bg, #f2f5fc); // 列表类型普通选项悬停背景
+$devui-list-item-hover-text: var(--devui-list-item-hover-text, #526ecc); // 列表类型普通选项悬停背景搭配文本
+$devui-list-item-selected-bg: var(--devui-list-item-selected-bg, #e9edfa); // 列表类型多选被选中的行色,仅用于表格类
+$devui-list-item-strip-bg: var(--devui-list-item-strip-bg, #f2f5fc); // 列表类型斑马纹色,仅用于表格类
+// 禁用
+$devui-disabled-bg: var(--devui-disabled-bg, #f5f5f6); // disabled背景颜色
+$devui-disabled-line: var(--devui-disabled-line, #dfe1e6); // disabled边框颜色
+$devui-disabled-text: var(--devui-disabled-text, #adb0b8); // disabled文字颜色
+$devui-primary-disabled: var(--devui-primary-disabled, #beccfa); //主要按钮disabled状态文字颜色
+$devui-icon-fill-active-disabled: var(--devui-icon-fill-active-disabled, #beccfa); // svg图标激活状态禁用色
+// 特殊背景色
+$devui-label-bg: var(--devui-label-bg, #eef0f5); // 默认标签化选项背景色
+$devui-connected-overlay-bg: var(--devui-connected-overlay-bg, #ffffff); // 有连接点的弹出菜单层背景色
+$devui-connected-overlay-line: var(--devui-connected-overlay-line, #526ecc); // 有连接点的弹出菜单层边框色
+$devui-fullscreen-overlay-bg: var(--devui-fullscreen-overlay-bg, #ffffff); // 全屏类型的弹出内容层背景色(模态弹窗)
+$devui-feedback-overlay-bg: var(--devui-feedback-overlay-bg, #464d6e); // 信息提示反馈类型的漂浮层背景色(toast、popover)
+$devui-feedback-overlay-text: var(--devui-feedback-overlay-text, #dfe1e6); // 信息提示反馈类型的漂浮层背景色搭配文本色
+$devui-embed-search-bg: var(--devui-embed-search-bg, #f2f5fc); // 被嵌套的无边框搜索框背景色
+$devui-embed-search-bg-hover: var(--devui-embed-search-bg-hover, #eef0f5); // 被嵌套的无边框搜索框背景色
+$devui-float-block-shadow: var(--devui-float-block-shadow, rgba(94, 124, 224, 0.3)); // 特殊浮层背景色(待修正)
+$devui-highlight-overlay: var(--devui-highlight-overlay, rgba(255, 255, 255, 0.8)); // 局部半透明全局浮层背景色(比如底部)
+$devui-range-item-hover-bg: var(--devui-range-item-hover-bg, #e9edfa); // datepicker范围中的日期hover的反馈背景色
+
+// 按钮
+$devui-primary: var(--devui-primary, #5e7ce0); // 主要按钮,同品牌色
+$devui-primary-hover: var(--devui-primary-hover, #7693f5); // 主要按钮悬停
+$devui-primary-active: var(--devui-primary-active, #344899); // 主要按钮激活
+
+$devui-contrast-hover: var(--devui-contrast-hover, #d64a52); // 突出按钮悬停
+$devui-contrast-active: var(--devui-contrast-active, #b12220);// 突出按钮激活
+
+// 状态
+$devui-danger-line: var(--devui-danger-line, #f66f6a); // 失败边框
+$devui-danger-bg: var(--devui-danger-bg, #ffeeed); // 失败底色
+
+$devui-warning-line: var(--devui-warning-line, #fa9841);// 警告边框
+$devui-warning-bg: var(--devui-warning-bg, #fff3e8); // 警告底色
+
+$devui-info-line: var(--devui-info-line, #5e7ce0); // 通知边框
+$devui-info-bg: var(--devui-info-bg, #f2f5fc); // 通知底色
+
+$devui-success-line: var(--devui-success-line, #50d4ab);// 成功边框
+$devui-success-bg: var(--devui-success-bg, #edfff9); // 成功底色
+$devui-primary-line: var(--devui-primary-line, #5e7ce0);// 主要边框
+$devui-primary-bg: var(--devui-primary-bg, #f2f5fc); // 主要底色
+
+$devui-default-line: var(--devui-default-line, #5e7ce0);// 默认边框
+$devui-default-bg: var(--devui-default-bg, #f3f6f8); // 默认底色
diff --git a/packages/devui-theme/src/styles-var/private/_corner.scss b/packages/devui-theme/src/styles-var/private/_corner.scss
new file mode 100644
index 0000000000..517e64beef
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_corner.scss
@@ -0,0 +1,5 @@
+//圆角变量
+
+$devui-border-radius: var(--devui-border-radius, 2px); //一般圆角
+$devui-border-radius-feedback: var(--devui-border-radius-feedback, 4px); //反馈类圆角
+$devui-border-radius-card: var(--devui-border-radius-card, 6px); //卡片圆角
diff --git a/packages/devui-theme/src/styles-var/private/_font.scss b/packages/devui-theme/src/styles-var/private/_font.scss
new file mode 100644
index 0000000000..d1acc6e835
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_font.scss
@@ -0,0 +1,21 @@
+// 字号大小变量
+
+$devui-font-size: var(--devui-font-size, 12px); //正文、卡片副标题
+$devui-font-size-card-title: var(--devui-font-size-card-title, 14px); //卡片标题
+$devui-font-size-page-title: var(--devui-font-size-page-title, 16px); //页面标题
+$devui-font-size-modal-title: var(--devui-font-size-modal-title, 18px); //弹窗标题、数字
+$devui-font-size-price: var(--devui-font-size-price, 20px); //购买价格
+$devui-font-size-data-overview: var(--devui-font-size-data-overview, 24px); //数据总览
+
+$devui-font-size-icon: var(--devui-font-size-icon, 16px); //图标大小
+$devui-font-size-sm: var(--devui-font-size-sm, 12px); //当组件size为'sm'时使用此字号大小
+$devui-font-size-md: var(--devui-font-size-md, 12px); //当组件size为''时使用此字号大小
+$devui-font-size-lg: var(--devui-font-size-lg, 14px); //当组件size为'lg'时使用此字号大小
+
+$devui-font-title-weight: bold; //标题文字粗细
+$devui-font-content-weight: normal; //内容文字粗细
+$devui-line-height-base: 1.5; //规范行高
+
+$font-title-weight: bold;
+$font-content-weight: normal;
+$line-height-base: 1.5;
diff --git a/packages/devui-theme/src/styles-var/private/_shadow.scss b/packages/devui-theme/src/styles-var/private/_shadow.scss
new file mode 100644
index 0000000000..a86b13c331
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_shadow.scss
@@ -0,0 +1,12 @@
+//阴影变量
+
+$devui-shadow-length-base: var(--devui-shadow-length-base, 0 1px 4px 0); //直接铺陈在页面上方的元素 (card等)
+
+$devui-shadow-length-slide-left: var(--devui-shadow-length-slide-left, -2px 0 8px 0); //向左滑动时出现在右侧边缘的阴影 (dataTable固定右侧列向左滑动)
+$devui-shadow-length-slide-right: var(--devui-shadow-length-slide-right, 2px 0 8px 0); //向右滑动时出现在左侧边缘的阴影 (dataTable固定左侧列向右滑动)
+$devui-shadow-length-connected-overlay : var(--devui-shadow-connected-overlay, 0 2px 8px 0); //有连接点的弹出(覆盖)层 (DatePicker Cascader Select TagsInput Pagination的下拉菜单等)
+
+$devui-shadow-length-hover : var(--devui-shadow-length-hover, 0 4px 16px 0); //涉及到hover的地方
+$devui-shadow-length-feedback-overlay : var(--devui-shadow-length-feedback-overlay, 0 4px 16px 0); //信息提示反馈类 (PopOver Tooltip Toast StepsGuide等)
+
+$devui-shadow-length-fullscreen-overlay: var(--devui-shadow-fullscreen-overlay, 0 8px 40px 0); //无连接点的弹出(覆盖)层 (Drawer Modal ImagePreview等)
diff --git a/packages/devui-theme/src/styles-var/private/_variables.scss b/packages/devui-theme/src/styles-var/private/_variables.scss
new file mode 100755
index 0000000000..69c096e3b9
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_variables.scss
@@ -0,0 +1,43 @@
+@import './color';
+@import '../core/font';
+@import '../theme/font';
+
+$text-color: $devui-text;
+$font-family: 'HuaweiFont', 'Helvetica','Arial', 'PingFangSC-Regular', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑','Microsoft JhengHei';
+$font-variant-base: tabular-nums;
+$body-background: $devui-base-bg;
+$devui-font-size: $devui-font-size; // 12px
+// $devui-font-size-modal-title: $devui-font-size-modal-title; // 18px
+$font-size-page-title: $devui-font-size-page-title; // 16px
+// $devui-font-size-card-title: $devui-font-size-card-title; // 14px
+.devui-font-size-base {
+ font-size: $devui-font-size; // 12px
+}
+
+.devui-font-base {
+ @include font-content(); // 12px normal 1.5
+}
+
+.devui-font-size-modal-title {
+ font-size: $devui-font-size-modal-title; // 18px
+}
+
+.devui-font-modal-title {
+ @include font-title($devui-font-size-modal-title); // 18px bold 1.5
+}
+
+.devui-font-size-page-title {
+ font-size: $devui-font-size-page-title; // 16px
+}
+
+.devui-font-page-title {
+ @include font-title(); // 16px bold 1.5
+}
+
+.devui-font-size-secondary-title {
+ font-size: $devui-font-size-card-title; // 14px
+}
+
+.devui-font-secondary-title {
+ @include font-title($devui-font-size-card-title); // 14px bold 1.5
+}
diff --git a/packages/devui-theme/src/styles-var/private/_z-index.scss b/packages/devui-theme/src/styles-var/private/_z-index.scss
new file mode 100644
index 0000000000..7846b41414
--- /dev/null
+++ b/packages/devui-theme/src/styles-var/private/_z-index.scss
@@ -0,0 +1,12 @@
+// 临时层
+// 若存在遮罩,则遮罩基于对应z-index值-1
+$devui-z-index-full-page-overlay: var(--devui-z-index-full-page-overlay, 1080); // 全屏覆盖类元素
+$devui-z-index-dropdown: var(--devui-z-index-dropdown, 1052); // 下拉菜单,dropdown等
+$devui-z-index-pop-up: var(--devui-z-index-pop-up, 1060); // 提示类信息,popover,tooltip等
+$devui-z-index-modal: var(--devui-z-index-modal, 1050);// 弹窗,
+$devui-z-index-drawer: var(--devui-z-index-drawer, 1040);// 抽屉板
+$devui-z-index-framework: var(--devui-z-index-framework, 1000);// 框架类元素,header,sideMenu等
+
+// 内容层,根据需要设置,zIndex需小于临时层
+
+// 背景层,根据需要设置,zIndex需小于内容层
diff --git a/packages/devui-theme/src/theme-collection/extend-theme.scss b/packages/devui-theme/src/theme-collection/extend-theme.scss
new file mode 100644
index 0000000000..78a4f4e547
--- /dev/null
+++ b/packages/devui-theme/src/theme-collection/extend-theme.scss
@@ -0,0 +1,334 @@
+@import '../styles-var/devui-var.scss';
+
+body[ui-theme='infinity-theme'],
+body[ui-theme='sweet-theme'],
+body[ui-theme='provence-theme'],
+body[ui-theme='deep-theme'],
+body[ui-theme='galaxy-theme'] {
+ // TODO: 组件支持全局配置默认尺寸参数后删除
+ // button default size change to '32px'
+ .devui-btn:not(.devui-btn-xs):not(.devui-btn-sm):not(.devui-btn-lg) {
+ height: 32px;
+ line-height: 32px;
+ }
+
+ d-tabs {
+ display: inline;
+
+ --devui-font-size-card-title: 14px;
+ }
+
+ d-tag {
+ --devui-font-size: var(--devui-font-size-sm, 12px);
+ }
+ // datepicker定制化处理高亮
+ d-datepicker,
+ d-date-range-picker,
+ d-datepicker-range-single,
+ d-two-datepicker-single {
+ --devui-list-item-active-bg: var(--devui-brand, #5e7ce0);
+ --devui-list-item-active-hover-bg: var(--devui-brand, #526ecc);
+ --devui-list-item-active-text: var(--devui-light-text, #ffffff);
+ --devui-font-size: var(--devui-font-size-sm, 12px);
+ }
+
+ // TODO: 表单底层统一使用一致的input,全局修改该input尺寸默认值即可
+ // input default size change to '32px'
+ d-multi-auto-complete .multi-auto-complete label.multiple-label-auto-complete ul.devui-dropdown-origin {
+ padding: 3px 2px 0 2px;
+ min-height: 32px;
+ }
+
+ d-search .devui-search {
+ .devui-input:not(.devui-input-sm):not(.devui-input-lg) {
+ height: 32px;
+ }
+
+ .devui-search-icon {
+ line-height: 32px;
+ }
+
+ .devui-search-clear:not(.devui-search-clear-lg):not(.devui-search-clear-sm) {
+ line-height: 32px;
+ }
+ }
+
+ d-select .devui-form-group {
+ .devui-form-control {
+ &.devui-select-input {
+ height: 30px;
+ }
+ }
+
+ .devui-select-list-wrapper.devui-form-control {
+ .devui-select-placeholder {
+ height: 28px;
+ line-height: 28px;
+ }
+
+ ul.devui-select-tag-list {
+ height: 28px;
+ }
+
+ .devui-select-tag-item {
+ margin: 3px 1px 0;
+ }
+ }
+ }
+
+ d-tree-select {
+ .devui-tree-select .popper .popper-activator .devui-select-input.devui-tree-select-input {
+ padding: 4px 10px;
+ min-height: 32px;
+ max-height: 64px;
+ }
+ }
+
+ input {
+ border-radius: $devui-border-radius;
+ }
+
+ [dTextInput] {
+ height: 32px;
+ }
+
+ .devui-form-controls input[type='text'],
+ .devui-form-controls input[type='password'],
+ [dTextInput] {
+ height: 32px;
+ }
+
+ // select带有搜索存在两层ul
+ .devui-dropdown-menu > ul.devui-dropdown-menu-wrap {
+ // 存在搜索框时
+ & > li {
+ padding: 12px 12px 0 12px;
+ }
+
+ ul {
+ padding: 12px;
+
+ li {
+ border-radius: $devui-border-radius;
+ }
+ }
+ }
+
+ // TODO: 下拉专项整改增加offset设置
+ .devui-dropdown-menu {
+ margin-top: 8px !important;
+ margin-bottom: 8px !important;
+ }
+
+ // breadcrumb dropdown menu
+ div.devui-search-container {
+ padding: 12px 12px 0 12px;
+ }
+ // autoComplete等
+ .devui-dropdown-menu > ul:not(.devui-dropdown-menu-wrap) {
+ padding: 12px;
+
+ li {
+ border-radius: $devui-border-radius;
+ }
+ }
+ // dropdown
+ ul.devui-dropdown-menu {
+ padding: 12px;
+
+ li .devui-dropdown-item {
+ border-radius: $devui-border-radius;
+ }
+ }
+
+ d-tags-input {
+ .devui-tags-autocomplete .devui-suggestion-list {
+ padding: 12px !important;
+ }
+ }
+
+ .devui-input-sm {
+ height: 24px;
+ }
+
+ .devui-input-lg {
+ height: 46px;
+ }
+
+ d-tabs .devui-nav {
+ display: block;
+
+ &.devui-nav-tabs,
+ &.devui-nav-pills {
+ li a {
+ line-height: 32px !important;
+ }
+ }
+ }
+
+ // 表单尺寸未统一,upload内部自定义了高度
+ d-single-upload,
+ d-multiple-upload {
+ .devui-input-group .devui-form-control {
+ min-height: 32px !important;
+
+ .devui-file-tag {
+ height: 26px !important;
+ }
+ }
+ }
+
+ div.popper-container div.popper-container-scrollable div.devui-tree-select.devui-options-container {
+ padding: 12px;
+ }
+
+ div.popper-container div.devui-tree-select span.devui-form-control-feedback {
+ top: 12px;
+ right: 12px;
+ }
+
+ .table-row-selected {
+ td {
+ color: #ffffff !important;
+ }
+ }
+
+ // pagination样式修改
+ d-pagination {
+ div.devui-pagination ul.devui-pagination-list li:not(.disabled) {
+ cursor: pointer;
+
+ a:hover,
+ span:hover,
+ a:focus,
+ span:focus {
+ background-color: transparent !important;
+ color: $devui-text !important;
+ border: 1px solid $devui-dividing-line !important;
+ box-shadow: 0 1px 3px 0 $devui-light-shadow !important;
+ }
+
+ a:active {
+ background-color: transparent !important;
+ color: $devui-text !important;
+ }
+
+ &.active a {
+ background-color: $devui-primary !important;
+ color: $devui-light-text !important;
+ cursor: pointer !important;
+
+ &:hover {
+ background-color: $devui-primary !important;
+ color: $devui-light-text !important;
+ border: 1px solid transparent !important;
+ box-shadow: none !important;
+ }
+ }
+
+ a.devui-pagination-link:hover:not(:active) svg g polygon {
+ fill: $devui-text !important;
+ }
+
+ a.devui-pagination-link:active svg g polygon {
+ fill: $devui-text !important;
+ }
+ }
+
+ ul.devui-pagination-list > li > a {
+ height: 28px !important;
+ min-width: 28px !important;
+ padding: 0 4px !important;
+ justify-content: center !important;
+ border: 1px solid transparent !important;
+ }
+
+ ul.devui-pagination-sm > li > a {
+ height: 24px !important;
+ min-width: 24px !important;
+ }
+
+ ul.devui-pagination-lg > li > a {
+ height: 40px !important;
+ min-width: 40px !important;
+ }
+
+ .devui-pagination-list > li:first-child > a,
+ .devui-pagination-list > li:last-child > a {
+ padding: 0 !important;
+ height: 28px !important;
+ line-height: 28px !important;
+ }
+
+ .devui-pagination-sm > li:first-child > a,
+ .devui-pagination-sm > li:last-child > a {
+ height: 24px !important;
+ line-height: 24px !important;
+ }
+
+ .devui-pagination-lg > li:first-child > a,
+ .devui-pagination-lg > li:last-child > a {
+ height: 40px !important;
+ line-height: 40px !important;
+ }
+
+ .devui-pagination-link {
+ height: 28px !important;
+ line-height: 28px !important;
+ }
+
+ .devui-pagination-sm > li:first-child > a {
+ padding: 0 !important;
+ }
+
+ .devui-pagination-sm > li:last-child > a {
+ padding: 0 !important;
+ }
+ }
+}
+
+body[ui-theme='deep-theme'] {
+ .devui-tree-node__content.active {
+ .devui-tree-node__title {
+ color: #ffffff;
+ }
+
+ svg.svg-icon rect {
+ stroke: #ffffff !important;
+ }
+
+ svg.svg-icon rect:last-child {
+ fill: #ffffff !important;
+ }
+ }
+
+ .table-row-selected {
+ td {
+ color: #ffffff !important;
+ }
+ }
+}
+
+body[ui-theme='infinity-theme'] {
+ d-tabs .devui-nav {
+ --devui-brand: #252b3a;
+ --devui-brand-active: #252b3a;
+ }
+}
+
+body[ui-theme='galaxy-theme'] {
+ d-tabs .devui-nav-slider {
+ --devui-tab-slider-bg: #313131;
+ --devui-text: #a3a3a3;
+ --devui-brand-active: #ffffff;
+ --devui-base-bg: #3f3f3f;
+ }
+
+ d-button .devui-btn-common {
+ --devui-block: transparent;
+ }
+
+ d-button .devui-btn-primary:disabled {
+ --devui-light-text: #838383;
+ }
+}
diff --git a/packages/devui-theme/src/theme-collection/extend-theme.ts b/packages/devui-theme/src/theme-collection/extend-theme.ts
new file mode 100644
index 0000000000..f68888310c
--- /dev/null
+++ b/packages/devui-theme/src/theme-collection/extend-theme.ts
@@ -0,0 +1,213 @@
+import { devuiDarkTheme, devuiLightTheme, Theme } from '../theme';
+export const infinityTheme: Theme = new Theme({
+ id: 'infinity-theme',
+ name: '无限主题',
+ data: { ...devuiLightTheme.data, 'devui-brand-foil': '#F2F2F3',
+ 'devui-global-bg': '#F8F8FA',
+ 'devui-base-bg': '#ffffff',
+ 'devui-text': '#252B3A',
+ 'devui-aide-text': '#71757f',
+ 'devui-placeholder': '#babbc0',
+ 'devui-disabled-text': '#cfd0d3',
+ 'devui-disabled-bg': '#f5f5f6',
+ 'devui-line': '#D7D8DA',
+ 'devui-dividing-line': '#F2F2F3',
+ 'devui-list-item-hover-bg': '#F2F2F3',
+ 'devui-list-item-active-bg': '#F2F5FC',
+ 'devui-list-item-active-hover-bg': '#F2F5FC',
+ 'devui-list-item-selected-bg': '#F2F5FC',
+ 'devui-list-item-hover-text': '#252b3a',
+ 'devui-list-item-active-text': '#252B3A',
+ 'devui-form-control-line-hover': '#A3A6AC',
+ 'devui-form-control-line': '#D7D8DA',
+ 'devui-icon-text': '#babbc0',
+ 'devui-label-bg': '#E9EDFA',
+ 'devui-border-radius': '4px',
+ 'devui-font-size': '14px',
+ 'devui-font-size-md': '14px',
+ 'devui-font-size-card-title': '16px',
+ 'devui-shadow-length-fullscreen-overlay': '0 0 6px 0',
+ 'devui-border-radius-card': '4px'},
+ extends: 'devui-light-theme',
+ isDark: false,
+});
+
+export const provenceTheme: Theme = new Theme({
+ id: 'provence-theme',
+ name: '紫罗兰主题',
+ data: {
+ ...infinityTheme.data,
+ 'devui-brand': '#7B69EE',
+ 'devui-brand-foil': '#F5F5F9',
+ 'devui-brand-active-focus': '#7B69EE',
+ 'devui-primary-active': '#7B69EE',
+ 'devui-brand-hover': '#7B69EE',
+ 'devui-global-bg': '#f9fafb',
+ 'devui-base-bg': '#ffffff',
+ 'devui-text': '#070036',
+ 'devui-aide-text': '#717087',
+ 'devui-placeholder': '#babbc0',
+ 'devui-disabled-text': '#cfd0d3',
+ 'devui-disabled-bg': '#f5f5f6',
+ 'devui-line': '#E2E2E5',
+ 'devui-dividing-line': '#F2F2F3',
+ 'devui-list-item-hover-bg': '#F5F5F9',
+ 'devui-list-item-active-bg': '#7B69EE',
+ 'devui-list-item-active-hover-bg': '#7B69EE',
+ 'devui-list-item-selected-bg': '#F4F2FF',
+ 'devui-list-item-hover-text': '#252b3a',
+ 'devui-list-item-active-text': '#ffffff',
+ 'devui-form-control-line-hover': '#A3A6AC',
+ 'devui-form-control-line': '#D7D8DA',
+ 'devui-icon-text': '#babbc0',
+ 'devui-brand-active': '#7B69EE',
+ 'devui-primary': '#7B69EE',
+ 'devui-primary-hover': '#7B69EE',
+ 'devui-form-control-line-active': '#7B69EE',
+ 'devui-form-control-line-active-hover': '#7B69EE',
+ 'devui-icon-fill-active': '#7B69EE',
+ 'devui-icon-fill-active-hover': '#7B69EE',
+ 'devui-label-bg': '#F4F2FF',
+ 'devui-embed-search-bg': '#F4F2FF',
+ 'devui-connected-overlay-line': '#7B69EE',
+ 'devui-primary-disabled': '#d8d2fa',
+ 'devui-icon-fill-active-disabled': '#d8d2fa'
+
+ },
+ extends: 'infinity-theme',
+ isDark: false
+});
+
+export const sweetTheme: Theme = new Theme({
+ id: 'sweet-theme',
+ name: '蜜糖主题',
+ data: {
+ ...infinityTheme.data,
+ 'devui-brand': '#ec66ab',
+ 'devui-brand-foil': '#f8f1f5',
+ 'devui-brand-active-focus': '#ec66ab',
+ 'devui-primary-active': '#ec66ab',
+ 'devui-brand-hover': '#ec66ab',
+ 'devui-global-bg': '#f9fafb',
+ 'devui-base-bg': '#ffffff',
+ 'devui-text': '#2f272f',
+ 'devui-aide-text': '#827d82',
+ 'devui-placeholder': '#bdb8bd',
+ 'devui-disabled-text': '#cbcacb',
+ 'devui-disabled-bg': '#f6f6f6',
+ 'devui-line': '#aea6ad',
+ 'devui-dividing-line': '#eae7e9',
+ 'devui-list-item-hover-bg': '#f8f1f5',
+ 'devui-list-item-active-bg': '#ffdcee',
+ 'devui-list-item-active-hover-bg': '#ffdcee',
+ 'devui-list-item-selected-bg': '#ffdcee',
+ 'devui-list-item-hover-text': '#252b3a',
+ 'devui-list-item-active-text': '#252b3a',
+ 'devui-form-control-line-hover': '#A3A6AC',
+ 'devui-form-control-line': '#D7D8DA',
+ 'devui-icon-text': '#babbc0',
+ 'devui-brand-active': '#ec66ab',
+ 'devui-primary': '#ec66ab',
+ 'devui-primary-hover': '#ec66ab',
+ 'devui-form-control-line-active': '#ec66ab',
+ 'devui-form-control-line-active-hover': '#ec66ab',
+ 'devui-icon-fill-active': '#ec66ab',
+ 'devui-icon-fill-active-hover': '#ec66ab',
+ 'devui-label-bg': '#ffdcee',
+ 'devui-embed-search-bg': '#ffdcee',
+ 'devui-connected-overlay-line': '#ec66ab',
+ 'devui-primary-disabled': '#fad1e6',
+ 'devui-icon-fill-active-disabled': '#fad1e6',
+ },
+ extends: 'infinity-theme',
+ isDark: false
+});
+
+export const deepTheme: Theme = new Theme({
+ id: 'deep-theme',
+ name: '深邃夜空主题',
+ data: {
+ ...infinityTheme.data,
+ 'devui-brand': '#252b3a',
+ 'devui-brand-foil': '#f3f4f7',
+ 'devui-brand-active-focus': '#252b3a',
+ 'devui-primary-active': '#252b3a',
+ 'devui-brand-active': '#252b3a',
+ 'devui-brand-hover': '#252b3a',
+ 'devui-global-bg': '#f7f8fa',
+ 'devui-base-bg': '#ffffff',
+ 'devui-text': '#252b3a',
+ 'devui-aide-text': '#505c7c',
+ 'devui-placeholder': '#9ba6bf',
+ 'devui-disabled-text': '#a8b1c7',
+ 'devui-disabled-bg': '#f7f8fa',
+ 'devui-line': '#cdd2df',
+ 'devui-dividing-line': '#e6e9ef',
+ 'devui-list-item-hover-bg': '#f3f4f7',
+ 'devui-list-item-active-bg': '#252b3a',
+ 'devui-list-item-active-hover-bg': '#252b3a',
+ 'devui-list-item-selected-bg': '#252b3a',
+ 'devui-list-item-hover-text': '#252b3a',
+ 'devui-list-item-active-text': '#ffffff',
+ 'devui-form-control-line-hover': '#A3A6AC',
+ 'devui-form-control-line': '#D7D8DA',
+ 'devui-icon-text': '#babbc0',
+ 'devui-primary': '#252b3a',
+ 'devui-primary-hover': '#252b3a',
+ 'devui-form-control-line-active': '#252b3a',
+ 'devui-form-control-line-active-hover': '#252b3a',
+ 'devui-icon-fill-active': '#252b3a',
+ 'devui-icon-fill-active-hover': '#252b3a',
+ 'devui-connected-overlay-line': '#252b3a',
+ 'devui-primary-disabled': '#bebfc4',
+ 'devui-icon-fill-active-disabled': '#bebfc4',
+ },
+ extends: 'infinity-theme',
+ isDark: false
+});
+
+export const galaxyTheme: Theme = new Theme({
+ id: 'galaxy-theme',
+ name: '追光主题',
+ data: {
+ ...devuiDarkTheme.data,
+ 'devui-brand-foil': '#F2F2F3',
+ 'devui-global-bg': '#000000',
+ 'devui-base-bg': '#1F1F1F',
+ 'devui-text': '#F5F5F5',
+ 'devui-aide-text': '#A3A3A3',
+ 'devui-placeholder': '#616161',
+ 'devui-disabled-text': '#838383',
+ 'devui-disabled-bg': '#3F3F3F',
+ 'devui-line': '#565656',
+ 'devui-dividing-line': '#303030',
+ 'devui-list-item-hover-bg': '#313131',
+ 'devui-list-item-active-bg': '#30333D',
+ 'devui-list-item-active-hover-bg': '#30333D',
+ 'devui-list-item-selected-bg': '#30333D',
+ 'devui-list-item-hover-text': '#F5F5F5',
+ 'devui-list-item-active-text': '#526ECC',
+ 'devui-primary-disabled': '#3f3f3f',
+ 'devui-form-control-line': '#565656',
+ 'devui-icon-text': '#A3A3A3',
+ 'devui-connected-overlay-bg': '#282828',
+ 'devui-fullscreen-overlay-bg': '#282828',
+ 'devui-warning-line': '#a2622a',
+ 'devui-warning-bg': '#4b2e14',
+ 'devui-success-line': '#27846b ',
+ 'devui-success-bg': '#123d32',
+ 'devui-danger-line': '#9f4844',
+ 'devui-danger-bg': '#4a2120',
+ 'devui-info-line': '#3c5091',
+ 'devui-info-bg': '#1c2543',
+ 'devui-default-bg': '#313131',
+ 'devui-border-radius': '4px',
+ 'devui-font-size': '14px',
+ 'devui-font-size-md': '14px',
+ 'devui-font-size-card-title': '16px',
+ 'devui-shadow-length-fullscreen-overlay': '0 0 6px 0',
+ 'devui-border-radius-card': '4px'
+ },
+ extends: 'devui-dark-theme',
+ isDark: true
+});
diff --git a/packages/devui-theme/src/theme-collection/index.ts b/packages/devui-theme/src/theme-collection/index.ts
new file mode 100644
index 0000000000..7e1a213e3e
--- /dev/null
+++ b/packages/devui-theme/src/theme-collection/index.ts
@@ -0,0 +1 @@
+export * from './public-api';
diff --git a/packages/devui-theme/src/theme-collection/public-api.ts b/packages/devui-theme/src/theme-collection/public-api.ts
new file mode 100644
index 0000000000..dab9199773
--- /dev/null
+++ b/packages/devui-theme/src/theme-collection/public-api.ts
@@ -0,0 +1 @@
+export * from './extend-theme';
diff --git a/packages/devui-theme/src/theme/index.ts b/packages/devui-theme/src/theme/index.ts
new file mode 100644
index 0000000000..7e1a213e3e
--- /dev/null
+++ b/packages/devui-theme/src/theme/index.ts
@@ -0,0 +1 @@
+export * from './public-api';
diff --git a/packages/devui-theme/src/theme/key-config.ts b/packages/devui-theme/src/theme/key-config.ts
new file mode 100644
index 0000000000..e55fcbdf06
--- /dev/null
+++ b/packages/devui-theme/src/theme/key-config.ts
@@ -0,0 +1,10 @@
+export const THEME_KEY = {
+ userLastPreferTheme : 'user-custom-theme', // localStorage 存储的上一次主题名字
+ userLastPreferThemeData: 'user-custom-theme-data', // localStorage 存储的上一次主题的变量,用于骨架屏阶段快速恢复主题
+ currentTheme: 'devuiCurrentTheme', // context(默认window) 当前主题名字
+ themeCollection: 'devuiThemes', // context(默认window) 存储所有主题集合
+ styleElementId: 'devuiThemeVariables', // DOM Style Element 的 id标识, 标记css变量声明的片段
+ transitionStyleElementId: 'devuiThemeColorTransition', // DOM Style Element 的 id标识,标记临时使用的css颜色动画
+ uiThemeAttributeName: 'ui-theme', // body 和 style元素标记用户数据
+ themeService: 'devuiThemeService' // 全局window下的theme service实例
+};
diff --git a/packages/devui-theme/src/theme/media-query.ts b/packages/devui-theme/src/theme/media-query.ts
new file mode 100644
index 0000000000..e159a33432
--- /dev/null
+++ b/packages/devui-theme/src/theme/media-query.ts
@@ -0,0 +1,54 @@
+import * as enquire from 'enquirejs-ssr';
+import { ReplaySubject } from 'rxjs';
+
+export class PrefersColorSchemeMediaQuery {
+ static enquire = enquire; // prevent code optimization excluding enquire out
+ private prefersColorSchemeSubject = new ReplaySubject(1);
+ public prefersColorSchemeChange = this.prefersColorSchemeSubject.asObservable();
+
+ register() {
+ PrefersColorSchemeMediaQuery.enquire
+ .register.bind(enquire)(PrefersColorSchemeMediaQuery.Query.light, {
+ match: () => {
+ this.handleColorSchemeChange('light');
+ }
+ })
+ .register(PrefersColorSchemeMediaQuery.Query.dark, {
+ match: () => {
+ this.handleColorSchemeChange('dark');
+ }
+ });
+ this.prefersColorSchemeSubject.next(this.getInitValue());
+ }
+
+ unregister() {
+ PrefersColorSchemeMediaQuery.enquire
+ .unregister(PrefersColorSchemeMediaQuery.Query.light)
+ .unregister(PrefersColorSchemeMediaQuery.Query.dark);
+ this.prefersColorSchemeSubject.complete();
+ }
+
+ handleColorSchemeChange = (value: PrefersColorSchemeMediaQuery.Value) => {
+ this.prefersColorSchemeSubject.next(value);
+ };
+
+ getInitValue(): PrefersColorSchemeMediaQuery.Value {
+ if (typeof window === 'undefined') {
+ return 'light';
+ }
+
+ return window.matchMedia(PrefersColorSchemeMediaQuery.Query.light).matches && 'light'
+ || window.matchMedia(PrefersColorSchemeMediaQuery.Query.dark).matches && 'dark'
+ || 'no-preference';
+ }
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-namespace */
+export namespace PrefersColorSchemeMediaQuery {
+ export type Value = 'light' | 'dark' | 'no-preference';
+ export enum Query {
+ 'light' = 'screen and (prefers-color-scheme: light)',
+ 'dark' = 'screen and (prefers-color-scheme: dark)',
+ 'noPreferences' = 'screen and (prefers-color-scheme: light)',
+ }
+}
diff --git a/packages/devui-theme/src/theme/public-api.ts b/packages/devui-theme/src/theme/public-api.ts
new file mode 100644
index 0000000000..e6396e2502
--- /dev/null
+++ b/packages/devui-theme/src/theme/public-api.ts
@@ -0,0 +1,5 @@
+export * from './theme';
+export * from './theme-management';
+export * from './theme-data';
+export * from './theme-service';
+export * from './utils/index';
diff --git a/packages/devui-theme/src/theme/theme-data.ts b/packages/devui-theme/src/theme/theme-data.ts
new file mode 100644
index 0000000000..cd82585e08
--- /dev/null
+++ b/packages/devui-theme/src/theme/theme-data.ts
@@ -0,0 +1,324 @@
+import { Theme } from './theme';
+export const devuiLightTheme: Theme = new Theme({
+ id: 'devui-light-theme',
+ name: 'Light Mode',
+ cnName: '浅色主题',
+ data: {
+ // 基础变量
+ 'devui-global-bg': '#f3f6f8',
+ 'devui-global-bg-normal': '#ffffff',
+ 'devui-base-bg': '#ffffff',
+ 'devui-base-bg-dark': '#333854',
+ 'devui-brand': '#5e7ce0',
+ 'devui-brand-foil': '#859bff',
+ 'devui-brand-hover': '#7693f5',
+ 'devui-brand-active': '#526ecc',
+ 'devui-brand-active-focus': '#344899',
+ 'devui-contrast': '#c7000b',
+ 'devui-text': '#252b3a',
+ 'devui-text-weak': '#575d6c',
+ 'devui-aide-text': '#8a8e99',
+ 'devui-aide-text-stress': '#575d6c',
+ 'devui-placeholder': '#8a8e99',
+ 'devui-light-text': '#ffffff',
+ 'devui-dark-text': '#252b3a',
+ 'devui-link': '#526ecc',
+ 'devui-link-active': '#526ecc',
+ 'devui-link-light': '#96adfa',
+ 'devui-link-light-active': '#beccfa',
+ 'devui-line': '#adb0b8',
+ 'devui-dividing-line': '#dfe1e6',
+ 'devui-block': '#ffffff',
+ 'devui-area': '#f8f8f8',
+ 'devui-danger': '#f66f6a',
+ 'devui-warning': '#fac20a',
+ 'devui-waiting': '#9faad7',
+ 'devui-success': '#50d4ab',
+ 'devui-info': '#5e7ce0',
+ 'devui-initial': '#e9edfa',
+ 'devui-unavailable': '#f5f5f6',
+ 'devui-shadow': 'rgba(0, 0, 0, 0.2)',
+ 'devui-light-shadow': 'rgba(0, 0, 0, 0.1)',
+ // 图标
+ 'devui-icon-text': '#252b3a',
+ 'devui-icon-bg': '#ffffff',
+ 'devui-icon-fill': '#d3d5d9',
+ 'devui-icon-fill-hover': '#adb5ce',
+ 'devui-icon-fill-active': '#5e7ce0',
+ 'devui-icon-fill-active-hover': '#526ecc',
+ // 表单
+ 'devui-form-control-line': '#adb0b8',
+ 'devui-form-control-line-hover': '#575d6c',
+ 'devui-form-control-line-active': '#5e7ce0',
+ 'devui-form-control-line-active-hover': '#344899',
+ 'devui-list-item-active-bg': '#5e7ce0',
+ 'devui-list-item-active-text': '#ffffff',
+ 'devui-list-item-active-hover-bg': '#526ecc',
+ 'devui-list-item-hover-bg': '#f2f5fc',
+ 'devui-list-item-hover-text': '#526ecc',
+ 'devui-list-item-selected-bg': '#e9edfa',
+ 'devui-list-item-strip-bg': '#f2f5fc',
+ // 禁用
+ 'devui-disabled-bg': '#f5f5f6',
+ 'devui-disabled-line': '#dfe1e6',
+ 'devui-disabled-text': '#adb0b8',
+ 'devui-primary-disabled': '#beccfa',
+ 'devui-icon-fill-active-disabled': '#beccfa',
+ // 特殊背景色
+ 'devui-label-bg': '#eef0f5',
+ 'devui-connected-overlay-bg': '#ffffff',
+ 'devui-connected-overlay-line': '#526ecc',
+ 'devui-fullscreen-overlay-bg': '#ffffff',
+ 'devui-feedback-overlay-bg': '#464d6e',
+ 'devui-feedback-overlay-text': '#dfe1e6',
+ 'devui-embed-search-bg': '#f2f5fc',
+ 'devui-embed-search-bg-hover': '#eef0f5',
+ 'devui-float-block-shadow': 'rgba(94, 124, 224, 0.3)',
+ 'devui-highlight-overlay': 'rgba(255, 255, 255, 0.8)',
+ 'devui-range-item-hover-bg': '#e9edfa',
+ // 按钮
+ 'devui-primary': '#5e7ce0',
+ 'devui-primary-hover': '#7693f5',
+ 'devui-primary-active': '#344899',
+ 'devui-contrast-hover': '#d64a52',
+ 'devui-contrast-active': '#b12220',
+ // 状态
+ 'devui-danger-line': '#f66f6a',
+ 'devui-danger-bg': '#ffeeed',
+ 'devui-warning-line': '#fa9841',
+ 'devui-warning-bg': '#fff3e8',
+ 'devui-info-line': '#5e7ce0',
+ 'devui-info-bg': '#f2f5fc',
+ 'devui-success-line': '#50d4ab',
+ 'devui-success-bg': '#edfff9',
+ 'devui-primary-line': '#5e7ce0',
+ 'devui-primary-bg': '#f2f5fc',
+ 'devui-default-line': '#5e7ce0',
+ 'devui-default-bg': '#f3f6f8',
+ // 字体设置相关
+ 'devui-font-size': '12px',
+ 'devui-font-size-card-title': '14px',
+ 'devui-font-size-page-title': '16px',
+ 'devui-font-size-modal-title': '18px',
+ 'devui-font-size-price': '20px',
+ 'devui-font-size-data-overview': '24px',
+ 'devui-font-size-icon': '16px',
+ 'devui-font-size-sm': '12px',
+ 'devui-font-size-md': '12px',
+ 'devui-font-size-lg': '14px',
+ 'devui-font-title-weight': 'bold',
+ 'devui-font-content-weight': 'normal',
+ 'devui-line-height-base': '1.5',
+ // 圆角
+ 'devui-border-radius': '2px',
+ 'devui-border-radius-feedback': '4px',
+ 'devui-border-radius-card': '6px',
+ // 阴影
+ 'devui-shadow-length-base': '0 1px 4px 0',
+ 'devui-shadow-length-slide-left': '-2px 0 8px 0',
+ 'devui-shadow-length-slide-right': '2px 0 8px 0',
+ 'devui-shadow-length-connected-overlay': '0 2px 8px 0',
+ 'devui-shadow-length-hover': '0 4px 16px 0',
+ 'devui-shadow-length-feedback-overlay': '0 4px 16px 0',
+ 'devui-shadow-fullscreen-overlay': '0 8px 40px 0',
+ // 动效
+ 'devui-animation-duration-slow': '300ms',
+ 'devui-animation-duration-base': '200ms',
+ 'devui-animation-duration-fast': '100ms',
+ 'devui-animation-ease-in': 'cubic-bezier(0.5, 0, 0.84, 0.25)',
+ 'devui-animation-ease-out': 'cubic-bezier(0.16, 0.75, 0.5, 1)',
+ 'devui-animation-ease-in-out': 'cubic-bezier(0.5, 0.05, 0.5, 0.95)',
+ 'devui-animation-ease-in-out-smooth': 'cubic-bezier(0.645, 0.045, 0.355, 1)',
+ 'devui-animation-linear': 'cubic-bezier(0, 0, 1, 1)',
+ // zIndex
+ 'devui-z-index-full-page-overlay': '1080',
+ 'devui-z-index-pop-up': '1060',
+ 'devui-z-index-dropdown': '1052',
+ 'devui-z-index-modal': '1050',
+ 'devui-z-index-drawer': '1040',
+ 'devui-z-index-framework': '1000'
+ },
+ isDark: false,
+});
+export const devuiGreenTheme: Theme = new Theme({
+ id: 'devui-green-theme',
+ name: 'Green - Light Mode',
+ cnName: '绿色主题',
+ data: { ...devuiLightTheme.data, 'devui-global-bg': '#f3f8f7',
+ 'devui-brand': '#3DCCA6',
+ 'devui-brand-foil': '#7fdac1',
+ 'devui-brand-hover': '#6DDEBB',
+ 'devui-brand-active': '#07c693',
+ 'devui-brand-active-focus': '#369676',
+ 'devui-link': '#07c693',
+ 'devui-link-active': '#07c693',
+ 'devui-link-light': '#96fac8',
+ 'devui-link-light-active': '#befade',
+ 'devui-info': '#079CCD',
+ 'devui-initial': '#CCCCCC',
+ 'devui-icon-fill-active': '#3DCCA6',
+ 'devui-icon-fill-active-hover': '#07c693',
+ 'devui-form-control-line-active': '#3DCCA6',
+ 'devui-form-control-line-active-hover': '#2EB28A',
+ 'devui-list-item-active-bg': '#3DCCA6',
+ 'devui-list-item-active-hover-bg': '#07c693',
+ 'devui-list-item-hover-bg': '#f3fef9',
+ 'devui-list-item-hover-text': '#07c693',
+ 'devui-list-item-selected-bg': '#f3fef9',
+ 'devui-list-item-strip-bg': '#f3fef9',
+ 'devui-connected-overlay-line': '#07c693',
+ 'devui-embed-search-bg': '#f3fef9',
+ 'devui-float-block-shadow': 'rgba(94, 224, 181, 0.3)',
+ 'devui-primary': '#3DCCA6',
+ 'devui-primary-hover': '#6DDEBB',
+ 'devui-primary-active': '#369676',
+ 'devui-info-line': '#0486b1',
+ 'devui-info-bg': '#e3f0f5',
+ 'devui-success-line': '#50d492',
+ 'devui-success-bg': '#edfff9',
+ 'devui-primary-line': '#3DCCA6',
+ 'devui-primary-bg': '#f3fef9',
+ 'devui-default-line': '#3DCCA6',
+ 'devui-default-bg': '#f3f8f7',
+ 'devui-primary-disabled': '#c5f0e5',
+ 'devui-icon-fill-active-disabled': '#c5f0e5',
+ 'devui-range-item-hover-bg': '#d8f9ea',},
+ extends: 'devui-light-theme',
+ isDark: false,
+});
+export const devuiDarkTheme: Theme = new Theme({
+ id: 'devui-dark-theme',
+ name: 'Dark Mode',
+ cnName: '深色主题',
+ data: {
+ 'devui-global-bg': '#202124',
+ 'devui-global-bg-normal': '#202124',
+ 'devui-base-bg': '#2E2F31',
+ 'devui-base-bg-dark': '#2e2f31',
+ 'devui-brand': '#5e7ce0',
+ 'devui-brand-foil': '#313a61',
+ 'devui-brand-hover': '#425288',
+ 'devui-brand-active': '#526ecc',
+ 'devui-brand-active-focus': '#344899',
+ 'devui-contrast': '#C7000B',
+ 'devui-text': '#E8E8E8',
+ 'devui-text-weak': '#A0A0A0',
+ 'devui-aide-text': '#909090',
+ 'devui-aide-text-stress': '#A0A0A0',
+ 'devui-placeholder': '#8A8A8A',
+ 'devui-light-text': '#ffffff',
+ 'devui-dark-text': '#252b3a',
+ 'devui-link': '#526ECC',
+ 'devui-link-active': '#344899',
+ 'devui-link-light': '#96adfa',
+ 'devui-link-light-active': '#beccfa',
+ 'devui-line': '#505153',
+ 'devui-dividing-line': '#3D3E40',
+ 'devui-block': '#606061',
+ 'devui-area': '#34363A',
+ 'devui-danger': '#f66f6a',
+ 'devui-warning': '#fac20a',
+ 'devui-waiting': '#5e6580',
+ 'devui-success': '#50d4ab',
+ 'devui-info': '#5e7ce0',
+ 'devui-initial': '#64676e',
+ 'devui-unavailable': '#5b5b5c',
+ 'devui-shadow': 'rgba(17, 18, 19, 0.4)',
+ 'devui-light-shadow': 'rgba(17, 18, 19, 0.5)',
+ // 图标
+ 'devui-icon-text': '#E8E8E8',
+ 'devui-icon-bg': '#2E2F31',
+ 'devui-icon-fill': '#606061',
+ 'devui-icon-fill-hover': '#73788a',
+ 'devui-icon-fill-active': '#5e7ce0',
+ 'devui-icon-fill-active-hover': '#526ecc',
+ // 表单
+ 'devui-form-control-line': '#505153',
+ 'devui-form-control-line-hover': '#909090',
+ 'devui-form-control-line-active': '#5e7ce0',
+ 'devui-form-control-line-active-hover': '#344899',
+ 'devui-list-item-active-bg': '#5e7ce0',
+ 'devui-list-item-active-text': '#ffffff',
+ 'devui-list-item-active-hover-bg': '#526ecc',
+ 'devui-list-item-hover-bg': '#383838',
+ 'devui-list-item-hover-text': '#526ecc',
+ 'devui-list-item-selected-bg': '#454545',
+ 'devui-list-item-strip-bg': '#383838',
+ // 禁用
+ 'devui-disabled-bg': '#3D3E44',
+ 'devui-disabled-line': '#505153',
+ 'devui-disabled-text': '#7D7D7D',
+ 'devui-primary-disabled': '#2B3458',
+ 'devui-icon-fill-active-disabled': '#2B3458',
+ // 特殊背景色
+ 'devui-label-bg': '#46443F',
+ 'devui-connected-overlay-bg': '#2F2F2F',
+ 'devui-connected-overlay-line': '#526ecc',
+ 'devui-fullscreen-overlay-bg': '#2E2F31',
+ 'devui-feedback-overlay-bg': '#4C4C4C',
+ 'devui-feedback-overlay-text': '#DFE1E6',
+ 'devui-embed-search-bg': '#383838',
+ 'devui-embed-search-bg-hover': '#3D3E40',
+ 'devui-float-block-shadow': 'rgba(94, 124, 224, 0.3)',
+ 'devui-highlight-overlay': 'rgba(255, 255, 255, 0.1)',
+ 'devui-range-item-hover-bg': '#454545',
+ // 按钮
+ 'devui-primary': '#5e7ce0',
+ 'devui-primary-hover': '#425288',
+ 'devui-primary-active': '#344899',
+ 'devui-contrast-hover': '#D64A52',
+ 'devui-contrast-active': '#B12220',
+ // 状态
+ 'devui-danger-line': '#985C5A',
+ 'devui-danger-bg': '#4B3A39',
+ 'devui-warning-line': '#8D6138',
+ 'devui-warning-bg': '#554434',
+ 'devui-info-line': '#546BB7',
+ 'devui-info-bg': '#383D4F',
+ 'devui-success-line': '#5D887D',
+ 'devui-success-bg': '#304642',
+ 'devui-primary-line': '#546BB7',
+ 'devui-primary-bg': '#383D4F',
+ 'devui-default-line': '#5e7ce0',
+ 'devui-default-bg': '#383838',
+ },
+ extends: 'devui-light-theme',
+ isDark: true,
+});
+export const devuiGreenDarkTheme: Theme = new Theme({
+ id: 'devui-green-dark-theme',
+ name: 'Green - Dark Mode',
+ cnName: '绿色深色主题',
+ data: { ...devuiDarkTheme.data, 'devui-brand': '#3DCCA6',
+ 'devui-brand-foil': '#395e54',
+ 'devui-brand-hover': '#4c9780',
+ 'devui-brand-active': '#07c693',
+ 'devui-brand-active-focus': '#297058',
+ 'devui-link': '#07c693',
+ 'devui-link-active': '#08a57b',
+ 'devui-info': '#046788',
+ 'devui-initial': '#64676e',
+ 'devui-icon-fill-active': '#3DCCA6',
+ 'devui-icon-fill-active-hover': '#07c693',
+ 'devui-form-control-line-active': '#3DCCA6',
+ 'devui-form-control-line-active-hover': '#297058',
+ 'devui-list-item-active-bg': '#3DCCA6',
+ 'devui-list-item-active-hover-bg': '#07c693',
+ 'devui-list-item-hover-text': '#07c693',
+ 'devui-connected-overlay-line': '#07c693',
+ 'devui-embed-search-bg': '#3f4241',
+ 'devui-float-block-shadow': 'rgba(94, 224, 181, 0.3)',
+ 'devui-primary': '#3DCCA6',
+ 'devui-primary-hover': '#6DDEBB',
+ 'devui-primary-active': '#369676',
+ 'devui-info-line': '#035e7c',
+ 'devui-info-bg': '#383c3d',
+ 'devui-primary-line': '#3DCCA6',
+ 'devui-primary-bg': '#3f4241',
+ 'devui-default-line': '#3DCCA6',
+ 'devui-default-bg': '#383838',
+ 'devui-primary-disabled': '#28544B',
+ 'devui-icon-fill-active-disabled': '#28544B',},
+ extends: 'devui-dark-theme',
+ isDark: true,
+});
diff --git a/packages/devui-theme/src/theme/theme-management.ts b/packages/devui-theme/src/theme/theme-management.ts
new file mode 100644
index 0000000000..91d34b4258
--- /dev/null
+++ b/packages/devui-theme/src/theme/theme-management.ts
@@ -0,0 +1,105 @@
+import cssVars from 'css-vars-ponyfill';
+import { Subscription } from 'rxjs';
+import { THEME_KEY } from './key-config';
+import { Theme } from './theme';
+import { devuiDarkTheme, devuiLightTheme } from './theme-data';
+import { ThemeService } from './theme-service';
+import { EventBus } from './utils';
+
+/**
+ * usage
+ * main.ts
+ ```ts
+ import { ThemeServiceInit } from 'devui-theme';
+ ThemeServiceInit();
+ ```
+ *
+*/
+export function ThemeServiceInit(
+ themes?: { [themeName: string]: Theme; },
+ defaultThemeName?: string,
+ extraData?: {
+ [themeName: string]: {
+ appendClasses?: Array
+ cssVariables?: {
+ [cssVarName: string]: string
+ }
+ }
+ },
+ ieSupport = false, // TODO:css-var-ponyflll 仍有一些问题待定位
+ allowDynamicTheme = false
+) {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ window[THEME_KEY.themeCollection] = themes || {
+ 'devui-light-theme': devuiLightTheme,
+ 'devui-dark-theme': devuiDarkTheme,
+ };
+ window[THEME_KEY.currentTheme] = defaultThemeName || 'devui-light-theme';
+ const eventBus = window['globalEventBus'] || new EventBus(); // window.globalEventBus 为 框架的事件总线
+ const themeService = new ThemeService(eventBus);
+ window[THEME_KEY.themeService] = themeService;
+
+ themeService.setExtraData(extraData || {
+ 'devui-dark-theme': {
+ appendClasses: ['dark-mode']
+ }
+ });
+ themeService.initializeTheme(null, allowDynamicTheme);
+ if (ieSupport) {
+ ieSupportCssVar();
+ }
+ return themeService;
+}
+
+export function ThemeServiceFollowSystemOn(themeConfig?: { lightThemeName: string; darkThemeName: string; }): Subscription {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ const themeService: ThemeService = window[THEME_KEY.themeService];
+ themeService.registerMediaQuery();
+ return themeService.mediaQuery.prefersColorSchemeChange.subscribe(value => {
+ if (value === 'dark') {
+ themeService.applyTheme(window[THEME_KEY.themeCollection][themeConfig && themeConfig.darkThemeName || 'devui-dark-theme']);
+ } else {
+ themeService.applyTheme(window[THEME_KEY.themeCollection][themeConfig && themeConfig.lightThemeName || 'devui-light-theme']);
+ }
+ });
+}
+export function ThemeServiceFollowSystemOff(sub?: Subscription) {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ if (sub) {
+ sub.unsubscribe();
+ }
+ const themeService = window[THEME_KEY.themeService];
+ themeService.unregisterMediaQuery();
+}
+
+export function ieSupportCssVar() {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ const isNativeSupport = window['CSS'] && CSS.supports && CSS.supports('(--a: 0)') || false;
+ if (isNativeSupport) { return; }
+ cssVars({ watch: true, silent: true });
+ const observer = new MutationObserver(function (mutations) {
+ mutations.forEach(function (mutation) {
+ cssVars({ watch: false, silent: true });
+ cssVars({ watch: true, silent: true });
+ });
+ });
+
+ const config = { attributes: true, attributeFilter: [THEME_KEY.uiThemeAttributeName] };
+
+ observer.observe(document.querySelector(`#${THEME_KEY.styleElementId}`), config);
+}
+
+// TODO: management should handle add / remove theme from theme collection.
+// TODO: move global variables(window.xxxx) to single namespace
diff --git a/packages/devui-theme/src/theme/theme-service.ts b/packages/devui-theme/src/theme/theme-service.ts
new file mode 100644
index 0000000000..8fcec9c922
--- /dev/null
+++ b/packages/devui-theme/src/theme/theme-service.ts
@@ -0,0 +1,189 @@
+import { THEME_KEY } from './key-config';
+import { PrefersColorSchemeMediaQuery } from './media-query';
+import { Theme } from './theme';
+import { ContextService, EventBus, IContextService, IEventBus, IStorageService, StorageService } from './utils/index';
+
+/**
+ * 负责CSS变量主题的装卸,主题元数据转换成主题数据
+ */
+export class ThemeService {
+ eventBus: IEventBus;
+ storage: IStorageService;
+ context: IContextService;
+ currentTheme: Theme;
+ contentElement: HTMLStyleElement;
+ colorTransitionElement: HTMLStyleElement;
+ extraData: {
+ [themeId: string]: {
+ cssVariables?: {
+ [varname: string]: string
+ }
+ appendClasses?: Array
+ }
+ };
+ private _appendedClasses: Array;
+ set appendClasses(classes: Array) {
+ if (this._appendedClasses) {
+ this.removeAppendedClass(this._appendedClasses);
+ }
+ if (classes) {
+ this.addAppendClass(classes);
+ }
+ this._appendedClasses = classes;
+ }
+
+ get appendClasses() {
+ return this._appendedClasses;
+ }
+
+ public mediaQuery: PrefersColorSchemeMediaQuery;
+
+ constructor(eventBus?: IEventBus, storage?: IStorageService, context?: IContextService) {
+ this.eventBus = eventBus === undefined ? new EventBus() : eventBus;
+ this.storage = storage === undefined ? new StorageService() : storage;
+ this.context = context === undefined ? new ContextService() : context;
+ }
+
+ initializeTheme(specificThemeId?: string, allowDynamicTheme?: boolean) {
+ const themeId = specificThemeId
+ || this.storage.tryGetLocalStorage(THEME_KEY.userLastPreferTheme)
+ || this.context.getDataFromNameSpace(THEME_KEY.currentTheme);
+ let theme;
+
+ if (themeId) {
+ const themes = this.context.getDataFromNameSpace(THEME_KEY.themeCollection);
+ if (themes && Object.keys(themes).length > 0) {
+ theme = themes[themeId];
+ }
+ }
+ this.currentTheme = theme || {
+ id: 'empty-theme',
+ name: '',
+ data: {}
+ };
+ this.createColorTransition();
+ if (!theme && allowDynamicTheme) {
+ return;
+ }
+ this.applyTheme(this.currentTheme);
+ }
+
+ formatCSSVariables(themeData: Theme['data']) {
+ return Object.keys(themeData).map(
+ cssVar => ('--' + cssVar + ':' + themeData[cssVar])
+ ).join(';');
+ }
+
+ applyTheme(theme: Theme) {
+ this.addColorTransition();
+ this.currentTheme = theme;
+ if (!this.contentElement) {
+ const styleElement = document.getElementById(THEME_KEY.styleElementId);
+ if (styleElement) {
+ this.contentElement = styleElement;
+ } else {
+ this.contentElement = document.createElement('style');
+ this.contentElement.id = THEME_KEY.styleElementId;
+ document.head.appendChild(this.contentElement);
+ }
+
+ }
+ this.contentElement.innerText = ':root { ' + this.formatCSSVariables(theme.data) + ' }';
+ this.contentElement.setAttribute(THEME_KEY.uiThemeAttributeName, this.currentTheme.id);
+ document.body.setAttribute(THEME_KEY.uiThemeAttributeName, this.currentTheme.id);
+
+ // 用于挂载额外变量和类名
+ this.applyExtraData();
+ this.saveCustomTheme(this.currentTheme);
+
+ // 通知外部主题变更
+ this.notify(theme, 'themeChanged');
+ setTimeout(() => {this.removeColorTransition(); }, 500);
+ }
+
+ saveCustomTheme(customTheme: Theme) {
+ this.storage.trySetLocalStorage(THEME_KEY.userLastPreferTheme, customTheme.id);
+ this.storage.trySetLocalStorage(THEME_KEY.userLastPreferThemeData, JSON.stringify(customTheme.data));
+ this.context.setDataFromNameSpace(THEME_KEY.currentTheme, customTheme.id);
+ }
+
+ private notify(theme: Theme, eventType: string) {
+ if (!this.eventBus) { return; }
+ this.eventBus.trigger(eventType, theme);
+ }
+
+ setEventBus(eb: IEventBus) {
+ this.eventBus = eb;
+ }
+
+ private addAppendClass(classNames: Array) {
+ document.body.classList.add(...classNames);
+ }
+
+ private removeAppendedClass(classNames: Array) {
+ document.body.classList.remove(...classNames);
+ }
+
+ setExtraData(data, apply = false) {
+ this.extraData = data;
+ if (apply) {
+ this.applyExtraData();
+ }
+ }
+
+ private applyExtraData() {
+ const theme = this.currentTheme;
+ if (this.extraData && this.extraData[theme.id] && this.extraData[theme.id].cssVariables) {
+ this.contentElement.innerText
+ = ':root { ' + this.formatCSSVariables(theme.data) + ' }'
+ + ':root { ' + this.formatCSSVariables(this.extraData[theme.id].cssVariables) + ' }';
+ }
+ if (this.extraData && this.extraData[theme.id] && this.extraData[theme.id].appendClasses) {
+ this.appendClasses = this.extraData[theme.id].appendClasses;
+ } else {
+ this.appendClasses = undefined;
+ }
+ }
+
+ public unloadTheme() {
+ if (this.contentElement && document.contains(this.contentElement)) {
+ this.contentElement.parentElement.removeChild(this.contentElement);
+ }
+ if (this.appendClasses) {
+ this.appendClasses = undefined;
+ }
+ }
+
+ public registerMediaQuery() {
+ if (!this.mediaQuery) {
+ this.mediaQuery = new PrefersColorSchemeMediaQuery();
+ }
+ this.mediaQuery.register();
+ }
+
+ public unregisterMediaQuery() {
+ if (!this.mediaQuery) {
+ return;
+ }
+ this.mediaQuery.unregister();
+ this.mediaQuery = undefined;
+ }
+
+ private createColorTransition() {
+ this.colorTransitionElement = document.createElement('style');
+ this.colorTransitionElement.id = THEME_KEY.transitionStyleElementId;
+ this.colorTransitionElement.innerText = `
+ * { transition: background .3s ease-out, background-color .3s ease-out,
+ border .3s ease-out, border-color .3s ease-out,
+ box-shadow .3s ease-out, box-shadow-color .3s ease-out}
+ `;
+ }
+
+ private addColorTransition() {
+ document.head.appendChild(this.colorTransitionElement);
+ }
+ private removeColorTransition() {
+ if (!this.colorTransitionElement.parentElement) {return; }
+ this.colorTransitionElement.parentElement.removeChild(this.colorTransitionElement);
+ }
+}
diff --git a/packages/devui-theme/src/theme/theme.ts b/packages/devui-theme/src/theme/theme.ts
new file mode 100644
index 0000000000..977192b188
--- /dev/null
+++ b/packages/devui-theme/src/theme/theme.ts
@@ -0,0 +1,43 @@
+export type ThemeId = string;
+
+export class Theme {
+ id: ThemeId;
+ name: string;
+ cnName?: string;
+ data: {
+ [cssVarName: string]: string
+ };
+ extends?: ThemeId;
+ isDark?: boolean;
+ isPreview?: boolean;
+ isExtendable?: boolean;
+ extra?: {
+ appendClass?: Array
+ cssVariables?: {
+ [cssVarName: string]: string
+ }
+ [prop: string]: any
+ } | any;
+
+ constructor(theme: {
+ id: ThemeId
+ name: string
+ cnName?: string
+ data: {
+ [cssVarName: string]: string
+ }
+ extends?: ThemeId
+ isDark?: boolean
+ isPreview?: boolean
+ isExtendable?: boolean
+ }) {
+ this.id = theme.id;
+ this.name = theme.name;
+ this.cnName = theme.cnName || this.name;
+ this.data = theme.data;
+ this.extends = theme.extends || null;
+ this.isDark = theme.isDark || undefined;
+ this.isPreview = theme.isPreview || false;
+ this.isExtendable = theme.isExtendable || true;
+ }
+}
diff --git a/packages/devui-theme/src/theme/utils/context-service.ts b/packages/devui-theme/src/theme/utils/context-service.ts
new file mode 100644
index 0000000000..577f966843
--- /dev/null
+++ b/packages/devui-theme/src/theme/utils/context-service.ts
@@ -0,0 +1,17 @@
+import { IContextService } from './interface';
+export class ContextService implements IContextService {
+ getDataFromNameSpace(nameSpace: string) {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ return window[nameSpace];
+ }
+ setDataFromNameSpace(nameSpace: string, value: any) {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ window[nameSpace] = value;
+ }
+}
diff --git a/packages/devui-theme/src/theme/utils/event-bus.ts b/packages/devui-theme/src/theme/utils/event-bus.ts
new file mode 100644
index 0000000000..0eb9218e4f
--- /dev/null
+++ b/packages/devui-theme/src/theme/utils/event-bus.ts
@@ -0,0 +1,87 @@
+import { IEventBus } from './interface';
+
+export class EventBus implements IEventBus {
+ private eventBusCore = [];
+ private areFuncEqual(a, b) {
+ return a.toString() === b.toString();
+ }
+ private isKeyValueObjInArr(arr, key, val) {
+ const filteredArr = arr.filter(entry => {
+ return entry[key] === val;
+ });
+ return filteredArr.length > 0;
+ }
+ private removeFuncInFuncArr(arr, fn) {
+ for (let z = 0; z < arr.length; z++) {
+ if (this.areFuncEqual(arr[z], fn)) {
+ arr.splice(z, 1);
+ }
+ }
+ return arr;
+ }
+ private getKeyValueObjInArr(arr, key, val) {
+ const filteredArr = arr.filter(entry => {
+ return entry[key] === val;
+ });
+ return filteredArr[0];
+ }
+ private addEvent(eventName, eventFunc) {
+ if (!this.isKeyValueObjInArr(this.eventBusCore, 'eventName', eventName)) {
+ this.eventBusCore.push({ eventName: eventName, eventFuncArr: [eventFunc] });
+ } else {
+ this.eventBusCore = this.eventBusCore.map(event => {
+ if (event['eventName'] === eventName) {
+ event.eventFuncArr.push(eventFunc);
+ }
+ return event;
+ });
+ }
+ }
+ public add(eventName, callbacks) {
+ if (!eventName) {
+ return;
+ }
+ if (typeof callbacks === 'function') {
+ for (let i = 1; i < arguments.length; i++) {
+ this.addEvent(eventName, arguments[i]);
+ }
+ }
+ if (typeof callbacks === 'object' && callbacks.forEach) {
+ callbacks.forEach(fn => {
+ this.addEvent(eventName, fn);
+ });
+ }
+ }
+ public remove(eventName, callbacks) {
+ if (!eventName) {
+ return;
+ }
+ for (let i = 0; i < this.eventBusCore.length; i++) {
+ if (this.eventBusCore[i].eventName === eventName) {
+ if (arguments.length === 1) {
+ return this.eventBusCore.splice(i, 1);
+ }
+ const removedEvent = this.eventBusCore.splice(i, 1)[0];
+ if (typeof callbacks === 'function') {
+ for (let k = 1; k < arguments.length; k++) {
+ removedEvent.eventFuncArr = this.removeFuncInFuncArr(removedEvent.eventFuncArr, arguments[k]);
+ }
+ }
+ if (typeof callbacks === 'object' && callbacks.length) {
+ for (let x = 0; x < callbacks.length; x++) {
+ removedEvent.eventFuncArr = this.removeFuncInFuncArr(removedEvent.eventFuncArr, callbacks[x]);
+ }
+ }
+ this.eventBusCore.push(removedEvent);
+ }
+ }
+ }
+ public trigger(eventName, data) {
+ const event = this.getKeyValueObjInArr(this.eventBusCore, 'eventName', eventName);
+ if (event) {
+ (event.eventFuncArr || []).forEach(fn => {
+ fn.apply(this, data);
+ });
+ }
+ }
+}
diff --git a/packages/devui-theme/src/theme/utils/index.ts b/packages/devui-theme/src/theme/utils/index.ts
new file mode 100644
index 0000000000..7c70488766
--- /dev/null
+++ b/packages/devui-theme/src/theme/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './interface';
+export * from './event-bus';
+export * from './context-service';
+export * from './storage-service';
diff --git a/packages/devui-theme/src/theme/utils/interface.ts b/packages/devui-theme/src/theme/utils/interface.ts
new file mode 100644
index 0000000000..f10233e9bd
--- /dev/null
+++ b/packages/devui-theme/src/theme/utils/interface.ts
@@ -0,0 +1,15 @@
+export interface IStorageService {
+ tryGetLocalStorage(key: string): any
+ trySetLocalStorage(key: string, value: any): void
+}
+
+export interface IContextService {
+ getDataFromNameSpace(nameSpace: string): any
+ setDataFromNameSpace(nameSpace: string, value: any): any
+}
+
+export interface IEventBus {
+ add(eventName: string, callbacks: Function): void
+ remove(eventName: string, callbacks: Function): void
+ trigger(eventName: string, data: any): void
+}
diff --git a/packages/devui-theme/src/theme/utils/storage-service.ts b/packages/devui-theme/src/theme/utils/storage-service.ts
new file mode 100644
index 0000000000..773c704069
--- /dev/null
+++ b/packages/devui-theme/src/theme/utils/storage-service.ts
@@ -0,0 +1,17 @@
+import { IStorageService } from './interface';
+export class StorageService implements IStorageService {
+ tryGetLocalStorage(key: string) {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ return window.localStorage.getItem(key);
+ }
+ trySetLocalStorage(key: string, value: any) {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ window.localStorage.setItem(key, value);
+ }
+}
diff --git a/packages/devui-theme/tsconfig.json b/packages/devui-theme/tsconfig.json
new file mode 100644
index 0000000000..af31eb8152
--- /dev/null
+++ b/packages/devui-theme/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/packages/devui-theme/tsconfig.node.json b/packages/devui-theme/tsconfig.node.json
new file mode 100644
index 0000000000..e993792cb1
--- /dev/null
+++ b/packages/devui-theme/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "esnext",
+ "moduleResolution": "node"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/packages/devui-theme/vite.config.ts b/packages/devui-theme/vite.config.ts
new file mode 100644
index 0000000000..d89419e5fb
--- /dev/null
+++ b/packages/devui-theme/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite'
+import { resolve } from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ publicDir: false,
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'src/index.ts'),
+ name: 'DevuiTheme',
+ fileName: (format) => `index.${format}.js`
+ },
+ rollupOptions: {
+ // 确保外部化处理那些你不想打包进库的依赖
+ external: ['vue'],
+ output: {
+ // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
+ globals: {
+ vue: 'Vue'
+ }
+ }
+ }
+ }
+})
diff --git a/packages/devui-vue/.eslintrc.js b/packages/devui-vue/.eslintrc.js
deleted file mode 100644
index 89aa01109a..0000000000
--- a/packages/devui-vue/.eslintrc.js
+++ /dev/null
@@ -1,48 +0,0 @@
-module.exports = {
- root: true,
- parser: '@typescript-eslint/parser',
- parserOptions: {
- sourceType: 'module',
- ecmaVersion: 6,
- ecmaFeatures: {
- jsx: true,
- tsx: true
- }
- },
- env: {
- browser: true,
- node: true,
- jest: true,
- es6: true
- },
- plugins: ['@typescript-eslint'],
- extends: [
- 'plugin:@typescript-eslint/recommended',
- 'plugin:vue/vue3-recommended',
- 'plugin:import/recommended',
- 'plugin:import/typescript'
- ],
- rules: {
- quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
- 'no-undef': 2,
- 'vue/max-attributes-per-line': 'off',
- 'vue/no-multiple-template-root': 'off',
- 'vue/script-setup-uses-vars': 'off',
- '@typescript-eslint/no-explicit-any': 'off',
- '@typescript-eslint/member-delimiter-style': [
- 'error',
- {
- multiline: {
- delimiter: 'none',
- requireLast: false
- },
- singleline: {
- delimiter: 'semi',
- requireLast: true
- }
- }
- ],
- 'no-unused-vars': 'off',
- '@typescript-eslint/no-unused-vars': ['error']
- }
-}
diff --git a/packages/devui-vue/.husky/commit-msg b/packages/devui-vue/.husky/commit-msg
deleted file mode 100755
index 80e240c9df..0000000000
--- a/packages/devui-vue/.husky/commit-msg
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-cd ./packages/devui-vue && npx commitlint --edit $1
\ No newline at end of file
diff --git a/packages/devui-vue/.husky/pre-commit b/packages/devui-vue/.husky/pre-commit
deleted file mode 100755
index 9a45e99d4e..0000000000
--- a/packages/devui-vue/.husky/pre-commit
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-cd ./packages/devui-vue && npx @ls-lint/ls-lint && npx lint-staged
diff --git a/packages/devui-vue/.yarnrc b/packages/devui-vue/.yarnrc
deleted file mode 100644
index f4074f57d7..0000000000
--- a/packages/devui-vue/.yarnrc
+++ /dev/null
@@ -1 +0,0 @@
-registry "https://registry.npm.taobao.org"
\ No newline at end of file
diff --git a/packages/devui-vue/LICENSE b/packages/devui-vue/LICENSE
deleted file mode 100644
index e3a2b0425c..0000000000
--- a/packages/devui-vue/LICENSE
+++ /dev/null
@@ -1,23 +0,0 @@
-MIT License
-
-Copyright (c) 2019 - present DevUI.
-Copyright (c) 2019 - present Huawei Technologies Co., Ltd.
-
-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.
-
diff --git a/packages/devui-vue/dc.config.ts b/packages/devui-vue/dc.config.ts
new file mode 100644
index 0000000000..6b5ac6d109
--- /dev/null
+++ b/packages/devui-vue/dc.config.ts
@@ -0,0 +1,10 @@
+import { defineCliConfig } from 'devui-cli';
+
+export default defineCliConfig({
+ componentRootDir: './devui',
+ libClassPrefix: 'devui',
+ libEntryFileName: 'vue-devui',
+ libEntryRootDir: './devui',
+ libPrefix: 'D',
+ libStyleFileSuffix: '.scss'
+})
diff --git a/packages/devui-vue/devui-cli/commands/build.js b/packages/devui-vue/devui-cli/commands/build.js
index 1795406945..5e0ae1ddb3 100644
--- a/packages/devui-vue/devui-cli/commands/build.js
+++ b/packages/devui-vue/devui-cli/commands/build.js
@@ -1,28 +1,28 @@
-const path = require('path')
-const fs = require('fs')
-const fsExtra = require('fs-extra')
-const { defineConfig, build } = require('vite')
-const vue = require('@vitejs/plugin-vue')
-const vueJsx = require('@vitejs/plugin-vue-jsx')
-const nuxtBuild = require('./build-nuxt-auto-import')
+const path = require('path');
+const fs = require('fs');
+const fsExtra = require('fs-extra');
+const { defineConfig, build } = require('vite');
+const vue = require('@vitejs/plugin-vue');
+const vueJsx = require('@vitejs/plugin-vue-jsx');
+const nuxtBuild = require('./build-nuxt-auto-import');
-const entryDir = path.resolve(__dirname, '../../devui')
-const outputDir = path.resolve(__dirname, '../../build')
+const entryDir = path.resolve(__dirname, '../../devui');
+const outputDir = path.resolve(__dirname, '../../build');
const baseConfig = defineConfig({
configFile: false,
publicDir: false,
- plugins: [vue(), vueJsx()]
-})
+ plugins: [vue(), vueJsx()],
+});
const rollupOptions = {
- external: ['vue', 'vue-router'],
+ external: ['vue', 'vue-router', '@vueuse/core', '@floating-ui/dom'],
output: {
globals: {
- vue: 'Vue'
- }
- }
-}
+ vue: 'Vue',
+ },
+ },
+};
const buildSingle = async (name) => {
await build(
@@ -34,13 +34,13 @@ const buildSingle = async (name) => {
entry: path.resolve(entryDir, name),
name: 'index',
fileName: 'index',
- formats: ['es', 'umd']
+ formats: ['es', 'umd'],
},
- outDir: path.resolve(outputDir, name)
- }
+ outDir: path.resolve(outputDir, name),
+ },
})
- )
-}
+ );
+};
const buildAll = async () => {
await build(
@@ -50,15 +50,15 @@ const buildAll = async () => {
rollupOptions,
lib: {
entry: path.resolve(entryDir, 'vue-devui.ts'),
- name: 'vue-devui',
+ name: 'VueDevui',
fileName: 'vue-devui',
- formats: ['es', 'umd']
+ formats: ['es', 'umd'],
},
- outDir: outputDir
- }
+ outDir: outputDir,
+ },
})
- )
-}
+ );
+};
const createPackageJson = (name) => {
const fileStr = `{
@@ -67,25 +67,25 @@ const createPackageJson = (name) => {
"main": "index.umd.js",
"module": "index.es.js",
"style": "style.css"
-}`
+}`;
- fsExtra.outputFile(path.resolve(outputDir, `${name}/package.json`), fileStr, 'utf-8')
-}
+ fsExtra.outputFile(path.resolve(outputDir, `${name}/package.json`), fileStr, 'utf-8');
+};
exports.build = async () => {
- await buildAll()
+ await buildAll();
const components = fs.readdirSync(entryDir).filter((name) => {
- const componentDir = path.resolve(entryDir, name)
- const isDir = fs.lstatSync(componentDir).isDirectory()
- return isDir && fs.readdirSync(componentDir).includes('index.ts')
- })
+ const componentDir = path.resolve(entryDir, name);
+ const isDir = fs.lstatSync(componentDir).isDirectory();
+ return isDir && fs.readdirSync(componentDir).includes('index.ts');
+ });
for (const name of components) {
- await buildSingle(name)
- createPackageJson(name)
- nuxtBuild.createAutoImportedComponent(name)
+ await buildSingle(name);
+ createPackageJson(name);
+ nuxtBuild.createAutoImportedComponent(name);
}
- nuxtBuild.createNuxtPlugin()
-}
+ nuxtBuild.createNuxtPlugin();
+};
diff --git a/packages/devui-vue/devui-cli/shared/utils.js b/packages/devui-vue/devui-cli/shared/utils.js
index 11fb60c706..0b1f1c4b8e 100644
--- a/packages/devui-vue/devui-cli/shared/utils.js
+++ b/packages/devui-vue/devui-cli/shared/utils.js
@@ -3,8 +3,8 @@ const { INDEX_FILE_NAME, DEVUI_DIR } = require('./constant')
const { resolve } = require('path')
const logger = require('./logger')
const fs = require('fs-extra')
-const traverse = require("@babel/traverse").default
-const babelParser = require("@babel/parser")
+const traverse = require('@babel/traverse').default
+const babelParser = require('@babel/parser')
exports.bigCamelCase = (str) => {
return upperFirst(camelCase(str))
diff --git a/packages/devui-vue/devui/accordion/src/accordion-item-hreflink.tsx b/packages/devui-vue/devui/accordion/src/accordion-item-hreflink.tsx
new file mode 100644
index 0000000000..9ac9501716
--- /dev/null
+++ b/packages/devui-vue/devui/accordion/src/accordion-item-hreflink.tsx
@@ -0,0 +1,127 @@
+import { defineComponent, toRefs, computed, inject } from 'vue'
+import { accordionProps } from './accordion-types'
+import { AccordionItemClickEvent, AccordionMenuItem, AccordionLinkableItem } from './accordion.type'
+import DAccordionItem from './accordion-item'
+import { getRootSlots } from './utils'
+
+export default defineComponent({
+ name: 'DAccordionItemHreflink',
+ component: {
+ DAccordionItem
+ },
+ props: {
+ item: Object as () => AccordionLinkableItem,
+ deepth: {
+ type: Number,
+ default: 0
+ },
+ parent: {
+ type: Object as () => AccordionMenuItem,
+ default: null
+ },
+ ...accordionProps
+ },
+ setup(props) {
+ const {
+ item,
+ deepth,
+ parent,
+ titleKey,
+ linkKey,
+ linkTargetKey,
+ linkDefaultTarget,
+ // activeKey,
+ disabledKey,
+ itemTemplate
+ } = toRefs(props)
+
+ const rootSlots = getRootSlots()
+ const accordionCtx = inject('accordionContext') as any
+
+ const title = computed(() => {
+ return item.value && item.value[titleKey.value]
+ })
+
+ const link = computed(() => {
+ return item.value && item.value[linkKey.value]
+ })
+
+ // const active = computed(() => {
+ // return item.value && item.value[activeKey.value]
+ // })
+
+ // const childActived = computed(() => {
+ // return active.value
+ // })
+
+ const target = computed(() => {
+ return item.value && (item.value[linkTargetKey.value] || linkDefaultTarget.value)
+ })
+
+ const disabled = computed(() => {
+ return item.value && item.value[disabledKey.value]
+ })
+
+ const parentValue = parent.value
+ const deepValue = deepth.value
+
+ const linkItemClickFn = (itemEvent: AccordionItemClickEvent) => {
+ if (item.value && !disabled.value) {
+ accordionCtx.itemClickFn(itemEvent)
+ }
+ }
+
+ const renderContent = () => {
+ return (
+ <>
+
+ {(!rootSlots.itemTemplate || itemTemplate.value === false) && <>{title.value}>}
+ {rootSlots.itemTemplate &&
+ itemTemplate.value !== false &&
+ rootSlots.itemTemplate?.({
+ parent: parentValue,
+ deepth: deepValue,
+ item: item.value
+ })}
+ >
+ )
+ }
+
+ return () => {
+ return (
+ <>
+
+ >
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/accordion/src/accordion-item-routerlink.tsx b/packages/devui-vue/devui/accordion/src/accordion-item-routerlink.tsx
new file mode 100644
index 0000000000..44e55522bb
--- /dev/null
+++ b/packages/devui-vue/devui/accordion/src/accordion-item-routerlink.tsx
@@ -0,0 +1,147 @@
+import { defineComponent, toRefs, computed, inject } from 'vue'
+import { useRoute } from 'vue-router'
+import { accordionProps } from './accordion-types'
+import { AccordionItemClickEvent, AccordionMenuItem, AccordionLinkableItem } from './accordion.type'
+import DAccordionItem from './accordion-item'
+import { getRootSlots } from './utils'
+
+export default defineComponent({
+ name: 'DAccordionItemRouterlink',
+ component: {
+ DAccordionItem
+ },
+ props: {
+ item: Object as () => AccordionLinkableItem,
+ deepth: {
+ type: Number,
+ default: 0
+ },
+ parent: {
+ type: Object as () => AccordionMenuItem,
+ default: null
+ },
+ ...accordionProps
+ },
+ setup(props) {
+ const {
+ item,
+ deepth,
+ parent,
+ titleKey,
+ linkKey,
+ linkDefaultTarget,
+ disabledKey,
+ itemTemplate
+ } = toRefs(props)
+
+ const route = useRoute()
+ const rootSlots = getRootSlots()
+ const accordionCtx = inject('accordionContext') as any
+ console.log(useRoute())
+
+ const title = computed(() => {
+ return item.value && item.value[titleKey.value]
+ })
+
+ const link = computed(() => {
+ return item.value && item.value[linkKey.value]
+ })
+
+ const isUsedVueRouter = computed(() => route !== undefined)
+
+ const routerLinkActive = computed(() => {
+ return route === link.value
+ })
+
+ const disabled = computed(() => {
+ return item.value && item.value[disabledKey.value]
+ })
+
+ const parentValue = parent.value
+ const deepValue = deepth.value
+
+ const linkItemClickFn = (itemEvent: AccordionItemClickEvent) => {
+ if (item.value && !disabled.value) {
+ accordionCtx.itemClickFn(itemEvent)
+ }
+ }
+
+ const renderContent = () => {
+ return (
+ <>
+
+ {(!rootSlots.itemTemplate || itemTemplate.value === false) && <>{title.value}>}
+ {rootSlots.itemTemplate &&
+ itemTemplate.value !== false &&
+ rootSlots.itemTemplate?.({
+ parent: parentValue,
+ deepth: deepValue,
+ item: item.value
+ })}
+ >
+ )
+ }
+
+ return () => {
+ return (
+ <>
+
+ >
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/accordion/src/accordion-item.tsx b/packages/devui-vue/devui/accordion/src/accordion-item.tsx
index 14e98c7acb..a20b8a919d 100644
--- a/packages/devui-vue/devui/accordion/src/accordion-item.tsx
+++ b/packages/devui-vue/devui/accordion/src/accordion-item.tsx
@@ -31,8 +31,8 @@ export default defineComponent({
const rootSlots = getRootSlots()
const accordionCtx = inject('accordionContext') as any
- let parentValue = parent.value
- let deepValue = deepth.value
+ const parentValue = parent.value
+ const deepValue = deepth.value
const disabled = computed(() => {
return item.value && item.value[disabledKey.value]
diff --git a/packages/devui-vue/devui/accordion/src/accordion-list.tsx b/packages/devui-vue/devui/accordion/src/accordion-list.tsx
index 4852054957..e079dcc030 100644
--- a/packages/devui-vue/devui/accordion/src/accordion-list.tsx
+++ b/packages/devui-vue/devui/accordion/src/accordion-list.tsx
@@ -1,22 +1,21 @@
-import {
- computed,
- defineComponent,
- inject,
- toRefs
-} from 'vue'
+import { computed, defineComponent, inject, toRefs } from 'vue'
import type { AccordionMenuItem } from './accordion.type'
import DAccordionMenu from './accordion-menu'
import DAccordionItem from './accordion-item'
+import DAccordionItemHreflink from './accordion-item-hreflink'
+import DAccordionItemRouterlink from './accordion-item-routerlink'
import { accordionProps } from './accordion-types'
import { getRootSlots } from '../src/utils'
export default defineComponent({
name: 'DAccordionList',
- inheritAttrs: false,
components: {
DAccordionMenu,
- DAccordionItem
+ DAccordionItem,
+ DAccordionItemHreflink,
+ DAccordionItemRouterlink
},
+ inheritAttrs: false,
props: {
data: {
type: Array as () => Array,
@@ -44,13 +43,14 @@ export default defineComponent({
showNoContent,
loadingKey,
titleKey,
+ linkTypeKey,
loadingTemplate,
noContentTemplate,
innerListTemplate
} = toRefs(props)
- let parentValue = parent.value
- let deepValue = deepth.value
+ const parentValue = parent.value
+ const deepValue = deepth.value
const rootSlots = getRootSlots()
@@ -59,15 +59,18 @@ export default defineComponent({
const loading = computed(() => {
return parentValue && parentValue[loadingKey.value]
})
+
const noContent = computed(() => {
- let dataValue = data.value
+ const dataValue = data.value
return dataValue === undefined || dataValue === null || dataValue.length === 0
})
return () => {
return (
<>
- {(!rootSlots.innerListTemplate || deepth.value === 0 || innerListTemplate.value === false) && (
+ {(!rootSlots.innerListTemplate ||
+ deepth.value === 0 ||
+ innerListTemplate.value === false) && (
{data.value.map((item) => {
return (
@@ -89,12 +92,60 @@ export default defineComponent({
{/* 普通类型 */}
{(!linkType.value || linkType.value === '') && (
)}
+ {/* 路由链接类型 */}
+ {linkType.value === 'routerLink' && (
+
+ )}
+ {/* 普通链接类型 */}
+ {linkType.value === 'hrefLink' && (
+
+ )}
+ {/* 动态链接类型 */}
+ {linkType.value === 'dependOnLinkTypeKey' && (
+ <>
+ {item[linkTypeKey.value] === 'routerLink' && (
+
+ )}
+ {item[linkTypeKey.value] === 'hrefLink' && (
+
+ )}
+ {item[linkTypeKey.value] !== 'routerLink' &&
+ item[linkTypeKey.value] !== 'hrefLink' && (
+
+ )}
+ >
+ )}
>
)}
@@ -130,7 +181,7 @@ export default defineComponent({
{
// 自定义加载
loading.value &&
- rootSlots.loadingTemplate &&
+ rootSlots.loadingTemplate &&
loadingTemplate.value !== false &&
rootSlots.loadingTemplate?.({
item: parentValue,
diff --git a/packages/devui-vue/devui/accordion/src/accordion-menu.tsx b/packages/devui-vue/devui/accordion/src/accordion-menu.tsx
index a2ca212abc..94d525a751 100644
--- a/packages/devui-vue/devui/accordion/src/accordion-menu.tsx
+++ b/packages/devui-vue/devui/accordion/src/accordion-menu.tsx
@@ -39,8 +39,8 @@ export default defineComponent({
const rootSlots = getRootSlots()
const accordionCtx = inject('accordionContext') as any
- let parentValue = parent.value
- let deepValue = deepth.value
+ const parentValue = parent.value
+ const deepValue = deepth.value
const toggle = (itemEvent: AccordionMenuToggleEvent) => {
accordionCtx.menuToggleFn(itemEvent)
diff --git a/packages/devui-vue/devui/accordion/src/accordion-types.ts b/packages/devui-vue/devui/accordion/src/accordion-types.ts
index 2aa7396ccc..a2cf54dacc 100644
--- a/packages/devui-vue/devui/accordion/src/accordion-types.ts
+++ b/packages/devui-vue/devui/accordion/src/accordion-types.ts
@@ -1,5 +1,5 @@
-import { ExtractPropTypes } from "vue";
-import { AccordionMenuType } from "./accordion.type";
+import { ExtractPropTypes } from 'vue';
+import { AccordionMenuType } from './accordion.type';
export const accordionProps = {
data: {
@@ -7,12 +7,12 @@ export const accordionProps = {
default: null,
},
/* Key值定义, 用于自定义数据结构 */
- titleKey: { type: String, default: "title" }, // 标题的key,item[titleKey]类型为string,为标题显示内容
- loadingKey: { type: String, default: "loading" }, // 子菜单动态加载item[loadingKey]类型为boolean
- childrenKey: { type: String, default: "children" }, // 子菜单Key
- disabledKey: { type: String, default: "disabled" }, // 是否禁用Key
- activeKey: { type: String, default: "active" }, // 菜单是否激活/选中
- openKey: { type: String, default: "open" }, // 菜单是否打开
+ titleKey: { type: String, default: 'title' }, // 标题的key,item[titleKey]类型为string,为标题显示内容
+ loadingKey: { type: String, default: 'loading' }, // 子菜单动态加载item[loadingKey]类型为boolean
+ childrenKey: { type: String, default: 'children' }, // 子菜单Key
+ disabledKey: { type: String, default: 'disabled' }, // 是否禁用Key
+ activeKey: { type: String, default: 'active' }, // 菜单是否激活/选中
+ openKey: { type: String, default: 'open' }, // 菜单是否打开
/* 菜单模板 */
menuItemTemplate: { type: Boolean, default: true }, // 可展开菜单内容条模板
@@ -42,21 +42,21 @@ export const accordionProps = {
/* 内置路由/链接/动态判断路由或链接类型 */
linkType: {
type: String as () =>
- | "routerLink"
- | "hrefLink"
- | "dependOnLinkTypeKey"
- | ""
+ | 'routerLink'
+ | 'hrefLink'
+ | 'dependOnLinkTypeKey'
+ | ''
| string,
- default: "",
+ default: '',
},
- linkTypeKey: { type: String, default: "linkType" }, // linkType为'dependOnLinkTypeKey'时指定对象linkType定义区
- linkKey: { type: String, default: "link" }, // 链接内容的key
- linkTargetKey: { type: String, default: "target" }, // 链接目标窗口的key
- linkDefaultTarget: { type: String, default: "_self" }, // 不设置target的时候target默认值
+ linkTypeKey: { type: String, default: 'linkType' }, // linkType为'dependOnLinkTypeKey'时指定对象linkType定义区
+ linkKey: { type: String, default: 'link' }, // 链接内容的key
+ linkTargetKey: { type: String, default: 'target' }, // 链接目标窗口的key
+ linkDefaultTarget: { type: String, default: '_self' }, // 不设置target的时候target默认值
accordionType: {
- type: String as () => "normal" | "embed",
- default: "normal",
+ type: String as () => 'normal' | 'embed',
+ default: 'normal',
},
} as const;
diff --git a/packages/devui-vue/devui/accordion/src/accordion.scss b/packages/devui-vue/devui/accordion/src/accordion.scss
index 34a19882a3..44bd109036 100644
--- a/packages/devui-vue/devui/accordion/src/accordion.scss
+++ b/packages/devui-vue/devui/accordion/src/accordion.scss
@@ -5,10 +5,13 @@
@import '../../style/theme/corner';
@import '../../style/core/animation';
-.devui-accordion-menu ol, ul {
- margin: 0 !important;
- line-height: 0 !important;
- font-size: 12px;
+.devui-accordion-menu {
+ ol,
+ ul {
+ margin: 0 !important;
+ line-height: 0 !important;
+ font-size: 12px;
+ }
}
:host {
@@ -118,7 +121,7 @@ d-accordion-item-routerlink {
/* 展开图标相关 */
.devui-accordion-menu-item > .devui-accordion-item-title {
position: relative;
-
+
& > .devui-accordion-open-icon {
display: inline-block;
text-indent: 0;
@@ -175,8 +178,8 @@ d-accordion-item-routerlink {
.devui-accordion-list .devui-accordion-menu-hidden {
// display: none;
- opacity: 0;
- height: 0px;
+ opacity: 0;
+ height: 0;
overflow: hidden;
}
diff --git a/packages/devui-vue/devui/accordion/src/composables/use-active.ts b/packages/devui-vue/devui/accordion/src/composables/use-active.ts
deleted file mode 100644
index 0ed36c8eaa..0000000000
--- a/packages/devui-vue/devui/accordion/src/composables/use-active.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { ref, Ref, toRefs } from 'vue'
-import { AccordionProps } from '../accordion-types'
-import { AccordionItemClickEvent, AccordionMenuItem } from '../accordion.type'
-
-import { flatten } from '../utils'
-
-type activeObjectRef = {
- [key: string]: {
- isDisable: boolean
- isActive: boolean
- }
-}
-
-type TypeUseActive = (
- props: AccordionProps,
- emit: (arg0: string, arg1: AccordionMenuItem) => void
-) => {
- activeObjectRef: Ref
- prevActiveItemIdRef: Ref
- initActiveItem: (item: AccordionMenuItem) => void
- activeItemFn: (item: AccordionMenuItem) => void
- // itemClickFn: (itemEvent: AccordionItemClickEvent) => void
- // linkItemClickFn: (itemEvent: AccordionItemClickEvent) => void
-}
-
-const useActive: TypeUseActive = (props, emit) => {
- const { activeKey, disabledKey } = toRefs(props)
- const activeObjectRef: Ref = ref({})
- const prevActiveItemIdRef: Ref = ref(null) //记录用户点击的激活菜单项
-
- const initActiveItem = (item) => {
- activeObjectRef.value[item.id] = {
- isDisable: item[disabledKey.value],
- isActive: item[activeKey.value]
- }
- if (item[activeKey.value]) {
- prevActiveItemIdRef.value = item.id
- }
- }
-
- // 激活子菜单项并去掉其他子菜单的激活
- const activeItemFn = (item: AccordionMenuItem) => {
- if (activeObjectRef.value[item.id].isDisable) return
- if (prevActiveItemIdRef.value === item.id) return
- if (prevActiveItemIdRef.value) {
- activeObjectRef.value[prevActiveItemIdRef.value].isActive = false
- }
- activeObjectRef.value[item.id].isActive = true
- prevActiveItemIdRef.value = item.id
- emit('activeItemChange', item)
- }
-
- // 点击了可点击菜单
- // const itemClickFn = (itemEvent: AccordionItemClickEvent) => {
- // const prevActiveItemIdRef = clickActiveItem
- // activeItemFn(itemEvent.item)
- // // emit('itemClick', {...itemEvent, prevActiveItem: prevActiveItem});
- // }
-
- // const linkItemClickFn = (itemEvent: AccordionItemClickEvent) => {
- // const prevActiveItem = clickActiveItem
- // clickActiveItem.value = itemEvent.item
- // // emit('itemClick', {...itemEvent, prevActiveItem: prevActiveItem});
- // }
-
- return {
- activeObjectRef,
- prevActiveItemIdRef,
- initActiveItem,
- activeItemFn
- // itemClickFn,
- // linkItemClickFn
- }
-}
-
-export default useActive
diff --git a/packages/devui-vue/devui/accordion/src/composables/use-toggle.ts b/packages/devui-vue/devui/accordion/src/composables/use-toggle.ts
deleted file mode 100644
index 646175ef9b..0000000000
--- a/packages/devui-vue/devui/accordion/src/composables/use-toggle.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { toRefs, ref } from 'vue'
-import { AccordionMenuItem, AccordionMenuToggleEvent } from '../accordion.type'
-import { flatten } from '../utils'
-
-type TypeMenuOpenStatusRef = {
- [key: string]: {
- isOpen: boolean
- }
-}
-
-const useToggle = (props) => {
- const menuOpenStatusRef = ref({})
- const preOpenFoldeRef = ref('')
- const { data, childrenKey, openKey, restrictOneOpen } = toRefs(props)
-
- const initAllOpenData = (item) => {
- menuOpenStatusRef.value[item.id] = {
- isOpen: item[openKey.value]
- }
- if (item[openKey.value]) {
- preOpenFoldeRef.value = item.id
- }
- }
-
- // 打开或关闭一级菜单,如果有限制只能展开一项则关闭其他一级菜单
- const openMenuFn = (item, open) => {
- if (open && restrictOneOpen.value) {
- if (preOpenFoldeRef.value === item.id) return
- if (preOpenFoldeRef.value) {
- menuOpenStatusRef.value[preOpenFoldeRef.value].isOpen = false
- }
- }
- menuOpenStatusRef.value[item.id].isOpen = open
- }
-
- // 打开或关闭可折叠菜单
- const menuToggleFn = (menuEvent: AccordionMenuToggleEvent) => {
- openMenuFn(menuEvent.item, menuEvent.open)
- }
-
- const cleanOpenData = () => {
- flatten(data.value, childrenKey.value, true, false).forEach(
- (item) => (item[openKey.value] = undefined)
- )
- }
-
- return {
- menuOpenStatusRef,
- initAllOpenData,
- openMenuFn,
- menuToggleFn,
- cleanOpenData
- }
-}
-
-export default useToggle
diff --git a/packages/devui-vue/devui/accordion/src/utils.ts b/packages/devui-vue/devui/accordion/src/utils.ts
index 91d03b0360..e4d01bb755 100644
--- a/packages/devui-vue/devui/accordion/src/utils.ts
+++ b/packages/devui-vue/devui/accordion/src/utils.ts
@@ -1,5 +1,5 @@
import { AccordionMenuItem, AccordionMenuType } from './accordion.type'
-import { ComponentInternalInstance, getCurrentInstance } from "vue"
+import { ComponentInternalInstance, getCurrentInstance } from 'vue'
const flatten = (
arr: Array,
diff --git a/packages/devui-vue/devui/alert/src/alert.scss b/packages/devui-vue/devui/alert/src/alert.scss
index 68704fc1ac..5b880a12e3 100644
--- a/packages/devui-vue/devui/alert/src/alert.scss
+++ b/packages/devui-vue/devui/alert/src/alert.scss
@@ -155,7 +155,6 @@
}
.devui-alter-close {
- height: 0 !important;
margin: 0;
padding-top: 0;
padding-bottom: 0;
diff --git a/packages/devui-vue/devui/alert/src/alert.tsx b/packages/devui-vue/devui/alert/src/alert.tsx
index afd1b29f97..bb3889e960 100644
--- a/packages/devui-vue/devui/alert/src/alert.tsx
+++ b/packages/devui-vue/devui/alert/src/alert.tsx
@@ -36,24 +36,22 @@ export default defineComponent({
const hide = ref(false)
const closing = ref(false)
const alertEl = ref()
-
+ let dismissTimer: undefined | number = undefined;
const close = (event?: MouseEvent) => {
- const dom = alertEl.value
- dom.style.height = `${dom.offsetHeight}px`
- // 重复一次后才能正确设置 height
- dom.style.height = `${dom.offsetHeight}px`
+ dismissTimer && clearTimeout(dismissTimer)
closing.value = true
ctx.emit('close', event)
}
const afterLeave = () => {
+ dismissTimer = undefined;
hide.value = true
closing.value = false
}
onMounted(() => {
if (props.dismissTime) {
- setTimeout(() => {
+ dismissTimer = window.setTimeout(() => {
close()
}, props.dismissTime)
}
@@ -65,9 +63,8 @@ export default defineComponent({
{props.closeable ? (
diff --git a/packages/devui-vue/devui/auto-complete/__tests__/auto-complete.spec.ts b/packages/devui-vue/devui/auto-complete/__tests__/auto-complete.spec.ts
new file mode 100644
index 0000000000..85df305ba4
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/__tests__/auto-complete.spec.ts
@@ -0,0 +1,570 @@
+import { mount } from '@vue/test-utils';
+import { nextTick, ref } from 'vue';
+import DAutoComplete from '../src/auto-complete';
+
+// delay api
+const wait = (delay = 300) =>
+ new Promise(resolve => setTimeout(() => resolve(true), delay))
+describe('auto-complete', () => {
+ it('init render & KeyboardEvent ', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+ `,
+ setup() {
+ const value = ref('')
+ const source = [
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'CoffeeScript',
+ ]
+ return {
+ value,
+ source,
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('click')
+ await nextTick()
+ expect(wrapper.find('.devui-select-open').exists()).toBe(true)
+ expect(wrapper.find('.devui-dropdown-item').exists()).toBe(false)
+ expect(wrapper.find('.devui-auto-complete').attributes('style')).toContain(
+ 'width: 450px'
+ )
+ await input.setValue('c')
+ await nextTick()
+ expect(wrapper.find('.devui-dropdown-menu').exists()).toBe(true)
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('.devui-list-unstyled').element.childElementCount).toBe(5)
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
+ await nextTick()
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
+ await nextTick()
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
+ await nextTick()
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
+ await nextTick()
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
+ await nextTick()
+ expect(wrapper.vm.value).toBe('C++')
+ input.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
+ await nextTick()
+ expect(wrapper.find('.devui-select-open').exists()).toBe(false)
+ })
+ it('disabled ', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+
+
+
+ `,
+ setup() {
+ const value = ref('')
+ const isDisabled = ref(false)
+ const source = [
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'CoffeeScript',
+ ]
+ function toggle(){
+ isDisabled.value= !isDisabled.value
+ }
+ return {
+ value,
+ source,
+ isDisabled,
+ toggle
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ const button = wrapper.find('button')
+ expect(input.element.value).toBe('')
+ expect(button.element.innerHTML).toBe('Disable AutoComplete')
+ await input.trigger('click')
+ await nextTick()
+ await input.setValue('c')
+ await nextTick()
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('ul .selected').exists()).toBe(true)
+ const li = wrapper.find('ul .selected')
+ li.trigger('click')
+ await nextTick()
+ expect(wrapper.vm.value).toBe('C#')
+ expect(wrapper.find('.devui-select-open').exists()).toBe(false)
+ button.trigger('click')
+ await nextTick()
+ expect(button.element.innerHTML).toBe('Enable AutoComplete')
+ expect(input.element.disabled).toBe(true)
+ })
+ it('Customized data matching method ', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+
+
+ {{slotProps}}
+
+
+
+ `,
+ setup() {
+ const value = ref('')
+ const mySource = ref([
+ {
+ label:'C#',
+ disabled:false
+ },{
+ label:'C++',
+ disabled:false
+ },{
+ label:'CPython',
+ disabled:false
+ },{
+ label:'Java',
+ disabled:false
+ },{
+ label:'JavaScript',
+ disabled:false
+ },{
+ label:'Go',
+ disabled:false
+ },{
+ label:'Ruby',
+ disabled:false
+ },{
+ label:'F#',
+ disabled:false
+ },{
+ label:'TypeScript',
+ disabled:false
+ },{
+ label:'SQL',
+ disabled:true
+ },{
+ label:'LiveScript',
+ disabled:false
+ },{
+ label:'CoffeeScript',
+ disabled:false
+ }
+ ])
+ const formatter = (item) =>{
+ return item.label;
+ }
+ //trem:input输入内容
+ const searchFn =async (trem)=>{
+ const arr = []
+ await new Promise((resolve)=>{
+ setTimeout(() => {
+ resolve(1)
+ }, 500);
+ })
+ mySource.value.forEach((item) => {
+ let cur = item.label
+ cur = cur.toLowerCase()
+ if (cur.startsWith(trem)) {
+ arr.push(item)
+ }
+ })
+ return arr
+ }
+ return {
+ value,
+ searchFn,
+ formatter
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('click')
+ await nextTick()
+ expect(wrapper.find('.devui-select-open').exists()).toBe(true)
+ await input.setValue('c')
+ await nextTick()
+ await wait(300)
+ expect(wrapper.find('#devui-is-searching-template').exists()).toBe(true)
+ expect(wrapper.find('#devui-is-searching-template').element.innerHTML).toBe('c')
+ await wait(500)
+ await nextTick()
+ expect(wrapper.find('.devui-list-unstyled').element.childElementCount).toBe(4)
+ await input.setValue('s')
+ await nextTick()
+ await wait(300)
+ await nextTick()
+ await wait(500)
+ expect(wrapper.find('li.disabled').exists()).toBe(true)
+ expect(wrapper.find('li.disabled').element.innerHTML).toBe('SQL')
+ })
+
+ it('Customized template display', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+
+
+ 第{{slotProps.index}}项: {{slotProps.item}}
+
+
+
+
+ {{slotProps}}
+
+
+
+ `,
+ setup() {
+ const value = ref('')
+ const source = ref([
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'Java',
+ 'JavaScript',
+ 'Go',
+ 'Python',
+ 'Ruby',
+ 'F#',
+ 'TypeScript',
+ 'SQL',
+ 'LiveScript',
+ 'CoffeeScript',
+ ])
+
+ return {
+ value,
+ source
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('click')
+ await nextTick()
+ expect(wrapper.find('.devui-select-open').exists()).toBe(true)
+ await input.setValue('c')
+ await nextTick()
+ await wait(300)
+ expect(wrapper.find('.devui-list-unstyled').exists()).toBe(true)
+ expect(wrapper.find('.devui-list-unstyled').element.childElementCount).toBe(5)
+ expect(wrapper.find('.selected div').element.innerHTML).toBe(' 第0项: C#')
+ await input.setValue('cc')
+ await nextTick()
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('#noResultItemTemplate').exists()).toBe(true)
+ expect(wrapper.find('#noResultItemTemplate').element.innerHTML).toBe('cc')
+ })
+
+ it('selectValue & transInputFocusEmit ', async () => {
+ const transInputFocusEmitCB = jest.fn()
+ const selectValueCB = jest.fn()
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+ `,
+ setup() {
+ const value = ref('')
+ const source = [
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'CoffeeScript',
+ ]
+ const selectValue = (e)=>{
+ selectValueCB(e)
+ }
+ const transInputFocusEmit = (e)=>{
+ transInputFocusEmitCB(e)
+ }
+ return {
+ value,
+ source,
+ selectValue,
+ transInputFocusEmit
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('focus')
+ await nextTick()
+ await input.setValue('c')
+ await nextTick()
+ await wait(300)
+ await nextTick()
+ expect(transInputFocusEmitCB).toHaveBeenCalledTimes(1)
+ const li = wrapper.find('ul .selected')
+ li.trigger('click')
+ await nextTick()
+ expect(selectValueCB).toHaveBeenCalledTimes(1)
+ })
+ it('allowEmptyValueSearch ', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+ `,
+ setup() {
+ const value = ref('')
+ const allowEmptyValueSearch = ref(true)
+ const source = [
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'CoffeeScript',
+ ]
+
+ return {
+ value,
+ source,
+ allowEmptyValueSearch
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('focus')
+ await nextTick()
+ expect(wrapper.find('ul').element.childElementCount).toBe(5)
+ })
+
+ it('appendToBody & appendToBodyDirections', async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template: `
+
+ `,
+ setup() {
+ const value = ref('')
+ const allowEmptyValueSearch = ref(true)
+ const source = [
+ 'CC#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'CoffeeScript',
+ ]
+ const appendToBodyDirections = ref({
+ originX: 'left',
+ originY: 'bottom',
+ overlayX: 'left',
+ overlayY: 'top',
+ })
+ return {
+ value,
+ source,
+ allowEmptyValueSearch,
+ appendToBodyDirections
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('focus')
+ await nextTick()
+ await input.setValue('c')
+ await nextTick()
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('ul').element.childElementCount).toBe(5)
+ expect(wrapper.find('.selected').element.innerHTML).toBe('CC#')
+ })
+
+ it('latestSource',async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template:`
+
+
+
+ `,
+ setup(){
+ const value = ref('')
+ const latestSource = ref(['JavaScript','TypeScript'])
+ const source = ref([
+ 'C#',
+ 'C',
+ 'C++',
+ 'Java',
+ 'JavaScript'
+ ])
+
+ return {
+ value,
+ source,
+ latestSource
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.trigger('click')
+ await nextTick()
+ expect(wrapper.find('ul .devui-popup-tips').exists()).toBe(true)
+ await input.setValue('j')
+ await wait(300)
+ await nextTick()
+ const li = wrapper.find('ul .selected')
+ li.trigger('click')
+ await nextTick()
+ expect(wrapper.vm.value).toBe('Java')
+ })
+ it('enableLazyLoad',async () => {
+ const wrapper = mount({
+ components: {'d-auto-complete': DAutoComplete },
+ template:`
+
+
+
+ `,
+ setup(){
+ const value = ref('')
+ const source = ref([
+ 'C#',
+ 'C',
+ 'C++',
+ 'CPython',
+ 'Java',
+ 'JavaScript',
+ 'Go',
+ 'Python',
+ 'Ruby',
+ 'F#',
+ 'TypeScript',
+ 'SQL',
+ 'LiveScript',
+ 'CoffeeScript',
+ 'C1',
+ 'C2',
+ 'C3',
+ 'C4',
+ 'C5',
+ 'C6',
+ 'C7',
+ ])
+ const autoCompleteRef =ref(null)
+
+ const loadMore = () => {
+ setTimeout(() => {
+ source.value.push('lazyData'+source.value.length)
+ autoCompleteRef.value?.loadFinish()
+ },3000)
+ }
+ return {
+ value,
+ source,
+ loadMore,
+ autoCompleteRef
+ }
+ }
+ })
+ expect(wrapper.find('.devui-auto-complete').exists()).toBe(true)
+ const input = wrapper.find('input')
+ expect(input.element.value).toBe('')
+ await input.setValue('c')
+ await nextTick()
+ expect(wrapper.find('.devui-dropdown-menu').exists()).toBe(true)
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('.devui-dropdown-item').exists()).toBe(true)
+ const ul = wrapper.find('.devui-list-unstyled')
+ const makeScroll = async (
+ dom: Element,
+ name: 'scrollTop',
+ offset: number
+ ) => {
+ const eventTarget = dom === document.documentElement ? window : dom
+ dom[name] = offset
+ const evt = new CustomEvent('scroll', {
+ detail: {
+ target: {
+ [name]: offset,
+ },
+ },
+ })
+ eventTarget.dispatchEvent(evt)
+ return await wait(3000)
+ }
+ await makeScroll(ul.element, 'scrollTop', 500)
+ await nextTick()
+ expect(wrapper.vm.value).toBe('c')
+ await nextTick()
+ await input.setValue('')
+ const length = wrapper.vm.source.length
+ expect(wrapper.vm.source[length - 1]).toBe('lazyData21')
+ await input.setValue('la')
+ await wait(300)
+ await nextTick()
+ expect(wrapper.find('.devui-dropdown-item').element.innerHTML).toBe('lazyData21')
+ })
+})
+
diff --git a/packages/devui-vue/devui/auto-complete/index.ts b/packages/devui-vue/devui/auto-complete/index.ts
new file mode 100644
index 0000000000..ecfbe3545b
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/index.ts
@@ -0,0 +1,17 @@
+import type { App } from 'vue'
+import AutoComplete from './src/auto-complete'
+
+AutoComplete.install = function(app: App): void {
+ app.component(AutoComplete.name, AutoComplete)
+}
+
+export { AutoComplete }
+
+export default {
+ title: 'AutoComplete 自动补全',
+ category: '数据录入',
+ status: '100%',
+ install(app: App): void {
+ app.use(AutoComplete as any)
+ }
+}
diff --git a/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts b/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts
new file mode 100644
index 0000000000..b3618cd2f2
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts
@@ -0,0 +1,140 @@
+import type { PropType, ExtractPropTypes, InjectionKey, SetupContext, Ref } from 'vue'
+const defaultFormatter = (item) => (item ? item.label || item.toString() : '');
+const defaultValueParse = (item) => item;
+// appendToBody使用
+export type HorizontalConnectionPos = 'left' | 'center' | 'right';
+export type VerticalConnectionPos = 'top' | 'center' | 'bottom';
+export interface ConnectionPosition {
+ originX: HorizontalConnectionPos
+ originY: VerticalConnectionPos
+ overlayX: HorizontalConnectionPos
+ overlayY: VerticalConnectionPos
+}
+export const autoCompleteProps = {
+ modelValue: {
+ type: String,
+ default:''
+ },
+ source:{
+ type :Array,
+ default:null
+ },
+ allowEmptyValueSearch:{
+ type:Boolean,
+ default:false
+ },
+ appendToBody :{
+ type:Boolean,
+ default:false
+ },
+ appendToBodyDirections :{
+ type: Object as PropType
,
+ default: (): ConnectionPosition => ({
+ originX: 'left',
+ originY: 'bottom',
+ overlayX: 'left',
+ overlayY: 'top',
+ }),
+ },
+ disabled:{
+ type:Boolean,
+ default:false
+ },
+ delay:{
+ type:Number,
+ default:300
+ },
+ disabledKey:{
+ type:String,
+ default:null
+ },
+ formatter: {
+ type:Function as PropType<(item: any) => string>,
+ default:defaultFormatter
+ },
+ isSearching: {
+ type:Boolean,
+ default:false
+ },
+ sceneType:{
+ type:String,
+ default:null
+ },
+ searchFn:{
+ type:Function as PropType<(term: string) => Array>,
+ default:null
+ },
+ tipsText:{
+ type:String,
+ default:'最近输入'
+ },
+ latestSource:{
+ type:Array,
+ default:null
+ },
+ valueParser:{
+ type:Function as PropType<(item: any) => any>,
+ default:defaultValueParse
+ },
+ enableLazyLoad: {
+ type:Boolean,
+ default:false
+ },
+ dAutoCompleteWidth:{
+ type: Number,
+ default:null
+ },
+ showAnimation:{
+ type:Boolean,
+ default:true
+ },
+ maxHeight:{
+ type:Number,
+ default:300
+ },
+ transInputFocusEmit:{
+ type:Function as PropType<(any) => void>,
+ default:null
+ },
+ selectValue:{
+ type:Function as PropType<(any) => void>,
+ default:null
+ },
+ loadMore:{
+ type:Function as PropType<() => void>,
+ default:null
+ }
+} as const
+
+export type AutoCompleteProps = ExtractPropTypes
+
+export interface AutoCompleteRootType {
+ ctx:SetupContext
+ props:AutoCompleteProps
+}
+export type SearchFnType = (term: string) => Array
+export type FormatterType = (item: any) => string
+export type DefaultFuncType = (any?) => any
+export type HandleSearch = (term?:string | string,enableLazyLoad?:boolean) => void
+export type RecentlyFocus = (latestSource:Array) => void
+export type InputDebounceCb = (...rest:any) => Promise
+export type TransInputFocusEmit = (any?: any) => void
+export type SelectOptionClick = (any?: any) => void
+//弹出选择框参数
+export type DropdownProps = {
+ props:AutoCompleteProps
+ searchList:Ref
+ searchStatus?:Ref
+ showNoResultItemTemplate:Ref
+ term?: string
+ visible: Ref
+ selectedIndex:Ref
+ selectOptionClick:HandleSearch
+ dropDownRef
+ showLoading:Ref
+ loadMore
+ latestSource
+ modelValue:Ref
+ hoverIndex:Ref
+}
+export const DropdownPropsKey:InjectionKey=Symbol('DropdownPropsKey')
diff --git a/packages/devui-vue/devui/auto-complete/src/auto-complete.scss b/packages/devui-vue/devui/auto-complete/src/auto-complete.scss
new file mode 100644
index 0000000000..4d80b0cd39
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/auto-complete.scss
@@ -0,0 +1,95 @@
+@import '../../style/mixins/index';
+@import '../../style/theme/color';
+
+.devui-auto-complete,
+.devui-auto-complete-menu {
+ .devui-dropdown-menu {
+ left: 0 !important;
+ top: 0 !important;
+ }
+}
+
+.devui-auto-complete {
+ .active {
+ background: $devui-list-item-hover-bg;
+ }
+
+ .devui-dropdown-menu {
+ width: 100%;
+ display: block;
+ }
+
+ .devui-dropdown-menu-cdk {
+ position: static;
+ }
+
+ .devui-dropdown-item {
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ padding: 8px 12px;
+ clear: both;
+ border: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 14px;
+ }
+
+ .devui-dropdown-menu {
+ .devui-dropdown-item:not(.disabled) {
+ &.selected {
+ color: $devui-list-item-active-text;
+ background-color: $devui-list-item-active-bg;
+ }
+ }
+ }
+
+ .devui-no-result-template,
+ .devui-is-searching-template {
+ display: block;
+ width: 100%;
+ padding: 8px 12px;
+ clear: both;
+ border: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: not-allowed;
+ background-color: $devui-disabled-bg;
+ color: $devui-disabled-text;
+ line-height: 14px;
+
+ &:hover,
+ &:active,
+ &:hover:active {
+ background-color: $devui-unavailable;
+ }
+ }
+
+ /* 选项disabled */
+ .devui-dropdown-item.disabled,
+ .devui-dropdown-item.disabled:hover {
+ cursor: not-allowed;
+ color: $devui-disabled-text;
+ }
+
+ ul.devui-list-unstyled {
+ margin: 0;
+ overflow-y: auto;
+ }
+
+ .devui-dropdown-bg {
+ background: $devui-list-item-hover-bg;
+ color: $devui-list-item-hover-text;
+ }
+
+ .devui-popup-tips {
+ color: $devui-text-weak; // TODO: Color-Question
+ padding: 4px 12px;
+ }
+
+ .devui-dropdown-latestSource ul {
+ line-height: initial !important;
+ }
+}
diff --git a/packages/devui-vue/devui/auto-complete/src/auto-complete.tsx b/packages/devui-vue/devui/auto-complete/src/auto-complete.tsx
new file mode 100644
index 0000000000..0f70a1e350
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/auto-complete.tsx
@@ -0,0 +1,127 @@
+import { defineComponent, provide, reactive, Transition,toRefs, ref, SetupContext } from 'vue'
+import { autoCompleteProps, AutoCompleteProps, DropdownPropsKey } from './auto-complete-types'
+import useCustomTemplate from './composables/use-custom-template'
+import useSearchFn from './composables/use-searchfn'
+import useInputHandle from './composables/use-input-handle'
+import useSelectHandle from './composables/use-select-handle'
+import useLazyHandle from './composables/use-lazy-handle'
+import useKeyBoardHandle from './composables/use-keyboard-select'
+import './auto-complete.scss'
+import DAutoCompleteDropdown from './components/dropdown'
+import ClickOutside from '../../shared/devui-directive/clickoutside'
+import {FlexibleOverlay} from '../../overlay/src/flexible-overlay'
+export default defineComponent({
+ name: 'DAutoComplete',
+ directives: { ClickOutside },
+ props: autoCompleteProps,
+ emits: ['update:modelValue'],
+ setup(props: AutoCompleteProps, ctx:SetupContext) {
+ const {
+ disabled,
+ modelValue,
+ appendToBody,
+ dAutoCompleteWidth,
+ delay,
+ allowEmptyValueSearch,
+ formatter,
+ transInputFocusEmit,
+ selectValue,
+ source,
+ searchFn,
+ appendToBodyDirections,
+ latestSource,
+ showAnimation
+ } = toRefs(props)
+
+ const {handleSearch,searchList,showNoResultItemTemplate,recentlyFocus} = useSearchFn(ctx,allowEmptyValueSearch,source,searchFn,formatter)
+ const {onInput,onFocus,inputRef,visible,searchStatus,handleClose,toggleMenu} = useInputHandle(ctx,searchList,showNoResultItemTemplate,modelValue,disabled,delay,handleSearch,transInputFocusEmit,recentlyFocus,latestSource)
+ const {selectedIndex,selectOptionClick} = useSelectHandle(ctx,searchList,selectValue,handleSearch,formatter,handleClose)
+ const {showLoading,dropDownRef,loadMore} = useLazyHandle(props,ctx,handleSearch)
+ const {customRenderSolts} = useCustomTemplate(ctx,modelValue)
+ const {hoverIndex,handlekeyDown} = useKeyBoardHandle(dropDownRef,visible,searchList,selectedIndex,searchStatus,showNoResultItemTemplate,selectOptionClick,handleClose)
+ provide(DropdownPropsKey, {
+ props,
+ visible,
+ term: '',
+ searchList:searchList,
+ selectedIndex,
+ searchStatus,
+ selectOptionClick,
+ dropDownRef,
+ showLoading,
+ loadMore,
+ latestSource,
+ modelValue,
+ showNoResultItemTemplate:showNoResultItemTemplate,
+ hoverIndex:hoverIndex
+ })
+ const origin = ref()
+ const position = reactive({appendToBodyDirections:{}})
+ position.appendToBodyDirections=appendToBodyDirections
+ const renderDropdown = () => {
+ if(appendToBody.value){
+ return (
+
+
+
+ )
+ }else{
+ return (
+ 0&&dAutoCompleteWidth.value+'px'
+ }}
+ >
+
+
+ {customRenderSolts()}
+
+
+
+ )
+ }
+
+ }
+ return () => {
+ return (
+ 0&&dAutoCompleteWidth.value+'px'
+ }}
+ >
+
+ {renderDropdown()}
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/auto-complete/src/components/dropdown.tsx b/packages/devui-vue/devui/auto-complete/src/components/dropdown.tsx
new file mode 100644
index 0000000000..38dd7ed3a3
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/components/dropdown.tsx
@@ -0,0 +1,103 @@
+import { defineComponent, inject } from 'vue'
+import { DropdownPropsKey } from '../auto-complete-types'
+import dLoading from '../../../loading/src/directive'
+// 后续会对接自带下拉组件,相关功能将全部抽离
+export default defineComponent({
+ name: 'DAutoCompleteDropdown',
+ directives: {dLoading},
+ setup(props,ctx) {
+ const propsData = inject(DropdownPropsKey)
+ const {
+ visible,
+ selectedIndex,
+ selectOptionClick,
+ searchList,
+ searchStatus,
+ dropDownRef,
+ loadMore,
+ showLoading,
+ showNoResultItemTemplate,
+ latestSource,
+ modelValue,
+ hoverIndex
+ } = propsData
+ const {
+ disabled,
+ maxHeight,
+ appendToBody,
+ formatter,
+ disabledKey,
+ isSearching,
+ } = propsData.props
+
+ const onSelect =(item:any)=>{
+ if(item[disabledKey]){return}
+ selectOptionClick(item)
+ }
+ return () => {
+ return (
+ 0)||(ctx.slots.noResultItemTemplate&&showNoResultItemTemplate.value)||(isSearching&&ctx.slots.searchingTemplate&&searchStatus.value)}
+ >
+
+
+ )
+ }
+
+ }
+})
\ No newline at end of file
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-custom-template.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-custom-template.ts
new file mode 100644
index 0000000000..5ddd8a3ec9
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-custom-template.ts
@@ -0,0 +1,37 @@
+import { Ref, SetupContext } from 'vue';
+
+export default function useCustomTemplate(ctx:SetupContext,modelValue:Ref): any {
+ const itemTemplate = (item, index) => {
+ const arr = { item, index }
+ if (ctx.slots.itemTemplate) {
+ return ctx.slots.itemTemplate(arr)
+ }
+ return null
+ }
+ const noResultItemTemplate = () => {
+ if (ctx.slots.noResultItemTemplate) {
+ return ctx.slots.noResultItemTemplate(modelValue.value)
+ }
+ return null
+ }
+ const searchingTemplate = () => {
+ if (ctx.slots.searchingTemplate) {
+ return ctx.slots.searchingTemplate(modelValue.value)
+ }
+ return null
+ }
+ const customRenderSolts = () => {
+ const slots = {}
+ if (ctx.slots.itemTemplate) {
+ slots['itemTemplate'] = itemTemplate
+ }
+ if (ctx.slots.noResultItemTemplate) {
+ slots['noResultItemTemplate'] = noResultItemTemplate
+ }
+ if (ctx.slots.searchingTemplate) {
+ slots['searchingTemplate'] = searchingTemplate
+ }
+ return slots
+ }
+ return {customRenderSolts}
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts
new file mode 100644
index 0000000000..090a867276
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts
@@ -0,0 +1,63 @@
+import { ref, Ref, SetupContext } from 'vue';
+import {HandleSearch,RecentlyFocus,InputDebounceCb,TransInputFocusEmit} from '../auto-complete-types'
+export default function useInputHandle(ctx: SetupContext,searchList:Ref,showNoResultItemTemplate:Ref, modelValue:Ref,disabled:Ref,delay:Ref,handleSearch: HandleSearch, transInputFocusEmit:Ref,recentlyFocus:RecentlyFocus,latestSource:Ref>): any {
+ const visible = ref(false)
+ const inputRef = ref()
+ const searchStatus = ref(false)
+ const debounce =(cb:InputDebounceCb,time:number) =>{
+ let timer
+ return (...args)=>{
+ if(timer){
+ clearTimeout(timer)
+ }
+ timer = setTimeout(async ()=>{
+ searchStatus.value=true
+ await cb(...args)
+ searchStatus.value=false
+ },time)
+ }
+ }
+ const onInputCb = async(value:string)=>{
+ await handleSearch(value)
+ visible.value = true
+ }
+ const onInputDebounce = debounce(onInputCb,delay.value)
+ const onInput =(e: Event) => {
+ const inp = e.target as HTMLInputElement
+ searchStatus.value=false
+ showNoResultItemTemplate.value=false
+ ctx.emit('update:modelValue', inp.value)
+ onInputDebounce(inp.value)
+ }
+ const onFocus =() => {
+ handleSearch(modelValue.value)
+ recentlyFocus(latestSource.value)
+ transInputFocusEmit.value && transInputFocusEmit.value()
+ }
+ const handleClose = ()=>{
+ visible.value=false
+ searchStatus.value=false
+ showNoResultItemTemplate.value=false
+ }
+ const toggleMenu =()=>{
+ if(!disabled.value){
+ if(visible.value){
+ handleClose()
+ }else{
+ visible.value=true
+ if (ctx.slots.noResultItemTemplate&&searchList.value.length==0&&modelValue.value.trim()!='') {
+ showNoResultItemTemplate.value=true
+ }
+ }
+ }
+ }
+ return {
+ handleClose,
+ toggleMenu,
+ onInput,
+ onFocus,
+ inputRef,
+ visible,
+ searchStatus
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-keyboard-select.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-keyboard-select.ts
new file mode 100644
index 0000000000..ad04b34793
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-keyboard-select.ts
@@ -0,0 +1,55 @@
+import { nextTick, ref, Ref } from 'vue';
+import { DefaultFuncType, SelectOptionClick } from '../auto-complete-types';
+
+export default function useKeyBoardHandle(dropDownRef: Ref, visible: Ref, searchList: Ref>, selectedIndex: Ref, searchStatus: Ref, showNoResultItemTemplate: Ref, selectOptionClick: SelectOptionClick, handleClose: DefaultFuncType): any {
+ const hoverIndex = ref(selectedIndex.value??0)
+ const scrollToActive = (index: number) => {
+ const ul = dropDownRef.value
+ const li = ul.children[index]
+ nextTick(() => {
+ if (li.scrollIntoViewIfNeeded) {
+ li.scrollIntoViewIfNeeded(false)
+ } else {
+ const containerInfo = ul.getBoundingClientRect()
+ const elementInfo = li.getBoundingClientRect()
+ if (elementInfo.bottom > containerInfo.bottom || elementInfo.top < containerInfo.top) {
+ li.scrollIntoView(false)
+ }
+ }
+ })
+ }
+ const handlekeyDown = (e: KeyboardEvent) => {
+ const keyCode = e.key || e.code
+ if (keyCode === 'Escape' && ( (visible.value && searchList.value.length) || searchStatus.value||showNoResultItemTemplate.value)) {
+ handleClose()
+ return
+ }
+ const status = visible.value && searchList.value.length && !searchStatus.value && !showNoResultItemTemplate.value
+ if (keyCode === 'ArrowDown' && status) {
+ if (hoverIndex.value === searchList.value.length - 1) {
+ hoverIndex.value = 0;
+ scrollToActive(hoverIndex.value);
+ return;
+ }
+ hoverIndex.value = hoverIndex.value + 1;
+ scrollToActive(hoverIndex.value);
+ } else if (keyCode === 'ArrowUp' && status) {
+ if (hoverIndex.value === 0) {
+ hoverIndex.value = searchList.value.length - 1;
+ scrollToActive(hoverIndex.value);
+ return;
+ }
+ hoverIndex.value = hoverIndex.value - 1;
+ scrollToActive(hoverIndex.value);
+ }
+ if (keyCode === 'Enter' && status) {
+ selectOptionClick(searchList.value[hoverIndex.value])
+ hoverIndex.value=selectedIndex.value??0
+ return
+ }
+ }
+ return {
+ hoverIndex,
+ handlekeyDown
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-lazy-handle.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-lazy-handle.ts
new file mode 100644
index 0000000000..96add63862
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-lazy-handle.ts
@@ -0,0 +1,30 @@
+import { ref,SetupContext } from 'vue'
+import { AutoCompleteProps,HandleSearch } from '../auto-complete-types'
+export default function useLazyHandle(props: AutoCompleteProps,ctx:SetupContext,handleSearch:HandleSearch):any {
+ const showLoading = ref(false)
+ const dropDownRef = ref()
+ const loadMore = () => {
+ if(!props.enableLazyLoad && showLoading) return
+ const dropDownValue = dropDownRef.value
+ const height = dropDownValue.scrollHeight
+ const scrollTop = dropDownValue.clientHeight + dropDownValue.scrollTop
+
+ if(scrollTop >= height && scrollTop >= props.maxHeight) {
+ props.loadMore()
+ showLoading.value = true
+ }
+ }
+ ctx.expose({loadFinish})
+
+ async function loadFinish (){
+ await handleSearch(props.modelValue,props.enableLazyLoad)
+ showLoading.value = false
+ }
+ return {
+ showLoading,
+ dropDownRef,
+ loadMore,
+ }
+}
+
+
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-searchfn.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-searchfn.ts
new file mode 100644
index 0000000000..8d90a98811
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-searchfn.ts
@@ -0,0 +1,45 @@
+import { ref, Ref, SetupContext } from 'vue';
+import { FormatterType, SearchFnType } from '../auto-complete-types';
+export default function useSearchFn(ctx: SetupContext,allowEmptyValueSearch:Ref,source:Ref>,searchFn:Ref,formatter:Ref): any {
+ const searchList = ref([])
+ const showNoResultItemTemplate = ref(false)
+ const handleSearch = async (term: string,enableLazyLoad:boolean) => {
+ if (term == ''&&!allowEmptyValueSearch.value) {
+ searchList.value = []
+ showNoResultItemTemplate.value=false
+ return
+ }
+ let arr = []
+ term = term.toLowerCase()
+ if(enableLazyLoad) {
+ arr = source.value
+ }else if (!searchFn.value) {
+ source.value.forEach((item) => {
+ let cur = formatter.value(item)
+ cur = cur.toLowerCase()
+ if (cur.startsWith(term)) {
+ arr.push(item)
+ }
+ })
+ } else {
+ arr = await searchFn.value(term)
+ }
+ searchList.value = arr
+ if(searchList.value.length==0){
+ showNoResultItemTemplate.value=true
+ }else{
+ showNoResultItemTemplate.value=false
+ }
+ }
+ const recentlyFocus = (latestSource:Array) => {
+ if(latestSource) {
+ searchList.value = latestSource
+ }
+ }
+ return {
+ handleSearch,
+ recentlyFocus,
+ searchList,
+ showNoResultItemTemplate
+ }
+}
diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-select-handle.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-select-handle.ts
new file mode 100644
index 0000000000..8261410c70
--- /dev/null
+++ b/packages/devui-vue/devui/auto-complete/src/composables/use-select-handle.ts
@@ -0,0 +1,25 @@
+import { ref, Ref, SetupContext } from 'vue';
+import { DefaultFuncType,FormatterType,HandleSearch } from '../auto-complete-types';
+
+export default function useSelectHandle(ctx: SetupContext,searchList: Ref>, selectValue: Ref, handleSearch: HandleSearch,formatter: Ref,handleClose:DefaultFuncType): any {
+ const selectedIndex = ref(0)
+ const getListIndex = (item: string) => {
+ if (searchList.value.length == 0) {
+ return 0
+ }
+ const ind = searchList.value.indexOf(item)
+ return ind == -1 ? 0 : ind
+ }
+ const selectOptionClick = async(item: any) => {
+ const cur = formatter.value(item)
+ ctx.emit('update:modelValue', cur)
+ handleClose()
+ await handleSearch(cur)
+ selectedIndex.value = getListIndex(cur)
+ selectValue.value && selectValue.value()
+ }
+ return {
+ selectedIndex,
+ selectOptionClick
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/back-top/src/back-top.scss b/packages/devui-vue/devui/back-top/src/back-top.scss
index e0d28a369d..20d44bf48c 100644
--- a/packages/devui-vue/devui/back-top/src/back-top.scss
+++ b/packages/devui-vue/devui/back-top/src/back-top.scss
@@ -6,6 +6,7 @@
height: 40px;
cursor: pointer;
z-index: 9;
+
.devui-back-top-base {
width: 40px;
height: 40px;
@@ -13,16 +14,19 @@
display: flex;
align-items: center;
justify-content: center;
+
&:hover {
opacity: 1;
}
}
+
.devui-back-top-content {
opacity: 0.4;
background-color: $devui-text-weak;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
+
.devui-backtop-custom {
- background-color: #fff;
+ background-color: #ffffff;
}
}
diff --git a/packages/devui-vue/devui/back-top/src/back-top.tsx b/packages/devui-vue/devui/back-top/src/back-top.tsx
index 1939a4e015..fa08d4857c 100644
--- a/packages/devui-vue/devui/back-top/src/back-top.tsx
+++ b/packages/devui-vue/devui/back-top/src/back-top.tsx
@@ -13,7 +13,7 @@ export default defineComponent({
const backTopRef = ref(null)
const position = usePosition(props)
- let isVisible = useVisibility(props, backTopRef)
+ const isVisible = useVisibility(props, backTopRef)
const scrollToTop = () => {
// toTop方法暂定
diff --git a/packages/devui-vue/devui/badge/index.ts b/packages/devui-vue/devui/badge/index.ts
index 8657a733b7..d6a4e971d7 100644
--- a/packages/devui-vue/devui/badge/index.ts
+++ b/packages/devui-vue/devui/badge/index.ts
@@ -1,17 +1,14 @@
-import type { App } from 'vue'
-import Badge from './src/badge'
+import type { App } from 'vue';
+import Badge from './src/badge';
+export * from './src/badge-types';
-Badge.install = function (app: App) {
- app.component(Badge.name, Badge)
-}
-
-export { Badge }
+export { Badge };
export default {
title: 'Badge 徽标',
category: '数据展示',
status: '100%',
install(app: App): void {
- app.use(Badge as any)
- }
-}
+ app.component(Badge.name, Badge);
+ },
+};
diff --git a/packages/devui-vue/devui/badge/src/badge-types.ts b/packages/devui-vue/devui/badge/src/badge-types.ts
index b0a5ba2fdf..ee212e5311 100644
--- a/packages/devui-vue/devui/badge/src/badge-types.ts
+++ b/packages/devui-vue/devui/badge/src/badge-types.ts
@@ -1,41 +1,41 @@
-import type { PropType, ExtractPropTypes } from 'vue'
+import type { PropType, ExtractPropTypes } from 'vue';
-type BadgeStatusType = PropType<'danger' | 'warning' | 'waiting' | 'success' | 'info'>
-type BadgePositionType = PropType<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'>
+export type BadgeStatusType = 'danger' | 'warning' | 'waiting' | 'success' | 'info';
+export type BadgePositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
-const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info']
-const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right']
+const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info'];
+const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
export const badgeProps = {
count: {
- type: [Number, String]
+ type: [Number, String],
},
maxCount: {
type: Number,
- default: 99
+ default: 99,
},
showDot: {
type: Boolean,
- default: false
+ default: false,
},
status: {
- type: String as BadgeStatusType,
- validator: (val: string) => badgeStatusType.includes(val)
+ type: String as PropType,
+ validator: (val: string): boolean => badgeStatusType.includes(val),
},
- badgePos: {
- type: String as BadgePositionType,
+ position: {
+ type: String as PropType,
default: 'top-right',
- validator: (val: string) => badgePositionType.includes(val)
+ validator: (val: string): boolean => badgePositionType.includes(val),
},
- offsetXY: {
- type: Array
+ offset: {
+ type: Array as PropType>,
},
bgColor: {
- type: String
+ type: String,
},
textColor: {
- type: String
- }
-}
+ type: String,
+ },
+};
-export type BadgeProps = ExtractPropTypes
+export type BadgeProps = ExtractPropTypes;
diff --git a/packages/devui-vue/devui/badge/src/badge.tsx b/packages/devui-vue/devui/badge/src/badge.tsx
index ad6024bd19..a4b428eca6 100644
--- a/packages/devui-vue/devui/badge/src/badge.tsx
+++ b/packages/devui-vue/devui/badge/src/badge.tsx
@@ -1,65 +1,60 @@
-import './badge.scss'
-
-import { defineComponent, computed } from 'vue'
-import { badgeProps, BadgeProps } from './badge-types'
+import { defineComponent, computed } from 'vue';
+import { badgeProps, BadgeProps } from './badge-types';
+import './badge.scss';
export default defineComponent({
name: 'DBadge',
props: badgeProps,
- emits: [],
setup(props: BadgeProps, ctx) {
const className = computed(() => {
- const base = 'devui-badge-content'
+ const base = 'devui-badge-content';
return [
base,
props.showDot ? `${base}-dot` : `${base}-count`,
props.status && `${base}-${props.status}`,
- ctx.slots.default && props.badgePos && `${base}-${props.badgePos}`,
- ctx.slots.default && `${base}-fixed`
- ].join(' ')
- })
+ ctx.slots.default && props.position && `${base}-${props.position}`,
+ ctx.slots.default && `${base}-fixed`,
+ ].join(' ');
+ });
const style = computed(() => {
const styleMap = {
bgColor: 'background',
- textColor: 'color'
- }
- const ret = Object.keys(styleMap).reduce((ret, key) => {
- if (props[key]) {
- ret[styleMap[key]] = props[key]
- }
- return ret
- }, {})
- // 偏移量
- if (ctx.slots.default && props.offsetXY) {
- const [x, y]: Array = props.offsetXY as Array
- const [yName, xName] = (props.badgePos as string).split('-')
- ret[yName] = y + 'px'
- ret[xName] = x + 'px'
+ textColor: 'color',
+ };
+ const ret = Object.keys(styleMap).reduce((result, key) => {
+ props[key] && (result[styleMap[key]] = props[key]);
+ return result;
+ }, {});
+ if (ctx.slots.default && props.offset) {
+ const [x, y]: Array = props.offset;
+ const [yName, xName] = props.position.split('-');
+ ret[yName] = y + 'px';
+ ret[xName] = x + 'px';
}
- return ret
- })
+ return ret;
+ });
const text = computed(() => {
if (props.showDot) {
- return
+ return;
}
if (typeof props.count === 'number' && typeof props.maxCount === 'number') {
- return props.count > props.maxCount ? `${props.maxCount}+` : props.count
+ return props.count > props.maxCount ? `${props.maxCount}+` : props.count;
}
- return props.count
- })
+ return props.count;
+ });
return () => {
return (
-
+
{ctx.slots.default?.()}
{text.value}
- )
- }
- }
-})
+ );
+ };
+ },
+});
diff --git a/packages/devui-vue/devui/button/__tests__/button.spec.ts b/packages/devui-vue/devui/button/__tests__/button.spec.ts
index a03789608d..3c60844552 100644
--- a/packages/devui-vue/devui/button/__tests__/button.spec.ts
+++ b/packages/devui-vue/devui/button/__tests__/button.spec.ts
@@ -2,9 +2,9 @@ import { mount } from '@vue/test-utils';
import Button from '../src/button';
describe('d-button', () => {
- it('btnStyle', () => {
+ it('variant', () => {
const wrapper = mount(Button, {
- props: { btnStyle: 'danger' },
+ props: { variant: 'danger' },
});
expect(wrapper.find('.devui-btn').classes()).toContain('devui-btn-danger');
});
@@ -36,17 +36,17 @@ describe('d-button', () => {
});
// 目前还不支持 loading
- // it('loading', async () => {
- // const handleClick = jest.fn();
- // const wrapper = mount(Button, {
- // props: {
- // showLoading: true,
- // btnClick: handleClick
- // },
- // });
- // await wrapper.trigger('click');
- // expect(handleClick).not.toBeCalled();
- // });
+ it('loading', async () => {
+ const handleClick = jest.fn();
+ const wrapper = mount(Button, {
+ props: {
+ showLoading: true,
+ btnClick: handleClick
+ },
+ });
+ await wrapper.trigger('click');
+ expect(handleClick).not.toBeCalled();
+ });
it('disabled', async () => {
const handleClick = jest.fn();
@@ -64,7 +64,7 @@ describe('d-button', () => {
it('slot', () => {
const btnText = 'vue3 devui';
const wrapper = mount(Button, {
- slots: {
+ slots: {
default: btnText
}
});
diff --git a/packages/devui-vue/devui/button/index.ts b/packages/devui-vue/devui/button/index.ts
index 91a9eb8d81..d2d44df9a1 100644
--- a/packages/devui-vue/devui/button/index.ts
+++ b/packages/devui-vue/devui/button/index.ts
@@ -1,21 +1,15 @@
-import type { App } from 'vue'
-import Button from './src/button'
-import { Loading } from '../loading/index'
+import type { App } from 'vue';
+import Button from './src/button';
-Button.install = function (app: App) {
- app.directive('dLoading', Loading)
- app.component(Button.name, Button)
-}
+export * from './src/button-types';
-export * from './src/button-types'
-
-export { Button }
+export { Button };
export default {
title: 'Button 按钮',
category: '通用',
status: '100%',
install(app: App): void {
- app.use(Button as any)
- }
-}
+ app.component(Button.name, Button);
+ },
+};
diff --git a/packages/devui-vue/devui/button/src/button-types.ts b/packages/devui-vue/devui/button/src/button-types.ts
index 2c09d2a67a..f489e4beb8 100644
--- a/packages/devui-vue/devui/button/src/button-types.ts
+++ b/packages/devui-vue/devui/button/src/button-types.ts
@@ -1,54 +1,40 @@
-import { ExtractPropTypes, PropType } from 'vue';
+import type { ComputedRef, ExtractPropTypes, PropType } from 'vue';
-export type IButtonType = 'button' | 'submit' | 'reset';
-export type IButtonStyle = 'common' | 'primary' | 'text' | 'text-dark' | 'danger';
-export type IButtonPosition = 'left' | 'right' | 'default';
+export type IButtonVariant = 'solid' | 'outline' | 'text';
+export type IButtonColor = 'secondary' | 'primary' | 'danger';
export type IButtonSize = 'lg' | 'md' | 'sm' | 'xs';
export const buttonProps = {
- type: {
- type: String as PropType
,
- default: 'button'
- },
- btnStyle: {
- type: String as PropType,
- default: 'primary'
+ variant: {
+ type: String as PropType,
+ default: 'outline',
},
size: {
type: String as PropType,
- default: 'md'
- },
- position: {
- type: String as PropType,
- default: 'default'
+ default: 'md',
},
- bordered: {
- type: Boolean,
- default: false
+ color: {
+ type: String as PropType,
},
icon: {
type: String,
- default: ''
+ default: '',
},
- showLoading: {
+ loading: {
type: Boolean,
- default: false
- },
- width: {
- type: String,
+ default: false,
},
disabled: {
type: Boolean,
- default: false
- },
- autofocus: {
- type: Boolean,
- default: false
+ default: false,
},
- onClick: {
- type: Function as PropType<(event: MouseEvent) => void>
- }
} as const;
+export type ButtonProps = ExtractPropTypes;
-export type ButtonProps = ExtractPropTypes;
\ No newline at end of file
+export interface UseButtonReturnType {
+ classes: ComputedRef<{
+ [key: string]: string | boolean;
+ }>;
+ iconClass: ComputedRef;
+}
diff --git a/packages/devui-vue/devui/button/src/button.scss b/packages/devui-vue/devui/button/src/button.scss
index a29c626694..9287577fa1 100644
--- a/packages/devui-vue/devui/button/src/button.scss
+++ b/packages/devui-vue/devui/button/src/button.scss
@@ -4,281 +4,236 @@
@import '../../style/theme/font';
@import '../../style/theme/corner';
-$devui-btn-loading-color: $devui-text;
-$devui-btn-xs-padding: 1px 5px;
-$devui-btn-xs-height: 24px;
-$devui-btn-icon-xs-min-width: 24px;
-$devui-btn-xs-min-width: 48px;
-$devui-btn-sm-padding: 1px 15px;
-$devui-btn-sm-min-width: 56px;
-$devui-btn-icon-sm-min-width: 24px;
-$devui-btn-sm-height: 24px;
-$devui-btn-min-width: 64px;
-$devui-btn-height: 28px;
-$devui-btn-padding: 3px 20px;
-$devui-btn-lg-padding: 5px 23px;
-$devui-btn-lg-min-width: 72px;
-$devui-btn-icon-lg-min-width: 32px;
-$devui-btn-lg-height: 32px;
-$devui-btn-xs-font-size: $devui-font-size-sm;
-$devui-btn-sm-font-size: $devui-font-size-sm;
-$devui-btn-font-size: $devui-font-size-md;
-$devui-btn-lg-font-size: $devui-font-size-lg;
-$devui-btn-line-height: $devui-line-height-base;
-
-$devui-btn-normal-config: (
- text: (
- color: $devui-brand-active,
- padding: 0
- ),
- text-dark: (
- color: $devui-brand-active,
- padding: 0
- ),
- common: (
- color: $devui-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-block,
- border-color: $devui-line
- ),
- stress: (
- color: $devui-light-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-primary
- ),
- primary: (
- color: $devui-light-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-primary
- ),
- danger: (
- color: $devui-light-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-contrast
- ),
- success: (
- color: $devui-light-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-success
- ),
- warning: (
- color: $devui-light-text,
- min-width: $devui-btn-min-width,
- background-color: $devui-warning
- ),
- xs: (
- padding: $devui-btn-xs-padding,
- height: $devui-btn-xs-height,
- font-size: $devui-btn-xs-font-size,
- min-width: $devui-btn-xs-min-width
- ),
- sm: (
- padding: $devui-btn-sm-padding,
- height: $devui-btn-sm-height,
- font-size: $devui-btn-sm-font-size,
- min-width: $devui-btn-sm-min-width,
- ),
- lg: (
- padding: $devui-btn-lg-padding,
- height: $devui-btn-lg-height,
- font-size: $devui-btn-lg-font-size,
- min-width: $devui-btn-lg-min-width,
- ),
- left: (
- border-radius: $devui-border-radius 0 0 $devui-border-radius,
- ),
- right: (
- border-radius: 0 $devui-border-radius $devui-border-radius 0,
- ),
- default: (
- border-radius: $devui-border-radius,
- )
-);
-
-$devui-btn-pseudo-config: (
- text: (
- hover: (
- background-color: transparent,
- color: $devui-brand-active-focus
- ),
- focus: (
- background-color: transparent,
- color: $devui-brand-active-focus
- ),
- active: (
- background-color: transparent,
- color: $devui-brand-active-focus
- )
- ),
- text-dark: (
- hover: (
- background-color: transparent,
- color: $devui-brand-active-focus
- ),
- focus: (
- background-color: transparent,
- color: $devui-brand-active-focus
- ),
- active: (
- background-color: transparent,
- color: $devui-brand-active-focus
- )
- ),
- common: (
- hover: (
- border-color: $devui-form-control-line-active,
- color: $devui-brand-active
- ),
- focus: (
- border-color: $devui-form-control-line-active,
- color: $devui-brand-active
- ),
- active: (
- border-color: $devui-form-control-line-active,
- color: $devui-brand-active
- )
- ),
- stress: (
- hover: (
- background-color: $devui-primary-hover
- ),
- focus: (
- background-color: $devui-primary-hover
- ),
- active: (
- background-color: $devui-primary-active
- )
- ),
- primary: (
- hover: (
- background-color: $devui-primary-hover
- ),
- focus: (
- background-color: $devui-primary-hover
- ),
- active: (
- background-color: $devui-primary-active
- )
- ),
- danger: (
- hover: (
- background-color: $devui-contrast-hover
- ),
- focus: (
- background-color: $devui-contrast-hover
- ),
- active: (
- background-color: $devui-contrast-active
- )
- )
-);
-
.devui-btn {
- padding: $devui-btn-padding;
- font-size: $devui-btn-font-size;
- height: $devui-btn-height;
- line-height: $devui-btn-line-height;
+ padding: 0 20px;
+ font-size: $devui-font-size-md;
+ height: 28px;
+ line-height: $devui-line-height-base;
+ border: none;
border-radius: $devui-border-radius;
border-width: 1px;
- border-color: transparent;
background-color: transparent;
- @each $type in text, text-dark, common, stress, primary, danger, success, warning, left, right, xs, sm, lg {
- &.devui-btn-#{$type} {
- @each $key, $value in map-get($devui-btn-normal-config, $type) {
- #{$key}: $value;
- }
- }
+
+ &:hover {
+ cursor: pointer;
}
- @each $type in text, text-dark, common, stress, primary, danger {
- &.devui-btn-#{$type} {
- @each $pseudo, $value in map-get($devui-btn-pseudo-config, $type) {
- &:#{$pseudo} {
- @each $key, $value2 in map-get(map-get($devui-btn-pseudo-config, $type), $pseudo) {
- #{$key}: $value2;
- }
- }
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+
+ &-solid {
+ color: $devui-light-text;
+ min-width: 64px;
+
+ &-secondary {
+ color: $devui-text;
+ background-color: $devui-dividing-line;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &:focus {
+ opacity: 0.8;
+ }
+
+ &:active {
+ opacity: 0.8;
+ }
+
+ &:disabled {
+ opacity: 0.8;
}
}
- }
- &.devui-btn-common {
- &:disabled {
- color: $devui-disabled-text;
- background: $devui-disabled-bg;
- border: 1px solid $devui-disabled-line;
+ &-primary {
+ background-color: $devui-primary;
+
+ &:hover {
+ background-color: $devui-primary-hover;
+ }
+
+ &:focus {
+ background-color: $devui-primary-hover;
+ }
+
+ &:active {
+ background-color: $devui-primary-active;
+ }
+
+ &:disabled {
+ color: $devui-light-text;
+ background: $devui-primary-disabled;
+ border: none;
+ }
}
- }
- &.devui-btn-primary {
- &:disabled {
- color: $devui-light-text;
- background: $devui-primary-disabled;
- border: none;
+ &-danger {
+ background-color: $devui-contrast;
+
+ &:hover {
+ background-color: $devui-contrast-hover;
+ }
+
+ &:focus {
+ background-color: $devui-contrast-hover;
+ }
+
+ &:active {
+ background-color: $devui-contrast-active;
+ }
+
+ &:disabled {
+ background-color: $devui-contrast-disabled;
+ }
}
}
- &.devui-btn-danger {
- &:disabled {
- color: $devui-disabled-text;
- background: $devui-disabled-bg;
- border: 1px solid $devui-disabled-line;
+ &-outline {
+ background-color: $devui-block;
+ border-style: solid;
+
+ &-secondary {
+ color: $devui-text;
+ border-color: $devui-line;
+
+ &:hover {
+ color: $devui-brand-active;
+ border-color: $devui-form-control-line-active;
+ }
+
+ &:focus {
+ color: $devui-brand-active;
+ border-color: $devui-form-control-line-active;
+ }
+
+ &:active {
+ color: $devui-brand-active;
+ border-color: $devui-form-control-line-active;
+ }
+
+ &:disabled {
+ color: $devui-disabled-text;
+ border-color: $devui-disabled-line;
+ background-color: $devui-disabled-bg;
+ }
}
- }
- &.devui-btn-text-dark {
- &:disabled {
- color: $devui-disabled-text;
+ &-primary {
+ color: $devui-brand-active;
+ border-color: $devui-form-control-line-active;
+
+ &:hover {
+ color: $devui-brand-active-focus;
+ border-color: $devui-form-control-line-active-hover;
+ }
+
+ &:focus {
+ color: $devui-brand-active-focus;
+ border-color: $devui-form-control-line-active-hover;
+ }
+
+ &:active {
+ color: $devui-brand-active-focus;
+ border-color: $devui-form-control-line-active-hover;
+ }
+
+ &:disabled {
+ opacity: 0.8;
+ }
}
- }
- &.devui-btn-text {
- &:disabled {
- color: $devui-disabled-text;
+ &-danger {
+ color: $devui-contrast;
+ border-color: $devui-contrast;
+
+ &:hover,
+ &:focus,
+ &:active,
+ &:disabled {
+ opacity: 0.8;
+ }
}
}
- &.bordered {
- &.devui-btn-common {
+ &-text {
+ padding: 0;
+
+ &-secondary {
color: $devui-text;
- border-color: transparent;
- &:hover,
+ &:hover {
+ color: $devui-list-item-hover-text;
+ }
+
&:focus {
- border-color: $devui-primary-hover;
+ color: $devui-list-item-hover-text;
}
&:active {
- border-color: $devui-primary-active;
+ color: $devui-list-item-hover-text;
}
- }
- &.devui-btn-stress,
- &.devui-btn-primary {
- border-color: $devui-brand;
- color: $devui-brand;
- background-color: $devui-block;
+ &:disabled {
+ color: $devui-disabled-text;
+ }
}
- &.devui-btn-success {
- border-color: $devui-success;
- color: $devui-success;
- background-color: $devui-block;
- }
+ &-primary {
+ color: $devui-brand-active;
+
+ &:hover {
+ color: $devui-brand-active-focus;
+ }
- &.devui-btn-warning {
- border-color: $devui-warning;
- color: $devui-warning;
- background-color: $devui-block;
+ &:focus {
+ color: $devui-brand-active-focus;
+ }
+
+ &:active {
+ color: $devui-brand-active-focus;
+ }
+
+ &:disabled {
+ opacity: 0.8;
+ }
}
- &.devui-btn-danger {
- border-color: $devui-danger;
- color: $devui-danger;
- background-color: $devui-block;
+ &-danger {
+ color: $devui-contrast;
+
+ &:hover,
+ &:focus,
+ &:active,
+ &:disabled {
+ opacity: 0.8;
+ }
}
}
- &.d-btn-icon {
+ &-xs {
+ padding: 0 4px;
+ height: 24px;
+ font-size: $devui-font-size-sm;
+ min-width: 48px;
+ }
+
+ &-sm {
+ padding: 0 16px;
+ height: 24px;
+ font-size: $devui-font-size-sm;
+ min-width: 56px;
+ }
+
+ &-lg {
+ padding: 0 24px;
+ height: 32px;
+ font-size: $devui-font-size-lg;
+ min-width: 72px;
+ }
+
+ &.devui-btn-icon {
&:hover,
&:focus {
border: 1px solid $devui-list-item-hover-bg;
@@ -291,9 +246,8 @@ $devui-btn-pseudo-config: (
}
&:not(:disabled) {
- &.devui-btn-text,
- &.devui-btn-text-dark {
- &.d-btn-icon-wrap {
+ &.devui-btn-text {
+ &.devui-btn-icon-wrap {
color: $devui-text;
.devui-icon-fix {
@@ -320,39 +274,29 @@ $devui-btn-pseudo-config: (
}
}
-.devui-btn-host {
- display: inline-block;
-}
-
.devui-btn {
transition: background-color 0.2s;
- border-style: solid;
white-space: nowrap;
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: center;
- &:disabled,
- &[disabled] {
- cursor: not-allowed;
- }
-
- &.d-btn-icon {
- padding: 7px 8px;
+ &.devui-btn-icon {
+ padding: 8px;
line-height: 1em;
border: 1px solid transparent;
&.devui-btn-xs,
&.devui-btn-sm {
- padding: 5px;
+ padding: 4px;
}
&.devui-btn-xs {
- min-width: $devui-btn-icon-xs-min-width;
+ min-width: 24px;
}
&.devui-btn-sm {
- min-width: $devui-btn-icon-sm-min-width;
+ min-width: 24px;
}
&.devui-btn-lg {
@@ -360,11 +304,11 @@ $devui-btn-pseudo-config: (
font-size: $devui-font-size-icon;
}
- min-width: $devui-btn-icon-lg-min-width;
+ min-width: 32px;
}
}
- &:not(.d-btn-icon) {
+ &:not(.devui-btn-icon) {
.icon-fix {
font-size: $devui-font-size-icon;
}
@@ -376,20 +320,6 @@ $devui-btn-pseudo-config: (
font-size: $devui-font-size;
}
-.devui-btn.devui-btn-common:not(:disabled) {
- .devui-icon-fix {
- color: $devui-text-weak;
- }
-
- &:hover,
- &:active,
- &:focus {
- .devui-icon-fix {
- color: $devui-icon-fill-active;
- }
- }
-}
-
.button-content {
display: inline-block;
position: relative;
diff --git a/packages/devui-vue/devui/button/src/button.tsx b/packages/devui-vue/devui/button/src/button.tsx
index 554a99b2a6..58e50f20a8 100644
--- a/packages/devui-vue/devui/button/src/button.tsx
+++ b/packages/devui-vue/devui/button/src/button.tsx
@@ -1,71 +1,36 @@
-import { computed, defineComponent, ref } from 'vue';
+import { defineComponent, toRefs } from 'vue';
+import type { SetupContext } from 'vue';
import { Icon } from '../../icon';
-import { buttonProps } from './button-types';
-
+import loadingDirective from '../../loading/src/directive';
+import { buttonProps, ButtonProps } from './button-types';
+import useButton from './use-button';
import './button.scss';
export default defineComponent({
name: 'DButton',
+ directives: {
+ devLoading: loadingDirective,
+ },
props: buttonProps,
- setup(props, ctx) {
- const buttonContent = ref(null);
+ emits: ['click'],
+ setup(props: ButtonProps, ctx: SetupContext) {
+ const { icon, disabled, loading } = toRefs(props);
+ const { classes, iconClass } = useButton(props, ctx);
const onClick = (e: MouseEvent) => {
- if (props.showLoading) {
+ if (loading.value) {
return;
}
- props.onClick?.(e);
- }
-
- const hasContent = computed(() => ctx.slots.default);
-
- const btnClass = computed(() => {
- const { btnStyle, size, position, bordered, icon } = props;
- const origin = `devui-btn devui-btn-${btnStyle} devui-btn-${size} devui-btn-${position}`;
- const borderedClass = bordered ? 'bordered' : '';
- const btnIcon = !!icon && !hasContent.value && btnStyle !== 'primary' ? 'd-btn-icon' : '';
- const btnIconWrap = !!icon ? 'd-btn-icon-wrap' : '';
- return `${origin} ${borderedClass} ${btnIcon} ${btnIconWrap}`;
- });
-
- const iconClass = computed(() => {
- if (!props.icon) {
- return;
- }
- const origin = 'devui-icon-fix icon';
- if (hasContent.value) {
- return `${origin} clear-right-5`;
- } else {
- return origin;
- }
- });
+ ctx.emit('click', e);
+ };
return () => {
- const {
- icon,
- type,
- disabled,
- showLoading,
- width
- } = props;
return (
-
-
-
+
);
- }
- }
+ };
+ },
});
diff --git a/packages/devui-vue/devui/button/src/use-button.ts b/packages/devui-vue/devui/button/src/use-button.ts
new file mode 100644
index 0000000000..a27c458832
--- /dev/null
+++ b/packages/devui-vue/devui/button/src/use-button.ts
@@ -0,0 +1,35 @@
+import { computed } from 'vue';
+import type { SetupContext } from 'vue';
+import { ButtonProps, UseButtonReturnType } from './button-types';
+
+export default function useButton(props: ButtonProps, ctx: SetupContext): UseButtonReturnType {
+ const hasContent = computed(() => ctx.slots.default);
+ const colorMap = {
+ solid: 'primary',
+ outline: 'secondary',
+ text: 'secondary',
+ };
+ const defaultColor = colorMap[props.variant];
+
+ const classes = computed(() => ({
+ 'devui-btn': true,
+ [`devui-btn-${props.variant}`]: true,
+ [`devui-btn-${props.variant}-${props.color || defaultColor}`]: true,
+ [`devui-btn-${props.size}`]: true,
+ 'devui-btn-icon-wrap': props.icon,
+ 'devui-btn-icon': props.icon && !hasContent.value && props.variant !== 'solid',
+ }));
+ const iconClass = computed(() => {
+ if (!props.icon) {
+ return;
+ }
+ const origin = 'devui-icon-fix icon';
+ if (hasContent.value) {
+ return `${origin} clear-right-5`;
+ } else {
+ return origin;
+ }
+ });
+
+ return { classes, iconClass };
+}
diff --git a/packages/devui-vue/devui/card/__tests__/__snapshots__/card.spec.ts.snap b/packages/devui-vue/devui/card/__tests__/__snapshots__/card.spec.ts.snap
new file mode 100644
index 0000000000..76e8d750da
--- /dev/null
+++ b/packages/devui-vue/devui/card/__tests__/__snapshots__/card.spec.ts.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`card should render correctly 1`] = `
+
+`;
diff --git a/packages/devui-vue/devui/carousel/__tests__/carousel.spec.ts b/packages/devui-vue/devui/carousel/__tests__/carousel.spec.ts
index 525d71d769..a900c84833 100644
--- a/packages/devui-vue/devui/carousel/__tests__/carousel.spec.ts
+++ b/packages/devui-vue/devui/carousel/__tests__/carousel.spec.ts
@@ -1,8 +1,7 @@
import { ref, nextTick } from 'vue'
import { mount } from '@vue/test-utils';
-import Carousel from '../carousel';
-import CarouselItem from '../item';
-import Button from '../../button'
+import { CarouselItem, Carousel } from '../index';
+import { Button } from '../../button'
const wait = (ms = 100) =>
new Promise(resolve => setTimeout(() => resolve(), ms))
@@ -140,13 +139,13 @@ describe('d-carousel', () => {
{{ item }}
- 上一张
- 下一张
- 第一张
+ 上一张
+ 下一张
+ 第一张
`,
setup() {
- const items = ref(["page 1", 'page 2', 'page 3', 'page 4'])
+ const items = ref(['page 1', 'page 2', 'page 3', 'page 4'])
const activeIndex = ref(0)
const carousel = ref()
diff --git a/packages/devui-vue/devui/carousel/index.ts b/packages/devui-vue/devui/carousel/index.ts
index 792419a4fc..6f05109ddf 100644
--- a/packages/devui-vue/devui/carousel/index.ts
+++ b/packages/devui-vue/devui/carousel/index.ts
@@ -1,16 +1,17 @@
import type { App } from 'vue'
import Carousel from './src/carousel'
-import CarouseItem from './src/item'
+import CarouselItem from './src/item'
Carousel.install = function(app: App) {
app.component(Carousel.name, Carousel)
}
-CarouseItem.install = function(app: App) {
- app.component(CarouseItem.name, CarouseItem);
+CarouselItem.install = function(app: App) {
+ app.component(CarouselItem.name, CarouselItem);
}
export { Carousel }
+export { CarouselItem }
export default {
title: 'Carousel 走马灯',
@@ -18,6 +19,6 @@ export default {
status: '80%',
install(app: App): void {
app.use(Carousel as any)
- app.use(CarouseItem as any)
+ app.use(CarouselItem as any)
}
}
diff --git a/packages/devui-vue/devui/cascader/components/cascader-item/index.scss b/packages/devui-vue/devui/cascader/components/cascader-item/index.scss
index f3368ead25..35161178bf 100644
--- a/packages/devui-vue/devui/cascader/components/cascader-item/index.scss
+++ b/packages/devui-vue/devui/cascader/components/cascader-item/index.scss
@@ -9,15 +9,18 @@
color: $devui-text;
cursor: pointer;
@include flex(flex-start);
+
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
+
.cascader-li__wraper {
flex: 1;
@include flex(flex-start);
}
+
.dropdown-item-label {
display: inline-block;
flex: 1;
@@ -49,6 +52,7 @@
margin-right: 4px;
font-size: $devui-font-size-icon;
color: $devui-text;
+
&.disabled {
color: $devui-disabled-text !important;
}
diff --git a/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss
index 848f54b3c6..315baf646f 100644
--- a/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss
+++ b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss
@@ -1,6 +1,7 @@
@import '../../../style/theme/color';
@import '../../../style/theme/corner';
@import '../../../style/core/font';
+
.devui-tags {
&-input {
flex: 1;
@@ -11,6 +12,7 @@
background-color: $devui-base-bg;
transition: border-color 300ms cubic-bezier(0.645, 0.045, 0.355, 1);
}
+
&-box {
width: 100%;
overflow: auto;
@@ -20,10 +22,11 @@
align-items: center;
flex-wrap: wrap;
}
+
&-placeholder {
font-size: $devui-font-size;
line-height: 22px;
margin-left: 6px;
color: $devui-placeholder;
}
-}
\ No newline at end of file
+}
diff --git a/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss b/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss
index 2e57f9bc2f..71dd8cb947 100644
--- a/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss
+++ b/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss
@@ -1,6 +1,7 @@
@import '../../../style/theme/color';
@import '../../../style/theme/corner';
@import '../../../style/core/font';
+
.devui-tag {
margin: 2px 4px 2px 0;
display: inline-block;
@@ -12,6 +13,7 @@
border-radius: $devui-border-radius;
border-color: inherit;
border: 0 solid;
+
span {
min-height: 20px;
line-height: 20px;
@@ -20,11 +22,12 @@
position: relative;
cursor: default;
}
+
&__close {
margin-left: 12px;
- font-size: $devui-font-size;
+ font-size: $devui-font-size;
cursor: pointer;
- color: #fff;
+ color: #ffffff;
width: 14px;
height: 14px;
line-height: 14px;
@@ -32,8 +35,9 @@
border-radius: 50%;
display: inline-block;
text-align: center;
+
&:hover {
background-color: $devui-brand;
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts
index 603d7b4a09..de4a70fbe0 100644
--- a/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts
+++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts
@@ -1,7 +1,7 @@
/**
* 处理cascader-item中需要的参数
*/
-import { cloneDeep } from 'lodash-es'
+import { cloneDeep } from 'lodash'
import { ref, reactive, Ref } from 'vue'
import { CascaderProps, UseCascaderItemCallback, CascaderItem } from '../src/cascader-types'
diff --git a/packages/devui-vue/devui/cascader/src/cascader.tsx b/packages/devui-vue/devui/cascader/src/cascader.tsx
index a0e87f10ed..4ff039a2ea 100644
--- a/packages/devui-vue/devui/cascader/src/cascader.tsx
+++ b/packages/devui-vue/devui/cascader/src/cascader.tsx
@@ -1,5 +1,5 @@
// 公共库
-import { cloneDeep } from 'lodash-es'
+import { cloneDeep } from 'lodash'
import { defineComponent, ref, Ref, reactive, watch, toRef } from 'vue'
// 组件
diff --git a/packages/devui-vue/devui/color-picker/__tests__/color-picker.spec.ts b/packages/devui-vue/devui/color-picker/__tests__/color-picker.spec.ts
new file mode 100644
index 0000000000..a4cd0eb1f6
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/__tests__/color-picker.spec.ts
@@ -0,0 +1,8 @@
+import { mount } from '@vue/test-utils';
+import { ColorPicker } from '../index';
+
+describe('color-picker test', () => {
+ it('color-picker init render', async () => {
+ // todo
+ })
+})
diff --git a/packages/devui-vue/devui/color-picker/index.ts b/packages/devui-vue/devui/color-picker/index.ts
new file mode 100644
index 0000000000..0b15cd3019
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/index.ts
@@ -0,0 +1,17 @@
+import type { App } from 'vue'
+import ColorPicker from './src/color-picker'
+
+ColorPicker.install = function (app: App): void {
+ app.component(ColorPicker.name, ColorPicker)
+}
+
+export { ColorPicker }
+
+export default {
+ title: 'ColorPicker 颜色选择器',
+ category: '数据录入',
+ status: '80%', // TODO: 组件若开发完成则填入"100%",并删除该注释
+ install(app: App): void {
+ app.use(ColorPicker as any)
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/color-picker-types.ts b/packages/devui-vue/devui/color-picker/src/color-picker-types.ts
new file mode 100644
index 0000000000..7d6c4164bf
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/color-picker-types.ts
@@ -0,0 +1,26 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+export const colorPickerProps = {
+ modelValue: {
+ type: [Object, String] as PropType
+ },
+ mode: {
+ type: String
+ },
+ showAlpha: {
+ type: Boolean,
+ default: true
+ },
+ dotSize: {
+ type: Number,
+ default: 15
+ },
+ swatches: {
+ type: Array as PropType
+ },
+ showHistory: {
+ type: Boolean,
+ default: true
+ }
+} as const
+
+export type ColorPickerProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/color-picker.scss b/packages/devui-vue/devui/color-picker/src/color-picker.scss
new file mode 100644
index 0000000000..1e51158300
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/color-picker.scss
@@ -0,0 +1,83 @@
+@import '../../style/theme/color';
+
+.devui-color-picker {
+ position: relative;
+
+ &-position {
+ position: absolute;
+ z-index: 9999;
+ background-color: $devui-connected-overlay-bg;
+ top: 0;
+ }
+
+ &-color-value {
+ display: flex;
+ position: absolute;
+ z-index: 4;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-weight: bold;
+ color: rgb(204, 15, 15);
+ }
+
+ &-container {
+ padding: 3px;
+ border: 1px solid rgb(224, 224, 230);
+ border-radius: 3px;
+
+ &-wrap {
+ width: 100%;
+ height: 26px;
+ box-sizing: content-box;
+ box-shadow: 3px 0 5px #00000014;
+ position: relative;
+ cursor: pointer;
+ overflow: hidden;
+ display: inline-block;
+ vertical-align: middle;
+
+ &-current-color {
+ top: 0;
+ right: 0;
+ left: 0;
+ position: absolute;
+ z-index: 3;
+ width: 100%;
+ height: 100%;
+ }
+
+ &-current-color-transparent {
+ top: 0;
+ right: 0;
+ left: 0;
+ overflow: hidden;
+ padding: 3px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ z-index: 2;
+ }
+
+ &-transparent {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+ background-repeat: repeat;
+ }
+ }
+ }
+}
+
+.color-picker-transition-enter-from,
+.color-picker-transition-leave-to {
+ opacity: 0;
+}
+
+.color-picker-transition-enter-to,
+.color-picker-transition-leave-from {
+ opacity: 1;
+}
+
+.color-picker-transition-enter-active,
+.color-picker-transition-leave-active {
+ transition: opacity 0.2s ease-in-out;
+}
diff --git a/packages/devui-vue/devui/color-picker/src/color-picker.tsx b/packages/devui-vue/devui/color-picker/src/color-picker.tsx
new file mode 100644
index 0000000000..a3c91a2132
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/color-picker.tsx
@@ -0,0 +1,178 @@
+import {
+ defineComponent,
+ ref,
+ computed,
+ onMounted,
+ watch,
+ nextTick,
+ provide,
+ Teleport,
+ unref,
+ readonly,
+ Transition
+} from 'vue'
+import {
+ useReactive,
+ colorPickerResize,
+ isExhibitionColorPicker,
+ changeColorValue
+} from './utils/composeable'
+import { colorPickerProps, ColorPickerProps } from './color-picker-types'
+import colorPanel from './components/color-picker-panel/color-picker-panel'
+import './color-picker.scss'
+import { parseColor, extractColor, RGBAtoCSS } from './utils/color-utils'
+import { ColorPickerColor } from './utils/color-utils-types'
+export default defineComponent({
+ name: 'DColorPicker',
+ components: {
+ colorPanel
+ },
+ props: colorPickerProps,
+ emits: ['update:modelValue'],
+ setup(props: ColorPickerProps, { emit }) {
+ const DEFAUTL_MODE = 'rgb'
+ const provideData = {
+ showAlpha: useReactive(() => props.showAlpha),
+ swatches: useReactive(() => props.swatches),
+ dotSize: useReactive(() => props.dotSize),
+ showHistory: useReactive(() => props.showHistory)
+ }
+ provide('provideData', readonly(provideData))
+ const initialColor = ref(null)
+ const colorCubeRef = ref()
+ const pickerRef = ref()
+ const containerRef = ref()
+ const left = ref(0)
+ const top = ref(0)
+ const isChangeTextColor = ref(true)
+ const showColorPicker = ref(false)
+ const formItemText = ref(`${props.mode ?? DEFAUTL_MODE}`)
+ const mode = ref(unref(props.mode))
+ onMounted(() => {
+ // resize 响应式 colorpicker
+ window.addEventListener('resize', resize)
+ // 点击展示 colorpicker
+ window.addEventListener('click', isExhibition)
+ })
+ // ** computeds
+ // colorpicker panel 组件位置
+ const colorPickerPostion = computed(() => {
+ if (colorCubeRef.value) {
+ return {
+ transform: `translate(${left.value}px, ${top.value}px)`
+ }
+ }
+ return null
+ })
+ // 交互触发item 颜色 面板 动态修改alpha后要还原 alpha 2021.12.18
+ const tiggerColor = computed(() => {
+ const currentColor = initialColor.value.rgba
+ const trigger = { ...currentColor, a: props.showAlpha ? currentColor.a : 1 }
+ return {
+ backgroundColor: `${RGBAtoCSS(trigger)}`
+ }
+ })
+ // 交互面板 的value 值 动态展示 根据不同 type
+ const formItemValue = computed(() => {
+ return extractColor(initialColor.value, '', formItemText.value, props.showAlpha)
+ })
+ // 动态 根据当前 透明度修改文本颜色 tips:根据不同 面板颜色 目前 不够优雅
+ const textColor = computed(() => {
+ // 数字代表 hsv 中的value 值 纵轴 动态切换 文本颜色
+ return changeColorValue(initialColor.value, 0.5)
+ })
+ // ** emits
+ // 动态 交互面板 文本展示颜色 tips:根据不同 面板颜色 目前 不够优雅
+ function changeTextColor(value: boolean): void {
+ isChangeTextColor.value = value
+ }
+ // 通过修改画板 颜色 修改 v-model 颜色
+ function changePaletteColor(colorMap: ColorPickerColor): void {
+ updateUserColor(colorMap)
+ }
+ // 通过用户点击触发修改 交互面板 文本类型
+ function changeTextModeType(type: string): void {
+ mode.value = type
+ formItemText.value = type
+ }
+
+ // 初始化的时候 确定 colopicker位置 由于 pickerref 默认 为 undefined 所以监听 showcolorpicker
+ watch(
+ () => showColorPicker.value,
+ (newValue) => {
+ const textPalette = colorCubeRef.value?.getBoundingClientRect()
+ newValue &&
+ nextTick(() => {
+ pickerRef.value.style.transform = `translate(${textPalette.left + 'px'}, ${
+ textPalette.top + window.scrollY + textPalette.height + 'px'
+ })`
+ })
+ }
+ )
+ // 监听用户输入 2021.12.10
+ watch(
+ () => props.modelValue,
+ (newValue) => {
+ // 全部转换成对象
+ updateUserColor(parseColor(newValue, initialColor.value))
+ },
+ { immediate: true }
+ )
+ // 更新用户输入颜色 2021.12.10
+ function updateUserColor(color) {
+ initialColor.value = color
+ // 提取颜色 2021.12.10
+ const value = extractColor(initialColor.value, props.modelValue, mode.value, props.showAlpha)
+ emit('update:modelValue', value)
+ }
+ function resize() {
+ return colorPickerResize(colorCubeRef, top, left)
+ }
+ function isExhibition(event: Event) {
+ return isExhibitionColorPicker(event, colorCubeRef, pickerRef, showColorPicker)
+ }
+ return () => {
+ return (
+
+
+
+
+
+
+
{formItemValue.value}
+
+
+
+
+
+ {showColorPicker.value ? (
+
+
+
+ ) : null}
+
+
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.scss b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.scss
new file mode 100644
index 0000000000..b624a0ea53
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.scss
@@ -0,0 +1,42 @@
+.devui-color-picker-alpha-slider {
+ position: relative;
+ margin-bottom: 15px;
+ width: 100%;
+ height: 14px;
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
+ border-radius: 15px;
+
+ &.transparent {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+ background-repeat: repeat;
+ }
+
+ &__bar {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ border-radius: 15px;
+
+ &-pointer {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ }
+
+ &-handle {
+ width: 14px;
+ height: 14px;
+ border-radius: 6px;
+ transform: translate(-7px, -2px);
+ background-color: #f8f8f8;
+ margin-top: 2px;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
+ cursor: pointer;
+
+ &.vertical {
+ transform: translate(0, -7px);
+ margin-top: 0;
+ }
+ }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx
new file mode 100644
index 0000000000..3df8c6c99e
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx
@@ -0,0 +1,98 @@
+import { computed, defineComponent, ref, onMounted } from 'vue'
+import { colorPickerAlphaSliderProps } from './color-picker-alpha-slider-types'
+import { DOMUtils } from '../../utils/domDragger'
+import { RGBtoCSS, fromHSVA } from '../../utils/color-utils'
+import './color-alpha-slider.scss'
+export default defineComponent({
+ name: 'ColorAlphaSlider',
+ props: colorPickerAlphaSliderProps,
+ emits: ['update:modelValue'],
+ setup(props: colorPickerAlphaSliderProps, ctx) {
+ const DEFAULT_TRANSITION = { transition: 'all 0.3s ease' }
+ const clickTransfrom = ref(DEFAULT_TRANSITION)
+ const barElement = ref(null)
+ const cursorElement = ref(null)
+ const onClickSider = (event: Event) => {
+ const target = event.target
+ if (target !== barElement.value) {
+ onMoveBar(event as MouseEvent)
+ }
+ }
+
+ const onMoveBar = (event: MouseEvent) => {
+ event.stopPropagation()
+ if (barElement.value && cursorElement.value) {
+ const rect = barElement.value.getBoundingClientRect()
+ const offsetWidth = cursorElement.value.offsetWidth
+ let left = event.clientX - rect.left
+ left = Math.max(offsetWidth / 2, left)
+ left = Math.min(left, rect.width - offsetWidth / 2)
+ const alpha = Math.round(((left - offsetWidth / 2) / (rect.width - offsetWidth)) * 100)
+ ctx.emit('update:modelValue', fromHSVA({ ...props.modelValue.hsva, a: alpha / 100 }))
+ }
+ }
+
+ const getBackgroundStyle = computed(() => {
+ return {
+ background: `linear-gradient(to right, transparent , ${RGBtoCSS(props.modelValue.rgba)})`
+ }
+ })
+
+ const getCursorLeft = computed(() => {
+ if (barElement.value && cursorElement.value) {
+ const alpha = props.modelValue.rgba.a
+ const rect = barElement.value.getBoundingClientRect()
+ const offsetWidth = cursorElement.value.offsetWidth
+ return Math.round(alpha * (rect.width - offsetWidth) + offsetWidth / 2)
+ }
+ return 0
+ })
+
+ const getCursorStyle = computed(() => {
+ const left = getCursorLeft.value
+ return {
+ left: left + 'px',
+ top: 0,
+ ...clickTransfrom.value
+ }
+ })
+ onMounted(() => {
+ const dragConfig = {
+ drag: (event: Event) => {
+ clickTransfrom.value = null
+ onMoveBar(event as MouseEvent)
+ },
+ end: (event: Event) => {
+ clickTransfrom.value = DEFAULT_TRANSITION
+ onMoveBar(event as MouseEvent)
+ }
+ }
+ if (barElement.value && cursorElement.value) {
+ DOMUtils.triggerDragEvent(barElement.value, dragConfig)
+ }
+ })
+ const alphaClass = computed(() => {
+ return ['devui-color-picker-alpha-slider', 'transparent']
+ })
+ return () => {
+ return (
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-picker-alpha-slider-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-picker-alpha-slider-types.ts
new file mode 100644
index 0000000000..51f23d935d
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-picker-alpha-slider-types.ts
@@ -0,0 +1,25 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import type { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerAlphaSliderProps = {
+ color: {
+ type: Object
+ },
+ modelValue: {
+ type: Object as PropType
+ },
+ rgba: {
+ type: Object,
+ default: null
+ },
+ height: {
+ type: Number,
+ default: 15
+ },
+ width: {
+ type: Number,
+ default: 300
+ }
+} as const
+
+export type colorPickerAlphaSliderProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic-types.ts
new file mode 100644
index 0000000000..b719464714
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic-types.ts
@@ -0,0 +1,10 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import type { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerBasicColorProps = {
+ color: {
+ type: Object as PropType
+ }
+} as const
+
+export type colorPickerBasicColorProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.scss b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.scss
new file mode 100644
index 0000000000..d3edefbe35
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.scss
@@ -0,0 +1,15 @@
+.devui-color-picker-basic {
+ flex-wrap: wrap;
+ // justify-content: space-between;
+ align-items: center;
+
+ &-div {
+ box-sizing: content-box;
+ margin: 4px;
+ width: 20px;
+ height: 20px;
+ border: solid 1px #e7e7e7;
+ cursor: pointer;
+ border-radius: 1px;
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.tsx b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.tsx
new file mode 100644
index 0000000000..3117b00cb1
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-basic/color-basic.tsx
@@ -0,0 +1,32 @@
+import { defineComponent, ref, inject } from 'vue'
+import { colorPickerBasicColorProps } from './color-basic-types'
+import { provideColorOptions } from '../../utils/color-utils-types'
+import { fromHex } from '../../utils/color-utils'
+import './color-basic.scss'
+import { color } from '../../utils/color'
+export default defineComponent({
+ name: 'ColorBasic',
+ props: colorPickerBasicColorProps,
+ setup(props) {
+ const swatchesInject: provideColorOptions = inject('provideData')
+
+ const currentColor = ref(props.color)
+ function changeBasicColor(hex: string) {
+ currentColor.value = fromHex(hex)
+ }
+ const swatches = ref(Object.values(swatchesInject.swatches ?? []))
+ return () => {
+ return (
+
+ {(swatches.value.length !== 0 ? swatches.value : color).map((hex: string) => (
+
changeBasicColor(hex)}
+ class={['devui-color-picker-basic-div']}
+ style={{ backgroundColor: hex }}
+ >
+ ))}
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.scss b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.scss
new file mode 100644
index 0000000000..5240bb27bb
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.scss
@@ -0,0 +1,65 @@
+.devui-color-picker-edit {
+ justify-content: space-between;
+ align-items: center;
+
+ &-text {
+ display: inline-block;
+ width: 50px;
+ padding: 0 10px;
+ user-select: none;
+ }
+
+ &-name {
+ user-select: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ width: 50px;
+ }
+
+ &-flex {
+ display: flex;
+ align-items: center;
+ }
+
+ &-input {
+ flex: 1;
+ padding: 0 5px;
+
+ input {
+ border: none;
+ outline: none;
+ width: 100%;
+ text-align: center;
+ }
+
+ &-wrapper {
+ width: 100%;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 2px;
+
+ &:focus-within {
+ border: 1px solid var(--devui-primary);
+ }
+ }
+
+ &.string-input &-wrapper {
+ border-radius: 5px;
+ }
+
+ &.number-input &-wrapper {
+ border-radius: 0;
+
+ &:first-child {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+
+ &:last-child {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+ }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx
new file mode 100644
index 0000000000..baa4b92bc5
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx
@@ -0,0 +1,287 @@
+import { defineComponent, computed, ref, inject } from 'vue'
+import { ColorPickerEditProps, colorPickerEditProps } from './color-picker-edit-types'
+import { provideColorOptions, ColorPickerColor } from '../../utils/color-utils-types'
+import './color-edit.scss'
+import { fromHex, fromHexa, fromHSLA, fromHSVA, fromRGBA } from '../../utils/color-utils'
+import Schema, { Rules } from 'async-validator'
+// 默认 mode
+const DEFAUTL_MODE = 'rgb'
+
+// MODE支持模式
+const MODE_SUPPORT = ['rgb', 'hex', 'hsl', 'hsv'] as const
+
+// 色值校验规则
+const colorRules: Rules = {
+ alpha: [
+ {
+ type: 'number',
+ required: true,
+ min: 0,
+ max: 1
+ }
+ ],
+ hex: [{ type: 'string', pattern: /^#[0-9a-fA-F]{6}/ }],
+ hexa: [{ type: 'string', pattern: /^#[0-9a-fA-F]{6,8}/ }],
+ rgba: {
+ type: 'object',
+ required: true,
+ fields: {
+ r: { type: 'number', required: true, min: 0, max: 255 },
+ g: { type: 'number', required: true, min: 0, max: 255 },
+ b: { type: 'number', required: true, min: 0, max: 255 },
+ a: { type: 'number', required: true, min: 0, max: 1 }
+ }
+ },
+ hsla: {
+ type: 'object',
+ required: true,
+ fields: {
+ h: { type: 'number', required: true, min: 0, max: 360 },
+ s: { type: 'number', required: true, min: 0, max: 1 },
+ l: { type: 'number', required: true, min: 0, max: 1 },
+ a: { type: 'number', required: true, min: 0, max: 1 }
+ }
+ },
+ hsva: {
+ type: 'object',
+ required: true,
+ fields: {
+ h: { type: 'number', required: true, min: 0, max: 360 },
+ s: { type: 'number', required: true, min: 0, max: 1 },
+ v: { type: 'number', required: true, min: 0, max: 1 },
+ a: { type: 'number', required: true, min: 0, max: 1 }
+ }
+ }
+}
+
+export default defineComponent({
+ name: 'ColorEdit',
+ props: colorPickerEditProps,
+ emits: ['changeTextModeColor', 'update:modelValue'],
+ setup(props: ColorPickerEditProps, { emit }) {
+ // 设置showalpha 为false 会报错 2021.12.14
+ const isShowAlpha: provideColorOptions = inject('provideData')
+ // 模式值
+ const modelValue = computed(
+ () => `${props.mode ?? DEFAUTL_MODE}${isShowAlpha.showAlpha ? 'a' : ''}`
+ )
+ // 颜色值
+ const colorValue = ref(props.color)
+ // 模式值类型
+ const modelValueType = computed(() =>
+ (props.mode ?? DEFAUTL_MODE) === 'hex' ? 'string' : 'number'
+ )
+
+ /**
+ * 获取有效颜色值
+ * @param color
+ * @returns
+ */
+ function getValidColor(color: ColorPickerColor) {
+ const validator = new Schema(colorRules)
+
+ // 使用ColorRules验证有效性
+ return new Promise((resolve, reject) => {
+ validator.validate(color, (errors) => {
+ errors && console.warn('色值校验异常:', errors)
+ errors ? reject() : resolve(color)
+ })
+ })
+ }
+
+ /**
+ * 修改Mode值
+ */
+ function onChangeModel() {
+ // 安装MODE_SUPPORT列表进行更换
+ const currentIndex = MODE_SUPPORT.findIndex((x) => x === props.mode ?? DEFAUTL_MODE)
+ const mode = MODE_SUPPORT[(currentIndex + 1) % MODE_SUPPORT.length]
+ emit('changeTextModeColor', mode)
+ }
+
+ /**
+ * 渲染字符类型组件
+ */
+ function renderStringValue() {
+ // 绑定KEy
+ const key = modelValue.value
+ const value = colorValue.value[key]
+
+ const getConvertColor = (v: string) => {
+ // 获取转换函数
+ const from = isShowAlpha.showAlpha ? fromHexa : fromHex
+
+ // 获取颜色值
+ const color = from(v)
+
+ // 获取色值有效性
+ return getValidColor(color)
+ }
+
+ /**
+ * 更新输入值
+ */
+ const updateValue = async (event: Event) => {
+ const target = event.target as HTMLInputElement
+
+ getConvertColor(target.value)
+ // 如果Color为有效值则进行更新
+ .then((color) => (colorValue.value = color))
+ // 如果Color为无效值则还原数值
+ .catch(() => (target.value = value))
+ }
+
+ return (
+
+ )
+ }
+
+ /**
+ * 渲染数值类型组件
+ * TODO: alpha需要进行白分化显示处理
+ * TODO: 监听键盘上下键进行值修改
+ */
+ function renderNumberValue() {
+ // 绑定缩放KEYS
+ const scaleKeys = ['s', 'v', 'l'] as const
+ const percentKeys = ['a'] as const
+
+ const key = modelValue.value
+ // 对指定数值进行缩放处理
+ const value = colorValue.value[key.replace(/a?$/, 'a')]
+
+ const getConvertColor = (model) => {
+ // 获取转换函数
+ const { from } = [
+ { mode: ['rgb', 'rgba'], from: fromRGBA },
+ { mode: ['hsv', 'hsva'], from: fromHSVA },
+ { mode: ['hsl', 'hsla'], from: fromHSLA }
+ ].find((x) => x.mode.includes(key))
+ // 获取颜色值
+ const color = from(isShowAlpha.showAlpha ? model : { ...model, a: 1 })
+
+ // 通过RGBA进行验证有效性
+ return getValidColor(color)
+ }
+
+ /**
+ * 更新输入值
+ */
+ const updateValue = (k: string) => async (event: Event) => {
+ const target = event.target as HTMLInputElement
+
+ // 获取有效颜色值
+ // 无效则进行还原
+ // 直接截位取整
+ getConvertColor({ ...value, [k]: parseValue(k, target.value) })
+ // 如果Color为有效值则进行更新
+ .then((color) => (colorValue.value = color))
+ // 如果Color为无效值则还原数值
+ .catch(() => (target.value = formatValue(k, value[k])))
+ }
+
+ /**
+ * 将存储值转换为显示值
+ * @param k
+ * @param v
+ * @returns
+ */
+ function formatValue(k, v: number): string {
+ switch (true) {
+ case scaleKeys.includes(k):
+ return (v * 100).toFixed()
+ case percentKeys.includes(k):
+ return `${Math.round(v * 100)}%`
+ default:
+ return v.toString()
+ }
+ }
+
+ /**
+ * 将显示值转换为存储值
+ * @param k
+ * @param v
+ * @returns
+ */
+ function parseValue(k, v: string): number {
+ switch (true) {
+ case scaleKeys.includes(k):
+ return parseFloat((parseInt(v) / 100).toFixed(2))
+ case percentKeys.includes(k):
+ return parseFloat((parseInt(v.replace(/%$/, '')) / 100).toFixed(2))
+ default:
+ return Number(v)
+ }
+ }
+
+ /**
+ * 方向键修改值处理
+ * @returns
+ */
+ function onKeyChangeValue() {
+ return (e: KeyboardEvent) => {
+ const target = e.target as HTMLInputElement
+
+ const changeValue = {
+ ArrowUp: 1,
+ ArrowDown: -1
+ }[e.code]
+
+ if (changeValue !== undefined) {
+ e.preventDefault()
+ const [v] = target.value.match(/\d+/g)
+ const newValue = (parseInt(v) + changeValue).toString()
+ target.value = target.value.replace(/\d+/g, newValue)
+ // 发送数值修改事件
+ target.dispatchEvent(new CustomEvent('change'))
+ }
+ }
+ }
+
+ return (
+
+ )
+ }
+
+ /**
+ * 渲染输入组件
+ * @returns
+ */
+ function renderValueInput() {
+ switch (modelValueType.value) {
+ case 'string':
+ return renderStringValue()
+ case 'number':
+ return renderNumberValue()
+ }
+ }
+
+ return () => (
+
+
+ {modelValue.value.toUpperCase()}
+
+ {renderValueInput()}
+
+ )
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-edit/color-picker-edit-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-picker-edit-types.ts
new file mode 100644
index 0000000000..e7310ebe00
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-picker-edit-types.ts
@@ -0,0 +1,22 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import { ColorPickerColor } from '../../utils/color-utils-types'
+export const colorPickerEditProps = {
+ /* test: {
+ type: Object as PropType<{ xxx: xxx }>
+ } */
+ /**
+ * 选择器圆点大小
+ */
+ showAlpha: {
+ type: Boolean,
+ default: false
+ },
+ mode: {
+ type: String
+ },
+ color: {
+ type: Object as PropType
+ }
+} as const
+
+export type ColorPickerEditProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.scss b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.scss
new file mode 100644
index 0000000000..94b4043e1b
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.scss
@@ -0,0 +1,23 @@
+.devui-color-picker-history {
+ margin-top: 10px;
+
+ &_color-box {
+ cursor: pointer;
+ margin: 5px;
+ height: 20px;
+ width: 20px;
+ border: solid 1px #e7e7e7;
+
+ &.transparent {
+ background:
+ linear-gradient(
+ to bottom right,
+ rgba(0, 0, 0, 0) 0%,
+ rgba(0, 0, 0, 0) calc(50% - 1.5px),
+ #f18887 50%,
+ rgba(0, 0, 0, 0) calc(50% + 1.5px),
+ rgba(0, 0, 0, 0) 100%
+ );
+ }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx
new file mode 100644
index 0000000000..266b13e8ee
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx
@@ -0,0 +1,100 @@
+import { defineComponent, Ref, ref, UnwrapRef, watch, inject } from 'vue'
+import { ColorPickerHistoryProps, colorPickerHistoryProps } from './color-picker-history-types'
+import { Icon } from '../../../../icon'
+import './color-history.scss'
+import { fromHexa } from '../../utils/color-utils'
+import { provideColorOptions, ColorPickerColor } from '../../utils/color-utils-types'
+import { debounce } from 'lodash'
+
+const STORAGE_KEY = 'STORAGE_COLOR_PICKER_HISTORY_KEY'
+const MAX_HISOTRY_COUNT = 8
+
+/**
+ * 创建支持存储Store
+ * @param v
+ * @param params
+ * @returns
+ */
+function useStore(v: T, { storage }: { storage?: boolean; } = {}): Ref> {
+ // 获取默认值
+ const getDefaultValue = (): T => {
+ if (storage) {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY)) || v
+ } else {
+ return v
+ }
+ }
+
+ // 创建Store
+ const store = ref(getDefaultValue())
+
+ // 监听Store修改
+ watch(store, (value) => {
+ if (storage) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(value))
+ }
+ })
+
+ return store
+}
+
+export default defineComponent({
+ name: 'ColorEdit',
+ components: {
+ Icon
+ },
+ props: colorPickerHistoryProps,
+ emits: ['update:color'],
+ setup(props: ColorPickerHistoryProps) {
+ // 获取 是否showalpha
+ const alphaInject: provideColorOptions = inject('provideData')
+
+ // 创建历史存储
+ const history = useStore([], { storage: true })
+ const color = ref(props.color)
+
+ // 更新历史值函数
+ // 进行缓冲处理
+ const updateHistory = debounce((value: ColorPickerColor) => {
+ const index = history.value.findIndex(
+ (x) => x === value.hexa || (x.endsWith('00') && value.alpha === 0)
+ )
+ if (index >= 0) {
+ history.value.splice(index, 1)
+ }
+
+ history.value = [alphaInject.showAlpha ? value.hexa : value.hex, ...history.value].slice(
+ 0,
+ MAX_HISOTRY_COUNT
+ )
+ }, 100)
+
+ // 更新历史值
+ watch(props.color, (value) => {
+ updateHistory(value)
+ })
+
+ /**
+ * 选择历史色
+ * @param value
+ */
+ function onChangeColor(value: string) {
+ color.value = fromHexa(value)
+ }
+
+ return () => (
+
+ {history.value.map((hexa: string) => (
+
onChangeColor(hexa)}
+ class={[
+ 'devui-color-picker-history_color-box',
+ hexa.endsWith('00') ? 'transparent' : ''
+ ]}
+ style={{ backgroundColor: hexa }}
+ >
+ ))}
+
+ )
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-history/color-picker-history-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-history/color-picker-history-types.ts
new file mode 100644
index 0000000000..454b44d98d
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-history/color-picker-history-types.ts
@@ -0,0 +1,10 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import type { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerHistoryProps = {
+ color: {
+ type: Object as PropType
+ }
+} as const
+
+export type ColorPickerHistoryProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.scss b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.scss
new file mode 100644
index 0000000000..42c760fe10
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.scss
@@ -0,0 +1,75 @@
+.devui-color-picker-hue-slider {
+ position: relative;
+ margin: 13px 0;
+ width: 100%;
+ height: 14px;
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
+ border-radius: 15px;
+
+ &.transparent {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+ background-repeat: repeat;
+ }
+
+ &__bar {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ border-radius: 15px;
+ background:
+ -webkit-linear-gradient(
+ left,
+ rgb(255, 0, 0) 0%,
+ rgb(255, 255, 0) 16.66%,
+ rgb(0, 255, 0) 33.33%,
+ rgb(0, 255, 255) 50%,
+ rgb(0, 0, 255) 66.66%,
+ rgb(255, 0, 255) 83.33%,
+ rgb(255, 0, 0) 100%
+ );
+ background:
+ -moz-linear-gradient(
+ left,
+ rgb(255, 0, 0) 0%,
+ rgb(255, 255, 0) 16.66%,
+ rgb(0, 255, 0) 33.33%,
+ rgb(0, 255, 255) 50%,
+ rgb(0, 0, 255) 66.66%,
+ rgb(255, 0, 255) 83.33%,
+ rgb(255, 0, 0) 100%
+ );
+ background:
+ -ms-linear-gradient(
+ left,
+ rgb(255, 0, 0) 0%,
+ rgb(255, 255, 0) 16.66%,
+ rgb(0, 255, 0) 33.33%,
+ rgb(0, 255, 255) 50%,
+ rgb(0, 0, 255) 66.66%,
+ rgb(255, 0, 255) 83.33%,
+ rgb(255, 0, 0) 100%
+ );
+
+ &-pointer {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ }
+
+ &-handle {
+ width: 14px;
+ height: 14px;
+ border-radius: 6px;
+ transform: translate(-7px, -2px);
+ background-color: #f8f8f8;
+ margin-top: 2px;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
+ cursor: pointer;
+
+ &.vertical {
+ transform: translate(0, -7px);
+ margin-top: 0;
+ }
+ }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx
new file mode 100644
index 0000000000..7f1e5dcd3b
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx
@@ -0,0 +1,95 @@
+import { computed, defineComponent, ref, onMounted, watch } from 'vue'
+import { colorPickerHueSliderProps } from './color-picker-hue-slider-types'
+import { DOMUtils } from '../../utils/domDragger'
+import { fromHSVA } from '../../utils/color-utils'
+import './color-hue-slider.scss'
+export default defineComponent({
+ name: 'ColorHueSlider',
+ props: colorPickerHueSliderProps,
+ emits: ['update:modelValue'],
+ setup(props, ctx) {
+ const DEFAULT_TRANSITION = { transition: 'all 0.3s ease' }
+ const barElement = ref(null)
+ const cursorElement = ref(null)
+ const clickTransfrom = ref(DEFAULT_TRANSITION)
+ const getCursorLeft = () => {
+ if (barElement.value && cursorElement.value) {
+ const rect = barElement.value.getBoundingClientRect()
+ const offsetWidth = cursorElement.value.offsetWidth
+ if (props.modelValue.hue === 360) {
+ return rect.width - offsetWidth / 2
+ }
+ return ((props.modelValue.hue % 360) * (rect.width - offsetWidth)) / 360 + offsetWidth / 2
+ }
+ return 0
+ }
+
+ const getCursorStyle = computed(() => {
+ const left = getCursorLeft()
+ return {
+ left: left + 'px',
+ top: 0,
+ ...clickTransfrom.value
+ }
+ })
+ const onClickSider = (event: Event) => {
+ const target = event.target
+ if (target !== barElement.value) {
+ onMoveBar(event as MouseEvent)
+ }
+ }
+
+ const onMoveBar = (event: MouseEvent) => {
+ event.stopPropagation()
+ if (barElement.value && cursorElement.value) {
+ const rect = barElement.value.getBoundingClientRect()
+ const offsetWidth = cursorElement.value.offsetWidth
+ let left = event.clientX - rect.left
+ left = Math.min(left, rect.width - offsetWidth / 2)
+ left = Math.max(offsetWidth / 2, left)
+ const hue = Math.round(((left - offsetWidth / 2) / (rect.width - offsetWidth)) * 360)
+ ctx.emit(
+ 'update:modelValue',
+ fromHSVA({
+ h: hue,
+ s: props.modelValue.hsva.s,
+ v: props.modelValue.hsva.v,
+ a: props.modelValue.hsva.a
+ })
+ )
+ }
+ }
+
+ onMounted(() => {
+ const dragConfig = {
+ drag: (event: Event) => {
+ clickTransfrom.value = null
+ onMoveBar(event as MouseEvent)
+ },
+ end: (event: Event) => {
+ clickTransfrom.value = DEFAULT_TRANSITION
+ onMoveBar(event as MouseEvent)
+ }
+ }
+
+ if (barElement.value && cursorElement.value) {
+ DOMUtils.triggerDragEvent(barElement.value, dragConfig)
+ }
+ })
+ return () => {
+ return (
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-picker-hue-slider-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-picker-hue-slider-types.ts
new file mode 100644
index 0000000000..c4bcd4314a
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-picker-hue-slider-types.ts
@@ -0,0 +1,27 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import type { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerHueSliderProps = {
+ /* test: {
+ type: Object as PropType<{ xxx: xxx }>
+ } */
+ /**
+ * 选择器圆点大小
+ */
+ color: {
+ type: Object
+ },
+ modelValue: {
+ type: Object as PropType
+ },
+ width: {
+ type: Number,
+ default: 300
+ },
+ height: {
+ type: Number,
+ default: 15
+ }
+} as const
+
+export type colorPickerHueSliderProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.scss b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.scss
new file mode 100644
index 0000000000..8204217424
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.scss
@@ -0,0 +1,34 @@
+.devui-color-picker-palette {
+ position: relative;
+ width: 100%;
+
+ &__white,
+ &__black {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ &__black {
+ background: linear-gradient(0deg, #000000, transparent);
+ }
+
+ &__white {
+ background: linear-gradient(90deg, #ffffff, hsla(0, 0%, 100%, 0));
+ }
+
+ &-handler {
+ position: absolute;
+ // z-index: 999;
+ div {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ border: 2px solid #ffffff;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
+ cursor: pointer;
+ }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx
new file mode 100644
index 0000000000..882d8f8746
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx
@@ -0,0 +1,131 @@
+import { defineComponent, ref, onMounted, computed, getCurrentInstance, watch, inject } from 'vue'
+import { DOMUtils } from '../../utils/domDragger'
+import { fromHSVA } from '../../utils/color-utils'
+import { clamp } from '../../utils/helpers'
+import { colorPickerPaletteProps } from './color-picker-palette-types'
+import { provideColorOptions } from '../../utils/color-utils-types'
+import './color-palette.scss'
+export default defineComponent({
+ name: 'ColorPallete',
+ props: colorPickerPaletteProps,
+ emits: ['update:modelValue', 'changeTextColor'],
+ setup(props: colorPickerPaletteProps, ctx) {
+ const DEFAULT_TRANSITION = { transition: 'all 0.3s ease' }
+ const dotSizeInject: provideColorOptions = inject('provideData')
+
+ const clickTransfrom = ref(DEFAULT_TRANSITION)
+ const paletteElement = ref()
+ const canvasElement = ref()
+ const handlerElement = ref()
+ const paletteInstance = getCurrentInstance()
+
+ const cursorTop = ref(0)
+ const cursorLeft = ref(0)
+ const getDotStyle = computed(() => {
+ return {
+ width: `${dotSizeInject.dotSize}px`,
+ height: `${dotSizeInject.dotSize}px`,
+ transform: `translate(-${dotSizeInject.dotSize / 2}px, -${dotSizeInject.dotSize / 2}px)`
+ }
+ })
+ const getCursorStyle = computed(() => {
+ return {
+ top: cursorTop.value + 'px',
+ left: cursorLeft.value + 'px',
+ ...clickTransfrom.value
+ }
+ })
+ function renderCanvas() {
+ const canvas = canvasElement.value.getContext('2d')
+ const parentWidth = paletteElement.value.offsetWidth
+ canvasElement.value.width = parentWidth
+ canvasElement.value.height = props.height
+ const saturationGradient = canvas.createLinearGradient(0, 0, parentWidth, 0)
+ saturationGradient.addColorStop(0, 'hsla(0, 0%, 100%, 1)') // white
+ saturationGradient.addColorStop(1, `hsla(${props.modelValue.hue}, 100%, 50%, 1)`)
+ canvas.fillStyle = saturationGradient
+ canvas.fillRect(0, 0, parentWidth, props.height)
+ const valueGradient = canvas.createLinearGradient(0, 0, 0, props.height)
+ valueGradient.addColorStop(0, 'hsla(0, 0%, 100%, 0)') // transparent
+ valueGradient.addColorStop(1, 'hsla(0, 0%, 0%, 1)') // black
+ canvas.fillStyle = valueGradient
+ canvas.fillRect(0, 0, parentWidth, props.height)
+ }
+ function clickPalette(event: Event) {
+ const target = event.target
+ if (target !== paletteElement.value) {
+ handleDrag(event as MouseEvent)
+ }
+ }
+ function updatePosition() {
+ if (paletteInstance) {
+ const parentWidth = paletteElement.value.offsetWidth
+ cursorLeft.value = Number(props.modelValue?.hsva.s) * parentWidth
+ cursorTop.value = (1 - Number(props.modelValue?.hsva.v)) * props.height
+ }
+ }
+ function handleDrag(event: any) {
+ const parentWidth = paletteElement.value.offsetWidth
+ if (paletteInstance) {
+ const el = canvasElement.value
+ const rect = el?.getBoundingClientRect()
+ let left = event.clientX - rect.left
+ let top = event.clientY - rect.top
+ left = clamp(left, 0, parentWidth)
+ top = clamp(top, 0, props.height)
+ cursorLeft.value = left
+ cursorTop.value = top
+ const isChangeTextColor = computed(() => {
+ if (left > rect.width / 2 || top > rect.height / 2) {
+ return true
+ } else {
+ return false
+ }
+ })
+ ctx.emit(
+ 'update:modelValue',
+ fromHSVA({
+ h: props.modelValue.hue,
+ s: clamp(event.clientX - rect.left, 0, rect.width) / rect.width,
+ v: 1 - clamp(event.clientY - rect.top, 0, rect.height) / rect.height,
+ a: props.modelValue.alpha
+ })
+ )
+ ctx.emit('changeTextColor', isChangeTextColor.value)
+ }
+ }
+ onMounted(() => {
+ renderCanvas()
+ if (paletteInstance && paletteInstance.vnode.el && handlerElement.value) {
+ DOMUtils.triggerDragEvent(paletteInstance.vnode.el as HTMLElement, {
+ drag: (event: Event) => {
+ clickTransfrom.value = null
+ handleDrag(event as MouseEvent)
+ },
+ end: (event) => {
+ clickTransfrom.value = DEFAULT_TRANSITION
+ handleDrag(event as MouseEvent)
+ }
+ })
+ updatePosition()
+ }
+ })
+ watch(
+ () => props.modelValue,
+ () => {
+ updatePosition()
+ }
+ )
+ ctx.expose({ renderCanvas })
+ return () => {
+ return (
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-palette/color-picker-palette-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-picker-palette-types.ts
new file mode 100644
index 0000000000..577f8a4328
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-picker-palette-types.ts
@@ -0,0 +1,16 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import { fromRGBA } from '../../utils/color-utils'
+import type { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerPaletteProps = {
+ modelValue: {
+ type: Object as PropType,
+ default: () => fromRGBA({ r: 255, g: 0, b: 0, a: 1 })
+ },
+ height: {
+ type: Number,
+ default: 200
+ }
+} as const
+
+export type colorPickerPaletteProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel-types.ts b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel-types.ts
new file mode 100644
index 0000000000..f6194b8264
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel-types.ts
@@ -0,0 +1,20 @@
+import type { PropType, ExtractPropTypes } from 'vue'
+import { ColorPickerColor } from '../../utils/color-utils-types'
+
+export const colorPickerProps = {
+ colorMap: {
+ type: Object
+ },
+ modelValue: {
+ type: Object as PropType
+ },
+ showAlpha: {
+ type: Boolean,
+ default: true
+ },
+ mode: {
+ type: String
+ }
+} as const
+
+export type ColorPickerProps = ExtractPropTypes
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.scss b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.scss
new file mode 100644
index 0000000000..35a3c23238
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.scss
@@ -0,0 +1,9 @@
+.devui-color-picker-panel {
+ width: 270px;
+ padding: 12px;
+ border-radius: 4px;
+ box-shadow:
+ 0 3px 5px -1px rgba(0, 0, 0, 0.1),
+ 0 5px 8px 0 rgba(0, 0, 0, 0.1),
+ 0 1px 14px 0 rgba(0, 0, 0, 0.1);
+}
diff --git a/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx
new file mode 100644
index 0000000000..a47afcc54e
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx
@@ -0,0 +1,86 @@
+import { defineComponent, ref, watch, nextTick, inject } from 'vue'
+import { colorPickerProps, ColorPickerProps } from './color-picker-panel-types'
+import { provideColorOptions } from '../../utils/color-utils-types'
+import { Tabs } from '../../../../tabs'
+import colorPalette from '../color-palette/color-palette'
+import colorHueSlider from '../color-hue-slider/color-hue-slider'
+import colorAlphaSlider from '../color-alpha-slider/color-alpha-slider'
+import colorEdit from '../color-edit/color-edit'
+import colorBasic from '../color-basic/color-basic'
+import './color-picker-panel.scss'
+import colorHistory from '../color-history/color-history'
+export default defineComponent({
+ name: 'ColorPanel',
+ components: {
+ colorPalette,
+ colorHueSlider,
+ colorAlphaSlider,
+ colorEdit,
+ colorBasic,
+ Tabs,
+ colorHistory
+ },
+ props: colorPickerProps,
+ emits: [
+ 'update:modelValue',
+ 'changeTextColor',
+ 'changeTiggerColor',
+ 'changePaletteColor',
+ 'changeTextModeType'
+ ],
+ setup(props: ColorPickerProps, { emit }) {
+ const injectData: provideColorOptions = inject('provideData')
+ const paletteElement = ref(null)
+ const showAlpha = injectData.showAlpha
+ const tab = ref('basic')
+ function changeTextColor(isChange: boolean) {
+ emit('changeTextColor', isChange)
+ }
+ function changeTextModeColor(currentType: string) {
+ emit('changeTextModeType', currentType)
+ }
+
+ // 画板值
+ const paletteColorMap = ref(props.modelValue)
+ // hue slider 值
+ watch(
+ () => paletteColorMap.value,
+ (newValue) => {
+ emit('update:modelValue', newValue)
+ emit('changePaletteColor', newValue)
+ nextTick(() => {
+ paletteElement.value && paletteElement.value.renderCanvas()
+ })
+ }
+ )
+ return () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {showAlpha ? (
+
+ ) : null}
+
+ {injectData.showHistory ? : null}
+
+ )
+ }
+ }
+})
diff --git a/packages/devui-vue/devui/color-picker/src/utils/color-utils-types.ts b/packages/devui-vue/devui/color-picker/src/utils/color-utils-types.ts
new file mode 100644
index 0000000000..6260930b5a
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/color-utils-types.ts
@@ -0,0 +1,42 @@
+import { Ref } from 'vue'
+
+export type ColorModeType = 'hsl' | 'rgb' | 'hsv' | 'hsv' | 'hex'
+export interface provideColorOptions {
+ mode?: ColorModeType
+ showAlpha?: boolean
+ tab?: string
+ dotSize?: number
+ swatches?: string[]
+ showHistory?: boolean
+}
+export interface CssColorObject {
+ color?: Hex
+}
+export interface ColorPickerColor {
+ alpha: number
+ hex: Hex
+ hexa: Hexa
+ hsla: HSLA
+ hsva: HSVA
+ hue: number
+ rgba: RGBA
+ hsv?: any
+ hsl?: any
+}
+export interface position {
+ left?: Ref
+ top?: Ref
+ right?: Ref
+ bottom?: Ref
+}
+// Types
+export type ColorInt = number
+export type HSV = { h: number; s: number; v: number; }
+export type HSVA = HSV & { a: number; }
+export type RGB = { r: number; g: number; b: number; }
+export type RGBA = RGB & { a: number; }
+export type HSL = { h: number; s: number; l: number; }
+export type HSLA = HSL & { a: number; }
+export type Hex = string
+export type Hexa = string
+export type Color = string | number | {}
diff --git a/packages/devui-vue/devui/color-picker/src/utils/color-utils.ts b/packages/devui-vue/devui/color-picker/src/utils/color-utils.ts
new file mode 100644
index 0000000000..eec542f0e1
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/color-utils.ts
@@ -0,0 +1,481 @@
+// Utilities
+import { ref } from 'vue'
+import { chunk, padEnd, has, keepDecimal } from './helpers'
+import {
+ ColorPickerColor,
+ position,
+ ColorInt,
+ HSV,
+ HSVA,
+ RGB,
+ RGBA,
+ HSL,
+ HSLA,
+ Hex,
+ Hexa,
+ Color
+} from './color-utils-types'
+export function isCssColor(color?: string | false): boolean {
+ return !!color && !!color.match(/^(#|var\(--|(rgb|hsl)a?\()/)
+}
+
+export function colorToInt(color: Color): ColorInt {
+ let rgb
+
+ if (typeof color === 'number') {
+ rgb = color
+ } else if (typeof color === 'string') {
+ let c = color[0] === '#' ? color.substring(1) : color
+ if (c.length === 3) {
+ c = c
+ .split('')
+ .map((char) => char + char)
+ .join('')
+ }
+ if (c.length !== 6) {
+ // consoleWarn(`'${color}' is not a valid rgb color`)
+ }
+ rgb = parseInt(c, 16)
+ } else {
+ throw new TypeError(
+ `Colors can only be numbers or strings, recieved ${
+ color == null ? color : color.constructor.name
+ } instead`
+ )
+ }
+
+ if (rgb < 0) {
+ // consoleWarn(`Colors cannot be negative: '${color}'`)
+ rgb = 0
+ } else if (rgb > 0xffffff || isNaN(rgb)) {
+ // consoleWarn(`'${color}' is not a valid rgb color`)
+ rgb = 0xffffff
+ }
+
+ return rgb
+}
+export function intToHex(color: ColorInt): string {
+ let hexColor: string = color.toString(16)
+
+ if (hexColor.length < 6) hexColor = '0'.repeat(6 - hexColor.length) + hexColor
+
+ return '#' + hexColor
+}
+
+export function colorToHex(color: Color): string {
+ return intToHex(colorToInt(color))
+}
+
+/**
+ * Converts HSVA to RGBA. Based on formula from https://en.wikipedia.org/wiki/HSL_and_HSV
+ *
+ * @param color HSVA color as an array [0-360, 0-1, 0-1, 0-1]
+ */
+export function HSVAtoRGBA(hsva: HSVA): RGBA {
+ const { h, s, v, a } = hsva
+ const f = (n: number) => {
+ const k = (n + h / 60) % 6
+ return v - v * s * Math.max(Math.min(k, 4 - k, 1), 0)
+ }
+
+ const rgb = [f(5), f(3), f(1)].map((v) => Math.round(v * 255))
+
+ return { r: rgb[0], g: rgb[1], b: rgb[2], a }
+}
+
+/**
+ * Converts RGBA to HSVA. Based on formula from https://en.wikipedia.org/wiki/HSL_and_HSV
+ *
+ * @param color RGBA color as an array [0-255, 0-255, 0-255, 0-1]
+ */
+export function RGBAtoHSVA(rgba: RGBA): HSVA {
+ if (!rgba) return { h: 0, s: 1, v: 1, a: 1 }
+
+ const r = rgba.r / 255
+ const g = rgba.g / 255
+ const b = rgba.b / 255
+ const max = Math.max(r, g, b)
+ const min = Math.min(r, g, b)
+
+ let h = 0
+
+ if (max !== min) {
+ if (max === r) {
+ h = 60 * (0 + (g - b) / (max - min))
+ } else if (max === g) {
+ h = 60 * (2 + (b - r) / (max - min))
+ } else if (max === b) {
+ h = 60 * (4 + (r - g) / (max - min))
+ }
+ }
+
+ if (h < 0) h = h + 360
+
+ const s = max === 0 ? 0 : (max - min) / max
+ const hsv = [h, s, max]
+
+ return { h: Math.round(hsv[0]), s: hsv[1], v: hsv[2], a: rgba.a }
+}
+
+export function HSVAtoHSLA(hsva: HSVA): HSLA {
+ const { h, s, v, a } = hsva
+
+ const l = v - (v * s) / 2
+
+ const sprime = l === 1 || l === 0 ? 0 : (v - l) / Math.min(l, 1 - l)
+
+ return { h: Math.round(h), s: sprime, l, a }
+}
+
+export function HSLAtoHSVA(hsl: HSLA): HSVA {
+ const { h, s, l, a } = hsl
+
+ const v = l + s * Math.min(l, 1 - l)
+
+ const sprime = v === 0 ? 0 : 2 - (2 * l) / v
+
+ return { h: Math.round(h), s: sprime, v, a }
+}
+
+export function RGBAtoCSS(rgba: RGBA): string {
+ return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
+}
+
+export function RGBtoCSS(rgba: RGBA): string {
+ return RGBAtoCSS({ ...rgba, a: 1 })
+}
+
+export function RGBAtoHex(rgba: RGBA): Hex {
+ const toHex = (v: number) => {
+ const h = Math.round(v).toString(16)
+ return ('00'.substring(0, 2 - h.length) + h).toUpperCase()
+ }
+
+ return `#${[toHex(rgba.r), toHex(rgba.g), toHex(rgba.b), toHex(Math.round(rgba.a * 255))].join(
+ ''
+ )}`
+}
+
+export function HexToRGBA(hex: Hex): RGBA {
+ const rgba = chunk(hex.slice(1), 2).map((c: string) => parseInt(c, 16))
+
+ return {
+ r: rgba[0],
+ g: rgba[1],
+ b: rgba[2],
+ a: Math.round((rgba[3] / 255) * 100) / 100
+ }
+}
+
+export function HexToHSVA(hex: Hex): HSVA {
+ const rgb = HexToRGBA(hex)
+ return RGBAtoHSVA(rgb)
+}
+
+export function HSVAtoHex(hsva: HSVA): Hex {
+ return RGBAtoHex(HSVAtoRGBA(hsva))
+}
+
+export function parseHex(hex: string): Hex {
+ if (hex.startsWith('#')) {
+ hex = hex.slice(1)
+ }
+
+ hex = hex.replace(/([^0-9a-f])/gi, 'F')
+
+ if (hex.length === 3 || hex.length === 4) {
+ hex = hex
+ .split('')
+ .map((x) => x + x)
+ .join('')
+ }
+
+ if (hex.length === 6) {
+ hex = padEnd(hex, 8, 'F')
+ } else {
+ hex = padEnd(padEnd(hex, 6), 8, 'F')
+ }
+
+ return `#${hex}`.toUpperCase().substring(0, 9)
+}
+
+export function RGBtoInt(rgba: RGBA): ColorInt {
+ return (rgba.r << 16) + (rgba.g << 8) + rgba.b
+}
+
+export function fromHSVA(hsva: HSVA): ColorPickerColor {
+ hsva = { ...hsva }
+ const hexa = HSVAtoHex(hsva)
+ const hsla = HSVAtoHSLA(hsva)
+ const rgba = HSVAtoRGBA(hsva)
+ return {
+ alpha: hsva.a,
+ hex: hexa.substring(0, 7),
+ hexa,
+ hsla,
+ hsva,
+ hue: hsva.h,
+ rgba
+ }
+}
+export function fromRGBA(rgba: RGBA): ColorPickerColor {
+ const hsva = RGBAtoHSVA(rgba)
+ const hexa = RGBAtoHex(rgba)
+ const hsla = HSVAtoHSLA(hsva)
+ const hsv = { h: hsva.h, s: hsva.s, v: hsva.v }
+ const hsl = { h: hsla.h, s: hsla.s, l: hsla.l }
+ return {
+ alpha: hsva.a,
+ hex: hexa.substring(0, 7),
+ hexa,
+ hsla,
+ hsva,
+ hsv,
+ hsl,
+ hue: hsva.h,
+ rgba
+ }
+}
+export function fromHexa(hexa: Hexa): ColorPickerColor {
+ const hsva = HexToHSVA(hexa)
+ const hsla = HSVAtoHSLA(hsva)
+ const rgba = HSVAtoRGBA(hsva)
+ return {
+ alpha: hsva.a,
+ hex: hexa.substring(0, 7),
+ hexa,
+ hsla,
+ hsva,
+ hue: hsva.h,
+ rgba
+ }
+}
+export function fromHSLA(hsla: HSLA): ColorPickerColor {
+ const hsva = HSLAtoHSVA(hsla)
+ const hexa = HSVAtoHex(hsva)
+ const rgba = HSVAtoRGBA(hsva)
+ return {
+ alpha: hsva.a,
+ hex: hexa.substring(0, 7),
+ hexa,
+ hsla,
+ hsva,
+ hue: hsva.h,
+ rgba
+ }
+}
+export function fromHex(hex: Hex): ColorPickerColor {
+ return fromHexa(parseHex(hex))
+}
+
+export function parseColor(color: Color, oldColor?: ColorPickerColor | null): ColorPickerColor {
+ if (!color) return fromRGBA({ r: 0, g: 0, b: 0, a: 1 })
+
+ if (typeof color === 'string') {
+ if (color.indexOf('#') !== -1) {
+ // const hex = color.replace('#', '').trim()
+ // return fromHexa(hex)
+ } else if (color.indexOf('hsl') !== -1) {
+ let alpha = null
+ const parts = color
+ .replace(/hsla|hsl|\(|\)/gm, '')
+ .split(/\s|,/g)
+ .filter((val) => val !== '')
+ .map((val) => parseFloat(val))
+ if (parts.length === 4) {
+ alpha = parts[3]
+ } else if (parts.length === 3) {
+ alpha = 1
+ }
+ return fromHSLA({ h: parts[0], s: parts[1], l: parts[2], a: alpha })
+ } else if (color.indexOf('rgb') !== -1) {
+ let alpha = null
+ const parts = color
+ .replace(/rgba|rgb|\(|\)/gm, '')
+ .split(/\s|,/g)
+ .filter((val) => val !== '')
+ .map((val) => parseFloat(val))
+
+ if (parts.length === 4) {
+ alpha = parts[3]
+ } else if (parts.length === 3) {
+ alpha = 1
+ }
+ return fromRGBA({ r: parts[0], g: parts[1], b: parts[2], a: alpha })
+ } else if (color.indexOf('hsv') !== -1) {
+ let alpha = null
+ const parts = color
+ .replace(/hsva|hsv|\(|\)/gm, '')
+ .split(/\s|,/g)
+ .filter((val) => val !== '')
+ .map((val) => parseFloat(val))
+
+ if (parts.length === 4) {
+ alpha = parts[3]
+ } else if (parts.length === 3) {
+ alpha = 1
+ }
+ return fromHSVA({ h: parts[0], s: parts[1], v: parts[2], a: alpha })
+ }
+ if (color === 'transparent') return fromHexa('#00000000')
+ const hex = parseHex(color)
+ if (oldColor && hex === oldColor.hexa) {
+ return oldColor
+ } else {
+ return fromHexa(hex)
+ }
+ }
+
+ if (typeof color === 'object') {
+ if (color.hasOwnProperty('alpha')) return color
+
+ const a = color.hasOwnProperty('a') ? parseFloat(color.a) : 1
+
+ if (has(color, ['r', 'g', 'b'])) {
+ if (oldColor && color === oldColor.rgba) return oldColor
+ else return fromRGBA({ ...color, a })
+ } else if (has(color, ['h', 's', 'l'])) {
+ if (oldColor && color === oldColor.hsla) return oldColor
+ else return fromHSLA({ ...color, a })
+ } else if (has(color, ['h', 's', 'v'])) {
+ if (oldColor && color === oldColor.hsva) return oldColor
+ else return fromHSVA({ ...color, a })
+ }
+ }
+
+ return fromRGBA({ r: 255, g: 0, b: 0, a: 1 })
+}
+
+function stripAlpha(color: Color, stripAlpha: boolean) {
+ if (stripAlpha) {
+ const { a, ...rest } = color
+
+ return rest
+ }
+
+ return color
+}
+
+export function extractColor(color: ColorPickerColor, input: Color, mode, showAlpha: boolean): any {
+ // 色相
+ const hue = keepDecimal(color.hsla.h, 2)
+ // 饱和度
+ const hslSaturation = keepDecimal(color.hsla.s, 2)
+ // 亮度
+ const lightness = keepDecimal(color.hsla.l, 2)
+ // red
+ const red = keepDecimal(color.rgba.r)
+ // green
+ const green = keepDecimal(color.rgba.g)
+ // blue
+ const blue = keepDecimal(color.rgba.b)
+ // HSV饱和度
+ const hsvSaturation = keepDecimal(color.hsva.s, 2)
+ // value
+ const value = keepDecimal(color.hsva.v, 2)
+ if (input == null) return color
+ function isShowAlpha(mode) {
+ return showAlpha ? mode + 'a' : mode
+ }
+ if (typeof input === 'string') {
+ if (mode === 'hex') {
+ return showAlpha ? color.hexa : color.hex
+ } else if (mode === 'hsl') {
+ return `${isShowAlpha(mode)}(${hue}, ${hslSaturation}, ${lightness}${
+ showAlpha ? ', ' + color.alpha : ''
+ })`
+ } else if (mode === 'rgb') {
+ return `${isShowAlpha(mode)}(${red}, ${green}, ${blue}${showAlpha ? ', ' + color.alpha : ''})`
+ } else if (mode === 'hsv') {
+ return `${isShowAlpha(mode)}(${hue}, ${hsvSaturation}, ${value}${
+ showAlpha ? ', ' + color.alpha : ''
+ })`
+ }
+ return input.length === 7 ? color.hex : color.hexa
+ }
+
+ if (typeof input === 'object') {
+ const shouldStrip = typeof input.a === 'number' && input.a === 0 ? !!input.a : !input.a
+ if (has(input, ['r', 'g', 'b'])) return stripAlpha(color.rgba, shouldStrip)
+ else if (has(input, ['h', 's', 'l'])) return stripAlpha(color.hsla, shouldStrip)
+ else if (has(input, ['h', 's', 'v'])) return stripAlpha(color.hsva, shouldStrip)
+ }
+}
+
+export function hasAlpha(color: Color): boolean {
+ if (!color) return false
+
+ if (typeof color === 'string') {
+ return color.length > 7
+ }
+
+ if (typeof color === 'object') {
+ return has(color, ['a']) || has(color, ['alpha'])
+ }
+
+ return false
+}
+export const elementResize = (parentElement: HTMLElement): position => {
+ const left = ref(0)
+ const top = ref(0)
+ window.addEventListener('resize', () => {
+ left.value = parentElement?.getBoundingClientRect().left
+ top.value =
+ parentElement?.getBoundingClientRect().top + parentElement?.getBoundingClientRect().height
+ })
+ return {
+ left,
+ top
+ }
+}
+
+export function RGBtoRGBA(rgba: RGBA): RGBA {
+ if (typeof rgba === 'string') {
+ rgba = (/rgba?\((.*?)\)/.exec(rgba) || ['', '0,0,0,1'])[1].split(',')
+ return {
+ r: Number(rgba[0]) || 0,
+ g: Number(rgba[1]) || 0,
+ b: Number(rgba[2]) || 0,
+ a: Number(rgba[3] ? rgba[3] : 1) // Avoid the case of 0
+ }
+ } else {
+ return rgba
+ }
+}
+export function RGBtoHSV(rgb: RGB): HSV {
+ if (!rgb) return { h: 0, s: 1, v: 1 }
+
+ const r = rgb.r / 255
+ const g = rgb.g / 255
+ const b = rgb.b / 255
+ const max = Math.max(r, g, b)
+ const min = Math.min(r, g, b)
+
+ let h = 0
+
+ if (max !== min) {
+ if (max === r) {
+ h = 60 * (0 + (g - b) / (max - min))
+ } else if (max === g) {
+ h = 60 * (2 + (b - r) / (max - min))
+ } else if (max === b) {
+ h = 60 * (4 + (r - g) / (max - min))
+ }
+ }
+
+ if (h < 0) h = h + 360
+
+ const s = max === 0 ? 0 : (max - min) / max
+ const hsv = [h, s, max]
+
+ return { h: hsv[0], s: hsv[1].toFixed(2), v: hsv[2].toFixed(2) }
+}
+export function HSVtoHSL(hsv: HSV): HSL {
+ const { h, s, v } = hsv
+
+ const l = Number((v - (v * s) / 2).toFixed(2))
+
+ const sprime = l === 1 || l === 0 ? 0 : (v - l) / Math.min(l, 1 - l)
+
+ return { h, s: Number(sprime.toFixed(2)), l }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/utils/color.ts b/packages/devui-vue/devui/color-picker/src/utils/color.ts
new file mode 100644
index 0000000000..435f0913c4
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/color.ts
@@ -0,0 +1,47 @@
+export const color: string[] = [
+ '#ffffff',
+ '#ffd7d5',
+ '#ffdaa9',
+ '#fffed5',
+ '#d4fa00',
+ '#73fcd6',
+ '#a5c8ff',
+ '#ffacd5',
+ '#ff7faa',
+ '#d6d6d6',
+ '#ffacaa',
+ '#ffb995',
+ '#fffb00',
+ '#73fa79',
+ '#00fcff',
+ '#78acfe',
+ '#d84fa9',
+ '#ff4f79',
+ '#b2b2b2',
+ '#d7aba9',
+ '#ff6827',
+ '#ffda51',
+ '#00d100',
+ '#00d5ff',
+ '#0080ff',
+ '#ac39ff',
+ '#ff2941',
+ '#888888',
+ '#7a4442',
+ '#ff4c00',
+ '#ffa900',
+ '#3da742',
+ '#3daad6',
+ '#0052ff',
+ '#7a4fd6',
+ '#d92142',
+ '#000000',
+ '#7b0c00',
+ '#ff4c41',
+ '#d6a841',
+ '#407600',
+ '#007aaa',
+ '#021eaa',
+ '#797baa',
+ '#ab1942'
+]
diff --git a/packages/devui-vue/devui/color-picker/src/utils/composeable.ts b/packages/devui-vue/devui/color-picker/src/utils/composeable.ts
new file mode 100644
index 0000000000..507f2cf1ad
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/composeable.ts
@@ -0,0 +1,43 @@
+import { Ref, ref, watch } from 'vue'
+import { ColorPickerColor, CssColorObject } from './color-utils-types'
+export function colorPickerResize(
+ colorCubeRef: Ref,
+ top: Ref,
+ left: Ref
+): void {
+ const rect = colorCubeRef.value?.getBoundingClientRect()
+ left.value = rect?.left
+ top.value = rect?.top + window.scrollY + rect.height
+}
+export function isExhibitionColorPicker(
+ event: PointerEvent,
+ colorCubeRef: Ref,
+ pickerRef: Ref,
+ showColorPicker: Ref
+): void {
+ if (colorCubeRef.value?.contains(event.target)) {
+ showColorPicker.value = true
+ }
+ if (!!pickerRef.value && !pickerRef.value?.contains(event.target)) {
+ showColorPicker.value = !showColorPicker.value
+ }
+}
+export function useReactive(source: () => T): Ref {
+ const model = ref()
+ model.value = source()
+ watch(source, (newValue) => {
+ model.value = newValue
+ })
+ return model
+}
+
+// 根据 value 饱和度 判断文本颜色
+export function changeColorValue(value: ColorPickerColor, maxValue: number): CssColorObject {
+ if (value.alpha > maxValue) {
+ return value.hsva.v > maxValue && value.hsva.s < maxValue
+ ? { color: '#000' }
+ : { color: '#fff' }
+ } else {
+ return { color: '#000' }
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/utils/domDragger.ts b/packages/devui-vue/devui/color-picker/src/utils/domDragger.ts
new file mode 100644
index 0000000000..5e520c8073
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/domDragger.ts
@@ -0,0 +1,365 @@
+export const isObject = (val: unknown): val is Record =>
+ val !== null && typeof val === 'object'
+
+export const isString = (val: unknown): val is string => typeof val === 'string'
+
+export const enum NodeType {
+ ELEMENT_NODE = 1,
+ ATTRIBUTE_NODE = 2,
+ TEXT_NODE = 3,
+ CDATA_SECTION_NODE = 4,
+ ENTITY_REFERENCE_NODE = 5,
+ COMMENT_NODE = 6,
+ PROCESSING_INSTRUCTION_NODE = 7,
+ DOCUMENT_NODE = 9
+}
+
+export interface DragEventOptions {
+ drag?: (event: Event) => void
+ start?: (event: Event) => void
+ end?: (event: Event) => void
+}
+
+export type ScrollElement = Element | Window
+
+export class DOMUtils {
+ static isWindow(val: unknown): val is Window {
+ return val === window
+ }
+
+ /**
+ * 添加事件
+ *
+ *
+ * @param element 如果为null将不会添加事件
+ * @param event
+ * @param handler
+ * @param options
+ */
+ static addEventListener(
+ element: HTMLElement | Document | Window | null,
+ event: string,
+ handler: EventListenerOrEventListenerObject,
+ options: boolean | AddEventListenerOptions = false
+ ): void {
+ if (element && event && handler) {
+ element.addEventListener(event, handler, options)
+ }
+ }
+
+ /**
+ * 移除事件
+ *
+ * @param element 如果为null将不会移除事件
+ * @param event
+ * @param handler
+ * @param options
+ */
+ static removeEventListener(
+ element: HTMLElement | Document | Window | null,
+ event: string,
+ handler: EventListenerOrEventListenerObject,
+ options: boolean | EventListenerOptions = false
+ ): void {
+ if (element && event && handler) {
+ element.removeEventListener(event, handler, options)
+ }
+ }
+
+ /**
+ * 触发拖拽事件
+ *
+ * @param element
+ * @param options
+ */
+ static triggerDragEvent(element: HTMLElement, options: DragEventOptions): void {
+ let isDragging = false
+
+ const moveFn = function (event: Event) {
+ options.drag?.(event)
+ }
+
+ const upFn = (event: Event) => {
+ DOMUtils.removeEventListener(document, 'mousemove', moveFn)
+ DOMUtils.removeEventListener(document, 'mouseup', upFn)
+ DOMUtils.removeEventListener(document, 'touchmove', moveFn)
+ DOMUtils.removeEventListener(document, 'touchend', upFn)
+ document.onselectstart = null
+ document.ondragstart = null
+
+ isDragging = false
+ options.end?.(event)
+ }
+ const downFn = (event: Event) => {
+ if (isDragging) return
+ document.onselectstart = () => false
+ document.ondragstart = () => false
+ DOMUtils.addEventListener(document, 'mousemove', moveFn)
+ DOMUtils.addEventListener(document, 'mouseup', upFn)
+ DOMUtils.addEventListener(document, 'touchmove', moveFn)
+ DOMUtils.addEventListener(document, 'touchend', upFn)
+ isDragging = true
+
+ options.start?.(event)
+ }
+ DOMUtils.addEventListener(element, 'mousedown', downFn)
+ DOMUtils.addEventListener(element, 'touchstart', downFn)
+ return
+ }
+
+ static getBoundingClientRect(element: HTMLElement): DOMRect | null {
+ if (element && isObject(element) && element.nodeType === NodeType.ELEMENT_NODE) {
+ return element.getBoundingClientRect()
+ }
+
+ return null
+ }
+
+ /**
+ * 判断是否存在className样式
+ *
+ * @param element
+ * @param className
+ */
+ public static hasClass(element: HTMLElement, className: string): boolean {
+ if (
+ element &&
+ isObject(element) &&
+ isString(className) &&
+ element.nodeType === NodeType.ELEMENT_NODE
+ ) {
+ return element.classList.contains(className.trim())
+ }
+ return false
+ }
+
+ /**
+ * 添加样式
+ *
+ * @param element
+ * @param className
+ */
+ public static addClass(element: HTMLElement, className: string): void {
+ if (
+ element &&
+ isObject(element) &&
+ isString(className) &&
+ element.nodeType === NodeType.ELEMENT_NODE
+ ) {
+ className = className.trim()
+ if (!DOMUtils.hasClass(element, className)) {
+ const cl = element.className
+ element.className = cl ? cl + ' ' + className : className
+ }
+ }
+ }
+
+ /**
+ * 移除样式
+ *
+ * @param element
+ * @param className
+ */
+ public static removeClass(element: HTMLElement, className: string): void {
+ if (
+ element &&
+ isObject(element) &&
+ isString(className) &&
+ element.nodeType === NodeType.ELEMENT_NODE &&
+ typeof element.className === 'string'
+ ) {
+ className = className.trim()
+ const classes = element.className.trim().split(' ')
+ for (let i = classes.length - 1; i >= 0; i--) {
+ classes[i] = classes[i].trim()
+ if (!classes[i] || classes[i] === className) {
+ classes.splice(i, 1)
+ }
+ }
+ element.className = classes.join(' ')
+ }
+ }
+
+ /**
+ * 切换样式
+ *
+ * @param element
+ * @param className
+ * @param force
+ */
+ public static toggleClass(element: HTMLElement, className: string, force?: boolean): void {
+ if (
+ element &&
+ isObject(element) &&
+ isString(className) &&
+ element.nodeType === NodeType.ELEMENT_NODE
+ ) {
+ element.classList.toggle(className, force)
+ }
+ }
+
+ /**
+ * 替换样式
+ *
+ * @param element
+ * @param oldClassName
+ * @param newClassName
+ */
+ public static replaceClass(
+ element: HTMLElement,
+ oldClassName: string,
+ newClassName: string
+ ): void {
+ if (
+ element &&
+ isObject(element) &&
+ isString(oldClassName) &&
+ isString(newClassName) &&
+ element.nodeType === NodeType.ELEMENT_NODE
+ ) {
+ oldClassName = oldClassName.trim()
+ newClassName = newClassName.trim()
+ DOMUtils.removeClass(element, oldClassName)
+ DOMUtils.addClass(element, newClassName)
+ }
+ }
+
+ static getScrollTop(el: ScrollElement): number {
+ const top = 'scrollTop' in el ? el.scrollTop : el.pageYOffset
+
+ // iOS scroll bounce cause minus scrollTop
+ return Math.max(top, 0)
+ }
+
+ static setScrollTop(el: ScrollElement, value: number): void {
+ if ('scrollTop' in el) {
+ el.scrollTop = value
+ } else {
+ el.scrollTo(el.scrollX, value)
+ }
+ }
+
+ static getRootScrollTop(): number {
+ return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
+ }
+
+ static setRootScrollTop(value: number): void {
+ DOMUtils.setScrollTop(window, value)
+ DOMUtils.setScrollTop(document.body, value)
+ }
+
+ static getElementTop(el: ScrollElement, scroller?: HTMLElement): number {
+ if (DOMUtils.isWindow(el)) {
+ return 0
+ }
+
+ const scrollTop = scroller ? DOMUtils.getScrollTop(scroller) : DOMUtils.getRootScrollTop()
+ return el.getBoundingClientRect().top + scrollTop
+ }
+
+ static getVisibleHeight(el: ScrollElement): number {
+ if (DOMUtils.isWindow(el)) {
+ return el.innerHeight
+ }
+ return el.getBoundingClientRect().height
+ }
+
+ static isHidden(el: HTMLElement): boolean {
+ if (!el) {
+ return false
+ }
+
+ const style = window.getComputedStyle(el)
+ const hidden = style.display === 'none'
+
+ // 在以下情况下,offsetParent返回null:
+ // 1. 元素或其父元素的display属性设置为none.
+ // 2. 元素的position属性设置为fixed
+ const parentHidden = el.offsetParent === null && style.position !== 'fixed'
+
+ return hidden || parentHidden
+ }
+
+ /**
+ * 触发事件
+ *
+ * @param el
+ * @param type
+ */
+ static triggerEvent(el: Element, type: string): void {
+ if ('createEvent' in document) {
+ // modern browsers, IE9+
+ const e = document.createEvent('HTMLEvents')
+ e.initEvent(type, false, true)
+ el.dispatchEvent(e)
+ }
+ }
+
+ /**
+ * 计算相对于中心点的旋转角度
+ * @param element
+ * @param event
+ */
+ static calcAngle(element: HTMLElement, event: MouseEvent): number {
+ const rect = element.getBoundingClientRect()
+
+ const originX = rect.left + rect.width / 2
+ const originY = rect.top + rect.height / 2
+
+ //获得中心点和鼠标坐标连线,与y轴正半轴之间的夹角
+ const x = Math.abs(originX - event.clientX)
+ const y = Math.abs(originY - event.clientY)
+ const z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
+ const cos = y / z
+ const rad = Math.acos(cos) //用反三角函数求弧度
+ let angle = Math.floor(180 / (Math.PI / rad)) //将弧度转换成角度
+
+ if (event.clientX > originX && event.clientY > originY) {
+ //鼠标在第四象限
+ angle = 180 - angle
+ }
+
+ if (event.clientX == originX && event.clientY > originY) {
+ //鼠标在y轴负方向上
+ angle = 180
+ }
+
+ if (event.clientX > originX && event.clientY == originY) {
+ //鼠标在x轴正方向上
+ angle = 90
+ }
+
+ if (event.clientX < originX && event.clientY > originY) {
+ //鼠标在第三象限
+ angle = 180 + angle
+ }
+
+ if (event.clientX < originX && event.clientY == originY) {
+ //鼠标在x轴负方向
+ angle = 270
+ }
+
+ if (event.clientX < originX && event.clientY < originY) {
+ //鼠标在第二象限
+ angle = 360 - angle
+ }
+
+ return angle
+ }
+
+ /**
+ * querySelector
+ *
+ * @param selectors
+ * @param parentElement
+ */
+ static querySelector(
+ selectors: string,
+ parentElement?: HTMLElement
+ ): E | null {
+ if (parentElement) {
+ return parentElement.querySelector(selectors)
+ }
+ return document.querySelector(selectors)
+ }
+}
diff --git a/packages/devui-vue/devui/color-picker/src/utils/helpers.ts b/packages/devui-vue/devui/color-picker/src/utils/helpers.ts
new file mode 100644
index 0000000000..9052074fb2
--- /dev/null
+++ b/packages/devui-vue/devui/color-picker/src/utils/helpers.ts
@@ -0,0 +1,48 @@
+export function padEnd(str: string, length: number, char = '0'): string {
+ return str + char.repeat(Math.max(0, length - str.length))
+}
+export function chunk(str: string, size = 1): string[] {
+ const chunked: string[] = []
+ let index = 0
+ while (index < str.length) {
+ chunked.push(str.substr(index, size))
+ index += size
+ }
+ return chunked
+}
+export function mergeObjects(source, target): any {
+ return Object.assign(source, target)
+}
+export function parseHex(value: string): number {
+ return parseInt(value, 16)
+}
+export const clamp = (value: number, min: number, max: number): number => {
+ return min < max
+ ? value < min
+ ? min
+ : value > max
+ ? max
+ : value
+ : value < max
+ ? max
+ : value > min
+ ? min
+ : value
+}
+export const upperCase = (word: string): string => {
+ return word.toLocaleUpperCase()
+}
+export const lowerCase = (word: string): string => {
+ return word.toLocaleLowerCase()
+}
+export function splitStr(str: string, chars: string): string {
+ return str.split(chars)[0]
+}
+// 保留小数工具
+export function keepDecimal(value: number, digits = 0): number {
+ const COUNT_VALUE = 10 ** digits
+ return Math.round(value * COUNT_VALUE) / COUNT_VALUE
+}
+export function has(obj: any, key: string[]): boolean {
+ return key.every((k) => obj.hasOwnProperty(k))
+}
diff --git a/packages/devui-vue/devui/comment/index.ts b/packages/devui-vue/devui/comment/index.ts
index d9fb1203f0..c55970c9e5 100644
--- a/packages/devui-vue/devui/comment/index.ts
+++ b/packages/devui-vue/devui/comment/index.ts
@@ -10,7 +10,7 @@ export { Comment }
export default {
title: 'Comment 评论',
category: '数据展示',
- status: '70%', // TODO: 组件若开发完成则填入"已完成",并删除该注释
+ status: '100%',
install(app: App): void {
app.use(Comment as any)
}
diff --git a/packages/devui-vue/devui/comment/src/comment.scss b/packages/devui-vue/devui/comment/src/comment.scss
index fb2752bd93..a8e5740774 100644
--- a/packages/devui-vue/devui/comment/src/comment.scss
+++ b/packages/devui-vue/devui/comment/src/comment.scss
@@ -1,30 +1,39 @@
@import '../../styles-var/devui-var.scss';
+
.devui-comment {
display: flex;
align-items: flex-start;
- &-avatar{
+
+ &-avatar {
margin: 0 16px 0 0;
}
+
&-right{
-
+
}
- &-head{
+
+ &-head {
display: flex;
align-items: center;
}
- &-author{
+
+ &-author {
padding: 0 8px 0 0;
font-size: $devui-font-size;
color: $devui-text-weak;
}
- &-datetime{
+
+ &-datetime {
font-size: $devui-font-size;
color: $devui-aide-text;
}
- &-content{
+
+ &-content {
font-size: $devui-text;
}
- &-actions{
+
+ &-actions {
+ padding: 0;
list-style-type: none;
margin: 12px 0 0;
}
diff --git a/packages/devui-vue/devui/comment/src/comment.tsx b/packages/devui-vue/devui/comment/src/comment.tsx
index aeec79fefd..438063c82d 100644
--- a/packages/devui-vue/devui/comment/src/comment.tsx
+++ b/packages/devui-vue/devui/comment/src/comment.tsx
@@ -1,7 +1,20 @@
import { defineComponent } from 'vue'
import { commentProps, CommentProps } from './comment-types'
import './comment.scss'
-
+/*
+ * date:2021-12-18
+ * author:njl
+ *
+ * actions 底部操作栏
+ * author 作者区域
+ * avatar 头像区域
+ * content 内容操作区域
+ * datetime 时间区域
+ * avatarDom 头像可只传入地址
+ * actionDom 操作区域根据传入的组件数量来生成相应的li标签
+ *
+ * 目前可成为参数的为 avatar,actions 其他均为具名插槽的形式,后期可继续根据需要改造
+**/
export default defineComponent({
name: 'DComment',
props: commentProps,
@@ -10,14 +23,15 @@ export default defineComponent({
setup(props, { slots }) {
return () => {
const getAction = (actions:any) => {
+
if (!actions || !actions.length) {
return null;
}
- const actionList = actions.map((action:any, index:number) => - {action}
);
+ const actionList = actions.map((action:any, index:number) => );
return actionList;
};
- const actions = props.actions ?? slots.actions?.();
+ const actions = props.actions ?? slots.actions?.();
const author = props.author ?? slots.author?.();
const avatar = props.avatar ?? slots.avatar?.();
const content = props.content ?? slots.content?.();
diff --git a/packages/devui-vue/devui/dragdrop/index.ts b/packages/devui-vue/devui/dragdrop/index.ts
index b203021a07..d1953c4993 100644
--- a/packages/devui-vue/devui/dragdrop/index.ts
+++ b/packages/devui-vue/devui/dragdrop/index.ts
@@ -1,8 +1,9 @@
import type { App } from 'vue'
import DraggableDirective from './src/draggable-directive'
import DroppableDirective from './src/droppable-directive'
+import SortableDirective from './src/sortable-directive'
-export { DraggableDirective, DroppableDirective }
+export { DraggableDirective, DroppableDirective, SortableDirective }
export default {
title: 'Dragdrop 拖拽',
@@ -11,5 +12,6 @@ export default {
install(app: App): void {
app.directive('DDraggable', DraggableDirective)
app.directive('DDroppable', DroppableDirective)
+ app.directive('DSortable', SortableDirective)
}
}
diff --git a/packages/devui-vue/devui/dragdrop/src/constant.ts b/packages/devui-vue/devui/dragdrop/src/constant.ts
new file mode 100644
index 0000000000..98c9224ec4
--- /dev/null
+++ b/packages/devui-vue/devui/dragdrop/src/constant.ts
@@ -0,0 +1 @@
+export const shadowId = 'shadow0611'
\ No newline at end of file
diff --git a/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts b/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts
index c53e3342c1..fa333e2bdf 100644
--- a/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts
+++ b/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts
@@ -1,11 +1,41 @@
+import { changeDragState, deleteInsertedSortableShadow } from './utils'
+import { shadowId } from './constant'
+
export default {
- mounted(el: HTMLElement): void {
- el.setAttribute('draggable', 'true')
+ /**
+ *
+ * @param el
+ * @description
+ * 1、绑定该指令的element将会具备拖拽能力
+ * 2、为各元素进行初始化配置
+ * 2.1、dragFlag: 是否处于拖拽中
+ * 2.2、dragOverFlag: 是否处于可放置区域
+ *
+ * 1、整体思路
+ * 1.1、为每个绑定drag指令的元素维护状态
+ * 1.1.1、状态集合:dragStart、drag、dragover、drop、shouldCreateShadow
+ *
+ * 1.2、进入drop区域后,确保drop区域能够获取正在进行drag的元素
+ */
+ mounted(el: HTMLElement, binding: unknown): void {
+ el.setAttribute('draggable', 'true');
el.style.cursor = 'grab'
// dragstart/drag/dragend
- el.addEventListener('dragstart', (event: DragEvent) => {
- event.dataTransfer.setData('originId', el.id)
- })
+ el.addEventListener('drag', () => {
+ changeDragState(el, el.id, 'true', 'true', 'false', 'false', 'false', 'true')
+ if (binding.instance.$root.dropElement && document.getElementById(shadowId)){
+ deleteInsertedSortableShadow(binding.instance.$root.dropElement) // 如何让它仅执行1次?
+ binding.instance.$root.dropElement = null
+ }
+ }, false)
+
+ // dragStart事件为每个绑定元素进行初始化
+ el.addEventListener('dragstart', ()=>{
+ // el or binding.instance or vnode.context
+ changeDragState(el, el.id, 'true', 'true', 'false', 'false', 'false', 'false')
+ binding.instance.$root.identity = el.id
+ el.dataset.dragArea = el.parentNode.className
+ }, false)
},
}
diff --git a/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts b/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts
index d60618868e..289ad8ef7e 100644
--- a/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts
+++ b/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts
@@ -1,15 +1,49 @@
+import { changeDragState } from './utils'
+
export default {
- mounted(el: HTMLElement): void {
- // dragenter/dragover/dragend/drop
+ /**
+ *
+ * @param el
+ * @description
+ * dragOver:
+ * 1、生成与清除阴影的时机
+ * 1.1、生成时机(只生成一次): dragFlag === true && dragOverFlag === true
+ * drop:
+ * 1、完成放的操作
+ * 1.1、清除相应的阴影
+ */
+ mounted(el: HTMLElement, binding:unknown): void {
+ // dragenter/dragover/dragend/drop
el.addEventListener('dragover', (event: DragEvent) => {
event.preventDefault()
- })
+ const dragId = binding.instance.$root.identity
+ changeDragState(document.getElementById(dragId), dragId, 'true', 'false', 'true', 'false', 'false', 'false')
+ document.getElementById(dragId).dataset.dropArea = [...el.childNodes][1].className
+ }, false)
+ // 新增两个标识解决战斗,即dragStart区域、drop区域、sortableDrop区域
el.addEventListener('drop', (event: DragEvent) => {
- const originId = event.dataTransfer.getData('originId')
- const originNodeCopy = document.getElementById(originId).cloneNode(true)
- const targetNode: any = event.target
- targetNode.append(originNodeCopy)
+ event.preventDefault()
+ const dragId = binding.instance.$root.identity
+ if (document.getElementById(dragId).dataset.dropArea == document.getElementById(dragId).dataset.dragArea){
+ return
+ }
+ // 如何定义可放置区域这个问题得商榷一下
+ const childrenArr = [...Array.from(el.children)[1].children]
+ if (childrenArr.length > 0){
+ for (let index = 0; index < childrenArr.length; index++){
+ const childrenYRange = childrenArr[index].getBoundingClientRect().top + childrenArr[index].offsetHeight / 2
+ if (parseFloat(event.clientY) < parseFloat(childrenYRange)){
+ el.children[1].insertBefore(document.getElementById(dragId), childrenArr[index])
+ break
+ }
+ if (index === childrenArr.length-1){
+ el.children[1].appendChild(document.getElementById(dragId))
+ }
+ }
+ }else {
+ el.childNodes[1].appendChild(document.getElementById(dragId))
+ }
})
},
}
diff --git a/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts b/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts
new file mode 100644
index 0000000000..2066b831d4
--- /dev/null
+++ b/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts
@@ -0,0 +1,48 @@
+import { shadowId } from './constant'
+import { changeDragState, createInsertSortableShadow, insertDragElement } from './utils'
+
+
+export default {
+ /**
+ *
+ * @param el
+ * @description
+ * 此命令用于将元素变为可放置的元素并且支持排序
+ * dragover:
+ * 1、说明此时进入可排序放置的区域
+ * 2、此时应该生成相应的可排序的shadow
+ * drop:
+ * 1、可放置区域里如果没有拖拽元素,直接放置
+ * 2、可放置区域里如果有其他的可拖拽元素,需要对比放置到正确的位置上
+ */
+ mounted(el: HTMLElement, binding:unknown):void {
+ el.addEventListener('dragover', function (event: DragEvent){
+ event.preventDefault()
+ const targetNode: any = event.target;
+ const dragId = binding.instance.$root.identity
+ if (!binding.instance.$root.dropElement){
+ binding.instance.$root.dropElement = [...el.childNodes][1]
+ }
+ changeDragState(document.getElementById(binding.instance.$root.identity), binding.instance.$root.identity, 'true', 'false', 'true', 'false', 'true', 'false')
+ const { dragover, shouldCreateShadow } = document.getElementById(dragId).dataset
+ if (dragover == 'true'){
+ if (shouldCreateShadow == 'true'){
+ createInsertSortableShadow([...targetNode.children][1], event, dragId)
+ }
+ }
+
+ })
+ el.addEventListener('drop', function (event: DragEvent){
+ // 获取可放置区域
+ const dropArea = [...el.childNodes][1]
+ const dragId = binding.instance.$root.identity
+ dropArea.removeChild(document.getElementById(shadowId))
+ if ([...dropArea.childNodes].length == 0){
+ dropArea.appendChild(document.getElementById(dragId))
+ }else {
+ insertDragElement(dropArea, dragId, event)
+ }
+ changeDragState(document.getElementById(dragId), dragId, 'false', 'false', 'false', 'true', 'false', 'false')
+ })
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/dragdrop/src/utils.ts b/packages/devui-vue/devui/dragdrop/src/utils.ts
new file mode 100644
index 0000000000..0b07447eba
--- /dev/null
+++ b/packages/devui-vue/devui/dragdrop/src/utils.ts
@@ -0,0 +1,143 @@
+import { shadowId } from './constant'
+
+/**
+ *
+ * @param id
+ * @descriprion
+ * 根据id获取非内联样式元素的样式
+ */
+function getElementStyle (id: string, styleName: string):string {
+ return document.getElementById(id).currentStyle ? document.getElementById(id).currentStyle[styleName] : window.getComputedStyle(
+ document.getElementById(id),
+ styleName
+ )
+}
+
+/**
+ *
+ * @param originId
+ * @description
+ * 根据拖拽的id生成相应的阴影
+ * 如何生成shadow?
+ * 情况一: dragable -> drop without sortable
+ * 情况二: anything -> drop without anything
+ */
+function createShadow (originId:string):HTMLElement {
+ const shadow = document.createElement('div');
+ shadow.id = shadowId
+ shadow.style.background = 'rgb(206, 215, 255)'
+ shadow.style.width = getElementStyle(originId, 'width')
+ shadow.style.height = '20px'
+ return shadow
+}
+
+/**
+ *
+ * @param el
+ * @param originId
+ * @param dragStart
+ * @param drag
+ * @param dragover
+ * @param drop
+ * @param shouldCreateShadow
+ * @param dragFlag
+ * @description
+ * 改变拖拽元素相应的状态
+ */
+function changeDragState (el:string, originId:string, dragStart:string, drag:string, dragover:string, drop:string, shouldCreateShadow:string, dragFlag: string): void{
+ el.dataset.originId = originId
+ el.dataset.dragStart = dragStart
+ el.dataset.dragover = dragover
+ el.dataset.drop = drop
+ el.dataset.shouldCreateShadow = shouldCreateShadow
+ el.dataset.dragFlag = dragFlag
+}
+
+/**
+ *
+ * @param compareElement
+ * @returns
+ * @description
+ * 计算可对比元素的高度
+ */
+function computeCompareElementHeight (compareElement: HTMLCollection): unknown{
+ return compareElement.getBoundingClientRect().top + Math.floor(compareElement.offsetHeight / 2)
+}
+
+/**
+ *
+ * @param sortDropArea
+ * @param mouseObject
+ * 1、首先确认可放置区域
+ * 2、确保每个元素只生成一次shadow
+ * 3、
+ */
+function createInsertSortableShadow (sortDropArea: unknown, mouseObject: unknown, originId: string):void {
+ const sortDropAreaArr: Array = [...sortDropArea.children]
+ if (sortDropAreaArr.length == 0){
+ if (!document.getElementById(shadowId)){
+ const shadowElement = createShadow(originId)
+ sortDropArea.appendChild(shadowElement)
+ }
+ }else {
+ for (let index = 0; index < sortDropAreaArr.length; index++){
+ const compareHeight = computeCompareElementHeight(sortDropAreaArr[index])
+ document.getElementById(shadowId) ? sortDropArea.removeChild(document.getElementById(shadowId)) : null
+ if (index == sortDropAreaArr.length-1){
+ sortDropArea.appendChild(createShadow(originId))
+ break
+ }
+ if (Math.floor(mouseObject.clientY)<= compareHeight){
+ sortDropArea.insertBefore(createShadow(originId), sortDropAreaArr[index])
+ break
+ }
+ }
+ }
+}
+
+/**
+ *
+ * @param dropAreaContainer
+ * @param dragId
+ * @param mouseObject
+ * @description
+ * 向sortable区域插入拖拽元素
+ */
+function insertDragElement (dropAreaContainer: HTMLCollection, dragId: string, mouseObject: MouseEvent): void {
+ for (let index = 0; index < [...dropAreaContainer.children].length; index++){
+ if (index == [...dropAreaContainer.children].length-1){
+ dropAreaContainer.appendChild(document.getElementById(dragId))
+ break
+ }
+ if (Math.floor(mouseObject.clientY) <= computeCompareElementHeight([...dropAreaContainer.children][index])){
+ dropAreaContainer.insertBefore(document.getElementById(dragId), [...dropAreaContainer.children][index])
+ break
+ }
+ }
+}
+
+/**
+ *
+ * @param dropSortArea
+ * @description
+ * 删除可排序区域中的shadow
+ */
+function deleteInsertedSortableShadow (dropSortArea: unknown):void{
+ if (dropSortArea){
+ if (document.getElementById(shadowId)){
+ if (dropSortArea.contains(document.getElementById(shadowId))){
+ dropSortArea.removeChild(document.getElementById(shadowId))
+ }
+ }
+ }
+}
+
+
+export {
+ createShadow,
+ changeDragState,
+ createInsertSortableShadow,
+ deleteInsertedSortableShadow,
+ computeCompareElementHeight,
+ insertDragElement
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/drawer/index.ts b/packages/devui-vue/devui/drawer/index.ts
index 2ad72eb001..f0b0a21e7d 100644
--- a/packages/devui-vue/devui/drawer/index.ts
+++ b/packages/devui-vue/devui/drawer/index.ts
@@ -1,18 +1,21 @@
-import type { App } from 'vue'
-import Drawer from './src/drawer'
+import type { App } from 'vue';
+import Drawer from './src/drawer';
+import DrawerService from './src/drawer-service';
-Drawer.install = function(app: App): void {
- app.component(Drawer.name, Drawer)
-}
-
-export { Drawer }
+export { Drawer, DrawerService };
+// TODO: no-service model exists memory leak
+// rest tasks
+// 1. draggable width
+// 2. function of the 1st icon in header-component
+// 3. rest service-model api
+// 4. typescript type of props
export default {
title: 'Drawer 抽屉板',
category: '反馈',
- status: '50%',
+ status: '75%',
install(app: App): void {
-
- app.use(Drawer as any)
- }
-}
+ app.component(Drawer.name, Drawer);
+ app.config.globalProperties.$drawerService = new DrawerService();
+ },
+};
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-body.scss b/packages/devui-vue/devui/drawer/src/components/drawer-body.scss
deleted file mode 100644
index 707fae3801..0000000000
--- a/packages/devui-vue/devui/drawer/src/components/drawer-body.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-@import '../../../style/devui.scss';
-
-.devui-drawer {
- position: fixed;
- top: 0;
- left: 0;
- height: 100vh;
-}
-
-.devui-overlay-wrapper {
- display: flex;
- justify-content: center;
- align-items: center;
- position: absolute;
- top: 0;
- bottom: 0;
- width: 100vw;
-}
-
-.devui-overlay-backdrop {
- position: absolute;
- top: 0;
- left: 0;
- background: $devui-shadow;
- width: 100vw;
- height: 100vh;
-}
-
-.devui-drawer-nav {
- position: absolute;
- top: 0;
- bottom: 0;
- border-radius: $devui-border-radius;
- background: $devui-base-bg;
-}
-
-.devui-drawer-content {
- border-radius: $devui-border-radius;
- overflow: auto;
- box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow;
- padding: 20px;
- height: 100vh;
-}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx
deleted file mode 100644
index a52965f7c0..0000000000
--- a/packages/devui-vue/devui/drawer/src/components/drawer-body.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { defineComponent, inject, computed } from 'vue'
-
-import './drawer-body.scss'
-
-export default defineComponent({
- name: 'DrawerBody',
- setup(props, { slots }) {
- const isFullScreen: any = inject('isFullScreen')
- const closeDrawer: any = inject('closeDrawer')
- const zindex: number = inject('zindex')
- const isCover: boolean = inject('isCover')
- const position: any = inject('position')
- const width: any = inject('width')
- const visible: boolean = inject('visible')
- const backdropCloseable: any = inject('backdropCloseable')
-
- const navRight = computed(() => position.value === 'right' ? { 'right': 0 } : { 'left': 0 })
- const navWidth = computed(() => isFullScreen.value ? '100vw' : width.value)
-
- const clickContent = (e) => {
- e.stopPropagation()
- }
-
- const handleDrawerClose = () => {
- if (!backdropCloseable.value) return;
- closeDrawer();
- }
-
- return {
- zindex,
- slots,
- isCover,
- navRight,
- navWidth,
- visible,
- clickContent,
- handleDrawerClose,
- }
- },
-
- render() {
- const {
- zindex, slots, isCover, navRight, navWidth, visible, handleDrawerClose
- } = this
-
- if (!visible) return null
-
- return (
-
- {isCover ?
: null}
-
-
-
- {slots.default ? slots.default() : null}
-
-
-
-
- )
- }
-})
\ No newline at end of file
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx
deleted file mode 100644
index c45f3b2545..0000000000
--- a/packages/devui-vue/devui/drawer/src/components/drawer-container.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { defineComponent, inject } from 'vue'
-
-export default defineComponent({
- name: 'DrawerContainer',
- setup() {
- const visible = inject('visible')
- return { visible }
- },
- render() {
- const { visible } = this
- if (!visible) return null
- return 内容区域
- }
-})
\ No newline at end of file
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-header.scss b/packages/devui-vue/devui/drawer/src/components/drawer-header.scss
deleted file mode 100644
index ee1776a5d0..0000000000
--- a/packages/devui-vue/devui/drawer/src/components/drawer-header.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import '../drawer.scss';
-
-.devui-drawer-header {
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
-
- & .devui-drawer-header-item + .devui-drawer-header-item {
- padding-left: 12px;
- }
-}
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx
deleted file mode 100644
index 7e3e3a05c0..0000000000
--- a/packages/devui-vue/devui/drawer/src/components/drawer-header.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { defineComponent, ref, inject, computed } from 'vue'
-
-import './drawer-header.scss'
-
-export default defineComponent({
- name: 'DrawerHeader', // 头部
- emits: ['toggleFullScreen', 'close'],
- setup(props, ctx) {
- const isFullScreen = ref(false)
-
- const visible: boolean = inject('visible')
-
- const fullScreenClassName = computed(() => isFullScreen.value ? 'icon icon-minimize' : 'icon icon-maxmize')
-
- const handleFullScreen = (e) => {
- e.stopPropagation()
- isFullScreen.value = !isFullScreen.value
- ctx.emit('toggleFullScreen')
- }
-
- const handleDrawerClose = () => {
- ctx.emit('close')
- }
-
- return { fullScreenClassName, visible, handleFullScreen, handleDrawerClose, }
- },
- render() {
- const { handleFullScreen, handleDrawerClose, visible, fullScreenClassName } = this
-
- if (!visible) return null
-
- return (
-
- )
- }
-})
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss
new file mode 100644
index 0000000000..1cec622700
--- /dev/null
+++ b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.scss
@@ -0,0 +1,20 @@
+@import '../../../styles-var/devui-var.scss';
+
+.devui-drawer-overlay {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: $devui-shadow;
+}
+
+.drawer-overlay-fade-enter-active,
+.drawer-overlay-fade-leave-active {
+ transition: opacity 0.1s linear;
+}
+
+.drawer-overlay-fade-enter-from,
+.drawer-overlay-fade-leave-to {
+ opacity: 0;
+}
diff --git a/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx
new file mode 100644
index 0000000000..973e4b3a0d
--- /dev/null
+++ b/packages/devui-vue/devui/drawer/src/components/drawer-overlay.tsx
@@ -0,0 +1,18 @@
+import { defineComponent, Transition } from 'vue';
+import type { SetupContext } from 'vue';
+import { drawerOverlayProps, DrawerOverlayProps } from '../drawer-types';
+import './drawer-overlay.scss';
+
+export default defineComponent({
+ name: 'DDrawerOverlay',
+ props: drawerOverlayProps,
+ emits: ['click'],
+ setup(props: DrawerOverlayProps, ctx: SetupContext) {
+ const handleClick = (e: Event) => {
+ ctx.emit('click', e);
+ };
+ return () => (
+ {props.visible && }
+ );
+ },
+});
diff --git a/packages/devui-vue/devui/drawer/src/drawer-service.tsx b/packages/devui-vue/devui/drawer/src/drawer-service.tsx
new file mode 100644
index 0000000000..22dbae350d
--- /dev/null
+++ b/packages/devui-vue/devui/drawer/src/drawer-service.tsx
@@ -0,0 +1,58 @@
+import { createApp, nextTick, onUnmounted, reactive } from 'vue';
+import type { App } from 'vue';
+import Drawer from './drawer';
+import { DrawerOptions } from './drawer-types';
+
+const defaultOptions: DrawerOptions = {
+ modelValue: false,
+ content: '',
+ zIndex: 1000,
+ showOverlay: true,
+ escKeyCloseable: true,
+ position: 'right',
+ lockScroll: true,
+ closeOnClickOverlay: true,
+};
+
+function initInstance(state: DrawerOptions): App {
+ const container = document.createElement('div');
+ const content = state.content;
+ delete state.content;
+
+ const app: App = createApp({
+ setup() {
+ const handleVisibleChange = () => {
+ state.modelValue = false;
+ };
+ onUnmounted(() => {
+ console.log(111);
+ document.body.removeChild(container);
+ });
+ return () => (
+
+ {content}
+
+ );
+ },
+ });
+
+ document.body.appendChild(container);
+ app.mount(container);
+ return app;
+}
+
+export default class DrawerService {
+ open(options: DrawerOptions): { close: () => void } {
+ const state: DrawerOptions = reactive({ ...defaultOptions, ...options });
+ const app = initInstance(state);
+
+ state.modelValue = true;
+
+ return {
+ close: () => {
+ state.modelValue = false;
+ app.unmount();
+ },
+ };
+ }
+}
diff --git a/packages/devui-vue/devui/drawer/src/drawer-types.ts b/packages/devui-vue/devui/drawer/src/drawer-types.ts
index 207cf89ef5..498f09c187 100644
--- a/packages/devui-vue/devui/drawer/src/drawer-types.ts
+++ b/packages/devui-vue/devui/drawer/src/drawer-types.ts
@@ -1,11 +1,7 @@
-import type { ExtractPropTypes, PropType } from 'vue'
+import type { ExtractPropTypes, PropType, Slot, Ref } from 'vue';
export const drawerProps = {
- width: {
- type: String,
- default: '300px',
- },
- visible: {
+ modelValue: {
type: Boolean,
default: false,
},
@@ -13,7 +9,7 @@ export const drawerProps = {
type: Number,
default: 1000,
},
- isCover: {
+ showOverlay: {
type: Boolean,
default: true,
},
@@ -23,15 +19,40 @@ export const drawerProps = {
},
position: {
type: String as PropType<'left' | 'right'>,
- default: 'left',
+ default: 'right',
+ },
+ lockScroll: {
+ type: Boolean,
+ default: true,
},
- backdropCloseable: {
+ closeOnClickOverlay: {
type: Boolean,
default: true,
},
- beforeHidden: {
- type: [Promise, Function] as PropType | (() => boolean | Promise)>,
+ beforeClose: {
+ type: Function as PropType<(done: () => void) => void>,
},
-} as const
+};
+
+export const drawerOverlayProps = {
+ visible: {
+ type: Boolean,
+ default: false,
+ },
+};
+
+type DrawerEmitEvent = 'update:modelValue' | 'close' | 'open';
+
+export type DrawerEmit = (event: DrawerEmitEvent, result?: unknown) => void;
+
+export type DrawerProps = ExtractPropTypes;
+
+export type DrawerOverlayProps = ExtractPropTypes;
+
+export type DrawerOptions = Partial & { content?: string | Slot };
-export type DrawerProps = ExtractPropTypes
+export type UseDrawerFn = {
+ drawerRef: Ref;
+ drawerClasses: Ref>;
+ handleOverlayClick: () => void;
+};
diff --git a/packages/devui-vue/devui/drawer/src/drawer.scss b/packages/devui-vue/devui/drawer/src/drawer.scss
index 8b13789179..3afef767e4 100644
--- a/packages/devui-vue/devui/drawer/src/drawer.scss
+++ b/packages/devui-vue/devui/drawer/src/drawer.scss
@@ -1 +1,52 @@
+@import '../../styles-var/devui-var.scss';
+.devui-drawer {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 300px;
+ border-radius: $devui-border-radius;
+ background-color: $devui-base-bg;
+ transform: translateX(0);
+ opacity: 1;
+ overflow: auto;
+ box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow;
+
+ &-left {
+ left: 0;
+ }
+
+ &-right {
+ right: 0;
+ }
+}
+
+.drawer-fly {
+ &-right-enter-active {
+ transition: all 0.3s cubic-bezier(0.16, 0.75, 0.5, 1);
+ }
+
+ &-right-leave-active {
+ transition: all 0.3s cubic-bezier(0.5, 0, 0.84, 0.25);
+ }
+
+ &-right-enter-from,
+ &-right-leave-to {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ &-left-enter-active {
+ transition: all 0.3s cubic-bezier(0.16, 0.75, 0.5, 1);
+ }
+
+ &-left-leave-active {
+ transition: all 0.3s cubic-bezier(0.5, 0, 0.84, 0.25);
+ }
+
+ &-left-enter-from,
+ &-left-leave-to {
+ opacity: 0;
+ transform: translateX(-100%);
+ }
+}
diff --git a/packages/devui-vue/devui/drawer/src/drawer.tsx b/packages/devui-vue/devui/drawer/src/drawer.tsx
index 09c84e4d9a..37e7c09b6f 100644
--- a/packages/devui-vue/devui/drawer/src/drawer.tsx
+++ b/packages/devui-vue/devui/drawer/src/drawer.tsx
@@ -1,91 +1,27 @@
-import { defineComponent, ref, toRefs, watch, onUnmounted, Teleport, provide } from 'vue'
-import { drawerProps, DrawerProps } from './drawer-types'
-
-import DrawerHeader from './components/drawer-header'
-import DrawerContainer from './components/drawer-container'
-import DrawerBody from './components/drawer-body'
+import { defineComponent, Teleport, Transition } from 'vue';
+import { drawerProps, DrawerProps } from './drawer-types';
+import DrawerOverlay from './components/drawer-overlay';
+import { useDrawer } from './use-drawer';
+import './drawer.scss';
export default defineComponent({
name: 'DDrawer',
+ inheritAttrs: false,
props: drawerProps,
- emits: ['close', 'update:visible', 'afterOpened'],
- setup(props: DrawerProps, { emit, slots }) {
- const {
- width, visible, zIndex, isCover, escKeyCloseable, position,
- backdropCloseable,
- } = toRefs(props)
- const isFullScreen = ref(false)
-
- const fullScreenEvent = () => {
- isFullScreen.value = !isFullScreen.value
- }
-
- const closeDrawer = async () => {
- const beforeHidden = props.beforeHidden;
- let result = (typeof beforeHidden === 'function' ? beforeHidden(): beforeHidden) ?? false;
- if (result instanceof Promise) {
- result = await result;
- }
- if (result) return;
-
- emit('update:visible', false)
- emit('close')
- }
-
- const escCloseDrawer = (e) => {
- if (e.code === 'Escape') {
- closeDrawer()
- }
- }
-
- watch(visible, (val) => {
- if (val) {
- emit('afterOpened')
- isFullScreen.value = false
- }
- if (escKeyCloseable && val) {
- document.addEventListener('keyup', escCloseDrawer)
- } else {
- document.removeEventListener('keyup', escCloseDrawer)
- }
- })
-
- provide('closeDrawer', closeDrawer)
- provide('zindex', zIndex)
- provide('isCover', isCover)
- provide('position', position)
- provide('width', width)
- provide('visible', visible)
- provide('isFullScreen', isFullScreen)
- provide('backdropCloseable', backdropCloseable)
-
- onUnmounted(() => {
- document.removeEventListener('keyup', escCloseDrawer)
- })
-
- return {
- isFullScreen,
- visible,
- slots,
- fullScreenEvent,
- closeDrawer,
- }
- },
- render() {
- const fullScreenEvent: any = this.fullScreenEvent
- const closeDrawer: any = this.closeDrawer
-
- if (!this.visible) return null
-
- return (
-
-
- {this.slots.header ? this.slots.header() :
-
- }
- {this.slots.default ? this.slots.default() : }
-
+ emits: ['close', 'update:modelValue', 'open'],
+ setup(props: DrawerProps, { emit, slots, attrs }) {
+ const { drawerRef, drawerClasses, handleOverlayClick } = useDrawer(props, emit);
+ return () => (
+
+ {props.showOverlay && }
+
+ {props.modelValue && (
+
+ {slots.default?.()}
+
+ )}
+
- )
- }
-})
+ );
+ },
+});
diff --git a/packages/devui-vue/devui/drawer/src/use-drawer.ts b/packages/devui-vue/devui/drawer/src/use-drawer.ts
new file mode 100644
index 0000000000..f328cc1201
--- /dev/null
+++ b/packages/devui-vue/devui/drawer/src/use-drawer.ts
@@ -0,0 +1,50 @@
+import { computed, onUnmounted, ref, watch } from 'vue';
+import { onClickOutside } from '@vueuse/core';
+import { DrawerEmit, DrawerProps, UseDrawerFn } from './drawer-types';
+import { lockScroll } from '../../shared/util/lock-scroll';
+
+export function useDrawer(props: DrawerProps, emit: DrawerEmit): UseDrawerFn {
+ const drawerRef = ref();
+ const drawerClasses = computed(() => ({
+ 'devui-drawer': true,
+ [`devui-drawer-${props.position}`]: true,
+ }));
+ const close = () => {
+ emit('update:modelValue', false);
+ emit('close');
+ };
+ let lockScrollCb: () => void;
+ const execClose = () => {
+ props.beforeClose ? props.beforeClose(close) : close();
+ };
+ const handleOverlayClick = () => {
+ props.closeOnClickOverlay && execClose();
+ };
+ const handleEscClose = (e: KeyboardEvent) => {
+ e.code === 'Escape' && execClose();
+ };
+
+ onClickOutside(drawerRef, execClose);
+
+ const removeBodyAdditions = () => {
+ lockScrollCb?.();
+ document.removeEventListener('keyup', handleEscClose);
+ };
+
+ watch(
+ () => props.modelValue,
+ (val) => {
+ if (val) {
+ emit('open');
+ props.lockScroll && (lockScrollCb = lockScroll());
+ props.escKeyCloseable && document.addEventListener('keyup', handleEscClose);
+ } else {
+ removeBodyAdditions();
+ }
+ }
+ );
+
+ onUnmounted(removeBodyAdditions);
+
+ return { drawerRef, drawerClasses, handleOverlayClick };
+}
diff --git a/packages/devui-vue/devui/dropdown/index.ts b/packages/devui-vue/devui/dropdown/index.ts
index 0075b1cbba..a99d4509c3 100644
--- a/packages/devui-vue/devui/dropdown/index.ts
+++ b/packages/devui-vue/devui/dropdown/index.ts
@@ -1,18 +1,16 @@
-import type { App } from 'vue'
+import type { App } from 'vue';
+import Dropdown from './src/dropdown';
+import DropdownMenu from './src/dropdown-menu';
+export * from './src/dropdown-menu-types';
-import Dropdown from './src/dropdown'
-
-Dropdown.install = function (app: App): void {
- app.component(Dropdown.name, Dropdown)
-}
-
-export { Dropdown }
+export { Dropdown, DropdownMenu };
export default {
title: 'Dropdown 下拉菜单',
category: '导航',
- status: '10%', // TODO: 组件若开发完成则填入"已完成",并删除该注释
+ status: '10%',
install(app: App): void {
- app.use(Dropdown as any)
- }
-}
+ app.component(Dropdown.name, Dropdown);
+ app.component(DropdownMenu.name, DropdownMenu);
+ },
+};
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts b/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts
deleted file mode 100644
index 38d67100b7..0000000000
--- a/packages/devui-vue/devui/dropdown/src/dropdown-directive.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const dDropdownDirective = {
-
-};
-
-export const dDropdownMenuDirective = {
-
-};
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts
new file mode 100644
index 0000000000..ac89d8711a
--- /dev/null
+++ b/packages/devui-vue/devui/dropdown/src/dropdown-menu-types.ts
@@ -0,0 +1,47 @@
+import type { PropType, ExtractPropTypes } from 'vue';
+
+export type CloseScopeArea = 'all' | 'blank' | 'none';
+export type Placement =
+ | 'top'
+ | 'right'
+ | 'bottom'
+ | 'left'
+ | 'top-start'
+ | 'top-end'
+ | 'right-start'
+ | 'right-end'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left-start'
+ | 'left-end';
+export type Alignment = 'start' | 'end';
+export type OffsetOptions = { mainAxis?: number; crossAxis?: number };
+
+export const dropdownMenuProps = {
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ origin: {
+ type: Object as PropType,
+ require: true,
+ },
+ position: {
+ type: Array as PropType>,
+ default: ['bottom'],
+ },
+ align: {
+ type: String as PropType | null,
+ default: null,
+ },
+ offset: {
+ type: [Number, Object] as PropType,
+ default: 4,
+ },
+ clickOutside: {
+ type: Function as PropType<() => boolean>,
+ default: (): boolean => true,
+ },
+};
+
+export type DropdownMenuProps = ExtractPropTypes;
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx b/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx
new file mode 100644
index 0000000000..34c6de7756
--- /dev/null
+++ b/packages/devui-vue/devui/dropdown/src/dropdown-menu.tsx
@@ -0,0 +1,48 @@
+import { defineComponent, ref, toRefs, Transition, Teleport, computed } from 'vue';
+import { onClickOutside } from '@vueuse/core';
+import { FlexibleOverlay } from '../../overlay';
+import { dropdownMenuProps, DropdownMenuProps } from './dropdown-menu-types';
+
+export default defineComponent({
+ name: 'DDropdownMenu',
+ inheritAttrs: false,
+ props: dropdownMenuProps,
+ emits: ['update:modelValue'],
+ setup(props: DropdownMenuProps, { slots, attrs, emit }) {
+ const { modelValue, origin, position, align, offset, clickOutside } = toRefs(props);
+ const dropdownMenuRef = ref(null);
+
+ onClickOutside(dropdownMenuRef, (value) => {
+ if (clickOutside.value?.() && !origin.value.contains(value.target)) {
+ emit('update:modelValue', false);
+ }
+ });
+
+ const currentPosition = ref('bottom');
+ const handlePositionChange = (pos) => {
+ currentPosition.value = pos.split('-')[0] === 'top' ? 'top' : 'bottom';
+ };
+ const styles = computed(() => ({
+ transformOrigin: currentPosition.value === 'top' ? '0% 100%' : '0% 0%',
+ }));
+
+ return () => (
+
+
+
+
+
+
+
+ );
+ },
+});
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts
index 706048dfb4..b0eca132ad 100644
--- a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts
+++ b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts
@@ -1,43 +1,65 @@
-import type { PropType, ExtractPropTypes, ComponentPublicInstance } from 'vue'
+import type { PropType, ExtractPropTypes, Ref } from 'vue';
export type TriggerType = 'click' | 'hover' | 'manually';
export type CloseScopeArea = 'all' | 'blank' | 'none';
+export type Placement =
+ | 'top'
+ | 'right'
+ | 'bottom'
+ | 'left'
+ | 'top-start'
+ | 'top-end'
+ | 'right-start'
+ | 'right-end'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left-start'
+ | 'left-end';
+export type Alignment = 'start' | 'end';
+export type OffsetOptions = { mainAxis?: number; crossAxis?: number };
-export const dropdownProps = {
- origin: {
- type: Object as PropType
- },
+type ReadonlyRef = Readonly[>;
- isOpen: {
- type: Boolean,
- default: false
- },
+export type EmitEvent = (event: 'toggle', result: boolean) => void;
- disabled: {
+export const dropdownProps = {
+ visible: {
type: Boolean,
- default: false
+ default: false,
},
-
trigger: {
type: String as PropType,
- default: 'click'
+ default: 'click',
},
-
closeScope: {
type: String as PropType,
- default: 'all'
+ default: 'all',
+ },
+ position: {
+ type: Array as PropType>,
+ default: ['bottom'],
+ },
+ align: {
+ type: String as PropType | null,
+ default: null,
+ },
+ offset: {
+ type: [Number, Object] as PropType,
+ default: 4,
},
-
closeOnMouseLeaveMenu: {
type: Boolean,
- default: false
+ default: false,
},
+};
- showAnimation: {
- type: Boolean,
- default: true
- }
-
-} as const
+export type DropdownProps = ExtractPropTypes;
-export type DropdownProps = ExtractPropTypes
+export interface UseDropdownProps {
+ id: string;
+ isOpen: Ref;
+ origin: ReadonlyRef;
+ dropdownRef: ReadonlyRef;
+ props: DropdownProps;
+ emit: EmitEvent;
+}
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.scss b/packages/devui-vue/devui/dropdown/src/dropdown.scss
index faa3cd509d..78c93a6141 100644
--- a/packages/devui-vue/devui/dropdown/src/dropdown.scss
+++ b/packages/devui-vue/devui/dropdown/src/dropdown.scss
@@ -1,4 +1,8 @@
-@import '../../styles-var/devui-var';
+@import '../../styles-var/devui-var.scss';
+
+.devui-dropdown-toggle {
+ display: inline-block;
+}
.devui-dropdown span {
&.icon-chevron-down,
@@ -8,6 +12,10 @@
}
}
+.devui-dropdown-menu-wrap .devui-dropdown-menu {
+ width: 100%;
+}
+
.devui-dropdown-animation span {
&.icon-chevron-down,
&.icon-select-arrow {
@@ -22,40 +30,80 @@
}
}
+.fade-in-top {
+ animation: fadeInTop 0.2s cubic-bezier(0.16, 0.75, 0.5, 1);
+}
+.fade-in-bottom {
+ animation: fadeInBottom 0.2s cubic-bezier(0.16, 0.75, 0.5, 1);
+}
-.devui-dropdown-fade {
- @mixin d-dropdown-fade-animation {
- animation-name: d-dropdown-fade;
- animation-duration: 0.3s;
+@keyframes fadeInTop {
+ from {
+ opacity: 0.8;
+ transform: scaleY(0.8) translateY(4px);
}
- @keyframes d-dropdown-fade {
- 0% {
- opacity: 0;
- transform: scaleY(0.9999) scaleY(0);
- }
- 100% {
- opacity: 1;
- transform: scaleY(0.8) scaleY(4px);
- }
+ to {
+ opacity: 1;
+ transform: scaleY(0.9999) translateY(0);
}
+}
- &-enter {
- opacity: 0;
+@keyframes fadeInBottom {
+ from {
+ opacity: 0.8;
+ transform: scaleY(0.8) translateY(-4px);
}
- &-enter-active {
- @include d-dropdown-fade-animation;
+ to {
+ opacity: 1;
+ transform: scaleY(0.9999) translateY(0);
}
+}
- &-leave {
- opacity: 1;
+.devui-dropdown-fade {
+ &-bottom {
+ &-enter-from,
+ &-leave-to {
+ opacity: 0.8;
+ transform: scaleY(0.8) translateY(-4px);
+ }
+
+ &-enter-to,
+ &-leave-from {
+ opacity: 1;
+ transform: scaleY(0.9999) translateY(0);
+ }
+
+ &-enter-active {
+ transition: transform 0.2s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.2s cubic-bezier(0.16, 0.75, 0.5, 1);
+ }
+
+ &-leave-active {
+ transition: transform 0.2s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.2s cubic-bezier(0.5, 0, 0.84, 0.25);
+ }
}
- &-leave-active {
- @include d-dropdown-fade-animation;
+ &-top {
+ &-enter-from,
+ &-leave-to {
+ opacity: 0.8;
+ transform: scaleY(0.8) translateY(4px);
+ }
+
+ &-enter-to,
+ &-leave-from {
+ opacity: 1;
+ transform: scaleY(0.9999) translateY(0);
+ }
- animation-direction: reverse;
+ &-enter-active {
+ transition: transform 0.2s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.2s cubic-bezier(0.16, 0.75, 0.5, 1);
+ }
+
+ &-leave-active {
+ transition: transform 0.2s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.2s cubic-bezier(0.5, 0, 0.84, 0.25);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/packages/devui-vue/devui/dropdown/src/dropdown.tsx b/packages/devui-vue/devui/dropdown/src/dropdown.tsx
index 5dd8d6ba64..53f2895fb7 100644
--- a/packages/devui-vue/devui/dropdown/src/dropdown.tsx
+++ b/packages/devui-vue/devui/dropdown/src/dropdown.tsx
@@ -1,66 +1,65 @@
-import { defineComponent, watch, ref, toRefs, Transition, computed } from 'vue'
-import { dropdownProps, DropdownProps } from './dropdown-types'
-import { useDropdown } from './use-dropdown';
-
+import { defineComponent, ref, toRefs, Transition, Teleport, computed } from 'vue';
+import { dropdownProps, DropdownProps } from './dropdown-types';
+import { useDropdown, useDropdownEvent } from './use-dropdown';
import { FlexibleOverlay } from '../../overlay';
+import './dropdown.scss';
-import './dropdown.scss'
+let dropdownId = 1;
export default defineComponent({
name: 'DDropdown',
+ inheritAttrs: false,
props: dropdownProps,
- emits: [],
- setup(props: DropdownProps, ctx) {
- const {
+ emits: ['toggle'],
+ setup(props: DropdownProps, { slots, attrs, emit }) {
+ const { visible, position, align, offset } = toRefs(props);
+ const origin = ref();
+ const dropdownRef = ref();
+ const id = `dropdown_${dropdownId++}`;
+ const isOpen = ref(false);
+ const currentPosition = ref('bottom');
+ const handlePositionChange = (pos) => {
+ currentPosition.value = pos.includes('top') || pos.includes('end') ? 'top' : 'bottom';
+ };
+ const styles = computed(() => ({
+ transformOrigin: currentPosition.value === 'top' ? '0% 100%' : '0% 0%',
+ }));
+ const classes = computed(() => ({
+ 'fade-in-bottom': isOpen.value && currentPosition.value === 'bottom',
+ 'fade-in-top': isOpen.value && currentPosition.value === 'top',
+ }));
+ useDropdownEvent({
+ id,
isOpen,
origin,
- trigger,
- closeScope,
- closeOnMouseLeaveMenu,
- } = toRefs(props);
-
- const visible = ref(false);
- watch(isOpen, (value) => {
- visible.value = value;
- }, { immediate: true });
-
- const position = {
- originX: 'center',
- originY: 'bottom',
- overlayX: 'center',
- overlayY: 'top'
- } as const;
-
- const { dropdownEl } = useDropdown({
- visible,
- origin,
- trigger,
- closeScope,
- closeOnMouseLeaveMenu,
- });
-
- const animatedVisible = computed(() => {
- return props.showAnimation ? visible.value : true;
+ dropdownRef,
+ props,
+ emit,
});
-
- return () => {
- // let vnodes = ctx.slots.default?.() ?? [];
- return (
- <>
-
-
- ]
- {ctx.slots.default?.()}
+ useDropdown(id, visible, isOpen, origin, dropdownRef, currentPosition, emit);
+ return () => (
+ <>
+
+ {slots.default?.()}
+
+
+
+
+
-
-
- >
- )
- };
- }
-})
+
+
+
+ >
+ );
+ },
+});
diff --git a/packages/devui-vue/devui/dropdown/src/use-dropdown.ts b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts
index 596a457931..0bae60f6f0 100644
--- a/packages/devui-vue/devui/dropdown/src/use-dropdown.ts
+++ b/packages/devui-vue/devui/dropdown/src/use-dropdown.ts
@@ -1,120 +1,157 @@
-import { Ref, ref, watch } from 'vue';
+import { watch, onMounted, onUnmounted, toRefs } from 'vue';
+import type { Ref } from 'vue';
import { getElement } from '../../shared/util/dom';
-import { CloseScopeArea, TriggerType } from './dropdown-types';
+import { UseDropdownProps, EmitEvent } from './dropdown-types';
-function subscribeEvent
(dom: Element | Document, type: string, callback: (event: E) => void) {
- dom?.addEventListener(type, callback as any);
- return () => {
- dom?.removeEventListener(type, callback as any);
- }
-}
+const dropdownMap = new Map();
-type ReadonlyRef = Readonly[>;
-
-interface UseDropdownProps {
- visible: Ref
- trigger: ReadonlyRef
- origin: ReadonlyRef
- closeScope: ReadonlyRef
- closeOnMouseLeaveMenu: ReadonlyRef
-}
-
-interface UseDropdownResult {
- dropdownEl: Ref
+function subscribeEvent(dom: Element | Document, type: string, callback: (event: any) => void) {
+ dom?.addEventListener(type, callback);
+ return () => {
+ dom?.removeEventListener(type, callback);
+ };
}
-export const useDropdown = ({
- visible,
- trigger,
- origin,
- closeScope,
- closeOnMouseLeaveMenu
-}: UseDropdownProps): UseDropdownResult => {
- const dropdownElRef = ref();
-
- const closeByScope = () => {
- if (closeScope.value === 'none') {
+export const useDropdownEvent = ({ id, isOpen, origin, dropdownRef, props, emit }: UseDropdownProps): void => {
+ let overlayEnter = false;
+ let originEnter = false;
+ const { trigger, closeScope, closeOnMouseLeaveMenu } = toRefs(props);
+ const toggle = (status: boolean) => {
+ isOpen.value = status;
+ emit('toggle', isOpen.value);
+ };
+ const handleLeave = async (elementType: 'origin' | 'dropdown', e?) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) {
return;
}
- visible.value = false;
- }
- watch(
- [trigger, origin, dropdownElRef],
- ([trigger, origin, dropdownEl], ov, onInvalidate) => {
- const originEl = getElement(origin);
- if (!originEl || !dropdownEl) {
- return;
- }
- const subscriptions = [
- subscribeEvent(dropdownEl, 'click', () => {
- if (closeScope.value === 'all') {
- visible.value = false;
+ if (e) {
+ [...dropdownMap.values()].reverse().forEach((item) => {
+ setTimeout(() => {
+ item.toggle?.();
+ }, 0);
+ });
+ }
+ toggle(false);
+ };
+ watch([trigger, origin, dropdownRef], ([triggerVal, originVal, dropdownEl], ov, onInvalidate) => {
+ const originEl = getElement(originVal);
+ const subscriptions = [];
+ setTimeout(() => {
+ subscriptions.push(
+ subscribeEvent(document, 'click', (e: Event) => {
+ const dropdownValues = [...dropdownMap.values()];
+ if (
+ !isOpen.value ||
+ closeScope.value === 'none' ||
+ (dropdownEl.contains(e.target) && closeScope.value === 'blank') ||
+ (dropdownValues.some((item) => item.toggleEl?.contains(e.target)) &&
+ dropdownValues.some((item) => item.menuEl?.contains(e.target)))
+ ) {
+ return;
+ }
+ [...dropdownMap.values()].reverse().forEach((item) => {
+ setTimeout(() => {
+ if (!item.toggleEl?.contains(e.target)) {
+ item.toggle?.();
+ }
+ }, 0);
+ });
+ overlayEnter = false;
+ })
+ );
+ }, 0);
+ if (triggerVal === 'click') {
+ subscriptions.push(
+ subscribeEvent(originEl, 'click', () => toggle(!isOpen.value)),
+ subscribeEvent(dropdownEl, 'mouseleave', (e: MouseEvent) => {
+ if (closeOnMouseLeaveMenu.value && !dropdownMap.get(id).child?.contains(e.relatedTarget)) {
+ handleLeave('dropdown', e);
}
+ })
+ );
+ } else if (triggerVal === 'hover') {
+ subscriptions.push(
+ subscribeEvent(originEl, 'mouseenter', () => {
+ originEnter = true;
+ toggle(true);
}),
- ];
-
- if (trigger === 'click') {
- // 点击触发
- subscriptions.push(
- subscribeEvent(originEl, 'click', () => visible.value = !visible.value),
- subscribeEvent(document, 'click', (e) => {
- if (!visible.value) {
- return;
- }
- const target = e.target as HTMLElement;
- const isContain = originEl.contains(target) || dropdownEl.contains(target);
- if (isContain) {
- return;
- }
- closeByScope();
- }),
- subscribeEvent(dropdownEl, 'mouseleave', () => {
- // 判断鼠标是否已经进入 origin
- if (closeOnMouseLeaveMenu.value) {
- visible.value = false;
- }
- })
- );
- } else if (trigger === 'hover') {
- // 鼠标悬浮触发
- let overlayEnter = false;
- let originEnter = false;
- const handleLeave = async (elementType: 'origin' | 'dropdown') => {
- // 由于关联元素和 dropdown 元素间有间距,
- // 悬浮时在两者之间移动可能会导致多次关闭打开,
- // 所以需要给关闭触发节流。
- await new Promise((resolve) => setTimeout(resolve, 50));
- if ((elementType === 'origin' && overlayEnter) || (elementType === 'dropdown' && originEnter)) {
+ subscribeEvent(originEl, 'mouseleave', () => {
+ originEnter = false;
+ handleLeave('origin');
+ }),
+ subscribeEvent(dropdownEl, 'mouseenter', () => {
+ overlayEnter = true;
+ isOpen.value = true;
+ }),
+ subscribeEvent(dropdownEl, 'mouseleave', (e: MouseEvent) => {
+ overlayEnter = false;
+ if (e.relatedTarget && (originEl?.contains(e.relatedTarget) || dropdownMap.get(id).child?.contains(e.relatedTarget))) {
return;
}
- closeByScope();
- };
- subscriptions.push(
- subscribeEvent(originEl, 'mouseenter', () => {
- originEnter = true;
- visible.value = true;
- }),
- subscribeEvent(originEl, 'mouseleave', () => {
- originEnter = false;
- // 判断鼠标是否已经进入 overlay
- if (!closeOnMouseLeaveMenu.value) {
- handleLeave('origin');
- }
- }),
- subscribeEvent(dropdownEl, 'mouseenter', () => {
- overlayEnter = true;
- visible.value = true;
- }),
- subscribeEvent(dropdownEl, 'mouseleave', () => {
- overlayEnter = false;
- // 判断鼠标是否已经进入 origin
- handleLeave('dropdown');
- })
- );
- }
- onInvalidate(() => subscriptions.forEach(v => v()));
+ handleLeave('dropdown', e);
+ })
+ );
}
- );
+ onInvalidate(() => subscriptions.forEach((v) => v()));
+ });
+};
- return { dropdownEl: dropdownElRef };
+export function useDropdown(
+ id: string,
+ visible: Ref,
+ isOpen: Ref,
+ origin: Ref,
+ dropdownRef: Ref,
+ popDirection: Ref,
+ emit: EmitEvent
+): void {
+ const calcPopDirection = (dropdownEl: HTMLElement) => {
+ const elementHeight = dropdownEl.offsetHeight;
+ const bottomDistance = window.innerHeight - origin.value.getBoundingClientRect().bottom;
+ const isBottomEnough = bottomDistance >= elementHeight;
+ if (!isBottomEnough) {
+ popDirection.value = 'top';
+ } else {
+ popDirection.value = 'bottom';
+ }
+ };
+
+ watch(
+ visible,
+ (newVal, oldVal) => {
+ if (oldVal === undefined) {
+ return;
+ }
+ isOpen.value = newVal;
+ emit('toggle', isOpen.value);
+ },
+ { immediate: true }
+ );
+ watch([isOpen, dropdownRef], ([isOpenVal, dropdownEl]) => {
+ if (isOpenVal) {
+ dropdownMap.set(id, {
+ ...dropdownMap.get(id),
+ menuEl: dropdownEl,
+ toggle: () => {
+ isOpen.value = false;
+ emit('toggle', isOpen.value);
+ },
+ });
+ for (const value of dropdownMap.values()) {
+ if (value.menuEl?.contains(origin.value)) {
+ value.child = dropdownEl;
+ }
+ }
+ }
+ if (dropdownEl) {
+ calcPopDirection(dropdownEl);
+ }
+ });
+ onMounted(() => {
+ dropdownMap.set(id, { toggleEl: origin.value });
+ });
+ onUnmounted(() => {
+ dropdownMap.delete(id);
+ });
}
diff --git a/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts b/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
index aa694a308b..2a754c3677 100644
--- a/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
+++ b/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
@@ -1,8 +1,287 @@
import { mount } from '@vue/test-utils';
+import { reactive, ref } from 'vue';
import { EditableSelect } from '../index';
+const createData = (len = 5) => {
+ return reactive(
+ Array.from({ length: len }).map((_, index) => {
+ return {
+ label: `label${index}`,
+ value: index
+ };
+ })
+ );
+};
describe('editable-select test', () => {
- it('editable-select init render', async () => {
- // todo
- })
-})
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ test('create', () => {
+ const wrapper = mount(EditableSelect);
+
+ expect(wrapper.find('.devui-editable-select').exists()).toBe(true);
+ });
+
+ test('should render correctly', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = '';
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+
+ const input = wrapper.find('input');
+
+ expect(wrapper.get('.devui-dropdown-item').isVisible()).toBe(false);
+
+ await input.trigger('click');
+
+ expect(wrapper.get('.devui-dropdown-item').isVisible()).toBe(true);
+ expect(wrapper.classes()).toContain('devui-select-open');
+
+ const options = wrapper.element.querySelectorAll('.devui-dropdown-item');
+
+ expect(options.length).toBe(5);
+ });
+
+ test('select on click ', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const options = wrapper.findAll('.devui-dropdown-item');
+
+ await options[0].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('label0');
+ });
+
+ test('disabled select', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``
+ });
+ expect(wrapper.find('input').element.disabled).toBe(true);
+ });
+
+ test('disabled option', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = reactive([
+ {
+ label: 'label0',
+ value: 0
+ },
+ {
+ label: 'label1',
+ value: 1,
+ disabled: true
+ },
+ {
+ label: 'label2',
+ value: 2,
+ disabled: false
+ }
+ ]);
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const options = wrapper.findAll('.devui-dropdown-item');
+
+ expect(options[1].classes()).toContain('disabled');
+
+ await options[1].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('');
+
+ await options[2].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('label2');
+ });
+
+ test('search', async () => {
+ const handleSearch = jest.fn();
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options,
+ handleSearch
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label');
+ await input.trigger('input');
+ expect(handleSearch).toBeCalled();
+ });
+
+ test('filter option', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label0');
+ await input.trigger('input');
+ expect(wrapper.findAll('.devui-dropdown-item').length).toBe(1);
+ });
+
+ test('custom filter options', async () => {
+ const filterOption = jest.fn();
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options,
+ filterOption
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label0');
+ await input.trigger('input');
+ expect(filterOption).toBeCalled();
+ });
+
+ test('render slot', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: `
+
+ ]
+ 第{{slotProps.index}}项: {{slotProps.item}}
+
+
+
+
+ {{slotProps}}
+
+
+ `,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ const options = wrapper.findAll('.devui-dropdown-item');
+ expect(options.length).toBe(5);
+ await input.setValue('aaa');
+ await input.trigger('input');
+ expect(wrapper.find('#noResultItemTemplate').exists()).toBe(true);
+ });
+
+ test('load more ', async () => {
+ const loadmore = jest.fn();
+ const makeScroll = async (dom: Element, name: 'scrollTop', offset: number) => {
+ const eventTarget = dom === document.documentElement ? window : dom;
+ dom[name] = offset;
+ const evt = new CustomEvent('scroll', {
+ detail: {
+ target: {
+ [name]: offset
+ }
+ }
+ });
+ eventTarget.dispatchEvent(evt);
+ };
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData(20);
+ return {
+ value,
+ options,
+ handleLoad: loadmore
+ };
+ }
+ });
+ const ul = wrapper.find('.devui-list-unstyled');
+ await makeScroll(ul.element, 'scrollTop', 300);
+ expect(loadmore).toBeCalled();
+ });
+ test('keyboard operations', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.trigger('click');
+ await input.trigger('keydown', { key: 'ArrowDown' });
+ await input.trigger('keydown', { key: 'ArrowDown' });
+ await input.trigger('keydown', { key: 'Enter' });
+ expect(input.element.value).toBe('label2');
+ });
+});
diff --git a/packages/devui-vue/devui/editable-select/index.ts b/packages/devui-vue/devui/editable-select/index.ts
index de0ccd55ba..3468c0868e 100644
--- a/packages/devui-vue/devui/editable-select/index.ts
+++ b/packages/devui-vue/devui/editable-select/index.ts
@@ -1,16 +1,17 @@
-import type { App } from 'vue'
-import EditableSelect from './src/editable-select'
+import type { App } from 'vue';
+import EditableSelect from './src/editable-select';
+
EditableSelect.install = function (app: App): void {
- app.component(EditableSelect.name, EditableSelect)
-}
+ app.component(EditableSelect.name, EditableSelect);
+};
-export { EditableSelect }
+export { EditableSelect };
export default {
title: 'EditableSelect 可输入下拉选择框',
category: '数据录入',
- status: '10%', // TODO: 组件若开发完成则填入"已完成",并删除该注释
+ status: '100%',
install(app: App): void {
- app.use(EditableSelect as any)
+ app.use(EditableSelect as any);
}
-}
+};
diff --git a/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx b/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx
deleted file mode 100644
index 7f1f029bad..0000000000
--- a/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { defineComponent, inject } from 'vue'
-import { OptionItem, selectDropdownProps } from '../editable-select-types'
-import { className } from '../utils'
-export default defineComponent({
- name: 'DSelectDropdown',
- props: selectDropdownProps,
- setup(props) {
- const select = inject('InjectionKey') as any
- const {
- props: selectProps,
- dropdownRef,
- visible,
- selectOptionClick,
- renderDefaultSlots,
- renderEmptySlots,
- selectedIndex,
- hoverIndex,
- loadMore
- } = select
- const { maxHeight } = selectProps
- return () => {
- const getLiCls = (item: OptionItem, index: number) => {
- const { disabledKey } = selectProps
- return className('devui-dropdown-item', {
- disabled: disabledKey ? !!item[disabledKey] : false,
- selected: selectedIndex.value === index,
- 'devui-dropdown-bg': hoverIndex.value === index
- })
- }
- return (
-
- )
- }
- }
-})
diff --git a/packages/devui-vue/devui/editable-select/src/composable/use-keyBoard-select.ts b/packages/devui-vue/devui/editable-select/src/composable/use-keyBoard-select.ts
deleted file mode 100644
index 3d9c59f96c..0000000000
--- a/packages/devui-vue/devui/editable-select/src/composable/use-keyBoard-select.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ComputedRef, nextTick, Ref } from 'vue'
-import { OptionItem } from '../editable-select-types'
-interface keyBoardSelectReturnType {
- handleKeydown: (e: KeyboardEvent) => void
-}
-
-export default function keyboardSelect(
- dropdownRef: Ref,
- visible: Ref,
- hoverIndex: Ref,
- selectedIndex: Ref,
- filteredOptions: ComputedRef,
- toggleMenu: () => void,
- selectOptionClick: (e: KeyboardEvent, item: OptionItem) => void
-): keyBoardSelectReturnType {
- const updateHoverIndex = (index: number) => {
- hoverIndex.value = index
- }
- const scrollToActive = (index: number) => {
- const dropdownVal = dropdownRef.value
- const li = dropdownVal.children[index]
-
- nextTick(() => {
- if (li.scrollIntoViewIfNeeded) {
- li.scrollIntoViewIfNeeded(false)
- } else {
- const containerInfo = dropdownVal.getBoundingClientRect()
- const elementInfo = li.getBoundingClientRect()
- if (elementInfo.bottom > containerInfo.bottom || elementInfo.top < containerInfo.top) {
- li.scrollIntoView(false)
- }
- }
- })
- }
- const onKeyboardSelect = (e: KeyboardEvent) => {
- const option = filteredOptions.value[hoverIndex.value]
- selectOptionClick(e, option)
- hoverIndex.value = selectedIndex.value
- }
- const handleKeydown = (e: KeyboardEvent) => {
- const keyCode = e.key || e.code
- let index = 0
- if (!visible.value) {
- toggleMenu()
- }
-
- if (keyCode === 'Backspace') {
- return
- }
-
- if (keyCode === 'ArrowUp') {
- index = hoverIndex.value - 1
- if (index < 0) {
- index = filteredOptions.value.length - 1
- }
- } else if (keyCode === 'ArrowDown') {
- index = hoverIndex.value + 1
- if (index > filteredOptions.value.length - 1) {
- index = 0
- }
- }
-
- if (keyCode === 'Enter') {
- return onKeyboardSelect(e)
- }
- updateHoverIndex(index)
- scrollToActive(index)
- }
-
- return {
- handleKeydown
- }
-}
diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts b/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts
new file mode 100644
index 0000000000..87c2e3514a
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts
@@ -0,0 +1,28 @@
+import { computed, Ref, ComputedRef } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+const getFilterFunc = () => (val: string, option: OptionObjectItem) =>
+ option.label.toLocaleLowerCase().indexOf(val.toLocaleLowerCase()) > -1;
+
+export const userFilterOptions: (
+ normalizeOptions: ComputedRef,
+ inputValue: Ref,
+ filteredOptions: boolean | ((val: string, option: OptionObjectItem) => boolean)
+) => ComputedRef = (normalizeOptions, inputValue, filterOption) =>
+ computed(() => {
+ const filteredOptions: OptionObjectItem[] = [];
+
+ if (!inputValue.value || filterOption === false) {
+ return normalizeOptions.value;
+ }
+
+ const filterFunc = typeof filterOption === 'function' ? filterOption : getFilterFunc();
+
+ normalizeOptions.value.forEach((option) => {
+ if (filterFunc(inputValue.value, option)) {
+ filteredOptions.push(option);
+ }
+ });
+
+ return filteredOptions;
+ });
diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-input.ts b/packages/devui-vue/devui/editable-select/src/composables/use-input.ts
new file mode 100644
index 0000000000..d553ad362a
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-input.ts
@@ -0,0 +1,22 @@
+import { SetupContext, Ref } from 'vue';
+interface userInputReturnType {
+ handleInput: (event: Event) => void
+}
+export const useInput: (inputValue: Ref, ctx: SetupContext) => userInputReturnType = (
+ inputValue,
+ ctx
+) => {
+ const onInputChange = (value: string) => {
+ ctx.emit('search', value);
+ };
+
+ const handleInput = (event: Event) => {
+ const value = (event.target as HTMLInputElement).value;
+ inputValue.value = value;
+ onInputChange(value);
+ };
+
+ return {
+ handleInput
+ };
+};
diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts b/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts
new file mode 100644
index 0000000000..45e6760d36
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts
@@ -0,0 +1,110 @@
+import { ComputedRef, nextTick, Ref } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+interface useKeyboardSelectReturnType {
+ handleKeydown: (event: KeyboardEvent) => void
+}
+export const useKeyboardSelect: (
+ dropdownRef: Ref,
+ disabled: string,
+ visible: Ref,
+ hoverIndex: Ref,
+ selectedIndex: Ref,
+ options: ComputedRef,
+ toggleMenu: () => void,
+ closeMenu: () => void,
+ handleClick: (options: OptionObjectItem) => void
+) => useKeyboardSelectReturnType = (
+ dropdownRef,
+ disabled,
+ visible,
+ hoverIndex,
+ selectedIndex,
+ options,
+ toggleMenu,
+ closeMenu,
+ handleClick
+) => {
+ const updateHoveringIndex = (index: number) => {
+ hoverIndex.value = index;
+ };
+ const scrollToItem = (index: number) => {
+ const ul = dropdownRef.value;
+ const li = ul.children[index];
+ nextTick(() => {
+ if (li.scrollIntoViewIfNeeded) {
+ li.scrollIntoViewIfNeeded(false);
+ } else {
+ const containerInfo = ul.getBoundingClientRect();
+ const elementInfo = li.getBoundingClientRect();
+ if (elementInfo.bottom > containerInfo.bottom || elementInfo.top < containerInfo.top) {
+ li.scrollIntoView(false);
+ }
+ }
+ });
+ };
+
+ const onKeyboardNavigation = (direction: string, newIndex?: number) => {
+ if (!newIndex) {
+ newIndex = hoverIndex.value;
+ }
+ if (!['ArrowDown', 'ArrowUp'].includes(direction)) return;
+ if (direction === 'ArrowUp') {
+ if (newIndex === 0) {
+ newIndex = options.value.length - 1;
+ scrollToItem(newIndex);
+ updateHoveringIndex(newIndex);
+ return;
+ }
+ newIndex = newIndex - 1;
+ } else if (direction === 'ArrowDown') {
+ if (newIndex === options.value.length - 1) {
+ newIndex = 0;
+ scrollToItem(newIndex);
+ updateHoveringIndex(newIndex);
+ return;
+ }
+ newIndex = newIndex + 1;
+ }
+
+ const option = options.value[newIndex];
+ if (option[disabled]) {
+ return onKeyboardNavigation(direction, newIndex);
+ }
+ scrollToItem(newIndex);
+ updateHoveringIndex(newIndex);
+ };
+
+ const handleKeydown = (event: KeyboardEvent) => {
+ const keyCode = event.key || event.code;
+
+ if (options.value.length === 0) return;
+
+ if (!visible.value) {
+ return toggleMenu();
+ }
+
+ const onKeydownEnter = () => {
+ handleClick(options.value[hoverIndex.value]);
+ closeMenu();
+ };
+
+ const onKeydownEsc = () => {
+ closeMenu();
+ };
+
+ switch (keyCode) {
+ case 'Enter':
+ onKeydownEnter();
+ break;
+ case 'Escape':
+ onKeydownEsc();
+ break;
+ default:
+ onKeyboardNavigation(keyCode);
+ }
+ };
+ return {
+ handleKeydown
+ };
+};
diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts b/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts
new file mode 100644
index 0000000000..2580eaf9ae
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts
@@ -0,0 +1,25 @@
+import { Ref } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+interface useLazyLoadReturenType {
+ loadMore: () => void
+}
+export const useLazyLoad: (
+ dropdownRef: Ref,
+ inputValue: Ref,
+ filterOtion: boolean | ((val: string, option: OptionObjectItem) => boolean),
+ load: (val: string) => void
+) => useLazyLoadReturenType = (dropdownRef, inputValue, filterOtion, load) => {
+ const loadMore = () => {
+ if (filterOtion !== false) return;
+
+ if (
+ dropdownRef.value.clientHeight + dropdownRef.value.scrollTop >=
+ dropdownRef.value.scrollHeight
+ ) {
+ load(inputValue.value);
+ }
+ };
+
+ return { loadMore };
+};
diff --git a/packages/devui-vue/devui/editable-select/src/editable-select-type.ts b/packages/devui-vue/devui/editable-select/src/editable-select-type.ts
new file mode 100644
index 0000000000..87a4508326
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/editable-select-type.ts
@@ -0,0 +1,7 @@
+export type OptionsType = Array;
+export type OptionType = string | number | OptionObjectItem;
+export interface OptionObjectItem {
+ label: string
+ value: string | number
+ [key: string]: any
+}
diff --git a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts
index 2fb55f9525..8ee593c50d 100644
--- a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts
+++ b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts
@@ -1,74 +1,47 @@
-import type { PropType, ExtractPropTypes } from 'vue'
-type HorizontalConnectionPos = 'left' | 'center' | 'right';
-type VerticalConnectionPos = 'top' | 'center' | 'bottom';
-
-export interface ConnectionPosition {
- originX: HorizontalConnectionPos
- originY: VerticalConnectionPos
- overlayX: HorizontalConnectionPos
- overlayY: VerticalConnectionPos
-}
-export interface OptionItem {
- name: string
- [key: string]: any
-}
-export type Options = Array
+import type { PropType, ExtractPropTypes } from 'vue';
+import { OptionObjectItem, OptionsType } from './editable-select-type';
export const editableSelectProps = {
+ /* test: {
+ type: Object as PropType<{ xxx: xxx }>
+ } */
appendToBody: {
- type: Boolean,
- default: false
- },
- modelValue: {
- type: [String, Number] as PropType
+ type: Boolean
},
options: {
- type: Array as PropType,
+ type: Array as PropType,
default: () => []
},
- width: {
- type: Number,
- default: 450
- },
- maxHeight: {
- type: Number
- },
disabled: {
- type: Boolean,
- default: false
- },
- disabledKey: {
- type: String,
- },
- remote: {
- type: Boolean,
- default: false
+ type: Boolean
},
loading: {
type: Boolean
},
- enableLazyLoad: {
- type: Boolean,
- default: false
+ optionDisabledKey: {
+ type: String,
+ default: ''
+ },
+ placeholder: {
+ type: String,
+ default: 'Search'
},
- remoteMethod: {
- type: Function as PropType<(inputValue: string) => Array>
+ modelValue: {
+ type: String
+ },
+ width: {
+ type: Number
},
- filterMethod: {
- type: Function as PropType<(inputValue: string) => Array>
+ maxHeight: {
+ type: Number
},
- searchFn: {
- type: Function as PropType<(term: string) => Array>,
+ filterOption: {
+ type: [Function, Boolean] as PropType<
+ boolean | ((input: string, option: OptionObjectItem) => boolean)
+ >
},
loadMore: {
- type: Function as PropType<() => Array>
+ type: Function as PropType<(val: string) => void>
}
-} as const
+} as const;
-export const selectDropdownProps = {
- options: {
- type: Array as PropType,
- default: () => []
- }
-} as const
-export type EditableSelectProps = ExtractPropTypes
-export type SelectDropdownProps = ExtractPropTypes
\ No newline at end of file
+export type EditableSelectProps = ExtractPropTypes;
diff --git a/packages/devui-vue/devui/editable-select/src/editable-select.scss b/packages/devui-vue/devui/editable-select/src/editable-select.scss
index ae3919734b..ae067b20c3 100644
--- a/packages/devui-vue/devui/editable-select/src/editable-select.scss
+++ b/packages/devui-vue/devui/editable-select/src/editable-select.scss
@@ -1,61 +1,37 @@
@import '../../style/theme/color';
@import '../../style/core/animation';
-.devui-editable-select {
- .devui-form-group {
- input::-ms-clear {
- display: none;
- }
-
- ul.devui-list-unstyled {
- margin: 0;
- overflow-y: auto;
- padding: 0;
- }
-
- .devui-dropdown-bg {
- background: $devui-list-item-hover-bg;
- }
-
- .devui-popup-tips {
- color: $devui-text-weak;
- padding: 4px 12px;
- }
- .devui-form-control {
- outline: none;
- padding-right: 24px;
- }
+.devui-editable-select {
+ .devui-select-chevron-icon {
+ display: inline-flex;
+ vertical-align: middle;
+ transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth;
}
- .devui-select-open {
- .devui-select-chevron-icon {
- transform: rotate(180deg);
-
- svg path {
- fill: $devui-text-weak;
- }
- }
+ input::-ms-clear {
+ display: none;
}
- .devui-form-control-feedback {
- .devui-select-chevron-icon {
- display: inline-flex;
- vertical-align: middle;
- transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth;
- }
+ .devui-no-data-tip {
+ user-select: none;
+ cursor: not-allowed;
}
- .devui-has-feedback > .devui-form-control-feedback {
- line-height: 26px;
+ .devui-form-control {
+ outline: none;
+ padding-right: 24px;
}
- .devui-dropdown-bg.devui-dropdown-bg {
- background-color: inherit;
- }
- // 下拉部分
.devui-dropdown-menu {
width: 100%;
display: block;
+ // TODO: 全局样式被污染,暂时只能这么写
+ top: auto !important;
+ left: auto !important;
+ }
+
+ .devui-dropdown-menu-cdk {
+ position: static;
}
.devui-dropdown-item {
@@ -101,7 +77,8 @@
background-color: $devui-unavailable;
}
}
- // 选项disabled
+
+ /* 选项disabled */
.devui-dropdown-item.disabled,
.devui-dropdown-item.disabled:hover {
cursor: not-allowed;
@@ -124,11 +101,32 @@
}
}
-.devui-dropdown {
+.devui-editable-select.devui-select-open {
+ .devui-select-chevron-icon {
+ transform: rotate(180deg);
+ }
+ .devui-select-chevron-icon svg path {
+ fill: $devui-text-weak;
+ }
+}
+.devui-editable-select.devui-form-group.devui-has-feedback > .devui-form-control-feedback {
+ line-height: 26px;
+}
+.devui-editable-select-dropdown {
.devui-dropdown-menu {
width: 100%;
display: block;
+ width: 100%;
+ display: block;
+ // TODO: 全局样式被污染,暂时只能这么写
+ top: auto !important;
+ left: auto !important;
+ }
+
+ .devui-dropdown-menu-cdk {
+ position: static;
}
+
.devui-dropdown-item {
cursor: pointer;
display: block;
@@ -172,7 +170,8 @@
background-color: $devui-unavailable;
}
}
- // 选项disabled
+
+ /* 选项disabled */
.devui-dropdown-item.disabled,
.devui-dropdown-item.disabled:hover {
cursor: not-allowed;
@@ -190,7 +189,7 @@
}
.devui-popup-tips {
- color: $devui-text-weak;
+ color: $devui-text-weak; // TODO: Color-Question
padding: 4px 12px;
}
}
diff --git a/packages/devui-vue/devui/editable-select/src/editable-select.tsx b/packages/devui-vue/devui/editable-select/src/editable-select.tsx
index b137d25b9c..b9dec3c05e 100644
--- a/packages/devui-vue/devui/editable-select/src/editable-select.tsx
+++ b/packages/devui-vue/devui/editable-select/src/editable-select.tsx
@@ -1,238 +1,232 @@
import {
defineComponent,
- Transition,
- ref,
+ withModifiers,
computed,
+ ref,
+ Transition,
+ SetupContext,
reactive,
- toRefs,
- provide,
- renderSlot
-} from 'vue'
-import {
- OptionItem,
- editableSelectProps,
- EditableSelectProps,
- ConnectionPosition
-} from './editable-select-types'
-import SelectDropdown from './components/dropdown'
-import './editable-select.scss'
-import ClickOutside from '../../shared/devui-directive/clickoutside'
-import { debounce } from 'lodash'
-import { className } from './utils'
-import keyboardSelect from './composable/use-keyboard-select'
+ watch
+} from 'vue';
+import { editableSelectProps, EditableSelectProps } from './editable-select-types';
+import clickOutside from '../../shared/devui-directive/clickoutside';
+import { className } from '../src/utils/index';
+import './editable-select.scss';
+import { OptionObjectItem } from './editable-select-type';
+import { userFilterOptions } from './composables/use-filter-options';
+import { useInput } from './composables/use-input';
+import { useLazyLoad } from './composables/use-lazy-load';
+import { useKeyboardSelect } from './composables/use-keyboard-select';
export default defineComponent({
name: 'DEditableSelect',
- directives: { ClickOutside },
+ directives: {
+ clickOutside
+ },
props: editableSelectProps,
- emits: ['update:modelValue'],
- setup(props: EditableSelectProps, ctx) {
- const renderDropdown = (condition: boolean, type: number) => {
- if (!condition && type === 0) {
- return (
-
-
-
- )
- } else if (condition && type === 1) {
+ emits: ['update:modelValue', 'search', 'loadMore'],
+ setup(props: EditableSelectProps, ctx: SetupContext) {
+ const getItemCls = (option: OptionObjectItem, index: number) => {
+ const { optionDisabledKey: disabledKey } = props;
+ return className('devui-dropdown-item', {
+ disabled: disabledKey ? !!option[disabledKey] : false,
+ selected: index === selectIndex.value,
+ 'devui-dropdown-bg': index === hoverIndex.value
+ });
+ };
+ // 渲染下拉列表,根据appendToBody属性判断是否渲染在body下
+ const renderDropdown = () => {
+ if (props.appendToBody) {
return (
-
+
- )
+ );
+ } else {
+ return (
+
+
+
+ );
}
- }
-
- const renderDefaultSlots = (item) => {
- return ctx.slots.default ? renderSlot(ctx.slots, 'default', { item }) : item.name
- }
+ };
+ //Ref
+ const dopdownRef = ref();
+ const origin = ref();
- const renderEmptySlots = () => {
- return ctx.slots.empty ? renderSlot(ctx.slots, 'empty') : emptyText.value
- }
-
- const origin = ref()
- const dropdownRef = ref()
- const visible = ref(false)
- const inputValue = ref('')
- const selectedIndex = ref(0)
- const hoverIndex = ref(0)
- const query = ref(props.modelValue)
- const position = reactive({
+ const position = reactive({
originX: 'left',
originY: 'bottom',
overlayX: 'left',
overlayY: 'top'
- })
- const wait = computed(() => (props.remote ? 300 : 0))
-
- const emptyText = computed(() => {
- const options = filteredOptions.value
- if (!props.remote && options.length === 0) {
- return '没有相关记录'
- }
- if (options.length === 0) {
- return '没有数据'
- }
- return null
- })
+ });
+ const visible = ref(false);
+ const inputValue = ref(props.modelValue);
+ const hoverIndex = ref(0);
+ const selectIndex = ref(0);
+ //标准化options,统一处理成[{}]的形式
const normalizeOptions = computed(() => {
- let options: OptionItem
- const { disabledKey } = props
- disabledKey ? disabledKey : 'disabled'
- return props.options.map((item) => {
- if (typeof item !== 'object') {
- options = {
- name: item
- }
- return options
+ return props.options.map((option) => {
+ if (typeof option === 'object') {
+ return {
+ label: option.label ? option.label : option.value,
+ value: option.value,
+ ...option
+ };
}
- return item
- })
- })
+ return {
+ label: option + '',
+ value: option
+ };
+ });
+ });
+ //非远程搜索的情况下对数组进行过滤
+ const filteredOptions = userFilterOptions(normalizeOptions, inputValue, props.filterOption);
- const filteredOptions = computed(() => {
- const isValidOption = (o: OptionItem) => {
- const query = inputValue.value
- const containsQueryString = query
- ? o.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) >= 0
- : true
- return containsQueryString
+ const emptyText = computed(() => {
+ let text: string;
+ if (props.filterOption !== false && !filteredOptions.value.length) {
+ text = '找不到相关记录';
+ } else if (props.filterOption === false && !filteredOptions.value.length) {
+ text = '没有数据';
}
- return normalizeOptions.value
- .map((item) => {
- if (props.remote || isValidOption(item)) {
- return item
- }
- return null
- })
- .filter((item) => item !== null)
- })
- const findIndex = (o: OptionItem) => {
- return normalizeOptions.value.findIndex((item) => {
- return item.name === o.name
- })
- }
-
- const handleClose = () => {
- visible.value = false
- }
+ return ctx.slots.noResultItemTemplate ? ctx.slots.noResultItemTemplate() : text;
+ });
+ //下拉列表显影切换
const toggleMenu = () => {
- if (!props.disabled) {
- visible.value = !visible.value
- }
- }
-
- const onInputChange = (val: string) => {
- if (props.filterMethod) {
- props.filterMethod(val)
- } else if (props.remote) {
- props.remoteMethod(val)
- }
- }
+ visible.value = !visible.value;
+ };
- const debouncedOnInputChange = debounce(onInputChange, wait.value)
+ const closeMenu = () => {
+ visible.value = false;
+ };
+ // 懒加载
+ const { loadMore } = useLazyLoad(dopdownRef, inputValue, props.filterOption, props.loadMore);
- const handleInput = (event) => {
- const value = event.target.value
- inputValue.value = value
- query.value = value
- if (props.remote) {
- debouncedOnInputChange(value)
- } else {
- onInputChange(value)
- }
- }
+ //输入框变化后的逻辑
+ const { handleInput } = useInput(inputValue, ctx);
- const selectOptionClick = (e, item: OptionItem) => {
- const { disabledKey } = props
- if (disabledKey && item[disabledKey]) {
- e.stopPropagation()
- } else {
- query.value = item.name
- selectedIndex.value = findIndex(item)
- inputValue.value = ''
- ctx.emit('update:modelValue', item.name)
- visible.value = false
- }
- }
-
- const loadMore = () => {
- if (!props.enableLazyLoad) return
- const dropdownVal = dropdownRef.value
- if (dropdownVal.clientHeight + dropdownVal.scrollTop >= dropdownVal.scrollHeight) {
- props.loadMore()
- }
- }
- const { handleKeydown } = keyboardSelect(
- dropdownRef,
+ const handleClick = (option: OptionObjectItem) => {
+ const { optionDisabledKey: disabledKey } = props;
+ if (disabledKey && !!option[disabledKey]) return;
+ ctx.emit('update:modelValue', option.label);
+ closeMenu();
+ };
+ // 键盘选择
+ const { handleKeydown } = useKeyboardSelect(
+ dopdownRef,
+ props.optionDisabledKey,
visible,
hoverIndex,
- selectedIndex,
+ selectIndex,
filteredOptions,
toggleMenu,
- selectOptionClick
- )
+ closeMenu,
+ handleClick
+ );
- provide('InjectionKey', {
- dropdownRef,
- props: reactive({
- ...toRefs(props)
- }),
- visible,
- emptyText,
- selectedIndex,
- hoverIndex,
- loadMore,
- selectOptionClick,
- renderDefaultSlots,
- renderEmptySlots
- })
+ watch(
+ () => props.modelValue,
+ (newVal) => {
+ inputValue.value = newVal;
+ }
+ );
return () => {
const selectCls = className('devui-editable-select devui-form-group devui-has-feedback', {
- 'devui-select-open': visible.value
- })
- const inputCls = className(
- 'devui-form-control devui-dropdown-origin devui-dropdown-origin-open',
- {
- disabled: props.disabled
- }
- )
-
+ 'devui-select-open': visible.value === true
+ });
return (
- <>
-
+ );
+ };
}
-})
+});
diff --git a/packages/devui-vue/devui/editable-select/src/utils/index.ts b/packages/devui-vue/devui/editable-select/src/utils/index.ts
index 91a72c870f..8b81777a72 100644
--- a/packages/devui-vue/devui/editable-select/src/utils/index.ts
+++ b/packages/devui-vue/devui/editable-select/src/utils/index.ts
@@ -1,31 +1,19 @@
-import { VNode } from 'vue';
-
/**
* 动态获取class字符串
* @param classStr 是一个字符串,固定的class名
* @param classOpt 是一个对象,key表示class名,value为布尔值,true则添加,否则不添加
* @returns 最终的class字符串
*/
-export function className(
+ export function className(
classStr: string,
classOpt?: { [key: string]: boolean; }
-): string {
+ ): string {
let classname = classStr;
if (typeof classOpt === 'object') {
- Object.keys(classOpt).forEach((key) => {
- classOpt[key] && (classname += ` ${key}`);
- });
+ Object.keys(classOpt).forEach((key) => {
+ classOpt[key] && (classname += ` ${key}`);
+ });
}
-
+
return classname;
-}
-/**
- *
- * @param condition 渲染条件
- * @param node1 待渲染的组件
- * @param node2
- * @returns 最终被渲染的组件
- */
-export function renderCondition(condition: unknown, node1: VNode, node2?: VNode): VNode {
- return !!condition ? node1 : node2;
-}
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts b/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts
index 0fd265013f..89aa54e664 100644
--- a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts
+++ b/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts
@@ -1,6 +1,6 @@
import AsyncValidator, { RuleItem } from 'async-validator';
import { VNode, DirectiveBinding } from 'vue';
-import { debounce } from 'lodash-es';
+import { debounce } from 'lodash';
import { EventBus, isObject, hasKey } from '../util';
import './style.scss';
@@ -14,6 +14,7 @@ interface ValidateFnParam {
messageShowType: MessageShowType
dfcUID: string
popPosition: PopPosition | Array
+ updateOn?: UpdateOn
}
interface CustomValidatorRuleObject {
@@ -46,6 +47,7 @@ export interface ShowPopoverErrorMessageEventData {
message?: string
uid?: string,
popPosition?: PopPosition
+ [prop : string]: any
}
type MessageShowType = 'popover' | 'text' | 'none' | 'toast';
@@ -199,18 +201,28 @@ function getKeyValueOfObjectList(obj): {key: string; value: any;}[] {
function handleErrorStrategy(el: HTMLElement): void {
const classList: Array = [...el.classList];
- classList.push('d-validate-rules-error-pristine');
+ classList.push('devui-validate-rules-error-pristine');
el.setAttribute('class', classList.join(' '));
}
function handleErrorStrategyPass(el: HTMLElement): void {
const classList: Array = [...el.classList];
- const index = classList.indexOf('d-validate-rules-error-pristine');
+ const index = classList.indexOf('devui-validate-rules-error-pristine');
index !== -1 && classList.splice(index, 1);
el.setAttribute('class', classList.join(' '));
}
-function handleValidateError({el, tipEl, message, isFormTag, messageShowType, dfcUID, popPosition = 'right-bottom'}: Partial): void {
+function getFormControlUID(el: HTMLElement): string {
+ if(el.tagName.toLocaleLowerCase() === 'body') return '';
+ let uid = ''
+ if(el.parentElement.id.startsWith('dfc-')) {
+ return el.parentElement.id;
+ }else {
+ uid = getFormControlUID(el.parentElement);
+ }
+}
+
+function handleValidateError({el, tipEl, message = '', isFormTag, messageShowType, dfcUID, popPosition = 'right-bottom', updateOn}: Partial): void {
// 如果该指令用在form标签上,这里做特殊处理
if(isFormTag && messageShowType === MessageShowTypeEnum.toast) {
// todo:待替换为toast
@@ -218,15 +230,19 @@ function handleValidateError({el, tipEl, message, isFormTag, messageShowType, df
return;
}
+ if(!dfcUID) {
+ dfcUID = getFormControlUID(el);
+ }
+
// messageShowType为popover时,设置popover
if(MessageShowTypeEnum.popover === messageShowType) {
- EventBus.emit("showPopoverErrorMessage", {showPopover: true, message, uid: dfcUID, popPosition} as ShowPopoverErrorMessageEventData);
+ EventBus.emit('showPopoverErrorMessage', {showPopover: true, message, uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData);
return;
}
tipEl.innerText = '' + message;
tipEl.style.display = 'inline-flex';
- tipEl.setAttribute('class', 'd-validate-tip');
+ tipEl.setAttribute('class', 'devui-validate-tip');
handleErrorStrategy(el);
}
@@ -250,7 +266,7 @@ function getFormName(binding: DirectiveBinding): string {
}
// 校验处理函数
-function validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, dfcUID, popPosition}: Partial) {
+function validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, dfcUID, popPosition, updateOn}: Partial) {
validator.validate({modelName: modelValue}).then(() => {
handleValidatePass(el, tipEl);
}).catch((err) => {
@@ -265,7 +281,7 @@ function validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowTyp
msg = errors[0].message;
}
- handleValidateError({el, tipEl, message: msg, isFormTag, messageShowType, dfcUID, popPosition});
+ handleValidateError({el, tipEl, message: msg, isFormTag, messageShowType, dfcUID, popPosition, updateOn});
})
}
@@ -295,7 +311,7 @@ export default {
if(refName) {
// 判断d-form是否传递了messageShowType属性
- messageShowType = binding.instance[refName]["messageShowType"] ?? "popover";
+ messageShowType = binding.instance[refName]['messageShowType'] ?? 'popover';
}
// errorStrategy可配置在options对象中
@@ -394,14 +410,21 @@ export default {
const htmlEventValidateHandler = (e) => {
const modelValue = e.target.value;
if(messageShowType === MessageShowTypeEnum.popover) {
- EventBus.emit("showPopoverErrorMessage", {showPopover: false, message: "", uid: dfcUID, popPosition} as ShowPopoverErrorMessageEventData);
+ EventBus.emit('showPopoverErrorMessage', {showPopover: false, message: '', uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData);
}
- validateFn({validator, modelValue, el, tipEl, isFormTag: false, messageShowType, dfcUID, popPosition});
+ validateFn({validator, modelValue, el, tipEl, isFormTag: false, messageShowType, dfcUID, popPosition, updateOn});
}
// 监听事件验证
vnode.children[0].el.addEventListener(updateOn, htmlEventValidateHandler);
+ // 如果校验时机为change,则在focus时关闭popover
+ if(messageShowType === MessageShowTypeEnum.popover && updateOn === UpdateOnEnum.change) {
+ vnode.children[0].el.addEventListener('focus', () => {
+ EventBus.emit('showPopoverErrorMessage', {showPopover: false, uid: dfcUID, updateOn} as ShowPopoverErrorMessageEventData);
+ });
+ }
+
// 设置errorStrategy
if(errorStrategy === ErrorStrategyEnum.pristine) {
handleErrorStrategy(el);
@@ -415,7 +438,7 @@ export default {
const modelValue = isFormTag ? '' : vnode.children[0].el.value;
// 进行提交验证
- validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType});
+ validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, updateOn: 'submit'});
});
}
diff --git a/packages/devui-vue/devui/form/src/directive/d-validate.ts b/packages/devui-vue/devui/form/src/directive/d-validate.ts
new file mode 100644
index 0000000000..76d5e03e96
--- /dev/null
+++ b/packages/devui-vue/devui/form/src/directive/d-validate.ts
@@ -0,0 +1,259 @@
+import { VNode, DirectiveBinding, h, render, nextTick } from 'vue';
+import { debounce } from 'lodash-es';
+import { EventBus, transformCamelToDash } from '../util';
+import useValidate from '../use-validate';
+import dPopover from '../../../popover/src/popover';
+import {DFormValidateSubmitData, positionType} from '../form-types';
+import './style.scss';
+
+interface BindingValueRules {
+ [prop:string]: unknown
+}
+
+interface BindingValue {
+ prop: string
+ modelName?: string
+ rules: BindingValueRules
+ validators?: any
+ asyncValidators?: any
+ errorStrategy?: 'pristine' | 'dirty'
+ updateOn: 'change' | 'input' | 'submit'
+ asyncDebounceTime?: number | string
+ messageShowType?: 'popover' | 'text' | 'none'
+ popPosition: string | string[]
+ messageChange?: (msg, { errors, fields }) => {}
+ [prop: string]: any
+}
+
+const getTargetElement = (el: HTMLElement, targetTag: string) => {
+ if (!el) return;
+ let tempEl:HTMLElement = el;
+ while(tempEl?.tagName && tempEl.tagName.toLocaleLowerCase() !== 'body') {
+ if(tempEl.tagName.toLocaleLowerCase() === targetTag) {
+ return tempEl;
+ }
+ tempEl = tempEl.parentElement;
+ }
+}
+
+export default {
+ mounted(el: HTMLElement, binding: DirectiveBinding): void {
+ let { prop, rules, validators, asyncValidators, errorStrategy, updateOn = 'input', asyncDebounceTime = 300, messageShowType = 'popover', messageChange, popPosition = ['right', 'bottom'] }: BindingValue = binding.value;
+ const {instance, arg: modelName} = binding;
+
+ const instanceRef = instance[Object.keys(instance.$refs)[0]];
+ if(instanceRef && instanceRef?.messageShowType) {
+ messageShowType = instanceRef.messageShowType;
+ }
+ const hasModelName = !!modelName;
+
+ const objToStyleString = (obj: any = {}) => {
+ let style = '';
+ for (const key in obj) {
+ style += `${transformCamelToDash(key)}: ${obj[key]};`
+ }
+ return style;
+ }
+
+ const renderPopover = (msg, visible = true) => {
+ if(messageShowType !== 'popover') return;
+ el.style.position = 'relative';
+ const popoverPosition = () => {
+ return Array.isArray(popPosition) ? popPosition.join('-') : popPosition;
+ }
+
+ const popover = h(dPopover, {
+ visible: visible,
+ controlled: updateOn !== 'change',
+ content: msg,
+ popType: 'error',
+ position: popoverPosition() as positionType,
+ });
+
+ // 这里使用比较hack的方法控制popover显隐,因为点击popover外部元素隐藏popover之后,再重新传入visible不起作用了,popover不会重新渲染了
+ nextTick(() => {
+ if(visible) {
+ addElClass(popover.el as HTMLElement, 'devui-popover-isVisible')
+ }else {
+ removeElClass(popover.el as HTMLElement, 'devui-popover-isVisible')
+ }
+ })
+
+ const popoverWrapperStyle = () => {
+ let rect = el.getBoundingClientRect();
+ let style: any = {
+ position: 'absolute',
+ height: 0,
+ top: (rect.height / 2) + 'px',
+ right: 0,
+ }
+
+ let p = popoverPosition();
+ if(popPosition === 'bottom' || popPosition === 'top') {
+ style.left = '50%';
+ }
+ if(popPosition === 'left' || popPosition === 'right') {
+ style.top = 0;
+ }
+ if(p.includes('top')) {
+ style.top = -(rect.height / 2) + 'px';
+ }
+ if(p.endsWith('-bottom')) {
+ style.top = (rect.height / 2) + 'px';
+ }
+ if(p.includes('left')) {
+ style.left = 0;
+ }
+ if(p.includes('right')) {
+ delete style.left;
+ style.right = 0;
+ }
+
+ if(p.startsWith('bottom')) {
+ delete style.top;
+ style.bottom = 0;
+ }
+ if(p.startsWith('top')) {
+ delete style.bottom;
+ }
+
+ return objToStyleString(style);
+ };
+
+ const vn = h('div', {
+ style: popoverWrapperStyle()
+ }, popover)
+ render(vn, el);
+ }
+
+ const tipEl = document.createElement('div');
+ if(messageShowType === 'text') {
+ el.parentNode.appendChild(tipEl);
+ }
+
+ const renderTipEl = (msg, visible = true) => {
+ tipEl.innerText = msg;
+ tipEl.style.display = visible ? 'block' : 'none';
+ tipEl.setAttribute('class', 'devui-validate-tip');
+ }
+
+ const addElClass = (el: HTMLElement, className: string) => {
+ let currentClasses = el.getAttribute('class');
+ if(!currentClasses.includes(className)) {
+ currentClasses = currentClasses.trim() + (currentClasses.trim() ? ' ' : '') + className;
+ }
+ el.setAttribute('class', currentClasses);
+ }
+
+ const removeElClass = (el: HTMLElement, className: string) => {
+ let currentClasses = el.getAttribute('class');
+ currentClasses = currentClasses.replace(className, '');
+ el.setAttribute('class', currentClasses);
+ }
+
+ const {validate, createDevUIBuiltinValidator} = useValidate();
+ let propRule = {} || [] as any; // 值为对象数组或单个对象
+
+ const isCustomValidator = validators !== undefined || asyncValidators !== undefined;
+ if(isCustomValidator) {
+ validators && (rules = validators);
+ asyncValidators && (rules = asyncValidators);
+ if(asyncValidators) {
+ let time = Number(asyncDebounceTime);
+ if(isNaN(time)) {
+ console.warn('[v-d-validate] invalid asyncDebounceTime');
+ time = 300;
+ }
+ rules = asyncValidators.map(item => {
+ let res = {
+ message: item.message,
+ asyncValidator: (rule, value) => {
+ return new Promise(debounce((resolve, reject) => {
+ const res = item.asyncValidator(rule, value);
+ if(res) {
+ resolve('');
+ }else {
+ reject(rule.message);
+ }
+ }, time))
+ },
+ } as any;
+ return res;
+ })
+ }
+ }else {
+ if(Array.isArray(rules)) {
+ rules.map(item => {
+ return createDevUIBuiltinValidator(item);
+ });
+ }else {
+ rules = createDevUIBuiltinValidator(rules);
+ }
+ }
+
+ let descriptor: any = {
+ [prop]: rules
+ }
+ const validateFn = async () => {
+ const validateModel = {
+ [prop]: hasModelName ? instance[modelName][prop] : instance[prop]
+ };
+ return validate(descriptor, validateModel).then(res => {
+ renderPopover('', false);
+ removeElClass(el, 'devui-error');
+ messageShowType === 'text' && renderTipEl('', true);
+ return res;
+ }).catch(({ errors, fields }) => {
+ let msg = propRule.message ?? fields[prop][0].message;
+ renderPopover(msg);
+ addElClass(el, 'devui-error');
+ messageShowType === 'text' && renderTipEl(msg, true);
+ if(messageChange && typeof messageChange === 'function') {
+ messageChange(msg, { errors, fields });
+ }
+ return { errors, fields };
+ })
+ }
+
+ if(errorStrategy === 'pristine') {
+ validateFn();
+ }else {
+ el.childNodes[0].addEventListener(updateOn, () => {
+ validateFn();
+ })
+ if(updateOn === 'change') {
+ el.childNodes[0].addEventListener('focus', () => {
+ renderPopover('', false);
+ })
+ }
+ }
+
+ // 处理表单提交校验
+ const formTag = getTargetElement(el, 'form') as HTMLFormElement;
+ if(formTag && updateOn === 'submit') {
+ const formName = formTag.name;
+ const formSubmitDataCallback: any = (val: DFormValidateSubmitData) => {
+ validateFn().then((res: any) => {
+ val.callback(!!!res?.errors, { errors: res?.errors, fields: res?.fields });
+ }).catch(({errors, fields}) => {
+ console.log('validateFn {errors, fields}', {errors, fields});
+ });
+ };
+ EventBus.on(`formSubmit:${formName}`, formSubmitDataCallback);
+ EventBus.on(`formReset:${formName}:${prop}`, () => {
+ renderPopover('', false);
+ removeElClass(el, 'devui-error');
+ messageShowType === 'text' && renderTipEl('', false);
+ });
+ }
+ },
+
+ beforeUnmount(el: HTMLElement, binding: DirectiveBinding) {
+ const {prop} = binding.value;
+ const formTag = getTargetElement(el, 'form') as HTMLFormElement;
+ const formName = formTag.name;
+
+ EventBus.off(`formSubmit:${formName}`);
+ EventBus.off(`formReset:${formName}:${prop}`);
+ }
+}
diff --git a/packages/devui-vue/devui/form/src/directive/style.scss b/packages/devui-vue/devui/form/src/directive/style.scss
index 56345b9d66..11a724fb1e 100644
--- a/packages/devui-vue/devui/form/src/directive/style.scss
+++ b/packages/devui-vue/devui/form/src/directive/style.scss
@@ -1,5 +1,5 @@
-.d-validate-rules-error-pristine {
+.devui-validate-rules-error-pristine {
// background-color: #ffeeed;
input {
background-color: #ffeeed;
@@ -15,10 +15,17 @@
}
}
-.d-validate-tip {
+.devui-validate-tip {
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: #f66f6a;
}
+
+.devui-error {
+ input, .devui-tags {
+ border-color: var(--devui-danger-line,#f66f6a) !important;
+ background-color: var(--devui-danger-bg,#ffeeed) !important;
+ }
+}
\ No newline at end of file
diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.scss b/packages/devui-vue/devui/form/src/form-control/form-control.scss
index 1c608a95e5..7e246ad1c8 100644
--- a/packages/devui-vue/devui/form/src/form-control/form-control.scss
+++ b/packages/devui-vue/devui/form/src/form-control/form-control.scss
@@ -1,14 +1,15 @@
-.form-control {
+.devui-form-control {
position: relative;
+ width: 100%;
- .star {
+ .devui-star {
color: red;
}
.devui-form-control-container {
position: relative;
- .feedback-status {
+ .devui-feedback-status {
position: absolute;
top: 50%;
right: 0;
@@ -31,6 +32,7 @@
.devui-form-control-container-horizontal {
display: flex;
+ width: 100%;
.devui-radio {
&:not(:last-child) {
@@ -50,43 +52,16 @@
}
}
- input,
- .devui-tags-host {
- width: 200px;
- }
-
- .d-validate-tip {
- margin: 0 10px;
+ .devui-validate-tip {
+ margin: 0;
}
}
-
.devui-control-content-wrapper {
- .devui-popover-wrapper {
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- z-index: 9;
-
- & > div {
- width: inherit;
- height: 100%;
- }
- }
+ width: 100%;
}
- .with-popover {
- position: relative;
- & > div {
- z-index: 10;
- }
- }
-
-
-
- .has-feedback {
+ .devui-has-feedback {
display: flex;
align-items: center;
@@ -95,7 +70,7 @@
}
}
- .feedback-error {
+ .devui-feedback-error {
border: 1px solid #f66f6a;
border-radius: 2px;
diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.tsx b/packages/devui-vue/devui/form/src/form-control/form-control.tsx
index 55612f3a6f..90d6397b9c 100644
--- a/packages/devui-vue/devui/form/src/form-control/form-control.tsx
+++ b/packages/devui-vue/devui/form/src/form-control/form-control.tsx
@@ -1,7 +1,8 @@
import { defineComponent, inject, ref, computed, reactive, onMounted, Teleport } from 'vue';
-import { uniqueId } from 'lodash-es';
+import { uniqueId } from 'lodash';
import { IForm, formControlProps, formInjectionKey } from '../form-types';
import { ShowPopoverErrorMessageEventData } from '../directive/d-validate-rules'
+import clickoutsideDirective from '../../../shared/devui-directive/clickoutside'
import { EventBus, getElOffset } from '../util';
import Icon from '../../../icon/src/icon';
import Popover from '../../../popover/src/popover';
@@ -11,16 +12,20 @@ type positionType = 'top' | 'right' | 'bottom' | 'left';
export default defineComponent({
name: 'DFormControl',
+ directives: {
+ clickoutside: clickoutsideDirective
+ },
props: formControlProps,
setup(props, ctx) {
const formControl = ref();
const dForm = reactive(inject(formInjectionKey, {} as IForm));
const labelData = reactive(dForm.labelData);
const isHorizontal = labelData.layout === 'horizontal';
- const uid = uniqueId("dfc-");
+ const uid = uniqueId('dfc-');
const showPopover = ref(false);
- const tipMessage = ref("");
- const popPosition = ref("bottom");
+ const updateOn = ref('change');
+ const tipMessage = ref('');
+ const popPosition = ref('bottom');
let rectInfo: Partial = {
width: 0,
height: 0
@@ -35,14 +40,15 @@ export default defineComponent({
onMounted(() => {
const el = document.getElementById(uid);
elOffset = getElOffset(el);
- EventBus.on("showPopoverErrorMessage", (data: ShowPopoverErrorMessageEventData) => {
+ EventBus.on('showPopoverErrorMessage', (data: ShowPopoverErrorMessageEventData) => {
if (uid === data.uid) {
rectInfo = el.getBoundingClientRect();
- popoverLeftPosition = popPosition.value === "top" || popPosition.value === "bottom" ? rectInfo.right - (rectInfo.width / 2) : rectInfo.right;
- popoverTopPosition = popPosition.value === "top" ? elOffset.top + (rectInfo.height / 2) - rectInfo.height : elOffset.top + (rectInfo.height / 2);
showPopover.value = data.showPopover;
tipMessage.value = data.message;
popPosition.value = data.popPosition as any; // todo: 待popover组件positionType完善类型之后再替换类型
+ popoverLeftPosition = popPosition.value === 'top' || popPosition.value === 'bottom' ? rectInfo.right - (rectInfo.width / 2) : rectInfo.right;
+ popoverTopPosition = popPosition.value === 'top' ? elOffset.top + (rectInfo.height / 2) - rectInfo.height : elOffset.top + (rectInfo.height / 2);
+ updateOn.value = data.updateOn ?? 'change';
}
});
});
@@ -60,13 +66,20 @@ export default defineComponent({
}
})
+ const handleClickOutside = () => {
+ if(updateOn.value !== 'change') {
+ showPopover.value = false;
+ }
+ }
+
return () => {
const {
feedbackStatus,
extraInfo,
} = props;
- return