mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 23:12:14 +08:00
将独立的 /admin/backup 和 /admin/data-management 页面整合到设置页, 作为「备份」和「Sora 存储」标签页,减少侧边栏条目,集中管理配置。 - 移除 BackupView 和 DataManagementView 的 AppLayout 包装 - 在 SettingsView 中以子组件形式嵌入,使用 v-show 切换标签 - 删除独立路由和侧边栏菜单入口 - 备份/数据标签页下隐藏主保存按钮(各自有独立保存) - 优化标签栏样式适配7个标签,PC端支持细滚动条 - 清理未使用的图标组件和 i18n 键
512 lines
20 KiB
Vue
512 lines
20 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<div class="card p-6">
|
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
|
{{ t('admin.settings.soraS3.title') }}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ t('admin.settings.soraS3.description') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" @click="startCreateSoraProfile">
|
|
{{ t('admin.settings.soraS3.newProfile') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary btn-sm" :disabled="loadingSoraProfiles" @click="loadSoraS3Profiles">
|
|
{{ loadingSoraProfiles ? t('common.loading') : t('admin.settings.soraS3.reloadProfiles') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full min-w-[1000px] text-sm">
|
|
<thead>
|
|
<tr class="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500 dark:border-dark-700 dark:text-gray-400">
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.profile') }}</th>
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.active') }}</th>
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.endpoint') }}</th>
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.bucket') }}</th>
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.quota') }}</th>
|
|
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.updatedAt') }}</th>
|
|
<th class="py-2">{{ t('admin.settings.soraS3.columns.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="profile in soraS3Profiles" :key="profile.profile_id" class="border-b border-gray-100 align-top dark:border-dark-800">
|
|
<td class="py-3 pr-4">
|
|
<div class="font-mono text-xs">{{ profile.profile_id }}</div>
|
|
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">{{ profile.name }}</div>
|
|
</td>
|
|
<td class="py-3 pr-4">
|
|
<span
|
|
class="rounded px-2 py-0.5 text-xs"
|
|
:class="profile.is_active ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' : 'bg-gray-100 text-gray-700 dark:bg-dark-800 dark:text-gray-300'"
|
|
>
|
|
{{ profile.is_active ? t('common.enabled') : t('common.disabled') }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 pr-4 text-xs">
|
|
<div>{{ profile.endpoint || '-' }}</div>
|
|
<div class="mt-1 text-gray-500 dark:text-gray-400">{{ profile.region || '-' }}</div>
|
|
</td>
|
|
<td class="py-3 pr-4 text-xs">{{ profile.bucket || '-' }}</td>
|
|
<td class="py-3 pr-4 text-xs">{{ formatStorageQuotaGB(profile.default_storage_quota_bytes) }}</td>
|
|
<td class="py-3 pr-4 text-xs">{{ formatDate(profile.updated_at) }}</td>
|
|
<td class="py-3 text-xs">
|
|
<div class="flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-secondary btn-xs" @click="editSoraProfile(profile.profile_id)">
|
|
{{ t('common.edit') }}
|
|
</button>
|
|
<button
|
|
v-if="!profile.is_active"
|
|
type="button"
|
|
class="btn btn-secondary btn-xs"
|
|
:disabled="activatingSoraProfile"
|
|
@click="activateSoraProfile(profile.profile_id)"
|
|
>
|
|
{{ t('admin.settings.soraS3.activateProfile') }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger btn-xs"
|
|
:disabled="deletingSoraProfile"
|
|
@click="removeSoraProfile(profile.profile_id)"
|
|
>
|
|
{{ t('common.delete') }}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="soraS3Profiles.length === 0">
|
|
<td colspan="7" class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
{{ t('admin.settings.soraS3.empty') }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Teleport to="body">
|
|
<Transition name="dm-drawer-mask">
|
|
<div
|
|
v-if="soraProfileDrawerOpen"
|
|
class="fixed inset-0 z-[54] bg-black/40 backdrop-blur-sm"
|
|
@click="closeSoraProfileDrawer"
|
|
></div>
|
|
</Transition>
|
|
|
|
<Transition name="dm-drawer-panel">
|
|
<div
|
|
v-if="soraProfileDrawerOpen"
|
|
class="fixed inset-y-0 right-0 z-[55] flex h-full w-full max-w-2xl flex-col border-l border-gray-200 bg-white shadow-2xl dark:border-dark-700 dark:bg-dark-900"
|
|
>
|
|
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-dark-700">
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{{ creatingSoraProfile ? t('admin.settings.soraS3.createTitle') : t('admin.settings.soraS3.editTitle') }}
|
|
</h4>
|
|
<button
|
|
type="button"
|
|
class="rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-dark-800 dark:hover:text-gray-200"
|
|
@click="closeSoraProfileDrawer"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto p-4">
|
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
<input
|
|
v-model="soraProfileForm.profile_id"
|
|
class="input w-full"
|
|
:placeholder="t('admin.settings.soraS3.profileID')"
|
|
:disabled="!creatingSoraProfile"
|
|
/>
|
|
<input
|
|
v-model="soraProfileForm.name"
|
|
class="input w-full"
|
|
:placeholder="t('admin.settings.soraS3.profileName')"
|
|
/>
|
|
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 md:col-span-2">
|
|
<input v-model="soraProfileForm.enabled" type="checkbox" />
|
|
<span>{{ t('admin.settings.soraS3.enabled') }}</span>
|
|
</label>
|
|
<input v-model="soraProfileForm.endpoint" class="input w-full" :placeholder="t('admin.settings.soraS3.endpoint')" />
|
|
<input v-model="soraProfileForm.region" class="input w-full" :placeholder="t('admin.settings.soraS3.region')" />
|
|
<input v-model="soraProfileForm.bucket" class="input w-full" :placeholder="t('admin.settings.soraS3.bucket')" />
|
|
<input v-model="soraProfileForm.prefix" class="input w-full" :placeholder="t('admin.settings.soraS3.prefix')" />
|
|
<input v-model="soraProfileForm.access_key_id" class="input w-full" :placeholder="t('admin.settings.soraS3.accessKeyId')" />
|
|
<input
|
|
v-model="soraProfileForm.secret_access_key"
|
|
type="password"
|
|
class="input w-full"
|
|
:placeholder="soraProfileForm.secret_access_key_configured ? t('admin.settings.soraS3.secretConfigured') : t('admin.settings.soraS3.secretAccessKey')"
|
|
/>
|
|
<input v-model="soraProfileForm.cdn_url" class="input w-full" :placeholder="t('admin.settings.soraS3.cdnUrl')" />
|
|
<div>
|
|
<input
|
|
v-model.number="soraProfileForm.default_storage_quota_gb"
|
|
type="number"
|
|
min="0"
|
|
step="0.1"
|
|
class="input w-full"
|
|
:placeholder="t('admin.settings.soraS3.defaultQuota')"
|
|
/>
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.soraS3.defaultQuotaHint') }}</p>
|
|
</div>
|
|
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
|
<input v-model="soraProfileForm.force_path_style" type="checkbox" />
|
|
<span>{{ t('admin.settings.soraS3.forcePathStyle') }}</span>
|
|
</label>
|
|
<label v-if="creatingSoraProfile" class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 md:col-span-2">
|
|
<input v-model="soraProfileForm.set_active" type="checkbox" />
|
|
<span>{{ t('admin.settings.soraS3.setActive') }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap justify-end gap-2 border-t border-gray-200 p-4 dark:border-dark-700">
|
|
<button type="button" class="btn btn-secondary btn-sm" @click="closeSoraProfileDrawer">
|
|
{{ t('common.cancel') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary btn-sm" :disabled="testingSoraProfile || !soraProfileForm.enabled" @click="testSoraProfileConnection">
|
|
{{ testingSoraProfile ? t('common.loading') : t('admin.settings.soraS3.testConnection') }}
|
|
</button>
|
|
<button type="button" class="btn btn-primary btn-sm" :disabled="savingSoraProfile" @click="saveSoraProfile">
|
|
{{ savingSoraProfile ? t('common.loading') : t('admin.settings.soraS3.saveProfile') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { SoraS3Profile } from '@/api/admin/settings'
|
|
import { adminAPI } from '@/api'
|
|
import { useAppStore } from '@/stores'
|
|
|
|
const { t } = useI18n()
|
|
const appStore = useAppStore()
|
|
|
|
const loadingSoraProfiles = ref(false)
|
|
const savingSoraProfile = ref(false)
|
|
const testingSoraProfile = ref(false)
|
|
const activatingSoraProfile = ref(false)
|
|
const deletingSoraProfile = ref(false)
|
|
const creatingSoraProfile = ref(false)
|
|
const soraProfileDrawerOpen = ref(false)
|
|
|
|
const soraS3Profiles = ref<SoraS3Profile[]>([])
|
|
const selectedSoraProfileID = ref('')
|
|
|
|
type SoraS3ProfileForm = {
|
|
profile_id: string
|
|
name: string
|
|
set_active: boolean
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key: string
|
|
secret_access_key_configured: boolean
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_gb: number
|
|
}
|
|
|
|
const soraProfileForm = ref<SoraS3ProfileForm>(newDefaultSoraS3ProfileForm())
|
|
|
|
async function loadSoraS3Profiles() {
|
|
loadingSoraProfiles.value = true
|
|
try {
|
|
const result = await adminAPI.settings.listSoraS3Profiles()
|
|
soraS3Profiles.value = result.items || []
|
|
if (!creatingSoraProfile.value) {
|
|
const stillExists = selectedSoraProfileID.value
|
|
? soraS3Profiles.value.some((item) => item.profile_id === selectedSoraProfileID.value)
|
|
: false
|
|
if (!stillExists) {
|
|
selectedSoraProfileID.value = pickPreferredSoraProfileID()
|
|
}
|
|
syncSoraProfileFormWithSelection()
|
|
}
|
|
} catch (error) {
|
|
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
|
} finally {
|
|
loadingSoraProfiles.value = false
|
|
}
|
|
}
|
|
|
|
function startCreateSoraProfile() {
|
|
creatingSoraProfile.value = true
|
|
selectedSoraProfileID.value = ''
|
|
soraProfileForm.value = newDefaultSoraS3ProfileForm()
|
|
soraProfileDrawerOpen.value = true
|
|
}
|
|
|
|
function editSoraProfile(profileID: string) {
|
|
selectedSoraProfileID.value = profileID
|
|
creatingSoraProfile.value = false
|
|
syncSoraProfileFormWithSelection()
|
|
soraProfileDrawerOpen.value = true
|
|
}
|
|
|
|
function closeSoraProfileDrawer() {
|
|
soraProfileDrawerOpen.value = false
|
|
if (creatingSoraProfile.value) {
|
|
creatingSoraProfile.value = false
|
|
selectedSoraProfileID.value = pickPreferredSoraProfileID()
|
|
syncSoraProfileFormWithSelection()
|
|
}
|
|
}
|
|
|
|
async function saveSoraProfile() {
|
|
if (!soraProfileForm.value.name.trim()) {
|
|
appStore.showError(t('admin.settings.soraS3.profileNameRequired'))
|
|
return
|
|
}
|
|
if (creatingSoraProfile.value && !soraProfileForm.value.profile_id.trim()) {
|
|
appStore.showError(t('admin.settings.soraS3.profileIDRequired'))
|
|
return
|
|
}
|
|
if (!creatingSoraProfile.value && !selectedSoraProfileID.value) {
|
|
appStore.showError(t('admin.settings.soraS3.profileSelectRequired'))
|
|
return
|
|
}
|
|
if (soraProfileForm.value.enabled) {
|
|
if (!soraProfileForm.value.endpoint.trim()) {
|
|
appStore.showError(t('admin.settings.soraS3.endpointRequired'))
|
|
return
|
|
}
|
|
if (!soraProfileForm.value.bucket.trim()) {
|
|
appStore.showError(t('admin.settings.soraS3.bucketRequired'))
|
|
return
|
|
}
|
|
if (!soraProfileForm.value.access_key_id.trim()) {
|
|
appStore.showError(t('admin.settings.soraS3.accessKeyRequired'))
|
|
return
|
|
}
|
|
}
|
|
|
|
savingSoraProfile.value = true
|
|
try {
|
|
if (creatingSoraProfile.value) {
|
|
const created = await adminAPI.settings.createSoraS3Profile({
|
|
profile_id: soraProfileForm.value.profile_id.trim(),
|
|
name: soraProfileForm.value.name.trim(),
|
|
set_active: soraProfileForm.value.set_active,
|
|
enabled: soraProfileForm.value.enabled,
|
|
endpoint: soraProfileForm.value.endpoint,
|
|
region: soraProfileForm.value.region,
|
|
bucket: soraProfileForm.value.bucket,
|
|
access_key_id: soraProfileForm.value.access_key_id,
|
|
secret_access_key: soraProfileForm.value.secret_access_key || undefined,
|
|
prefix: soraProfileForm.value.prefix,
|
|
force_path_style: soraProfileForm.value.force_path_style,
|
|
cdn_url: soraProfileForm.value.cdn_url,
|
|
default_storage_quota_bytes: Math.round((soraProfileForm.value.default_storage_quota_gb || 0) * 1024 * 1024 * 1024)
|
|
})
|
|
selectedSoraProfileID.value = created.profile_id
|
|
creatingSoraProfile.value = false
|
|
soraProfileDrawerOpen.value = false
|
|
appStore.showSuccess(t('admin.settings.soraS3.profileCreated'))
|
|
} else {
|
|
await adminAPI.settings.updateSoraS3Profile(selectedSoraProfileID.value, {
|
|
name: soraProfileForm.value.name.trim(),
|
|
enabled: soraProfileForm.value.enabled,
|
|
endpoint: soraProfileForm.value.endpoint,
|
|
region: soraProfileForm.value.region,
|
|
bucket: soraProfileForm.value.bucket,
|
|
access_key_id: soraProfileForm.value.access_key_id,
|
|
secret_access_key: soraProfileForm.value.secret_access_key || undefined,
|
|
prefix: soraProfileForm.value.prefix,
|
|
force_path_style: soraProfileForm.value.force_path_style,
|
|
cdn_url: soraProfileForm.value.cdn_url,
|
|
default_storage_quota_bytes: Math.round((soraProfileForm.value.default_storage_quota_gb || 0) * 1024 * 1024 * 1024)
|
|
})
|
|
soraProfileDrawerOpen.value = false
|
|
appStore.showSuccess(t('admin.settings.soraS3.profileSaved'))
|
|
}
|
|
await loadSoraS3Profiles()
|
|
} catch (error) {
|
|
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
|
} finally {
|
|
savingSoraProfile.value = false
|
|
}
|
|
}
|
|
|
|
async function testSoraProfileConnection() {
|
|
testingSoraProfile.value = true
|
|
try {
|
|
const result = await adminAPI.settings.testSoraS3Connection({
|
|
profile_id: creatingSoraProfile.value ? undefined : selectedSoraProfileID.value,
|
|
enabled: soraProfileForm.value.enabled,
|
|
endpoint: soraProfileForm.value.endpoint,
|
|
region: soraProfileForm.value.region,
|
|
bucket: soraProfileForm.value.bucket,
|
|
access_key_id: soraProfileForm.value.access_key_id,
|
|
secret_access_key: soraProfileForm.value.secret_access_key || undefined,
|
|
prefix: soraProfileForm.value.prefix,
|
|
force_path_style: soraProfileForm.value.force_path_style,
|
|
cdn_url: soraProfileForm.value.cdn_url,
|
|
default_storage_quota_bytes: Math.round((soraProfileForm.value.default_storage_quota_gb || 0) * 1024 * 1024 * 1024)
|
|
})
|
|
appStore.showSuccess(result.message || t('admin.settings.soraS3.testSuccess'))
|
|
} catch (error) {
|
|
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
|
} finally {
|
|
testingSoraProfile.value = false
|
|
}
|
|
}
|
|
|
|
async function activateSoraProfile(profileID: string) {
|
|
activatingSoraProfile.value = true
|
|
try {
|
|
await adminAPI.settings.setActiveSoraS3Profile(profileID)
|
|
appStore.showSuccess(t('admin.settings.soraS3.profileActivated'))
|
|
await loadSoraS3Profiles()
|
|
} catch (error) {
|
|
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
|
} finally {
|
|
activatingSoraProfile.value = false
|
|
}
|
|
}
|
|
|
|
async function removeSoraProfile(profileID: string) {
|
|
if (!window.confirm(t('admin.settings.soraS3.deleteConfirm', { profileID }))) {
|
|
return
|
|
}
|
|
deletingSoraProfile.value = true
|
|
try {
|
|
await adminAPI.settings.deleteSoraS3Profile(profileID)
|
|
if (selectedSoraProfileID.value === profileID) {
|
|
selectedSoraProfileID.value = ''
|
|
}
|
|
appStore.showSuccess(t('admin.settings.soraS3.profileDeleted'))
|
|
await loadSoraS3Profiles()
|
|
} catch (error) {
|
|
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
|
} finally {
|
|
deletingSoraProfile.value = false
|
|
}
|
|
}
|
|
|
|
function formatDate(value?: string): string {
|
|
if (!value) {
|
|
return '-'
|
|
}
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) {
|
|
return value
|
|
}
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
function formatStorageQuotaGB(bytes: number): string {
|
|
if (!bytes || bytes <= 0) {
|
|
return '0 GB'
|
|
}
|
|
const gb = bytes / (1024 * 1024 * 1024)
|
|
return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`
|
|
}
|
|
|
|
function pickPreferredSoraProfileID(): string {
|
|
const active = soraS3Profiles.value.find((item) => item.is_active)
|
|
if (active) {
|
|
return active.profile_id
|
|
}
|
|
return soraS3Profiles.value[0]?.profile_id || ''
|
|
}
|
|
|
|
function syncSoraProfileFormWithSelection() {
|
|
const profile = soraS3Profiles.value.find((item) => item.profile_id === selectedSoraProfileID.value)
|
|
soraProfileForm.value = newDefaultSoraS3ProfileForm(profile)
|
|
}
|
|
|
|
function newDefaultSoraS3ProfileForm(profile?: SoraS3Profile): SoraS3ProfileForm {
|
|
if (!profile) {
|
|
return {
|
|
profile_id: '',
|
|
name: '',
|
|
set_active: false,
|
|
enabled: false,
|
|
endpoint: '',
|
|
region: '',
|
|
bucket: '',
|
|
access_key_id: '',
|
|
secret_access_key: '',
|
|
secret_access_key_configured: false,
|
|
prefix: 'sora/',
|
|
force_path_style: false,
|
|
cdn_url: '',
|
|
default_storage_quota_gb: 0
|
|
}
|
|
}
|
|
|
|
const quotaBytes = profile.default_storage_quota_bytes || 0
|
|
|
|
return {
|
|
profile_id: profile.profile_id,
|
|
name: profile.name,
|
|
set_active: false,
|
|
enabled: profile.enabled,
|
|
endpoint: profile.endpoint || '',
|
|
region: profile.region || '',
|
|
bucket: profile.bucket || '',
|
|
access_key_id: profile.access_key_id || '',
|
|
secret_access_key: '',
|
|
secret_access_key_configured: Boolean(profile.secret_access_key_configured),
|
|
prefix: profile.prefix || '',
|
|
force_path_style: Boolean(profile.force_path_style),
|
|
cdn_url: profile.cdn_url || '',
|
|
default_storage_quota_gb: Number((quotaBytes / (1024 * 1024 * 1024)).toFixed(2))
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadSoraS3Profiles()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dm-drawer-mask-enter-active,
|
|
.dm-drawer-mask-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.dm-drawer-mask-enter-from,
|
|
.dm-drawer-mask-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.dm-drawer-panel-enter-active,
|
|
.dm-drawer-panel-leave-active {
|
|
transition:
|
|
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1),
|
|
opacity 0.2s ease;
|
|
}
|
|
|
|
.dm-drawer-panel-enter-from,
|
|
.dm-drawer-panel-leave-to {
|
|
opacity: 0.96;
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.dm-drawer-mask-enter-active,
|
|
.dm-drawer-mask-leave-active,
|
|
.dm-drawer-panel-enter-active,
|
|
.dm-drawer-panel-leave-active {
|
|
transition-duration: 0s;
|
|
}
|
|
}
|
|
</style>
|