wui-components-v2 1.1.69 → 1.1.71
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/api/core/index.ts +74 -74
- package/api/feishu.ts +20 -20
- package/api/index.ts +4 -4
- package/api/login.ts +51 -51
- package/api/menu.ts +45 -45
- package/api/page.ts +114 -114
- package/api/sys.ts +12 -12
- package/api/tree.ts +28 -28
- package/components/action-popup/action-popup.vue +116 -116
- package/components/add-address-list/add-address-list.vue +187 -187
- package/components/add-address-page/add-address-page.vue +77 -77
- package/components/add-address-page/config.ts +297 -297
- package/components/audio-play/audio-play.vue +220 -220
- package/components/batch-upload-file/batch-upload-file.vue +75 -75
- package/components/card-botom-buttons/card-botom-buttons.vue +279 -279
- package/components/custom-date-picker/custom-date-picker.vue +106 -106
- package/components/custom-select-picker/custom-select-picker.vue +95 -95
- package/components/demo-block/demo-block.vue +63 -63
- package/components/demo-card/demo-card.vue +44 -44
- package/components/detail-popup/detail-popup.vue +99 -99
- package/components/evaluation-page/evaluation-page.vue +196 -196
- package/components/fold-card/fold-card.vue +171 -171
- package/components/form-control/form-control.vue +661 -661
- package/components/global-loading/GlobalLoading.md +366 -366
- package/components/global-loading/global-loading.vue +55 -55
- package/components/global-message/GlobalMessage.md +570 -570
- package/components/global-message/global-message.vue +68 -68
- package/components/global-toast/GlobalToast.md +261 -261
- package/components/global-toast/global-toast.vue +56 -56
- package/components/label-value/label-value.vue +144 -144
- package/components/label-value/open-value-more.vue +56 -56
- package/components/list-top-buttons/list-top-buttons.vue +19 -19
- package/components/loading/loading.vue +5 -5
- package/components/login-form/login-form.vue +126 -126
- package/components/login-form-box/login-form-box.vue +65 -65
- package/components/mulselect-picker/mulselect-picker.vue +86 -86
- package/components/phone-login-form/phone-login-form.vue +204 -204
- package/components/privacy-popup/privacy-popup.vue +191 -191
- package/components/product-card/product-card.vue +201 -201
- package/components/scan-input/scan-input.vue +406 -406
- package/components/search/search.vue +128 -128
- package/components/select-col-picker/select-col-picker.vue +168 -168
- package/components/tab-search/tab-search.vue +73 -73
- package/components/tree-select/components/TreeNode.vue +200 -200
- package/components/tree-select/components/index.vue +471 -471
- package/components/tree-select/components/tree-select.vue +56 -56
- package/components/tree-select/index.vue +93 -93
- package/components/user-choose/user-choose.vue +169 -151
- package/components/video-play/video-play.vue +56 -56
- package/components/wui-auto-update-component/wui-auto-update-component.vue +132 -132
- package/components/wui-default/wui-default.vue +49 -49
- package/components/wui-details-page/wui-details-page.vue +147 -147
- package/components/wui-edit-page/wui-edit-page-copy.vue +757 -757
- package/components/wui-edit-page/wui-edit-page.vue +793 -793
- package/components/wui-enume-select-control/wui-enume-select-control.vue +92 -92
- package/components/wui-list/wui-list.vue +235 -235
- package/components/wui-login/wui-login.vue +43 -43
- package/components/wui-login1/wui-login.vue +41 -41
- package/components/wui-menus/wui-menus-top.vue +137 -137
- package/components/wui-menus/wui-menus-top1.vue +8 -8
- package/components/wui-menus/wui-menus.vue +247 -247
- package/components/wui-menus1/components/banner-carousel.vue +320 -320
- package/components/wui-menus1/components/navbar.vue +43 -43
- package/components/wui-menus1/components/quick-panel.vue +55 -55
- package/components/wui-menus1/components/search-bar.vue +49 -49
- package/components/wui-menus1/components/section-menus.vue +208 -208
- package/components/wui-menus1/wui-menus-top.vue +718 -718
- package/components/wui-menus1/wui-menus-top1.vue +398 -398
- package/components/wui-menus1/wui-menus.vue +564 -564
- package/components/wui-notify-info/notify-handle.vue +53 -53
- package/components/wui-notify-info/wui-notify-info.vue +280 -280
- package/components/wui-scan-binding-sensor/wui-scan-binding-sensor.vue +75 -75
- package/components/wui-search-history-babbar/components/SearchBar.vue +70 -70
- package/components/wui-search-history-babbar/wui-search-history-babbar.vue +204 -204
- package/components/wui-select-list/wui-select-list.vue +310 -310
- package/components/wui-select-popup/wui-select-popup.vue +613 -612
- package/components/wui-system-settings/wui-system-settings.vue +144 -144
- package/components/wui-tabbar/wui-tabbar.vue +106 -106
- package/components/wui-tree-page/components/tree-item.vue +238 -238
- package/components/wui-tree-page/wui-tree-page.vue +297 -297
- package/components/wui-update-component/wui-update-component.vue +221 -221
- package/components/wui-user/wui-user.vue +202 -202
- package/composables/types/theme.ts +65 -65
- package/composables/types/user.ts +5 -5
- package/composables/useColorGenerator.ts +221 -221
- package/composables/useCompanyFieldFilter.ts +91 -91
- package/composables/useEnumes.ts +43 -43
- package/composables/useEventBus.ts +37 -37
- package/composables/useGlobalLoading.ts +47 -47
- package/composables/useGlobalMessage.ts +53 -53
- package/composables/useGlobalToast.ts +63 -63
- package/composables/useLocale.ts +54 -54
- package/composables/useManualTheme.ts +179 -179
- package/composables/useMenus.ts +193 -193
- package/composables/useSectionMenus.ts +120 -120
- package/composables/useTabbar.ts +50 -50
- package/composables/useTheme.ts +74 -74
- package/composables/useUser.ts +24 -24
- package/index.d.ts +90 -90
- package/index.ts +83 -83
- package/package.json +24 -24
- package/static/iconfont/iconfont.css +63 -63
- package/store/language.ts +151 -151
- package/store/manualThemeStore.ts +143 -143
- package/store/persist.ts +41 -41
- package/store/themeStore.ts +74 -74
- package/store/userStore.ts +22 -22
- package/styles/dark-mode.css +523 -523
- package/styles/dark-mode.min.css +1 -1
- package/styles/dark-mode.scss +498 -498
- package/type.ts +154 -154
- package/utils/control-tree.ts +353 -353
- package/utils/control-type-supportor.ts +148 -148
- package/utils/eventBus.ts +36 -36
- package/utils/idata-scan.ts +99 -99
- package/utils/index.ts +979 -979
- package/utils/rsaEncrypt.ts +10 -10
|
@@ -1,320 +1,320 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* BannerCarousel.vue
|
|
4
|
-
* 原生实现的 Banner 轮播组件
|
|
5
|
-
* 功能:自动播放、手动滑动切换、鼠标悬停暂停、分页器点击跳转
|
|
6
|
-
*/
|
|
7
|
-
import { onMounted, onUnmounted, ref } from 'vue'
|
|
8
|
-
|
|
9
|
-
/** 从 mockData 获取轮播数据 */
|
|
10
|
-
const banners: any = [
|
|
11
|
-
{
|
|
12
|
-
id: 1,
|
|
13
|
-
title: '欢迎使用企业门户',
|
|
14
|
-
subtitle: '高效协同,智慧办公',
|
|
15
|
-
bgColor: 'bg-gradient-to-r from-indigo-500 to-purple-600',
|
|
16
|
-
icon: 'building-office',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: 2,
|
|
20
|
-
title: '本周会议安排',
|
|
21
|
-
subtitle: '3场会议待参加',
|
|
22
|
-
bgColor: 'bg-gradient-to-r from-emerald-500 to-teal-600',
|
|
23
|
-
icon: 'calendar',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 3,
|
|
27
|
-
title: '系统升级通知',
|
|
28
|
-
subtitle: 'v2.5.0 新功能上线',
|
|
29
|
-
bgColor: 'bg-gradient-to-r from-orange-500 to-amber-600',
|
|
30
|
-
icon: 'sparkles',
|
|
31
|
-
},
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 图标映射表
|
|
36
|
-
* 根据 banner.icon 字段映射对应的 Heroicons 组件
|
|
37
|
-
*/
|
|
38
|
-
// const iconMap: Record<string, any> = {
|
|
39
|
-
// 'building-office': BuildingOfficeIcon,
|
|
40
|
-
// 'calendar': CalendarIcon,
|
|
41
|
-
// 'sparkles': SparklesIcon
|
|
42
|
-
// }
|
|
43
|
-
|
|
44
|
-
/** 当前显示的轮播项索引 */
|
|
45
|
-
const currentIndex = ref(0)
|
|
46
|
-
|
|
47
|
-
/** 鼠标是否悬停在轮播区域 */
|
|
48
|
-
const isHovered = ref(false)
|
|
49
|
-
|
|
50
|
-
/** 是否正在拖动(手动滑动中) */
|
|
51
|
-
const isDragging = ref(false)
|
|
52
|
-
|
|
53
|
-
/** 拖动偏移量(px),用于实时跟随手指/鼠标移动 */
|
|
54
|
-
const dragOffset = ref(0)
|
|
55
|
-
|
|
56
|
-
/** 自动播放的定时器引用 */
|
|
57
|
-
let autoplayTimer: ReturnType<typeof setInterval> | null = null
|
|
58
|
-
|
|
59
|
-
/** 拖动开始时的 X 坐标 */
|
|
60
|
-
let startX = 0
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 启动自动播放
|
|
64
|
-
* 每 3 秒切换到下一张,循环播放
|
|
65
|
-
*/
|
|
66
|
-
function startAutoplay() {
|
|
67
|
-
if (autoplayTimer)
|
|
68
|
-
clearInterval(autoplayTimer)
|
|
69
|
-
autoplayTimer = setInterval(() => {
|
|
70
|
-
currentIndex.value = (currentIndex.value + 1) % banners.length
|
|
71
|
-
}, 10000)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 停止自动播放
|
|
76
|
-
* 清除定时器并置空引用
|
|
77
|
-
*/
|
|
78
|
-
function stopAutoplay() {
|
|
79
|
-
if (autoplayTimer) {
|
|
80
|
-
clearInterval(autoplayTimer)
|
|
81
|
-
autoplayTimer = null
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 跳转到指定索引的轮播项
|
|
87
|
-
* @param index 目标索引
|
|
88
|
-
*/
|
|
89
|
-
function goToSlide(index: number) {
|
|
90
|
-
currentIndex.value = index
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 切换到下一张轮播项
|
|
95
|
-
* 使用取模实现循环(最后一张 -> 第一张)
|
|
96
|
-
*/
|
|
97
|
-
function nextSlide() {
|
|
98
|
-
currentIndex.value = (currentIndex.value + 1) % banners.length
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 切换到上一张轮播项
|
|
103
|
-
* 使用取模实现循环(第一张 -> 最后一张)
|
|
104
|
-
*/
|
|
105
|
-
function prevSlide() {
|
|
106
|
-
currentIndex.value = (currentIndex.value - 1 + banners.length) % banners.length
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 鼠标进入轮播区域
|
|
111
|
-
* 停止自动播放
|
|
112
|
-
*/
|
|
113
|
-
function onMouseEnter() {
|
|
114
|
-
isHovered.value = true
|
|
115
|
-
stopAutoplay()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 拖动开始
|
|
120
|
-
* 记录起始 X 坐标,重置拖动偏移量
|
|
121
|
-
* @param clientX 鼠标/触摸的 X 坐标
|
|
122
|
-
*/
|
|
123
|
-
function onDragStart(clientX: number) {
|
|
124
|
-
isDragging.value = true
|
|
125
|
-
startX = clientX
|
|
126
|
-
dragOffset.value = 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 拖动中
|
|
131
|
-
* 实时更新 dragOffset,实现跟随效果
|
|
132
|
-
* @param clientX 鼠标/触摸的当前 X 坐标
|
|
133
|
-
*/
|
|
134
|
-
function onDragMove(clientX: number) {
|
|
135
|
-
if (!isDragging.value)
|
|
136
|
-
return
|
|
137
|
-
dragOffset.value = clientX - startX
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 拖动结束
|
|
142
|
-
* 判断滑动方向和距离:
|
|
143
|
-
* - 左滑 > 50px:切换下一张
|
|
144
|
-
* - 右滑 > 50px:切换上一张
|
|
145
|
-
* - 否则:回弹到原位
|
|
146
|
-
* 拖动结束后恢复自动播放
|
|
147
|
-
*/
|
|
148
|
-
function onDragEnd() {
|
|
149
|
-
if (!isDragging.value)
|
|
150
|
-
return
|
|
151
|
-
isDragging.value = false
|
|
152
|
-
|
|
153
|
-
const threshold = 50
|
|
154
|
-
if (dragOffset.value < -threshold) {
|
|
155
|
-
nextSlide()
|
|
156
|
-
}
|
|
157
|
-
else if (dragOffset.value > threshold) {
|
|
158
|
-
prevSlide()
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
dragOffset.value = 0
|
|
162
|
-
startAutoplay()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 触摸开始事件(移动端) */
|
|
166
|
-
function onTouchStart(e: TouchEvent) {
|
|
167
|
-
onDragStart(e.touches[0]?.clientX ?? 0)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** 触摸移动事件(移动端) */
|
|
171
|
-
function onTouchMove(e: TouchEvent) {
|
|
172
|
-
onDragMove(e.touches[0]?.clientX ?? 0)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** 触摸结束事件(移动端) */
|
|
176
|
-
function onTouchEnd() {
|
|
177
|
-
onDragEnd()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** 鼠标按下事件(桌面端) */
|
|
181
|
-
function onMouseDown(e: MouseEvent) {
|
|
182
|
-
onDragStart(e.clientX)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** 鼠标移动事件(桌面端拖动中) */
|
|
186
|
-
function onMouseMove(e: MouseEvent) {
|
|
187
|
-
onDragMove(e.clientX)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 鼠标释放事件(桌面端拖动结束) */
|
|
191
|
-
function onMouseUp() {
|
|
192
|
-
onDragEnd()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* 鼠标离开轮播区域
|
|
197
|
-
* 如果正在拖动中则触发拖动结束
|
|
198
|
-
* 恢复自动播放
|
|
199
|
-
*/
|
|
200
|
-
function onMouseLeave() {
|
|
201
|
-
if (isDragging.value) {
|
|
202
|
-
onDragEnd()
|
|
203
|
-
}
|
|
204
|
-
isHovered.value = false
|
|
205
|
-
startAutoplay()
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** 组件挂载时启动自动播放 */
|
|
209
|
-
onMounted(() => {
|
|
210
|
-
startAutoplay()
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
/** 组件卸载时停止自动播放,防止内存泄漏 */
|
|
214
|
-
onUnmounted(() => {
|
|
215
|
-
stopAutoplay()
|
|
216
|
-
})
|
|
217
|
-
</script>
|
|
218
|
-
|
|
219
|
-
<template>
|
|
220
|
-
<!--
|
|
221
|
-
外层容器:
|
|
222
|
-
- select-none:防止拖动时选中文本
|
|
223
|
-
- 监听 mouseenter/mouseleave 用于悬停暂停
|
|
224
|
-
-->
|
|
225
|
-
<view
|
|
226
|
-
class="mt-6 select-none overflow-hidden rounded-2xl pb-2"
|
|
227
|
-
@mouseenter="onMouseEnter"
|
|
228
|
-
@mouseleave="onMouseLeave"
|
|
229
|
-
>
|
|
230
|
-
<!--
|
|
231
|
-
轮播内容区:
|
|
232
|
-
- overflow-hidden:裁剪只显示当前 slide
|
|
233
|
-
- cursor-grab/active:拖动时显示抓取手势
|
|
234
|
-
- 监听 mousedown/mousemove/mouseup(桌面端)
|
|
235
|
-
- 监听 touchstart/touchmove/touchend(移动端)
|
|
236
|
-
-->
|
|
237
|
-
<view
|
|
238
|
-
class="cursor-grab overflow-hidden active:cursor-grabbing"
|
|
239
|
-
@mousedown="onMouseDown"
|
|
240
|
-
@mousemove="onMouseMove"
|
|
241
|
-
@mouseup="onMouseUp"
|
|
242
|
-
@mouseleave="onMouseLeave"
|
|
243
|
-
@touchstart="onTouchStart"
|
|
244
|
-
@touchmove="onTouchMove"
|
|
245
|
-
@touchend="onTouchEnd"
|
|
246
|
-
>
|
|
247
|
-
<!--
|
|
248
|
-
轮播轨道:
|
|
249
|
-
- flex:水平排列所有 slide
|
|
250
|
-
- transition-transform:
|
|
251
|
-
- 拖动中:transition-none(实时跟随,无动画)
|
|
252
|
-
- 正常切换:transition-transform duration-[600ms] ease(600ms 动画)
|
|
253
|
-
- translateX 计算:
|
|
254
|
-
- 基准位置:-${currentIndex * 100}%
|
|
255
|
-
- 拖动偏移:+ ${dragOffset}px
|
|
256
|
-
-->
|
|
257
|
-
<view
|
|
258
|
-
class="flex"
|
|
259
|
-
:class="isDragging ? 'transition-none' : 'transition-transform duration-[600ms] ease'"
|
|
260
|
-
:style="{ transform: `translateX(calc(-${currentIndex * 100}% + ${dragOffset}px))` }"
|
|
261
|
-
>
|
|
262
|
-
<!--
|
|
263
|
-
单个轮播项:
|
|
264
|
-
- w-full flex-shrink-0:每个 slide 占满宽度且不压缩
|
|
265
|
-
-->
|
|
266
|
-
<view
|
|
267
|
-
v-for="banner in banners"
|
|
268
|
-
:key="banner.id"
|
|
269
|
-
class="w-full flex-shrink-0 cursor-pointer"
|
|
270
|
-
>
|
|
271
|
-
<!--
|
|
272
|
-
Banner 内容卡片:
|
|
273
|
-
- 动态背景色(bgColor)
|
|
274
|
-
- h-36:固定高度
|
|
275
|
-
- flex items-center:垂直居中
|
|
276
|
-
- px-6:水平内边距
|
|
277
|
-
- rounded-2xl:圆角
|
|
278
|
-
-->
|
|
279
|
-
<view
|
|
280
|
-
:class="banner.bgColor"
|
|
281
|
-
class="h-36 flex items-center rounded-2xl px-6"
|
|
282
|
-
>
|
|
283
|
-
<!-- 左侧文字区域 -->
|
|
284
|
-
<view class="flex-1">
|
|
285
|
-
<h3 class="text-lg text-white font-semibold">
|
|
286
|
-
{{ banner.title }}
|
|
287
|
-
</h3>
|
|
288
|
-
<p class="mt-1 text-sm text-white/80">
|
|
289
|
-
{{ banner.subtitle }}
|
|
290
|
-
</p>
|
|
291
|
-
</view>
|
|
292
|
-
|
|
293
|
-
<!-- 右侧图标区域 -->
|
|
294
|
-
<view class="h-14 w-14 flex items-center justify-center rounded-full bg-white/20">
|
|
295
|
-
<!-- <component :is="iconMap[banner.icon]" class="w-8 h-8 text-white" /> -->
|
|
296
|
-
<wd-icon name="calendar" size="26px" />
|
|
297
|
-
</view>
|
|
298
|
-
</view>
|
|
299
|
-
</view>
|
|
300
|
-
</view>
|
|
301
|
-
</view>
|
|
302
|
-
|
|
303
|
-
<!--
|
|
304
|
-
分页器:
|
|
305
|
-
- flex justify-center gap-2:水平居中,dot 之间 8px 间距
|
|
306
|
-
- mt-4:与轮播内容保持间距
|
|
307
|
-
-->
|
|
308
|
-
<view class="mt-4 flex justify-center gap-2">
|
|
309
|
-
<view
|
|
310
|
-
v-for="(_, index) in banners"
|
|
311
|
-
:key="index"
|
|
312
|
-
class="h-1 w-1 cursor-pointer rounded-full transition-all duration-300"
|
|
313
|
-
:class="index === currentIndex
|
|
314
|
-
? 'bg-white opacity-100'
|
|
315
|
-
: 'bg-white/50'"
|
|
316
|
-
@click="goToSlide(Number(index))"
|
|
317
|
-
/>
|
|
318
|
-
</view>
|
|
319
|
-
</view>
|
|
320
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BannerCarousel.vue
|
|
4
|
+
* 原生实现的 Banner 轮播组件
|
|
5
|
+
* 功能:自动播放、手动滑动切换、鼠标悬停暂停、分页器点击跳转
|
|
6
|
+
*/
|
|
7
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
8
|
+
|
|
9
|
+
/** 从 mockData 获取轮播数据 */
|
|
10
|
+
const banners: any = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
title: '欢迎使用企业门户',
|
|
14
|
+
subtitle: '高效协同,智慧办公',
|
|
15
|
+
bgColor: 'bg-gradient-to-r from-indigo-500 to-purple-600',
|
|
16
|
+
icon: 'building-office',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 2,
|
|
20
|
+
title: '本周会议安排',
|
|
21
|
+
subtitle: '3场会议待参加',
|
|
22
|
+
bgColor: 'bg-gradient-to-r from-emerald-500 to-teal-600',
|
|
23
|
+
icon: 'calendar',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 3,
|
|
27
|
+
title: '系统升级通知',
|
|
28
|
+
subtitle: 'v2.5.0 新功能上线',
|
|
29
|
+
bgColor: 'bg-gradient-to-r from-orange-500 to-amber-600',
|
|
30
|
+
icon: 'sparkles',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 图标映射表
|
|
36
|
+
* 根据 banner.icon 字段映射对应的 Heroicons 组件
|
|
37
|
+
*/
|
|
38
|
+
// const iconMap: Record<string, any> = {
|
|
39
|
+
// 'building-office': BuildingOfficeIcon,
|
|
40
|
+
// 'calendar': CalendarIcon,
|
|
41
|
+
// 'sparkles': SparklesIcon
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
/** 当前显示的轮播项索引 */
|
|
45
|
+
const currentIndex = ref(0)
|
|
46
|
+
|
|
47
|
+
/** 鼠标是否悬停在轮播区域 */
|
|
48
|
+
const isHovered = ref(false)
|
|
49
|
+
|
|
50
|
+
/** 是否正在拖动(手动滑动中) */
|
|
51
|
+
const isDragging = ref(false)
|
|
52
|
+
|
|
53
|
+
/** 拖动偏移量(px),用于实时跟随手指/鼠标移动 */
|
|
54
|
+
const dragOffset = ref(0)
|
|
55
|
+
|
|
56
|
+
/** 自动播放的定时器引用 */
|
|
57
|
+
let autoplayTimer: ReturnType<typeof setInterval> | null = null
|
|
58
|
+
|
|
59
|
+
/** 拖动开始时的 X 坐标 */
|
|
60
|
+
let startX = 0
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 启动自动播放
|
|
64
|
+
* 每 3 秒切换到下一张,循环播放
|
|
65
|
+
*/
|
|
66
|
+
function startAutoplay() {
|
|
67
|
+
if (autoplayTimer)
|
|
68
|
+
clearInterval(autoplayTimer)
|
|
69
|
+
autoplayTimer = setInterval(() => {
|
|
70
|
+
currentIndex.value = (currentIndex.value + 1) % banners.length
|
|
71
|
+
}, 10000)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 停止自动播放
|
|
76
|
+
* 清除定时器并置空引用
|
|
77
|
+
*/
|
|
78
|
+
function stopAutoplay() {
|
|
79
|
+
if (autoplayTimer) {
|
|
80
|
+
clearInterval(autoplayTimer)
|
|
81
|
+
autoplayTimer = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 跳转到指定索引的轮播项
|
|
87
|
+
* @param index 目标索引
|
|
88
|
+
*/
|
|
89
|
+
function goToSlide(index: number) {
|
|
90
|
+
currentIndex.value = index
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 切换到下一张轮播项
|
|
95
|
+
* 使用取模实现循环(最后一张 -> 第一张)
|
|
96
|
+
*/
|
|
97
|
+
function nextSlide() {
|
|
98
|
+
currentIndex.value = (currentIndex.value + 1) % banners.length
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 切换到上一张轮播项
|
|
103
|
+
* 使用取模实现循环(第一张 -> 最后一张)
|
|
104
|
+
*/
|
|
105
|
+
function prevSlide() {
|
|
106
|
+
currentIndex.value = (currentIndex.value - 1 + banners.length) % banners.length
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 鼠标进入轮播区域
|
|
111
|
+
* 停止自动播放
|
|
112
|
+
*/
|
|
113
|
+
function onMouseEnter() {
|
|
114
|
+
isHovered.value = true
|
|
115
|
+
stopAutoplay()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 拖动开始
|
|
120
|
+
* 记录起始 X 坐标,重置拖动偏移量
|
|
121
|
+
* @param clientX 鼠标/触摸的 X 坐标
|
|
122
|
+
*/
|
|
123
|
+
function onDragStart(clientX: number) {
|
|
124
|
+
isDragging.value = true
|
|
125
|
+
startX = clientX
|
|
126
|
+
dragOffset.value = 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 拖动中
|
|
131
|
+
* 实时更新 dragOffset,实现跟随效果
|
|
132
|
+
* @param clientX 鼠标/触摸的当前 X 坐标
|
|
133
|
+
*/
|
|
134
|
+
function onDragMove(clientX: number) {
|
|
135
|
+
if (!isDragging.value)
|
|
136
|
+
return
|
|
137
|
+
dragOffset.value = clientX - startX
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 拖动结束
|
|
142
|
+
* 判断滑动方向和距离:
|
|
143
|
+
* - 左滑 > 50px:切换下一张
|
|
144
|
+
* - 右滑 > 50px:切换上一张
|
|
145
|
+
* - 否则:回弹到原位
|
|
146
|
+
* 拖动结束后恢复自动播放
|
|
147
|
+
*/
|
|
148
|
+
function onDragEnd() {
|
|
149
|
+
if (!isDragging.value)
|
|
150
|
+
return
|
|
151
|
+
isDragging.value = false
|
|
152
|
+
|
|
153
|
+
const threshold = 50
|
|
154
|
+
if (dragOffset.value < -threshold) {
|
|
155
|
+
nextSlide()
|
|
156
|
+
}
|
|
157
|
+
else if (dragOffset.value > threshold) {
|
|
158
|
+
prevSlide()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dragOffset.value = 0
|
|
162
|
+
startAutoplay()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 触摸开始事件(移动端) */
|
|
166
|
+
function onTouchStart(e: TouchEvent) {
|
|
167
|
+
onDragStart(e.touches[0]?.clientX ?? 0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 触摸移动事件(移动端) */
|
|
171
|
+
function onTouchMove(e: TouchEvent) {
|
|
172
|
+
onDragMove(e.touches[0]?.clientX ?? 0)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 触摸结束事件(移动端) */
|
|
176
|
+
function onTouchEnd() {
|
|
177
|
+
onDragEnd()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 鼠标按下事件(桌面端) */
|
|
181
|
+
function onMouseDown(e: MouseEvent) {
|
|
182
|
+
onDragStart(e.clientX)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 鼠标移动事件(桌面端拖动中) */
|
|
186
|
+
function onMouseMove(e: MouseEvent) {
|
|
187
|
+
onDragMove(e.clientX)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 鼠标释放事件(桌面端拖动结束) */
|
|
191
|
+
function onMouseUp() {
|
|
192
|
+
onDragEnd()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 鼠标离开轮播区域
|
|
197
|
+
* 如果正在拖动中则触发拖动结束
|
|
198
|
+
* 恢复自动播放
|
|
199
|
+
*/
|
|
200
|
+
function onMouseLeave() {
|
|
201
|
+
if (isDragging.value) {
|
|
202
|
+
onDragEnd()
|
|
203
|
+
}
|
|
204
|
+
isHovered.value = false
|
|
205
|
+
startAutoplay()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 组件挂载时启动自动播放 */
|
|
209
|
+
onMounted(() => {
|
|
210
|
+
startAutoplay()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
/** 组件卸载时停止自动播放,防止内存泄漏 */
|
|
214
|
+
onUnmounted(() => {
|
|
215
|
+
stopAutoplay()
|
|
216
|
+
})
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<template>
|
|
220
|
+
<!--
|
|
221
|
+
外层容器:
|
|
222
|
+
- select-none:防止拖动时选中文本
|
|
223
|
+
- 监听 mouseenter/mouseleave 用于悬停暂停
|
|
224
|
+
-->
|
|
225
|
+
<view
|
|
226
|
+
class="mt-6 select-none overflow-hidden rounded-2xl pb-2"
|
|
227
|
+
@mouseenter="onMouseEnter"
|
|
228
|
+
@mouseleave="onMouseLeave"
|
|
229
|
+
>
|
|
230
|
+
<!--
|
|
231
|
+
轮播内容区:
|
|
232
|
+
- overflow-hidden:裁剪只显示当前 slide
|
|
233
|
+
- cursor-grab/active:拖动时显示抓取手势
|
|
234
|
+
- 监听 mousedown/mousemove/mouseup(桌面端)
|
|
235
|
+
- 监听 touchstart/touchmove/touchend(移动端)
|
|
236
|
+
-->
|
|
237
|
+
<view
|
|
238
|
+
class="cursor-grab overflow-hidden active:cursor-grabbing"
|
|
239
|
+
@mousedown="onMouseDown"
|
|
240
|
+
@mousemove="onMouseMove"
|
|
241
|
+
@mouseup="onMouseUp"
|
|
242
|
+
@mouseleave="onMouseLeave"
|
|
243
|
+
@touchstart="onTouchStart"
|
|
244
|
+
@touchmove="onTouchMove"
|
|
245
|
+
@touchend="onTouchEnd"
|
|
246
|
+
>
|
|
247
|
+
<!--
|
|
248
|
+
轮播轨道:
|
|
249
|
+
- flex:水平排列所有 slide
|
|
250
|
+
- transition-transform:
|
|
251
|
+
- 拖动中:transition-none(实时跟随,无动画)
|
|
252
|
+
- 正常切换:transition-transform duration-[600ms] ease(600ms 动画)
|
|
253
|
+
- translateX 计算:
|
|
254
|
+
- 基准位置:-${currentIndex * 100}%
|
|
255
|
+
- 拖动偏移:+ ${dragOffset}px
|
|
256
|
+
-->
|
|
257
|
+
<view
|
|
258
|
+
class="flex"
|
|
259
|
+
:class="isDragging ? 'transition-none' : 'transition-transform duration-[600ms] ease'"
|
|
260
|
+
:style="{ transform: `translateX(calc(-${currentIndex * 100}% + ${dragOffset}px))` }"
|
|
261
|
+
>
|
|
262
|
+
<!--
|
|
263
|
+
单个轮播项:
|
|
264
|
+
- w-full flex-shrink-0:每个 slide 占满宽度且不压缩
|
|
265
|
+
-->
|
|
266
|
+
<view
|
|
267
|
+
v-for="banner in banners"
|
|
268
|
+
:key="banner.id"
|
|
269
|
+
class="w-full flex-shrink-0 cursor-pointer"
|
|
270
|
+
>
|
|
271
|
+
<!--
|
|
272
|
+
Banner 内容卡片:
|
|
273
|
+
- 动态背景色(bgColor)
|
|
274
|
+
- h-36:固定高度
|
|
275
|
+
- flex items-center:垂直居中
|
|
276
|
+
- px-6:水平内边距
|
|
277
|
+
- rounded-2xl:圆角
|
|
278
|
+
-->
|
|
279
|
+
<view
|
|
280
|
+
:class="banner.bgColor"
|
|
281
|
+
class="h-36 flex items-center rounded-2xl px-6"
|
|
282
|
+
>
|
|
283
|
+
<!-- 左侧文字区域 -->
|
|
284
|
+
<view class="flex-1">
|
|
285
|
+
<h3 class="text-lg text-white font-semibold">
|
|
286
|
+
{{ banner.title }}
|
|
287
|
+
</h3>
|
|
288
|
+
<p class="mt-1 text-sm text-white/80">
|
|
289
|
+
{{ banner.subtitle }}
|
|
290
|
+
</p>
|
|
291
|
+
</view>
|
|
292
|
+
|
|
293
|
+
<!-- 右侧图标区域 -->
|
|
294
|
+
<view class="h-14 w-14 flex items-center justify-center rounded-full bg-white/20">
|
|
295
|
+
<!-- <component :is="iconMap[banner.icon]" class="w-8 h-8 text-white" /> -->
|
|
296
|
+
<wd-icon name="calendar" size="26px" />
|
|
297
|
+
</view>
|
|
298
|
+
</view>
|
|
299
|
+
</view>
|
|
300
|
+
</view>
|
|
301
|
+
</view>
|
|
302
|
+
|
|
303
|
+
<!--
|
|
304
|
+
分页器:
|
|
305
|
+
- flex justify-center gap-2:水平居中,dot 之间 8px 间距
|
|
306
|
+
- mt-4:与轮播内容保持间距
|
|
307
|
+
-->
|
|
308
|
+
<view class="mt-4 flex justify-center gap-2">
|
|
309
|
+
<view
|
|
310
|
+
v-for="(_, index) in banners"
|
|
311
|
+
:key="index"
|
|
312
|
+
class="h-1 w-1 cursor-pointer rounded-full transition-all duration-300"
|
|
313
|
+
:class="index === currentIndex
|
|
314
|
+
? 'bg-white opacity-100'
|
|
315
|
+
: 'bg-white/50'"
|
|
316
|
+
@click="goToSlide(Number(index))"
|
|
317
|
+
/>
|
|
318
|
+
</view>
|
|
319
|
+
</view>
|
|
320
|
+
</template>
|