refactor: restore public transfer pages
This commit is contained in:
+29
-428
@@ -1,351 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen px-4 py-12 transition-colors duration-300 sm:px-6 lg:px-8">
|
|
||||||
<main class="mx-auto flex min-h-[calc(100vh-6rem)] w-full max-w-6xl items-center">
|
|
||||||
<div class="grid w-full gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)]">
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 shadow-2xl transition-colors duration-300 sm:p-8"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-gray-700 bg-gray-900/70 shadow-black/30'
|
|
||||||
: 'border-white/80 bg-white/90 shadow-sky-100'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="toSend"
|
|
||||||
class="text-left text-3xl font-extrabold leading-tight transition-colors"
|
|
||||||
:class="[
|
|
||||||
isDarkMode ? 'text-white hover:text-sky-200' : 'text-gray-950 hover:text-sky-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ config.name }}
|
|
||||||
</button>
|
|
||||||
<p
|
|
||||||
class="mt-3 max-w-2xl text-sm leading-6"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ config.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
to="/send"
|
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full border px-4 py-2 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-gray-700 text-sky-200 hover:border-sky-400 hover:bg-sky-500/10'
|
|
||||||
: 'border-sky-100 bg-sky-50 text-sky-700 hover:border-sky-200 hover:bg-sky-100'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<SendIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('retrieve.workspace.sendFile') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 grid gap-3 sm:grid-cols-3">
|
|
||||||
<div
|
<div
|
||||||
v-for="item in workspaceStats"
|
class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300"
|
||||||
:key="item.label"
|
>
|
||||||
class="rounded-2xl border px-4 py-3"
|
<div class="w-full max-w-md relative z-10">
|
||||||
|
<div
|
||||||
|
class="rounded-3xl shadow-2xl overflow-hidden border transform transition-all duration-300"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode ? 'border-gray-700 bg-gray-800/60' : 'border-gray-100 bg-gray-50'
|
isDarkMode
|
||||||
|
? 'bg-gray-800 bg-opacity-50 backdrop-filter backdrop-blur-xl border-gray-700'
|
||||||
|
: 'bg-white border-gray-200'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
<div class="p-8">
|
||||||
{{ item.label }}
|
<PageHeader :title="config.name" @title-click="toSend" />
|
||||||
</p>
|
<RetrieveForm
|
||||||
<p
|
|
||||||
class="mt-1 truncate text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ item.value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="mt-8 space-y-4" @submit.prevent="inspectCode">
|
|
||||||
<label
|
|
||||||
for="retrieve-code"
|
|
||||||
class="block text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-200' : 'text-gray-800']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.codeInput.label') }}
|
|
||||||
</label>
|
|
||||||
<div class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto_auto]">
|
|
||||||
<input
|
|
||||||
id="retrieve-code"
|
|
||||||
v-model="code"
|
v-model="code"
|
||||||
type="text"
|
:input-status="inputStatus"
|
||||||
inputmode="text"
|
:error="!!error"
|
||||||
maxlength="5"
|
@submit="handleSubmit"
|
||||||
autocomplete="one-time-code"
|
ref="retrieveFormRef"
|
||||||
class="h-12 w-full rounded-2xl border px-4 text-lg font-semibold tracking-[0.2em] outline-none transition focus:ring-2 focus:ring-sky-500"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-gray-700 bg-gray-800 text-white placeholder-gray-500'
|
|
||||||
: 'border-gray-200 bg-white text-gray-950 placeholder-gray-400',
|
|
||||||
error ? 'border-red-400 focus:ring-red-400' : ''
|
|
||||||
]"
|
|
||||||
:placeholder="t('retrieve.codeInput.placeholder')"
|
|
||||||
:readonly="isWorking"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
type="submit"
|
|
||||||
variant="secondary"
|
|
||||||
:loading="isInspecting"
|
|
||||||
:disabled="!hasValidCode || isRetrieving"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<SearchIcon class="mr-2 h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
{{
|
|
||||||
isInspecting
|
|
||||||
? t('retrieve.workspace.inspecting')
|
|
||||||
: t('retrieve.workspace.inspect')
|
|
||||||
}}
|
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
type="button"
|
|
||||||
:loading="isRetrieving"
|
|
||||||
:disabled="!hasValidCode || isInspecting"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<DownloadCloudIcon class="mr-2 h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
{{ isRetrieving ? t('retrieve.workspace.retrieving') : t('retrieve.submit') }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
<PageFooter
|
||||||
<p v-if="error" class="text-sm text-red-500">
|
:link-text="$t('retrieve.needSendFile')"
|
||||||
{{ error }}
|
link-to="/send"
|
||||||
</p>
|
:drawer-text="$t('retrieve.recordsDrawer')"
|
||||||
</form>
|
@toggle-drawer="toggleDrawer"
|
||||||
|
|
||||||
<section
|
|
||||||
class="mt-8 rounded-3xl border p-5"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-800/60' : 'border-gray-100 bg-gray-50']"
|
|
||||||
>
|
|
||||||
<div v-if="inspectedFile" class="space-y-5">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div class="flex min-w-0 items-start gap-3">
|
|
||||||
<div
|
|
||||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
|
|
||||||
:class="[isDarkMode ? 'bg-sky-500/15 text-sky-200' : 'bg-sky-100 text-sky-700']"
|
|
||||||
>
|
|
||||||
<FileTextIcon v-if="inspectedFile.is_text" class="h-5 w-5" />
|
|
||||||
<FileIcon v-else class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p
|
|
||||||
class="truncate text-lg font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ inspectedFile.name }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-1 text-sm"
|
|
||||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
|
|
||||||
>
|
|
||||||
{{ inspectedTypeLabel }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="rounded-full px-3 py-1 text-xs font-semibold"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-emerald-500/15 text-emerald-200'
|
|
||||||
: 'bg-emerald-100 text-emerald-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.ready') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div
|
|
||||||
v-for="item in inspectedMetaItems"
|
|
||||||
:key="item.label"
|
|
||||||
class="rounded-2xl px-4 py-3"
|
|
||||||
:class="[isDarkMode ? 'bg-gray-900/70' : 'bg-white']"
|
|
||||||
>
|
|
||||||
<dt class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
|
||||||
{{ item.label }}
|
|
||||||
</dt>
|
|
||||||
<dd
|
|
||||||
class="mt-1 truncate text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ item.value }}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex min-h-48 flex-col items-center justify-center text-center">
|
|
||||||
<PackageSearchIcon
|
|
||||||
class="h-12 w-12"
|
|
||||||
:class="[isDarkMode ? 'text-gray-500' : 'text-gray-300']"
|
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
class="mt-4 text-lg font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.noPreview') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 max-w-md text-sm"
|
|
||||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.noPreviewDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="grid gap-6">
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.latestRecord') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-xl font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ latestRecord ? latestRecord.filename : t('retrieve.workspace.noRecord') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ArchiveRestoreIcon class="h-10 w-10 text-emerald-400" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="latestRecord" class="mt-5 space-y-3">
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div
|
|
||||||
class="rounded-2xl px-4 py-3"
|
|
||||||
:class="[isDarkMode ? 'bg-gray-800/70' : 'bg-gray-50']"
|
|
||||||
>
|
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
|
||||||
{{ t('retrieve.workspace.currentCode') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-1 text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ latestRecord.code }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="rounded-2xl px-4 py-3"
|
|
||||||
:class="[isDarkMode ? 'bg-gray-800/70' : 'bg-gray-50']"
|
|
||||||
>
|
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
|
||||||
{{ t('retrieve.workspace.fileSize') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-1 text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ latestRecord.size }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<SideDrawer :visible="showDrawer" :title="$t('retrieve.recordsDrawer')" @close="toggleDrawer">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-2 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-gray-800 text-gray-100 hover:bg-gray-700'
|
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
|
||||||
]"
|
|
||||||
@click="viewDetails(latestRecord)"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.viewDetail') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-2 text-sm font-medium text-white transition hover:opacity-90"
|
|
||||||
:class="[isDarkMode ? 'bg-sky-500' : 'bg-sky-600']"
|
|
||||||
@click="downloadRecord(latestRecord)"
|
|
||||||
>
|
|
||||||
{{ t('fileDetail.download') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.recordsDrawer') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-xl font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.historyCount', { count: records.length }) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-gray-800 text-sky-200 hover:bg-gray-700'
|
|
||||||
: 'bg-sky-50 text-sky-700 hover:bg-sky-100'
|
|
||||||
]"
|
|
||||||
:aria-label="t('retrieve.workspace.openRecords')"
|
|
||||||
>
|
|
||||||
<ClipboardListIcon class="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<ShieldCheckIcon class="h-10 w-10 text-teal-400" />
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('retrieve.workspace.security') }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
|
||||||
{{ t('retrieve.workspace.securityState') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<SideDrawer :visible="showDrawer" :title="t('retrieve.recordsDrawer')" @close="toggleDrawer">
|
|
||||||
<FileRecordList
|
<FileRecordList
|
||||||
:records="records"
|
:records="records"
|
||||||
@view-details="viewDetails"
|
@view-details="viewDetails"
|
||||||
@@ -371,22 +56,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, onMounted, watch } from 'vue'
|
import { inject, onMounted, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import PageHeader from '@/components/common/PageHeader.vue'
|
||||||
ArchiveRestoreIcon,
|
import RetrieveForm from '@/components/common/RetrieveForm.vue'
|
||||||
ClipboardListIcon,
|
import PageFooter from '@/components/common/PageFooter.vue'
|
||||||
DownloadCloudIcon,
|
|
||||||
FileIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
PackageSearchIcon,
|
|
||||||
SearchIcon,
|
|
||||||
SendIcon,
|
|
||||||
ShieldCheckIcon
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import BaseButton from '@/components/common/BaseButton.vue'
|
|
||||||
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'
|
||||||
@@ -395,18 +70,13 @@ import { useRetrieveFlow } from '@/composables'
|
|||||||
import { useConfigStore } from '@/stores/configStore'
|
import { useConfigStore } from '@/stores/configStore'
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
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()
|
||||||
const { config } = storeToRefs(configStore)
|
const { config } = storeToRefs(configStore)
|
||||||
const {
|
const {
|
||||||
code,
|
code,
|
||||||
inspectedFile,
|
inputStatus,
|
||||||
isInspecting,
|
|
||||||
isRetrieving,
|
|
||||||
isWorking,
|
|
||||||
hasValidCode,
|
|
||||||
error,
|
error,
|
||||||
records,
|
records,
|
||||||
selectedRecord,
|
selectedRecord,
|
||||||
@@ -419,7 +89,6 @@ const {
|
|||||||
deleteRecord,
|
deleteRecord,
|
||||||
downloadRecord,
|
downloadRecord,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
inspectCode,
|
|
||||||
showContentPreview,
|
showContentPreview,
|
||||||
toggleDrawer,
|
toggleDrawer,
|
||||||
viewDetails
|
viewDetails
|
||||||
@@ -429,84 +98,16 @@ const toSend = () => {
|
|||||||
router.push('/send')
|
router.push('/send')
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
|
||||||
if (bytes <= 0) return `0 ${t('fileSize.bytes')}`
|
|
||||||
const units = [t('fileSize.bytes'), t('fileSize.kb'), t('fileSize.mb'), t('fileSize.gb')]
|
|
||||||
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
||||||
return `${parseFloat((bytes / Math.pow(1024, index)).toFixed(2))} ${units[index]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => {
|
|
||||||
if (!value) return t('retrieve.workspace.noExpiry')
|
|
||||||
const date = new Date(value)
|
|
||||||
if (Number.isNaN(date.getTime())) return t('retrieve.workspace.noExpiry')
|
|
||||||
return date.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatRemainingDownloads = (value?: number | null) => {
|
|
||||||
if (value === null || value === undefined) return t('retrieve.workspace.unlimited')
|
|
||||||
return t('retrieve.workspace.remainingCount', { count: value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestRecord = computed(() => {
|
|
||||||
if (records.value.length === 0) return null
|
|
||||||
return records.value[records.value.length - 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
const inspectedTypeLabel = computed(() => {
|
|
||||||
if (!inspectedFile.value) return ''
|
|
||||||
return inspectedFile.value.is_text
|
|
||||||
? t('retrieve.workspace.textType')
|
|
||||||
: t('retrieve.workspace.fileType')
|
|
||||||
})
|
|
||||||
|
|
||||||
const inspectedMetaItems = computed(() => {
|
|
||||||
if (!inspectedFile.value) return []
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.fileSize'),
|
|
||||||
value: formatFileSize(inspectedFile.value.size)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.expiresAt'),
|
|
||||||
value: formatDate(inspectedFile.value.expires_at || inspectedFile.value.expired_at)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.remainingDownloads'),
|
|
||||||
value: formatRemainingDownloads(inspectedFile.value.remaining_downloads)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.usedCount'),
|
|
||||||
value: String(inspectedFile.value.used_count || 0)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const workspaceStats = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.currentCode'),
|
|
||||||
value: code.value || t('retrieve.workspace.emptyCode')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('retrieve.workspace.previewState'),
|
|
||||||
value: inspectedFile.value ? t('retrieve.workspace.ready') : 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.trim().length === 5 && !isWorking.value) {
|
if (newCode.length === 5) {
|
||||||
void inspectCode()
|
void handleSubmit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+30
-253
@@ -1,74 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="min-h-screen px-4 py-16 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"
|
||||||
@paste.prevent="handlePaste"
|
@paste.prevent="handlePaste"
|
||||||
>
|
>
|
||||||
<main class="mx-auto flex min-h-[calc(100vh-8rem)] w-full max-w-6xl items-center">
|
|
||||||
<div class="grid w-full gap-6 lg:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]">
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 shadow-2xl transition-colors duration-300 sm:p-8"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-gray-700 bg-gray-900/70 shadow-black/30'
|
|
||||||
: 'border-white/80 bg-white/90 shadow-indigo-100'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="toRetrieve"
|
|
||||||
class="text-left text-3xl font-extrabold leading-tight transition-colors"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'text-white hover:text-indigo-200'
|
|
||||||
: 'text-gray-950 hover:text-indigo-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ config.name }}
|
|
||||||
</button>
|
|
||||||
<p
|
|
||||||
class="mt-3 max-w-2xl text-sm leading-6"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ config.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
to="/"
|
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full border px-4 py-2 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'border-gray-700 text-indigo-200 hover:border-indigo-400 hover:bg-indigo-500/10'
|
|
||||||
: 'border-indigo-100 bg-indigo-50 text-indigo-700 hover:border-indigo-200 hover:bg-indigo-100'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ t('send.needRetrieveFile') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 grid gap-3 sm:grid-cols-3">
|
|
||||||
<div
|
<div
|
||||||
v-for="item in workspaceStats"
|
class="rounded-3xl shadow-2xl overflow-hidden border w-full max-w-md transition-colors duration-300"
|
||||||
:key="item.label"
|
|
||||||
class="rounded-2xl border px-4 py-3"
|
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode ? 'border-gray-700 bg-gray-800/60' : 'border-gray-100 bg-gray-50'
|
isDarkMode
|
||||||
|
? 'bg-white bg-opacity-10 backdrop-filter backdrop-blur-xl border-gray-700'
|
||||||
|
: 'bg-white border-gray-200'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
<div class="p-8">
|
||||||
{{ item.label }}
|
<PageHeader :title="config.name" @title-click="toRetrieve" />
|
||||||
</p>
|
<form @submit.prevent="handleSubmit" class="space-y-8">
|
||||||
<p
|
|
||||||
class="mt-1 truncate text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
|
||||||
{{ item.value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" class="mt-8 space-y-7">
|
|
||||||
<SendTypeSelector :selected-type="sendType" @update:selected-type="sendType = $event" />
|
<SendTypeSelector :selected-type="sendType" @update:selected-type="sendType = $event" />
|
||||||
|
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
@@ -78,18 +23,13 @@
|
|||||||
:selected-files="selectedFiles"
|
:selected-files="selectedFiles"
|
||||||
:progress="uploadProgress"
|
:progress="uploadProgress"
|
||||||
:description="uploadDescription"
|
:description="uploadDescription"
|
||||||
:upload-status="isSubmitting ? 'uploading' : 'idle'"
|
|
||||||
@file-selected="handleFileSelected"
|
@file-selected="handleFileSelected"
|
||||||
@files-selected="handleFilesSelected"
|
@files-selected="handleFilesSelected"
|
||||||
@file-drop="handleFileDrop"
|
@file-drop="handleFileDrop"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else key="text" class="grid grid-cols-1 gap-8">
|
<div v-else key="text" class="grid grid-cols-1 gap-8">
|
||||||
<TextInputArea
|
<TextInputArea v-model="textContent" :placeholder="t('send.uploadArea.textInput')" />
|
||||||
v-model="textContent"
|
|
||||||
:rows="10"
|
|
||||||
:placeholder="t('send.uploadArea.textInput')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<ExpirationSelector
|
<ExpirationSelector
|
||||||
@@ -97,13 +37,14 @@
|
|||||||
v-model:expiration-value="expirationValue"
|
v-model:expiration-value="expirationValue"
|
||||||
:options="expirationOptions"
|
:options="expirationOptions"
|
||||||
/>
|
/>
|
||||||
|
<!-- 提交按钮 -->
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
class="group relative w-full overflow-hidden rounded-2xl bg-gray-950 px-6 py-4 text-white transition-all duration-300 hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:translate-y-0"
|
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"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inset-0 bg-gradient-to-r from-indigo-500 via-sky-500 to-emerald-400 opacity-90 transition-opacity duration-300 group-hover:opacity-100"
|
class="absolute top-0 left-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
|
||||||
></span>
|
></span>
|
||||||
<span class="relative z-10 flex items-center justify-center text-lg">
|
<span class="relative z-10 flex items-center justify-center text-lg">
|
||||||
<svg
|
<svg
|
||||||
@@ -132,139 +73,34 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
<div class="mt-6 text-center">
|
||||||
|
<router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300">
|
||||||
<aside class="grid gap-6">
|
{{ t('send.needRetrieveFile') }}
|
||||||
<section
|
</router-link>
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.currentTask') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-2xl font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ selectedPayloadLabel }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ShieldCheckIcon class="h-10 w-10 text-emerald-400" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="mt-6 space-y-3">
|
|
||||||
<div
|
<div
|
||||||
v-for="item in readinessItems"
|
class="px-8 py-4 bg-opacity-50 flex justify-between items-center"
|
||||||
:key="item.label"
|
:class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']"
|
||||||
class="flex items-center justify-between gap-4 rounded-2xl px-4 py-3"
|
|
||||||
:class="[isDarkMode ? 'bg-gray-800/70' : 'bg-gray-50']"
|
|
||||||
>
|
>
|
||||||
<dt class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
<span
|
||||||
{{ item.label }}
|
class="text-sm flex items-center"
|
||||||
</dt>
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||||
<dd
|
|
||||||
class="text-right text-sm font-semibold"
|
|
||||||
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
|
||||||
>
|
>
|
||||||
{{ item.value }}
|
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||||
</dd>
|
{{ t('send.secureEncryption') }}
|
||||||
</div>
|
</span>
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.latestRecord') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-xl font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ latestRecord ? latestRecord.retrieveCode : t('send.workspace.noRecord') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ClipboardListIcon class="h-10 w-10 text-sky-400" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="latestRecord" class="mt-5 space-y-3">
|
|
||||||
<p class="truncate text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']">
|
|
||||||
{{ latestRecord.filename }}
|
|
||||||
</p>
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-2 text-sm font-medium transition"
|
|
||||||
:class="[
|
|
||||||
isDarkMode
|
|
||||||
? 'bg-gray-800 text-gray-100 hover:bg-gray-700'
|
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
|
||||||
]"
|
|
||||||
@click="copySentRecordLink(latestRecord)"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.copyLink') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-2 text-sm font-medium text-white transition hover:opacity-90"
|
|
||||||
:class="[isDarkMode ? 'bg-indigo-500' : 'bg-indigo-600']"
|
|
||||||
@click="viewDetails(latestRecord)"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.viewDetail') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="rounded-3xl border p-6 transition-colors duration-300"
|
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/70' : 'border-white/80 bg-white/85']"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.historyTitle') }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-xl font-bold"
|
|
||||||
:class="[isDarkMode ? 'text-white' : 'text-gray-950']"
|
|
||||||
>
|
|
||||||
{{ t('send.workspace.historyCount', { count: sendRecords.length }) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full transition"
|
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
|
||||||
:class="[
|
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||||
isDarkMode
|
|
||||||
? 'bg-gray-800 text-indigo-200 hover:bg-gray-700'
|
|
||||||
: 'bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
|
|
||||||
]"
|
|
||||||
:aria-label="t('send.workspace.openRecords')"
|
|
||||||
>
|
>
|
||||||
<ClipboardListIcon class="h-5 w-5" />
|
{{ t('send.sendRecords') }}
|
||||||
|
<ClipboardListIcon class="w-4 h-4 ml-1" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
||||||
<SentRecordList
|
<SentRecordList
|
||||||
@@ -287,10 +123,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject } 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 { SendIcon, ClipboardListIcon, ShieldCheckIcon } from 'lucide-vue-next'
|
import { SendIcon, ClipboardListIcon, ShieldCheckIcon } from 'lucide-vue-next'
|
||||||
|
import PageHeader from '@/components/common/PageHeader.vue'
|
||||||
import SendTypeSelector from '@/components/common/SendTypeSelector.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'
|
||||||
@@ -299,7 +136,6 @@ 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 { useSendFlow } from '@/composables'
|
import { useSendFlow } from '@/composables'
|
||||||
import { getStorageUnit } from '@/utils/convert'
|
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
const isDarkMode = inject('isDarkMode')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -330,7 +166,6 @@ const {
|
|||||||
handleFilesSelected,
|
handleFilesSelected,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
getUnit,
|
|
||||||
toggleDrawer,
|
toggleDrawer,
|
||||||
viewDetails
|
viewDetails
|
||||||
} = useSendFlow()
|
} = useSendFlow()
|
||||||
@@ -338,64 +173,6 @@ const {
|
|||||||
const toRetrieve = () => {
|
const toRetrieve = () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
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 readinessItems = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('send.workspace.payload'),
|
|
||||||
value: selectedPayloadLabel.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('send.workspace.expirationPreview'),
|
|
||||||
value: expirationPreview.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('send.workspace.security'),
|
|
||||||
value: t('send.secureEncryption')
|
|
||||||
}
|
|
||||||
])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user