这篇文章介绍主页中左边的分类树是如何实现的。
1. 首先打开主页,https://localhost:8443/ecommerce/control/main
2.找到 上面分类树界面对应的html源码。可以看到开始是以 component://ecommerce/widget/CatalogScreens.xml#productCategories 开始的。
3.打开component://ecommerce/widget/CatalogScreens.xml#productCategories
<screen name="productCategories">
<section>
<actions>
<set field="tabButtonItem" value="LookupProductCategories"/>
<script location="component://ecommerce/groovyScripts/catalog/ProductCategories.groovy"/>
</actions>
<widgets>
<platform-specific><html><html-template location="component://ecommerce/template/catalog/ProductCategories.ftl"/></html></platform-specific>
</widgets>
</section>
</screen>
打开component://ecommerce/groovyScripts/catalog/ProductCategories.groovy
/*
* This script is also referenced by the ecommerce's screens and
* should not contain order component's specific code.
*/
import org.apache.ofbiz.entity.util.EntityUtil
import org.apache.ofbiz.base.util.*
import org.apache.ofbiz.product.catalog.*
import org.apache.ofbiz.product.category.*
import org.apache.ofbiz.entity.*
List fillTree(rootCat ,CatLvl, parentCategoryId) {
if(rootCat) {
rootCat.sort{ it.productCategoryId }
def listTree = []
for(root in rootCat) {
preCatChilds = from("ProductCategoryRollup").where("parentProductCategoryId", root.productCategoryId).queryList()
catChilds = EntityUtil.getRelated("CurrentProductCategory",null,preCatChilds,false)
def childList = []
// CatLvl uses for identify the Category level for display different css class
if(catChilds) {
if(CatLvl==2)
childList = fillTree(catChilds,CatLvl+1, parentCategoryId.replaceAll("/", "")+'/'+root.productCategoryId)
// replaceAll and '/' uses for fix bug in the breadcrum for href of category
else if(CatLvl==1)
childList = fillTree(catChilds,CatLvl+1, parentCategoryId.replaceAll("/", "")+root.productCategoryId)
else
childList = fillTree(catChilds,CatLvl+1, parentCategoryId+'/'+root.productCategoryId)
}
productsInCat = from("ProductCategoryAndMember").where("productCategoryId", root.productCategoryId).queryList()
// Display the category if this category containing products or contain the category that's containing products
if(productsInCat || childList) {
def rootMap = [:]
category = from("ProductCategory").where("productCategoryId", root.productCategoryId).queryOne()
categoryContentWrapper = new CategoryContentWrapper(category, request)
context.title = categoryContentWrapper.get("CATEGORY_NAME", "html")
categoryDescription = categoryContentWrapper.get("DESCRIPTION", "html")
if(categoryContentWrapper.get("CATEGORY_NAME", "html").toString())
rootMap["categoryName"] = categoryContentWrapper.get("CATEGORY_NAME", "html")
else
rootMap["categoryName"] = root.categoryName
if(categoryContentWrapper.get("DESCRIPTION", "html").toString())
rootMap["categoryDescription"] = categoryContentWrapper.get("DESCRIPTION", "html")
else
rootMap["categoryDescription"] = root.description
rootMap["productCategoryId"] = root.productCategoryId
rootMap["parentCategoryId"] = parentCategoryId
rootMap["child"] = childList
listTree.add(rootMap)
}
}
return listTree
}
}
CategoryWorker.getRelatedCategories(request, "topLevelList", CatalogWorker.getCatalogTopCategoryId(request, CatalogWorker.getCurrentCatalogId(request)), true)
curCategoryId = parameters.category_id ?: parameters.CATEGORY_ID ?: ""
request.setAttribute("curCategoryId", curCategoryId)
CategoryWorker.setTrail(request, curCategoryId)
categoryList = request.getAttribute("topLevelList")
if (categoryList) {
catContentWrappers = [:]
CategoryWorker.getCategoryContentWrappers(catContentWrappers, categoryList, request)
context.catContentWrappers = catContentWrappers
completedTree = fillTree(categoryList, 1, "")
context.completedTree = completedTree
}
<script language="javascript" type="text/javascript"
src="<@ofbizContentUrl>/images/jquery/plugins/jsTree/jquery.jstree.js</@ofbizContentUrl>"></script>
<script type="text/javascript"
src="<@ofbizContentUrl>/images/jquery/ui/js/jquery.cookie-1.4.0.js</@ofbizContentUrl>"></script>
<script type="text/javascript">
<#-- some labels are not unescaped in the JSON object so we have to do this manuely -->
function unescapeHtmlText(text) {
return jQuery('<div />').html(text).text()
}
jQuery(window).load(createTree());
<#-- creating the JSON Data -->
var rawdata = [
<#if (requestAttributes.topLevelList)??>
<#assign topLevelList = requestAttributes.topLevelList>
</#if>
<#if (topLevelList?has_content)>
<@fillTree rootCat=completedTree/>
</#if>
<#macro fillTree rootCat>
<#if (rootCat?has_content)>
<#list rootCat?sort_by("productCategoryId") as root>
{
"data": {
"title": unescapeHtmlText(
"<#if root.categoryName??>${root.categoryName?js_string}<#elseif root.categoryDescription??>${root.categoryDescription?js_string}<#else>${root.productCategoryId?js_string}</#if>"),
"attr": {
"href": "javascript: void(0);",
"onClick": "callDocument('${root.productCategoryId}', '${root.parentCategoryId}')",
"class": "${root.cssClass!}"
}
},
"attr": {"id": "${root.productCategoryId}"}
<#if root.child?has_content>
, "children": [
<@fillTree rootCat=root.child/>]
</#if>
<#if root_has_next>
},
<#else>
}
</#if>
</#list>
</#if>
</#macro>
]
;
<#-------------------------------------------------------------------------------------define Requests-->
var editDocumentTreeUrl = '<@ofbizUrl>/views/EditDocumentTree</@ofbizUrl>';
var listDocument = '<@ofbizUrl>/views/ListDocument</@ofbizUrl>';
var editDocumentUrl = '<@ofbizUrl>/views/EditDocument</@ofbizUrl>';
var deleteDocumentUrl = '<@ofbizUrl>removeDocumentFromTree</@ofbizUrl>';
<#-------------------------------------------------------------------------------------create Tree-->
function createTree() {
jQuery(function () {
jQuery("#tree").jstree({
"themes": {
"theme": "classic",
"icons": false
},
"cookies": {
"cookie_options": {path: '/'}
},
"plugins": ["themes", "json_data", "cookies"],
"json_data": {
"data": rawdata
}
});
});
}
<#-------------------------------------------------------------------------------------callDocument function-->
function callDocument(id, parentCategoryStr) {
var checkUrl = '<@ofbizUrl>productCategoryList</@ofbizUrl>';
if (checkUrl.search("http"))
var ajaxUrl = '<@ofbizUrl>productCategoryList</@ofbizUrl>';
else
var ajaxUrl = '<@ofbizUrl>productCategoryListSecure</@ofbizUrl>';
//jQuerry Ajax Request
jQuery.ajax({
url: ajaxUrl,
type: 'POST',
data: {"category_id": id, "parentCategoryStr": parentCategoryStr},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#div3').html(msg);
}
});
}
<#-------------------------------------------------------------------------------------callCreateDocumentTree function-->
function callCreateDocumentTree(contentId) {
jQuery.ajax({
url: editDocumentTreeUrl,
type: 'POST',
data: {
contentId: contentId,
contentAssocTypeId: 'TREE_CHILD'
},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#Document').html(msg);
}
});
}
<#-------------------------------------------------------------------------------------callCreateSection function-->
function callCreateDocument(contentId) {
jQuery.ajax({
url: editDocumentUrl,
type: 'POST',
data: {contentId: contentId},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#Document').html(msg);
}
});
}
<#-------------------------------------------------------------------------------------callEditSection function-->
function callEditDocument(contentIdTo) {
jQuery.ajax({
url: editDocumentUrl,
type: 'POST',
data: {contentIdTo: contentIdTo},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#Document').html(msg);
}
});
}
<#-------------------------------------------------------------------------------------callDeleteItem function-->
function callDeleteDocument(contentId, contentIdTo, contentAssocTypeId, fromDate) {
jQuery.ajax({
url: deleteDocumentUrl,
type: 'POST',
data: {
contentId: contentId,
contentIdTo: contentIdTo,
contentAssocTypeId: contentAssocTypeId,
fromDate: fromDate
},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
location.reload();
}
});
}
<#-------------------------------------------------------------------------------------callRename function-->
function callRenameDocumentTree(contentId) {
jQuery.ajax({
url: editDocumentTreeUrl,
type: 'POST',
data: {
contentId: contentId,
contentAssocTypeId: 'TREE_CHILD',
rename: 'Y'
},
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#Document').html(msg);
}
});
}
<#------------------------------------------------------pagination function -->
function nextPrevDocumentList(url) {
url = '<@ofbizUrl>'+ url+'</@ofbizUrl>';
jQuery.ajax({
url: url,
type: 'POST',
error: function (msg) {
alert("An error occurred loading content! : " + msg);
},
success: function (msg) {
jQuery('#Document').html(msg);
}
});
}
</script>
<div id="quickadd" class="screenlet">
<div class="screenlet-title-bar">
<ul>
<li class="h3">${uiLabelMap.ProductCategories}</li>
</ul>
</div>
<div class="screenlet-body" id="tree">
</div>
</div>
数据填充部分主要看<#-- creating the JSON Data -->那一块。看rawdata是如何组织的。
分析ProductCategories.groovy代码:
1. CatalogWorker.getCurrentCatalogId(request)
这个应该是获取当前目录id。默认是DemoCatalog
2. CategoryWorker.getRelatedCategories(request, "topLevelList", CatalogWorker.getCatalogTopCategoryId(request, CatalogWorker.getCurrentCatalogId(request)), true)
查看源码CategoryWorker.getRelatedCategories
public static String getCatalogTopCategoryId(ServletRequest request, String prodCatalogId) {
if (UtilValidate.isEmpty(prodCatalogId)) return null;
List<GenericValue> prodCatalogCategories = getProdCatalogCategories(request, prodCatalogId, "PCCT_BROWSE_ROOT");
if (UtilValidate.isNotEmpty(prodCatalogCategories)) {
GenericValue prodCatalogCategory = EntityUtil.getFirst(prodCatalogCategories);
return prodCatalogCategory.getString("productCategoryId");
} else {
return null;
}
}
可以知道实际执行的sql语句是:
SELECT * FROM Prod_Catalog_Category t WHERE t.prod_catalog_id='DemoCatalog' AND t.prod_catalog_category_type_id='PCCT_BROWSE_ROOT'
结果如下:
查询结果会放入request的topLevelList属性里。即categoryList就是上面的查询结果。这个例子的查询结果只有一条记录。
3.接下来重点看fillTree方法。
3.1 for(root in rootCat) { //由于categoryList结果只有一条记录,所以for只会循环一次。
3.2 preCatChilds = from("ProductCategoryRollup").where("parentProductCategoryId", root.productCategoryId).queryList() 等价于
SELECT * FROM Product_Category_Rollup WHERE parent_Product_Category_Id='CATALOG1'
root.productCategoryId的取值就是上面
categoryList结果的product_Category_Id字段的值。
结果显示:
3.3 catChilds = EntityUtil.getRelated("CurrentProductCategory",null,preCatChilds,false)
这行代码是根据preCatChilds关联对应的分类记录。等价于下面sql的查询结果:
SELECT t2.* FROM Product_Category_Rollup t1
INNER JOIN Product_Category t2 ON t1.Product_Category_id = t2.Product_Category_id
WHERE t1.parent_Product_Category_Id='CATALOG1'
结果显示:
3.4 递归调用获取分类的子分类。
// CatLvl uses for identify the Category level for display different css class
if(catChilds) {
if(CatLvl==2)
childList = fillTree(catChilds,CatLvl+1, parentCategoryId.replaceAll("/", "")+'/'+root.productCategoryId)
// replaceAll and '/' uses for fix bug in the breadcrum for href of category
else if(CatLvl==1)
childList = fillTree(catChilds,CatLvl+1, parentCategoryId.replaceAll("/", "")+root.productCategoryId)
else
childList = fillTree(catChilds,CatLvl+1, parentCategoryId+'/'+root.productCategoryId)
}
第一次执行时CatLvl是等于1.
SELECT t2.* FROM Product_Category_Rollup t1
INNER JOIN Product_Category t2 ON t1.Product_Category_id = t2.Product_Category_id
WHERE t1.parent_Product_Category_Id=?
上面的参数?表示catChilds列表每条记录的产品分类id即Product_Category_id,取值有:100,200,dropShip,FA-100,FOOD-001,GC-100,PC-100,SERV-001 八条记录循环遍历。
3.5 构造rootMap。
rootMap的主要属性有:categoryName,categoryDescription,productCategoryId,parentCategoryId,child,cssClass
研究了很久,发现下面三条记录的category_name和description字段都为null,而分类树中确显示Configurables Foods,Gift Cards,Configurables PCs。不知道这些内容是哪里来的,找了好久也没找到,暂时先不管了。以后有时间再研究。