feat: 添加 PCascadeSelect 组件,更新相关配置和依赖
This commit is contained in:
@ -15,6 +15,7 @@ import type { App } from 'vue';
|
||||
import { addAsteriskPlugin } from './formkit.config.plugin.addAsteriskPlugin';
|
||||
import { debugPlugin } from './formkit.config.plugin.debug';
|
||||
import { PDatePicker } from '@/__fk-inputs__/inputs/p-date-picker';
|
||||
import { PCascadeSelect } from '@/__fk-inputs__/inputs/p-cascade-select';
|
||||
|
||||
const plugins: FormKitPlugin[] = [
|
||||
// createLibraryPlugin(fkLibrary),
|
||||
@ -28,6 +29,7 @@ const plugins: FormKitPlugin[] = [
|
||||
PInputPassword,
|
||||
PSelect,
|
||||
PDatePicker,
|
||||
PCascadeSelect,
|
||||
}),
|
||||
// createLibraryPlugin(
|
||||
// {
|
||||
|
@ -30,6 +30,7 @@
|
||||
"primevue": "^4.2.5",
|
||||
"sweetalert2": "^11.15.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"utils4u": "^2.19.2",
|
||||
"vue": "^3.5.13",
|
||||
"zod-i18n-map": "^2.27.0"
|
||||
},
|
||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -65,6 +65,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.17
|
||||
utils4u:
|
||||
specifier: ^2.19.2
|
||||
version: 2.19.2
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.13(typescript@5.7.2)
|
||||
@ -1296,8 +1299,8 @@ packages:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
fastq@1.18.0:
|
||||
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
|
||||
|
||||
fdir@6.4.2:
|
||||
resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
|
||||
@ -1800,6 +1803,9 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
utils4u@2.19.2:
|
||||
resolution: {integrity: sha512-QjnPId4oadpieslF4SxxXHOxGJKG5xFlY/GqJS4a/1tUUDlC3AoMWksmXaX4FWO9AVANn5qgy5tLuYgS+VndRw==}
|
||||
|
||||
vite@6.0.5:
|
||||
resolution: {integrity: sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@ -2448,7 +2454,7 @@ snapshots:
|
||||
'@nodelib/fs.walk@1.2.8':
|
||||
dependencies:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.17.1
|
||||
fastq: 1.18.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
@ -3078,7 +3084,7 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fastq@1.17.1:
|
||||
fastq@1.18.0:
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
@ -3591,6 +3597,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utils4u@2.19.2: {}
|
||||
|
||||
vite@6.0.5(jiti@2.4.2)(tsx@4.19.2)(yaml@2.6.1):
|
||||
dependencies:
|
||||
esbuild: 0.24.0
|
||||
|
146
src/App.vue
146
src/App.vue
@ -4,13 +4,81 @@ import AllCustom from './all-custom/all-custom.vue';
|
||||
import TutorialForm from './tutorial-form/index.vue';
|
||||
import ZodForm from './zod-form/index.vue';
|
||||
|
||||
const datetime12h = ref();
|
||||
const datetime24h = ref();
|
||||
const time = ref();
|
||||
const date = ref();
|
||||
const dates = ref();
|
||||
|
||||
|
||||
const selectedCity = ref();
|
||||
const countries = ref([
|
||||
{
|
||||
name: 'Australia',
|
||||
code: 'AU',
|
||||
states: [
|
||||
{
|
||||
name: 'New South Wales',
|
||||
cities: [
|
||||
{ cname: 'Sydney', code: 'A-SY' },
|
||||
{ cname: 'Newcastle', code: 'A-NE' },
|
||||
{ cname: 'Wollongong', code: 'A-WO' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Queensland',
|
||||
cities: [
|
||||
{ cname: 'Brisbane', code: 'A-BR' },
|
||||
{ cname: 'Townsville', code: 'A-TO' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Canada',
|
||||
code: 'CA',
|
||||
states: [
|
||||
{
|
||||
name: 'Quebec',
|
||||
cities: [
|
||||
{ cname: 'Montreal', code: 'C-MO' },
|
||||
{ cname: 'Quebec City', code: 'C-QU' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Ontario',
|
||||
cities: [
|
||||
{ cname: 'Ottawa', code: 'C-OT' },
|
||||
{ cname: 'Toronto', code: 'C-TO' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'United States',
|
||||
code: 'US',
|
||||
states: [
|
||||
{
|
||||
name: 'California',
|
||||
cities: [
|
||||
{ cname: 'Los Angeles', code: 'US-LA' },
|
||||
{ cname: 'San Diego', code: 'US-SD' },
|
||||
{ cname: 'San Francisco', code: 'US-SF' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Florida',
|
||||
cities: [
|
||||
{ cname: 'Jacksonville', code: 'US-JA' },
|
||||
{ cname: 'Miami', code: 'US-MI' },
|
||||
{ cname: 'Tampa', code: 'US-TA' },
|
||||
{ cname: 'Orlando', code: 'US-OR' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Texas',
|
||||
cities: [
|
||||
{ cname: 'Austin', code: 'US-AU' },
|
||||
{ cname: 'Dallas', code: 'US-DA' },
|
||||
{ cname: 'Houston', code: 'US-HO' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -26,60 +94,16 @@ const dates = ref();
|
||||
|
||||
<div class="p-4 w-full bg-white rounded-lg shadow-md dark:bg-gray-800 dark:text-white mt-4">
|
||||
|
||||
|
||||
<div class="card flex flex-wrap gap-4 items-start">
|
||||
<DatePicker
|
||||
v-model="date"
|
||||
view="month"
|
||||
dateFormat="mm/yy"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
v-model="dates"
|
||||
selectionMode="range"
|
||||
:manualInput="false"
|
||||
/>
|
||||
|
||||
|
||||
<div class="flex-auto">
|
||||
<label
|
||||
for="datepicker-12h"
|
||||
class="font-bold block mb-2"
|
||||
> 12h Format </label>
|
||||
<DatePicker
|
||||
id="datepicker-12h"
|
||||
v-model="datetime12h"
|
||||
showTime
|
||||
hourFormat="12"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-auto">
|
||||
<label
|
||||
for="datepicker-24h"
|
||||
class="font-bold block mb-2"
|
||||
> 24h Format </label>
|
||||
<DatePicker
|
||||
id="datepicker-24h"
|
||||
v-model="datetime24h"
|
||||
showTime
|
||||
hourFormat="24"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-auto">
|
||||
<label
|
||||
for="datepicker-timeonly"
|
||||
class="font-bold block mb-2"
|
||||
> Time Only </label>
|
||||
<DatePicker
|
||||
id="datepicker-timeonly"
|
||||
v-model="time"
|
||||
timeOnly
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CascadeSelect
|
||||
loading
|
||||
v-model="selectedCity"
|
||||
:options="countries"
|
||||
optionLabel="cname"
|
||||
optionGroupLabel="name"
|
||||
:optionGroupChildren="['states', 'cities']"
|
||||
class="w-56"
|
||||
placeholder="Select a City"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
113
src/__fk-inputs__/inputs/p-cascade-select.tsx
Normal file
113
src/__fk-inputs__/inputs/p-cascade-select.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import type { FormKitFrameworkContext, FormKitTypeDefinition } from '@formkit/core';
|
||||
import type { FormKitInputs } from '@formkit/inputs';
|
||||
import { createSection, label, outer } from '@formkit/inputs';
|
||||
import type { CascadeSelectEmitsOptions, CascadeSelectProps } from 'primevue/cascadeselect';
|
||||
import PrimevueCascadeselect from 'primevue/cascadeselect';
|
||||
import { defineComponent, markRaw, ref } from 'vue';
|
||||
import { floatLabel } from '../sections/floatLabel';
|
||||
import { messages } from '../sections/messages';
|
||||
|
||||
// https://formkit.com/inputs/dropdown
|
||||
|
||||
type PrimevueSelectListeners = {
|
||||
'onUpdate:modelValue': CascadeSelectEmitsOptions['update:modelValue'];
|
||||
'onBlur': CascadeSelectEmitsOptions['blur'];
|
||||
};
|
||||
|
||||
const SchemaComponent = defineComponent(
|
||||
(vueProps: { context: FormKitFrameworkContext }) => {
|
||||
const formkitContext = vueProps.context;
|
||||
const listeners: PrimevueSelectListeners = {
|
||||
'onUpdate:modelValue': (value: unknown) => {
|
||||
formkitContext.node.input(value);
|
||||
},
|
||||
'onBlur': async e => {
|
||||
setTimeout(
|
||||
() => formkitContext.handlers.blur.call(undefined, e as never),
|
||||
166, // 因为会触发两次。所以让blur事件延迟一点,可以考虑优化。(Cascadeselect好像没有这个问题)
|
||||
);
|
||||
},
|
||||
};
|
||||
const p_options = ref();
|
||||
const loading = ref(true);
|
||||
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
let result;
|
||||
if (formkitContext.options instanceof Promise) {
|
||||
result = await formkitContext.options;
|
||||
} else {
|
||||
console.warn('未支持的 options 类型 :>> ', typeof formkitContext.options);
|
||||
}
|
||||
p_options.value = result;
|
||||
} catch (error) {
|
||||
console.error('Failed to load options:', error);
|
||||
p_options.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
loadOptions(); // 立即加载options
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<PrimevueCascadeselect
|
||||
inputId={formkitContext.id}
|
||||
optionLabel={formkitContext.optionLabel as never}
|
||||
optionValue={formkitContext.optionValue as never}
|
||||
optionGroupChildren={(formkitContext.optionGroupChildren as never) || 'children'}
|
||||
optionGroupLabel={(formkitContext.optionGroupLabel as never) || (formkitContext.optionLabel as never)}
|
||||
loading={loading.value}
|
||||
options={p_options.value}
|
||||
invalid={formkitContext.state.invalid}
|
||||
fluid
|
||||
disabled={!!formkitContext.disabled}
|
||||
modelValue={formkitContext._value}
|
||||
{...listeners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
// https://cn.vuejs.org/api/general#definecomponent
|
||||
// 目前仍然需要手动声明运行时的 props
|
||||
// 在将来,我们计划提供一个 Babel 插件,自动推断并注入运行时 props (就像在单文件组件中的 defineProps 一样),以便省略运行时 props 的声明。
|
||||
{
|
||||
props: ['context'],
|
||||
},
|
||||
);
|
||||
|
||||
const input = createSection('input', () => ({
|
||||
$cmp: markRaw(SchemaComponent) as never,
|
||||
bind: '$attrs',
|
||||
props: {
|
||||
context: '$node.context',
|
||||
},
|
||||
}));
|
||||
|
||||
export const PCascadeSelect: FormKitTypeDefinition = {
|
||||
type: 'input',
|
||||
schema: outer(
|
||||
floatLabel(
|
||||
input(), //
|
||||
label('$label'),
|
||||
),
|
||||
messages(),
|
||||
),
|
||||
props: ['options', 'optionLabel', 'optionValue'],
|
||||
// schemaMemoKey: '72psvunq45', // Math.random().toString(36).substring(2, 15)
|
||||
};
|
||||
|
||||
type OptionsType = CascadeSelectProps['options'];
|
||||
|
||||
declare module '@formkit/inputs' {
|
||||
// https://formkit.com/essentials/custom-inputs#typescript-support
|
||||
interface FormKitInputProps<Props extends FormKitInputs<Props>> {
|
||||
PCascadeSelect: {
|
||||
type: 'PCascadeSelect';
|
||||
options: Promise<OptionsType>;
|
||||
optionLabel: string;
|
||||
optionValue: string;
|
||||
};
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ const SchemaComponent = defineComponent(
|
||||
};
|
||||
// [optionsLoader](https://github.com/formkit/formkit/blob/2d5387ba98597775cb2a752af65aee84bc438863/packages/inputs/src/features/options.ts#L125)
|
||||
|
||||
const poptions = ref();
|
||||
const p_options = ref();
|
||||
const loading = ref(true);
|
||||
|
||||
const loadOptions = async () => {
|
||||
@ -46,10 +46,10 @@ const SchemaComponent = defineComponent(
|
||||
} else {
|
||||
result = formkitContext.options;
|
||||
}
|
||||
poptions.value = result;
|
||||
p_options.value = result;
|
||||
} catch (error) {
|
||||
console.error('Failed to load options:', error);
|
||||
poptions.value = [];
|
||||
p_options.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -76,7 +76,7 @@ const SchemaComponent = defineComponent(
|
||||
invalid={formkitContext.state.invalid || valueSlot.value !== undefined}
|
||||
disabled={!!formkitContext.disabled}
|
||||
loading={loading.value}
|
||||
options={poptions.value}
|
||||
options={p_options.value}
|
||||
modelValue={formkitContext._value}
|
||||
optionLabel={formkitContext.optionLabel as never}
|
||||
optionValue={formkitContext.optionValue as never}
|
||||
|
@ -3,6 +3,7 @@ import { FormKitNode } from '@formkit/core';
|
||||
import { text } from '@formkit/inputs';
|
||||
import Swal from 'sweetalert2';
|
||||
import dayjs from 'dayjs';
|
||||
import { arrayToTree } from 'utils4u/array';
|
||||
|
||||
async function submit(formData: Record<string, any>, formNode: FormKitNode) {
|
||||
console.group('submit');
|
||||
@ -27,6 +28,44 @@ const K_OPTIONS = [
|
||||
const promiseOptions = new Promise<typeof K_OPTIONS>(resolve => {
|
||||
setTimeout(() => resolve(K_OPTIONS), 1000);
|
||||
});
|
||||
|
||||
const K_FLAT_TREE = [
|
||||
{
|
||||
dictLabel: '北京市',
|
||||
dictValue: '110000',
|
||||
fullName: '北京市',
|
||||
abbrName: '北京',
|
||||
dictParent: '00',
|
||||
},
|
||||
{
|
||||
dictLabel: '山西省',
|
||||
dictValue: '140000',
|
||||
fullName: '山西省',
|
||||
abbrName: '山西',
|
||||
dictParent: '00',
|
||||
},
|
||||
{
|
||||
dictLabel: '太原市',
|
||||
dictValue: '140100',
|
||||
fullName: '山西省,太原市',
|
||||
abbrName: '山西太原',
|
||||
dictParent: '140000',
|
||||
},
|
||||
{
|
||||
dictLabel: '大同市',
|
||||
dictValue: '140200',
|
||||
fullName: '山西省,大同市',
|
||||
abbrName: '山西大同',
|
||||
dictParent: '140000',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const promiseCascadeOptions = new Promise<typeof K_FLAT_TREE>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(arrayToTree(K_FLAT_TREE, { id: 'dictValue', parentId: 'dictParent', rootId: '00' }));
|
||||
}, 1000);
|
||||
});
|
||||
/* const funcOptions = async () => {
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
return K_OPTIONS;
|
||||
@ -99,6 +138,15 @@ const promiseOptions = new Promise<typeof K_OPTIONS>(resolve => {
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
/>
|
||||
<FormKit
|
||||
type="PCascadeSelect"
|
||||
name="PCascadeSelect"
|
||||
label="级联选择框"
|
||||
validation="required"
|
||||
:options="promiseCascadeOptions"
|
||||
optionLabel="dictLabel"
|
||||
optionValue="dictValue"
|
||||
/>
|
||||
<FormKit
|
||||
type="PDatePicker"
|
||||
name="PDatePicker"
|
||||
|
Reference in New Issue
Block a user