@@ -97,14 +97,24 @@ const createLabeledQr = async (text: string, displayName: string) => {
9797 )
9898 } )
9999
100- const padding = 28
101- const headerLines = 2
102- const lineHeight = 22
103- const headerHeight = padding + headerLines * lineHeight
104- const footerPadding = 30
100+ const horizontalPadding = 32
101+ const topPadding = 42
102+ const titleFontSize = 22
103+ const titleSpacing = 10
104+ const nameFontSize = 18
105+ const nameSpacing = 20
106+ const footerPadding = 42
107+
105108 const canvas = document . createElement ( 'canvas' )
106- canvas . width = qrSize + padding * 2
107- canvas . height = headerHeight + qrSize + footerPadding
109+ canvas . width = qrSize + horizontalPadding * 2
110+ canvas . height =
111+ topPadding +
112+ titleFontSize +
113+ titleSpacing +
114+ nameFontSize +
115+ nameSpacing +
116+ qrSize +
117+ footerPadding
108118
109119 const ctx = canvas . getContext ( '2d' )
110120 if ( ! ctx ) {
@@ -118,22 +128,29 @@ const createLabeledQr = async (text: string, displayName: string) => {
118128 ctx . textAlign = 'center'
119129 ctx . textBaseline = 'top'
120130
121- ctx . font = '600 20px "Inter", "Noto Sans", "Helvetica Neue", Arial, sans-serif'
122- ctx . fillText ( 'SHIOAJI API TOKEN' , canvas . width / 2 , padding - 6 )
131+ const centerX = canvas . width / 2
132+ const titleY = topPadding
133+ ctx . font = `700 ${ titleFontSize } px "Inter", "Noto Sans", "Helvetica Neue", Arial, sans-serif`
134+ ctx . fillText ( 'SHIOAJI API QRCODE' , centerX , titleY )
123135
124- ctx . font = '400 16px "Inter", "Noto Sans", "Helvetica Neue", Arial, sans-serif'
125- ctx . fillText ( `NAME: ${ displayName || '未命名' } ` , canvas . width / 2 , padding - 6 + lineHeight )
136+ const nameY = titleY + titleFontSize + titleSpacing
137+ ctx . font = `500 ${ nameFontSize } px "Inter", "Noto Sans", "Helvetica Neue", Arial, sans-serif`
138+ ctx . fillText ( `NAME: ${ displayName || '未命名' } ` , centerX , nameY )
126139
127- const qrY = headerHeight - 12
128- ctx . drawImage ( qrCanvas , padding , qrY , qrSize , qrSize )
140+ const qrY = nameY + nameFontSize + nameSpacing
141+ ctx . drawImage ( qrCanvas , horizontalPadding , qrY , qrSize , qrSize )
129142
130143 ctx . strokeStyle = '#e5e7eb'
131144 ctx . lineWidth = 1
132- ctx . strokeRect ( padding - 1 , qrY - 1 , qrSize + 2 , qrSize + 2 )
145+ ctx . strokeRect ( horizontalPadding - 1 , qrY - 1 , qrSize + 2 , qrSize + 2 )
133146
134147 ctx . font = '400 12px "Inter", "Noto Sans", "Helvetica Neue", Arial, sans-serif'
135148 ctx . fillStyle = '#6b7280'
136- ctx . fillText ( 'https://sinotrade.github.io/sj-qrcode/' , canvas . width / 2 , canvas . height - footerPadding + 6 )
149+ ctx . fillText (
150+ 'https://sinotrade.github.io/sj-qrcode/' ,
151+ centerX ,
152+ canvas . height - footerPadding + 12 ,
153+ )
137154
138155 return canvas . toDataURL ( 'image/png' )
139156}
@@ -163,13 +180,18 @@ function App() {
163180 const formatted = JSON . stringify ( payload , null , 2 )
164181
165182 try {
166- const labeledQr = await createLabeledQr (
167- formatted ,
168- values . name . trim ( ) || '未命名' ,
169- )
183+ const dataUrl = await QRCode . toDataURL ( formatted , {
184+ errorCorrectionLevel : 'M' ,
185+ width : 320 ,
186+ margin : 1 ,
187+ color : {
188+ dark : '#030712' ,
189+ light : '#ffffff' ,
190+ } ,
191+ } )
170192
171193 setJsonPayload ( formatted )
172- setQrCodeDataUrl ( labeledQr )
194+ setQrCodeDataUrl ( dataUrl )
173195 toast . success ( 'QR code 已完成' )
174196 } catch ( error ) {
175197 console . error ( 'Failed to generate QR code' , error )
@@ -192,20 +214,28 @@ function App() {
192214 }
193215 }
194216
195- const handleDownloadQr = ( ) => {
196- if ( ! qrCodeDataUrl ) {
217+ const handleDownloadQr = async ( ) => {
218+ if ( ! jsonPayload ) {
197219 toast . error ( '尚未生成 QR code' )
198220 return
199221 }
200222
201- const link = document . createElement ( 'a' )
202- const timestamp = new Date ( ) . toISOString ( ) . replaceAll ( / [: .] / g, '-' ) . slice ( 0 , 19 )
203- const normalizedName = formatFileNameSegment ( form . getValues ( 'name' ) ?? '' )
204- link . href = qrCodeDataUrl
205- link . download = `sj-token-${ normalizedName } -${ timestamp } .png`
206- link . click ( )
223+ try {
224+ const displayName = form . getValues ( 'name' ) ?. trim ( ) || '未命名'
225+ const labeledQr = await createLabeledQr ( jsonPayload , displayName )
226+
227+ const link = document . createElement ( 'a' )
228+ const timestamp = new Date ( ) . toISOString ( ) . replaceAll ( / [: .] / g, '-' ) . slice ( 0 , 19 )
229+ const normalizedName = formatFileNameSegment ( displayName )
230+ link . href = labeledQr
231+ link . download = `sj-token-${ normalizedName } -${ timestamp } .png`
232+ link . click ( )
207233
208- toast . success ( 'QR code 已開始下載' )
234+ toast . success ( 'QR code 已開始下載' )
235+ } catch ( error ) {
236+ console . error ( 'Failed to export labeled QR code' , error )
237+ toast . error ( '下載 QR code 時發生錯誤' )
238+ }
209239 }
210240
211241 const handleDownloadJson = ( ) => {
@@ -369,6 +399,15 @@ function App() {
369399 alt = "SJ QR code"
370400 className = "h-auto w-60 rounded border bg-white p-2 shadow"
371401 />
402+ < Button
403+ type = "button"
404+ variant = "secondary"
405+ className = "w-full max-w-xs"
406+ disabled = { ! qrCodeDataUrl }
407+ onClick = { handleDownloadQr }
408+ >
409+ < Download className = "mr-2 h-4 w-4" /> 下載 QR code
410+ </ Button >
372411 < p className = "text-center text-xs text-muted-foreground" >
373412 如果顯示模糊,請再試一次或提高螢幕亮度後掃描。
374413 </ p >
@@ -384,19 +423,20 @@ function App() {
384423
385424 < Separator />
386425
387- < div className = "flex flex-col gap-3 lg:flex-[1.3 ]" >
426+ < div className = "flex flex-col gap-3 lg:flex-[1.05 ]" >
388427 < p className = "text-sm font-medium text-muted-foreground" > JSON 資料</ p >
389428 < Textarea
390429 value = { jsonPayload }
391430 readOnly
392431 placeholder = "完成表單後會自動生成 JSON 資料"
393432 wrap = "off"
394433 spellCheck = { false }
395- className = "min-h-[220px] resize-y overflow-auto font-mono text-sm leading-relaxed lg:h-full lg:min-h-0"
434+ rows = { 5 }
435+ className = "resize-y overflow-auto font-mono text-sm leading-relaxed lg:h-full lg:min-h-0"
396436 />
397437 </ div >
398438 </ CardContent >
399- < CardFooter className = "flex gap-3 overflow-x-auto pb-4 pt-0 flex-nowrap " >
439+ < CardFooter className = "flex flex-wrap items-center justify-center gap-3 pb-4 pt-0" >
400440 < Button
401441 type = "button"
402442 variant = "secondary"
@@ -413,14 +453,6 @@ function App() {
413453 >
414454 < FileDown className = "mr-2 h-4 w-4" /> 下載 JSON
415455 </ Button >
416- < Button
417- type = "button"
418- variant = "secondary"
419- disabled = { ! qrCodeDataUrl }
420- onClick = { handleDownloadQr }
421- >
422- < Download className = "mr-2 h-4 w-4" /> 下載 QR code
423- </ Button >
424456 </ CardFooter >
425457 </ Card >
426458 </ section >
0 commit comments