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,16 +1,4 @@
<template> <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"> <div class="flex h-full flex-col gap-4 px-5">
<ResponseInput <ResponseInput
v-model="modelUrl" v-model="modelUrl"
@@ -66,32 +54,27 @@
</div> </div>
</ResponseScroll> </ResponseScroll>
</div> </div>
<DialogResizer :min-width="390"></DialogResizer>
</Dialog>
</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,35 +1,11 @@
<template> <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>
</template>
<div class="flex h-full flex-col gap-4"> <div class="flex h-full flex-col gap-4">
<div class="whitespace-nowrap px-4 @container"> <div class="whitespace-nowrap px-4 @container">
<div class="flex gap-4 @sm:justify-end"> <div class="flex gap-4 @sm:justify-end">
<Button <Button
class="w-full @sm:w-auto" class="w-full @sm:w-auto"
:label="$t('createDownloadTask')" :label="$t('createDownloadTask')"
@click="toggleCreateTask" @click="openCreateTask"
></Button> ></Button>
</div> </div>
</div> </div>
@@ -90,78 +66,28 @@
</li> </li>
</ul> </ul>
</div> </div>
<!-- <ul class="m-0 flex list-none flex-col gap-4 p-0 px-4 pb-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"
>
<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
v-show="item.status === 'doing'"
class="pi pi-pause-circle"
@click="item.pauseTask"
></i>
<i
v-show="item.status === 'pause'"
class="pi pi-play-circle"
@click="item.resumeTask"
></i>
<i
class="pi pi-trash text-red-400"
@click="item.deleteTask"
></i>
</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>
</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> </ResponseScroll>
</div> </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,36 +1,4 @@
<template> <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"
>
<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="flex h-full flex-col gap-4 overflow-hidden @container/content"
:style="{ :style="{
@@ -56,9 +24,7 @@
suffix-icon="pi pi-search" suffix-icon="pi pi-search"
></ResponseInput> ></ResponseInput>
<div <div class="flex items-center justify-between gap-4 overflow-hidden">
class="flex items-center justify-between gap-4 overflow-hidden"
>
<ResponseSelect <ResponseSelect
v-model="currentType" v-model="currentType"
:items="typeOptions" :items="typeOptions"
@@ -88,11 +54,11 @@
'@lg/content:px-4', '@lg/content:px-4',
]" ]"
> >
<DialogModelCard <ModelCard
v-for="model in item" v-for="model in item"
:key="genModelKey(model)" :key="genModelKey(model)"
:model="model" :model="model"
></DialogModelCard> ></ModelCard>
<div class="col-span-full"></div> <div class="col-span-full"></div>
</div> </div>
</template> </template>
@@ -105,39 +71,24 @@
</template> </template>
</ResponseScroll> </ResponseScroll>
</div> </div>
<DialogResizer
:min-width="cardWidth * 2 + gutter + 42"
:min-height="(cardWidth / aspect) * 0.5 + 162"
></DialogResizer>
</Dialog>
</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,15 +1,4 @@
<template> <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"> <ResponseScroll class="h-full">
<div class="px-8"> <div class="px-8">
<ModelContent <ModelContent
@@ -50,55 +39,34 @@
</ModelContent> </ModelContent>
</div> </div>
</ResponseScroll> </ResponseScroll>
<DialogResizer :min-width="390"></DialogResizer>
</Dialog>
</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>
<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="allowResize" data-dialog-resizer>
<div <div
v-if="allow?.x" v-if="resizeAllow?.x"
data-resize-pos="left" data-resize-pos="left"
class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize" class="absolute -left-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.x" v-if="resizeAllow?.x"
data-resize-pos="right" data-resize-pos="right"
class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize" class="absolute -right-1 top-0 h-full w-2 cursor-ew-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.y" v-if="resizeAllow?.y"
data-resize-pos="top" data-resize-pos="top"
class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize" class="absolute -top-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.y" v-if="resizeAllow?.y"
data-resize-pos="bottom" data-resize-pos="bottom"
class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize" class="absolute -bottom-1 left-0 h-2 w-full cursor-ns-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.x && allow?.y" v-if="resizeAllow?.x && resizeAllow?.y"
data-resize-pos="top-left" data-resize-pos="top-left"
class="absolute -left-1 -top-1 h-2 w-2 cursor-se-resize" class="absolute -left-1 -top-1 h-2 w-2 cursor-se-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.x && allow?.y" v-if="resizeAllow?.x && resizeAllow?.y"
data-resize-pos="top-right" data-resize-pos="top-right"
class="absolute -right-1 -top-1 h-2 w-2 cursor-sw-resize" class="absolute -right-1 -top-1 h-2 w-2 cursor-sw-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.x && allow?.y" v-if="resizeAllow?.x && resizeAllow?.y"
data-resize-pos="bottom-left" data-resize-pos="bottom-left"
class="absolute -bottom-1 -left-1 h-2 w-2 cursor-sw-resize" class="absolute -bottom-1 -left-1 h-2 w-2 cursor-sw-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
<div <div
v-if="allow?.x && allow?.y" v-if="resizeAllow?.x && resizeAllow?.y"
data-resize-pos="bottom-right" data-resize-pos="bottom-right"
class="absolute -bottom-1 -right-1 h-2 w-2 cursor-se-resize" class="absolute -bottom-1 -right-1 h-2 w-2 cursor-se-resize"
@mousedown="startResize" @mousedown="startResize"
></div> ></div>
</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,6 +295,7 @@ const startResize = (event: MouseEvent) => {
} }
onMounted(() => { onMounted(() => {
nextTick(() => {
if (allowResize.value) { if (allowResize.value) {
updateContainerSize(containerSize.value) updateContainerSize(containerSize.value)
} else { } else {
@@ -273,11 +304,11 @@ onMounted(() => {
height: props.defaultMobileSize?.height ?? window.innerHeight, height: props.defaultMobileSize?.height ?? window.innerHeight,
}) })
} }
recordContainerPosition() recordContainerPosition()
updateContainerPosition(containerPosition.value) updateContainerPosition(containerPosition.value)
getContainer().style.position = 'fixed' getContainer().style.position = 'fixed'
}) })
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopResize() stopResize()

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