rewrite MkFormDialog

This commit is contained in:
Lhcfl 2024-04-13 23:08:46 +08:00
parent bb9a58ce34
commit 3e43819ba1
3 changed files with 228 additions and 107 deletions

View file

@ -8,7 +8,7 @@
@click="cancel()" @click="cancel()"
@ok="ok()" @ok="ok()"
@close="cancel()" @close="cancel()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ title }} {{ title }}
@ -17,86 +17,84 @@
<MkSpacer :margin-min="20" :margin-max="32"> <MkSpacer :margin-min="20" :margin-max="32">
<div class="_formRoot"> <div class="_formRoot">
<template <template
v-for="item in Object.keys(form).filter( v-for="[formItem, formItemName] in unHiddenForms()"
(item) => !form[item].hidden,
)"
> >
<FormInput <FormInput
v-if="form[item].type === 'number'" v-if="formItem.type === 'number'"
v-model="values[item]" v-model="values[formItemName]"
type="number" type="number"
:step="form[item].step || 1" :step="formItem.step || 1"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormInput> </FormInput>
<FormInput <FormInput
v-else-if=" v-else-if="
form[item].type === 'string' && formItem.type === 'string' &&
!form[item].multiline !formItem.multiline
" "
v-model="values[item]" v-model="values[formItemName]"
type="text" type="text"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormInput> </FormInput>
<FormTextarea <FormTextarea
v-else-if=" v-else-if="
form[item].type === 'string' && form[item].multiline formItem.type === 'string' && formItem.multiline
" "
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormTextarea> </FormTextarea>
<FormSwitch <FormSwitch
v-else-if="form[item].type === 'boolean'" v-else-if="formItem.type === 'boolean'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<span v-text="form[item].label || item"></span> <span v-text="formItem.label || formItemName"></span>
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormSwitch> </FormSwitch>
<FormSelect <FormSelect
v-else-if="form[item].type === 'enum'" v-else-if="formItem.type === 'enum'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label>
><span v-text="form[item].label || item"></span <span v-text="formItem.label || formItemName"></span>
><span v-if="form[item].required === false"> <span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template
> >
</template>
<option <option
v-for="item in form[item].enum" v-for="item in formItem.enum"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
> >
@ -104,18 +102,18 @@
</option> </option>
</FormSelect> </FormSelect>
<FormRadios <FormRadios
v-else-if="form[item].type === 'radio'" v-else-if="formItem.type === 'radio'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<option <option
v-for="item in form[item].options" v-for="item in formItem.options"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
> >
@ -123,30 +121,30 @@
</option> </option>
</FormRadios> </FormRadios>
<FormRange <FormRange
v-else-if="form[item].type === 'range'" v-else-if="formItem.type === 'range'"
v-model="values[item]" v-model="values[formItemName]"
:min="form[item].min" :min="formItem.min"
:max="form[item].max" :max="formItem.max"
:step="form[item].step" :step="formItem.step"
:text-converter="form[item].textConverter" :text-converter="formItem.textConverter"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormRange> </FormRange>
<MkButton <MkButton
v-else-if="form[item].type === 'button'" v-else-if="formItem.type === 'button'"
class="_formBlock" class="_formBlock"
@click="form[item].action($event, values)" @click="formItem.action($event, values)"
> >
<span v-text="form[item].content || item"></span> <span v-text="formItem.content || formItemName"></span>
</MkButton> </MkButton>
</template> </template>
</div> </div>
@ -154,8 +152,8 @@
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { ref } from "vue";
import FormInput from "./form/input.vue"; import FormInput from "./form/input.vue";
import FormTextarea from "./form/textarea.vue"; import FormTextarea from "./form/textarea.vue";
import FormSwitch from "./form/switch.vue"; import FormSwitch from "./form/switch.vue";
@ -165,59 +163,50 @@ import MkButton from "./MkButton.vue";
import FormRadios from "./form/radios.vue"; import FormRadios from "./form/radios.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import type { FormItemType } from "@/types/form";
export default defineComponent({ const props = defineProps<{
components: { title: string;
XModalWindow, form: Record<string, FormItemType>;
FormInput, }>();
FormTextarea,
FormSwitch,
FormSelect,
FormRange,
MkButton,
FormRadios,
},
props: { // biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
title: { type ValueType = Record<string, any>;
type: String,
required: true, const emit = defineEmits<{
done: [
status: {
result?: Record<string, FormItemType["default"]>;
canceled?: true;
}, },
form: { ];
type: Object, closed: [];
required: true, }>();
},
},
emits: ["done"], const values = ref<ValueType>({});
const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
data() { for (const item in props.form) {
return { values.value[item] = props.form[item].default ?? null;
values: {}, }
i18n,
};
},
created() { function unHiddenForms(): [FormItemType, string][] {
for (const item in this.form) { return Object.keys(props.form)
this.values[item] = this.form[item].default ?? null; .filter((itemName) => !props.form[itemName].hidden)
} .map((itemName) => [props.form[itemName], itemName]);
}, }
methods: { function ok() {
ok() { emit("done", {
this.$emit("done", { result: values.value,
result: this.values, });
}); dialog.value!.close();
this.$refs.dialog.close(); }
},
cancel() { function cancel() {
this.$emit("done", { emit("done", {
canceled: true, canceled: true,
}); });
this.$refs.dialog.close(); dialog.value!.close();
}, }
},
});
</script> </script>

