Wijmo 5 + Ionic Framework之:费用跟踪 App

费用跟踪应用采用了Wijmo5和Ionic Framework创建,目的是构建一个hybird app。

我们基于《Mobile first! Wijmo 5 + Ionic Framework之:Hello World!》的环境,将在本教程中完成费用跟踪App的构建。下面的代码结构是本教程完成要达到的效果,请预先创建好文件和目录。

www/                     --> 工程根目录
index.html --> app 布局文件 (主HTML文件)
css/ --> css 目录
js/ --> javascript 目录
app.js --> 主模块
app.routes.js --> 主路由模块
controllers/ --> app控制器目录
models/ --> app模块目录
services/ --> app 数据Service目录
templates/ --> angularJs视图代码目录(通过UI-Router调用)
lib/ --> 第三方类库, 包括Ionic, Wijmo, jQuery等

数据模型(Data Model)

在费用跟踪App中,我们先要创建Data Model,E-R图如下

  • Category:开支分类
  • Expense:开支记录
  • Budget: 预算(下面会用到)

在代码中,我们需要在www/js/services构建AngularJs Services来对数据模型进行建模。我们会用到HTML5的localStorage进行数据本地存储, 采用的格式为JSON。 需要注意的是,HTML5本地存储只能存字符串,任何格式存储的时候都会被自动转为字符串,所以读取的时候,需要自己进行类型的转换。目前我们实现的是HTML5 本地存储,有兴趣的读者还可移植为RESTful API、SQLite等数据存储方法。

运行demo后,通过Chrome调试查看的本地存储截图:

浏览开支历史记录

在开支历史页面中,提供了2个功能:浏览开支历史记录、删除开支记录。为了实现这些功能,在www\js\controllers\history.js文件中,添加如下代码:

//从localStorage获得开支数据
$scope.expenses = ExpenseSvc.getExpensesWithCategory();

这行代码提供了返回本地存储的开支记录。ExpenseSvc 服务,不仅返回了开支对象,同时也返回了开支分类。基于这些数据,在

www\templates\history.tpl.htm文件中,在ion-context指令内添加Ionic的ion-list指令,代码如下:

<ion-view title="History">
<ion-nav-buttons side="right">
<a class="button button-icon icon ion-plus" href="#/app/create"></a>
</ion-nav-buttons>
<ion-content class="has-header">
<ion-list>
<ion-item ng-repeat="expense in expenses | orderBy:'date':reverse track by expense.id" class="item item-icon-left">
<i class="icon ion-record {{ expense.category.cssClass }}"></i>
<div class="row">
<div class="col-50">
<h2>{{ expense.title }}</h2>
</div>
<div class="col-25">
<small class="light-grey">{{ expense.date | date: 'shortDate' }}</small>
</div>
<div class="col-25">
{{ expense.amount | currency }}
</div>
</div>
</ion-item>
</ion-list>
</ion-content>
</ion-view>

ion-list指令,用于生成排序的HTML列表,其子标签ion-item指令用于生成HTML列表项。 在ngRepeat指令中,我们使用了“track by”,目的是在对开支集合修改时提升性能,相关教程可参考博客《Using Track-By With ngRepeat In AngularJS 1.2 》。

现在添加删除开支记录按钮,用于向左滑动出现删除按钮、点击删除可删除开支记录。

在ion-item标签关闭前添加ion-option-button标签,代码如下:

<ion-option-button class="button button-assertive" on-tap="confirmDelete(expense.id)">Delete</ion-option-button>

ion-option-button 是Ionic提供的另一个指令,用于在ion-item指令内试用。默认的,ion-option-button 是隐藏的,当在ion-item内向左滑动,则按钮会可见。这个功能尤其对小屏幕设备非常重要。另外,还可通过该指令内置的can-swipe来实现对这个权限的管理--如有的用户不允许删除操作权限。

在删除函数中(控制器),可看到代码片段如下:

function confirmDelete(expenseId) {
// delete expense by its id property
$scope.expenses = ExpenseSvc.deleteExpense(expenseId);
}

通过这个代码,我们调用ExpenseSvc服务的deleteExpense进行删除指定的开支记录(expenseId),同时这个方法也会返回开支记录集合用于更新页面数据。在真实的场景中,删除记录返回整个集合不是最理想的,但在此处我们用于演示说明。可动手试着删除几行数据试试。

另外,在删除这种比较危险的操作中,应该需要添加对话框再次提醒一下用户。这里我们使用了Ionic提供的$ionicActionSheet service服务来实现。更新www\js\controllers\history.js控制器代码的confirmDelete函数如下:

//删除开支记录
$scope.confirmDelete = function (expenseId) {
//ionic的 确认对话框
// show()函数返回了一个函数,用于隐藏actionSheet
var hideSheet = $ionicActionSheet.show({
titleText: 'Are you sure that you\'d like to delete this expense?',
cancelText: 'Cancel',
destructiveText: 'Delete',
cancel: function () {
// 如果用户选择cancel, 则会隐藏删除按钮
$ionicListDelegate.closeOptionButtons();
},
destructiveButtonClicked: function () {
// 通过id删除开支记录
$scope.expenses = ExpenseSvc.deleteExpense(expenseId); // 隐藏对话框
hideSheet();
}
});
};

ionicActionSheet服务提供了自定义接口,可实现各种提示对话框。上面代码实现的提示对话框效果截图如下:

创建开支记录

点击History页面右上角的可实现手工创建一条新的开支记录。在www\templates\createExpense.tpl.htm文件中,代码如下:

<ion-view title="Create">
<ion-content class="has-header padding">
<form name="frmCreate">
<div class="custom-form-list list">
<label class="item item-input">
<i class="icon ion-alert-circled placeholder-icon assertive" ng-show="!frmCreate.title.$pristine && frmCreate.title.$invalid"></i>
<input name="title" type="text" placeholder="Title" ng-model="expense.title" ng-maxlength="55" required>
</label>
<wj-input-number value="expense.amount" min="0" step="5" format="c2"></wj-input-number>
<wj-calendar value="expense.date"></wj-calendar>
<wj-combo-box items-source="categories"
display-member-path="htmlContent"
selected-value-path="id"
selected-value="expense.categoryId"
is-editable="false"
is-content-html="true"></wj-combo-box>
<label class="item item-input">
<textarea placeholder="Description" ng-model="expense.description"></textarea>
</label>
</div>
<div class="button-bar">
<button type="button" class="button button-dark icon-left ion-close" on-tap="cancel()">Cancel</button>
<button type="button" class="button button-balanced icon-left ion-checkmark" on-tap="addExpense()" ng-disabled="frmCreate.title.$invalid">Save</button>
</div>
</form>
</ion-content>
</ion-view>

这里使用ion-view 和 ion-content 指令进行内容展现。然后再添加Form,用ng-show指令验证输入内容---Wijmo的指令已经在输入门限做了限制,故不需要验证。同时Wijmo Calendar 和InputNumber应该是自解释,ComboBox中可能不是。

ComboBox关联数据模型中的开支分类,我们通过其itemsSource属性进行数据绑定。ComboBox的displayMemberPath 用于设置显示内容,selectedItem的selectedValue用于选择开支分类的id属性。

在createExpense 控制器中,可看到如下的代码片段:

// 初始化Expense object
$scope.expense = new Expense('', 0, new Date(), '', null); // 获得HTML类型的开支分类
$scope.categories = CategorySvc.getCategoriesWithHtmlContent(); // 用localStorage存储开支数据
$scope.addExpense = function () {
// insert expense
ExpenseSvc.insertExpense($scope.expense);
$scope.cancel();
}; // 取消方法 (如,可回到主页面)
$scope.cancel = function () {
$state.go('app.overview'); };

上面的第一行代码用于初始化一个开支记录,用Expense的构造函数来实现,并赋值给$scope.expense对象。 开支分类,通过调用CategorySvc服务的接口,从localStorage获得数组。addExpense 方法用于提交新增的开支记录,同样用到了ExpenseSvc服务。最后一个函数$scope.canel使用了UI Router的 $state 服务,导航到主页面。

运行app,截图如下:

Details Grid

在前面几节中,我们分别学习了如何查看、创建、删除开支记录。在本节,我们将通过Wijmo5的FlexGrid和CollectionView批量对开支记录进行呈现,打开detailsGrid 模板文件,添加如下代码片段:

<ion-view title="Details Grid">
<!-- set overflow-scroll="true" and hand scrolling to native -->
<ion-content class="has-header" overflow-scroll="true">
<wj-flex-grid auto-generate-columns="false" items-source="data" selection-mode="Row" row-edit-ending="rowEditEnding(s,e)" style="position:relative">
<wj-flex-grid-column width="2*" min-width="250" header="Title" binding="title"></wj-flex-grid-column>
<wj-flex-grid-column width="*" min-width="100" header="Amount" binding="amount" format="c2"></wj-flex-grid-column>
<wj-flex-grid-column width="*" min-width="100" header="Date" binding="date"></wj-flex-grid-column>
<wj-flex-grid-column width="2*" min-width="250" header="Description" binding="description"></wj-flex-grid-column>
</wj-flex-grid>
</ion-content>
<ion-footer-bar class="bar button-bar-footer">
<div class="button-bar">
<button type="button" class="button button-dark icon-left ion-close" on-tap="cancel()">Cancel</button>
<button type="button" class="button button-balanced icon-left ion-checkmark" ng-disabled="!data.itemsEdited.length" on-tap="update()">Save</button>
</div>
</ion-footer-bar>
</ion-view>

在FlexGrid指令下面,我们添加了2个按钮,Cancel和Save,分别用于当点击的时候进行取消和存储操作,数据存储于localStorage。其中,Save按钮的默认不可用,通过ngDisabled的表达式进行控制。

FlexGrid 指令,用于在模板内生成Wijmo5的FlexGrid 控件。我们使用itemsSource 进行数据源绑定,同时通过autoGenerateColumns=”false”关闭自动生成数据列,以及SelectMode类型为整行Row。同时也设置了FlexGrid的rowEditEnding事件,用于验证数据输入。在FlexGrid内部,定义了Columns,分别指定了header、binding、width。

如下代码是detailsGrid 控制器片段:

// 通过localStorage获得开支记录数据,并初始化CollectionView
$scope.data = new wijmo.collections.CollectionView(ExpenseSvc.getExpenses()); // CollectionView的变更可跟踪
$scope.data.trackChanges = true; // 批量更新开支记录
$scope.update = function () {
// make sure items have been edited
if ($scope.data.itemsEdited.length) {
// bulk update expenses
ExpenseSvc.updateExpenses($scope.data.itemsEdited); // return to overview page
$scope.cancel();
}
}; // 取消方法 (如导航到主页面)
$scope.cancel = function () {
$state.go('app.overview');
}; // FlexGrid.rowEditEnding事件处理
$scope.rowEditEnding = function (sender, args) {
var expense = $scope.data.currentEditItem, // get expense being edited
isValid = isExpenseValid(expense); // validate expense // if the expense isn't valid, cancel the edit operation
if (!isValid) {
$scope.data.cancelEdit();
return;
}
}; // 验证函数:确保开支记录数据合法有效
function isExpenseValid(expense) {
return expense &&
expense.title !== '' &&
expense.title.length < 55 &&
wijmo.isNumber(expense.amount) &&
wijmo.isDate(expense.date) &&
expense.amount >= 0;
}

上面代码的第一行,通过从localStorage 加载数据,然后初始化CollectionView的对象,继而赋值给$scope.data对象,用于给前端HTML进行Data-Source绑定数据源。

接下来看cancel、update方法,cancel方法和上面的一样,使用了UI Router的$state服务进行回到首页。update方法,先进行数据判断,通过核查$scope.data.itemsEdited.length是否有效(是否有开支记录变更),然后再调用ExpenseSvc 进行数据修改,对localStorage数据进行存储处理。

最后,FlexGrid的rowEditEnding事件触发了rowEditEnding函数,即当row修改完成后尚未cancel、update前触发。在这里进行有效性判断,若无效则cancel并返回。这里,我们使用了Wijmo 5提供的工具函数:isNumber和isDate来进行判断。

运行Details Grid截图如下:

概述

修改app.routes.js 文件,从默认的history页面到overview页面:

$urlRouterProvider.otherwise('/app/history');

to:

$urlRouterProvider.otherwise('/app/overview');

这个细小的改变使得UI Router 会对没有明确重定向的,均会导向overview页面。

overview页面代码如下所示:

<ion-view title="Overview">
<ion-nav-buttons side="right">
<a class="button button-icon icon ion-plus" href="#/app/create"></a>
</ion-nav-buttons>
<ion-content class="has-header padding">
<div ng-show="hasExpenses">
<hgroup class="text-center padding-vertical">
<h2 class="title">
<span ng-class="expensesCssClass">{{ totalExpenses | currency }}</span> of {{ budget | currency }}
</h2>
<h4>{{ budgetMsg }}</h4>
</hgroup>
<wj-flex-chart items-source="categories"
chart-type="Bar" binding-x="name"
tooltip-content=""
selection-mode="Point"
footer="Tap the chart's bars to see history by category"
selection-changed="selectionChanged(s)"
item-formatter="itemFormatter"
style="height: 400px;">
<wj-flex-chart-series binding="total"></wj-flex-chart-series>
<wj-flex-chart-axis wj-property="axisX" format="c0"></wj-flex-chart-axis>
<wj-flex-chart-axis wj-property="axisY" reversed="true" major-grid="false" axis-line="true"></wj-flex-chart-axis>
</wj-flex-chart>
</div>
<div ng-hide="hasExpenses">
<h4 class="padding text-center">You haven't added any expenses yet! Click the <i class="icon ion-plus"></i> button to get started!</h4>
</div>
</ion-content>
</ion-view>

上面的代码,首先使用hgroup元素呈现了开支记录的总和。下面接着使用了Wijmo 5 FlexChart 渲染了每个开支分类的开支金额,在FlexChart 指令内,我们指定了一些属性,如数据序列、x、y轴,同时当点击Bar的时候会触发FlexChart的plot elements 事件,对当前分类详情做列表呈现。

上面这些功能的实现,基于overview.js文件的逻辑:

// 通过BudgetSvc服务,获得localStorage数据
$scope.budget = BudgetSvc.getBudget(); // 判断是否有开支记录,返回bool
$scope.hasExpenses = ExpenseSvc.hasExpenses(); // 获得开支的总金额
$scope.totalExpenses = ExpenseSvc.getExpenseTotal(); // 获得各个分类的小计金额
$scope.categories = ExpenseSvc.getCategoriesExpenseSummary(); // 初始化CSS 样式
$scope.expensesCssClass = 'energized'; //设置开支金额显示字符串
// NOTE: use $filter service to format the total prior to concatenating the string
$scope.budgetMsg = $scope.totalExpenses <= $scope.budget
? $filter('currency')($scope.budget - $scope.totalExpenses).concat(' until you reach your monthly limit')
: $filter('currency')($scope.totalExpenses - $scope.budget).concat(' over your monthly limit'); //设置开支css样式
$scope.expensesCssClass = 0 === $scope.totalExpenses
? 'dark'
: $scope.totalExpenses === $scope.budget
? 'energized'
: $scope.totalExpenses > $scope.budget
? 'assertive'
: 'balanced'; //*** FlexChart's selectionChanged event handler
$scope.selectionChanged = function (sender) {
var category = null;
if (sender.selection && sender.selection.collectionView.currentItem) {
// get the currently selected category
category = sender.selection.collectionView.currentItem; // navigate to the category history page to display expenses for selected category
$state.go('app.category-history', { category: category.slug });
}
}; //*** set color of FlexChart's plot elements
$scope.itemFormatter = function (engine, hitTestInfo, defaultFormat) {
if (hitTestInfo.chartElement === wijmo.chart.ChartElement.SeriesSymbol) {
// set the SVG fill and stroke based on the category's bgColor property
engine.fill = hitTestInfo.item.bgColor;
engine.stroke = hitTestInfo.item.bgColor;
defaultFormat();
} };

预览截图如下:

下载地址

Wijmo 5 + Ionic Framework之:费用跟踪 App的更多相关文章

  1. Mobile first! Wijmo 5 + Ionic Framework之:费用跟踪 App

    费用跟踪应用采用了Wijmo5和Ionic Framework创建,目的是构建一个hybird app. 我们基于<Mobile first! Wijmo 5 + Ionic Framework ...

  2. Wijmo 5 + Ionic Framework之:Hello World!

    Wijmo 5 + Ionic Framework之:Hello World! 本教程中,我们用Wijmo 5 和 Ionic Framework实现一个Mobile的工程:Hello World. ...

  3. Mobile first! Wijmo 5 + Ionic Framework之:Hello World!

    本教程中,我们用Wijmo 5 和 Ionic Framework实现一个Mobile的工程:Hello World. Ionic是什么? Ionic是一个HTML5框架.免费.开源,用于帮助生成hy ...

  4. 170多个Ionic Framework学习资源(转载)

    在Ionic官网找到的学习资源:http://blog.ionic.io/learning-ionic-in-your-living-room/ 网上的文章比较多,但是很多时候我们很难找到自己需要的. ...

  5. 快乐学习 Ionic Framework+PhoneGap 手册1-1{创建APP项目}

    快乐学习 Ionic Framework+PhoneGap 手册1-1 * 前提必须安装 Node.js,安装PhoneGap,搭建Android开发环境,建议使用真机调试 {1.1}= 创建APP项 ...

  6. 【Ionic】---Using Local Notifications In Your Ionic Framework App

    Using Local Notifications In Your Ionic Framework App 配置好ng-cordova先 <script src="lib/ngCord ...

  7. 一个先进的App框架:使用Ionic创建一个简单的APP

    原文  http://www.w3cplus.com/mobile/building-simple-app-using-ionic-advanced-html5-mobile-app-framewor ...

  8. 案例:1 Ionic Framework+AngularJS+ASP.NET MVC WebApi Jsonp 移动开发

    落叶的庭院扫的一干二净之后,还要轻轻把树摇一下,抖落几片叶子,这才是Wabi Sabi的境界. 介绍:Ionic是移动框架,angularjs这就不用说了,ASP.Net MVC WebApi提供数据 ...

  9. Ionic – 强大的 HTML5 Mobile App 开发框架

    Ionic 是一个强大的 HTML5 应用程序开发框架,可以帮助您使用 Web 技术,比如 HTML.CSS 和 Javascript 构建接近原生体验的移动应用程序.Ionic 主要关注外观和体验, ...

随机推荐

  1. Sql示例说明如何分组后求中间值--【叶子】

    原文:Sql示例说明如何分组后求中间值--[叶子] 这里所谓的分组后求中间值是个什么概念呢? 我举个例子来说明一下: 假设我们现在有下面这样一个表: type        name price -- ...

  2. .Net反编译实战

    原文:.Net反编译实战 当你面对一个已经部署好的网站,功能,性能都非常不给力的时候,你会怎么办? 当你尝试去了解这个网站业务逻辑,代码逻辑和数据库逻辑时却发现根本没有任何资料时你会怎么办? 当你准备 ...

  3. 高通公司 MSM8K GPT异常原因分析无法开机的问题

    问题分析过程如下面: 一. MSM8916台gpt概率问题:采用QPST emmc software download下载软件工具后,无法开机.例如下面的附图: log分析是userdata分区未成功 ...

  4. Web API之消息处理管道

    Web API之消息处理管道 前言 MVC有一套请求处理的机制,当然Web API也有自己的一套消息处理管道,该消息处理管道贯穿始终都是通过HttpMessageHandler来完成.我们知道请求信息 ...

  5. docker搭建基础的tomcat应用

    tomcat server是眼下比較流行的开源中间件server,以下介绍怎样使用 docker 来做一个 tomcat 数据库服务.官方的仓里没有标 OFFICIAL 的 tomcat 的镜像,只是 ...

  6. opencv在arm和x86在移植

    一个.开发环境 操作系统:fedora14 Opencv版本号:2.0 Qt版本号:4.7 arm:mini6410 交叉编译工具:arm-linux-gcc-4.5.1 二.安装与配置 Linux系 ...

  7. Android 学习历程摘要(一)

    初学Android,可能有些地方可能理解不正确,假设有朋友看到的话麻烦指正我一下,万分感谢. 1. 善用API DEMO,刚開始学习的人的需求基本在里面都能够满足,Eclipse导入API Demop ...

  8. Base64中文不能加密问题

    最近用到了Base64.js来对url参数进行加密,字母和数字都可以很好地加密/解密. 但测试中文时发现不能进行转换,貌似Base64.js不支持中文字符. 联想到encodeURI()对url的编码 ...

  9. malloc实现原理

    记得早一段时间,看到一本书上写过delete的一个..今天突然找啦一下资料: malloc()是C语言中动态存储管理 的一组标准库函数之中的一个.其作用是在内存的动态存储区中分配一个长度为size的连 ...

  10. Ohloh研究报告

    1.底       由于近期接合ospaf同样是一个开源项目的成熟度分析工具,由于该项目现在Ohloh我们有共同的东西,这么ohloh我们进行了调查.       简单的说,初始ohloh是一个代码搜 ...