feat: redesign send workspace experience
This commit is contained in:
@@ -1,14 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="rounded-xl p-8 flex flex-col items-center justify-center border-2 border-dashed transition-all duration-300 group cursor-pointer relative"
|
class="rounded-2xl p-8 flex flex-col items-center justify-center border-2 border-dashed transition-all duration-300 group cursor-pointer relative min-h-72 overflow-hidden"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'bg-gray-800 bg-opacity-50 border-gray-600 hover:border-indigo-500'
|
? 'bg-gray-800/60 border-gray-600 hover:border-indigo-400'
|
||||||
: 'bg-gray-100 border-gray-300 hover:border-indigo-500',
|
: 'bg-white/80 border-gray-300 hover:border-indigo-500',
|
||||||
|
isDragActive
|
||||||
|
? isDarkMode
|
||||||
|
? 'border-indigo-400 bg-indigo-500/10 shadow-lg shadow-indigo-900/30'
|
||||||
|
: 'border-indigo-500 bg-indigo-50 shadow-lg shadow-indigo-100'
|
||||||
|
: '',
|
||||||
statusClass
|
statusClass
|
||||||
]"
|
]"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-busy="isUploading"
|
||||||
@click="triggerFileUpload"
|
@click="triggerFileUpload"
|
||||||
@dragover.prevent
|
@keydown.enter.prevent="triggerFileUpload"
|
||||||
|
@keydown.space.prevent="triggerFileUpload"
|
||||||
|
@dragenter.prevent="handleDragEnter"
|
||||||
|
@dragover.prevent="handleDragOver"
|
||||||
|
@dragleave.prevent="handleDragLeave"
|
||||||
@drop.prevent="handleFileDrop"
|
@drop.prevent="handleFileDrop"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -20,6 +32,18 @@
|
|||||||
:disabled="isUploading"
|
:disabled="isUploading"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 opacity-0 transition-opacity duration-300"
|
||||||
|
:class="[isDragActive ? 'opacity-100' : '']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-3 rounded-2xl border"
|
||||||
|
:class="[
|
||||||
|
isDarkMode ? 'border-indigo-300/30 bg-indigo-400/5' : 'border-indigo-300 bg-indigo-50/80'
|
||||||
|
]"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
|
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
|
||||||
<BorderProgressBar :progress="progress" />
|
<BorderProgressBar :progress="progress" />
|
||||||
</div>
|
</div>
|
||||||
@@ -27,24 +51,20 @@
|
|||||||
<!-- 上传状态图标 -->
|
<!-- 上传状态图标 -->
|
||||||
<component
|
<component
|
||||||
:is="statusIcon"
|
:is="statusIcon"
|
||||||
:class="['w-16 h-16 transition-colors duration-300', statusIconClass]"
|
:class="['relative z-10 w-16 h-16 transition-colors duration-300', statusIconClass]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 文件名或占位文本 -->
|
<!-- 文件名或占位文本 -->
|
||||||
<p
|
<p
|
||||||
:class="[
|
:class="[
|
||||||
'mt-4 text-sm transition-colors duration-300 w-full text-center',
|
'relative z-10 mt-4 text-sm transition-colors duration-300 w-full text-center',
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'text-gray-400 group-hover:text-indigo-400'
|
? 'text-gray-400 group-hover:text-indigo-400'
|
||||||
: 'text-gray-600 group-hover:text-indigo-600'
|
: 'text-gray-600 group-hover:text-indigo-600'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span v-if="selectedFiles && selectedFiles.length > 1" class="block">
|
<span v-if="selectedFiles && selectedFiles.length > 1" class="block">
|
||||||
<span
|
<span v-for="(f, i) in selectedFiles" :key="i" class="block truncate">{{ f.name }}</span>
|
||||||
v-for="(f, i) in selectedFiles"
|
|
||||||
:key="i"
|
|
||||||
class="block truncate"
|
|
||||||
>{{ f.name }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="block truncate">
|
<span v-else class="block truncate">
|
||||||
{{ displayText }}
|
{{ displayText }}
|
||||||
@@ -52,12 +72,33 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 状态描述或默认描述 -->
|
<!-- 状态描述或默认描述 -->
|
||||||
<p :class="['mt-2 text-xs', statusDescriptionClass]">
|
<p :class="['relative z-10 mt-2 text-xs text-center leading-5', statusDescriptionClass]">
|
||||||
{{ statusDescription }}
|
{{ statusDescription }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedFiles.length > 1"
|
||||||
|
class="relative z-10 mt-4 flex flex-wrap items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="file in selectedFiles.slice(0, 3)"
|
||||||
|
:key="`${file.name}-${file.size}`"
|
||||||
|
class="max-w-40 truncate rounded-full px-3 py-1 text-xs"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-700/80 text-gray-200' : 'bg-gray-100 text-gray-700']"
|
||||||
|
>
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selectedFiles.length > 3"
|
||||||
|
class="rounded-full px-3 py-1 text-xs"
|
||||||
|
:class="[isDarkMode ? 'bg-indigo-500/20 text-indigo-200' : 'bg-indigo-100 text-indigo-700']"
|
||||||
|
>
|
||||||
|
+{{ selectedFiles.length - 3 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 进度详情(上传中显示) -->
|
<!-- 进度详情(上传中显示) -->
|
||||||
<div v-if="isUploading && showProgressDetails" class="mt-3 w-full">
|
<div v-if="isUploading && showProgressDetails" class="relative z-10 mt-3 w-full">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between text-xs mb-1"
|
class="flex justify-between text-xs mb-1"
|
||||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
|
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
|
||||||
@@ -148,6 +189,7 @@ const isDarkMode = useInjectedDarkMode()
|
|||||||
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.placeholder'))
|
const placeholderText = computed(() => props.placeholder || t('send.uploadArea.placeholder'))
|
||||||
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const isDragActive = ref(false)
|
||||||
|
|
||||||
const isUploading = computed(() => {
|
const isUploading = computed(() => {
|
||||||
return ['uploading', 'initializing', 'confirming'].includes(props.uploadStatus)
|
return ['uploading', 'initializing', 'confirming'].includes(props.uploadStatus)
|
||||||
@@ -245,6 +287,24 @@ const triggerFileUpload = () => {
|
|||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragEnter = () => {
|
||||||
|
if (isUploading.value) return
|
||||||
|
isDragActive.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = () => {
|
||||||
|
if (isUploading.value) return
|
||||||
|
isDragActive.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = (event: DragEvent) => {
|
||||||
|
const target = event.currentTarget as HTMLElement
|
||||||
|
const relatedTarget = event.relatedTarget as Node | null
|
||||||
|
if (!relatedTarget || !target.contains(relatedTarget)) {
|
||||||
|
isDragActive.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleFileUpload = (event: Event) => {
|
const handleFileUpload = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
const files = target.files
|
const files = target.files
|
||||||
@@ -261,6 +321,7 @@ const handleFileUpload = (event: Event) => {
|
|||||||
|
|
||||||
const handleFileDrop = (event: DragEvent) => {
|
const handleFileDrop = (event: DragEvent) => {
|
||||||
// 上传中不允许拖放
|
// 上传中不允许拖放
|
||||||
|
isDragActive.value = false
|
||||||
if (isUploading.value) return
|
if (isUploading.value) return
|
||||||
emit('fileDrop', event)
|
emit('fileDrop', event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,29 @@ export default {
|
|||||||
needRetrieveFile: 'Need to retrieve? Click here',
|
needRetrieveFile: 'Need to retrieve? Click here',
|
||||||
sendRecords: 'Send Records',
|
sendRecords: 'Send Records',
|
||||||
secureEncryption: 'Secure Encryption',
|
secureEncryption: 'Secure Encryption',
|
||||||
|
workspace: {
|
||||||
|
uploadLimit: 'Single File Limit',
|
||||||
|
uploadMode: 'Upload Mode',
|
||||||
|
standardMode: 'Standard',
|
||||||
|
chunkMode: 'Chunked',
|
||||||
|
guestPolicy: 'Guest Upload',
|
||||||
|
guestOpen: 'Allowed',
|
||||||
|
guestClosed: 'Disabled',
|
||||||
|
currentTask: 'Current Task',
|
||||||
|
awaitingFile: 'Waiting for files',
|
||||||
|
fileReady: '{count} files ready',
|
||||||
|
textDraft: 'Text draft, {count} chars',
|
||||||
|
payload: 'Payload',
|
||||||
|
expirationPreview: 'Expiration',
|
||||||
|
security: 'Security',
|
||||||
|
latestRecord: 'Latest Send',
|
||||||
|
noRecord: 'No records yet',
|
||||||
|
copyLink: 'Copy Link',
|
||||||
|
viewDetail: 'View Detail',
|
||||||
|
historyTitle: 'Send History',
|
||||||
|
historyCount: '{count} records',
|
||||||
|
openRecords: 'Open send records'
|
||||||
|
},
|
||||||
fileDetails: 'File Details',
|
fileDetails: 'File Details',
|
||||||
expirationMethod: 'Expiration Method',
|
expirationMethod: 'Expiration Method',
|
||||||
expiration: {
|
expiration: {
|
||||||
|
|||||||
@@ -224,6 +224,29 @@ export default {
|
|||||||
needRetrieveFile: '需要取件?点击这里',
|
needRetrieveFile: '需要取件?点击这里',
|
||||||
sendRecords: '发件记录',
|
sendRecords: '发件记录',
|
||||||
secureEncryption: '安全加密',
|
secureEncryption: '安全加密',
|
||||||
|
workspace: {
|
||||||
|
uploadLimit: '单文件上限',
|
||||||
|
uploadMode: '上传模式',
|
||||||
|
standardMode: '标准上传',
|
||||||
|
chunkMode: '分片上传',
|
||||||
|
guestPolicy: '游客上传',
|
||||||
|
guestOpen: '允许',
|
||||||
|
guestClosed: '已关闭',
|
||||||
|
currentTask: '当前任务',
|
||||||
|
awaitingFile: '等待选择文件',
|
||||||
|
fileReady: '已选择 {count} 个文件',
|
||||||
|
textDraft: '文本草稿 {count} 字',
|
||||||
|
payload: '发送内容',
|
||||||
|
expirationPreview: '有效期',
|
||||||
|
security: '安全状态',
|
||||||
|
latestRecord: '最近一次发送',
|
||||||
|
noRecord: '暂无记录',
|
||||||
|
copyLink: '复制链接',
|
||||||
|
viewDetail: '查看详情',
|
||||||
|
historyTitle: '发送历史',
|
||||||
|
historyCount: '{count} 条记录',
|
||||||
|
openRecords: '打开发送记录'
|
||||||
|
},
|
||||||
fileDetail: {
|
fileDetail: {
|
||||||
title: '文件详情',
|
title: '文件详情',
|
||||||
content: '文件内容',
|
content: '文件内容',
|
||||||
|
|||||||
+256
-40
@@ -1,23 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300"
|
class="min-h-screen px-4 py-16 transition-colors duration-300 sm:px-6 lg:px-8"
|
||||||
@paste.prevent="handlePaste"
|
@paste.prevent="handlePaste"
|
||||||
>
|
>
|
||||||
<div
|
<main class="mx-auto flex min-h-[calc(100vh-8rem)] w-full max-w-6xl items-center">
|
||||||
class="rounded-3xl shadow-2xl overflow-hidden border w-full max-w-md transition-colors duration-300"
|
<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="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'bg-white bg-opacity-10 backdrop-filter backdrop-blur-xl border-gray-700'
|
? 'border-gray-700 bg-gray-900/70 shadow-black/30'
|
||||||
: 'bg-white border-gray-200'
|
: 'border-white/80 bg-white/90 shadow-indigo-100'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="p-8">
|
<div class="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<PageHeader :title="config.name" @title-click="toRetrieve" />
|
<div>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-8">
|
<button
|
||||||
<SendTypeSelector
|
type="button"
|
||||||
:selected-type="sendType"
|
@click="toRetrieve"
|
||||||
@update:selected-type="sendType = $event"
|
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
|
||||||
|
v-for="item in workspaceStats"
|
||||||
|
:key="item.label"
|
||||||
|
class="rounded-2xl border px-4 py-3"
|
||||||
|
:class="[
|
||||||
|
isDarkMode ? 'border-gray-700 bg-gray-800/60' : 'border-gray-100 bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||||
|
{{ item.label }}
|
||||||
|
</p>
|
||||||
|
<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" />
|
||||||
|
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8">
|
<div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8">
|
||||||
@@ -26,13 +78,18 @@
|
|||||||
: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 v-model="textContent" :placeholder="t('send.uploadArea.textInput')" />
|
<TextInputArea
|
||||||
|
v-model="textContent"
|
||||||
|
:rows="10"
|
||||||
|
:placeholder="t('send.uploadArea.textInput')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<ExpirationSelector
|
<ExpirationSelector
|
||||||
@@ -40,14 +97,13 @@
|
|||||||
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="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"
|
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"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute top-0 left-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
|
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"
|
||||||
></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
|
||||||
@@ -76,34 +132,139 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-6 text-center">
|
</section>
|
||||||
<router-link to="/" class="text-indigo-400 hover:text-indigo-300 transition duration-300">
|
|
||||||
{{ t('send.needRetrieveFile') }}
|
<aside class="grid gap-6">
|
||||||
</router-link>
|
<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.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
|
||||||
class="px-8 py-4 bg-opacity-50 flex justify-between items-center"
|
v-for="item in readinessItems"
|
||||||
:class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']"
|
:key="item.label"
|
||||||
|
class="flex items-center justify-between gap-4 rounded-2xl px-4 py-3"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-800/70' : 'bg-gray-50']"
|
||||||
>
|
>
|
||||||
<span
|
<dt class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||||
class="text-sm flex items-center"
|
{{ item.label }}
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="text-right text-sm font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-gray-100' : 'text-gray-900']"
|
||||||
>
|
>
|
||||||
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
{{ item.value }}
|
||||||
{{ t('send.secureEncryption') }}
|
</dd>
|
||||||
</span>
|
</div>
|
||||||
|
</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
|
||||||
@click="toggleDrawer"
|
type="button"
|
||||||
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
|
class="rounded-xl px-4 py-2 text-sm font-medium transition"
|
||||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
: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.sendRecords') }}
|
{{ t('send.workspace.copyLink') }}
|
||||||
<ClipboardListIcon class="w-4 h-4 ml-1" />
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
||||||
|
class="inline-flex h-11 w-11 items-center justify-center rounded-full transition"
|
||||||
|
:class="[
|
||||||
|
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" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
<SideDrawer :visible="showDrawer" :title="t('send.sendRecords')" @close="toggleDrawer">
|
||||||
<SentRecordList
|
<SentRecordList
|
||||||
@@ -126,15 +287,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject } from 'vue'
|
import { computed, 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 { SendIcon, ClipboardListIcon, ShieldCheckIcon } from 'lucide-vue-next'
|
||||||
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'
|
||||||
@@ -143,6 +299,7 @@ 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()
|
||||||
@@ -173,6 +330,7 @@ const {
|
|||||||
handleFilesSelected,
|
handleFilesSelected,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
getUnit,
|
||||||
toggleDrawer,
|
toggleDrawer,
|
||||||
viewDetails
|
viewDetails
|
||||||
} = useSendFlow()
|
} = useSendFlow()
|
||||||
@@ -180,6 +338,64 @@ 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