完善原型 mock 与交互细节
This commit is contained in:
@@ -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 => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
})[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);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user