StepItem.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <template>
  2. <view
  3. :style="{
  4. ...innerStyle,
  5. ...(context.direction.value === 'vertical' ?
  6. themeStyles.itemVertical.value :
  7. themeStyles.itemHorizontal.value) ,
  8. flexBasis: context.direction.value === 'horizontal' ?
  9. themeContext.resolveSize(context.lineItemWidth.value) :
  10. undefined,
  11. width: context.direction.value === 'horizontal' ?
  12. themeContext.resolveSize(context.lineItemWidth.value) :
  13. undefined,
  14. }"
  15. >
  16. <view
  17. :style="{
  18. ...themeStyles.iconContainer.value,
  19. width: themeContext.resolveSize(iconConSize),
  20. height: themeContext.resolveSize(iconSize)
  21. }"
  22. >
  23. <StepItemInternalDotNumberIcon
  24. v-if="useDefaultIcon && inactiveIcon === '__default_number'"
  25. :index="index + 1"
  26. :size="(iconProps.size as number)"
  27. :color="state === 'inactive' ? context.inactiveColor.value : context.activeColor.value"
  28. />
  29. <StepItemInternalDotIcon
  30. v-else-if="useDefaultIcon && inactiveIcon === '__default_dot'"
  31. :size="(iconProps.size as number)"
  32. :color="state === 'inactive' ? context.inactiveColor.value : context.activeColor.value" />
  33. <Icon
  34. v-else
  35. :color="state === 'active' || state === 'finish' ? context.activeColor.value : context.inactiveColor.value"
  36. :icon="state === 'active' ? (activeIcon || inactiveIcon) : (state === 'finish' ? finishIcon : inactiveIcon)"
  37. v-bind="iconProps"
  38. />
  39. </View>
  40. <view :style="themeStyles.content.value">
  41. <text
  42. :style="{
  43. color: context.textColor.value,
  44. ...themeStyles.text.value,
  45. ...textStyle,
  46. }"
  47. >
  48. {{ text }}
  49. </text>
  50. <slot name="extra">
  51. <text v-if="extra" :style="{
  52. color: context.textColor.value,
  53. ...themeStyles.text.value,
  54. ...textStyle,
  55. }">
  56. {{ extra }}
  57. </text>
  58. </slot>
  59. </view>
  60. <!-- 渲染垂直线段 -->
  61. <view
  62. v-if="context.direction.value === 'vertical' && !isLast"
  63. :style="{
  64. position: 'absolute',
  65. left: themeContext.resolveSize(iconConSize / 2 - context.lineWidth.value! / 2),
  66. top: themeContext.resolveSize(iconConSize - 2),
  67. bottom: themeContext.resolveSize(-iconConSize / 2),
  68. backgroundColor: themeContext.resolveThemeColor(state === 'finish' ? context.activeColor.value : context.inactiveColor.value),
  69. width: themeContext.resolveSize(context.lineWidth.value!),
  70. }"
  71. />
  72. </view>
  73. <!-- 水平条目之间还需要渲染线段 -->
  74. <view
  75. v-if="context.direction.value === 'horizontal' && !isLast"
  76. :style="{
  77. position: 'absolute',
  78. left: themeContext.resolveSize((index + 1) * context.lineItemWidth.value! - context.lineItemWidth.value! / 4),
  79. top: themeContext.resolveSize(context.lineOffset.value!),
  80. backgroundColor: themeContext.resolveThemeColor(context.activeIndex.value > index ? context.activeColor.value : context.inactiveColor.value),
  81. height: themeContext.resolveSize(context.lineWidth.value!),
  82. width: themeContext.resolveSize(context.lineItemWidth.value! / 2),
  83. }"
  84. />
  85. </template>
  86. <script setup lang="ts">
  87. import { computed, inject, onMounted, onUpdated, ref } from 'vue';
  88. import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  89. import { DynamicSize } from '../theme/ThemeTools';
  90. import type { StepContext } from './Step.vue';
  91. import type { IconProps } from '../basic/Icon.vue';
  92. import Icon from '../basic/Icon.vue';
  93. import StepItemInternalDotIcon from './step/StepItemInternalDotIcon.vue';
  94. import StepItemInternalDotNumberIcon from './step/StepItemInternalDotNumberIcon.vue';
  95. const themeContext = useTheme();
  96. export type StepItemState = 'inactive'|'active'|'finish';
  97. export interface StepItemProps {
  98. /**
  99. * 自定义激活状态图标。为空时尝试使用 inactiveIcon 的值。
  100. */
  101. activeIcon?: string,
  102. /**
  103. * 自定义未激活状态图标。有2个特殊值 `__default_number` 表示一个圆圈中间一个当前步骤的序号;`__default_dot` 表示一个圆圈。
  104. * @default 横向默认是 '__default_number',竖向默认是 '__default_dot'
  105. */
  106. inactiveIcon?: string,
  107. /**
  108. * 自定义已完成步骤对应的底部图标,优先级高于 `inactiveIcon`
  109. * @default 'success-filling'
  110. */
  111. finishIcon?: string,
  112. /**
  113. * 图标的附加属性
  114. * @default { size: 48 }
  115. */
  116. iconProps?: IconProps;
  117. /**
  118. * 步骤的文字自定义样式
  119. */
  120. textStyle?: TextStyle,
  121. /**
  122. * 步骤的自定义样式
  123. */
  124. innerStyle?: ViewStyle,
  125. /**
  126. * 当前步骤的文字
  127. */
  128. text?: string;
  129. /**
  130. * 垂直模式下,允许你渲染附加内容
  131. */
  132. extra?: string;
  133. }
  134. const context = inject('StepContext') as StepContext;
  135. const emit = defineEmits([ 'click' ]);
  136. const props = withDefaults(defineProps<StepItemProps>(), {
  137. iconProps: () => ({ size: propGetThemeVar('StepItemIconDefaultSize', 48) }),
  138. activeIcon: () => propGetThemeVar('StepItemActiveIcon', ''),
  139. finishIcon: () => propGetThemeVar('StepItemFinishIcon', 'success-filling'),
  140. inactiveIcon: () => (inject('StepContext') as StepContext).direction.value === 'horizontal' ?
  141. '__default_number' : '__default_dot',
  142. });
  143. const index = computed(() => context.getPosition());
  144. const useDefaultIcon = computed(() => (state.value === 'inactive' || (state.value === 'active' && !props.activeIcon)));
  145. const iconConSize = computed(() => props.iconProps.size as number + 0);
  146. const iconSize = computed(() => props.iconProps.size as number);
  147. const state = computed(() => context.activeIndex.value === index.value ? 'active' : (context.activeIndex.value > index.value ? 'finish' : 'inactive'));
  148. const isLast = ref(false);
  149. function updateWithCount() {
  150. isLast.value = index.value >= context.getLength() - 1;
  151. }
  152. onUpdated(updateWithCount);
  153. onMounted(updateWithCount);
  154. const themeStyles = themeContext.useThemeStyles({
  155. itemVertical: {
  156. display: 'flex',
  157. position: 'relative',
  158. flexDirection: 'row',
  159. justifyContent: 'flex-start',
  160. alignItems: 'flex-start',
  161. marginVertical: DynamicSize('StepItemMarginVertical', 10),
  162. },
  163. itemHorizontal: {
  164. flex: '0 0 0%',
  165. display: 'flex',
  166. position: 'relative',
  167. flexDirection: 'column',
  168. justifyContent: 'center',
  169. alignItems: 'center',
  170. },
  171. text: {
  172. fontSize: DynamicSize('StepItemTextFontSize', 26),
  173. },
  174. content: {
  175. fontSize: DynamicSize('StepItemContentFontSize', 26),
  176. marginTop: DynamicSize('StepItemContentMarginTop', 6),
  177. marginLeft: DynamicSize('StepItemContentMarginLeft', 6),
  178. display: 'flex',
  179. flexDirection: 'column',
  180. justifyContent: 'center',
  181. alignItems: 'flex-start',
  182. },
  183. iconContainer: {
  184. display: 'flex',
  185. flexDirection: 'row',
  186. justifyContent: 'center',
  187. alignItems: 'center',
  188. },
  189. });
  190. </script>