aegon-lv82 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.
Files changed (148) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +44 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/build-report/reportEditor.ts +526 -0
  5. package/src/api/modules/build-report/reportPermission.ts +64 -0
  6. package/src/api/modules/index.ts +69 -0
  7. package/src/api/modules/industry/green_mortgage.ts +42 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +124 -0
  10. package/src/assets/sample-access-icon.png +0 -0
  11. package/src/assets/sample-pie-chart.png +0 -0
  12. package/src/assets/vue.svg +1 -0
  13. package/src/components/CapsuleScrollbar.vue +7 -0
  14. package/src/components/Export/ExcelExport.vue +592 -0
  15. package/src/components/Export/ExcelExport2.vue +494 -0
  16. package/src/components/Export/ExcelExport3.vue +342 -0
  17. package/src/components/Export/ExcelExport4.vue +665 -0
  18. package/src/components/Export/excelExport.js +547 -0
  19. package/src/components/Export/excelExport.ts +551 -0
  20. package/src/components/GEN-AI/index.vue +140 -0
  21. package/src/components/GEN-AI/index1.vue +422 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +548 -0
  24. package/src/components/GEN-AI/index3.vue +605 -0
  25. package/src/components/GEN-AI/index4.vue +609 -0
  26. package/src/components/GEN-AI/index5.vue +558 -0
  27. package/src/components/GEN-AI/index6.vue +634 -0
  28. package/src/components/GEN-AI/index7.vue +688 -0
  29. package/src/components/GEN-AI/index8.vue +458 -0
  30. package/src/components/GEN-AI/index9.vue +1217 -0
  31. package/src/components/GEN-AI/types.ts +12 -0
  32. package/src/components/GEN-AI/utils.ts +42 -0
  33. package/src/components/HelloWorld.vue +41 -0
  34. package/src/components/PageCard.vue +124 -0
  35. package/src/components/PageHeader.vue +32 -0
  36. package/src/components/backup/index5 copy.vue +556 -0
  37. package/src/components/backup/index5.vue +620 -0
  38. package/src/components/backup/index9 copy.vue +1029 -0
  39. package/src/components/backup/index9-pro.vue +1065 -0
  40. package/src/components/backup/index9.vue +1057 -0
  41. package/src/directives/btnLoading.ts +455 -0
  42. package/src/directives/debounce copy.ts +670 -0
  43. package/src/directives/debounce.ts +98 -0
  44. package/src/directives/index.ts +25 -0
  45. package/src/layouts/MainLayout.vue +129 -0
  46. package/src/main.ts +76 -0
  47. package/src/router/index.ts +142 -0
  48. package/src/router/menus.ts +28 -0
  49. package/src/store/modules/gen-ai/index.ts +1 -0
  50. package/src/store/modules/gen-ai/reportBuild.ts +37 -0
  51. package/src/style.css +79 -0
  52. package/src/styles/report-config-popover.css +39 -0
  53. package/src/styles/report-icon-btn.css +19 -0
  54. package/src/test/mock.ts +101 -0
  55. package/src/test/test1.vue +402 -0
  56. package/src/test/test2.vue +1689 -0
  57. package/src/types/gen-ai/build-report/reportEditor.ts +684 -0
  58. package/src/utils/function.ts +46 -0
  59. package/src/utils/gen-ai-navigation.ts +1 -0
  60. package/src/utils/gen-ai-scroll.ts +1 -0
  61. package/src/utils/openDataLoaderWordExport.ts +33 -0
  62. package/src/utils/pageScrollbar.ts +115 -0
  63. package/src/utils/randomTranscode.ts +87 -0
  64. package/src/utils/reportPdfExport.ts +44 -0
  65. package/src/views/Blank.vue +68 -0
  66. package/src/views/Home.vue +29 -0
  67. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  68. package/src/views/TemplateCenter/data.d.ts +10 -0
  69. package/src/views/TemplateCenter/index.vue +1128 -0
  70. package/src/views/TemplateCenter/service.ts +69 -0
  71. package/src/views/gen-ai/build-report/chartStreamRefs.ts +174 -0
  72. package/src/views/gen-ai/build-report/coherence-check/index.vue +559 -0
  73. package/src/views/gen-ai/build-report/components/ReportChapterWorkspace.vue +4258 -0
  74. package/src/views/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  75. package/src/views/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  76. package/src/views/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  77. package/src/views/gen-ai/build-report/components/ReportPromptPanel.vue +614 -0
  78. package/src/views/gen-ai/build-report/components/ReportSourceFileSummary.vue +656 -0
  79. package/src/views/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  80. package/src/views/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  81. package/src/views/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  82. package/src/views/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  83. package/src/views/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  84. package/src/views/gen-ai/build-report/components/paramsSetting.vue +886 -0
  85. package/src/views/gen-ai/build-report/components/reportEdit.vue +2283 -0
  86. package/src/views/gen-ai/build-report/index.vue +856 -0
  87. package/src/views/gen-ai/build-report/report-domain.ts +1133 -0
  88. package/src/views/gen-ai/build-report/report-template-data.ts +295 -0
  89. package/src/views/gen-ai/build-report/useReportWorkflow.ts +1408 -0
  90. package/src/views/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  91. package/src/views/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  92. package/src/views/gen-ai/common/assets/NotoSans.woff2 +0 -0
  93. package/src/views/gen-ai/common/assets/fonts.css +278 -0
  94. package/src/views/gen-ai/common/assets/pagination.css +163 -0
  95. package/src/views/gen-ai/common/directives/btnLoading.ts +455 -0
  96. package/src/views/gen-ai/common/directives/debounce.ts +98 -0
  97. package/src/views/gen-ai/common/directives/index.ts +30 -0
  98. package/src/views/gen-ai/common/en.md +417 -0
  99. package/src/views/gen-ai/common/index.ts +36 -0
  100. package/src/views/gen-ai/common/types/component-expose.ts +9 -0
  101. package/src/views/gen-ai/common/utils/api-res-status.ts +7 -0
  102. package/src/views/gen-ai/common/utils/navigation.ts +59 -0
  103. package/src/views/gen-ai/common/utils/remote-request.ts +45 -0
  104. package/src/views/gen-ai/common/utils/scroll.ts +577 -0
  105. package/src/views/gen-ai/components/AIExport/docx-export.ts +950 -0
  106. package/src/views/gen-ai/components/AIExport/docxExport.ts +2566 -0
  107. package/src/views/gen-ai/components/AIExport/index.ts +52 -0
  108. package/src/views/gen-ai/components/AIExport/parseMarkdownHtml.ts +649 -0
  109. package/src/views/gen-ai/components/CapsuleScrollbar.vue +149 -0
  110. package/src/views/gen-ai/components/ChapterTitleScroll.vue +468 -0
  111. package/src/views/gen-ai/components/EntryHeroNavCards.vue +379 -0
  112. package/src/views/gen-ai/components/GenAiPagination.vue +52 -0
  113. package/src/views/gen-ai/components/ModelCard.vue +1114 -0
  114. package/src/views/gen-ai/components/Preview/FilePreviewPage.vue +947 -0
  115. package/src/views/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  116. package/src/views/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  117. package/src/views/gen-ai/components/Preview/index.ts +11 -0
  118. package/src/views/gen-ai/components/Preview/preview.ts +722 -0
  119. package/src/views/gen-ai/components/PromptField.vue +505 -0
  120. package/src/views/gen-ai/components/RecentReportsTable.vue +974 -0
  121. package/src/views/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  122. package/src/views/gen-ai/components/ReportHistoryList.vue +637 -0
  123. package/src/views/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  124. package/src/views/gen-ai/components/editor/ChapterTiptapEditor.vue +3139 -0
  125. package/src/views/gen-ai/components/editor/StructureBlockView.vue +441 -0
  126. package/src/views/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  127. package/src/views/gen-ai/components/editor/TypewriterDocEditor.vue +398 -0
  128. package/src/views/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  129. package/src/views/gen-ai/components/editor/index.ts +12 -0
  130. package/src/views/gen-ai/components/editor/parseMarkdownHtml.ts +667 -0
  131. package/src/views/gen-ai/gen-entry/api/index.ts +32 -0
  132. package/src/views/gen-ai/gen-entry/index.vue +287 -0
  133. package/src/views/gen-ai/gen-entry/types/index.ts +4 -0
  134. package/src/views/gen-ai/i.mark +2 -0
  135. package/src/views/gen-ai/management-center/index.vue +1815 -0
  136. package/src/views/gen-ai/management-center/mockData.ts +248 -0
  137. package/src/views/gen-ai/management-center/types/index.ts +37 -0
  138. package/src/views/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  139. package/src/views/gen-ai/model-manager/api/index.ts +32 -0
  140. package/src/views/gen-ai/model-manager/api/mockApi.ts +33 -0
  141. package/src/views/gen-ai/model-manager/index.vue +1583 -0
  142. package/src/views/gen-ai/model-manager/mockData.ts +177 -0
  143. package/src/views/gen-ai/model-manager/template-detail-shared.ts +154 -0
  144. package/src/views/gen-ai/model-manager/template-editor.vue +345 -0
  145. package/src/views/gen-ai/model-manager/types/index.ts +40 -0
  146. package/src/views/gen-ai/model-manager/utils.ts +25 -0
  147. package/src/views/greenMortgage.vue +1834 -0
  148. package/src/vite-env.d.ts +46 -0
