feat: add AirShelf core implementation
@@ -0,0 +1,94 @@
|
||||
(function () {
|
||||
const PATHS = {
|
||||
home: '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 10v10h14V10"/><path d="M9 20v-6h6v6"/>',
|
||||
layoutDashboard: '<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>',
|
||||
package: '<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/><path d="m7.5 4.3 9 5.1"/>',
|
||||
boxes: '<path d="M11 21.7a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/><path d="m7.5 4.3 9 5.1"/>',
|
||||
clapperboard: '<path d="m12.3 3.5 3 4"/><path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z"/><path d="m6.2 5.3 3.1 3.9"/><path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>',
|
||||
film: '<path d="m12.3 3.5 3 4"/><path d="M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z"/><path d="m6.2 5.3 3.1 3.9"/><path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>',
|
||||
video: '<path d="m16 13 5.2 3.1a.5.5 0 0 0 .8-.4V8.3a.5.5 0 0 0-.8-.4L16 11"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
|
||||
sparkles: '<path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3Z"/><path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15Z"/>',
|
||||
images: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
|
||||
folder: '<path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H9l2 2h7.5A2.5 2.5 0 0 1 21 8.5v8A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5Z"/><path d="M3 9h18"/>',
|
||||
library: '<path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H9l2 2h7.5A2.5 2.5 0 0 1 21 8.5v8A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5Z"/><path d="M3 9h18"/>',
|
||||
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
||||
wallet: '<rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/><path d="M16 14h2"/>',
|
||||
creditCard: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/>',
|
||||
settings: '<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.2a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.2A1.7 1.7 0 0 0 4.7 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1.1-1.5V3a2 2 0 1 1 4 0v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1h.1a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/>',
|
||||
airshelf: '<path d="M4.5 19 12 4.5 19.5 19"/><path d="M8 14h8"/><path d="M7 17h10"/><path d="M9.5 10.3c1.5-1.2 3.5-1.2 5 0"/>',
|
||||
flame: '<path d="M8.5 14.5c0-2 1.5-3.4 3.5-5.5.4 2.2 2.8 3.1 2.8 5.6a3.3 3.3 0 0 1-6.3-.1Z"/><path d="M12 22c4 0 7-2.8 7-7 0-4.7-3.3-7.3-5-12-2 3.3-7 6-7 12a5 5 0 0 0 5 7Z"/>',
|
||||
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
|
||||
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/>',
|
||||
list: '<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>',
|
||||
chevronLeft: '<path d="m15 18-6-6 6-6"/>',
|
||||
chevronRight: '<path d="m9 18 6-6-6-6"/>',
|
||||
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
plus: '<path d="M12 5v14"/><path d="M5 12h14"/>',
|
||||
productPlus: '<path d="M12 22V12"/><path d="M16 17h6"/><path d="M19 14v6"/><path d="M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="m7.5 4.3 9 5.1"/>',
|
||||
arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
||||
arrowLeft: '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
|
||||
arrowUp: '<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>',
|
||||
chevronDown: '<path d="m6 9 6 6 6-6"/>',
|
||||
rotateCcw: '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/>',
|
||||
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>',
|
||||
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
|
||||
moreHorizontal: '<circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/>',
|
||||
trash: '<path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m19 6-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>',
|
||||
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z"/>',
|
||||
play: '<path d="m6 4 14 8-14 8Z"/>',
|
||||
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
alertCircle: '<circle cx="12" cy="12" r="9"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
|
||||
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/>',
|
||||
shieldCheck: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="m9 12 2 2 4-4"/>',
|
||||
messageCircle: '<path d="M21 11.5a8.5 8.5 0 0 1-12.5 7.5L3 20l1-5.5A8.5 8.5 0 1 1 21 11.5Z"/>',
|
||||
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
|
||||
lock: '<rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||||
eye: '<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
|
||||
image: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
|
||||
grid: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>',
|
||||
copy: '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
|
||||
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/>',
|
||||
userPlus: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6"/><path d="M22 11h-6"/>',
|
||||
helpCircle: '<circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.5-2.5 2-2.5 4"/><path d="M12 17h.01"/>'
|
||||
};
|
||||
|
||||
const ALIASES = {
|
||||
dashboard: 'layoutDashboard',
|
||||
products: 'package',
|
||||
projects: 'clapperboard',
|
||||
assetFactory: 'sparkles',
|
||||
library: 'folder',
|
||||
account: 'creditCard',
|
||||
billing: 'creditCard',
|
||||
team: 'users',
|
||||
queue: 'list',
|
||||
arrow: 'arrowRight',
|
||||
back: 'arrowLeft',
|
||||
close: 'x',
|
||||
rerun: 'rotateCcw',
|
||||
more: 'moreHorizontal',
|
||||
danger: 'alertCircle'
|
||||
};
|
||||
|
||||
function svg(name, opts) {
|
||||
opts = opts || {};
|
||||
const key = ALIASES[name] || name;
|
||||
const body = PATHS[key] || PATHS.helpCircle;
|
||||
const size = opts.size || 16;
|
||||
const strokeWidth = opts.strokeWidth || 1.5;
|
||||
const className = ['ui-icon', opts.className || ''].join(' ').trim();
|
||||
const label = opts.label ? ` role="img" aria-label="${escapeAttr(opts.label)}"` : ' aria-hidden="true"';
|
||||
return `<svg class="${className}" data-icon="${key}" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" focusable="false"${label}>${body}</svg>`;
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
window.IconKit = {
|
||||
svg,
|
||||
names: () => Object.keys(PATHS),
|
||||
has: name => !!PATHS[ALIASES[name] || name]
|
||||
};
|
||||
})();
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 201 KiB |
@@ -0,0 +1,152 @@
|
||||
(function () {
|
||||
const base = 'assets/mock/';
|
||||
const media = {
|
||||
products: {
|
||||
mask: base + 'product-mask.png',
|
||||
earbuds: base + 'product-earbuds.png',
|
||||
noodle: base + 'product-noodle.png',
|
||||
sunscreen: base + 'product-sunscreen.png',
|
||||
coffee: base + 'product-coffee.png',
|
||||
airFryer: base + 'product-air-fryer.png',
|
||||
yoga: base + 'product-yoga-pants.png',
|
||||
storage: base + 'product-storage.png'
|
||||
},
|
||||
covers: {
|
||||
mask: base + 'cover-mask-v3.png',
|
||||
maskFinal: base + 'cover-mask-final.png',
|
||||
noodle: base + 'cover-noodle.png',
|
||||
sunscreen: base + 'cover-sunscreen.png',
|
||||
coffee: base + 'cover-coffee.png',
|
||||
earbuds: base + 'cover-earbuds.png',
|
||||
yoga: base + 'cover-yoga.png',
|
||||
airFryer: base + 'cover-air-fryer.png'
|
||||
},
|
||||
people: {
|
||||
linxi: base + 'person-linxi.png',
|
||||
ajie: base + 'person-ajie.png',
|
||||
aqiang: base + 'person-aqiang.png',
|
||||
xiaosu: base + 'person-xiaosu.png'
|
||||
},
|
||||
scenes: {
|
||||
bedroom: base + 'scene-bedroom.png',
|
||||
bathroom: base + 'scene-bathroom.png',
|
||||
living: base + 'scene-living.png',
|
||||
kitchen: base + 'scene-kitchen.png',
|
||||
office: base + 'scene-office.png',
|
||||
cafe: base + 'scene-cafe.png',
|
||||
street: base + 'scene-night-street.png',
|
||||
tabletop: base + 'scene-tabletop.png'
|
||||
}
|
||||
};
|
||||
|
||||
function clean(text) {
|
||||
return String(text || '').replace(/\s+/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
function contextText(el) {
|
||||
const card = el.closest('[data-name], [data-product], [data-project], [data-cat], [data-asset-kind], [data-scene-type]');
|
||||
const bits = [
|
||||
el.textContent,
|
||||
card?.dataset.name,
|
||||
card?.dataset.product,
|
||||
card?.dataset.project,
|
||||
card?.dataset.cat,
|
||||
card?.dataset.assetKind,
|
||||
card?.dataset.sceneType,
|
||||
card?.querySelector('.product-name, .card-name, .asset-name, .prod-name, strong')?.textContent
|
||||
];
|
||||
return clean(bits.filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function productFor(t) {
|
||||
if (/蓝牙|耳机|earbud|南卡/.test(t)) return media.products.earbuds;
|
||||
if (/速食|牛肉面|泡面|面条|noodle/.test(t)) return media.products.noodle;
|
||||
if (/防晒|sunscreen/.test(t)) return media.products.sunscreen;
|
||||
if (/咖啡|冻干|coffee/.test(t)) return media.products.coffee;
|
||||
if (/空气炸锅|airfryer|小熊/.test(t)) return media.products.airFryer;
|
||||
if (/瑜伽裤|露露|yoga/.test(t)) return media.products.yoga;
|
||||
if (/收纳|storage|北欧/.test(t)) return media.products.storage;
|
||||
if (/面膜|补水|玻尿酸|mask|透真/.test(t)) return media.products.mask;
|
||||
return '';
|
||||
}
|
||||
|
||||
function coverFor(t) {
|
||||
if (/蓝牙|耳机|earbud|南卡/.test(t)) return media.covers.earbuds;
|
||||
if (/速食|牛肉面|泡面|面条|noodle/.test(t)) return media.covers.noodle;
|
||||
if (/防晒|sunscreen/.test(t)) return media.covers.sunscreen;
|
||||
if (/咖啡|冻干|coffee/.test(t)) return media.covers.coffee;
|
||||
if (/空气炸锅|airfryer|小熊/.test(t)) return media.covers.airFryer;
|
||||
if (/瑜伽裤|露露|yoga/.test(t)) return media.covers.yoga;
|
||||
if (/v1|final|已完成|5\/5|成片|敷面膜|化妆台/.test(t)) return media.covers.maskFinal;
|
||||
if (/面膜|补水|玻尿酸|mask|透真|场1|场2|场3/.test(t)) return media.covers.mask;
|
||||
return '';
|
||||
}
|
||||
|
||||
function personFor(t) {
|
||||
if (/阿杰|通勤男|男青年/.test(t)) return media.people.ajie;
|
||||
if (/阿强|健身男|健身/.test(t)) return media.people.aqiang;
|
||||
if (/小苏|文艺女|短发|阿楠|同事|小七|学生女|闺蜜|妈妈|王姐|豆豆/.test(t)) return media.people.xiaosu;
|
||||
if (/林夕|主播|都市白领|主角|女性|女生/.test(t)) return media.people.linxi;
|
||||
if (/小宇|李爷爷|男性|男/.test(t)) return media.people.ajie;
|
||||
return '';
|
||||
}
|
||||
|
||||
function sceneFor(t) {
|
||||
if (/卧室|床头|bedroom/.test(t)) return media.scenes.bedroom;
|
||||
if (/浴室|梳妆台|bathroom/.test(t)) return media.scenes.bathroom;
|
||||
if (/客厅|living/.test(t)) return media.scenes.living;
|
||||
if (/厨房|中岛|kitchen/.test(t)) return media.scenes.kitchen;
|
||||
if (/办公室|办公桌|会议室|office|深夜办公/.test(t)) return media.scenes.office;
|
||||
if (/咖啡店|窗边|cafe/.test(t)) return media.scenes.cafe;
|
||||
if (/街景|夜|street/.test(t)) return media.scenes.street;
|
||||
if (/平台|套图|布景|tabletop/.test(t)) return media.scenes.tabletop;
|
||||
return '';
|
||||
}
|
||||
|
||||
function imageFor(el) {
|
||||
if (el.classList.contains('missing') || el.querySelector('.spinner, .fail-icon')) return '';
|
||||
const t = contextText(el);
|
||||
const card = el.closest('.asset-card, .asset-card-2, .proj-card, .product-card, .video-card');
|
||||
if (el.id === 'ed-canvas') return media.covers.maskFinal;
|
||||
if (el.id === 'sb-main-img' || el.id === 'vd-main-img') return media.covers.mask;
|
||||
if (el.matches('.video-thumb')) {
|
||||
if (card?.dataset.videoId === 'v2') return media.products.mask;
|
||||
if (card?.dataset.videoId === 'v3') return media.covers.maskFinal;
|
||||
}
|
||||
if (el.matches('.card-thumb, .proj-thumb, .video-thumb') || card?.classList.contains('video')) return coverFor(t);
|
||||
if (el.matches('.product-thumb, .prod-thumb, .pl-thumb')) return productFor(t);
|
||||
if (el.matches('.m-thumb')) return personFor(t);
|
||||
if (el.matches('.thumb-2, .asset-thumb')) {
|
||||
return personFor(t) || sceneFor(t) || productFor(t) || coverFor(t);
|
||||
}
|
||||
if (el.matches('.sb-scene-thumb, .sb-history-thumb, .vd-history-thumb')) return coverFor(t) || media.covers.mask;
|
||||
return productFor(t) || personFor(t) || sceneFor(t) || coverFor(t);
|
||||
}
|
||||
|
||||
function applyOne(el) {
|
||||
if (!el || el.dataset.mockMediaApplied === '1') return;
|
||||
const src = imageFor(el);
|
||||
if (!src) return;
|
||||
el.dataset.mockMediaApplied = '1';
|
||||
el.classList.add('has-mock-media');
|
||||
el.style.setProperty('--mock-media-url', `url("${src}")`);
|
||||
el.style.backgroundImage = `url("${src}")`;
|
||||
}
|
||||
|
||||
function apply() {
|
||||
document.querySelectorAll('.placeholder, #ed-canvas').forEach(applyOne);
|
||||
}
|
||||
|
||||
function boot() {
|
||||
apply();
|
||||
const mo = new MutationObserver(() => apply());
|
||||
mo.observe(document.body, { childList: true, subtree: true });
|
||||
window.MockMedia = { apply, media };
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', boot, { once: true });
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 678 KiB |
|
After Width: | Height: | Size: 683 KiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 738 KiB |
|
After Width: | Height: | Size: 894 KiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 779 KiB |
|
After Width: | Height: | Size: 888 KiB |
|
After Width: | Height: | Size: 997 KiB |
|
After Width: | Height: | Size: 927 KiB |
|
After Width: | Height: | Size: 1008 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 800 KiB |
|
After Width: | Height: | Size: 1001 KiB |
|
After Width: | Height: | Size: 930 KiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 902 KiB |
|
After Width: | Height: | Size: 643 KiB |
@@ -0,0 +1,784 @@
|
||||
/* ============================================================
|
||||
新建商品 · 共享 Drawer 模块
|
||||
----------------------------------------------------------
|
||||
在任意页面只需 <script src="assets/new-product-drawer.js"> 引入,
|
||||
然后调用 NewProductDrawer.open({ onSave: fn }) 即可在当前页之上
|
||||
弹出右侧 Drawer。点击遮罩 / X / 取消 / ESC 关闭后,用户停在原页面。
|
||||
|
||||
提供:
|
||||
window.NewProductDrawer.open(opts?)
|
||||
window.NewProductDrawer.close()
|
||||
opts 字段:
|
||||
onSave(product) — 保存时回调,product = { id, name, cat, target,
|
||||
points: string[], images: { id, dataUrl, name }[] }
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.NewProductDrawer) return; // idempotent
|
||||
|
||||
const DRAWER_ID = 'npd-drawer';
|
||||
const DRAWER_BG_ID = 'npd-drawer-bg';
|
||||
|
||||
/* ---------- 注入样式(独立 namespace 以免与 products.html 冲突) ---------- */
|
||||
|
||||
const CSS = `
|
||||
/* drawer base (相同尺寸/动画) */
|
||||
#${DRAWER_BG_ID} {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .32);
|
||||
display: none; z-index: 1100;
|
||||
}
|
||||
#${DRAWER_BG_ID}.show { display: block; }
|
||||
#${DRAWER_ID} {
|
||||
position: fixed; right: 0; top: 0; bottom: 0;
|
||||
width: 820px; max-width: 100vw;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border-faint);
|
||||
z-index: 1101;
|
||||
transform: translateX(100%);
|
||||
transition: transform .25s cubic-bezier(.32, .72, 0, 1);
|
||||
display: flex; flex-direction: column;
|
||||
box-shadow: -4px 0 24px rgba(21, 20, 15, .04);
|
||||
}
|
||||
#${DRAWER_ID}.show { transform: translateX(0); }
|
||||
#${DRAWER_ID} .drawer-h {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-h .x {
|
||||
margin-left: auto; width: 32px; height: 32px;
|
||||
border-radius: var(--r-md);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-56); cursor: pointer;
|
||||
background: transparent; border: 0;
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-b { padding: 24px 28px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
|
||||
#${DRAWER_ID} .drawer-f {
|
||||
padding: 14px 24px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
display: flex; gap: 10px; align-items: center;
|
||||
background: var(--surface);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-f .btn-guide {
|
||||
margin-right: auto;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; color: var(--black-alpha-56);
|
||||
background: transparent; border: 0; cursor: pointer;
|
||||
padding: 8px 10px; border-radius: var(--r-md);
|
||||
font-family: inherit;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
|
||||
#${DRAWER_ID} .drawer-f .btn-guide svg { width: 14px; height: 14px; }
|
||||
|
||||
/* form-card */
|
||||
#${DRAWER_ID} .form-h {
|
||||
font-size: 15px; font-weight: 600; color: var(--accent-black);
|
||||
margin-bottom: 18px; padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
}
|
||||
#${DRAWER_ID} .field { margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field:last-child { margin-bottom: 0; }
|
||||
#${DRAWER_ID} .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field-label {
|
||||
display: block; font-size: 13px; font-weight: 500;
|
||||
color: var(--accent-black); margin-bottom: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .field-label .req { color: var(--heat); margin-left: 2px; }
|
||||
#${DRAWER_ID} .field-label .opt {
|
||||
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .input,
|
||||
#${DRAWER_ID} .select {
|
||||
width: 100%; height: 38px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
border-radius: var(--r-md);
|
||||
padding: 0 14px;
|
||||
font-size: 13.5px; color: var(--accent-black);
|
||||
outline: none; font-family: inherit;
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .input:focus,
|
||||
#${DRAWER_ID} .select:focus {
|
||||
border-color: var(--heat-40);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
|
||||
/* upload */
|
||||
#${DRAWER_ID} .pf-upload-row {
|
||||
display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 16px; align-items: stretch;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone {
|
||||
border: 1.5px dashed var(--black-alpha-24);
|
||||
border-radius: var(--r-md);
|
||||
padding: 28px 20px;
|
||||
background: var(--background-lighter);
|
||||
cursor: pointer; text-align: center;
|
||||
transition: border-color var(--t-base), background var(--t-base);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-ic {
|
||||
width: 44px; height: 44px;
|
||||
margin: 0 auto 10px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--heat-20);
|
||||
border-radius: var(--r-md);
|
||||
color: var(--heat);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-d {
|
||||
margin-top: 8px;
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example {
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 16px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-h { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .pf-example .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb {
|
||||
aspect-ratio: 1;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden; position: relative;
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-32);
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
|
||||
pointer-events: none;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-d { font-size: 12px; color: var(--black-alpha-56); line-height: 1.5; }
|
||||
#${DRAWER_ID} .pf-grid {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px; margin-top: 12px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-grid:empty { display: none; }
|
||||
#${DRAWER_ID} .pf-thumb {
|
||||
aspect-ratio: 1;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
position: relative; overflow: hidden; cursor: pointer;
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
#${DRAWER_ID} .pf-thumb .pf-x {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
width: 22px; height: 22px;
|
||||
background: rgba(0,0,0,.7); color: var(--accent-white);
|
||||
border: 0; border-radius: 50%; cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
opacity: 0; transition: opacity var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb:hover .pf-x { opacity: 1; }
|
||||
#${DRAWER_ID} .pf-thumb .pf-x svg { width: 11px; height: 11px; }
|
||||
|
||||
/* bullet-list */
|
||||
#${DRAWER_ID} .bullet-list { list-style: none; padding: 0; margin: 0; }
|
||||
#${DRAWER_ID} .bullet-list .bl-item,
|
||||
#${DRAWER_ID} .bullet-list .bl-add {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
|
||||
#${DRAWER_ID} .bullet-list .bl-add-row {
|
||||
margin: 8px 0 0;
|
||||
display: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add-row.show { display: flex; justify-content: flex-start; }
|
||||
#${DRAWER_ID} .bl-add-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 7px 14px;
|
||||
background: var(--heat); color: #fff;
|
||||
border: 0; border-radius: var(--r-md);
|
||||
font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: filter var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .bl-add-btn:hover { filter: brightness(.94); }
|
||||
#${DRAWER_ID} .bl-add-btn svg { width: 12px; height: 12px; }
|
||||
#${DRAWER_ID} .bullet-list .num {
|
||||
width: 22px; height: 22px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; color: var(--heat); font-weight: 700;
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .bullet-list .bl-input {
|
||||
flex: 1; background: transparent; border: 0; outline: none;
|
||||
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-x {
|
||||
width: 22px; height: 22px;
|
||||
color: var(--black-alpha-48);
|
||||
cursor: pointer; display: grid; place-items: center;
|
||||
border-radius: var(--r-sm);
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
|
||||
#${DRAWER_ID} .bullet-list .bl-x svg { width: 11px; height: 11px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 放弃填写 · 确认弹窗 · 覆盖在 drawer 上 */
|
||||
#npd-confirm-bg {
|
||||
position: fixed; inset: 0; z-index: 1200;
|
||||
background: rgba(21, 20, 15, .42);
|
||||
display: grid; place-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
#npd-confirm-bg[hidden] { display: none; }
|
||||
.npd-confirm-modal {
|
||||
width: min(420px, 92vw);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 16px 48px rgba(21, 20, 15, .18);
|
||||
overflow: hidden;
|
||||
}
|
||||
.npd-confirm-modal .dh {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 18px 20px 14px;
|
||||
}
|
||||
.npd-confirm-modal .dh .ic {
|
||||
width: 36px; height: 36px; flex-shrink: 0;
|
||||
border-radius: var(--r-md);
|
||||
background: rgba(180, 30, 30, .08); color: var(--accent-crimson);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.npd-confirm-modal .dh .ic svg { width: 18px; height: 18px; }
|
||||
.npd-confirm-modal .dh .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||
.npd-confirm-modal .dh .ti strong { font-size: 14.5px; color: var(--accent-black); font-weight: 600; }
|
||||
.npd-confirm-modal .dh .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.5; }
|
||||
.npd-confirm-modal .df {
|
||||
display: flex; gap: 8px;
|
||||
padding: 0 20px 18px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.npd-confirm-modal .df button {
|
||||
height: 32px; padding: 0 14px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 12.5px; color: var(--accent-black);
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.npd-confirm-modal .df button:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }
|
||||
.npd-confirm-modal .df button.danger { color: var(--accent-crimson); }
|
||||
.npd-confirm-modal .df button.danger:hover {
|
||||
background: rgba(180, 30, 30, .08);
|
||||
border-color: rgba(180, 30, 30, .35);
|
||||
color: var(--accent-crimson);
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML = `
|
||||
<div id="${DRAWER_BG_ID}"></div>
|
||||
<aside id="${DRAWER_ID}" role="dialog" aria-label="新建商品" aria-hidden="true">
|
||||
<div class="drawer-h">
|
||||
<h3>新建商品</h3>
|
||||
<button class="x" type="button" data-act="close" aria-label="关闭">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="drawer-b">
|
||||
<div class="form-card">
|
||||
<div class="form-h">基础信息</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" data-f="name" placeholder="请输入商品名称(必填)" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div>
|
||||
<label class="field-label">品类<span class="req">*</span></label>
|
||||
<select class="select" data-f="cat">
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
|
||||
<input class="input" data-f="target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品主图<span class="req">*</span></label>
|
||||
<input type="file" data-f="file" accept="image/*" multiple hidden>
|
||||
<div class="pf-upload-row">
|
||||
<div class="pf-upload-zone" data-act="upload-zone">
|
||||
<div class="uz-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
</div>
|
||||
<div class="uz-t">点击上传或<strong>拖拽图片</strong>到此处</div>
|
||||
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
|
||||
</div>
|
||||
<div class="pf-example">
|
||||
<div class="ex-h">示例图</div>
|
||||
<div class="ex-grid">
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
|
||||
</div>
|
||||
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-grid" data-f="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" data-f="bullets">
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车或点击下方按钮"></li>
|
||||
<li class="bl-add-row" data-f="add-row">
|
||||
<button type="button" class="bl-add-btn" data-act="bl-add">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
添加卖点
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-f">
|
||||
<button class="btn-guide" type="button" data-act="guide">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
|
||||
使用指南
|
||||
</button>
|
||||
<button class="btn" type="button" data-act="cancel">取消</button>
|
||||
<button class="btn btn-primary" type="button" data-act="save">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
创建商品
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 放弃填写 · 确认弹窗(覆盖在 drawer 之上) -->
|
||||
<div id="npd-confirm-bg" hidden>
|
||||
<div class="npd-confirm-modal" role="dialog" aria-label="放弃填写">
|
||||
<div class="dh">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
|
||||
</div>
|
||||
<div class="ti">
|
||||
<strong>放弃当前填写?</strong>
|
||||
<span class="mono">// 已填写的商品名、卖点、图片等内容将不会保留</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="df">
|
||||
<button type="button" data-act="keep">继续填写</button>
|
||||
<button type="button" class="danger" data-act="discard">放弃退出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/* ---------- DOM refs (populated by ensureInjected) ---------- */
|
||||
let injected = false;
|
||||
let bg, drawer, $f, $grid, $bullets, $blInput;
|
||||
let confirmBg;
|
||||
let _forceClose = false; // 保存成功后绕过脏检查
|
||||
let _initialCat = ''; // 打开时类目下拉的初始值,用来判断是否被改过
|
||||
let currentOpts = {};
|
||||
const PF_MAX = 5;
|
||||
let pfFiles = []; // { id, dataUrl, name }
|
||||
const blXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]); }
|
||||
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
|
||||
function toast(msg, sub) {
|
||||
if (typeof Shell !== 'undefined' && Shell && Shell.toast) Shell.toast(msg, sub);
|
||||
}
|
||||
|
||||
function ensureInjected() {
|
||||
if (injected) return;
|
||||
// style
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = CSS;
|
||||
document.head.appendChild(styleEl);
|
||||
// html
|
||||
const wrap = document.createElement('div');
|
||||
wrap.innerHTML = HTML;
|
||||
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
|
||||
|
||||
bg = document.getElementById(DRAWER_BG_ID);
|
||||
drawer = document.getElementById(DRAWER_ID);
|
||||
confirmBg = document.getElementById('npd-confirm-bg');
|
||||
$f = {
|
||||
name: drawer.querySelector('[data-f="name"]'),
|
||||
cat: drawer.querySelector('[data-f="cat"]'),
|
||||
target: drawer.querySelector('[data-f="target"]'),
|
||||
file: drawer.querySelector('[data-f="file"]'),
|
||||
};
|
||||
$grid = drawer.querySelector('[data-f="grid"]');
|
||||
$bullets = drawer.querySelector('[data-f="bullets"]');
|
||||
$blInput = $bullets.querySelector('.bl-add .bl-input');
|
||||
|
||||
bindEvents();
|
||||
injected = true;
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// 关闭交互
|
||||
bg.addEventListener('click', close);
|
||||
drawer.addEventListener('click', e => {
|
||||
const a = e.target.closest('[data-act]');
|
||||
if (!a) return;
|
||||
const act = a.dataset.act;
|
||||
if (act === 'close') return close();
|
||||
if (act === 'cancel') return close();
|
||||
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
|
||||
if (act === 'save') return save();
|
||||
if (act === 'upload-zone') return openFilePicker();
|
||||
if (act === 'bl-add') {
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
$blInput.focus();
|
||||
updateBlAddBtn();
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
|
||||
});
|
||||
|
||||
// 上传
|
||||
$f.file.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; });
|
||||
const zone = drawer.querySelector('[data-act="upload-zone"]');
|
||||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });
|
||||
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault(); zone.style.borderColor = '';
|
||||
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
// 卖点 bullet-list
|
||||
$blInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
});
|
||||
$blInput.addEventListener('input', updateBlAddBtn);
|
||||
}
|
||||
|
||||
function updateBlAddBtn() {
|
||||
const row = $bullets && $bullets.querySelector('[data-f="add-row"]');
|
||||
if (!row) return;
|
||||
row.classList.toggle('show', !!($blInput.value || '').trim());
|
||||
}
|
||||
|
||||
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
|
||||
|
||||
function pfRender() {
|
||||
$grid.innerHTML = pfFiles.map(u => `
|
||||
<div class="pf-thumb" data-id="${u.id}">
|
||||
<img src="${u.dataUrl}" alt="${esc(u.name)}">
|
||||
<button class="pf-x" type="button" title="删除">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
$grid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
|
||||
b.onclick = e => {
|
||||
e.stopPropagation();
|
||||
const id = b.closest('.pf-thumb').dataset.id;
|
||||
const i = pfFiles.findIndex(f => f.id === id);
|
||||
if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addFiles(fileList) {
|
||||
const room = PF_MAX - pfFiles.length;
|
||||
if (room <= 0) { toast('已达上限', PF_MAX + ' / ' + PF_MAX + ' 张'); return; }
|
||||
const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);
|
||||
let done = 0;
|
||||
incoming.forEach(f => {
|
||||
const r = new FileReader();
|
||||
r.onload = e => {
|
||||
pfFiles.push({ id: uid(), dataUrl: e.target.result, name: f.name });
|
||||
if (++done === incoming.length) {
|
||||
pfRender();
|
||||
toast('已上传', '+ ' + done + ' 张 · 共 ' + pfFiles.length + ' / ' + PF_MAX);
|
||||
}
|
||||
};
|
||||
r.readAsDataURL(f);
|
||||
});
|
||||
}
|
||||
|
||||
function blRenumber() {
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach((li, i) => {
|
||||
li.querySelector('.num').textContent = i + 1;
|
||||
});
|
||||
}
|
||||
function blAdd(text) {
|
||||
const t = (text || '').trim();
|
||||
if (!t) return;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bl-item';
|
||||
li.innerHTML = '<span class="num">0</span><span class="bl-text">' + esc(t) + '</span><span class="bl-x" title="删除">' + blXSvg + '</span>';
|
||||
$bullets.querySelector('.bl-add').before(li);
|
||||
li.querySelector('.bl-x').addEventListener('click', () => {
|
||||
li.style.transition = 'opacity .15s, transform .15s';
|
||||
li.style.opacity = 0;
|
||||
li.style.transform = 'translateX(-8px)';
|
||||
setTimeout(() => { li.remove(); blRenumber(); }, 150);
|
||||
});
|
||||
blRenumber();
|
||||
}
|
||||
function getBullets() {
|
||||
return [...$bullets.querySelectorAll('.bl-item .bl-text')].map(t => t.textContent.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/* ---------- API ---------- */
|
||||
|
||||
function resetForm() {
|
||||
$f.name.value = '';
|
||||
$f.cat.value = $f.cat.options[0].value;
|
||||
$f.target.value = '';
|
||||
pfFiles = [];
|
||||
pfRender();
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
|
||||
function lockBody() {
|
||||
// 优先用 Shell 的引用计数实现(避免多 overlay 互相解锁)
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.lockScroll === 'function') {
|
||||
Shell.lockScroll();
|
||||
return;
|
||||
}
|
||||
// 兜底: Shell 未加载时本地锁
|
||||
const docEl = document.documentElement;
|
||||
const sbw = window.innerWidth - docEl.clientWidth;
|
||||
drawer._lockSnap = {
|
||||
bodyOverflow: document.body.style.overflow,
|
||||
bodyPaddingRight: document.body.style.paddingRight,
|
||||
htmlOverflow: docEl.style.overflow,
|
||||
};
|
||||
document.body.style.overflow = 'hidden';
|
||||
docEl.style.overflow = 'hidden';
|
||||
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
|
||||
}
|
||||
function unlockBody() {
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.unlockScroll === 'function') {
|
||||
Shell.unlockScroll();
|
||||
return;
|
||||
}
|
||||
const s = drawer._lockSnap;
|
||||
if (s) {
|
||||
document.body.style.overflow = s.bodyOverflow;
|
||||
document.body.style.paddingRight = s.bodyPaddingRight;
|
||||
document.documentElement.style.overflow = s.htmlOverflow;
|
||||
drawer._lockSnap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function open(opts) {
|
||||
ensureInjected();
|
||||
if (drawer.classList.contains('show')) return; // 已开则不重复锁
|
||||
currentOpts = opts || {};
|
||||
resetForm();
|
||||
_initialCat = $f.cat.value; // 快照「打开时类目」用于脏检查
|
||||
bg.classList.add('show');
|
||||
drawer.classList.add('show');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
lockBody();
|
||||
setTimeout(() => $f.name.focus(), 280);
|
||||
}
|
||||
|
||||
/* ---------- 脏检查 + 退出确认 ---------- */
|
||||
function isDirty() {
|
||||
if (!injected) return false;
|
||||
if (($f.name.value || '').trim()) return true;
|
||||
if (($f.target.value || '').trim()) return true;
|
||||
if (($blInput.value || '').trim()) return true;
|
||||
if (pfFiles.length > 0) return true;
|
||||
if ($bullets.querySelectorAll('.bl-item').length > 0) return true;
|
||||
if ($f.cat.value !== _initialCat) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function confirmDiscard() {
|
||||
return new Promise(resolve => {
|
||||
confirmBg.hidden = false;
|
||||
const buttons = confirmBg.querySelectorAll('button[data-act]');
|
||||
function done(choice) {
|
||||
confirmBg.hidden = true;
|
||||
buttons.forEach(b => b.onclick = null);
|
||||
confirmBg.onclick = null;
|
||||
document.removeEventListener('keydown', escHandler, true);
|
||||
resolve(choice === 'discard');
|
||||
}
|
||||
function escHandler(e) {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
done('keep');
|
||||
}
|
||||
}
|
||||
buttons.forEach(b => b.onclick = () => done(b.dataset.act));
|
||||
confirmBg.onclick = e => { if (e.target === confirmBg) done('keep'); };
|
||||
// 用 capture · 截在外层 Esc 监听器之前
|
||||
document.addEventListener('keydown', escHandler, true);
|
||||
});
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!injected) return;
|
||||
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
|
||||
if (!_forceClose && isDirty()) {
|
||||
const ok = await confirmDiscard();
|
||||
if (!ok) return;
|
||||
}
|
||||
bg.classList.remove('show');
|
||||
drawer.classList.remove('show');
|
||||
drawer.setAttribute('aria-hidden', 'true');
|
||||
unlockBody();
|
||||
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
|
||||
}
|
||||
|
||||
function closeForce() {
|
||||
_forceClose = true;
|
||||
try { close(); } finally { _forceClose = false; }
|
||||
}
|
||||
|
||||
function save() {
|
||||
// 兜底:用户在卖点输入框敲了字但没回车/没点「添加卖点」就直接点创建,
|
||||
// 这里自动提交一次,避免静默丢失最后一条卖点(常见误操作)
|
||||
if ($blInput && ($blInput.value || '').trim()) {
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
|
||||
const name = ($f.name.value || '').trim();
|
||||
const cat = $f.cat.value;
|
||||
const target = ($f.target.value || '').trim();
|
||||
const points = getBullets();
|
||||
const images = pfFiles.slice();
|
||||
|
||||
if (!name) {
|
||||
toast('请填写商品名称');
|
||||
$f.name.focus();
|
||||
return;
|
||||
}
|
||||
if (images.length === 0) {
|
||||
toast('请上传商品主图', '至少 1 张');
|
||||
return;
|
||||
}
|
||||
if (points.length === 0) {
|
||||
toast('请添加核心卖点', '至少 1 条');
|
||||
$blInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const product = {
|
||||
id: 'np-' + uid(),
|
||||
name, cat, target,
|
||||
points,
|
||||
images,
|
||||
imgs: images.length,
|
||||
};
|
||||
// 持久化到 sessionStorage('fs-extra-products') · 仅当前标签页生命周期内有效
|
||||
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
|
||||
list.push({
|
||||
id: product.id,
|
||||
name, cat, target,
|
||||
tags: '',
|
||||
assets: 0,
|
||||
videos: 0,
|
||||
bullets: points,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
sessionStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
// 短期 sessionStorage 缓存完整 product(含图片 dataUrl),供 detail 页 ?id=new
|
||||
// 读出后渲染。读完即清除,避免污染后续操作。
|
||||
try {
|
||||
sessionStorage.setItem('npd-last-created', JSON.stringify(product));
|
||||
} catch (e) { /* dataUrl 可能过大 → 退化为不带图,detail 仍能渲染文本字段 */ }
|
||||
|
||||
if (typeof currentOpts.onSave === 'function') {
|
||||
toast('商品已创建', '+ ' + name);
|
||||
currentOpts.onSave(product);
|
||||
closeForce();
|
||||
return;
|
||||
}
|
||||
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底
|
||||
// 消除"闪 host"(浏览器导航开始后,整页被新页面替换,drawer 自然消失)
|
||||
toast('商品已创建', '+ ' + name);
|
||||
const url = 'product-detail.html?product=' + encodeURIComponent(name) + '&id=new';
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
window.NewProductDrawer = { open, close };
|
||||
|
||||
/* ---------- sessionStorage 自动打开钩子 ---------- */
|
||||
// 任何页面只要在跳转前 sessionStorage.setItem('npd-auto-open','1') 即可,
|
||||
// 落地页加载完模块后,会自动 open() 一次并清掉 flag。
|
||||
// 用于:product-create.html 重定向后让落地页弹出 drawer,而不是用户重新点击。
|
||||
function checkAutoOpen() {
|
||||
try {
|
||||
if (sessionStorage.getItem('npd-auto-open') === '1') {
|
||||
sessionStorage.removeItem('npd-auto-open');
|
||||
// 延后一拍,确保宿主页面自己的 init 已经跑完
|
||||
setTimeout(() => open(), 50);
|
||||
}
|
||||
} catch (e) { /* sessionStorage 不可用就静默放弃 */ }
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', checkAutoOpen);
|
||||
} else {
|
||||
checkAutoOpen();
|
||||
}
|
||||
})();
|
||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 66 KiB |