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>
<DialogManager></DialogManager>
<DialogDownload></DialogDownload>
<GlobalToast></GlobalToast>
<ConfirmDialog></ConfirmDialog>
<GlobalConfirm></GlobalConfirm>
<GlobalLoading></GlobalLoading>
<GlobalDialogStack></GlobalDialogStack>
</template>
<script setup lang="ts">
import GlobalToast from 'components/GlobalToast.vue'
import DialogManager from 'components/DialogManager.vue'
import DialogDownload from 'components/DialogDownload.vue'
import GlobalToast from 'components/GlobalToast.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 { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStoreProvider } from 'hooks/store'
import { useToast } from 'hooks/toast'
const { t } = useI18n()
const { dialogManager } = useStoreProvider()
const { dialog, models, config, download } = useStoreProvider()
const { toast } = useToast()
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(
$el('button', {
id: 'comfyui-model-manager-button',
textContent: t('modelManager'),
onclick: () => dialogManager.toggle(),
onclick: openManagerDialog,
}),
)
@@ -34,7 +82,7 @@ onMounted(() => {
icon: 'folder-search',
tooltip: t('openModelManager'),
content: t('modelManager'),
action: () => dialogManager.toggle(),
action: openManagerDialog,
}),
)
})

View File

@@ -1,97 +1,80 @@
<template>
<Dialog
v-model:visible="visible"
:header="$t('parseModelUrl')"
:modal="true"
:maximizable="!isMobile"
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
pt:mask:style="--p-mask-background: rgba(0, 0, 0, 0.3)"
pt:root:class="max-h-full"
pt:content:class="px-0"
@after-hide="clearContent"
>
<div class="flex h-full flex-col gap-4 px-5">
<ResponseInput
v-model="modelUrl"
:allow-clear="true"
:placeholder="$t('pleaseInputModelUrl')"
@keypress.enter="searchModelsByUrl"
<div class="flex h-full flex-col gap-4 px-5">
<ResponseInput
v-model="modelUrl"
:allow-clear="true"
:placeholder="$t('pleaseInputModelUrl')"
@keypress.enter="searchModelsByUrl"
>
<template #suffix>
<span
class="pi pi-search pi-inputicon"
@click="searchModelsByUrl"
></span>
</template>
</ResponseInput>
<div v-show="data.length > 0">
<ResponseSelect
v-model="current"
:items="data"
:type="isMobile ? 'drop' : 'button'"
>
<template #suffix>
<span
class="pi pi-search pi-inputicon"
@click="searchModelsByUrl"
></span>
<template #prefix>
<span>version:</span>
</template>
</ResponseInput>
<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>
</ResponseSelect>
</div>
<DialogResizer :min-width="390"></DialogResizer>
</Dialog>
<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>
</template>
<script setup lang="ts">
import DialogResizer from 'components/DialogResizer.vue'
import ModelContent from 'components/ModelContent.vue'
import ResponseInput from 'components/ResponseInput.vue'
import ResponseSelect from 'components/ResponseSelect.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import ModelContent from 'components/ModelContent.vue'
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 { ref } from 'vue'
import { previewUrlToFile } from 'utils/common'
import { useLoading } from 'hooks/loading'
import { request } from 'hooks/request'
import { useToast } from 'hooks/toast'
import { useConfig } from 'hooks/config'
const visible = defineModel<boolean>('visible')
import { previewUrlToFile } from 'utils/common'
import { ref } from 'vue'
const { isMobile } = useConfig()
const { toast } = useToast()
const loading = useLoading()
const dialog = useDialog()
const modelUrl = ref<string>()
@@ -103,11 +86,6 @@ const searchModelsByUrl = async () => {
}
}
const clearContent = () => {
modelUrl.value = undefined
data.value = []
}
const createDownTask = async (data: VersionModel) => {
const formData = new FormData()
@@ -143,7 +121,7 @@ const createDownTask = async (data: VersionModel) => {
body: formData,
})
.then(() => {
visible.value = false
dialog.close({ key: 'model-manager-create-task' })
})
.catch((e) => {
toast.add({

View File

@@ -1,167 +1,93 @@
<template>
<Dialog
v-model:visible="visible"
:modal="true"
pt:mask:style="--p-mask-background: rgba(0, 0, 0, 0.3)"
pt:root:class="max-h-full"
pt:content:class="px-0"
>
<template #header>
<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 class="flex h-full flex-col gap-4">
<div class="whitespace-nowrap px-4 @container">
<div class="flex gap-4 @sm:justify-end">
<Button
class="w-full @sm:w-auto"
:label="$t('createDownloadTask')"
@click="openCreateTask"
></Button>
</div>
</template>
</div>
<div class="flex h-full flex-col gap-4">
<div class="whitespace-nowrap px-4 @container">
<div class="flex gap-4 @sm:justify-end">
<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">
<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="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">
<img
:src="`/model-manager/preview/download/${item.preview}`"
alt=""
/>
</div>
<div class="flex flex-1 flex-col gap-3">
<div class="flex items-center justify-between gap-4">
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
{{ item.fullname }}
</div>
<div class="flex items-center gap-4">
<i v-show="item.status === 'waiting'">
{{ $t('waiting') }}...
</i>
<i
<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="pi pi-pause-circle"
class="h-4 cursor-pointer"
@click="item.pauseTask"
></i>
<i
>
<i class="pi pi-pause-circle"></i>
</span>
<span
v-show="item.status === 'pause'"
class="pi pi-play-circle"
class="h-4 cursor-pointer"
@click="item.resumeTask"
></i>
<i
class="pi pi-trash text-red-400"
@click="item.deleteTask"
></i>
>
<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>
<div class="flex items-center gap-2">
<div class="h-2 flex-1 overflow-hidden rounded bg-gray-200">
<div class="h-full *:h-full *:bg-blue-500 *:transition-all">
<div :style="{ width: `${item.progress}%` }"></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 class="flex items-center justify-between">
<div>{{ item.downloadProgress }}</div>
<div v-show="item.status === 'doing'">
{{ item.downloadSpeed }}
</div>
</div>
</div>
</li>
</ul> -->
</ResponseScroll>
</div>
<DialogResizer :min-width="390" :min-height="390"></DialogResizer>
</Dialog>
<DialogCreateTask v-model:visible="openCreateTask"></DialogCreateTask>
</ul>
</div>
</ResponseScroll>
</div>
</template>
<script setup lang="ts">
import DialogCreateTask from 'components/DialogCreateTask.vue'
import DialogResizer from 'components/DialogResizer.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
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>

View File

@@ -1,143 +1,94 @@
<template>
<Dialog
:visible="visible"
@update:visible="updateVisible"
:maximizable="!isMobile"
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
:pt:mask:class="['group', { open }]"
pt:root:class="max-h-full group-[:not(.open)]:!hidden"
pt:content:class="px-0 flex-1"
<div
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
:style="{
['--card-width']: `${cardWidth}px`,
['--gutter']: `${gutter}px`,
}"
v-resize="onContainerResize"
>
<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
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
:style="{
['--card-width']: `${cardWidth}px`,
['--gutter']: `${gutter}px`,
}"
v-resize="onContainerResize"
:class="[
'grid grid-cols-1 justify-center gap-4 px-8',
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
'@lg/content:gap-[var(--gutter)]',
'@lg/content:px-4',
]"
>
<div
:class="[
'grid grid-cols-1 justify-center gap-4 px-8',
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
'@lg/content:gap-[var(--gutter)]',
'@lg/content:px-4',
]"
>
<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 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
class="flex items-center justify-between gap-4 overflow-hidden"
>
<ResponseSelect
v-model="currentType"
:items="typeOptions"
:type="isMobile ? 'drop' : 'button'"
></ResponseSelect>
<ResponseSelect
v-model="sortOrder"
:items="sortOrderOptions"
></ResponseSelect>
</div>
<div class="flex items-center justify-between gap-4 overflow-hidden">
<ResponseSelect
v-model="currentType"
:items="typeOptions"
:type="isMobile ? 'drop' : 'button'"
></ResponseSelect>
<ResponseSelect
v-model="sortOrder"
:items="sortOrderOptions"
></ResponseSelect>
</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>
<DialogResizer
:min-width="cardWidth * 2 + gutter + 42"
:min-height="(cardWidth / aspect) * 0.5 + 162"
></DialogResizer>
</Dialog>
<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',
]"
>
<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>
<script setup lang="ts" name="manager-dialog">
import { useConfig } from 'hooks/config'
import { useDialogManager } from 'hooks/manager'
import { useModels } from 'hooks/model'
import DialogResizer from 'components/DialogResizer.vue'
import DialogModelCard from 'components/DialogModelCard.vue'
import ModelCard from 'components/ModelCard.vue'
import ResponseInput from 'components/ResponseInput.vue'
import ResponseSelect from 'components/ResponseSelect.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import Dialog from 'primevue/dialog'
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import { useToast } from 'hooks/toast'
import { useDownload } from 'hooks/download'
import { useI18n } from 'vue-i18n'
import { chunk } from 'lodash'
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 { visible, updateVisible, open } = useDialogManager()
const { data, refresh } = useModels()
const { toast } = useToast()
const { data } = useModels()
const { t } = useI18n()
const searchContent = ref<string>()
@@ -222,15 +173,6 @@ const list = computed(() => {
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 entry = entries[0]
if (isMobile.value) {
@@ -241,8 +183,4 @@ const onContainerResize = defineResizeCallback((entries) => {
colSpanWidth.value = colSpan.value * (cardWidth + gutter) - gutter
}
})
const genModelKey = (model: BaseModel) => {
return `${model.type}:${model.pathIndex}:${model.fullname}`
}
</script>

View File

@@ -1,104 +1,72 @@
<template>
<Dialog
v-model:visible="visible"
:header="filename"
:maximizable="!isMobile"
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
pt:title:class="whitespace-nowrap text-ellipsis overflow-hidden"
pt:root:class="max-h-full"
pt:content:class="px-0"
@after-hide="handleCancel"
>
<ResponseScroll class="h-full">
<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>
<ResponseScroll class="h-full">
<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>
</ModelContent>
</div>
</ResponseScroll>
<DialogResizer :min-width="390"></DialogResizer>
</Dialog>
<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>
</ModelContent>
</div>
</ResponseScroll>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import ModelContent from 'components/ModelContent.vue'
import DialogResizer from 'components/DialogResizer.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import { useConfig } from 'hooks/config'
import { computed, ref, watchEffect } from 'vue'
import Button from 'primevue/button'
import { useModelNodeAction, useModels } from 'hooks/model'
import { useRequest } from 'hooks/request'
import { computed, ref } from 'vue'
const visible = defineModel<boolean>('visible')
interface Props {
model: Model
}
const props = defineProps<Props>()
const { isMobile } = useConfig()
const { remove, update } = useModels()
const editable = ref(false)
const { data: extraInfo, refresh: fetchExtraInfo } = useRequest(
`/model/${props.model.type}/${props.model.pathIndex}/${props.model.fullname}`,
{
method: 'GET',
manual: true,
},
)
const modelDetailUrl = `/model/${props.model.type}/${props.model.pathIndex}/${props.model.fullname}`
const { data: extraInfo } = useRequest(modelDetailUrl, {
method: 'GET',
})
const modelContent = computed(() => {
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 = () => {
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>
<div
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 bg-gray-500 duration-300 group-hover/card:scale-110">
@@ -62,20 +62,15 @@
</div>
</div>
</div>
<DialogModelDetail
v-model:visible="visible"
:model="model"
></DialogModelDetail>
</template>
<script setup lang="ts">
import { useBoolean } from 'hooks/utils'
import DialogModelDetail from 'components/DialogModelDetail.vue'
import Button from 'primevue/button'
import { resolveModelType } from 'utils/model'
import { genModelKey, resolveModelType } from 'utils/model'
import { computed } from 'vue'
import { useModelNodeAction } from 'hooks/model'
import { useDialog } from 'hooks/dialog'
interface Props {
model: Model
@@ -83,7 +78,19 @@ interface 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 preview = computed(() =>

View File

@@ -1,100 +1,130 @@
<template>
<div v-if="allowResize" data-dialog-resizer>
<div
v-if="allow?.x"
data-resize-pos="left"
class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize"
></div>
<div
v-if="allow?.x"
data-resize-pos="right"
class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize"
></div>
<div
v-if="allow?.y"
data-resize-pos="top"
class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize"
></div>
<div
v-if="allow?.y"
data-resize-pos="bottom"
class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize"
></div>
<div
v-if="allow?.x && allow?.y"
data-resize-pos="top-left"
class="absolute -left-1 -top-1 h-2 w-2 cursor-se-resize"
@mousedown="startResize"
></div>
<div
v-if="allow?.x && allow?.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="allow?.x && allow?.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="allow?.x && allow?.y"
data-resize-pos="bottom-right"
class="absolute -bottom-1 -right-1 h-2 w-2 cursor-se-resize"
@mousedown="startResize"
></div>
</div>
<Dialog
ref="dialogRef"
:visible="true"
@update:visible="updateVisible"
:close-on-escape="false"
:maximizable="!isMobile"
maximizeIcon="pi pi-arrow-up-right-and-arrow-down-left-from-center"
minimizeIcon="pi pi-arrow-down-left-and-arrow-up-right-to-center"
:pt:mask:class="['group', { open: visible }]"
pt:root:class="max-h-full group-[:not(.open)]:!hidden"
pt:content:class="px-0 flex-1"
:base-z-index="1000"
:auto-z-index="isNil(zIndex)"
:pt:mask:style="isNil(zIndex) ? {} : { zIndex: 1000 + zIndex }"
v-bind="$attrs"
>
<template #header>
<slot name="header"></slot>
</template>
<slot name="default"></slot>
<div v-if="allowResize" data-dialog-resizer>
<div
v-if="resizeAllow?.x"
data-resize-pos="left"
class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize"
></div>
<div
v-if="resizeAllow?.x"
data-resize-pos="right"
class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize"
></div>
<div
v-if="resizeAllow?.y"
data-resize-pos="top"
class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize"
></div>
<div
v-if="resizeAllow?.y"
data-resize-pos="bottom"
class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize"
></div>
<div
v-if="resizeAllow?.x && resizeAllow?.y"
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>
<script setup lang="ts">
import { clamp } from 'lodash'
import Dialog from 'primevue/dialog'
import { clamp, isNil } from 'lodash'
import { useConfig } from 'hooks/config'
import {
computed,
getCurrentInstance,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
type ContainerSize = { width: number; height: number }
type ContainerPosition = { left: number; top: number }
interface ResizableProps {
interface Props {
keepAlive?: boolean
defaultSize?: Partial<ContainerSize>
defaultMobileSize?: Partial<ContainerSize>
allow?: { x?: boolean; y?: boolean }
resizeAllow?: { x?: boolean; y?: boolean }
minWidth?: number
maxWidth?: number
minHeight?: number
maxHeight?: number
zIndex?: number
}
const props = withDefaults(defineProps<ResizableProps>(), {
allow: () => ({ x: true, y: true }),
const props = withDefaults(defineProps<Props>(), {
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(() => {
return !config.isMobile.value
return !isMobile.value
})
const instance = getCurrentInstance()
const resizeDirection = ref<string[]>([])
const getContainer = () => {
return instance!.parent!.vnode.el as HTMLDivElement
return dialogRef.value.container
}
const minWidth = computed(() => {
const defaultMinWidth = 100
const defaultMinWidth = 390
return props.minWidth ?? defaultMinWidth
})
@@ -104,7 +134,7 @@ const maxWidth = computed(() => {
})
const minHeight = computed(() => {
const defaultMinHeight = 100
const defaultMinHeight = 390
return props.minHeight ?? defaultMinHeight
})
@@ -265,18 +295,19 @@ const startResize = (event: MouseEvent) => {
}
onMounted(() => {
if (allowResize.value) {
updateContainerSize(containerSize.value)
} else {
updateContainerSize({
width: props.defaultMobileSize?.width ?? window.innerWidth,
height: props.defaultMobileSize?.height ?? window.innerHeight,
})
}
recordContainerPosition()
updateContainerPosition(containerPosition.value)
getContainer().style.position = 'fixed'
nextTick(() => {
if (allowResize.value) {
updateContainerSize(containerSize.value)
} else {
updateContainerSize({
width: props.defaultMobileSize?.width ?? window.innerWidth,
height: props.defaultMobileSize?.height ?? window.innerHeight,
})
}
recordContainerPosition()
updateContainerPosition(containerPosition.value)
getContainer().style.position = 'fixed'
})
})
onBeforeUnmount(() => {

View File

@@ -23,7 +23,7 @@ export const useConfig = defineStore('config', () => {
window.removeEventListener('resize', checkDeviceType)
})
const refreshSetting = async () => {
const refresh = async () => {
return Promise.all([refreshModelFolders()])
}
@@ -33,7 +33,7 @@ export const useConfig = defineStore('config', () => {
cardWidth: 240,
aspect: 7 / 9,
modelFolders,
refreshSetting,
refresh,
}
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 { defineStore } from 'hooks/store'
import { useToast } from 'hooks/toast'
import { useBoolean } from 'hooks/utils'
import { bytesToSize } from 'utils/common'
import { onBeforeMount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export const useDownload = defineStore('download', (store) => {
const [visible, toggle] = useBoolean()
const { toast, confirm } = useToast()
const { t } = useI18n()
@@ -118,7 +116,7 @@ export const useDownload = defineStore('download', (store) => {
refresh()
})
return { visible, toggle, data: taskList, refresh }
return { data: taskList, refresh }
})
declare module 'hooks/store' {

View File

@@ -1,5 +1,4 @@
import { defineStore } from 'hooks/store'
import { useBoolean } from 'hooks/utils'
import { Ref, ref } from 'vue'
class GlobalLoading {
@@ -25,7 +24,7 @@ class GlobalLoading {
export const globalLoading = new GlobalLoading()
export const useGlobalLoading = defineStore('loading', () => {
const [loading] = useBoolean()
const loading = ref(false)
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 {
id: number | string
fullname: string

View File

@@ -43,3 +43,7 @@ export const resolveModelType = (type: string) => {
loader: loader[type],
}
}
export const genModelKey = (model: BaseModel) => {
return `${model.type}:${model.pathIndex}:${model.fullname}`
}