@tplc/business 0.7.51 → 0.7.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.7.53](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/compare/v0.7.51...v0.7.53) (2026-01-07)
6
+
7
+
8
+ ### 🚀 Chore | 构建/工程依赖/工具
9
+
10
+ * **release:** 0.7.52 ([ee19331](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/ee193313d84f6116acdc38630ecc010fa29fcc37))
11
+
12
+
13
+ ### ✨ Features | 新功能
14
+
15
+ * **lcb-wrapper-list:** implement pagination support with z-paging and enhance dynamic style handling in lcb-block ([f345e4e](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/f345e4e0d84231db8e72fd55fefebdf01eac3bd6))
16
+
17
+
18
+ ### 🐛 Bug Fixes | Bug 修复
19
+
20
+ * **lcb-area:** set default value for compareValues function to prevent potential errors ([4e5e48a](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/4e5e48a373ba00e491c5cd002da5792e57006aec))
21
+ * **lcb-area:** use nullish coalescing for default flex value in itemStylesCache ([421d71c](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/421d71c428f4a2f3db37169e9d5dd6ccdc1d6d07))
22
+
23
+
24
+ ### ♻️ Code Refactoring | 代码重构
25
+
26
+ * **lcb-area, lcb-button:** optimize actionProps computation and streamline data source selection ([117ce0d](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/117ce0df62ee17b25a341c4dd3b51b90a0f78bd8))
27
+ * **lcb-wrapper-list:** streamline pagination implementation and enhance style handling in lcb-block ([9bb70e5](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/9bb70e58d28cb5575760d6fad0da81dec229beab))
28
+
29
+ ### [0.7.52](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/compare/v0.7.48...v0.7.52) (2026-01-07)
30
+
31
+
32
+ ### ♻️ Code Refactoring | 代码重构
33
+
34
+ * **lcb-area:** update comparison logic to use compareType prop for improved clarity ([525e606](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/525e606ba08288fc1f048a68208575214cf8f274))
35
+
36
+
37
+ ### ✨ Features | 新功能
38
+
39
+ * **lcb-block:** add styleKey to dynamic options and update style handling for improved flexibility ([014469d](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/014469da25766f682cff0d63a932b82504f41d7d))
40
+
41
+
42
+ ### 🚀 Chore | 构建/工程依赖/工具
43
+
44
+ * **release:** 0.7.49 ([173d24d](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/173d24dc7bcdc7c3112db232ac14c3509232cd89))
45
+ * **release:** 0.7.50 ([c6f5712](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/c6f5712c7bf9558a6336a148f15a4ba225d11104))
46
+ * **release:** 0.7.51 ([4acced8](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/4acced89cd89f3105b4797644ad7feefab55aa09))
47
+
48
+
49
+ ### 🐛 Bug Fixes | Bug 修复
50
+
51
+ * **lcb-area:** set default value for compareValues function to prevent potential errors ([4e5e48a](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/commit/4e5e48a373ba00e491c5cd002da5792e57006aec))
52
+
5
53
  ### [0.7.51](https://gitlab888.30jia.com.cn/tourism-front/zero-code-pro/compare/v0.7.50...v0.7.51) (2026-01-06)
6
54
 
7
55
 
@@ -31,7 +31,7 @@ import { LcbAreaProps } from './types'
31
31
  import { getFlexStyle, transformValueUnit } from '@tplc/business/utils/transform'
32
32
  import { get } from 'lodash-es'
33
33
  import { dynamicRequest } from '../../utils/request'
34
- import { getDynamicData } from '../../utils/utils'
34
+ import { resolveActionProps, selectDynamicSource } from '../../utils/componentHelpers'
35
35
  import useDynamicData from '../../hooks/useDynamicData'
