cl-svg.uvue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <template>
  2. <!-- App 平台:使用原生视图渲染 SVG,性能最佳 -->
  3. <!-- #ifdef APP-ANDROID || APP-IOS -->
  4. <!-- @vue-ignore -->
  5. <native-view @init="onInit"></native-view>
  6. <!-- #endif -->
  7. <!-- 小程序平台:使用 image 标签显示 SVG -->
  8. <!-- #ifdef MP || APP-HARMONY -->
  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-ANDROID || APP-IOS
  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. let encodedSvg: string;
  68. // #ifdef APP-ANDROID || APP-IOS
  69. // App 平台:简单的空格替换即可,无需完整 URL 编码
  70. encodedSvg = svgString.replace(/\+/g, "%20");
  71. // #endif
  72. // #ifndef APP-ANDROID || APP-IOS
  73. // 非 App 平台:使用标准 URL 编码
  74. encodedSvg = encodeURIComponent(svgString)!.replace(/\+/g, "%20");
  75. // #endif
  76. // 确保返回完整的数据 URL 格式
  77. return encodedSvg.startsWith("data:image/svg+xml,")
  78. ? encodedSvg
  79. : `data:image/svg+xml,${encodedSvg}`;
  80. }
  81. /**
  82. * 计算最终的 SVG 数据源
  83. * 自动判断数据类型并进行相应处理
  84. */
  85. const svgSrc = computed((): string => {
  86. let val = props.src;
  87. if (val == "") {
  88. return "";
  89. }
  90. // 处理颜色
  91. if (color.value != "") {
  92. if (val.includes("fill")) {
  93. val = val.replace(/(<path\b[^>]*\bfill=")[^"]*("[^>]*>)/g, `$1${color.value}$2`);
  94. } else {
  95. val = val.replace(/<svg /g, `<svg fill="${color.value}" `);
  96. }
  97. }
  98. // 判断是否为 标签 SVG(以 <svg 开头)
  99. if (val.startsWith("<svg")) {
  100. return svgToDataUrl(val);
  101. }
  102. // 其他情况直接返回原始数据源(文件路径、base64 等)
  103. return val;
  104. });
  105. /**
  106. * 生成符合 RFC4122 标准的 UUID v4
  107. * 用于 Web 平台创建唯一的元素 ID
  108. * @returns UUID 字符串
  109. */
  110. function generateUuid(): string {
  111. const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
  112. const uuid: string[] = [];
  113. // 生成 36 位字符
  114. for (let i = 0; i < 36; i++) {
  115. const randomIndex = Math.floor(Math.random() * 16);
  116. const char = chars[i == 19 ? (randomIndex & 0x3) | 0x8 : randomIndex];
  117. uuid.push(char);
  118. }
  119. // 设置 RFC4122 标准要求的固定位
  120. uuid[8] = "-"; // 第一个连字符
  121. uuid[13] = "-"; // 第二个连字符
  122. uuid[18] = "-"; // 第三个连字符
  123. uuid[23] = "-"; // 第四个连字符
  124. uuid[14] = "4"; // 版本号 v4
  125. return uuid.join("");
  126. }
  127. // Web 平台使用的唯一元素 ID
  128. const svgId = `cool-svg-${generateUuid()}`;
  129. // #ifdef APP-ANDROID || APP-IOS
  130. // App 平台 SVG 渲染器实例
  131. let svgRenderer: CoolSvg | null = null;
  132. // 重新加载
  133. function reload() {
  134. if (svgRenderer != null) {
  135. svgRenderer!.load(svgSrc.value, color.value);
  136. }
  137. }
  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. reload();
  147. }
  148. }
  149. /**
  150. * 监听 SVG 数据源变化,重新渲染
  151. */
  152. watch(svgSrc, (newSrc: string) => {
  153. if (svgRenderer != null && newSrc != "") {
  154. reload();
  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-ANDROID || APP-IOS
  186. if (svgRenderer != null && svgSrc.value != "") {
  187. reload();
  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>