feat(components): make story creation/editing components
This commit is contained in:
parent
52f5b4a811
commit
b2e950e798
54
components/story/atoms/bands.vue
Normal file
54
components/story/atoms/bands.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DefaultOptionType } from "ant-design-vue/es/select";
|
||||||
|
import { RuleExpression, useField } from "vee-validate";
|
||||||
|
import { cs } from "~/lib/client/storyFormSchema";
|
||||||
|
import { IBand } from "~/models/band";
|
||||||
|
import { log } from "~/lib/server/logger";
|
||||||
|
|
||||||
|
import iconEl from "../icon.vue";
|
||||||
|
|
||||||
|
const bandlist = inject<IBand[]>("bandlist");
|
||||||
|
const fname = inject<string>("curName");
|
||||||
|
const { updateBands, sb } = inject<any>("selectedBands");
|
||||||
|
|
||||||
|
const bl = ref(bandlist || []);
|
||||||
|
const options: DefaultOptionType[] = bl.value
|
||||||
|
.filter((a) => a.name != "")
|
||||||
|
.map((a) => ({
|
||||||
|
value: a._id,
|
||||||
|
label: a.name,
|
||||||
|
disabled: a.locked || false,
|
||||||
|
}));
|
||||||
|
let bandField = useField(
|
||||||
|
fname + "bands",
|
||||||
|
cs.fields.bands as unknown as MaybeRef<RuleExpression<number[]>>,
|
||||||
|
);
|
||||||
|
const { value, errorMessage, name, setValue } = bandField;
|
||||||
|
// setValue(sb)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-form-item
|
||||||
|
:validate-status="!!errorMessage ? 'error' : undefined"
|
||||||
|
:help="errorMessage"
|
||||||
|
label="Bands"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
:allow-clear="true"
|
||||||
|
mode="multiple"
|
||||||
|
@change="
|
||||||
|
(val) => {
|
||||||
|
// log.debug(val);
|
||||||
|
updateBands(val);
|
||||||
|
setValue(val as number[]);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
v-model:value="value"
|
||||||
|
:options="options"
|
||||||
|
>
|
||||||
|
<template #removeIcon>
|
||||||
|
<i class="far fa-circle-x" />
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
35
components/story/atoms/characters.vue
Normal file
35
components/story/atoms/characters.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RuleExpression, useField } from "vee-validate";
|
||||||
|
import { cs } from "~/lib/client/storyFormSchema";
|
||||||
|
import { IBand } from "~/models/band";
|
||||||
|
const fname = inject<string>("curName");
|
||||||
|
const { sb: selectedBands } = inject<any>("selectedBands");
|
||||||
|
const allBands = inject<Ref<IBand[]>>("bandlist");
|
||||||
|
const opts = computed(() => {
|
||||||
|
const unflattened: any[] = [];
|
||||||
|
selectedBands?.value?.forEach((v: number) => {
|
||||||
|
unflattened.push(allBands?.value.find((a) => a._id == v)?.characters);
|
||||||
|
});
|
||||||
|
return unflattened.flat(Infinity).map((a) => ({ value: a, label: a }));
|
||||||
|
});
|
||||||
|
const charField = useField<string[]>(
|
||||||
|
fname + "characters",
|
||||||
|
cs.fields.characters as unknown as MaybeRef<RuleExpression<string[]>>,
|
||||||
|
);
|
||||||
|
const { value, errorMessage, name: bandName, setValue } = charField;
|
||||||
|
// setValue([]);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<a-form-item
|
||||||
|
:help="errorMessage"
|
||||||
|
label="Characters"
|
||||||
|
:name="bandName as string"
|
||||||
|
:validate-status="!!errorMessage ? 'error' : undefined"
|
||||||
|
>
|
||||||
|
<a-select mode="multiple" :options="opts" v-model:value="value">
|
||||||
|
<template #removeIcon>
|
||||||
|
<i class="far fa-circle-x" />
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
28
components/story/atoms/genre.vue
Normal file
28
components/story/atoms/genre.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
const fname = inject<string>("curName");
|
||||||
|
let data = await useApiFetch("/genres");
|
||||||
|
let opts = (data.data.value as unknown as any[]).map((a) => ({
|
||||||
|
value: a,
|
||||||
|
label: a,
|
||||||
|
}));
|
||||||
|
const { value, errorMessage, name, setValue } = useField(fname + "genre");
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<a-form-item
|
||||||
|
:help="errorMessage"
|
||||||
|
label="Genre(s)"
|
||||||
|
:validate-status="!!errorMessage ? 'error' : undefined"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
:allow-clear="true"
|
||||||
|
:options="opts"
|
||||||
|
v-model:value="value as string[]"
|
||||||
|
mode="multiple"
|
||||||
|
>
|
||||||
|
<template #removeIcon>
|
||||||
|
<i class="far fa-circle-x" />
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
50
components/story/atoms/pairings.vue
Normal file
50
components/story/atoms/pairings.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Field, useFieldArray } from "vee-validate";
|
||||||
|
import { IBand } from "~/models/band";
|
||||||
|
|
||||||
|
const { sb: selectedBands } = inject<any>("selectedBands");
|
||||||
|
const allBands = inject<Ref<IBand[]>>("bandlist");
|
||||||
|
const fname = inject<string>("curName");
|
||||||
|
const opts = computed(() => {
|
||||||
|
const uf: any[] = [];
|
||||||
|
selectedBands.value.forEach((v: number) => {
|
||||||
|
uf.push(allBands?.value.find((a) => a._id == v)?.characters);
|
||||||
|
});
|
||||||
|
return uf.flat(Infinity).map((a) => ({ value: a, label: a }));
|
||||||
|
});
|
||||||
|
const { fields, push, remove, replace, update } = useFieldArray<string[]>(
|
||||||
|
fname + "relationships",
|
||||||
|
);
|
||||||
|
// replace([]);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<a-form-item label="Pairings">
|
||||||
|
<a-row
|
||||||
|
:gutter="5"
|
||||||
|
:wrap="true"
|
||||||
|
v-for="(field, idx) in fields"
|
||||||
|
:key="field.key"
|
||||||
|
>
|
||||||
|
<Field :name="fname + 'relationships' + `[${idx}]`">
|
||||||
|
<a-select
|
||||||
|
mode="multiple"
|
||||||
|
:options="opts"
|
||||||
|
v-model:value="field.value as string[]"
|
||||||
|
@change="(val) => update(idx, val as string[])"
|
||||||
|
>
|
||||||
|
<template #removeIcon>
|
||||||
|
<i class="far fa-circle-x" />
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</Field>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-button @click="(e) => remove(idx)"> - </a-button>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row justify="end">
|
||||||
|
<a-col :span="2">
|
||||||
|
<a-button @click="(e) => push([])"> + </a-button>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
17
components/story/atoms/test1.vue
Normal file
17
components/story/atoms/test1.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
mode="multiple"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="Please select"
|
||||||
|
:options="
|
||||||
|
[...Array(25)].map((_, i) => ({ value: (i + 10).toString(36) + (i + 1) }))
|
||||||
|
"
|
||||||
|
@change="handleChange"
|
||||||
|
></a-select>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { SelectValue } from "ant-design-vue/es/select";
|
||||||
|
import { ref } from "vue";
|
||||||
|
const handleChange = (value: SelectValue) => {};
|
||||||
|
const value = ref(["a1", "b2"]);
|
||||||
|
</script>
|
135
components/story/create/singleChapter.vue
Normal file
135
components/story/create/singleChapter.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Form as VeeForm,
|
||||||
|
Field,
|
||||||
|
useForm,
|
||||||
|
useField,
|
||||||
|
ErrorMessage,
|
||||||
|
} from "vee-validate";
|
||||||
|
import { NamePath } from "ant-design-vue/es/form/interface";
|
||||||
|
import { FormChapter } from "~/lib/client/types/form/story";
|
||||||
|
|
||||||
|
import { story, bare } from "~/lib/client/editorConfig";
|
||||||
|
import elBands from "../atoms/bands.vue";
|
||||||
|
import genre from "../atoms/genre.vue";
|
||||||
|
import elCharacters from "../atoms/characters.vue";
|
||||||
|
import elPairings from "../atoms/pairings.vue";
|
||||||
|
import uploadOrPaste from "./uploadOrPaste.vue";
|
||||||
|
// import test1 from
|
||||||
|
|
||||||
|
let { name, data } = defineProps<{
|
||||||
|
name: NamePath;
|
||||||
|
data: FormChapter;
|
||||||
|
}>();
|
||||||
|
let acData = toRef(data);
|
||||||
|
let { data: _bands } = await useApiFetch("/bands/all");
|
||||||
|
let bands = ref(_bands);
|
||||||
|
provide("curName", name + ".");
|
||||||
|
provide("bandlist", bands);
|
||||||
|
const selectedBands = ref(data.bands || []);
|
||||||
|
const updateBands = (val) => {
|
||||||
|
selectedBands.value = val;
|
||||||
|
// data.value.bands = val;
|
||||||
|
};
|
||||||
|
provide("selectedBands", {
|
||||||
|
sb: selectedBands,
|
||||||
|
updateBands,
|
||||||
|
});
|
||||||
|
const wrapc = { span: 12 };
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-row :gutter="[10, 0]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<Field
|
||||||
|
:name="name + '.chapterTitle'"
|
||||||
|
v-slot="{ value, field, errorMessage }"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
:name="[field.name]"
|
||||||
|
label="Chapter title"
|
||||||
|
:help="errorMessage"
|
||||||
|
:status="!!errorMessage ? 'error' : undefined"
|
||||||
|
>
|
||||||
|
<a-input v-bind="field" />
|
||||||
|
</a-form-item>
|
||||||
|
</Field>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<el-bands />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="[10, 0]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<base-editor
|
||||||
|
v-model:val="acData.summary"
|
||||||
|
:name="name + '.summary'"
|
||||||
|
:wrap-col="wrapc"
|
||||||
|
label="Summary"
|
||||||
|
:init="bare"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<base-editor
|
||||||
|
v-model:val="acData.notes"
|
||||||
|
:name="name + '.notes'"
|
||||||
|
:wrap-col="wrapc"
|
||||||
|
label="Author's notes"
|
||||||
|
:init="bare"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="[10, 0]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<el-characters />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<el-pairings />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<Field
|
||||||
|
:name="name + '.nsfw'"
|
||||||
|
type="checkbox"
|
||||||
|
:unchecked-value="false"
|
||||||
|
:value="true"
|
||||||
|
v-slot="{ value, field, errorMessage }"
|
||||||
|
>
|
||||||
|
<a-checkbox v-bind="field" v-model="field.value">
|
||||||
|
Has NSFW content
|
||||||
|
</a-checkbox>
|
||||||
|
<error-message :name="field.name" />
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
:name="name + '.loggedInOnly'"
|
||||||
|
type="checkbox"
|
||||||
|
:unchecked-value="false"
|
||||||
|
:value="true"
|
||||||
|
v-slot="{ value, field, errorMessage }"
|
||||||
|
>
|
||||||
|
<a-checkbox v-bind="field"> Visible only to registered users </a-checkbox>
|
||||||
|
<error-message :name="field.name" />
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
:name="name + '.hidden'"
|
||||||
|
type="checkbox"
|
||||||
|
:unchecked-value="false"
|
||||||
|
:value="true"
|
||||||
|
v-slot="{ value, field, errorMessage }"
|
||||||
|
>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
Hides your story from everyone except you and site admins.
|
||||||
|
</template>
|
||||||
|
<a-checkbox v-bind="field"> Hidden </a-checkbox>
|
||||||
|
</a-tooltip>
|
||||||
|
</Field>
|
||||||
|
<a-row>
|
||||||
|
<a-col :span="24">
|
||||||
|
<genre />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-divider> Contents </a-divider>
|
||||||
|
<upload-or-paste />
|
||||||
|
<Field :name="name + '.id'" v-if="!!data.id" :model-value="data.id" />
|
||||||
|
</div>
|
||||||
|
</template>
|
151
components/story/create/storyform.vue
Normal file
151
components/story/create/storyform.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import lmove from "lodash-move";
|
||||||
|
import { Field, Form as veeForm, FieldArray, FieldEntry } from "vee-validate";
|
||||||
|
import { log } from "~/lib/server/logger";
|
||||||
|
import { storySchema } from "~/lib/client/storyFormSchema";
|
||||||
|
import {
|
||||||
|
FormChapter,
|
||||||
|
FormStory,
|
||||||
|
defaultChapter,
|
||||||
|
} from "~/lib/client/types/form/story";
|
||||||
|
|
||||||
|
import singleChapter from "./singleChapter.vue";
|
||||||
|
import icon from "~/components/icon.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: FormStory;
|
||||||
|
canDraft?: boolean;
|
||||||
|
endpoint: string;
|
||||||
|
}>();
|
||||||
|
let drag = false;
|
||||||
|
let acData = ref(props.data);
|
||||||
|
const expandos = ref<string[]>([]);
|
||||||
|
|
||||||
|
function logSubmit(values, actions) {
|
||||||
|
// log.debug("VALUE");
|
||||||
|
// log.debug(values);
|
||||||
|
// log.debug(actions);
|
||||||
|
}
|
||||||
|
function onSubmit(values, actions) {
|
||||||
|
logSubmit(values, actions);
|
||||||
|
}
|
||||||
|
function inval({ values, errors, results }) {
|
||||||
|
logSubmit(values, undefined);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<vee-form
|
||||||
|
:keep-values="true"
|
||||||
|
v-slot="{ values, setFieldValue }"
|
||||||
|
:validation-schema="storySchema"
|
||||||
|
:initial-values="data"
|
||||||
|
@submit="onSubmit"
|
||||||
|
@invalid-submit="inval"
|
||||||
|
>
|
||||||
|
<!-- <a-form v-bind:model="acData"> -->
|
||||||
|
<Field name="title" v-slot="{ value, field, errorMessage }">
|
||||||
|
<a-form-item
|
||||||
|
label="Title"
|
||||||
|
:help="errorMessage"
|
||||||
|
:validate-status="!!errorMessage ? 'error' : undefined"
|
||||||
|
>
|
||||||
|
<a-input v-bind="field" :value="value" />
|
||||||
|
</a-form-item>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
:unchecked-value="false"
|
||||||
|
:value="true"
|
||||||
|
type="checkbox"
|
||||||
|
name="completed"
|
||||||
|
v-slot="{ value, field, errorMessage }"
|
||||||
|
>
|
||||||
|
<a-checkbox v-bind="field"> Complete </a-checkbox>
|
||||||
|
</Field>
|
||||||
|
<a-divider />
|
||||||
|
<!-- <test1/> -->
|
||||||
|
<field-array name="chapters" v-slot="{ fields, push, remove, move }">
|
||||||
|
<draggable
|
||||||
|
:component-data="{ type: 'transtion-group' }"
|
||||||
|
@start="drag = true"
|
||||||
|
@end="drag = false"
|
||||||
|
v-model="values.chapters"
|
||||||
|
tag="div"
|
||||||
|
item-key="uuidKey"
|
||||||
|
@change="
|
||||||
|
(e) => {
|
||||||
|
if (e.moved) {
|
||||||
|
// log.debug(e.moved);
|
||||||
|
move(e.moved.oldIndex, e.moved.newIndex);
|
||||||
|
acData.chapters = lmove(
|
||||||
|
acData.chapters,
|
||||||
|
e.moved.oldIndex,
|
||||||
|
e.moved.newIndex,
|
||||||
|
);
|
||||||
|
// log.debug(toRaw(acData.chapters.map((a) => toRaw(a))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<a-collapse v-model:active-key="expandos" collapsible="icon">
|
||||||
|
<a-collapse-panel :key="`${element.uuidKey}`">
|
||||||
|
<template #header>
|
||||||
|
{{ values.chapters[index]?.chapterTitle || "Untitled" }}
|
||||||
|
<a-button
|
||||||
|
@click="
|
||||||
|
(e) => {
|
||||||
|
let localFields = toRaw(fields);
|
||||||
|
// log.debug(`${index} | ${element.index}`);
|
||||||
|
// log.debug('fields->', localFields);
|
||||||
|
acData.chapters.splice(index, 1);
|
||||||
|
remove(index);
|
||||||
|
// todo renumber
|
||||||
|
// renumber()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<icon istyle="regular" name="trash" />
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<single-chapter :data="element" :name="`chapters[${index}]`" />
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<a-button
|
||||||
|
@click="
|
||||||
|
(e) => {
|
||||||
|
if (!Array.isArray(values.chapters)) {
|
||||||
|
setFieldValue('chapters', []);
|
||||||
|
}
|
||||||
|
const chaps = [...toRaw(values.chapters)];
|
||||||
|
let lastIndex = chaps.length - 1;
|
||||||
|
if (lastIndex < 0) lastIndex = 0;
|
||||||
|
let lastChapter = chaps[lastIndex];
|
||||||
|
// log.debug('chaptrs->', chaps);
|
||||||
|
// log.debug('lastIndex->', lastIndex, lastChapter);
|
||||||
|
let newChapter = Object.assign({}, defaultChapter, {
|
||||||
|
summary: lastChapter?.summary || '',
|
||||||
|
index: (lastChapter?.index || 0) + 1,
|
||||||
|
bands: lastChapter?.bands || [],
|
||||||
|
characters: lastChapter?.characters || [],
|
||||||
|
relationships: lastChapter?.relationships || [],
|
||||||
|
uuidKey: v4(),
|
||||||
|
genre: lastChapter?.genre || [],
|
||||||
|
});
|
||||||
|
// log.debug('nc->', newChapter);
|
||||||
|
push(newChapter);
|
||||||
|
acData.chapters.push(newChapter);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Add chapter
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</field-array>
|
||||||
|
<a-button type="primary" html-type="submit">go.</a-button>
|
||||||
|
</vee-form>
|
||||||
|
</template>
|
53
components/story/create/uploadOrPaste.vue
Normal file
53
components/story/create/uploadOrPaste.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
Form as VeeForm,
|
||||||
|
Field as veeField,
|
||||||
|
useForm,
|
||||||
|
useField,
|
||||||
|
ErrorMessage,
|
||||||
|
} from "vee-validate";
|
||||||
|
import { story } from "~/lib/client/editorConfig";
|
||||||
|
import icon from "~/components/icon.vue";
|
||||||
|
import baseEditor from "../../baseEditor.vue";
|
||||||
|
const fname = inject<string>("curName");
|
||||||
|
const fileList = ref([]);
|
||||||
|
const potField = useField(fname + "pot");
|
||||||
|
const fileField = useField(fname + "file");
|
||||||
|
const { value: pvalue } = potField;
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<a-radio-group v-model:value="pvalue">
|
||||||
|
<a-radio value="pasteOrType">Paste or type content</a-radio>
|
||||||
|
<a-radio value="upload">Upload a file</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<base-editor
|
||||||
|
label=""
|
||||||
|
v-if="pvalue === 'pasteOrType'"
|
||||||
|
:init="story"
|
||||||
|
:name="fname + 'content'"
|
||||||
|
/>
|
||||||
|
<a-upload
|
||||||
|
:with-credentials="true"
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
v-else-if="pvalue === 'upload'"
|
||||||
|
:name="fname + 'file'"
|
||||||
|
accept=".doc,.docx,.md,.markdown"
|
||||||
|
:max-count="1"
|
||||||
|
@change="
|
||||||
|
({ file }) => {
|
||||||
|
let resp = JSON.parse(file.response);
|
||||||
|
if (resp.success) {
|
||||||
|
fileField.setValue(resp.fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-button type="primary">
|
||||||
|
<icon istyle="regular" name="upload" /><span style="margin-left: 0.5em">
|
||||||
|
Upload a file</span
|
||||||
|
>
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
</template>
|
Loading…
x
Reference in New Issue
Block a user