294 lines
5.0 KiB
JavaScript
294 lines
5.0 KiB
JavaScript
/**
|
||
|
||
* 微信小程序组件/页面 WXSS 不支持本地 background-image,
|
||
|
||
* 将 /static/images/ 背景图改为 <image> 标签,并清理 CSS。
|
||
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
|
||
const path = require('path');
|
||
|
||
|
||
|
||
const root = path.join(__dirname, '..');
|
||
|
||
|
||
|
||
const COMPONENT_MAP = {
|
||
|
||
MemberInfoStatusBar: 'member-info-status-bar.css',
|
||
|
||
MemberInfoHeader: 'member-info-header.css',
|
||
|
||
MemberInfoMemberCard: 'member-info-member-card.css',
|
||
|
||
MemberInfoQuickActions: 'member-info-quick-actions.css',
|
||
|
||
MemberInfoBookingList: 'member-info-booking-list.css',
|
||
|
||
MemberInfoCheckInList: 'member-info-check-in-list.css',
|
||
|
||
MemberInfoBodyReport: 'member-info-body-report.css',
|
||
|
||
MemberInfoCouponPoints: 'member-info-coupon-points.css',
|
||
|
||
MemberInfoReferral: 'member-info-referral.css',
|
||
|
||
MemberInfoSettings: 'member-info-settings.css',
|
||
|
||
MemberInfoLogout: 'member-info-logout.css'
|
||
|
||
};
|
||
|
||
|
||
|
||
const PAGE_MAP = {
|
||
|
||
booking: ['booking-pixso.css'],
|
||
|
||
memberCard: ['member-card-pixso.css'],
|
||
|
||
userInfo: ['user-info-pixso.css']
|
||
|
||
};
|
||
|
||
|
||
|
||
function extractBgImages(css) {
|
||
|
||
const map = new Map();
|
||
|
||
const ruleRe = /^\.([a-zA-Z0-9_-]+)\s*\{([^}]*)\}/gm;
|
||
|
||
let match;
|
||
|
||
while ((match = ruleRe.exec(css)) !== null) {
|
||
|
||
const className = match[1];
|
||
|
||
const body = match[2];
|
||
|
||
const urlMatch = body.match(/background-image:\s*url\(\/static\/images\/([^)]+)\)/);
|
||
|
||
if (urlMatch) {
|
||
|
||
map.set(className, `/static/images/${urlMatch[1]}`);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return map;
|
||
|
||
}
|
||
|
||
|
||
|
||
function ensureImageDisplay(css, className) {
|
||
|
||
const ruleRe = new RegExp(`(\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{)([^}]*)(\\})`);
|
||
|
||
return css.replace(ruleRe, (full, head, body, tail) => {
|
||
|
||
if (/display:\s*block/.test(body)) return full;
|
||
|
||
return `${head}${body.trim()}\n display: block;\n${tail}`;
|
||
|
||
});
|
||
|
||
}
|
||
|
||
|
||
|
||
function getMode(className) {
|
||
|
||
if (/avatar|banner|photo|card-preview|AC\d/i.test(className)) return 'aspectFill';
|
||
|
||
return 'aspectFit';
|
||
|
||
}
|
||
|
||
|
||
|
||
function replaceViewWithImage(template, className, src) {
|
||
|
||
const mode = getMode(className);
|
||
|
||
const imageTag = `<image class="${className}" src="${src}" mode="${mode}" />`;
|
||
|
||
const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
||
|
||
|
||
const patterns = [
|
||
|
||
new RegExp(`<view\\s+class="${escaped}"\\s*></view>`, 'g'),
|
||
|
||
new RegExp(`<view\\s+class="${escaped}"\\s*/>`, 'g'),
|
||
|
||
new RegExp(`<view\\s+class="${escaped}"\\s*>\\s*</view>`, 'g'),
|
||
|
||
new RegExp(`<view\\s+\\n\\s*class="${escaped}"\\s*\\n\\s*></view>`, 'g'),
|
||
|
||
new RegExp(`<view\\s+\\n\\s*class="${escaped}"\\s*\\n\\s*/>`, 'g'),
|
||
|
||
new RegExp(`<view\\s+[^>]*class="${escaped}"[^>]*>\\s*</view>`, 'g')
|
||
|
||
];
|
||
|
||
|
||
|
||
let result = template;
|
||
|
||
for (const re of patterns) {
|
||
|
||
result = result.replace(re, imageTag);
|
||
|
||
}
|
||
|
||
return result;
|
||
|
||
}
|
||
|
||
|
||
|
||
function stripAllLocalBgFromCss(css) {
|
||
|
||
let next = css;
|
||
|
||
next = next.replace(/background-image:\s*url\(\/static\/images\/[^)]+\);/g, '');
|
||
|
||
next = next.replace(/\n\s*background-size:\s*100%\s*100%;/g, '');
|
||
|
||
next = next.replace(/\n\s*background-repeat:\s*no-repeat;/g, '');
|
||
|
||
return next;
|
||
|
||
}
|
||
|
||
|
||
|
||
function convertPair(vuePath, cssPath) {
|
||
|
||
if (!fs.existsSync(vuePath) || !fs.existsSync(cssPath)) return;
|
||
|
||
|
||
|
||
let css = fs.readFileSync(cssPath, 'utf8');
|
||
|
||
const hadLocalBg = /background-image:\s*url\(\/static\/images\//.test(css);
|
||
|
||
if (!hadLocalBg) return;
|
||
|
||
|
||
|
||
const bgMap = extractBgImages(css);
|
||
|
||
let template = fs.readFileSync(vuePath, 'utf8');
|
||
|
||
const templateMatch = template.match(/<template>([\s\S]*?)<\/template>/);
|
||
|
||
if (!templateMatch) {
|
||
|
||
css = stripAllLocalBgFromCss(css);
|
||
|
||
fs.writeFileSync(cssPath, css, 'utf8');
|
||
|
||
console.log('css-only (no template)', path.relative(root, cssPath));
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
|
||
|
||
let inner = templateMatch[1];
|
||
|
||
let changed = false;
|
||
|
||
|
||
|
||
for (const [className, src] of bgMap.entries()) {
|
||
|
||
const before = inner;
|
||
|
||
inner = replaceViewWithImage(inner, className, src);
|
||
|
||
if (inner !== before) {
|
||
|
||
changed = true;
|
||
|
||
css = ensureImageDisplay(css, className);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
css = stripAllLocalBgFromCss(css);
|
||
|
||
|
||
|
||
if (changed) {
|
||
|
||
template = template.replace(templateMatch[1], inner);
|
||
|
||
fs.writeFileSync(vuePath, template, 'utf8');
|
||
|
||
}
|
||
|
||
fs.writeFileSync(cssPath, css, 'utf8');
|
||
|
||
console.log(
|
||
|
||
changed ? 'converted' : 'css-only',
|
||
|
||
path.relative(root, vuePath),
|
||
|
||
`(${bgMap.size} rules stripped)`
|
||
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
for (const [name, cssFile] of Object.entries(COMPONENT_MAP)) {
|
||
|
||
convertPair(
|
||
|
||
path.join(root, 'components/memberInfo', `${name}.vue`),
|
||
|
||
path.join(root, 'common/style/memberInfo', cssFile)
|
||
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
for (const [page, cssFiles] of Object.entries(PAGE_MAP)) {
|
||
|
||
for (const cssFile of cssFiles) {
|
||
|
||
convertPair(
|
||
|
||
path.join(root, 'pages/memberInfo', `${page}.vue`),
|
||
|
||
path.join(root, 'common/style/memberInfo/pages', cssFile)
|
||
|
||
);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
console.log('done');
|
||
|
||
|