@peng_kai/kit 0.2.18 → 0.2.20
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.
- package/admin/components/settings/index.ts +1 -0
- package/admin/components/settings/src/SchemaForm.vue +283 -0
- package/admin/components/settings/src/Settings.vue +23 -274
- package/antd/hooks/useAntdForm.ts +1 -1
- package/package.json +1 -1
- package/utils/number.ts +20 -0
- package/vue/components/nav-manager/NavManager.vue +46 -24
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cloneDeep, mapKeys, mapValues } from 'lodash-es';
|
|
3
|
+
import { CheckboxGroup, DatePicker, type DatePickerProps, Form, FormItem, Input, InputNumber, RadioGroup, RangePicker, Select, Textarea } from 'ant-design-vue';
|
|
4
|
+
import { computed, toRef } from 'vue';
|
|
5
|
+
import dayjs from '../../../../libs/dayjs';
|
|
6
|
+
import { type ItemSchema, useAntdForm } from '../../../../antd';
|
|
7
|
+
|
|
8
|
+
export interface IConfigDetail {
|
|
9
|
+
category_id: number
|
|
10
|
+
data_type: string
|
|
11
|
+
form_type: number
|
|
12
|
+
key: string
|
|
13
|
+
label: string
|
|
14
|
+
options: any
|
|
15
|
+
required: number
|
|
16
|
+
sort: number
|
|
17
|
+
status: number
|
|
18
|
+
summary: string
|
|
19
|
+
value: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum FormTypes {
|
|
23
|
+
/** 数字输入框 */
|
|
24
|
+
NUMBER_INPUT = 0,
|
|
25
|
+
/** 文本输入框 */
|
|
26
|
+
TEXT_INPUT = 1,
|
|
27
|
+
/** 多行文本输入框 */
|
|
28
|
+
TEXTAREA = 2,
|
|
29
|
+
/** 开关 */
|
|
30
|
+
SWITCH = 3,
|
|
31
|
+
/** 单选框 */
|
|
32
|
+
RADIO = 4,
|
|
33
|
+
/** 多选框 */
|
|
34
|
+
CHECKBOX = 5,
|
|
35
|
+
/** 单选下拉选择器 */
|
|
36
|
+
SINGLE_SELECT = 6,
|
|
37
|
+
/** 多选下拉选择器 */
|
|
38
|
+
MULTIPLE_SELECT = 7,
|
|
39
|
+
/** 单一日期选择器 */
|
|
40
|
+
SINGLE_DATE_PICKER = 13,
|
|
41
|
+
/** 日期范围选择器 */
|
|
42
|
+
RANGE_DATE_PICKER = 14,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const antdPropsResolvers: Record<number, (config: IConfigDetail) => IConfigDetail> = {
|
|
46
|
+
[FormTypes.SWITCH](config) {
|
|
47
|
+
const props: any = {};
|
|
48
|
+
|
|
49
|
+
if (config.options) {
|
|
50
|
+
props.options = config.options.split(',').map((item: any) => {
|
|
51
|
+
const [label, value] = item.split(':');
|
|
52
|
+
return { value, label };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
props.options = [{ value: '1', label: '是' }, { value: '0', label: '否' }];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
config.options = props;
|
|
60
|
+
|
|
61
|
+
return config;
|
|
62
|
+
},
|
|
63
|
+
[FormTypes.RADIO](config) {
|
|
64
|
+
return this[FormTypes.SWITCH](config);
|
|
65
|
+
},
|
|
66
|
+
[FormTypes.CHECKBOX](config) {
|
|
67
|
+
const props: any = {};
|
|
68
|
+
props.options = config.options.split(',').map((item: any) => {
|
|
69
|
+
const [label, value] = item.split(':');
|
|
70
|
+
return { value, label };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
config.value = config.value.split(',');
|
|
74
|
+
config.options = props;
|
|
75
|
+
|
|
76
|
+
return config;
|
|
77
|
+
},
|
|
78
|
+
[FormTypes.SINGLE_SELECT](config) {
|
|
79
|
+
return this[FormTypes.SWITCH](config);
|
|
80
|
+
},
|
|
81
|
+
[FormTypes.MULTIPLE_SELECT](config) {
|
|
82
|
+
return this[FormTypes.CHECKBOX](config);
|
|
83
|
+
},
|
|
84
|
+
[FormTypes.SINGLE_DATE_PICKER](config) {
|
|
85
|
+
const props: DatePickerProps = {};
|
|
86
|
+
|
|
87
|
+
if (config.options) {
|
|
88
|
+
const [minDate, maxDate] = (config.options || '/').split('/').map(dayjs);
|
|
89
|
+
props.disabledDate = (current) => {
|
|
90
|
+
return current.isBefore(minDate) || current.isAfter(maxDate);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
config.value = config.value === '' ? undefined : dayjs(config.value);
|
|
95
|
+
config.options = props;
|
|
96
|
+
|
|
97
|
+
return config;
|
|
98
|
+
},
|
|
99
|
+
[FormTypes.RANGE_DATE_PICKER](config) {
|
|
100
|
+
const props: DatePickerProps = {};
|
|
101
|
+
|
|
102
|
+
if (config.options) {
|
|
103
|
+
const [minDate, maxDate] = (config.options || '/').split('/').map(dayjs);
|
|
104
|
+
props.disabledDate = (current) => {
|
|
105
|
+
return current.isBefore(minDate) || current.isAfter(maxDate);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.value === '')
|
|
110
|
+
config.value = [undefined, undefined];
|
|
111
|
+
else
|
|
112
|
+
config.value = (config.value || '/').split('/').map(dayjs);
|
|
113
|
+
|
|
114
|
+
config.options = props;
|
|
115
|
+
|
|
116
|
+
return config;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const antdValueResolvers: Record<number, (value: any) => any> = {
|
|
121
|
+
[FormTypes.NUMBER_INPUT](value) {
|
|
122
|
+
return String(value);
|
|
123
|
+
},
|
|
124
|
+
[FormTypes.CHECKBOX](value) {
|
|
125
|
+
if (Array.isArray(value))
|
|
126
|
+
return value.join(',');
|
|
127
|
+
|
|
128
|
+
return value;
|
|
129
|
+
},
|
|
130
|
+
[FormTypes.MULTIPLE_SELECT](value) {
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
return value.join(',');
|
|
133
|
+
|
|
134
|
+
return value;
|
|
135
|
+
},
|
|
136
|
+
[FormTypes.SINGLE_DATE_PICKER](value) {
|
|
137
|
+
if (dayjs.isDayjs(value))
|
|
138
|
+
return value.format('YYYY-MM-DD');
|
|
139
|
+
|
|
140
|
+
return value;
|
|
141
|
+
},
|
|
142
|
+
[FormTypes.RANGE_DATE_PICKER](value) {
|
|
143
|
+
if (dayjs.isDayjs(value[0]) && dayjs.isDayjs(value[1]))
|
|
144
|
+
return [value[0].format('YYYY-MM-DD'), value[1].format('YYYY-MM-DD')].join('/');
|
|
145
|
+
|
|
146
|
+
return value;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<script setup lang="ts">
|
|
152
|
+
const props = withDefaults(defineProps<{
|
|
153
|
+
formCofnig: IConfigDetail[]
|
|
154
|
+
disabled?: boolean
|
|
155
|
+
}>(), {
|
|
156
|
+
disabled: false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const configList = computed(() => {
|
|
160
|
+
const list = cloneDeep(props.formCofnig);
|
|
161
|
+
|
|
162
|
+
if (!list?.length)
|
|
163
|
+
return;
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < list.length; i++) {
|
|
166
|
+
const config = { ...list[i] };
|
|
167
|
+
list[i] = antdPropsResolvers[config.form_type]?.(config) ?? config;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return list;
|
|
171
|
+
});
|
|
172
|
+
const configMap = computed(() => mapKeys(cloneDeep(props.formCofnig ?? []), 'key'));
|
|
173
|
+
function formSchema() {
|
|
174
|
+
const _configList = configList.value ?? [];
|
|
175
|
+
const entries = _configList.map(config => [
|
|
176
|
+
config.key,
|
|
177
|
+
{
|
|
178
|
+
value: cloneDeep(config.value),
|
|
179
|
+
rules: config.required ? [{ required: true }] : undefined,
|
|
180
|
+
},
|
|
181
|
+
] as [string, ItemSchema]);
|
|
182
|
+
const schema = Object.fromEntries(entries);
|
|
183
|
+
|
|
184
|
+
return schema;
|
|
185
|
+
}
|
|
186
|
+
const settingForm = useAntdForm(formSchema, {
|
|
187
|
+
transform(state) {
|
|
188
|
+
return mapValues(state, (v, k) => {
|
|
189
|
+
const formType = configMap.value[k].form_type;
|
|
190
|
+
const resolver = antdValueResolvers[formType];
|
|
191
|
+
|
|
192
|
+
return resolver ? resolver(v) : v;
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function reset() {
|
|
198
|
+
configList.value?.forEach(config => settingForm.state[config.key] = cloneDeep(config.value));
|
|
199
|
+
settingForm.$form.clearValidate?.();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function validate() {
|
|
203
|
+
const data = await settingForm.$form.validate?.();
|
|
204
|
+
return data ? settingForm.stateTF : undefined;
|
|
205
|
+
// return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
defineExpose({ reset, validate });
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<template>
|
|
212
|
+
<Form
|
|
213
|
+
v-bind="settingForm.props"
|
|
214
|
+
class="ant-cover__col2-form"
|
|
215
|
+
:model="settingForm.state"
|
|
216
|
+
layout="vertical"
|
|
217
|
+
:disabled="props.disabled"
|
|
218
|
+
>
|
|
219
|
+
<template v-for="item of configList" :key="item.key">
|
|
220
|
+
<FormItem v-bind="settingForm.itemProps[item.key]" :label="item.label" :extra="item.summary">
|
|
221
|
+
<slot
|
|
222
|
+
v-if="$slots[item.key]"
|
|
223
|
+
:name="item.key"
|
|
224
|
+
:state="toRef(settingForm.state, item.key)"
|
|
225
|
+
:config="item"
|
|
226
|
+
:orginConfig="configMap[item.key]"
|
|
227
|
+
/>
|
|
228
|
+
<template v-else>
|
|
229
|
+
<slot v-if="item.form_type === FormTypes.NUMBER_INPUT" :name="FormTypes.NUMBER_INPUT">
|
|
230
|
+
<InputNumber v-model:value="settingForm.state[item.key]" class="w-full" />
|
|
231
|
+
</slot>
|
|
232
|
+
|
|
233
|
+
<slot v-else-if="item.form_type === FormTypes.TEXT_INPUT" :name="FormTypes.TEXT_INPUT">
|
|
234
|
+
<Input v-model:value="settingForm.state[item.key]" allowClear />
|
|
235
|
+
</slot>
|
|
236
|
+
|
|
237
|
+
<slot v-else-if="item.form_type === FormTypes.TEXTAREA" :name="FormTypes.TEXTAREA">
|
|
238
|
+
<Textarea v-model:value="settingForm.state[item.key]" :rows="4" />
|
|
239
|
+
</slot>
|
|
240
|
+
|
|
241
|
+
<slot v-else-if="item.form_type === FormTypes.SWITCH" :name="FormTypes.SWITCH">
|
|
242
|
+
<RadioGroup
|
|
243
|
+
v-model:value="settingForm.state[item.key]"
|
|
244
|
+
v-bind="item.options"
|
|
245
|
+
buttonStyle="solid"
|
|
246
|
+
optionType="button"
|
|
247
|
+
/>
|
|
248
|
+
</slot>
|
|
249
|
+
|
|
250
|
+
<slot v-else-if="item.form_type === FormTypes.RADIO" :name="FormTypes.RADIO">
|
|
251
|
+
<RadioGroup
|
|
252
|
+
v-model:value="settingForm.state[item.key]"
|
|
253
|
+
buttonStyle="solid"
|
|
254
|
+
v-bind="item.options"
|
|
255
|
+
/>
|
|
256
|
+
</slot>
|
|
257
|
+
|
|
258
|
+
<slot v-else-if="item.form_type === FormTypes.CHECKBOX" :name="FormTypes.CHECKBOX">
|
|
259
|
+
<CheckboxGroup v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
260
|
+
</slot>
|
|
261
|
+
|
|
262
|
+
<slot v-else-if="item.form_type === FormTypes.SINGLE_SELECT" :name="FormTypes.SINGLE_SELECT">
|
|
263
|
+
<Select v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
264
|
+
</slot>
|
|
265
|
+
|
|
266
|
+
<slot v-else-if="item.form_type === FormTypes.MULTIPLE_SELECT" :name="FormTypes.MULTIPLE_SELECT">
|
|
267
|
+
<Select v-model:value="settingForm.state[item.key]" v-bind="item.options" mode="multiple" />
|
|
268
|
+
</slot>
|
|
269
|
+
|
|
270
|
+
<slot v-else-if="item.form_type === FormTypes.SINGLE_DATE_PICKER" :name="FormTypes.SINGLE_DATE_PICKER">
|
|
271
|
+
<DatePicker v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
272
|
+
</slot>
|
|
273
|
+
|
|
274
|
+
<slot v-else-if="item.form_type === FormTypes.RANGE_DATE_PICKER" :name="FormTypes.RANGE_DATE_PICKER">
|
|
275
|
+
<RangePicker v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
276
|
+
</slot>
|
|
277
|
+
|
|
278
|
+
<span v-else class="text-red">没有找到预设 form_type:{{ item.form_type }},可以通过 {{ item.key }} 插槽自定义</span>
|
|
279
|
+
</template>
|
|
280
|
+
</FormItem>
|
|
281
|
+
</template>
|
|
282
|
+
</Form>
|
|
283
|
+
</template>
|
|
@@ -1,156 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { computed, ref, toRef } from 'vue';
|
|
2
|
+
import { Button, Card } from 'ant-design-vue';
|
|
3
|
+
import { computed, ref } from 'vue';
|
|
5
4
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
interface IConfigDetail {
|
|
10
|
-
category_id: number
|
|
11
|
-
data_type: string
|
|
12
|
-
form_type: number
|
|
13
|
-
key: string
|
|
14
|
-
label: string
|
|
15
|
-
options: any
|
|
16
|
-
required: number
|
|
17
|
-
sort: number
|
|
18
|
-
status: number
|
|
19
|
-
summary: string
|
|
20
|
-
value: any
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
enum FormTypes {
|
|
24
|
-
/** 数字输入框 */
|
|
25
|
-
NUMBER_INPUT = 0,
|
|
26
|
-
/** 文本输入框 */
|
|
27
|
-
TEXT_INPUT = 1,
|
|
28
|
-
/** 多行文本输入框 */
|
|
29
|
-
TEXTAREA = 2,
|
|
30
|
-
/** 开关 */
|
|
31
|
-
SWITCH = 3,
|
|
32
|
-
/** 单选框 */
|
|
33
|
-
RADIO = 4,
|
|
34
|
-
/** 多选框 */
|
|
35
|
-
CHECKBOX = 5,
|
|
36
|
-
/** 单选下拉选择器 */
|
|
37
|
-
SINGLE_SELECT = 6,
|
|
38
|
-
/** 多选下拉选择器 */
|
|
39
|
-
MULTIPLE_SELECT = 7,
|
|
40
|
-
/** 单一日期选择器 */
|
|
41
|
-
SINGLE_DATE_PICKER = 13,
|
|
42
|
-
/** 日期范围选择器 */
|
|
43
|
-
RANGE_DATE_PICKER = 14,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const antdPropsResolvers: Record<number, (config: IConfigDetail) => IConfigDetail> = {
|
|
47
|
-
[FormTypes.SWITCH](config) {
|
|
48
|
-
const props: any = {};
|
|
49
|
-
|
|
50
|
-
if (config.options) {
|
|
51
|
-
props.options = config.options.split(',').map((item: any) => {
|
|
52
|
-
const [label, value] = item.split(':');
|
|
53
|
-
return { value, label };
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
props.options = [{ value: '1', label: '是' }, { value: '0', label: '否' }];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
config.options = props;
|
|
61
|
-
|
|
62
|
-
return config;
|
|
63
|
-
},
|
|
64
|
-
[FormTypes.RADIO](config) {
|
|
65
|
-
return this[FormTypes.SWITCH](config);
|
|
66
|
-
},
|
|
67
|
-
[FormTypes.CHECKBOX](config) {
|
|
68
|
-
const props: any = {};
|
|
69
|
-
props.options = config.options.split(',').map((item: any) => {
|
|
70
|
-
const [label, value] = item.split(':');
|
|
71
|
-
return { value, label };
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
config.value = config.value.split(',');
|
|
75
|
-
config.options = props;
|
|
76
|
-
|
|
77
|
-
return config;
|
|
78
|
-
},
|
|
79
|
-
[FormTypes.SINGLE_SELECT](config) {
|
|
80
|
-
return this[FormTypes.SWITCH](config);
|
|
81
|
-
},
|
|
82
|
-
[FormTypes.MULTIPLE_SELECT](config) {
|
|
83
|
-
return this[FormTypes.CHECKBOX](config);
|
|
84
|
-
},
|
|
85
|
-
[FormTypes.SINGLE_DATE_PICKER](config) {
|
|
86
|
-
const props: DatePickerProps = {};
|
|
87
|
-
|
|
88
|
-
if (config.options) {
|
|
89
|
-
const [minDate, maxDate] = (config.options || '/').split('/').map(dayjs);
|
|
90
|
-
props.disabledDate = (current) => {
|
|
91
|
-
return current.isBefore(minDate) || current.isAfter(maxDate);
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
config.value = config.value === '' ? undefined : dayjs(config.value);
|
|
96
|
-
config.options = props;
|
|
97
|
-
|
|
98
|
-
return config;
|
|
99
|
-
},
|
|
100
|
-
[FormTypes.RANGE_DATE_PICKER](config) {
|
|
101
|
-
const props: DatePickerProps = {};
|
|
102
|
-
|
|
103
|
-
if (config.options) {
|
|
104
|
-
const [minDate, maxDate] = (config.options || '/').split('/').map(dayjs);
|
|
105
|
-
props.disabledDate = (current) => {
|
|
106
|
-
return current.isBefore(minDate) || current.isAfter(maxDate);
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (config.value === '')
|
|
111
|
-
config.value = [undefined, undefined];
|
|
112
|
-
else
|
|
113
|
-
config.value = (config.value || '/').split('/').map(dayjs);
|
|
114
|
-
|
|
115
|
-
config.options = props;
|
|
116
|
-
|
|
117
|
-
return config;
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
const antdValueResolvers: Record<number, (value: any) => any> = {
|
|
121
|
-
[FormTypes.NUMBER_INPUT](value) {
|
|
122
|
-
return String(value);
|
|
123
|
-
},
|
|
124
|
-
[FormTypes.CHECKBOX](value) {
|
|
125
|
-
if (Array.isArray(value))
|
|
126
|
-
return value.join(',');
|
|
127
|
-
|
|
128
|
-
return value;
|
|
129
|
-
},
|
|
130
|
-
[FormTypes.MULTIPLE_SELECT](value) {
|
|
131
|
-
if (Array.isArray(value))
|
|
132
|
-
return value.join(',');
|
|
133
|
-
|
|
134
|
-
return value;
|
|
135
|
-
},
|
|
136
|
-
[FormTypes.SINGLE_DATE_PICKER](value) {
|
|
137
|
-
if (dayjs.isDayjs(value))
|
|
138
|
-
return value.format('YYYY-MM-DD');
|
|
139
|
-
|
|
140
|
-
return value;
|
|
141
|
-
},
|
|
142
|
-
[FormTypes.RANGE_DATE_PICKER](value) {
|
|
143
|
-
if (dayjs.isDayjs(value[0]) && dayjs.isDayjs(value[1]))
|
|
144
|
-
return [value[0].format('YYYY-MM-DD'), value[1].format('YYYY-MM-DD')].join('/');
|
|
145
|
-
|
|
146
|
-
return value;
|
|
147
|
-
},
|
|
148
|
-
};
|
|
5
|
+
import { useTemplateRefs } from '../../../../vue';
|
|
6
|
+
import type SchemaForm from './SchemaForm.vue';
|
|
7
|
+
import type { IConfigDetail } from './SchemaForm.vue';
|
|
149
8
|
</script>
|
|
150
9
|
|
|
151
10
|
<script setup lang="ts">
|
|
152
11
|
const props = withDefaults(defineProps<{
|
|
153
|
-
categoryApi
|
|
12
|
+
categoryApi?: Api.Request
|
|
154
13
|
configApi: Api.Request
|
|
155
14
|
updateApi: Api.Request
|
|
156
15
|
readonly?: boolean
|
|
@@ -159,35 +18,25 @@ const props = withDefaults(defineProps<{
|
|
|
159
18
|
});
|
|
160
19
|
|
|
161
20
|
const queryClient = useQueryClient();
|
|
21
|
+
const [refs, setRefs] = useTemplateRefs<{
|
|
22
|
+
form: typeof SchemaForm
|
|
23
|
+
}>();
|
|
162
24
|
|
|
163
25
|
/* 配置分类 */
|
|
164
26
|
const categoryId = ref(1);
|
|
165
27
|
const categoryQuerier = useQuery({
|
|
166
|
-
queryKey: [props.categoryApi
|
|
167
|
-
queryFn: () => props.categoryApi(undefined),
|
|
28
|
+
queryKey: [props.categoryApi?.id, 'system-setting-category'],
|
|
29
|
+
queryFn: () => props.categoryApi?.(undefined) ?? null,
|
|
168
30
|
});
|
|
169
31
|
const categoryList = computed(() => categoryQuerier.data.value?.map((item: any) => ({ key: String(item.type), tab: item.label })));
|
|
170
32
|
|
|
171
33
|
/* 当前分类下的配置 */
|
|
34
|
+
const configQuerierEnabled = computed(() => categoryQuerier.data.value !== undefined);
|
|
172
35
|
const configQuerier = useQuery<IConfigDetail[] | undefined>({
|
|
173
|
-
enabled:
|
|
36
|
+
enabled: configQuerierEnabled,
|
|
174
37
|
queryKey: [props.configApi.id, categoryId],
|
|
175
38
|
queryFn: () => props.configApi({ category_id: categoryId.value }),
|
|
176
39
|
});
|
|
177
|
-
const configList = computed(() => {
|
|
178
|
-
const list = cloneDeep(configQuerier.data.value);
|
|
179
|
-
|
|
180
|
-
if (!list?.length)
|
|
181
|
-
return;
|
|
182
|
-
|
|
183
|
-
for (let i = 0; i < list.length; i++) {
|
|
184
|
-
const config = { ...list[i] };
|
|
185
|
-
list[i] = antdPropsResolvers[config.form_type]?.(config) ?? config;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return list;
|
|
189
|
-
});
|
|
190
|
-
const configMap = computed(() => mapKeys(cloneDeep(configQuerier.data.value ?? []), 'key'));
|
|
191
40
|
|
|
192
41
|
/* 配置表单 */
|
|
193
42
|
const settingMutator = useMutation({
|
|
@@ -198,42 +47,12 @@ const settingMutator = useMutation({
|
|
|
198
47
|
queryClient.invalidateQueries({ queryKey: [props.configApi.id], exact: false });
|
|
199
48
|
},
|
|
200
49
|
});
|
|
201
|
-
const formSchema = computed(() => {
|
|
202
|
-
const _configList = configList.value ?? [];
|
|
203
|
-
const entries = _configList.map(config => [
|
|
204
|
-
config.key,
|
|
205
|
-
{
|
|
206
|
-
value: cloneDeep(config.value),
|
|
207
|
-
rules: config.required ? [{ required: true }] : undefined,
|
|
208
|
-
},
|
|
209
|
-
] as [string, ItemSchema]);
|
|
210
|
-
const schema = Object.fromEntries(entries);
|
|
211
|
-
|
|
212
|
-
return schema;
|
|
213
|
-
});
|
|
214
|
-
const settingForm = useAntdForm(formSchema, {
|
|
215
|
-
transform(state) {
|
|
216
|
-
return mapValues(state, (v, k) => {
|
|
217
|
-
const formType = configMap.value[k].form_type;
|
|
218
|
-
const resolver = antdValueResolvers[formType];
|
|
219
|
-
|
|
220
|
-
return resolver ? resolver(v) : v;
|
|
221
|
-
});
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
50
|
|
|
225
51
|
/** 提交表单内容 */
|
|
226
52
|
async function submitSetting() {
|
|
227
|
-
const body = await
|
|
53
|
+
const body = await refs.form?.validate();
|
|
228
54
|
|
|
229
|
-
|
|
230
|
-
await settingMutator.mutateAsync({ requestBody: settingForm.stateTF });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** 重置表单内容 */
|
|
234
|
-
function resetSetting() {
|
|
235
|
-
configList.value?.forEach(config => settingForm.state[config.key] = cloneDeep(config.value));
|
|
236
|
-
settingForm.$form.clearValidate?.();
|
|
55
|
+
body && await settingMutator.mutateAsync({ requestBody: body });
|
|
237
56
|
}
|
|
238
57
|
</script>
|
|
239
58
|
|
|
@@ -245,79 +64,16 @@ function resetSetting() {
|
|
|
245
64
|
:loading="configQuerier.isPending.value"
|
|
246
65
|
@tabChange="key => categoryId = Number(key)"
|
|
247
66
|
>
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<FormItem v-bind="settingForm.itemProps[item.key]" :label="item.label" :extra="item.summary">
|
|
256
|
-
<slot
|
|
257
|
-
v-if="$slots[item.key]"
|
|
258
|
-
:name="item.key"
|
|
259
|
-
:state="toRef(settingForm.state, item.key)"
|
|
260
|
-
:config="item"
|
|
261
|
-
:orginConfig="configMap[item.key]"
|
|
262
|
-
/>
|
|
263
|
-
<template v-else>
|
|
264
|
-
<slot v-if="item.form_type === FormTypes.NUMBER_INPUT" :name="FormTypes.NUMBER_INPUT">
|
|
265
|
-
<InputNumber v-model:value="settingForm.state[item.key]" class="w-full" />
|
|
266
|
-
</slot>
|
|
267
|
-
|
|
268
|
-
<slot v-else-if="item.form_type === FormTypes.TEXT_INPUT" :name="FormTypes.TEXT_INPUT">
|
|
269
|
-
<Input v-model:value="settingForm.state[item.key]" allowClear />
|
|
270
|
-
</slot>
|
|
271
|
-
|
|
272
|
-
<slot v-else-if="item.form_type === FormTypes.TEXTAREA" :name="FormTypes.TEXTAREA">
|
|
273
|
-
<Textarea v-model:value="settingForm.state[item.key]" :rows="4" />
|
|
274
|
-
</slot>
|
|
275
|
-
|
|
276
|
-
<slot v-else-if="item.form_type === FormTypes.SWITCH" :name="FormTypes.SWITCH">
|
|
277
|
-
<RadioGroup
|
|
278
|
-
v-model:value="settingForm.state[item.key]"
|
|
279
|
-
v-bind="item.options"
|
|
280
|
-
buttonStyle="solid"
|
|
281
|
-
optionType="button"
|
|
282
|
-
/>
|
|
283
|
-
</slot>
|
|
284
|
-
|
|
285
|
-
<slot v-else-if="item.form_type === FormTypes.RADIO" :name="FormTypes.RADIO">
|
|
286
|
-
<RadioGroup
|
|
287
|
-
v-model:value="settingForm.state[item.key]"
|
|
288
|
-
buttonStyle="solid"
|
|
289
|
-
v-bind="item.options"
|
|
290
|
-
/>
|
|
291
|
-
</slot>
|
|
292
|
-
|
|
293
|
-
<slot v-else-if="item.form_type === FormTypes.CHECKBOX" :name="FormTypes.CHECKBOX">
|
|
294
|
-
<CheckboxGroup v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
295
|
-
</slot>
|
|
296
|
-
|
|
297
|
-
<slot v-else-if="item.form_type === FormTypes.SINGLE_SELECT" :name="FormTypes.SINGLE_SELECT">
|
|
298
|
-
<Select v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
299
|
-
</slot>
|
|
300
|
-
|
|
301
|
-
<slot v-else-if="item.form_type === FormTypes.MULTIPLE_SELECT" :name="FormTypes.MULTIPLE_SELECT">
|
|
302
|
-
<Select v-model:value="settingForm.state[item.key]" v-bind="item.options" mode="multiple" />
|
|
303
|
-
</slot>
|
|
304
|
-
|
|
305
|
-
<slot v-else-if="item.form_type === FormTypes.SINGLE_DATE_PICKER" :name="FormTypes.SINGLE_DATE_PICKER">
|
|
306
|
-
<DatePicker v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
307
|
-
</slot>
|
|
308
|
-
|
|
309
|
-
<slot v-else-if="item.form_type === FormTypes.RANGE_DATE_PICKER" :name="FormTypes.RANGE_DATE_PICKER">
|
|
310
|
-
<RangePicker v-model:value="settingForm.state[item.key]" v-bind="item.options" />
|
|
311
|
-
</slot>
|
|
312
|
-
|
|
313
|
-
<span v-else class="text-red">没有找到预设 form_type:{{ item.form_type }},可以通过 {{ item.key }} 插槽自定义</span>
|
|
314
|
-
</template>
|
|
315
|
-
</FormItem>
|
|
316
|
-
</template>
|
|
317
|
-
</Form>
|
|
67
|
+
<component
|
|
68
|
+
:is="$slots.default?.({
|
|
69
|
+
formCofnig: configQuerier.data.value ?? [],
|
|
70
|
+
disabled: settingMutator.isPending.value || props.readonly,
|
|
71
|
+
})[0]"
|
|
72
|
+
:ref="setRefs.form"
|
|
73
|
+
/>
|
|
318
74
|
|
|
319
75
|
<template v-if="!props.readonly" #actions>
|
|
320
|
-
<Button :disabled="settingMutator.isPending.value" @click="
|
|
76
|
+
<Button :disabled="settingMutator.isPending.value" @click="refs.form?.reset()">
|
|
321
77
|
重置
|
|
322
78
|
</Button>
|
|
323
79
|
<Button type="primary" :loading="settingMutator.isPending.value" @click="submitSetting()">
|
|
@@ -326,10 +82,3 @@ function resetSetting() {
|
|
|
326
82
|
</template>
|
|
327
83
|
</Card>
|
|
328
84
|
</template>
|
|
329
|
-
|
|
330
|
-
<style lang="scss" scoped>
|
|
331
|
-
.ant-cover__col2-form :deep(.ant-form-item-label > label) {
|
|
332
|
-
// padding-left: 0.5em;
|
|
333
|
-
// border-left: 2px solid #2361d0;
|
|
334
|
-
}
|
|
335
|
-
</style>
|
|
@@ -87,7 +87,7 @@ function useAntdForm<S extends Record<string, unknown>, TS = S>(schemas: MaybeRe
|
|
|
87
87
|
|
|
88
88
|
for (const k in schemasR.value) {
|
|
89
89
|
const item = schemasR.value[k];
|
|
90
|
-
_state[k] = (k in state.value) ? toRef(state.value, k) :
|
|
90
|
+
_state[k] = (k in state.value) ? toRef(state.value, k) : ref(item.value);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
state.value = _state;
|
package/package.json
CHANGED
package/utils/number.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { calc } from 'a-calc';
|
|
2
|
+
import type { BigNumber } from 'bignumber.js';
|
|
3
|
+
import bn from 'bignumber.js';
|
|
2
4
|
|
|
3
5
|
export { default as bn } from 'bignumber.js';
|
|
4
6
|
export { calc } from 'a-calc';
|
|
@@ -47,3 +49,21 @@ export function toAmount(amount: any, decimal: string | number = 0) {
|
|
|
47
49
|
|
|
48
50
|
return fmt;
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 乘以精度,即将小数金额转换为整数金额
|
|
55
|
+
* @param value 数值
|
|
56
|
+
* @param precision 精度
|
|
57
|
+
*/
|
|
58
|
+
export function mulPrecision(value: BigNumber.Value, precision: number) {
|
|
59
|
+
return bn(value).times(bn(10).pow(precision));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 除以精度,即将整数金额转换为小数金额
|
|
64
|
+
* @param value 数值
|
|
65
|
+
* @param precision 精度
|
|
66
|
+
*/
|
|
67
|
+
export function divPrecision(value: BigNumber.Value, precision: number) {
|
|
68
|
+
return bn(value).div(bn(10).pow(precision));
|
|
69
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { computed, reactive, ref, toValue, watch } from 'vue';
|
|
3
|
+
import { useElementBounding, useMutationObserver } from '@vueuse/core';
|
|
4
|
+
import { useTemplateRefs } from '../../hooks/useTemplateRefs';
|
|
3
5
|
import { type IProvide as INavManagerProvide, useNavManagerRouter } from './useNavManager';
|
|
4
6
|
|
|
5
7
|
function useHistory(initialValue: string[] = []) {
|
|
@@ -49,10 +51,26 @@ const emits = defineEmits<{
|
|
|
49
51
|
(e: 'close'): void
|
|
50
52
|
}>();
|
|
51
53
|
|
|
54
|
+
const [refs, setRefs] = useTemplateRefs<{
|
|
55
|
+
header: HTMLDivElement
|
|
56
|
+
}>();
|
|
52
57
|
const viewMap = reactive<Record<string, Parameters<INavManagerProvide['addView']>[0]>>({});
|
|
53
58
|
const history = useHistory([props.indexName]);
|
|
54
59
|
const currentView = computed(() => viewMap[history.current]);
|
|
55
60
|
const transitionName = computed(() => `view-${history.lastOperation}`);
|
|
61
|
+
const { height: headerHeight } = useElementBounding(() => refs.header);
|
|
62
|
+
const cssVars = computed(() => ({
|
|
63
|
+
'--header-height-neg': `-${headerHeight.value}px`,
|
|
64
|
+
'--header-height': `${headerHeight.value}px`,
|
|
65
|
+
}));
|
|
66
|
+
// 检查 header 中是否从外部插入了DOM(例如通过Teleport),如果有插入则将header中的所有需要隐藏的本地DOM都隐藏
|
|
67
|
+
const isCoverHeader = ref(false);
|
|
68
|
+
const headerNeedCoverClasses = ['nav-back-btn', 'nav-title', 'nav-close-btn'].map(cls => `.${cls}.need-cover`);
|
|
69
|
+
const headerNeedCoverSelector = `:scope > :not(${headerNeedCoverClasses.join(',')})`;
|
|
70
|
+
useMutationObserver(() => refs.header, () => {
|
|
71
|
+
const children = refs.header?.querySelectorAll(headerNeedCoverSelector);
|
|
72
|
+
isCoverHeader.value = !!children?.length;
|
|
73
|
+
}, { childList: true });
|
|
56
74
|
|
|
57
75
|
watch(currentView, (view, oldView) => {
|
|
58
76
|
oldView?.hide();
|
|
@@ -71,33 +89,37 @@ useNavManagerRouter(navManager);
|
|
|
71
89
|
</script>
|
|
72
90
|
|
|
73
91
|
<template>
|
|
74
|
-
<div class="tt-nav-manager box-border
|
|
75
|
-
<div class="
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
<div class="tt-nav-manager box-border" :class="[currentView?.name]" :style="cssVars">
|
|
93
|
+
<div class="relative h-full overflow-x-hidden bg-inherit">
|
|
94
|
+
<div :ref="setRefs.header" class="nav-header min-h6 flex items-center sticky z-5 bg-inherit">
|
|
95
|
+
<slot name="header" />
|
|
96
|
+
<template v-if="!isCoverHeader || (!isCoverHeader && !$slots.header)">
|
|
97
|
+
<Transition name="back-btn">
|
|
98
|
+
<div
|
|
99
|
+
v-if="!history.isFirst"
|
|
100
|
+
class="nav-back-btn need-cover mr-2 size-6 flex cursor-pointer items-center justify-center"
|
|
101
|
+
@click="navManager.back()"
|
|
102
|
+
>
|
|
103
|
+
<i class="i-ri:arrow-left-s-line text-6" />
|
|
104
|
+
</div>
|
|
105
|
+
</Transition>
|
|
106
|
+
|
|
107
|
+
<div class="nav-title need-cover flex-1">
|
|
108
|
+
{{ toValue(currentView?.title) }}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div
|
|
112
|
+
class="nav-close-btn need-cover size-6 flex cursor-pointer items-center justify-center"
|
|
113
|
+
@click="navManager.close()"
|
|
114
|
+
>
|
|
115
|
+
<i class="i-ri:close-line text-6" />
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
88
118
|
</div>
|
|
89
119
|
|
|
90
|
-
<div
|
|
91
|
-
class="nav-close-btn size-6 flex cursor-pointer items-center justify-center"
|
|
92
|
-
@click="navManager.close()"
|
|
93
|
-
>
|
|
94
|
-
<i class="i-ri:close-line text-6" />
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<div class="relative flex-1 overflow-x-hidden">
|
|
99
120
|
<Transition :name="transitionName">
|
|
100
|
-
<div :key="currentView?.name" class="absolute left-0 top-0 h-full w-full overflow-
|
|
121
|
+
<div :key="currentView?.name" class="scroll-container absolute left-0 top-0 h-full w-full overflow-y-auto overflow-x-hidden" :class="$props.viewClass">
|
|
122
|
+
<div class="h-$header-height" />
|
|
101
123
|
<slot />
|
|
102
124
|
</div>
|
|
103
125
|
</Transition>
|