@ethan1979/jf-lib 0.0.32

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.
Files changed (47) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/README.md +3 -0
  3. package/babel.config.js +3 -0
  4. package/dist/App.vue.d.ts +2 -0
  5. package/dist/components/index.d.ts +1 -0
  6. package/dist/components/v-table/components/content/components/operations.vue.d.ts +47 -0
  7. package/dist/components/v-table/components/content/index.vue.d.ts +13 -0
  8. package/dist/components/v-table/components/modal-form.vue.d.ts +30 -0
  9. package/dist/components/v-table/components/search.vue.d.ts +12 -0
  10. package/dist/components/v-table/components/toolbar.vue.d.ts +11 -0
  11. package/dist/components/v-table/get-render-function.d.ts +6 -0
  12. package/dist/components/v-table/hooks.d.ts +80 -0
  13. package/dist/components/v-table/index.vue.d.ts +17 -0
  14. package/dist/components/v-table/util.d.ts +2 -0
  15. package/dist/favicon.ico +0 -0
  16. package/dist/jf-lib.es.js +6553 -0
  17. package/dist/jf-lib.umd.js +27 -0
  18. package/dist/main.d.ts +1 -0
  19. package/dist/style.css +1 -0
  20. package/dist/types/global.d.ts +43 -0
  21. package/dist/utils/utils.d.ts +11 -0
  22. package/docs/superpowers/plans/2026-09-19-collapsible-table-search.md +194 -0
  23. package/docs/superpowers/specs/2026-09-19-collapsible-table-search-design.md +80 -0
  24. package/index.html +13 -0
  25. package/package.json +31 -0
  26. package/public/favicon.ico +0 -0
  27. package/scripts/check-collapsible-search.mjs +67 -0
  28. package/src/App.vue +98 -0
  29. package/src/assets/logo.png +0 -0
  30. package/src/components/index.ts +18 -0
  31. package/src/components/v-table/components/content/components/operations.vue +119 -0
  32. package/src/components/v-table/components/content/index.vue +295 -0
  33. package/src/components/v-table/components/modal-form.vue +111 -0
  34. package/src/components/v-table/components/search.vue +181 -0
  35. package/src/components/v-table/components/toolbar.vue +121 -0
  36. package/src/components/v-table/get-render-function.ts +15 -0
  37. package/src/components/v-table/hooks.ts +128 -0
  38. package/src/components/v-table/index.vue +338 -0
  39. package/src/components/v-table/typings.d.ts +164 -0
  40. package/src/components/v-table/util.ts +14 -0
  41. package/src/env.d.ts +12 -0
  42. package/src/main.ts +13 -0
  43. package/src/types/global.ts +48 -0
  44. package/src/utils/utils.ts +77 -0
  45. package/tsconfig.json +38 -0
  46. package/tsconfig.node.json +8 -0
  47. package/vite.config.ts +47 -0
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <a-modal
3
+ v-model:visible="_visible"
4
+ modal-class="table-modal"
5
+ unmount-on-close
6
+ @before-ok="handleOk"
7
+ @close="handleClose"
8
+ >
9
+ <template #title> {{ props.title }} </template>
10
+ <div>
11
+ <a-form
12
+ ref="formRef"
13
+ :model="formData"
14
+ :style="{ width: '100%' }"
15
+ :label-col-props="{ span: 6 }"
16
+ :wrapper-col-props="{ span: 18 }"
17
+ :rules="rules"
18
+ >
19
+ <a-row>
20
+ <a-form-item
21
+ v-for="{
22
+ formItem: { dataIndex, title: label, tooltip },
23
+ render,
24
+ } in form || []"
25
+ :key="dataIndex"
26
+ :span="6"
27
+ :field="dataIndex"
28
+ :label="label"
29
+ >
30
+ <a-col :span="24">
31
+ <component
32
+ :is="getRenderFunction(render, props.renderParams)"
33
+ v-model="formData[dataIndex]"
34
+ :style="{
35
+ width: hasTooltip ? '94%' : '100%',
36
+ }"
37
+ />
38
+ <a-tooltip v-if="tooltip" :content="tooltip">
39
+ <div
40
+ style="display: inline-block; width: 6%; text-align: right"
41
+ >
42
+ <icon-info-circle />
43
+ </div>
44
+ </a-tooltip>
45
+ </a-col>
46
+ <!-- <a-col
47
+ :span="24"
48
+ style="font-size: 12px; color: var(--color-text-3)"
49
+ >
50
+ 123
51
+ </a-col> -->
52
+ </a-form-item>
53
+ </a-row>
54
+ </a-form>
55
+ </div>
56
+ </a-modal>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { Form } from '@arco-design/web-vue'
61
+ import { cloneDeep } from 'lodash'
62
+ import { computed, inject, ref } from 'vue'
63
+ import { BaseObj } from '../../../types/global'
64
+ import getRenderFunction from '../get-render-function'
65
+ import { useTableForm } from '../hooks'
66
+ import { TableConfig } from '../typings'
67
+
68
+ const props = defineProps<{
69
+ type: 'Add' | 'Update'
70
+ visible: boolean
71
+ title: string
72
+ renderParams?: BaseObj
73
+ }>()
74
+ const emit = defineEmits(['update:visible', 'okEvent'])
75
+
76
+ const config = inject<TableConfig>('config')
77
+ const { form, formData, rules, hasTooltip, resetFormData } = useTableForm(
78
+ config?.columns,
79
+ props.type
80
+ )
81
+ const _visible = computed<boolean>({
82
+ get() {
83
+ return props.visible
84
+ },
85
+ set(val) {
86
+ emit('update:visible', val)
87
+ },
88
+ })
89
+ const formRef = ref<InstanceType<typeof Form>>()
90
+ const handleOk = (done: (closed: boolean) => void) => {
91
+ formRef.value?.validate().then((errors) => {
92
+ if (errors) {
93
+ done(false)
94
+ return
95
+ }
96
+ emit('okEvent', cloneDeep(formData.value))
97
+ done(true)
98
+ })
99
+ }
100
+ const handleClose = () => {
101
+ resetFormData()
102
+ formRef?.value?.resetFields()
103
+ }
104
+
105
+ defineExpose({
106
+ formData,
107
+ resetFormData,
108
+ })
109
+ </script>
110
+
111
+ <style scoped></style>
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="form.length > 0" class="search-form-wrapper">
4
+ <a-row>
5
+ <a-col :flex="1">
6
+ <a-form
7
+ ref="formRef"
8
+ :model="formData"
9
+ :style="{ width: '100%' }"
10
+ label-align="left"
11
+ >
12
+ <a-row :gutter="16">
13
+ <a-col
14
+ v-for="{
15
+ formItem: { dataIndex, title },
16
+ render,
17
+ searchColSpan,
18
+ } in visibleForm"
19
+ :key="dataIndex"
20
+ :span="getSpan(searchColSpan)"
21
+ >
22
+ <a-form-item
23
+ :field="dataIndex"
24
+ :label="title"
25
+ :label-col-props="{
26
+ span: getFormItemSpan(searchColSpan, 'label'),
27
+ }"
28
+ :wrapper-col-props="{
29
+ span: getFormItemSpan(searchColSpan, 'wrapper'),
30
+ }"
31
+ >
32
+ <component
33
+ :is="getRenderFunction(render)"
34
+ @change="handleSearchParamsChange"
35
+ v-model="formData[dataIndex]"
36
+ style="width: 100%"
37
+ />
38
+ </a-form-item>
39
+ </a-col>
40
+ </a-row>
41
+ </a-form>
42
+ </a-col>
43
+ <a-divider
44
+ :style="{ height: isVertical ? '84px' : '32px' }"
45
+ direction="vertical"
46
+ />
47
+ <a-col :flex="'86px'" style="text-align: right">
48
+ <a-space
49
+ :direction="isVertical ? 'vertical' : 'horizontal'"
50
+ :size="18"
51
+ >
52
+ <a-button type="primary" @click="search">
53
+ <template #icon>
54
+ <icon-search />
55
+ </template>
56
+ 搜索
57
+ </a-button>
58
+ <a-button v-if="!config?.search?.hiddenReset" @click="reset">
59
+ <template #icon>
60
+ <icon-refresh />
61
+ </template>
62
+ 重置
63
+ </a-button>
64
+ <a-button
65
+ v-if="hasExpandableFields"
66
+ type="text"
67
+ :aria-expanded="expanded"
68
+ @click="expanded = !expanded"
69
+ >
70
+ <template #icon>
71
+ <icon-up v-if="expanded" />
72
+ <icon-down v-else />
73
+ </template>
74
+ {{ expanded ? '收起' : '展开' }}
75
+ </a-button>
76
+ </a-space>
77
+ </a-col>
78
+ </a-row>
79
+ </div>
80
+ <!-- <div class="search-btn-wrapper"></div> -->
81
+ </div>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ import { computed, nextTick, onMounted, PropType, ref } from 'vue'
86
+ import Form from '@arco-design/web-vue/es/form'
87
+ import { SearchColSpan, TableConfig } from '../typings'
88
+ import getRenderFunction from '../get-render-function'
89
+ import { useTableForm } from '../hooks'
90
+
91
+ const props = defineProps({
92
+ config: Object as PropType<TableConfig>,
93
+ })
94
+ const emit = defineEmits(['search'])
95
+
96
+ const { form, formData, resetFormData } = useTableForm(
97
+ props.config?.columns,
98
+ 'Search'
99
+ )
100
+ const formRef = ref<InstanceType<typeof Form>>()
101
+ const expanded = ref(false)
102
+ const defaultVisibleFields = computed(
103
+ () => props.config?.search?.defaultVisibleFields || []
104
+ )
105
+ const hasExpandableFields = computed(
106
+ () =>
107
+ props.config?.search?.collapsible === true &&
108
+ defaultVisibleFields.value.length > 0 &&
109
+ form.value.some(
110
+ ({ formItem: { dataIndex } }) =>
111
+ !defaultVisibleFields.value.includes(dataIndex)
112
+ )
113
+ )
114
+ const visibleForm = computed(() => {
115
+ if (!hasExpandableFields.value || expanded.value) return form.value
116
+ return form.value.filter(({ formItem: { dataIndex } }) =>
117
+ defaultVisibleFields.value.includes(dataIndex)
118
+ )
119
+ })
120
+ const search = () => {
121
+ emit('search', formData.value)
122
+ }
123
+
124
+ const handleSearchParamsChange=()=>{
125
+ if(props.config?.search?.searchOnchange!==false) search()
126
+ }
127
+
128
+ const reset = () => {
129
+ resetFormData()
130
+ search()
131
+ // formRef?.value?.resetFields()
132
+ }
133
+
134
+ const getSpan = (config: SearchColSpan | undefined) => {
135
+ if (!config) return 6
136
+ if (typeof config === 'number') return config
137
+ return config.span ?? 6
138
+ }
139
+ const formItemSpanSize = {
140
+ label: 7,
141
+ wrapper: 17,
142
+ }
143
+ const getFormItemSpan = (
144
+ config: SearchColSpan | undefined,
145
+ type: 'label' | 'wrapper'
146
+ ) => {
147
+ const size = formItemSpanSize[type]
148
+ if (!config || typeof config === 'number' || !config.formItem) {
149
+ return size
150
+ }
151
+ return config?.formItem[`${type}ColSpan`] || size
152
+ }
153
+
154
+ const isVertical = computed(() => {
155
+ let colSpans = 0
156
+ visibleForm.value.forEach(({ searchColSpan }) => {
157
+ colSpans += getSpan(searchColSpan)
158
+ })
159
+ return colSpans > 24
160
+ })
161
+
162
+ onMounted(()=>{
163
+ nextTick(()=>{
164
+ search()
165
+ })
166
+ })
167
+
168
+ defineExpose({
169
+ reset,
170
+ search,
171
+ })
172
+ </script>
173
+
174
+ <style lang="less">
175
+ .search-form-wrapper{
176
+ margin-top: 16px;
177
+ .arco-form-item{
178
+ margin-bottom: 16px;
179
+ }
180
+ }
181
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="table-toolbar">
3
+ <a-divider
4
+ style="margin-top: 0; margin-bottom: 16px"
5
+ divider-style
6
+ :style="{
7
+ ...dividerStyle,
8
+ borderBottom: `${
9
+ searchForm.length > 0 ? 1 : 0
10
+ }px solid var(--color-neutral-3)`,
11
+ }"
12
+ />
13
+ <a-row
14
+ style="padding-bottom: 16px"
15
+ v-if="config?.batchDelete || form.length > 0 || onBeforeAddModalOpen"
16
+ >
17
+ <a-col :span="16" class="toolbar-left">
18
+ <a-space>
19
+ <slot name="toolbar-left">
20
+ <a-button
21
+ v-if="form.length > 0 || onBeforeAddModalOpen"
22
+ type="primary"
23
+ @click="handleClickAdd"
24
+ >
25
+ <template #icon>
26
+ <icon-plus />
27
+ </template>
28
+ 新增
29
+ </a-button>
30
+ <a-popconfirm
31
+ v-if="config?.batchDelete"
32
+ :content="`是否删除选中的${selectedRowKeys?.length}项内容`"
33
+ :disabled="selectedRowKeys?.length === 0"
34
+ :ok-button-props="{
35
+ status: 'danger',
36
+ }"
37
+ type="error"
38
+ position="bl"
39
+ @ok="emit('batchDelete')"
40
+ >
41
+ <a-button
42
+ type="primary"
43
+ status="danger"
44
+ :disabled="selectedRowKeys?.length === 0"
45
+ >
46
+ <template #icon>
47
+ <icon-minus />
48
+ </template>
49
+ 删除
50
+ </a-button>
51
+ </a-popconfirm>
52
+ </slot>
53
+ <slot name="toolbar-left-extend"></slot>
54
+ </a-space>
55
+ </a-col>
56
+ <a-col :span="8" class="toolbar-right">
57
+ <slot name="toolbar-right"></slot>
58
+ </a-col>
59
+ </a-row>
60
+ <template v-if="form.length > 0">
61
+ <modal-form
62
+ v-model:visible="visible"
63
+ title="新增"
64
+ type="Add"
65
+ @ok-event="(data) => emit('add', data)"
66
+ />
67
+ </template>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { PropType, Ref, inject, ref, computed } from "vue";
73
+ import { useTableForm } from "../hooks";
74
+ import { TableConfig } from "../typings";
75
+
76
+ import ModalForm from "./modal-form.vue";
77
+
78
+ const props = defineProps({
79
+ config: Object as PropType<TableConfig>,
80
+ });
81
+ const emit = defineEmits(["add", "batchDelete"]);
82
+
83
+ const selectedRowKeys = inject<Ref<number[] | string[]>>("selectedRowKeys");
84
+
85
+ const visible = ref(false);
86
+ const { form } = useTableForm(props.config?.columns, "Add");
87
+ const onBeforeAddModalOpen = computed(() => {
88
+ return props.config?.toolbar?.onBeforeAddModalOpen;
89
+ });
90
+ const handleClickAdd = async () => {
91
+ if (typeof onBeforeAddModalOpen.value === "function") {
92
+ const result = await onBeforeAddModalOpen.value();
93
+ if (result === false) return;
94
+ }
95
+ visible.value = true;
96
+ };
97
+
98
+ const { form: searchForm } = useTableForm(props.config?.columns, "Search");
99
+ const dividerStyle = computed(() => {
100
+ return props.config?.card
101
+ ? {
102
+ left: "-16px",
103
+ width: "calc(100% + 32px)",
104
+ minWidth: "calc(100% + 32px)",
105
+ }
106
+ : {};
107
+ });
108
+ </script>
109
+
110
+ <style lang="less">
111
+ .table-toolbar {
112
+ // padding-bottom: 16px;
113
+ .arco-space {
114
+ width: 100%;
115
+ }
116
+
117
+ .toolbar-right {
118
+ text-align: right;
119
+ }
120
+ }
121
+ </style>
@@ -0,0 +1,15 @@
1
+ import { h, getCurrentInstance } from 'vue'
2
+ import { toPascalCase } from '../../utils/utils'
3
+ import { FormRender } from './typings'
4
+ import { BaseObj } from '../../types/global'
5
+
6
+ export default (render: FormRender, renderParams?: BaseObj | undefined) => {
7
+ if (typeof render === 'function') {
8
+ return render(renderParams)
9
+ }
10
+ const component =
11
+ getCurrentInstance()?.appContext.components[toPascalCase(render.type)] ||
12
+ render.type
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ return h(component as any, render.props)
15
+ }
@@ -0,0 +1,128 @@
1
+ import { FieldRule } from '@arco-design/web-vue/es/form/interface'
2
+ import { cloneDeep } from 'lodash'
3
+ import { ref, toRefs, watchEffect } from 'vue'
4
+ import { BaseObj } from '../../types/global'
5
+ import { Columns, FormRender, SearchColSpan } from './typings'
6
+
7
+ const defineRender = (title: string) => ({
8
+ type: 'a-input',
9
+ props: {
10
+ placeholder: `请输入${title}`,
11
+ allowClear: true,
12
+ maxLength: 100,
13
+ },
14
+ })
15
+
16
+ type FormType = 'Search' | 'Update' | 'Add'
17
+
18
+ const getIndexAndConvert = (column: Columns, type: FormType) => {
19
+ const { form } = column
20
+ if (typeof form !== 'object') return 0
21
+ const _key = type.toLocaleLowerCase() as 'add' | 'update' | 'search'
22
+ // console.log('_key', _key, form[`${_key}Config`], form.defaultValue=11111)
23
+ if (form[`${_key}Config`]) {
24
+ column.form = {
25
+ ...form,
26
+ ...form[`${_key}Config`],
27
+ }
28
+ }
29
+ return form?.yIndex || 0
30
+ }
31
+
32
+ const getForm = (columns: Array<Columns> | undefined, type: FormType) => {
33
+ // const formData: BaseObj = {}
34
+ const defaultValues: BaseObj = {}
35
+ const rules: Record<string, FieldRule[]> = {}
36
+ let hasTooltip = false
37
+ const addRule = (dataIndex: string, rule: FieldRule | FieldRule[]) => {
38
+ if (!rules[dataIndex]) rules[dataIndex] = []
39
+ if (rule instanceof Array) {
40
+ rules[dataIndex].push(...rule)
41
+ } else {
42
+ rules[dataIndex].push(rule)
43
+ }
44
+ }
45
+ const _columns = cloneDeep(columns?.filter((column) => column.form))
46
+ _columns?.sort((a, b) => {
47
+ return getIndexAndConvert(b, type) - getIndexAndConvert(a, type)
48
+ })
49
+ const form = _columns
50
+ ?.map(({ form, title, dataIndex }) => {
51
+ let formItem: BaseObj = {
52
+ title,
53
+ dataIndex,
54
+ }
55
+ if (typeof form !== 'object') {
56
+ defaultValues[dataIndex as string] = ''
57
+ const render =
58
+ typeof form === 'boolean' ? defineRender(title as string) : form
59
+ return {
60
+ formItem,
61
+ render,
62
+ }
63
+ }
64
+ if (form[`hidden${type}`]) return false
65
+ const { searchColSpan, tooltip } = form
66
+ if (tooltip) hasTooltip = true
67
+ formItem = {
68
+ ...formItem,
69
+ tooltip,
70
+ }
71
+ if (form.title) formItem.title = form.title
72
+ // console.log('form.defaultValue', form.defaultValue, form)
73
+ defaultValues[dataIndex as string] = form.defaultValue ?? ''
74
+ if (form.required)
75
+ addRule(dataIndex as string, {
76
+ required: true,
77
+ message: `${title}不能为空`,
78
+ })
79
+ if (form.rule) addRule(dataIndex as string, form.rule)
80
+ return {
81
+ formItem,
82
+ render: form.render || defineRender(title as string),
83
+ searchColSpan,
84
+ }
85
+ })
86
+ .filter((item) => item)
87
+ return {
88
+ form: form as Array<{
89
+ formItem: {
90
+ title: string
91
+ dataIndex: string
92
+ tooltip?: string
93
+ }
94
+ render: FormRender
95
+ searchColSpan?: SearchColSpan
96
+ }>,
97
+ formData: cloneDeep(defaultValues),
98
+ defaultValues,
99
+ rules,
100
+ hasTooltip,
101
+ }
102
+ }
103
+
104
+ export function useTableForm(
105
+ columns: Array<Columns> | undefined,
106
+ type: 'Search' | 'Update' | 'Add'
107
+ ) {
108
+ const form = ref(getForm(columns, type))
109
+ watchEffect(() => {
110
+ // form.value = getForm(columns, type)
111
+ const {
112
+ form: _form,
113
+ formData,
114
+ rules,
115
+ defaultValues,
116
+ } = getForm(columns, type)
117
+ form.value.form = _form
118
+ form.value.formData = formData
119
+ form.value.rules = rules
120
+ form.value.defaultValues = defaultValues
121
+ })
122
+ return {
123
+ ...toRefs(form.value),
124
+ resetFormData() {
125
+ form.value.formData = cloneDeep(form.value.defaultValues)
126
+ },
127
+ }
128
+ }