feat: Optimize dialog
- Change the method of open dialog - Fix the problem of open dialog disappearing due to virtual scrolling - Float the active dialog to the top
This commit is contained in:
64
src/App.vue
64
src/App.vue
@@ -1,31 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<DialogManager></DialogManager>
|
|
||||||
<DialogDownload></DialogDownload>
|
|
||||||
<GlobalToast></GlobalToast>
|
<GlobalToast></GlobalToast>
|
||||||
<ConfirmDialog></ConfirmDialog>
|
<GlobalConfirm></GlobalConfirm>
|
||||||
<GlobalLoading></GlobalLoading>
|
<GlobalLoading></GlobalLoading>
|
||||||
|
<GlobalDialogStack></GlobalDialogStack>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import GlobalToast from 'components/GlobalToast.vue'
|
|
||||||
import DialogManager from 'components/DialogManager.vue'
|
import DialogManager from 'components/DialogManager.vue'
|
||||||
import DialogDownload from 'components/DialogDownload.vue'
|
import DialogDownload from 'components/DialogDownload.vue'
|
||||||
|
import GlobalToast from 'components/GlobalToast.vue'
|
||||||
import GlobalLoading from 'components/GlobalLoading.vue'
|
import GlobalLoading from 'components/GlobalLoading.vue'
|
||||||
import ConfirmDialog from 'primevue/confirmdialog'
|
import GlobalDialogStack from 'components/GlobalDialogStack.vue'
|
||||||
|
import GlobalConfirm from 'primevue/confirmdialog'
|
||||||
import { $el, app, ComfyButton } from 'scripts/comfyAPI'
|
import { $el, app, ComfyButton } from 'scripts/comfyAPI'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStoreProvider } from 'hooks/store'
|
import { useStoreProvider } from 'hooks/store'
|
||||||
|
import { useToast } from 'hooks/toast'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { dialogManager } = useStoreProvider()
|
const { dialog, models, config, download } = useStoreProvider()
|
||||||
|
const { toast } = useToast()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const refreshModelsAndConfig = async () => {
|
||||||
|
await Promise.all([models.refresh(), config.refresh()])
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Refreshed Models',
|
||||||
|
life: 2000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDownloadDialog = () => {
|
||||||
|
dialog.open({
|
||||||
|
key: 'model-manager-download-list',
|
||||||
|
title: t('downloadList'),
|
||||||
|
content: DialogDownload,
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
icon: 'pi pi-refresh',
|
||||||
|
command: () => download.refresh(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openManagerDialog = () => {
|
||||||
|
const { cardWidth, gutter, aspect } = config
|
||||||
|
|
||||||
|
dialog.open({
|
||||||
|
key: 'model-manager',
|
||||||
|
title: t('modelManager'),
|
||||||
|
content: DialogManager,
|
||||||
|
keepAlive: true,
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
icon: 'pi pi-refresh',
|
||||||
|
command: refreshModelsAndConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'pi pi-download',
|
||||||
|
command: openDownloadDialog,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
minWidth: cardWidth * 2 + gutter + 42,
|
||||||
|
minHeight: (cardWidth / aspect) * 0.5 + 162,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
app.ui?.menuContainer?.appendChild(
|
app.ui?.menuContainer?.appendChild(
|
||||||
$el('button', {
|
$el('button', {
|
||||||
id: 'comfyui-model-manager-button',
|
id: 'comfyui-model-manager-button',
|
||||||
textContent: t('modelManager'),
|
textContent: t('modelManager'),
|
||||||
onclick: () => dialogManager.toggle(),
|
onclick: openManagerDialog,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +82,7 @@ onMounted(() => {
|
|||||||
icon: 'folder-search',
|
icon: 'folder-search',
|
||||||
tooltip: t('openModelManager'),
|
tooltip: t('openModelManager'),
|
||||||
content: t('modelManager'),
|
content: t('modelManager'),
|
||||||
action: () => dialogManager.toggle(),
|
action: openManagerDialog,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,97 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<div class="flex h-full flex-col gap-4 px-5">
|
||||||
v-model:visible="visible"
|
<ResponseInput
|
||||||
:header="$t('parseModelUrl')"
|
v-model="modelUrl"
|
||||||
:modal="true"
|
:allow-clear="true"
|
||||||
:maximizable="!isMobile"
|
:placeholder="$t('pleaseInputModelUrl')"
|
||||||
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
@keypress.enter="searchModelsByUrl"
|
||||||
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
|
>
|
||||||
pt:mask:style="--p-mask-background: rgba(0, 0, 0, 0.3)"
|
<template #suffix>
|
||||||
pt:root:class="max-h-full"
|
<span
|
||||||
pt:content:class="px-0"
|
class="pi pi-search pi-inputicon"
|
||||||
@after-hide="clearContent"
|
@click="searchModelsByUrl"
|
||||||
>
|
></span>
|
||||||
<div class="flex h-full flex-col gap-4 px-5">
|
</template>
|
||||||
<ResponseInput
|
</ResponseInput>
|
||||||
v-model="modelUrl"
|
|
||||||
:allow-clear="true"
|
<div v-show="data.length > 0">
|
||||||
:placeholder="$t('pleaseInputModelUrl')"
|
<ResponseSelect
|
||||||
@keypress.enter="searchModelsByUrl"
|
v-model="current"
|
||||||
|
:items="data"
|
||||||
|
:type="isMobile ? 'drop' : 'button'"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #prefix>
|
||||||
<span
|
<span>version:</span>
|
||||||
class="pi pi-search pi-inputicon"
|
|
||||||
@click="searchModelsByUrl"
|
|
||||||
></span>
|
|
||||||
</template>
|
</template>
|
||||||
</ResponseInput>
|
</ResponseSelect>
|
||||||
|
|
||||||
<div v-show="data.length > 0">
|
|
||||||
<ResponseSelect
|
|
||||||
v-model="current"
|
|
||||||
:items="data"
|
|
||||||
:type="isMobile ? 'drop' : 'button'"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<span>version:</span>
|
|
||||||
</template>
|
|
||||||
</ResponseSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ResponseScroll class="-mx-5 h-full">
|
|
||||||
<div class="px-5">
|
|
||||||
<ModelContent
|
|
||||||
v-for="{ item } in data"
|
|
||||||
v-show="current == item.id"
|
|
||||||
:key="item.id"
|
|
||||||
:model="item"
|
|
||||||
:editable="true"
|
|
||||||
@submit="createDownTask"
|
|
||||||
>
|
|
||||||
<template #action>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-download"
|
|
||||||
:label="$t('download')"
|
|
||||||
type="submit"
|
|
||||||
></Button>
|
|
||||||
</template>
|
|
||||||
</ModelContent>
|
|
||||||
|
|
||||||
<div v-show="data.length === 0">
|
|
||||||
<div class="flex flex-col items-center gap-4 py-8">
|
|
||||||
<i class="pi pi-box text-3xl"></i>
|
|
||||||
<div>No Models Found</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResponseScroll>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogResizer :min-width="390"></DialogResizer>
|
<ResponseScroll class="-mx-5 h-full">
|
||||||
</Dialog>
|
<div class="px-5">
|
||||||
|
<ModelContent
|
||||||
|
v-for="{ item } in data"
|
||||||
|
v-show="current == item.id"
|
||||||
|
:key="item.id"
|
||||||
|
:model="item"
|
||||||
|
:editable="true"
|
||||||
|
@submit="createDownTask"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-download"
|
||||||
|
:label="$t('download')"
|
||||||
|
type="submit"
|
||||||
|
></Button>
|
||||||
|
</template>
|
||||||
|
</ModelContent>
|
||||||
|
|
||||||
|
<div v-show="data.length === 0">
|
||||||
|
<div class="flex flex-col items-center gap-4 py-8">
|
||||||
|
<i class="pi pi-box text-3xl"></i>
|
||||||
|
<div>No Models Found</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResponseScroll>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DialogResizer from 'components/DialogResizer.vue'
|
import ModelContent from 'components/ModelContent.vue'
|
||||||
import ResponseInput from 'components/ResponseInput.vue'
|
import ResponseInput from 'components/ResponseInput.vue'
|
||||||
import ResponseSelect from 'components/ResponseSelect.vue'
|
import ResponseSelect from 'components/ResponseSelect.vue'
|
||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
import ModelContent from 'components/ModelContent.vue'
|
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Dialog from 'primevue/dialog'
|
import { useConfig } from 'hooks/config'
|
||||||
|
import { useDialog } from 'hooks/dialog'
|
||||||
import { useModelSearch } from 'hooks/download'
|
import { useModelSearch } from 'hooks/download'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { previewUrlToFile } from 'utils/common'
|
|
||||||
import { useLoading } from 'hooks/loading'
|
import { useLoading } from 'hooks/loading'
|
||||||
import { request } from 'hooks/request'
|
import { request } from 'hooks/request'
|
||||||
import { useToast } from 'hooks/toast'
|
import { useToast } from 'hooks/toast'
|
||||||
import { useConfig } from 'hooks/config'
|
import { previewUrlToFile } from 'utils/common'
|
||||||
|
import { ref } from 'vue'
|
||||||
const visible = defineModel<boolean>('visible')
|
|
||||||
|
|
||||||
const { isMobile } = useConfig()
|
const { isMobile } = useConfig()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const modelUrl = ref<string>()
|
const modelUrl = ref<string>()
|
||||||
|
|
||||||
@@ -103,11 +86,6 @@ const searchModelsByUrl = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearContent = () => {
|
|
||||||
modelUrl.value = undefined
|
|
||||||
data.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const createDownTask = async (data: VersionModel) => {
|
const createDownTask = async (data: VersionModel) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
@@ -143,7 +121,7 @@ const createDownTask = async (data: VersionModel) => {
|
|||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
visible.value = false
|
dialog.close({ key: 'model-manager-create-task' })
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.add({
|
toast.add({
|
||||||
|
|||||||
@@ -1,167 +1,93 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<div class="flex h-full flex-col gap-4">
|
||||||
v-model:visible="visible"
|
<div class="whitespace-nowrap px-4 @container">
|
||||||
:modal="true"
|
<div class="flex gap-4 @sm:justify-end">
|
||||||
pt:mask:style="--p-mask-background: rgba(0, 0, 0, 0.3)"
|
<Button
|
||||||
pt:root:class="max-h-full"
|
class="w-full @sm:w-auto"
|
||||||
pt:content:class="px-0"
|
:label="$t('createDownloadTask')"
|
||||||
>
|
@click="openCreateTask"
|
||||||
<template #header>
|
></Button>
|
||||||
<div class="flex flex-1 items-center justify-between pr-2">
|
|
||||||
<span class="p-dialog-title select-none">
|
|
||||||
{{ $t('downloadList') }}
|
|
||||||
</span>
|
|
||||||
<div class="p-dialog-header-actions">
|
|
||||||
<Button
|
|
||||||
icon="pi pi-refresh"
|
|
||||||
severity="secondary"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
@click="refresh"
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<div class="flex h-full flex-col gap-4">
|
<ResponseScroll>
|
||||||
<div class="whitespace-nowrap px-4 @container">
|
<div class="w-full px-4">
|
||||||
<div class="flex gap-4 @sm:justify-end">
|
<ul class="m-0 flex list-none flex-col gap-4 p-0">
|
||||||
<Button
|
|
||||||
class="w-full @sm:w-auto"
|
|
||||||
:label="$t('createDownloadTask')"
|
|
||||||
@click="toggleCreateTask"
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ResponseScroll>
|
|
||||||
<div class="w-full px-4">
|
|
||||||
<ul class="m-0 flex list-none flex-col gap-4 p-0">
|
|
||||||
<li
|
|
||||||
v-for="item in data"
|
|
||||||
:key="item.taskId"
|
|
||||||
class="rounded-lg border border-gray-500 p-4"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4 overflow-hidden whitespace-nowrap">
|
|
||||||
<div class="h-18 preview-aspect">
|
|
||||||
<img :src="item.preview" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-3 overflow-hidden">
|
|
||||||
<div class="flex items-center gap-3 overflow-hidden">
|
|
||||||
<span class="flex-1 overflow-hidden text-ellipsis">
|
|
||||||
{{ item.fullname }}
|
|
||||||
</span>
|
|
||||||
<span v-show="item.status === 'waiting'" class="h-4">
|
|
||||||
<i class="pi pi-spinner pi-spin"></i>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-show="item.status === 'doing'"
|
|
||||||
class="h-4 cursor-pointer"
|
|
||||||
@click="item.pauseTask"
|
|
||||||
>
|
|
||||||
<i class="pi pi-pause-circle"></i>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-show="item.status === 'pause'"
|
|
||||||
class="h-4 cursor-pointer"
|
|
||||||
@click="item.resumeTask"
|
|
||||||
>
|
|
||||||
<i class="pi pi-play-circle"></i>
|
|
||||||
</span>
|
|
||||||
<span class="h-4 cursor-pointer" @click="item.deleteTask">
|
|
||||||
<i class="pi pi-trash text-red-400"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-2 overflow-hidden rounded bg-gray-200">
|
|
||||||
<div
|
|
||||||
class="h-full bg-blue-500 transition-[width]"
|
|
||||||
:style="{ width: `${item.progress}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div>{{ item.downloadProgress }}</div>
|
|
||||||
<div v-show="item.status === 'doing'">
|
|
||||||
{{ item.downloadSpeed }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<!-- <ul class="m-0 flex list-none flex-col gap-4 p-0 px-4 pb-0">
|
|
||||||
<li
|
<li
|
||||||
v-for="item in data"
|
v-for="item in data"
|
||||||
:key="item.taskId"
|
:key="item.taskId"
|
||||||
class="flex flex-row gap-3 overflow-hidden rounded-lg border border-gray-500 p-4"
|
class="rounded-lg border border-gray-500 p-4"
|
||||||
>
|
>
|
||||||
<div class="h-18 preview-aspect">
|
<div class="flex gap-4 overflow-hidden whitespace-nowrap">
|
||||||
<img
|
<div class="h-18 preview-aspect">
|
||||||
:src="`/model-manager/preview/download/${item.preview}`"
|
<img :src="item.preview" />
|
||||||
alt=""
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
<div class="flex flex-1 flex-col gap-3 overflow-hidden">
|
||||||
<div class="flex flex-1 flex-col gap-3">
|
<div class="flex items-center gap-3 overflow-hidden">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<span class="flex-1 overflow-hidden text-ellipsis">
|
||||||
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
|
{{ item.fullname }}
|
||||||
{{ item.fullname }}
|
</span>
|
||||||
</div>
|
<span v-show="item.status === 'waiting'" class="h-4">
|
||||||
<div class="flex items-center gap-4">
|
<i class="pi pi-spinner pi-spin"></i>
|
||||||
<i v-show="item.status === 'waiting'">
|
</span>
|
||||||
{{ $t('waiting') }}...
|
<span
|
||||||
</i>
|
|
||||||
<i
|
|
||||||
v-show="item.status === 'doing'"
|
v-show="item.status === 'doing'"
|
||||||
class="pi pi-pause-circle"
|
class="h-4 cursor-pointer"
|
||||||
@click="item.pauseTask"
|
@click="item.pauseTask"
|
||||||
></i>
|
>
|
||||||
<i
|
<i class="pi pi-pause-circle"></i>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
v-show="item.status === 'pause'"
|
v-show="item.status === 'pause'"
|
||||||
class="pi pi-play-circle"
|
class="h-4 cursor-pointer"
|
||||||
@click="item.resumeTask"
|
@click="item.resumeTask"
|
||||||
></i>
|
>
|
||||||
<i
|
<i class="pi pi-play-circle"></i>
|
||||||
class="pi pi-trash text-red-400"
|
</span>
|
||||||
@click="item.deleteTask"
|
<span class="h-4 cursor-pointer" @click="item.deleteTask">
|
||||||
></i>
|
<i class="pi pi-trash text-red-400"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="h-2 overflow-hidden rounded bg-gray-200">
|
||||||
<div class="flex items-center gap-2">
|
<div
|
||||||
<div class="h-2 flex-1 overflow-hidden rounded bg-gray-200">
|
class="h-full bg-blue-500 transition-[width]"
|
||||||
<div class="h-full *:h-full *:bg-blue-500 *:transition-all">
|
:style="{ width: `${item.progress}%` }"
|
||||||
<div :style="{ width: `${item.progress}%` }"></div>
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>{{ item.downloadProgress }}</div>
|
||||||
|
<div v-show="item.status === 'doing'">
|
||||||
|
{{ item.downloadSpeed }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>{{ item.downloadProgress }}</div>
|
|
||||||
<div v-show="item.status === 'doing'">
|
|
||||||
{{ item.downloadSpeed }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul> -->
|
</ul>
|
||||||
</ResponseScroll>
|
</div>
|
||||||
</div>
|
</ResponseScroll>
|
||||||
|
</div>
|
||||||
<DialogResizer :min-width="390" :min-height="390"></DialogResizer>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<DialogCreateTask v-model:visible="openCreateTask"></DialogCreateTask>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
||||||
import DialogResizer from 'components/DialogResizer.vue'
|
|
||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
import { useDownload } from 'hooks/download'
|
import { useDownload } from 'hooks/download'
|
||||||
import { useBoolean } from 'hooks/utils'
|
import { useDialog } from 'hooks/dialog'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { visible, data, refresh } = useDownload()
|
const { data } = useDownload()
|
||||||
|
|
||||||
const [openCreateTask, toggleCreateTask] = useBoolean()
|
const { t } = useI18n()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const openCreateTask = () => {
|
||||||
|
dialog.open({
|
||||||
|
key: 'model-manager-create-task',
|
||||||
|
title: t('parseModelUrl'),
|
||||||
|
content: DialogCreateTask,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,143 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<div
|
||||||
:visible="visible"
|
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
|
||||||
@update:visible="updateVisible"
|
:style="{
|
||||||
:maximizable="!isMobile"
|
['--card-width']: `${cardWidth}px`,
|
||||||
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
['--gutter']: `${gutter}px`,
|
||||||
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
|
}"
|
||||||
:pt:mask:class="['group', { open }]"
|
v-resize="onContainerResize"
|
||||||
pt:root:class="max-h-full group-[:not(.open)]:!hidden"
|
|
||||||
pt:content:class="px-0 flex-1"
|
|
||||||
>
|
>
|
||||||
<template #header>
|
|
||||||
<div class="flex flex-1 items-center justify-between pr-2">
|
|
||||||
<span class="p-dialog-title select-none">{{ $t('modelManager') }}</span>
|
|
||||||
<div class="p-dialog-header-actions">
|
|
||||||
<Button
|
|
||||||
icon="pi pi-refresh"
|
|
||||||
severity="secondary"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
@click="refreshModels"
|
|
||||||
></Button>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-download"
|
|
||||||
severity="secondary"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
@click="download.toggle"
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
|
:class="[
|
||||||
:style="{
|
'grid grid-cols-1 justify-center gap-4 px-8',
|
||||||
['--card-width']: `${cardWidth}px`,
|
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
||||||
['--gutter']: `${gutter}px`,
|
'@lg/content:gap-[var(--gutter)]',
|
||||||
}"
|
'@lg/content:px-4',
|
||||||
v-resize="onContainerResize"
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div class="col-span-full @container/toolbar">
|
||||||
:class="[
|
<div :class="['flex flex-col gap-4', '@2xl/toolbar:flex-row']">
|
||||||
'grid grid-cols-1 justify-center gap-4 px-8',
|
<ResponseInput
|
||||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
v-model="searchContent"
|
||||||
'@lg/content:gap-[var(--gutter)]',
|
:placeholder="$t('searchModels')"
|
||||||
'@lg/content:px-4',
|
:allow-clear="true"
|
||||||
]"
|
suffix-icon="pi pi-search"
|
||||||
>
|
></ResponseInput>
|
||||||
<div class="col-span-full @container/toolbar">
|
|
||||||
<div :class="['flex flex-col gap-4', '@2xl/toolbar:flex-row']">
|
|
||||||
<ResponseInput
|
|
||||||
v-model="searchContent"
|
|
||||||
:placeholder="$t('searchModels')"
|
|
||||||
:allow-clear="true"
|
|
||||||
suffix-icon="pi pi-search"
|
|
||||||
></ResponseInput>
|
|
||||||
|
|
||||||
<div
|
<div class="flex items-center justify-between gap-4 overflow-hidden">
|
||||||
class="flex items-center justify-between gap-4 overflow-hidden"
|
<ResponseSelect
|
||||||
>
|
v-model="currentType"
|
||||||
<ResponseSelect
|
:items="typeOptions"
|
||||||
v-model="currentType"
|
:type="isMobile ? 'drop' : 'button'"
|
||||||
:items="typeOptions"
|
></ResponseSelect>
|
||||||
:type="isMobile ? 'drop' : 'button'"
|
<ResponseSelect
|
||||||
></ResponseSelect>
|
v-model="sortOrder"
|
||||||
<ResponseSelect
|
:items="sortOrderOptions"
|
||||||
v-model="sortOrder"
|
></ResponseSelect>
|
||||||
:items="sortOrderOptions"
|
|
||||||
></ResponseSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResponseScroll
|
|
||||||
:items="list"
|
|
||||||
:itemSize="cardWidth / aspect + gutter"
|
|
||||||
:row-key="(item) => item.map(genModelKey).join(',')"
|
|
||||||
class="h-full flex-1"
|
|
||||||
>
|
|
||||||
<template #item="{ item }">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'grid grid-cols-1 justify-center gap-8 px-8',
|
|
||||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
|
||||||
'@lg/content:gap-[var(--gutter)]',
|
|
||||||
'@lg/content:px-4',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<DialogModelCard
|
|
||||||
v-for="model in item"
|
|
||||||
:key="genModelKey(model)"
|
|
||||||
:model="model"
|
|
||||||
></DialogModelCard>
|
|
||||||
<div class="col-span-full"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #empty>
|
|
||||||
<div class="flex flex-col items-center gap-4 pt-20 opacity-70">
|
|
||||||
<i class="pi pi-box text-4xl"></i>
|
|
||||||
<div class="select-none text-lg font-bold">No models found</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ResponseScroll>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogResizer
|
<ResponseScroll
|
||||||
:min-width="cardWidth * 2 + gutter + 42"
|
:items="list"
|
||||||
:min-height="(cardWidth / aspect) * 0.5 + 162"
|
:itemSize="cardWidth / aspect + gutter"
|
||||||
></DialogResizer>
|
:row-key="(item) => item.map(genModelKey).join(',')"
|
||||||
</Dialog>
|
class="h-full flex-1"
|
||||||
|
>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'grid grid-cols-1 justify-center gap-8 px-8',
|
||||||
|
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
||||||
|
'@lg/content:gap-[var(--gutter)]',
|
||||||
|
'@lg/content:px-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<ModelCard
|
||||||
|
v-for="model in item"
|
||||||
|
:key="genModelKey(model)"
|
||||||
|
:model="model"
|
||||||
|
></ModelCard>
|
||||||
|
<div class="col-span-full"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<div class="flex flex-col items-center gap-4 pt-20 opacity-70">
|
||||||
|
<i class="pi pi-box text-4xl"></i>
|
||||||
|
<div class="select-none text-lg font-bold">No models found</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ResponseScroll>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="manager-dialog">
|
<script setup lang="ts" name="manager-dialog">
|
||||||
import { useConfig } from 'hooks/config'
|
import { useConfig } from 'hooks/config'
|
||||||
import { useDialogManager } from 'hooks/manager'
|
|
||||||
import { useModels } from 'hooks/model'
|
import { useModels } from 'hooks/model'
|
||||||
import DialogResizer from 'components/DialogResizer.vue'
|
import ModelCard from 'components/ModelCard.vue'
|
||||||
import DialogModelCard from 'components/DialogModelCard.vue'
|
|
||||||
import ResponseInput from 'components/ResponseInput.vue'
|
import ResponseInput from 'components/ResponseInput.vue'
|
||||||
import ResponseSelect from 'components/ResponseSelect.vue'
|
import ResponseSelect from 'components/ResponseSelect.vue'
|
||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
import Button from 'primevue/button'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useToast } from 'hooks/toast'
|
|
||||||
import { useDownload } from 'hooks/download'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { chunk } from 'lodash'
|
import { chunk } from 'lodash'
|
||||||
import { defineResizeCallback } from 'hooks/resize'
|
import { defineResizeCallback } from 'hooks/resize'
|
||||||
|
import { genModelKey } from 'utils/model'
|
||||||
|
|
||||||
const { isMobile, cardWidth, gutter, aspect, refreshSetting } = useConfig()
|
const { isMobile, cardWidth, gutter, aspect } = useConfig()
|
||||||
|
|
||||||
const download = useDownload()
|
const { data } = useModels()
|
||||||
|
|
||||||
const { visible, updateVisible, open } = useDialogManager()
|
|
||||||
const { data, refresh } = useModels()
|
|
||||||
const { toast } = useToast()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const searchContent = ref<string>()
|
const searchContent = ref<string>()
|
||||||
@@ -222,15 +173,6 @@ const list = computed(() => {
|
|||||||
return chunk(sortedList, colSpan.value)
|
return chunk(sortedList, colSpan.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshModels = async () => {
|
|
||||||
await Promise.all([refresh(), refreshSetting()])
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Refreshed Models',
|
|
||||||
life: 2000,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onContainerResize = defineResizeCallback((entries) => {
|
const onContainerResize = defineResizeCallback((entries) => {
|
||||||
const entry = entries[0]
|
const entry = entries[0]
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
@@ -241,8 +183,4 @@ const onContainerResize = defineResizeCallback((entries) => {
|
|||||||
colSpanWidth.value = colSpan.value * (cardWidth + gutter) - gutter
|
colSpanWidth.value = colSpan.value * (cardWidth + gutter) - gutter
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const genModelKey = (model: BaseModel) => {
|
|
||||||
return `${model.type}:${model.pathIndex}:${model.fullname}`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,104 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<ResponseScroll class="h-full">
|
||||||
v-model:visible="visible"
|
<div class="px-8">
|
||||||
:header="filename"
|
<ModelContent
|
||||||
:maximizable="!isMobile"
|
v-model:editable="editable"
|
||||||
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
:model="modelContent"
|
||||||
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
|
@submit="handleSave"
|
||||||
pt:title:class="whitespace-nowrap text-ellipsis overflow-hidden"
|
@reset="handleCancel"
|
||||||
pt:root:class="max-h-full"
|
>
|
||||||
pt:content:class="px-0"
|
<template #action="{ metadata }">
|
||||||
@after-hide="handleCancel"
|
<template v-if="editable">
|
||||||
>
|
<Button :label="$t('cancel')" type="reset"></Button>
|
||||||
<ResponseScroll class="h-full">
|
<Button :label="$t('save')" type="submit"></Button>
|
||||||
<div class="px-8">
|
|
||||||
<ModelContent
|
|
||||||
v-model:editable="editable"
|
|
||||||
:model="modelContent"
|
|
||||||
@submit="handleSave"
|
|
||||||
@reset="handleCancel"
|
|
||||||
>
|
|
||||||
<template #action="{ metadata }">
|
|
||||||
<template v-if="editable">
|
|
||||||
<Button :label="$t('cancel')" type="reset"></Button>
|
|
||||||
<Button :label="$t('save')" type="submit"></Button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<Button
|
|
||||||
v-show="metadata.modelPage"
|
|
||||||
icon="pi pi-eye"
|
|
||||||
@click="openModelPage(metadata.modelPage)"
|
|
||||||
></Button>
|
|
||||||
<Button icon="pi pi-plus" @click.stop="addModelNode"></Button>
|
|
||||||
<Button icon="pi pi-copy" @click.stop="copyModelNode"></Button>
|
|
||||||
<Button
|
|
||||||
v-show="model.preview"
|
|
||||||
icon="pi pi-file-import"
|
|
||||||
@click.stop="loadPreviewWorkflow"
|
|
||||||
></Button>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-pen-to-square"
|
|
||||||
@click="editable = true"
|
|
||||||
></Button>
|
|
||||||
<Button
|
|
||||||
severity="danger"
|
|
||||||
icon="pi pi-trash"
|
|
||||||
@click="handleDelete"
|
|
||||||
></Button>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</ModelContent>
|
<template v-else>
|
||||||
</div>
|
<Button
|
||||||
</ResponseScroll>
|
v-show="metadata.modelPage"
|
||||||
<DialogResizer :min-width="390"></DialogResizer>
|
icon="pi pi-eye"
|
||||||
</Dialog>
|
@click="openModelPage(metadata.modelPage)"
|
||||||
|
></Button>
|
||||||
|
<Button icon="pi pi-plus" @click.stop="addModelNode"></Button>
|
||||||
|
<Button icon="pi pi-copy" @click.stop="copyModelNode"></Button>
|
||||||
|
<Button
|
||||||
|
v-show="model.preview"
|
||||||
|
icon="pi pi-file-import"
|
||||||
|
@click.stop="loadPreviewWorkflow"
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-pen-to-square"
|
||||||
|
@click="editable = true"
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
severity="danger"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
@click="handleDelete"
|
||||||
|
></Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ModelContent>
|
||||||
|
</div>
|
||||||
|
</ResponseScroll>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
import ModelContent from 'components/ModelContent.vue'
|
import ModelContent from 'components/ModelContent.vue'
|
||||||
import DialogResizer from 'components/DialogResizer.vue'
|
|
||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
import { useConfig } from 'hooks/config'
|
import Button from 'primevue/button'
|
||||||
import { computed, ref, watchEffect } from 'vue'
|
|
||||||
import { useModelNodeAction, useModels } from 'hooks/model'
|
import { useModelNodeAction, useModels } from 'hooks/model'
|
||||||
import { useRequest } from 'hooks/request'
|
import { useRequest } from 'hooks/request'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible')
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: Model
|
model: Model
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const { isMobile } = useConfig()
|
|
||||||
const { remove, update } = useModels()
|
const { remove, update } = useModels()
|
||||||
|
|
||||||
const editable = ref(false)
|
const editable = ref(false)
|
||||||
|
|
||||||
const { data: extraInfo, refresh: fetchExtraInfo } = useRequest(
|
const modelDetailUrl = `/model/${props.model.type}/${props.model.pathIndex}/${props.model.fullname}`
|
||||||
`/model/${props.model.type}/${props.model.pathIndex}/${props.model.fullname}`,
|
const { data: extraInfo } = useRequest(modelDetailUrl, {
|
||||||
{
|
method: 'GET',
|
||||||
method: 'GET',
|
})
|
||||||
manual: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const modelContent = computed(() => {
|
const modelContent = computed(() => {
|
||||||
return Object.assign({}, props.model, extraInfo.value)
|
return Object.assign({}, props.model, extraInfo.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (visible.value === true) {
|
|
||||||
fetchExtraInfo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const filename = computed(() => {
|
|
||||||
const basename = props.model.fullname.split('/').pop()!
|
|
||||||
return basename.replace(props.model.extension, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
editable.value = false
|
editable.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form @submit="handleSubmit" @reset="handleReset">
|
|
||||||
<slot name="default"></slot>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const emits = defineEmits(['submit', 'reset'])
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
emits('reset')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
emits('submit')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
46
src/components/GlobalDialogStack.vue
Normal file
46
src/components/GlobalDialogStack.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<ResponseDialog
|
||||||
|
v-for="(item, index) in stack"
|
||||||
|
v-model:visible="item.visible"
|
||||||
|
:key="item.key"
|
||||||
|
:keep-alive="item.keepAlive"
|
||||||
|
:default-size="item.defaultSize"
|
||||||
|
:default-mobile-size="item.defaultMobileSize"
|
||||||
|
:resize-allow="item.resizeAllow"
|
||||||
|
:min-width="item.minWidth"
|
||||||
|
:max-width="item.maxWidth"
|
||||||
|
:min-height="item.minHeight"
|
||||||
|
:max-height="item.maxHeight"
|
||||||
|
:z-index="index"
|
||||||
|
:pt:root:onMousedown="() => rise(item)"
|
||||||
|
@hide="() => close(item)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-1 items-center justify-between pr-2">
|
||||||
|
<span class="p-dialog-title select-none">{{ item.title }}</span>
|
||||||
|
<div class="p-dialog-header-actions">
|
||||||
|
<Button
|
||||||
|
v-for="action in item.headerButtons"
|
||||||
|
severity="secondary"
|
||||||
|
:text="true"
|
||||||
|
:rounded="true"
|
||||||
|
:icon="action.icon"
|
||||||
|
@click.stop="action.command"
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default>
|
||||||
|
<component :is="item.content" v-bind="item.contentProps"></component>
|
||||||
|
</template>
|
||||||
|
</ResponseDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import ResponseDialog from 'components/ResponseDialog.vue'
|
||||||
|
import { useDialog } from 'hooks/dialog'
|
||||||
|
|
||||||
|
const { stack, rise, close } = useDialog()
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="group/card relative w-full cursor-pointer select-none preview-aspect"
|
class="group/card relative w-full cursor-pointer select-none preview-aspect"
|
||||||
@click.stop.prevent="toggle"
|
@click.stop="openDetailDialog"
|
||||||
>
|
>
|
||||||
<div class="h-full overflow-hidden rounded-lg">
|
<div class="h-full overflow-hidden rounded-lg">
|
||||||
<div class="h-full bg-gray-500 duration-300 group-hover/card:scale-110">
|
<div class="h-full bg-gray-500 duration-300 group-hover/card:scale-110">
|
||||||
@@ -62,20 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogModelDetail
|
|
||||||
v-model:visible="visible"
|
|
||||||
:model="model"
|
|
||||||
></DialogModelDetail>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBoolean } from 'hooks/utils'
|
|
||||||
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { resolveModelType } from 'utils/model'
|
import { genModelKey, resolveModelType } from 'utils/model'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useModelNodeAction } from 'hooks/model'
|
import { useModelNodeAction } from 'hooks/model'
|
||||||
|
import { useDialog } from 'hooks/dialog'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: Model
|
model: Model
|
||||||
@@ -83,7 +78,19 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const [visible, toggle] = useBoolean()
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const openDetailDialog = () => {
|
||||||
|
const basename = props.model.fullname.split('/').pop()!
|
||||||
|
const filename = basename.replace(props.model.extension, '')
|
||||||
|
|
||||||
|
dialog.open({
|
||||||
|
key: genModelKey(props.model),
|
||||||
|
title: filename,
|
||||||
|
content: DialogModelDetail,
|
||||||
|
contentProps: { model: props.model },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const displayType = computed(() => resolveModelType(props.model.type).display)
|
const displayType = computed(() => resolveModelType(props.model.type).display)
|
||||||
const preview = computed(() =>
|
const preview = computed(() =>
|
||||||
@@ -1,100 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="allowResize" data-dialog-resizer>
|
<Dialog
|
||||||
<div
|
ref="dialogRef"
|
||||||
v-if="allow?.x"
|
:visible="true"
|
||||||
data-resize-pos="left"
|
@update:visible="updateVisible"
|
||||||
class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize"
|
:close-on-escape="false"
|
||||||
@mousedown="startResize"
|
:maximizable="!isMobile"
|
||||||
></div>
|
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
|
||||||
<div
|
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
|
||||||
v-if="allow?.x"
|
:pt:mask:class="['group', { open: visible }]"
|
||||||
data-resize-pos="right"
|
pt:root:class="max-h-full group-[:not(.open)]:!hidden"
|
||||||
class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize"
|
pt:content:class="px-0 flex-1"
|
||||||
@mousedown="startResize"
|
:base-z-index="1000"
|
||||||
></div>
|
:auto-z-index="isNil(zIndex)"
|
||||||
<div
|
:pt:mask:style="isNil(zIndex) ? {} : { zIndex: 1000 + zIndex }"
|
||||||
v-if="allow?.y"
|
v-bind="$attrs"
|
||||||
data-resize-pos="top"
|
>
|
||||||
class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize"
|
<template #header>
|
||||||
@mousedown="startResize"
|
<slot name="header"></slot>
|
||||||
></div>
|
</template>
|
||||||
<div
|
|
||||||
v-if="allow?.y"
|
<slot name="default"></slot>
|
||||||
data-resize-pos="bottom"
|
|
||||||
class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize"
|
<div v-if="allowResize" data-dialog-resizer>
|
||||||
@mousedown="startResize"
|
<div
|
||||||
></div>
|
v-if="resizeAllow?.x"
|
||||||
<div
|
data-resize-pos="left"
|
||||||
v-if="allow?.x && allow?.y"
|
class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize"
|
||||||
data-resize-pos="top-left"
|
@mousedown="startResize"
|
||||||
class="absolute -left-1 -top-1 h-2 w-2 cursor-se-resize"
|
></div>
|
||||||
@mousedown="startResize"
|
<div
|
||||||
></div>
|
v-if="resizeAllow?.x"
|
||||||
<div
|
data-resize-pos="right"
|
||||||
v-if="allow?.x && allow?.y"
|
class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize"
|
||||||
data-resize-pos="top-right"
|
@mousedown="startResize"
|
||||||
class="absolute -right-1 -top-1 h-2 w-2 cursor-sw-resize"
|
></div>
|
||||||
@mousedown="startResize"
|
<div
|
||||||
></div>
|
v-if="resizeAllow?.y"
|
||||||
<div
|
data-resize-pos="top"
|
||||||
v-if="allow?.x && allow?.y"
|
class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize"
|
||||||
data-resize-pos="bottom-left"
|
@mousedown="startResize"
|
||||||
class="absolute -bottom-1 -left-1 h-2 w-2 cursor-sw-resize"
|
></div>
|
||||||
@mousedown="startResize"
|
<div
|
||||||
></div>
|
v-if="resizeAllow?.y"
|
||||||
<div
|
data-resize-pos="bottom"
|
||||||
v-if="allow?.x && allow?.y"
|
class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize"
|
||||||
data-resize-pos="bottom-right"
|
@mousedown="startResize"
|
||||||
class="absolute -bottom-1 -right-1 h-2 w-2 cursor-se-resize"
|
></div>
|
||||||
@mousedown="startResize"
|
<div
|
||||||
></div>
|
v-if="resizeAllow?.x && resizeAllow?.y"
|
||||||
</div>
|
data-resize-pos="top-left"
|
||||||
|
class="absolute -left-1 -top-1 h-2 w-2 cursor-se-resize"
|
||||||
|
@mousedown="startResize"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="resizeAllow?.x && resizeAllow?.y"
|
||||||
|
data-resize-pos="top-right"
|
||||||
|
class="absolute -right-1 -top-1 h-2 w-2 cursor-sw-resize"
|
||||||
|
@mousedown="startResize"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="resizeAllow?.x && resizeAllow?.y"
|
||||||
|
data-resize-pos="bottom-left"
|
||||||
|
class="absolute -bottom-1 -left-1 h-2 w-2 cursor-sw-resize"
|
||||||
|
@mousedown="startResize"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="resizeAllow?.x && resizeAllow?.y"
|
||||||
|
data-resize-pos="bottom-right"
|
||||||
|
class="absolute -bottom-1 -right-1 h-2 w-2 cursor-se-resize"
|
||||||
|
@mousedown="startResize"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { clamp } from 'lodash'
|
import Dialog from 'primevue/dialog'
|
||||||
|
import { clamp, isNil } from 'lodash'
|
||||||
import { useConfig } from 'hooks/config'
|
import { useConfig } from 'hooks/config'
|
||||||
import {
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
computed,
|
|
||||||
getCurrentInstance,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onMounted,
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
} from 'vue'
|
|
||||||
|
|
||||||
type ContainerSize = { width: number; height: number }
|
interface Props {
|
||||||
type ContainerPosition = { left: number; top: number }
|
keepAlive?: boolean
|
||||||
|
|
||||||
interface ResizableProps {
|
|
||||||
defaultSize?: Partial<ContainerSize>
|
defaultSize?: Partial<ContainerSize>
|
||||||
defaultMobileSize?: Partial<ContainerSize>
|
defaultMobileSize?: Partial<ContainerSize>
|
||||||
allow?: { x?: boolean; y?: boolean }
|
resizeAllow?: { x?: boolean; y?: boolean }
|
||||||
minWidth?: number
|
minWidth?: number
|
||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
minHeight?: number
|
minHeight?: number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
|
zIndex?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ResizableProps>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
allow: () => ({ x: true, y: true }),
|
resizeAllow: () => ({ x: true, y: true }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const config = useConfig()
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible')
|
||||||
|
|
||||||
|
const emit = defineEmits(['hide'])
|
||||||
|
|
||||||
|
const updateVisible = (val: boolean) => {
|
||||||
|
visible.value = val
|
||||||
|
emit('hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isMobile } = useConfig()
|
||||||
|
|
||||||
|
const dialogRef = ref()
|
||||||
|
|
||||||
const allowResize = computed(() => {
|
const allowResize = computed(() => {
|
||||||
return !config.isMobile.value
|
return !isMobile.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const instance = getCurrentInstance()
|
|
||||||
|
|
||||||
const resizeDirection = ref<string[]>([])
|
const resizeDirection = ref<string[]>([])
|
||||||
|
|
||||||
const getContainer = () => {
|
const getContainer = () => {
|
||||||
return instance!.parent!.vnode.el as HTMLDivElement
|
return dialogRef.value.container
|
||||||
}
|
}
|
||||||
|
|
||||||
const minWidth = computed(() => {
|
const minWidth = computed(() => {
|
||||||
const defaultMinWidth = 100
|
const defaultMinWidth = 390
|
||||||
return props.minWidth ?? defaultMinWidth
|
return props.minWidth ?? defaultMinWidth
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,7 +134,7 @@ const maxWidth = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const minHeight = computed(() => {
|
const minHeight = computed(() => {
|
||||||
const defaultMinHeight = 100
|
const defaultMinHeight = 390
|
||||||
return props.minHeight ?? defaultMinHeight
|
return props.minHeight ?? defaultMinHeight
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -265,18 +295,19 @@ const startResize = (event: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (allowResize.value) {
|
nextTick(() => {
|
||||||
updateContainerSize(containerSize.value)
|
if (allowResize.value) {
|
||||||
} else {
|
updateContainerSize(containerSize.value)
|
||||||
updateContainerSize({
|
} else {
|
||||||
width: props.defaultMobileSize?.width ?? window.innerWidth,
|
updateContainerSize({
|
||||||
height: props.defaultMobileSize?.height ?? window.innerHeight,
|
width: props.defaultMobileSize?.width ?? window.innerWidth,
|
||||||
})
|
height: props.defaultMobileSize?.height ?? window.innerHeight,
|
||||||
}
|
})
|
||||||
|
}
|
||||||
recordContainerPosition()
|
recordContainerPosition()
|
||||||
updateContainerPosition(containerPosition.value)
|
updateContainerPosition(containerPosition.value)
|
||||||
getContainer().style.position = 'fixed'
|
getContainer().style.position = 'fixed'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -23,7 +23,7 @@ export const useConfig = defineStore('config', () => {
|
|||||||
window.removeEventListener('resize', checkDeviceType)
|
window.removeEventListener('resize', checkDeviceType)
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshSetting = async () => {
|
const refresh = async () => {
|
||||||
return Promise.all([refreshModelFolders()])
|
return Promise.all([refreshModelFolders()])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export const useConfig = defineStore('config', () => {
|
|||||||
cardWidth: 240,
|
cardWidth: 240,
|
||||||
aspect: 7 / 9,
|
aspect: 7 / 9,
|
||||||
modelFolders,
|
modelFolders,
|
||||||
refreshSetting,
|
refresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
useAddConfigSettings(config)
|
useAddConfigSettings(config)
|
||||||
|
|||||||
66
src/hooks/dialog.ts
Normal file
66
src/hooks/dialog.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineStore } from 'hooks/store'
|
||||||
|
import { Component, markRaw, ref } from 'vue'
|
||||||
|
|
||||||
|
interface HeaderButton {
|
||||||
|
icon: string
|
||||||
|
command: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogItem {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
content: Component
|
||||||
|
contentProps?: Record<string, any>
|
||||||
|
keepAlive?: boolean
|
||||||
|
headerButtons?: HeaderButton[]
|
||||||
|
defaultSize?: Partial<ContainerSize>
|
||||||
|
defaultMobileSize?: Partial<ContainerSize>
|
||||||
|
resizeAllow?: { x?: boolean; y?: boolean }
|
||||||
|
minWidth?: number
|
||||||
|
maxWidth?: number
|
||||||
|
minHeight?: number
|
||||||
|
maxHeight?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDialog = defineStore('dialog', () => {
|
||||||
|
const stack = ref<(DialogItem & { visible?: boolean })[]>([])
|
||||||
|
|
||||||
|
const rise = (dialog: { key: string }) => {
|
||||||
|
const index = stack.value.findIndex((item) => item.key === dialog.key)
|
||||||
|
if (index !== -1) {
|
||||||
|
const item = stack.value.splice(index, 1)
|
||||||
|
stack.value.push(...item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (dialog: DialogItem) => {
|
||||||
|
const item = stack.value.find((item) => item.key === dialog.key)
|
||||||
|
if (item) {
|
||||||
|
item.visible = true
|
||||||
|
rise(dialog)
|
||||||
|
} else {
|
||||||
|
stack.value.push({
|
||||||
|
...dialog,
|
||||||
|
content: markRaw(dialog.content),
|
||||||
|
visible: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = (dialog: { key: string }) => {
|
||||||
|
const item = stack.value.find((item) => item.key === dialog.key)
|
||||||
|
if (item?.keepAlive) {
|
||||||
|
item.visible = false
|
||||||
|
} else {
|
||||||
|
stack.value = stack.value.filter((item) => item.key !== dialog.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stack, open, close, rise }
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module 'hooks/store' {
|
||||||
|
interface StoreProvider {
|
||||||
|
dialog: ReturnType<typeof useDialog>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,11 @@ import { MarkdownTool, useMarkdown } from 'hooks/markdown'
|
|||||||
import { socket } from 'hooks/socket'
|
import { socket } from 'hooks/socket'
|
||||||
import { defineStore } from 'hooks/store'
|
import { defineStore } from 'hooks/store'
|
||||||
import { useToast } from 'hooks/toast'
|
import { useToast } from 'hooks/toast'
|
||||||
import { useBoolean } from 'hooks/utils'
|
|
||||||
import { bytesToSize } from 'utils/common'
|
import { bytesToSize } from 'utils/common'
|
||||||
import { onBeforeMount, onMounted, ref } from 'vue'
|
import { onBeforeMount, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
export const useDownload = defineStore('download', (store) => {
|
export const useDownload = defineStore('download', (store) => {
|
||||||
const [visible, toggle] = useBoolean()
|
|
||||||
const { toast, confirm } = useToast()
|
const { toast, confirm } = useToast()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ export const useDownload = defineStore('download', (store) => {
|
|||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
return { visible, toggle, data: taskList, refresh }
|
return { data: taskList, refresh }
|
||||||
})
|
})
|
||||||
|
|
||||||
declare module 'hooks/store' {
|
declare module 'hooks/store' {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'hooks/store'
|
import { defineStore } from 'hooks/store'
|
||||||
import { useBoolean } from 'hooks/utils'
|
|
||||||
import { Ref, ref } from 'vue'
|
import { Ref, ref } from 'vue'
|
||||||
|
|
||||||
class GlobalLoading {
|
class GlobalLoading {
|
||||||
@@ -25,7 +24,7 @@ class GlobalLoading {
|
|||||||
export const globalLoading = new GlobalLoading()
|
export const globalLoading = new GlobalLoading()
|
||||||
|
|
||||||
export const useGlobalLoading = defineStore('loading', () => {
|
export const useGlobalLoading = defineStore('loading', () => {
|
||||||
const [loading] = useBoolean()
|
const loading = ref(false)
|
||||||
|
|
||||||
globalLoading.bind(loading)
|
globalLoading.bind(loading)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { defineStore } from 'hooks/store'
|
|
||||||
import { useBoolean } from 'hooks/utils'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
export const useDialogManager = defineStore('dialogManager', () => {
|
|
||||||
const [visible, toggle] = useBoolean()
|
|
||||||
|
|
||||||
const mounted = ref(false)
|
|
||||||
const open = ref(false)
|
|
||||||
|
|
||||||
watch(visible, (visible) => {
|
|
||||||
open.value = visible
|
|
||||||
mounted.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateVisible = (val: boolean) => {
|
|
||||||
visible.value = val
|
|
||||||
}
|
|
||||||
|
|
||||||
return { visible: mounted, open, updateVisible, toggle }
|
|
||||||
})
|
|
||||||
|
|
||||||
declare module 'hooks/store' {
|
|
||||||
interface StoreProvider {
|
|
||||||
dialogManager: ReturnType<typeof useDialogManager>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
export const useBoolean = (defaultValue?: boolean) => {
|
|
||||||
const target = ref(defaultValue ?? false)
|
|
||||||
|
|
||||||
const toggle = (value?: any) => {
|
|
||||||
target.value = typeof value === 'boolean' ? value : !target.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return [target, toggle] as const
|
|
||||||
}
|
|
||||||
3
src/types/typings.d.ts
vendored
3
src/types/typings.d.ts
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
type ContainerSize = { width: number; height: number }
|
||||||
|
type ContainerPosition = { left: number; top: number }
|
||||||
|
|
||||||
interface BaseModel {
|
interface BaseModel {
|
||||||
id: number | string
|
id: number | string
|
||||||
fullname: string
|
fullname: string
|
||||||
|
|||||||
@@ -43,3 +43,7 @@ export const resolveModelType = (type: string) => {
|
|||||||
loader: loader[type],
|
loader: loader[type],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const genModelKey = (model: BaseModel) => {
|
||||||
|
return `${model.type}:${model.pathIndex}:${model.fullname}`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user