import config from '../../config/env'; import { reactive, watch, onMounted, computed, onBeforeUnmount } from 'vue'; import { nav } from '../../../config.json'; import { isJDB, isJDT } from '.'; type Obj = { [k: string]: any; }; type Store = { variables: Obj[]; variablesMap: Obj; rawStyles: string; [k: string]: any; }; const components = (nav as any[]).map(({ packages }) => (packages as any[]).map(({ name }) => name)).flat(1); const getRawFileText = async function (url: string) { const response = await fetch(url); const res = await response.text(); return res; }; const getInputType = (value: string) => { if (/^\d+$/.test(value)) { return 'number'; } if (/^#[A-Za-z0-9]+$/.test(value)) { return 'hex'; } if (/^(rgb|hsl)a?\((\s*\/?\s*[+-]?\d*(\.\d+)?%?,?\s*){3,5}\)/gim.test(value)) { return 'rgb'; } return 'input'; }; const loadScript = async (url: string) => new Promise((resolve, reject) => { const script = document.createElement('script'); script.onload = resolve; script.onerror = reject; script.src = url; document.head.appendChild(script); }); // 提取变量 const extractVariables = (matched: string[], name: string, lowerCaseName: string) => matched.reduce((res, str) => { const extract = str.replace(/\s+!default/, '').match(/(.*):(?:\s+)?([\s\S]*)(?:\s+)?;/); if (extract) { const key = extract[1]; const value = extract[2]; res.push({ name, // 组件名 lowerCaseName, // 组件名小写 key, // 变量名 rawValue: value, // 原始值 computedRawValue: '', // 计算后的原始值 value, // 编辑的值 // 编辑的类型 inputType: getInputType(value) }); } return res; }, [] as Obj[]); // 提取样式代码,只保留有使用变量的行 const extractStyle = (style: string) => { if (!store.variables.length) { return ''; } // comment style = style .split('\n') .filter((str) => !/^(\s+)?\/\//.test(str)) .join('\n'); // todo: parse mixin style = style .split('\n') .filter((str) => !/^(\s+)?@include/.test(str)) .join('\n'); style = style.replace(/(?:({|;|\s|\n))[\w-]+:([^;{}]|;base64)+;(?!base64)/g, (matched) => { const matchedKey = matched.match(/\$[\w-]+\b/g); if (matchedKey && matchedKey.some((k) => store.variablesMap[k])) { return matched; } return ''; }); // console.log(style); return style; }; const parseSassVariables = (text: string, components: string[]) => { const matchedComponentVariables = components .map((name) => { const lowerCaseName = name.toLowerCase(); const reg = new RegExp( `(? (item && `${item.name}|${item.lowerCaseName}`) || '') .join('|')})\\b)[\\w-]+:[^:]+;`, 'g' ); const variables = matchedComponentVariables as Obj[]; const matchedBaseVariables = text.match(baseVariablesReg); // 组件变量以外的都作为基础变量 if (matchedBaseVariables) { variables.unshift(...extractVariables(matchedBaseVariables, 'Base', 'base')); } return variables; }; let cachedStyles = ''; const store: Store = reactive({ init: false, variables: [], variablesMap: {}, rawStyles: '' }); const getSassVariables = async (sassFileUrl: string) => { if (sassFileUrl) { const customVariablesText = await getRawFileText(sassFileUrl); const customVariables = parseSassVariables(customVariablesText, components); const variablesMap = customVariables.reduce((map, item) => { map[item.key] = 1; return map; }, {}); store.variables = customVariables; store.variablesMap = variablesMap; } }; export const getRawSassStyle = async (): Promise => { const style = await getRawFileText(`${config.themeUrl}/styles/sass-styles.scss_source`); store.rawStyles = style; }; export const useThemeEditor = function () { const cssText = computed(() => { const variablesText = store.variables.map(({ key, value }) => `${key}:${value}`).join(';'); cachedStyles = cachedStyles || extractStyle(store.rawStyles); return variablesText ? `${variablesText};${cachedStyles}` : ''; }); onMounted(async () => { if (!store.init) { // 固定自定义主题的访问链接: https://nutui.jd.com/theme/?theme=自定义变量的文件地址#/ // e.g. https://nutui.jd.com/theme/?theme=xxx.com%2variables.scss#/ // vite issue https://github.com/vitejs/vite/issues/6894 let customUrl = ''; if (isJDT()) { customUrl = 'https://storage.360buyimg.com/nutui-static/source/variables-jdt.scss_source'; } else if (isJDB()) { customUrl = 'https://storage.360buyimg.com/nutui-static/source/variables-jdb.scss_source'; } if (customUrl) { loadScript('https://storage.360buyimg.com/nutui-static/cdn/sass.sync.min.js').then((res) => { Promise.all([getSassVariables(customUrl), getRawSassStyle()]); }); store.init = true; } } }); let timer: any = null; onBeforeUnmount(() => { clearTimeout(timer); }); watch( () => cssText.value, (css: string) => { if (!css) { return; } clearTimeout(timer); timer = setTimeout(() => { const Sass = (window as any).Sass; let beginTime = new Date().getTime(); console.log('sass编译开始', beginTime); Sass && Sass.compile(css, async (res: Obj) => { const iframe = window as any; if (res.text && iframe) { console.log('sass编译成功', new Date().getTime() - beginTime); try { if (!iframe.__styleEl) { const style = iframe.document.createElement('style'); style.id = 'theme'; iframe.__styleEl = style; } iframe.__styleEl.innerHTML = res.text; iframe.document.head.appendChild(iframe.__styleEl); console.info('insert success!'); } catch (error) { console.error(error); } } else { console.log('sass编译失败', new Date().getTime() - beginTime); console.error(res); } if (res.status !== 0 && res.message) { console.log(res.message); } }); }, 300); }, { immediate: true } ); };