aegon-lv38 1.0.0
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.
- package/greenMortgage.vue +1395 -0
- package/package.json +12 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="green-mortgage-page">
|
|
3
|
+
<el-card class="green-mortgage" shadow="never">
|
|
4
|
+
<header class="green-mortgage__header">
|
|
5
|
+
<h1 class="green-mortgage__title">
|
|
6
|
+
Residential Properties that hold valid Gold/Platinum ratings under BEAM Plus New
|
|
7
|
+
Buildings (NB) / Existing Buildings (EB)
|
|
8
|
+
</h1>
|
|
9
|
+
<p class="green-mortgage__subtitle">
|
|
10
|
+
持有有效的綠建環評新建建築 (NB) / 既有建築 (EB) 金級/鉑金級的住宅物業
|
|
11
|
+
</p>
|
|
12
|
+
<p class="green-mortgage__meta">Data Update Date: {{ dataUpdateDate || 'YYYY-MM-DD' }}</p>
|
|
13
|
+
<p class="green-mortgage__meta">Data Download Status: {{ dataDownloadStatus || 'Automatically Downloaded' }}</p>
|
|
14
|
+
</header>
|
|
15
|
+
|
|
16
|
+
<section class="green-mortgage__filters">
|
|
17
|
+
<div class="green-mortgage__filter-row">
|
|
18
|
+
<div class="green-mortgage__filter-group">
|
|
19
|
+
<label class="green-mortgage__filter-label" for="gm-rating-select">Select Rating(s)</label>
|
|
20
|
+
<el-select
|
|
21
|
+
id="gm-rating-select"
|
|
22
|
+
v-model="selectedRatings"
|
|
23
|
+
multiple
|
|
24
|
+
collapse-tags
|
|
25
|
+
collapse-tags-tooltip
|
|
26
|
+
placeholder=""
|
|
27
|
+
class="green-mortgage__rating-select"
|
|
28
|
+
clearable
|
|
29
|
+
>
|
|
30
|
+
<el-option
|
|
31
|
+
v-for="item in ratingOptions"
|
|
32
|
+
:key="item"
|
|
33
|
+
:label="item"
|
|
34
|
+
:value="item"
|
|
35
|
+
/>
|
|
36
|
+
</el-select>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="green-mortgage__filter-group green-mortgage__filter-group--period">
|
|
40
|
+
<span class="green-mortgage__filter-label">Certification period:</span>
|
|
41
|
+
<div class="green-mortgage__period-controls">
|
|
42
|
+
<el-date-picker
|
|
43
|
+
v-model="startYear"
|
|
44
|
+
type="date"
|
|
45
|
+
format="YYYY-MM-DD"
|
|
46
|
+
value-format="YYYY-MM-DD"
|
|
47
|
+
placeholder=""
|
|
48
|
+
class="green-mortgage__date-input"
|
|
49
|
+
clearable
|
|
50
|
+
@clear="startYear = ''"
|
|
51
|
+
/>
|
|
52
|
+
<span class="green-mortgage__to-text">to</span>
|
|
53
|
+
<el-date-picker
|
|
54
|
+
v-model="endYear"
|
|
55
|
+
type="date"
|
|
56
|
+
format="YYYY-MM-DD"
|
|
57
|
+
value-format="YYYY-MM-DD"
|
|
58
|
+
placeholder=""
|
|
59
|
+
class="green-mortgage__date-input"
|
|
60
|
+
clearable
|
|
61
|
+
@clear="endYear = ''"
|
|
62
|
+
/>
|
|
63
|
+
<el-button class="green-mortgage__refresh-btn" @click="handleRefresh">
|
|
64
|
+
Refresh
|
|
65
|
+
</el-button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="green-mortgage__action-row">
|
|
71
|
+
<el-button class="green-mortgage__export-btn" @click="handleExportExcel">
|
|
72
|
+
Export to Excel
|
|
73
|
+
</el-button>
|
|
74
|
+
|
|
75
|
+
<div class="green-mortgage__search">
|
|
76
|
+
<label class="green-mortgage__search-label" for="gm-search">Search:</label>
|
|
77
|
+
<el-input
|
|
78
|
+
id="gm-search"
|
|
79
|
+
v-model="searchKeyword"
|
|
80
|
+
class="green-mortgage__search-input"
|
|
81
|
+
clearable
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section class="green-mortgage__table-wrap">
|
|
88
|
+
<div
|
|
89
|
+
ref="tableRootRef"
|
|
90
|
+
class="green-mortgage__table-shell"
|
|
91
|
+
:class="{ 'is-scrollbar-visible': showScrollbars }"
|
|
92
|
+
@mouseenter="onTableShellEnter"
|
|
93
|
+
@mouseleave="onTableShellLeave"
|
|
94
|
+
>
|
|
95
|
+
<el-table
|
|
96
|
+
ref="tableRef"
|
|
97
|
+
v-loading="tableLoading"
|
|
98
|
+
:data="paginatedRows"
|
|
99
|
+
border
|
|
100
|
+
stripe
|
|
101
|
+
class="green-mortgage__table"
|
|
102
|
+
empty-text=" "
|
|
103
|
+
height="100%"
|
|
104
|
+
@header-dragend="onHeaderDragEnd"
|
|
105
|
+
>
|
|
106
|
+
<el-table-column
|
|
107
|
+
prop="propertyNameEng"
|
|
108
|
+
:width="columnWidths.propertyNameEng"
|
|
109
|
+
sortable
|
|
110
|
+
:sort-method="sortByPropertyName"
|
|
111
|
+
show-overflow-tooltip
|
|
112
|
+
>
|
|
113
|
+
<template #header>
|
|
114
|
+
<div class="green-mortgage__th green-mortgage__th--single">
|
|
115
|
+
<span>Property Name</span>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
118
|
+
</el-table-column>
|
|
119
|
+
|
|
120
|
+
<el-table-column
|
|
121
|
+
prop="propertyNameChi"
|
|
122
|
+
:width="columnWidths.propertyNameChi"
|
|
123
|
+
show-overflow-tooltip
|
|
124
|
+
>
|
|
125
|
+
<template #header>
|
|
126
|
+
<div class="green-mortgage__th green-mortgage__th--single">
|
|
127
|
+
<span>物業名稱</span>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
</el-table-column>
|
|
131
|
+
|
|
132
|
+
<el-table-column
|
|
133
|
+
prop="propertyAddress"
|
|
134
|
+
:width="columnWidths.propertyAddress"
|
|
135
|
+
sortable
|
|
136
|
+
:sort-method="sortByPropertyAddress"
|
|
137
|
+
show-overflow-tooltip
|
|
138
|
+
>
|
|
139
|
+
<template #header>
|
|
140
|
+
<div class="green-mortgage__th green-mortgage__th--inline">
|
|
141
|
+
<span>Property Address</span>
|
|
142
|
+
<span>物業地址</span>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
145
|
+
</el-table-column>
|
|
146
|
+
|
|
147
|
+
<el-table-column
|
|
148
|
+
prop="owner"
|
|
149
|
+
:width="columnWidths.owner"
|
|
150
|
+
sortable
|
|
151
|
+
:sort-method="sortByOwner"
|
|
152
|
+
show-overflow-tooltip
|
|
153
|
+
>
|
|
154
|
+
<template #header>
|
|
155
|
+
<div class="green-mortgage__th">
|
|
156
|
+
<span>Owner/Developer Co. Name</span>
|
|
157
|
+
<span>項目持有人/發展商名稱</span>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</el-table-column>
|
|
161
|
+
|
|
162
|
+
<el-table-column
|
|
163
|
+
prop="district"
|
|
164
|
+
:width="columnWidths.district"
|
|
165
|
+
sortable
|
|
166
|
+
:sort-method="sortByDistrict"
|
|
167
|
+
show-overflow-tooltip
|
|
168
|
+
>
|
|
169
|
+
<template #header>
|
|
170
|
+
<div class="green-mortgage__th green-mortgage__th--inline">
|
|
171
|
+
<span>District</span>
|
|
172
|
+
<span>地區</span>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
</el-table-column>
|
|
176
|
+
|
|
177
|
+
<el-table-column
|
|
178
|
+
prop="tool"
|
|
179
|
+
:width="columnWidths.tool"
|
|
180
|
+
sortable
|
|
181
|
+
:sort-method="sortByTool"
|
|
182
|
+
show-overflow-tooltip
|
|
183
|
+
>
|
|
184
|
+
<template #header>
|
|
185
|
+
<div class="green-mortgage__th green-mortgage__th--inline">
|
|
186
|
+
<span>Tool</span>
|
|
187
|
+
<span>評估工具</span>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
</el-table-column>
|
|
191
|
+
|
|
192
|
+
<el-table-column
|
|
193
|
+
prop="projectType"
|
|
194
|
+
:width="columnWidths.projectType"
|
|
195
|
+
sortable
|
|
196
|
+
:sort-method="sortByProjectType"
|
|
197
|
+
show-overflow-tooltip
|
|
198
|
+
>
|
|
199
|
+
<template #header>
|
|
200
|
+
<div class="green-mortgage__th">
|
|
201
|
+
<span>Project Type</span>
|
|
202
|
+
<span>項目類型</span>
|
|
203
|
+
</div>
|
|
204
|
+
</template>
|
|
205
|
+
</el-table-column>
|
|
206
|
+
|
|
207
|
+
<el-table-column
|
|
208
|
+
prop="achievedRating"
|
|
209
|
+
:width="columnWidths.achievedRating"
|
|
210
|
+
sortable
|
|
211
|
+
:sort-method="sortByAchievedRating"
|
|
212
|
+
show-overflow-tooltip
|
|
213
|
+
>
|
|
214
|
+
<template #header>
|
|
215
|
+
<div class="green-mortgage__th">
|
|
216
|
+
<span>Achieved Rating</span>
|
|
217
|
+
<span>獲得評級</span>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
</el-table-column>
|
|
221
|
+
|
|
222
|
+
<el-table-column
|
|
223
|
+
prop="year"
|
|
224
|
+
:width="columnWidths.year"
|
|
225
|
+
align="center"
|
|
226
|
+
sortable
|
|
227
|
+
:sort-method="sortByYear"
|
|
228
|
+
>
|
|
229
|
+
<template #header>
|
|
230
|
+
<div class="green-mortgage__th green-mortgage__th--single">
|
|
231
|
+
<span>Year</span>
|
|
232
|
+
</div>
|
|
233
|
+
</template>
|
|
234
|
+
</el-table-column>
|
|
235
|
+
|
|
236
|
+
<el-table-column
|
|
237
|
+
prop="month"
|
|
238
|
+
:width="columnWidths.month"
|
|
239
|
+
align="center"
|
|
240
|
+
sortable
|
|
241
|
+
:sort-method="sortByMonth"
|
|
242
|
+
>
|
|
243
|
+
<template #header>
|
|
244
|
+
<div class="green-mortgage__th green-mortgage__th--single">
|
|
245
|
+
<span>Month</span>
|
|
246
|
+
</div>
|
|
247
|
+
</template>
|
|
248
|
+
</el-table-column>
|
|
249
|
+
|
|
250
|
+
<el-table-column
|
|
251
|
+
prop="gbCertLabelNo"
|
|
252
|
+
:min-width="columnWidths.gbCertLabelNo"
|
|
253
|
+
sortable
|
|
254
|
+
:sort-method="sortByGbCertLabelNo"
|
|
255
|
+
show-overflow-tooltip
|
|
256
|
+
>
|
|
257
|
+
<template #header>
|
|
258
|
+
<div class="green-mortgage__th">
|
|
259
|
+
<span>GB Cert Label No</span>
|
|
260
|
+
<span>售樓書上的標籤號碼</span>
|
|
261
|
+
</div>
|
|
262
|
+
</template>
|
|
263
|
+
</el-table-column>
|
|
264
|
+
</el-table>
|
|
265
|
+
|
|
266
|
+
<!-- 外側豎向滑塊:默認隱藏,移入表格且可滾動時顯示 -->
|
|
267
|
+
<div
|
|
268
|
+
v-show="scrollCan && showScrollbars"
|
|
269
|
+
class="green-mortgage__vbar"
|
|
270
|
+
:style="vbarTrackStyle"
|
|
271
|
+
aria-hidden="true"
|
|
272
|
+
>
|
|
273
|
+
<div
|
|
274
|
+
class="green-mortgage__vthumb"
|
|
275
|
+
:style="vbarThumbStyle"
|
|
276
|
+
@mousedown.prevent="onThumbMouseDown"
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="green-mortgage__pagination">
|
|
282
|
+
<el-pagination
|
|
283
|
+
v-model:current-page="currentPage"
|
|
284
|
+
v-model:page-size="pageSize"
|
|
285
|
+
:total="filteredRows.length"
|
|
286
|
+
:page-sizes="[10, 20, 50, 100]"
|
|
287
|
+
:pager-count="pagerCount"
|
|
288
|
+
background
|
|
289
|
+
:layout="paginationLayout"
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
</section>
|
|
293
|
+
</el-card>
|
|
294
|
+
</div>
|
|
295
|
+
</template>
|
|
296
|
+
|
|
297
|
+
<script setup lang="ts">
|
|
298
|
+
import greenMortgageApi from '@/api/modules/industry/green_mortgage'
|
|
299
|
+
import { industryExportToExcel } from '@/utils/function'
|
|
300
|
+
import { lockDocumentScroll } from '@/views/gen-ai/common/utils/scroll'
|
|
301
|
+
import type { TableInstance } from 'element-plus'
|
|
302
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
303
|
+
|
|
304
|
+
interface GreenMortgageRow {
|
|
305
|
+
propertyNameEng: string
|
|
306
|
+
propertyNameChi: string
|
|
307
|
+
propertyAddress: string
|
|
308
|
+
owner: string
|
|
309
|
+
district: string
|
|
310
|
+
tool: string
|
|
311
|
+
projectType: string
|
|
312
|
+
achievedRating: string
|
|
313
|
+
year: string
|
|
314
|
+
month: string
|
|
315
|
+
gbCertLabelNo: string
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const EXPORT_TABLE_HEADERS = [
|
|
319
|
+
'Property Name',
|
|
320
|
+
'物業名稱',
|
|
321
|
+
'Property Address',
|
|
322
|
+
'Owner/Developer Co. Name',
|
|
323
|
+
'District',
|
|
324
|
+
'Tool',
|
|
325
|
+
'Project Type',
|
|
326
|
+
'Achieved Rating',
|
|
327
|
+
'Year',
|
|
328
|
+
'Month',
|
|
329
|
+
'GB Cert Label No',
|
|
330
|
+
] as const
|
|
331
|
+
|
|
332
|
+
const COLUMN_WIDTH_STORAGE_KEY = 'green-mortgage-table-column-widths'
|
|
333
|
+
|
|
334
|
+
const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
|
|
335
|
+
propertyNameEng: 120,
|
|
336
|
+
propertyNameChi: 120,
|
|
337
|
+
propertyAddress: 170,
|
|
338
|
+
owner: 210,
|
|
339
|
+
district: 90,
|
|
340
|
+
tool: 90,
|
|
341
|
+
projectType: 110,
|
|
342
|
+
achievedRating: 120,
|
|
343
|
+
year: 72,
|
|
344
|
+
month: 72,
|
|
345
|
+
gbCertLabelNo: 150,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function loadColumnWidths(): Record<string, number> {
|
|
349
|
+
try {
|
|
350
|
+
const raw = localStorage.getItem(COLUMN_WIDTH_STORAGE_KEY)
|
|
351
|
+
if (!raw) return { ...DEFAULT_COLUMN_WIDTHS }
|
|
352
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
353
|
+
const next = { ...DEFAULT_COLUMN_WIDTHS }
|
|
354
|
+
for (const key of Object.keys(DEFAULT_COLUMN_WIDTHS)) {
|
|
355
|
+
const value = Number(parsed[key])
|
|
356
|
+
if (Number.isFinite(value) && value >= 48) next[key] = Math.round(value)
|
|
357
|
+
}
|
|
358
|
+
return next
|
|
359
|
+
} catch {
|
|
360
|
+
return { ...DEFAULT_COLUMN_WIDTHS }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const columnWidths = ref<Record<string, number>>(loadColumnWidths())
|
|
365
|
+
|
|
366
|
+
function persistColumnWidths() {
|
|
367
|
+
localStorage.setItem(COLUMN_WIDTH_STORAGE_KEY, JSON.stringify(columnWidths.value))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function onHeaderDragEnd(
|
|
371
|
+
newWidth: number,
|
|
372
|
+
_oldWidth: number,
|
|
373
|
+
column: { property?: string },
|
|
374
|
+
) {
|
|
375
|
+
const prop = column?.property
|
|
376
|
+
if (!prop || !(prop in DEFAULT_COLUMN_WIDTHS)) return
|
|
377
|
+
columnWidths.value = {
|
|
378
|
+
...columnWidths.value,
|
|
379
|
+
[prop]: Math.max(48, Math.round(newWidth)),
|
|
380
|
+
}
|
|
381
|
+
persistColumnWidths()
|
|
382
|
+
void nextTick(() => refreshTableScrollUi())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const tableLoading = ref(false)
|
|
386
|
+
const dataUpdateDate = ref('')
|
|
387
|
+
const dataDownloadStatus = ref('')
|
|
388
|
+
const ratingOptions = ref<string[]>([])
|
|
389
|
+
const selectedRatings = ref<string[]>([])
|
|
390
|
+
const startYear = ref<string | null>('')
|
|
391
|
+
const endYear = ref<string | null>('')
|
|
392
|
+
const searchKeyword = ref('')
|
|
393
|
+
const tableRows = ref<GreenMortgageRow[]>([])
|
|
394
|
+
const currentPage = ref(1)
|
|
395
|
+
const pageSize = ref(10)
|
|
396
|
+
const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
|
|
397
|
+
|
|
398
|
+
const isMobile = computed(() => viewportWidth.value < 768)
|
|
399
|
+
const isTablet = computed(() => viewportWidth.value >= 768 && viewportWidth.value < 1200)
|
|
400
|
+
|
|
401
|
+
const paginationLayout = computed(() => {
|
|
402
|
+
if (isMobile.value) return 'total, prev, pager, next'
|
|
403
|
+
if (isTablet.value) return 'total, sizes, prev, pager, next'
|
|
404
|
+
return 'total, sizes, prev, pager, next, jumper'
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const pagerCount = computed(() => (isMobile.value ? 5 : 7))
|
|
408
|
+
|
|
409
|
+
const tableRootRef = ref<HTMLElement | null>(null)
|
|
410
|
+
const tableRef = ref<TableInstance>()
|
|
411
|
+
|
|
412
|
+
/** 外側豎向滑塊(對齊 RecentReportsTable) */
|
|
413
|
+
const THUMB_PX = 50
|
|
414
|
+
const SCROLL_THUMB_COLOR = '#c1c1c1'
|
|
415
|
+
/** 判定可豎滾的溢出閾值(避免 10 條內仍誤顯滾動條) */
|
|
416
|
+
const V_OVERFLOW_THRESHOLD = 8
|
|
417
|
+
/** 底部為橫向滾動條預留,避免豎條被裁切 */
|
|
418
|
+
const V_BAR_BOTTOM_RESERVE = 12
|
|
419
|
+
const scrollCan = ref(false)
|
|
420
|
+
const thumbHeight = ref(THUMB_PX)
|
|
421
|
+
const thumbOffset = ref(0)
|
|
422
|
+
const trackTop = ref(0)
|
|
423
|
+
const trackHeight = ref(0)
|
|
424
|
+
const tableHovered = ref(false)
|
|
425
|
+
const thumbDragging = ref(false)
|
|
426
|
+
|
|
427
|
+
const showScrollbars = computed(() => tableHovered.value || thumbDragging.value)
|
|
428
|
+
|
|
429
|
+
const vbarTrackStyle = computed(() => ({
|
|
430
|
+
top: `${trackTop.value}px`,
|
|
431
|
+
height: `${trackHeight.value}px`,
|
|
432
|
+
}))
|
|
433
|
+
|
|
434
|
+
const vbarThumbStyle = computed(() => ({
|
|
435
|
+
height: `${thumbHeight.value}px`,
|
|
436
|
+
transform: `translateY(${thumbOffset.value}px)`,
|
|
437
|
+
backgroundColor: SCROLL_THUMB_COLOR,
|
|
438
|
+
}))
|
|
439
|
+
|
|
440
|
+
function onTableShellEnter() {
|
|
441
|
+
tableHovered.value = true
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function onTableShellLeave() {
|
|
445
|
+
if (thumbDragging.value) return
|
|
446
|
+
tableHovered.value = false
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getScrollWrap(): HTMLElement | null {
|
|
450
|
+
return (
|
|
451
|
+
(tableRootRef.value?.querySelector(
|
|
452
|
+
'.el-table__body-wrapper .el-scrollbar__wrap',
|
|
453
|
+
) as HTMLElement | null) ?? null
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function refreshOuterBar() {
|
|
458
|
+
const root = tableRootRef.value
|
|
459
|
+
const wrap = getScrollWrap()
|
|
460
|
+
if (!root || !wrap || paginatedRows.value.length === 0) {
|
|
461
|
+
scrollCan.value = false
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const { scrollTop, scrollHeight, clientHeight } = wrap
|
|
466
|
+
const overflow = scrollHeight - clientHeight
|
|
467
|
+
const can = overflow > V_OVERFLOW_THRESHOLD
|
|
468
|
+
scrollCan.value = can
|
|
469
|
+
if (!can) return
|
|
470
|
+
|
|
471
|
+
const header = root.querySelector('.el-table__header-wrapper') as HTMLElement | null
|
|
472
|
+
const bodyWrapper = root.querySelector('.el-table__body-wrapper') as HTMLElement | null
|
|
473
|
+
const headerH = header?.offsetHeight ?? 0
|
|
474
|
+
const bodyH = bodyWrapper?.clientHeight ?? clientHeight
|
|
475
|
+
|
|
476
|
+
trackTop.value = headerH + 2
|
|
477
|
+
// 軌道高度限制在表體可見區內,底部預留橫條空間,避免被遮擋
|
|
478
|
+
const available = Math.min(clientHeight, bodyH, root.clientHeight - trackTop.value)
|
|
479
|
+
trackHeight.value = Math.max(0, available - V_BAR_BOTTOM_RESERVE)
|
|
480
|
+
|
|
481
|
+
const trackH = trackHeight.value
|
|
482
|
+
if (trackH <= 0) {
|
|
483
|
+
scrollCan.value = false
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
thumbHeight.value = Math.min(trackH, THUMB_PX)
|
|
488
|
+
const maxTravel = Math.max(0, trackH - thumbHeight.value)
|
|
489
|
+
const scrollMax = Math.max(0, overflow)
|
|
490
|
+
thumbOffset.value = scrollMax > 0 ? Math.round((scrollTop / scrollMax) * maxTravel) : 0
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function onWrapScroll() {
|
|
494
|
+
refreshOuterBar()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let dragCleanup: (() => void) | undefined
|
|
498
|
+
let boundWrap: HTMLElement | null = null
|
|
499
|
+
let tableResizeObserver: ResizeObserver | undefined
|
|
500
|
+
|
|
501
|
+
function onThumbMouseDown(event: MouseEvent) {
|
|
502
|
+
const wrapEl = getScrollWrap()
|
|
503
|
+
if (!wrapEl || !scrollCan.value) return
|
|
504
|
+
const scrollEl: HTMLElement = wrapEl
|
|
505
|
+
|
|
506
|
+
thumbDragging.value = true
|
|
507
|
+
const startY = event.clientY
|
|
508
|
+
const startTop = thumbOffset.value
|
|
509
|
+
const trackH = trackHeight.value
|
|
510
|
+
const maxTravel = Math.max(0, trackH - thumbHeight.value)
|
|
511
|
+
const scrollMax = scrollEl.scrollHeight - scrollEl.clientHeight
|
|
512
|
+
|
|
513
|
+
function onMove(e: MouseEvent) {
|
|
514
|
+
if (maxTravel <= 0 || scrollMax <= 0) return
|
|
515
|
+
const delta = e.clientY - startY
|
|
516
|
+
const nextOffset = Math.min(maxTravel, Math.max(0, startTop + delta))
|
|
517
|
+
thumbOffset.value = nextOffset
|
|
518
|
+
scrollEl.scrollTop = (nextOffset / maxTravel) * scrollMax
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function onUp() {
|
|
522
|
+
thumbDragging.value = false
|
|
523
|
+
const root = tableRootRef.value
|
|
524
|
+
if (!root?.matches(':hover')) tableHovered.value = false
|
|
525
|
+
dragCleanup?.()
|
|
526
|
+
dragCleanup = undefined
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
window.addEventListener('mousemove', onMove)
|
|
530
|
+
window.addEventListener('mouseup', onUp, { once: true })
|
|
531
|
+
dragCleanup = () => {
|
|
532
|
+
window.removeEventListener('mousemove', onMove)
|
|
533
|
+
window.removeEventListener('mouseup', onUp)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function bindWrapScroll() {
|
|
538
|
+
unbindWrapScroll()
|
|
539
|
+
const wrap = getScrollWrap()
|
|
540
|
+
if (!wrap) return
|
|
541
|
+
wrap.addEventListener('scroll', onWrapScroll, { passive: true })
|
|
542
|
+
boundWrap = wrap
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function unbindWrapScroll() {
|
|
546
|
+
boundWrap?.removeEventListener('scroll', onWrapScroll)
|
|
547
|
+
boundWrap = null
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function refreshTableScrollUi() {
|
|
551
|
+
tableRef.value?.doLayout()
|
|
552
|
+
void nextTick(() => {
|
|
553
|
+
bindWrapScroll()
|
|
554
|
+
refreshOuterBar()
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function syncViewportWidth() {
|
|
559
|
+
viewportWidth.value = window.innerWidth
|
|
560
|
+
refreshTableScrollUi()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const filteredRows = computed(() => filterRowsByCurrentCriteria(tableRows.value))
|
|
564
|
+
|
|
565
|
+
const paginatedRows = computed(() => {
|
|
566
|
+
const start = (currentPage.value - 1) * pageSize.value
|
|
567
|
+
return filteredRows.value.slice(start, start + pageSize.value)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
watch([searchKeyword, selectedRatings, startYear, endYear], () => {
|
|
571
|
+
currentPage.value = 1
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
watch(
|
|
575
|
+
() => tableRows.value.length,
|
|
576
|
+
() => {
|
|
577
|
+
currentPage.value = 1
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
watch(paginatedRows, () => {
|
|
582
|
+
void nextTick(() => refreshTableScrollUi())
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
watch([currentPage, pageSize], () => {
|
|
586
|
+
void nextTick(() => refreshTableScrollUi())
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
function pickString(record: Record<string, unknown>, keys: string[]) {
|
|
590
|
+
for (const key of keys) {
|
|
591
|
+
const value = record[key]
|
|
592
|
+
if (value != null && value !== '') return String(value)
|
|
593
|
+
}
|
|
594
|
+
return ''
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function normalizeRow(item: unknown): GreenMortgageRow {
|
|
598
|
+
const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
|
|
599
|
+
return {
|
|
600
|
+
propertyNameEng: pickString(record, ['propertyNameEng', 'PROPERTY_NAME_ENG']),
|
|
601
|
+
propertyNameChi: pickString(record, ['propertyNameChi', 'PROPERTY_NAME_CHI']),
|
|
602
|
+
propertyAddress: pickString(record, ['propertyAddress', 'PROPERTY_ADDRESS']),
|
|
603
|
+
owner: pickString(record, ['owner', 'OWNER']),
|
|
604
|
+
district: pickString(record, ['district', 'DISTRICT']),
|
|
605
|
+
tool: pickString(record, ['tool', 'TOOL']),
|
|
606
|
+
projectType: pickString(record, ['projectType', 'PROJECT_TYPE']),
|
|
607
|
+
achievedRating: pickString(record, ['achievedRating', 'ACHIEVED_RATING']),
|
|
608
|
+
year: pickString(record, ['year', 'YEAR']),
|
|
609
|
+
month: pickString(record, ['month', 'MONTH']),
|
|
610
|
+
gbCertLabelNo: pickString(record, ['gbCertLabelNo', 'GB_CERT_LABEL_NO']),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function parseQueryResponse(data: unknown) {
|
|
615
|
+
if (!data || typeof data !== 'object') {
|
|
616
|
+
return { rows: [] as GreenMortgageRow[], dataUpdateDate: '', dataDownloadStatus: '' }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const record = data as Record<string, unknown>
|
|
620
|
+
const statusVO =
|
|
621
|
+
record.riskModelDataStatusVO && typeof record.riskModelDataStatusVO === 'object'
|
|
622
|
+
? (record.riskModelDataStatusVO as Record<string, unknown>)
|
|
623
|
+
: {}
|
|
624
|
+
|
|
625
|
+
const list = record.riskModelDataVOList
|
|
626
|
+
const rows = Array.isArray(list) ? list.map(normalizeRow) : []
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
rows,
|
|
630
|
+
dataUpdateDate: pickString(statusVO, ['dataUpdateDate', 'DataUpdateDate']),
|
|
631
|
+
dataDownloadStatus: pickString(statusVO, ['dataDownloadStatus', 'DataDownloadStatus']),
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parseRatingsResponse(data: unknown): string[] {
|
|
636
|
+
if (!Array.isArray(data)) return []
|
|
637
|
+
return data.map((item) => String(item).trim()).filter(Boolean)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function compareLexicographic(a: unknown, b: unknown) {
|
|
641
|
+
return String(a ?? '').localeCompare(String(b ?? ''), undefined, {
|
|
642
|
+
sensitivity: 'base',
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function compareText(a: unknown, b: unknown) {
|
|
647
|
+
return String(a ?? '').localeCompare(String(b ?? ''), undefined, {
|
|
648
|
+
numeric: true,
|
|
649
|
+
sensitivity: 'base',
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function compareAlphabeticalEmptyFirst(a: unknown, b: unknown) {
|
|
654
|
+
const strA = String(a ?? '').trim()
|
|
655
|
+
const strB = String(b ?? '').trim()
|
|
656
|
+
const emptyA = strA === ''
|
|
657
|
+
const emptyB = strB === ''
|
|
658
|
+
|
|
659
|
+
if (emptyA && emptyB) return 0
|
|
660
|
+
if (emptyA) return -1
|
|
661
|
+
if (emptyB) return 1
|
|
662
|
+
|
|
663
|
+
return strA.localeCompare(strB, undefined, { sensitivity: 'base' })
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function createAlphabeticalEmptyFirstSorter(key: keyof GreenMortgageRow) {
|
|
667
|
+
return (a: GreenMortgageRow, b: GreenMortgageRow) =>
|
|
668
|
+
compareAlphabeticalEmptyFirst(a[key], b[key])
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** 首字元:數字 0 → 字母 1 → 其他 2 */
|
|
672
|
+
function getFirstCharSortCategory(text: string) {
|
|
673
|
+
const trimmed = text.trim()
|
|
674
|
+
if (!trimmed) return 2
|
|
675
|
+
const first = trimmed.charAt(0)
|
|
676
|
+
if (/\d/.test(first)) return 0
|
|
677
|
+
if (/[a-zA-Z]/.test(first)) return 1
|
|
678
|
+
return 2
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function getPropertyNameSortKey(row: GreenMortgageRow) {
|
|
682
|
+
return (row.propertyNameEng || '').trim()
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function getPropertyNameLeadingNumber(name: string) {
|
|
686
|
+
const matched = name.trim().match(/^(\d+)/)
|
|
687
|
+
return matched ? Number(matched[1]) : Number.NaN
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const sortByPropertyName = (a: GreenMortgageRow, b: GreenMortgageRow) => {
|
|
691
|
+
const nameA = getPropertyNameSortKey(a)
|
|
692
|
+
const nameB = getPropertyNameSortKey(b)
|
|
693
|
+
const categoryA = getFirstCharSortCategory(nameA)
|
|
694
|
+
const categoryB = getFirstCharSortCategory(nameB)
|
|
695
|
+
|
|
696
|
+
if (categoryA !== categoryB) return categoryA - categoryB
|
|
697
|
+
|
|
698
|
+
if (categoryA === 0) {
|
|
699
|
+
const leadingA = getPropertyNameLeadingNumber(nameA)
|
|
700
|
+
const leadingB = getPropertyNameLeadingNumber(nameB)
|
|
701
|
+
if (!Number.isNaN(leadingA) && !Number.isNaN(leadingB) && leadingA !== leadingB) {
|
|
702
|
+
return leadingA - leadingB
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' })
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const sortByPropertyAddress = (a: GreenMortgageRow, b: GreenMortgageRow) => {
|
|
710
|
+
const addressA = (a.propertyAddress || '').trim()
|
|
711
|
+
const addressB = (b.propertyAddress || '').trim()
|
|
712
|
+
const categoryA = getFirstCharSortCategory(addressA)
|
|
713
|
+
const categoryB = getFirstCharSortCategory(addressB)
|
|
714
|
+
|
|
715
|
+
if (categoryA !== categoryB) return categoryA - categoryB
|
|
716
|
+
|
|
717
|
+
return compareLexicographic(addressA, addressB)
|
|
718
|
+
}
|
|
719
|
+
const sortByOwner = (a: GreenMortgageRow, b: GreenMortgageRow) => compareText(a.owner, b.owner)
|
|
720
|
+
const sortByDistrict = createAlphabeticalEmptyFirstSorter('district')
|
|
721
|
+
const sortByTool = createAlphabeticalEmptyFirstSorter('tool')
|
|
722
|
+
const sortByProjectType = createAlphabeticalEmptyFirstSorter('projectType')
|
|
723
|
+
const sortByAchievedRating = createAlphabeticalEmptyFirstSorter('achievedRating')
|
|
724
|
+
const sortByYear = createAlphabeticalEmptyFirstSorter('year')
|
|
725
|
+
const sortByMonth = createAlphabeticalEmptyFirstSorter('month')
|
|
726
|
+
const sortByGbCertLabelNo = createAlphabeticalEmptyFirstSorter('gbCertLabelNo')
|
|
727
|
+
|
|
728
|
+
async function fetchGreenMortgageRatings() {
|
|
729
|
+
try {
|
|
730
|
+
const res = await greenMortgageApi.getGreenMortgageRatings()
|
|
731
|
+
ratingOptions.value = parseRatingsResponse(res?.data)
|
|
732
|
+
} catch (error) {
|
|
733
|
+
console.error('getGreenMortgageRatings failed', error)
|
|
734
|
+
ratingOptions.value = []
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function asTrimmedText(value: string | null | undefined) {
|
|
739
|
+
return String(value ?? '').trim()
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function filterRowsByKeyword(rows: GreenMortgageRow[]) {
|
|
743
|
+
const keyword = asTrimmedText(searchKeyword.value).toLowerCase()
|
|
744
|
+
if (!keyword) return rows
|
|
745
|
+
|
|
746
|
+
return rows.filter((row) =>
|
|
747
|
+
Object.values(row).some((value) => String(value).toLowerCase().includes(keyword)),
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** 從 Certification period 取值中提取年份(相容 YYYY / YYYY-MM-DD) */
|
|
752
|
+
function getYearFromPeriod(value: string | null | undefined): number | null {
|
|
753
|
+
const trimmed = asTrimmedText(value)
|
|
754
|
+
if (!trimmed) return null
|
|
755
|
+
const matched = trimmed.match(/^(\d{4})/)
|
|
756
|
+
if (!matched) return null
|
|
757
|
+
const year = Number(matched[1])
|
|
758
|
+
return Number.isNaN(year) ? null : year
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* 當前頁篩選:Select Rating(s) + Certification period + Search
|
|
763
|
+
* 進頁無篩選條件時返回全部資料
|
|
764
|
+
*/
|
|
765
|
+
function filterRowsByCurrentCriteria(rows: GreenMortgageRow[]) {
|
|
766
|
+
let result = rows
|
|
767
|
+
|
|
768
|
+
if (selectedRatings.value.length > 0) {
|
|
769
|
+
const ratingSet = new Set(selectedRatings.value)
|
|
770
|
+
result = result.filter((row) => ratingSet.has(row.achievedRating))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const start = getYearFromPeriod(startYear.value)
|
|
774
|
+
const end = getYearFromPeriod(endYear.value)
|
|
775
|
+
if (start != null || end != null) {
|
|
776
|
+
result = result.filter((row) => {
|
|
777
|
+
const year = Number(row.year)
|
|
778
|
+
if (Number.isNaN(year)) return false
|
|
779
|
+
if (start != null && year < start) return false
|
|
780
|
+
if (end != null && year > end) return false
|
|
781
|
+
return true
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return filterRowsByKeyword(result)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function buildWorksheetData(rows: GreenMortgageRow[]) {
|
|
789
|
+
return [
|
|
790
|
+
[...EXPORT_TABLE_HEADERS],
|
|
791
|
+
...rows.map((row) => [
|
|
792
|
+
row.propertyNameEng,
|
|
793
|
+
row.propertyNameChi,
|
|
794
|
+
row.propertyAddress,
|
|
795
|
+
row.owner,
|
|
796
|
+
row.district,
|
|
797
|
+
row.tool,
|
|
798
|
+
row.projectType,
|
|
799
|
+
row.achievedRating,
|
|
800
|
+
row.year,
|
|
801
|
+
row.month,
|
|
802
|
+
row.gbCertLabelNo,
|
|
803
|
+
]),
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** 進頁預設空條件 = 查詢全部;Refresh / 匯出請求共用 */
|
|
808
|
+
function getGreenMortgageQueryParams() {
|
|
809
|
+
return {
|
|
810
|
+
endYear: asTrimmedText(endYear.value),
|
|
811
|
+
ratingList: [...selectedRatings.value],
|
|
812
|
+
startYear: asTrimmedText(startYear.value),
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function fetchGreenMortgageQuery() {
|
|
817
|
+
tableLoading.value = true
|
|
818
|
+
try {
|
|
819
|
+
const res = await greenMortgageApi.getGreenMortgageQuery(getGreenMortgageQueryParams())
|
|
820
|
+
|
|
821
|
+
if (res.data) {
|
|
822
|
+
const parsed = parseQueryResponse(res.data)
|
|
823
|
+
tableRows.value = parsed.rows
|
|
824
|
+
if (parsed.dataUpdateDate) dataUpdateDate.value = parsed.dataUpdateDate
|
|
825
|
+
if (parsed.dataDownloadStatus) dataDownloadStatus.value = parsed.dataDownloadStatus
|
|
826
|
+
} else {
|
|
827
|
+
tableRows.value = []
|
|
828
|
+
}
|
|
829
|
+
} catch (error) {
|
|
830
|
+
console.error('getGreenMortgageQuery failed', error)
|
|
831
|
+
} finally {
|
|
832
|
+
tableLoading.value = false
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function handleRefresh() {
|
|
837
|
+
fetchGreenMortgageQuery()
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** 匯出當前 Select Rating(s) + Certification period + Search 篩選後的資料(格式不變) */
|
|
841
|
+
async function handleExportExcel() {
|
|
842
|
+
try {
|
|
843
|
+
const res = await greenMortgageApi.getGreenMortgageQuery(getGreenMortgageQueryParams())
|
|
844
|
+
if (!res?.data) return
|
|
845
|
+
|
|
846
|
+
const parsed = parseQueryResponse(res.data)
|
|
847
|
+
const rows = filterRowsByCurrentCriteria(parsed.rows)
|
|
848
|
+
if (!rows.length) return
|
|
849
|
+
|
|
850
|
+
const worksheetData = buildWorksheetData(rows)
|
|
851
|
+
await industryExportToExcel(worksheetData, 'Green Mortgage', 'Green_Mortgage')
|
|
852
|
+
} catch (error) {
|
|
853
|
+
console.error('handleExportExcel failed', error)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** 匯出當前頁已篩選資料(Select Rating / Certification period / Search 過濾後的結果) */
|
|
858
|
+
async function handleExportExcel2() {
|
|
859
|
+
try {
|
|
860
|
+
const rows = filteredRows.value
|
|
861
|
+
if (!rows.length) return
|
|
862
|
+
|
|
863
|
+
const worksheetData = buildWorksheetData(rows)
|
|
864
|
+
await industryExportToExcel(worksheetData, 'Green Mortgage', 'Green_Mortgage')
|
|
865
|
+
} catch (error) {
|
|
866
|
+
console.error('handleExportExcel2 failed', error)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** 鎖定 document 原有滾動條 cleanup */
|
|
871
|
+
let restoreDocScroll: (() => void) | null = null
|
|
872
|
+
|
|
873
|
+
onMounted(async () => {
|
|
874
|
+
syncViewportWidth()
|
|
875
|
+
window.addEventListener('resize', syncViewportWidth)
|
|
876
|
+
void nextTick(() => {
|
|
877
|
+
restoreDocScroll?.()
|
|
878
|
+
restoreDocScroll = lockDocumentScroll()
|
|
879
|
+
refreshTableScrollUi()
|
|
880
|
+
const root = tableRootRef.value
|
|
881
|
+
if (root) {
|
|
882
|
+
tableResizeObserver = new ResizeObserver(() => {
|
|
883
|
+
void nextTick(() => refreshTableScrollUi())
|
|
884
|
+
})
|
|
885
|
+
tableResizeObserver.observe(root)
|
|
886
|
+
}
|
|
887
|
+
})
|
|
888
|
+
await fetchGreenMortgageRatings()
|
|
889
|
+
await fetchGreenMortgageQuery()
|
|
890
|
+
void nextTick(() => refreshTableScrollUi())
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
onUnmounted(() => {
|
|
894
|
+
restoreDocScroll?.()
|
|
895
|
+
restoreDocScroll = null
|
|
896
|
+
window.removeEventListener('resize', syncViewportWidth)
|
|
897
|
+
tableResizeObserver?.disconnect()
|
|
898
|
+
unbindWrapScroll()
|
|
899
|
+
dragCleanup?.()
|
|
900
|
+
})
|
|
901
|
+
</script>
|
|
902
|
+
|
|
903
|
+
<style lang="scss" scoped>
|
|
904
|
+
.green-mortgage-page {
|
|
905
|
+
box-sizing: border-box;
|
|
906
|
+
width: 100%;
|
|
907
|
+
height: 100%;
|
|
908
|
+
min-height: 0;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.green-mortgage {
|
|
912
|
+
box-sizing: border-box;
|
|
913
|
+
display: flex;
|
|
914
|
+
flex-direction: column;
|
|
915
|
+
width: 100%;
|
|
916
|
+
height: 100%;
|
|
917
|
+
min-height: 0;
|
|
918
|
+
overflow: hidden;
|
|
919
|
+
border: none;
|
|
920
|
+
border-radius: 0;
|
|
921
|
+
box-shadow: none;
|
|
922
|
+
background: #fff;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.green-mortgage :deep(.el-card__body) {
|
|
926
|
+
box-sizing: border-box;
|
|
927
|
+
display: flex;
|
|
928
|
+
flex-direction: column;
|
|
929
|
+
height: 100%;
|
|
930
|
+
min-height: 0;
|
|
931
|
+
padding: 20px;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.green-mortgage__header {
|
|
935
|
+
flex-shrink: 0;
|
|
936
|
+
margin-bottom: 14px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.green-mortgage__title,
|
|
940
|
+
.green-mortgage__subtitle {
|
|
941
|
+
margin: 0;
|
|
942
|
+
color: #c0504d;
|
|
943
|
+
font-family: 'Times New Roman', Times, serif;
|
|
944
|
+
font-size: 18px;
|
|
945
|
+
font-weight: 400;
|
|
946
|
+
line-height: 1.35;
|
|
947
|
+
word-break: break-word;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.green-mortgage__meta {
|
|
951
|
+
margin: 4px 0 0;
|
|
952
|
+
font-size: 14px;
|
|
953
|
+
line-height: 1.4;
|
|
954
|
+
color: var(--el-text-color-primary);
|
|
955
|
+
word-break: break-word;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.green-mortgage__filters {
|
|
959
|
+
flex-shrink: 0;
|
|
960
|
+
margin-bottom: 10px;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.green-mortgage__filter-row {
|
|
964
|
+
display: flex;
|
|
965
|
+
flex-wrap: wrap;
|
|
966
|
+
align-items: center;
|
|
967
|
+
gap: 10px 16px;
|
|
968
|
+
margin-bottom: 10px;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.green-mortgage__filter-group {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-wrap: wrap;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 8px 10px;
|
|
976
|
+
min-width: 0;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.green-mortgage__filter-group--period {
|
|
980
|
+
flex: 1 1 auto;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.green-mortgage__period-controls {
|
|
984
|
+
display: flex;
|
|
985
|
+
flex-wrap: wrap;
|
|
986
|
+
align-items: center;
|
|
987
|
+
gap: 8px 10px;
|
|
988
|
+
min-width: 0;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.green-mortgage__filter-label {
|
|
992
|
+
font-size: 14px;
|
|
993
|
+
white-space: nowrap;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.green-mortgage__rating-select {
|
|
997
|
+
width: 220px;
|
|
998
|
+
max-width: 100%;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.green-mortgage__date-input {
|
|
1002
|
+
width: 150px;
|
|
1003
|
+
max-width: 100%;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.green-mortgage__date-input.el-date-editor {
|
|
1007
|
+
--el-date-editor-width: 150px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.green-mortgage__to-text {
|
|
1011
|
+
font-size: 14px;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.green-mortgage__refresh-btn,
|
|
1015
|
+
.green-mortgage__export-btn {
|
|
1016
|
+
border-radius: 4px;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.green-mortgage__action-row {
|
|
1020
|
+
display: flex;
|
|
1021
|
+
flex-wrap: wrap;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
justify-content: space-between;
|
|
1024
|
+
gap: 12px 16px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.green-mortgage__search {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
flex-wrap: wrap;
|
|
1030
|
+
align-items: center;
|
|
1031
|
+
gap: 8px;
|
|
1032
|
+
min-width: 0;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.green-mortgage__search-label {
|
|
1036
|
+
font-size: 14px;
|
|
1037
|
+
white-space: nowrap;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.green-mortgage__search-input {
|
|
1041
|
+
width: 220px;
|
|
1042
|
+
max-width: 100%;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/* 有值時滑鼠移入顯示清空 x */
|
|
1046
|
+
.green-mortgage__rating-select :deep(.el-select__clear),
|
|
1047
|
+
.green-mortgage__date-input :deep(.el-input__clear),
|
|
1048
|
+
.green-mortgage__search-input :deep(.el-input__clear) {
|
|
1049
|
+
display: inline-flex !important;
|
|
1050
|
+
opacity: 0;
|
|
1051
|
+
transition: opacity 0.15s ease;
|
|
1052
|
+
cursor: pointer;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.green-mortgage__rating-select:hover :deep(.el-select__clear),
|
|
1056
|
+
.green-mortgage__rating-select :deep(.el-select__wrapper.is-focused .el-select__clear),
|
|
1057
|
+
.green-mortgage__date-input:hover :deep(.el-input__clear),
|
|
1058
|
+
.green-mortgage__date-input :deep(.el-input__wrapper.is-focus .el-input__clear),
|
|
1059
|
+
.green-mortgage__search-input:hover :deep(.el-input__clear),
|
|
1060
|
+
.green-mortgage__search-input :deep(.el-input__wrapper.is-focus .el-input__clear) {
|
|
1061
|
+
opacity: 1;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.green-mortgage__table-wrap {
|
|
1065
|
+
flex: 1;
|
|
1066
|
+
min-height: 0;
|
|
1067
|
+
display: flex;
|
|
1068
|
+
flex-direction: column;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.green-mortgage__table-shell {
|
|
1072
|
+
--gm-scroll-thumb: #c1c1c1;
|
|
1073
|
+
position: relative;
|
|
1074
|
+
flex: 1;
|
|
1075
|
+
min-height: 240px;
|
|
1076
|
+
min-width: 0;
|
|
1077
|
+
box-sizing: border-box;
|
|
1078
|
+
overflow: visible;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.green-mortgage__table-shell :deep(.el-table) {
|
|
1082
|
+
overflow: hidden;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.green-mortgage__table-shell :deep(.el-table__inner-wrapper) {
|
|
1086
|
+
height: 100%;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.green-mortgage__pagination {
|
|
1090
|
+
flex-shrink: 0;
|
|
1091
|
+
display: flex;
|
|
1092
|
+
justify-content: flex-end;
|
|
1093
|
+
flex-wrap: wrap;
|
|
1094
|
+
gap: 8px;
|
|
1095
|
+
margin-top: 12px;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.green-mortgage__pagination :deep(.el-pagination) {
|
|
1099
|
+
flex-wrap: wrap;
|
|
1100
|
+
row-gap: 8px;
|
|
1101
|
+
justify-content: flex-end;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
.green-mortgage__th {
|
|
1105
|
+
display: flex;
|
|
1106
|
+
flex-direction: column;
|
|
1107
|
+
align-items: center;
|
|
1108
|
+
justify-content: center;
|
|
1109
|
+
gap: 2px;
|
|
1110
|
+
font-size: 13px;
|
|
1111
|
+
line-height: 1.25;
|
|
1112
|
+
text-align: center;
|
|
1113
|
+
white-space: normal;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.green-mortgage__th--single {
|
|
1117
|
+
min-height: 34px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.green-mortgage__th--inline {
|
|
1121
|
+
flex-direction: row;
|
|
1122
|
+
flex-wrap: wrap;
|
|
1123
|
+
gap: 4px;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.green-mortgage__table {
|
|
1127
|
+
width: 100%;
|
|
1128
|
+
height: 100%;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.green-mortgage__table :deep(.el-table__header-wrapper),
|
|
1132
|
+
.green-mortgage__table :deep(.el-table__header) {
|
|
1133
|
+
background: #556b7d;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.green-mortgage__table :deep(.el-table__header table) {
|
|
1137
|
+
width: 100% !important;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell) {
|
|
1141
|
+
padding: 6px 0;
|
|
1142
|
+
background: #556b7d;
|
|
1143
|
+
color: #fff;
|
|
1144
|
+
border-color: #f5f7fa;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/* 豎向滾動預留 gutter:與表頭同色,避免右側留白 */
|
|
1148
|
+
.green-mortgage__table :deep(.el-table__header th.el-table__cell.gutter) {
|
|
1149
|
+
background: #556b7d !important;
|
|
1150
|
+
border-bottom-color: #556b7d !important;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell .cell) {
|
|
1154
|
+
padding: 0 8px;
|
|
1155
|
+
color: #fff;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.green-mortgage__table :deep(.el-table__body td.el-table__cell) {
|
|
1159
|
+
border-color: #f5f7fa;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.green-mortgage__table :deep(.el-table--border::before),
|
|
1163
|
+
.green-mortgage__table :deep(.el-table--border::after),
|
|
1164
|
+
.green-mortgage__table :deep(.el-table__inner-wrapper::after),
|
|
1165
|
+
.green-mortgage__table :deep(.el-table__border-left-patch) {
|
|
1166
|
+
background-color: #f5f7fa;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/* 隱藏 table 內建豎向滑塊;橫向滑塊樣式對齊 RecentReportsTable */
|
|
1170
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__bar.is-vertical) {
|
|
1171
|
+
display: none !important;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__wrap) {
|
|
1175
|
+
scrollbar-width: none !important;
|
|
1176
|
+
-ms-overflow-style: none !important;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__wrap::-webkit-scrollbar) {
|
|
1180
|
+
width: 0 !important;
|
|
1181
|
+
height: 0 !important;
|
|
1182
|
+
display: none !important;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__bar.is-horizontal) {
|
|
1186
|
+
display: block !important;
|
|
1187
|
+
height: 8px !important;
|
|
1188
|
+
left: 2px !important;
|
|
1189
|
+
right: 2px !important;
|
|
1190
|
+
bottom: 0 !important;
|
|
1191
|
+
opacity: 0 !important;
|
|
1192
|
+
pointer-events: none;
|
|
1193
|
+
z-index: 10;
|
|
1194
|
+
transition: opacity 0.15s ease;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.green-mortgage__table-shell.is-scrollbar-visible :deep(.el-scrollbar__bar.is-horizontal) {
|
|
1198
|
+
opacity: 1 !important;
|
|
1199
|
+
pointer-events: auto;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__bar.is-horizontal .el-scrollbar__thumb) {
|
|
1203
|
+
display: block !important;
|
|
1204
|
+
height: 8px !important;
|
|
1205
|
+
width: 50px !important;
|
|
1206
|
+
min-width: 50px !important;
|
|
1207
|
+
border-radius: 4px !important;
|
|
1208
|
+
background-color: var(--gm-scroll-thumb, #c1c1c1) !important;
|
|
1209
|
+
opacity: 1 !important;
|
|
1210
|
+
cursor: grab;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.green-mortgage__table-shell :deep(.el-scrollbar__bar.is-horizontal .el-scrollbar__thumb:active) {
|
|
1214
|
+
cursor: grabbing;
|
|
1215
|
+
background-color: #a8a8a8 !important;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.green-mortgage__vbar {
|
|
1219
|
+
position: absolute;
|
|
1220
|
+
right: 4px;
|
|
1221
|
+
width: 8px;
|
|
1222
|
+
z-index: 12;
|
|
1223
|
+
pointer-events: none;
|
|
1224
|
+
overflow: visible;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.green-mortgage__vthumb {
|
|
1228
|
+
width: 8px;
|
|
1229
|
+
height: 50px;
|
|
1230
|
+
min-height: 50px;
|
|
1231
|
+
border-radius: 4px;
|
|
1232
|
+
background-color: var(--gm-scroll-thumb, #c1c1c1);
|
|
1233
|
+
cursor: grab;
|
|
1234
|
+
pointer-events: auto;
|
|
1235
|
+
will-change: transform;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.green-mortgage__vthumb:active {
|
|
1239
|
+
cursor: grabbing;
|
|
1240
|
+
background-color: #a8a8a8;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/* 排序圖示:預設隱藏,懸停/已排序時顯示;顏色沿用 el-table 主題 */
|
|
1244
|
+
.green-mortgage__table :deep(.caret-wrapper) {
|
|
1245
|
+
position: absolute;
|
|
1246
|
+
top: 50%;
|
|
1247
|
+
right: 6px;
|
|
1248
|
+
transform: translateY(-50%);
|
|
1249
|
+
opacity: 0;
|
|
1250
|
+
visibility: hidden;
|
|
1251
|
+
transition: opacity 0.15s ease;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.is-sortable:hover .caret-wrapper) {
|
|
1255
|
+
opacity: 1;
|
|
1256
|
+
visibility: visible;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.ascending .caret-wrapper),
|
|
1260
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.descending .caret-wrapper) {
|
|
1261
|
+
right: 2px;
|
|
1262
|
+
opacity: 1;
|
|
1263
|
+
visibility: visible;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.green-mortgage__table :deep(.sort-caret.ascending) {
|
|
1267
|
+
border-bottom-color: #fff;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.green-mortgage__table :deep(.sort-caret.descending) {
|
|
1271
|
+
border-top-color: #fff;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.is-sortable .cell) {
|
|
1275
|
+
position: relative;
|
|
1276
|
+
padding-right: 24px;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/* 大屏 */
|
|
1280
|
+
@media (min-width: 1200px) {
|
|
1281
|
+
.green-mortgage__title,
|
|
1282
|
+
.green-mortgage__subtitle {
|
|
1283
|
+
font-size: 18px;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.green-mortgage__table {
|
|
1287
|
+
min-width: 100%;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/* 平板 */
|
|
1292
|
+
@media (max-width: 1199px) {
|
|
1293
|
+
.green-mortgage__title,
|
|
1294
|
+
.green-mortgage__subtitle {
|
|
1295
|
+
font-size: 16px;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.green-mortgage__rating-select,
|
|
1299
|
+
.green-mortgage__search-input {
|
|
1300
|
+
width: 200px;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.green-mortgage__date-input {
|
|
1304
|
+
width: 140px;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.green-mortgage__date-input.el-date-editor {
|
|
1308
|
+
--el-date-editor-width: 140px;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.green-mortgage__th {
|
|
1312
|
+
font-size: 12px;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/* 手機 */
|
|
1317
|
+
@media (max-width: 767px) {
|
|
1318
|
+
.green-mortgage {
|
|
1319
|
+
overflow: auto;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.green-mortgage :deep(.el-card__body) {
|
|
1323
|
+
padding: 20px;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.green-mortgage__header {
|
|
1327
|
+
margin-bottom: 10px;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
.green-mortgage__title,
|
|
1331
|
+
.green-mortgage__subtitle {
|
|
1332
|
+
font-size: 14px;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
.green-mortgage__meta {
|
|
1336
|
+
font-size: 12px;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.green-mortgage__filter-row,
|
|
1340
|
+
.green-mortgage__action-row {
|
|
1341
|
+
flex-direction: column;
|
|
1342
|
+
align-items: stretch;
|
|
1343
|
+
gap: 10px;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.green-mortgage__filter-group,
|
|
1347
|
+
.green-mortgage__filter-group--period,
|
|
1348
|
+
.green-mortgage__period-controls,
|
|
1349
|
+
.green-mortgage__search {
|
|
1350
|
+
width: 100%;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.green-mortgage__filter-label,
|
|
1354
|
+
.green-mortgage__search-label {
|
|
1355
|
+
white-space: normal;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.green-mortgage__rating-select,
|
|
1359
|
+
.green-mortgage__search-input,
|
|
1360
|
+
.green-mortgage__date-input {
|
|
1361
|
+
width: 100%;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.green-mortgage__date-input.el-date-editor {
|
|
1365
|
+
--el-date-editor-width: 100%;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.green-mortgage__period-controls {
|
|
1369
|
+
display: grid;
|
|
1370
|
+
grid-template-columns: 1fr auto 1fr;
|
|
1371
|
+
align-items: center;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.green-mortgage__period-controls .green-mortgage__refresh-btn {
|
|
1375
|
+
grid-column: 1 / -1;
|
|
1376
|
+
width: 100%;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.green-mortgage__export-btn {
|
|
1380
|
+
width: 100%;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.green-mortgage__table-shell {
|
|
1384
|
+
min-height: 280px;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
.green-mortgage__pagination {
|
|
1388
|
+
justify-content: center;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.green-mortgage__pagination :deep(.el-pagination) {
|
|
1392
|
+
justify-content: center;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
</style>
|