adtec-core-package 3.1.6 → 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/adtec-core-package/adtec-core-package.css +1 -0
  2. package/adtec-core-package/adtec-core-package.js +41216 -0
  3. package/adtec-core-package/adtec-core-package.umd.cjs +87 -0
  4. package/adtec-core-package/favicon.ico +0 -0
  5. package/adtec-core-package-3.1.7.tgz +0 -0
  6. package/package/.editorconfig +6 -0
  7. package/package/adtec-core-package/adtec-core-package.css +1 -0
  8. package/package/adtec-core-package/adtec-core-package.js +41216 -0
  9. package/package/adtec-core-package/adtec-core-package.umd.cjs +87 -0
  10. package/package/adtec-core-package/favicon.ico +0 -0
  11. package/package/index.html +13 -0
  12. package/package/prebuilt/umo-editor/favicon.ico +0 -0
  13. package/package/prebuilt/umo-editor/umo-editor.css +1 -0
  14. package/package/public/favicon.ico +0 -0
  15. package/package/src/assets/base.css +86 -0
  16. package/package/src/assets/main.css +35 -0
  17. package/package/src/components/editor-main/src/extensions/bookmark.js +110 -0
  18. package/package.json +4 -2
  19. package/src/api/DocumentApi.ts +11 -1
  20. package/src/components/upload/DocxJsViewer.vue +286 -0
  21. package/src/components/upload/ExcelJsViewer.vue +444 -0
  22. package/src/components/upload/FileView.vue +102 -159
  23. package/src/components/upload/OfficePreview.vue +481 -0
  24. package/src/components/upload/OfficePreviewHeaderBar.vue +315 -0
  25. package/src/components/upload/OfficePreviewToolbar.vue +203 -0
  26. package/src/components/upload/PdfJsViewer.vue +920 -0
  27. package/src/utils/docxPreviewUtil.ts +108 -0
  28. package/src/utils/excelPreviewUtil.ts +307 -0
  29. package/src/utils/officePreviewUtil.ts +122 -0
  30. package/src/utils/pdfSearchUtil.ts +127 -0
  31. package/src/utils/toSameOriginFileUrl.ts +1 -1
