LeveragePopup.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <template>
  2. <Teleport to="body">
  3. <transition name="fade">
  4. <div
  5. v-if="visible"
  6. class="mask"
  7. @click="handleClose"
  8. ></div>
  9. </transition>
  10. <transition name="slide-up">
  11. <div v-if="visible" class="popup-container">
  12. <div class="drag-handle-area">
  13. <div class="drag-handle"></div>
  14. </div>
  15. <div class="popup-content">
  16. <h3 class="title">切换倍数</h3>
  17. <div class="stepper-box">
  18. <button class="step-btn minus" @click="decrement" :disabled="currentIndex <= 0">
  19. <span class="icon">−</span>
  20. </button>
  21. <div class="value-display">{{ currentValue }}x</div>
  22. <button class="step-btn plus" @click="increment" :disabled="currentIndex >= marks.length - 1">
  23. <span class="icon">+</span>
  24. </button>
  25. </div>
  26. <div class="slider-container">
  27. <div class="slider-track-bg"></div>
  28. <div
  29. class="slider-track-active"
  30. :style="{ width: getProgressPercent + '%' }"
  31. ></div>
  32. <div
  33. class="slider-thumb"
  34. :style="{ left: getProgressPercent + '%' }"
  35. @touchstart="startDrag"
  36. >
  37. <div class="inner-circle"></div>
  38. </div>
  39. <input
  40. type="range"
  41. min="0"
  42. :max="marks.length - 1"
  43. step="1"
  44. v-model="currentIndex"
  45. class="native-input"
  46. />
  47. </div>
  48. <div class="slider-labels">
  49. <span
  50. v-for="(mark, index) in marks"
  51. :key="mark"
  52. :class="{ active: index === currentIndex }"
  53. >
  54. {{ mark }}x
  55. </span>
  56. </div>
  57. <button class="confirm-btn" @click="confirmSelection">
  58. 确认
  59. </button>
  60. </div>
  61. </div>
  62. </transition>
  63. </Teleport>
  64. </template>
  65. <script setup>
  66. import { ref, computed, watch } from 'vue';
  67. const props = defineProps({
  68. visible: {
  69. type: Boolean,
  70. default: false
  71. },
  72. initialValue: {
  73. type: Number,
  74. default: 50
  75. }
  76. });
  77. const emit = defineEmits(['update:visible', 'confirm']);
  78. // 定义倍数阶梯 (非线性)
  79. const marks = [10, 50, 100, 500, 1000];
  80. // 当前选中的索引 (对应 marks 数组)
  81. const currentIndex = ref(1);
  82. // 监听弹窗打开,初始化数值
  83. watch(() => props.visible, (newVal) => {
  84. if (newVal) {
  85. const idx = marks.indexOf(props.initialValue);
  86. currentIndex.value = idx !== -1 ? idx : 1; // 默认选中50x或传入值
  87. }
  88. });
  89. // 计算当前显示的具体倍数值
  90. const currentValue = computed(() => marks[currentIndex.value]);
  91. // 计算进度条百分比
  92. const getProgressPercent = computed(() => {
  93. return (currentIndex.value / (marks.length - 1)) * 100;
  94. });
  95. // 步进器逻辑
  96. const increment = () => {
  97. if (currentIndex.value < marks.length - 1) currentIndex.value++;
  98. };
  99. const decrement = () => {
  100. if (currentIndex.value > 0) currentIndex.value--;
  101. };
  102. // 关闭弹窗
  103. const handleClose = () => {
  104. emit('update:visible', false);
  105. };
  106. // 确认选择
  107. const confirmSelection = () => {
  108. emit('confirm', currentValue.value);
  109. handleClose();
  110. };
  111. </script>
  112. <style scoped>
  113. /* 动画效果 */
  114. .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
  115. .fade-enter-from, .fade-leave-to { opacity: 0; }
  116. .slide-up-enter-active, .slide-up-leave-active { transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1); }
  117. .slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%); }
  118. /* 遮罩层 */
  119. .mask {
  120. position: fixed;
  121. top: 0; left: 0; right: 0; bottom: 0;
  122. background: rgba(0, 0, 0, 0.5);
  123. z-index: 998;
  124. }
  125. /* 弹窗容器 */
  126. .popup-container {
  127. position: fixed;
  128. bottom: 0; left: 0; right: 0;
  129. background: #ffffff;
  130. border-top-left-radius: 16px;
  131. border-top-right-radius: 16px;
  132. z-index: 999;
  133. padding-bottom: env(safe-area-inset-bottom);
  134. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  135. }
  136. /* 拖拽条 */
  137. .drag-handle-area {
  138. padding: 10px 0;
  139. display: flex;
  140. justify-content: center;
  141. }
  142. .drag-handle {
  143. width: 40px;
  144. height: 4px;
  145. background: #E0E0E0;
  146. border-radius: 2px;
  147. }
  148. .popup-content {
  149. padding: 0 20px 20px 20px;
  150. }
  151. .title {
  152. font-size: 18px;
  153. font-weight: 600;
  154. color: #333;
  155. margin: 0 0 20px 0;
  156. }
  157. /* 步进器样式 */
  158. .stepper-box {
  159. display: flex;
  160. align-items: center;
  161. justify-content: space-between;
  162. background: #F7F8FA;
  163. border-radius: 8px;
  164. padding: 5px;
  165. margin-bottom: 30px;
  166. height: 48px;
  167. }
  168. .step-btn {
  169. border: none;
  170. background: transparent;
  171. width: 48px;
  172. height: 100%;
  173. font-size: 24px;
  174. color: #999;
  175. cursor: pointer;
  176. display: flex;
  177. align-items: center;
  178. justify-content: center;
  179. }
  180. .step-btn:disabled { opacity: 0.3; }
  181. .step-btn:active { opacity: 0.6; }
  182. .value-display {
  183. font-size: 18px;
  184. font-weight: 500;
  185. color: #333;
  186. }
  187. /* 滑块样式核心 */
  188. .slider-container {
  189. position: relative;
  190. height: 30px; /* 增加触控区域 */
  191. display: flex;
  192. align-items: center;
  193. margin-bottom: 10px;
  194. }
  195. .slider-track-bg {
  196. position: absolute;
  197. left: 0; right: 0;
  198. height: 6px;
  199. background: #F2F3F5;
  200. border-radius: 3px;
  201. }
  202. .slider-track-active {
  203. position: absolute;
  204. left: 0;
  205. height: 6px;
  206. background: #E54755; /* 主红色 */
  207. border-radius: 3px;
  208. pointer-events: none;
  209. }
  210. .slider-thumb {
  211. position: absolute;
  212. width: 24px;
  213. height: 24px;
  214. background: #fff;
  215. border: 2px solid #E54755;
  216. border-radius: 50%;
  217. transform: translateX(-50%); /* 居中对齐 */
  218. display: flex;
  219. align-items: center;
  220. justify-content: center;
  221. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  222. pointer-events: none; /* 让点击事件穿透到 input */
  223. z-index: 2;
  224. }
  225. .inner-circle {
  226. width: 0px; /* 图片中看起来是实心的或者空心的,这里做个空心白底红圈 */
  227. height: 0px;
  228. }
  229. /* 原生 Input 覆盖在上面负责交互,但完全透明 */
  230. .native-input {
  231. position: absolute;
  232. width: 100%;
  233. height: 100%;
  234. opacity: 0;
  235. cursor: pointer;
  236. z-index: 3;
  237. margin: 0;
  238. }
  239. /* 刻度标签 */
  240. .slider-labels {
  241. display: flex;
  242. justify-content: space-between;
  243. color: #9FA2A8;
  244. font-size: 12px;
  245. margin-bottom: 30px;
  246. padding: 0 2px; /* 微微修正对齐 */
  247. }
  248. /* 确认按钮 */
  249. .confirm-btn {
  250. width: 100%;
  251. height: 48px;
  252. background: #E54755;
  253. color: white;
  254. border: none;
  255. border-radius: 24px;
  256. font-size: 16px;
  257. font-weight: 600;
  258. cursor: pointer;
  259. }
  260. .confirm-btn:active {
  261. background: #D13E4A;
  262. }
  263. </style>