@peng_kai/kit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ <script lang="ts" setup>
2
+ import { ref } from "vue";
3
+
4
+ const emits = defineEmits<{
5
+ (e: 'filter'): void
6
+ (e: 'reset', value: number): void
7
+ }>()
8
+ const filterVisible = ref(false)
9
+
10
+ function filter() {
11
+ emits('filter')
12
+ filterVisible.value = false
13
+ }
14
+
15
+ function reset() {
16
+ emits('reset', 1)
17
+ filterVisible.value = false
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <div class="p-3 mb-2 bg-white text-14px" @click="filterVisible = true">
23
+ <!-- .filter-params 为空时显示 .filter-params-tips -->
24
+ <div class="filter-params">
25
+ <slot name="params" />
26
+ </div>
27
+ <div class="filter-params-tips">
28
+ 条件筛选,点击打开
29
+ </div>
30
+ </div>
31
+
32
+ <ADrawer v-model:open="filterVisible" class="filter-drawer" placement="bottom" height="50vh">
33
+ <template #extra>
34
+ <AButton class="mr-3 my--3" @click="reset()">
35
+ 重置
36
+ </AButton>
37
+ <AButton class="my--3" type="primary" @click="filter()">
38
+ 筛选
39
+ </AButton>
40
+ </template>
41
+ <template #default>
42
+ <slot />
43
+ </template>
44
+ </ADrawer>
45
+ </template>
46
+
47
+ <style scoped lang="scss">
48
+ .filter-params {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ justify-content: flex-start;
52
+ gap: 5px 15px;
53
+ }
54
+
55
+ // .filter-params 为空时显示 .filter-params-tips
56
+ .filter-params-tips {
57
+ display: none;
58
+ color: theme('colors.gray.DEFAULT');
59
+ }
60
+
61
+ .filter-params:empty {
62
+ display: none;
63
+ }
64
+
65
+ .filter-params:empty + .filter-params-tips {
66
+ display: block;
67
+ }
68
+ </style>
69
+
70
+ <style lang="scss">
71
+ .filter-drawer {
72
+ .ant-drawer-header {
73
+ padding: 16px;
74
+
75
+ .ant-drawer-close {
76
+ --expand: 5px;
77
+
78
+ padding: var(--expand);
79
+ margin: calc(var(--expand) * -1) var(--expand) calc(var(--expand) * -1) 0;
80
+ }
81
+ }
82
+
83
+ .ant-drawer-body {
84
+ padding: 16px;
85
+ }
86
+
87
+ .ant-form-item {
88
+ margin-bottom: 0;
89
+
90
+ .ant-form-item-label {
91
+ padding-bottom: 0;
92
+ }
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import isNil from 'lodash-es/isNil'
3
+ import isFinite from 'lodash-es/isFinite'
4
+ import dayjs from 'dayjs'
5
+ import bignumber from 'bignumber.js'
6
+
7
+ export const paramTypes = { numberRange, datetimeRange, options }
8
+
9
+ /**
10
+ * 时间范围格式化
11
+ * @param range 数字范围
12
+ * @param unit 单位
13
+ */
14
+ function numberRange(range?: [number, number], unit?: string) {
15
+ if (!range?.every(isFinite))
16
+ return ''
17
+
18
+ return `${bignumber(range[0]).toFormat()}~${bignumber(range[1]).toFormat()}${unit}`
19
+ }
20
+
21
+ /**
22
+ * 时间范围格式化
23
+ * @param range 时间范围
24
+ * @param template 格式化模板(文档:https://dayjs.gitee.io/docs/zh-CN/display/format )
25
+ */
26
+ function datetimeRange(range?: [string | dayjs.Dayjs, string | dayjs.Dayjs], template = 'YYYY-MM-DD') {
27
+ if (!range?.every(v => dayjs(v).isValid()))
28
+ return ''
29
+
30
+ return `${dayjs(range[0]).format(template)} ~ ${dayjs(range[1]).format(template)}`
31
+ }
32
+
33
+ function options(
34
+ value?: string | number | Array<string | number>,
35
+ options?: Array<{ value: string | number; label: any }>,
36
+ ) {
37
+ if (isNil(value) || isNil(options))
38
+ return
39
+ if (value === '')
40
+ return
41
+
42
+ const _value = Array.isArray(value) ? value : [value]
43
+
44
+ return options
45
+ .filter(o => _value.includes(o.value))
46
+ .map(o => o.label)
47
+ .join(', ')
48
+ }
49
+ </script>
50
+
51
+ <script setup lang="ts">
52
+ const props = defineProps<{
53
+ label: string
54
+ content?: any
55
+ }>()
56
+ </script>
57
+
58
+ <template>
59
+ <div v-if="props.content" class="item-param">
60
+ <span class="label">{{ props.label }}</span>
61
+ <span class="content">{{ props.content }}</span>
62
+ </div>
63
+ </template>
64
+
65
+ <style lang="scss" scoped>
66
+ .label {
67
+ display: inline-block;
68
+ margin-right: 0.3em;
69
+ color: theme('colors.gray.DEFAULT');
70
+ }
71
+
72
+ .content {
73
+ color: theme('colors.primary.DEFAULT');
74
+ word-break: break-all;
75
+ }
76
+ </style>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ loading?: boolean
4
+ }>()
5
+ const emits = defineEmits<{
6
+ (e: 'filter'): void
7
+ (e: 'reset'): void
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <div class="flex w-min mb-6">
13
+ <AButton class="mr-2" type="primary" :loading="props.loading" @click="emits('filter')">
14
+ 查询
15
+ </AButton>
16
+ <AButton :disabled="props.loading" @click="emits('reset')">
17
+ 重置
18
+ </AButton>
19
+ </div>
20
+ </template>
21
+
22
+ <style scoped lang="scss"></style>
@@ -0,0 +1,3 @@
1
+ export { default as FilterDrawer } from './FilterDrawer.vue'
2
+ export { default as FilterReset } from './FilterReset.vue'
3
+ export { default as FilterParam, paramTypes} from './FilterParam.vue'
@@ -0,0 +1,61 @@
1
+ import { createVNode, defineComponent, reactive, render as vueRender } from 'vue'
2
+ import { Modal as AntModal } from 'ant-design-vue'
3
+ import { tryOnUnmounted } from '@vueuse/core'
4
+ import type { VNode, Component } from 'vue'
5
+ import type { ModalProps } from 'ant-design-vue'
6
+ import type { ComponentProps } from 'vue-component-type-helpers'
7
+ import type { Writable } from 'type-fest'
8
+
9
+ export function createAntdModal<Comp extends Component>(comp: Comp, defaultModalProps: ModalProps = {}) {
10
+ type TCompProps = Writable<ComponentProps<Comp>>
11
+
12
+ let holderUsed = false // 是否是使用 ContextHolder 的方式挂载弹窗
13
+ let container: DocumentFragment
14
+ let instance: VNode
15
+ const compProps = reactive({} as TCompProps)
16
+ const modalProps = reactive<ModalProps>({
17
+ 'open': false,
18
+ 'destroyOnClose': true,
19
+ ...defaultModalProps,
20
+ 'onUpdate:open': (value) => {
21
+ modalProps.open = value
22
+ },
23
+ 'afterClose': () => {
24
+ container && vueRender(null, container as any);
25
+ (instance as any) = null;
26
+ (container as any) = null
27
+ },
28
+ })
29
+ const ContextHolder = defineComponent({
30
+ setup() {
31
+ if (!instance)
32
+ holderUsed = true
33
+ },
34
+ render() {
35
+ return createVNode(AntModal, modalProps, { modalRender: () => createVNode(comp, compProps) })
36
+ },
37
+ })
38
+
39
+ const open = (newCompProps: TCompProps) => {
40
+ Object.keys(compProps).forEach(k => delete compProps[k])
41
+ Object.assign(compProps, {
42
+ ...newCompProps,
43
+ onClose: close,
44
+ })
45
+
46
+ if (!holderUsed) {
47
+ container = document.createDocumentFragment()
48
+ instance = createVNode(ContextHolder)
49
+ vueRender(instance, container as any)
50
+ }
51
+
52
+ modalProps.open = true
53
+ }
54
+ const close = () => {
55
+ modalProps.open = false
56
+ }
57
+
58
+ tryOnUnmounted(close)
59
+
60
+ return { open, close, ContextHolder }
61
+ }
@@ -0,0 +1,73 @@
1
+ import { Button as AButton, Drawer as ADrawer, Space as ASpace } from 'ant-design-vue'
2
+ import { defineComponent, isProxy, reactive, toRefs, toRef, h } from 'vue'
3
+ import type { Component } from 'vue'
4
+ import type { ButtonProps, DrawerProps } from 'ant-design-vue'
5
+ import type { ComponentProps } from 'vue-component-type-helpers'
6
+ import type { Writable } from 'type-fest'
7
+ import { useComponentRef } from '../../vue'
8
+
9
+ const defaultDrawerProps: DrawerProps = { open: false, destroyOnClose: true }
10
+
11
+ interface IComponentConfig<Comp extends Component> {
12
+ is: Comp
13
+ props?: Writable<ComponentProps<Comp>>
14
+ }
15
+
16
+ export function useAntdDrawer<Comp extends Component>(
17
+ comp: IComponentConfig<Comp> | Comp,
18
+ drawerProps = defaultDrawerProps,
19
+ ) {
20
+ const _comp = ({ props: {}, ...((comp as any)?.is ? comp : { is: comp }) }) as Required<IComponentConfig<Comp>>
21
+ const compProps = reactive(_comp.props)
22
+ const compRef = useComponentRef(_comp.is)
23
+ const _drawerProps: DrawerProps = reactive({
24
+ ...defaultDrawerProps,
25
+ ...isProxy(drawerProps) ? toRefs(drawerProps) : drawerProps,
26
+ })
27
+
28
+ const open = (newBodyProps?: Partial<typeof compProps>, newAntdModalProps?: Omit<Partial<DrawerProps>, 'open'>) => {
29
+ Object.assign(_drawerProps, newAntdModalProps)
30
+ Object.assign(compProps, newBodyProps)
31
+ _drawerProps.open = true
32
+ }
33
+ const close = () => {
34
+ _drawerProps.open = false
35
+ }
36
+
37
+ const DrawerExtra = defineComponent({
38
+ setup() {
39
+ const cancelBtnProps: ButtonProps = reactive({ onClick: close })
40
+ const confirmBtnProps: ButtonProps = reactive({
41
+ type: 'primary',
42
+ loading: toRef(() => (compRef as any)?.loading),
43
+ onClick: () => (compRef as any)?.confirm?.(),
44
+ })
45
+
46
+ return { cancelBtnProps, confirmBtnProps }
47
+ },
48
+ render() {
49
+ const { cancelBtnProps, confirmBtnProps } = this
50
+
51
+ return h(ASpace, {}, () => [
52
+ h(AButton, cancelBtnProps, () => '取消'),
53
+ h(AButton, confirmBtnProps, () => '确定'),
54
+ ])
55
+ },
56
+ })
57
+ const PresetComponent = defineComponent({
58
+ render() {
59
+ return h(ADrawer, _drawerProps, {
60
+ default: () => h(_comp.is, compProps as any),
61
+ })
62
+ },
63
+ })
64
+
65
+ _drawerProps.extra = _drawerProps.extra === undefined ? h(DrawerExtra) : _drawerProps.extra
66
+ _drawerProps['onUpdate:open'] = (visiable) => {
67
+ _drawerProps.open = visiable
68
+ }
69
+ (compProps as any).ref = compRef;
70
+ (compProps as any).onClose = close
71
+
72
+ return { PresetComponent, drawerProps: _drawerProps, open, close }
73
+ }
@@ -0,0 +1,48 @@
1
+ export { buildGroupFieldName, parseGroupFieldName, isSameGroup, isSameField, getGroupIndex, GROUP_SEP }
2
+
3
+ const GROUP_SEP = '__'
4
+ const groupFieldNameRE = new RegExp(
5
+ `(?<groupName>[a-zA-Z0-9]+)${GROUP_SEP}(?<index>\\d+)${GROUP_SEP}(?<fieldName>[a-zA-Z0-9]+)`,
6
+ )
7
+
8
+ function buildGroupFieldName(name: string, index: number, field: string) {
9
+ return name + GROUP_SEP + index + GROUP_SEP + field
10
+ }
11
+
12
+ function parseGroupFieldName(name: string) {
13
+ const res = name.match(groupFieldNameRE)
14
+ const ret = {
15
+ fieldName: '',
16
+ index: -1,
17
+ groupName: '',
18
+ }
19
+
20
+ if (res?.groups) {
21
+ ret.fieldName = res.groups.fieldName
22
+ ret.index = Number(res.groups.index)
23
+ ret.groupName = res.groups.groupName
24
+ }
25
+
26
+ return ret
27
+ }
28
+
29
+ function getGroupIndex(name: string, schemas: Record<string, any>) {
30
+ const indexs = Object.keys(schemas).map((key) => {
31
+ const { groupName, index } = parseGroupFieldName(key)
32
+ return groupName === name ? index : -1
33
+ })
34
+
35
+ return Math.max.apply(undefined, indexs)
36
+ }
37
+
38
+ function isSameGroup(name: string, groupName: string) {
39
+ const { groupName: gn } = parseGroupFieldName(name)
40
+
41
+ return gn === groupName
42
+ }
43
+
44
+ function isSameField(name: string, fieldName: string) {
45
+ const { fieldName: fn } = parseGroupFieldName(name)
46
+
47
+ return fn === fieldName
48
+ }
@@ -0,0 +1,91 @@
1
+ import { reactiveComputed } from '@vueuse/core'
2
+ import type { FormInstance, FormItemProps, FormProps, RuleObject } from 'ant-design-vue/es/form'
3
+ import { ref, computed } from 'vue'
4
+ import type { ComputedRef, UnwrapRef } from 'vue'
5
+ import { GROUP_SEP, buildGroupFieldName, getGroupIndex } from './useAntdForm.helpers'
6
+
7
+ export { useAntdForm }
8
+ export type { TField }
9
+
10
+ function useAntdForm<F extends Record<string, TField>, S extends GetFormState<F>, STF = S>(
11
+ schemas: F,
12
+ _options?: {
13
+ transform?: (state: S) => STF
14
+ },
15
+ ) {
16
+ const formRef = ref<FormInstance>()
17
+ const $form = {
18
+ get clearValidate() {
19
+ return formRef.value?.clearValidate
20
+ },
21
+ get getFieldsValue() {
22
+ return formRef.value?.getFieldsValue
23
+ },
24
+ get resetFields() {
25
+ return formRef.value?.resetFields
26
+ },
27
+ get scrollToField() {
28
+ return formRef.value?.scrollToField
29
+ },
30
+ get validate() {
31
+ return formRef.value?.validate
32
+ },
33
+ get validateFields() {
34
+ return formRef.value?.validateFields
35
+ },
36
+ setField(name: string, schema: TField) {
37
+ (schemas as any)[name] = schema
38
+ },
39
+ addGroup(groupName: string, groupSchemas: Record<string, TField>) {
40
+ const i = getGroupIndex(groupName, schemas) + 1
41
+
42
+ Object.keys(groupSchemas).forEach((key) => {
43
+ const name = buildGroupFieldName(groupName, i, key);
44
+ (schemas as any)[name] = groupSchemas[key]
45
+ })
46
+ },
47
+ removeGroup(groupName: string, index?: number) {
48
+ const i = index ?? getGroupIndex(groupName, schemas)
49
+
50
+ Object.keys(schemas).forEach((key) => {
51
+ if (key.startsWith(groupName + GROUP_SEP + i))
52
+ Reflect.deleteProperty(schemas, key)
53
+ })
54
+ },
55
+ }
56
+ const state = reactiveComputed(() => {
57
+ return Object.fromEntries(Object.entries(schemas).map(([k, v]) => [k, v.value])) as S
58
+ })
59
+ const props = reactiveComputed<FormProps>(() => {
60
+ return {
61
+ model: state,
62
+ ref: (c: any) => (formRef.value = c),
63
+ }
64
+ })
65
+ const itemProps = reactiveComputed(() => {
66
+ const propsTuple = Object.entries(schemas).map(([key, value]): [string, FormItemProps] => [
67
+ key,
68
+ { name: key, rules: value.rules },
69
+ ])
70
+ const props = Object.fromEntries(propsTuple) as Record<keyof S, FormItemProps>
71
+
72
+ return props
73
+ })
74
+ const stateTF = _options?.transform ? computed(() => _options.transform!(state)) : computed(() => state)
75
+
76
+ return {
77
+ $form,
78
+ state,
79
+ stateTF: stateTF as ComputedRef<STF>,
80
+ props,
81
+ itemProps,
82
+ }
83
+ }
84
+
85
+ interface TField {
86
+ value: any
87
+ rules?: RuleObject[]
88
+ }
89
+ type GetFormState<C extends Record<string, TField>> = {
90
+ [K in keyof C]: UnwrapRef<C[K]['value']>;
91
+ }
@@ -0,0 +1,65 @@
1
+ import { Modal as AntModal } from 'ant-design-vue'
2
+ import { createVNode, defineComponent, isProxy, reactive, toRefs, toRef } from 'vue'
3
+ import type { Component } from 'vue'
4
+ import type { ModalProps } from 'ant-design-vue'
5
+ import type { ComponentProps } from 'vue-component-type-helpers'
6
+ import type { Writable } from 'type-fest'
7
+ import { useComponentRef } from '../../vue'
8
+
9
+ const defaultModalProps: ModalProps = { open: false, destroyOnClose: true, wrapClassName: 'antd-cover__basic-modal' }
10
+
11
+ interface IComponentConfig<Comp extends Component> {
12
+ is: Comp
13
+ props?: Writable<ComponentProps<Comp>>
14
+ type?: 'modal' | 'body'
15
+ }
16
+
17
+ /**
18
+ * AntdModal 组件的辅助 Hook
19
+ * @param comp 组件配置,也可以直接传入组件,既 comp.is 参数
20
+ * @param comp.is 组件
21
+ * @param comp.props 组件的 Props
22
+ * @param comp.type 组件类型,modal 为弹窗组件,body 为弹窗内容组件
23
+ * @param modalProps AntdModal 组件的 Props
24
+ */
25
+ export function useAntdModal<Comp extends Component>(
26
+ comp: IComponentConfig<Comp> | Comp,
27
+ modalProps = defaultModalProps,
28
+ ) {
29
+ const _comp = ({ props: {}, type: 'body', ...((comp as any)?.is ? comp : { is: comp }) }) as Required<IComponentConfig<Comp>>
30
+ const compProps = reactive(_comp.props)
31
+ const compRef = useComponentRef(_comp.is)
32
+ const _modalProps: ModalProps = reactive({
33
+ ...defaultModalProps,
34
+ ...isProxy(modalProps) ? toRefs(modalProps) : modalProps,
35
+ confirmLoading: toRef(() => (compRef as any)?.loading),
36
+ onOk: () => (compRef as any)?.confirm?.(),
37
+ })
38
+ const modalSlotName = _comp.type === 'body' ? 'default' : 'modalRender'
39
+
40
+ const open = (newCompProps?: Partial<typeof compProps>, newAntdModalProps?: Omit<Partial<ModalProps>, 'open'>) => {
41
+ Object.assign(_modalProps, newAntdModalProps)
42
+ Object.assign(compProps, newCompProps)
43
+
44
+ _modalProps.open = true
45
+ }
46
+ const close = () => {
47
+ _modalProps.open = false
48
+ }
49
+
50
+ const PresetComponent = defineComponent({
51
+ render() {
52
+ return createVNode(AntModal, _modalProps, {
53
+ [modalSlotName]: () => createVNode(_comp.is, compProps as any),
54
+ })
55
+ },
56
+ })
57
+
58
+ _modalProps['onUpdate:open'] = (visiable) => {
59
+ _modalProps.open = visiable
60
+ }
61
+ (compProps as any).ref = compRef;
62
+ (compProps as any).onClose = close
63
+
64
+ return { PresetComponent, modalProps: _modalProps, compProps, $comp: compRef, open, close }
65
+ }
@@ -0,0 +1,70 @@
1
+ import { computed } from 'vue'
2
+ import type { UseQueryReturnType } from '@tanstack/vue-query'
3
+ import type { Table, TableProps } from 'ant-design-vue'
4
+ import type { ColumnType } from 'ant-design-vue/es/table/interface'
5
+ import type { ComponentProps } from 'vue-component-type-helpers'
6
+
7
+ export function useAntdTable<
8
+ UQRR extends UseQueryReturnType<any, any>,
9
+ QP extends Partial<{ page?: string | number; page_size?: string | number }>,
10
+ >(uqrt: UQRR, queryParams: QP) {
11
+ type RecordType = GetRecordType<UQRR>
12
+ type LocalTableProps = TableProps<RecordType>
13
+ type LocalColumnsType = NonNullable<LocalTableProps['columns']>
14
+
15
+ const { data, isFetching, isLoading } = uqrt
16
+
17
+ const onPaginationChange: ComponentProps<typeof Table>['onChange'] = (pagination) => {
18
+ const page = queryParams.page_size !== pagination.pageSize ? 1 : pagination.current
19
+ Object.assign(queryParams, { page, page_size: pagination.pageSize ?? 10 })
20
+ }
21
+ const defineColumns = (columnsFn: () => LocalColumnsType) => computed(columnsFn)
22
+
23
+ const tableProps = computed<LocalTableProps>(() => {
24
+ const { list, pagination } = data.value ?? {}
25
+
26
+ return {
27
+ dataSource: list,
28
+ pagination: {
29
+ disabled: isFetching.value,
30
+ current: Number(queryParams.page ?? 1),
31
+ pageSize: Number(queryParams.page_size ?? 10),
32
+ total: pagination?.total ?? 0,
33
+ },
34
+ loading: isLoading.value,
35
+ scroll: { x: 'max-content' },
36
+ sticky: true,
37
+ onChange: onPaginationChange as any,
38
+ }
39
+ })
40
+ const dataIndexs = new Proxy({} as Record<keyof RecordType, string>, {
41
+ get(_, p) {
42
+ return p
43
+ },
44
+ })
45
+ const bodyCellType = {} as {
46
+ index: number
47
+ text: any
48
+ value: any
49
+ record: RecordType
50
+ column: ColumnType<RecordType>
51
+ }
52
+
53
+ return {
54
+ /** ATable 的预设 Props */
55
+ tableProps,
56
+ /** 【类型辅助】基于接口数据类型推导出的 dataIndex,供 columns 的 dataIndex 使用 */
57
+ dataIndexs,
58
+ /** 【类型辅助】bodyCell 插槽数据的精确类型描述 */
59
+ bodyCellType,
60
+ /** 【类型辅助】用于定义出类型精确的 columns */
61
+ defineColumns,
62
+ onPaginationChange,
63
+ }
64
+ }
65
+
66
+ type GetRecordType<T> = T extends UseQueryReturnType<infer D, any>
67
+ ? D extends Api.PageData
68
+ ? NonNullable<D['list']>[0]
69
+ : never
70
+ : never
package/antd/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { useAntdModal } from './hooks/useAntdModal'
2
+ export { useAntdDrawer } from './hooks/useAntdDrawer'
3
+ export { useAntdTable } from './hooks/useAntdTable'
4
+ export { useAntdForm } from './hooks/useAntdForm'
5
+ export { createAntdModal } from './hooks/createAntdModal'
@@ -0,0 +1 @@
1
+ export { default as InfiniteQuery } from './src/InfiniteQuery.vue'