hy-app 0.6.4 → 0.6.6

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 (106) hide show
  1. package/attributes.json +1 -1
  2. package/components/hy-address-picker/hy-address-picker.vue +249 -249
  3. package/components/hy-address-picker/props.ts +103 -103
  4. package/components/hy-button/hy-button.vue +320 -289
  5. package/components/hy-button/props.ts +143 -143
  6. package/components/hy-button/typing.d.ts +43 -35
  7. package/components/hy-calendar/header.vue +58 -58
  8. package/components/hy-calendar/hy-calendar.vue +8 -6
  9. package/components/hy-calendar/month.vue +402 -402
  10. package/components/hy-calendar/props.ts +169 -169
  11. package/components/hy-calendar/typing.d.ts +47 -45
  12. package/components/hy-cell-item/hy-cell-item.vue +161 -161
  13. package/components/hy-cell-item/props.ts +59 -59
  14. package/components/hy-check-button/hy-check-button.vue +135 -135
  15. package/components/hy-code-input/hy-code-input.vue +231 -231
  16. package/components/hy-code-input/props.ts +90 -90
  17. package/components/hy-config-provider/hy-config-provider.vue +53 -53
  18. package/components/hy-config-provider/props.ts +30 -30
  19. package/components/hy-coupon/hy-coupon.vue +183 -183
  20. package/components/hy-coupon/props.ts +108 -108
  21. package/components/hy-datetime-picker/hy-datetime-picker.vue +41 -55
  22. package/components/hy-datetime-picker/props.ts +144 -144
  23. package/components/hy-datetime-picker/typing.d.ts +2 -0
  24. package/components/hy-divider/props.ts +83 -83
  25. package/components/hy-empty/icon.ts +72 -72
  26. package/components/hy-folding-panel/hy-folding-panel-group.vue +162 -162
  27. package/components/hy-form/hy-form.vue +220 -220
  28. package/components/hy-icon/hy-icon.vue +112 -112
  29. package/components/hy-index-bar/hy-index-bar.vue +185 -185
  30. package/components/hy-index-bar/index.scss +64 -64
  31. package/components/hy-index-bar/props.ts +94 -94
  32. package/components/hy-index-bar/typing.d.ts +36 -36
  33. package/components/hy-input/hy-input.vue +333 -333
  34. package/components/hy-input/props.ts +186 -186
  35. package/components/hy-modal/hy-modal.vue +211 -211
  36. package/components/hy-modal/props.ts +94 -94
  37. package/components/hy-modal/typing.d.ts +16 -16
  38. package/components/hy-notice-bar/hy-row-notice.vue +121 -121
  39. package/components/hy-notify/hy-notify.vue +174 -174
  40. package/components/hy-number-step/hy-number-step.vue +367 -367
  41. package/components/hy-overlay/hy-overlay.vue +61 -61
  42. package/components/hy-overlay/props.ts +38 -38
  43. package/components/hy-pagination/hy-pagination.vue +136 -136
  44. package/components/hy-pagination/props.ts +58 -58
  45. package/components/hy-parse/hy-parse.vue +550 -550
  46. package/components/hy-parse/node/node.vue +781 -781
  47. package/components/hy-parse/parser.js +1455 -1455
  48. package/components/hy-parse/props.ts +19 -19
  49. package/components/hy-parse/typing.d.ts +68 -68
  50. package/components/hy-picker/hy-picker.vue +435 -435
  51. package/components/hy-picker/props.ts +122 -122
  52. package/components/hy-picker/typing.d.ts +38 -38
  53. package/components/hy-qrcode/props.ts +72 -72
  54. package/components/hy-qrcode/qrcode.js.bak +1433 -1433
  55. package/components/hy-radio/props.ts +97 -97
  56. package/components/hy-read-more/props.ts +48 -48
  57. package/components/hy-search/props.ts +133 -133
  58. package/components/hy-signature/canvasHelper.ts +51 -51
  59. package/components/hy-signature/props.ts +121 -121
  60. package/components/hy-skeleton/hy-skeleton.vue +142 -142
  61. package/components/hy-skeleton/props.ts +46 -46
  62. package/components/hy-skeleton/typing.d.ts +31 -31
  63. package/components/hy-steps/hy-steps.vue +275 -275
  64. package/components/hy-steps/typing.d.ts +25 -25
  65. package/components/hy-swiper/hy-swiper.vue +3 -3
  66. package/components/hy-swiper/index.scss +5 -5
  67. package/components/hy-swiper/props.ts +0 -1
  68. package/components/hy-table/hy-table.vue +630 -630
  69. package/components/hy-table/props.ts +62 -62
  70. package/components/hy-table/typing.d.ts +29 -29
  71. package/components/hy-tabs/hy-tabs.vue +336 -335
  72. package/components/hy-tabs/props.ts +84 -77
  73. package/components/hy-tag/hy-tag.vue +173 -173
  74. package/components/hy-tag/props.ts +89 -89
  75. package/components/hy-text/hy-text.vue +237 -237
  76. package/components/hy-text/props.ts +115 -115
  77. package/components/hy-textarea/hy-textarea.vue +198 -198
  78. package/components/hy-toast/hy-toast.vue +200 -200
  79. package/components/hy-toast/props.ts +3 -3
  80. package/components/hy-transition/hy-transition.vue +157 -157
  81. package/components/hy-transition/props.ts +32 -32
  82. package/components/hy-upload/hy-upload.vue +384 -384
  83. package/components/hy-watermark/hy-watermark.vue +1058 -1058
  84. package/components/hy-watermark/props.ts +109 -109
  85. package/global.d.ts +94 -94
  86. package/libs/api/http.ts +119 -119
  87. package/libs/composables/index.ts +8 -8
  88. package/libs/composables/useMessage.ts +149 -149
  89. package/libs/composables/useToast.ts +45 -45
  90. package/libs/composables/useTranslate.ts +10 -10
  91. package/libs/css/_config.scss +5 -5
  92. package/libs/index.ts +8 -8
  93. package/libs/locale/index.ts +32 -32
  94. package/libs/locale/lang/en-US.ts +84 -84
  95. package/libs/locale/lang/zh-CN.ts +87 -87
  96. package/libs/typing/index.ts +2 -2
  97. package/libs/typing/modules/common.d.ts +139 -139
  98. package/libs/typing/modules/form.ts +176 -176
  99. package/libs/typing/modules/http.d.ts +19 -19
  100. package/libs/typing/modules/index.d.ts +12 -12
  101. package/libs/utils/inside.ts +340 -340
  102. package/libs/utils/inspect.ts +140 -140
  103. package/libs/utils/utils.ts +525 -525
  104. package/package.json +81 -81
  105. package/tags.json +1 -1
  106. package/web-types.json +1 -1
