fix: restore main transfer pages with smooth routing
This commit is contained in:
@@ -110,6 +110,7 @@ const rules = [
|
|||||||
/^src\/utils\/auth-storage\.ts$/,
|
/^src\/utils\/auth-storage\.ts$/,
|
||||||
/^src\/utils\/config-storage\.ts$/,
|
/^src\/utils\/config-storage\.ts$/,
|
||||||
/^src\/utils\/preference-storage\.ts$/,
|
/^src\/utils\/preference-storage\.ts$/,
|
||||||
|
/^src\/utils\/record-storage\.ts$/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+33
-2
@@ -5,7 +5,14 @@ import LanguageSwitcher from './components/common/LanguageSwitcher.vue'
|
|||||||
import AlertComponent from '@/components/common/AlertComponent.vue'
|
import AlertComponent from '@/components/common/AlertComponent.vue'
|
||||||
import { useAppShell } from '@/composables'
|
import { useAppShell } from '@/composables'
|
||||||
|
|
||||||
const { isDarkMode, isLoading, routeViewKey, showGlobalControls } = useAppShell()
|
const {
|
||||||
|
isDarkMode,
|
||||||
|
isLoading,
|
||||||
|
routeTransitionMode,
|
||||||
|
routeTransitionName,
|
||||||
|
routeViewKey,
|
||||||
|
showGlobalControls
|
||||||
|
} = useAppShell()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,7 +25,7 @@ const { isDarkMode, isLoading, routeViewKey, showGlobalControls } = useAppShell(
|
|||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<transition name="fade" mode="out-in">
|
<transition :name="routeTransitionName" :mode="routeTransitionMode">
|
||||||
<component :is="Component" :key="routeViewKey" />
|
<component :is="Component" :key="routeViewKey" />
|
||||||
</transition>
|
</transition>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
@@ -52,6 +59,30 @@ const { isDarkMode, isLoading, routeViewKey, showGlobalControls } = useAppShell(
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-fade-enter-active,
|
||||||
|
.transfer-fade-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-fade-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export function useAppShell() {
|
|||||||
const { isLoading, setupRouteLoading } = useRouteLoading(router)
|
const { isLoading, setupRouteLoading } = useRouteLoading(router)
|
||||||
const { syncPublicConfig } = usePublicConfigBootstrap()
|
const { syncPublicConfig } = usePublicConfigBootstrap()
|
||||||
const showGlobalControls = computed(() => route.meta.showGlobalControls !== false)
|
const showGlobalControls = computed(() => route.meta.showGlobalControls !== false)
|
||||||
|
const routeTransitionName = computed(() => String(route.meta.routeTransition || 'fade'))
|
||||||
|
const routeTransitionMode = computed(() => (route.meta.routeTransition ? undefined : 'out-in'))
|
||||||
const routeViewKey = computed(() =>
|
const routeViewKey = computed(() =>
|
||||||
route.path === ROUTES.ADMIN || route.path.startsWith(`${ROUTES.ADMIN}/`)
|
route.path === ROUTES.ADMIN || route.path.startsWith(`${ROUTES.ADMIN}/`)
|
||||||
? ROUTES.ADMIN
|
? ROUTES.ADMIN
|
||||||
@@ -52,6 +54,8 @@ export function useAppShell() {
|
|||||||
isDarkMode,
|
isDarkMode,
|
||||||
isLoading,
|
isLoading,
|
||||||
route,
|
route,
|
||||||
|
routeTransitionMode,
|
||||||
|
routeTransitionName,
|
||||||
routeViewKey,
|
routeViewKey,
|
||||||
showGlobalControls
|
showGlobalControls
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ import { readStoredToken } from '@/utils/auth-storage'
|
|||||||
|
|
||||||
const publicPageMeta = {
|
const publicPageMeta = {
|
||||||
showGlobalControls: true,
|
showGlobalControls: true,
|
||||||
showRouteLoading: true
|
showRouteLoading: false,
|
||||||
|
routeTransition: 'transfer-fade'
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminPageMeta = {
|
const adminPageMeta = {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export function readStoredRecords<T>(key: string): T[] {
|
||||||
|
if (typeof window === 'undefined') return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
if (!raw) return []
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return Array.isArray(parsed) ? (parsed as T[]) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeStoredRecords<T>(key: string, records: T[]) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(records))
|
||||||
|
} catch {
|
||||||
|
// 本地存储不可用时保持内存记录,不影响当前会话。
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-283
@@ -1,241 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="transfer-page relative min-h-screen overflow-x-hidden px-4 py-6 transition-colors duration-300 sm:px-6 lg:px-8"
|
class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300"
|
||||||
:class="[isDarkMode ? 'text-slate-100' : 'text-slate-950']"
|
|
||||||
>
|
>
|
||||||
<main
|
<div class="w-full max-w-md relative z-10">
|
||||||
class="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-6xl items-start lg:items-center"
|
<div
|
||||||
>
|
class="rounded-3xl shadow-2xl overflow-hidden border transform transition-all duration-300"
|
||||||
<section
|
:class="[
|
||||||
class="grid w-full items-stretch gap-5 lg:grid-cols-[minmax(0,0.92fr)_minmax(380px,1fr)]"
|
isDarkMode
|
||||||
|
? 'bg-gray-800 bg-opacity-50 backdrop-filter backdrop-blur-xl border-gray-700'
|
||||||
|
: 'bg-white border-gray-200'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<aside
|
<div class="p-8">
|
||||||
class="order-2 flex min-h-0 flex-col justify-between rounded-[28px] border p-5 shadow-sm sm:p-8 lg:order-1 lg:min-h-[520px]"
|
<PageHeader :title="config.name" @title-click="toSend" />
|
||||||
:class="[
|
<RetrieveForm
|
||||||
isDarkMode
|
v-model="code"
|
||||||
? 'border-white/10 bg-slate-950/45 shadow-black/20'
|
:input-status="inputStatus"
|
||||||
: 'border-white/80 bg-white/70 shadow-slate-200/70'
|
:error="!!error"
|
||||||
]"
|
@submit="handleSubmit"
|
||||||
>
|
ref="retrieveFormRef"
|
||||||
<div>
|
/>
|
||||||
<div class="flex items-center justify-between gap-4">
|
</div>
|
||||||
<button
|
<PageFooter
|
||||||
type="button"
|
:link-text="$t('retrieve.needSendFile')"
|
||||||
class="flex min-w-0 items-center gap-3 text-left"
|
link-to="/send"
|
||||||
@click="toSend"
|
:drawer-text="$t('retrieve.recordsDrawer')"
|
||||||
>
|
@toggle-drawer="toggleDrawer"
|
||||||
<span
|
/>
|
||||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
|
</div>
|
||||||
:class="[
|
</div>
|
||||||
isDarkMode ? 'bg-cyan-400/15 text-cyan-200' : 'bg-cyan-50 text-cyan-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<BoxIcon class="h-5 w-5" />
|
|
||||||
</span>
|
|
||||||
<span class="min-w-0">
|
|
||||||
<span
|
|
||||||
class="block truncate text-lg font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-slate-950']"
|
|
||||||
>
|
|
||||||
{{ config.name }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="mt-1 block text-xs"
|
|
||||||
:class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']"
|
|
||||||
>
|
|
||||||
FileCodeBox
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<router-link
|
|
||||||
to="/send"
|
|
||||||
class="inline-flex h-10 items-center justify-center rounded-full border px-4 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 text-cyan-100 hover:border-cyan-300/40 hover:bg-cyan-300/10'
|
|
||||||
: 'border-cyan-100 bg-white text-cyan-700 hover:border-cyan-200 hover:bg-cyan-50'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<SendIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('retrieve.workspace.sendFile') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 max-w-xl lg:mt-14">
|
|
||||||
<p
|
|
||||||
class="mb-4 inline-flex rounded-full px-3 py-1 text-xs font-medium"
|
|
||||||
:class="[isDarkMode ? 'bg-cyan-400/10 text-cyan-200' : 'bg-cyan-50 text-cyan-700']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.security') }}
|
|
||||||
</p>
|
|
||||||
<h1 class="text-4xl font-semibold leading-tight sm:text-5xl">
|
|
||||||
{{ t('retrieve.title') }}
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="mt-5 max-w-lg text-sm leading-7"
|
|
||||||
:class="[isDarkMode ? 'text-slate-300' : 'text-slate-600']"
|
|
||||||
>
|
|
||||||
{{ config.description || t('retrieve.workspace.securityState') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-7 grid gap-3 sm:grid-cols-3 lg:mt-10 lg:grid-cols-1 xl:grid-cols-3">
|
|
||||||
<div
|
|
||||||
v-for="item in statusCards"
|
|
||||||
:key="item.label"
|
|
||||||
class="rounded-2xl border px-4 py-3"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'border-white/10 bg-white/[0.04]' : 'border-slate-200/70 bg-white/75'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']">
|
|
||||||
{{ item.label }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 truncate text-sm font-semibold">
|
|
||||||
{{ item.value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mt-7 rounded-2xl border p-4 lg:mt-10"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'border-white/10 bg-slate-900/45' : 'border-slate-200/70 bg-white/65'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<ShieldCheckIcon class="mt-0.5 h-5 w-5 shrink-0 text-emerald-500" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">
|
|
||||||
{{ t('send.secureEncryption') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-1 text-xs leading-5"
|
|
||||||
:class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.securityState') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="order-1 rounded-[28px] border p-5 shadow-xl shadow-slate-900/5 sm:p-8 lg:order-2"
|
|
||||||
:class="[isDarkMode ? 'border-white/10 bg-slate-950/70' : 'border-white/90 bg-white/90']"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-cyan-200' : 'text-cyan-700']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.codeInput.label') }}
|
|
||||||
</p>
|
|
||||||
<h2 class="mt-2 text-2xl font-semibold">
|
|
||||||
{{ t('retrieve.workspace.noPreview') }}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 text-slate-300 hover:bg-white/10'
|
|
||||||
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
|
|
||||||
]"
|
|
||||||
:aria-label="t('retrieve.recordsDrawer')"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<ClipboardListIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="mt-8 space-y-5" @submit.prevent="handleSubmit">
|
|
||||||
<label class="block">
|
|
||||||
<span class="sr-only">{{ t('retrieve.codeInput.label') }}</span>
|
|
||||||
<input
|
|
||||||
v-model="code"
|
|
||||||
type="text"
|
|
||||||
inputmode="text"
|
|
||||||
maxlength="5"
|
|
||||||
autocomplete="one-time-code"
|
|
||||||
class="h-20 w-full rounded-3xl border px-6 text-center text-4xl font-semibold outline-none transition focus:ring-4"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-white/[0.06] text-white placeholder-slate-600 focus:border-cyan-300/60 focus:ring-cyan-300/10'
|
|
||||||
: 'border-slate-200 bg-slate-50/80 text-slate-950 placeholder-slate-300 focus:border-cyan-400 focus:ring-cyan-100',
|
|
||||||
error ? 'border-rose-400 focus:border-rose-400 focus:ring-rose-100' : ''
|
|
||||||
]"
|
|
||||||
:placeholder="t('retrieve.codeInput.placeholder')"
|
|
||||||
:readonly="inputStatus.readonly"
|
|
||||||
@input="normalizeCode"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<p v-if="error" class="rounded-2xl bg-rose-500/10 px-4 py-3 text-sm text-rose-500">
|
|
||||||
{{ error }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="group flex h-14 w-full items-center justify-center rounded-2xl px-5 text-sm font-semibold text-white transition disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'bg-cyan-500 hover:bg-cyan-400' : 'bg-slate-950 hover:bg-slate-800',
|
|
||||||
inputStatus.loading ? 'pointer-events-none' : ''
|
|
||||||
]"
|
|
||||||
:disabled="inputStatus.loading || !hasValidCode"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="inputStatus.loading"
|
|
||||||
class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white"
|
|
||||||
></span>
|
|
||||||
<DownloadCloudIcon
|
|
||||||
v-else
|
|
||||||
class="mr-2 h-4 w-4 transition group-hover:translate-y-0.5"
|
|
||||||
/>
|
|
||||||
{{ inputStatus.loading ? t('common.loading') : t('retrieve.submit') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-8 grid gap-3 sm:grid-cols-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-2xl border p-4 text-left transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-white/[0.04] hover:bg-white/[0.07]'
|
|
||||||
: 'border-slate-200 bg-white hover:bg-slate-50'
|
|
||||||
]"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<ClipboardListIcon class="h-5 w-5 text-cyan-500" />
|
|
||||||
<p class="mt-3 text-sm font-medium">{{ t('retrieve.recordsDrawer') }}</p>
|
|
||||||
<p class="mt-1 text-xs" :class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']">
|
|
||||||
{{ t('retrieve.workspace.historyCount', { count: records.length }) }}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
to="/login"
|
|
||||||
class="rounded-2xl border p-4 text-left transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-white/[0.04] hover:bg-white/[0.07]'
|
|
||||||
: 'border-slate-200 bg-white hover:bg-slate-50'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<UserIcon class="h-5 w-5 text-slate-400" />
|
|
||||||
<p class="mt-3 text-sm font-medium">{{ t('admin.dashboard.footerProduct') }}</p>
|
|
||||||
<p class="mt-1 text-xs" :class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']">
|
|
||||||
{{ t('common.appDescription') }}
|
|
||||||
</p>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<SideDrawer :visible="showDrawer" :title="$t('retrieve.recordsDrawer')" @close="toggleDrawer">
|
<SideDrawer :visible="showDrawer" :title="$t('retrieve.recordsDrawer')" @close="toggleDrawer">
|
||||||
<FileRecordList
|
<FileRecordList
|
||||||
@@ -263,27 +56,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { inject, onMounted, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import PageHeader from '@/components/common/PageHeader.vue'
|
||||||
BoxIcon,
|
import RetrieveForm from '@/components/common/RetrieveForm.vue'
|
||||||
ClipboardListIcon,
|
import PageFooter from '@/components/common/PageFooter.vue'
|
||||||
DownloadCloudIcon,
|
|
||||||
SendIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
UserIcon
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import SideDrawer from '@/components/common/SideDrawer.vue'
|
import SideDrawer from '@/components/common/SideDrawer.vue'
|
||||||
import FileDetailModal from '@/components/common/FileDetailModal.vue'
|
import FileDetailModal from '@/components/common/FileDetailModal.vue'
|
||||||
import FileRecordList from '@/components/common/FileRecordList.vue'
|
import FileRecordList from '@/components/common/FileRecordList.vue'
|
||||||
import ContentPreviewModal from '@/components/common/ContentPreviewModal.vue'
|
import ContentPreviewModal from '@/components/common/ContentPreviewModal.vue'
|
||||||
import { useInjectedDarkMode, useRetrieveFlow } from '@/composables'
|
import { useRetrieveFlow } from '@/composables'
|
||||||
import { useConfigStore } from '@/stores/configStore'
|
import { useConfigStore } from '@/stores/configStore'
|
||||||
|
|
||||||
const isDarkMode = useInjectedDarkMode()
|
const isDarkMode = inject('isDarkMode')
|
||||||
const { t } = useI18n()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
@@ -297,7 +83,6 @@ const {
|
|||||||
showDrawer,
|
showDrawer,
|
||||||
showPreview,
|
showPreview,
|
||||||
renderedContent,
|
renderedContent,
|
||||||
hasValidCode,
|
|
||||||
closeContentPreview,
|
closeContentPreview,
|
||||||
closeDetails,
|
closeDetails,
|
||||||
copyContent,
|
copyContent,
|
||||||
@@ -313,49 +98,16 @@ const toSend = () => {
|
|||||||
router.push('/send')
|
router.push('/send')
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeCode = () => {
|
|
||||||
code.value = code.value.trim().slice(0, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusCards = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.currentCode'),
|
|
||||||
value: code.value || t('retrieve.workspace.emptyCode')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.previewState'),
|
|
||||||
value: inputStatus.value.loading ? t('common.loading') : t('retrieve.workspace.waiting')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.recordsDrawer'),
|
|
||||||
value: t('retrieve.workspace.historyCount', { count: records.value.length })
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const queryCode = route.query.code
|
const queryCode = route.query.code
|
||||||
if (queryCode && typeof queryCode === 'string') {
|
if (queryCode && typeof queryCode === 'string') {
|
||||||
code.value = queryCode.slice(0, 5)
|
code.value = queryCode
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(code, (newCode) => {
|
watch(code, (newCode) => {
|
||||||
if (newCode.length === 5 && !inputStatus.value.loading) {
|
if (newCode.length === 5) {
|
||||||
void handleSubmit()
|
void handleSubmit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.transfer-page::before {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgb(148 163 184 / 0.12) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgb(148 163 184 / 0.12) 1px, transparent 1px);
|
|
||||||
background-size: 56px 56px;
|
|
||||||
mask-image: linear-gradient(to bottom, black, transparent 82%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+184
-350
@@ -1,277 +1,109 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="transfer-page relative min-h-screen overflow-x-hidden px-4 py-6 transition-colors duration-300 sm:px-6 lg:px-8"
|
class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300"
|
||||||
:class="[isDarkMode ? 'text-slate-100' : 'text-slate-950']"
|
|
||||||
@paste.prevent="handlePaste"
|
@paste.prevent="handlePaste"
|
||||||
>
|
>
|
||||||
<main
|
<div
|
||||||
class="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-6xl items-start lg:items-center"
|
class="rounded-3xl shadow-2xl overflow-hidden border w-full max-w-md transition-colors duration-300"
|
||||||
|
:class="[
|
||||||
|
isDarkMode
|
||||||
|
? 'bg-white bg-opacity-10 backdrop-filter backdrop-blur-xl border-gray-700'
|
||||||
|
: 'bg-white border-gray-200'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<section
|
<div class="p-8">
|
||||||
class="grid w-full items-stretch gap-5 lg:grid-cols-[minmax(390px,1.04fr)_minmax(0,0.88fr)]"
|
<PageHeader :title="config.name" @title-click="toRetrieve" />
|
||||||
>
|
<form @submit.prevent="handleSubmit" class="space-y-8">
|
||||||
<section
|
<SendTypeSelector
|
||||||
class="rounded-[28px] border p-5 shadow-xl shadow-slate-900/5 sm:p-8"
|
:selected-type="sendType"
|
||||||
:class="[isDarkMode ? 'border-white/10 bg-slate-950/70' : 'border-white/90 bg-white/90']"
|
@update:selected-type="sendType = $event"
|
||||||
>
|
/>
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div>
|
<transition name="fade" mode="out-in">
|
||||||
<p
|
<div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8">
|
||||||
class="text-sm font-medium"
|
<FileUploadArea
|
||||||
:class="[isDarkMode ? 'text-emerald-200' : 'text-emerald-700']"
|
:selected-file="selectedFile"
|
||||||
>
|
:selected-files="selectedFiles"
|
||||||
{{ t('send.title') }}
|
:progress="uploadProgress"
|
||||||
</p>
|
:description="uploadDescription"
|
||||||
<h1 class="mt-2 text-3xl font-semibold">
|
@file-selected="handleFileSelected"
|
||||||
{{ selectedPayloadLabel }}
|
@files-selected="handleFilesSelected"
|
||||||
</h1>
|
@file-drop="handleFileDrop"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div v-else key="text" class="grid grid-cols-1 gap-8">
|
||||||
to="/"
|
<TextInputArea v-model="textContent" :placeholder="t('send.uploadArea.textInput')" />
|
||||||
class="inline-flex h-10 shrink-0 items-center justify-center rounded-full border px-4 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 text-emerald-100 hover:border-emerald-300/40 hover:bg-emerald-300/10'
|
|
||||||
: 'border-emerald-100 bg-white text-emerald-700 hover:border-emerald-200 hover:bg-emerald-50'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<ArchiveRestoreIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('send.needRetrieveFile') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="mt-8 space-y-6" @submit.prevent="handleSubmit">
|
|
||||||
<div
|
|
||||||
class="grid rounded-2xl border p-1 sm:grid-cols-2"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'border-white/10 bg-white/[0.04]' : 'border-slate-200 bg-slate-100/70'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-11 items-center justify-center rounded-xl text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
sendType === 'file'
|
|
||||||
? isDarkMode
|
|
||||||
? 'bg-white text-slate-950 shadow-sm'
|
|
||||||
: 'bg-white text-slate-950 shadow-sm'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-slate-400 hover:text-white'
|
|
||||||
: 'text-slate-500 hover:text-slate-900'
|
|
||||||
]"
|
|
||||||
@click="sendType = 'file'"
|
|
||||||
>
|
|
||||||
<CloudUploadIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('nav.sendFile') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-11 items-center justify-center rounded-xl text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
sendType === 'text'
|
|
||||||
? isDarkMode
|
|
||||||
? 'bg-white text-slate-950 shadow-sm'
|
|
||||||
: 'bg-white text-slate-950 shadow-sm'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-slate-400 hover:text-white'
|
|
||||||
: 'text-slate-500 hover:text-slate-900'
|
|
||||||
]"
|
|
||||||
@click="sendType = 'text'"
|
|
||||||
>
|
|
||||||
<TextCursorInputIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('send.sendText') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
<transition name="fade" mode="out-in">
|
<ExpirationSelector
|
||||||
<div v-if="sendType === 'file'" key="file" class="upload-shell">
|
v-model:expiration-method="expirationMethod"
|
||||||
<FileUploadArea
|
v-model:expiration-value="expirationValue"
|
||||||
:selected-file="selectedFile"
|
:options="expirationOptions"
|
||||||
:selected-files="selectedFiles"
|
/>
|
||||||
:progress="uploadProgress"
|
<!-- 提交按钮 -->
|
||||||
:description="uploadDescription"
|
<button
|
||||||
:upload-status="isSubmitting ? 'uploading' : 'idle'"
|
type="submit"
|
||||||
@file-selected="handleFileSelected"
|
:disabled="isSubmitting"
|
||||||
@files-selected="handleFilesSelected"
|
class="w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold py-4 px-6 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg relative overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:hover:scale-100"
|
||||||
@file-drop="handleFileDrop"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else key="text" class="text-shell">
|
|
||||||
<TextInputArea
|
|
||||||
v-model="textContent"
|
|
||||||
:rows="9"
|
|
||||||
:placeholder="t('send.uploadArea.textInput')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<ExpirationSelector
|
|
||||||
v-model:expiration-method="expirationMethod"
|
|
||||||
v-model:expiration-value="expirationValue"
|
|
||||||
:options="expirationOptions"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
class="group flex h-14 w-full items-center justify-center rounded-2xl px-5 text-sm font-semibold text-white transition disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-emerald-500 hover:bg-emerald-400'
|
|
||||||
: 'bg-slate-950 hover:bg-slate-800'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="isSubmitting"
|
|
||||||
class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white"
|
|
||||||
></span>
|
|
||||||
<SendIcon v-else class="mr-2 h-4 w-4 transition group-hover:translate-x-0.5" />
|
|
||||||
{{ isSubmitting ? t('send.submitting') : t('send.submit') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside
|
|
||||||
class="flex min-h-0 flex-col justify-between rounded-[28px] border p-5 shadow-sm sm:p-8 lg:min-h-[520px]"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-slate-950/45 shadow-black/20'
|
|
||||||
: 'border-white/80 bg-white/70 shadow-slate-200/70'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex min-w-0 items-center gap-3 text-left"
|
|
||||||
@click="toRetrieve"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-emerald-400/15 text-emerald-200'
|
|
||||||
: 'bg-emerald-50 text-emerald-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<BoxIcon class="h-5 w-5" />
|
|
||||||
</span>
|
|
||||||
<span class="min-w-0">
|
|
||||||
<span class="block truncate text-lg font-semibold">
|
|
||||||
{{ config.name }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="mt-1 block text-xs"
|
|
||||||
:class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']"
|
|
||||||
>
|
|
||||||
FileCodeBox
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mt-8 max-w-xl lg:mt-14">
|
|
||||||
<p
|
|
||||||
class="mb-4 inline-flex rounded-full px-3 py-1 text-xs font-medium"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-emerald-400/10 text-emerald-200'
|
|
||||||
: 'bg-emerald-50 text-emerald-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ t('send.secureEncryption') }}
|
|
||||||
</p>
|
|
||||||
<h2 class="text-4xl font-semibold leading-tight sm:text-5xl">
|
|
||||||
{{ t('nav.sendFile') }}
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
class="mt-5 max-w-lg text-sm leading-7"
|
|
||||||
:class="[isDarkMode ? 'text-slate-300' : 'text-slate-600']"
|
|
||||||
>
|
|
||||||
{{ config.description || t('common.appDescription') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-7 grid gap-3 sm:grid-cols-3 lg:mt-10 lg:grid-cols-1 xl:grid-cols-3">
|
|
||||||
<div
|
|
||||||
v-for="item in workspaceStats"
|
|
||||||
:key="item.label"
|
|
||||||
class="rounded-2xl border px-4 py-3"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'border-white/10 bg-white/[0.04]' : 'border-slate-200/70 bg-white/75'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']">
|
|
||||||
{{ item.label }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 truncate text-sm font-semibold">
|
|
||||||
{{ item.value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-2xl border p-4 text-left transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-white/[0.04] hover:bg-white/[0.07]'
|
|
||||||
: 'border-slate-200 bg-white hover:bg-slate-50'
|
|
||||||
]"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<ClipboardListIcon class="h-5 w-5 text-emerald-500" />
|
|
||||||
<p class="mt-3 text-sm font-medium">{{ t('send.sendRecords') }}</p>
|
|
||||||
<p class="mt-1 text-xs" :class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']">
|
|
||||||
{{ t('send.workspace.historyCount', { count: sendRecords.length }) }}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-2xl border p-4 text-left transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-white/10 bg-white/[0.04] hover:bg-white/[0.07]'
|
|
||||||
: 'border-slate-200 bg-white hover:bg-slate-50'
|
|
||||||
]"
|
|
||||||
@click="viewLatestRecord"
|
|
||||||
>
|
|
||||||
<ClockIcon class="h-5 w-5 text-slate-400" />
|
|
||||||
<p class="mt-3 text-sm font-medium">{{ t('send.workspace.latestRecord') }}</p>
|
|
||||||
<p
|
|
||||||
class="mt-1 truncate text-xs"
|
|
||||||
:class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']"
|
|
||||||
>
|
|
||||||
{{ latestRecord ? latestRecord.retrieveCode : t('send.workspace.noRecord') }}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mt-7 rounded-2xl border p-4 lg:mt-10"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'border-white/10 bg-slate-900/45' : 'border-slate-200/70 bg-white/65'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<span
|
||||||
<ShieldCheckIcon class="mt-0.5 h-5 w-5 shrink-0 text-emerald-500" />
|
class="absolute top-0 left-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
|
||||||
<div>
|
></span>
|
||||||
<p class="text-sm font-medium">
|
<span class="relative z-10 flex items-center justify-center text-lg">
|
||||||
{{ t('send.workspace.security') }}
|
<svg
|
||||||
</p>
|
v-if="isSubmitting"
|
||||||
<p
|
class="w-6 h-6 mr-2 animate-spin"
|
||||||
class="mt-1 text-xs leading-5"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
:class="[isDarkMode ? 'text-slate-400' : 'text-slate-500']"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
{{ t('send.secureEncryption') }} · {{ expirationPreview }}
|
>
|
||||||
</p>
|
<circle
|
||||||
</div>
|
class="opacity-25"
|
||||||
</div>
|
cx="12"
|
||||||
</div>
|
cy="12"
|
||||||
</aside>
|
r="10"
|
||||||
</section>
|
stroke="currentColor"
|
||||||
</main>
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<SendIcon v-else class="w-6 h-6 mr-2" />
|
||||||
|
<span>{{ isSubmitting ? t('send.submitting') : t('send.submit') }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300">
|
||||||
|
{{ t('send.needRetrieveFile') }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-8 py-4 bg-opacity-50 flex justify-between items-center"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm flex items-center"
|
||||||
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||||
|
>
|
||||||
|
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||||
|
{{ t('send.secureEncryption') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
@click="toggleDrawer"
|
||||||
|
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
|
||||||
|
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||||
|
>
|
||||||
|
{{ t('send.sendRecords') }}
|
||||||
|
<ClipboardListIcon class="w-4 h-4 ml-1" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
||||||
<SentRecordList
|
<SentRecordList
|
||||||
@@ -294,29 +126,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { inject } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
ArchiveRestoreIcon,
|
|
||||||
BoxIcon,
|
|
||||||
ClipboardListIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CloudUploadIcon,
|
|
||||||
SendIcon,
|
SendIcon,
|
||||||
ShieldCheckIcon,
|
ClipboardListIcon,
|
||||||
TextCursorInputIcon
|
ShieldCheckIcon
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import PageHeader from '@/components/common/PageHeader.vue'
|
||||||
|
import SendTypeSelector from '@/components/common/SendTypeSelector.vue'
|
||||||
import FileUploadArea from '@/components/common/FileUploadArea.vue'
|
import FileUploadArea from '@/components/common/FileUploadArea.vue'
|
||||||
import ExpirationSelector from '@/components/common/ExpirationSelector.vue'
|
import ExpirationSelector from '@/components/common/ExpirationSelector.vue'
|
||||||
import TextInputArea from '@/components/common/TextInputArea.vue'
|
import TextInputArea from '@/components/common/TextInputArea.vue'
|
||||||
import SideDrawer from '@/components/common/SideDrawer.vue'
|
import SideDrawer from '@/components/common/SideDrawer.vue'
|
||||||
import SentRecordList from '@/components/common/SentRecordList.vue'
|
import SentRecordList from '@/components/common/SentRecordList.vue'
|
||||||
import SentRecordDetailModal from '@/components/common/SentRecordDetailModal.vue'
|
import SentRecordDetailModal from '@/components/common/SentRecordDetailModal.vue'
|
||||||
import { useInjectedDarkMode, useSendFlow } from '@/composables'
|
import { useSendFlow } from '@/composables'
|
||||||
import { getStorageUnit } from '@/utils/convert'
|
|
||||||
|
|
||||||
const isDarkMode = useInjectedDarkMode()
|
const isDarkMode = inject('isDarkMode')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {
|
const {
|
||||||
@@ -340,7 +168,6 @@ const {
|
|||||||
copySentRecordWgetCommand,
|
copySentRecordWgetCommand,
|
||||||
deleteRecord,
|
deleteRecord,
|
||||||
getQRCodeValue,
|
getQRCodeValue,
|
||||||
getUnit,
|
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
handleFileSelected,
|
handleFileSelected,
|
||||||
handleFilesSelected,
|
handleFilesSelected,
|
||||||
@@ -350,95 +177,102 @@ const {
|
|||||||
viewDetails
|
viewDetails
|
||||||
} = useSendFlow()
|
} = useSendFlow()
|
||||||
|
|
||||||
const selectedFileCount = computed(() =>
|
|
||||||
selectedFiles.value.length > 0 ? selectedFiles.value.length : selectedFile.value ? 1 : 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedPayloadLabel = computed(() => {
|
|
||||||
if (sendType.value === 'text') {
|
|
||||||
return t('send.workspace.textDraft', { count: textContent.value.trim().length })
|
|
||||||
}
|
|
||||||
if (selectedFileCount.value > 0) {
|
|
||||||
return t('send.workspace.fileReady', { count: selectedFileCount.value })
|
|
||||||
}
|
|
||||||
return t('send.workspace.awaitingFile')
|
|
||||||
})
|
|
||||||
|
|
||||||
const latestRecord = computed(() => {
|
|
||||||
if (sendRecords.value.length === 0) return null
|
|
||||||
return sendRecords.value[sendRecords.value.length - 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
const expirationPreview = computed(() => {
|
|
||||||
if (expirationMethod.value === 'forever') {
|
|
||||||
return getUnit('forever')
|
|
||||||
}
|
|
||||||
return `${expirationValue.value || 1} ${getUnit(expirationMethod.value)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const workspaceStats = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('send.workspace.uploadLimit'),
|
|
||||||
value: getStorageUnit(config.value.uploadSize)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('send.workspace.uploadMode'),
|
|
||||||
value: config.value.enableChunk
|
|
||||||
? t('send.workspace.chunkMode')
|
|
||||||
: t('send.workspace.standardMode')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('send.workspace.guestPolicy'),
|
|
||||||
value: config.value.openUpload ? t('send.workspace.guestOpen') : t('send.workspace.guestClosed')
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const toRetrieve = () => {
|
const toRetrieve = () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewLatestRecord = () => {
|
|
||||||
if (latestRecord.value) {
|
|
||||||
viewDetails(latestRecord.value)
|
|
||||||
} else {
|
|
||||||
toggleDrawer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.transfer-page::before {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgb(148 163 184 / 0.12) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgb(148 163 184 / 0.12) 1px, transparent 1px);
|
|
||||||
background-size: 56px 56px;
|
|
||||||
mask-image: linear-gradient(to bottom, black, transparent 82%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition:
|
transition:
|
||||||
opacity 0.22s ease,
|
opacity 0.3s ease,
|
||||||
transform 0.22s ease;
|
transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.upload-shell > div) {
|
.fade-enter-to,
|
||||||
min-height: 18rem;
|
.fade-leave-from {
|
||||||
border-radius: 1.5rem;
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.text-shell textarea) {
|
select option {
|
||||||
min-height: 18rem;
|
padding: 8px;
|
||||||
border-radius: 1.5rem;
|
margin: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:checked {
|
||||||
|
background: linear-gradient(to right, rgb(99 102 241 / 0.5), rgb(168 85 247 / 0.5)) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark select option:checked {
|
||||||
|
background: linear-gradient(to right, rgb(99 102 241 / 0.7), rgb(168 85 247 / 0.7)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:hover {
|
||||||
|
background-color: rgb(99 102 241 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark select option:hover {
|
||||||
|
background-color: rgb(99 102 241 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 */
|
||||||
|
.custom-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.4) rgba(243, 244, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(243, 244, 246, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.6), rgba(168, 85, 247, 0.6));
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式下的滚动条样式 */
|
||||||
|
.dark .custom-scrollbar {
|
||||||
|
scrollbar-color: rgba(75, 85, 99, 0.6) rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.7), rgba(168, 85, 247, 0.7));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.9), rgba(168, 85, 247, 0.9));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user