cl-svg.uvue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <!-- App 平台:使用原生视图渲染 SVG,性能最佳 -->
  3. <!-- #ifdef APP -->
  4. <!-- @vue-ignore -->
  5. <native-view @init="onInit"></native-view>
  6. <!-- #endif -->
  7. <!-- 小程序平台:使用 image 标签显示 SVG -->
  8. <!-- #ifdef MP -->
  9. <!-- @vue-ignore -->
  10. <image class="cl-svg" :src="svgSrc"></image>
  11. <!-- #endif -->
  12. <!-- Web 平台:使用 object 标签以支持 SVG 交互和样式控制 -->
  13. <!-- #ifdef WEB -->
  14. <object :id="svgId" :data="svgSrc" type="image/svg+xml" class="cl-svg"></object>
  15. <!-- #endif -->
  16. </template>
  17. <script lang="ts" setup>
  18. import { computed, watch, onMounted } from "vue";
  19. import { getColor, isDark } from "@/cool";
  20. // #ifdef APP
  21. // @ts-ignore
  22. import { CoolSvg } from "@/uni_modules/cool-svg";
  23. // #endif
  24. // 组件属性定义
  25. const props = defineProps({
  26. /**
  27. * SVG 数据源
  28. * 支持格式:
  29. * - 文件路径:'/static/icon.svg'
  30. * - base64 数据:'data:image/svg+xml;base64,PHN2Zw...'
  31. * - 标签 SVG:'<svg>...</svg>'
  32. */
  33. src: {
  34. type: String,
  35. default: ""
  36. },
  37. /**
  38. * SVG 填充颜色
  39. * 支持格式:#hex、rgb()、rgba()、颜色名称
  40. * 会自动替换 SVG 中 path 元素的 fill 属性
  41. */
  42. color: {
  43. type: String,
  44. default: ""
  45. }
  46. });
  47. // 颜色值
  48. const color = computed(() => {
  49. if (props.color == "none") {
  50. return "";
  51. }
  52. if (props.color != "") {
  53. if (props.color == "primary") {
  54. return getColor("primary-500");
  55. }
  56. return props.color;
  57. } else {
  58. return isDark.value ? "white" : "black";
  59. }
  60. });
  61. /**
  62. * 将 SVG 字符串转换为数据 URL
  63. * @param svgString 原始 SVG 字符串
  64. * @returns 转换后的数据 URL
  65. */
  66. function svgToDataUrl(svgString: string): string {
  67. // 如果指定了颜色,替换 SVG 中所有 path 元素的 fill 属性
  68. if (color.value != "") {
  69. svgString = svgString.replace(
  70. /(<path\b[^>]*\bfill=")[^"]*("[^>]*>)/g,
  71. `$1${color.value}$2`
  72. );
  73. }
  74. let encodedSvg: string;
  75. // #ifdef APP
  76. // App 平台:简单的空格替换即可,无需完整 URL 编码
  77. encodedSvg = svgString.replace(/\+/g, "%20");
  78. // #endif
  79. // #ifndef APP
  80. // 非 App 平台:使用标准 URL 编码
  81. encodedSvg = encodeURIComponent(svgString)!.replace(/\+/g, "%20");
  82. // #endif
  83. // 确保返回完整的数据 URL 格式
  84. return encodedSvg.startsWith("data:image/svg+xml,")
  85. ? encodedSvg
  86. : `data:image/svg+xml,${encodedSvg}`;
  87. }
  88. /**
  89. * 计算最终的 SVG 数据源
  90. * 自动判断数据类型并进行相应处理
  91. */
  92. const svgSrc = computed((): string => {
  93. let val = props.src;
  94. if (val == "") {
  95. return "";
  96. }
  97. // 判断是否为 标签 SVG(以 <svg 开头)
  98. if (val.startsWith("<svg")) {
  99. return svgToDataUrl(val);
  100. }
  101. // #ifdef MP
  102. if (val.includes("fill")) {
  103. val = val.replace(/fill="[^"]*"/g, `fill="${color.value}"`);
  104. } else {
  105. val = val.replace("<svg ", `<svg fill="${color.value}" `);
  106. }
  107. // #endif
  108. // 其他情况直接返回原始数据源(文件路径、base64 等)
  109. return val;
  110. });
  111. /**
  112. * 生成符合 RFC4122 标准的 UUID v4
  113. * 用于 Web 平台创建唯一的元素 ID
  114. * @returns UUID 字符串
  115. */
  116. function generateUuid(): string {
  117. const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
  118. const uuid: string[] = [];
  119. // 生成 36 位字符
  120. for (let i = 0; i < 36; i++) {
  121. const randomIndex = Math.floor(Math.random() * 16);
  122. const char = chars[i == 19 ? (randomIndex & 0x3) | 0x8 : randomIndex];
  123. uuid.push(char);
  124. }
  125. // 设置 RFC4122 标准要求的固定位
  126. uuid[8] = "-"; // 第一个连字符
  127. uuid[13] = "-"; // 第二个连字符
  128. uuid[18] = "-"; // 第三个连字符
  129. uuid[23] = "-"; // 第四个连字符
  130. uuid[14] = "4"; // 版本号 v4
  131. return uuid.join("");
  132. }
  133. // Web 平台使用的唯一元素 ID
  134. const svgId = `cool-svg-${generateUuid()}`;
  135. // #ifdef APP
  136. // App 平台 SVG 渲染器实例
  137. let svgRenderer: CoolSvg | null = null;
  138. /**
  139. * App 平台原生视图初始化回调
  140. * @param e 原生视图初始化事件
  141. */
  142. function onInit(e: UniNativeViewInitEvent) {
  143. svgRenderer = new CoolSvg(e.detail.element);
  144. // 立即加载 SVG 内容
  145. if (svgRenderer != null) {
  146. svgRenderer!.load(svgSrc.value, color.value);
  147. }
  148. }
  149. /**
  150. * 监听 SVG 数据源变化,重新渲染
  151. */
  152. watch(svgSrc, (newSrc: string) => {
  153. if (svgRenderer != null && newSrc != "") {
  154. svgRenderer!.load(newSrc, color.value);
  155. }
  156. });
  157. // #endif
  158. /**
  159. * 设置颜色
  160. */
  161. function setColor() {
  162. if (color.value == "") {
  163. return;
  164. }
  165. // #ifdef WEB
  166. const element = document.getElementById(svgId) as HTMLObjectElement;
  167. if (element != null) {
  168. const set = () => {
  169. const svgDoc = element.getSVGDocument();
  170. if (svgDoc != null) {
  171. // 查找所有 path 元素并应用颜色
  172. const paths = svgDoc.querySelectorAll("path");
  173. paths?.forEach((path) => {
  174. path.setAttribute("fill", color.value);
  175. });
  176. }
  177. };
  178. if (element.getSVGDocument() != null) {
  179. set();
  180. } else {
  181. element.addEventListener("load", set);
  182. }
  183. }
  184. // #endif
  185. // #ifdef APP
  186. if (svgRenderer != null && svgSrc.value != "") {
  187. svgRenderer!.load(svgSrc.value, color.value);
  188. }
  189. // #endif
  190. }
  191. /**
  192. * 监听颜色变化,重新渲染
  193. */
  194. watch(
  195. computed(() => [props.color, isDark.value]),
  196. () => {
  197. setColor();
  198. }
  199. );
  200. onMounted(() => {
  201. setColor();
  202. });
  203. </script>