zui-svg-icon.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <template>
  2. <view class="zui-svg-icon">
  3. <view :class="clazz" :style="style">
  4. <!-- #ifndef H5 -->
  5. <view class="click-helper" @click="doClick" @tap="doTap"></view>
  6. <!-- #endif -->
  7. <image class="zui-svg-icon-image" :src="svgDataurl" mode="aspectFit"></image>
  8. </view>
  9. </view>
  10. </template>
  11. <script>
  12. import SvgIconLib from '@/static/svg-icons-lib.js'
  13. import { rpx2px } from '../../utils/utils'
  14. export default {
  15. name: 'zui-svg-icon',
  16. components: {},
  17. props: {
  18. icon: {
  19. type: String,
  20. required: true,
  21. },
  22. /**
  23. * 图标颜色
  24. *
  25. * 单色图标: '#FFF'
  26. *
  27. * 多色图标: ['#FFF', '#FFF', '#FFF'], 颜色数量需要匹配
  28. */
  29. color: [String, Array],
  30. width: {
  31. type: [Number, String],
  32. default: '1.2em',
  33. },
  34. height: {
  35. type: [Number, String],
  36. default: undefined,
  37. },
  38. /**
  39. * 图标灰度系数
  40. * boolean true => 1
  41. * number => [0, 1]
  42. */
  43. gray: {
  44. type: [Boolean, Number],
  45. default: false,
  46. },
  47. /**
  48. * 图标旋转
  49. *
  50. * Boolean, true 旋转, 旋转一周时间 1s; false 不旋转, 默认值
  51. * Number, 旋转一周所需要时间. > 0 顺时针旋转; < 0 逆时针旋转;
  52. */
  53. spin: {
  54. type: [Number, Boolean],
  55. default: false,
  56. },
  57. /**
  58. * 圆角设置
  59. */
  60. borderRadius: [Number, String],
  61. /**
  62. * 图标集合
  63. */
  64. collection: {
  65. type: String,
  66. default: 'default',
  67. },
  68. /**
  69. * 宽高比
  70. * aspectRatio = width / height
  71. *
  72. * @deprecated 直接使用宽高设置尺寸
  73. *
  74. */
  75. aspectRatio: {
  76. type: Number,
  77. default: undefined,
  78. },
  79. },
  80. data() {
  81. return {
  82. isFilled: false,
  83. colorMap: {},
  84. colorPlaceholder: null,
  85. isColorCountMatch: true,
  86. }
  87. },
  88. computed: {
  89. cWidth() {
  90. const wid = /rpx$/i.test(this.width) ? rpx2px(this.width, true) : this.width
  91. return typeof wid === 'number' ? `${wid}px` : wid
  92. },
  93. cHeight() {
  94. if (!this.height) {
  95. if (this.aspectRatio) {
  96. const unit = `${this.cWidth}`.replace(/[\d.]+/g, '')
  97. const hei = parseFloat(this.cWidth) / this.aspectRatio
  98. return `${hei}${unit}`
  99. } else {
  100. return this.cWidth
  101. }
  102. }
  103. const hei = /rpx$/i.test(this.height) ? rpx2px(this.height, true) : this.height
  104. return typeof hei === 'number' ? `${hei}px` : hei
  105. },
  106. svgIconLib() {
  107. return SvgIconLib.getCollection(this.collection || 'default')
  108. },
  109. /**
  110. * 是否文件来源
  111. *
  112. * 包含 url, svg原始字符串, 未进行 base64 编码的 data:image/svg+xml uri
  113. */
  114. isFileSource() {
  115. if (/^https?\:\/\//i.test(this.icon)) return true
  116. if (/^data:image\//i.test(this.icon)) return true
  117. if (/\.svg([?#].*)?$/i.test(this.icon)) return true
  118. if (this.icon.indexOf('/') > -1) return true
  119. return false
  120. },
  121. svgRaw() {
  122. if (this.isFileSource) return this.icon
  123. const iconId = this.icon.toLowerCase()
  124. const iconPreset = this.svgIconLib.icons[iconId]
  125. if (!iconPreset) {
  126. console.warn(`Svg icon [${iconId}] not defined and no fallback icon set.`)
  127. return
  128. }
  129. let svg = iconPreset[0]
  130. if (this.color && this.isColorCountMatch) {
  131. svg = svg.replace(this.colorPlaceholder, (_, a, b) => {
  132. return this.colorMap[a.toLowerCase()] + b
  133. })
  134. }
  135. return svg
  136. },
  137. svgDataurl() {
  138. if (!this.isFileSource) {
  139. return `data:image/svg+xml,${encodeURIComponent(this.svgRaw)}`
  140. }
  141. if (/^data:image\/svg\+xml,<svg/i.test(this.icon)) {
  142. return `data:image/svg+xml,${encodeURIComponent(this.icon.substring(19))}`
  143. }
  144. if (/^<svg/i.test(this.icon)) {
  145. return `data:image/svg+xml,${encodeURIComponent(this.icon)}`
  146. }
  147. return this.icon
  148. },
  149. clazz() {
  150. const clazz = ['zui-svg-icon-wrapper']
  151. if (this.spin && this.spin > 0) clazz.push('rotate-clockwise')
  152. if (this.spin && this.spin < 0) clazz.push('rotate-counterclockwise')
  153. // 必须转换成字符串, 不然 支付宝小程序 会以逗号连接类名导致错误
  154. return clazz.join(' ')
  155. },
  156. style() {
  157. const style = {
  158. '--zui-svg-icon-width': this.cWidth,
  159. '--zui-svg-icon-height': this.cHeight,
  160. }
  161. if (this.borderRadius) {
  162. let br = this.borderRadius
  163. if (typeof this.borderRadius === 'string') {
  164. if (!/[^a-z%]/i.test(this.borderRadius)) {
  165. const v = parseFloat(this.borderRadius)
  166. if (v < 1) {
  167. br = `${v * 100}%`
  168. } else {
  169. br = `${v}px`
  170. }
  171. }
  172. } else {
  173. if (this.borderRadius < 1) {
  174. br = `${this.borderRadius * 100}%`
  175. } else {
  176. br = `${this.borderRadius}px`
  177. }
  178. }
  179. style['--zui-svg-icon-border-radius'] = br
  180. }
  181. if (this.gray) {
  182. if (typeof this.gray === 'number') {
  183. style['filter'] = `grayscale(${this.gray})`
  184. } else {
  185. style['filter'] = 'grayscale(1)'
  186. }
  187. }
  188. if (this.spin) {
  189. const rotateDur = this.spin === true ? 5 : Math.abs(this.spin)
  190. style['--zui-svg-icon-rotate-duration'] = `${rotateDur}s`
  191. }
  192. return Object.keys(style)
  193. .map(key => `${key}:${style[key]}`)
  194. .join('; ')
  195. },
  196. },
  197. watch: {
  198. icon() {
  199. this.initialIcon()
  200. },
  201. color() {
  202. this.initialIconColor()
  203. },
  204. },
  205. mounted() {
  206. this.initialIcon()
  207. },
  208. methods: {
  209. doClick(evt) {
  210. // #ifdef MP-ALIPAY || MP-DINTTALK || MP-DINGDING
  211. this.$emit('tap', evt)
  212. // #endif
  213. setTimeout(() => {
  214. this.$emit('click', evt)
  215. }, 1)
  216. },
  217. doTap(evt) {
  218. setTimeout(() => {
  219. this.$emit('click', evt)
  220. }, 1)
  221. },
  222. initialIconColor() {
  223. // if (this.isFileSource && !!this.color) {
  224. // console.warn(`<zui-svg-icon /> 使用了未经过预处理的图标格式, 将不支持更换颜色. 未经过预处理的图标格式包括: URI, base64 图片, 原始SVG代码`)
  225. // }
  226. // Initial color map
  227. const oriColors = this.getOriginalColors()
  228. if (this.color && oriColors.length) {
  229. const newColors = typeof this.color === 'string' ? this.color.split(',') : this.color
  230. this.colorPlaceholder = new RegExp(`(${oriColors.map(item => item.replace(/([\(\)])/g, '\\$1')).join('|')})([^\\w])`, 'gi')
  231. this.colorMap = oriColors.reduce((a, b, idx) => {
  232. return {
  233. ...a,
  234. [b.toLowerCase()]: newColors[idx] || oriColors[idx],
  235. }
  236. }, {})
  237. this.isColorCountMatch = oriColors.length === newColors.length
  238. } else {
  239. this.colorPlaceholder = null
  240. this.colorMap = null
  241. this.isColorCountMatch = true
  242. }
  243. },
  244. initialIcon() {
  245. this.initialIconColor()
  246. },
  247. getOriginalColors() {
  248. const iconPreset = this.svgIconLib.icons[this.icon]
  249. return iconPreset ? iconPreset.slice(1).map(idx => this.svgIconLib.$_colorPalette[idx]) : []
  250. },
  251. },
  252. }
  253. </script>
  254. <style lang="scss" scoped>
  255. @keyframes rotateClockwise {
  256. 0% {
  257. transform: rotate(0);
  258. }
  259. 100% {
  260. transform: rotate(360deg);
  261. }
  262. }
  263. @keyframes rotateCounterclockwise {
  264. 0% {
  265. transform: rotate(360deg);
  266. }
  267. 100% {
  268. transform: rotate(0);
  269. }
  270. }
  271. .zui-svg-icon {
  272. position: relative;
  273. display: inline-flex;
  274. }
  275. .zui-svg-icon-wrapper {
  276. position: relative;
  277. display: inline-flex;
  278. justify-content: center;
  279. align-items: center;
  280. width: var(--zui-svg-icon-width);
  281. height: var(--zui-svg-icon-height);
  282. line-height: 1;
  283. vertical-align: middle;
  284. border-radius: var(--zui-svg-icon-border-radius, 0);
  285. overflow: hidden;
  286. .zui-svg-icon-image {
  287. width: 100%;
  288. height: 100%;
  289. vertical-align: middle;
  290. }
  291. &.rotate-clockwise {
  292. animation: rotateClockwise var(--zui-svg-icon-rotate-duration, 5s) linear infinite;
  293. }
  294. &.rotate-counterclockwise {
  295. animation: rotateCounterclockwise var(--zui-svg-icon-rotate-duration, 5s) linear infinite;
  296. }
  297. }
  298. .click-helper {
  299. position: absolute;
  300. z-index: 10;
  301. top: 0;
  302. left: 0;
  303. width: 100%;
  304. height: 100%;
  305. opacity: 0;
  306. }
  307. </style>