feat: 优化上传组件
parent
ce93e87e38
commit
7bea445253
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
|
<a-button type="primary" @click="visible = true"> 上传文件 </a-button>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="visible"
|
v-model:visible="visible"
|
||||||
title="上传文件"
|
title="上传文件"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
:width="820"
|
:width="860"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:on-before-cancel="onBeforeCancel"
|
:on-before-cancel="onBeforeCancel"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
|
|
@ -18,6 +19,7 @@
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
@success="onUploadSuccess"
|
@success="onUploadSuccess"
|
||||||
|
@error="onUploadError"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<a-button type="outline"> 选择文件 </a-button>
|
<a-button type="outline"> 选择文件 </a-button>
|
||||||
|
|
@ -31,13 +33,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="fileList.length" class="h-[400px] divide-y overflow-hidden p-0 m-0">
|
<ul v-if="fileList.length" class="h-[424px] overflow-hidden p-0 m-0">
|
||||||
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto pr-[20px] divide-y">
|
||||||
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-2 py-3">
|
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-2 py-3">
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="truncate text-slate-900">
|
<div class="truncate text-slate-900">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px]">
|
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-1">
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400">
|
||||||
{{ numeral(item.file?.size).format("0 b") }}
|
{{ numeral(item.file?.size).format("0 b") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -45,12 +48,18 @@
|
||||||
<span v-if="item.status === 'init'"> </span>
|
<span v-if="item.status === 'init'"> </span>
|
||||||
<span v-else-if="item.status === 'uploading'">
|
<span v-else-if="item.status === 'uploading'">
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
{{ Math.floor((item.percent || 0) * 100) }}%(
|
速度:{{ numeral(fileMap.get(item.uid)?.speed || 0).format("0 b") }}/s, 进度:{{
|
||||||
{{ numeral(itemMap.get(item.uid)?.speed || 0).format("0 b") }}/s )
|
Math.floor((item.percent || 0) * 100)
|
||||||
|
}}
|
||||||
|
%
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.status === 'done'" class="text-green-600"> 完成 </span>
|
<span v-else-if="item.status === 'done'" class="text-green-600">
|
||||||
<span v-else="item.status === 'error'" class="text-red-500"> 失败 </span>
|
完成(耗时:{{ fileMap.get(item.uid)?.cost || 0 }}秒)
|
||||||
|
</span>
|
||||||
|
<span v-else="item.status === 'error'" class="text-red-500">
|
||||||
|
失败(原因:{{ fileMap.get(item.uid)?.error }})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a-progress :percent="Math.floor((item.percent || 0) * 100) / 100" :show-text="false"></a-progress>
|
<a-progress :percent="Math.floor((item.percent || 0) * 100) / 100" :show-text="false"></a-progress>
|
||||||
|
|
@ -61,28 +70,21 @@
|
||||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">删除</a-link>
|
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">删除</a-link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
</a-scrollbar>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-else class="h-[400px] flex items-center justify-center">
|
<div v-else class="h-[424px] flex items-center justify-center">
|
||||||
<a-empty description="选择文件后显示"></a-empty>
|
<a-empty description="选择文件后显示"></a-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-between gap-2 items-center">
|
<div class="flex justify-between gap-2 items-center">
|
||||||
<div class="text-gray-400 text-xs">已上传 {{ successCount }} 项</div>
|
<div class="text-gray-400">已上传 {{ stat.doneCount }}/{{ fileList.length }} 项</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<a-button
|
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded">
|
||||||
type="text"
|
清空
|
||||||
:disabled="!fileList.length || !fileList.some((i) => i.status === 'done')"
|
|
||||||
@click="clearUploaded"
|
|
||||||
>
|
|
||||||
清空已上传
|
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload">
|
||||||
type="primary"
|
|
||||||
:disabled="!fileList.length || !fileList.some((i) => i.status === 'init')"
|
|
||||||
@click="startUpload"
|
|
||||||
>
|
|
||||||
开始上传
|
开始上传
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -93,6 +95,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RequestParams, api } from "@/api";
|
import { RequestParams, api } from "@/api";
|
||||||
|
import { delConfirm } from "@/utils";
|
||||||
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import numeral from "numeral";
|
import numeral from "numeral";
|
||||||
|
|
@ -104,9 +107,35 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const uploadRef = ref<UploadInstance | null>(null);
|
const uploadRef = ref<UploadInstance | null>(null);
|
||||||
const successCount = ref(0);
|
|
||||||
const fileList = ref<FileItem[]>([]);
|
const fileList = ref<FileItem[]>([]);
|
||||||
const itemMap = reactive<Map<string, { lastTime: number; lastLoaded: number; speed: number } | null>>(new Map());
|
const fileMap = reactive<
|
||||||
|
Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
lastTime: number;
|
||||||
|
lastLoaded: number;
|
||||||
|
speed: number;
|
||||||
|
cost: number;
|
||||||
|
error: string;
|
||||||
|
} | null
|
||||||
|
>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
const stat = computed(() => {
|
||||||
|
const result = {
|
||||||
|
initCount: 0,
|
||||||
|
doneCount: 0,
|
||||||
|
uploadingCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
};
|
||||||
|
for (const item of fileList.value) {
|
||||||
|
if (item.status === "init") result.initCount++;
|
||||||
|
if (item.status === "uploading") result.uploadingCount++;
|
||||||
|
if (item.status === "done") result.doneCount++;
|
||||||
|
if (item.status === "error") result.errorCount++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始上传
|
* 开始上传
|
||||||
|
|
@ -121,6 +150,10 @@ const startUpload = () => {
|
||||||
*/
|
*/
|
||||||
const pauseItem = (item: FileItem) => {
|
const pauseItem = (item: FileItem) => {
|
||||||
uploadRef.value?.abort(item);
|
uploadRef.value?.abort(item);
|
||||||
|
const file = fileMap.get(item.uid);
|
||||||
|
if (file) {
|
||||||
|
file.error = "手动中止";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -145,8 +178,11 @@ const retryItem = (item: FileItem) => {
|
||||||
/**
|
/**
|
||||||
* 清空已上传
|
* 清空已上传
|
||||||
*/
|
*/
|
||||||
const clearUploaded = () => {
|
const clearUploaded = async () => {
|
||||||
fileList.value = fileList.value.filter((i) => i.status !== "done");
|
if (stat.value.doneCount !== fileList.value.length) {
|
||||||
|
await delConfirm("当前有未上传完成的文件,是否继续清空?");
|
||||||
|
}
|
||||||
|
fileList.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,10 +190,20 @@ const clearUploaded = () => {
|
||||||
* @param item 文件
|
* @param item 文件
|
||||||
*/
|
*/
|
||||||
const onUploadSuccess = (item: FileItem) => {
|
const onUploadSuccess = (item: FileItem) => {
|
||||||
successCount.value += 1;
|
|
||||||
emit("success", item);
|
emit("success", item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传失败后处理
|
||||||
|
* @param item 文件
|
||||||
|
*/
|
||||||
|
const onUploadError = (item: FileItem) => {
|
||||||
|
const file = fileMap.get(item.uid);
|
||||||
|
if (file) {
|
||||||
|
file.error = item.response?.data?.message || "网络异常";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭前检测
|
* 关闭前检测
|
||||||
*/
|
*/
|
||||||
|
|
@ -173,8 +219,9 @@ const onBeforeCancel = () => {
|
||||||
* 关闭后处理
|
* 关闭后处理
|
||||||
*/
|
*/
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
|
fileMap.clear();
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
emit("close", successCount.value);
|
emit("close", stat.value.doneCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -184,14 +231,17 @@ const onClose = () => {
|
||||||
const upload = (option: RequestOption) => {
|
const upload = (option: RequestOption) => {
|
||||||
const { fileItem, onError, onProgress, onSuccess } = option;
|
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||||
const source = axios.CancelToken.source();
|
const source = axios.CancelToken.source();
|
||||||
if (!itemMap.has(fileItem.uid)) {
|
if (!fileMap.has(fileItem.uid)) {
|
||||||
itemMap.set(fileItem.uid, {
|
fileMap.set(fileItem.uid, {
|
||||||
lastTime: Date.now(),
|
lastTime: Date.now(),
|
||||||
lastLoaded: 0,
|
lastLoaded: 0,
|
||||||
|
cost: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
|
error: "网络异常",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const item = itemMap.get(fileItem.uid)!;
|
const item = fileMap.get(fileItem.uid)!;
|
||||||
|
const startTime = Date.now();
|
||||||
const up = async () => {
|
const up = async () => {
|
||||||
const data = { file: fileItem.file as any };
|
const data = { file: fileItem.file as any };
|
||||||
const params: RequestParams = {
|
const params: RequestParams = {
|
||||||
|
|
@ -213,7 +263,8 @@ const upload = (option: RequestOption) => {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await api.file.addFile(data, params);
|
const res = await api.file.addFile(data, params);
|
||||||
itemMap.delete(fileItem.uid);
|
const currentTime = Date.now();
|
||||||
|
item.cost = Math.floor((currentTime - startTime) / 1000);
|
||||||
onSuccess(res);
|
onSuccess(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError(e);
|
onError(e);
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,10 @@
|
||||||
<div>
|
<div>
|
||||||
<Table v-bind="table">
|
<Table v-bind="table">
|
||||||
<template #action>
|
<template #action>
|
||||||
<a-button type="primary" @click="uploadRef?.open()">
|
<ani-upload></ani-upload>
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-upload"></i>
|
|
||||||
</template>
|
|
||||||
上传
|
|
||||||
</a-button>
|
|
||||||
<a-button type="outline" status="danger" :disabled="!selected.length" @click="onDeleteMany">
|
<a-button type="outline" status="danger" :disabled="!selected.length" @click="onDeleteMany">
|
||||||
批量删除
|
批量删除
|
||||||
</a-button>
|
</a-button>
|
||||||
<ani-upload ref="uploadRef"></ani-upload>
|
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
||||||
|
|
@ -26,10 +20,10 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||||
|
import { delConfirm } from "@/utils";
|
||||||
import numeral from "numeral";
|
import numeral from "numeral";
|
||||||
import AniGroup from "./components/group.vue";
|
import AniGroup from "./components/group.vue";
|
||||||
import AniUpload from "./components/upload.vue";
|
import AniUpload from "./components/upload.vue";
|
||||||
import { delConfirm } from "@/utils";
|
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const image = ref("");
|
const image = ref("");
|
||||||
|
|
@ -47,8 +41,6 @@ const onDeleteMany = async () => {
|
||||||
await delConfirm();
|
await delConfirm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadRef = ref<InstanceType<typeof AniUpload>>();
|
|
||||||
|
|
||||||
const getIcon = (mimetype: string) => {
|
const getIcon = (mimetype: string) => {
|
||||||
if (mimetype.startsWith("image")) {
|
if (mimetype.startsWith("image")) {
|
||||||
return "icon-file-iimage";
|
return "icon-file-iimage";
|
||||||
|
|
@ -94,7 +86,10 @@ const table = useTable({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span class="hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer" onClick={() => preview(record)}>
|
<span
|
||||||
|
class="hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
||||||
|
onClick={() => preview(record)}
|
||||||
|
>
|
||||||
{record.name}
|
{record.name}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 text-xs truncate">{numeral(record.size).format("0 b")}</span>
|
<span class="text-gray-400 text-xs truncate">{numeral(record.size).format("0 b")}</span>
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,22 @@ export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AAlert: typeof import('@arco-design/web-vue')['Alert']
|
|
||||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
|
||||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||||
ACard: typeof import('@arco-design/web-vue')['Card']
|
ACard: typeof import('@arco-design/web-vue')['Card']
|
||||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
|
||||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||||
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
||||||
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
|
|
||||||
AEmpty: typeof import('@arco-design/web-vue')['Empty']
|
AEmpty: typeof import('@arco-design/web-vue')['Empty']
|
||||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||||
AImage: typeof import('@arco-design/web-vue')['Image']
|
|
||||||
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
|
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
|
||||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||||
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
|
||||||
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
||||||
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
|
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
|
||||||
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
||||||
|
|
@ -36,29 +30,15 @@ declare module '@vue/runtime-core' {
|
||||||
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
|
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
|
||||||
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
|
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
|
||||||
ALink: typeof import('@arco-design/web-vue')['Link']
|
ALink: typeof import('@arco-design/web-vue')['Link']
|
||||||
AList: typeof import('@arco-design/web-vue')['List']
|
|
||||||
AListItem: typeof import('@arco-design/web-vue')['ListItem']
|
|
||||||
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta']
|
|
||||||
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
|
||||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
|
||||||
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||||
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
|
||||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
|
||||||
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||||
ASpace: typeof import('@arco-design/web-vue')['Space']
|
ASpace: typeof import('@arco-design/web-vue')['Space']
|
||||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
||||||
ATable: typeof import('@arco-design/web-vue')['Table']
|
|
||||||
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn']
|
|
||||||
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
|
||||||
ATabs: typeof import('@arco-design/web-vue')['Tabs']
|
|
||||||
ATag: typeof import('@arco-design/web-vue')['Tag']
|
|
||||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
|
||||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||||
ATree: typeof import('@arco-design/web-vue')['Tree']
|
|
||||||
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
||||||
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
|
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
|
||||||
Block: typeof import('./../components/editor/panel-main/components/block.vue')['default']
|
Block: typeof import('./../components/editor/panel-main/components/block.vue')['default']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue