Portal 文档
基于jqurey easyUI的portal组件
Portal 技术综述
代码:
panels:
Object { title="服务查询", url="widget/serviceQuery.do", wId=1},
Object { title="服务订阅", url="widget/serviceOrder.do", wId=2},
Object { title="订阅审批", url="widget/serviceOrderApprove.do"
...
jquery.cookie.js:
$.cookie(‘the_cookie’); // 读取 cookie
$.cookie(‘the_cookie’, 'the_value’); // 存储 cookie
$.cookie(‘the_cookie’, 'the_value’, { expires: 7 }); // 存储一个带7天期限的 cookie
$.cookie(‘the_cookie’, '', { expires: -1 }); // 删除 cookie
用户表SYS_USER中有个WIDGET_STATE字段,用于保存当前用户的widget状态,
其中
:分割列
,分割行
48,44:46,49 即表示有两行两列,第一列的两个widget ID 为48 44
state:
"1,3,5:2,4"
:分割列
,分割行
登录之后:
function afterLogin(data) { var returnStr = ""; if ("1" == data) { returnStr = "用户名或密码错误,请重新输入!"; focusPassword(); } if ("2" == data) { returnStr = "用户已经登录!"; } if ("3" == data) { returnStr = "密码错误!"; } if ("4" == data) { returnStr = "用户不存在!"; } if ("5" == data) { returnStr = "用户角色不存在!"; } if ("6" == data) { returnStr = "登录出错!"; } if ("0" == data ) { location.href = root + 'main.do'; } showErrorMessage(returnStr); }
@RequestMapping(value="/main.do",method=RequestMethod.GET)
public String main(HttpServletRequest request,HttpSession session) {
logger.info("main...");
return "admin";
}
layout.xml:
系统登录后页面admin.jsp:
登录后进入的页面的JS:
$(function () { // west var firstMenu = $('#firstMenu'); //var fistMenuUrl = root + 'js/app/home/menuMain.json'; // ================================================================================ $.ajax({ url : root + "login/initRootMenu.do", type : 'POST', dataType : 'json', success : function(data) { //构造左侧树 addAccordions(data); //构造进入时默认内容页(即默认的个人工作台的TAB页,各Widget都放在该TAB里) addHome(); } }); var centerTabs = $('#centerTabs'); //个人工作台右侧的“添加组件”按钮 //点击弹出“添加组件”页面 centerTabs.tabs({ fit : true, border : false, onContextMenu : function(e, title) { e.preventDefault(); tabsMenu.menu('show', { left : e.pageX, top : e.pageY }).data('tabTitle', title); }, tools : [{ iconCls : 'icon-add', text : '添加组件', handler : function() { addAppDialog(1); } }] }); addHome = function(){ var title = '个人工作台'; if(centerTabs.tabs('exists', title)){ centerTabs.tabs('select', title); }else{ var url = "/index.do"; centerTabs.tabs( 'add', { title : title, closable : false, content : '' }); } } ..
此处用于构造一个个人工作台的TAB页
跳转到真正的个人工作台页面index.jsp:
@RequestMapping(value="/index.do",method=RequestMethod.GET)
public String index(HttpServletRequest request,HttpSession session) {
SysUser currentUser = (SysUser) session.getAttribute(SessionKeys.SESSION_SYS_USER);
request.setAttribute("USER_ID", currentUser.getId());
return "index";
}
index.jsp页面:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
个人工作台展示部分:
入口portal.js:
$(function() {
$.ajax({
url : root + "widget/initPortalPage.do",
type : 'post',
cache : false,
success : function(response) {
if (response.flag == '0') {
responseObj = response.data;
initPortalPage();
} else {
$.messager.alert('系统提示', '获取Widget失败!', 'error');
return;
}
},
error:function(data){
$.messager.alert('系统提示', '获取Widget失败!', 'error');
return;
}
});
});
个人工作台,所以可以使用用户Bean定位:
/**
* 初始化所有Widget
* @param session
* @return
*/
@RequestMapping(value="widget/initPortalPage.do",method=RequestMethod.POST)
@ResponseBody
public Map initPortalPage(HttpSession session) {
logger.info("initPortalPage begin .. ");
SysUser currentUser = (SysUser) session.getAttribute(SessionKeys.SESSION_SYS_USER);
Map result = new HashMap();
List ls = esbWidgetDS.findWidgetVListByUserId(currentUser.getId());
List
CREATE
OR REPLACE VIEW ESB_WIDGET_V
AS
SELECT
W.ID,
W.MENU_ID,
W.USER_ID,
M.MENU_NAME,
W.URL,
W.ENABLED_FLAG
FROM
ESB_WIDGET W
LEFT JOIN SYS_MENU M ON W.MENU_ID = M.ID;
widget.java:
@Entity
@Table(name = "ESB_WIDGET", uniqueConstraints= {@UniqueConstraint(columnNames={"MENU_ID", "USER_ID"})})
public class EsbWidget implements java.io.Serializable {
private static final long serialVersionUID = 7332317625490883826L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID", nullable = false)
private Long id;
@Column(name = "MENU_ID")
private Long menuId;
@Column(name = "USER_ID")
private Long userId;
@Column(name = "URL")
private String url;
..
}
widght表中含有USER_ID、MENU_ID,对应各个用户的菜单Widght
各Widght其实就是各菜单的快速入口
添加并展示Widget:
function initPortalPage(){
var maxWidgetId = $('#maxWidget' + responseObj.id) // 最大工作台
var portalId = $('#portal' + responseObj.id);
portalId.children().remove();
var panels = responseObj.porlets; // 工作台下widget明细
if (panels.length == 0) {
$.cookie("portal"+responseObj.id, null);
var content = '抱歉,在个人工作台中未找到相关组件.
'
portalId.panel({
fit : true,
border : false,
content : content
})
} else {
responseObj.columnType = 6; /
/*portalId.append(" ");
responseObj.columnTemplate = 3;*/
portalId.append(" ");
responseObj.columnTemplate = 2;
var columnTemplate = responseObj.columnTemplate; // 列
var portal = portalId.portal({
border : false,
fit : true,
// cid : responseObj.cid,
// cname : responseObj.cname,// 自定义属性 cname
onStateChange : function() {
var id = $(this).attr('id');
var oldS = $.cookie(id).split(':'); // 拖动前cookie
var state = getPortalState(id);
$.cookie(id, state, {
expires : 7
});
var newS = $.cookie(id).split(':');// 拖动后cookie
// 更新用户工作台widget状态
updateUserPlatformState(id.substring(6), state);
// 拖动后更新tab类型widget
updateTabWidget(oldS, newS);
}
});
var state = responseObj.state;
if (yxui.replaceAll(state, ':', '') == '') {// 工作台下无widget
state = null;
} else if (state.split(':').length != responseObj.columnTemplate) {// 工作台模板变更
state = null;
} else {// 工作台上已有widget编号与工作台widget状态比较
var flag = false;
var compareState = yxui.replaceAll(state, ':', ',').split(',');
for (var i = 0; i < panels.length; i++) {
flag = false;
var wid = panels[i].wId;
for (var j = 0; j < compareState.length; j++) {
if (wid == compareState[j]) {
flag = true;
break;
}
}
if (flag) {
continue;
} else {
break;
}
}
if (!flag) {
state = null;
}
}
if (!state) {
var col0 = [], col1 = [], col2 = [], col3 = [], col4 = [], stateArr = [];
for (var i = 1; i <= panels.length; i++) {
var mo = i % columnTemplate;
switch (mo) {
case 0 :
col0.push(panels[i - 1].wId); id => wId
break;
case 1 :
col1.push(panels[i - 1].wId);
break;
case 2 :
col2.push(panels[i - 1].wId);
break;
case 3 :
col3.push(panels[i - 1].wId);
break;
case 4 :
col4.push(panels[i - 1].wId);
break;
}
}
if (col1.join(",") != '') {
stateArr.push(col1.join(","));
}
if (col2.join(",") != '') {
stateArr.push(col2.join(","));
}
if (col3.join(",") != '') {
stateArr.push(col3.join(","));
}
if (col4.join(",") != '') {
stateArr.push(col4.join(","));
}
if (col0.join(",") != '') {
stateArr.push(col0.join(","));
}
// state = 'w1,w2:w3';/* 冒号代表列,逗号代表行
state = stateArr.join(":");
var stateCha = columnTemplate - stateArr.length;
if (stateCha > 0) {
for (var i = 0; i < stateCha; i++) {
state = state + ":";
}
}
$.cookie('portal' + responseObj.id, state, {
expires : 7
});
// 更新用户工作台widget状态
updateUserPlatformState(responseObj.id, state);
} else {
$.cookie('portal' + responseObj.id, state, {
expires : 7
});
}
addPanels(portal, panels, state);
portal.portal('resize');
}
}
组件Widget操作widget.js:
/*
* 初始化添加widget
*/
function addPanels(portal, panels, portalState) {
var columns = portalState.split(':');
for (var columnIndex = 0; columnIndex < columns.length; columnIndex++) {
var cc = columns[columnIndex].split(',');
for (var j = 0; j < cc.length; j++) {
var tmp = "";
if(cc[j] != ""){
tmp = parseInt(cc[j]);
}
var options = getPanelOptions(tmp, panels);
if (options) {
// panelWidgetUrlSet
options.url = panelWidgetUrlSet(options);
var p = $('').attr('id', options.wId).appendTo('body');
var result = /^http:\/\/+/.test(options.url); // 是否外网
if (result) {
options.content = '';
options.href = '';
} else {
options.content = '';
options.href = options.url;
}
/*options.collapsible = true;
options.closable = true;*/
options.tools = panelToolsSet(options);
p.panel(options);
// panelAddTips
panelAddTips(p);
portal.portal('add', {
panel : p,
columnIndex : columnIndex
});
}
}
}
}
几个其中的主要JS方法:
/*
* 更新用户工作台widget状态
*/
function updateUserPlatformState(userId, state) {
$.ajax({
url : root + "widget/updateUserState.do",
type : 'post',
data : {'state' : state},
cache : false
});
}
/*
* 获取工作台状态,组装state
*/
function getPortalState(id) {
var columnTemplate = $('#' + id).children().children().children().children().length;
var aa = [];
for (var columnIndex = 0; columnIndex < columnTemplate; columnIndex++) {
var cc = [];
var panels = $('#' + id).portal('getPanels', columnIndex);
for (var i = 0; i < panels.length; i++) {
cc.push(panels[i].attr('id'));
}
aa.push(cc.join(','));
}
return aa.join(':');
}
/*
* 刷新widget
*/
function panelRefresh(id, isMax) {
var isMax = isMax == null ? false : isMax;
var $wid = $('#' + id);
var options = $wid.panel('options');
if(options.url.indexOf("widget") < 0){
options.url = options.href;
}
var result = /^http:\/\/+/.test(options.url); // 是否外网
if (result) {
options.content = '';
$('#' + id).children().remove();
$('#' + id).append(options.content);
} else {
options.content = '';
if (isMax) {
$('#' + id).panel('refresh', yxui.refreshUrlLink(options.url, 'isMax=true'));
} else {
$('#' + id).panel('refresh', options.url);
}
}
}
/*
* panel的工具设置
*/
function panelToolsSet(options) {
var argObj = yxui.getUrlArg(options.url);
var toolSet = '111'; // 刷新/折叠/最大化/删除
if (argObj != null && argObj._toolset != null && argObj._toolset.length == 4) {
toolSet = argObj._toolset;
}
var tools = [];// 返回工具数组对象
for (var i = 0; i < toolSet.length; i++) {
// 刷新
if (i == 0 && toolSet.substr(i, 1) == 1) {
tools.push({
iconCls : 'icon-reload',
handler : function(button) {
panelRefresh(findId(this));
}
});
}
// 折叠
if (i == 1 && toolSet.substr(i, 1) == 1) {
tools.push({
iconCls : 'panel-tool-collapse',
handler : function() {
if ($(this).attr('class') == 'panel-tool-collapse') {
panelCollapse(findId(this));
} else {
panelExpand(findId(this));
}
}
});
}
// 删除
if (i == 2 && toolSet.substr(i, 1) == 1) {
tools.push({
iconCls : 'panel-tool-close',
handler : function() {
var id = findId(this);
var message = '您确定删除此组件?';
$.messager.confirm('系统提示', message, function(r) {
if (r) {
panelClose(id);
}
});
}
});
}
}
return tools;
}
/*
* 最大化widget
*/
function panelMax(wId,userId,maxUrl) {
var portalId = $('#portal' + userId);
var maxWidgetId = $('#maxWidget' + userId);
portalId.hide();
var wOptions = $('#' + wId).panel('options'); // widget options
var maxOptions = {
fit : true,
title : wOptions.title,
style : {
padding : '10px'
}
}
wOptions.url = maxUrl;
var result = /^http:\/\/+/.test(wOptions.url); // is other adderss
if (result) {
maxOptions.content = '';
maxOptions.url = wOptions.url;
} else {
maxOptions.content = '';
maxOptions.url = wOptions.url;
}
maxOptions.tools = [{
iconCls : 'icon-reload',
handler : function(button) {
panelRefresh(findId(this), true);
}
}, {
iconCls : 'panel-tool-restore',
handler : function() {
maxWidgetId.hide().panel('close');
portalId.show();
refreshAll(userId);
}
}];
// panelWidgetUrlSet
maxOptions.url = panelWidgetUrlSet(maxOptions)
maxOptions.maxUrl = yxui.refreshUrlLink(maxOptions.url, 'isMax=true');
/*if(type!=null)
{
maxOptions.maxUrl = yxui.refreshUrlLink(maxOptions.maxUrl, 'type=true');
}*/
maxWidgetId.panel(maxOptions);
result = /^http:\/\/+/.test(maxOptions.url);
if (result) {
maxWidgetId.show().panel('open').panel('refresh');
} else {
maxWidgetId.show().panel('open').panel('refresh', maxOptions.maxUrl);
}
// panelAddTips
panelAddTips(maxWidgetId, true);
}
/*
* 查找widget id
*/
function findId(button) {
return $(button).parent().parent().next().attr("id");
}
/*
* 折叠widget
*/
function panelCollapse(id) {
$('#' + id).panel('collapse', true);
}
/*
* 展开widget
*/
function panelExpand(id) {
$('#' + id).panel('expand', true);
}
/*
* 关闭widget
*/
function panelClose(wid) {
$.ajax({
url : root + "widget/deleteWidget.do",
data : {
wId : wid
},
type : 'post',
cache : false,
success : function(response) {
if (response.flag == '0') {
var user = response.data;
// $('#' + wid).panel('close');
var portalId = 'portal'+ user.id;
var cookieState = $.cookie(portalId);
//alert("userId : "+user.id+" cookieState : "+cookieState);
var portal = $('#' + portalId);
portal.portal('remove', $('#' + wid));
// 更新用户工作台widget状态
var newState = getPortalState(portalId);
updateUserPlatformState(user.id, newState);
var state = yxui.replaceAll(newState, ':', '');
if (state == '') {
$.cookie(portalId, null);
var content = '抱歉,在个人工作台中未找到相关组件.
'
portal.panel({
fit : true,
border : false,
content : content
});
} else {
$.cookie(portalId, newState, {
expires : 7
});
}
}
}
});
}
function getPanelOptions(id, panels) {
for (var i = 0; i < panels.length; i++) {
var wid = panels[i].wId;
if (id == wid) {
return panels[i];
}
}
return undefined;
}
..
每个widget的展示模块:
/**
* 服务查询WIdget
* @param request
* @return
*/
@RequestMapping(value="widget/serviceQuery.do",method=RequestMethod.GET)
public String serviceQuery(HttpServletRequest request) {
logger.info("add serviceOrder widget begin...");
Page page = new Page();
EsbServiceV esbServiceV = new EsbServiceV();
PageQueryParameter pageQueryParameter = new PageQueryParameter();
pageQueryParameter.setRows(Constants.WIDGET_PAGE_ROWS);
pageQueryParameter.setPage(Constants.WIDGET_CURRENT_PAGE);
List esbServiceVList = esbServiceVDS.findEsbServiceVPagedList(pageQueryParameter, esbServiceV);
long rows = esbServiceVDS.getAllEsbServiceVsTotalCount(pageQueryParameter, esbServiceV);
page.setTotal(rows);
page.setRows(esbServiceVList);
SysUser currentUser = getCurrentUser();
request.setAttribute("USER_ID", currentUser.getId());
request.setAttribute("SRV_DATA", esbServiceVList);
request.setAttribute("SRV_MAX_URL", "esbService/query.do");
logger.info("add serviceQuery widget end...");
return "srvQueryWidget";
}
srvQueryWidget.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
服务查询Widget
-
${notice.serviceNameEn}
- 暂无相关内容!
添加组件模块:
appAdd.js:
var $addDialog;
//入口方法,进入添加展示页面
function addAppDialog(typeId){
var titleStr = "添加应用";
if (typeId == 1) {
titleStr = "添加组件";
}
$addDialog = $('#addAppDialog');
$addDialog.dialog({
title: titleStr,
iconCls : 'pag-add',
href: root + "widget/initAddApp.do?type="+typeId,
width : 600,
height : 330,
modal:true,
cache: false,
resizable : false,
minimizable : false,
maximizable : false,
collapsible : false
});
$addDialog.dialog('open');
}
//真正添加方法
function addApp(menuId, type) {
$.ajax({
url : root + "widget/addApp.do",
type : 'post',
cache : false,
data : {
'type' : type,
'menuId' : menuId
},
success : function(result) {
var data = result.data;
if (result.flag == '0' && data != null) {
//$("#span-" + resId).html('');
if (type == 1) {
protalAddWidgets(data);
}
} else {
$.messager.alert('系统提示', '添加失败!', 'error');
}
$addDialog.dialog('close');
}
});
}
/*
* 添加组件后更新工作台上widget
*/
function protalAddWidgets(data) {
var widgetId = data.id;
var userId = data.userId;
var state = $.cookie('portal'+userId);
//alert("userId : "+userId+" state : "+state);
var centerTabs = $("#centerTabs");
var tab = centerTabs.tabs('getTab',0);
var options = tab.panel('options');
var content = options.content;
var iframe = $(content);
var src = iframe.attr("src");
tab.panel('refresh',src);
}
/**
* 初始化Widget添加页面
* @param request
* @param session
* @param type
* @return
*/
@RequestMapping(value="widget/initAddApp.do",method=RequestMethod.GET)
public String initAddApp(HttpServletRequest request,HttpSession session,String type) {
logger.info("initAddApp begin .. ");
logger.info("parameter type : " + type);
List rootList = (List) session.getAttribute(SessionKeys.SESSION_USER_ROOT_MENU);
long resCount = 0l;
List widgetList = new ArrayList();
for(SysMenu m : rootList){
String menuName = m.getMenuName().trim();
if(menuName.equals(Constants.SERVICE_DIRECTORY)
|| menuName.equals(Constants.SERVICE_MONITOR)
|| menuName.equals(Constants.MY_SERVICE)){
widgetList.add(m);
}
}
for(SysMenu m : widgetList){
if(m.getChildren() != null && m.getChildren().size() > 0){
resCount += m.getChildren().size();
}
}
esbWidgetDS.initWidgetStatus(widgetList);
session.setAttribute("WIDGET_LIST", widgetList);
session.setAttribute("resCount", resCount);
session.setAttribute("type", type);
logger.info("initAddApp end .. ");
return "appAdd";
}
/**
* 添加WIdget到工作台
* @param request
* @param session
* @param type
* @param menuId
* @return
*/
@RequestMapping(value="widget/addApp.do",method=RequestMethod.POST)
@ResponseBody
public Map addApp(HttpServletRequest request,HttpSession session,String type,Long menuId) {
logger.info("addApp begin .. ");
logger.info("parameter type : " + type);
logger.info("parameter menuId : " + menuId);
Map result = new HashMap();
EsbWidget widget = null;
if("1".equals(type)){
try{
widget = esbWidgetDS.save(menuId);
}catch(Exception e){
logger.error(e.getMessage());
result.put("flag", "1");
return result;
}
}
result.put("flag", "0");
result.put("data", widget);
logger.info("addApp end .. ");
return result;
}
view.xml
添加页面appAdd.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%
String ctx = request.getContextPath();
request.setAttribute("ctx", ctx);
String fullPath = request.getLocalAddr() + ":" + request.getLocalPort() + "/";
request.setAttribute("fullpath", fullPath);
%>
应用列表
组件列表
共有${resCount}
个应用
个组件
${res.menuName}
${res.menuName}
0}">none">
<%-- --%>
${t.menuName}
${fn:substring(t.remark, 0, 45)}...
${t.remark}
添加方法DS:
public Boolean checkWidgetAdded(Long menuId){
SysUser currentUser = getCurrentUser();
String hql = "from EsbWidgetV w where w.menuId = " + menuId + " and w.userId = " + currentUser.getId() +" and w.enabledFlag = 'Y'";
List ls = esbWidgetVDao.findByHql(hql);
if(ls != null && ls.size() > 0){
return true;
}
return false;
}
/**
* 初始化Widget添加状态 , 默认都为“可添加”
* @param menuId
*/
@Override
public void initWidgetStatus(List widgetList){
logger.info("initWidgetStatus begin ..");
for(SysMenu m : widgetList){
for(SysMenu c : m.getChildren()){
Boolean isAdded = checkWidgetAdded(c.getId());
if(isAdded){
c.setIsAdd(true);
}else{
c.setIsAdd(false);
}
}
}
logger.info("initWidgetStatus end ..");
}
/**
* 添加WIdget并更新添加状态,一个组件一条widget记录
*/
@Transactional
@Override
public EsbWidget save(Long menuId) {
SysUser currentUser = getCurrentUser();
HttpSession session = GlobalSession.getHttpSession();
Map allMenu = (Map) session.getAttribute(SessionKeys.SESSION_USER_ALL_MENU);
SysMenu m = allMenu.get(menuId);
if(m == null) return null;
String widgetUrl = null;
String menuName = m.getMenuName().trim();
if(menuName.equals(Constants.SERVICE_QUERY)){
widgetUrl = "widget/serviceQuery.do";
}else if(menuName.equals(Constants.SERVICE_ORDER)){
widgetUrl = "widget/serviceOrder.do";
}else if(menuName.equals(Constants.SERVICE_ORDER_APPROVE)){
widgetUrl = "widget/serviceOrderApprove.do";
}else if(menuName.equals(Constants.MONITOR_LOG)){
widgetUrl = "widget/monitorLog.do";
}else if(menuName.equals(Constants.MY_ORDER)){
widgetUrl = "widget/myOrder.do";
}
EsbWidget widget = new EsbWidget();
widget.setMenuId(menuId);
widget.setUrl(widgetUrl);
widget.setUserId(currentUser.getId());
widget.setEnabledFlag(Constants.ENABLED_FLAG_Y);
ObjectUtil.setCreatedBy(widget);
widget = esbWidgetDao.save(widget);
//更新添加状态
updateWidgetStatus(menuId,true);
return widget;
}
/**
* 添加、删除Widget后更新添加状态
* @param menuId
* @param isAdd
*/
@Override
public void updateWidgetStatus(Long menuId,Boolean isAdd){
logger.info("updateWidgetStatus begin ..");
HttpSession session = GlobalSession.getHttpSession();
List widgetList = (List) session.getAttribute("WIDGET_LIST");
if(widgetList == null) return;
for(SysMenu menu : widgetList){
SysMenu m = null;
List children = menu.getChildren();
for(int i = children.size()-1;i>=0;i--){
SysMenu tmp = children.get(i);
if(tmp.getId() == menuId){
m = tmp;
m.setIsAdd(isAdd);
children.set(children.indexOf(tmp), m);
break;
}
}
if(m != null) break;
}
session.removeAttribute("WIDGET_LIST");
session.setAttribute("WIDGET_LIST",widgetList);
logger.info("updateWidgetStatus end ..");
}
效果:
点“更多”可以最大化Widget:
点“添加组件”可以添加:
..