36
36
  defineOptions({
37
37
  name: 'LcbArea',
@@ -96,7 +96,7 @@ const itemStylesCache = computed(() => {
96
96
  return {
97
97
  ...commonItemStyle.value,
98
98
  gridColumn: colSpan && isGrid ? `span ${colSpan} / span ${colSpan}` : undefined,
99
- flex: isFlex ? flex || 'auto' : undefined,
99
+ flex: isFlex ? (flex ?? 'auto') : undefined,
100
100
  flexShrink: isFlex ? flexShrink : undefined,
101
101
  width: flex ? '100%' : 'auto',
102
102
  }
@@ -121,24 +121,12 @@ const getData = async () => {
121
121
 
122
122
  /** 处理跳转链接中的动态参数 */
123
123
  const actionProps = computed(() => {
124
- const jumpUrl = props.action?.jumpUrl
125
- if (!jumpUrl) {
126
- return props.action
127
- }
128
-
129
- // 优化: 提前返回,避免不必要的计算
130
- if (props.dynamicActionKey) {
131
- return get(innerDynamicData.value, props.dynamicActionKey)
132
- }
133
-
134
- // 优化: 只在 jumpUrl 存在时才调用 getDynamicData
135
-
136
- return {
137
- ...props.action,
138
- jumpUrl: getDynamicData(jumpUrl, {
139
- store: innerDynamicData.value,
140
- }),
141
- }
124
+ return resolveActionProps({
125
+ dynamicActionKey: props.dynamicActionKey,
126
+ action: props.action as any,
127
+ innerDynamicData: innerDynamicData.value,
128
+ templateStore: innerDynamicData.value,
129
+ })
142
130
  })
143
131
 
144
132
  // 优化: 移除 deep: true,只监听引用变化
@@ -176,7 +164,7 @@ const compareValues = (value = '', compareValue: string, compareType: string) =>
176
164
 
177
165
  // 优化: 缓存数据源
178
166
  const dependDataSource = computed(() => {
179
- return props.keyFromUser ? userStore?.userInfo : innerDynamicData.value
167
+ return selectDynamicSource(props.keyFromUser, userStore?.userInfo, innerDynamicData.value)
180
168
  })
181
169
 
182
170
  const showArea = computed(() => {
@@ -48,24 +48,14 @@ const innerBackgroundColor = computed(() => {
48
48
  })
49
49
 
50
50
  const getInnerStyle = computed(() => {
51
- const innerStyle = props.dynamicStyleOptions?.reduce(
52
- (acc, { styleKey }) => {
53
- if (styleKey) {
54
- const currentOptions = props.dynamicStyleOptions?.[styleKey]
55
- const data = get(
56
- currentOptions?.keyFrom === 'user' ? userStore?.userInfo : innerDynamicData.value,
57
- currentOptions?.dynamicKey || '',
58
- )
59
- if (data) {
60
- acc[styleKey] = data
61
- }
62
- }
63
-
64
- return acc
65
- },
66
- {} as Record<string, string>,
67
- )
68
- return innerStyle
51
+ if (props.dynamicStyleOptions?.dynamicKey) {
52
+ const innerStyle = get(
53
+ props.dynamicStyleOptions?.keyFrom === 'user' ? userStore?.userInfo : innerDynamicData.value,
54
+ props.dynamicStyleOptions?.dynamicKey,
55
+ )
56
+ return innerStyle
57
+ }
58
+ return {}
69
59
  })
70
60
 
71
61
  // 缓存 padding 计算结果,减少 transformValueUnit 调用次数
@@ -57,7 +57,7 @@ export interface LcbBlockProps {
57
57
  | 'bottom-right'
58
58
 
59
59
  // 动态样式集合
60
- dynamicStyleOptions?: DynamicOptions[]
60
+ dynamicStyleOptions?: DynamicOptions
61
61
  dynamicBgImage?: string
62
62
  }
63
63
  export interface LcbBlockInnerProps extends LcbBlockProps {
@@ -11,7 +11,11 @@
11
11
  @avatar="onAvatar"
12
12
  @click="handleClick"
13
13
  :customStyle="actionPaddingStyle"
14
- :renderMode="!actionProps?.jumpType && mode !== 'image' ? 'noClick' : 'button'"
14
+ :renderMode="
15
+ !actionProps?.jumpType && (mode === 'image' ? !enablePreview : true)
16
+ ? 'noClick'
17
+ : 'button'
18
+ "
15
19
  >
16
20
  <view class="!flex items-center justify-center" :style="iconGapStyle">
17
21
  <wd-icon
@@ -63,6 +67,7 @@ import { computed } from 'vue'
63
67
  import useDynamicData from '../../hooks/useDynamicData'
64
68
  import { getFlexStyle, transformValueUnit } from '../../utils/transform'
65
69
  import { getDynamicData } from '../../utils/utils'
70
+ import { resolveActionProps, selectDynamicSource } from '../../utils/componentHelpers'
66
71
  import { LcbButtonProps } from './types'
67
72
  defineOptions({
68
73
  name: 'LcbButton',
@@ -85,7 +90,7 @@ const { userStore, innerDynamicData } = useDynamicData()
85
90
 
86
91
  // 优化: 缓存 store 数据源
87
92
  const store = computed(() => {
88
- return props.keyFromUser ? userStore?.userInfo : innerDynamicData.value
93
+ return selectDynamicSource(props.keyFromUser, userStore?.userInfo, innerDynamicData.value)
89
94
  })
90
95
 
91
96
  const dynamicValue = computed(() => {
@@ -122,25 +127,13 @@ const innerValue = computed(() => {
122
127
  })
123
128
  })
124
129
 
125
- /** 处理跳转链接中的动态参数 */
126
130
  const actionProps = computed(() => {
127
- // 优化: 提前返回,避免不必要的计算
128
- if (props.dynamicActionKey) {
129
- return get(innerDynamicData.value, props.dynamicActionKey)
130
- }
131
-
132
- // 优化: 只在 jumpUrl 存在时才调用 getDynamicData
133
- const jumpUrl = props.action?.jumpUrl
134
- if (!jumpUrl) {
135
- return props.action
136
- }
137
-
138
- return {
139
- ...props.action,
140
- jumpUrl: getDynamicData(jumpUrl, {
141
- store: store.value,
142
- }),
143
- }
131
+ return resolveActionProps({
132
+ dynamicActionKey: props.dynamicActionKey,
133
+ action: props.action as any,
134
+ innerDynamicData: innerDynamicData.value,
135
+ templateStore: store.value,
136
+ })
144
137
  })
145
138
 
146
139
  const onAvatar = (headImgUrl) => {
@@ -44,7 +44,14 @@
44
44
  v-bind="inputLink"
45
45
  custom-class="!w-full !truncate !pr-2"
46
46
  :customStyle="{
47
- color: form.keywords ? 'var(--content-color)' : '#969696',
47
+ fontSize: placeholderFontSize ? `${placeholderFontSize}rpx` : undefined,
48
+ fontWeight: placeholderFontWeight,
49
+ fontFamily: placeholderFontFamily,
50
+ color: placeholderColor
51
+ ? placeholderColor
52
+ : form.keywords
53
+ ? 'var(--content-color)'
54
+ : '#969696',
48
55
  }"
49
56
  :url-params="urlParams"
50
57
  >
@@ -8,4 +8,8 @@ export interface LcbCalendarSearchProps extends LcbBlockProps {
8
8
  icon?: string
9
9
  mode?: 'link' | 'search'
10
10
  inputLink?: LcbActionViewProps
11
+ placeholderColor?: string
12
+ placeholderFontSize?: number
13
+ placeholderFontWeight?: number
14
+ placeholderFontFamily?: string
11
15
  }
@@ -146,26 +146,6 @@ const coverImgWidth = computed(() => {
146
146
  mode="heightFix"
147
147
  />
148
148
  </view>
149
- <!-- area -->
150
- <!-- && itemProps.areaOnImg -->
151
- <view
152
- v-if="
153
- itemProps.showArea &&
154
- itemAttrs.cityArea &&
155
- itemProps.areaOnImg &&
156
- !itemProps.imgBottomIcon
157
- "
158
- class="flex absolute bottom-0 left-0 z-1 gap-1 p-1 w-full box-border text-white text-3.5 items-center text-shadow-xl"
159
- :style="{
160
- color: itemProps.areaColor,
161
- fontSize: `${itemProps.areaFontSize}rpx`,
162
- }"
163
- >
164
- <wd-icon name="location"></wd-icon>
165
- <view class="line-clamp-1">
166
- {{ itemAttrs.cityArea }}
167
- </view>
168
- </view>
169
149
  <slot></slot>
170
150
  </view>
171
151
  </block>
@@ -63,6 +63,9 @@ const props = withDefaults(defineProps<LcbProductItemProps>(), {
63
63
  itemContentPl: 0,
64
64
  itemGap: 10,
65
65
  titleOnImg: false,
66
+ areaOnImg: false,
67
+ addressOnImg: false,
68
+ priceOnImg: false,
66
69
  })
67
70
  const attrs = useAttrs() as Record<string, any>
68
71
  provide('lcb-product-item-props', props)
@@ -147,6 +150,48 @@ const titleOnImgTextStyle = computed(() => {
147
150
  ...(props.productNameStyle || {}),
148
151
  }
149
152
  })
153
+
154
+ const blockedByImgBottomIcon = computed(() => !!(props.imgBottomIcon && props.imgBottomIconVisible))
155
+
156
+ const titleOnImgVisible = computed(() => {
157
+ return (
158
+ !!props.titleOnImg &&
159
+ !!props.productNameVisible &&
160
+ !!props.productName &&
161
+ !blockedByImgBottomIcon.value
162
+ )
163
+ })
164
+
165
+ const areaOnImgVisible = computed(() => {
166
+ return !!(props.areaOnImg && props.showArea && attrs.cityArea && !blockedByImgBottomIcon.value)
167
+ })
168
+
169
+ const addressOnImgVisible = computed(() => {
170
+ return !!(
171
+ props.addressOnImg &&
172
+ props.addressIntroVisible &&
173
+ !!props.addressIntro &&
174
+ !blockedByImgBottomIcon.value
175
+ )
176
+ })
177
+
178
+ const priceOnImgVisible = computed(() => {
179
+ if (!props.priceOnImg || blockedByImgBottomIcon.value) return false
180
+
181
+ const hasScribePrice = props.scribePriceVisible && isNumber(props.scribePrice)
182
+ const hasPrice = props.priceVisible && isNumber(props.price)
183
+ const hasPriceTips = props.priceTipsVisible && !!props.priceTips
184
+ return !!(hasScribePrice || hasPrice || hasPriceTips)
185
+ })
186
+
187
+ const imgOverlayVisible = computed(() => {
188
+ return (
189
+ titleOnImgVisible.value ||
190
+ areaOnImgVisible.value ||
191
+ addressOnImgVisible.value ||
192
+ priceOnImgVisible.value
193
+ )
194
+ })
150
195
  </script>
151
196
 
152
197
  <template>
@@ -169,28 +214,84 @@ const titleOnImgTextStyle = computed(() => {
169
214
  <template #default>
170
215
  <slot name="coverImgSection" />
171
216
  <view
172
- v-if="
173
- titleOnImg &&
174
- productNameVisible &&
175
- !!productName &&
176
- !(imgBottomIcon && imgBottomIconVisible)
177
- "
217
+ v-if="imgOverlayVisible"
178
218
  class="absolute left-0 w-full z-1 p-1 box-border text-white text-shadow-xl bottom-0"
179
219
  :style="titleOnImgMaskStyle"
180
220
  >
181
- <slot name="productName" :value="productName">
221
+ <view class="flex flex-col gap-1 items-start">
222
+ <view v-if="titleOnImgVisible" class="w-full">
223
+ <slot name="productName" :value="productName">
224
+ <view
225
+ :class="[
226
+ productNameClass,
227
+ 'inline text-[32rpx] font-bold',
228
+ `line-clamp-${titleLineClamp}`,
229
+ 'whitespace-pre-wrap',
230
+ ]"
231
+ :style="titleOnImgTextStyle"
232
+ >
233
+ {{ productName }}
234
+ </view>
235
+ </slot>
236
+ </view>
237
+
182
238
  <view
183
- :class="[
184
- productNameClass,
185
- 'inline text-[32rpx] font-bold',
186
- `line-clamp-${titleLineClamp}`,
187
- 'whitespace-pre-wrap',
188
- ]"
189
- :style="titleOnImgTextStyle"
239
+ v-if="areaOnImgVisible || addressOnImgVisible"
240
+ class="flex gap-1 items-center w-full"
241
+ :style="{
242
+ color: areaColor,
243
+ fontSize: `${areaFontSize}rpx`,
244
+ }"
190
245
  >
191
- {{ productName }}
246
+ <wd-icon name="location"></wd-icon>
247
+ <view class="line-clamp-1">
248
+ {{ areaOnImgVisible ? attrs.cityArea : addressIntro }}
249
+ </view>
192
250
  </view>
193
- </slot>
251
+
252
+ <view v-if="priceOnImgVisible" class="flex flex-col gap-1 items-start leading-none">
253
+ <view class="flex gap-1 justify-start w-full flex-wrap">
254
+ <view
255
+ class="flex gap-[2rpx] items-end line-through"
256
+ v-if="scribePriceVisible && isNumber(scribePrice)"
257
+ >
258
+ <template
259
+ v-for="propName in ['scribePriceUnit', 'scribePrice', 'scribePriceSuffix']"
260
+ :key="propName"
261
+ >
262
+ <ItemValue :prop="propName" v-if="!!$slots?.[propName]">
263
+ <template #scribePrice="{ value }">
264
+ <slot name="scribePrice" :value="value" />
265
+ </template>
266
+ <template #scribePriceUnit="{ value }">
267
+ <slot name="scribePriceUnit" :value="value" />
268
+ </template>
269
+ <template #scribePriceSuffix="{ value }">
270
+ <slot name="scribePriceSuffix" :value="value" />
271
+ </template>
272
+ </ItemValue>
273
+ <ItemValue :prop="propName" v-else />
274
+ </template>
275
+ </view>
276
+
277
+ <view
278
+ class="flex gap-[4rpx] items-end justify-start"
279
+ v-if="priceVisible && isNumber(price)"
280
+ >
281
+ <ItemValue prop="pricePrefix" />
282
+ <ItemValue prop="priceUnit" />
283
+ <ItemValue prop="price" />
284
+ <ItemValue prop="priceSuffix" />
285
+ </view>
286
+ </view>
287
+ <ItemValue prop="priceTips" v-if="!!$slots?.priceTips">
288
+ <template #priceTips="{ value }">
289
+ <slot name="priceTips" :value="value" />
290
+ </template>
291
+ </ItemValue>
292
+ <ItemValue prop="priceTips" v-else />
293
+ </view>
294
+ </view>
194
295
  </view>
195
296
  </template>
196
297
  <template #coverImg="{ value }">
@@ -205,28 +306,84 @@ const titleOnImgTextStyle = computed(() => {
205
306
  <template #default>
206
307
  <slot name="coverImgSection" />
207
308
  <view
208
- v-if="
209
- titleOnImg &&
210
- productNameVisible &&
211
- !!productName &&
212
- !(imgBottomIcon && imgBottomIconVisible)
213
- "
309
+ v-if="imgOverlayVisible"
214
310
  class="absolute left-0 w-full z-1 p-1 box-border text-white text-shadow-xl bottom-0"
215
311
  :style="titleOnImgMaskStyle"
216
312
  >
217
- <slot name="productName" :value="productName">
313
+ <view class="flex flex-col gap-1 items-start">
314
+ <view v-if="titleOnImgVisible" class="w-full">
315
+ <slot name="productName" :value="productName">
316
+ <view
317
+ :class="[
318
+ productNameClass,
319
+ 'inline text-[32rpx] font-bold',
320
+ `line-clamp-${titleLineClamp}`,
321
+ 'whitespace-pre-wrap',
322
+ ]"
323
+ :style="titleOnImgTextStyle"
324
+ >
325
+ {{ productName }}
326
+ </view>
327
+ </slot>
328
+ </view>
329
+
218
330
  <view
219
- :class="[
220
- productNameClass,
221
- 'inline text-[32rpx] font-bold',
222
- `line-clamp-${titleLineClamp}`,
223
- 'whitespace-pre-wrap',
224
- ]"
225
- :style="titleOnImgTextStyle"
331
+ v-if="areaOnImgVisible || addressOnImgVisible"
332
+ class="flex gap-1 items-center w-full"
333
+ :style="{
334
+ color: areaColor,
335
+ fontSize: `${areaFontSize}rpx`,
336
+ }"
226
337
  >
227
- {{ productName }}
338
+ <wd-icon name="location"></wd-icon>
339
+ <view class="line-clamp-1">
340
+ {{ areaOnImgVisible ? attrs.cityArea : addressIntro }}
341
+ </view>
228
342
  </view>
229
- </slot>
343
+
344
+ <view v-if="priceOnImgVisible" class="flex flex-col gap-1 items-start leading-none">
345
+ <view class="flex gap-1 justify-start w-full flex-wrap">
346
+ <view
347
+ class="flex gap-[2rpx] items-end line-through"
348
+ v-if="scribePriceVisible && isNumber(scribePrice)"
349
+ >
350
+ <template
351
+ v-for="propName in ['scribePriceUnit', 'scribePrice', 'scribePriceSuffix']"
352
+ :key="propName"
353
+ >
354
+ <ItemValue :prop="propName" v-if="!!$slots?.[propName]">
355
+ <template #scribePrice="{ value }">
356
+ <slot name="scribePrice" :value="value" />
357
+ </template>
358
+ <template #scribePriceUnit="{ value }">
359
+ <slot name="scribePriceUnit" :value="value" />
360
+ </template>
361
+ <template #scribePriceSuffix="{ value }">
362
+ <slot name="scribePriceSuffix" :value="value" />
363
+ </template>
364
+ </ItemValue>
365
+ <ItemValue :prop="propName" v-else />
366
+ </template>
367
+ </view>
368
+
369
+ <view
370
+ class="flex gap-[4rpx] items-end justify-start"
371
+ v-if="priceVisible && isNumber(price)"
372
+ >
373
+ <ItemValue prop="pricePrefix" />
374
+ <ItemValue prop="priceUnit" />
375
+ <ItemValue prop="price" />
376
+ <ItemValue prop="priceSuffix" />
377
+ </view>
378
+ </view>
379
+ <ItemValue prop="priceTips" v-if="!!$slots?.priceTips">
380
+ <template #priceTips="{ value }">
381
+ <slot name="priceTips" :value="value" />
382
+ </template>
383
+ </ItemValue>
384
+ <ItemValue prop="priceTips" v-else />
385
+ </view>
386
+ </view>
230
387
  </view>
231
388
  </template>
232
389
  </ItemValue>
@@ -274,7 +431,7 @@ const titleOnImgTextStyle = computed(() => {
274
431
  </ItemValue>
275
432
  </view>
276
433
 
277
- <ItemValue prop="addressIntro" v-if="addressIntroVisible">
434
+ <ItemValue prop="addressIntro" v-if="addressIntroVisible && !addressOnImg">
278
435
  <!-- <template #addressIntro="{ value }"> -->
279
436
  <!-- <slot name="addressIntro" :value="value" /> -->
280
437
  <!-- </template> -->
@@ -311,6 +468,7 @@ const titleOnImgTextStyle = computed(() => {
311
468
  </view>
312
469
  <view class="flex-1"></view>
313
470
  <view
471
+ v-if="!priceOnImg"
314
472
  class="flex flex-col gap-1 items-end leading-none"
315
473
  :style="{
316
474
  marginTop: transformValueUnit(itemGap),
@@ -399,28 +557,84 @@ const titleOnImgTextStyle = computed(() => {
399
557
  <template #default>
400
558
  <slot name="coverImgSection" />
401
559
  <view
402
- v-if="
403
- titleOnImg &&
404
- productNameVisible &&
405
- !!productName &&
406
- !(imgBottomIcon && imgBottomIconVisible)
407
- "
560
+ v-if="imgOverlayVisible"
408
561
  class="absolute left-0 w-full z-1 p-1 box-border text-white text-shadow-xl bottom-0"
409
562
  :style="titleOnImgMaskStyle"
410
563
  >
411
- <slot name="productName" :value="productName">
564
+ <view class="flex flex-col gap-1 items-start">
565
+ <view v-if="titleOnImgVisible" class="w-full">
566
+ <slot name="productName" :value="productName">
567
+ <view
568
+ :class="[
569
+ productNameClass,
570
+ 'inline text-[32rpx] font-bold',
571
+ `line-clamp-${titleLineClamp}`,
572
+ 'whitespace-pre-wrap',
573
+ ]"
574
+ :style="titleOnImgTextStyle"
575
+ >
576
+ {{ productName }}
577
+ </view>
578
+ </slot>
579
+ </view>
580
+
412
581
  <view
413
- :class="[
414
- productNameClass,
415
- 'inline text-[32rpx] font-bold',
416
- `line-clamp-${titleLineClamp}`,
417
- 'whitespace-pre-wrap',
418
- ]"
419
- :style="titleOnImgTextStyle"
582
+ v-if="areaOnImgVisible || addressOnImgVisible"
583
+ class="flex gap-1 items-center w-full"
584
+ :style="{
585
+ color: areaColor,
586
+ fontSize: `${areaFontSize}rpx`,
587
+ }"
420
588
  >
421
- {{ productName }}
589
+ <wd-icon name="location"></wd-icon>
590
+ <view class="line-clamp-1">
591
+ {{ areaOnImgVisible ? attrs.cityArea : addressIntro }}
592
+ </view>
593
+ </view>
594
+
595
+ <view v-if="priceOnImgVisible" class="flex flex-col gap-1 items-start leading-none">
596
+ <view class="flex gap-1 justify-start w-full flex-wrap">
597
+ <view
598
+ class="flex gap-[2rpx] items-end line-through"
599
+ v-if="scribePriceVisible && isNumber(scribePrice)"
600
+ >
601
+ <template
602
+ v-for="propName in ['scribePriceUnit', 'scribePrice', 'scribePriceSuffix']"
603
+ :key="propName"
604
+ >
605
+ <ItemValue :prop="propName" v-if="!!$slots?.[propName]">
606
+ <template #scribePrice="{ value }">
607
+ <slot name="scribePrice" :value="value" />
608
+ </template>
609
+ <template #scribePriceUnit="{ value }">
610
+ <slot name="scribePriceUnit" :value="value" />
611
+ </template>
612
+ <template #scribePriceSuffix="{ value }">
613
+ <slot name="scribePriceSuffix" :value="value" />
614
+ </template>
615
+ </ItemValue>
616
+ <ItemValue :prop="propName" v-else />
617
+ </template>
618
+ </view>
619
+
620
+ <view
621
+ class="flex gap-[4rpx] items-end justify-start"
622
+ v-if="priceVisible && isNumber(price)"
623
+ >
624
+ <ItemValue prop="pricePrefix" />
625
+ <ItemValue prop="priceUnit" />
626
+ <ItemValue prop="price" />
627
+ <ItemValue prop="priceSuffix" />
628
+ </view>
629
+ </view>
630
+ <ItemValue prop="priceTips" v-if="!!$slots?.priceTips">
631
+ <template #priceTips="{ value }">
632
+ <slot name="priceTips" :value="value" />
633
+ </template>
634
+ </ItemValue>
635
+ <ItemValue prop="priceTips" v-else />
422
636
  </view>
423
- </slot>
637
+ </view>
424
638
  </view>
425
639
  </template>
426
640
  <template #coverImg="{ value }">
@@ -431,28 +645,84 @@ const titleOnImgTextStyle = computed(() => {
431
645
  <template #default>
432
646
  <slot name="coverImgSection" />
433
647
  <view
434
- v-if="
435
- titleOnImg &&
436
- productNameVisible &&
437
- !!productName &&
438
- !(imgBottomIcon && imgBottomIconVisible)
439
- "
648
+ v-if="imgOverlayVisible"
440
649
  class="absolute left-0 w-full z-1 p-1 box-border text-white text-shadow-xl bottom-0"
441
650
  :style="titleOnImgMaskStyle"
442
651
  >
443
- <slot name="productName" :value="productName">
652
+ <view class="flex flex-col gap-1 items-start">
653
+ <view v-if="titleOnImgVisible" class="w-full">
654
+ <slot name="productName" :value="productName">
655
+ <view
656
+ :class="[
657
+ productNameClass,
658
+ 'inline text-[32rpx] font-bold',
659
+ `line-clamp-${titleLineClamp}`,
660
+ 'whitespace-pre-wrap',
661
+ ]"
662
+ :style="titleOnImgTextStyle"
663
+ >
664
+ {{ productName }}
665
+ </view>
666
+ </slot>
667
+ </view>
668
+
444
669
  <view
445
- :class="[
446
- productNameClass,
447
- 'inline text-[32rpx] font-bold',
448
- `line-clamp-${titleLineClamp}`,
449
- 'whitespace-pre-wrap',
450
- ]"
451
- :style="titleOnImgTextStyle"
670
+ v-if="areaOnImgVisible || addressOnImgVisible"
671
+ class="flex gap-1 items-center w-full"
672
+ :style="{
673
+ color: areaColor,
674
+ fontSize: `${areaFontSize}rpx`,
675
+ }"
452
676
  >
453
- {{ productName }}
677
+ <wd-icon name="location"></wd-icon>
678
+ <view class="line-clamp-1">
679
+ {{ areaOnImgVisible ? attrs.cityArea : addressIntro }}
680
+ </view>
454
681
  </view>
455
- </slot>
682
+
683
+ <view v-if="priceOnImgVisible" class="flex flex-col gap-1 items-start leading-none">
684
+ <view class="flex gap-1 justify-start w-full flex-wrap">
685
+ <view
686
+ class="flex gap-[2rpx] items-end line-through"
687
+ v-if="scribePriceVisible && isNumber(scribePrice)"
688
+ >
689
+ <template
690
+ v-for="propName in ['scribePriceUnit', 'scribePrice', 'scribePriceSuffix']"
691
+ :key="propName"
692
+ >
693
+ <ItemValue :prop="propName" v-if="!!$slots?.[propName]">
694
+ <template #scribePrice="{ value }">
695
+ <slot name="scribePrice" :value="value" />
696
+ </template>
697
+ <template #scribePriceUnit="{ value }">
698
+ <slot name="scribePriceUnit" :value="value" />
699
+ </template>
700
+ <template #scribePriceSuffix="{ value }">
701
+ <slot name="scribePriceSuffix" :value="value" />
702
+ </template>
703
+ </ItemValue>
704
+ <ItemValue :prop="propName" v-else />
705
+ </template>
706
+ </view>
707
+
708
+ <view
709
+ class="flex gap-[4rpx] items-end justify-start"
710
+ v-if="priceVisible && isNumber(price)"
711
+ >
712
+ <ItemValue prop="pricePrefix" />
713
+ <ItemValue prop="priceUnit" />
714
+ <ItemValue prop="price" />
715
+ <ItemValue prop="priceSuffix" />
716
+ </view>
717
+ </view>
718
+ <ItemValue prop="priceTips" v-if="!!$slots?.priceTips">
719
+ <template #priceTips="{ value }">
720
+ <slot name="priceTips" :value="value" />
721
+ </template>
722
+ </ItemValue>
723
+ <ItemValue prop="priceTips" v-else />
724
+ </view>
725
+ </view>
456
726
  </view>
457
727
  </template>
458
728
  </ItemValue>
@@ -494,7 +764,7 @@ const titleOnImgTextStyle = computed(() => {
494
764
  </ItemValue>
495
765
  </view>
496
766
 
497
- <ItemValue prop="addressIntro" v-if="addressIntroVisible">
767
+ <ItemValue prop="addressIntro" v-if="addressIntroVisible && !addressOnImg">
498
768
  <!-- <template #addressIntro="{ value }">
499
769
  <slot name="addressIntro" :value="value" />
500
770
  </template> -->
@@ -529,6 +799,7 @@ const titleOnImgTextStyle = computed(() => {
529
799
  </view>
530
800
  <view class="flex-1"></view>
531
801
  <view
802
+ v-if="!priceOnImg"
532
803
  class="flex flex-col gap-1 items-end leading-none"
533
804
  :style="{
534
805
  marginTop: transformValueUnit(itemGap),
@@ -16,6 +16,8 @@ export interface LcbProductItemProps {
16
16
  tagType?: TagType
17
17
  tagContentColor?: string
18
18
  addressOnImg?: boolean
19
+ // 价格块显示在图片上(包含 price / scribePrice 及其相关字段)
20
+ priceOnImg?: boolean
19
21
  tagBgColor?: string
20
22
  tagPlain?: boolean
21
23
  tagMark?: boolean
@@ -1,5 +1,27 @@
1
1
  <template>
2
- <lcb-block v-bind="$props" :custom-style="listStyle" v-if="renderList.length">
2
+ <z-paging
3
+ ref="paging"
4
+ :auto="true"
5
+ v-model="renderList"
6
+ @query="queryList"
7
+ :fixed="false"
8
+ :default-page-size="props.pageProps?.pageLimit"
9
+ :use-page-scroll="props.pageProps?.pagingUsePageScroll"
10
+ :height="props.pageProps?.pagingUsePageScroll ? undefined : props.pageProps?.pagingHeight"
11
+ v-if="props.pageProps?.pagingEnabled"
12
+ >
13
+ <lcb-block v-bind="$props" :custom-style="listStyle">
14
+ <view
15
+ v-for="(item, index) in renderList"
16
+ :key="getItemKey(item, index)"
17
+ class="flex-shrink-0"
18
+ :style="itemStyle"
19
+ >
20
+ <slot :data="item" :list="list" />
21
+ </view>
22
+ </lcb-block>
23
+ </z-paging>
24
+ <lcb-block v-bind="$props" :custom-style="listStyle" v-else>
3
25
  <view
4
26
  v-for="(item, index) in renderList"
5
27
  :key="getItemKey(item, index)"
@@ -13,7 +35,8 @@
13
35
 
14
36
  <script setup lang="ts">
15
37
  import { transformValueUnit } from '@tplc/business/utils/transform'
16
- import { computed, watch, shallowRef } from 'vue'
38
+ import { computed, watch, shallowRef, ref } from 'vue'
39
+ import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging'
17
40
  import useDynamicData from '../../hooks/useDynamicData'
18
41
  import { dynamicRequest } from '../../utils/request'
19
42
  import { LcbWrapperListProps } from './types'
@@ -28,10 +51,19 @@ defineOptions({
28
51
  const props = withDefaults(defineProps<LcbWrapperListProps>(), {
29
52
  display: 'flex',
30
53
  flexDirection: 'column',
54
+ pagingEnabled: false,
55
+ pageLimit: 10,
56
+ pagingUsePageScroll: true,
31
57
  })
32
58
  const { userStore, innerDynamicData } = useDynamicData()
33
59
  // 使用 shallowRef 优化大数组的响应式性能
34
60
  const renderList = shallowRef<unknown[]>([])
61
+ const paging = ref()
62
+ useZPaging(paging)
63
+ const pageSearch = shallowRef<{ limit: number; page: number }>({
64
+ limit: props.pageLimit,
65
+ page: 1,
66
+ })
35
67
 
36
68
  // 优化:检查是否需要转换,避免不必要的遍历
37
69
  const transformStringArrayToObjectArray = (data: unknown[]): unknown[] => {
@@ -47,10 +79,33 @@ const transformStringArrayToObjectArray = (data: unknown[]): unknown[] => {
47
79
  })
48
80
  }
49
81
 
82
+ const queryList = async (page: number, limit: number) => {
83
+ pageSearch.value = { page, limit }
84
+ const data = await dynamicRequest(props.dataSource, innerDynamicData.value, userStore?.userInfo, {
85
+ pageSearch: pageSearch.value,
86
+ })
87
+ paging.value?.complete?.(data)
88
+ }
89
+
50
90
  watch(
51
- () => [props.dataSource, innerDynamicData.value, userStore?.userInfo] as const,
91
+ () =>
92
+ [
93
+ props.dataSource,
94
+ innerDynamicData.value,
95
+ userStore?.userInfo,
96
+ props.pageProps?.pagingEnabled,
97
+ props.pageProps?.pageLimit,
98
+ ] as const,
52
99
 
53
- async ([dataSource, dynamicData, userInfo]) => {
100
+ async ([dataSource, dynamicData, userInfo, pagingEnabled, pageLimit = 10]) => {
101
+ if (pagingEnabled) {
102
+ pageSearch.value = {
103
+ limit: pageLimit,
104
+ page: 1,
105
+ }
106
+ paging.value?.reload?.()
107
+ return
108
+ }
54
109
  // 防止重复请求
55
110
  let data: unknown[] = []
56
111
  const result = await dynamicRequest(dataSource, dynamicData, userInfo)
@@ -81,11 +136,12 @@ const itemStyle = computed(() => {
81
136
 
82
137
  // 优化:减少对象创建,使用固定的样式对象
83
138
  const listStyle = computed(() => {
139
+ const gap = transformValueUnit(props.gap)
84
140
  return {
85
141
  width: '100%',
86
142
  display: props.display,
87
143
  'flex-direction': props.flexDirection,
88
- gap: transformValueUnit(props.gap),
144
+ gap,
89
145
  'align-items': 'stretch',
90
146
  'grid-template-columns': `repeat(${props.gridColumns}, minmax(0, 1fr))`,
91
147
  'overflow-x': props.scrollX ? 'auto' : 'hidden',
@@ -11,4 +11,22 @@ export interface LcbWrapperListProps extends LcbBlockProps {
11
11
  scrollX?: boolean
12
12
  list?: LcbAreaProps[]
13
13
  childrenAutoWidth?: boolean
14
+ pageProps: {
15
+ /**
16
+ * 开启下拉刷新+上拉加载(基于 z-paging)
17
+ */
18
+ pagingEnabled?: boolean
19
+ /**
20
+ * 分页大小,对应 pageSearch.limit,默认 10
21
+ */
22
+ pageLimit?: number
23
+ /**
24
+ * z-paging 是否使用页面滚动模式,默认 true
25
+ */
26
+ pagingUsePageScroll?: boolean
27
+ /**
28
+ * 非页面滚动模式下的高度(pagingUsePageScroll=false 时生效)
29
+ */
30
+ pagingHeight?: string
31
+ }
14
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tplc/business",
3
- "version": "0.7.51",
3
+ "version": "0.7.53",
4
4
  "keywords": [
5
5
  "业务组件"
6
6
  ],
@@ -54,7 +54,7 @@ export interface LcbBlockProps {
54
54
  | 'bottom-left'
55
55
  | 'bottom-center'
56
56
  | 'bottom-right'
57
- dynamicStyleOptions?: DynamicOptions[]
57
+ dynamicStyleOptions?: DynamicOptions
58
58
  dynamicBgImage?: string
59
59
  }
60
60
  export interface LcbBlockInnerProps extends LcbBlockProps {
@@ -6,4 +6,8 @@ export interface LcbCalendarSearchProps extends LcbBlockProps {
6
6
  icon?: string
7
7
  mode?: 'link' | 'search'
8
8
  inputLink?: LcbActionViewProps
9
+ placeholderColor?: string
10
+ placeholderFontSize?: number
11
+ placeholderFontWeight?: number
12
+ placeholderFontFamily?: string
9
13
  }
@@ -66,7 +66,7 @@ declare const __VLS_component: import('vue').DefineComponent<
66
66
  | 'bottom-left'
67
67
  | 'bottom-center'
68
68
  | 'bottom-right'
69
- dynamicStyleOptions: import('../../action').DynamicOptions[]
69
+ dynamicStyleOptions: import('../../action').DynamicOptions
70
70
  dynamicBgImage: string
71
71
  gap: number
72
72
  imageWidth: number
@@ -120,6 +120,9 @@ declare const __VLS_component: import('vue').DefineComponent<
120
120
  itemContentPl: number
121
121
  itemGap: number
122
122
  titleOnImg: boolean
123
+ areaOnImg: boolean
124
+ addressOnImg: boolean
125
+ priceOnImg: boolean
123
126
  }
124
127
  >,
125
128
  {},
@@ -184,6 +187,9 @@ declare const __VLS_component: import('vue').DefineComponent<
184
187
  itemContentPl: number
185
188
  itemGap: number
186
189
  titleOnImg: boolean
190
+ areaOnImg: boolean
191
+ addressOnImg: boolean
192
+ priceOnImg: boolean
187
193
  }
188
194
  >
189
195
  >
@@ -204,8 +210,11 @@ declare const __VLS_component: import('vue').DefineComponent<
204
210
  itemContentPl: number
205
211
  priceUnit: any
206
212
  scribePriceUnit: any
213
+ areaOnImg: boolean
207
214
  tagOverflowWrap: boolean
208
215
  tagType: import('@tplc/wot/components/wd-tag/types').TagType
216
+ addressOnImg: boolean
217
+ priceOnImg: boolean
209
218
  tagPlain: boolean
210
219
  tagMark: boolean
211
220
  tagRound: boolean
@@ -13,6 +13,7 @@ export interface LcbProductItemProps {
13
13
  tagType?: TagType
14
14
  tagContentColor?: string
15
15
  addressOnImg?: boolean
16
+ priceOnImg?: boolean
16
17
  tagBgColor?: string
17
18
  tagPlain?: boolean
18
19
  tagMark?: boolean
@@ -1,6 +1,7 @@
1
1
  import { LcbWrapperListProps } from './types'
2
2
  declare function __VLS_template(): {
3
3
  default?(_: { data: unknown; list: import('../lcb-area/types').LcbAreaProps[] | undefined }): any
4
+ default?(_: { data: unknown; list: import('../lcb-area/types').LcbAreaProps[] | undefined }): any
4
5
  }
5
6
  declare const __VLS_component: import('vue').DefineComponent<
6
7
  __VLS_WithDefaults<
@@ -8,6 +9,9 @@ declare const __VLS_component: import('vue').DefineComponent<
8
9
  {
9
10
  display: string
10
11
  flexDirection: string
12
+ pagingEnabled: boolean
13
+ pageLimit: number
14
+ pagingUsePageScroll: boolean
11
15
  }
12
16
  >,
13
17
  {},
@@ -26,6 +30,9 @@ declare const __VLS_component: import('vue').DefineComponent<
26
30
  {
27
31
  display: string
28
32
  flexDirection: string
33
+ pagingEnabled: boolean
34
+ pageLimit: number
35
+ pagingUsePageScroll: boolean
29
36
  }
30
37
  >
31
38
  >
@@ -11,4 +11,22 @@ export interface LcbWrapperListProps extends LcbBlockProps {
11
11
  scrollX?: boolean
12
12
  list?: LcbAreaProps[]
13
13
  childrenAutoWidth?: boolean
14
+ pageProps: {
15
+ /**
16
+ * 开启下拉刷新+上拉加载(基于 z-paging)
17
+ */
18
+ pagingEnabled?: boolean
19
+ /**
20
+ * 分页大小,对应 pageSearch.limit,默认 10
21
+ */
22
+ pageLimit?: number
23
+ /**
24
+ * z-paging 是否使用页面滚动模式,默认 true
25
+ */
26
+ pagingUsePageScroll?: boolean
27
+ /**
28
+ * 非页面滚动模式下的高度(pagingUsePageScroll=false 时生效)
29
+ */
30
+ pagingHeight?: string
31
+ }
14
32
  }
@@ -0,0 +1,28 @@
1
+ type AnyRecord = Record<string, any>
2
+ /**
3
+ * 根据 keyFromUser 选择动态数据源
4
+ * - true: userStore.userInfo
5
+ * - false: innerDynamicData
6
+ */
7
+ export declare function selectDynamicSource(
8
+ keyFromUser: boolean | undefined,
9
+ userInfo: AnyRecord | undefined,
10
+ innerDynamicData: AnyRecord | undefined,
11
+ ): AnyRecord | undefined
12
+ /**
13
+ * 统一处理 actionProps:
14
+ * - dynamicActionKey 优先:从 innerDynamicData 取整段 action 配置
15
+ * - 否则对 action.jumpUrl 做动态参数替换(${xxx})
16
+ */
17
+ export declare function resolveActionProps({
18
+ dynamicActionKey,
19
+ action,
20
+ innerDynamicData,
21
+ templateStore,
22
+ }: {
23
+ dynamicActionKey?: string
24
+ action?: AnyRecord
25
+ innerDynamicData?: AnyRecord
26
+ templateStore?: AnyRecord
27
+ }): any
28
+ export {}
@@ -3,4 +3,5 @@ export declare const dynamicRequest: (
3
3
  dataSource?: DataSource,
4
4
  pageInfo?: Record<string, any>,
5
5
  userInfo?: Record<string, any>,
6
+ extraParams?: Record<string, any>,
6
7
  ) => Promise<any>
@@ -0,0 +1,50 @@
1
+ import { get } from 'lodash-es'
2
+ import { getDynamicData } from './utils'
3
+
4
+ type AnyRecord = Record<string, any>
5
+
6
+ /**
7
+ * 根据 keyFromUser 选择动态数据源
8
+ * - true: userStore.userInfo
9
+ * - false: innerDynamicData
10
+ */
11
+ export function selectDynamicSource(
12
+ keyFromUser: boolean | undefined,
13
+ userInfo: AnyRecord | undefined,
14
+ innerDynamicData: AnyRecord | undefined,
15
+ ) {
16
+ return (keyFromUser ? userInfo : innerDynamicData) as AnyRecord | undefined
17
+ }
18
+
19
+ /**
20
+ * 统一处理 actionProps:
21
+ * - dynamicActionKey 优先:从 innerDynamicData 取整段 action 配置
22
+ * - 否则对 action.jumpUrl 做动态参数替换(${xxx})
23
+ */
24
+ export function resolveActionProps({
25
+ dynamicActionKey,
26
+ action,
27
+ innerDynamicData,
28
+ templateStore,
29
+ }: {
30
+ dynamicActionKey?: string
31
+ action?: AnyRecord
32
+ innerDynamicData?: AnyRecord
33
+ templateStore?: AnyRecord
34
+ }) {
35
+ if (dynamicActionKey) {
36
+ return get(innerDynamicData, dynamicActionKey)
37
+ }
38
+
39
+ const jumpUrl = action?.jumpUrl
40
+ if (!jumpUrl) {
41
+ return action
42
+ }
43
+
44
+ return {
45
+ ...action,
46
+ jumpUrl: getDynamicData(jumpUrl, {
47
+ store: templateStore,
48
+ }),
49
+ }
50
+ }
package/utils/request.ts CHANGED
@@ -5,12 +5,17 @@ export const dynamicRequest = async (
5
5
  dataSource?: DataSource,
6
6
  pageInfo?: Record<string, any>,
7
7
  userInfo?: Record<string, any>,
8
+ extraParams?: Record<string, any>,
8
9
  ) => {
9
10
  if (dataSource?.source === 'remote') {
10
11
  if (dataSource.requestInfo?.requestUrl) {
12
+ const baseParams = JSON.parse(dataSource.requestInfo.requestParams || '{}')
11
13
  const response = await uni.$lcb.http.post(
12
14
  dataSource.requestInfo.requestUrl,
13
- JSON.parse(dataSource.requestInfo.requestParams || '{}'),
15
+ {
16
+ ...baseParams,
17
+ ...(extraParams || {}),
18
+ },
14
19
  )
15
20
  /** 如果依赖key存在,则取依赖key的值 */
16
21
  return response[dataSource?.dependKey || 'data'] as unknown