feat: Optimize dialog
- Change the method of open dialog - Fix the problem of open dialog disappearing due to virtual scrolling - Float the active dialog to the top
This commit is contained in:
64
src/App.vue
64
src/App.vue
@@ -1,31 +1,79 @@
|
||||
<template>
|
||||
<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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<form @submit="handleSubmit" @reset="handleReset">
|
||||
<slot name="default"></slot>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emits = defineEmits(['submit', 'reset'])
|
||||
|
||||
const handleReset = () => {
|
||||
emits('reset')
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
emits('submit')
|
||||
}
|
||||
</script>
|
||||
46
src/components/GlobalDialogStack.vue
Normal file
46
src/components/GlobalDialogStack.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<ResponseDialog
|
||||
v-for="(item, index) in stack"
|
||||
v-model:visible="item.visible"
|
||||
:key="item.key"
|
||||
:keep-alive="item.keepAlive"
|
||||
:default-size="item.defaultSize"
|
||||
:default-mobile-size="item.defaultMobileSize"
|
||||
:resize-allow="item.resizeAllow"
|
||||
:min-width="item.minWidth"
|
||||
:max-width="item.maxWidth"
|
||||
:min-height="item.minHeight"
|
||||
:max-height="item.maxHeight"
|
||||
:z-index="index"
|
||||
:pt:root:onMousedown="() => rise(item)"
|
||||
@hide="() => close(item)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-1 items-center justify-between pr-2">
|
||||
<span class="p-dialog-title select-none">{{ item.title }}</span>
|
||||
<div class="p-dialog-header-actions">
|
||||
<Button
|
||||
v-for="action in item.headerButtons"
|
||||
severity="secondary"
|
||||
:text="true"
|
||||
:rounded="true"
|
||||
:icon="action.icon"
|
||||
@click.stop="action.command"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<component :is="item.content" v-bind="item.contentProps"></component>
|
||||
</template>
|
||||
</ResponseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ResponseDialog from 'components/ResponseDialog.vue'
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
|
||||
const { stack, rise, close } = useDialog()
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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(() =>
|
||||
@@ -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(() => {
|
||||
@@ -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
66
src/hooks/dialog.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { Component, markRaw, ref } from 'vue'
|
||||
|
||||
interface HeaderButton {
|
||||
icon: string
|
||||
command: () => void
|
||||
}
|
||||
|
||||
interface DialogItem {
|
||||
key: string
|
||||
title: string
|
||||
content: Component
|
||||
contentProps?: Record<string, any>
|
||||
keepAlive?: boolean
|
||||
headerButtons?: HeaderButton[]
|
||||
defaultSize?: Partial<ContainerSize>
|
||||
defaultMobileSize?: Partial<ContainerSize>
|
||||
resizeAllow?: { x?: boolean; y?: boolean }
|
||||
minWidth?: number
|
||||
maxWidth?: number
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
}
|
||||
|
||||
export const useDialog = defineStore('dialog', () => {
|
||||
const stack = ref<(DialogItem & { visible?: boolean })[]>([])
|
||||
|
||||
const rise = (dialog: { key: string }) => {
|
||||
const index = stack.value.findIndex((item) => item.key === dialog.key)
|
||||
if (index !== -1) {
|
||||
const item = stack.value.splice(index, 1)
|
||||
stack.value.push(...item)
|
||||
}
|
||||
}
|
||||
|
||||
const open = (dialog: DialogItem) => {
|
||||
const item = stack.value.find((item) => item.key === dialog.key)
|
||||
if (item) {
|
||||
item.visible = true
|
||||
rise(dialog)
|
||||
} else {
|
||||
stack.value.push({
|
||||
...dialog,
|
||||
content: markRaw(dialog.content),
|
||||
visible: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const close = (dialog: { key: string }) => {
|
||||
const item = stack.value.find((item) => item.key === dialog.key)
|
||||
if (item?.keepAlive) {
|
||||
item.visible = false
|
||||
} else {
|
||||
stack.value = stack.value.filter((item) => item.key !== dialog.key)
|
||||
}
|
||||
}
|
||||
|
||||
return { stack, open, close, rise }
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@ import { MarkdownTool, useMarkdown } from 'hooks/markdown'
|
||||
import { socket } from 'hooks/socket'
|
||||
import { 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' {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useBoolean } from 'hooks/utils'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export const useDialogManager = defineStore('dialogManager', () => {
|
||||
const [visible, toggle] = useBoolean()
|
||||
|
||||
const mounted = ref(false)
|
||||
const open = ref(false)
|
||||
|
||||
watch(visible, (visible) => {
|
||||
open.value = visible
|
||||
mounted.value = true
|
||||
})
|
||||
|
||||
const updateVisible = (val: boolean) => {
|
||||
visible.value = val
|
||||
}
|
||||
|
||||
return { visible: mounted, open, updateVisible, toggle }
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
interface StoreProvider {
|
||||
dialogManager: ReturnType<typeof useDialogManager>
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useBoolean = (defaultValue?: boolean) => {
|
||||
const target = ref(defaultValue ?? false)
|
||||
|
||||
const toggle = (value?: any) => {
|
||||
target.value = typeof value === 'boolean' ? value : !target.value
|
||||
}
|
||||
|
||||
return [target, toggle] as const
|
||||
}
|
||||
3
src/types/typings.d.ts
vendored
3
src/types/typings.d.ts
vendored
@@ -1,3 +1,6 @@
|
||||
type ContainerSize = { width: number; height: number }
|
||||
type ContainerPosition = { left: number; top: number }
|
||||
|
||||
interface BaseModel {
|
||||
id: number | string
|
||||
fullname: string
|
||||
|
||||
@@ -43,3 +43,7 @@ export const resolveModelType = (type: string) => {
|
||||
loader: loader[type],
|
||||
}
|
||||
}
|
||||
|
||||
export const genModelKey = (model: BaseModel) => {
|
||||
return `${model.type}:${model.pathIndex}:${model.fullname}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user