@@ -1,630 +1,630 @@
1
- <template>
2
- <view class="hy-table" :style="{ height: addUnit(containerHeight) }">
3
- <!-- 列头 -->
4
- <view class="hy-table__header" v-if="showHeader">
5
- <scroll-view
6
- class="hy-table__header--scroll"
7
- scroll-x
8
- :scroll-left="topScrollLeft"
9
- :scroll-y="false"
10
- lower-threshold="0"
11
- @scroll="onHeaderScroll"
12
- @scrolltolower="isReachBottom = true"
13
- >
14
- <view class="hy-table__header--wrapper" :style="{ width: addUnit(totalWidth) }">
15
- <!-- 左侧固定列头 -->
16
- <view
17
- v-if="leftFixedColumns.length > 0"
18
- :class="[
19
- 'hy-table__header--wrapper__left',
20
- isShadow('left') && 'is-shadow'
21
- ]"
22
- :style="{ width: addUnit(leftFixedWidth), zIndex: 3 }"
23
- >
24
- <view
25
- v-for="(col, colIndex) in leftFixedColumns"
26
- :key="colIndex"
27
- class="hy-table__header--wrapper__cell"
28
- :style="getHeaderCellStyle(col)"
29
- @tap="handleSort(col)"
30
- >
31
- <slot v-if="$slots['left-head']" name="left-head" :col="col">
32
- <text class="header-text">{{ col.title }}</text>
33
- </slot>
34
- <text v-else class="hy-table__header--wrapper__cell--text">{{
35
- col.title
36
- }}</text>
37
- <view
38
- v-if="col.sortable"
39
- class="hy-table__header--wrapper__cell--sortable"
40
- >
41
- <hy-icon
42
- :name="IconConfig.ARROW_UP_FILL"
43
- size="12"
44
- :custom-class="
45
- sortField === col.key && sortOrder === 'asc'
46
- ? 'is-active'
47
- : ''
48
- "
49
- @click.stop="handleSort(col, 'asc')"
50
- ></hy-icon>
51
-
52
- <hy-icon
53
- :name="IconConfig.ARROW_DOWN_FILL"
54
- size="12"
55
- :custom-class="
56
- sortField === col.key && sortOrder === 'desc'
57
- ? 'is-active'
58
- : ''
59
- "
60
- @click.stop="handleSort(col, 'desc')"
61
- ></hy-icon>
62
- </view>
63
- </view>
64
- </view>
65
-
66
- <!-- 中间滚动列头 -->
67
- <view class="hy-table__header--wrapper__center">
68
- <view
69
- v-for="(col, colIndex) in scrollColumns"
70
- :key="colIndex"
71
- class="hy-table__header--wrapper__cell"
72
- :style="getHeaderCellStyle(col)"
73
- @tap="handleSort(col)"
74
- >
75
- <slot v-if="$slots.head" name="head" :col="col"></slot>
76
- <text v-else class="hy-table__header--wrapper__cell--text">{{
77
- col.title
78
- }}</text>
79
- <view
80
- v-if="col.sortable"
81
- class="hy-table__header--wrapper__cell--sortable"
82
- >
83
- <hy-icon
84
- :name="IconConfig.ARROW_UP_FILL"
85
- size="12"
86
- :custom-class="
87
- sortField === col.key && sortOrder === 'asc'
88
- ? 'is-active'
89
- : ''
90
- "
91
- @click.stop="handleSort(col, 'asc')"
92
- ></hy-icon>
93
-
94
- <hy-icon
95
- :name="IconConfig.ARROW_DOWN_FILL"
96
- size="12"
97
- :custom-class="
98
- sortField === col.key && sortOrder === 'desc'
99
- ? 'is-active'
100
- : ''
101
- "
102
- @click.stop="handleSort(col, 'desc')"
103
- ></hy-icon>
104
- </view>
105
- </view>
106
- </view>
107
-
108
- <!-- 右侧固定列头 -->
109
- <view
110
- v-if="rightFixedColumns.length > 0"
111
- :class="[
112
- 'hy-table__header--wrapper__right',
113
- isShadow('right') && 'is-shadow'
114
- ]"
115
- :style="{ width: addUnit(rightFixedWidth), zIndex: 3 }"
116
- >
117
- <view
118
- v-for="(col, colIndex) in rightFixedColumns"
119
- :key="colIndex"
120
- class="hy-table__header--wrapper__cell"
121
- :style="getHeaderCellStyle(col)"
122
- @tap="handleSort(col)"
123
- >
124
- <slot v-if="$slots['right-head']" name="right-head" :col="col"></slot>
125
- <text v-else class="hy-table__header--wrapper__cell--text">{{
126
- col.title
127
- }}</text>
128
- <view
129
- v-if="col.sortable"
130
- class="hy-table__header--wrapper__cell--sortable"
131
- >
132
- <hy-icon
133
- :name="IconConfig.ARROW_UP_FILL"
134
- size="12"
135
- :custom-class="
136
- sortField === col.key && sortOrder === 'asc'
137
- ? 'is-active'
138
- : ''
139
- "
140
- @click.stop="handleSort(col, 'asc')"
141
- ></hy-icon>
142
-
143
- <hy-icon
144
- :name="IconConfig.ARROW_DOWN_FILL"
145
- size="12"
146
- :custom-class="
147
- sortField === col.key && sortOrder === 'desc'
148
- ? 'is-active'
149
- : ''
150
- "
151
- @click.stop="handleSort(col, 'desc')"
152
- ></hy-icon>
153
- </view>
154
- </view>
155
- </view>
156
- </view>
157
- </scroll-view>
158
- </view>
159
-
160
- <!-- 表格主体 -->
161
- <view class="hy-table__body">
162
- <view v-if="loading" class="hy-table__body--loading">
163
- <hy-loading text="加载中..." mode="circle"></hy-loading>
164
- </view>
165
- <view v-if="!data.length" class="hy-table__body--empty">
166
- <slot v-if="$slots.empty" name="empty"></slot>
167
- <hy-empty v-else :image-url="emptyUrl" :description="emptyDes"></hy-empty>
168
- </view>
169
- <!-- 左侧固定列 -->
170
- <scroll-view
171
- v-if="processedData.length"
172
- :class="['hy-table__body--left', isShadow('left') && 'is-shadow']"
173
- scroll-y
174
- :scroll-top="leftScrollTop"
175
- :style="{ width: addUnit(leftFixedWidth), height: addUnit(bodyHeight) }"
176
- @scroll="onLeftScroll"
177
- >
178
- <view class="hy-table__body--content">
179
- <view
180
- v-for="(row, rowIndex) in processedData"
181
- :key="rowIndex"
182
- :class="rowClass(rowIndex)"
183
- :style="getRowStyle(rowIndex)"
184
- >
185
- <view
186
- v-for="(col, colIndex) in leftFixedColumns"
187
- :key="colIndex"
188
- class="hy-table__body--content__row--cell"
189
- :style="getBodyCellStyle(col, rowIndex)"
190
- @tap="onCellClick(row, rowIndex)"
191
- >
192
- <slot
193
- v-if="$slots.left"
194
- name="left"
195
- :row="row"
196
- :col="col"
197
- :index="rowIndex"
198
- ></slot>
199
- <text
200
- v-else
201
- class="hy-table__body--content__row--cell__text"
202
- :class="{ 'is-ellipsis': col.ellipsis }"
203
- >
204
- {{ getCellValue(row, col) }}
205
- </text>
206
- </view>
207
- </view>
208
- </view>
209
- </scroll-view>
210
-
211
- <!-- 中间滚动区域 -->
212
- <scroll-view
213
- v-if="processedData.length"
214
- class="hy-table__body--center"
215
- scroll-y
216
- :scroll-top="centerScrollTop"
217
- @scroll="onScroll"
218
- :style="{
219
- width: `calc(100% - ${leftFixedWidth + rightFixedWidth}px)`,
220
- height: addUnit(bodyHeight),
221
- left: addUnit(leftFixedWidth),
222
- right: addUnit(rightFixedWidth)
223
- }"
224
- >
225
- <scroll-view
226
- scroll-x
227
- :scroll-y="false"
228
- :scroll-left="centerScrollLeft"
229
- class="hy-table__body--content"
230
- lower-threshold="5"
231
- @scroll="onCrosswiseScroll"
232
- @scrolltolower="isReachBottom = true"
233
- >
234
- <view
235
- v-for="(row, rowIndex) in processedData"
236
- :key="rowIndex"
237
- :class="rowClass(rowIndex)"
238
- :style="getRowStyle(rowIndex)"
239
- >
240
- <view
241
- v-for="(col, colIndex) in scrollColumns"
242
- :key="colIndex"
243
- class="hy-table__body--content__row--cell"
244
- :class="{ ellipsis: col.ellipsis }"
245
- :style="getBodyCellStyle(col, rowIndex)"
246
- @tap="onCellClick(row, rowIndex)"
247
- >
248
- <slot
249
- v-if="$slots.default"
250
- name="default"
251
- :row="row"
252
- :col="col"
253
- :index="rowIndex"
254
- ></slot>
255
- <text
256
- v-else
257
- class="hy-table__body--content__row--cell__text"
258
- :class="{ 'is-ellipsis': col.ellipsis }"
259
- >
260
- {{ getCellValue(row, col) }}
261
- </text>
262
- </view>
263
- </view>
264
- </scroll-view>
265
- </scroll-view>
266
-
267
- <!-- 右侧固定列 -->
268
- <scroll-view
269
- v-if="processedData.length"
270
- :class="['hy-table__body--right', isShadow('right') && 'is-shadow']"
271
- scroll-y
272
- :scroll-top="rightScrollTop"
273
- :style="{
274
- width: addUnit(rightFixedWidth),
275
- height: addUnit(bodyHeight),
276
- right: '0'
277
- }"
278
- @scroll="onRightScroll"
279
- >
280
- <view class="hy-table__body--content">
281
- <view
282
- v-for="(row, rowIndex) in processedData"
283
- :key="rowIndex"
284
- :class="rowClass(rowIndex)"
285
- :style="getRowStyle(rowIndex)"
286
- >
287
- <view
288
- v-for="(col, colIndex) in rightFixedColumns"
289
- :key="colIndex"
290
- class="hy-table__body--content__row--cell"
291
- :style="getBodyCellStyle(col, rowIndex)"
292
- @tap="onCellClick(row, rowIndex)"
293
- >
294
- <slot
295
- v-if="$slots.right"
296
- name="right"
297
- :row="row"
298
- :col="col"
299
- :index="rowIndex"
300
- ></slot>
301
- <text
302
- v-else
303
- class="hy-table__body--content__row--cell__text"
304
- :class="{ 'is-ellipsis': col.ellipsis }"
305
- >
306
- {{ getCellValue(row, col) }}
307
- </text>
308
- </view>
309
- </view>
310
- </view>
311
- </scroll-view>
312
- </view>
313
- </view>
314
- </template>
315
-
316
- <script lang="ts">
317
- export default {
318
- name: 'hy-tabs',
319
- options: {
320
- addGlobalClass: true,
321
- virtualHost: true,
322
- styleIsolation: 'shared'
323
- }
324
- }
325
- </script>
326
-
327
- <script setup lang="ts">
328
- import { ref, computed, watch, onMounted } from 'vue'
329
- import { addUnit, IconConfig, getPx } from '../../libs'
330
- import type { ITableColumn, ITableEmits } from './typing'
331
- import tableProps from './props'
332
- // 组件
333
- import HyIcon from '../hy-icon/hy-icon.vue'
334
- import HyEmpty from '../hy-empty/hy-empty.vue'
335
- import HyLoading from '../hy-loading/hy-loading.vue'
336
-
337
- /**
338
- * Table是一个基于Uniapp开发的高性能表格组件,支持固定列、排序、斑马纹、自定义插槽等功能,适用于各种数据展示场景。
339
- * @displayName hy-table
340
- */
341
- defineOptions({})
342
-
343
- const props = defineProps(tableProps)
344
- const emit = defineEmits<ITableEmits>()
345
-
346
- // 响应式数据
347
- const rowHeights = ref<number[]>([])
348
- const sortField = ref<string>('')
349
- const sortOrder = ref<'asc' | 'desc'>('asc')
350
- const SLEEP_TIME = 60
351
- // 防止滚动循环触发和抖动的标志位
352
- const activeScroller = ref('')
353
- const isSyncing = ref(false)
354
- const leftScrollTop = ref(0)
355
- const centerScrollTop = ref(0)
356
- const rightScrollTop = ref(0)
357
- const centerScrollLeft = ref(0)
358
- const topScrollLeft = ref(0)
359
- const isReachBottom = ref(false)
360
-
361
- // 计算属性
362
- const leftFixedColumns = computed(() => props.columns.filter((col) => col.fixed === 'left'))
363
-
364
- const rightFixedColumns = computed(() => props.columns.filter((col) => col.fixed === 'right'))
365
-
366
- const scrollColumns = computed(() => props.columns.filter((col) => !col?.fixed))
367
-
368
- const leftFixedWidth = computed(() =>
369
- leftFixedColumns.value.reduce((sum, col) => sum + col.width, 0)
370
- )
371
-
372
- const rightFixedWidth = computed(() =>
373
- rightFixedColumns.value.reduce((sum, col) => sum + col.width, 0)
374
- )
375
-
376
- const scrollWidth = computed(() => scrollColumns.value.reduce((sum, col) => sum + col.width, 0))
377
-
378
- const totalWidth = computed(() => leftFixedWidth.value + scrollWidth.value + rightFixedWidth.value)
379
- // 表格高度
380
- const containerHeight = computed(() => props.height)
381
- // 表格高度
382
- const bodyHeight = computed(() => {
383
- // 减去表头高度
384
- return props.showHeader ? getPx(props.height) - 50 : props.height
385
- })
386
-
387
- const isShadow = computed(() => {
388
- return (type: 'left' | 'right') => {
389
- if (type === 'left') {
390
- return topScrollLeft.value !== 0 || centerScrollLeft.value !== 0
391
- } else {
392
- return !isReachBottom.value
393
- }
394
- }
395
- })
396
-
397
- const processedData = computed(() => {
398
- let data = [...props.data]
399
-
400
- if (sortField.value) {
401
- data.sort((a, b) => {
402
- const aVal = a[sortField.value]
403
- const bVal = b[sortField.value]
404
-
405
- if (aVal === bVal) return 0
406
- if (sortOrder.value === 'asc') {
407
- return aVal > bVal ? 1 : -1
408
- } else {
409
- return aVal < bVal ? 1 : -1
410
- }
411
- })
412
- }
413
-
414
- return data
415
- })
416
-
417
- onMounted(() => {})
418
-
419
- // 方法
420
- const getHeaderCellStyle = (col: ITableColumn) => {
421
- return {
422
- width: addUnit(col.width),
423
- textAlign: col.align || 'left'
424
- }
425
- }
426
-
427
- const getBodyCellStyle = (col: ITableColumn, rowIndex: number) => {
428
- const rowHeight = rowHeights.value[rowIndex] || props.rowHeight
429
- return {
430
- width: addUnit(col.width),
431
- textAlign: col.align || 'left',
432
- height: addUnit(rowHeight)
433
- }
434
- }
435
-
436
- /**
437
- * 行类名
438
- * */
439
- const rowClass = computed(() => {
440
- return (rowIndex: number) => {
441
- const classes = ['hy-table__body--content__row']
442
-
443
- if (props.stripe && rowIndex % 2 === 1) {
444
- classes.push('is-stripe')
445
- }
446
-
447
- return classes
448
- }
449
- })
450
- /**
451
- * 行样式
452
- * */
453
- const getRowStyle = computed(() => {
454
- return (rowIndex: number) => {
455
- const rowHeight = rowHeights.value[rowIndex] || props.rowHeight
456
- const styles: any = {
457
- width: addUnit(scrollWidth.value),
458
- height: addUnit(rowHeight)
459
- }
460
-
461
- return styles
462
- }
463
- })
464
-
465
- const getCellValue = (row: any, col: ITableColumn) => {
466
- if (col.formatter) {
467
- return col.formatter(row[col.key], row)
468
- }
469
- return row[col.key] || ''
470
- }
471
-
472
- /**
473
- * 头部横向滚动
474
- * */
475
- const onHeaderScroll = (e: any) => {
476
- if (activeScroller.value && activeScroller.value !== 'top') return
477
- syncScroll('top', {
478
- scrollLeft: e.detail.scrollLeft
479
- })
480
- }
481
-
482
- /**
483
- * 中间内容竖直滚动
484
- * */
485
- const onScroll = (e: any) => {
486
- if (activeScroller.value && activeScroller.value !== 'center') return
487
- syncScroll('center', {
488
- scrollTop: e.detail.scrollTop
489
- })
490
- }
491
-
492
- /**
493
- * 中间内容横向滚动
494
- * */
495
- const onCrosswiseScroll = (e: any) => {
496
- if (activeScroller.value && activeScroller.value !== 'bottom') return
497
- syncScroll('bottom', {
498
- scrollLeft: e.detail.scrollLeft
499
- })
500
- }
501
-
502
- /**
503
- * 左侧列表竖向滚动
504
- * */
505
- const onLeftScroll = (e: any) => {
506
- if (activeScroller.value && activeScroller.value !== 'left') return
507
- syncScroll('left', {
508
- scrollTop: e.detail.scrollTop
509
- })
510
- }
511
-
512
- /**
513
- * 右侧列表竖向滚动
514
- * */
515
- const onRightScroll = (e: any) => {
516
- if (activeScroller.value && activeScroller.value != 'right') return
517
- syncScroll('right', {
518
- scrollTop: e.detail.scrollTop
519
- })
520
- }
521
-
522
- /**
523
- * 点击排序
524
- * @param col 列标题
525
- * @param sort 排序
526
- * */
527
- const handleSort = (col: ITableColumn, sort?: 'asc' | 'desc') => {
528
- if (!col.sortable) return
529
-
530
- if (sort) {
531
- sortOrder.value = sort
532
- sortField.value = col.key
533
- } else {
534
- if (sortField.value === col.key) {
535
- sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
536
- } else {
537
- sortField.value = col.key
538
- sortOrder.value = 'asc'
539
- }
540
- }
541
-
542
- emit('sort-change', sortField.value, sortOrder.value)
543
- }
544
-
545
- /**
546
- * 点击单元格
547
- * @param row 行数据
548
- * @param index 第几列
549
- * */
550
- const onCellClick = (row: AnyObject, index: number) => {
551
- emit('row-click', row, index)
552
- }
553
-
554
- // 初始化行高
555
- onMounted(() => {
556
- rowHeights.value = new Array(props.data.length).fill(props.rowHeight)
557
- })
558
-
559
- // 监听数据变化
560
- watch(
561
- () => props.data,
562
- () => {
563
- rowHeights.value = new Array(props.data.length).fill(props.rowHeight)
564
- },
565
- { deep: true }
566
- )
567
-
568
- /**
569
- * 通用滚动同步函数
570
- * @param source 哪组滚动数据
571
- * @param payload 滚动实例
572
- * */
573
- const syncScroll = (source: string, payload: { scrollTop?: number; scrollLeft?: number }) => {
574
- if (isSyncing.value) return
575
-
576
- isSyncing.value = true
577
- activeScroller.value = source
578
-
579
- if (payload.scrollTop !== undefined) {
580
- if (activeScroller.value !== 'left') leftScrollTop.value = payload.scrollTop
581
- if (activeScroller.value !== 'center') centerScrollTop.value = payload.scrollTop
582
- if (activeScroller.value !== 'right') rightScrollTop.value = payload.scrollTop
583
- }
584
-
585
- if (payload.scrollLeft !== undefined) {
586
- isReachBottom.value = false
587
- // centerScrollLeft.value = payload.scrollLeft
588
- if (activeScroller.value !== 'top') topScrollLeft.value = payload.scrollLeft
589
- if (activeScroller.value !== 'bottom') centerScrollLeft.value = payload.scrollLeft
590
- }
591
-
592
- // 下一帧解锁
593
- nextFrame(() => {
594
- isSyncing.value = false
595
- activeScroller.value = ''
596
- })
597
- }
598
-
599
- const nextFrame = (cb: () => void) => {
600
- if (typeof requestAnimationFrame !== 'undefined') {
601
- requestAnimationFrame(cb)
602
- } else {
603
- setTimeout(cb, SLEEP_TIME)
604
- }
605
- }
606
- </script>
607
-
608
- <style scoped lang="scss">
609
- @import './index.scss';
610
-
611
- .header-text {
612
- white-space: nowrap;
613
- }
614
-
615
- .sort-icons {
616
- display: flex;
617
- flex-direction: column;
618
- margin-left: 4px;
619
- }
620
-
621
- .sort-icon {
622
- font-size: 10px;
623
- color: #ccc;
624
- line-height: 0.8;
625
- }
626
-
627
- .sort-icon.active {
628
- color: #1890ff;
629
- }
630
- </style>
1
+ <template>
2
+ <view class="hy-table" :style="{ height: addUnit(containerHeight) }">
3
+ <!-- 列头 -->
4
+ <view class="hy-table__header" v-if="showHeader">
5
+ <scroll-view
6
+ class="hy-table__header--scroll"
7
+ scroll-x
8
+ :scroll-left="topScrollLeft"
9
+ :scroll-y="false"
10
+ lower-threshold="0"
11
+ @scroll="onHeaderScroll"
12
+ @scrolltolower="isReachBottom = true"
13
+ >
14
+ <view class="hy-table__header--wrapper" :style="{ width: addUnit(totalWidth) }">
15
+ <!-- 左侧固定列头 -->
16
+ <view
17
+ v-if="leftFixedColumns.length > 0"
18
+ :class="[
19
+ 'hy-table__header--wrapper__left',
20
+ isShadow('left') && 'is-shadow'
21
+ ]"
22
+ :style="{ width: addUnit(leftFixedWidth), zIndex: 3 }"
23
+ >
24
+ <view
25
+ v-for="(col, colIndex) in leftFixedColumns"
26
+ :key="colIndex"
27
+ class="hy-table__header--wrapper__cell"
28
+ :style="getHeaderCellStyle(col)"
29
+ @tap="handleSort(col)"
30
+ >
31
+ <slot v-if="$slots['left-head']" name="left-head" :col="col">
32
+ <text class="header-text">{{ col.title }}</text>
33
+ </slot>
34
+ <text v-else class="hy-table__header--wrapper__cell--text">{{
35
+ col.title
36
+ }}</text>
37
+ <view
38
+ v-if="col.sortable"
39
+ class="hy-table__header--wrapper__cell--sortable"
40
+ >
41
+ <hy-icon
42
+ :name="IconConfig.ARROW_UP_FILL"
43
+ size="12"
44
+ :custom-class="
45
+ sortField === col.key && sortOrder === 'asc'
46
+ ? 'is-active'
47
+ : ''
48
+ "
49
+ @click.stop="handleSort(col, 'asc')"
50
+ ></hy-icon>
51
+
52
+ <hy-icon
53
+ :name="IconConfig.ARROW_DOWN_FILL"
54
+ size="12"
55
+ :custom-class="
56
+ sortField === col.key && sortOrder === 'desc'
57
+ ? 'is-active'
58
+ : ''
59
+ "
60
+ @click.stop="handleSort(col, 'desc')"
61
+ ></hy-icon>
62
+ </view>
63
+ </view>
64
+ </view>
65
+
66
+ <!-- 中间滚动列头 -->
67
+ <view class="hy-table__header--wrapper__center">
68
+ <view
69
+ v-for="(col, colIndex) in scrollColumns"
70
+ :key="colIndex"
71
+ class="hy-table__header--wrapper__cell"
72
+ :style="getHeaderCellStyle(col)"
73
+ @tap="handleSort(col)"
74
+ >
75
+ <slot v-if="$slots.head" name="head" :col="col"></slot>
76
+ <text v-else class="hy-table__header--wrapper__cell--text">{{
77
+ col.title
78
+ }}</text>
79
+ <view
80
+ v-if="col.sortable"
81
+ class="hy-table__header--wrapper__cell--sortable"
82
+ >
83
+ <hy-icon
84
+ :name="IconConfig.ARROW_UP_FILL"
85
+ size="12"
86
+ :custom-class="
87
+ sortField === col.key && sortOrder === 'asc'
88
+ ? 'is-active'
89
+ : ''
90
+ "
91
+ @click.stop="handleSort(col, 'asc')"
92
+ ></hy-icon>
93
+
94
+ <hy-icon
95
+ :name="IconConfig.ARROW_DOWN_FILL"
96
+ size="12"
97
+ :custom-class="
98
+ sortField === col.key && sortOrder === 'desc'
99
+ ? 'is-active'
100
+ : ''
101
+ "
102
+ @click.stop="handleSort(col, 'desc')"
103
+ ></hy-icon>
104
+ </view>
105
+ </view>
106
+ </view>
107
+
108
+ <!-- 右侧固定列头 -->
109
+ <view
110
+ v-if="rightFixedColumns.length > 0"
111
+ :class="[
112
+ 'hy-table__header--wrapper__right',
113
+ isShadow('right') && 'is-shadow'
114
+ ]"
115
+ :style="{ width: addUnit(rightFixedWidth), zIndex: 3 }"
116
+ >
117
+ <view
118
+ v-for="(col, colIndex) in rightFixedColumns"
119
+ :key="colIndex"
120
+ class="hy-table__header--wrapper__cell"
121
+ :style="getHeaderCellStyle(col)"
122
+ @tap="handleSort(col)"
123
+ >
124
+ <slot v-if="$slots['right-head']" name="right-head" :col="col"></slot>
125
+ <text v-else class="hy-table__header--wrapper__cell--text">{{
126
+ col.title
127
+ }}</text>
128
+ <view
129
+ v-if="col.sortable"
130
+ class="hy-table__header--wrapper__cell--sortable"
131
+ >
132
+ <hy-icon
133
+ :name="IconConfig.ARROW_UP_FILL"
134
+ size="12"
135
+ :custom-class="
136
+ sortField === col.key && sortOrder === 'asc'
137
+ ? 'is-active'
138
+ : ''
139
+ "
140
+ @click.stop="handleSort(col, 'asc')"
141
+ ></hy-icon>
142
+
143
+ <hy-icon
144
+ :name="IconConfig.ARROW_DOWN_FILL"
145
+ size="12"
146
+ :custom-class="
147
+ sortField === col.key && sortOrder === 'desc'
148
+ ? 'is-active'
149
+ : ''
150
+ "
151
+ @click.stop="handleSort(col, 'desc')"
152
+ ></hy-icon>
153
+ </view>
154
+ </view>
155
+ </view>
156
+ </view>
157
+ </scroll-view>
158
+ </view>
159
+
160
+ <!-- 表格主体 -->
161
+ <view class="hy-table__body">
162
+ <view v-if="loading" class="hy-table__body--loading">
163
+ <hy-loading text="加载中..." mode="circle"></hy-loading>
164
+ </view>
165
+ <view v-if="!data.length" class="hy-table__body--empty">
166
+ <slot v-if="$slots.empty" name="empty"></slot>
167
+ <hy-empty v-else :image-url="emptyUrl" :description="emptyDes"></hy-empty>
168
+ </view>
169
+ <!-- 左侧固定列 -->
170
+ <scroll-view
171
+ v-if="processedData.length"
172
+ :class="['hy-table__body--left', isShadow('left') && 'is-shadow']"
173
+ scroll-y
174
+ :scroll-top="leftScrollTop"
175
+ :style="{ width: addUnit(leftFixedWidth), height: addUnit(bodyHeight) }"
176
+ @scroll="onLeftScroll"
177
+ >
178
+ <view class="hy-table__body--content">
179
+ <view
180
+ v-for="(row, rowIndex) in processedData"
181
+ :key="rowIndex"
182
+ :class="rowClass(rowIndex)"
183
+ :style="getRowStyle(rowIndex)"
184
+ >
185
+ <view
186
+ v-for="(col, colIndex) in leftFixedColumns"
187
+ :key="colIndex"
188
+ class="hy-table__body--content__row--cell"
189
+ :style="getBodyCellStyle(col, rowIndex)"
190
+ @tap="onCellClick(row, rowIndex)"
191
+ >
192
+ <slot
193
+ v-if="$slots.left"
194
+ name="left"
195
+ :row="row"
196
+ :col="col"
197
+ :index="rowIndex"
198
+ ></slot>
199
+ <text
200
+ v-else
201
+ class="hy-table__body--content__row--cell__text"
202
+ :class="{ 'is-ellipsis': col.ellipsis }"
203
+ >
204
+ {{ getCellValue(row, col) }}
205
+ </text>
206
+ </view>
207
+ </view>
208
+ </view>
209
+ </scroll-view>
210
+
211
+ <!-- 中间滚动区域 -->
212
+ <scroll-view
213
+ v-if="processedData.length"
214
+ class="hy-table__body--center"
215
+ scroll-y
216
+ :scroll-top="centerScrollTop"
217
+ @scroll="onScroll"
218
+ :style="{
219
+ width: `calc(100% - ${leftFixedWidth + rightFixedWidth}px)`,
220
+ height: addUnit(bodyHeight),
221
+ left: addUnit(leftFixedWidth),
222
+ right: addUnit(rightFixedWidth)
223
+ }"
224
+ >
225
+ <scroll-view
226
+ scroll-x
227
+ :scroll-y="false"
228
+ :scroll-left="centerScrollLeft"
229
+ class="hy-table__body--content"
230
+ lower-threshold="5"
231
+ @scroll="onCrosswiseScroll"
232
+ @scrolltolower="isReachBottom = true"
233
+ >
234
+ <view
235
+ v-for="(row, rowIndex) in processedData"
236
+ :key="rowIndex"
237
+ :class="rowClass(rowIndex)"
238
+ :style="getRowStyle(rowIndex)"
239
+ >
240
+ <view
241
+ v-for="(col, colIndex) in scrollColumns"
242
+ :key="colIndex"
243
+ class="hy-table__body--content__row--cell"
244
+ :class="{ ellipsis: col.ellipsis }"
245
+ :style="getBodyCellStyle(col, rowIndex)"
246
+ @tap="onCellClick(row, rowIndex)"
247
+ >
248
+ <slot
249
+ v-if="$slots.default"
250
+ name="default"
251
+ :row="row"
252
+ :col="col"
253
+ :index="rowIndex"
254
+ ></slot>
255
+ <text
256
+ v-else
257
+ class="hy-table__body--content__row--cell__text"
258
+ :class="{ 'is-ellipsis': col.ellipsis }"
259
+ >
260
+ {{ getCellValue(row, col) }}
261
+ </text>
262
+ </view>
263
+ </view>
264
+ </scroll-view>
265
+ </scroll-view>
266
+
267
+ <!-- 右侧固定列 -->
268
+ <scroll-view
269
+ v-if="processedData.length"
270
+ :class="['hy-table__body--right', isShadow('right') && 'is-shadow']"
271
+ scroll-y
272
+ :scroll-top="rightScrollTop"
273
+ :style="{
274
+ width: addUnit(rightFixedWidth),
275
+ height: addUnit(bodyHeight),
276
+ right: '0'
277
+ }"
278
+ @scroll="onRightScroll"
279
+ >
280
+ <view class="hy-table__body--content">
281
+ <view
282
+ v-for="(row, rowIndex) in processedData"
283
+ :key="rowIndex"
284
+ :class="rowClass(rowIndex)"
285
+ :style="getRowStyle(rowIndex)"
286
+ >
287
+ <view
288
+ v-for="(col, colIndex) in rightFixedColumns"
289
+ :key="colIndex"
290
+ class="hy-table__body--content__row--cell"
291
+ :style="getBodyCellStyle(col, rowIndex)"
292
+ @tap="onCellClick(row, rowIndex)"
293
+ >
294
+ <slot
295
+ v-if="$slots.right"
296
+ name="right"
297
+ :row="row"
298
+ :col="col"
299
+ :index="rowIndex"
300
+ ></slot>
301
+ <text
302
+ v-else
303
+ class="hy-table__body--content__row--cell__text"
304
+ :class="{ 'is-ellipsis': col.ellipsis }"
305
+ >
306
+ {{ getCellValue(row, col) }}
307
+ </text>
308
+ </view>
309
+ </view>
310
+ </view>
311
+ </scroll-view>
312
+ </view>
313
+ </view>
314
+ </template>
315
+
316
+ <script lang="ts">
317
+ export default {
318
+ name: 'hy-tabs',
319
+ options: {
320
+ addGlobalClass: true,
321
+ virtualHost: true,
322
+ styleIsolation: 'shared'
323
+ }
324
+ }
325
+ </script>
326
+
327
+ <script setup lang="ts">
328
+ import { ref, computed, watch, onMounted } from 'vue'
329
+ import { addUnit, IconConfig, getPx } from '../../libs'
330
+ import type { ITableColumn, ITableEmits } from './typing'
331
+ import tableProps from './props'
332
+ // 组件
333
+ import HyIcon from '../hy-icon/hy-icon.vue'
334
+ import HyEmpty from '../hy-empty/hy-empty.vue'
335
+ import HyLoading from '../hy-loading/hy-loading.vue'
336
+
337
+ /**
338
+ * Table是一个基于Uniapp开发的高性能表格组件,支持固定列、排序、斑马纹、自定义插槽等功能,适用于各种数据展示场景。
339
+ * @displayName hy-table
340
+ */
341
+ defineOptions({})
342
+
343
+ const props = defineProps(tableProps)
344
+ const emit = defineEmits<ITableEmits>()
345
+
346
+ // 响应式数据
347
+ const rowHeights = ref<number[]>([])
348
+ const sortField = ref<string>('')
349
+ const sortOrder = ref<'asc' | 'desc'>('asc')
350
+ const SLEEP_TIME = 60
351
+ // 防止滚动循环触发和抖动的标志位
352
+ const activeScroller = ref('')
353
+ const isSyncing = ref(false)
354
+ const leftScrollTop = ref(0)
355
+ const centerScrollTop = ref(0)
356
+ const rightScrollTop = ref(0)
357
+ const centerScrollLeft = ref(0)
358
+ const topScrollLeft = ref(0)
359
+ const isReachBottom = ref(false)
360
+
361
+ // 计算属性
362
+ const leftFixedColumns = computed(() => props.columns.filter((col) => col.fixed === 'left'))
363
+
364
+ const rightFixedColumns = computed(() => props.columns.filter((col) => col.fixed === 'right'))
365
+
366
+ const scrollColumns = computed(() => props.columns.filter((col) => !col?.fixed))
367
+
368
+ const leftFixedWidth = computed(() =>
369
+ leftFixedColumns.value.reduce((sum, col) => sum + col.width, 0)
370
+ )
371
+
372
+ const rightFixedWidth = computed(() =>
373
+ rightFixedColumns.value.reduce((sum, col) => sum + col.width, 0)
374
+ )
375
+
376
+ const scrollWidth = computed(() => scrollColumns.value.reduce((sum, col) => sum + col.width, 0))
377
+
378
+ const totalWidth = computed(() => leftFixedWidth.value + scrollWidth.value + rightFixedWidth.value)
379
+ // 表格高度
380
+ const containerHeight = computed(() => props.height)
381
+ // 表格高度
382
+ const bodyHeight = computed(() => {
383
+ // 减去表头高度
384
+ return props.showHeader ? getPx(props.height) - 50 : props.height
385
+ })
386
+
387
+ const isShadow = computed(() => {
388
+ return (type: 'left' | 'right') => {
389
+ if (type === 'left') {
390
+ return topScrollLeft.value !== 0 || centerScrollLeft.value !== 0
391
+ } else {
392
+ return !isReachBottom.value
393
+ }
394
+ }
395
+ })
396
+
397
+ const processedData = computed(() => {
398
+ let data = [...props.data]
399
+
400
+ if (sortField.value) {
401
+ data.sort((a, b) => {
402
+ const aVal = a[sortField.value]
403
+ const bVal = b[sortField.value]
404
+
405
+ if (aVal === bVal) return 0
406
+ if (sortOrder.value === 'asc') {
407
+ return aVal > bVal ? 1 : -1
408
+ } else {
409
+ return aVal < bVal ? 1 : -1
410
+ }
411
+ })
412
+ }
413
+
414
+ return data
415
+ })
416
+
417
+ onMounted(() => {})
418
+
419
+ // 方法
420
+ const getHeaderCellStyle = (col: ITableColumn) => {
421
+ return {
422
+ width: addUnit(col.width),
423
+ textAlign: col.align || 'left'
424
+ }
425
+ }
426
+
427
+ const getBodyCellStyle = (col: ITableColumn, rowIndex: number) => {
428
+ const rowHeight = rowHeights.value[rowIndex] || props.rowHeight
429
+ return {
430
+ width: addUnit(col.width),
431
+ textAlign: col.align || 'left',
432
+ height: addUnit(rowHeight)
433
+ }
434
+ }
435
+
436
+ /**
437
+ * 行类名
438
+ * */
439
+ const rowClass = computed(() => {
440
+ return (rowIndex: number) => {
441
+ const classes = ['hy-table__body--content__row']
442
+
443
+ if (props.stripe && rowIndex % 2 === 1) {
444
+ classes.push('is-stripe')
445
+ }
446
+
447
+ return classes
448
+ }
449
+ })
450
+ /**
451
+ * 行样式
452
+ * */
453
+ const getRowStyle = computed(() => {
454
+ return (rowIndex: number) => {
455
+ const rowHeight = rowHeights.value[rowIndex] || props.rowHeight
456
+ const styles: any = {
457
+ width: addUnit(scrollWidth.value),
458
+ height: addUnit(rowHeight)
459
+ }
460
+
461
+ return styles
462
+ }
463
+ })
464
+
465
+ const getCellValue = (row: any, col: ITableColumn) => {
466
+ if (col.formatter) {
467
+ return col.formatter(row[col.key], row)
468
+ }
469
+ return row[col.key] || ''
470
+ }
471
+
472
+ /**
473
+ * 头部横向滚动
474
+ * */
475
+ const onHeaderScroll = (e: any) => {
476
+ if (activeScroller.value && activeScroller.value !== 'top') return
477
+ syncScroll('top', {
478
+ scrollLeft: e.detail.scrollLeft
479
+ })
480
+ }
481
+
482
+ /**
483
+ * 中间内容竖直滚动
484
+ * */
485
+ const onScroll = (e: any) => {
486
+ if (activeScroller.value && activeScroller.value !== 'center') return
487
+ syncScroll('center', {
488
+ scrollTop: e.detail.scrollTop
489
+ })
490
+ }
491
+
492
+ /**
493
+ * 中间内容横向滚动
494
+ * */
495
+ const onCrosswiseScroll = (e: any) => {
496
+ if (activeScroller.value && activeScroller.value !== 'bottom') return
497
+ syncScroll('bottom', {
498
+ scrollLeft: e.detail.scrollLeft
499
+ })
500
+ }
501
+
502
+ /**
503
+ * 左侧列表竖向滚动
504
+ * */
505
+ const onLeftScroll = (e: any) => {
506
+ if (activeScroller.value && activeScroller.value !== 'left') return
507
+ syncScroll('left', {
508
+ scrollTop: e.detail.scrollTop
509
+ })
510
+ }
511
+
512
+ /**
513
+ * 右侧列表竖向滚动
514
+ * */
515
+ const onRightScroll = (e: any) => {
516
+ if (activeScroller.value && activeScroller.value != 'right') return
517
+ syncScroll('right', {
518
+ scrollTop: e.detail.scrollTop
519
+ })
520
+ }
521
+
522
+ /**
523
+ * 点击排序
524
+ * @param col 列标题
525
+ * @param sort 排序
526
+ * */
527
+ const handleSort = (col: ITableColumn, sort?: 'asc' | 'desc') => {
528
+ if (!col.sortable) return
529
+
530
+ if (sort) {
531
+ sortOrder.value = sort
532
+ sortField.value = col.key
533
+ } else {
534
+ if (sortField.value === col.key) {
535
+ sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
536
+ } else {
537
+ sortField.value = col.key
538
+ sortOrder.value = 'asc'
539
+ }
540
+ }
541
+
542
+ emit('sort-change', sortField.value, sortOrder.value)
543
+ }
544
+
545
+ /**
546
+ * 点击单元格
547
+ * @param row 行数据
548
+ * @param index 第几列
549
+ * */
550
+ const onCellClick = (row: AnyObject, index: number) => {
551
+ emit('row-click', row, index)
552
+ }
553
+
554
+ // 初始化行高
555
+ onMounted(() => {
556
+ rowHeights.value = new Array(props.data.length).fill(props.rowHeight)
557
+ })
558
+
559
+ // 监听数据变化
560
+ watch(
561
+ () => props.data,
562
+ () => {
563
+ rowHeights.value = new Array(props.data.length).fill(props.rowHeight)
564
+ },
565
+ { deep: true }
566
+ )
567
+
568
+ /**
569
+ * 通用滚动同步函数
570
+ * @param source 哪组滚动数据
571
+ * @param payload 滚动实例
572
+ * */
573
+ const syncScroll = (source: string, payload: { scrollTop?: number; scrollLeft?: number }) => {
574
+ if (isSyncing.value) return
575
+
576
+ isSyncing.value = true
577
+ activeScroller.value = source
578
+
579
+ if (payload.scrollTop !== undefined) {
580
+ if (activeScroller.value !== 'left') leftScrollTop.value = payload.scrollTop
581
+ if (activeScroller.value !== 'center') centerScrollTop.value = payload.scrollTop
582
+ if (activeScroller.value !== 'right') rightScrollTop.value = payload.scrollTop
583
+ }
584
+
585
+ if (payload.scrollLeft !== undefined) {
586
+ isReachBottom.value = false
587
+ // centerScrollLeft.value = payload.scrollLeft
588
+ if (activeScroller.value !== 'top') topScrollLeft.value = payload.scrollLeft
589
+ if (activeScroller.value !== 'bottom') centerScrollLeft.value = payload.scrollLeft
590
+ }
591
+
592
+ // 下一帧解锁
593
+ nextFrame(() => {
594
+ isSyncing.value = false
595
+ activeScroller.value = ''
596
+ })
597
+ }
598
+
599
+ const nextFrame = (cb: () => void) => {
600
+ if (typeof requestAnimationFrame !== 'undefined') {
601
+ requestAnimationFrame(cb)
602
+ } else {
603
+ setTimeout(cb, SLEEP_TIME)
604
+ }
605
+ }
606
+ </script>
607
+
608
+ <style scoped lang="scss">
609
+ @import './index.scss';
610
+
611
+ .header-text {
612
+ white-space: nowrap;
613
+ }
614
+
615
+ .sort-icons {
616
+ display: flex;
617
+ flex-direction: column;
618
+ margin-left: 4px;
619
+ }
620
+
621
+ .sort-icon {
622
+ font-size: 10px;
623
+ color: #ccc;
624
+ line-height: 0.8;
625
+ }
626
+
627
+ .sort-icon.active {
628
+ color: #1890ff;
629
+ }
630
+ </style>