| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- <template>
- <div class="steps-scroll-container">
- <!-- 左箭头 -->
- <div v-if="showArrows" class="nav-arrow left-arrow" :class="{ disabled: isScrollStart }" @click="scrollTo('left')">
- <slot name="left-arrow">
- <up-icon name="arrow-left" size="20" />
- </slot>
- </div>
- <!-- 步骤滚动区域 -->
- <scroll-view :scroll-x="true" :scroll-left="scrollLeft" :scroll-with-animation="true" class="steps-scroll-view"
- @scroll="handleScroll" ref="scrollViewRef">
- <up-steps class="custom-steps" :current="current" :active-color="activeColor" :inactive-color="inactiveColor"
- direction="row">
- <up-steps-item class="step-item" v-for="(item, index) in steps" :key="item.id || index"
- :title="renderStepTitle(item, index)" :status="getStepStatus(index)" @click="handleStepClick(index)">
- <!-- 自定义图标插槽 -->
- <template #icon>
- <slot name="icon" :item="item" :index="index">
- <!-- <up-icon :name="getStepIcon(index)" :size="iconSize" /> -->
- </slot>
- </template>
- <!-- 描述内容插槽 -->
- <template #description>
- <slot name="description" :item="item" :index="index">
- <div v-if="showDescription && index === current" class="step-description">
- {{ item.description }}
- </div>
- </slot>
- </template>
- </up-steps-item>
- </up-steps>
- </scroll-view>
- <!-- 右箭头 -->
- <div v-if="showArrows" class="nav-arrow right-arrow" :class="{ disabled: isScrollEnd }" @click="scrollTo('right')">
- <slot name="right-arrow">
- <up-icon name="arrow-right" size="20" />
- </slot>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, nextTick, computed } from 'vue'
- const props = defineProps({
- steps: {
- type: Array,
- required: true,
- default: () => []
- },
- current: {
- type: Number,
- default: 0
- },
- activeColor: {
- type: String,
- default: '#1890ff'
- },
- inactiveColor: {
- type: String,
- default: '#d9d9d9'
- },
- iconSize: {
- type: [Number, String],
- default: 16
- },
- maxTitleLength: {
- type: Number,
- default: 56
- },
- showDescription: {
- type: Boolean,
- default: true
- },
- showArrows: {
- type: Boolean,
- default: false
- },
- scrollStep: {
- type: Number,
- default: 200
- }
- })
- const emit = defineEmits(['step-click'])
- const scrollViewRef = ref(null)
- const scrollLeft = ref(0)
- const maxScrollLeft = ref(0)
- const isScrollStart = ref(true)
- const isScrollEnd = ref(false)
- // 渲染步骤标题(带序号和省略)
- const renderStepTitle = (item, index) => {
- const title = item.name || item.title || `${index + 1}`
- const maxLen = props.maxTitleLength
- return `${title.length > maxLen ? title.substring(0, maxLen) + '...' : title}`
- }
- // 获取步骤状态
- const getStepStatus = (index) => {
- if (index < props.current) return 'finish'
- if (index === props.current) return 'process'
- return 'wait'
- }
- // 获取步骤图标
- const getStepIcon = (index) => {
- const defaultIcons = ['check-circle', 'edit-pen', 'setting', 'user', 'more']
- return props.steps[index]?.icon || defaultIcons[index % defaultIcons.length]
- }
- // 处理步骤点击
- const handleStepClick = (index) => {
- emit('step-click', { index, step: props.steps[index] })
- }
- // 处理滚动事件
- const handleScroll = (e) => {
- scrollLeft.value = e.detail.scrollLeft
- isScrollStart.value = scrollLeft.value <= 0
- isScrollEnd.value = scrollLeft.value >= maxScrollLeft.value - 10
- }
- // 滚动到指定方向
- const scrollTo = (direction) => {
- if ((direction === 'left' && isScrollStart.value)) return
- if ((direction === 'right' && isScrollEnd.value)) return
- let newScrollLeft = scrollLeft.value
- if (direction === 'left') {
- newScrollLeft = Math.max(0, scrollLeft.value - props.scrollStep)
- } else {
- newScrollLeft = Math.min(maxScrollLeft.value, scrollLeft.value + props.scrollStep)
- }
- scrollLeft.value = newScrollLeft
- }
- // 计算最大滚动距离
- const calculateMaxScroll = async () => {
- await nextTick()
- if (!scrollViewRef.value) return
- const query = uni.createSelectorQuery().in(scrollViewRef.value)
- query.select('.custom-steps').boundingClientRect(data => {
- const stepsWidth = data?.width || 0
- query.select('.steps-scroll-view').boundingClientRect(data => {
- const containerWidth = data?.width || 0
- maxScrollLeft.value = stepsWidth - containerWidth
- isScrollEnd.value = maxScrollLeft.value <= 0
- }).exec()
- }).exec()
- }
- // 暴露方法
- defineExpose({
- scrollTo,
- calculateMaxScroll
- })
- onMounted(() => {
- calculateMaxScroll()
- // 监听窗口变化重新计算
- uni.onWindowResize(() => {
- calculateMaxScroll()
- })
- })
- </script>
- <style lang="less" scoped>
- .steps-scroll-container {
- position: relative;
- width: 100%;
- display: flex;
- align-items: center;
- padding: 0 36px; // 为箭头留出空间
- box-sizing: border-box;
- .steps-scroll-view {
- flex: 1;
- width: 100%;
- white-space: nowrap;
- padding: 12px 0;
- scroll-behavior: smooth;
- .custom-steps {
- display: inline-flex;
- min-width: 100%;
- .step-item {
- min-width: 120px;
- max-width: 180px;
- padding: 0 12px;
- box-sizing: border-box;
- white-space: normal;
- word-break: break-word;
- cursor: pointer;
- transition: all 0.3s;
- &:hover {
- transform: translateY(-2px);
- }
- &:deep(.up-steps-item-title) {
- font-size: 14px;
- line-height: 1.4;
- color: #333;
- font-weight: 500;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- &:deep(.up-steps-item-description) {
- font-size: 12px;
- color: #666;
- margin-top: 4px;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- }
- }
- .nav-arrow {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fff;
- border-radius: 50%;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- color: v-bind('props.activeColor');
- cursor: pointer;
- transition: all 0.3s;
- z-index: 1;
- &:hover {
- background: #f0f7ff;
- }
- &.disabled {
- color: #ccc;
- cursor: not-allowed;
- background: #f5f5f5;
- }
- &.left-arrow {
- left: 0;
- }
- &.right-arrow {
- right: 0;
- }
- }
- }
- </style>
|