From 2179e7d9d26bd6784ae21c878ca650a3d872040f Mon Sep 17 00:00:00 2001 From: Codewoc <947380458@qq.com> Date: Tue, 7 Apr 2026 10:49:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(review-tablet):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B9=B3=E6=9D=BF=E6=96=87=E4=BB=B6=E9=A2=84=E8=A7=88=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/vite/proxy.ts | 15 + package.json | 1 + pnpm-lock.yaml | 141 ++++++ src/utils/promiseWithResolvers.ts | 28 ++ src/utils/urlParse.ts | 20 + src/views/review/tablet/TabletPdfViewer.vue | 453 ++++++++++++++++++++ src/views/review/tablet/index.vue | 26 +- src/views/review/tablet/previewMode.mjs | 51 +++ tests/promise-with-resolvers.test.cjs | 20 + tests/tablet-pdf-preview.test.cjs | 60 +++ tests/url-parse.test.cjs | 18 + tests/vite-proxy.test.cjs | 21 + vite.config.ts | 12 +- 13 files changed, 856 insertions(+), 10 deletions(-) create mode 100644 build/vite/proxy.ts create mode 100644 src/utils/promiseWithResolvers.ts create mode 100644 src/utils/urlParse.ts create mode 100644 src/views/review/tablet/TabletPdfViewer.vue create mode 100644 src/views/review/tablet/previewMode.mjs create mode 100644 tests/promise-with-resolvers.test.cjs create mode 100644 tests/tablet-pdf-preview.test.cjs create mode 100644 tests/url-parse.test.cjs create mode 100644 tests/vite-proxy.test.cjs diff --git a/build/vite/proxy.ts b/build/vite/proxy.ts new file mode 100644 index 000000000..9e9dfdea4 --- /dev/null +++ b/build/vite/proxy.ts @@ -0,0 +1,15 @@ +import type { IncomingMessage } from 'node:http' + +type ProxyRequestLike = { + setHeader: (name: string, value: string) => void +} + +export const forwardOriginalHost = ( + proxyReq: ProxyRequestLike, + req: IncomingMessage +) => { + const host = req.headers.host + if (typeof host === 'string' && host.trim()) { + proxyReq.setHeader('host', host) + } +} diff --git a/package.json b/package.json index 1a12256e9..954911506 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "min-dash": "^4.1.1", "mitt": "^3.0.1", "nprogress": "^0.2.0", + "pdfjs-dist": "^5.6.205", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "qrcode": "^1.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfe393a5e..95a6495ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: nprogress: specifier: ^0.2.0 version: 0.2.0 + pdfjs-dist: + specifier: ^5.6.205 + version: 5.6.205 pinia: specifier: ^2.1.7 version: 2.3.1(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3)) @@ -1405,6 +1408,81 @@ packages: '@microsoft/fetch-event-source@2.0.1': resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + '@napi-rs/canvas-android-arm64@0.1.97': + resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.97': + resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.97': + resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': + resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.97': + resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@0.1.97': + resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': + resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@0.1.97': + resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@0.1.97': + resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.97': + resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.97': + resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.97': + resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4224,6 +4302,9 @@ packages: node-html-parser@7.0.2: resolution: {integrity: sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ==} + node-readable-to-web-readable-stream@0.4.2: + resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -4375,6 +4456,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pdfjs-dist@5.6.205: + resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==} + engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -6681,6 +6766,54 @@ snapshots: '@microsoft/fetch-event-source@2.0.1': {} + '@napi-rs/canvas-android-arm64@0.1.97': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.97': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.97': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.97': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.97': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.97': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.97': + optional: true + + '@napi-rs/canvas@0.1.97': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.97 + '@napi-rs/canvas-darwin-arm64': 0.1.97 + '@napi-rs/canvas-darwin-x64': 0.1.97 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.97 + '@napi-rs/canvas-linux-arm64-musl': 0.1.97 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.97 + '@napi-rs/canvas-linux-x64-gnu': 0.1.97 + '@napi-rs/canvas-linux-x64-musl': 0.1.97 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.97 + '@napi-rs/canvas-win32-x64-msvc': 0.1.97 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9760,6 +9893,9 @@ snapshots: css-select: 5.2.2 he: 1.2.0 + node-readable-to-web-readable-stream@0.4.2: + optional: true + node-releases@2.0.27: {} nopt@7.2.1: @@ -9894,6 +10030,11 @@ snapshots: pathe@2.0.3: {} + pdfjs-dist@5.6.205: + optionalDependencies: + '@napi-rs/canvas': 0.1.97 + node-readable-to-web-readable-stream: 0.4.2 + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} diff --git a/src/utils/promiseWithResolvers.ts b/src/utils/promiseWithResolvers.ts new file mode 100644 index 000000000..dfddcd3f2 --- /dev/null +++ b/src/utils/promiseWithResolvers.ts @@ -0,0 +1,28 @@ +type PromiseWithResolversResult = { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (reason?: unknown) => void +} + +declare global { + interface PromiseConstructor { + withResolvers?(): PromiseWithResolversResult + } +} + +export const ensurePromiseWithResolvers = () => { + if (typeof Promise.withResolvers === 'function') return Promise.withResolvers.bind(Promise) + + Promise.withResolvers = function withResolvers(): PromiseWithResolversResult { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } + } + + return Promise.withResolvers.bind(Promise) +} + diff --git a/src/utils/urlParse.ts b/src/utils/urlParse.ts new file mode 100644 index 000000000..4f9a264f0 --- /dev/null +++ b/src/utils/urlParse.ts @@ -0,0 +1,20 @@ +declare global { + interface URLConstructor { + parse?(url: string | URL, base?: string | URL): URL | null + } +} + +export const ensureUrlParse = () => { + if (typeof URL.parse === 'function') return URL.parse.bind(URL) + + URL.parse = function parse(url: string | URL, base?: string | URL) { + try { + return typeof base === 'undefined' ? new URL(url) : new URL(url, base) + } catch { + return null + } + } + + return URL.parse.bind(URL) +} + diff --git a/src/views/review/tablet/TabletPdfViewer.vue b/src/views/review/tablet/TabletPdfViewer.vue new file mode 100644 index 000000000..c3d138262 --- /dev/null +++ b/src/views/review/tablet/TabletPdfViewer.vue @@ -0,0 +1,453 @@ + + + + + diff --git a/src/views/review/tablet/index.vue b/src/views/review/tablet/index.vue index 48fef80e2..04e2863f3 100644 --- a/src/views/review/tablet/index.vue +++ b/src/views/review/tablet/index.vue @@ -80,7 +80,7 @@ 刷新预览 - 新窗口打开 打开侧栏 @@ -441,7 +441,11 @@ import { ElMessage, ElMessageBox } from 'element-plus' import MarkdownIt from 'markdown-it' import { formatDate } from '@/utils/formatTime' import TabletPdfViewer from './TabletPdfViewer.vue' -import { resolvePdfSourceUrl, resolveTabletPreviewMode } from './previewMode.mjs' +import { + resolveNewWindowPreviewUrl, + resolvePdfSourceUrl, + resolveTabletPreviewMode +} from './previewMode.mjs' import { getFileOpenUrl, getProjectFiles, @@ -627,7 +631,14 @@ const previewLoadingSubtitle = computed(() => ? 'Office 文件首次打开可能需要一些时间,请稍候' : '正在为您打开当前资料' ) -const previewMode = computed(() => resolveTabletPreviewMode(previewPayload.value)) +const previewMode = computed(() => + resolveTabletPreviewMode(previewPayload.value, { + preferIframeForPdf: isTouchTablet.value && !!previewPayload.value?.openUrl + }) +) +const newWindowPreviewUrl = computed(() => + resolveNewWindowPreviewUrl(previewPayload.value, previewUrl.value) +) const isTouchTablet = computed(() => coarsePointer.value) const isCompactTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1400) const isDenseTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1180) @@ -786,8 +797,11 @@ const loadPreview = async (fileId: number) => { const payload = await getFileOpenUrl(fileId) if (requestToken !== previewRequestToken.value) return previewPayload.value = payload - const mode = resolveTabletPreviewMode(payload) - const sourceUrl = resolvePdfSourceUrl(payload, payload.openUrl) + const mode = resolveTabletPreviewMode(payload, { + preferIframeForPdf: isTouchTablet.value && !!payload.openUrl + }) + const sourceUrl = + mode === 'pdf' ? resolvePdfSourceUrl(payload, payload.openUrl) : payload.openUrl || '' if (!sourceUrl) { previewUrl.value = '' previewFrameLoading.value = false @@ -822,7 +836,7 @@ const selectFileNode = async (fileNode: FileTreeNode) => { } const openInNewWindow = () => { - const url = previewUrl.value || previewPayload.value?.openUrl + const url = newWindowPreviewUrl.value if (!url) { ElMessage.warning('暂无可打开的预览地址') return diff --git a/src/views/review/tablet/previewMode.mjs b/src/views/review/tablet/previewMode.mjs new file mode 100644 index 000000000..56fd0fa04 --- /dev/null +++ b/src/views/review/tablet/previewMode.mjs @@ -0,0 +1,51 @@ +const PDF_EXT = 'pdf' +const PREVIEW_SOURCE_PATH = '/admin-api/project/review-tablet/file-preview-source/' +const CLIENT_UNREACHABLE_HOSTS = new Set([ + 'localhost', + '127.0.0.1', + '0.0.0.0', + 'host.docker.internal' +]) + +const normalizeFileType = (value) => + String(value || '') + .trim() + .toLowerCase() + +const toSameOriginPreviewPathIfNeeded = (url) => { + const normalized = String(url || '').trim() + if (!normalized) return '' + if (normalized.startsWith(PREVIEW_SOURCE_PATH)) return normalized + try { + const parsed = new URL(normalized) + if (!parsed.pathname.startsWith(PREVIEW_SOURCE_PATH)) return normalized + if (!CLIENT_UNREACHABLE_HOSTS.has(parsed.hostname.toLowerCase())) return normalized + return `${parsed.pathname}${parsed.search}${parsed.hash}` + } catch { + return normalized + } +} + +export const resolveTabletPreviewMode = (payload, options = {}) => { + if (!payload) return 'none' + if (options.preferIframeForPdf && normalizeFileType(payload.fileType) === PDF_EXT) { + return 'iframe' + } + return normalizeFileType(payload.fileType) === PDF_EXT ? 'pdf' : 'iframe' +} + +export const resolvePdfSourceUrl = (payload, fallbackUrl = '') => { + if (!payload) return fallbackUrl || '' + if (resolveTabletPreviewMode(payload) !== 'pdf') return fallbackUrl || '' + const visitUrl = toSameOriginPreviewPathIfNeeded(payload.visitUrl) + return visitUrl || fallbackUrl || '' +} + +export const resolveNewWindowPreviewUrl = (payload, fallbackUrl = '') => { + if (!payload) return fallbackUrl || '' + const openUrl = String(payload.openUrl || '').trim() + if (resolveTabletPreviewMode(payload) === 'pdf') { + return openUrl || resolvePdfSourceUrl(payload, fallbackUrl) + } + return openUrl || fallbackUrl || '' +} diff --git a/tests/promise-with-resolvers.test.cjs b/tests/promise-with-resolvers.test.cjs new file mode 100644 index 000000000..3a58894b0 --- /dev/null +++ b/tests/promise-with-resolvers.test.cjs @@ -0,0 +1,20 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +test('ensurePromiseWithResolvers installs a compatible polyfill when unavailable', async () => { + const original = Promise.withResolvers + try { + Promise.withResolvers = undefined + const { ensurePromiseWithResolvers } = await import('../src/utils/promiseWithResolvers.ts') + + ensurePromiseWithResolvers() + + assert.equal(typeof Promise.withResolvers, 'function') + const deferred = Promise.withResolvers() + assert.equal(typeof deferred.promise?.then, 'function') + assert.equal(typeof deferred.resolve, 'function') + assert.equal(typeof deferred.reject, 'function') + } finally { + Promise.withResolvers = original + } +}) diff --git a/tests/tablet-pdf-preview.test.cjs b/tests/tablet-pdf-preview.test.cjs new file mode 100644 index 000000000..f40d3d975 --- /dev/null +++ b/tests/tablet-pdf-preview.test.cjs @@ -0,0 +1,60 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +test('pdf files resolve to local pdf viewer mode and prefer visitUrl as source', async () => { + const { resolveTabletPreviewMode, resolvePdfSourceUrl } = + await import('../src/views/review/tablet/previewMode.mjs') + const payload = { + fileType: 'pdf', + visitUrl: '/admin-api/project/review-tablet/file-preview-source/12/preview-12.pdf?sign=abc', + openUrl: 'http://kkfileview.example.com/onlinePreview?url=xxx' + } + assert.equal(resolveTabletPreviewMode(payload), 'pdf') + assert.equal(resolvePdfSourceUrl(payload, payload.openUrl), payload.visitUrl) +}) + +test('office files stay on iframe preview mode and keep remote preview url', async () => { + const { resolveTabletPreviewMode, resolvePdfSourceUrl } = + await import('../src/views/review/tablet/previewMode.mjs') + const payload = { + fileType: 'docx', + visitUrl: '/admin-api/project/review-tablet/file-preview-source/12/preview-12.docx?sign=abc', + openUrl: 'http://kkfileview.example.com/onlinePreview?url=xxx' + } + assert.equal(resolveTabletPreviewMode(payload), 'iframe') + assert.equal(resolvePdfSourceUrl(payload, payload.openUrl), payload.openUrl) +}) + +test('pdf visitUrl with non-client host falls back to same-origin preview-source path', async () => { + const { resolvePdfSourceUrl } = await import('../src/views/review/tablet/previewMode.mjs') + const payload = { + fileType: 'pdf', + visitUrl: + 'http://host.docker.internal:48080/admin-api/project/review-tablet/file-preview-source/12/preview-12.pdf?sign=abc', + openUrl: 'http://10.11.74.40:8012/onlinePreview?url=xxx' + } + assert.equal( + resolvePdfSourceUrl(payload, payload.openUrl), + '/admin-api/project/review-tablet/file-preview-source/12/preview-12.pdf?sign=abc' + ) +}) + +test('pdf new window prefers unified preview url instead of raw source url', async () => { + const { resolveNewWindowPreviewUrl } = await import('../src/views/review/tablet/previewMode.mjs') + const payload = { + fileType: 'pdf', + visitUrl: '/admin-api/project/review-tablet/file-preview-source/12/preview-12.pdf?sign=abc', + openUrl: 'http://192.168.10.24:8012/onlinePreview?url=xxx' + } + assert.equal(resolveNewWindowPreviewUrl(payload, payload.visitUrl), payload.openUrl) +}) + +test('pdf files can fall back to iframe preview mode on touch tablets', async () => { + const { resolveTabletPreviewMode } = await import('../src/views/review/tablet/previewMode.mjs') + const payload = { + fileType: 'pdf', + visitUrl: '/admin-api/project/review-tablet/file-preview-source/12/preview-12.pdf?sign=abc', + openUrl: 'http://192.168.10.24:8012/onlinePreview?url=xxx' + } + assert.equal(resolveTabletPreviewMode(payload, { preferIframeForPdf: true }), 'iframe') +}) diff --git a/tests/url-parse.test.cjs b/tests/url-parse.test.cjs new file mode 100644 index 000000000..bd800be5d --- /dev/null +++ b/tests/url-parse.test.cjs @@ -0,0 +1,18 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +test('ensureUrlParse installs a compatible URL.parse polyfill when unavailable', async () => { + const original = URL.parse + try { + URL.parse = undefined + const { ensureUrlParse } = await import('../src/utils/urlParse.ts') + + ensureUrlParse() + + assert.equal(typeof URL.parse, 'function') + assert.equal(URL.parse('/demo.pdf', 'http://192.168.10.24:48000/base/').href, 'http://192.168.10.24:48000/demo.pdf') + assert.equal(URL.parse('not a valid url'), null) + } finally { + URL.parse = original + } +}) diff --git a/tests/vite-proxy.test.cjs b/tests/vite-proxy.test.cjs new file mode 100644 index 000000000..71c593fd1 --- /dev/null +++ b/tests/vite-proxy.test.cjs @@ -0,0 +1,21 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +test('forwardOriginalHost preserves the incoming host header for proxied requests', async () => { + const { forwardOriginalHost } = await import('../build/vite/proxy.ts') + const calls = [] + const proxyReq = { + setHeader(name, value) { + calls.push([name, value]) + } + } + const req = { + headers: { + host: '192.168.10.24:48000' + } + } + + forwardOriginalHost(proxyReq, req) + + assert.deepEqual(calls, [['host', '192.168.10.24:48000']]) +}) diff --git a/vite.config.ts b/vite.config.ts index 4d840a47c..17ccf4804 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import {resolve} from 'path' import type {ConfigEnv, UserConfig} from 'vite' import {loadEnv} from 'vite' import {createVitePlugins} from './build/vite' +import {forwardOriginalHost} from './build/vite/proxy' import {exclude, include} from "./build/vite/optimize" // 当前执行node命令时文件夹的地址(工作目录) const root = process.cwd() @@ -34,7 +35,12 @@ export default ({command, mode}: ConfigEnv): UserConfig => { ['/admin-api']: { target: apiProxyTarget, ws: false, - changeOrigin: true + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + forwardOriginalHost(proxyReq, req) + }) + } }, // 开发环境下,直接代理 bucket 根路径,避免预签名 URL 因路径重写导致签名失效 ['/ncc-dev']: { @@ -43,9 +49,7 @@ export default ({command, mode}: ConfigEnv): UserConfig => { changeOrigin: false, configure: (proxy) => { proxy.on('proxyReq', (proxyReq, req) => { - if (req.headers.host) { - proxyReq.setHeader('host', req.headers.host) - } + forwardOriginalHost(proxyReq, req) }) } }