1
+ import React from 'react' ;
2
+ import { useAuth } from '@/components/AuthProvider' ;
3
+ import { fetchFileContent } from '@/hooks/react-query/files/use-file-queries' ;
4
+
5
+ declare global {
6
+ interface Window {
7
+ XLSX ?: any ;
8
+ luckysheet ?: any ;
9
+ $ ?: any ;
10
+ jQuery ?: any ;
11
+ }
12
+ }
13
+
14
+ function loadScript ( src : string ) : Promise < void > {
15
+ return new Promise ( ( resolve , reject ) => {
16
+ if ( document . querySelector ( `script[src="${ src } "]` ) ) return resolve ( ) ;
17
+ const s = document . createElement ( 'script' ) ;
18
+ s . src = src ;
19
+ s . async = true ;
20
+ s . onload = ( ) => resolve ( ) ;
21
+ s . onerror = ( ) => reject ( new Error ( `Failed to load ${ src } ` ) ) ;
22
+ document . body . appendChild ( s ) ;
23
+ } ) ;
24
+ }
25
+
26
+ function loadStyle ( href : string ) : void {
27
+ if ( document . querySelector ( `link[href="${ href } "]` ) ) return ;
28
+ const l = document . createElement ( 'link' ) ;
29
+ l . rel = 'stylesheet' ;
30
+ l . href = href ;
31
+ document . head . appendChild ( l ) ;
32
+ }
33
+
34
+ function argbToHex ( argb ?: string ) : string | undefined {
35
+ if ( ! argb || typeof argb !== 'string' ) return undefined ;
36
+ const v = argb . replace ( / ^ # / , '' ) ;
37
+ if ( v . length === 8 ) return `#${ v . slice ( 2 ) } ` ;
38
+ if ( v . length === 6 ) return `#${ v } ` ;
39
+ return undefined ;
40
+ }
41
+
42
+ function mapType ( t : string | undefined ) : string {
43
+ switch ( t ) {
44
+ case 'n' :
45
+ case 'd' :
46
+ case 'b' :
47
+ case 's' :
48
+ case 'str' :
49
+ case 'e' :
50
+ return t ;
51
+ default :
52
+ return 'g' ;
53
+ }
54
+ }
55
+
56
+ export interface LuckysheetViewerProps {
57
+ xlsxPath : string ;
58
+ sandboxId ?: string ;
59
+ className ?: string ;
60
+ height ?: number | string ;
61
+ }
62
+
63
+ export function LuckysheetViewer ( { xlsxPath, sandboxId, className, height } : LuckysheetViewerProps ) {
64
+ const { session } = useAuth ( ) ;
65
+ const wrapperRef = React . useRef < HTMLDivElement | null > ( null ) ;
66
+ const containerRef = React . useRef < HTMLDivElement | null > ( null ) ;
67
+ const containerIdRef = React . useRef < string > ( `luckysheet-${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ) ;
68
+ const [ error , setError ] = React . useState < string | null > ( null ) ;
69
+ const [ loading , setLoading ] = React . useState < boolean > ( true ) ;
70
+ const [ measuredHeight , setMeasuredHeight ] = React . useState < number > ( 0 ) ;
71
+
72
+ React . useEffect ( ( ) => {
73
+ const el = wrapperRef . current ;
74
+ if ( ! el || height ) return ;
75
+ const ro = new ResizeObserver ( ( ) => {
76
+ const rect = el . getBoundingClientRect ( ) ;
77
+ setMeasuredHeight ( Math . max ( 0 , rect . height ) ) ;
78
+ try { window . luckysheet ?. resize ?.( ) ; } catch { }
79
+ } ) ;
80
+ ro . observe ( el ) ;
81
+ const rect = el . getBoundingClientRect ( ) ;
82
+ setMeasuredHeight ( Math . max ( 0 , rect . height ) ) ;
83
+ return ( ) => ro . disconnect ( ) ;
84
+ } , [ height ] ) ;
85
+
86
+ React . useEffect ( ( ) => {
87
+ let disposed = false ;
88
+ async function init ( ) {
89
+ try {
90
+ setLoading ( true ) ;
91
+ setError ( null ) ;
92
+ loadStyle ( 'https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/plugins/css/pluginsCss.css' ) ;
93
+ loadStyle ( 'https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/plugins/plugins.css' ) ;
94
+ loadStyle ( 'https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/css/luckysheet.css' ) ;
95
+
96
+ await loadScript ( 'https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js' ) ;
97
+ if ( ! window . $ && ( window as any ) . jQuery ) window . $ = ( window as any ) . jQuery ;
98
+ await loadScript ( 'https://cdn.jsdelivr.net/npm/[email protected] /jquery.mousewheel.min.js' ) ;
99
+ await loadScript ( 'https://cdn.jsdelivr.net/npm/[email protected] /dist/xlsx.full.min.js' ) ;
100
+ await loadScript ( 'https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/plugins/js/plugin.js' ) ;
101
+ await loadScript ( 'https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/luckysheet.umd.js' ) ;
102
+ if ( disposed ) return ;
103
+
104
+ let ab : ArrayBuffer ;
105
+ if ( sandboxId && session ?. access_token ) {
106
+ const blob = ( await fetchFileContent (
107
+ sandboxId ,
108
+ xlsxPath ,
109
+ 'blob' ,
110
+ session . access_token
111
+ ) ) as Blob ;
112
+ ab = await blob . arrayBuffer ( ) ;
113
+ } else {
114
+ const resp = await fetch ( xlsxPath ) ;
115
+ if ( ! resp . ok ) throw new Error ( `Fetch failed: ${ resp . status } ` ) ;
116
+ ab = await resp . arrayBuffer ( ) ;
117
+ }
118
+
119
+ const XLSX = window . XLSX ;
120
+ const wb = XLSX . read ( ab , { type : 'array' , cellStyles : true } ) ;
121
+
122
+ const sheetsForLucky : any [ ] = [ ] ;
123
+
124
+ wb . SheetNames . forEach ( ( name : string , idx : number ) => {
125
+ const ws = wb . Sheets [ name ] ;
126
+ const ref = ws [ '!ref' ] || 'A1:A1' ;
127
+ const range = XLSX . utils . decode_range ( ref ) ;
128
+
129
+ const celldata : any [ ] = [ ] ;
130
+ for ( let r = range . s . r ; r <= range . e . r ; r ++ ) {
131
+ for ( let c = range . s . c ; c <= range . e . c ; c ++ ) {
132
+ const addr = XLSX . utils . encode_cell ( { r, c } ) ;
133
+ const cell = ws [ addr ] ;
134
+ if ( ! cell ) continue ;
135
+ const v : any = {
136
+ v : cell . v ,
137
+ m : ( cell . w ?? String ( cell . v ?? '' ) ) ,
138
+ ct : { t : mapType ( cell . t ) , fa : cell . z || 'General' } ,
139
+ } ;
140
+ const s = ( cell as any ) . s || { } ;
141
+ const font = s . font || { } ;
142
+ const fill = s . fill || { } ;
143
+ const alignment = s . alignment || { } ;
144
+
145
+ if ( font . bold ) v . bl = 1 ;
146
+ if ( font . italic ) v . it = 1 ;
147
+ if ( font . sz ) v . fs = Number ( font . sz ) ;
148
+ const fc = font . color ?. rgb || font . color ?. rgbColor || font . color ;
149
+ const bg = fill . fgColor ?. rgb || fill . bgColor ?. rgb || fill . fgColor || fill . bgColor ;
150
+ const fcHex = argbToHex ( typeof fc === 'string' ? fc : undefined ) ;
151
+ const bgHex = argbToHex ( typeof bg === 'string' ? bg : undefined ) ;
152
+ if ( fcHex ) v . fc = fcHex ;
153
+ if ( bgHex ) v . bg = bgHex ;
154
+
155
+ if ( alignment ) {
156
+ if ( alignment . horizontal ) v . ht = alignment . horizontal ;
157
+ if ( alignment . vertical ) v . vt = alignment . vertical ;
158
+ if ( alignment . wrapText ) v . tb = 1 ;
159
+ }
160
+
161
+ celldata . push ( { r, c, v } ) ;
162
+ }
163
+ }
164
+
165
+ const mergeConfig : Record < string , any > = { } ;
166
+ const merges = ws [ '!merges' ] || [ ] ;
167
+ merges . forEach ( ( m : any ) => {
168
+ const rs = m . e . r - m . s . r + 1 ;
169
+ const cs = m . e . c - m . s . c + 1 ;
170
+ mergeConfig [ `${ m . s . r } _${ m . s . c } ` ] = { r : m . s . r , c : m . s . c , rs, cs } ;
171
+ } ) ;
172
+
173
+ const columnlen : Record < number , number > = { } ;
174
+ const cols = ws [ '!cols' ] || [ ] ;
175
+ cols . forEach ( ( col : any , i : number ) => {
176
+ const wpx = col . wpx || ( col . wch ? Math . round ( col . wch * 7 ) : undefined ) ;
177
+ if ( wpx ) columnlen [ i ] = wpx ;
178
+ } ) ;
179
+
180
+ const rowlen : Record < number , number > = { } ;
181
+ const rows = ws [ '!rows' ] || [ ] ;
182
+ rows . forEach ( ( row : any , i : number ) => {
183
+ const hpx = row . hpx || ( row . hpt ? Math . round ( row . hpt * 1.33 ) : undefined ) ;
184
+ if ( hpx ) rowlen [ i ] = hpx ;
185
+ } ) ;
186
+
187
+ const config : any = { } ;
188
+ if ( Object . keys ( mergeConfig ) . length ) config . merge = mergeConfig ;
189
+ if ( Object . keys ( columnlen ) . length ) config . columnlen = columnlen ;
190
+ if ( Object . keys ( rowlen ) . length ) config . rowlen = rowlen ;
191
+
192
+ sheetsForLucky . push ( {
193
+ name,
194
+ index : idx ,
195
+ status : 1 ,
196
+ order : idx ,
197
+ celldata,
198
+ config,
199
+ } ) ;
200
+ } ) ;
201
+
202
+ if ( ! containerRef . current ) return ;
203
+ containerRef . current . innerHTML = '' ;
204
+ window . luckysheet ?. create ( {
205
+ container : containerIdRef . current ,
206
+ data : sheetsForLucky ,
207
+ showtoolbar : true ,
208
+ showinfobar : false ,
209
+ showsheetbar : true ,
210
+ allowCopy : true ,
211
+ } ) ;
212
+ if ( ! disposed ) setLoading ( false ) ;
213
+ } catch ( e : any ) {
214
+ if ( ! disposed ) {
215
+ setError ( e ?. message || 'Failed to load sheet' ) ;
216
+ setLoading ( false ) ;
217
+ }
218
+ }
219
+ }
220
+ init ( ) ;
221
+ return ( ) => { disposed = true ; } ;
222
+ } , [ xlsxPath , sandboxId , session ?. access_token ] ) ;
223
+
224
+ const resolvedHeight = height ?? measuredHeight ?? 0 ;
225
+
226
+ return (
227
+ < div ref = { wrapperRef } className = { className } style = { { height : height ? ( typeof height === 'number' ? `${ height } px` : height ) : undefined } } >
228
+ { error ? (
229
+ < div className = "text-sm text-red-600" > { error } </ div >
230
+ ) : (
231
+ < div id = { containerIdRef . current } ref = { containerRef } style = { { height : resolvedHeight , width : '100%' } } />
232
+ ) }
233
+ { loading && ! error && (
234
+ < div className = "text-xs text-muted-foreground mt-2" > Loading formatted viewer…</ div >
235
+ ) }
236
+ </ div >
237
+ ) ;
238
+ }
0 commit comments