angularjs+bootstrap后台管理樣例


ng+bootstrap可以做出很漂亮的管理系統出來,https://wrapbootstrap.com/可以付費購買,下文會提供一個免費的,要講解如何從0到1把ng前端結構搭出來是很漫長的教程,本文僅僅介紹一下這個免費后台模版的結構,然后重點講解如何改寫這個結構。 
開始閱讀之前,假設讀者已經ng入門並且對於ui.router,bootstrap有一定了解。


下載鏈接http://pan.baidu.com/s/1o73ewfK 
下載下來以后我們可以掛載到IIS里面看看這個模版的運行效果 
這里寫圖片描述


1) 后台結構 
這里寫圖片描述

其中我們需要關注的如下:

index.html: 入口 
js: 存放所有業務邏輯代碼 
js/app.js:定義ng需要加載的模塊 
js/main.js:定義ng的全局配置信息 
js/config.router.js:ng的路由器 
tpl:存放所有頁面模版 
tpl/blocks:頁面框架(頭、尾、側邊欄…) 
vendor:存放所有第三方模塊


2) 改寫結構,接管路由

結構簡圖如下 
這里寫圖片描述 
這里接管的原則是盡量不改動原始結構,首先在根目錄創建我們自己的目錄結構.

> mkdir admin > mkdir admin\blocks > copy js\main.js admin\
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

編寫頁面框架模版,我們也可以直接從tpl目錄復制過來再做修改。簡單起見,我們在模版里面直接使用了中文,這樣會導致頁面亂碼,解決方法:html文件用utf-8編碼格式存儲。

<!--admin/blocks/aside.html--> <div class="aside-wrap"> <div class="navi-wrap"> <!-- nav --> <nav ui-nav class="navi" ng-include="'admin/blocks/nav.html'"></nav> <!-- nav --> </div> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
<!--admin/blocks/header.html--> <!-- navbar header --> <div class="navbar-header {{app.settings.navbarHeaderColor}}"> <button class="pull-right visible-xs dk" ui-toggle-class="show" data-target=".navbar-collapse"> <i class="glyphicon glyphicon-cog"></i> </button> <button class="pull-right visible-xs" ui-toggle-class="off-screen" data-target=".app-aside" ui-scroll="app"> <i class="glyphicon glyphicon-align-justify"></i> </button> <!-- brand --> <a href="#/" class="navbar-brand text-lt"> <i class="fa fa-btc"></i> <img src="img/logo.png" alt="." class="hide"> <span class="hidden-folded m-l-xs">{{app.name}}</span> </a> <!-- / brand --> </div> <!-- / navbar header --> <!-- navbar collapse --> <div class="collapse pos-rlt navbar-collapse box-shadow {{app.settings.navbarCollapseColor}}"> <!-- buttons --> <div class="nav navbar-nav hidden-xs"> <a href class="btn no-shadow navbar-btn" ng-click="app.settings.asideFolded = !app.settings.asideFolded"> <i class="fa {{app.settings.asideFolded ? 'fa-indent' : 'fa-dedent'}} fa-fw"></i> </a> </div> <!-- / buttons --> <!-- nabar right --> <ul class="nav navbar-nav navbar-right"> <li class="hidden-xs"> <a ui-fullscreen></a> </li> <li class="dropdown" dropdown> <a href class="dropdown-toggle clear" dropdown-toggle> <span class="thumb-sm avatar pull-right m-t-n-sm m-b-n-sm m-l-sm"> <img src="img/a0.jpg" alt="..."> <i class="on md b-white bottom"></i> </span> <span class="hidden-sm hidden-md"></span> <b class="caret"></b> </a> <!-- dropdown --> <ul class="dropdown-menu animated fadeInRight w"> <li> <a href="#">Logout</a> </li> </ul> <!-- / dropdown --> </li> </ul> <!-- / navbar right --> </div> <!-- / navbar collapse -->
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
<!--admin/blocks/nav.html--> <!-- list --> <ul class="nav"> <li class="hidden-folded padder m-t m-b-sm text-muted text-xs"> <span translate="導航">導航</span> </li> </ul> <!-- / list -->
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
<!--admin/app.html--> <!-- navbar --> <div data-ng-include=" 'admin/blocks/header.html' " class="app-header navbar"> </div> <!-- / navbar --> <!-- menu --> <div data-ng-include=" 'admin/blocks/aside.html' " class="app-aside hidden-xs {{app.settings.asideColor}}"> </div> <!-- / menu --> <!-- content --> <div class="app-content"> <div ui-butterbar></div> <a href class="off-screen-toggle hide" ui-toggle-class="off-screen" data-target=".app-aside" ></a> <div ncy-breadcrumb></div> <div class="app-content-body fade-in-up" ui-view></div> </div> <!-- /content --> <!-- footer --> <div class="app-footer wrapper b-t bg-light"> <span class="pull-right">{{app.version}} <a href ui-scroll="app" class="m-l-sm text-muted"><i class="fa fa-long-arrow-up"></i></a></span> &copy; 2016 Copyright. </div> <!-- / footer --> <div data-ng-include=" 'tpl/blocks/settings.html' " class="settings panel panel-default"> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

