diff --git a/.vscode/settings.json b/.vscode/settings.json index 2af112a1..e143b663 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,7 @@ "domtoimage", "EDITMSG", "iife", + "internalkey", "linebreak", "macarons", "menutag", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4934214e..3ab0d7b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,118 @@ # CHANGE LOG +## 4.7.4 + +对于 `RTable`, `RForm`, `RChart` 组件都新增了对应的 `useTable`, `useForm`, `useChart` 方法;让你在业务开发中抛弃注册 `ref` 实例方式调用组件方法。 + +补充拓展了 `useModal` 方法,支持 `dad`, `fullscreen` 等拓展配置项。 + +```ts +import { useTable, useForm } from '@/components' + +const [registerTable, { getTableInstance }] = useTable() +const [registerForm, { getFormInstance }] = useForm() + +// 做点什么... +``` + +## Feats + +- 更新 `vite` 版本至 `5.2.6` +- `useDevice` 方法支持自定义 `media` 配置项,用于配置自定义媒体查询尺寸 +- `RTable` 组件 + - 新增 `tool` 配置项,配置是否显示工具栏 + - 优化工具栏渲染逻辑 + - 新增 `useTable` 方法,用于便捷调用表格方法 + +> 该方法比起常见的 `ref` 注册,然后 `tableRef.value.xxx` 的方法获取表格方法更为简洁一点。但是也值得注意的是,需要手动调用一次 `register` 方法,否则会报错;还有值得注意的是,需要注意表格方法的调用时机,需要等待表格注册完成后才能正常调用。如果需要在 `Parent Create` 阶段调用,可以尝试 `nextTick` 包裹一层。 + +```tsx +import { RTable } from '@/components' +import { useTable } from '@/components' + +defineComponent({ + setup() { + const [ + register, + { getTableInstance, clearFilters, clearSorter, scrollTo, filters, sort }, + ] = useTable() + + const columns = [ + { + title: 'No', + key: 'no', + }, + { + title: 'Title', + key: 'title', + }, + ] + const data = [ + { + no: 1, + title: 'title', + }, + ] + + return { + register, + getTableInstance, + clearFilters, + clearSorter, + scrollTo, + filters, + sort, + columns, + data, + } + }, + render() { + const { columns, data } = this + const { register } = this + + return ( + + ) + }, +}) +``` + +- `RForm` 组件 + - 新增组件,所有行为、方法与 `NForm` 保持一致 + - `useForm` 方法,使用方法与 `useTable` 几乎一致 +- `canUseDom`, `isBrowser` 方法统一为函数导出 +- `RModal` 组件新增 `useModal` 方法 + - 新增 `useModal` 方法,允许拓展配置 `dad`, `fullscreen` 等配置项。但是由于 `useModal` 生成组件的特殊性,不支持 `memo` 属性配置,其余配置项维持一致 + > 该方法在当前版本存在一个 bug,`preset = card` 时,不能正确的显示 content,查看相应的 [issues](https://github.com/tusen-ai/naive-ui/issues/5746)。 + - 重写部分代码,优化组件逻辑,补全 `ts` 类型 +- `RChart` + - 新增 `useChart` 方法,使用方法与 `useTable` 几乎一致 +- 新增 `usePagination` 方法与 `usePagination.spec` 单元测试模块 + +```ts +import { usePagination } from '@/hooks' + +const { + updatePage, + updatePageSize, + getItemCount, + setItemCount, + getPage, + setPage, + getPageSize, + setPageSize, + getPagination, + getCallback, +} = usePagination( + () => { + // do something... + }, + { + // ...options + }, +) +``` + ## 4.7.3 补全 `hooks` 包下的单测模块。 diff --git a/__test__/hooks/usePagination.spec.ts b/__test__/hooks/usePagination.spec.ts new file mode 100644 index 00000000..e6e91fc0 --- /dev/null +++ b/__test__/hooks/usePagination.spec.ts @@ -0,0 +1,71 @@ +import { usePagination } from '../../src/hooks/web/usePagination' + +describe('usePagination', () => { + let count = 0 + const defaultOptions = { + itemCount: 200, + page: 1, + pageSize: 10, + } + + const { + getItemCount, + getCallback, + getPage, + getPageSize, + getPagination, + setItemCount, + setPage, + setPageSize, + } = usePagination(() => { + count++ + }, defaultOptions) + + it('should get current itemCount', () => { + setItemCount(200) + + expect(getItemCount()).toBe(200) + + setItemCount(100) + + expect(getItemCount()).toBe(100) + }) + + it('should get current page', () => { + setPage(1) + + expect(getPage()).toBe(1) + }) + + it('should get current pageSize', () => { + setPageSize(10) + + expect(getPageSize()).toBe(10) + }) + + it('should get current pagination', () => { + setItemCount(200) + + expect(getPagination()).toMatchObject(defaultOptions) + }) + + it('should update count when page or pageSize changes', () => { + count = 0 + + setPage(2) + + expect(count).toBe(1) + + setPageSize(20) + + expect(count).toBe(2) + }) + + it('should get callback', () => { + count = 0 + + getCallback() + + expect(count).toBe(1) + }) +}) diff --git a/__test__/utils/canUseDom.ts b/__test__/utils/canUseDom.ts index c14d0fbf..aacc8c56 100644 --- a/__test__/utils/canUseDom.ts +++ b/__test__/utils/canUseDom.ts @@ -12,4 +12,5 @@ const canUseDom = () => { window.document.createElement ) } + export default canUseDom diff --git a/__test__/utils/isBrowser.ts b/__test__/utils/isBrowser.ts index 4500a4e6..33d59321 100644 --- a/__test__/utils/isBrowser.ts +++ b/__test__/utils/isBrowser.ts @@ -5,9 +5,11 @@ * * 如果是浏览器环境,则返回 true,否则返回 false。 */ -const isBrowser = !!( - typeof window !== 'undefined' && - window.document && - window.document.createElement -) +const isBrowser = () => + !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement + ) + export default isBrowser diff --git a/package.json b/package.json index fea9af27..9ae8f61f 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ray-template", "private": false, - "version": "4.7.3", + "version": "4.7.4", "type": "module", "engines": { "node": "^18.0.0 || >=20.0.0", @@ -96,7 +96,7 @@ "typescript": "^5.2.2", "unplugin-auto-import": "^0.17.5", "unplugin-vue-components": "^0.26.0", - "vite": "^5.1.6", + "vite": "^5.2.6", "vite-bundle-analyzer": "0.8.1", "vite-plugin-cdn2": "1.1.0", "vite-plugin-compression": "^0.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5b36ceb..186c7ac8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,10 +108,10 @@ devDependencies: version: 6.21.0(eslint@8.57.0)(typescript@5.2.2) '@vitejs/plugin-vue': specifier: ^5.0.4 - version: 5.0.4(vite@5.2.2)(vue@3.4.21) + version: 5.0.4(vite@5.2.6)(vue@3.4.21) '@vitejs/plugin-vue-jsx': specifier: ^3.1.0 - version: 3.1.0(vite@5.2.2)(vue@3.4.21) + version: 3.1.0(vite@5.2.6)(vue@3.4.21) '@vitest/ui': specifier: 1.4.0 version: 1.4.0(vitest@1.4.0) @@ -191,8 +191,8 @@ devDependencies: specifier: ^0.26.0 version: 0.26.0(vue@3.4.21) vite: - specifier: ^5.1.6 - version: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + specifier: ^5.2.6 + version: 5.2.6(@types/node@20.5.1)(sass@1.71.1) vite-bundle-analyzer: specifier: 0.8.1 version: 0.8.1 @@ -201,31 +201,31 @@ devDependencies: version: 1.1.0 vite-plugin-compression: specifier: ^0.5.1 - version: 0.5.1(vite@5.2.2) + version: 0.5.1(vite@5.2.6) vite-plugin-ejs: specifier: ^1.7.0 - version: 1.7.0(vite@5.2.2) + version: 1.7.0(vite@5.2.6) vite-plugin-eslint: specifier: 1.8.1 - version: 1.8.1(eslint@8.57.0)(vite@5.2.2) + version: 1.8.1(eslint@8.57.0)(vite@5.2.6) vite-plugin-imp: specifier: ^2.4.0 - version: 2.4.0(vite@5.2.2) + version: 2.4.0(vite@5.2.6) vite-plugin-inspect: specifier: ^0.8.3 - version: 0.8.3(vite@5.2.2) + version: 0.8.3(vite@5.2.6) vite-plugin-mock-dev-server: specifier: 1.4.7 - version: 1.4.7(vite@5.2.2) + version: 1.4.7(vite@5.2.6) vite-plugin-svg-icons: specifier: ^2.0.1 - version: 2.0.1(vite@5.2.2) + version: 2.0.1(vite@5.2.6) vite-svg-loader: specifier: ^4.0.0 version: 4.0.0 vite-tsconfig-paths: specifier: 4.3.2 - version: 4.3.2(typescript@5.2.2)(vite@5.2.2) + version: 4.3.2(typescript@5.2.2)(vite@5.2.6) vitest: specifier: 1.4.0 version: 1.4.0(@types/node@20.5.1)(@vitest/ui@1.4.0)(happy-dom@14.3.1)(sass@1.71.1) @@ -1816,7 +1816,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-vue-jsx@3.1.0(vite@5.2.2)(vue@3.4.21): + /@vitejs/plugin-vue-jsx@3.1.0(vite@5.2.6)(vue@3.4.21): resolution: {integrity: sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -1826,20 +1826,20 @@ packages: '@babel/core': 7.24.1 '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.24.1) '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.24.1) - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) vue: 3.4.21(typescript@5.2.2) transitivePeerDependencies: - supports-color dev: true - /@vitejs/plugin-vue@5.0.4(vite@5.2.2)(vue@3.4.21): + /@vitejs/plugin-vue@5.0.4(vite@5.2.6)(vue@3.4.21): resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 vue: ^3.2.25 dependencies: - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) vue: 3.4.21(typescript@5.2.2) dev: true @@ -7886,7 +7886,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - '@types/node' - less @@ -7912,7 +7912,7 @@ packages: - supports-color dev: true - /vite-plugin-compression@0.5.1(vite@5.2.2): + /vite-plugin-compression@0.5.1(vite@5.2.6): resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} peerDependencies: vite: '>=2.0.0' @@ -7920,21 +7920,21 @@ packages: chalk: 4.1.2 debug: 4.3.4 fs-extra: 10.1.0 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - supports-color dev: true - /vite-plugin-ejs@1.7.0(vite@5.2.2): + /vite-plugin-ejs@1.7.0(vite@5.2.6): resolution: {integrity: sha512-JNP3zQDC4mSbfoJ3G73s5mmZITD8NGjUmLkq4swxyahy/W0xuokK9U9IJGXw7KCggq6UucT6hJ0p+tQrNtqTZw==} peerDependencies: vite: '>=5.0.0' dependencies: ejs: 3.1.9 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) dev: true - /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.2): + /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.6): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' @@ -7944,10 +7944,10 @@ packages: '@types/eslint': 8.56.6 eslint: 8.57.0 rollup: 2.79.1 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) dev: true - /vite-plugin-imp@2.4.0(vite@5.2.2): + /vite-plugin-imp@2.4.0(vite@5.2.6): resolution: {integrity: sha512-L/6/nvOw+MyNh4UxAlCZHsmKd5MitmHamqqAWB15sbUgVIEz/OQ8jpKr6kkQU0eA/AIe8fkCVbQBlP81ajrqWg==} peerDependencies: vite: '>= 2.0.0-beta.5' @@ -7959,12 +7959,12 @@ packages: chalk: 4.1.2 param-case: 3.0.4 pascal-case: 3.1.2 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - supports-color dev: true - /vite-plugin-inspect@0.8.3(vite@5.2.2): + /vite-plugin-inspect@0.8.3(vite@5.2.6): resolution: {integrity: sha512-SBVzOIdP/kwe6hjkt7LSW4D0+REqqe58AumcnCfRNw4Kt3mbS9pEBkch+nupu2PBxv2tQi69EQHQ1ZA1vgB/Og==} engines: {node: '>=14'} peerDependencies: @@ -7983,13 +7983,13 @@ packages: perfect-debounce: 1.0.0 picocolors: 1.0.0 sirv: 2.0.4 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - rollup - supports-color dev: true - /vite-plugin-mock-dev-server@1.4.7(vite@5.2.2): + /vite-plugin-mock-dev-server@1.4.7(vite@5.2.6): resolution: {integrity: sha512-vGNW423fkmMibf0BfYL89n2n4tNKDt51d6Ee14gC1LlLiJAp6jabJBPsjWgU+uMgtp68+1uBb5F1qTlqdAhnoQ==} engines: {node: ^16 || ^18 || >= 20} peerDependencies: @@ -8011,7 +8011,7 @@ packages: mime-types: 2.1.35 path-to-regexp: 6.2.1 picocolors: 1.0.0 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) ws: 8.16.0 transitivePeerDependencies: - bufferutil @@ -8020,7 +8020,7 @@ packages: - utf-8-validate dev: true - /vite-plugin-svg-icons@2.0.1(vite@5.2.2): + /vite-plugin-svg-icons@2.0.1(vite@5.2.6): resolution: {integrity: sha512-6ktD+DhV6Rz3VtedYvBKKVA2eXF+sAQVaKkKLDSqGUfnhqXl3bj5PPkVTl3VexfTuZy66PmINi8Q6eFnVfRUmA==} peerDependencies: vite: '>=2.0.0' @@ -8033,7 +8033,7 @@ packages: pathe: 0.2.0 svg-baker: 1.7.0 svgo: 2.8.0 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - supports-color dev: true @@ -8045,7 +8045,7 @@ packages: svgo: 3.1.0 dev: true - /vite-tsconfig-paths@4.3.2(typescript@5.2.2)(vite@5.2.2): + /vite-tsconfig-paths@4.3.2(typescript@5.2.2)(vite@5.2.6): resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: vite: '*' @@ -8056,14 +8056,14 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.2.2) - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.2.2(@types/node@20.5.1)(sass@1.71.1): - resolution: {integrity: sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==} + /vite@5.2.6(@types/node@20.5.1)(sass@1.71.1): + resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -8144,7 +8144,7 @@ packages: strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.2.2(@types/node@20.5.1)(sass@1.71.1) + vite: 5.2.6(@types/node@20.5.1)(sass@1.71.1) vite-node: 1.4.0(@types/node@20.5.1)(sass@1.71.1) why-is-node-running: 2.2.2 transitivePeerDependencies: diff --git a/src/components/RChart/hooks/useChart.ts b/src/components/RChart/hooks/useChart.ts new file mode 100644 index 00000000..46d63134 --- /dev/null +++ b/src/components/RChart/hooks/useChart.ts @@ -0,0 +1,114 @@ +import type { ECharts } from 'echarts/core' +import type { VoidFC } from '@/types' + +/** + * + * @description + * 获取 RChart 实例。 + * 让你能够便捷的调用相关的一些已封装方法。 + * + * @warning + * 值得注意的是,必须手动调用 register 方法,否则不能正常使用。 + * 在使用 相关 hooks 的时候,需要注意生命周期,确保 register 方法已经被调用与表格实例已经被初始化; + * 不要在父组件 create 阶段就去调用 hook,如果需要,请使用 nextTick 包裹。 + * + * @example + * defineComponent({ + * setup() { + * const [register, { ...Hooks }] = useChart() + * + * return { + * register, + * ...Hooks, + * } + * }, + * render() { + * const { register, ...Hooks } = this + * + * return + * }, + * }) + */ +const useChart = () => { + let echartInst: ECharts + let _dispose: VoidFC + let _render: VoidFC + + /** + * + * @param inst echart instance + * + * @description + * 注册当前 echart 实例,用于使用 useChart hook。 + */ + const register = (inst: ECharts, render: VoidFC, dispose: VoidFC) => { + if (inst) { + echartInst = inst + _dispose = dispose + _render = render + } + } + + /** + * + * @description + * 获取当前 echart 实例。 + * + * 如果未在调用前注册 onRegister 事件,则会抛出异常。 + * + * @example + * const [register, { getChartInstance }] = useChart() + * + * const inst = getChartInstance() + */ + const getChartInstance = () => { + if (!echartInst) { + throw new Error( + '[useChart]: echart instance is not ready yet. if you are using useChart, please make sure you have called register method in onRegister event.', + ) + } + + return { + dispose: _dispose, + render: _render, + echartInst, + } + } + + /** + * + * @description + * chart 是否已经销毁。 + * 如果销毁则返回 true, 否则返回 false。 + */ + const isDispose = () => + !(echartInst && getChartInstance().echartInst.getDom()) + + /** + * + * @description + * 销毁当前 chart 实例。 + */ + const dispose = () => getChartInstance().dispose.call(null) + + /** + * + * @description + * 渲染当前 chart 实例。 + */ + const render = () => getChartInstance().render.call(null) + + return [ + register, + { + getChartInstance, + isDispose, + dispose, + render, + }, + ] as const +} + +export type UseChartReturn = ReturnType + +export default useChart diff --git a/src/components/RChart/index.ts b/src/components/RChart/index.ts index 76e91d32..5882e192 100644 --- a/src/components/RChart/index.ts +++ b/src/components/RChart/index.ts @@ -1,11 +1,12 @@ import RChart from './src' import chartProps from './src/props' +import useChart from './hooks/useChart' import type { ExtractPublicPropTypes } from 'vue' - import type * as RChartType from './src/types' +import type { UseChartReturn } from './hooks/useChart' export type ChartProps = ExtractPublicPropTypes -export type { RChartType } +export type { RChartType, UseChartReturn } -export { RChart, chartProps } +export { RChart, chartProps, useChart } diff --git a/src/components/RChart/src/index.tsx b/src/components/RChart/src/index.tsx index 9ca46fb2..ad3b444f 100644 --- a/src/components/RChart/src/index.tsx +++ b/src/components/RChart/src/index.tsx @@ -70,14 +70,18 @@ echartThemes.forEach((curr) => { * * @example * * * */ export default defineComponent({ name: 'RChart', @@ -348,6 +352,13 @@ export default defineComponent({ // 初始化完成后移除 intersectionObserver 监听 intersectionObserverReturn?.stop() + + // 注册 register,用于 useChart hook + const { onRegister } = props + + if (onRegister && echartInst) { + call(onRegister, echartInst, mount, unmount) + } } if (props.intersectionObserver) { diff --git a/src/components/RChart/src/props.ts b/src/components/RChart/src/props.ts index 55faa362..fc41f37f 100644 --- a/src/components/RChart/src/props.ts +++ b/src/components/RChart/src/props.ts @@ -1,3 +1,5 @@ +import { loadingOptions } from './utils' + import type * as echarts from 'echarts/core' // echarts 核心模块 import type { PropType, VNode } from 'vue' import type { MaybeArray } from '@/types' @@ -15,8 +17,7 @@ import type { RChartDownloadOptions, } from './types' import type { CardProps, DropdownProps, DropdownOption } from 'naive-ui' - -import { loadingOptions } from './utils' +import type { VoidFC } from '@/types' const props = { /** @@ -354,6 +355,20 @@ const props = { replaceMerge: [], }), }, + /** + * + * @description + * RChart 注册挂载成功后触发的事件。 + * 可以结合 useChart 方法中的 register 方法使用,然后便捷的使用 hooks。 + * + * @default null + */ + onRegister: { + type: [Function, Array] as PropType< + MaybeArray<(chartInst: ECharts, render: VoidFC, dispose: VoidFC) => void> + >, + default: null, + }, } export default props diff --git a/src/components/RForm/index.ts b/src/components/RForm/index.ts new file mode 100644 index 00000000..499e2d84 --- /dev/null +++ b/src/components/RForm/index.ts @@ -0,0 +1,12 @@ +import RForm from './src/Form' +import formProps from './src/props' +import useForm from './src/hooks/useForm' + +import type * as RFormType from './src/types' +import type { ExtractPublicPropTypes } from 'vue' +import type { UseFormReturn } from './src/hooks/useForm' + +export type FormProps = ExtractPublicPropTypes +export type { RFormType, UseFormReturn } + +export { RForm, formProps, useForm } diff --git a/src/components/RForm/src/Form.tsx b/src/components/RForm/src/Form.tsx new file mode 100644 index 00000000..b11ad432 --- /dev/null +++ b/src/components/RForm/src/Form.tsx @@ -0,0 +1,49 @@ +/** + * + * @author Ray + * + * @date 2024-03-27 + * + * @workspace ray-template + * + * @remark 今天也是元气满满撸代码的一天 + */ + +import { NForm } from 'naive-ui' + +import props from './props' +import { call } from '@/utils' + +import type { RFormInst } from './types' + +export default defineComponent({ + name: 'RForm', + props, + setup(props) { + const formRef = ref() + + onMounted(() => { + // 主动调用 register 方法,满足 useForm 方法正常调用 + const { onRegister } = props + + if (onRegister && formRef.value) { + call(onRegister, formRef.value) + } + }) + + return { + formRef, + } + }, + render() { + const { $attrs, $props, $slots } = this + + return ( + + {{ + ...$slots, + }} + + ) + }, +}) diff --git a/src/components/RForm/src/hooks/useForm.ts b/src/components/RForm/src/hooks/useForm.ts new file mode 100644 index 00000000..808d1942 --- /dev/null +++ b/src/components/RForm/src/hooks/useForm.ts @@ -0,0 +1,113 @@ +import { cloneDeep } from 'lodash-es' + +import type { + RFormInst, + FormValidateCallback, + ShouldRuleBeApplied, + RFormRules, +} from '../types' + +/** + * + * @description + * 获取 RForm 实例。 + * 让你能够便捷的调用相关的一些已封装方法。 + * + * @warning + * 值得注意的是,必须手动调用 register 方法,否则不能正常使用。 + * 在使用 相关 hooks 的时候,需要注意生命周期,确保 register 方法已经被调用与表格实例已经被初始化; + * 不要在父组件 create 阶段就去调用 hook,如果需要,请使用 nextTick 包裹。 + * + * @example + * defineComponent({ + * setup() { + * const [register, { ...Hooks }] = useForm() + * + * return { + * register, + * ...Hooks, + * } + * }, + * render() { + * const { register, ...Hooks } = this + * + * return + * }, + * }) + */ +const useForm = , R extends RFormRules>( + model?: T, + rules?: R, +) => { + const formRef = ref() + + const register = (inst: RFormInst) => { + if (inst) { + formRef.value = inst + } + } + + const getFormInstance = () => { + if (!formRef.value) { + throw new Error( + '[useForm]: form instance is not ready yet. if you are using useForm, please make sure you have called register method in onRegister event.', + ) + } + + return formRef.value + } + + /** + * + * @description + * 验证表单,Promise rejection 的返回值类型是 FormValidationError[]。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/form#inline.vue + */ + const validate = ( + callback?: FormValidateCallback, + shouldRuleBeApplied?: ShouldRuleBeApplied, + ) => getFormInstance().validate.call(null, callback, shouldRuleBeApplied) + + /** + * + * @description + * 还原表单到未验证状态。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/form#Form-Methods + */ + const restoreValidation = () => getFormInstance().restoreValidation.call(null) + + /** + * + * @description + * 获取表项中收集到的值的对象。 + * + * 调用该方法时,需要确保初始化 useForm 方法的时候传入了 model,否则可能有意想不到的问题发生。 + */ + const formModel = () => cloneDeep(model) || ({} as T) + + /** + * + * @description + * 获取验证表单项的规则。 + * + * 调用该方法时,需要确保初始化 useForm 方法的时候传入了 rules,否则可能有意想不到的问题发生。 + */ + const formRules = () => cloneDeep(rules) || ({} as R) + + return [ + register, + { + getFormInstance, + validate, + restoreValidation, + formModel, + formRules, + }, + ] as const +} + +export type UseFormReturn = ReturnType + +export default useForm diff --git a/src/components/RForm/src/props.ts b/src/components/RForm/src/props.ts new file mode 100644 index 00000000..d0c835aa --- /dev/null +++ b/src/components/RForm/src/props.ts @@ -0,0 +1,24 @@ +import { formProps } from 'naive-ui' + +import type { MaybeArray } from '@/types' +import type { RFormInst } from './types' + +const props = { + ...formProps, + /** + * + * @description + * RForm 注册挂载成功后触发的事件。 + * 可以结合 useForm 方法中的 register 方法使用,然后便捷的使用 hooks。 + * + * @default null + */ + onRegister: { + type: [Function, Array] as PropType< + MaybeArray<(formInst: RFormInst) => void> + >, + default: null, + }, +} + +export default props diff --git a/src/components/RForm/src/types.ts b/src/components/RForm/src/types.ts new file mode 100644 index 00000000..dcf4e8de --- /dev/null +++ b/src/components/RForm/src/types.ts @@ -0,0 +1,13 @@ +import type { FormInst, FormItemRule, FormRules } from 'naive-ui' + +export type RFormInst = FormInst + +type FormValidateParameters = Parameters + +export type FormValidateCallback = FormValidateParameters[0] + +export type ShouldRuleBeApplied = FormValidateParameters[1] + +export interface RFormRules { + [itemValidatePath: string]: FormItemRule | Array | FormRules +} diff --git a/src/components/RModal/index.ts b/src/components/RModal/index.ts index f8e679bb..582c747a 100644 --- a/src/components/RModal/index.ts +++ b/src/components/RModal/index.ts @@ -1,8 +1,12 @@ import RModal from './src/Modal' import modalProps from './src/props' +import useModal from './src/hooks/useModal' +import type * as RModalType from './src/types' import type { ExtractPublicPropTypes } from 'vue' +import type { UseModalReturn } from './src/hooks/useModal' export type ModalProps = ExtractPublicPropTypes +export type { RModalType, UseModalReturn } -export { RModal, modalProps } +export { RModal, modalProps, useModal } diff --git a/src/components/RModal/src/Modal.tsx b/src/components/RModal/src/Modal.tsx index 2cc8aaf1..b131f46b 100644 --- a/src/components/RModal/src/Modal.tsx +++ b/src/components/RModal/src/Modal.tsx @@ -15,8 +15,12 @@ import { NModal } from 'naive-ui' import props from './props' import { completeSize, uuid } from '@/utils' -import { useWindowSize } from '@vueuse/core' -import { setupDraggable } from './utils' +import { setupInteract } from './utils' +import { + FULLSCREEN_CARD_TYPE_CLASS, + R_MODAL_CLASS, + CSS_VARS_KEYS, +} from './constant' import type interact from 'interactjs' @@ -24,11 +28,10 @@ export default defineComponent({ name: 'RModal', props, setup(props) { - const { height } = useWindowSize() const cssVars = computed(() => ({ - '--r-modal-width': completeSize(props.width ?? 600), - '--r-modal-card-width': completeSize(props.cardWidth ?? 600), - '--r-modal-dialog-width': completeSize(props.dialogWidth ?? 446), + [CSS_VARS_KEYS['width']]: completeSize(props.width ?? 600), + [CSS_VARS_KEYS['cardWidth']]: completeSize(props.cardWidth ?? 600), + [CSS_VARS_KEYS['dialogWidth']]: completeSize(props.dialogWidth ?? 446), })) const uuidEl = uuid() let intractable: null | ReturnType @@ -37,32 +40,10 @@ export default defineComponent({ x: 0, y: 0, } - - /** - * - * 获取当前是否为 card 风格并且为全屏 - */ - const isFullscreenCardType = () => - props.preset === 'card' && props.fullscreen - - const setupInteract = () => { - const target = document.getElementById(uuidEl) - - if (target) { - setupDraggable(target, props.preset, { - scheduler: (event) => { - const target = event.target - - position.x += event.dx - position.y += event.dy - - target.style.transform = `translate(${position.x}px, ${position.y}px)` - }, - }).then((res) => { - intractable = res - }) - } - } + // 当前是否为预设 card 类型并且设置了 fullscreen + const isFullscreenCardType = computed( + () => props.preset === 'card' && props.fullscreen, + ) watch( () => props.show, @@ -73,10 +54,22 @@ export default defineComponent({ (props.preset === 'card' || props.preset === 'dialog') ) { nextTick(() => { - setupInteract() - const target = document.getElementById(uuidEl) + if (target) { + setupInteract(target, { + preset: props.preset, + x: position.x, + y: position.y, + dargCallback: (x, y) => { + position.x = x + position.y = y + }, + }).then((res) => { + intractable = res + }) + } + if (props.memo && target) { target.style.transform = `translate(${position.x}px, ${position.y}px)` } @@ -94,24 +87,22 @@ export default defineComponent({ return { cssVars, - height, isFullscreenCardType, uuidEl, } }, render() { - const { isFullscreenCardType } = this const { $props, $slots, $attrs } = this const { preset, ...$otherProps } = $props - const { cssVars, height, uuidEl } = this + const { cssVars, uuidEl, isFullscreenCardType } = this return ( {} + +const useModal = () => { + const { create: naiveCreate, destroyAll: naiveDestroyAll } = useNaiveModal() + + const create = (options: UseModalCreateOptions) => { + const { preset, dad, fullscreen, width, cardWidth, dialogWidth } = options + const modalReactive = naiveCreate(options) + const { key } = modalReactive + const cssVars = { + [CSS_VARS_KEYS['width']]: completeSize(width ?? 600), + [CSS_VARS_KEYS['cardWidth']]: completeSize(cardWidth ?? 600), + [CSS_VARS_KEYS['dialogWidth']]: completeSize(dialogWidth ?? 446), + } + + nextTick(() => { + const [modalElement] = + queryElements(`[internalkey="${key}"]`) || [] + + if (!modalElement) { + return + } + + if (dad) { + setupInteract(modalElement, { + preset, + x: 0, + y: 0, + }) + } + + if (fullscreen && preset === 'card') { + setStyle(modalElement, { + width: '100%', + height: '100vh', + }) + } + + setStyle(modalElement, cssVars) + setClass(modalElement, R_MODAL_CLASS) + }) + + return modalReactive + } + + return { + create, + destroyAll: naiveDestroyAll, + } +} + +export type UseModalReturn = ReturnType + +export default useModal diff --git a/src/components/RModal/src/props.ts b/src/components/RModal/src/props.ts index a1f48ade..007b87f1 100644 --- a/src/components/RModal/src/props.ts +++ b/src/components/RModal/src/props.ts @@ -38,7 +38,7 @@ const props = { /** * * @description - * preset 空时宽度设置。 + * preset 为空时宽度设置。 * * @default 600 */ diff --git a/src/components/RModal/src/types.ts b/src/components/RModal/src/types.ts new file mode 100644 index 00000000..5c4c8438 --- /dev/null +++ b/src/components/RModal/src/types.ts @@ -0,0 +1,53 @@ +import type { ModalOptions as NaiveModalOptions } from 'naive-ui' + +export interface RModalProps extends NaiveModalOptions { + /** + * + * @description + * 是否记住上一次的位置。 + * + * @default true + */ + memo?: boolean + /** + * + * @description + * 是否全屏。 + * + * @default false + */ + fullscreen?: boolean + /** + * + * @description + * preset 为空时宽度设置。 + * + * @default 600 + */ + width?: number + /** + * + * @description + * preset 为 card 时宽度设置。 + * + * @default 600 + */ + cardWidth?: number + /** + * + * @description + * preset 为 dialog 时宽度设置。 + * + * @default 446 + */ + dialogWidth?: number + /** + * + * @description + * 是否启用拖拽。 + * 当启用拖拽时,可以通过拖拽 header 部分控制模态框。 + * + * @default false + */ + dad?: boolean +} diff --git a/src/components/RModal/src/utils.ts b/src/components/RModal/src/utils.ts index af85d9ee..b40fc05f 100644 --- a/src/components/RModal/src/utils.ts +++ b/src/components/RModal/src/utils.ts @@ -1,10 +1,18 @@ import interact from 'interactjs' import type { ModalProps } from 'naive-ui' -import type { AnyFC } from '@/types' +import type { RModalProps } from './types' interface SetupDraggableOptions { - scheduler?: AnyFC + scheduler?: (event: Interact.DragEvent) => void +} + +interface SetupInteractOptions { + preset: ModalProps['preset'] + memo?: RModalProps['memo'] + x: number + y: number + dargCallback?: (x: number, y: number, event: Interact.DragEvent) => void } /** @@ -12,6 +20,7 @@ interface SetupDraggableOptions { * @param bindModal modal 预设元素 * @param preset 预设类型 * + * @description * 根据预设模态框设置拖拽效果 * 但是该效果有且仅有 card, dialog 有效 * @@ -54,3 +63,39 @@ export const setupDraggable = ( }, 30) }) } + +export const setupInteract = ( + target: HTMLElement | string, + options: SetupInteractOptions, +): Promise> => { + const _target = + typeof target === 'string' + ? (document.querySelector(target) as HTMLElement) + : target + + return new Promise((resolve, reject) => { + if (_target) { + _target.setAttribute('can-drag', 'true') + + const { preset, dargCallback } = options + let { x, y } = options + + setupDraggable(_target, preset, { + scheduler: (event) => { + const target = event.target + + x += event.dx + y += event.dy + + target.style.transform = `translate(${x}px, ${y}px)` + + dargCallback?.(x, y, event) + }, + }).then((res) => { + resolve(res) + }) + } else { + reject() + } + }) +} diff --git a/src/components/RTable/index.ts b/src/components/RTable/index.ts index bc875a17..2c10b725 100644 --- a/src/components/RTable/index.ts +++ b/src/components/RTable/index.ts @@ -1,10 +1,12 @@ import RTable from './src/Table' import tableProps from './src/props' +import useTable from './src/hooks/useTable' import type * as RTableType from './src/types' +import type { UseTableReturn } from './src/hooks/useTable' import type { ExtractPublicPropTypes } from 'vue' export type TableProps = ExtractPublicPropTypes -export type { RTableType } +export type { RTableType, UseTableReturn } -export { RTable, tableProps } +export { RTable, tableProps, useTable } diff --git a/src/components/RTable/src/Table.tsx b/src/components/RTable/src/Table.tsx index 0244470b..22ef8728 100644 --- a/src/components/RTable/src/Table.tsx +++ b/src/components/RTable/src/Table.tsx @@ -30,7 +30,7 @@ export default defineComponent({ name: 'RTable', props, setup(props, ctx) { - const { expose } = ctx + const { expose, emit } = ctx const rTableInst = ref() const wrapperRef = ref() @@ -161,8 +161,14 @@ export default defineComponent({ * 处理 toolOptions,合并渲染所有的 toolOptions */ const tool = (p: typeof props) => { + const { tool } = p + + if (!tool) { + return + } + const renderDefaultToolOptions = () => ( - <> + @@ -172,25 +178,34 @@ export default defineComponent({ onPopselectChange={popselectChange.bind(this)} onInitialed={popselectChange.bind(this)} /> - + ) if (!props.toolOptions) { return renderDefaultToolOptions } else { if (props.coverTool) { - return renderToolOptions + return {renderToolOptions()} } else { return () => ( - <> + {renderDefaultToolOptions()} {renderToolOptions()} - + ) } } } + onMounted(() => { + // 主动调用 register 方法,满足 useTable 方法正常调用 + const { onRegister } = props + + if (onRegister && rTableInst.value) { + call(onRegister, rTableInst.value) + } + }) + provide(config.tableKey, { uuidTable, uuidWrapper, @@ -242,13 +257,13 @@ export default defineComponent({ default: () => ( <> {{ ...$slots, @@ -273,12 +288,8 @@ export default defineComponent({ header: renderNode(title, { defaultElement:
, }), - 'header-extra': () => ( - - {/* eslint-disable @typescript-eslint/no-explicit-any */} - {tool($props as any)} - - ), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 'header-extra': tool($props as any), footer: () => $slots.tableFooter?.(), action: () => $slots.tableAction?.(), }} diff --git a/src/components/RTable/src/hooks/useTable.ts b/src/components/RTable/src/hooks/useTable.ts new file mode 100644 index 00000000..a205a3dc --- /dev/null +++ b/src/components/RTable/src/hooks/useTable.ts @@ -0,0 +1,147 @@ +import type { + RTableInst, + CsvOptionsType, + FilterState, + ScrollToOptions, + ColumnKey, + SortOrder, +} from '../types' + +/** + * + * @description + * 获取 RTable 实例。 + * 让你能够便捷的调用相关的一些已封装方法。 + * + * @warning + * 值得注意的是,必须手动调用 register 方法,否则不能正常使用。 + * 在使用 相关 hooks 的时候,需要注意生命周期,确保 register 方法已经被调用与表格实例已经被初始化; + * 不要在父组件 create 阶段就去调用 hook,如果需要,请使用 nextTick 包裹。 + * + * @example + * defineComponent({ + * setup() { + * const [register, { ...Hooks }] = useTable() + * + * return { + * register, + * ...Hooks, + * } + * }, + * render() { + * const { register, ...Hooks } = this + * + * return + * }, + * }) + */ +const useTable = () => { + const tableRef = ref() + + const register = (inst: RTableInst) => { + if (inst) { + tableRef.value = inst + } + } + + /** + * + * @description + * 获取 RTable 实例。 + */ + const getTableInstance = () => { + if (!tableRef.value) { + throw new Error( + '[useTable]: table instance is not ready yet. if you are using useTable, please make sure you have called register method in onRegister event.', + ) + } + + return tableRef.value + } + + /** + * + * @description + * 清空所有 filter 状态。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#filter-and-sorter + */ + const clearFilters = () => getTableInstance().clearFilters.call(null) + + /** + * + * @description + * 清空所有 sort 状态。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#filter-and-sorter + */ + const clearSorter = () => getTableInstance().clearSorter.call(null) + + /** + * + * @description + * 下载 CSV。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#export-csv.vue + */ + const downloadCsv = (options?: CsvOptionsType) => + getTableInstance().downloadCsv.call(null, options) + + /** + * + * @description + * 设定表格当前的过滤器。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#filter-and-sorter + */ + const filters = (filters: FilterState | null) => + getTableInstance().filters.call(null, filters) + + /** + * + * @description + * 手动设置 page。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#DataTable-Methods + */ + const page = (page: number) => getTableInstance().page.call(null, page) + + /** + * + * @description + * 滚动内容。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#DataTable-Methods + */ + const scrollTo: ScrollToOptions = (options) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getTableInstance().scrollTo(options as any) + + /** + * + * @description + * 设定表格的过滤状态。 + * + * @see https://www.naiveui.com/zh-CN/dark/components/data-table#DataTable-Methods + */ + const sort = (columnKey: ColumnKey, order: SortOrder) => + getTableInstance().sort.call(null, columnKey, order) + + return [ + register, + { + getTableInstance, + clearFilters, + clearSorter, + downloadCsv, + filters, + page, + scrollTo, + sort, + }, + ] as const +} + +export type UseTableReturn = ReturnType + +export default useTable diff --git a/src/components/RTable/src/props.ts b/src/components/RTable/src/props.ts index 1914b95b..013dd380 100644 --- a/src/components/RTable/src/props.ts +++ b/src/components/RTable/src/props.ts @@ -14,101 +14,198 @@ import { dataTableProps } from 'naive-ui' import type { PropType, VNode } from 'vue' import type { MaybeArray } from '@/types' import type { DropdownOption, DataTableColumn } from 'naive-ui' -import type { DownloadCsvTableOptions, PrintTableOptions } from './types' +import type { + DownloadCsvTableOptions, + PrintTableOptions, + RTableInst, +} from './types' import type { Recordable } from '@/types' const props = { ...dataTableProps, + /** + * + * @description + * 是否启用表格工具栏。 + * + * 当设置为 false 时,不会显示表格工具栏。不论是否配置了 toolOptions。 + * + * @default true + */ + tool: { + type: Boolean, + default: true, + }, + /** + * + * @description + * 下载表格配置项。 + * + * @default {} + */ downloadCsvTableOptions: { - /** - * - * 配置下载表格配置项 - */ type: Object as PropType, default: () => ({}), }, + /** + * + * @description + * 表格标题,支持 VNode。 + * + * @default null + */ title: { - /** - * - * 表格标题 - * 支持自定义渲染 - */ type: [String, Number, Object] as PropType, default: null, }, + /** + * + * @description + * 自定义工具栏配置项。 + * + * @default undefined + */ toolOptions: { - /** 自定义拓展工具栏 */ type: Array as PropType<(VNode | (() => VNode))[]>, }, + /** + * + * @description + * 是否覆盖原工具栏功能按钮。 + * + * @default false + */ coverTool: { - /** 当 toolOptions 配置时,是否覆盖原工具栏 */ type: Boolean, default: false, }, + /** + * + * @description + * 右键菜单配置项。 + * + * 基于 NDropdown 实现。 + * + * @default undefined + */ contextMenuOptions: { - /** - * - * 右键菜单配置项 - * 基于 `NDropdown` 实现 - */ type: Array as PropType, }, + /** + * + * @description + * 是否禁用右键菜单。 + * + * 如果设置为 false 则不会唤起右键菜单。 + * + * @default false + */ disabledContextMenu: { - /** - * - * 是否禁用右键菜单 - * 如果设置为 false 则不会唤起右键菜单 - */ type: Boolean, default: false, }, + /** + * + * @description + * 右键菜单点击事件回调。 + * + * 该属性用于配置右键菜单点击事件。 + * + * @default null + */ onContextMenuClick: { - /** 右键菜单点击 */ type: [Function, Array] as PropType< MaybeArray<(key: string | number, option: DropdownOption) => void> >, default: null, }, + /** + * + * @description + * 表格容器边框。 + * + * 与表格边框为两个不同配置项。 + * + * @default false + */ wrapperBordered: { - /** - * - * 表格容器边框 - * 与表格边框为两个不同配置项 - */ type: Boolean, default: false, }, + /** + * + * @description + * 打印表格配置项。 + * + * 基于 dom printDom 实现。 + * + * @default {} + */ printTableOptions: { - /** - * - * 配置打印表格配置项 - */ type: Object as PropType, default: () => ({}), }, + /** + * + * @description + * 双向绑定列表列配置项事件。 + * + * 当配置为 v-model:columns 时,或者是单独调用 onUpdateColumns 时,该属性生效。 + * 双向绑定语法糖事件。 + * + * @default null + */ onUpdateColumns: { type: [Function, Array] as PropType< MaybeArray<(arr: DataTableColumn[]) => void> >, default: null, }, + /** + * + * @description + * 双向绑定列表列配置项事件。 + * + * 当配置为 v-model:columns 时,或者是单独调用 onUpdateColumns 时,该属性生效。 + * 双向绑定语法糖事件。 + * + * @default null + */ 'onUpdate:columns': { type: [Function, Array] as PropType< MaybeArray<(arr: DataTableColumn[]) => void> >, default: null, }, + /** + * + * @description + * 该属性用于启用右键菜单后被 Table 强行代理后的右键点击事件回调。 + * + * 当右键菜单不启用时,不生效。只需要使用 rowProps 属性配置右键菜单事件即可。 + * + * @default null + */ onContextmenu: { - /** - * - * 该属性用于启用右键菜单后被 Table 强行代理后的右键点击事件回调 - * 当右键菜单不启用时,不生效。只需要使用 rowProps 属性配置右键菜单事件即可 - */ type: [Function, Array] as PropType< MaybeArray<(row: Recordable, index: number, e: MouseEvent) => void> >, default: null, }, + /** + * + * @description + * RTable 注册挂载成功后触发的事件。 + * 可以结合 useTable 方法中的 register 方法使用,然后便捷的使用 hooks。 + * + * @default null + */ + onRegister: { + type: [Function, Array] as PropType< + MaybeArray<(tableInst: RTableInst) => void> + >, + default: null, + }, } export default props diff --git a/src/components/RTable/src/types.ts b/src/components/RTable/src/types.ts index b70d40b2..01e840fb 100644 --- a/src/components/RTable/src/types.ts +++ b/src/components/RTable/src/types.ts @@ -42,10 +42,28 @@ export interface C extends DataTableBaseColumn { children?: C[] } +export interface RTableInst extends Omit {} + export type OverridesTableColumn = C | DataTableColumn export interface TableInst extends Omit { - rTableInst: Omit + rTableInst: RTableInst } export type PropsComponentPopselectKeys = 'striped' | 'bordered' + +type DownloadCsvParameters = Parameters + +export type CsvOptionsType = DownloadCsvParameters[0] + +type FiltersParameters = Parameters + +export type FilterState = FiltersParameters[0] + +export type ScrollToOptions = RTableInst['scrollTo'] + +type SortParameters = Parameters + +export type ColumnKey = SortParameters[0] + +export type SortOrder = SortParameters[1] diff --git a/src/components/index.ts b/src/components/index.ts index b94376ad..becb3323 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ export * from './RMoreDropdown' export * from './RQRCode' export * from './RTable' export * from './RTransitionComponent' +export * from './RForm' // 导出自定义组件类型 export type * from './RChart/src/types' @@ -16,3 +17,5 @@ export type * from './RIframe/src/types' export type * from './RQRCode/src/types' export type * from './RTable/src/types' export type * from './RTransitionComponent/src/types' +export type * from './RForm/src/types' +export type * from './RModal/src/types' diff --git a/src/hooks/web/index.ts b/src/hooks/web/index.ts index c888c554..dc789aeb 100644 --- a/src/hooks/web/index.ts +++ b/src/hooks/web/index.ts @@ -5,3 +5,4 @@ export * from './useDevice' export * from './useElementFullscreen' export * from './useDomToImage' export * from './usePrint' +export * from './usePagination' diff --git a/src/hooks/web/useDevice.ts b/src/hooks/web/useDevice.ts index 69030dbb..24437c78 100644 --- a/src/hooks/web/useDevice.ts +++ b/src/hooks/web/useDevice.ts @@ -19,7 +19,16 @@ import { watchEffectWithTarget } from '@/utils' import type { UseWindowSizeOptions } from '@vueuse/core' -export interface UseDeviceOptions extends UseWindowSizeOptions {} +export interface UseDeviceOptions extends UseWindowSizeOptions { + /** + * + * @description + * 自定义 isTabletOrSmaller 的判断尺寸。 + * + * @default 768 + */ + media?: number +} /** * @@ -40,7 +49,9 @@ export function useDevice(options?: UseDeviceOptions) { const isTabletOrSmaller = ref(false) const update = () => { - isTabletOrSmaller.value = width.value <= 768 + const { media = 768 } = options ?? {} + + isTabletOrSmaller.value = width.value <= media } watchEffectWithTarget(update) diff --git a/src/hooks/web/usePagination.ts b/src/hooks/web/usePagination.ts new file mode 100644 index 00000000..9ae190a4 --- /dev/null +++ b/src/hooks/web/usePagination.ts @@ -0,0 +1,163 @@ +import { omit } from '@/utils' + +import type { AnyFC } from '@/types' +import type { PaginationProps } from 'naive-ui' + +type OmitKeys = + | 'themeOverrides' + | 'theme' + | 'on-update:page' + | 'on-update:page-size' + | 'onUpdatePage' + | 'onUpdatePageSize' + | 'onUpdate:page' + | 'onUpdate:page-size' + +export interface UsePaginationOptions extends Omit {} + +const defaultOptions: UsePaginationOptions = { + page: 1, + pageSize: 10, + showSizePicker: true, + pageSizes: [10, 20, 50, 100], +} + +/** + * + * @param callback 页码、页条数更新时的回调函数 + * @param options 配置项 + * + * @description + * 便捷分页 hook。 + * + * @warning + * callback 暂不支持异步函数。 + */ +export const usePagination = ( + callback: T, + options?: UsePaginationOptions, +) => { + if (typeof callback !== 'function') { + throw new Error( + '[usePagination]: callback expected a function, but got ' + + typeof callback, + ) + } + + const omitOptions = omit(options, [ + 'on-update:page', + 'on-update:page-size', + 'onUpdatePage', + 'onUpdatePageSize', + 'onUpdate:page', + 'onUpdate:page-size', + ]) + const methodsOptions = { + onUpdatePage: (page: number) => { + paginationRef.page = page + + callback() + }, + onUpdatePageSize: (pageSize: number) => { + paginationRef.pageSize = pageSize + paginationRef.page = 1 + + callback() + }, + } + const paginationRef = reactive( + Object.assign({}, defaultOptions, omitOptions, methodsOptions), + ) + + const updatePage = paginationRef.onUpdatePage as (page: number) => void + const updatePageSize = paginationRef.onUpdatePageSize as ( + pageSize: number, + ) => void + + /** + * + * @description + * 获取总条数。 + */ + const getItemCount = () => paginationRef.itemCount + + /** + * + * @param itemCount 总条数 + * + * @description + * 设置总条数。 + */ + const setItemCount = (itemCount: number) => { + paginationRef.itemCount = itemCount + } + + /** + * + * @description + * 获取当前页页码。 + */ + const getPage = () => paginationRef.page + + /** + * + * @param page 当前页页码 + * + * @description + * 设置当前页页码。 + * + * 为了避免响应式丢失,手动调用 updatePage 方法。 + */ + const setPage = (page: number) => { + updatePage(page) + } + + /** + * + * @description + * 获取每页条数。 + */ + const getPageSize = () => paginationRef.pageSize + + /** + * + * @param pageSize 每页条数 + * + * @description + * 设置每页条数。 + * + * 为了避免响应式丢失,手动调用 updatePageSize 方法。 + */ + const setPageSize = (pageSize: number) => { + updatePageSize(pageSize) + } + + /** + * + * @description + * 获取分页配置,通常可以用来传递给 RTable 组件。 + */ + const getPagination = () => paginationRef as UsePaginationOptions + + /** + * + * @description + * 获取回调函数。 + */ + const getCallback = callback + + return { + updatePage, + updatePageSize, + getItemCount, + setItemCount, + getPage, + setPage, + getPageSize, + setPageSize, + getPagination, + getCallback, + } +} + +export type UsePaginationReturn = ReturnType diff --git a/src/locales/lang/en-US/menu.json b/src/locales/lang/en-US/menu.json index 2b1bd1b7..77ece1c8 100644 --- a/src/locales/lang/en-US/menu.json +++ b/src/locales/lang/en-US/menu.json @@ -24,5 +24,6 @@ "TemplateHooks": "Template Api", "Modal": "Modal", "ContextMenu": "Right Click Menu", - "CacheDemo": "Cache Utils Demo" + "CacheDemo": "Cache Utils Demo", + "Form": "Form" } diff --git a/src/locales/lang/zh-CN/menu.json b/src/locales/lang/zh-CN/menu.json index e6624546..fda97e14 100644 --- a/src/locales/lang/zh-CN/menu.json +++ b/src/locales/lang/zh-CN/menu.json @@ -24,5 +24,6 @@ "TemplateHooks": "模板内置 Api", "Modal": "模态框", "ContextMenu": "右键菜单", - "CacheDemo": "缓存工具函数" + "CacheDemo": "缓存工具函数", + "Form": "表单" } diff --git a/src/router/modules/demo/cache-demo.ts b/src/router/modules/demo/cache-demo.ts index f3a5f3e3..f9dfdbee 100644 --- a/src/router/modules/demo/cache-demo.ts +++ b/src/router/modules/demo/cache-demo.ts @@ -11,9 +11,6 @@ const cacheDemo: AppRouteRecordRaw = { i18nKey: t('menu.CacheDemo'), icon: 'other', order: 3, - extra: { - label: 'new', - }, }, } diff --git a/src/router/modules/demo/echart.ts b/src/router/modules/demo/echart.ts index 3706de57..5ec03d9a 100644 --- a/src/router/modules/demo/echart.ts +++ b/src/router/modules/demo/echart.ts @@ -11,6 +11,9 @@ const echart: AppRouteRecordRaw = { i18nKey: t('menu.Echart'), icon: 'echart', order: 1, + extra: { + label: 'useChart', + }, }, } diff --git a/src/router/modules/demo/form.ts b/src/router/modules/demo/form.ts new file mode 100644 index 00000000..53467a63 --- /dev/null +++ b/src/router/modules/demo/form.ts @@ -0,0 +1,20 @@ +import { t } from '@/hooks/web/useI18n' +import { LAYOUT } from '@/router/constant' + +import type { AppRouteRecordRaw } from '@/router/types' + +const form: AppRouteRecordRaw = { + path: '/form', + name: 'FormView', + component: () => import('@/views/demo/form'), + meta: { + i18nKey: t('menu.Form'), + icon: 'other', + order: 2, + extra: { + label: 'useForm', + }, + }, +} + +export default form diff --git a/src/router/modules/demo/mock.ts b/src/router/modules/demo/mock.ts index 1935a6ac..0b17c934 100644 --- a/src/router/modules/demo/mock.ts +++ b/src/router/modules/demo/mock.ts @@ -12,6 +12,9 @@ const mockDemo: AppRouteRecordRaw = { icon: 'other', order: 3, keepAlive: false, + extra: { + label: 'usePagination', + }, }, } diff --git a/src/router/modules/demo/table.ts b/src/router/modules/demo/table.ts index e7e1a045..c9a2148d 100644 --- a/src/router/modules/demo/table.ts +++ b/src/router/modules/demo/table.ts @@ -11,6 +11,9 @@ const table: AppRouteRecordRaw = { i18nKey: t('menu.Table'), icon: 'other', order: 2, + extra: { + label: 'useTable', + }, }, } diff --git a/src/store/modules/menu/index.ts b/src/store/modules/menu/index.ts index cf452454..fde9049e 100644 --- a/src/store/modules/menu/index.ts +++ b/src/store/modules/menu/index.ts @@ -136,9 +136,8 @@ export const piniaMenuStore = defineStore( default: () => label.value, }), breadcrumbLabel: label.value, - /** 检查该菜单项是否展示 */ } as AppMenuOption - /** 合并 icon */ + /** 合并 icon, extra */ const attr: AppMenuOption = Object.assign({}, route, { icon: createMenuIcon(option), extra: createMenuExtra(option), diff --git a/src/types/modules/utils.ts b/src/types/modules/utils.ts index 3ab0b032..01579fc6 100644 --- a/src/types/modules/utils.ts +++ b/src/types/modules/utils.ts @@ -92,6 +92,8 @@ export type CipherParams = CryptoJS.lib.CipherParams export type AnyFC

= (...args: P[]) => R +export type VoidFC = (...args: any[]) => void + export type PartialCSSStyleDeclaration = Partial< Record > diff --git a/src/views/demo/echart/index.tsx b/src/views/demo/echart/index.tsx index 29a49b50..196818f6 100644 --- a/src/views/demo/echart/index.tsx +++ b/src/views/demo/echart/index.tsx @@ -3,12 +3,25 @@ import './index.scss' import { NCard, NSwitch, NFlex, NH2, NButton } from 'naive-ui' import { RChart } from '@/components' +import { useChart } from '@/components' + import type { RChartType } from '@/components' const Echart = defineComponent({ name: 'REchart', setup() { - const baseChartRef = ref() + const [register, { getChartInstance, dispose, render, isDispose }] = + useChart() + const [ + register2, + { + getChartInstance: getChartInstance2, + dispose: dispose2, + render: render2, + isDispose: isDispose2, + }, + ] = useChart() + const chartLoading = ref(false) const chartAria = ref(false) const state = reactive({ @@ -179,15 +192,15 @@ const Echart = defineComponent({ } const mountChart = () => { - if (!baseChartRef.value?.isDispose()) { - baseChartRef.value?.render() + if (isDispose()) { + render() } else { window.$message.warning('图表已经渲染') } } const unmountChart = () => { - baseChartRef.value?.dispose() + dispose() } const updateChartOptions = () => { @@ -203,7 +216,6 @@ const Echart = defineComponent({ return { baseOptions, - baseChartRef, chartLoading, handleLoadingShow, chartAria, @@ -214,9 +226,16 @@ const Echart = defineComponent({ mountChart, unmountChart, updateChartOptions, + register, + register2, + dispose2, + render2, + isDispose2, } }, render() { + const { register, register2, dispose2, render2, isDispose2 } = this + return (

@@ -246,6 +265,9 @@ const Echart = defineComponent({ 属性,只有元素在可见范围才会渲染图表,可以滚动查看效果 +
  • +

    7. useChart 方法

    +
  • @@ -258,8 +280,8 @@ const Echart = defineComponent({
    -
    - -
    + + + { + if (isDispose2()) { + render2() + } else { + window.$message.warning('图表已经渲染') + } + }} + > + 渲染 + + 卸载 + +
    + +
    +
    + * + * @date 2024-03-27 + * + * @workspace ray-template + * + * @remark 今天也是元气满满撸代码的一天 + */ + +import { RForm } from '@/components' +import { + NFormItemGi, + NDatePicker, + NGrid, + NInput, + NInputNumber, + NFlex, + NButton, + NRadio, + NRadioGroup, +} from 'naive-ui' + +import { useForm } from '@/components' + +import type { RFormRules } from '@/components' + +export default defineComponent({ + name: 'RFormDemo', + setup() { + // 使用以下 hooks 的时候,应该注意调用时机 + const [ + register, + { getFormInstance, validate, restoreValidation, formModel, formRules }, + ] = useForm( + { + name: null, + age: null, + gender: null, + date: null, + remark: null, + }, + { + name: { + required: true, + message: '请输入姓名', + trigger: ['blur', 'change'], + }, + date: { + required: true, + message: '请选择日期', + trigger: ['blur', 'change'], + type: 'number', + }, + gender: { + required: true, + message: '请选择性别', + trigger: 'change', + }, + age: { + required: true, + message: '请输入年龄', + trigger: ['blur', 'change'], + type: 'number', + }, + }, + ) + + /** + * + * @description + * 如果待验证数据类型为: number, array 等,需要手动设置 type 类型。 + * 具体可以吃查看: async-validator type + * @see https://github.com/yiminghe/async-validator?tab=readme-ov-file#type + * + * 如果你需要自定义验证,可以查看:naive ui custom validation + * @see https://www.naiveui.com/zh-CN/dark/components/form#custom-validation.vue + * + * 如果只是简单的 rules 管理,可以在初始化 useForm 的时候传入第二个参数; + * 然后使用 formRules 方法获取到初始化 rules 数据。 + */ + const rules = ref(formRules()) + /** + * + * @description + * 如果只是简单的数据,可以在初始化 useForm 的时候直接传入第一个参数; + * 然后使用 formModel 方法获取到初始化 model 数据。 + * + * 动态的复杂数据,不建议使用该方法管理 model;手动的拆分出来是一个更加好的选择。 + */ + const condition = ref(formModel()) + + return { + register, + rules, + condition, + restoreValidation, + formModel, + validate, + } + }, + render() { + const { rules } = this + const { register, restoreValidation, formModel, validate } = this + + return ( + + + + + + + + + + + + + + + + + + + + + + + { + this.condition = formModel() + }} + > + 重置表单为初始状态 + + + 移除校验状态 + + validate()}> + 校验 + + + + + + ) + }, +}) diff --git a/src/views/demo/mock-demo/index.tsx b/src/views/demo/mock-demo/index.tsx index 630bab62..ef53550c 100644 --- a/src/views/demo/mock-demo/index.tsx +++ b/src/views/demo/mock-demo/index.tsx @@ -14,6 +14,7 @@ import { RCollapseGrid, RTable } from '@/components' import { useHookPlusRequest } from '@/axios' import { getPersonList } from '@/api/demo/mock/person' +import { usePagination } from '@/hooks' import type { Person } from '@/api/demo/mock/person' @@ -86,24 +87,23 @@ const MockDemo = defineComponent({ const condition = reactive({ email: null, }) - const paginationRef = reactive({ - page: 1, - pageSize: 10, - itemCount: 0, - pageSizes: [10, 20, 30, 40, 50], - showSizePicker: true, - onUpdatePage: (page: number) => { - paginationRef.page = page - getPerson() - }, - onUpdatePageSize: (pageSize: number) => { - paginationRef.pageSize = pageSize - paginationRef.page = 1 - - getPerson() - }, + const { + getPagination, + getPage, + getPageSize, + setItemCount, + getCallback, + setPage, + setPageSize, + } = usePagination(() => { + personFetchRun({ + page: getPage(), + pageSize: getPageSize(), + email: condition.email, + }) }) + const paginationRef = getPagination() const { data: personData, loading: personLoading, @@ -111,36 +111,26 @@ const MockDemo = defineComponent({ } = useHookPlusRequest(getPersonList, { defaultParams: [ { - page: paginationRef.page, - pageSize: paginationRef.pageSize, + page: getPage(), + pageSize: getPageSize(), email: condition.email, }, ], onSuccess: (res) => { const { total } = res - paginationRef.itemCount = total + setItemCount(total) }, }) - const getPerson = () => { - const { pageSize, page } = paginationRef - const { email } = condition - - personFetchRun({ - page, - pageSize, - email, - }) - } - return { personData, personLoading, - paginationRef, + getPagination, columns, ...toRefs(condition), - getPerson, + getCallback, + paginationRef, } }, render() { @@ -170,7 +160,7 @@ const MockDemo = defineComponent({ ), action: () => ( - + 搜索 ), diff --git a/src/views/demo/modal-demo/index.tsx b/src/views/demo/modal-demo/index.tsx index a5476adc..64fe1511 100644 --- a/src/views/demo/modal-demo/index.tsx +++ b/src/views/demo/modal-demo/index.tsx @@ -12,6 +12,8 @@ import { RModal } from '@/components' import { NButton, NCard, NFlex } from 'naive-ui' +import { useModal } from '@/components' + export default defineComponent({ name: 'ModalDemo', setup() { @@ -20,12 +22,36 @@ export default defineComponent({ modal2: false, modal3: false, }) + const { create } = useModal() + + const createCardModal = () => { + create({ + title: '卡片模态框', + dad: true, + preset: 'card', + content: '我可以被拖拽的全屏card模态框', + fullscreen: true, + }) + } + + const createDialogModal = () => { + create({ + title: '模态框', + content: '内容', + preset: 'dialog', + dad: true, + }) + } return { ...toRefs(state), + createCardModal, + createDialogModal, } }, render() { + const { createCardModal, createDialogModal } = this + return ( @@ -91,6 +117,12 @@ export default defineComponent({ 所有的宽度配置属性都会注入一个对应的 `css variable`,有时候会用上。 + + 创建卡片模态框 + + 创建对话框模态框 + + ) }, diff --git a/src/views/demo/table/index.tsx b/src/views/demo/table/index.tsx index e3d86d49..36b45a9d 100644 --- a/src/views/demo/table/index.tsx +++ b/src/views/demo/table/index.tsx @@ -23,20 +23,29 @@ import { } from 'naive-ui' import { RCollapseGrid, RTable, RIcon, RMoreDropdown } from '@/components' +import { uuid } from '@/utils' +import { useTable } from '@/components' + import type { DataTableColumns } from 'naive-ui' -import type { RTableType } from '@/components' type RowData = { - key: number + key: number | string name: string age: number address: string tags: string[] + remark: string } const TableView = defineComponent({ name: 'TableView', setup() { + // 使用以下 hooks 的时候,应该注意调用时机 + const [ + register, + { getTableInstance, clearFilters, clearSorter, scrollTo, filters, sort }, + ] = useTable() + const baseColumns = [ { title: 'Name', @@ -68,7 +77,7 @@ const TableView = defineComponent({ { title: 'Remark', key: 'remark', - width: 300, + width: 100, }, { title: 'Action', @@ -98,32 +107,7 @@ const TableView = defineComponent({ const actionColumns = ref>( [...baseColumns].map((curr) => ({ ...curr, width: 400 })), ) - const tableData = ref([ - { - key: 0, - name: 'John Brown', - age: 32, - address: 'New York No. 1 Lake Park', - tags: ['nice', 'developer'], - remark: '我是一条很长很长的备注', - }, - { - key: 1, - name: 'Jim Green', - age: 42, - address: 'London No. 1 Lake Park', - tags: ['wow'], - remark: '我是一条很长很长的备注', - }, - { - key: 2, - name: 'Joe Black', - age: 32, - address: 'Sidney No. 1 Lake Park', - tags: ['cool', 'teacher'], - remark: '我是一条很长很长的备注', - }, - ]) + const tableData = ref([]) const tableMenuOptions = [ { label: '编辑', @@ -140,20 +124,38 @@ const TableView = defineComponent({ tableLoading: false, }) - const handleMenuSelect = (key: string | number) => { + const createTableData = () => { + for (let i = 0; i < 20; i++) { + tableData.value.push({ + key: uuid(), + name: 'John Brown', + age: i + 20, + address: 'New York No. 1 Lake Park', + tags: ['nice', 'developer'], + remark: '我是一条很长很长的备注', + }) + } + } + + const menuSelect = (key: string | number) => { window.$message.info(`${key}`) } + createTableData() + return { ...toRefs(state), tableData, actionColumns, baseColumns, tableMenuOptions, - handleMenuSelect, + menuSelect, + register, } }, render() { + const { register } = this + return ( @@ -199,7 +201,8 @@ const TableView = defineComponent({ }} 标题插槽: @@ -215,7 +218,7 @@ const TableView = defineComponent({ }} contextMenuOptions={this.tableMenuOptions} loading={this.tableLoading} - onContextMenuClick={this.handleMenuSelect.bind(this)} + onContextMenuClick={this.menuSelect.bind(this)} toolOptions={[ {{ diff --git a/vitest.config.ts b/vitest.config.ts index e7713ec1..04a310df 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,14 @@ export default defineConfig((configEnv) => environment: 'happy-dom', globals: true, poolOptions: { + /** + * + * 如此配置是为避免: Module did not self-register... 错误; + * 该错误是一个历史悠久遗留问题,可以查看该 issues: + * @see https://github.com/vitest-dev/vitest/issues/740 + * + * 目前暂时没有更好的解决方案,这么做会导致单测执行速度变慢,但是可以避免错误,后续有更好的解决方案会更新。 + */ threads: { maxThreads: 1, minThreads: 0,