From 9d9b9e417697bc73ff65d0ed4608b1871df459fb Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 12 Oct 2026 10:28:54 +0800 Subject: [PATCH] feat: add fuzzy brand search to product editor --- static/js/app.js | 40 ++++++++++++++++++++++++++++-- tests/test_product_brand_search.py | 28 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/test_product_brand_search.py diff --git a/static/js/app.js b/static/js/app.js index d395943..97a6202 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3389,9 +3389,9 @@ function renderBrandManagementPagination(total,size,page){var el=document.getEle 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 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='';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 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) { @@ -28890,6 +28890,8 @@ function _buildProductEditForm(row) { opts += ''; }); html += '
'; + } else if (f.key === 'brand') { + html += '
'; } else if (f.supSearch) { html += '
'; } 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 = '
未找到匹配品牌
'; + dd.style.display = 'block'; + return; + } + dd.innerHTML = matched.slice(0, 50).map(function(brand) { + return '
' + escHtml(brand) + '
'; + }).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) { var isNonProd = prodActiveTab === '非成品'; var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at']; diff --git a/tests/test_product_brand_search.py b/tests/test_product_brand_search.py new file mode 100644 index 0000000..3d746e4 --- /dev/null +++ b/tests/test_product_brand_search.py @@ -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()