給我們的首頁創建一個空白模版admin/dashboard.html

<!--admin/dashboard.html-->
  • 1
  • 1

寫我們新的路由

// admin/router.js 'use strict'; app .run( function ($rootScope, $state, $stateParams) { $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; } ) .config( function ($stateProvider, $urlRouterProvider) { $urlRouterProvider .otherwise('/app/dashboard'); $stateProvider .state('app', { abstract: true, url: '/app', templateUrl: 'admin/app.html', }) .state('app.dashboard', { url: '/dashboard', templateUrl: 'admin/dashboard.html', }) } );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

修改入口,注釋或刪除掉對原config.router.js、main.js的引用,換成我們的控制接管

<!--index.html--> ... <!--<script src="js/config.router.js"></script>--> <!--<script src="js/main.js"></script>--> <script src="admin/router.js"></script> <script src="admin/main.js"></script> ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這里寫圖片描述
到此我們就得到了一套可自由擴展的前端框架


3) 接下來我們基於BasicAuth加入系統的用戶驗證功能。

這里寫圖片描述 
這里我們采用按功能模塊來建立子目錄,區別於原模版框架(原框架是按文件類型區分子目錄,例如腳本放在js/里面,模版放在tpl里面)。

首先啟動Restful服務器,然后我們為服務器配置一個全局變量,代碼里面的host需要修改成服務器真實地址

