vue2server7 4.0.0 → 5.0.1
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.
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button @click="handleExport">导出Excel</button>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { exportBankExcelWithDefaultLogo } from '../utils/exprotExcel'
|
|
9
|
+
|
|
10
|
+
const handleExport = async (): Promise<void> => {
|
|
11
|
+
await exportBankExcelWithDefaultLogo()
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import TablePage from '../pages/TablePage.vue'
|
|
2
2
|
import CascaderPage from '../pages/CascaderPage.vue'
|
|
3
|
+
import ExportExcelPage from '../pages/ExportExcelPage.vue'
|
|
3
4
|
|
|
4
5
|
export const routes = [
|
|
5
6
|
{
|
|
@@ -23,5 +24,14 @@ export const routes = [
|
|
|
23
24
|
title: '级联选择',
|
|
24
25
|
showInMenu: true
|
|
25
26
|
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '/export-excel',
|
|
30
|
+
name: 'ExportExcel',
|
|
31
|
+
component: ExportExcelPage,
|
|
32
|
+
meta: {
|
|
33
|
+
title: '导出Excel',
|
|
34
|
+
showInMenu: true
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
]
|
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
|
|
2
|
+
import ExcelJS from 'exceljs'
|
|
3
|
+
|
|
4
|
+
const saveBlobAsFile = (blob: Blob, fileName: string): void => {
|
|
5
|
+
const url = URL.createObjectURL(blob)
|
|
6
|
+
const a = document.createElement('a')
|
|
7
|
+
a.href = url
|
|
8
|
+
a.download = fileName
|
|
9
|
+
a.rel = 'noopener'
|
|
10
|
+
document.body.appendChild(a)
|
|
11
|
+
a.click()
|
|
12
|
+
a.remove()
|
|
13
|
+
URL.revokeObjectURL(url)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 导出 Excel 所需的业务数据结构。
|
|
18
|
+
* - 该结构尽量与表格展示字段保持 1:1,对应关系更直观
|
|
19
|
+
* - 数值字段建议统一使用“亿”为单位(与表格右侧单位一致)
|
|
20
|
+
*/
|
|
21
|
+
export interface ExportFormData {
|
|
22
|
+
/**
|
|
23
|
+
* 导出文件与表格标题。
|
|
24
|
+
* - 同时用于生成文件名:`${title}.xlsx`
|
|
25
|
+
*/
|
|
26
|
+
title: string
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 表格日期显示文本(建议格式:YYYY-MM-DD)。
|
|
30
|
+
*/
|
|
31
|
+
date: string
|
|
32
|
+
|
|
33
|
+
// 顶部当前可用余额
|
|
34
|
+
currentAvailableBalance: number
|
|
35
|
+
|
|
36
|
+
// 今日大额收付款报备情况
|
|
37
|
+
reportAmountIncome: number
|
|
38
|
+
reportAmountPay: number
|
|
39
|
+
|
|
40
|
+
unverifiedWriteOffIncome: number
|
|
41
|
+
unverifiedWriteOffPay: number
|
|
42
|
+
|
|
43
|
+
// 未核销业务流水
|
|
44
|
+
unliquidatedBusinessIncome: number
|
|
45
|
+
unliquidatedBusinessPay: number
|
|
46
|
+
|
|
47
|
+
// 日初可用资金(红字)
|
|
48
|
+
initialAvailableFunds: number
|
|
49
|
+
|
|
50
|
+
// 今日预计调拨金额
|
|
51
|
+
expectedTransferIncome: number
|
|
52
|
+
expectedTransferPay: number
|
|
53
|
+
expectedTransferDiff: number
|
|
54
|
+
|
|
55
|
+
// 跨境人民币头寸清算账户预留金额
|
|
56
|
+
reserveAmount: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 示例数据:页面点击“导出Excel”时使用该对象生成报表。
|
|
61
|
+
* 实际接入时可替换为接口返回结果或表单输入值。
|
|
62
|
+
*/
|
|
63
|
+
export const exportFormDataDemo: ExportFormData = {
|
|
64
|
+
title: '跨境人民币头寸压算表',
|
|
65
|
+
date: '2026-02-02',
|
|
66
|
+
|
|
67
|
+
currentAvailableBalance: 0,
|
|
68
|
+
|
|
69
|
+
reportAmountIncome: 0,
|
|
70
|
+
reportAmountPay: 0,
|
|
71
|
+
|
|
72
|
+
unverifiedWriteOffIncome: 0,
|
|
73
|
+
unverifiedWriteOffPay: 0,
|
|
74
|
+
|
|
75
|
+
unliquidatedBusinessIncome: 0,
|
|
76
|
+
unliquidatedBusinessPay: 0,
|
|
77
|
+
|
|
78
|
+
initialAvailableFunds: 0,
|
|
79
|
+
|
|
80
|
+
expectedTransferIncome: 2,
|
|
81
|
+
expectedTransferPay: 2,
|
|
82
|
+
expectedTransferDiff: 0,
|
|
83
|
+
|
|
84
|
+
reserveAmount: 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Logo 插入说明(重点)
|
|
89
|
+
* 1) 只允许插入“静态图片”:PNG / JPG(JPEG),不插入 GIF 等动图
|
|
90
|
+
* 2) 图片来自 public 目录(打包时会被静态托管),通过 fetch 拉取后转 base64 传给 ExcelJS
|
|
91
|
+
* 3) 通过缓存避免每次点击“导出Excel”都重复请求与转换,提升稳定性
|
|
92
|
+
*
|
|
93
|
+
* public 目录下的静态资源会映射到站点根路径:
|
|
94
|
+
* - 文件:frontEnd/public/logo.jpg
|
|
95
|
+
* - URL: /logo.jpg
|
|
96
|
+
*/
|
|
97
|
+
const LOGO_PUBLIC_PATH = '/logo.jpg'
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 仅允许的静态图片扩展名。
|
|
101
|
+
* - ExcelJS 的 addImage 需要显式的 extension 参数
|
|
102
|
+
*/
|
|
103
|
+
export type StaticLogoExtension = 'png' | 'jpeg'
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 静态 Logo 的内存表示。
|
|
107
|
+
* - base64:不包含 data: 前缀,仅为纯 base64 内容
|
|
108
|
+
* - extension:与图片真实格式一致
|
|
109
|
+
*/
|
|
110
|
+
export type StaticLogo = { base64: string; extension: StaticLogoExtension }
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Logo 缓存:避免重复 fetch 与 base64 转换,提升导出稳定性与性能。
|
|
114
|
+
*/
|
|
115
|
+
let cachedStaticLogo: StaticLogo | null = null
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 将 Blob 转换为纯 base64 字符串(去掉 dataURL 头部)。
|
|
119
|
+
* @param blob 通过 fetch 获取到的图片二进制
|
|
120
|
+
* @returns 纯 base64 字符串;解析失败时返回空字符串
|
|
121
|
+
*/
|
|
122
|
+
const blobToBase64 = (blob: Blob): Promise<string> => {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const reader = new FileReader()
|
|
125
|
+
reader.onload = () => {
|
|
126
|
+
const result = reader.result
|
|
127
|
+
if (typeof result !== 'string') {
|
|
128
|
+
resolve('')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const commaIndex = result.indexOf(',')
|
|
133
|
+
resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result)
|
|
134
|
+
}
|
|
135
|
+
reader.onerror = () => reject(new Error('读取图片失败'))
|
|
136
|
+
reader.readAsDataURL(blob)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 根据 content-type 与 URL 后缀推断图片扩展名。
|
|
142
|
+
* - 优先使用 content-type(更可靠)
|
|
143
|
+
* - content-type 缺失时再回退到 URL 后缀
|
|
144
|
+
* @param contentType response header: content-type
|
|
145
|
+
* @param urlPath 图片 URL 路径(例如 /logo.jpg)
|
|
146
|
+
* @returns 可支持的 extension;不支持时返回 null
|
|
147
|
+
*/
|
|
148
|
+
const guessLogoExtension = (contentType: string, urlPath: string): StaticLogoExtension | null => {
|
|
149
|
+
const ct = contentType.toLowerCase()
|
|
150
|
+
if (ct.includes('image/png')) return 'png'
|
|
151
|
+
if (ct.includes('image/jpeg') || ct.includes('image/jpg')) return 'jpeg'
|
|
152
|
+
|
|
153
|
+
const lowerPath = urlPath.toLowerCase()
|
|
154
|
+
if (lowerPath.endsWith('.png')) return 'png'
|
|
155
|
+
if (lowerPath.endsWith('.jpg') || lowerPath.endsWith('.jpeg')) return 'jpeg'
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 获取“可插入 Excel 的静态 Logo”。
|
|
161
|
+
* - 从 public 目录拉取图片(站点根路径映射)
|
|
162
|
+
* - 过滤 GIF 等动图格式,避免 ExcelJS 插入异常
|
|
163
|
+
* - 首次成功后写入内存缓存,后续直接复用
|
|
164
|
+
* @returns Logo 信息;不可用或失败时返回 null
|
|
165
|
+
*/
|
|
166
|
+
export const getStaticLogo = async (): Promise<StaticLogo | null> => {
|
|
167
|
+
if (cachedStaticLogo) return cachedStaticLogo
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(LOGO_PUBLIC_PATH)
|
|
171
|
+
if (!res.ok) return null
|
|
172
|
+
|
|
173
|
+
const contentType = res.headers.get('content-type') ?? ''
|
|
174
|
+
const extension = guessLogoExtension(contentType, LOGO_PUBLIC_PATH)
|
|
175
|
+
if (!extension) return null
|
|
176
|
+
|
|
177
|
+
if (contentType.toLowerCase().includes('image/gif')) return null
|
|
178
|
+
|
|
179
|
+
const blob = await res.blob()
|
|
180
|
+
if (blob.type.toLowerCase().includes('image/gif')) return null
|
|
181
|
+
|
|
182
|
+
const base64 = await blobToBase64(blob)
|
|
183
|
+
if (!base64) return null
|
|
184
|
+
|
|
185
|
+
cachedStaticLogo = { base64, extension }
|
|
186
|
+
return cachedStaticLogo
|
|
187
|
+
} catch {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
export interface ExportBankExcelOptions {
|
|
195
|
+
/**
|
|
196
|
+
* 需要插入到工作表中的静态 Logo(可选)。
|
|
197
|
+
* - 为 null/undefined 时回退到文字 Logo
|
|
198
|
+
*/
|
|
199
|
+
logo?: StaticLogo | null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 生成并下载“跨境人民币头寸压算表”Excel。
|
|
204
|
+
* @param data 业务数据
|
|
205
|
+
* @param options 可选配置(例如 Logo)
|
|
206
|
+
*/
|
|
207
|
+
export async function exportBankExcel(data: ExportFormData, options: ExportBankExcelOptions = {}): Promise<void> {
|
|
208
|
+
const workbook = new ExcelJS.Workbook()
|
|
209
|
+
const worksheet = workbook.addWorksheet('跨境人民币头寸压算表', {
|
|
210
|
+
views: [{ showGridLines: true }]
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// 页面设置
|
|
214
|
+
worksheet.pageSetup = {
|
|
215
|
+
paperSize: 9,
|
|
216
|
+
orientation: 'landscape',
|
|
217
|
+
fitToPage: true,
|
|
218
|
+
fitToWidth: 1,
|
|
219
|
+
fitToHeight: 0,
|
|
220
|
+
margins: {
|
|
221
|
+
left: 0.3,
|
|
222
|
+
right: 0.3,
|
|
223
|
+
top: 0.3,
|
|
224
|
+
bottom: 0.3,
|
|
225
|
+
header: 0.1,
|
|
226
|
+
footer: 0.1
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 列宽说明(重点)
|
|
231
|
+
// 1) ExcelJS 的 column.width 单位是“字符宽度”(大致相当于默认字体下能容纳多少个字符),不是像素
|
|
232
|
+
// 2) 合并单元格(mergeCells)并不会单独有“合并后的宽度”,它的可视宽度 = 被合并列宽之和
|
|
233
|
+
// 3) 因此要调整某个合并区域(例如 A3:B3)的显示效果,应优先调整 A 列和 B 列的宽度
|
|
234
|
+
//
|
|
235
|
+
// 下面给一组更适配当前表格内容的默认列宽(可直接改这里达到你想要的效果)
|
|
236
|
+
const COLUMN_WIDTHS = {
|
|
237
|
+
A: 28,
|
|
238
|
+
B: 16,
|
|
239
|
+
C: 18,
|
|
240
|
+
D: 18,
|
|
241
|
+
E: 14
|
|
242
|
+
} as const
|
|
243
|
+
|
|
244
|
+
const applyColumnWidths = (): void => {
|
|
245
|
+
worksheet.columns = [
|
|
246
|
+
{ width: COLUMN_WIDTHS.A },
|
|
247
|
+
{ width: COLUMN_WIDTHS.B },
|
|
248
|
+
{ width: COLUMN_WIDTHS.C },
|
|
249
|
+
{ width: COLUMN_WIDTHS.D },
|
|
250
|
+
{ width: COLUMN_WIDTHS.E }
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
applyColumnWidths()
|
|
255
|
+
|
|
256
|
+
// 行高说明
|
|
257
|
+
// 1) row.height 单位是“点”(points),会直接影响打印/分页效果
|
|
258
|
+
// 2) 建议用配置表统一管理,避免散落在代码中导致后续维护不稳定
|
|
259
|
+
const ROW_HEIGHTS: Record<number, number> = {
|
|
260
|
+
1: 34,
|
|
261
|
+
2: 26,
|
|
262
|
+
3: 24,
|
|
263
|
+
4: 24,
|
|
264
|
+
5: 24,
|
|
265
|
+
6: 24,
|
|
266
|
+
7: 24,
|
|
267
|
+
8: 24,
|
|
268
|
+
9: 24,
|
|
269
|
+
10: 24,
|
|
270
|
+
11: 20
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const [rowIndex, height] of Object.entries(ROW_HEIGHTS)) {
|
|
274
|
+
worksheet.getRow(Number(rowIndex)).height = height
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const BORDER_THIN: Partial<ExcelJS.Borders> = {
|
|
278
|
+
top: { style: 'thin', color: { argb: '000000' } },
|
|
279
|
+
left: { style: 'thin', color: { argb: '000000' } },
|
|
280
|
+
bottom: { style: 'thin', color: { argb: '000000' } },
|
|
281
|
+
right: { style: 'thin', color: { argb: '000000' } }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const BORDER_MEDIUM: Partial<ExcelJS.Borders> = {
|
|
285
|
+
top: { style: 'medium', color: { argb: '000000' } },
|
|
286
|
+
left: { style: 'medium', color: { argb: '000000' } },
|
|
287
|
+
bottom: { style: 'medium', color: { argb: '000000' } },
|
|
288
|
+
right: { style: 'medium', color: { argb: '000000' } }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const FONT_NORMAL: Partial<ExcelJS.Font> = {
|
|
292
|
+
name: 'Microsoft YaHei',
|
|
293
|
+
size: 12,
|
|
294
|
+
color: { argb: '333333' }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const FONT_TITLE: Partial<ExcelJS.Font> = {
|
|
298
|
+
name: 'Microsoft YaHei',
|
|
299
|
+
size: 22,
|
|
300
|
+
color: { argb: '333333' }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const FONT_DATE: Partial<ExcelJS.Font> = {
|
|
304
|
+
name: 'Microsoft YaHei',
|
|
305
|
+
size: 12,
|
|
306
|
+
color: { argb: '666666' }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const FONT_RED_BOLD: Partial<ExcelJS.Font> = {
|
|
310
|
+
name: 'Microsoft YaHei',
|
|
311
|
+
size: 14,
|
|
312
|
+
bold: true,
|
|
313
|
+
color: { argb: 'C00000' }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const FONT_RED_BANK: Partial<ExcelJS.Font> = {
|
|
317
|
+
name: 'Microsoft YaHei',
|
|
318
|
+
size: 18,
|
|
319
|
+
bold: true,
|
|
320
|
+
color: { argb: 'C00000' }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ALIGN_CENTER: Partial<ExcelJS.Alignment> = {
|
|
324
|
+
vertical: 'middle',
|
|
325
|
+
horizontal: 'center',
|
|
326
|
+
wrapText: true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const ALIGN_RIGHT: Partial<ExcelJS.Alignment> = {
|
|
330
|
+
vertical: 'middle',
|
|
331
|
+
horizontal: 'right'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const PINK_FILL: ExcelJS.Fill = {
|
|
335
|
+
type: 'pattern',
|
|
336
|
+
pattern: 'solid',
|
|
337
|
+
fgColor: { argb: 'F3D4DC' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const BLUE_FILL: ExcelJS.Fill = {
|
|
341
|
+
type: 'pattern',
|
|
342
|
+
pattern: 'solid',
|
|
343
|
+
fgColor: { argb: '18B6E6' }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const CYAN_FILL: ExcelJS.Fill = {
|
|
347
|
+
type: 'pattern',
|
|
348
|
+
pattern: 'solid',
|
|
349
|
+
fgColor: { argb: '00B7E8' }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const LIGHT_BLUE_FILL: ExcelJS.Fill = {
|
|
353
|
+
type: 'pattern',
|
|
354
|
+
pattern: 'solid',
|
|
355
|
+
fgColor: { argb: 'FFDCE6F1' }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const GREEN_FILL: ExcelJS.Fill = {
|
|
359
|
+
type: 'pattern',
|
|
360
|
+
pattern: 'solid',
|
|
361
|
+
fgColor: { argb: '8EBB43' }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 设置单个单元格的值与样式。
|
|
366
|
+
* @param address 单元格地址(例如 A1、C3)
|
|
367
|
+
* @param value 单元格值
|
|
368
|
+
* @param options 样式选项(字体/对齐/填充/边框/数字格式)
|
|
369
|
+
*/
|
|
370
|
+
const setCell = (
|
|
371
|
+
address: string,
|
|
372
|
+
value: string | number,
|
|
373
|
+
options?: {
|
|
374
|
+
font?: Partial<ExcelJS.Font>
|
|
375
|
+
alignment?: Partial<ExcelJS.Alignment>
|
|
376
|
+
fill?: ExcelJS.Fill
|
|
377
|
+
border?: Partial<ExcelJS.Borders>
|
|
378
|
+
numFmt?: string
|
|
379
|
+
}
|
|
380
|
+
): void => {
|
|
381
|
+
const cell = worksheet.getCell(address)
|
|
382
|
+
cell.value = value
|
|
383
|
+
|
|
384
|
+
if (options?.font) {
|
|
385
|
+
cell.font = options.font as ExcelJS.Font
|
|
386
|
+
}
|
|
387
|
+
if (options?.alignment) {
|
|
388
|
+
cell.alignment = options.alignment as ExcelJS.Alignment
|
|
389
|
+
}
|
|
390
|
+
if (options?.fill) {
|
|
391
|
+
cell.fill = options.fill
|
|
392
|
+
}
|
|
393
|
+
if (options?.border) {
|
|
394
|
+
cell.border = options.border as ExcelJS.Borders
|
|
395
|
+
}
|
|
396
|
+
if (options?.numFmt) {
|
|
397
|
+
cell.numFmt = options.numFmt
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 给指定区域批量设置边框。
|
|
403
|
+
* @param startRow 起始行(从 1 开始)
|
|
404
|
+
* @param endRow 结束行(包含)
|
|
405
|
+
* @param startCol 起始列(A=1)
|
|
406
|
+
* @param endCol 结束列(包含)
|
|
407
|
+
* @param border 边框样式,默认细边框
|
|
408
|
+
*/
|
|
409
|
+
const setRangeBorder = (
|
|
410
|
+
startRow: number,
|
|
411
|
+
endRow: number,
|
|
412
|
+
startCol: number,
|
|
413
|
+
endCol: number,
|
|
414
|
+
border: Partial<ExcelJS.Borders> = BORDER_THIN
|
|
415
|
+
): void => {
|
|
416
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
417
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
418
|
+
worksheet.getCell(r, c).border = border as ExcelJS.Borders
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 给指定区域设置“外边框加粗、内部细线”的效果。
|
|
425
|
+
* @param startRow 起始行(从 1 开始)
|
|
426
|
+
* @param endRow 结束行(包含)
|
|
427
|
+
* @param startCol 起始列(A=1)
|
|
428
|
+
* @param endCol 结束列(包含)
|
|
429
|
+
*/
|
|
430
|
+
const setOuterMediumBorder = (
|
|
431
|
+
startRow: number,
|
|
432
|
+
endRow: number,
|
|
433
|
+
startCol: number,
|
|
434
|
+
endCol: number
|
|
435
|
+
): void => {
|
|
436
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
437
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
438
|
+
const cell = worksheet.getCell(r, c)
|
|
439
|
+
const border: Partial<ExcelJS.Borders> = {
|
|
440
|
+
top: { style: 'thin', color: { argb: '000000' } },
|
|
441
|
+
left: { style: 'thin', color: { argb: '000000' } },
|
|
442
|
+
bottom: { style: 'thin', color: { argb: '000000' } },
|
|
443
|
+
right: { style: 'thin', color: { argb: '000000' } }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (r === startRow) border.top = { style: 'medium', color: { argb: '000000' } }
|
|
447
|
+
if (r === endRow) border.bottom = { style: 'medium', color: { argb: '000000' } }
|
|
448
|
+
if (c === startCol) border.left = { style: 'medium', color: { argb: '000000' } }
|
|
449
|
+
if (c === endCol) border.right = { style: 'medium', color: { argb: '000000' } }
|
|
450
|
+
|
|
451
|
+
cell.border = border as ExcelJS.Borders
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 统一数值兜底,避免 undefined/null 导致单元格为空或计算报错。
|
|
458
|
+
*/
|
|
459
|
+
const formatAmount = (value: number | null | undefined): number => value ?? 0
|
|
460
|
+
|
|
461
|
+
// ========== 顶部区域 ==========
|
|
462
|
+
worksheet.mergeCells('A1:B2')
|
|
463
|
+
worksheet.mergeCells('C1:E1')
|
|
464
|
+
|
|
465
|
+
const logo = options.logo
|
|
466
|
+
|
|
467
|
+
if (logo?.base64) {
|
|
468
|
+
const imageId = workbook.addImage({
|
|
469
|
+
base64: logo.base64,
|
|
470
|
+
extension: logo.extension
|
|
471
|
+
})
|
|
472
|
+
worksheet.addImage(imageId, {
|
|
473
|
+
tl: { col: 0.15, row: 0.25 },
|
|
474
|
+
ext: { width: 240, height: 52 }
|
|
475
|
+
})
|
|
476
|
+
} else {
|
|
477
|
+
setCell('A1', '北京银行\nBANK OF BEIJING', {
|
|
478
|
+
font: FONT_RED_BANK,
|
|
479
|
+
alignment: {
|
|
480
|
+
vertical: 'middle',
|
|
481
|
+
horizontal: 'center',
|
|
482
|
+
wrapText: true
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 顶部粉色标题块
|
|
488
|
+
for (const addr of ['C1', 'D1', 'E1', 'C2', 'D2', 'E2']) {
|
|
489
|
+
worksheet.getCell(addr).fill = PINK_FILL
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
setCell('C1', data.title, {
|
|
493
|
+
font: FONT_TITLE,
|
|
494
|
+
alignment: ALIGN_CENTER,
|
|
495
|
+
fill: PINK_FILL
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
setCell('D2', data.date, {
|
|
499
|
+
font: FONT_DATE,
|
|
500
|
+
alignment: ALIGN_RIGHT,
|
|
501
|
+
fill: PINK_FILL
|
|
502
|
+
})
|
|
503
|
+
setCell('E2', '备注', {
|
|
504
|
+
font: FONT_NORMAL,
|
|
505
|
+
alignment: ALIGN_CENTER,
|
|
506
|
+
fill: PINK_FILL
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// ========== 主体表格 ==========
|
|
510
|
+
// A3:B3
|
|
511
|
+
worksheet.mergeCells('A3:B3')
|
|
512
|
+
setCell('A3', '跨境人民币头寸账户当前可用余额', {
|
|
513
|
+
font: FONT_NORMAL,
|
|
514
|
+
alignment: ALIGN_CENTER,
|
|
515
|
+
border: BORDER_THIN
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// C3:D3
|
|
519
|
+
worksheet.mergeCells('C3:D3')
|
|
520
|
+
setCell('C3', formatAmount(data.currentAvailableBalance), {
|
|
521
|
+
font: FONT_NORMAL,
|
|
522
|
+
alignment: ALIGN_CENTER,
|
|
523
|
+
border: BORDER_THIN,
|
|
524
|
+
numFmt: '0.00'
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
// E3
|
|
528
|
+
setCell('E3', '单位:亿元', {
|
|
529
|
+
font: FONT_NORMAL,
|
|
530
|
+
alignment: ALIGN_CENTER,
|
|
531
|
+
border: BORDER_THIN
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
worksheet.mergeCells('A4:B4')
|
|
535
|
+
setCell('A4', '', {
|
|
536
|
+
font: FONT_NORMAL,
|
|
537
|
+
alignment: ALIGN_CENTER,
|
|
538
|
+
border: BORDER_THIN
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
worksheet.mergeCells('A5:A6')
|
|
542
|
+
setCell('A5', '今日大额收付款报备情况', {
|
|
543
|
+
font: FONT_NORMAL,
|
|
544
|
+
alignment: ALIGN_CENTER,
|
|
545
|
+
border: BORDER_THIN
|
|
546
|
+
})
|
|
547
|
+
setCell('B5', '报备金额', {
|
|
548
|
+
font: FONT_NORMAL,
|
|
549
|
+
alignment: ALIGN_CENTER,
|
|
550
|
+
border: BORDER_THIN
|
|
551
|
+
})
|
|
552
|
+
setCell('B6', '未核销金额', {
|
|
553
|
+
font: FONT_NORMAL,
|
|
554
|
+
alignment: ALIGN_CENTER,
|
|
555
|
+
border: BORDER_THIN
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
// C4
|
|
559
|
+
setCell('C4', '收入', {
|
|
560
|
+
font: FONT_NORMAL,
|
|
561
|
+
alignment: ALIGN_CENTER,
|
|
562
|
+
border: BORDER_THIN
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// D4
|
|
566
|
+
setCell('D4', '支付', {
|
|
567
|
+
font: FONT_NORMAL,
|
|
568
|
+
alignment: ALIGN_CENTER,
|
|
569
|
+
border: BORDER_THIN
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
// E4
|
|
573
|
+
setCell('E4', '轧差', {
|
|
574
|
+
font: FONT_NORMAL,
|
|
575
|
+
alignment: ALIGN_CENTER,
|
|
576
|
+
border: BORDER_THIN
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// C5 / D5 / E5
|
|
580
|
+
setCell('C5', formatAmount(data.reportAmountIncome), {
|
|
581
|
+
font: FONT_NORMAL,
|
|
582
|
+
alignment: ALIGN_CENTER,
|
|
583
|
+
border: BORDER_THIN,
|
|
584
|
+
numFmt: '0.00'
|
|
585
|
+
})
|
|
586
|
+
setCell('D5', formatAmount(data.reportAmountPay), {
|
|
587
|
+
font: FONT_NORMAL,
|
|
588
|
+
alignment: ALIGN_CENTER,
|
|
589
|
+
border: BORDER_THIN,
|
|
590
|
+
numFmt: '0.00'
|
|
591
|
+
})
|
|
592
|
+
setCell('E5', formatAmount(data.reportAmountIncome - data.reportAmountPay), {
|
|
593
|
+
font: FONT_NORMAL,
|
|
594
|
+
alignment: ALIGN_CENTER,
|
|
595
|
+
border: BORDER_THIN,
|
|
596
|
+
fill: BLUE_FILL,
|
|
597
|
+
numFmt: '0.00'
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// C6 / D6 / E6
|
|
601
|
+
setCell('C6', formatAmount(data.unverifiedWriteOffIncome), {
|
|
602
|
+
font: FONT_NORMAL,
|
|
603
|
+
alignment: ALIGN_CENTER,
|
|
604
|
+
border: BORDER_THIN,
|
|
605
|
+
numFmt: '0.00'
|
|
606
|
+
})
|
|
607
|
+
setCell('D6', formatAmount(data.unverifiedWriteOffPay), {
|
|
608
|
+
font: FONT_NORMAL,
|
|
609
|
+
alignment: ALIGN_CENTER,
|
|
610
|
+
border: BORDER_THIN,
|
|
611
|
+
numFmt: '0.00'
|
|
612
|
+
})
|
|
613
|
+
setCell('E6', formatAmount(data.unverifiedWriteOffIncome - data.unverifiedWriteOffPay), {
|
|
614
|
+
font: FONT_NORMAL,
|
|
615
|
+
alignment: ALIGN_CENTER,
|
|
616
|
+
border: BORDER_THIN,
|
|
617
|
+
fill: CYAN_FILL,
|
|
618
|
+
numFmt: '0.00'
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
// A7:B7
|
|
622
|
+
worksheet.mergeCells('A7:B7')
|
|
623
|
+
setCell('A7', '未核销业务流水', {
|
|
624
|
+
font: FONT_NORMAL,
|
|
625
|
+
alignment: ALIGN_CENTER,
|
|
626
|
+
border: BORDER_THIN
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
setCell('C7', formatAmount(data.unliquidatedBusinessIncome), {
|
|
630
|
+
font: FONT_NORMAL,
|
|
631
|
+
alignment: ALIGN_CENTER,
|
|
632
|
+
border: BORDER_THIN,
|
|
633
|
+
numFmt: '0.00'
|
|
634
|
+
})
|
|
635
|
+
setCell('D7', formatAmount(data.unliquidatedBusinessPay), {
|
|
636
|
+
font: FONT_NORMAL,
|
|
637
|
+
alignment: ALIGN_CENTER,
|
|
638
|
+
border: BORDER_THIN,
|
|
639
|
+
numFmt: '0.00'
|
|
640
|
+
})
|
|
641
|
+
setCell('E7', formatAmount(data.unliquidatedBusinessIncome - data.unliquidatedBusinessPay), {
|
|
642
|
+
font: FONT_NORMAL,
|
|
643
|
+
alignment: ALIGN_CENTER,
|
|
644
|
+
border: BORDER_THIN,
|
|
645
|
+
fill: GREEN_FILL,
|
|
646
|
+
numFmt: '0.00'
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// A8:B8
|
|
650
|
+
worksheet.mergeCells('A8:B8')
|
|
651
|
+
setCell('A8', '日初可用资金', {
|
|
652
|
+
font: FONT_NORMAL,
|
|
653
|
+
alignment: ALIGN_CENTER,
|
|
654
|
+
border: BORDER_THIN
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// C8:E8
|
|
658
|
+
worksheet.mergeCells('C8:E8')
|
|
659
|
+
setCell('C8', formatAmount(data.initialAvailableFunds), {
|
|
660
|
+
font: FONT_RED_BOLD,
|
|
661
|
+
alignment: ALIGN_CENTER,
|
|
662
|
+
border: BORDER_THIN,
|
|
663
|
+
numFmt: '0.00'
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
worksheet.mergeCells('A9:E9')
|
|
667
|
+
setCell('A9', '', {
|
|
668
|
+
alignment: ALIGN_CENTER,
|
|
669
|
+
border: BORDER_THIN
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
// A10:B10
|
|
673
|
+
worksheet.mergeCells('A10:B10')
|
|
674
|
+
setCell('A10', '今日预计调拨金额', {
|
|
675
|
+
font: FONT_NORMAL,
|
|
676
|
+
alignment: ALIGN_CENTER,
|
|
677
|
+
border: BORDER_THIN
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
setCell('C10', formatAmount(data.expectedTransferIncome), {
|
|
681
|
+
font: FONT_NORMAL,
|
|
682
|
+
alignment: ALIGN_CENTER,
|
|
683
|
+
border: BORDER_THIN,
|
|
684
|
+
fill: LIGHT_BLUE_FILL,
|
|
685
|
+
numFmt: data.expectedTransferIncome % 1 === 0 ? '0' : '0.00'
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
setCell('D10', formatAmount(data.expectedTransferPay), {
|
|
689
|
+
font: FONT_NORMAL,
|
|
690
|
+
alignment: ALIGN_CENTER,
|
|
691
|
+
border: BORDER_THIN,
|
|
692
|
+
fill: LIGHT_BLUE_FILL,
|
|
693
|
+
numFmt: data.expectedTransferPay % 1 === 0 ? '0' : '0.00'
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
setCell('E10', formatAmount(data.expectedTransferDiff), {
|
|
697
|
+
font: FONT_NORMAL,
|
|
698
|
+
alignment: ALIGN_CENTER,
|
|
699
|
+
border: BORDER_THIN,
|
|
700
|
+
fill: BLUE_FILL,
|
|
701
|
+
numFmt: '0.00'
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// A11:B11
|
|
705
|
+
worksheet.mergeCells('A11:B11')
|
|
706
|
+
setCell('A11', '跨境人民币头寸清算账户预留金额', {
|
|
707
|
+
font: FONT_NORMAL,
|
|
708
|
+
alignment: ALIGN_CENTER,
|
|
709
|
+
border: BORDER_THIN
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// C11:E11
|
|
713
|
+
worksheet.mergeCells('C11:E11')
|
|
714
|
+
setCell('C11', formatAmount(data.reserveAmount), {
|
|
715
|
+
font: {
|
|
716
|
+
name: 'Microsoft YaHei',
|
|
717
|
+
size: 14,
|
|
718
|
+
bold: true,
|
|
719
|
+
color: { argb: '000000' }
|
|
720
|
+
},
|
|
721
|
+
alignment: ALIGN_CENTER,
|
|
722
|
+
border: BORDER_THIN,
|
|
723
|
+
numFmt: '0.00'
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
// 主体所有区域边框
|
|
727
|
+
setRangeBorder(3, 11, 1, 5, BORDER_THIN)
|
|
728
|
+
|
|
729
|
+
// 外边框加粗,更接近图里效果
|
|
730
|
+
setOuterMediumBorder(3, 11, 1, 5)
|
|
731
|
+
|
|
732
|
+
// 分隔线:行 8 红字区域略强调
|
|
733
|
+
for (let c = 1; c <= 5; c++) {
|
|
734
|
+
worksheet.getCell(8, c).border = {
|
|
735
|
+
top: { style: 'medium', color: { argb: '000000' } },
|
|
736
|
+
left: c === 1 ? { style: 'medium', color: { argb: '000000' } } : { style: 'thin', color: { argb: '000000' } },
|
|
737
|
+
bottom: { style: 'medium', color: { argb: '000000' } },
|
|
738
|
+
right: c === 5 ? { style: 'medium', color: { argb: '000000' } } : { style: 'thin', color: { argb: '000000' } }
|
|
739
|
+
} as ExcelJS.Borders
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 如果想让顶部标题区也更像图片,可给 A1:B2 加一点边距感
|
|
743
|
+
worksheet.getCell('A1').alignment = {
|
|
744
|
+
vertical: 'middle',
|
|
745
|
+
horizontal: 'center',
|
|
746
|
+
wrapText: true
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 导出
|
|
750
|
+
const buffer = await workbook.xlsx.writeBuffer()
|
|
751
|
+
const blob = new Blob([buffer], {
|
|
752
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
753
|
+
})
|
|
754
|
+
saveBlobAsFile(blob, `${data.title}.xlsx`)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export async function exportBankExcelWithDefaultLogo(data: ExportFormData = exportFormDataDemo): Promise<void> {
|
|
758
|
+
const logo = await getStaticLogo()
|
|
759
|
+
await exportBankExcel(data, { logo })
|
|
760
|
+
}
|