index.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. <template>
  2. <movable-area class="movable-area">
  3. <movable-view
  4. class="movable-view"
  5. direction="all"
  6. :x="snapPos.x"
  7. :y="snapPos.y"
  8. :animation="true"
  9. @change="onDrag"
  10. @touchend="onTouchEnd"
  11. >
  12. <view class="cs-button">
  13. <uv-icon name="kefu-ermai" color="#FFFFFF" size="24"></uv-icon>
  14. </view>
  15. <view class="contact-btn" @click.stop="handleContactClick" />
  16. </movable-view>
  17. </movable-area>
  18. </template>
  19. <script setup lang="ts">
  20. import { ref } from 'vue'
  21. const POS_KEY = 'kefuPos'
  22. /** 客服在线时间 9:30 ~ 21:30 */
  23. const isInServiceHours = (): boolean => {
  24. const now = new Date()
  25. const current = now.getHours() * 60 + now.getMinutes()
  26. return current >= 9 * 60 + 30 && current <= 21 * 60 + 30
  27. }
  28. /* 实时拖拽位置(非响应式,避免与原生拖拽冲突) */
  29. let dragX = 600
  30. let dragY = 800
  31. /* 初始位置从缓存读取 */
  32. try {
  33. const last = uni.getStorageSync(POS_KEY)
  34. if (last) {
  35. dragX = last.x
  36. dragY = last.y
  37. }
  38. } catch {}
  39. const snapPos = ref({ x: dragX, y: dragY })
  40. let windowInfo: any = null
  41. function getWindowInfo() {
  42. if (!windowInfo) {
  43. windowInfo = uni.getWindowInfo()
  44. }
  45. return windowInfo
  46. }
  47. function onDrag(e: any) {
  48. dragX = e.detail.x
  49. dragY = e.detail.y
  50. }
  51. function onTouchEnd() {
  52. const { windowWidth, windowHeight } = getWindowInfo()
  53. const btnPx = windowWidth * 104 / 750
  54. const maxX = windowWidth - btnPx
  55. const snapX = dragX < maxX / 2 ? 0 : maxX
  56. const snapY = Math.max(0, Math.min(dragY, windowHeight - btnPx))
  57. snapPos.value = { x: snapX, y: snapY }
  58. uni.setStorageSync(POS_KEY, { x: snapX, y: snapY })
  59. }
  60. const openCustomerService = () => {
  61. // #ifdef MP-WEIXIN
  62. wx.openCustomerServiceConversation({
  63. sessionFrom: '',
  64. showMessageCard: true,
  65. sendMessageTitle: 'YesWash洗车客服'
  66. })
  67. // #endif
  68. }
  69. const handleContactClick = () => {
  70. if (isInServiceHours()) {
  71. openCustomerService()
  72. return
  73. }
  74. uni.showModal({
  75. title: '客服提示',
  76. content: '客服在线时间 9:30~21:30,当前留言可能会延迟回复',
  77. cancelText: '取消',
  78. confirmText: '继续留言',
  79. success: (res) => {
  80. if (res.confirm) {
  81. openCustomerService()
  82. }
  83. }
  84. })
  85. }
  86. </script>
  87. <style scoped lang="scss">
  88. .movable-area {
  89. position: fixed;
  90. left: 0;
  91. top: 0;
  92. width: 100vw;
  93. height: 100vh;
  94. pointer-events: none;
  95. z-index: 999999;
  96. }
  97. .movable-view {
  98. width: 104rpx;
  99. height: 104rpx;
  100. pointer-events: auto;
  101. position: relative;
  102. }
  103. .cs-button {
  104. width: 96rpx;
  105. height: 96rpx;
  106. border-radius: 50%;
  107. background: #C6171E;
  108. display: flex;
  109. align-items: center;
  110. justify-content: center;
  111. box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.28);
  112. position: absolute;
  113. left: 0;
  114. top: 0;
  115. transition: transform 0.15s ease;
  116. &:active {
  117. transform: scale(0.92);
  118. }
  119. }
  120. .contact-btn {
  121. position: absolute;
  122. left: 0;
  123. top: 0;
  124. width: 104rpx;
  125. height: 104rpx;
  126. opacity: 0;
  127. z-index: 2;
  128. padding: 0;
  129. margin: 0;
  130. }
  131. </style>