// admin/main.js
... $scope.app = { host: "http://172.17.9.92:8000", name: 'Angulr', ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

創建認證功能模塊目錄auth

> mkdir admin\auth\
  • 1
  • 1

編寫controller(控制器)

// admin/auth/ctrl.js app.controller('LoadingController',function($scope,$resource,$state){ var $com = $resource($scope.app.host + "/auth/info/?"); $com.get(function(){ $state.go('app.dashboard'); },function(){ $state.go('auth.login'); }) }); app.controller('LoginController',function($scope,$state,$http,$resource,Base64){ $scope.login = function(){ $scope.authError = "" var authdata = Base64.encode($scope.user.username + ':' + $scope.user.password); $http.defaults.headers.common['Authorization'] = 'Basic ' + authdata; var $com = $resource($scope.app.host + "/auth/info/?"); $com.get(function(){ $state.go('app.dashboard'); },function(){ $scope.authError = "服務器登錄錯誤" }) } }); app.factory('Base64',function(){ var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; return { encode: function (input) { var output = ""; var chr1, chr2, chr3 = ""; var enc1, enc2, enc3, enc4 = ""; var i = 0; do { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); chr1 = chr2 = chr3 = ""; enc1 = enc2 = enc3 = enc4 = ""; } while (i < input.length); return output; }, decode: function (input) { var output = ""; var chr1, chr2, chr3 = ""; var enc1, enc2, enc3, enc4 = ""; var i = 0; // remove all characters that are not A-Z, a-z, 0-9, +, /, or = var base64test = /[^A-Za-z0-9\+\/\=]/g; if (base64test.exec(input)) { window.alert("There were invalid base64 characters in the input text.\n" + "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" + "Expect errors in decoding."); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); do { enc1 = keyStr.indexOf(input.charAt(i++)); enc2 = keyStr.indexOf(input.charAt(i++)); enc3 = keyStr.indexOf(input.charAt(i++)); enc4 = keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } chr1 = chr2 = chr3 = ""; enc1 = enc2 = enc3 = enc4 = ""; } while (i < input.length); return output; } }; }) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

編寫template(模版)

<!--admin/auth/loading.html--> <div class="form-group" ng-controller="LoadingController"> <div class="col-md-12 text-center"> <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>&nbsp;&nbsp;loading... </div> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
<!--admin/auth/login.html--> <div class="container w-xxl w-auto-xs" ng-controller="LoginController"> <a href class="navbar-brand block m-t">{{app.name}}</a> <div class="m-b-lg"> <div class="wrapper text-center"> <strong>Sign in to get in touch</strong> </div> <form name="form" class="form-validation"> <div class="list-group list-group-sm"> <div class="list-group-item"> <input type="text" placeholder="username" class="form-control no-border" ng-model="user.username" required> </div> <div class="list-group-item"> <input type="password" placeholder="password" class="form-control no-border" ng-model="user.password" required> </div> </div> <button type="submit" class="btn btn-lg btn-primary btn-block" ng-click="login()" ng-disabled='form.$invalid'>Log in</button> <div class="line line-dashed"></div> <div class="text-danger wrapper text-center" ng-show="authError"> {{authError}} </div> </form> </div> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

改寫路由

// admin/router.js
... $urlRouterProvider .otherwise('/auth/loading'); $stateProvider .state('auth',{ abstract: true, url:'/auth', template: '<div ui-view class="fade-in"></div>', resolve: { deps: ['$ocLazyLoad', function( $ocLazyLoad ){ return $ocLazyLoad.load('admin/auth/ctrl.js'); }] } }) .state('auth.loading',{ url:'/loading', templateUrl:'admin/auth/loading.html', }) .state('auth.login',{ url:'/login', templateUrl:'admin/auth/login.html', }) ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

代碼挺多,就不逐行解釋了,最核心的就是: 
用Base64加密[用戶名:密碼],在請求頭加入 Authorization: Basic [加密串]

var authdata = Base64.encode(username + ":" + password); $http.defaults.headers.common['Authorization'] = 'Basic ' + authdata;
  • 1
  • 1

改完以后重新訪問,將實現流程圖中的home->loading->login->dashboard。 
事情還沒完,我們再重新訪問,又會重復這個流程,並不會自動登錄,這是因為BasicAuth的特性是在每一次web請求的時候都需要加入Authorization請求頭才行。所以我們還要做點工作:1.登錄成功以后將authdata存在本地,2.給全局http請求統一加入這個請求頭

// admin/auth/ctrl.js
... app.controller('LoginController',function($scope,$state,$http,$resource,Base64,$localStorage) ... $com.get(function(){ $localStorage.auth = authdata; $state.go('app.dashboard'); },function(){ $scope.authError = "服務器登錄錯誤" }) ... 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
// admin/router.js app .run( function ($rootScope, $state, $stateParams,$localStorage,$http) { $http.defaults.headers.common['Authorization'] = 'Basic ' + $localStorage.auth; $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; } ) ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

重新刷新首頁,頁面將實現自動登錄了,但是事情還沒完,進入系統以后,雖然每次Web請求我們都加入了BasicAuth的請求頭,但是如果服務器端做了帳號修改,一樣會產生401的錯誤,產生的結果就是客戶端點什么操作都不會有反應,我們應該在全局來攔截401,引導客戶端跳轉到重新登錄的界面:

// admin/router.js ... app.config(function ($httpProvider) { $httpProvider.interceptors.push('AuthInterceptor'); }) app.factory('AuthInterceptor', function ($rootScope, $q,$location) { return { responseError: function (response) { if(response.status==401) { $location.url('/auth/login'); } return $q.reject(response); } }; })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

大功即將告成,還有最后一步,有了登錄必然有登出,BasicAuth協議本身是沒有登出概念的,我們這里做的登出,就是刪除本地那個保存的authdata。

// admin/main.js
angular.module('app') .controller('AppCtrl', ['$scope', '$translate', '$localStorage', '$window', '$state','$http', function( $scope, $translate, $localStorage, $window ,$state,$http) { ... $scope.logout = function(){ $localStorage.auth = null; $http.defaults.headers.common['Authorization'] = "Basic"; $state.go("auth.login"); } function isSmartDevice( $window ){...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
<!--admin/blocks/header.html--> ... <!-- dropdown --> <ul class="dropdown-menu animated fadeInRight w"> <li> <a ng-click="logout()">Logout</a> </li> </ul> <!-- / dropdown --> ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

錦上添花,標准的后台系統一般會在頁面右上角顯示登錄用戶的帳號信息,我們定義的協議/auth/info/是會把這些信息帶下來的,我們來補全一下這個功能:

// admin/auth/ctrl.js app.controller('LoadingController',function($scope,$resource,$state,$localStorage){ var $com = $resource($scope.app.host + "/auth/info/?"); $com.get(function(data){//引入data $scope.session_user = $localStorage.user = data; //保存用戶信息 $state.go('app.dashboard'); }) }); app.controller('LoginController',function($scope,$state,$http,$resource,Base64,$localStorage){ $scope.login = function(){ $scope.authError = "" var authdata = Base64.encode($scope.user.username + ':' + $scope.user.password); $http.defaults.headers.common['Authorization'] = 'Basic ' + authdata; var $com = $resource($scope.app.host + "/auth/info/?"); $com.get(function(data){//引入data $scope.session_user = $localStorage.user = data; //保存用戶信息 $localStorage.auth = authdata; $state.go('app.dashboard'); },function(){ $scope.authError = "服務器登錄錯誤" }) } }); ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
// admin/main.js
... $scope.session_user = $localStorage.user; $scope.logout = function(){...}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
<!--admin/blocks/header.html-->
... <span class="hidden-sm hidden-md">{{session_user.username}}</span> <b class="caret"></b> ...
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

4) CRUD

創建news目錄

> mkdir admin\news\
  • 1
  • 1

增加news導航

<!-- admin/blocks/nav.html --> <!-- list --> <ul class="nav"> <li class="hidden-folded padder m-t m-b-sm text-muted text-xs"> <span translate="導航">導航</span> </li> <li ui-sref-active="active"> <a ui-sref="app.news.list"> <i class="glyphicon glyphicon-book icon text-info-dker"></i> <span class="font-bold">新聞管理</span> </a> </li> </ul> <!-- / list -->
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

書寫控制器

// admin/news/ctrl.js 'use strict'; app.controller('ListController', function($scope, $resource,$stateParams,$modal,$state) { //查詢 $scope.query = function(page,filter){ var $com = $resource($scope.app.host + "/news/?page=:page&search=:filter",{page:'@page',filter:'@filter'}); if(!page){ page=1; }else{ page=parseInt(page); } $com.get({page:page,filter:filter},function(data){ //擴展分頁數據,顯示頁簽,最終效果為 < 1 2 3 4 5 > data.page_index = page; data.pages = []; //頁簽表 var N = 5; //每次顯示5個頁簽 var s = Math.floor(page/N)*N; if(s==page)s-=N; s += 1; var e = Math.min(data.page_count,s+N-1) for(var i=s;i<=e;i++) data.pages.push(i) $scope.data = data; $scope.search_context = filter; }); } //搜索跳轉 $scope.search = function(){ $state.go('app.news.list',{search:$scope.search_context}); } //全選 var selected = false; $scope.selectAll = function(){ selected = !selected; angular.forEach($scope.data.results,function(item){ item.selected = selected; }); } //自定義操作處理,其中1為刪除所選記錄 $scope.exec = function(){ if($scope.operate=="1"){ var ids = []; angular.forEach($scope.data.results,function(item){ if(item.selected){ ids.push(item.id); } }); if(ids.length>0){ //彈出刪除確認 var modalInstance = $modal.open({ templateUrl: 'admin/confirm.html', controller: 'ConfirmController', size:'sm', }); modalInstance.result.then(function () { var $com = $resource($scope.app.host + "/news/deletes/?"); $com.delete({'ids':ids.join(',')},function(){ $state.go('app.news.list'); }); }); } } } //根據url參數(分頁、搜索關鍵字)查詢數據 $scope.query($stateParams.page,$stateParams.search); }); app.controller('ConfirmController', ['$scope', '$modalInstance', function($scope, $modalInstance){ $scope.ok = function () { $modalInstance.close(); }; $scope.cancel = function () { $modalInstance.dismiss('cancel'); }; }]); app.controller('DetailController', function($rootScope,$scope, $resource, $stateParams,$state) { $scope.edit_mode = !!$stateParams.id; if($scope.edit_mode){ var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'}); var resp = $com.get({id:$stateParams.id},function(data){ $scope.data = resp; }); } else{ $scope.data = {}; } $scope.submit = function(){ if($scope.edit_mode){ var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'},{ 'update': { method:'PUT' }, }); $com.update({id:$stateParams.id},$scope.data,function(data){ $state.go($rootScope.previousState,$rootScope.previousStateParams); }); } else{ var $com = $resource($scope.app.host + "/news/?"); $com.save($scope.data,function(data){ $state.go('app.news.list'); }); } }; $scope.delete = function(){ var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'}); $com.delete({id:$stateParams.id},function(){ $state.go('app.news.list'); }) } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111

書寫列表模版

<!-- admin/news/list.html --> <div class="wrapper-md" ng-controller="ListController"> <div class="panel panel-default"> <div class="panel-heading"> <ul class="nav nav-pills pull-right"> <li style=" padding-top:4px; padding-right:4px"><button class="btn m-b-xs btn-sm btn-primary btn-addon" ui-sref="app.news.create()"><i class="fa fa-plus"></i>新增</button></li> </ul> 新聞列表 </div> <div class="row wrapper"> <div class="col-sm-5 m-b-xs"> <select class="input-sm form-control w-sm inline v-middle" ng-model="operate" ng-init="operate=0"> <option value="0">---</option> <option value="1">刪除所選記錄</option> </select> <button class="btn btn-sm btn-default" ng-click="exec()">執行</button> </div> <div class="col-sm-4"> </div> <div class="col-sm-3"> <div class="input-group"> <input type="text" class="input-sm form-control" placeholder="Search" ng-model="search_context"> <span class="input-group-btn"> <button class="btn btn-sm btn-default" ng-click="search()" type="button">Go!</button> </span> </div> </div> </div> <div class="table-responsive" ng-if="data.total_count>0"> <table class="table table-striped b-t b-light"> <thead> <tr> <th style="width:20px;"> <label class="i-checks m-b-none"> <input type="checkbox" ng-click="selectAll()"><i></i> </label> </th> <th>標題</th> <th>創建時間</th> <th style="width:30px;"></th> </tr> </thead> <tbody> <tr ng-repeat="data in data.results"> <td><label class="i-checks m-b-none"><input type="checkbox" ng-model="data.selected"><i></i></label></td> <td>{{data.title}}</td> <td>{{data.create_time|date:'yyyy-MM-dd HH:mm:ss Z'}}</td> <td> <a ui-sref="app.news.detail({id:data.id})" class="active"><i class="fa fa-edit"></i></a> </td> </tr> </tbody> </table> </div> <footer class="panel-footer"> <div class="row"> <div class="col-sm-8 text-left"> <small class="text-muted inline m-t-sm m-b-sm">{{data.total_count}}條記錄</small> </div> <div ng-if="data.page_count>1" class="col-sm-4 text-right text-center-xs"> <ul class="pagination pagination-sm m-t-none m-b-none"> <li ng-class="{disabled:!data.previous}"><a ui-sref="app.news.list({page:data.page_index-1,search:search_context})"><i class="fa fa-chevron-left"></i></a></li> <li ng-repeat="page in data.pages" ng-class="{active:page==data.page_index}"><a ui-sref="app.news.list({page:page,search:search_context})">{{page}}</a></li> <li ng-class="{disabled:!data.next}"><a ui-sref="app.news.list({page:data.page_index+1,search:search_context})"><i class="fa fa-chevron-right"></i></a></li> </ul> </div> </div> </footer> </div> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

書寫詳情、新增的模版,兩者是可以復用一個模版的

<!-- admin/news/detail.html --> <div ng-controller="DetailController"> <div class="wrapper-md" > <div class="panel panel-default"> <form class="form-horizontal ng-pristine ng-valid ng-valid-date ng-valid-required ng-valid-parse ng-valid-date-disabled" ng-submit="submit()"> <div class="panel-body"> <div class="form-group"> <label class="col-sm-2 control-label">標題</label> <div class="col-sm-10"> <input type="text" class="form-control" ng-model="data.title" required> </div> </div> <div class="line line-dashed b-b line-lg pull-in"></div> <div class="form-group"> <label class="col-sm-2 control-label">內容</label> <div class="col-sm-10"> <textarea class="form-control" rows="6" ng-model="data.content"></textarea> </div> </div> </div> <footer class="panel-footer text-right bg-light lter"> <input type="button" ng-click="delete()" ng-if="edit_mode" class="btn btn-danger pull-left" value="刪除"/> <input type="submit" class="btn btn-success" value="提交"/> </footer> </form> </div> </div> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

書寫刪除確認框模版

<!-- admin/confirm.html --> <div class="modal-header"> <h3>確認刪除?</h3> </div> <div class="modal-footer"> <button class="btn btn-default" ng-click="cancel()">Cancel</button> <button class="btn btn-primary" ng-click="ok()">OK</button> </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

添加路由,注意run里面增加了事件監聽,后文詳細說

// admin/router.js
app
  .run(
      function (...) { ... $rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) { $rootScope.previousState = from; $rootScope.previousStateParams = fromParams; }); } ) ... .state('app.news', { abstract: true, url: '/news', template: '<div ui-view class="fade-in"></div>', resolve: { deps: ['$ocLazyLoad', function( $ocLazyLoad ){ return $ocLazyLoad.load('admin/news/ctrl.js'); }] } }) .state('app.news.list', { url: '/list?page&search', templateUrl: 'admin/news/list.html', }) .state('app.news.detail', { url: '/detail/{id}', templateUrl: 'admin/news/detail.html', }) .state('app.news.create', { url: '/create', templateUrl: 'admin/news/detail.html', }) ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

成果如下 
這里寫圖片描述
這里寫圖片描述

我們這里搜索和分頁都是采用URL跳轉的方式(#/news/list/?search=str&page=int),這樣能保證刷新頁面的時候能停留在之前的頁面結果上,ng默認的頁面跳轉是不保留前一個頁面狀態的(鏈接和參數),如果我們跳轉到第2頁,編輯,再返回,是會回到第1頁去,為了比較好的用戶體驗所以我們有了如下代碼

監聽全局頁面跳轉信號($statChangeSuccess),將參數保存下來

// admin/router.js
app.run(
    ... $rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) { $rootScope.previousState = from; $rootScope.previousStateParams = fromParams; }); ... )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

詳情頁編輯完成返回時讀取參數跳轉

// admin/news/ctrls.js
app.controller('DetailController',...){ ... $com.update({id:$stateParams.id},$scope.data,function(data){ $state.go($rootScope.previousState,$rootScope.previousStateParams); }); ... });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

完整的后台少不了導航 
這里寫圖片描述 
我們這里選用github上面一個寫得挺棒的ng導航插件angular-breadcrumb,求簡,我們直接下載https://raw.github.com/ncuillery/angular-breadcrumb/master/dist/angular-breadcrumb.min.js,由於我們是給ng裝插件,所以建議放到vendor/里面去,接下來的改動也是針對原模版框架。

注入JS

<!-- index.html -->
... <script src="vendor/angular/angular-breadcrumb/angular-breadcrumb.min.js"></script> ...
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

引入模塊

// js/app.js
angular.module('app', [ ... 'ncy-angular-breadcrumb', ]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

配置擴展,讓導航能支持HTML(具體就是能顯示回首頁的圖標) 
這里寫圖片描述

// js/config.js
... app.config(function($breadcrumbProvider) { $breadcrumbProvider.setOptions({ templateUrl: 'tpl/blocks/breadcrumb.html' }); });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

擴展的導航模版,寫法參考插件官方文檔

<!-- tpl/blocks/breadcrumb.html --> <ul class="breadcrumb bg-white b-a"> <li ng-repeat="step in steps | limitTo:(steps.length-1)"> <a href="{{step.ncyBreadcrumbLink}}" ng-bind-html="step.ncyBreadcrumbLabel"></a> </li> <li ng-repeat="step in steps | limitTo:-1" class="active"> <span ng-bind-html="step.ncyBreadcrumbLabel"></span> </li> </ul>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

配置路由,加入導航

// admin/router.js
... .state('app.dashboard', { ... ncyBreadcrumb: { label: '<i class="fa fa-home"></i> 首頁' } }) ... .state('app.news.list', { ... ncyBreadcrumb: { parent:'app.dashboard', label: '新聞列表', } }) .state('app.news.detail', { ... ncyBreadcrumb: { parent:'app.news.list', label: '編輯', } }) .state('app.news.create', { ... ncyBreadcrumb: { parent:'app.news.list', label: '新增', } }) ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

這里寫圖片描述


 
3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM