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:
hayden
2024-10-29 15:32:30 +08:00
parent 14a31a8ca8
commit 6934fbb331
17 changed files with 547 additions and 590 deletions

View File

@@ -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,
}), }),
) )
}) })

View File

@@ -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({

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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>

View 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>

View File

@@ -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(() =>

View File

@@ -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(() => {

View File

@@ -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
View 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>
}
}

View File

@@ -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' {

View File

@@ -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)

View File

@@ -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>
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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}`
}