wl-ripple.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <template>
  2. <view
  3. class="ripple-container"
  4. :class="[wlType ? 'ripple-right' : 'ripple-error', { 'ripple-animate': !imageError }]"
  5. :style="rippleStyle"
  6. @click="handleClick"
  7. >
  8. <view class="custom-content" :style="contentStyle">
  9. <!-- 图片类型 -->
  10. <image
  11. v-if="contentType === 'image'"
  12. :src="resolvedImageSrc"
  13. class="custom-image"
  14. @error="handleImageError"
  15. />
  16. <!-- 自定义视图类型 -->
  17. <view v-else-if="contentType === 'view'" class="custom-view">
  18. <slot name="custom-content"></slot>
  19. </view>
  20. <!-- 图标类型 -->
  21. <view v-else class="custom-icon" v-html="customContent"></view>
  22. </view>
  23. </view>
  24. </template>
  25. <script>
  26. // 兼容 UniApp 和 UniAppX 的写法
  27. export default {
  28. name: "WlRipple",
  29. props: {
  30. wlType: {
  31. type: Boolean,
  32. default: true
  33. },
  34. customContent: {
  35. type: String,
  36. default: '<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'
  37. },
  38. contentType: {
  39. type: String,
  40. default: 'icon',
  41. validator: (value) => ['icon', 'image', 'view'].includes(value)
  42. },
  43. rippleColor: {
  44. type: String,
  45. default: '#FFF'
  46. },
  47. iconColor: {
  48. type: String,
  49. default: '#008000'
  50. },
  51. rippleEffectColor: {
  52. type: String,
  53. default: 'rgba(0,128,0,0.1)'
  54. },
  55. width: {
  56. type: [Number, String],
  57. default: 100
  58. },
  59. height: {
  60. type: [Number, String],
  61. default: 100
  62. },
  63. contentPadding: {
  64. type: [Number, String],
  65. default: 0
  66. },
  67. maxRippleSize: {
  68. type: [Number, String],
  69. default: 80
  70. }
  71. },
  72. data() {
  73. return {
  74. imageError: false
  75. };
  76. },
  77. computed: {
  78. rippleStyle() {
  79. return {
  80. backgroundColor: this.rippleColor,
  81. '--ripple-color': this.rippleEffectColor,
  82. '--max-ripple-size': this.px(this.maxRippleSize),
  83. width: this.px(this.width),
  84. height: this.px(this.height),
  85. borderRadius: this.px(this.width)
  86. };
  87. },
  88. contentStyle() {
  89. return {
  90. color: this.iconColor,
  91. padding: this.px(this.contentPadding)
  92. };
  93. },
  94. resolvedImageSrc() {
  95. if (this.contentType !== 'image') return '';
  96. return this.resolvePath(this.customContent);
  97. }
  98. },
  99. methods: {
  100. px(value) {
  101. // 处理 UniAppX 和 UniApp 的单位差异
  102. if (typeof value === 'string' && value.endsWith('px')) {
  103. return value;
  104. }
  105. return `${value}px`;
  106. },
  107. resolvePath(path) {
  108. if (!path || path === '') return '';
  109. if (path.startsWith('http')) return path;
  110. let normalizedPath = path.replace(/^@\/|^\/static\/|^static\//, '');
  111. normalizedPath = `/static/${normalizedPath}`;
  112. return normalizedPath;
  113. },
  114. handleImageError(e) {
  115. console.error('Image load error:', e.detail || e);
  116. this.imageError = true;
  117. this.$emit('image-error', e);
  118. },
  119. handleClick(e) {
  120. this.$emit('click', e);
  121. }
  122. }
  123. };
  124. </script>
  125. <style scoped>
  126. /* 基础样式 */
  127. .ripple-container {
  128. position: relative;
  129. overflow: hidden;
  130. display: inline-flex;
  131. align-items: center;
  132. justify-content: center;
  133. }
  134. /* 水波动画 */
  135. .ripple-animate {
  136. animation: ripple 0.6s linear infinite;
  137. }
  138. @keyframes ripple {
  139. 0% {
  140. box-shadow:
  141. 0 0 0 0 var(--ripple-color),
  142. 0 0 0 calc(var(--max-ripple-size) / 4) var(--ripple-color),
  143. 0 0 0 calc(var(--max-ripple-size) / 2) var(--ripple-color),
  144. 0 0 0 calc(var(--max-ripple-size) * 3 / 4) var(--ripple-color);
  145. }
  146. 100% {
  147. box-shadow:
  148. 0 0 0 calc(var(--max-ripple-size) / 4) var(--ripple-color),
  149. 0 0 0 calc(var(--max-ripple-size) / 2) var(--ripple-color),
  150. 0 0 0 calc(var(--max-ripple-size) * 3 / 4) var(--ripple-color),
  151. 0 0 0 var(--max-ripple-size) rgba(0,0,0,0);
  152. }
  153. }
  154. /* 内容区域 */
  155. .custom-content {
  156. width: 100%;
  157. height: 100%;
  158. display: flex;
  159. align-items: center;
  160. justify-content: center;
  161. box-sizing: border-box;
  162. }
  163. /* 图标样式 */
  164. .custom-icon >>> svg {
  165. width: 100%;
  166. height: 100%;
  167. fill: currentColor;
  168. }
  169. /* 图片样式 */
  170. .custom-image {
  171. width: 100%;
  172. height: 100%;
  173. border-radius: 50%;
  174. object-fit: cover;
  175. }
  176. /* 自定义视图样式 */
  177. .custom-view {
  178. width: 100%;
  179. height: 100%;
  180. display: flex;
  181. align-items: center;
  182. justify-content: center;
  183. }
  184. </style>