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

View File

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