完善原型 mock 与交互细节

This commit is contained in:
iye
2026-05-27 14:34:16 +08:00
parent 086d92991e
commit 54b57f76d0
33 changed files with 609 additions and 4 deletions
+147
View File
@@ -292,6 +292,144 @@ window.Shell = {
document.getElementById('global-search')?.focus();
}
});
this.enhanceSelects(document);
},
enhanceSelects(root = document) {
const scope = root || document;
const nodes = [];
if (scope.matches?.('select')) nodes.push(scope);
scope.querySelectorAll?.('select:not([data-rs-select-bound])').forEach(el => nodes.push(el));
const selects = [...new Set(nodes)].filter(select =>
select.tagName === 'SELECT' &&
!select.multiple &&
Number(select.size || 0) <= 1 &&
!select.closest('.rs-select') &&
select.dataset.nativeSelect !== 'true'
);
if (!selects.length) {
this._bindCustomSelectInfrastructure();
return;
}
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[c]);
const checkSvg = ShellIcon('check', { className: 'mi-check', size: 13 });
const caretSvg = ShellIcon('chevronDown', { size: 12 });
selects.forEach(select => {
const wrap = document.createElement('span');
wrap.className = 'rs-select';
if (select.classList.contains('v-edit')) wrap.classList.add('v-edit');
if (select.hidden) wrap.hidden = true;
if (
select.classList.contains('select') ||
select.classList.contains('v-select') ||
select.classList.contains('nm-select') ||
select.closest('.field') ||
select.closest('.form-row')
) wrap.classList.add('rs-select-fill');
if (select.closest('.filter-bar')) wrap.classList.add('rs-select-filter');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'rs-select-btn';
btn.setAttribute('aria-haspopup', 'listbox');
btn.setAttribute('aria-expanded', 'false');
const menu = document.createElement('div');
menu.className = 'rs-select-menu';
menu.setAttribute('role', 'listbox');
select.parentNode.insertBefore(wrap, select);
wrap.appendChild(select);
wrap.appendChild(btn);
wrap.appendChild(menu);
select.dataset.rsSelectBound = '1';
select.tabIndex = -1;
const sync = () => {
const selected = select.selectedOptions[0] || select.options[select.selectedIndex] || select.options[0];
btn.disabled = select.disabled;
btn.innerHTML = `<span class="rs-select-label">${esc(selected?.textContent || '')}</span>${caretSvg}`;
menu.innerHTML = [...select.options].map((opt, i) => `
<button class="rs-select-option${i === select.selectedIndex ? ' selected' : ''}" type="button" role="option" data-index="${i}" aria-selected="${i === select.selectedIndex ? 'true' : 'false'}" ${opt.disabled ? 'disabled' : ''}>
${checkSvg}
<span>${esc(opt.textContent)}</span>
</button>
`).join('');
};
btn.addEventListener('click', e => {
e.stopPropagation();
if (select.disabled) return;
const willOpen = !wrap.classList.contains('open');
this.closeCustomSelects(wrap);
wrap.classList.toggle('open', willOpen);
btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
if (willOpen) {
menu.classList.remove('align-right');
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth - 12) menu.classList.add('align-right');
}
});
btn.addEventListener('keydown', e => {
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'ArrowDown') return;
e.preventDefault();
btn.click();
});
menu.addEventListener('click', e => {
const item = e.target.closest('.rs-select-option');
if (!item || item.disabled) return;
const idx = Number(item.dataset.index);
if (!Number.isNaN(idx) && select.selectedIndex !== idx) {
select.selectedIndex = idx;
select.dispatchEvent(new Event('change', { bubbles: true }));
}
sync();
this.closeCustomSelects();
btn.focus();
});
select.addEventListener('change', sync);
select._rsSelectSync = sync;
sync();
});
this._bindCustomSelectInfrastructure();
},
refreshCustomSelects(root = document) {
root.querySelectorAll?.('select[data-rs-select-bound="1"]').forEach(select => {
if (typeof select._rsSelectSync === 'function') select._rsSelectSync();
});
},
closeCustomSelects(except) {
document.querySelectorAll('.rs-select.open').forEach(wrap => {
if (except && wrap === except) return;
wrap.classList.remove('open');
wrap.querySelector('.rs-select-btn')?.setAttribute('aria-expanded', 'false');
});
},
_bindCustomSelectInfrastructure() {
if (this._rsSelectInfrastructureBound) return;
this._rsSelectInfrastructureBound = true;
document.addEventListener('click', e => {
if (!e.target.closest('.rs-select')) this.closeCustomSelects();
requestAnimationFrame(() => this.refreshCustomSelects(document));
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') this.closeCustomSelects();
});
this._rsSelectObserver = new MutationObserver(records => {
if (!records.some(r => [...r.addedNodes].some(n =>
n.nodeType === 1 && (n.matches?.('select') || n.querySelector?.('select'))
))) return;
requestAnimationFrame(() => this.enhanceSelects(document));
});
if (document.body) this._rsSelectObserver.observe(document.body, { childList: true, subtree: true });
},
// [DEPRECATED] 新建商品弹窗 · 已废弃,创建商品改为直跳 product-create.html
@@ -612,3 +750,12 @@ window.Shell = {
this.unlockScroll();
}
};
(function loadMockMedia() {
if (window.__AIRSHELF_DISABLE_MOCK_MEDIA__) return;
if (document.querySelector('script[data-mock-media]')) return;
const script = document.createElement('script');
script.src = 'assets/mock-media.js?v=2026052703';
script.dataset.mockMedia = '1';
document.head.appendChild(script);
})();