From 31154555164e17ee42ffe9a35212ca086aeb65d8 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 03:27:16 +0800 Subject: [PATCH] feat: redesign retrieve workspace --- src/composables/useRetrieveFlow.ts | 110 +++++-- src/i18n/locales/en-US.ts | 34 +- src/i18n/locales/zh-CN.ts | 32 +- src/services/file.ts | 5 + src/types/file.ts | 26 ++ src/views/RetrievewFileView.vue | 479 ++++++++++++++++++++++++++--- 6 files changed, 626 insertions(+), 60 deletions(-) diff --git a/src/composables/useRetrieveFlow.ts b/src/composables/useRetrieveFlow.ts index 60dc4ef..e287a84 100644 --- a/src/composables/useRetrieveFlow.ts +++ b/src/composables/useRetrieveFlow.ts @@ -1,10 +1,10 @@ -import { ref, watch } from 'vue' +import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { storeToRefs } from 'pinia' import { FileService } from '@/services' import { useAlertStore } from '@/stores/alertStore' import { useFileDataStore } from '@/stores/fileData' -import type { ReceivedFileRecord } from '@/types' +import type { ReceivedFileRecord, ShareMetadataResponse, ShareSelectResponse } from '@/types' import { copyToClipboard } from '@/utils/clipboard' import { getErrorMessage } from '@/utils/common' import { renderMarkdownPreview } from '@/utils/content-preview' @@ -22,6 +22,9 @@ export function useRetrieveFlow() { const { receiveData: records } = storeToRefs(fileStore) const code = ref('') + const inspectedFile = ref(null) + const isInspecting = ref(false) + const isRetrieving = ref(false) const inputStatus = ref({ readonly: false, loading: false @@ -31,6 +34,9 @@ export function useRetrieveFlow() { const showDrawer = ref(false) const showPreview = ref(false) const renderedContent = ref('') + const normalizedCode = computed(() => code.value.trim()) + const isWorking = computed(() => isInspecting.value || isRetrieving.value) + const hasValidCode = computed(() => normalizedCode.value.length === 5) const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 ' + t('fileSize.bytes') @@ -46,52 +52,103 @@ export function useRetrieveFlow() { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } - const createRecord = (detail: { - code: string - name: string - text: string - size: number - }): ReceivedFileRecord => { - const isFile = detail.text.startsWith('/share/download') || detail.name !== 'Text' + const createRecord = (detail: ShareSelectResponse): ReceivedFileRecord => { + const isText = + typeof detail.is_text === 'boolean' + ? detail.is_text + : detail.type + ? detail.type === 'text' + : detail.name === 'Text' && !detail.text.startsWith('/share/download') + const content = isText ? (detail.content ?? detail.text) : null + const downloadUrl = isText ? null : (detail.download_url ?? detail.text) + return { id: Date.now(), code: detail.code, filename: detail.name, size: formatFileSize(detail.size), - downloadUrl: isFile ? detail.text : null, - content: isFile ? null : detail.text, - date: new Date().toLocaleString() + downloadUrl, + content, + date: new Date().toLocaleString(), + type: isText ? 'text' : 'file', + remainingDownloads: detail.remaining_downloads } } - const handleSubmit = async () => { - if (code.value.length !== 5) { + const resetInspection = () => { + inspectedFile.value = null + error.value = '' + } + + const inspectCode = async () => { + if (!hasValidCode.value) { alertStore.showAlert(t('retrieve.messages.invalidCode'), 'error') return } + isInspecting.value = true inputStatus.value.readonly = true inputStatus.value.loading = true + error.value = '' try { - const res = await FileService.selectFile(code.value) + const res = await FileService.inspectFile(normalizedCode.value) + if (res.code === 200 && res.detail) { + inspectedFile.value = res.detail + } else { + inspectedFile.value = null + error.value = String(res.detail || res.message || '') + alertStore.showAlert(t('retrieve.messages.retrieveFailure') + error.value, 'error') + } + } catch { + inspectedFile.value = null + error.value = t('retrieve.messages.previewUnavailable') + alertStore.showAlert(error.value, 'warning') + } finally { + isInspecting.value = false + inputStatus.value.readonly = false + inputStatus.value.loading = false + } + } + + const handleSubmit = async () => { + if (!hasValidCode.value) { + alertStore.showAlert(t('retrieve.messages.invalidCode'), 'error') + return + } + + isRetrieving.value = true + inputStatus.value.readonly = true + inputStatus.value.loading = true + error.value = '' + + try { + const res = await FileService.selectFile(normalizedCode.value) if (res.code === 200 && res.detail) { const newFileData = createRecord(res.detail) - if (!fileStore.receiveData.some((file) => file.code === newFileData.code)) { - fileStore.addReceiveData(newFileData) + const existingIndex = fileStore.receiveData.findIndex( + (file) => file.code === newFileData.code + ) + if (existingIndex !== -1) { + fileStore.deleteReceiveData(existingIndex) } + fileStore.addReceiveData(newFileData) selectedRecord.value = newFileData if (newFileData.content) { showPreview.value = true } + inspectedFile.value = null alertStore.showAlert(t('retrieve.messages.retrieveSuccess'), 'success') } else { - alertStore.showAlert(t('retrieve.messages.retrieveFailure') + res.detail, 'error') + error.value = String(res.detail || '') + alertStore.showAlert(t('retrieve.messages.retrieveFailure') + error.value, 'error') } } catch (err: unknown) { const errorMessage = getErrorMessage(err, t('retrieve.messages.unknownError')) + error.value = errorMessage alertStore.showAlert(t('retrieve.messages.networkError') + errorMessage, 'error') } finally { + isRetrieving.value = false inputStatus.value.readonly = false inputStatus.value.loading = false code.value = '' @@ -151,8 +208,23 @@ export function useRetrieveFlow() { { immediate: true } ) + watch(normalizedCode, (nextCode) => { + if (inspectedFile.value && inspectedFile.value.code !== nextCode) { + resetInspection() + } + + if (nextCode.length < 5 && error.value) { + error.value = '' + } + }) + return { code, + inspectedFile, + isInspecting, + isRetrieving, + isWorking, + hasValidCode, inputStatus, error, records, @@ -166,6 +238,8 @@ export function useRetrieveFlow() { deleteRecord, downloadRecord, handleSubmit, + inspectCode, + resetInspection, showContentPreview, toggleDrawer, viewDetails diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 7a1d55d..91d328f 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -202,7 +202,39 @@ export default { invalidCodeError: 'Invalid retrieval code', retrieveFailure: 'Failed to retrieve file: ', networkError: 'Retrieval failed, please try again later: ', - unknownError: 'Unknown error' + unknownError: 'Unknown error', + previewUnavailable: 'Preview is unavailable, you can still retrieve directly' + }, + workspace: { + sendFile: 'Send File', + inspect: 'Preview', + inspecting: 'Previewing', + retrieving: 'Retrieving', + ready: 'Ready', + noPreview: 'Waiting for a code', + noPreviewDesc: + 'Enter a 5-character retrieval code to preview file details before consuming a retrieval.', + latestRecord: 'Latest Retrieval', + noRecord: 'No retrieval records yet', + currentCode: 'Code', + fileSize: 'File Size', + viewDetail: 'View Detail', + historyCount: '{count} records', + openRecords: 'Open retrieval records', + security: 'Safe Retrieval', + securityState: + 'Previewing does not consume retrieval count. Access is recorded only after confirmation.', + noExpiry: 'No limit', + unlimited: 'Unlimited', + remainingCount: '{count} left', + textType: 'Text Content', + fileType: 'File Download', + expiresAt: 'Expires At', + remainingDownloads: 'Remaining', + usedCount: 'Retrieved', + emptyCode: 'Empty', + previewState: 'Preview State', + waiting: 'Waiting' } }, diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0e5d958..51d6610 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -202,7 +202,37 @@ export default { invalidCodeError: '无效的取件码', retrieveFailure: '获取文件失败:', networkError: '取件失败,请稍后重试:', - unknownError: '未知错误' + unknownError: '未知错误', + previewUnavailable: '暂时无法预览文件信息,可直接取件' + }, + workspace: { + sendFile: '发送文件', + inspect: '预览', + inspecting: '预览中', + retrieving: '取件中', + ready: '可取件', + noPreview: '等待取件码', + noPreviewDesc: '输入 5 位取件码后会先读取文件信息,确认后再消耗取件次数。', + latestRecord: '最近取件', + noRecord: '暂无取件记录', + currentCode: '取件码', + fileSize: '文件大小', + viewDetail: '查看详情', + historyCount: '{count} 条记录', + openRecords: '打开取件记录', + security: '安全取件', + securityState: '预览不会消耗取件次数,确认取件后才会记录访问。', + noExpiry: '不限制', + unlimited: '不限次数', + remainingCount: '剩余 {count} 次', + textType: '文本内容', + fileType: '文件下载', + expiresAt: '过期时间', + remainingDownloads: '剩余次数', + usedCount: '已取件', + emptyCode: '未输入', + previewState: '预览状态', + waiting: '等待输入' } }, diff --git a/src/services/file.ts b/src/services/file.ts index 979855b..f88ae9e 100644 --- a/src/services/file.ts +++ b/src/services/file.ts @@ -10,6 +10,7 @@ import type { FileInfo, FileListResponse, FileUploadResponse, + ShareMetadataResponse, ShareSelectResponse, TextSendResponse, UploadProgress @@ -100,6 +101,10 @@ export class FileService { return api.post('/share/select/', { code }) } + static async inspectFile(code: string): Promise> { + return api.post('/share/metadata/', { code }) + } + static async getFile(code: string): Promise> { return api.get(`/file/${code}`) } diff --git a/src/types/file.ts b/src/types/file.ts index 4102f0d..662d465 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -57,6 +57,30 @@ export interface ShareSelectResponse { name: string text: string size: number + type?: 'file' | 'text' + is_text?: boolean + content?: string | null + download_url?: string | null + created_at?: string | null + expired_at?: string | null + expires_at?: string | null + expired_count?: number | null + used_count?: number + remaining_downloads?: number | null +} + +export interface ShareMetadataResponse { + code: string + name: string + size: number + type: 'file' | 'text' + is_text: boolean + created_at?: string | null + expired_at?: string | null + expires_at?: string | null + expired_count?: number | null + used_count?: number + remaining_downloads?: number | null } export interface ReceivedFileRecord { @@ -67,6 +91,8 @@ export interface ReceivedFileRecord { downloadUrl: string | null content: string | null date: string + type?: 'file' | 'text' + remainingDownloads?: number | null } export interface SentFileRecord { diff --git a/src/views/RetrievewFileView.vue b/src/views/RetrievewFileView.vue index 9430646..0699bdf 100644 --- a/src/views/RetrievewFileView.vue +++ b/src/views/RetrievewFileView.vue @@ -1,36 +1,351 @@