@@ -0,0 +1,1834 @@
1
+ <template>
2
+ <div class="green-mortgage-page">
3
+ <div class="green-mortgage">
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 Buildings
7
+ (NB) / Existing Buildings (EB)
8
+ </h1>
9
+ <p class="green-mortgage__subtitle">
10
+ 持有有效的綠建環評新建建築 (NB) / 既有建築 (EB) 金級/鉑金級的住宅物業
11
+ </p>
12
+ <p class="green-mortgage__meta">
13
+ Data Update Date
14
+ <span class="green-mortgage__meta-value">{{ dataUpdateDate || 'YYYY-MM-DD' }}</span>
15
+ </p>
16
+ <p class="green-mortgage__meta">
17
+ Data Download Status
18
+ <span class="green-mortgage__meta-value">{{
19
+ dataDownloadStatus || 'Automatically Downloaded'
20
+ }}</span>
21
+ </p>
22
+ </header>
23
+
24
+ <section class="green-mortgage__filters">
25
+ <div class="green-mortgage__filter-row">
26
+ <div class="green-mortgage__filter-group">
27
+ <label class="green-mortgage__filter-label" for="gm-rating-select"
28
+ >Select Rating(s)</label
29
+ >
30
+ <el-select
31
+ id="gm-rating-select"
32
+ v-model="selectedRatings"
33
+ multiple
34
+ collapse-tags
35
+ collapse-tags-tooltip
36
+ placeholder=""
37
+ class="green-mortgage__rating-select"
38
+ clearable
39
+ >
40
+ <el-option v-for="item in ratingOptions" :key="item" :label="item" :value="item" />
41
+ </el-select>
42
+ </div>
43
+
44
+ <div class="green-mortgage__filter-group green-mortgage__filter-group--period">
45
+ <span class="green-mortgage__filter-label">Certification period</span>
46
+ <div class="green-mortgage__period-controls">
47
+ <el-date-picker
48
+ v-model="yearRange"
49
+ type="yearrange"
50
+ format="YYYY"
51
+ value-format="YYYY"
52
+ range-separator="to"
53
+ start-placeholder=""
54
+ end-placeholder=""
55
+ class="green-mortgage__date-input"
56
+ style="width: 320px; --el-date-editor-width: 320px"
57
+ clearable
58
+ />
59
+ <el-button class="green-mortgage__refresh-btn" @click="handleRefresh">
60
+ Refresh
61
+ </el-button>
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="green-mortgage__action-row">
67
+ <el-button class="green-mortgage__export-btn" @click="handleExportExcel">
68
+ Export to Excel
69
+ </el-button>
70
+
71
+ <div class="green-mortgage__search">
72
+ <label class="green-mortgage__search-label" for="gm-search">Search:</label>
73
+ <el-input
74
+ id="gm-search"
75
+ v-model="searchKeyword"
76
+ class="green-mortgage__search-input"
77
+ clearable
78
+ />
79
+ </div>
80
+ </div>
81
+ </section>
82
+
83
+ <section
84
+ class="green-mortgage__table-wrap"
85
+ @mouseenter="onTableShellEnter"
86
+ @mouseleave="onTableShellLeave"
87
+ >
88
+ <div class="green-mortgage__table-main">
89
+ <div class="green-mortgage__table-row">
90
+ <div
91
+ ref="tableRootRef"
92
+ class="green-mortgage__table-shell"
93
+ :class="{ 'is-scrollbar-visible': showScrollbars }"
94
+ >
95
+ <el-table
96
+ ref="tableRef"
97
+ v-loading="tableLoading"
98
+ :data="filteredRows"
99
+ border
100
+ stripe
101
+ class="green-mortgage__table"
102
+ empty-text=" "
103
+ height="100%"
104
+ :default-sort="{ prop: 'propertyNameEng', order: 'ascending' }"
105
+ @header-dragend="onHeaderDragEnd"
106
+ >
107
+ <el-table-column
108
+ prop="propertyNameEng"
109
+ :width="columnWidths.propertyNameEng"
110
+ sortable
111
+ :sort-method="sortByPropertyName"
112
+ show-overflow-tooltip
113
+ >
114
+ <template #header>
115
+ <div class="green-mortgage__th green-mortgage__th--single">
116
+ <span>Property Name</span>
117
+ </div>
118
+ </template>
119
+ </el-table-column>
120
+
121
+ <el-table-column
122
+ prop="propertyNameChi"
123
+ :width="columnWidths.propertyNameChi"
124
+ show-overflow-tooltip
125
+ >
126
+ <template #header>
127
+ <div class="green-mortgage__th green-mortgage__th--single">
128
+ <span>物業名稱</span>
129
+ </div>
130
+ </template>
131
+ </el-table-column>
132
+
133
+ <el-table-column
134
+ prop="propertyAddress"
135
+ :width="columnWidths.propertyAddress"
136
+ sortable
137
+ :sort-method="sortByPropertyAddress"
138
+ show-overflow-tooltip
139
+ >
140
+ <template #header>
141
+ <div class="green-mortgage__th green-mortgage__th--inline">
142
+ <span>Property Address</span>
143
+ <span>物業地址</span>
144
+ </div>
145
+ </template>
146
+ </el-table-column>
147
+
148
+ <el-table-column
149
+ prop="owner"
150
+ :width="columnWidths.owner"
151
+ sortable
152
+ :sort-method="sortByOwner"
153
+ show-overflow-tooltip
154
+ >
155
+ <template #header>
156
+ <div class="green-mortgage__th">
157
+ <span>Owner/Developer Co. Name</span>
158
+ <span>項目持有人/發展商名稱</span>
159
+ </div>
160
+ </template>
161
+ </el-table-column>
162
+
163
+ <el-table-column
164
+ prop="district"
165
+ :width="columnWidths.district"
166
+ sortable
167
+ :sort-method="sortByDistrict"
168
+ show-overflow-tooltip
169
+ >
170
+ <template #header>
171
+ <div class="green-mortgage__th green-mortgage__th--inline">
172
+ <span>District</span>
173
+ <span>地區</span>
174
+ </div>
175
+ </template>
176
+ </el-table-column>
177
+
178
+ <el-table-column
179
+ prop="tool"
180
+ :width="columnWidths.tool"
181
+ sortable
182
+ :sort-method="sortByTool"
183
+ show-overflow-tooltip
184
+ >
185
+ <template #header>
186
+ <div class="green-mortgage__th green-mortgage__th--inline">
187
+ <span>Tool</span>
188
+ <span>評估工具</span>
189
+ </div>
190
+ </template>
191
+ </el-table-column>
192
+
193
+ <el-table-column
194
+ prop="projectType"
195
+ :width="columnWidths.projectType"
196
+ sortable
197
+ :sort-method="sortByProjectType"
198
+ show-overflow-tooltip
199
+ >
200
+ <template #header>
201
+ <div class="green-mortgage__th">
202
+ <span>Project Type</span>
203
+ <span>項目類型</span>
204
+ </div>
205
+ </template>
206
+ </el-table-column>
207
+
208
+ <el-table-column
209
+ prop="achievedRating"
210
+ :width="columnWidths.achievedRating"
211
+ sortable
212
+ :sort-method="sortByAchievedRating"
213
+ show-overflow-tooltip
214
+ >
215
+ <template #header>
216
+ <div class="green-mortgage__th">
217
+ <span>Achieved Rating</span>
218
+ <span>獲得評級</span>
219
+ </div>
220
+ </template>
221
+ </el-table-column>
222
+
223
+ <el-table-column
224
+ prop="year"
225
+ :width="columnWidths.year"
226
+ align="center"
227
+ sortable
228
+ :sort-method="sortByYear"
229
+ >
230
+ <template #header>
231
+ <div class="green-mortgage__th green-mortgage__th--single">
232
+ <span>Year</span>
233
+ </div>
234
+ </template>
235
+ </el-table-column>
236
+
237
+ <el-table-column
238
+ prop="month"
239
+ :width="columnWidths.month"
240
+ align="center"
241
+ sortable
242
+ :sort-method="sortByMonth"
243
+ >
244
+ <template #header>
245
+ <div class="green-mortgage__th green-mortgage__th--single">
246
+ <span>Month</span>
247
+ </div>
248
+ </template>
249
+ </el-table-column>
250
+
251
+ <el-table-column
252
+ prop="gbCertLabelNo"
253
+ :min-width="columnWidths.gbCertLabelNo"
254
+ sortable
255
+ :sort-method="sortByGbCertLabelNo"
256
+ show-overflow-tooltip
257
+ >
258
+ <template #header>
259
+ <div class="green-mortgage__th">
260
+ <span>GB Cert Label No</span>
261
+ <span>售樓書上的標籤號碼</span>
262
+ </div>
263
+ </template>
264
+ </el-table-column>
265
+ </el-table>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- 外側橫向滑塊:表格底邊外側,不佔表高 -->
270
+ <div class="green-mortgage__hbar-row">
271
+ <div
272
+ v-show="hScrollCan"
273
+ ref="hbarRef"
274
+ class="green-mortgage__hbar"
275
+ aria-hidden="true"
276
+ @mousedown.prevent="onHTrackMouseDown"
277
+ >
278
+ <div
279
+ class="green-mortgage__hthumb"
280
+ :class="{ 'is-visible': showScrollbars }"
281
+ :style="hbarThumbStyle"
282
+ @mousedown.stop.prevent="onHThumbMouseDown"
283
+ />
284
+ </div>
285
+ </div>
286
+
287
+ <!-- 外側豎向滑塊:表格右邊外側(落在 card 右 padding),不佔列寬 -->
288
+ <div
289
+ v-show="scrollCan"
290
+ class="green-mortgage__vbar"
291
+ :style="vbarTrackStyle"
292
+ @mousedown.prevent="onTrackMouseDown"
293
+ >
294
+ <div
295
+ class="green-mortgage__vthumb"
296
+ :class="{ 'is-visible': showScrollbars }"
297
+ :style="vbarThumbStyle"
298
+ @mousedown.stop.prevent="onThumbMouseDown"
299
+ />
300
+ </div>
301
+ </div>
302
+
303
+ <div class="green-mortgage__table-footer">
304
+ <span class="green-mortgage__table-footer-left">
305
+ {{ filteredRows.length }} result{{ filteredRows.length === 1 ? '' : 's' }} 搜索結果
306
+ </span>
307
+ <span class="green-mortgage__table-footer-right">
308
+ excluding student hostels and staff quarters 不包括職員/學生宿舍
309
+ </span>
310
+ </div>
311
+ </section>
312
+ </div>
313
+ </div>
314
+ </template>
315
+
316
+ <script setup lang="ts">
317
+ import greenMortgageApi from '@/api/modules/industry/green_mortgage'
318
+ import { industryExportToExcel } from '@/utils/function'
319
+ import { useHiddenLayoutScroll } from '@/utils/gen-ai-scroll'
320
+ import type { TableInstance } from 'element-plus'
321
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
322
+
323
+ interface GreenMortgageRow {
324
+ propertyNameEng: string
325
+ propertyNameChi: string
326
+ propertyAddress: string
327
+ owner: string
328
+ district: string
329
+ tool: string
330
+ projectType: string
331
+ achievedRating: string
332
+ year: string
333
+ month: string
334
+ gbCertLabelNo: string
335
+ }
336
+
337
+ const EXPORT_TABLE_HEADERS = [
338
+ 'Property Name',
339
+ '物業名稱',
340
+ 'Property Address',
341
+ 'Owner/Developer Co. Name',
342
+ 'District',
343
+ 'Tool',
344
+ 'Project Type',
345
+ 'Achieved Rating',
346
+ 'Year',
347
+ 'Month',
348
+ 'GB Cert Label No',
349
+ ] as const
350
+
351
+ const COLUMN_WIDTH_STORAGE_KEY = 'green-mortgage-table-column-widths'
352
+
353
+ const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
354
+ propertyNameEng: 120,
355
+ propertyNameChi: 120,
356
+ propertyAddress: 170,
357
+ owner: 210,
358
+ district: 90,
359
+ tool: 90,
360
+ projectType: 110,
361
+ achievedRating: 120,
362
+ year: 72,
363
+ month: 72,
364
+ gbCertLabelNo: 150,
365
+ }
366
+
367
+ function loadColumnWidths(): Record<string, number> {
368
+ try {
369
+ const raw = localStorage.getItem(COLUMN_WIDTH_STORAGE_KEY)
370
+ if (!raw) return { ...DEFAULT_COLUMN_WIDTHS }
371
+ const parsed = JSON.parse(raw) as Record<string, unknown>
372
+ const next = { ...DEFAULT_COLUMN_WIDTHS }
373
+ for (const key of Object.keys(DEFAULT_COLUMN_WIDTHS)) {
374
+ const value = Number(parsed[key])
375
+ if (Number.isFinite(value) && value >= 48) next[key] = Math.round(value)
376
+ }
377
+ return next
378
+ } catch {
379
+ return { ...DEFAULT_COLUMN_WIDTHS }
380
+ }
381
+ }
382
+
383
+ const columnWidths = ref<Record<string, number>>(loadColumnWidths())
384
+
385
+ function persistColumnWidths() {
386
+ localStorage.setItem(COLUMN_WIDTH_STORAGE_KEY, JSON.stringify(columnWidths.value))
387
+ }
388
+
389
+ function onHeaderDragEnd(newWidth: number, _oldWidth: number, column: { property?: string }) {
390
+ const prop = column?.property
391
+ if (!prop || !(prop in DEFAULT_COLUMN_WIDTHS)) return
392
+ columnWidths.value = {
393
+ ...columnWidths.value,
394
+ [prop]: Math.max(48, Math.round(newWidth)),
395
+ }
396
+ persistColumnWidths()
397
+ void nextTick(() => refreshTableScrollUi())
398
+ }
399
+
400
+ const tableLoading = ref(false)
401
+ const dataUpdateDate = ref('')
402
+ const dataDownloadStatus = ref('')
403
+ const ratingOptions = ref<string[]>([])
404
+ /** 下拉選單當前選值(改動不立刻刷表,需點 Refresh) */
405
+ const selectedRatings = ref<string[]>([])
406
+ /** Year Range:[startYear, endYear],元件保證 end >= start(改動不立刻刷表,需點 Refresh) */
407
+ const yearRange = ref<string[] | null>(null)
408
+ /** 已套用到列表的 Rating / Year(僅進頁預設與 Refresh 時寫入) */
409
+ const appliedRatings = ref<string[]>([])
410
+ const appliedYearRange = ref<string[] | null>(null)
411
+ const searchKeyword = ref('')
412
+ const tableRows = ref<GreenMortgageRow[]>([])
413
+ const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
414
+
415
+ const startYear = computed(() => {
416
+ const start = yearRange.value?.[0]
417
+ return start != null ? String(start) : ''
418
+ })
419
+ const endYear = computed(() => {
420
+ const end = yearRange.value?.[1]
421
+ return end != null ? String(end) : ''
422
+ })
423
+
424
+ const tableRootRef = ref<HTMLElement | null>(null)
425
+ const tableRef = ref<TableInstance>()
426
+ const hbarRef = ref<HTMLElement | null>(null)
427
+
428
+ /** 表體內側自繪滾動條 */
429
+ const THUMB_PX = 50
430
+ const H_THUMB_PX = 50
431
+ const SCROLL_THUMB_COLOR = '#c1c1c1'
432
+ /** 判定可豎滾的溢出閾值(避免 10 條內仍誤顯滾動條) */
433
+ const V_OVERFLOW_THRESHOLD = 8
434
+ const H_OVERFLOW_THRESHOLD = 8
435
+ /** 豎軌道底部預留,避免與橫軌道打架 */
436
+ const V_BAR_BOTTOM_RESERVE = 4
437
+ const scrollCan = ref(false)
438
+ const hScrollCan = ref(false)
439
+ const thumbHeight = ref(THUMB_PX)
440
+ const thumbOffset = ref(0)
441
+ const trackTop = ref(0)
442
+ const trackHeight = ref(0)
443
+ const hThumbWidth = ref(H_THUMB_PX)
444
+ const hThumbOffset = ref(0)
445
+ const hTrackWidth = ref(0)
446
+ const tableHovered = ref(false)
447
+ const thumbDragging = ref(false)
448
+ const hThumbDragging = ref(false)
449
+
450
+ const showScrollbars = computed(
451
+ () => tableHovered.value || thumbDragging.value || hThumbDragging.value
452
+ )
453
+
454
+ const vbarTrackStyle = computed(() => ({
455
+ top: `${trackTop.value}px`,
456
+ height: `${trackHeight.value}px`,
457
+ }))
458
+
459
+ const vbarThumbStyle = computed(() => ({
460
+ transform: `translateY(${thumbOffset.value}px)`,
461
+ backgroundColor: SCROLL_THUMB_COLOR,
462
+ }))
463
+
464
+ const hbarThumbStyle = computed(() => ({
465
+ transform: `translateX(${hThumbOffset.value}px)`,
466
+ backgroundColor: SCROLL_THUMB_COLOR,
467
+ }))
468
+
469
+ function onTableShellEnter() {
470
+ tableHovered.value = true
471
+ void nextTick(() => refreshOuterBar())
472
+ }
473
+
474
+ function onTableShellLeave() {
475
+ if (thumbDragging.value || hThumbDragging.value) return
476
+ tableHovered.value = false
477
+ }
478
+
479
+ /** 真正可滾的 wrap(優先 el-scrollbar__wrap) */
480
+ function getScrollWrap(): HTMLElement | null {
481
+ const root = tableRootRef.value
482
+ if (!root) return null
483
+
484
+ const wrap =
485
+ (root.querySelector('.el-table__body-wrapper .el-scrollbar__wrap') as HTMLElement | null) ||
486
+ (root.querySelector('.el-scrollbar__wrap') as HTMLElement | null) ||
487
+ (root.querySelector('.el-table__body-wrapper') as HTMLElement | null)
488
+
489
+ return wrap
490
+ }
491
+
492
+ /** 橫滾時同步表頭,避免表體動、表頭不動 */
493
+ function syncHeaderScrollLeft(left: number) {
494
+ const root = tableRootRef.value
495
+ if (!root) return
496
+ const headerWrap = root.querySelector('.el-table__header-wrapper') as HTMLElement | null
497
+ if (headerWrap && headerWrap.scrollLeft !== left) headerWrap.scrollLeft = left
498
+ }
499
+
500
+ function refreshOuterBar() {
501
+ const root = tableRootRef.value
502
+ const wrap = getScrollWrap()
503
+ if (!root || !wrap) {
504
+ scrollCan.value = false
505
+ hScrollCan.value = false
506
+ return
507
+ }
508
+
509
+ const { scrollTop, scrollHeight, clientHeight, scrollLeft, scrollWidth, clientWidth } = wrap
510
+
511
+ // —— 豎向(無資料時不顯示) ——
512
+ const vOverflow = scrollHeight - clientHeight
513
+ const canV = filteredRows.value.length > 0 && vOverflow > V_OVERFLOW_THRESHOLD
514
+ scrollCan.value = canV
515
+ if (canV) {
516
+ const header = root.querySelector('.el-table__header-wrapper') as HTMLElement | null
517
+ const headerH = header?.offsetHeight ?? 0
518
+ trackTop.value = headerH + 2
519
+ trackHeight.value = Math.max(0, clientHeight - V_BAR_BOTTOM_RESERVE)
520
+ const trackH = trackHeight.value
521
+ if (trackH <= 0) {
522
+ scrollCan.value = false
523
+ } else {
524
+ thumbHeight.value = THUMB_PX
525
+ const maxTravel = Math.max(0, trackH - THUMB_PX)
526
+ thumbOffset.value =
527
+ vOverflow > 0 && maxTravel > 0 ? Math.round((scrollTop / vOverflow) * maxTravel) : 0
528
+ }
529
+ }
530
+
531
+ // —— 橫向(列寬溢出) ——
532
+ const hOverflow = scrollWidth - clientWidth
533
+ const canH = hOverflow > H_OVERFLOW_THRESHOLD
534
+ hScrollCan.value = canH
535
+ if (canH) {
536
+ // 軌道寬以可見 hbar / 表體為準
537
+ const trackW = Math.max(0, hbarRef.value?.clientWidth || root.clientWidth || clientWidth)
538
+ hTrackWidth.value = trackW
539
+ hThumbWidth.value = H_THUMB_PX
540
+ const maxTravel = Math.max(0, trackW - H_THUMB_PX)
541
+ hThumbOffset.value =
542
+ hOverflow > 0 && maxTravel > 0 ? Math.round((scrollLeft / hOverflow) * maxTravel) : 0
543
+ syncHeaderScrollLeft(scrollLeft)
544
+ }
545
+ }
546
+
547
+ function onWrapScroll() {
548
+ if (thumbDragging.value || hThumbDragging.value) return
549
+ const wrap = getScrollWrap()
550
+ if (wrap) syncHeaderScrollLeft(wrap.scrollLeft)
551
+ refreshOuterBar()
552
+ }
553
+
554
+ let dragCleanup: (() => void) | undefined
555
+ let boundWrap: HTMLElement | null = null
556
+ let tableResizeObserver: ResizeObserver | undefined
557
+
558
+ function applyThumbScroll(
559
+ scrollEl: HTMLElement,
560
+ nextOffset: number,
561
+ maxTravel: number,
562
+ scrollMax: number
563
+ ) {
564
+ const offset = Math.min(maxTravel, Math.max(0, nextOffset))
565
+ thumbOffset.value = offset
566
+ if (maxTravel <= 0 || scrollMax <= 0) return
567
+ scrollEl.scrollTop = (offset / maxTravel) * scrollMax
568
+ }
569
+
570
+ function applyHThumbScroll(
571
+ scrollEl: HTMLElement,
572
+ nextOffset: number,
573
+ maxTravel: number,
574
+ scrollMax: number
575
+ ) {
576
+ const offset = Math.min(maxTravel, Math.max(0, nextOffset))
577
+ hThumbOffset.value = offset
578
+ if (maxTravel <= 0 || scrollMax <= 0) return
579
+ const left = (offset / maxTravel) * scrollMax
580
+ scrollEl.scrollLeft = left
581
+ syncHeaderScrollLeft(left)
582
+ }
583
+
584
+ function onThumbMouseDown(event: MouseEvent) {
585
+ const wrapEl = getScrollWrap()
586
+ if (!wrapEl || !scrollCan.value) return
587
+ const scrollEl: HTMLElement = wrapEl
588
+
589
+ thumbDragging.value = true
590
+ tableHovered.value = true
591
+
592
+ const startY = event.clientY
593
+ const startTop = thumbOffset.value
594
+ const startScrollTop = scrollEl.scrollTop
595
+ const trackH = trackHeight.value
596
+ const maxTravel = Math.max(0, trackH - thumbHeight.value)
597
+ const scrollMax = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight)
598
+
599
+ function onMove(e: MouseEvent) {
600
+ if (scrollMax <= 0 || trackH <= 0) return
601
+ const delta = e.clientY - startY
602
+ if (maxTravel <= 0) {
603
+ scrollEl.scrollTop = Math.min(
604
+ scrollMax,
605
+ Math.max(0, startScrollTop + (delta / trackH) * scrollMax)
606
+ )
607
+ return
608
+ }
609
+ applyThumbScroll(scrollEl, startTop + delta, maxTravel, scrollMax)
610
+ }
611
+
612
+ function onUp() {
613
+ thumbDragging.value = false
614
+ const root = tableRootRef.value?.closest('.green-mortgage__table-wrap')
615
+ if (!root?.matches(':hover')) tableHovered.value = false
616
+ dragCleanup?.()
617
+ dragCleanup = undefined
618
+ refreshOuterBar()
619
+ }
620
+
621
+ window.addEventListener('mousemove', onMove, { passive: true })
622
+ window.addEventListener('mouseup', onUp, { once: true })
623
+ dragCleanup = () => {
624
+ window.removeEventListener('mousemove', onMove)
625
+ window.removeEventListener('mouseup', onUp)
626
+ }
627
+ }
628
+
629
+ /** 點擊豎軌道空白處跳轉 */
630
+ function onTrackMouseDown(event: MouseEvent) {
631
+ const wrapEl = getScrollWrap()
632
+ if (!wrapEl || !scrollCan.value) return
633
+ const trackH = trackHeight.value
634
+ const maxTravel = Math.max(0, trackH - thumbHeight.value)
635
+ const scrollMax = Math.max(0, wrapEl.scrollHeight - wrapEl.clientHeight)
636
+ if (trackH <= 0 || scrollMax <= 0) return
637
+
638
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
639
+ const y = event.clientY - rect.top - thumbHeight.value / 2
640
+ if (maxTravel <= 0) {
641
+ wrapEl.scrollTop = Math.min(scrollMax, Math.max(0, (y / trackH) * scrollMax))
642
+ } else {
643
+ applyThumbScroll(wrapEl, y, maxTravel, scrollMax)
644
+ }
645
+ onThumbMouseDown(event)
646
+ }
647
+
648
+ function onHThumbMouseDown(event: MouseEvent) {
649
+ const wrapEl = getScrollWrap()
650
+ if (!wrapEl || !hScrollCan.value) return
651
+ const scrollEl: HTMLElement = wrapEl
652
+
653
+ hThumbDragging.value = true
654
+ tableHovered.value = true
655
+
656
+ const startX = event.clientX
657
+ const startLeft = hThumbOffset.value
658
+ const startScrollLeft = scrollEl.scrollLeft
659
+ const trackW = Math.max(
660
+ 0,
661
+ hTrackWidth.value || hbarRef.value?.clientWidth || scrollEl.clientWidth
662
+ )
663
+ const maxTravel = Math.max(0, trackW - hThumbWidth.value)
664
+ const scrollMax = Math.max(0, scrollEl.scrollWidth - scrollEl.clientWidth)
665
+
666
+ function onMove(e: MouseEvent) {
667
+ if (scrollMax <= 0 || trackW <= 0) return
668
+ const delta = e.clientX - startX
669
+ if (maxTravel <= 0) {
670
+ const left = Math.min(scrollMax, Math.max(0, startScrollLeft + (delta / trackW) * scrollMax))
671
+ scrollEl.scrollLeft = left
672
+ syncHeaderScrollLeft(left)
673
+ return
674
+ }
675
+ applyHThumbScroll(scrollEl, startLeft + delta, maxTravel, scrollMax)
676
+ }
677
+
678
+ function onUp() {
679
+ hThumbDragging.value = false
680
+ const root = tableRootRef.value?.closest('.green-mortgage__table-wrap')
681
+ if (!root?.matches(':hover')) tableHovered.value = false
682
+ dragCleanup?.()
683
+ dragCleanup = undefined
684
+ refreshOuterBar()
685
+ }
686
+
687
+ window.addEventListener('mousemove', onMove, { passive: true })
688
+ window.addEventListener('mouseup', onUp, { once: true })
689
+ dragCleanup = () => {
690
+ window.removeEventListener('mousemove', onMove)
691
+ window.removeEventListener('mouseup', onUp)
692
+ }
693
+ }
694
+
695
+ function onHTrackMouseDown(event: MouseEvent) {
696
+ const wrapEl = getScrollWrap()
697
+ if (!wrapEl || !hScrollCan.value) return
698
+ const trackW = Math.max(0, hTrackWidth.value || hbarRef.value?.clientWidth || wrapEl.clientWidth)
699
+ const maxTravel = Math.max(0, trackW - hThumbWidth.value)
700
+ const scrollMax = Math.max(0, wrapEl.scrollWidth - wrapEl.clientWidth)
701
+ if (trackW <= 0 || scrollMax <= 0) return
702
+
703
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
704
+ const x = event.clientX - rect.left - hThumbWidth.value / 2
705
+ if (maxTravel <= 0) {
706
+ const left = Math.min(scrollMax, Math.max(0, (x / trackW) * scrollMax))
707
+ wrapEl.scrollLeft = left
708
+ syncHeaderScrollLeft(left)
709
+ } else {
710
+ applyHThumbScroll(wrapEl, x, maxTravel, scrollMax)
711
+ }
712
+ onHThumbMouseDown(event)
713
+ }
714
+
715
+ function bindWrapScroll() {
716
+ unbindWrapScroll()
717
+ const wrap = getScrollWrap()
718
+ if (!wrap) return
719
+ wrap.addEventListener('scroll', onWrapScroll, { passive: true })
720
+ boundWrap = wrap
721
+ }
722
+
723
+ function unbindWrapScroll() {
724
+ boundWrap?.removeEventListener('scroll', onWrapScroll)
725
+ boundWrap = null
726
+ }
727
+
728
+ function refreshTableScrollUi() {
729
+ tableRef.value?.doLayout()
730
+ void nextTick(() => {
731
+ bindWrapScroll()
732
+ refreshOuterBar()
733
+ })
734
+ }
735
+
736
+ function syncViewportWidth() {
737
+ viewportWidth.value = window.innerWidth
738
+ refreshTableScrollUi()
739
+ }
740
+
741
+ const filteredRows = computed(() => filterRowsByCurrentCriteria(tableRows.value))
742
+
743
+ watch(filteredRows, () => {
744
+ void nextTick(() => refreshTableScrollUi())
745
+ })
746
+
747
+ function pickString(record: Record<string, unknown>, keys: string[]) {
748
+ for (const key of keys) {
749
+ const value = record[key]
750
+ if (value != null && value !== '') return String(value)
751
+ }
752
+ return ''
753
+ }
754
+
755
+ function normalizeRow(item: unknown): GreenMortgageRow {
756
+ const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
757
+ return {
758
+ propertyNameEng: pickString(record, ['propertyNameEng', 'PROPERTY_NAME_ENG']),
759
+ propertyNameChi: pickString(record, ['propertyNameChi', 'PROPERTY_NAME_CHI']),
760
+ propertyAddress: pickString(record, ['propertyAddress', 'PROPERTY_ADDRESS']),
761
+ owner: pickString(record, ['owner', 'OWNER']),
762
+ district: pickString(record, ['district', 'DISTRICT']),
763
+ tool: pickString(record, ['tool', 'TOOL']),
764
+ projectType: pickString(record, ['projectType', 'PROJECT_TYPE']),
765
+ achievedRating: pickString(record, ['achievedRating', 'ACHIEVED_RATING']),
766
+ year: pickString(record, ['year', 'YEAR', 'Year']),
767
+ month: pickString(record, ['month', 'MONTH']),
768
+ gbCertLabelNo: pickString(record, ['gbCertLabelNo', 'GB_CERT_LABEL_NO']),
769
+ }
770
+ }
771
+
772
+ function parseQueryResponse(data: unknown) {
773
+ if (!data || typeof data !== 'object') {
774
+ return { rows: [] as GreenMortgageRow[], dataUpdateDate: '', dataDownloadStatus: '' }
775
+ }
776
+
777
+ const record = data as Record<string, unknown>
778
+ // 兼容 data 再包一層
779
+ const inner =
780
+ record.data && typeof record.data === 'object' && !Array.isArray(record.data)
781
+ ? (record.data as Record<string, unknown>)
782
+ : record
783
+
784
+ const statusVO =
785
+ (inner.riskModelDataStatusVO && typeof inner.riskModelDataStatusVO === 'object'
786
+ ? (inner.riskModelDataStatusVO as Record<string, unknown>)
787
+ : null) ||
788
+ (record.riskModelDataStatusVO && typeof record.riskModelDataStatusVO === 'object'
789
+ ? (record.riskModelDataStatusVO as Record<string, unknown>)
790
+ : {})
791
+
792
+ const list = inner.riskModelDataVOList ?? record.riskModelDataVOList
793
+ const rows = Array.isArray(list) ? list.map(normalizeRow) : []
794
+
795
+ return {
796
+ rows,
797
+ dataUpdateDate: pickString(statusVO, ['dataUpdateDate', 'DataUpdateDate']),
798
+ dataDownloadStatus: pickString(statusVO, ['dataDownloadStatus', 'DataDownloadStatus']),
799
+ }
800
+ }
801
+
802
+ function parseRatingsResponse(data: unknown): string[] {
803
+ if (!Array.isArray(data)) return []
804
+ return data.map((item) => String(item).trim()).filter(Boolean)
805
+ }
806
+
807
+ function compareLexicographic(a: unknown, b: unknown) {
808
+ return String(a ?? '').localeCompare(String(b ?? ''), undefined, {
809
+ sensitivity: 'base',
810
+ })
811
+ }
812
+
813
+ function compareText(a: unknown, b: unknown) {
814
+ return String(a ?? '').localeCompare(String(b ?? ''), undefined, {
815
+ numeric: true,
816
+ sensitivity: 'base',
817
+ })
818
+ }
819
+
820
+ function compareAlphabeticalEmptyFirst(a: unknown, b: unknown) {
821
+ const strA = String(a ?? '').trim()
822
+ const strB = String(b ?? '').trim()
823
+ const emptyA = strA === ''
824
+ const emptyB = strB === ''
825
+
826
+ if (emptyA && emptyB) return 0
827
+ if (emptyA) return -1
828
+ if (emptyB) return 1
829
+
830
+ return strA.localeCompare(strB, undefined, { sensitivity: 'base' })
831
+ }
832
+
833
+ function createAlphabeticalEmptyFirstSorter(key: keyof GreenMortgageRow) {
834
+ return (a: GreenMortgageRow, b: GreenMortgageRow) => compareAlphabeticalEmptyFirst(a[key], b[key])
835
+ }
836
+
837
+ /** 首字元:數字 0 → 字母 1 → 其他 2 */
838
+ function getFirstCharSortCategory(text: string) {
839
+ const trimmed = text.trim()
840
+ if (!trimmed) return 2
841
+ const first = trimmed.charAt(0)
842
+ if (/\d/.test(first)) return 0
843
+ if (/[a-zA-Z]/.test(first)) return 1
844
+ return 2
845
+ }
846
+
847
+ function getPropertyNameSortKey(row: GreenMortgageRow) {
848
+ return (row.propertyNameEng || '').trim()
849
+ }
850
+
851
+ function getPropertyNameLeadingNumber(name: string) {
852
+ const matched = name.trim().match(/^(\d+)/)
853
+ return matched ? Number(matched[1]) : Number.NaN
854
+ }
855
+
856
+ const sortByPropertyName = (a: GreenMortgageRow, b: GreenMortgageRow) => {
857
+ const nameA = getPropertyNameSortKey(a)
858
+ const nameB = getPropertyNameSortKey(b)
859
+ const categoryA = getFirstCharSortCategory(nameA)
860
+ const categoryB = getFirstCharSortCategory(nameB)
861
+
862
+ if (categoryA !== categoryB) return categoryA - categoryB
863
+
864
+ if (categoryA === 0) {
865
+ const leadingA = getPropertyNameLeadingNumber(nameA)
866
+ const leadingB = getPropertyNameLeadingNumber(nameB)
867
+ if (!Number.isNaN(leadingA) && !Number.isNaN(leadingB) && leadingA !== leadingB) {
868
+ return leadingA - leadingB
869
+ }
870
+ }
871
+
872
+ return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' })
873
+ }
874
+
875
+ const sortByPropertyAddress = (a: GreenMortgageRow, b: GreenMortgageRow) => {
876
+ const addressA = (a.propertyAddress || '').trim()
877
+ const addressB = (b.propertyAddress || '').trim()
878
+ const categoryA = getFirstCharSortCategory(addressA)
879
+ const categoryB = getFirstCharSortCategory(addressB)
880
+
881
+ if (categoryA !== categoryB) return categoryA - categoryB
882
+
883
+ return compareLexicographic(addressA, addressB)
884
+ }
885
+ const sortByOwner = (a: GreenMortgageRow, b: GreenMortgageRow) => compareText(a.owner, b.owner)
886
+ const sortByDistrict = createAlphabeticalEmptyFirstSorter('district')
887
+ const sortByTool = createAlphabeticalEmptyFirstSorter('tool')
888
+ const sortByProjectType = createAlphabeticalEmptyFirstSorter('projectType')
889
+ const sortByAchievedRating = createAlphabeticalEmptyFirstSorter('achievedRating')
890
+ const sortByYear = createAlphabeticalEmptyFirstSorter('year')
891
+ /** Month:按 1–12 月份數值排序(空值靠前),非字母序 */
892
+ const sortByMonth = (a: GreenMortgageRow, b: GreenMortgageRow) => {
893
+ const monthA = parseMonthNumber(a.month)
894
+ const monthB = parseMonthNumber(b.month)
895
+ const emptyA = monthA == null
896
+ const emptyB = monthB == null
897
+ if (emptyA && emptyB) return 0
898
+ if (emptyA) return -1
899
+ if (emptyB) return 1
900
+ return monthA - monthB
901
+ }
902
+ const sortByGbCertLabelNo = createAlphabeticalEmptyFirstSorter('gbCertLabelNo')
903
+
904
+ function parseMonthNumber(value: unknown): number | null {
905
+ const raw = String(value ?? '').trim()
906
+ if (!raw) return null
907
+ const direct = Number(raw)
908
+ if (Number.isFinite(direct) && direct >= 1 && direct <= 12) return Math.trunc(direct)
909
+ const matched = raw.match(/(\d{1,2})/)
910
+ if (matched?.[1]) {
911
+ const n = Number(matched[1])
912
+ if (Number.isFinite(n) && n >= 1 && n <= 12) return Math.trunc(n)
913
+ }
914
+ const key = raw.toLowerCase().slice(0, 3)
915
+ const nameMap: Record<string, number> = {
916
+ jan: 1,
917
+ feb: 2,
918
+ mar: 3,
919
+ apr: 4,
920
+ may: 5,
921
+ jun: 6,
922
+ jul: 7,
923
+ aug: 8,
924
+ sep: 9,
925
+ oct: 10,
926
+ nov: 11,
927
+ dec: 12,
928
+ }
929
+ return nameMap[key] ?? null
930
+ }
931
+
932
+ async function fetchGreenMortgageRatings() {
933
+ try {
934
+ const res = await greenMortgageApi.getGreenMortgageRatings()
935
+ ratingOptions.value = parseRatingsResponse(res?.data)
936
+ selectedRatings.value = [...ratingOptions.value]
937
+ } catch (error) {
938
+ console.error('getGreenMortgageRatings failed', error)
939
+ ratingOptions.value = []
940
+ selectedRatings.value = []
941
+ }
942
+ }
943
+
944
+ function asTrimmedText(value: string | null | undefined) {
945
+ return String(value ?? '').trim()
946
+ }
947
+
948
+ function filterRowsByKeyword(rows: GreenMortgageRow[]) {
949
+ const keyword = asTrimmedText(searchKeyword.value).toLowerCase()
950
+ if (!keyword) return rows
951
+
952
+ return rows.filter((row) =>
953
+ Object.values(row).some((value) => String(value).toLowerCase().includes(keyword))
954
+ )
955
+ }
956
+
957
+ /** 從 Certification period / Year 欄取值中提取年份(相容 YYYY、YYYY-MM-DD、夾雜文字) */
958
+ function getYearFromPeriod(value: string | null | undefined): number | null {
959
+ const trimmed = asTrimmedText(value)
960
+ if (!trimmed) return null
961
+ const matched = trimmed.match(/(\d{4})/)
962
+ if (!matched?.[1]) return null
963
+ const year = Number(matched[1])
964
+ if (Number.isNaN(year) || year < 1900 || year > 2100) return null
965
+ return year
966
+ }
967
+
968
+ /** 接口返回後:遍歷列表 Year 欄,寫入最小 / 最大年份到 yearRange(進頁可見預設值) */
969
+ function applyYearRangeFromTableYearColumn(rows: GreenMortgageRow[]) {
970
+ let min = Infinity
971
+ let max = -Infinity
972
+ for (const row of rows) {
973
+ const year = getYearFromPeriod(row.year)
974
+ if (year == null) continue
975
+ if (year < min) min = year
976
+ if (year > max) max = year
977
+ }
978
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
979
+ yearRange.value = null
980
+ return
981
+ }
982
+ // 新建陣列,確保 yearrange 能偵測到變更並顯示
983
+ yearRange.value = [String(min), String(max)]
984
+ }
985
+
986
+ /**
987
+ * 列表篩選:已套用的 Select Rating(s) + Certification period + 即時 Search
988
+ * Rating / Year 僅在進頁預設與點 Refresh 後生效
989
+ */
990
+ function filterRowsByCurrentCriteria(rows: GreenMortgageRow[]) {
991
+ let result = rows
992
+
993
+ if (appliedRatings.value.length > 0) {
994
+ const ratingSet = new Set(appliedRatings.value)
995
+ result = result.filter((row) => ratingSet.has(row.achievedRating))
996
+ }
997
+
998
+ const start = getYearFromPeriod(appliedYearRange.value?.[0])
999
+ const end = getYearFromPeriod(appliedYearRange.value?.[1])
1000
+ if (start != null || end != null) {
1001
+ result = result.filter((row) => {
1002
+ const raw = row.year
1003
+ if (raw == null || String(raw).trim() === '') return true
1004
+ const year = Number(raw)
1005
+ if (Number.isNaN(year)) return true
1006
+ if (start != null && year < start) return false
1007
+ if (end != null && year > end) return false
1008
+ return true
1009
+ })
1010
+ }
1011
+
1012
+ return filterRowsByKeyword(result)
1013
+ }
1014
+
1015
+ /** 將當前下拉 / 日期選值套用到列表篩選 */
1016
+ function commitFilterCriteria() {
1017
+ appliedRatings.value = [...selectedRatings.value]
1018
+ appliedYearRange.value = yearRange.value?.length ? [...yearRange.value] : null
1019
+ }
1020
+
1021
+ function buildWorksheetData(rows: GreenMortgageRow[]) {
1022
+ return [
1023
+ [...EXPORT_TABLE_HEADERS],
1024
+ ...rows.map((row) => [
1025
+ row.propertyNameEng,
1026
+ row.propertyNameChi,
1027
+ row.propertyAddress,
1028
+ row.owner,
1029
+ row.district,
1030
+ row.tool,
1031
+ row.projectType,
1032
+ row.achievedRating,
1033
+ row.year,
1034
+ row.month,
1035
+ row.gbCertLabelNo,
1036
+ ]),
1037
+ ]
1038
+ }
1039
+
1040
+ /** 進頁預設空條件 = 查詢全部;Refresh 用當前下拉選值;匯出用已套用條件 */
1041
+ function getGreenMortgageQueryParams(source: 'draft' | 'applied' = 'draft') {
1042
+ if (source === 'applied') {
1043
+ return {
1044
+ endYear: asTrimmedText(appliedYearRange.value?.[1]),
1045
+ ratingList: [...appliedRatings.value],
1046
+ startYear: asTrimmedText(appliedYearRange.value?.[0]),
1047
+ }
1048
+ }
1049
+ return {
1050
+ endYear: asTrimmedText(endYear.value),
1051
+ ratingList: [...selectedRatings.value],
1052
+ startYear: asTrimmedText(startYear.value),
1053
+ }
1054
+ }
1055
+
1056
+ async function fetchGreenMortgageQuery(options?: { applyYearDefaults?: boolean }) {
1057
+ tableLoading.value = true
1058
+ try {
1059
+ const res = await greenMortgageApi.getGreenMortgageQuery(getGreenMortgageQueryParams())
1060
+ // 兼容 { data: {...} } / 直接 {...}
1061
+ const payload =
1062
+ res && typeof res === 'object' && 'data' in res && (res as { data?: unknown }).data != null
1063
+ ? (res as { data: unknown }).data
1064
+ : res
1065
+
1066
+ if (payload) {
1067
+ const parsed = parseQueryResponse(payload)
1068
+ tableRows.value = parsed.rows
1069
+ if (parsed.dataUpdateDate) dataUpdateDate.value = parsed.dataUpdateDate
1070
+ if (parsed.dataDownloadStatus) dataDownloadStatus.value = parsed.dataDownloadStatus
1071
+ // 進頁:列表 Year 回來後立刻寫入 Certification period,刷新頁面可見預設區間
1072
+ if (options?.applyYearDefaults) {
1073
+ applyYearRangeFromTableYearColumn(parsed.rows)
1074
+ await nextTick()
1075
+ }
1076
+ } else {
1077
+ tableRows.value = []
1078
+ if (options?.applyYearDefaults) yearRange.value = null
1079
+ }
1080
+ } catch (error) {
1081
+ console.error('getGreenMortgageQuery failed', error)
1082
+ } finally {
1083
+ tableLoading.value = false
1084
+ }
1085
+ }
1086
+
1087
+ function handleRefresh() {
1088
+ commitFilterCriteria()
1089
+ fetchGreenMortgageQuery()
1090
+ }
1091
+
1092
+ /** 匯出已套用的 Select Rating(s) + Certification period + Search 篩選後的資料(格式不變) */
1093
+ async function handleExportExcel() {
1094
+ try {
1095
+ const res = await greenMortgageApi.getGreenMortgageQuery(getGreenMortgageQueryParams('applied'))
1096
+ if (!res?.data) return
1097
+
1098
+ const parsed = parseQueryResponse(res.data)
1099
+ const rows = filterRowsByCurrentCriteria(parsed.rows)
1100
+ if (!rows.length) return
1101
+
1102
+ const worksheetData = buildWorksheetData(rows)
1103
+ await industryExportToExcel(worksheetData, 'Green Mortgage', 'Green_Mortgage')
1104
+ } catch (error) {
1105
+ console.error('handleExportExcel failed', error)
1106
+ }
1107
+ }
1108
+
1109
+ /** 匯出當前頁已篩選資料(Select Rating / Certification period / Search 過濾後的結果) */
1110
+ async function handleExportExcel2() {
1111
+ try {
1112
+ const rows = filteredRows.value
1113
+ if (!rows.length) return
1114
+
1115
+ const worksheetData = buildWorksheetData(rows)
1116
+ await industryExportToExcel(worksheetData, 'Green Mortgage', 'Green_Mortgage')
1117
+ } catch (error) {
1118
+ console.error('handleExportExcel2 failed', error)
1119
+ }
1120
+ }
1121
+
1122
+ /** 備用導出入口:與 handleExportExcel 二選一替換按鈕 @click 即可 */
1123
+ defineExpose({ handleExportExcel2 })
1124
+
1125
+ // 鎖定 .layout 外層滾動(進/離頁自動處理)
1126
+ useHiddenLayoutScroll()
1127
+
1128
+ onMounted(async () => {
1129
+ // 以下僅本頁自繪表內滾動條:resize / 軌道同步(與 layout 鎖定無關)
1130
+ syncViewportWidth()
1131
+ window.addEventListener('resize', syncViewportWidth)
1132
+ void nextTick(() => {
1133
+ refreshTableScrollUi()
1134
+ const root = tableRootRef.value
1135
+ if (root) {
1136
+ tableResizeObserver = new ResizeObserver(() => {
1137
+ void nextTick(() => refreshTableScrollUi())
1138
+ })
1139
+ tableResizeObserver.observe(root)
1140
+ }
1141
+ })
1142
+ await fetchGreenMortgageRatings()
1143
+ await fetchGreenMortgageQuery({ applyYearDefaults: true })
1144
+ // 保險:若首次寫入未生效,再依 tableRows.Year 補一次
1145
+ if (!yearRange.value?.length && tableRows.value.length) {
1146
+ applyYearRangeFromTableYearColumn(tableRows.value)
1147
+ }
1148
+ // 進頁預設值寫入後立刻套用,之後改下拉/日期需點 Refresh 才更新列表
1149
+ commitFilterCriteria()
1150
+ await nextTick()
1151
+ tableRef.value?.sort('propertyNameEng', 'ascending')
1152
+ void nextTick(() => refreshTableScrollUi())
1153
+ })
1154
+
1155
+ onUnmounted(() => {
1156
+ window.removeEventListener('resize', syncViewportWidth)
1157
+ tableResizeObserver?.disconnect()
1158
+ unbindWrapScroll()
1159
+ dragCleanup?.()
1160
+ })
1161
+ </script>
1162
+
1163
+ <style lang="scss" scoped>
1164
+ .green-mortgage-page {
1165
+ box-sizing: border-box;
1166
+ width: 100%;
1167
+ height: 100%;
1168
+ min-height: 0;
1169
+ padding: 0;
1170
+ background: transparent;
1171
+ /* 允許豎條伸入 el-card__body 右 padding */
1172
+ overflow: visible;
1173
+ }
1174
+
1175
+ .green-mortgage {
1176
+ /* 與 el-card__body 的 --gm-card-padding 對齊,供豎條外側定位 */
1177
+ --gm-card-padding: 20px;
1178
+ --gm-h-scroll-gutter: 14px;
1179
+ /* 豎條在右 padding 內居中:不改動其他邊距 */
1180
+ --gm-v-scroll-thumb: 8px;
1181
+ /* 篩選區控件高度與 el-button 默認 32px 對齊 */
1182
+ --gm-control-height: 32px;
1183
+ --el-component-size: var(--gm-control-height);
1184
+ --el-color-primary: #c53355;
1185
+ --el-color-primary-light-3: #d66b82;
1186
+ --el-color-primary-light-5: #e299a9;
1187
+ --el-color-primary-light-7: #efc6cf;
1188
+ --el-color-primary-light-9: #fce8ec;
1189
+ box-sizing: border-box;
1190
+ display: flex;
1191
+ flex-direction: column;
1192
+ width: 100%;
1193
+ height: 100%;
1194
+ min-height: 0;
1195
+ /* 勿裁切伸入 card 右 padding 的豎條 */
1196
+ overflow: visible;
1197
+ border: none;
1198
+ border-radius: 0;
1199
+ box-shadow: none;
1200
+ background: transparent;
1201
+ padding: 0;
1202
+ }
1203
+
1204
+ .green-mortgage__header {
1205
+ flex-shrink: 0;
1206
+ margin-bottom: 14px;
1207
+ }
1208
+
1209
+ .green-mortgage__title,
1210
+ .green-mortgage__subtitle {
1211
+ margin: 0;
1212
+ color: #c0504d;
1213
+ font-family: 'Times New Roman', Times, serif;
1214
+ font-size: 18px;
1215
+ font-weight: 400;
1216
+ line-height: 1.35;
1217
+ word-break: break-word;
1218
+ }
1219
+
1220
+ .green-mortgage__meta {
1221
+ margin: 4px 0 0;
1222
+ font-size: 14px;
1223
+ line-height: 1.4;
1224
+ color: var(--el-text-color-primary);
1225
+ word-break: break-word;
1226
+ }
1227
+
1228
+ .green-mortgage__meta-value {
1229
+ font-size: 16px;
1230
+ font-weight: 600;
1231
+ }
1232
+
1233
+ .green-mortgage__filters {
1234
+ flex-shrink: 0;
1235
+ margin-bottom: 10px;
1236
+ }
1237
+
1238
+ .green-mortgage__filter-row {
1239
+ display: flex;
1240
+ flex-wrap: wrap;
1241
+ align-items: center;
1242
+ gap: 10px 16px;
1243
+ margin-bottom: 10px;
1244
+ }
1245
+
1246
+ .green-mortgage__filter-group {
1247
+ display: flex;
1248
+ flex-wrap: wrap;
1249
+ align-items: center;
1250
+ gap: 8px 10px;
1251
+ min-width: 0;
1252
+ }
1253
+
1254
+ .green-mortgage__filter-group--period {
1255
+ flex: 1 1 auto;
1256
+ }
1257
+
1258
+ .green-mortgage__period-controls {
1259
+ display: flex;
1260
+ flex-wrap: wrap;
1261
+ align-items: center;
1262
+ gap: 8px 10px;
1263
+ min-width: 0;
1264
+ }
1265
+
1266
+ .green-mortgage__filter-label {
1267
+ font-size: 14px;
1268
+ white-space: nowrap;
1269
+ }
1270
+
1271
+ .green-mortgage__rating-select {
1272
+ width: 220px;
1273
+ max-width: 100%;
1274
+ height: var(--gm-control-height);
1275
+ }
1276
+
1277
+ .green-mortgage__rating-select :deep(.el-select__wrapper) {
1278
+ min-height: var(--gm-control-height);
1279
+ height: var(--gm-control-height);
1280
+ padding-top: 0;
1281
+ padding-bottom: 0;
1282
+ box-sizing: border-box;
1283
+ }
1284
+
1285
+ .green-mortgage__rating-select :deep(.el-select__selection) {
1286
+ gap: 4px;
1287
+ }
1288
+
1289
+ .green-mortgage__date-input {
1290
+ width: 320px !important;
1291
+ max-width: 320px;
1292
+ height: var(--gm-control-height);
1293
+ --el-input-height: var(--gm-control-height);
1294
+ }
1295
+
1296
+ .green-mortgage__date-input.el-date-editor {
1297
+ --el-date-editor-width: 320px;
1298
+ width: 320px !important;
1299
+ height: var(--gm-control-height);
1300
+ }
1301
+
1302
+ .green-mortgage__date-input :deep(.el-input__wrapper),
1303
+ .green-mortgage__date-input.el-input__wrapper {
1304
+ min-height: var(--gm-control-height);
1305
+ height: var(--gm-control-height);
1306
+ padding-top: 0;
1307
+ padding-bottom: 0;
1308
+ box-sizing: border-box;
1309
+ }
1310
+
1311
+ .green-mortgage__refresh-btn,
1312
+ .green-mortgage__export-btn {
1313
+ height: var(--gm-control-height);
1314
+ min-height: var(--gm-control-height);
1315
+ padding: 0 15px;
1316
+ border-radius: 4px;
1317
+ box-sizing: border-box;
1318
+ }
1319
+
1320
+ /* Refresh:默認主色底 */
1321
+ .green-mortgage__refresh-btn {
1322
+ --el-button-bg-color: #c53355;
1323
+ --el-button-border-color: #c53355;
1324
+ --el-button-text-color: #fff;
1325
+ --el-button-hover-bg-color: #d14a68;
1326
+ --el-button-hover-border-color: #d14a68;
1327
+ --el-button-hover-text-color: #fff;
1328
+ --el-button-active-bg-color: #a82b48;
1329
+ --el-button-active-border-color: #a82b48;
1330
+ --el-button-active-text-color: #fff;
1331
+ }
1332
+
1333
+ /* Export to Excel:移入主色描邊/文字 */
1334
+ .green-mortgage__export-btn {
1335
+ --el-button-hover-text-color: #c53355;
1336
+ --el-button-hover-bg-color: var(--el-color-primary-light-9, #fce8ec);
1337
+ --el-button-hover-border-color: #c53355;
1338
+ --el-button-active-text-color: #c53355;
1339
+ --el-button-active-border-color: #c53355;
1340
+ --el-button-active-bg-color: var(--el-color-primary-light-9, #fce8ec);
1341
+ }
1342
+
1343
+ .green-mortgage__action-row {
1344
+ display: flex;
1345
+ flex-wrap: wrap;
1346
+ align-items: center;
1347
+ justify-content: space-between;
1348
+ gap: 12px 16px;
1349
+ }
1350
+
1351
+ .green-mortgage__search {
1352
+ display: flex;
1353
+ flex-wrap: wrap;
1354
+ align-items: center;
1355
+ gap: 8px;
1356
+ min-width: 0;
1357
+ }
1358
+
1359
+ .green-mortgage__search-label {
1360
+ font-size: 14px;
1361
+ white-space: nowrap;
1362
+ }
1363
+
1364
+ .green-mortgage__search-input {
1365
+ width: 220px;
1366
+ max-width: 100%;
1367
+ height: var(--gm-control-height);
1368
+ --el-input-height: var(--gm-control-height);
1369
+ }
1370
+
1371
+ .green-mortgage__search-input :deep(.el-input__wrapper) {
1372
+ min-height: var(--gm-control-height);
1373
+ height: var(--gm-control-height);
1374
+ padding-top: 0;
1375
+ padding-bottom: 0;
1376
+ box-sizing: border-box;
1377
+ }
1378
+
1379
+ /* 有值時滑鼠移入顯示清空 x */
1380
+ .green-mortgage__rating-select :deep(.el-select__clear),
1381
+ .green-mortgage__date-input :deep(.el-input__clear),
1382
+ .green-mortgage__search-input :deep(.el-input__clear) {
1383
+ display: inline-flex !important;
1384
+ opacity: 0;
1385
+ transition: opacity 0.15s ease;
1386
+ cursor: pointer;
1387
+ }
1388
+
1389
+ .green-mortgage__rating-select:hover :deep(.el-select__clear),
1390
+ .green-mortgage__rating-select :deep(.el-select__wrapper.is-focused .el-select__clear),
1391
+ .green-mortgage__date-input:hover :deep(.el-input__clear),
1392
+ .green-mortgage__date-input :deep(.el-input__wrapper.is-focus .el-input__clear),
1393
+ .green-mortgage__search-input:hover :deep(.el-input__clear),
1394
+ .green-mortgage__search-input :deep(.el-input__wrapper.is-focus .el-input__clear) {
1395
+ opacity: 1;
1396
+ }
1397
+
1398
+ .green-mortgage__table-wrap {
1399
+ flex: 1;
1400
+ min-height: 0;
1401
+ display: flex;
1402
+ flex-direction: column;
1403
+ position: relative;
1404
+ box-sizing: border-box;
1405
+ /* 伸入 el-card__body 既有右 padding,表寬不變;豎條放在這段 padding 裡 */
1406
+ margin-right: calc(-1 * var(--gm-card-padding));
1407
+ padding-right: var(--gm-card-padding);
1408
+ overflow: visible;
1409
+ }
1410
+
1411
+ /* 列表底部:左側結果數、右側排除說明 */
1412
+ .green-mortgage__table-footer {
1413
+ flex-shrink: 0;
1414
+ display: flex;
1415
+ flex-wrap: wrap;
1416
+ align-items: baseline;
1417
+ justify-content: space-between;
1418
+ gap: 8px 16px;
1419
+ margin-top: 10px;
1420
+ padding: 0 2px;
1421
+ font-size: 14px;
1422
+ line-height: 1.4;
1423
+ color: #333;
1424
+ font-family: 'Times New Roman', Times, serif;
1425
+ box-sizing: border-box;
1426
+ }
1427
+
1428
+ .green-mortgage__table-footer-left,
1429
+ .green-mortgage__table-footer-right {
1430
+ min-width: 0;
1431
+ }
1432
+
1433
+ .green-mortgage__table-footer-right {
1434
+ text-align: right;
1435
+ }
1436
+
1437
+ /* 上:table;下:橫條(底邊外側);豎條絕對定位在右邊外側 */
1438
+ .green-mortgage__table-main {
1439
+ flex: 1;
1440
+ min-height: 0;
1441
+ min-width: 0;
1442
+ display: flex;
1443
+ flex-direction: column;
1444
+ position: relative;
1445
+ }
1446
+
1447
+ .green-mortgage__table-row {
1448
+ flex: 1;
1449
+ min-height: 0;
1450
+ min-width: 0;
1451
+ display: flex;
1452
+ flex-direction: row;
1453
+ align-items: stretch;
1454
+ }
1455
+
1456
+ .green-mortgage__table-shell {
1457
+ --gm-scroll-thumb: #c1c1c1;
1458
+ position: relative;
1459
+ flex: 1;
1460
+ min-height: 240px;
1461
+ min-width: 0;
1462
+ width: 100%;
1463
+ padding: 0;
1464
+ box-sizing: border-box;
1465
+ overflow: hidden;
1466
+ }
1467
+
1468
+ .green-mortgage__table-shell :deep(.el-table) {
1469
+ overflow: hidden;
1470
+ }
1471
+
1472
+ .green-mortgage__table-shell :deep(.el-table__inner-wrapper) {
1473
+ height: 100%;
1474
+ border-radius: 4px;
1475
+ overflow: hidden;
1476
+ }
1477
+
1478
+ .green-mortgage__table-shell :deep(.el-table__body-wrapper),
1479
+ .green-mortgage__table-shell :deep(.el-scrollbar__wrap) {
1480
+ overflow-x: auto !important;
1481
+ overflow-y: auto !important;
1482
+ }
1483
+
1484
+ .green-mortgage__th {
1485
+ display: flex;
1486
+ flex-direction: column;
1487
+ align-items: center;
1488
+ justify-content: center;
1489
+ gap: 2px;
1490
+ font-size: 13px;
1491
+ line-height: 1.25;
1492
+ text-align: center;
1493
+ white-space: normal;
1494
+ }
1495
+
1496
+ .green-mortgage__th--single {
1497
+ min-height: 34px;
1498
+ }
1499
+
1500
+ .green-mortgage__th--inline {
1501
+ flex-direction: row;
1502
+ flex-wrap: wrap;
1503
+ gap: 4px;
1504
+ }
1505
+
1506
+ .green-mortgage__table {
1507
+ width: 100%;
1508
+ height: 100%;
1509
+ }
1510
+
1511
+ /* 勿強制表體 width:100%,否則列寬總和無法溢出,橫向滾動條永久失效 */
1512
+ .green-mortgage__table :deep(.el-table) {
1513
+ width: 100%;
1514
+ --el-table-row-hover-bg-color: #edf2fa;
1515
+ }
1516
+
1517
+ .green-mortgage__table :deep(.el-table__body tr.el-table__row:hover > td.el-table__cell) {
1518
+ background-color: #edf2fa;
1519
+ }
1520
+
1521
+ .green-mortgage__table :deep(.el-table__body tr.el-table__row--striped td.el-table__cell) {
1522
+ background-color: #f2f2f2;
1523
+ }
1524
+
1525
+ .green-mortgage__table :deep(.el-table__header-wrapper),
1526
+ .green-mortgage__table :deep(.el-table__header) {
1527
+ background: #556b7d;
1528
+ }
1529
+
1530
+ /* 去掉 EP 內建 gutter 空白列(豎條已疊加在表內,避免表頭多一截空白) */
1531
+ .green-mortgage__table :deep(.el-table__header th.gutter),
1532
+ .green-mortgage__table :deep(.el-table__body td.gutter),
1533
+ .green-mortgage__table :deep(col[name='gutter']) {
1534
+ display: none !important;
1535
+ width: 0 !important;
1536
+ min-width: 0 !important;
1537
+ padding: 0 !important;
1538
+ border: none !important;
1539
+ overflow: hidden !important;
1540
+ }
1541
+
1542
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell) {
1543
+ position: relative;
1544
+ padding: 6px 0;
1545
+ background: #556b7d;
1546
+ color: #fff;
1547
+ border-color: #f5f7fa;
1548
+ }
1549
+
1550
+ /* 列寬拖拽縫:懸停 / 拖動指示條用系統主色 */
1551
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell::after) {
1552
+ content: '';
1553
+ position: absolute;
1554
+ top: 0;
1555
+ right: -2px;
1556
+ width: 4px;
1557
+ height: 100%;
1558
+ background: transparent;
1559
+ z-index: 3;
1560
+ pointer-events: none;
1561
+ transition: background-color 0.12s ease;
1562
+ }
1563
+
1564
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell:hover::after) {
1565
+ background: var(--el-color-primary, #c53355);
1566
+ }
1567
+
1568
+ .green-mortgage__table :deep(.el-table__column-resize-proxy) {
1569
+ border-left: 2px solid var(--el-color-primary, #c53355) !important;
1570
+ }
1571
+
1572
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell .cell) {
1573
+ padding: 0 8px;
1574
+ color: #fff;
1575
+ }
1576
+
1577
+ .green-mortgage__table :deep(.el-table__body td.el-table__cell) {
1578
+ border-color: #f5f7fa;
1579
+ }
1580
+
1581
+ .green-mortgage__table :deep(.el-table--border::before),
1582
+ .green-mortgage__table :deep(.el-table--border::after),
1583
+ .green-mortgage__table :deep(.el-table__inner-wrapper::after),
1584
+ .green-mortgage__table :deep(.el-table__border-left-patch) {
1585
+ background-color: #f5f7fa;
1586
+ }
1587
+
1588
+ /* 隱藏 table 內建橫/豎滑塊(改外側自繪) */
1589
+ .green-mortgage__table-shell :deep(.el-scrollbar__bar.is-vertical),
1590
+ .green-mortgage__table-shell :deep(.el-scrollbar__bar.is-horizontal) {
1591
+ display: none !important;
1592
+ }
1593
+
1594
+ .green-mortgage__table-shell :deep(.el-scrollbar__wrap) {
1595
+ scrollbar-width: none !important;
1596
+ -ms-overflow-style: none !important;
1597
+ }
1598
+
1599
+ .green-mortgage__table-shell :deep(.el-scrollbar__wrap::-webkit-scrollbar) {
1600
+ width: 0 !important;
1601
+ height: 0 !important;
1602
+ display: none !important;
1603
+ }
1604
+
1605
+ /* 橫向滑塊:表格底邊外側(不擠壓表體) */
1606
+ .green-mortgage__hbar-row {
1607
+ flex-shrink: 0;
1608
+ display: flex;
1609
+ flex-direction: row;
1610
+ align-items: stretch;
1611
+ min-height: 0;
1612
+ }
1613
+
1614
+ .green-mortgage__hbar {
1615
+ flex: 1;
1616
+ min-width: 0;
1617
+ position: relative;
1618
+ height: var(--gm-h-scroll-gutter);
1619
+ z-index: 12;
1620
+ pointer-events: auto;
1621
+ }
1622
+
1623
+ .green-mortgage__hthumb {
1624
+ position: absolute;
1625
+ top: 3px;
1626
+ left: 0;
1627
+ width: 50px;
1628
+ min-width: 50px;
1629
+ max-width: 50px;
1630
+ height: 8px;
1631
+ border-radius: 4px;
1632
+ background-color: var(--gm-scroll-thumb, #c1c1c1);
1633
+ cursor: grab;
1634
+ pointer-events: auto;
1635
+ will-change: transform;
1636
+ opacity: 0;
1637
+ visibility: hidden;
1638
+ transition: opacity 0.15s ease;
1639
+ }
1640
+
1641
+ .green-mortgage__hthumb.is-visible {
1642
+ opacity: 1;
1643
+ visibility: visible;
1644
+ }
1645
+
1646
+ .green-mortgage__hthumb:active {
1647
+ cursor: grabbing;
1648
+ background-color: #a8a8a8;
1649
+ }
1650
+
1651
+ /* 豎向滑塊:只移到表格右外側(card 既有 padding 內),其他邊距不變 */
1652
+ .green-mortgage__vbar {
1653
+ position: absolute;
1654
+ top: 0;
1655
+ right: calc((var(--gm-card-padding) - var(--gm-v-scroll-thumb)) / 2);
1656
+ left: auto;
1657
+ transform: none;
1658
+ width: var(--gm-v-scroll-thumb);
1659
+ z-index: 12;
1660
+ pointer-events: none;
1661
+ overflow: visible;
1662
+ }
1663
+
1664
+ .green-mortgage__vthumb {
1665
+ width: 8px;
1666
+ min-width: 8px;
1667
+ max-width: 8px;
1668
+ height: 50px;
1669
+ min-height: 50px;
1670
+ max-height: 50px;
1671
+ border-radius: 9999px;
1672
+ background-color: var(--gm-scroll-thumb, #c1c1c1);
1673
+ cursor: grab;
1674
+ pointer-events: auto;
1675
+ will-change: transform;
1676
+ opacity: 0;
1677
+ visibility: hidden;
1678
+ transition: opacity 0.15s ease;
1679
+ }
1680
+
1681
+ .green-mortgage__vthumb.is-visible {
1682
+ opacity: 1;
1683
+ visibility: visible;
1684
+ }
1685
+
1686
+ .green-mortgage__vthumb:active {
1687
+ cursor: grabbing;
1688
+ background-color: #a8a8a8;
1689
+ }
1690
+
1691
+ .green-mortgage__table :deep(.caret-wrapper) {
1692
+ position: absolute;
1693
+ top: 50%;
1694
+ right: 6px;
1695
+ transform: translateY(-50%);
1696
+ opacity: 1;
1697
+ visibility: visible;
1698
+ }
1699
+
1700
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.ascending .caret-wrapper),
1701
+ .green-mortgage__table
1702
+ :deep(.el-table__header-wrapper th.el-table__cell.descending .caret-wrapper) {
1703
+ right: 2px;
1704
+ }
1705
+
1706
+ .green-mortgage__table :deep(.sort-caret.ascending) {
1707
+ border-bottom-color: #c0c4cc;
1708
+ }
1709
+
1710
+ .green-mortgage__table :deep(.sort-caret.descending) {
1711
+ border-top-color: #c0c4cc;
1712
+ }
1713
+
1714
+ .green-mortgage__table
1715
+ :deep(.el-table__header-wrapper th.el-table__cell.ascending .sort-caret.ascending) {
1716
+ border-bottom-color: #c53355;
1717
+ }
1718
+
1719
+ .green-mortgage__table
1720
+ :deep(.el-table__header-wrapper th.el-table__cell.descending .sort-caret.descending) {
1721
+ border-top-color: #c53355;
1722
+ }
1723
+
1724
+ .green-mortgage__table :deep(.el-table__header-wrapper th.el-table__cell.is-sortable .cell) {
1725
+ position: relative;
1726
+ padding-right: 24px;
1727
+ }
1728
+
1729
+ /* 大屏 */
1730
+ @media (min-width: 1200px) {
1731
+ .green-mortgage__title,
1732
+ .green-mortgage__subtitle {
1733
+ font-size: 18px;
1734
+ }
1735
+
1736
+ .green-mortgage__table {
1737
+ min-width: 100%;
1738
+ }
1739
+ }
1740
+
1741
+ /* 平板 */
1742
+ @media (max-width: 1199px) {
1743
+ .green-mortgage__title,
1744
+ .green-mortgage__subtitle {
1745
+ font-size: 16px;
1746
+ }
1747
+
1748
+ .green-mortgage__rating-select,
1749
+ .green-mortgage__search-input {
1750
+ width: 200px;
1751
+ }
1752
+
1753
+ .green-mortgage__date-input {
1754
+ width: 320px;
1755
+ }
1756
+
1757
+ .green-mortgage__date-input.el-date-editor {
1758
+ --el-date-editor-width: 320px;
1759
+ }
1760
+
1761
+ .green-mortgage__th {
1762
+ font-size: 12px;
1763
+ }
1764
+ }
1765
+
1766
+ /* 手機 */
1767
+ @media (max-width: 767px) {
1768
+ .green-mortgage {
1769
+ overflow: auto;
1770
+ }
1771
+
1772
+ .green-mortgage__header {
1773
+ margin-bottom: 10px;
1774
+ }
1775
+
1776
+ .green-mortgage__title,
1777
+ .green-mortgage__subtitle {
1778
+ font-size: 14px;
1779
+ }
1780
+
1781
+ .green-mortgage__meta {
1782
+ font-size: 12px;
1783
+ }
1784
+
1785
+ .green-mortgage__filter-row,
1786
+ .green-mortgage__action-row {
1787
+ flex-direction: column;
1788
+ align-items: stretch;
1789
+ gap: 10px;
1790
+ }
1791
+
1792
+ .green-mortgage__filter-group,
1793
+ .green-mortgage__filter-group--period,
1794
+ .green-mortgage__period-controls,
1795
+ .green-mortgage__search {
1796
+ width: 100%;
1797
+ }
1798
+
1799
+ .green-mortgage__filter-label,
1800
+ .green-mortgage__search-label {
1801
+ white-space: normal;
1802
+ }
1803
+
1804
+ .green-mortgage__rating-select,
1805
+ .green-mortgage__search-input {
1806
+ width: 100%;
1807
+ }
1808
+
1809
+ .green-mortgage__date-input,
1810
+ .green-mortgage__date-input.el-date-editor {
1811
+ width: 320px !important;
1812
+ max-width: 100%;
1813
+ --el-date-editor-width: 320px;
1814
+ }
1815
+
1816
+ .green-mortgage__period-controls {
1817
+ display: flex;
1818
+ flex-direction: column;
1819
+ align-items: stretch;
1820
+ }
1821
+
1822
+ .green-mortgage__period-controls .green-mortgage__refresh-btn {
1823
+ width: 100%;
1824
+ }
1825
+
1826
+ .green-mortgage__export-btn {
1827
+ width: 100%;
1828
+ }
1829
+
1830
+ .green-mortgage__table-shell {
1831
+ min-height: 280px;
1832
+ }
1833
+ }
1834
+ </style>