/** * 开发期 Mock(VITE_MOCK=1 时启用):在 service worker 之前仅用于本地调试, * 通过拦截 fetch/XHR 实现无后端推进前端。 */ import type { MockRoute } from './types' const ok = (data: unknown, msg = '') => ({ code: 200, msg, data }) const err = (code: number, msg: string) => ({ code, msg, data: null }) let seq = 1 function textToCode(text: string): string { let h = 0 for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0 return (h % 100000).toString().padStart(5, '0') } const shares: { code: string; kind: 'text' | 'file'; name: string; size: number; text?: string }[] = [] const routes: MockRoute[] = [ { method: 'GET', pattern: /\/api\/v1\/health$/, reply: () => ok({ status: 'ok', version: 'mock', storage: 'local', cache: 'memory' }) }, { method: 'GET', pattern: /\/api\/v1\/config$/, reply: () => ok({ config: { name: '文件快传 (Mock)', site_domain: '', description: '开发 Mock 模式 · 开箱即用的文件快传系统', explain: '', uploadSize: 10 * 1024 * 1024, allowedFileTypes: [], expireStyle: ['day', 'hour', 'minute', 'forever', 'count'], enableChunk: true, openUpload: true, notify_enabled: 1, notify_title: '系统通知', notify_content: '欢迎使用文件快传(Mock)。', logo_url: '', favicon_url: '', background_url: '', footer_text: '', footer_beian: '', max_file_size: 0, max_save_seconds: 0, max_save_count: 0, uploadCount: 10, uploadMinute: 1, }, meta: { version: 'mock', features: { chunkUpload: true, guestUpload: true } }, }), }, { method: 'POST', pattern: /\/share\/text$/, reply: (_m, body) => { const text = String((body as Record)?.text ?? '') const code = textToCode(text + seq++) shares.push({ code, kind: 'text', name: 'Text.txt', size: new Blob([text]).size, text }) return ok({ code }) }, }, { method: 'POST', pattern: /\/share\/file$/, reply: (_m, body) => { const fd = body as FormData const file = fd?.get?.('file') if (!(file instanceof File)) return err(400, '缺少文件') const code = textToCode(file.name + seq++) shares.push({ code, kind: 'file', name: file.name, size: file.size }) return ok({ code, name: file.name }) }, }, { method: 'GET', pattern: /\/share\/metadata$/, reply: (_m, _body, q) => { const s = shares.find((x) => x.code === q?.get('code')) if (!s) return err(404, '文件不存在') return ok({ code: s.code, name: s.name, size: s.size, type: s.kind, is_text: s.kind === 'text', created_at: new Date().toISOString(), expired_at: null, expired_count: -1, used_count: 0, remaining_downloads: null, }) }, }, { method: 'GET', pattern: /\/share\/select$/, reply: (_m, _body, q) => { const s = shares.find((x) => x.code === q?.get('code')) if (!s) return { __rawError: 404 } if (s.kind === 'text') return { __rawText: s.text ?? '' } return { __rawBlob: new Blob([`mock-content-of-${s.name}`], { type: 'application/octet-stream' }), __rawName: s.name } }, }, { method: 'POST', pattern: /\/chunk\/upload\/init$/, reply: (_m, body) => { const b = body as Record const total = Math.ceil(Number(b?.file_size ?? 0) / Number(b?.chunk_size ?? 1024)) return ok({ existed: false, upload_id: `mock-${seq++}`, chunk_size: b?.chunk_size, total_chunks: total, uploaded_chunks: [] }) }, }, { method: 'POST', pattern: /\/chunk\/upload\/[^/]+\/\d+$/, reply: () => ok({ chunk_hash: 'mockhash' }) }, { method: 'GET', pattern: /\/chunk\/upload\/status\/[^/]+$/, reply: (m) => { const id = String(m?.[0]?.split('/').pop() ?? 'mock') return ok({ upload_id: id, chunk_size: 1024, total_chunks: 1, uploaded_chunks: [0] }) }, }, { method: 'POST', pattern: /\/chunk\/upload\/complete\/[^/]+$/, reply: () => { const code = textToCode('chunk' + seq++) shares.push({ code, kind: 'file', name: 'chunked.bin', size: 1024 }) return ok({ code, name: 'chunked.bin' }) }, }, { method: 'DELETE', pattern: /\/chunk\/upload\/[^/]+$/, reply: () => ok({ message: '已取消' }) }, { method: 'POST', pattern: /\/admin\/login$/, reply: (_m, body) => ((body as { password?: string })?.password === 'admin123' ? ok({ id: 'admin', username: 'admin', token: 'mock-token', token_type: 'Bearer', expires_in: 86400 }) : err(401, '密码错误')) }, { method: 'GET', pattern: /\/admin\/verify$/, reply: (_m, _b, _q) => (getToken() === 'mock-token' ? ok({ is_admin: true }) : err(401, '未登录')) }, { method: 'POST', pattern: /\/admin\/logout$/, reply: () => ok({ ok: true }) }, { method: 'GET', pattern: /\/admin\/file\/list$/, reply: () => ok({ page: 1, size: 10, total: shares.length, data: shares.map((s, i) => ({ id: i + 1, code: s.code, name: s.name, suffix: s.name.includes('.') ? s.name.split('.').pop() : '', size: s.size, isText: s.kind === 'text', expiredAt: null, expiredCount: -1, usedCount: 0, createdAt: new Date().toISOString(), isExpired: false, })), }), }, { method: 'DELETE', pattern: /\/admin\/file\/delete$/, reply: () => ok(null) }, { method: 'POST', pattern: /\/admin\/file\/batch-delete$/, reply: () => ok(null) }, { method: 'PATCH', pattern: /\/admin\/file\/update$/, reply: () => ok('更新成功') }, { method: 'GET', pattern: /\/admin\/config\/get$/, reply: () => ok({ site_name: '文件快传 (Mock)', logo_url: '', favicon_url: '', background_url: '', footer_text: '', footer_beian: '', notify_enabled: 1, notify_title: '系统通知', notify_content: '欢迎使用文件快传(Mock)。', max_save_seconds: 0, max_save_count: 0, max_file_size: 0, allowed_file_types: ['*'], uploadCount: 10, uploadMinute: 1, openUpload: 1, }), }, { method: 'PATCH', pattern: /\/admin\/config\/update$/, reply: () => ok(null) }, { method: 'GET', pattern: /\/admin\/audit\/list$/, reply: () => ok({ page: 1, size: 20, total: 2, data: [ { id: 1, action: 'upload', result: 'success', file_code: shares[0]?.code ?? '10001', file_name: 'demo.txt', size_bytes: 1024, transferred_bytes: 1024, ip: '127.0.0.1', user_agent: 'Mozilla/5.0 (Mock)', device_os: 'macOS', device_browser: 'Chrome', device_type: 'desktop', actor: 'guest', error_msg: '', duration_ms: 42, created_at: new Date().toISOString() }, { id: 2, action: 'download', result: 'success', file_code: shares[0]?.code ?? '10001', file_name: 'demo.txt', size_bytes: 1024, transferred_bytes: 1024, ip: '192.168.1.2', user_agent: 'Mozilla/5.0 (iPhone)', device_os: 'iOS', device_browser: 'Safari', device_type: 'mobile', actor: 'guest', error_msg: '', duration_ms: 88, created_at: new Date().toISOString() }, ], }), }, ] function getToken(): string { try { return localStorage.getItem('fcb_admin_token') ?? '' } catch { return '' } } /** 安装 fetch 拦截(dev-only,不覆盖 XHR —— mock 下上传走 XHR 也会失败,可接受) */ export function installMock(): void { if (import.meta.env.VITE_MOCK !== '1') return const origFetch = globalThis.fetch.bind(globalThis) globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url const method = (init?.method ?? 'GET').toUpperCase() const qIndex = url.indexOf('?') const path = qIndex >= 0 ? url.slice(0, qIndex) : url const query = qIndex >= 0 ? new URL(url, location.origin).searchParams : undefined for (const r of routes) { if (r.method !== method) continue const m = r.pattern.exec(path) if (!m) continue let body: unknown = undefined if (init?.body) { if (typeof init.body === 'string') { try { body = JSON.parse(init.body) } catch { body = Object.fromEntries(new URLSearchParams(init.body)) } } else if (init.body instanceof FormData) { body = init.body } } const res = r.reply(m, body, query) await new Promise((res) => setTimeout(res, 120)) // 模拟网络延迟 if (res && typeof res === 'object' && '__rawText' in (res as Record)) { return new Response((res as { __rawText: string }).__rawText, { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' } }) } if (res && typeof res === 'object' && '__rawBlob' in (res as Record)) { const rb = res as { __rawBlob: Blob; __rawName: string } return new Response(rb.__rawBlob, { status: 200, headers: { 'content-type': 'application/octet-stream', 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(rb.__rawName)}` } }) } if (res && typeof res === 'object' && '__rawError' in (res as Record)) { const e = err((res as { __rawError: number }).__rawError, '文件不存在') return new Response(JSON.stringify(e), { status: 200, headers: { 'content-type': 'application/json' } }) } return new Response(JSON.stringify(res), { status: 200, headers: { 'content-type': 'application/json' } }) } return origFetch(input, init) } console.info('[mock] 文件快传 API mock enabled (VITE_MOCK=1)') }