feat: add fuzzy brand search to product editor

This commit is contained in:
Codex 2026-10-12 10:28:54 +08:00
parent 6a94176f8a
commit 9d9b9e4176
2 changed files with 66 additions and 2 deletions

View File

@ -3389,9 +3389,9 @@ function renderBrandManagementPagination(total,size,page){var el=document.getEle
function showBrandAdd(){showBrandModal('新增品牌','','');} function showBrandAdd(){showBrandModal('新增品牌','','');}
function showBrandEdit(brand){fetch('/api/brand-management/list?search='+encodeURIComponent(brand)+'&size=10').then(function(r){return r.json();}).then(function(res){var row=(res.data||[]).find(function(x){return x.brand===brand;});showBrandModal('编辑品牌',brand,row?row.category:'');}).catch(function(){showBrandModal('编辑品牌',brand,'');});} function showBrandEdit(brand){fetch('/api/brand-management/list?search='+encodeURIComponent(brand)+'&size=10').then(function(r){return r.json();}).then(function(res){var row=(res.data||[]).find(function(x){return x.brand===brand;});showBrandModal('编辑品牌',brand,row?row.category:'');}).catch(function(){showBrandModal('编辑品牌',brand,'');});}
function showBrandModal(title,brand,category){var old=document.getElementById('brandManageModal');if(old)old.remove();var overlay=document.createElement('div');overlay.id='brandManageModal';overlay.className='modal-overlay';overlay.style.display='flex';overlay.onclick=function(e){if(e.target===overlay){}};overlay.innerHTML='<div class="modal" style="width:460px"><div class="modal-header"><h3>'+title+'</h3><span class="modal-close" onclick="document.getElementById(\'brandManageModal\').remove()">&times;</span></div><div class="modal-body" style="padding:20px"><label style="display:block;margin-bottom:6px">品牌名称</label><input id="brandManageName" class="form-input" style="width:100%;box-sizing:border-box" autocomplete="off" value="'+escHtml(brand)+'" placeholder="请输入品牌名称"><label style="display:block;margin:14px 0 6px">分类</label><select id="brandManageModalCategory" class="form-input" style="width:100%;text-align:center"><option value="">未分类</option><option value="国内">国内</option><option value="海外">海外</option></select></div><div class="modal-footer"><button class="btn" onclick="document.getElementById(\'brandManageModal\').remove()">取消</button><button class="btn btn-primary" onclick="submitBrandModal('+inlineJsString(brand)+')">保存</button></div></div>';document.body.appendChild(overlay);document.getElementById('brandManageModalCategory').value=category||'';document.getElementById('brandManageName').focus();} function showBrandModal(title,brand,category){var old=document.getElementById('brandManageModal');if(old)old.remove();var overlay=document.createElement('div');overlay.id='brandManageModal';overlay.className='modal-overlay';overlay.style.display='flex';overlay.onclick=function(e){if(e.target===overlay){}};overlay.innerHTML='<div class="modal" style="width:460px"><div class="modal-header"><h3>'+title+'</h3><span class="modal-close" onclick="document.getElementById(\'brandManageModal\').remove()">&times;</span></div><div class="modal-body" style="padding:20px"><label style="display:block;margin-bottom:6px">品牌名称</label><input id="brandManageName" class="form-input" style="width:100%;box-sizing:border-box" autocomplete="off" value="'+escHtml(brand)+'" placeholder="请输入品牌名称"><label style="display:block;margin:14px 0 6px">分类</label><select id="brandManageModalCategory" class="form-input" style="width:100%;text-align:center"><option value="">未分类</option><option value="国内">国内</option><option value="海外">海外</option></select></div><div class="modal-footer"><button class="btn" onclick="document.getElementById(\'brandManageModal\').remove()">取消</button><button class="btn btn-primary" onclick="submitBrandModal('+inlineJsString(brand)+')">保存</button></div></div>';document.body.appendChild(overlay);document.getElementById('brandManageModalCategory').value=category||'';document.getElementById('brandManageName').focus();}
function submitBrandModal(oldBrand){var name=(document.getElementById('brandManageName').value||'').trim(),category=document.getElementById('brandManageModalCategory').value||'';if(!name){alert('品牌名称不能为空');return;}var isEdit=!!oldBrand;fetch(isEdit?'/api/brand-management/update':'/api/brand-management/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(isEdit?{old_brand:oldBrand,new_brand:name,category:category}:{brand:name,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');return;}document.getElementById('brandManageModal').remove();loadBrandManagementData(window._brandManagePage||1);}).catch(function(){alert('网络异常,保存失败');});} function submitBrandModal(oldBrand){var name=(document.getElementById('brandManageName').value||'').trim(),category=document.getElementById('brandManageModalCategory').value||'';if(!name){alert('品牌名称不能为空');return;}var isEdit=!!oldBrand;fetch(isEdit?'/api/brand-management/update':'/api/brand-management/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(isEdit?{old_brand:oldBrand,new_brand:name,category:category}:{brand:name,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');return;}window._brandList=null;document.getElementById('brandManageModal').remove();loadBrandManagementData(window._brandManagePage||1);}).catch(function(){alert('网络异常,保存失败');});}
function saveBrandCategory(brand,category){fetch('/api/brand-management/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({old_brand:brand,new_brand:brand,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');}else{loadBrandManagementData(window._brandManagePage||1);}}).catch(function(){alert('网络异常,保存失败');});} function saveBrandCategory(brand,category){fetch('/api/brand-management/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({old_brand:brand,new_brand:brand,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');}else{loadBrandManagementData(window._brandManagePage||1);}}).catch(function(){alert('网络异常,保存失败');});}
function deleteBrand(brand){showCenteredConfirm('确认删除','确定删除品牌“'+brand+'”吗?删除后不可恢复;正在被产品使用的品牌将受到保护,无法删除。').then(function(ok){if(!ok)return;fetch('/api/brand-management/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({brand:brand})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){showCenteredAlert('删除失败',res.msg||'删除失败');return;}showCenteredAlert('删除成功',res.msg||'品牌已删除');loadBrandManagementData(window._brandManagePage||1);}).catch(function(){showCenteredAlert('删除失败','网络异常,请稍后重试');});});} function deleteBrand(brand){showCenteredConfirm('确认删除','确定删除品牌“'+brand+'”吗?删除后不可恢复;正在被产品使用的品牌将受到保护,无法删除。').then(function(ok){if(!ok)return;fetch('/api/brand-management/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({brand:brand})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){showCenteredAlert('删除失败',res.msg||'删除失败');return;}window._brandList=null;showCenteredAlert('删除成功',res.msg||'品牌已删除');loadBrandManagementData(window._brandManagePage||1);}).catch(function(){showCenteredAlert('删除失败','网络异常,请稍后重试');});});}
// ─── 类目管理页面 ─── // ─── 类目管理页面 ───
function loadProductCategoryPage(container) { function loadProductCategoryPage(container) {
@ -28890,6 +28890,8 @@ function _buildProductEditForm(row) {
opts += '<option value="' + o + '"' + sel + '>' + o + '</option>'; opts += '<option value="' + o + '"' + sel + '>' + o + '</option>';
}); });
html += '<div><label style="font-size:12px;color:#888">' + f.label + '</label><select id="pe_' + f.key + '" class="form-input" style="width:100%;box-sizing:border-box">' + opts + '</select></div>'; html += '<div><label style="font-size:12px;color:#888">' + f.label + '</label><select id="pe_' + f.key + '" class="form-input" style="width:100%;box-sizing:border-box">' + opts + '</select></div>';
} else if (f.key === 'brand') {
html += '<div style="position:relative"><label style="font-size:12px;color:#888">' + f.label + '</label><input id="pe_brand" type="text" class="form-input" style="width:100%;box-sizing:border-box" value="' + val + '" autocomplete="off" placeholder="输入品牌名称搜索" oninput="editBrandSearch(this)" onfocus="editBrandSearch(this,true)" onblur="setTimeout(hideEditBrandDropdown,150)"><div class="pe-brand-dd" style="position:absolute;top:100%;left:0;right:0;z-index:101;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:180px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>';
} else if (f.supSearch) { } else if (f.supSearch) {
html += '<div style="position:relative"><label style="font-size:12px;color:#888">' + f.label + '</label><input id="pe_' + f.key + '" type="text" class="form-input" style="width:100%;box-sizing:border-box" value="' + val + '"' + roAttr + ' autocomplete="off" oninput="editSupSearch(this)" onfocus="editSupSearch(this)"><div class="pe-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>'; html += '<div style="position:relative"><label style="font-size:12px;color:#888">' + f.label + '</label><input id="pe_' + f.key + '" type="text" class="form-input" style="width:100%;box-sizing:border-box" value="' + val + '"' + roAttr + ' autocomplete="off" oninput="editSupSearch(this)" onfocus="editSupSearch(this)"><div class="pe-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>';
} else { } else {
@ -28926,6 +28928,40 @@ window.editSupSearch = function(input) {
}); });
}; };
// ─── 产品信息管理编辑弹窗:品牌模糊搜索 ───
window.editBrandSearch = function(input, showAll) {
var dd = input && input.parentElement ? input.parentElement.querySelector('.pe-brand-dd') : null;
if (!dd) return;
window.loadBrandList(function(list) {
if (!document.body.contains(input)) return;
var keyword = showAll ? '' : String(input.value || '').trim().toLowerCase();
var matched = (list || []).filter(function(brand) {
return !keyword || String(brand).toLowerCase().indexOf(keyword) >= 0;
});
if (!matched.length) {
dd.innerHTML = '<div style="padding:7px 10px;color:#999;font-size:12px">未找到匹配品牌</div>';
dd.style.display = 'block';
return;
}
dd.innerHTML = matched.slice(0, 50).map(function(brand) {
return '<div class="pe-brand-dd-item" style="padding:6px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0" onmousedown="event.preventDefault()" onmouseenter="this.style.background=\'#e6f7ff\'" onmouseleave="this.style.background=\'\'" onclick="selectEditBrand(' + inlineJsString(brand) + ')">' + escHtml(brand) + '</div>';
}).join('');
dd.style.display = 'block';
});
};
window.selectEditBrand = function(brand) {
var input = document.getElementById('pe_brand');
if (input) input.value = brand || '';
window.hideEditBrandDropdown();
};
window.hideEditBrandDropdown = function() {
var modal = document.getElementById('productEditModal');
var dd = modal ? modal.querySelector('.pe-brand-dd') : null;
if (dd) dd.style.display = 'none';
};
window.saveQuickEditProduct = function(productCode) { window.saveQuickEditProduct = function(productCode) {
var isNonProd = prodActiveTab === '非成品'; var isNonProd = prodActiveTab === '非成品';
var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at']; var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at'];

View File

@ -0,0 +1,28 @@
"""产品信息管理编辑弹窗品牌模糊搜索回归测试。"""
import unittest
from pathlib import Path
APP_JS = (Path(__file__).resolve().parents[1] / 'static/js/app.js').read_text(encoding='utf-8')
class ProductBrandSearchTest(unittest.TestCase):
def test_quick_edit_brand_uses_searchable_input(self):
self.assertIn('id="pe_brand" type="text"', APP_JS)
self.assertIn('oninput="editBrandSearch(this)"', APP_JS)
self.assertIn('onfocus="editBrandSearch(this,true)"', APP_JS)
self.assertIn('class="pe-brand-dd"', APP_JS)
def test_brand_search_uses_shared_brand_list_and_fuzzy_match(self):
self.assertIn('window.editBrandSearch = function(input, showAll)', APP_JS)
self.assertIn('window.loadBrandList(function(list)', APP_JS)
self.assertIn("String(brand).toLowerCase().indexOf(keyword) >= 0", APP_JS)
self.assertIn('window.selectEditBrand = function(brand)', APP_JS)
def test_brand_changes_invalidate_search_cache(self):
self.assertGreaterEqual(APP_JS.count('window._brandList=null'), 2)
if __name__ == '__main__':
unittest.main()