adtec-core-package 3.1.5 → 3.1.7

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.
@@ -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,101 @@
1
+ import * as XLSX from 'xlsx-js-style'
2
+
3
+ export interface ExcelPreviewCell {
4
+ value: string
5
+ row: number
6
+ col: number
7
+ rowSpan?: number
8
+ colSpan?: number
9
+ }
10
+
11
+ export interface ExcelPreviewSheet {
12
+ name: string
13
+ rows: ExcelPreviewCell[][]
14
+ }
15
+
16
+ export interface ExcelSearchMatch {
17
+ sheetIndex: number
18
+ row: number
19
+ col: number
20
+ text: string
21
+ }
22
+
23
+ const formatCellValue = (cell?: XLSX.CellObject) => {
24
+ if (!cell) return ''
25
+ if (cell.w != null && cell.w !== '') return String(cell.w)
26
+ if (cell.v == null) return ''
27
+ if (cell.t === 'd') {
28
+ const date = cell.v instanceof Date ? cell.v : new Date(cell.v as number)
29
+ if (!Number.isNaN(date.getTime())) return date.toLocaleString()
30
+ }
31
+ return String(cell.v)
32
+ }
33
+
34
+ export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<ExcelPreviewSheet[]> => {
35
+ const buffer = src instanceof Blob ? await src.arrayBuffer() : src
36
+ const workbook = XLSX.read(buffer, { type: 'array', cellDates: true })
37
+ return workbook.SheetNames.map((name) => ({
38
+ name,
39
+ rows: buildSheetRows(workbook.Sheets[name]),
40
+ }))
41
+ }
42
+
43
+ const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
44
+ if (!sheet || !sheet['!ref']) return []
45
+ const range = XLSX.utils.decode_range(sheet['!ref'])
46
+ const mergeStarts = new Map<string, { rowSpan: number; colSpan: number }>()
47
+ const covered = new Set<string>()
48
+
49
+ for (const merge of sheet['!merges'] || []) {
50
+ const rowSpan = merge.e.r - merge.s.r + 1
51
+ const colSpan = merge.e.c - merge.s.c + 1
52
+ mergeStarts.set(`${merge.s.r},${merge.s.c}`, { rowSpan, colSpan })
53
+ for (let r = merge.s.r; r <= merge.e.r; r++) {
54
+ for (let c = merge.s.c; c <= merge.e.c; c++) {
55
+ if (r !== merge.s.r || c !== merge.s.c) covered.add(`${r},${c}`)
56
+ }
57
+ }
58
+ }
59
+
60
+ const rows: ExcelPreviewCell[][] = []
61
+ for (let r = range.s.r; r <= range.e.r; r++) {
62
+ const row: ExcelPreviewCell[] = []
63
+ for (let c = range.s.c; c <= range.e.c; c++) {
64
+ if (covered.has(`${r},${c}`)) continue
65
+ const addr = XLSX.utils.encode_cell({ r, c })
66
+ const merge = mergeStarts.get(`${r},${c}`)
67
+ row.push({
68
+ value: formatCellValue(sheet[addr]),
69
+ row: r,
70
+ col: c,
71
+ rowSpan: merge?.rowSpan,
72
+ colSpan: merge?.colSpan,
73
+ })
74
+ }
75
+ if (row.length > 0) rows.push(row)
76
+ }
77
+ return rows
78
+ }
79
+
80
+ export const searchExcelSheets = (
81
+ sheets: ExcelPreviewSheet[],
82
+ query: string,
83
+ ): ExcelSearchMatch[] => {
84
+ const keyword = query.trim().toLowerCase()
85
+ if (!keyword) return []
86
+ const matches: ExcelSearchMatch[] = []
87
+ sheets.forEach((sheet, sheetIndex) => {
88
+ for (const row of sheet.rows) {
89
+ for (const cell of row) {
90
+ const text = cell.value
91
+ if (text.toLowerCase().includes(keyword)) {
92
+ matches.push({ sheetIndex, row: cell.row, col: cell.col, text })
93
+ }
94
+ }
95
+ }
96
+ })
97
+ return matches
98
+ }
99
+
100
+ export const cellKey = (sheetIndex: number, row: number, col: number) =>
101
+ `${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
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 浏览器侧下载/拉取文件:将 fileurl 内网绝对地址转为当前页同源相对路径。
3
+ * 将内网绝对地址转为当前页同源相对路径,供浏览器侧拉取文件。
4
+ */
5
+ export function toSameOriginFileUrl(url?: string | null): string {
6
+ if (!url) return ''
7
+ if (url.startsWith('/')) return url
8
+ try {
9
+ const parsed = new URL(url)
10
+ return parsed.pathname + parsed.search + parsed.hash
11
+ } catch {
12
+ return url
13
+ }
14
+ }