@@ -0,0 +1,108 @@
1
+ export type WordBinaryFormat = 'docx' | 'doc' | 'unknown'
2
+
3
+ export interface DocxSearchMatch {
4
+ pageIndex: number
5
+ markId: string
6
+ }
7
+
8
+ const OLE_DOC_MAGIC = [0xd0, 0xcf, 0x11, 0xe0]
9
+ const ZIP_MAGIC = [0x50, 0x4b]
10
+
11
+ export const detectWordBinaryFormat = (buffer: ArrayBuffer): WordBinaryFormat => {
12
+ const bytes = new Uint8Array(buffer, 0, 4)
13
+ if (bytes.length < 4) return 'unknown'
14
+ if (ZIP_MAGIC.every((value, index) => bytes[index] === value)) return 'docx'
15
+ if (OLE_DOC_MAGIC.every((value, index) => bytes[index] === value)) return 'doc'
16
+ return 'unknown'
17
+ }
18
+
19
+ export const resolveRawWordExt = (type?: string, name?: string, fileUrl?: string) => {
20
+ const normalizedType = (type || '').toLowerCase().replace(/^\./, '')
21
+ if (normalizedType === 'doc' || normalizedType === 'docx') return normalizedType
22
+ const fromName = (name || '').match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
23
+ if (fromName === 'doc' || fromName === 'docx') return fromName
24
+ const urlStr = (fileUrl || '').toLowerCase()
25
+ if (urlStr.includes('.docx')) return 'docx'
26
+ if (urlStr.match(/\.doc(\?|#|$)/)) return 'doc'
27
+ return ''
28
+ }
29
+
30
+ const collectTextNodes = (root: ParentNode) => {
31
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
32
+ const nodes: Text[] = []
33
+ let current = walker.nextNode()
34
+ while (current) {
35
+ if (current.textContent?.trim()) nodes.push(current as Text)
36
+ current = walker.nextNode()
37
+ }
38
+ return nodes
39
+ }
40
+
41
+ const unwrapMarks = (root: ParentNode) => {
42
+ root.querySelectorAll('mark.docx-search-mark').forEach((mark) => {
43
+ const parent = mark.parentNode
44
+ if (!parent) return
45
+ while (mark.firstChild) parent.insertBefore(mark.firstChild, mark)
46
+ parent.removeChild(mark)
47
+ })
48
+ root.normalize()
49
+ }
50
+
51
+ export const clearDocxSearchMarks = (root: ParentNode) => {
52
+ unwrapMarks(root)
53
+ root.querySelectorAll('.docx-search-mark--active').forEach((el) => {
54
+ el.classList.remove('docx-search-mark--active')
55
+ })
56
+ }
57
+
58
+ export const searchDocxPages = (
59
+ pages: HTMLElement[],
60
+ query: string,
61
+ ): DocxSearchMatch[] => {
62
+ const keyword = query.trim().toLowerCase()
63
+ if (!keyword) return []
64
+ const matches: DocxSearchMatch[] = []
65
+
66
+ pages.forEach((page, pageIndex) => {
67
+ unwrapMarks(page)
68
+ const textNodes = collectTextNodes(page)
69
+ for (const node of [...textNodes]) {
70
+ let text = node.textContent || ''
71
+ let lower = text.toLowerCase()
72
+ let fromIndex = 0
73
+ while (fromIndex < lower.length) {
74
+ const hit = lower.indexOf(keyword, fromIndex)
75
+ if (hit < 0) break
76
+ const parent = node.parentNode
77
+ if (!parent) break
78
+ const mark = document.createElement('mark')
79
+ mark.className = 'docx-search-mark'
80
+ const markId = `${pageIndex}-${matches.length}`
81
+ mark.dataset.matchId = markId
82
+ const before = text.slice(0, hit)
83
+ const middle = text.slice(hit, hit + keyword.length)
84
+ const after = text.slice(hit + keyword.length)
85
+ if (before) parent.insertBefore(document.createTextNode(before), node)
86
+ mark.textContent = middle
87
+ parent.insertBefore(mark, node)
88
+ if (after) {
89
+ node.textContent = after
90
+ text = after
91
+ lower = after.toLowerCase()
92
+ fromIndex = 0
93
+ } else {
94
+ parent.removeChild(node)
95
+ break
96
+ }
97
+ matches.push({ pageIndex, markId })
98
+ }
99
+ }
100
+ })
101
+ return matches
102
+ }
103
+
104
+ export const activateDocxSearchMark = (root: ParentNode, markId: string) => {
105
+ root.querySelectorAll('.docx-search-mark').forEach((el) => {
106
+ el.classList.toggle('docx-search-mark--active', el instanceof HTMLElement && el.dataset.matchId === markId)
107
+ })
108
+ }
@@ -0,0 +1,307 @@
1
+ import * as XLSX from 'xlsx-js-style'
2
+
3
+ export interface ExcelPreviewCellStyle {
4
+ backgroundColor?: string
5
+ color?: string
6
+ fontWeight?: string
7
+ fontStyle?: string
8
+ textDecoration?: string
9
+ textAlign?: 'left' | 'center' | 'right' | 'justify'
10
+ verticalAlign?: 'top' | 'middle' | 'bottom'
11
+ borderTop?: string
12
+ borderRight?: string
13
+ borderBottom?: string
14
+ borderLeft?: string
15
+ fontSize?: string
16
+ whiteSpace?: string
17
+ }
18
+
19
+ export interface ExcelPreviewCell {
20
+ value: string
21
+ row: number
22
+ col: number
23
+ rowSpan?: number
24
+ colSpan?: number
25
+ style?: ExcelPreviewCellStyle
26
+ width?: number
27
+ }
28
+
29
+ export interface ExcelPreviewSheet {
30
+ name: string
31
+ rows: ExcelPreviewCell[][]
32
+ startCol: number
33
+ endCol: number
34
+ colWidths: Record<number, number>
35
+ }
36
+
37
+ export interface ExcelSearchMatch {
38
+ sheetIndex: number
39
+ row: number
40
+ col: number
41
+ text: string
42
+ }
43
+
44
+ type ColorStyle = { rgb?: string; theme?: number; tint?: number; indexed?: number }
45
+
46
+ /** Excel 默认主题色(Office 2013+) */
47
+ const EXCEL_THEME_COLORS: Record<number, string> = {
48
+ 0: '#FFFFFF',
49
+ 1: '#000000',
50
+ 2: '#E7E6E6',
51
+ 3: '#44546A',
52
+ 4: '#4472C4',
53
+ 5: '#ED7D31',
54
+ 6: '#A5A5A5',
55
+ 7: '#FFC000',
56
+ 8: '#5B9BD5',
57
+ 9: '#70AD47',
58
+ }
59
+
60
+ const BORDER_WIDTH: Record<string, string> = {
61
+ hair: '1px',
62
+ thin: '1px',
63
+ medium: '2px',
64
+ thick: '3px',
65
+ dashed: '1px',
66
+ dotted: '1px',
67
+ double: '3px',
68
+ }
69
+
70
+ const DEFAULT_COL_WIDTH = 72
71
+
72
+ const hexToRgb = (hex: string) => {
73
+ const normalized = hex.replace('#', '')
74
+ const value =
75
+ normalized.length === 8 ? normalized.slice(2) : normalized.length === 6 ? normalized : ''
76
+ if (value.length !== 6) return null
77
+ return {
78
+ r: parseInt(value.slice(0, 2), 16),
79
+ g: parseInt(value.slice(2, 4), 16),
80
+ b: parseInt(value.slice(4, 6), 16),
81
+ }
82
+ }
83
+
84
+ const rgbToHex = (r: number, g: number, b: number) =>
85
+ `#${[r, g, b]
86
+ .map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, '0'))
87
+ .join('')}`
88
+
89
+ const applyTint = (hex: string, tint: number) => {
90
+ const rgb = hexToRgb(hex)
91
+ if (!rgb) return hex
92
+ const mix = tint < 0 ? { r: 0, g: 0, b: 0 } : { r: 255, g: 255, b: 255 }
93
+ const ratio = Math.abs(tint)
94
+ return rgbToHex(
95
+ rgb.r + (mix.r - rgb.r) * ratio,
96
+ rgb.g + (mix.g - rgb.g) * ratio,
97
+ rgb.b + (mix.b - rgb.b) * ratio,
98
+ )
99
+ }
100
+
101
+ const resolveColor = (color?: ColorStyle): string | undefined => {
102
+ if (!color) return undefined
103
+ if (color.rgb) {
104
+ const raw = color.rgb.replace(/[^0-9a-f]/gi, '')
105
+ if (raw.length === 8) return `#${raw.slice(2)}`
106
+ if (raw.length === 6) return `#${raw}`
107
+ return undefined
108
+ }
109
+ if (color.theme != null) {
110
+ const base = EXCEL_THEME_COLORS[color.theme] ?? '#FFFFFF'
111
+ return color.tint ? applyTint(base, color.tint) : base
112
+ }
113
+ return undefined
114
+ }
115
+
116
+ const resolveBorderSide = (side?: { style?: string; color?: ColorStyle }) => {
117
+ if (!side?.style || side.style === 'none') return undefined
118
+ const width = BORDER_WIDTH[side.style] ?? '1px'
119
+ const color = resolveColor(side.color) ?? '#d0d0d0'
120
+ const lineStyle = side.style === 'dashed' ? 'dashed' : side.style === 'dotted' ? 'dotted' : 'solid'
121
+ return `${width} ${lineStyle} ${color}`
122
+ }
123
+
124
+ const extractCellStyle = (cell?: XLSX.CellObject): ExcelPreviewCellStyle | undefined => {
125
+ const rawStyle = cell?.s
126
+ if (!rawStyle || typeof rawStyle === 'number') return undefined
127
+ const style = rawStyle as Record<string, unknown>
128
+ const css: ExcelPreviewCellStyle = {}
129
+
130
+ const fill = style.fill as { patternType?: string; fgColor?: ColorStyle; bgColor?: ColorStyle } | undefined
131
+ if (fill && fill.patternType !== 'none') {
132
+ const bg = resolveColor(fill.fgColor) || resolveColor(fill.bgColor)
133
+ if (bg) css.backgroundColor = bg
134
+ }
135
+
136
+ const font = style.font as {
137
+ bold?: boolean
138
+ italic?: boolean
139
+ underline?: boolean | string
140
+ sz?: number | string
141
+ color?: ColorStyle
142
+ } | undefined
143
+ if (font) {
144
+ if (font.bold) css.fontWeight = 'bold'
145
+ if (font.italic) css.fontStyle = 'italic'
146
+ if (font.underline) css.textDecoration = 'underline'
147
+ if (font.sz) css.fontSize = `${font.sz}pt`
148
+ const color = resolveColor(font.color)
149
+ if (color) css.color = color
150
+ }
151
+
152
+ const alignment = style.alignment as {
153
+ horizontal?: string
154
+ vertical?: string
155
+ wrapText?: boolean
156
+ } | undefined
157
+ if (alignment) {
158
+ if (alignment.horizontal && alignment.horizontal !== 'general') {
159
+ css.textAlign = alignment.horizontal as ExcelPreviewCellStyle['textAlign']
160
+ }
161
+ if (alignment.vertical) {
162
+ css.verticalAlign =
163
+ alignment.vertical === 'center'
164
+ ? 'middle'
165
+ : (alignment.vertical as ExcelPreviewCellStyle['verticalAlign'])
166
+ }
167
+ if (alignment.wrapText) css.whiteSpace = 'pre-wrap'
168
+ }
169
+
170
+ const border = style.border as Record<string, { style?: string; color?: ColorStyle }> | undefined
171
+ if (border) {
172
+ const top = resolveBorderSide(border.top)
173
+ const right = resolveBorderSide(border.right)
174
+ const bottom = resolveBorderSide(border.bottom)
175
+ const left = resolveBorderSide(border.left)
176
+ if (top) css.borderTop = top
177
+ if (right) css.borderRight = right
178
+ if (bottom) css.borderBottom = bottom
179
+ if (left) css.borderLeft = left
180
+ }
181
+
182
+ return Object.keys(css).length > 0 ? css : undefined
183
+ }
184
+
185
+ export const cellStyleToCss = (style?: ExcelPreviewCellStyle): Record<string, string> => {
186
+ if (!style) return {}
187
+ const css: Record<string, string> = {}
188
+ if (style.backgroundColor) css.backgroundColor = style.backgroundColor
189
+ if (style.color) css.color = style.color
190
+ if (style.fontWeight) css.fontWeight = style.fontWeight
191
+ if (style.fontStyle) css.fontStyle = style.fontStyle
192
+ if (style.textDecoration) css.textDecoration = style.textDecoration
193
+ if (style.textAlign) css.textAlign = style.textAlign
194
+ if (style.verticalAlign) css.verticalAlign = style.verticalAlign
195
+ if (style.borderTop) css.borderTop = style.borderTop
196
+ if (style.borderRight) css.borderRight = style.borderRight
197
+ if (style.borderBottom) css.borderBottom = style.borderBottom
198
+ if (style.borderLeft) css.borderLeft = style.borderLeft
199
+ if (style.fontSize) css.fontSize = style.fontSize
200
+ if (style.whiteSpace) css.whiteSpace = style.whiteSpace
201
+ return css
202
+ }
203
+
204
+ export const toExcelColumnLabel = (colIndex: number) => XLSX.utils.encode_col(colIndex)
205
+
206
+ const formatCellValue = (cell?: XLSX.CellObject) => {
207
+ if (!cell) return ''
208
+ if (cell.w != null && cell.w !== '') return String(cell.w)
209
+ if (cell.v == null) return ''
210
+ if (cell.t === 'd') {
211
+ const date = cell.v instanceof Date ? cell.v : new Date(cell.v as number)
212
+ if (!Number.isNaN(date.getTime())) return date.toLocaleString()
213
+ }
214
+ return String(cell.v)
215
+ }
216
+
217
+ const parseColWidths = (sheet: XLSX.WorkSheet, startCol: number, endCol: number) => {
218
+ const cols = sheet['!cols'] || []
219
+ const widths: Record<number, number> = {}
220
+ for (let c = startCol; c <= endCol; c++) {
221
+ const col = cols[c]
222
+ if (col?.wpx) widths[c] = col.wpx
223
+ else if (col?.wch) widths[c] = Math.max(48, col.wch * 7)
224
+ else widths[c] = DEFAULT_COL_WIDTH
225
+ }
226
+ return widths
227
+ }
228
+
229
+ export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<ExcelPreviewSheet[]> => {
230
+ const buffer = src instanceof Blob ? await src.arrayBuffer() : src
231
+ const workbook = XLSX.read(buffer, { type: 'array', cellDates: true, cellStyles: true })
232
+ return workbook.SheetNames.map((name) => {
233
+ const sheet = workbook.Sheets[name]
234
+ const range = sheet?.['!ref'] ? XLSX.utils.decode_range(sheet['!ref']) : null
235
+ return {
236
+ name,
237
+ rows: buildSheetRows(sheet),
238
+ startCol: range?.s.c ?? 0,
239
+ endCol: range?.e.c ?? 0,
240
+ colWidths: range ? parseColWidths(sheet, range.s.c, range.e.c) : {},
241
+ }
242
+ })
243
+ }
244
+
245
+ const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
246
+ if (!sheet || !sheet['!ref']) return []
247
+ const range = XLSX.utils.decode_range(sheet['!ref'])
248
+ const colWidths = parseColWidths(sheet, range.s.c, range.e.c)
249
+ const mergeStarts = new Map<string, { rowSpan: number; colSpan: number }>()
250
+ const covered = new Set<string>()
251
+
252
+ for (const merge of sheet['!merges'] || []) {
253
+ const rowSpan = merge.e.r - merge.s.r + 1
254
+ const colSpan = merge.e.c - merge.s.c + 1
255
+ mergeStarts.set(`${merge.s.r},${merge.s.c}`, { rowSpan, colSpan })
256
+ for (let r = merge.s.r; r <= merge.e.r; r++) {
257
+ for (let c = merge.s.c; c <= merge.e.c; c++) {
258
+ if (r !== merge.s.r || c !== merge.s.c) covered.add(`${r},${c}`)
259
+ }
260
+ }
261
+ }
262
+
263
+ const rows: ExcelPreviewCell[][] = []
264
+ for (let r = range.s.r; r <= range.e.r; r++) {
265
+ const row: ExcelPreviewCell[] = []
266
+ for (let c = range.s.c; c <= range.e.c; c++) {
267
+ if (covered.has(`${r},${c}`)) continue
268
+ const addr = XLSX.utils.encode_cell({ r, c })
269
+ const cellObj = sheet[addr]
270
+ const merge = mergeStarts.get(`${r},${c}`)
271
+ row.push({
272
+ value: formatCellValue(cellObj),
273
+ row: r,
274
+ col: c,
275
+ rowSpan: merge?.rowSpan,
276
+ colSpan: merge?.colSpan,
277
+ style: extractCellStyle(cellObj),
278
+ width: colWidths[c],
279
+ })
280
+ }
281
+ if (row.length > 0) rows.push(row)
282
+ }
283
+ return rows
284
+ }
285
+
286
+ export const searchExcelSheets = (
287
+ sheets: ExcelPreviewSheet[],
288
+ query: string,
289
+ ): ExcelSearchMatch[] => {
290
+ const keyword = query.trim().toLowerCase()
291
+ if (!keyword) return []
292
+ const matches: ExcelSearchMatch[] = []
293
+ sheets.forEach((sheet, sheetIndex) => {
294
+ for (const row of sheet.rows) {
295
+ for (const cell of row) {
296
+ const text = cell.value
297
+ if (text.toLowerCase().includes(keyword)) {
298
+ matches.push({ sheetIndex, row: cell.row, col: cell.col, text })
299
+ }
300
+ }
301
+ }
302
+ })
303
+ return matches
304
+ }
305
+
306
+ export const cellKey = (sheetIndex: number, row: number, col: number) =>
307
+ `${sheetIndex}:${row}:${col}`
@@ -0,0 +1,122 @@
1
+ export type OfficePreviewType = 'pdf' | 'docx' | 'excel' | 'pptx' | 'txt' | ''
2
+
3
+ /** 当前支持在线预览的格式 */
4
+ export const SUPPORTED_PREVIEW_TYPES: ReadonlySet<OfficePreviewType> = new Set([
5
+ 'pdf',
6
+ 'docx',
7
+ 'excel',
8
+ 'txt',
9
+ ])
10
+
11
+ /** 使用内置 OfficePreviewHeaderBar 的格式(与 PDF 同款工具栏) */
12
+ export const HEADER_PREVIEW_TYPES: ReadonlySet<OfficePreviewType> = new Set([
13
+ 'pdf',
14
+ 'docx',
15
+ 'excel',
16
+ ])
17
+
18
+ export const isSupportedPreviewType = (type: OfficePreviewType) =>
19
+ SUPPORTED_PREVIEW_TYPES.has(type)
20
+
21
+ export const resolveOfficeFileExt = (type?: string, name?: string, fileUrl?: string) => {
22
+ const normalizedType = (type || '').toLowerCase().replace(/^\./, '')
23
+ if (normalizedType) {
24
+ return normalizedType === 'doc' ? 'docx' : normalizedType
25
+ }
26
+ const fromName = (name || '').match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
27
+ if (fromName) {
28
+ return fromName === 'doc' ? 'docx' : fromName
29
+ }
30
+ const urlStr = (fileUrl || '').toLowerCase()
31
+ if (urlStr.includes('.docx') || urlStr.includes('.doc')) return 'docx'
32
+ if (urlStr.includes('.pdf')) return 'pdf'
33
+ if (urlStr.includes('.xlsx') || urlStr.includes('.xls')) return 'xlsx'
34
+ if (urlStr.includes('.pptx') || urlStr.includes('.ppt')) return 'pptx'
35
+ if (urlStr.includes('.txt')) return 'txt'
36
+ return ''
37
+ }
38
+
39
+ export const toOfficePreviewType = (
40
+ type?: string,
41
+ name?: string,
42
+ fileUrl?: string,
43
+ ): OfficePreviewType => {
44
+ const ext = resolveOfficeFileExt(type, name, fileUrl)
45
+ if (ext === 'pdf') return 'pdf'
46
+ if (ext === 'docx') return 'docx'
47
+ if (ext === 'xlsx' || ext === 'xls' || ext === 'excel') return 'excel'
48
+ if (ext === 'pptx' || ext === 'ppt') return 'pptx'
49
+ if (ext === 'txt') return 'txt'
50
+ return ''
51
+ }
52
+
53
+ export const MIN_OFFICE_ZOOM = 0.5
54
+ /** 非 PDF 预览(当前无在线预览)缩放上限 */
55
+ export const MAX_OFFICE_ZOOM = 1
56
+ /** pdf / excel 自研查看器支持高清放大 */
57
+ export const MAX_RICH_PREVIEW_ZOOM = 3
58
+ /** @deprecated 使用 MAX_RICH_PREVIEW_ZOOM */
59
+ export const MAX_PDF_ZOOM = MAX_RICH_PREVIEW_ZOOM
60
+ export const MAX_EXCEL_ZOOM = MAX_RICH_PREVIEW_ZOOM
61
+ export const OFFICE_ZOOM_STEP = 0.1
62
+
63
+ export const clampOfficeZoom = (value: number) =>
64
+ Math.min(MAX_OFFICE_ZOOM, Math.max(MIN_OFFICE_ZOOM, +value.toFixed(2)))
65
+
66
+ export const clampPdfZoom = (value: number) =>
67
+ Math.min(MAX_RICH_PREVIEW_ZOOM, Math.max(MIN_OFFICE_ZOOM, +value.toFixed(2)))
68
+
69
+ export const clampExcelZoom = clampPdfZoom
70
+
71
+ export const clampRichPreviewZoom = clampPdfZoom
72
+
73
+ export const clampPreviewZoom = (value: number, type: OfficePreviewType) =>
74
+ type === 'pdf' || type === 'excel' || type === 'docx'
75
+ ? clampRichPreviewZoom(value)
76
+ : clampOfficeZoom(value)
77
+
78
+ /** PDF 点阵 DPI(pdfjs scale=1) */
79
+ export const PDF_POINT_DPI = 72
80
+ /** 屏幕 CSS 像素 DPI,用于「实际大小」换算 */
81
+ export const SCREEN_CSS_DPI = 96
82
+ /** A4 纸宽度(PDF 点) */
83
+ export const A4_WIDTH_PT = 595.28
84
+
85
+ /** A4 在屏幕上的目标宽度(px,约 794) */
86
+ export const getA4ScreenWidthPx = () =>
87
+ Math.round(A4_WIDTH_PT * (SCREEN_CSS_DPI / PDF_POINT_DPI))
88
+
89
+ /**
90
+ * 默认以 A4 实际大小展示;预览区比 A4 窄时等比缩小以完整显示。
91
+ */
92
+ export const computePdfDefaultFitScale = (
93
+ pageWidthPt: number,
94
+ containerWidth: number,
95
+ ) => {
96
+ if (!pageWidthPt || !containerWidth) return SCREEN_CSS_DPI / PDF_POINT_DPI
97
+ const a4Scale = getA4ScreenWidthPx() / pageWidthPt
98
+ const containerScale = containerWidth / pageWidthPt
99
+ return Math.min(a4Scale, containerScale)
100
+ }
101
+
102
+ export const isPdfPreviewFile = (type?: string, name?: string, fileUrl?: string) =>
103
+ toOfficePreviewType(type, name, fileUrl) === 'pdf'
104
+
105
+ export const isExcelPreviewFile = (type?: string, name?: string, fileUrl?: string) =>
106
+ toOfficePreviewType(type, name, fileUrl) === 'excel'
107
+
108
+ export const isDocxPreviewFile = (type?: string, name?: string, fileUrl?: string) =>
109
+ toOfficePreviewType(type, name, fileUrl) === 'docx'
110
+
111
+ export const isLegacyDocFile = (type?: string, name?: string, fileUrl?: string) => {
112
+ const raw = (type || '').toLowerCase().replace(/^\./, '')
113
+ if (raw === 'doc') return true
114
+ const fromName = (name || '').match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
115
+ if (fromName === 'doc') return true
116
+ const urlStr = (fileUrl || '').toLowerCase()
117
+ return !!urlStr.match(/\.doc(\?|#|$)/) && !urlStr.includes('.docx')
118
+ }
119
+
120
+ /** 弹层等场景:使用 OfficePreview 内置标题栏(PDF / Word / Excel) */
121
+ export const isHeaderPreviewFile = (type?: string, name?: string, fileUrl?: string) =>
122
+ HEADER_PREVIEW_TYPES.has(toOfficePreviewType(type, name, fileUrl))
@@ -0,0 +1,127 @@
1
+ import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
2
+
3
+ export interface PdfSearchMatch {
4
+ page: number
5
+ globalIndex: number
6
+ rects: Array<{ left: number; top: number; width: number; height: number }>
7
+ }
8
+
9
+ interface TextSpan {
10
+ start: number
11
+ end: number
12
+ left: number
13
+ top: number
14
+ width: number
15
+ height: number
16
+ }
17
+
18
+ type PdfViewportLike = {
19
+ height: number
20
+ width: number
21
+ scale: number
22
+ transform: number[]
23
+ }
24
+
25
+ type PdfPageLike = {
26
+ getViewport: (opts: { scale: number }) => PdfViewportLike
27
+ getTextContent: () => Promise<{ items: Array<Record<string, unknown>> }>
28
+ }
29
+
30
+ type PdfDocumentLike = {
31
+ numPages: number
32
+ getPage: (pageNum: number) => Promise<PdfPageLike>
33
+ }
34
+
35
+ const normalizeQuery = (query: string) => query.trim().toLowerCase()
36
+
37
+ const getItemRect = (item: Record<string, unknown>, viewport: PdfViewportLike) => {
38
+ const transform = pdfjsLib.Util.transform(
39
+ viewport.transform,
40
+ item.transform as number[],
41
+ )
42
+ const fontHeight = Math.hypot(transform[2], transform[3]) || Number(item.height) || 12
43
+ const width = Math.abs(Number(item.width) || 0) * viewport.scale
44
+ return {
45
+ left: transform[4],
46
+ top: transform[5] - fontHeight,
47
+ width: Math.max(width, 4),
48
+ height: Math.max(fontHeight, 10),
49
+ }
50
+ }
51
+
52
+ const getPartialRect = (span: TextSpan, start: number, end: number) => {
53
+ const overlapStart = Math.max(start, span.start)
54
+ const overlapEnd = Math.min(end, span.end)
55
+ if (overlapEnd <= overlapStart) return null
56
+
57
+ const spanLen = Math.max(span.end - span.start, 1)
58
+ const offsetRatio = (overlapStart - span.start) / spanLen
59
+ const lengthRatio = (overlapEnd - overlapStart) / spanLen
60
+ const width = Math.max(span.width * lengthRatio, 4)
61
+
62
+ return {
63
+ left: span.left + span.width * offsetRatio,
64
+ top: span.top,
65
+ width,
66
+ height: span.height,
67
+ }
68
+ }
69
+
70
+ const getRangeRects = (spans: TextSpan[], start: number, end: number) => {
71
+ const rects: Array<{ left: number; top: number; width: number; height: number }> = []
72
+ for (const span of spans) {
73
+ const rect = getPartialRect(span, start, end)
74
+ if (rect) rects.push(rect)
75
+ }
76
+ return rects
77
+ }
78
+
79
+ export const searchPdfDocument = async (
80
+ doc: PdfDocumentLike,
81
+ query: string,
82
+ scale: number,
83
+ ): Promise<PdfSearchMatch[]> => {
84
+ const normalizedQuery = normalizeQuery(query)
85
+ if (!normalizedQuery) return []
86
+
87
+ const matches: PdfSearchMatch[] = []
88
+ let globalIndex = 0
89
+
90
+ for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
91
+ const page = await doc.getPage(pageNum)
92
+ const viewport = page.getViewport({ scale })
93
+ const content = await page.getTextContent()
94
+
95
+ let fullText = ''
96
+ const spans: TextSpan[] = []
97
+
98
+ for (const item of content.items) {
99
+ if (!('str' in item) || typeof item.str !== 'string' || !item.str) continue
100
+ const rect = getItemRect(item, viewport)
101
+ const start = fullText.length
102
+ fullText += item.str
103
+ spans.push({
104
+ start,
105
+ end: fullText.length,
106
+ ...rect,
107
+ })
108
+ }
109
+
110
+ const lower = fullText.toLowerCase()
111
+ let idx = lower.indexOf(normalizedQuery)
112
+ while (idx !== -1) {
113
+ const endIdx = idx + normalizedQuery.length
114
+ const rects = getRangeRects(spans, idx, endIdx)
115
+ if (rects.length > 0) {
116
+ matches.push({
117
+ page: pageNum,
118
+ globalIndex: globalIndex++,
119
+ rects,
120
+ })
121
+ }
122
+ idx = lower.indexOf(normalizedQuery, idx + 1)
123
+ }
124
+ }
125
+
126
+ return matches
127
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 浏览器侧下载/拉取文件:将 fileurl 内网绝对地址转为当前页同源相对路径。
3
- * OnlyOffice 预览仍须使用原始内网 URL(Document Server 服务端拉取)。
3
+ * 将内网绝对地址转为当前页同源相对路径,供浏览器侧拉取文件。
4
4
  */
5
5
  export function toSameOriginFileUrl(url?: string | null): string {
6
6
  if (!url) return ''