helper.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import config from '../../config/env';
  2. import { reactive, watch, onMounted, computed, onBeforeUnmount } from 'vue';
  3. import { nav } from '../../../config.json';
  4. type Obj = {
  5. [k: string]: any;
  6. };
  7. type Store = {
  8. variables: Obj[];
  9. variablesMap: Obj;
  10. rawStyles: string;
  11. [k: string]: any;
  12. };
  13. const components = (nav as any[]).map(({ packages }) => (packages as any[]).map(({ name }) => name)).flat(1);
  14. const getRawFileText = async function (url: string) {
  15. const response = await fetch(url);
  16. const res = await response.text();
  17. return res;
  18. };
  19. const getInputType = (value: string) => {
  20. if (/^\d+$/.test(value)) {
  21. return 'number';
  22. }
  23. if (/^#[A-Za-z0-9]+$/.test(value)) {
  24. return 'hex';
  25. }
  26. if (/^(rgb|hsl)a?\((\s*\/?\s*[+-]?\d*(\.\d+)?%?,?\s*){3,5}\)/gim.test(value)) {
  27. return 'rgb';
  28. }
  29. return 'input';
  30. };
  31. const loadScript = async (url: string) =>
  32. new Promise((resolve, reject) => {
  33. const script = document.createElement('script');
  34. script.onload = resolve;
  35. script.onerror = reject;
  36. script.src = url;
  37. document.head.appendChild(script);
  38. });
  39. // 提取变量
  40. const extractVariables = (matched: string[], name: string, lowerCaseName: string) =>
  41. matched.reduce((res, str) => {
  42. const extract = str.replace(/\s+!default/, '').match(/(.*):(?:\s+)?([\s\S]*)(?:\s+)?;/);
  43. if (extract) {
  44. const key = extract[1];
  45. const value = extract[2];
  46. res.push({
  47. name, // 组件名
  48. lowerCaseName, // 组件名小写
  49. key, // 变量名
  50. rawValue: value, // 原始值
  51. computedRawValue: '', // 计算后的原始值
  52. value, // 编辑的值
  53. // 编辑的类型
  54. inputType: getInputType(value)
  55. });
  56. }
  57. return res;
  58. }, [] as Obj[]);
  59. // 提取样式代码,只保留有使用变量的行
  60. const extractStyle = (style: string) => {
  61. if (!store.variables.length) {
  62. return '';
  63. }
  64. // comment
  65. style = style
  66. .split('\n')
  67. .filter((str) => !/^(\s+)?\/\//.test(str))
  68. .join('\n');
  69. // todo: parse mixin
  70. style = style
  71. .split('\n')
  72. .filter((str) => !/^(\s+)?@include/.test(str))
  73. .join('\n');
  74. style = style.replace(/(?:({|;|\s|\n))[\w-]+:([^;{}]|;base64)+;(?!base64)/g, (matched) => {
  75. const matchedKey = matched.match(/\$[\w-]+\b/g);
  76. if (matchedKey && matchedKey.some((k) => store.variablesMap[k])) {
  77. return matched;
  78. }
  79. return '';
  80. });
  81. // console.log(style);
  82. return style;
  83. };
  84. const parseSassVariables = (text: string, components: string[]) => {
  85. const matchedComponentVariables = components
  86. .map((name) => {
  87. const lowerCaseName = name.toLowerCase();
  88. const reg = new RegExp(
  89. `(?<!\\/\\/(\\s+)?)\\$(${name}|${lowerCaseName})\\b[\\w-]+:([^;{}]|;base64)+;(?!base64)`,
  90. 'g'
  91. );
  92. const matched = text.match(reg);
  93. if (matched) {
  94. return extractVariables(matched, name, lowerCaseName);
  95. }
  96. })
  97. .filter(Boolean)
  98. .flat(2);
  99. const baseVariablesReg = new RegExp(
  100. `\\$(?!(${matchedComponentVariables
  101. .map((item) => (item && `${item.name}|${item.lowerCaseName}`) || '')
  102. .join('|')})\\b)[\\w-]+:[^:]+;`,
  103. 'g'
  104. );
  105. const variables = matchedComponentVariables as Obj[];
  106. const matchedBaseVariables = text.match(baseVariablesReg);
  107. // 组件变量以外的都作为基础变量
  108. if (matchedBaseVariables) {
  109. variables.unshift(...extractVariables(matchedBaseVariables, 'Base', 'base'));
  110. }
  111. return variables;
  112. };
  113. let cachedStyles = '';
  114. const store: Store = reactive({
  115. init: false,
  116. variables: [],
  117. variablesMap: {},
  118. rawStyles: ''
  119. });
  120. const getSassVariables = async () => {
  121. // 固定自定义主题的访问链接: https://nutui.jd.com/theme/?theme=自定义变量的文件地址#/
  122. // e.g. https://nutui.jd.com/theme/?theme=xxx.com%2variables.scss#/
  123. // vite issue https://github.com/vitejs/vite/issues/6894
  124. const params = new URLSearchParams(window.parent.location.search);
  125. const param = params.get('theme') as string;
  126. const source = {
  127. jdt: 'https://storage.360buyimg.com/nutui-static/source/variables-jdt.scss_source'
  128. } as any;
  129. const customUrl = param && source[param.replace('/', '')];
  130. if (customUrl) {
  131. const customVariablesText = await getRawFileText(customUrl);
  132. const customVariables = parseSassVariables(customVariablesText, components);
  133. const variablesMap = customVariables.reduce((map, item) => {
  134. map[item.key] = 1;
  135. return map;
  136. }, {});
  137. store.variables = customVariables;
  138. store.variablesMap = variablesMap;
  139. }
  140. };
  141. export const getRawSassStyle = async (): Promise<void> => {
  142. const style = await getRawFileText(`${config.themeUrl}/styles/sass-styles.scss_source`);
  143. store.rawStyles = style;
  144. };
  145. export const useThemeEditor = function () {
  146. const cssText = computed(() => {
  147. const variablesText = store.variables.map(({ key, value }) => `${key}:${value}`).join(';');
  148. cachedStyles = cachedStyles || extractStyle(store.rawStyles);
  149. return `${variablesText};${cachedStyles}`;
  150. });
  151. onMounted(async () => {
  152. if (!store.init) {
  153. await Promise.all([
  154. getSassVariables(),
  155. loadScript('https://cdnout.com/sass.js/sass.sync.min.js'),
  156. getRawSassStyle()
  157. ]);
  158. store.init = true;
  159. }
  160. });
  161. let timer: any = null;
  162. onBeforeUnmount(() => {
  163. clearTimeout(timer);
  164. });
  165. watch(
  166. () => cssText.value,
  167. (css: string) => {
  168. clearTimeout(timer);
  169. timer = setTimeout(() => {
  170. const Sass = (window as any).Sass;
  171. let beginTime = new Date().getTime();
  172. console.log('sass编译开始', beginTime);
  173. Sass &&
  174. Sass.compile(css, async (res: Obj) => {
  175. const iframe = window as any;
  176. if (res.text && iframe) {
  177. console.log('sass编译成功', new Date().getTime() - beginTime);
  178. try {
  179. if (!iframe.__styleEl) {
  180. const style = iframe.document.createElement('style');
  181. style.id = 'theme';
  182. iframe.__styleEl = style;
  183. }
  184. iframe.__styleEl.innerHTML = res.text;
  185. iframe.document.head.appendChild(iframe.__styleEl);
  186. console.info('insert success!');
  187. } catch (error) {
  188. console.error(error);
  189. }
  190. } else {
  191. console.log('sass编译失败1s 重新加载', new Date().getTime() - beginTime);
  192. setTimeout(() => {
  193. window.location.reload();
  194. }, 1000);
  195. console.error(res);
  196. }
  197. if (res.status !== 0 && res.message) {
  198. console.log(res.message);
  199. }
  200. });
  201. }, 300);
  202. },
  203. { immediate: true }
  204. );
  205. };