├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── deploy.bat ├── docs ├── .vitepress │ ├── config │ │ └── index.ts │ └── theme │ │ ├── components │ │ └── home.vue │ │ ├── index.ts │ │ └── styles │ │ ├── index.css │ │ ├── override.css │ │ └── vars.css ├── components │ ├── button.md │ ├── countup.md │ ├── date-picker.md │ ├── ellipsis.md │ ├── form.md │ ├── iframe.md │ ├── image.md │ ├── pro-table.md │ ├── select.md │ └── table.md ├── examples │ ├── button │ │ ├── debounce.vue │ │ └── throttle.vue │ ├── date-picker │ │ ├── basic.vue │ │ └── range.vue │ ├── ellipsis │ │ ├── auto.vue │ │ ├── basic.vue │ │ ├── line-clamp.vue │ │ └── slot.vue │ ├── form │ │ ├── basic.vue │ │ ├── full.vue │ │ ├── layout.vue │ │ └── slot.vue │ ├── image │ │ ├── basic.vue │ │ ├── download.vue │ │ ├── operation.vue │ │ └── preview.vue │ ├── pro-table │ │ ├── basic.vue │ │ ├── form.vue │ │ ├── mock.ts │ │ ├── only-table.vue │ │ └── slot.vue │ ├── select │ │ ├── any-data.vue │ │ ├── basic.vue │ │ ├── before-select.vue │ │ ├── change.vue │ │ ├── echo.vue │ │ ├── group.vue │ │ ├── multiple.vue │ │ ├── object.vue │ │ ├── remote.vue │ │ └── slot.vue │ └── table │ │ ├── basic.vue │ │ ├── children.vue │ │ ├── multiple.vue │ │ ├── render.vue │ │ └── slot.vue ├── guide │ └── start.md └── index.md ├── package.json ├── packages ├── components │ ├── package.json │ ├── script │ │ └── build.js │ ├── src │ │ ├── button │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── button.ts │ │ │ │ └── button.vue │ │ ├── date-picker │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── date-picker.ts │ │ │ │ └── date-picker.vue │ │ ├── ellipsis │ │ │ ├── index.ts │ │ │ ├── src │ │ │ │ ├── ellipsis.ts │ │ │ │ └── ellipsis.vue │ │ │ └── style │ │ │ │ └── index.scss │ │ ├── form │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── form.ts │ │ │ │ └── form.vue │ │ ├── image │ │ │ ├── index.ts │ │ │ ├── src │ │ │ │ ├── image-viewer.vue │ │ │ │ ├── image.ts │ │ │ │ └── image.vue │ │ │ └── style │ │ │ │ └── index.scss │ │ ├── index.ts │ │ ├── pro-table │ │ │ ├── index.ts │ │ │ ├── src │ │ │ │ ├── pro-table.ts │ │ │ │ ├── pro-table.vue │ │ │ │ └── use-pro-table.ts │ │ │ └── style │ │ │ │ └── index.scss │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── src │ │ │ │ ├── content.vue │ │ │ │ ├── list-item-group.vue │ │ │ │ ├── list-item-option.vue │ │ │ │ ├── list-item.vue │ │ │ │ ├── list.vue │ │ │ │ ├── props.ts │ │ │ │ ├── select-dropdown.vue │ │ │ │ ├── select.types.ts │ │ │ │ ├── select.vue │ │ │ │ ├── token.ts │ │ │ │ ├── use-select-list.ts │ │ │ │ ├── use-select.ts │ │ │ │ └── util.ts │ │ │ └── style │ │ │ │ └── index.scss │ │ ├── styles.ts │ │ ├── table │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── table-columns.vue │ │ │ │ ├── table.ts │ │ │ │ └── table.vue │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── cxz-ui │ ├── README.md │ └── package.json └── utils │ ├── download.ts │ ├── index.ts │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | quote_type = single 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | pnpm-lock.yaml 4 | play 5 | cache 6 | packages/cxz-ui/es 7 | packages/cxz-ui/lib 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:vue/vue3-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier' 14 | ], 15 | parser: 'vue-eslint-parser', 16 | parserOptions: { 17 | ecmaVersion: 'latest', 18 | parser: '@typescript-eslint/parser', 19 | sourceType: 'module' 20 | }, 21 | plugins: ['vue', '@typescript-eslint', 'prettier'], 22 | rules: { 23 | 'prettier/prettier': 'error', 24 | 'vue/multi-word-component-names': 'off', 25 | '@typescript-eslint/no-var-requires': 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["master"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pages: write 16 | id-token: write 17 | environment: 18 | name: github-pages 19 | url: ${{ steps.deployment.outputs.page_url }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - uses: pnpm/action-setup@v2 25 | with: 26 | version: 7 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 16 30 | cache: pnpm 31 | - run: pnpm install 32 | - name: Build 33 | run: pnpm run docs:build 34 | - uses: actions/configure-pages@v2 35 | - uses: actions/upload-pages-artifact@v1 36 | with: 37 | path: docs/.vitepress/dist 38 | - name: Deploy 39 | id: deployment 40 | uses: actions/deploy-pages@v1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | cache 15 | packages/cxz-ui/es 16 | packages/cxz-ui/lib 17 | 18 | # Editor directories and files 19 | .idea 20 | .temp 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | types 28 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": ["prettier --write .", "eslint --fix"], 3 | "*.md": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | CHANGELOG.en-US.md 5 | pnpm-lock.yaml 6 | docs/components.d.ts 7 | cache 8 | packages/cxz-ui/es 9 | packages/cxz-ui/lib 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80 3 | tabWidth: 2, // 一个 tab 代表几个空格数,默认为 2 个 4 | useTabs: false, //是否使用 tab 进行缩进,默认为false,表示用空格进行缩减 5 | singleQuote: true, // 字符串是否使用单引号,默认为 false,使用双引号 6 | semi: false, // 行尾是否使用分号,默认为true 7 | trailingComma: 'none', // 是否使用尾逗号 8 | bracketSpacing: true, // 对象大括号直接是否有空格,默认为 true,效果:{ a: 1 } 9 | arrowParens: 'always', 10 | htmlWhitespaceSensitivity: 'ignore', 11 | jsxSingleQuote: true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cxzhub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cxz-ui 2 | 3 | 基于 Element Plus 组件库二次封装 4 | 5 | ## 准备工作 6 | 7 | 项目中需要先安装[Vue3](https://cn.vuejs.org/)、[Element Plus](https://element-plus.org/zh-CN/guide/design.html) 8 | 9 | ## 安装 10 | 11 | ```bash 12 | pnpm i cxz-ui 13 | ``` 14 | 15 | ## 使用 16 | 17 | - 在入口文件中导入样式 18 | 19 | ```js 20 | import 'cxz-ui/es/style.css' 21 | ``` 22 | 23 | - 示例 24 | 25 | ```vue 26 | 32 | 33 | 43 | ``` 44 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } 4 | -------------------------------------------------------------------------------- /deploy.bat: -------------------------------------------------------------------------------- 1 | call pnpm docs:build 2 | cd docs/.vitepress/dist 3 | 4 | git init 5 | git add -A 6 | git commit -m "deploy" 7 | 8 | git push -f https://github.com/cxzhub/cxz-ui.git master:gh-pages 9 | -------------------------------------------------------------------------------- /docs/.vitepress/config/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | containerPreview, 3 | componentPreview 4 | } from '@vitepress-demo-preview/plugin' 5 | 6 | module.exports = { 7 | title: 'Cxz UI', 8 | themeConfig: { 9 | socialLinks: [{ icon: 'github', link: 'https://github.com/cxzhub/cxz-ui' }], 10 | 11 | footer: { 12 | message: `Released under the MIT License.`, 13 | copyright: 'Copyright © 2023-present Chen Xing zhi' 14 | }, 15 | 16 | nav: [ 17 | { text: '指南', link: '/guide/start' }, 18 | { text: '组件', link: '/components/select' } 19 | ], 20 | 21 | sidebar: { 22 | '/guide/': [ 23 | { 24 | items: [{ text: '快速开始', link: '' }] 25 | } 26 | ], 27 | '/components/': [ 28 | { 29 | text: '全新开发', 30 | items: [ 31 | { text: 'Select 选择器', link: '/components/select' }, 32 | { text: 'Ellipsis 文本省略', link: '/components/ellipsis' } 33 | ] 34 | }, 35 | { 36 | text: '功能增强', 37 | items: [ 38 | { text: 'Button 按钮', link: '/components/button' }, 39 | { text: 'DatePicker 日期选择器', link: '/components/date-picker' }, 40 | { text: 'Image 图片', link: '/components/image' } 41 | ] 42 | }, 43 | { 44 | text: '配置化', 45 | items: [ 46 | { text: 'Table 表格', link: '/components/table' }, 47 | { text: 'Form 表单', link: '/components/form' }, 48 | { text: 'ProTable 高级表格', link: '/components/pro-table' } 49 | ] 50 | }, 51 | { 52 | text: '其他', 53 | items: [ 54 | { text: 'Countup 数字滚动', link: '/components/countup' }, 55 | { text: 'Iframe 内联框架', link: '/components/iframe' } 56 | ] 57 | } 58 | ] 59 | } 60 | }, 61 | markdown: { 62 | config(md) { 63 | md.use(containerPreview) 64 | md.use(componentPreview) 65 | } 66 | }, 67 | base: '/cxz-ui/', 68 | vite: { 69 | resolve: { 70 | alias: { 71 | 'cxz-ui': '../../../packages/components' 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/home.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 46 | 47 | 126 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import { ElementPlusContainer } from '@vitepress-demo-preview/component' 3 | import ElementPlus from 'element-plus' 4 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 5 | import '@vitepress-demo-preview/component/dist/style.css' 6 | import 'element-plus/dist/index.css' 7 | import 'element-plus/theme-chalk/dark/css-vars.css' 8 | import './styles/index.css' 9 | 10 | export default { 11 | ...DefaultTheme, 12 | enhanceApp: async ({ app, router, siteData }) => { 13 | app.component('demo-preview', ElementPlusContainer) 14 | app.use(ElementPlus, { locale: zhCn }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/index.css: -------------------------------------------------------------------------------- 1 | @import url('./vars.css'); 2 | @import url('./override.css'); 3 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/override.css: -------------------------------------------------------------------------------- 1 | .vp-doc .el-table table { 2 | display: unset; 3 | border-collapse: unset; 4 | margin: unset; 5 | overflow-x: unset; 6 | } 7 | 8 | .vp-doc .el-table tr { 9 | border-top: unset; 10 | transition: unset; 11 | } 12 | 13 | .vp-doc .el-table tr:nth-child(2n) { 14 | background-color: unset; 15 | } 16 | 17 | .vp-doc .el-table th, 18 | .vp-doc .el-table td { 19 | border: unset; 20 | padding: unset; 21 | } 22 | 23 | .vp-doc .el-table th { 24 | text-align: unset; 25 | font-size: unset; 26 | font-weight: unset; 27 | color: unset; 28 | background-color: unset; 29 | } 30 | 31 | .vp-doc .el-table td { 32 | font-size: unset; 33 | } 34 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand: #409eff; 3 | --component-preview-bg: #1e1e20; 4 | } 5 | -------------------------------------------------------------------------------- /docs/components/button.md: -------------------------------------------------------------------------------- 1 | # Button 按钮 2 | 3 | ## 简介 4 | 5 | 相比于 `ElButton`,添加了防抖、节流 6 | 7 | ## 防抖 8 | 9 | :::preview 防抖 10 | demo-preview=../examples/button/debounce.vue 11 | ::: 12 | 13 | ## 节流 14 | 15 | :::preview 节流 16 | demo-preview=../examples/button/throttle.vue 17 | ::: 18 | 19 | ## Button 新增属性 20 | 21 | | 属性名 | 说明 | 类型 | 默认值 | 22 | | ------------ | ------------ | ------ | ------ | 23 | | throttleWait | 节流等待时间 | number | 0 | 24 | | debounceWait | 防抖等待时间 | number | 0 | 25 | -------------------------------------------------------------------------------- /docs/components/countup.md: -------------------------------------------------------------------------------- 1 | # Countup 数字滚动 2 | 3 | ## 简介 4 | 5 | 基于 `countup.js` 开发的组件。与 `Element Plus` 没有关联,已经单独抽出另建库。 6 | 详情见[vue3-countup](https://cxzhub.github.io/vue3-countup/)。 7 | -------------------------------------------------------------------------------- /docs/components/date-picker.md: -------------------------------------------------------------------------------- 1 | # DatePicker 日期选择器 2 | 3 | ## 简介 4 | 5 | 添加时间前后禁用、限制选择范围属性,不需要重复写方法控制。`ElDatePicker` 原来所有属性都支持。 6 | 7 | ## 时间前后禁用 8 | 9 | :::preview 时间前后禁用 10 | demo-preview=../examples/date-picker/basic.vue 11 | ::: 12 | 13 | ## 限制选择范围 14 | 15 | :::preview 限制选择范围 16 | demo-preview=../examples/date-picker/range.vue 17 | ::: 18 | 19 | ## DatePicker 新增属性 20 | 21 | | 属性名 | 说明 | 类型 | 默认值 | 22 | | ------------------ | ------------------------ | -------------------- | --------- | 23 | | beforeDisabledDate | 在此之前的时间禁用 | String, Number, Date | undefined | 24 | | afterDisabledDate | 在此之后的时间禁用 | String, Number, Date | undefined | 25 | | dateRangeLimit | 能够选择的时间范围(天) | Number | undefined | 26 | -------------------------------------------------------------------------------- /docs/components/ellipsis.md: -------------------------------------------------------------------------------- 1 | # Ellipsis 文本省略 2 | 3 | ## 简介 4 | 5 | 工作中经常碰到需要文本溢出省略的场景,但是产品希望鼠标悬浮时显示文字提示,展示全部内容。常规的做法是加个 title,或者用 `ElTooltip` 包裹一层,但是这些做法,文字提示会一直在,即便内容没有溢出,效果不理想。综上,我开发了 Ellipsis 组件。 6 | 7 | ## 基础用法 8 | 9 | 当内容长度没有超出时,不显示文字提示;当内容长度超出时,显示文字提示 10 | :::preview 基础用法 11 | demo-preview=../examples/ellipsis/basic.vue 12 | ::: 13 | 14 | ## 设置行数 15 | 16 | :::preview 设置行数 17 | demo-preview=../examples/ellipsis/line-clamp.vue 18 | ::: 19 | 20 | ## 响应式 21 | 22 | 根据内容和容器大小自动计算是否显示文字提示 23 | :::preview 响应式 24 | demo-preview=../examples/ellipsis/auto.vue 25 | ::: 26 | 27 | ## 插槽 28 | 29 | :::preview 设置行数 30 | demo-preview=../examples/ellipsis/slot.vue 31 | ::: 32 | 33 | ## Ellipsis 属性 34 | 35 | | 属性名 | 说明 | 类型 | 默认值 | 36 | | ------------ | --------------- | -------------- | --------- | 37 | | content | 内容 | string | undefined | 38 | | tooltipProps | el-tooltip 属性 | ElTooltipProps | {} | 39 | | lineClamp | 行数 | number | 1 | 40 | | maxWidth | 最大宽度 | number,string | 'auto' | 41 | | style | 额外样式 | StyleValue | {} | 42 | | class | 额外类名 | style | undefined | 43 | 44 | ## Ellipsis 插槽 45 | 46 | | 插槽名 | 说明 | 47 | | ------- | ------------ | 48 | | default | 内容 | 49 | | tooltip | 文字提示内容 | 50 | -------------------------------------------------------------------------------- /docs/components/form.md: -------------------------------------------------------------------------------- 1 | # Form 表单 2 | 3 | ## 简介 4 | 5 | 配置化表单。内部集成 `ElLayout` 布局,可以快速搭建表单。 6 | 7 | ## 基础用法 8 | 9 | 使用配置`schema`替换原来模版声明`ElFormItem`方式,`schema`是在原有`ElFormItem`属性基础上添加若干属性。
10 | `component`添加组件,`componentAttrs`为组件添加属性 11 | :::preview 基础用法 12 | demo-preview=../examples/form/basic.vue 13 | ::: 14 | 15 | ## 布局 16 | 17 | `layoutRow`为表单添加`ElRow`属性,`layoutCol`为表单添加`ElCol`属性。`schema`中的每项可以添加`layout`修改公共的`layoutCol`布局。 18 | :::preview 布局 19 | demo-preview=../examples/form/layout.vue 20 | ::: 21 | 22 | ## 插槽 23 | 24 | :::preview 插槽 25 | demo-preview=../examples/form/slot.vue 26 | ::: 27 | 28 | ## 完整例子 29 | 30 | :::preview 完整例子 31 | demo-preview=../examples/form/full.vue 32 | ::: 33 | 34 | ## Form 新增属性 35 | 36 | | 属性名 | 说明 | 类型 | 默认值 | 37 | | ---------- | -------- | --------------- | ------ | 38 | | schema | 配置 | CxzFormSchema[] | [] | 39 | | layoutRow | row 属性 | layoutRowProps | {} | 40 | | layoutCol | col 属性 | layoutColProps | {} | 41 | | modelValue | 绑定值 | Object | {} | 42 | 43 | ## Form Expose 44 | 45 | | 名称 | 说明 | 46 | | ------- | -------------- | 47 | | formRef | el-from 的 ref | 48 | 49 | ## Schema 相比 ElFormItem 新增属性 50 | 51 | | 属性名 | 说明 | 类型 | 默认值 | 52 | | -------------- | -------------- | -------------- | --------- | 53 | | layout | 布局 | layoutColProps | undefined | 54 | | slot | 内容插槽名称 | string | undefined | 55 | | labelSlot | label 插槽名称 | string | undefined | 56 | | errorSlot | 错误插槽名称 | string | undefined | 57 | | component | 组件 | any | undefined | 58 | | componentAttrs | 组件属性 | Object | undefined | 59 | -------------------------------------------------------------------------------- /docs/components/iframe.md: -------------------------------------------------------------------------------- 1 | # Iframe 内联框架 2 | 3 | ## 简介 4 | 5 | 项目里的一些样式会存在互相干扰,如富文本的样式与主页面样式,这时候需要使用 iframe 进行样式隔离。本组件与 `Element Plus` 没有关联,已经单独抽出建库。详情见[vue3-iframe](https://cxzhub.github.io/vue3-iframe/)。 6 | -------------------------------------------------------------------------------- /docs/components/image.md: -------------------------------------------------------------------------------- 1 | # Image 图片 2 | 3 | ## 简介 4 | 5 | 相对 `ElIamge`,添加蒙层,添加预览、下载按钮及功能。`ElImage` 原来预览功能不再支持。 6 | 7 | ## 基础用法 8 | 9 | 基础用法与 `ElImage` 没有区别 10 | :::preview 基础用法 11 | demo-preview=../examples/image/basic.vue 12 | ::: 13 | 14 | ## 预览 15 | 16 | 使用 with-preview 开启预览。不传 preview-src-list 默认预览当前图片,传了可以预览多张图片。initialIndex 不传默认预览当前图片,传了可以预览其他图片。 17 | :::preview 预览 18 | demo-preview=../examples/image/preview.vue 19 | ::: 20 | 21 | ## 下载 22 | 23 | 使用 with-download 开启下载。使用 file-name 可以修改下载文件名 24 | :::preview 下载 25 | demo-preview=../examples/image/download.vue 26 | ::: 27 | 28 | ## 更多操作 29 | 30 | 使用 operation 插槽添加更多操作 31 | :::preview 更多操作 32 | demo-preview=../examples/image/operation.vue 33 | ::: 34 | 35 | ## Image 新增或修改的属性 36 | 37 | | 属性名 | 说明 | 类型 | 默认值 | 38 | | ----------------- | --------------------------------- | ------------ | --------- | 39 | | previewTeleported | image-viewer 是否插入至 body 元素 | boolean | true | 40 | | initialIndex | 初始预览图像索引 | number | undefined | 41 | | withPreview | 是否开启预览 | boolean | false | 42 | | previewIcon | 预览图标 | iconPropType | ZoomIn | 43 | | withDownload | 是否开启下载 | boolean | false | 44 | | downloadIcon | 下载图标 | iconPropType | Download | 45 | | fileName | 下载文件名称 | string | undefined | 46 | -------------------------------------------------------------------------------- /docs/components/pro-table.md: -------------------------------------------------------------------------------- 1 | # ProTable 高级表格 2 | 3 | ## 简介 4 | 5 | 集成`Form`、`Table`、`Pagination`,配置化,方便快速搭建列表页。没有添加过多样式,可以根据具体业务进行覆盖 6 | 7 | ## 基础用法 8 | 9 | 通过`useProTable`配置,getList 为获取数据方法,form 传`CxzForm`的属性,form 中添加`defaultValue`设置默认值,table 传`CxzTable`的属性 10 | :::preview 基础用法 11 | demo-preview=../examples/pro-table/basic.vue 12 | ::: 13 | 14 | ## 表单折叠个数 15 | 16 | 通过 show-count 控制表单折叠显示个数 17 | :::preview 基础用法 18 | demo-preview=../examples/pro-table/form.vue 19 | ::: 20 | 21 | ## 仅表格 22 | 23 | form 不传 schema 就不会显示搜索表单 24 | :::preview 仅表格 25 | demo-preview=../examples/pro-table/only-table.vue 26 | ::: 27 | 28 | ## 插槽 29 | 30 | :::preview 插槽 31 | demo-preview=../examples/pro-table/slot.vue 32 | ::: 33 | 34 | ## ProTable 属性 35 | 36 | | 属性名 | 说明 | 类型 | 默认值 | 37 | | ---------- | ----------------- | -------------------------- | ------ | 38 | | init | 初始化方法 | ProTableInitFunctionParams | {} | 39 | | showCount | 表单折叠显示个数 | number | 2 | 40 | | pagination | ElPagination 属性 | - | - | 41 | 42 | ## ProTable 事件 43 | 44 | 同原 ElTable 事件 45 | 46 | ## ProTable Expose 47 | 48 | | 名称 | 说明 | 49 | | -------- | ----------------------------------- | 50 | | tableRef | 通过 tableRef 可以调用 ElTable 方法 | 51 | 52 | ## ProTable 插槽 53 | 54 | | 插槽名 | 说明 | 55 | | ---------- | ------------------------------------------------ | 56 | | $append | 原 el-table append 插槽,加$防和其他插槽名称冲突 | 57 | | $empty | 原 el-table empty 插槽,加$防和其他插槽名称冲突 | 58 | | $operation | 表格头部操作列,加$防和其他插槽名称冲突 | 59 | -------------------------------------------------------------------------------- /docs/components/select.md: -------------------------------------------------------------------------------- 1 | # Select 选择器 2 | 3 | ## 简介 4 | 5 | ### ElSelect 的痛点 6 | 7 | 1. 需要传入特定结构的数据({label:string,value:number}[]),不够灵活; 8 | 2. change 事件只返回了当前的 value 值,没能把整个对象返回,仍需使用 find()方法查一遍; 9 | 3. 远程搜索或者编辑页回显时,当所选的值不在 options 中时会显示 id; 10 | 4. 远程搜索时,当没有选项,没有出现“暂无数据”; 11 | 5. 一些业务中,需要在点击选项时,先进行一些判断,然后不满足条件时阻止选中,即需要 beforeSelect 事件。 12 | 13 | ### 本组件优势 14 | 15 | 针对上述缺陷与不足,完全重写了下拉选择器。 16 | 17 | 1. 解决上述所以痛点; 18 | 2. 完全复用 `ElSelect` 样式,风格样式保持统一; 19 | 3. 下拉选项使用虚拟列表,不惧数据量大。 20 |
21 | 22 | ## 基础用法 23 | 24 | :::preview 基础用法 25 | demo-preview=../examples/select/basic.vue 26 | ::: 27 | 28 | ## 多选 29 | 30 | :::preview 多选 31 | demo-preview=../examples/select/multiple.vue 32 | ::: 33 | 34 | ## 任意对象数组 35 | 36 | 通过 props 确定 label、value 的键 37 | :::preview 任意对象数组 38 | demo-preview=../examples/select/any-data.vue 39 | ::: 40 | 41 | ## change 事件 42 | 43 | :::preview change 事件 44 | demo-preview=../examples/select/change.vue 45 | ::: 46 | 47 | ## 远程搜索 48 | 49 | :::preview 远程搜索 50 | demo-preview=../examples/select/remote.vue 51 | ::: 52 | 53 | ## 将整个对象作为值 54 | 55 | :::preview 将整个对象作为值 56 | demo-preview=../examples/select/object.vue 57 | ::: 58 | 59 | ## 编辑页面回显 60 | 61 | :::preview 编辑页面回显 62 | demo-preview=../examples/select/echo.vue 63 | ::: 64 | 65 | ## 选中之前判断 66 | 67 | 第一项不能被选择 68 | :::preview 选中之前判断 69 | demo-preview=../examples/select/before-select.vue 70 | ::: 71 | 72 | ## 分组 73 | 74 | :::preview 分组 75 | demo-preview=../examples/select/group.vue 76 | ::: 77 | 78 | ## 插槽 79 | 80 | 默认插槽 81 | :::preview 插槽 82 | demo-preview=../examples/select/slot.vue 83 | ::: 84 | 85 | ## Select 属性 86 | 87 | | 属性名 | 说明 | 类型 | 默认值 | 88 | | ------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------- | ------------------ | 89 | | v-model | 绑定值 | any | undefined | 90 | | disabled | 是否禁用 | boolean | false | 91 | | multiple | 是否多选 | boolean | false | 92 | | props | 配置选项,具体看下表 | Object | undefined | 93 | | options | 选项 | Object[] | [] | 94 | | popperClass | 选择器下拉菜单的自定义类名 | string | '' | 95 | | name | 选择器的原生 name 属性 | string | undefined | 96 | | placeholder | placeholder | string | '请选择' | 97 | | suffixIcon | 选择器后缀图标 | iconPropType | ArrowDown | 98 | | clearable | 是否可清空 | boolean | true | 99 | | clearIcon | 清空图标 | iconPropType | CircleClose | 100 | | collapseTags | 多选时是否将选中值按文字的形式展示 | boolean | false | 101 | | collapseTagsTooltip | 当鼠标悬停于折叠标签的文本时,是否显示所有选中的标签。 只有当 collapse-tags 设置为 true 时才会生效。 | boolean | false | 102 | | tagType | tag 类型 | 'success','warning','info' | 'info' | 103 | | tagsHeight | tags 高度,超出显示滚动条 | string、number | undefined | 104 | | loading | loading | boolean | false | 105 | | loadingText | loading 文字 | string | '加载中...' | 106 | | emptyText | 无数据时文字 | string | '暂无数据' | 107 | | remote | 是否远程搜索 | boolean | false | 108 | | remoteMethod | 远程搜索方法 | function | undefined | 109 | | filterable | 是否可过滤 | boolean | false | 110 | | filterMethod | 过滤方法 | function | undefined | 111 | | beforeSelect | 选中之前的操作 | function | undefined | 112 | | multipleLimit | 限制多选个数 | number | 0 | 113 | | multipleLimitText | 超出选中个数的文字 | string | '选择个数超过限制' | 114 | | itemHeight | 选项高度 | number | 32 | 115 | | height | 下拉框最大高度 | number | 192 | 116 | | scrollbarAlwaysOn | 滚动条是否一直在 | boolean | false | 117 | | isObject | 返回的 modelValue 是否是整个对象 | boolean | false | 118 | | valueOptions | 用于回显的 options | array | [] | 119 | 120 | ## Select props 配置项 121 | 122 | | 属性 | 说明 | 类型 | 默认值 | 123 | | -------- | ---------------------- | ------ | -------------- | 124 | | value | 绑定值 | string | 'value' | 125 | | label | 显示的字段 | string | 'label' | 126 | | disabled | 是否禁用字段 | string | 'disabled' | 127 | | children | 树的子树,或分组的内容 | string | '\_\_children' | 128 | 129 | ## Select 事件 130 | 131 | | 事件名 | 说明 | 回调参数 | 132 | | -------------- | ---------------------------------------- | ----------------------------------- | 133 | | change | 选中值发生变化时触发 | value 选中的值,option 整个选项对象 | 134 | | visible-change | 下拉框出现/隐藏时触发 | val,出现则为 true,隐藏则为 false | 135 | | remove-tag | 多选模式下移除 tag 时触发 | val,移除的 tag 值 | 136 | | clear | 可清空的单选模式下用户点击清空按钮时触发 | —— | 137 | | blur | 失焦 | —— | 138 | | focus | 聚焦 | —— | 139 | 140 | ## Select Expose 141 | 142 | | 名称 | 说明 | 143 | | ------------ | ---------------- | 144 | | selectedMaps | 选中值详情的集合 | 145 | 146 | ## Select 插槽 147 | 148 | | 插槽名 | 说明 | 149 | | ------- | ------------------------------------------------------ | 150 | | default | 参数 option: 当前选项,index,disable:是否禁用 | 151 | | empty | 自定义当选项为空时的内容 | 152 | | prefix | 输入框的前缀 | 153 | | content | 自定义输入框回显内容。 作用域参数为 maps,选中值的 map | 154 | | tag | 作用域参数为 value: 选中的值, option:当前选项对象 | 155 | | header | 下拉框头部 | 156 | | footer | 下拉框底部 | 157 | -------------------------------------------------------------------------------- /docs/components/table.md: -------------------------------------------------------------------------------- 1 | # Table 表格 2 | 3 | ## 简介 4 | 5 | 配置化的 `ElTable` 6 | 7 | ## 基础用法 8 | 9 | 使用配置代替了模版声明 `ElTableColumn` , `ElTable`原本的属性、事件与方法均支持 10 | :::preview 基础用法 11 | demo-preview=../examples/table/basic.vue 12 | ::: 13 | 14 | ## 渲染函数 15 | 16 | 头部渲染可以使用`ElTableColumn`原本的 renderHeader, 17 | 内容渲染使用 render 18 | :::preview 渲染函数 19 | demo-preview=../examples/table/render.vue 20 | ::: 21 | 22 | ## 插槽 23 | 24 | 头部插槽 headerSlot, 25 | 内容插槽 slot 26 | :::preview 渲染函数 27 | demo-preview=../examples/table/slot.vue 28 | ::: 29 | 30 | ## 多级表头 31 | 32 | 使用 children 实现 33 | :::preview 多级表头 34 | demo-preview=../examples/table/children.vue 35 | ::: 36 | 37 | ## 多选 38 | 39 | 暴露出 tableRef,可以调用原 el-table 方法 40 | :::preview 多级表头 41 | demo-preview=../examples/table/multiple.vue 42 | ::: 43 | 44 | ## Table 新增属性 45 | 46 | | 属性名 | 说明 | 类型 | 默认值 | 47 | | ------- | ------ | ---------------- | ------ | 48 | | columns | 配置列 | CxzTableColumn[] | [] | 49 | 50 | ## TableColumn 新增属性 51 | 52 | | 属性名 | 说明 | 类型 | 默认值 | 53 | | ---------- | ------------ | ------------------------ | --------- | 54 | | render | 渲染函数 | (row,column,$index)=>any | undefined | 55 | | slot | 插槽名称 | string | undefined | 56 | | headerSlot | 头部插槽名称 | string | undefined | 57 | | children | 子列 | CxzTableColumn[] | undefined | 58 | 59 | ## Table 插槽 60 | 61 | | 插槽名 | 说明 | 62 | | ------- | ------------------------------------------------ | 63 | | $append | 原 el-table append 插槽,加$防和其他插槽名称冲突 | 64 | | $empty | 原 el-table empty 插槽,加$防和其他插槽名称冲突 | 65 | -------------------------------------------------------------------------------- /docs/examples/button/debounce.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /docs/examples/button/throttle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /docs/examples/date-picker/basic.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /docs/examples/date-picker/range.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /docs/examples/ellipsis/auto.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /docs/examples/ellipsis/basic.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 16 | -------------------------------------------------------------------------------- /docs/examples/ellipsis/line-clamp.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /docs/examples/ellipsis/slot.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /docs/examples/form/basic.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 63 | -------------------------------------------------------------------------------- /docs/examples/form/full.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 158 | -------------------------------------------------------------------------------- /docs/examples/form/layout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 59 | -------------------------------------------------------------------------------- /docs/examples/form/slot.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /docs/examples/image/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /docs/examples/image/download.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /docs/examples/image/operation.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /docs/examples/image/preview.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /docs/examples/pro-table/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 76 | -------------------------------------------------------------------------------- /docs/examples/pro-table/form.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 76 | -------------------------------------------------------------------------------- /docs/examples/pro-table/mock.ts: -------------------------------------------------------------------------------- 1 | export function delay(timestamp: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, timestamp) 4 | }) 5 | } 6 | 7 | export async function getMock(keys: string[], page: number, pageSize: number) { 8 | await delay(300) 9 | const pageCount = Math.floor(Math.random() * 5) 10 | function createObj(index: number) { 11 | const newObj: Record = {} 12 | for (let i = 0; i < keys.length; i++) { 13 | newObj[keys[i]] = `${keys[i]}-label-${index}` 14 | } 15 | return newObj 16 | } 17 | 18 | return { 19 | data: { 20 | data: new Array(pageSize).fill(null).map((_, index) => createObj(index)), 21 | page, 22 | pageCount, 23 | itemCount: pageCount * pageSize 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/examples/pro-table/only-table.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | -------------------------------------------------------------------------------- /docs/examples/pro-table/slot.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 100 | -------------------------------------------------------------------------------- /docs/examples/select/any-data.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /docs/examples/select/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /docs/examples/select/before-select.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /docs/examples/select/change.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | -------------------------------------------------------------------------------- /docs/examples/select/echo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /docs/examples/select/group.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /docs/examples/select/multiple.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | -------------------------------------------------------------------------------- /docs/examples/select/object.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /docs/examples/select/remote.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | -------------------------------------------------------------------------------- /docs/examples/select/slot.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 65 | 66 | 95 | -------------------------------------------------------------------------------- /docs/examples/table/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | -------------------------------------------------------------------------------- /docs/examples/table/children.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 54 | -------------------------------------------------------------------------------- /docs/examples/table/multiple.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 107 | -------------------------------------------------------------------------------- /docs/examples/table/render.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 65 | -------------------------------------------------------------------------------- /docs/examples/table/slot.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /docs/guide/start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 准备工作 4 | 5 | 项目中需要先安装[Vue3](https://cn.vuejs.org/)、[Element Plus](https://element-plus.org/zh-CN/guide/design.html) 6 | 7 | ## 安装 8 | 9 | ```bash 10 | pnpm i cxz-ui 11 | ``` 12 | 13 | ## 使用 14 | 15 | - 在入口文件中导入样式 16 | 17 | ```js 18 | import 'cxz-ui/es/style.css' 19 | ``` 20 | 21 | - 使用组件 22 | 23 | ```vue 24 | 30 | 31 | 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Cxz UI 5 | titleTemplate: 前端组件库 6 | --- 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "scripts": { 4 | "postinstall": "husky install", 5 | "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue --fix --quiet ./", 6 | "prettier": "prettier --write .", 7 | "dev": "vitepress dev docs", 8 | "docs:dev": "vitepress dev docs", 9 | "docs:build": "vitepress build docs", 10 | "docs:serve": "vitepress serve docs", 11 | "build": "pnpm -F components run build2", 12 | "docs:deploy": "powershell ./deploy.bat" 13 | }, 14 | "devDependencies": { 15 | "@commitlint/cli": "^17.6.5", 16 | "@commitlint/config-conventional": "^17.6.5", 17 | "@types/lodash-es": "^4.17.7", 18 | "@types/node": "^20.2.5", 19 | "@typescript-eslint/eslint-plugin": "^5.50.0", 20 | "@typescript-eslint/parser": "^5.59.8", 21 | "@vitejs/plugin-vue": "^4.1.0", 22 | "@vitepress-demo-preview/component": "^2.3.0", 23 | "@vitepress-demo-preview/plugin": "^1.1.11", 24 | "@vue/compiler-sfc": "^3.3.4", 25 | "child_process": "^1.0.2", 26 | "commitlint": "^17.6.5", 27 | "eslint": "^8.0.1", 28 | "eslint-config-prettier": "^8.8.0", 29 | "eslint-config-standard-with-typescript": "^35.0.0", 30 | "eslint-plugin-import": "^2.25.2", 31 | "eslint-plugin-n": "^15.0.0", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "eslint-plugin-promise": "^6.0.0", 34 | "eslint-plugin-vue": "^9.14.1", 35 | "fs-extra": "^11.1.1", 36 | "husky": "^8.0.3", 37 | "path": "^0.12.7", 38 | "prettier": "^2.8.8", 39 | "sass": "^1.62.1", 40 | "typescript": "^5.0.2", 41 | "vite": "^4.3.9", 42 | "vite-plugin-dts": "^2.3.0", 43 | "vitepress": "1.0.0-beta.1", 44 | "vue-tsc": "^1.4.2" 45 | }, 46 | "dependencies": { 47 | "@cxz-ui/components": "workspace:*", 48 | "@cxz-ui/utils": "workspace:*", 49 | "@element-plus/icons-vue": "^2.1.0", 50 | "@vueuse/core": "^10.1.2", 51 | "dayjs": "^1.11.8", 52 | "element-plus": "^2.3.6", 53 | "lodash-es": "^4.17.21", 54 | "vue": "^3.2.47" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cxz-ui/components", 3 | "version": "0.0.0", 4 | "main": "src/index.ts", 5 | "module": "src/index.ts", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview", 10 | "build2": "node script/build.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/components/script/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const { spawn } = require('child_process') 4 | 5 | const root = path.resolve(__dirname, '..') 6 | const esPath = path.resolve(root, '../cxz-ui/es') 7 | const libPath = path.resolve(root, '../cxz-ui/lib') 8 | 9 | const main = async () => { 10 | if (fs.existsSync(esPath)) { 11 | await fs.remove(esPath) 12 | } 13 | if (fs.existsSync(libPath)) { 14 | await fs.remove(libPath) 15 | } 16 | 17 | spawn('pnpm', ['build'], { 18 | cwd: root, 19 | stdio: 'inherit', 20 | shell: true 21 | }) 22 | } 23 | 24 | main() 25 | -------------------------------------------------------------------------------- /packages/components/src/button/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzButton } from './src/button.vue' 2 | export * from './src/button' 3 | -------------------------------------------------------------------------------- /packages/components/src/button/src/button.ts: -------------------------------------------------------------------------------- 1 | import { buttonProps } from 'element-plus' 2 | 3 | import type { ExtractPropTypes, PropType } from 'vue' 4 | import type { DebounceSettings, ThrottleSettings } from 'lodash-es' 5 | 6 | export const cxzButtonProps = { 7 | ...buttonProps, 8 | throttleWait: { 9 | type: Number 10 | }, 11 | throttleSetting: { 12 | type: Object as PropType, 13 | default: () => 14 | ({ 15 | leading: true 16 | } as ThrottleSettings) 17 | }, 18 | debounceWait: { 19 | type: Number 20 | }, 21 | debounceSetting: { 22 | type: Object as PropType 23 | } 24 | } 25 | 26 | export type CxzButtonProps = ExtractPropTypes 27 | -------------------------------------------------------------------------------- /packages/components/src/button/src/button.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 55 | -------------------------------------------------------------------------------- /packages/components/src/date-picker/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzDatePicker } from './src/date-picker.vue' 2 | export * from './src/date-picker' 3 | -------------------------------------------------------------------------------- /packages/components/src/date-picker/src/date-picker.ts: -------------------------------------------------------------------------------- 1 | import { datePickerProps } from 'element-plus' 2 | 3 | import type { ExtractPropTypes } from 'vue' 4 | 5 | export const cxzDatePickerProp = { 6 | ...datePickerProps, 7 | beforeDisabledDate: { 8 | type: [String, Number, Date] 9 | }, 10 | afterDisabledDate: { 11 | type: [String, Number, Date] 12 | }, 13 | dateRangeLimit: { 14 | type: Number 15 | } 16 | } 17 | 18 | export type CxzDatePickerProp = ExtractPropTypes 19 | -------------------------------------------------------------------------------- /packages/components/src/date-picker/src/date-picker.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 103 | -------------------------------------------------------------------------------- /packages/components/src/ellipsis/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzEllipsis } from './src/ellipsis.vue' 2 | export * from './src/ellipsis' 3 | -------------------------------------------------------------------------------- /packages/components/src/ellipsis/src/ellipsis.ts: -------------------------------------------------------------------------------- 1 | import type { ExtractPropTypes, PropType, StyleValue } from 'vue' 2 | import type { ElTooltipProps } from 'element-plus' 3 | 4 | type TooltipProps = Partial< 5 | Omit< 6 | ElTooltipProps, 7 | | 'disabled' 8 | | 'content' 9 | | 'visible' 10 | | 'manual' 11 | | 'virtualTriggering' 12 | | 'virtualRef' 13 | > 14 | > 15 | 16 | export const defaultTooltipProps: TooltipProps = { 17 | placement: 'auto', 18 | enterable: true 19 | } 20 | 21 | export const ellipsisProps = { 22 | content: { 23 | type: String 24 | }, 25 | tooltipProps: { 26 | type: Object as PropType, 27 | default: () => ({}) 28 | }, 29 | lineClamp: { 30 | type: Number, 31 | default: 1 32 | }, 33 | maxWidth: { 34 | type: [Number, String], 35 | default: 'auto' 36 | }, 37 | style: { 38 | type: [Object, String] as PropType, 39 | default: () => ({}) 40 | }, 41 | class: { 42 | type: String 43 | } 44 | } 45 | 46 | export type CxzEllipsisProps = ExtractPropTypes 47 | -------------------------------------------------------------------------------- /packages/components/src/ellipsis/src/ellipsis.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 85 | -------------------------------------------------------------------------------- /packages/components/src/ellipsis/style/index.scss: -------------------------------------------------------------------------------- 1 | .cxz-ellipsis { 2 | --cxz-ellipsis-max-width: 100%; 3 | --cxz-ellipsis-line-clamp: 1; 4 | 5 | max-width: var(--cxz-ellipsis-max-width); 6 | overflow: hidden; 7 | text-overflow: ellipsis; 8 | display: -webkit-inline-box; 9 | -webkit-line-clamp: var(--cxz-ellipsis-line-clamp); 10 | -webkit-box-orient: vertical; 11 | word-break: break-all; 12 | } 13 | -------------------------------------------------------------------------------- /packages/components/src/form/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzForm } from './src/form.vue' 2 | 3 | export * from './src/form' 4 | -------------------------------------------------------------------------------- /packages/components/src/form/src/form.ts: -------------------------------------------------------------------------------- 1 | import { formProps, rowProps } from 'element-plus' 2 | 3 | import type { PropType, ExtractPropTypes } from 'vue' 4 | import type { FormItemProps, ColProps } from 'element-plus' 5 | 6 | export type CxzFormSchema = Partial> & { 7 | prop: string 8 | layout?: layoutColProps 9 | slot?: string 10 | labelSlot?: string 11 | errorSlot?: string 12 | component?: any 13 | componentAttrs?: Record 14 | } 15 | type layoutRowProps = Partial> 16 | type layoutColProps = Partial 17 | 18 | export const cxzFormProps = { 19 | ...formProps, 20 | schema: { 21 | type: Array as PropType, 22 | default: () => [] 23 | }, 24 | layoutRow: { 25 | type: Object as PropType, 26 | default: () => ({ gutter: 0 }) 27 | }, 28 | layoutCol: { 29 | type: Object as PropType, 30 | default: () => ({ span: 24 }) 31 | }, 32 | modelValue: { 33 | type: Object as PropType>, 34 | default: () => ({}) 35 | }, 36 | showCount: { 37 | type: Number, 38 | default: Infinity 39 | } 40 | } 41 | 42 | export type CxzFormProps = Partial> 43 | -------------------------------------------------------------------------------- /packages/components/src/form/src/form.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 83 | -------------------------------------------------------------------------------- /packages/components/src/image/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzImage } from './src/image.vue' 2 | 3 | export * from './src/image' 4 | -------------------------------------------------------------------------------- /packages/components/src/image/src/image-viewer.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 110 | -------------------------------------------------------------------------------- /packages/components/src/image/src/image.ts: -------------------------------------------------------------------------------- 1 | import { imageProps, imageEmits } from 'element-plus' 2 | import { iconPropType } from 'element-plus/es/utils/index' 3 | import { ZoomIn, Download } from '@element-plus/icons-vue' 4 | 5 | import type { ExtractPropTypes } from 'vue' 6 | 7 | export const cxzImageProps = { 8 | ...imageProps, 9 | previewTeleported: { 10 | type: Boolean, 11 | default: true 12 | }, 13 | initialIndex: { 14 | type: Number 15 | }, 16 | withPreview: { 17 | type: Boolean, 18 | default: false 19 | }, 20 | previewIcon: { 21 | type: iconPropType, 22 | default: ZoomIn 23 | }, 24 | withDownload: { 25 | type: Boolean, 26 | default: false 27 | }, 28 | downloadIcon: { 29 | type: iconPropType, 30 | default: Download 31 | }, 32 | fileName: { 33 | type: String 34 | } 35 | } 36 | 37 | export const cxzImageEmit = { 38 | ...imageEmits 39 | } 40 | 41 | export type CxzImageProps = ExtractPropTypes 42 | -------------------------------------------------------------------------------- /packages/components/src/image/src/image.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 101 | -------------------------------------------------------------------------------- /packages/components/src/image/style/index.scss: -------------------------------------------------------------------------------- 1 | .cxz-image { 2 | position: relative; 3 | display: inline-flex; 4 | overflow: hidden; 5 | 6 | &-actions { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | left: 0; 11 | top: 0; 12 | cursor: default; 13 | display: inline-flex; 14 | justify-content: center; 15 | align-items: center; 16 | color: #fff; 17 | opacity: 0; 18 | font-size: 20px; 19 | background-color: var(--el-overlay-color-lighter); 20 | transition: opacity var(--el-transition-duration); 21 | 22 | &:hover { 23 | opacity: 1; 24 | } 25 | 26 | .el-icon { 27 | cursor: pointer; 28 | } 29 | } 30 | 31 | .el-image { 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | &-empty { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | width: 100%; 42 | border-radius: 1px; 43 | height: 100%; 44 | background: var(--el-fill-color-light); 45 | color: var(--el-text-color-secondary); 46 | font-size: 16px; 47 | 48 | .desc { 49 | font-size: 8px; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/components/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select' 2 | export * from './ellipsis' 3 | export * from './button' 4 | export * from './date-picker' 5 | export * from './image' 6 | export * from './table' 7 | export * from './form' 8 | export * from './pro-table' 9 | 10 | import './styles.ts' 11 | -------------------------------------------------------------------------------- /packages/components/src/pro-table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzProTable } from './src/pro-table.vue' 2 | 3 | export * from './src/use-pro-table' 4 | export * from './src/pro-table' 5 | -------------------------------------------------------------------------------- /packages/components/src/pro-table/src/pro-table.ts: -------------------------------------------------------------------------------- 1 | import type { PropType, ExtractPropTypes } from 'vue' 2 | import type { paginationProps } from 'element-plus' 3 | import type { ProTableInitFunctionParams } from './use-pro-table' 4 | 5 | type PaginationProps = Omit< 6 | Partial>, 7 | 'currentPage' | 'pageSize' | 'total' 8 | > & 9 | Record 10 | 11 | export const cxzProTableProps = { 12 | init: { 13 | type: Function as PropType<(val: ProTableInitFunctionParams) => void>, 14 | default: () => ({}) 15 | }, 16 | showCount: { 17 | type: Number, 18 | default: 2 19 | }, 20 | pagination: { 21 | type: Object as PropType, 22 | default: () => ({ 23 | layout: 'total, sizes, prev, pager, next, jumper', 24 | pagerCount: 5, 25 | pageSizes: [10, 20, 50, 100] 26 | }) 27 | } 28 | } 29 | 30 | export type CxzProTableProps = Partial< 31 | ExtractPropTypes 32 | > 33 | -------------------------------------------------------------------------------- /packages/components/src/pro-table/src/pro-table.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 235 | -------------------------------------------------------------------------------- /packages/components/src/pro-table/src/use-pro-table.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | 3 | import type { CxzTableProps } from '../../table' 4 | import type { CxzFormProps } from '../../form' 5 | 6 | interface CxzProTableFormProps 7 | extends Omit { 8 | defaultValue?: Record 9 | } 10 | type CxzProTableTableProps = Omit 11 | 12 | export interface UseProTableGetListParams extends Record { 13 | page: number 14 | pageSize: number 15 | } 16 | export interface UseProTableGetListRes extends Record { 17 | data: T[] 18 | page: number 19 | pageCount: number 20 | itemCount: number 21 | } 22 | export interface UseProTableGetList { 23 | (params: UseProTableGetListParams): Promise> 24 | } 25 | 26 | export type UseProTableParam = { 27 | form?: CxzProTableFormProps 28 | table?: CxzProTableTableProps 29 | getList: UseProTableGetList 30 | manual?: boolean 31 | } 32 | 33 | export const proTableDefaultState = { 34 | loading: false, 35 | formValue: {}, 36 | data: [] as any[], 37 | pagination: { 38 | page: 1, 39 | pageSize: 10, 40 | defaultPage: 1, 41 | defaultPageSize: 10, 42 | pageCount: 0, 43 | itemCount: 0 44 | } 45 | } 46 | 47 | export type ProTableState = typeof proTableDefaultState 48 | export type ProTableInnerProps = { 49 | formProps?: CxzProTableFormProps 50 | tableProps?: CxzProTableTableProps 51 | } 52 | export type ProTableInnerMethod = { 53 | onRefresh?: () => void 54 | onReset?: () => void 55 | } 56 | export type ProTableInitFunctionParams = { 57 | bindState: (val: ProTableState) => void 58 | bindProps: (val: ProTableInnerProps) => void 59 | bindMethod: (val: ProTableInnerMethod) => void 60 | } 61 | 62 | export function useProTable(param: UseProTableParam) { 63 | const state = reactive( 64 | JSON.parse(JSON.stringify(proTableDefaultState)) 65 | ) 66 | 67 | if (param.form?.defaultValue) { 68 | Object.assign(state.formValue, param.form.defaultValue) 69 | } 70 | 71 | async function getTableData(filter: Record = {}) { 72 | try { 73 | state.loading = true 74 | const res = await param.getList({ 75 | ...filter, 76 | page: state.pagination.page, 77 | pageSize: state.pagination.pageSize 78 | }) 79 | state.data = res.data 80 | state.pagination.page = res.page 81 | state.pagination.pageCount = res.pageCount 82 | state.pagination.itemCount = res.itemCount 83 | } catch (e) { 84 | state.data = [] 85 | } finally { 86 | state.loading = false 87 | } 88 | } 89 | 90 | const defaultFormProps: CxzProTableFormProps = { 91 | layoutRow: { gutter: 20 }, 92 | layoutCol: { span: 8 }, 93 | labelWidth: 'auto' 94 | } 95 | 96 | const onRefresh = () => { 97 | getTableData(state.formValue) 98 | } 99 | 100 | const onReset = () => { 101 | state.formValue = JSON.parse(JSON.stringify(param.form?.defaultValue ?? {})) 102 | state.pagination.page = 1 103 | onRefresh() 104 | } 105 | 106 | const init = (params: ProTableInitFunctionParams) => { 107 | if (!param.manual) { 108 | state.pagination.page = 1 109 | onReset() 110 | } 111 | params.bindState(state) 112 | params.bindProps({ 113 | formProps: { ...defaultFormProps, ...param.form }, 114 | tableProps: param.table 115 | }) 116 | params.bindMethod({ 117 | onRefresh, 118 | onReset 119 | }) 120 | } 121 | 122 | return { 123 | init, 124 | refresh: onRefresh, 125 | state 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/components/src/pro-table/style/index.scss: -------------------------------------------------------------------------------- 1 | .cxz-pro-table { 2 | &__filter { 3 | &-btns { 4 | text-align: right; 5 | flex: 1; 6 | } 7 | } 8 | 9 | &__main { 10 | &-pagination { 11 | display: flex; 12 | justify-content: flex-end; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/components/src/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzSelect } from './src/select.vue' 2 | 3 | export * from './src/props' 4 | -------------------------------------------------------------------------------- /packages/components/src/select/src/content.vue: -------------------------------------------------------------------------------- 1 | 128 | 129 | 161 | -------------------------------------------------------------------------------- /packages/components/src/select/src/list-item-group.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /packages/components/src/select/src/list-item-option.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 59 | -------------------------------------------------------------------------------- /packages/components/src/select/src/list-item.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 97 | -------------------------------------------------------------------------------- /packages/components/src/select/src/list.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 89 | -------------------------------------------------------------------------------- /packages/components/src/select/src/props.ts: -------------------------------------------------------------------------------- 1 | import { ArrowDown, CircleClose } from '@element-plus/icons-vue' 2 | import { iconPropType } from 'element-plus/es/utils/index' 3 | import { tagProps } from 'element-plus' 4 | 5 | import type { ExtractPropTypes, PropType } from 'vue' 6 | import type { Option, Config } from './select.types' 7 | 8 | export const selectProps = { 9 | // base 10 | modelValue: [Array, String, Number, Boolean, Object] as PropType< 11 | any[] | string | number | boolean | Record | any 12 | >, 13 | disabled: Boolean, 14 | multiple: Boolean, 15 | props: { 16 | type: Object as PropType> 17 | }, 18 | options: { 19 | type: Array as PropType, 20 | default: () => [] 21 | }, 22 | 23 | // tooltip 24 | popperClass: { 25 | type: String, 26 | default: '' 27 | }, 28 | 29 | // select input 30 | name: String, 31 | placeholder: { 32 | type: String, 33 | default: '请选择' 34 | }, 35 | suffixIcon: { 36 | type: iconPropType, 37 | default: ArrowDown 38 | }, 39 | clearable: { 40 | type: Boolean, 41 | default: true 42 | }, 43 | clearIcon: { 44 | type: iconPropType, 45 | default: CircleClose 46 | }, 47 | 48 | // content 49 | collapseTags: Boolean, 50 | collapseTagsTooltip: Boolean, 51 | // eslint-disable-next-line vue/require-prop-types 52 | tagType: { ...tagProps.type, default: 'info' }, 53 | tagsHeight: [String, Number], 54 | 55 | // Options 56 | // commom 57 | loading: Boolean, 58 | loadingText: { 59 | type: String, 60 | default: '加载中...' 61 | }, 62 | emptyText: { 63 | type: String, 64 | default: '暂无数据' 65 | }, 66 | remote: Boolean, 67 | remoteMethod: Function, 68 | filterable: Boolean, 69 | filterMethod: Function, 70 | beforeSelect: { 71 | type: Function as PropType< 72 | ( 73 | value: any, 74 | option: Record, 75 | isSelect: boolean 76 | ) => Promise | boolean 77 | > 78 | }, 79 | // list 80 | fitInputWidth: { 81 | type: Boolean, 82 | default: false 83 | }, 84 | multipleLimit: { 85 | type: Number, 86 | default: 0 87 | }, 88 | multipleLimitText: { 89 | type: String, 90 | default: '选择个数超过限制' 91 | }, 92 | itemHeight: { 93 | type: Number, 94 | default: 32 95 | }, 96 | height: { 97 | type: Number, 98 | default: 192 99 | }, 100 | estimatedOptionHeight: Number, 101 | scrollbarAlwaysOn: Boolean, 102 | validateEvent: { 103 | type: Boolean, 104 | default: true 105 | }, 106 | isObject: Boolean, 107 | 108 | // 用于回显 109 | valueOptions: { 110 | type: Array as PropType, 111 | default: () => [] 112 | } 113 | } 114 | 115 | export type CxzSelectProps = ExtractPropTypes 116 | -------------------------------------------------------------------------------- /packages/components/src/select/src/select-dropdown.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /packages/components/src/select/src/select.types.ts: -------------------------------------------------------------------------------- 1 | export type Option = Record 2 | export type Config = { 3 | value: string 4 | label: string 5 | disabled: string 6 | children: string 7 | type: string 8 | } 9 | -------------------------------------------------------------------------------- /packages/components/src/select/src/select.vue: -------------------------------------------------------------------------------- 1 | 138 | 139 | 246 | -------------------------------------------------------------------------------- /packages/components/src/select/src/token.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey, Ref } from 'vue' 2 | import type { TooltipInstance } from 'element-plus' 3 | import type { CxzSelectProps } from './props' 4 | 5 | import { Config } from './select.types' 6 | 7 | export interface SelectContext { 8 | props: CxzSelectProps 9 | selectRef: HTMLElement 10 | popperRef: Ref 11 | expanded: boolean 12 | selectedMaps: Map 13 | query: string 14 | config: Config 15 | disabled: boolean 16 | selectSize: string 17 | inputWidth: number 18 | setSoftFocus: () => void 19 | toggleMenu: () => void 20 | } 21 | 22 | export const selectInjectionKey: InjectionKey = 23 | Symbol('CxzSelectInjection') 24 | -------------------------------------------------------------------------------- /packages/components/src/select/src/use-select-list.ts: -------------------------------------------------------------------------------- 1 | import { computed, nextTick, ref, watch } from 'vue' 2 | import { flattenOptions, isValidOption } from './util' 3 | import { isEmpty, isFunction } from 'element-plus/es/utils/index' 4 | import { difference, isPlainObject, isArray } from 'lodash-es' 5 | import { ElMessage } from 'element-plus' 6 | 7 | import type { Option } from './select.types' 8 | import type { SelectContext } from './token' 9 | 10 | export function useSelectList(select: SelectContext) { 11 | const listRef = ref() 12 | const hoveringIndex = ref(-1) 13 | 14 | const filteredOptions = computed(() => { 15 | const { options, loading } = select.props 16 | const { config, query } = select 17 | if (loading) { 18 | return [] 19 | } 20 | const temp = options 21 | .map((v) => { 22 | if (isArray(v[config.children])) { 23 | const filtered = v[config.children].filter((el: Option) => 24 | isValidOption(el, config, query) 25 | ) 26 | if (filtered.length > 0) { 27 | return { 28 | ...v, 29 | [config.children]: filtered 30 | } 31 | } 32 | } else if (isValidOption(v, config, query)) { 33 | return v 34 | } 35 | return null 36 | }) 37 | .filter(Boolean) as Option[] 38 | 39 | return flattenOptions(temp, config) 40 | }) 41 | 42 | const optionsAllDisabled = computed(() => 43 | filteredOptions.value.every((option) => option.disabled) 44 | ) 45 | 46 | const updateHoveringIndex = (idx: number) => { 47 | hoveringIndex.value = idx 48 | } 49 | 50 | const scrollToItem = (index: number) => { 51 | listRef.value?.scrollToItem(index) 52 | } 53 | 54 | const indexRef = computed(() => { 55 | const { multiple, modelValue } = select.props 56 | const { config } = select 57 | if (multiple) { 58 | const len = (modelValue as [])?.length 59 | if (len) { 60 | return filteredOptions.value.findIndex( 61 | (o) => o[config.value] === modelValue[len - 1] 62 | ) 63 | } 64 | } else if (!isEmpty(modelValue)) { 65 | return filteredOptions.value.findIndex( 66 | (o) => o[config.value] === modelValue 67 | ) 68 | } 69 | return -1 70 | }) 71 | 72 | watch( 73 | () => select.expanded, 74 | (val) => { 75 | if (val) { 76 | nextTick(() => { 77 | if (~indexRef.value) { 78 | updateHoveringIndex(indexRef.value) 79 | scrollToItem(hoveringIndex.value) 80 | } 81 | }) 82 | } 83 | }, 84 | { 85 | immediate: true 86 | } 87 | ) 88 | 89 | const onKeyboardNavigate = ( 90 | direction: 'forward' | 'backward', 91 | index: number = hoveringIndex.value 92 | ) => { 93 | const options = filteredOptions.value 94 | const { disabled, config } = select 95 | 96 | if ( 97 | !['forward', 'backward'].includes(direction) || 98 | options.length <= 0 || 99 | disabled || 100 | optionsAllDisabled.value 101 | ) { 102 | return 103 | } 104 | if (!select.expanded) { 105 | select.toggleMenu() 106 | return 107 | } 108 | 109 | let newIndex = -1 110 | if (direction === 'forward') { 111 | newIndex = index + 1 112 | if (newIndex >= options.length) { 113 | newIndex = 0 114 | } 115 | } else if (direction === 'backward') { 116 | newIndex = index - 1 117 | if (newIndex < 0 || newIndex >= options.length) { 118 | newIndex = options.length - 1 119 | } 120 | } 121 | const option = options[newIndex] 122 | if (option[config.disabled] || option.type === 'Group') { 123 | onKeyboardNavigate(direction, newIndex) 124 | return 125 | } 126 | updateHoveringIndex(newIndex) 127 | scrollToItem(newIndex) 128 | } 129 | 130 | const onKeyboardSelect = () => { 131 | if (!select.expanded) { 132 | select.toggleMenu() 133 | } else if ( 134 | ~hoveringIndex.value && 135 | filteredOptions.value[hoveringIndex.value] 136 | ) { 137 | onSelect(filteredOptions.value[hoveringIndex.value]) 138 | } 139 | } 140 | 141 | const onHover = (val: any) => { 142 | hoveringIndex.value = val 143 | } 144 | 145 | const onSelect = async (val: Record) => { 146 | const { config, selectedMaps } = select 147 | const { beforeSelect, multiple, multipleLimitText } = select.props 148 | 149 | const key = select.props.isObject ? val : val[config.value] 150 | const hasSelected = selectedMaps.has(key) 151 | 152 | if (isFunction(beforeSelect)) { 153 | try { 154 | const before = await beforeSelect(key, { ...val }, !hasSelected) 155 | if (before === false) { 156 | return 157 | } 158 | } catch (error) { 159 | return 160 | } 161 | } 162 | 163 | if (multiple) { 164 | if (hasSelected) { 165 | selectedMaps.delete(key) 166 | } else if ( 167 | select.props.multipleLimit && 168 | selectedMaps.size >= select.props.multipleLimit 169 | ) { 170 | ElMessage.warning(multipleLimitText) 171 | } else { 172 | selectedMaps.set(key, { ...val }) 173 | } 174 | select.setSoftFocus() 175 | } else { 176 | if (!hasSelected) { 177 | selectedMaps.clear() 178 | selectedMaps.set(key, { ...val }) 179 | } 180 | select.expanded = false 181 | } 182 | } 183 | 184 | watch( 185 | () => select.props.modelValue, 186 | (val) => { 187 | const { selectedMaps, config } = select 188 | if (isEmpty(val)) { 189 | selectedMaps.clear() 190 | return 191 | } 192 | 193 | const temp = isArray(val) ? [...val] : [val] 194 | const toDelete = difference([...selectedMaps.keys()], temp) 195 | 196 | temp.forEach((el) => { 197 | const isObject = isPlainObject(el) 198 | const key = isObject ? el[config.value] : el 199 | if (isEmpty(key)) { 200 | return 201 | } 202 | const item = 203 | filteredOptions.value?.find((o) => o[config.value] === key) ?? // 从options中获取 204 | selectedMaps.get(el) ?? // 看看是否已经暂存 205 | select.props.valueOptions?.find((o) => o[config.value] === key) ?? // 从valueOptions中获取 206 | (isObject ? el : {}) // 从modelValue中获取 207 | 208 | selectedMaps.set(el, { ...item }) 209 | }) 210 | 211 | toDelete.forEach((el) => { 212 | selectedMaps.delete(el) 213 | }) 214 | }, 215 | { 216 | immediate: true 217 | } 218 | ) 219 | 220 | watch(filteredOptions, (val) => { 221 | if (!val?.length) { 222 | return 223 | } 224 | const { selectedMaps, config } = select 225 | 226 | for (const key of selectedMaps.keys()) { 227 | const item = val.find((o) => { 228 | const plainKey = isPlainObject(key) ? key[config.value] : key 229 | return o[config.value] === plainKey 230 | }) 231 | if (item) { 232 | selectedMaps.set(key, item) 233 | } 234 | } 235 | }) 236 | 237 | const listHeight = computed(() => { 238 | const { height, itemHeight } = select.props 239 | 240 | const totalHeight = filteredOptions.value.length * itemHeight 241 | return totalHeight > height ? height : totalHeight 242 | }) 243 | 244 | const emptyText = computed(() => { 245 | const { loading, loadingText, emptyText: text } = select.props 246 | if (loading) { 247 | return loadingText 248 | } 249 | if (filteredOptions.value.length === 0) { 250 | return text 251 | } 252 | return null 253 | }) 254 | 255 | return { 256 | listRef, 257 | filteredOptions, 258 | listHeight, 259 | emptyText, 260 | hoveringIndex, 261 | onKeyboardNavigate, 262 | onKeyboardSelect, 263 | onHover, 264 | onSelect 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /packages/components/src/select/src/use-select.ts: -------------------------------------------------------------------------------- 1 | import { computed, nextTick, reactive, ref, unref, watch } from 'vue' 2 | import { useFormItem, useSize, useNamespace } from 'element-plus' 3 | import { 4 | getComponentSize, 5 | isEmpty, 6 | isFunction 7 | } from 'element-plus/es/utils/index' 8 | import { useElementSize } from '@vueuse/core' 9 | import { debounce as lodashDebounce, isEqual, isArray } from 'lodash-es' 10 | 11 | import type { CxzSelectProps } from './props' 12 | import type { Config } from './select.types' 13 | 14 | export function useSelect(props: CxzSelectProps, emit: any, config: Config) { 15 | const selectRef = ref() 16 | const popperRef = ref() 17 | const popperPaneRef = computed(() => popperRef.value?.popperRef?.contentRef) 18 | const inputRef = ref() 19 | const optionsRef = ref() 20 | const filterRef = ref() 21 | const tagsRef = ref() 22 | const prefixRef = ref() 23 | 24 | const selectSize = useSize() 25 | const { form, formItem } = useFormItem() 26 | const { width: inputWidth } = useElementSize(inputRef) 27 | const { height: tagsHeight } = useElementSize(tagsRef) 28 | const { width: prefixWidth } = useElementSize(prefixRef) 29 | const nsSelect = useNamespace('select') 30 | 31 | const states = reactive({ 32 | expanded: false, // 是否展开 33 | mouseEnter: false, // 鼠标进入 34 | selectedMaps: new Map(), // 用来存储选中值的详细内容 35 | query: '', // 搜索框的值 36 | displayValue: '', // input框显示的值 37 | isFocus: false // 是否focus 38 | }) 39 | const filterValue = ref('') 40 | 41 | const selectDisabled = computed(() => !!(props.disabled || form?.disabled)) 42 | 43 | const showClose = computed( 44 | () => 45 | props.clearable && 46 | !selectDisabled.value && 47 | states.mouseEnter && 48 | !isEmpty(props.modelValue) 49 | ) 50 | 51 | const handleClickOutside = () => { 52 | states.expanded = false 53 | } 54 | 55 | const toggleMenu = () => { 56 | if (!selectDisabled.value) { 57 | states.expanded = !states.expanded 58 | } 59 | } 60 | 61 | watch( 62 | () => states.expanded, 63 | (val) => { 64 | nextTick(() => { 65 | if (val) { 66 | setFocus() 67 | } else { 68 | setBlur() 69 | } 70 | }) 71 | emit('visible-change', val) 72 | } 73 | ) 74 | 75 | const setFocus = () => { 76 | if (!states.isFocus) { 77 | states.isFocus = true 78 | if (props.filterable) { 79 | filterRef.value?.focus?.() 80 | } else { 81 | inputRef.value?.focus?.() 82 | } 83 | emit('focus') 84 | } 85 | } 86 | 87 | const setSoftFocus = () => { 88 | inputRef.value?.focus() 89 | } 90 | 91 | const setBlur = () => { 92 | if (states.isFocus) { 93 | states.isFocus = false 94 | filterRef.value?.blur?.() 95 | inputRef.value?.blur?.() 96 | filterValue.value = '' 97 | emit('blur') 98 | } 99 | } 100 | 101 | const emitChange = (val1: any[] = [], val2: Record[] = []) => { 102 | const value = props.multiple ? val1 : val1[0] ?? '' 103 | const option = props.multiple ? val2 : val2[0] ?? {} 104 | emit('update:modelValue', value) 105 | emit('change', value, option) 106 | 107 | if (props.validateEvent) { 108 | formItem?.validate('change').catch((err: any) => { 109 | console.warn(err) 110 | }) 111 | } 112 | } 113 | 114 | const handleClearClick = () => { 115 | emitChange() 116 | filterValue.value = '' 117 | emit('clear') 118 | } 119 | 120 | watch( 121 | () => states.selectedMaps.keys(), 122 | (val) => { 123 | const keys = [...val] 124 | const equal = props.multiple 125 | ? isEqual(keys, props.modelValue) 126 | : isEqual(keys[0] ?? '', props.modelValue ?? '') 127 | 128 | if (!equal) { 129 | const values = [...states.selectedMaps.values()] 130 | emitChange(keys, values) 131 | } 132 | } 133 | ) 134 | 135 | const currentPlaceholder = computed(() => { 136 | if (filterValue.value) { 137 | return '' 138 | } 139 | if (!isEmpty(props.modelValue)) { 140 | if (!props.multiple && states.isFocus && props.filterable) { 141 | return states.selectedMaps.get(props.modelValue)[config.label] 142 | } 143 | return '' 144 | } 145 | return props.placeholder 146 | }) 147 | 148 | const currentDisplayValue = computed(() => { 149 | if (props.multiple || isEmpty(props.modelValue)) { 150 | return '' 151 | } 152 | if (!props.multiple && states.isFocus && props.filterable) { 153 | return '' 154 | } 155 | const val = isArray(props.modelValue) 156 | ? props.modelValue[0] 157 | : props.modelValue 158 | return states.selectedMaps.get(val)?.[config.label] ?? val 159 | }) 160 | 161 | watch(currentDisplayValue, (val) => { 162 | // 解决回显文字被选中问题 163 | nextTick(() => { 164 | states.displayValue = val 165 | }) 166 | }) 167 | 168 | const debounce = computed(() => (props.remote ? 300 : 0)) 169 | const handleQueryChange = (val: string) => { 170 | if (props.remote && isFunction(props.remoteMethod)) { 171 | props.remoteMethod(val) 172 | } else if (isFunction(props.filterMethod)) { 173 | props.filterMethod(val) 174 | } else { 175 | states.query = val 176 | } 177 | } 178 | const debouncedQueryChange = lodashDebounce((e) => { 179 | handleQueryChange(e) 180 | }, debounce.value) 181 | 182 | watch(filterValue, (val) => { 183 | debouncedQueryChange(val) 184 | }) 185 | 186 | const onEscOrTab = () => { 187 | states.expanded = false 188 | } 189 | 190 | const onForward = () => { 191 | optionsRef.value?.onKeyboardNavigate('forward') 192 | } 193 | 194 | const onBackward = () => { 195 | optionsRef.value?.onKeyboardNavigate('backward') 196 | } 197 | 198 | const onKeyboardSelect = () => { 199 | optionsRef.value?.onKeyboardSelect() 200 | } 201 | 202 | watch(tagsHeight, () => { 203 | const input = inputRef.value.$el.querySelector('input') as HTMLInputElement 204 | const sizeInMap = getComponentSize(selectSize.value || form?.size) 205 | const height = Math.max( 206 | sizeInMap, 207 | tagsHeight.value + (tagsHeight.value > sizeInMap ? 6 : 0) 208 | ) 209 | input.style.height = `${height - 2}px` 210 | popperRef.value?.updatePopper?.() 211 | }) 212 | 213 | const wrapperKls = computed(() => { 214 | const classList = [nsSelect.b()] 215 | const _selectSize = unref(selectSize) 216 | if (_selectSize) { 217 | classList.push(nsSelect.m(_selectSize)) 218 | } 219 | if (props.disabled) { 220 | classList.push(nsSelect.m('disabled')) 221 | } 222 | return classList 223 | }) 224 | 225 | const selectTagsStyle = computed(() => ({ 226 | maxWidth: `${inputWidth.value - 32}px`, 227 | width: '100%' 228 | })) 229 | 230 | return { 231 | // DOM 232 | selectRef, 233 | popperRef, 234 | popperPaneRef, 235 | inputRef, 236 | optionsRef, 237 | filterRef, 238 | tagsRef, 239 | prefixRef, 240 | 241 | // readonly data 242 | states, 243 | inputWidth, 244 | selectSize, 245 | selectDisabled, 246 | showClose, 247 | currentPlaceholder, 248 | nsSelect, 249 | prefixWidth, 250 | 251 | // style 252 | wrapperKls, 253 | selectTagsStyle, 254 | 255 | filterValue, 256 | 257 | // method 258 | handleClickOutside, 259 | toggleMenu, 260 | handleClearClick, 261 | setSoftFocus, 262 | onEscOrTab, 263 | onForward, 264 | onBackward, 265 | onKeyboardSelect 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /packages/components/src/select/src/util.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from 'lodash-es' 2 | import { escapeStringRegexp } from 'element-plus/es/utils/index' 3 | 4 | import { Config } from './select.types' 5 | 6 | export const flattenOptions = ( 7 | options: Record[], 8 | config: Config 9 | ) => { 10 | const flattened: Record[] = [] 11 | options.forEach((option) => { 12 | if (isArray(option[config.children])) { 13 | flattened.push({ 14 | [config.label]: option[config.label], 15 | isTitle: true, 16 | [config.type]: 'Group' 17 | }) 18 | option[config.children].forEach((o: Record) => { 19 | flattened.push(o) 20 | }) 21 | flattened.push({ 22 | [config.type]: 'Group' 23 | }) 24 | } else { 25 | flattened.push(option) 26 | } 27 | }) 28 | 29 | return flattened 30 | } 31 | 32 | export function isValidOption( 33 | o: Record, 34 | config: Config, 35 | query?: string 36 | ) { 37 | const regexp = new RegExp(escapeStringRegexp(query), 'i') 38 | const containsQueryString = query ? regexp.test(o[config.label] || '') : true 39 | return containsQueryString 40 | } 41 | -------------------------------------------------------------------------------- /packages/components/src/select/style/index.scss: -------------------------------------------------------------------------------- 1 | .cxz-select { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /packages/components/src/styles.ts: -------------------------------------------------------------------------------- 1 | import './select/style/index.scss' 2 | import './ellipsis/style/index.scss' 3 | import './image/style/index.scss' 4 | import './pro-table/style/index.scss' 5 | -------------------------------------------------------------------------------- /packages/components/src/table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CxzTable } from './src/table.vue' 2 | 3 | export * from './src/table' 4 | -------------------------------------------------------------------------------- /packages/components/src/table/src/table-columns.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 58 | -------------------------------------------------------------------------------- /packages/components/src/table/src/table.ts: -------------------------------------------------------------------------------- 1 | import elTableProps from 'element-plus/es/components/table/src/table/defaults' 2 | import elTableColumnProps from 'element-plus/es/components/table/src/table-column/defaults' 3 | 4 | import type { PropType, ExtractPropTypes } from 'vue' 5 | import type { TableColumnCtx } from 'element-plus' 6 | 7 | type ElTableColumnProps = ExtractPropTypes 8 | type RenderHeader = { 9 | column: TableColumnCtx 10 | $index: number 11 | } 12 | type RenderDefault = RenderHeader & { 13 | row: T 14 | } 15 | 16 | export interface CxzTableColumn extends Partial { 17 | children?: CxzTableColumn[] 18 | render?: (data: RenderDefault) => any 19 | slot?: string 20 | headerSlot?: string 21 | } 22 | 23 | export const cxzTableProps = { 24 | ...elTableProps, 25 | columns: { 26 | type: Array as PropType, 27 | default: () => [] 28 | } 29 | } 30 | 31 | export type CxzTableProps = Partial> 32 | -------------------------------------------------------------------------------- /packages/components/src/table/src/table.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | -------------------------------------------------------------------------------- /packages/components/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "node", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "allowSyntheticDefaultImports": true 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /packages/components/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import dts from 'vite-plugin-dts' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue(), dts({ outputDir: ['../cxz-ui/es', '../cxz-ui/lib'] })], 8 | build: { 9 | outDir: 'es', 10 | rollupOptions: { 11 | external: ['vue', 'element-plus'], 12 | input: ['src/index.ts'], 13 | output: [ 14 | { 15 | format: 'es', 16 | //不用打包成.es.js,这里我们想把它打包成.js 17 | entryFileNames: '[name].mjs', 18 | //让打包目录和我们目录对应 19 | preserveModules: true, 20 | //配置打包根目录 21 | dir: '../cxz-ui/es' 22 | }, 23 | { 24 | format: 'cjs', 25 | entryFileNames: '[name].js', 26 | //让打包目录和我们目录对应 27 | preserveModules: true, 28 | //配置打包根目录 29 | dir: '../cxz-ui/lib' 30 | } 31 | ] 32 | }, 33 | lib: { 34 | entry: './src/index.ts' 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /packages/cxz-ui/README.md: -------------------------------------------------------------------------------- 1 | # cxz-ui 2 | 3 | 基于 Element Plus 组件库二次封装 4 | 5 | ## 准备工作 6 | 7 | 项目中需要先安装[Vue3](https://cn.vuejs.org/)、[Element Plus](https://element-plus.org/zh-CN/guide/design.html) 8 | 9 | ## 安装 10 | 11 | ```bash 12 | pnpm i cxz-ui 13 | ``` 14 | 15 | ## 使用 16 | 17 | - 在入口文件中导入样式 18 | 19 | ```js 20 | import 'cxz-ui/es/style.css' 21 | ``` 22 | 23 | - 使用组件 24 | 25 | ```vue 26 | 32 | 33 | 43 | ``` 44 | -------------------------------------------------------------------------------- /packages/cxz-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cxz-ui", 3 | "version": "0.0.10", 4 | "main": "lib/packages/components/src/index.js", 5 | "module": "es/packages/components/src/index.mjs", 6 | "types": "es/packages/components/src/index.d.ts", 7 | "files": [ 8 | "es", 9 | "lib" 10 | ], 11 | "sideEffects": [ 12 | "**/*.css" 13 | ], 14 | "keywords": [ 15 | "cxz-ui", 16 | "vue3组件库", 17 | "element-plus" 18 | ], 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "element-plus": "^2.3.6", 22 | "vue": "^3.2.47" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/cxzhub/cxz-ui.git" 27 | }, 28 | "homepage": "https://cxzhub.github.io/cxz-ui/", 29 | "bugs": { 30 | "url": "https://github.com/cxzhub/cxz-ui/issues" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/utils/download.ts: -------------------------------------------------------------------------------- 1 | export function download(fileUrl: string, fileName?: string) { 2 | const pathArr = fileUrl.split('/') 3 | const defaultFileName = pathArr[pathArr.length - 1] 4 | 5 | if ((window as any).fetch) { 6 | fetch(fileUrl) 7 | .then((res) => res.blob()) 8 | .then((blob) => { 9 | const href = window.URL.createObjectURL(blob) 10 | downloadFile(href, fileName || defaultFileName) 11 | }) 12 | } else { 13 | downloadFile(fileUrl, fileName || defaultFileName) 14 | } 15 | } 16 | 17 | export function downloadFile(href: string, filename: string) { 18 | // create "a" HTML element with href to file & click 19 | const link = document.createElement('a') 20 | link.target = '__blank' 21 | link.href = href 22 | link.setAttribute('download', filename) 23 | document.body.appendChild(link) 24 | link.click() 25 | 26 | // clean up "a" element & remove ObjectURL 27 | document.body.removeChild(link) 28 | URL.revokeObjectURL(href) 29 | } 30 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './download' 2 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cxz-ui/utils", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - docs 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "allowJs": true, 16 | "baseUrl": "./", 17 | "paths": { 18 | "cxz-ui": ["packages/components"] 19 | } 20 | }, 21 | "include": [ 22 | "packages/**/*.ts", 23 | "packages/**/*.d.ts", 24 | "packages/**/*.tsx", 25 | "packages/**/*.vue", 26 | "docs/**/*.vue", 27 | "docs/**/*.ts" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------