View file

@ -13,6 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
import { apiUrl, url } from "@/config"; import { apiUrl, url } from "@/config";
import { me } from "@/me"; import { me } from "@/me";
import type { MenuItem } from "@/types/menu"; import type { MenuItem } from "@/types/menu";
import type { FormItemType, GetFormResultType } from "@/types/form";
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -611,8 +612,16 @@ export function waiting(): Promise<void> {
}); });
} }
export function form(title, form) { export function form<T extends Record<string, FormItemType>>(
return new Promise((resolve, reject) => { title: string,
form: T,
) {
return new Promise<{
result?: {
[K in keyof T]: GetFormResultType<T[K]["type"]>;
};
canceled?: true;
}>((resolve, _reject) => {
popup( popup(
defineAsyncComponent({ defineAsyncComponent({
loader: () => import("@/components/MkFormDialog.vue"), loader: () => import("@/components/MkFormDialog.vue"),
@ -622,7 +631,7 @@ export function form(title, form) {
{ title, form }, { title, form },
{ {
done: (result) => { done: (result) => {
resolve(result); resolve(result as never);
}, },
}, },
"closed", "closed",
@ -631,7 +640,7 @@ export function form(title, form) {
} }
export async function selectUser() { export async function selectUser() {
return new Promise((resolve, reject) => { return new Promise<entities.UserDetailed>((resolve, reject) => {
popup( popup(
defineAsyncComponent({ defineAsyncComponent({
loader: () => import("@/components/MkUserSelectDialog.vue"), loader: () => import("@/components/MkUserSelectDialog.vue"),

View file

@ -0,0 +1,123 @@
export type BaseFormItem = {
hidden?: boolean;
label?: string;
description?: string;
required?: boolean;
};
export type FormItemTextInput = BaseFormItem & {
type: "string";
default?: string | null;
multiline?: false;
};
export type FormItemTextarea = BaseFormItem & {
type: "string";
default?: string | null;
multiline: true;
};
export type FormItemText = FormItemTextInput | FormItemTextarea;
export type FormItemNumber = BaseFormItem & {
type: "number";
default?: number | null;
step?: number | null;
};
export type FormItemEmail = BaseFormItem & {
type: "email";
default?: string | null;
};
export type FormItemPassword = BaseFormItem & {
type: "password";
default?: never;
__result_typedef?: string;
};
export type FormItemUrl = BaseFormItem & {
type: "url";
default?: string | null;
};
export type FormItemDate = BaseFormItem & {
type: "date";
default?: Date | null;
};
export type FormItemTime = BaseFormItem & {
type: "time";
default?: number | Date | null;
};
export type FormItemSearch = BaseFormItem & {
type: "search";
default?: string | null;
};
export type FormItemSwitch = BaseFormItem & {
type: "boolean";
default?: boolean | null;
};
export type FormItemSelect = BaseFormItem & {
type: "enum";
default?: string | null;
enum: {
value: string | number | symbol | undefined;
label: string;
}[];
};
export type FormItemRadios = BaseFormItem & {
type: "radio";
default?: string | number | symbol | undefined | null;
options: {
label: string;
value: string | number | symbol | undefined;
}[];
};
export type FormItemRange = BaseFormItem & {
type: "range";
default?: number | null;
min: number;
max: number;
step?: number;
textConverter?: (value: number) => string;
};
export type FormItemButton = BaseFormItem & {
type: "button";
content?: string;
action: (event, values) => unknown;
default?: never;
};
export type FormItemObject = BaseFormItem & {
type: "object";
default: Record<string, unknown> | null;
hidden: true;
};
export type FormItemInputArray = [
FormItemTextInput,
FormItemNumber,
FormItemEmail,
FormItemPassword,
FormItemUrl,
FormItemDate,
FormItemTime,
FormItemSearch,
];
export type FormItemTypeArray = [
...FormItemInputArray,
FormItemTextarea,
FormItemSwitch,
FormItemSelect,
FormItemButton,
FormItemRadios,
FormItemRange,
FormItemObject,
];
export type FormItemInput = FormItemInputArray[number];
export type FormItemType = FormItemTypeArray[number];
export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never;
type NonUndefindAble<T> = T extends undefined ? never : T;
export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble<
"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
>