| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import config from '../../config/env';
- import { reactive, watch, onMounted, computed, onBeforeUnmount } from 'vue';
- import { nav } from '../../../config.json';
- 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(
- `(?<!\\/\\/(\\s+)?)\\$(${name}|${lowerCaseName})\\b[\\w-]+:([^;{}]|;base64)+;(?!base64)`,
- 'g'
- );
- const matched = text.match(reg);
- if (matched) {
- return extractVariables(matched, name, lowerCaseName);
- }
- })
- .filter(Boolean)
- .flat(2);
- const baseVariablesReg = new RegExp(
- `\\$(?!(${matchedComponentVariables
- .map((item) => (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 () => {
- // 固定自定义主题的访问链接: 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
- const params = new URLSearchParams(window.parent.location.search);
- const param = params.get('theme') as string;
- const source = {
- jdt: 'https://storage.360buyimg.com/nutui-static/source/variables-jdt.scss_source'
- } as any;
- const customUrl = param && source[param.replace('/', '')];
- if (customUrl) {
- const customVariablesText = await getRawFileText(customUrl);
- 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<void> => {
- 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};${cachedStyles}`;
- });
- onMounted(async () => {
- if (!store.init) {
- await Promise.all([
- getSassVariables(),
- loadScript('https://cdnout.com/sass.js/sass.sync.min.js'),
- getRawSassStyle()
- ]);
- store.init = true;
- }
- });
- let timer: any = null;
- onBeforeUnmount(() => {
- clearTimeout(timer);
- });
- watch(
- () => cssText.value,
- (css: string) => {
- 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编译失败1s 重新加载', new Date().getTime() - beginTime);
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- console.error(res);
- }
- if (res.status !== 0 && res.message) {
- console.log(res.message);
- }
- });
- }, 300);
- },
- { immediate: true }
- );
- };
|