qzyReal 2 роки тому
коміт
c0c126d511
64 змінених файлів з 13172 додано та 0 видалено
  1. 70 0
      index.html
  2. 138 0
      login.html
  3. 21 0
      main.html
  4. BIN
      sa-frame/admin-logo.png
  5. 241 0
      sa-frame/com/sa-info.vue
  6. 422 0
      sa-frame/com/sa-item.vue
  7. 244 0
      sa-frame/com/sa-td.vue
  8. BIN
      sa-frame/index/admin-loading.gif
  9. 106 0
      sa-frame/index/admin-util.js
  10. 65 0
      sa-frame/index/index.css
  11. 495 0
      sa-frame/index/index.js
  12. 225 0
      sa-frame/index/theme.css
  13. 127 0
      sa-frame/login/app.js
  14. BIN
      sa-frame/login/bg.jpg
  15. BIN
      sa-frame/login/name.png
  16. 8 0
      sa-frame/login/particles.min.js
  17. BIN
      sa-frame/login/password.png
  18. 35 0
      sa-frame/login/reset.css
  19. 152 0
      sa-frame/login/style.css
  20. 108 0
      sa-frame/menu-list-sp.js
  21. 61 0
      sa-frame/nav/com-add-tab.vue
  22. 160 0
      sa-frame/nav/com-right-menu.vue
  23. 37 0
      sa-frame/nav/nav-logo.vue
  24. 169 0
      sa-frame/nav/nav-menu-bar.vue
  25. 158 0
      sa-frame/nav/nav-tab-bar.vue
  26. 326 0
      sa-frame/nav/nav-tool-bar.vue
  27. 57 0
      sa-frame/nav/nav-view-vessel.vue
  28. 134 0
      sa-frame/sa-code.js
  29. 108 0
      sa-view-sp/console/com-chart-1.vue
  30. 114 0
      sa-view-sp/console/com-chart-2.vue
  31. 113 0
      sa-view-sp/console/com-chart-3.vue
  32. 49 0
      sa-view-sp/console/com-intro.vue
  33. 47 0
      sa-view-sp/console/com-origin.vue
  34. 129 0
      sa-view-sp/console/com-sta-data.vue
  35. 67 0
      sa-view-sp/console/com-stack.vue
  36. 289 0
      sa-view-sp/console/com-update-log.vue
  37. 117 0
      sa-view-sp/console/console-main.html
  38. 98 0
      sa-view-sp/sp-admin/admin-add.html
  39. 88 0
      sa-view-sp/sp-admin/admin-info.html
  40. 254 0
      sa-view-sp/sp-admin/admin-list.html
  41. 82 0
      sa-view-sp/sp-admin/update-password.html
  42. 88 0
      sa-view-sp/sp-apilog/api-log-list-delete.html
  43. 336 0
      sa-view-sp/sp-apilog/api-log-list.html
  44. 133 0
      sa-view-sp/sp-cfg/app-cfg.html
  45. 131 0
      sa-view-sp/sp-cfg/server-cfg.html
  46. 344 0
      sa-view-sp/sp-console/redis-console.html
  47. 106 0
      sa-view-sp/sp-console/redis-key-add.html
  48. 20 0
      sa-view-sp/sp-console/sql-console.html
  49. 63 0
      sa-view-sp/sp-role/menu-list.html
  50. 168 0
      sa-view-sp/sp-role/menu-setup.html
  51. 104 0
      sa-view-sp/sp-role/role-add.html
  52. 138 0
      sa-view-sp/sp-role/role-list.html
  53. BIN
      static/icon/icon-article.png
  54. BIN
      static/icon/icon-comment.png
  55. BIN
      static/icon/icon-goods.png
  56. BIN
      static/icon/icon-money.png
  57. BIN
      static/icon/icon-order.png
  58. BIN
      static/icon/icon-user.png
  59. BIN
      static/img/kulian.png
  60. BIN
      static/img/up-icon.png
  61. 276 0
      static/kj/upload-util.js
  62. 4662 0
      static/kj/wangEditor.up.js
  63. 195 0
      static/sa.css
  64. 1294 0
      static/sa.js

+ 70 - 0
index.html

@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title></title>
+		<meta charset="utf-8">
+		<link rel="shortcut icon" type="image/x-icon" href="sa-frame/admin-logo.png" class="admin-icon">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="sa-frame/index/index.css">
+		<link rel="stylesheet" href="sa-frame/index/theme.css">
+		<link rel="stylesheet" href="static/sa.css">
+	</head>
+	<body>
+		<!-- App -->
+		<div class="app" style="display: none;" :style="'display: block;'" 
+			:class="['theme-0', 'theme-' + themeV, (isOpen ? '' : 'app-fold'), (isOpenRight ? '' : 'app-fold-right')]" >
+			<!-- 左 -->
+			<div class="nav-left">
+				<!-- logo部分 -->
+				<div class="nav-left-top">
+					<nav-logo></nav-logo>
+				</div>
+				<!-- 左下:菜单 -->
+				<div class="nav-left-bottom">
+					<nav-menu-bar ref="nav-menu-bar"></nav-menu-bar>
+				</div>
+			</div>
+			<!-- 右 -->
+			<div class="nav-right">
+				<!-- 工具栏 -->
+				<div class="nav-right-1" des="">
+					<nav-tool-bar ref="nav-tool-bar"></nav-tool-bar>
+				</div>
+				<!-- Tab栏 -->
+				<div class="nav-right-2">
+					<nav-tab-bar ref="nav-tab-bar"></nav-tab-bar>
+				</div>
+				<!-- 视图容器 -->
+				<div class="nav-right-3">
+					<nav-view-vessel></nav-view-vessel>
+				</div>
+			</div>
+			<!-- 右键菜单 -->
+			<com-right-menu ref="com-right-menu"></com-right-menu>
+			<!-- 双击添加新tab -->
+			<com-add-tab ref="com-add-tab"></com-add-tab>
+		</div>
+		
+		<!-- js依赖库 -->
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
+		<script type="text/javascript">Vue.config.productionTip = false;</script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="https://unpkg.com/sortablejs@1.14.0/Sortable.min.js"></script>
+		<script src="https://unpkg.com/vuedraggable@2.24.3/dist/vuedraggable.umd.min.js"></script>
+		<!-- <script src="https://www.itxst.com/package/sortable/sortable.min.js"></script> -->
+		<!-- <script src="https://www.itxst.com/package/vuedraggable/vuedraggable.umd.min.js"></script> -->
+		
+		<!-- js本地库 -->
+		<script src="sa-frame/index/admin-util.js"></script>
+		<script src="sa-frame/index/index.js"></script>
+		<script src="sa-frame/menu-list.js"></script>
+		<script src="sa-frame/menu-list-sp.js"></script>
+		<script src="static/sa.js"></script>
+		<script src="sa-frame/sa-code.js"></script>
+
+	</body>
+</html>

+ 138 - 0
login.html

@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html lang="zh">
+	<head>
+		<meta charset="utf-8">
+		<title>登录</title>
+		<meta name="description" content="particles.js is a lightweight JavaScript library for creating particles.">
+		<meta name="author" content="Vincent Garreau" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+		<link rel="stylesheet" media="screen" href="sa-frame/login/style.css">
+		<link rel="stylesheet" href="static/sa.css">
+		<style type="text/css">
+			/* 背景图片 */
+			#particles-js{
+				background-image: url(sa-frame/login/bg.jpg);
+			}
+			
+			/* 样式调整 */
+			.login-box{display: flex; justify-content: center; align-items: center; position: fixed; width: inherit; height: 100%; pointer-events: none;}
+			.login{height: auto; padding: 50px 50px; position: static; margin: 0 auto !important; pointer-events: all; border-radius: 0px;}
+			.login-top{margin-top: 0px; margin-bottom: 30px;}
+			.logo-img{width: 50px; height: 50px; vertical-align: middle; position: relative; top: -3px; border-radius: 50%; margin-left: -10px; margin-right: 10px;}
+			.logo-img{display: none;}
+			.login-button{width: 270px; border-radius: 0px; transition: all 0.2s;}
+			.login-button:hover{background-color: #0E80eF;}
+			/* .page-title{line-height: 50px;} */
+			.sk-rotating-plane{}
+			/* 动画相关 */
+			/* .login{background-color: rgba(0,0,0,0); } */
+			.login{opacity: 0;}
+			
+		</style>
+	</head>
+	<body>
+
+		<div id="particles-js">
+			<div class="login-box">
+				<div class="login">
+					<div class="login-top">
+						<img src="" class="logo-img" alt="">
+						<span class="page-title">登录</span>
+					</div>
+					<div class="login-center clearfix">
+						<div class="login-center-img"><img src="sa-frame/login/name.png" /></div>
+						<div class="login-center-input">
+							<input type="text" name="key" value="" placeholder="请输入账号" />
+							<div class="login-center-input-text">账号</div>
+						</div>
+					</div>
+					<div class="login-center clearfix">
+						<div class="login-center-img"><img src="sa-frame/login/password.png" /></div>
+						<div class="login-center-input">
+							<input type="password" name="password" value="" placeholder="请输入密码" />
+							<div class="login-center-input-text">密码</div>
+						</div>
+					</div>
+					<div class="login-button">登录</div>
+				</div>
+			</div>
+			<div class="sk-rotating-plane"></div>
+		</div>
+
+		<!-- scripts -->
+		<script src="sa-frame/login/particles.min.js"></script>
+		<script src="sa-frame/login/app.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="static/sa.js"></script>
+		<script type="text/javascript">
+			
+			// 你所有要改的代码全在这里   ↓↓↓↓↓ 
+			
+			// 所有参考属性  
+			var page_title = 'sa-plus 后台登录';		// 页面标题 
+			var key = 'sa';							// 默认的账号
+			var password = '123456';				// 默认的password  
+			var logo = 'sa-frame/admin-logo.png';	// logo地址,为空字符串则不显示 
+			
+			// 点击登录按钮 
+			document.querySelector(".login-button").onclick = function() {
+				// 1、取值 
+				var p = {
+					key: $('[name=key]').val(),
+					password: $('[name=password]').val()
+				}
+				// 2、判断
+				if(p.key == '' || p.password == ''){
+					return layer.msg('请输入账号密码');
+				}
+				// 3、请求后台
+				sa.ajax('/AccAdmin/doLogin', p, function(res){
+					// 写入token
+					if(res.data.tokenInfo) {
+						localStorage.tokenName = res.data.tokenInfo.tokenName; 
+						localStorage.tokenValue = res.data.tokenInfo.tokenValue; 
+					}
+					// 写入权限码 
+					sa.setAuth(res.data.per_list);		
+					// 打个招呼,进入 index.html 
+					sa.msg('登录成功,欢迎你:' + p.key);
+					setTimeout(function () {
+						if(parent == window){
+							location.href = "index.html";
+						}else{
+							sa.closeCurrIframe();
+							parent.location.reload();
+						}
+					}, 500);
+				})
+			}
+			// 你所有要改的代码全在这里   ↑↑↑↑↑	
+			
+		</script>
+		<script type="text/javascript">
+			// 替换属性 
+			$('.page-title').html(page_title);
+			$('title').html(page_title);
+			$('[name=key]').val(key);
+			$('[name=password]').val(password);
+			if(logo != null && logo != '') {
+				$('.logo-img').attr('src', logo);
+				$('.logo-img').show();
+			}
+			// 绑定回车事件
+			$('[name=password]').bind('keypress', function(event){
+				if(event.keyCode == "13") {
+					$('.login-button').click();
+				}
+			});
+			// 去掉透明
+			setTimeout(function() {
+				// document.querySelector('.login').style.backgroundColor = 'rgba(256,256,256,1)';
+				document.querySelector('.login').style.opacity = '1';
+			}, 0)
+			
+			console.log('本页面参考于jq22,原作者:http://www.jq22.com/jquery-info20074');
+		</script>
+	</body>
+</html>

+ 21 - 0
main.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<title></title>
+		<link rel="stylesheet" href="static/sa.css" />
+		<style type="text/css">
+			html{background-color: #EEE;}
+		</style>
+		<script type="text/javascript">
+			location.href="sa-view-sp/console/console-main.html";
+		</script>
+	</head>
+	<body>
+		<div style="width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; color: #000;">
+			<div style="text-align: center;">
+				<h1>欢迎使用 SA-后台管理 </h1>
+			</div>
+		</div>
+	</body>
+</html>

BIN
sa-frame/admin-logo.png


+ 241 - 0
sa-frame/com/sa-info.vue

@@ -0,0 +1,241 @@
+<template>
+	<!-- 自定义slot -->
+	<div class="c-item" :class="{br: br}" v-if="$slots.default">
+		<label class="c-label" v-if="name && name.length > 0">{{name}}:</label> 
+		<span v-else-if="name === undefined"></span> 
+		<label class="c-label" v-else></label> 
+		<span v-else></span> 
+		<slot></slot>
+	</div>
+	<!-- 普通信息 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'text'">
+		<label class="c-label">{{name}}:</label> 
+		<span>{{value}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	<!-- num -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'num'">
+		<label class="c-label">{{name}}:</label> 
+		<span class="tc-num">{{value}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	<!-- textarea -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'textarea'">
+		<label class="c-label">{{name}}:</label> 
+		<span class="c-item-mline">{{value}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	<!-- img -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'img'">
+		<label class="c-label">{{name}}:</label> 
+		<img :src="value" class="info-img" @click="sa.showImage(value, '400px', '400px')" v-if="value">
+		<span v-else>无</span>
+	</div>
+	<!-- audio、video、file -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'audio' || type == 'video' || type == 'file'">
+		<label class="c-label">{{name}}:</label> 
+		<el-link type="info" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+		<span v-else>无</span>
+	</div>
+	<!-- img-list -形如:url1,url2,url3 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'img-list'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="c-item-mline image-box image-box-info" v-if="value_arr.length > 0">
+			<div class="image-box-2" v-for="image in value_arr">
+				<img :src="image" @click="sa.showImage(image, '500px', '400px')" />
+			</div>
+		</div>
+		<span v-else>无</span>
+	</div>
+	<!-- audio-list、video-list、file-list、img-video-list -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'audio-list' || type == 'video-list' || type == 'file-list' || type == 'img-video-list'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="c-item-mline" v-if="value_arr.length > 0">
+			<div v-for="item in value_arr">
+				<el-link type="info" :href="item" target="_blank">{{item}}</el-link>
+			</div>
+		</div>
+		<span v-else>无</span>
+	</div>
+	<!-- 钱 money (单位 元) -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'money'">
+		<label class="c-label">{{name}}:</label> 
+		<b class="c-price">¥{{value}}</b>
+	</div>
+	<!-- 钱 price-f (单位 分) -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'money-f'">
+		<label class="c-label">{{name}}:</label> 
+		<b class="c-price">¥{{value / 100}}</b>
+	</div>
+	<!-- 富文本 richtext f -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'richtext' || type == 'f'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="editor-box content-box-info c-item-mline">
+			<div v-html="value"></div>
+		</div>
+	</div>
+	<!-- 显示枚举 j、num -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'enum' || type == 'j' || type == 'switch'">
+		<label class="c-label">{{name}}:</label> 
+		<span v-for="j in jvList" :key="j.key">
+			<b :style="{color: j.color || '#303236'}" v-if="value == j.key">{{j.value}}</b>
+		</span>
+	</div>
+	<!-- link -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'link'">
+		<label class="c-label">{{name}}:</label> 
+		<!-- <span class="c-item-mline">{{value}}</span> -->
+		<el-link type="primary" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+		<span v-else>无</span>
+	</div>
+	
+	<!-- 日期 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'date'">
+		<label class="c-label">{{name}}:</label> 
+		<span class="tc-date">{{sa.forDate(value, 1)}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	<!-- 日期时间 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'datetime'">
+		<label class="c-label">{{name}}:</label> 
+		<span class="tc-date">{{sa.forDate(value, 2)}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	<!-- 时间 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'time'">
+		<label class="c-label">{{name}}:</label> 
+		<span class="tc-date">{{value}}</span>
+		<span v-if="sa.isNull(value)">无</span>
+	</div>
+	
+	<!-- 评分组件 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'rate'">
+		<label class="c-label">{{name}}:</label> 
+		<div style="display: inline-block;">
+			<el-rate :value="value <= 5 ? value : 5" show-text disabled></el-rate>
+			<span v-if="sa.isNull(value)">无</span>
+		</div>
+	</div>
+	
+	
+</template>
+
+<script>
+	module.exports = {
+		// props: ['name', 'value'],
+		props: {
+			// text、num、
+			type: {
+				default: 'text'
+			},
+			// label提示文字
+			name: {
+				type: String
+			},
+			// 绑定的值 
+			value: {},
+			// 提示文字
+			placeholder: {},
+			// 是否禁用
+			disabled: {},
+			// 是否换行 
+			br: {
+				type: Boolean,
+				default: false
+			},
+			// type=menu时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+			jv: {default: ''},
+			// type=menu时,具体的枚举类型 -- 1=单选框,2=单选文字,3=单选按钮,4=单选下拉框
+			jtype: {default: 1},
+			// 级联选择的数据列表
+			options: {},
+			// 快捷按钮显示列表,形如:add,get,delete,export,reset 
+			show: {},	
+			// 分页信息 
+			curr: {}, size: {}, total: {}, sizes: {}, 
+			// 空值时显示的文字
+			not: {default: '无'}
+			
+		},
+		data() {
+			return {
+				// 日期范围时的值 
+				dateRangeValue: [],
+				// 快捷按钮显示按钮列表 
+				showBtns: [],
+				// type=menu时,解析的值列表    -- 形如:[{key: 1, value: '正常', color: 'green'}]
+				jvList: [],
+				// type = img-list 时,解析的元素List
+				value_arr: []
+			}
+		},
+		watch: {
+			// 监听一些类型的 value 变动 
+			value: function(oldValue, newValue) {
+				// img-list、audio-list、video-list、file-list、img-video-list
+				if(this.type == 'img-list' || this.type == 'audio-list' || this.type == 'video-list' || this.type == 'file-list' || this.type == 'img-video-list') {
+					this.value_to_arr(this.value); 
+				}
+			},
+		},
+		methods: {
+			// 解析枚举 
+			parseJv: function() {
+				for(let key in this.jv) {
+					let value = this.jv[key];
+					let color = '';
+					// 
+					if(value.indexOf('[') != -1 && value.endsWith(']')) {
+						let index = value.indexOf('[');
+						color = value.substring(index + 1, value.length - 1);
+						value = value.substring(0, index);
+						// console.log(color + ' --- ' + value);
+					}
+					// 
+					if(isNaN(key) == false) {
+						key = parseInt(key);
+					}
+					// 
+					this.jvList.push({
+						key: key,
+						value: value,
+						color: color
+					})
+				}
+			},
+			// 解析 value 为 value_arr
+			value_to_arr: function(value) {
+				this.value_arr = sa.isNull(value) ? [] : value.split(',');		
+				for (var i = 0; i < this.value_arr.length; i++) {
+					if(this.value_arr[i] == '' || this.value_arr[i].trim() == '') {
+						sa.arrayDelete(this.value_arr, this.value_arr[i]);
+						i--;
+					}
+				}
+				console.log('长度:' + this.value_arr.length);
+			},
+			
+		},
+		created() {
+			// console.log(this.br);
+			if(this.type == 'fast-btn') {
+				this.showBtns = this.show.split(',');
+				for (var i = 0; i < this.showBtns.length; i++) {
+					this.showBtns[i] = this.showBtns[i].trim();
+				}
+			}
+			// 如果是枚举
+			if(this.type == 'enum' || this.type == 'j' || this.type == 'switch') {	
+				this.parseJv();
+				console.log(this.jvList);
+			}
+			// 如果是 img-list 等 
+			if(this.type == 'img-list' || this.type == 'audio-list' || this.type == 'video-list' || this.type == 'file-list' || this.type == 'img-video-list') {
+				this.value_to_arr(this.value);
+			}
+		}
+	}
+</script>
+
+<style scoped>
+</style>

+ 422 - 0
sa-frame/com/sa-item.vue

@@ -0,0 +1,422 @@
+<template>
+	<!-- 自定义slot -->
+	<div class="c-item" :class="{br: br}" v-if="$slots.default && type != 'fast-btn'">
+		<label class="c-label" v-if="name && name.length > 0">{{name}}:</label> 
+		<span v-else-if="name === undefined"></span> 
+		<label class="c-label" v-else></label> 
+		<span v-else></span> 
+		<slot></slot>
+	</div>
+	<!-- 普通input -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'text' || type == 'link'">
+		<label class="c-label">{{name}}:</label> 
+		<el-input type="text" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-input>
+	</div>
+	<!-- 数字input -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'num'">
+		<label class="c-label">{{name}}:</label> 
+		<el-input type="number" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-input>
+	</div>
+	<!-- 密码input -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'password'">
+		<label class="c-label">{{name}}:</label> 
+		<el-input type="password" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-input>
+	</div>
+	<!-- 多行文本域 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'textarea'">
+		<label class="c-label">{{name}}:</label> 
+		<div style="display: inline-block;">
+			<el-input type="textarea" :autosize="{ minRows: 3, maxRows: 10}" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-input>
+		</div>
+	</div>
+	<!-- 钱 money (单位 元) -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'money'">
+		<label class="c-label">{{name}}:</label> 
+		<el-input type="text" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-input>
+		<span>元</span>
+	</div>
+	<!-- 钱 price-f (单位 分) -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'money-f'">
+		<label class="c-label">{{name}}:</label> 
+		<el-input type="text" v-model="valueReal" @input="$emit('input', $event * 100)" :placeholder="placeholder" :disabled="disabled"></el-input>
+		<span>元</span>
+	</div>
+	<!-- img -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'img'">
+		<label class="c-label">{{name}}:</label> 
+		<img :src="value" class="info-img" @click="sa.showImage(value, '400px', '400px')" v-if="!sa.isNull(value)">
+		<el-link type="primary" @click="sa.uploadImage(src => {$emit('input', src); sa.ok2('上传成功');})">上传</el-link>
+	</div>
+	<!-- audio -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'audio'">
+		<label class="c-label">{{name}}:</label> 
+		<el-link type="info" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+		<el-link type="primary" @click="sa.uploadAudio(src => {$emit('input', src); sa.ok2('上传成功');})">上传</el-link>
+	</div>
+	<!-- video -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'video'">
+		<label class="c-label">{{name}}:</label> 
+		<el-link type="info" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+		<el-link type="primary" @click="sa.uploadVideo(src => {$emit('input', src); sa.ok2('上传成功');})">上传</el-link>
+	</div>
+	<!-- file -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'file'">
+		<label class="c-label">{{name}}:</label> 
+		<el-link type="info" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+		<el-link type="primary" @click="sa.uploadFile(src => {$emit('input', src); sa.ok2('上传成功');})">上传</el-link>
+	</div>
+	<!-- img-list -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'img-list'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="c-item-mline image-box">
+			<div class="image-box-2" v-for="item in value_arr">
+				<img :src="item" @click="sa.showImage(item, '500px', '400px')" />
+				<p>
+					<i class="el-icon-close" style="position: relative; top: 2px;"></i>
+					<el-link @click="value_arr_delete(item)" style="color: #999;">删除这张 </el-link>
+				</p>
+			</div>
+			<!-- 上传图集 -->
+			<div class="image-box-2 up_img" @click="sa.uploadImageList(src => value_arr_push(src))">
+				<img src="../../static/img/up-icon.png">
+			</div>
+		</div>
+	</div>
+	<!-- audio-list、video-list、file-list、img-video-list -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'audio-list' || type == 'video-list' || type == 'file-list' || type == 'img-video-list'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="c-item-mline">
+			<div v-for="item in value_arr">
+				<el-link type="info" :href="item" target="_blank">{{item}}</el-link>
+				<el-link type="danger" class="del-rr" @click="value_arr_delete(item)">
+					<i class="el-icon-close"></i>
+					<small style="vertical-align: top;">删除</small>
+				</el-link>
+			</div>
+			<el-link type="primary" @click="sa.uploadAudioList(src => value_arr_push(src))" v-if="type == 'audio-list'">上传</el-link>
+			<el-link type="primary" @click="sa.uploadVideoList(src => value_arr_push(src))" v-if="type == 'video-list'">上传</el-link>
+			<el-link type="primary" @click="sa.uploadFileList(src => value_arr_push(src))" v-if="type == 'file-list'">上传</el-link>
+			<el-link type="primary" @click="sa.uploadImageList(src => value_arr_push(src))" v-if="type == 'img-video-list'">上传图片</el-link>
+			<el-link type="primary" @click="sa.uploadVideoList(src => value_arr_push(src))" v-if="type == 'img-video-list'" style="margin-left: 7px;">上传视频</el-link>
+		</div>
+	</div>
+	<!-- 富文本 richtext f -->
+	<div class="c-item" style="margin-top: 10px;" :class="{br: br}" v-else-if="type == 'richtext' || type == 'f'">
+		<label class="c-label">{{name}}:</label> 
+		<div class="editor-box c-item-mline">
+			<div :id="'editor-' + editor_id"></div>
+		</div>
+		<div style="clear: both;"></div>
+	</div>
+	<!-- enum 枚举 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'enum' || type == 'j' || type == 'switch'">
+		<label class="c-label">{{name}}:</label> 
+		<el-radio-group v-if="jtype == 1 || jtype == 2" :class="{'s-radio-text': jtype == 2}" :value="value" @input="onInput">
+			<el-radio label="" v-if="def">{{def}}</el-radio>
+			<el-radio v-for="j in jvList" :key="j.key" :label="j.key">{{j.value}}</el-radio>
+		</el-radio-group>
+		<el-radio-group v-if="jtype == 3" :value="value" @input="onInput">
+			<el-radio-button label="" v-if="def">{{def}}</el-radio-button>
+			<el-radio-button v-for="j in jvList" :key="j.key" :label="j.key">{{j.value}}</el-radio-button>
+		</el-radio-group>
+		<el-select v-if="jtype == 4" :value="value" @input="onInput">
+			<el-option label="" v-if="def" :value="def"></el-option>
+			<el-option v-for="j in jvList" :key="j.key" :label="j.value" :value="j.key"></el-option>
+		</el-select>
+	</div>
+	<!-- 日期选择器 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'date'">
+		<label class="c-label">{{name}}:</label> 
+		<el-date-picker type="date" value-format="yyyy-MM-dd" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-date-picker>
+	</div>
+	<!-- 日期时间选择器 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'datetime'">
+		<label class="c-label">{{name}}:</label> 
+		<el-date-picker type="datetime" value-format="yyyy-MM-dd HH:mm:ss" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-date-picker>
+	</div>
+	<!-- 时间选择器 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'time'">
+		<label class="c-label">{{name}}:</label> 
+		<el-time-picker value-format="HH:mm:ss" :value="value" @input="onInput" :placeholder="placeholder" :disabled="disabled"></el-time-picker>
+	</div>
+	<!-- 日期范围选择 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'date-range'">
+		<label class="c-label">{{name}}:</label> 
+		<el-date-picker
+			type="daterange"
+			range-separator="至"
+			start-placeholder="开始日期"
+			end-placeholder="结束日期"
+			value-format="yyyy-MM-dd"
+			:value="dateRangeValue" 
+			@input="dateRangeOnChange"
+			:disabled="disabled"
+			>
+		</el-date-picker>
+	</div>
+	<!-- 滑块 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'slider'">
+		<label class="c-label">{{name}}:</label> 
+		<div style="display: inline-block; height: 0px; vertical-align: top; width: 250px;">
+			<el-slider :value="value" @input="onInput" style="position: relative; top: -5px;" :disabled="disabled"></el-slider>
+		</div>
+	</div>
+	<!-- 级联输入 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'cascader'">
+		<label class="c-label">{{name}}:</label> 
+		<el-cascader :value="value" @input="onInput" :options="options" :props="{expandTrigger: 'hover'}" :placeholder="placeholder" :disabled="disabled"></el-cascader>
+	</div>
+	<!-- 颜色输入 -->
+	<div class="c-item" :class="{br: br}" style="height: 0px;" v-else-if="type == 'color'">
+		<label class="c-label">{{name}}:</label> 
+		<el-color-picker :value="value" @input="onInput" :disabled="disabled"></el-color-picker>
+		<span class="c-remark" style="vertical-align: top;">{{value}}</span>
+	</div>
+	<!-- 评分组件 -->
+	<div class="c-item" :class="{br: br}" v-else-if="type == 'rate'">
+		<label class="c-label">{{name}}:</label> 
+		<div style="display: inline-block;">
+			<el-rate :value="value" @input="onInput" show-text :disabled="disabled"></el-rate>
+		</div>
+	</div>
+	<!-- 快捷增删改查按钮 -->
+	<div class="fast-btn" v-else-if="type == 'fast-btn'">
+		<el-button type="primary" icon="el-icon-plus" @click="$parent.add()" v-if="showBtns.indexOf('add') != -1">新增</el-button>
+		<el-button type="success" icon="el-icon-view" @click="$parent.getBySelect()" v-if="showBtns.indexOf('get') != -1">查看</el-button>
+		<el-button type="danger" icon="el-icon-delete" @click="$parent.deleteByIds()" v-if="showBtns.indexOf('delete') != -1">删除</el-button>
+		<el-button type="warning" icon="el-icon-download" @click="sa.exportExcel()" v-if="showBtns.indexOf('export') != -1">导出</el-button>
+		<el-button type="info"  icon="el-icon-refresh"  @click="sa.f5()" v-if="showBtns.indexOf('reset') != -1">重置</el-button>
+		<slot></slot>
+	</div>
+	<!-- 分页组件 -->
+	<div class="page-box" v-else-if="type == 'page'">
+		<el-pagination background
+			layout="total, prev, pager, next, sizes, jumper" 
+			:current-page.sync="curr" 
+			:page-size.sync="size" 
+			:total="total" 
+			:page-sizes="sizes || [1, 10, 20, 30, 40, 50, 100]" 
+			@current-change="changePage()" 
+			@size-change="changePage()">
+		</el-pagination>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		// props: ['name', 'value'],
+		props: {
+			// text、num、
+			type: {
+				default: 'text'
+			},
+			// label提示文字
+			name: {
+				type: String
+			},
+			// 绑定的值 
+			value: {},
+			// 提示文字
+			placeholder: {},
+			// 是否禁用
+			disabled: {},
+			// 是否换行 
+			br: {
+				type: Boolean,
+				default: false
+			},
+			// 日期范围时的选择字段,调用方需要加 .sync 修饰符 
+			start: {}, end: {},
+			// type=menu时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+			jv: {default: ''},
+			// type=menu时,具体的枚举类型 -- 1=单选框,2=单选文字,3=单选按钮,4=单选下拉框
+			jtype: {default: 1},
+			// type=menu时,增加的默认项文字 
+			def: {},
+			// 级联选择的数据列表
+			options: {},
+			// 快捷按钮显示列表,形如:add,get,delete,export,reset 
+			show: {},	
+			// 分页信息 
+			curr: {}, size: {}, total: {}, sizes: {}
+			
+		},
+		data() {
+			return {
+				// 日期范围时的值 
+				dateRangeValue: [],
+				// 快捷按钮显示按钮列表 
+				showBtns: [],
+				// type=menu时,解析的值列表    -- 形如:[{key: 1, value: '正常', color: 'green'}]
+				jvList: [],
+				// type = img-list 时,解析的元素List
+				value_arr: [],
+				// 富文本编辑器id
+				editor_id: '',
+				// 富文本编辑器对象 
+				editor: null,
+				// money-f 的底层字段
+				valueReal: ''
+			}
+		},
+		watch: {
+			// 监听一些类型的 value 变动 
+			value: function(oldValue, newValue) {
+				// img-list、audio-list、video-list、file-list、img-video-list
+				if(this.type == 'img-list' || this.type == 'audio-list' || this.type == 'video-list' || this.type == 'file-list' || this.type == 'img-video-list') {
+					this.value_to_arr(this.value); 
+				}
+				// 如果是富文本
+				// if(this.type == 'richtext' || this.type == 'f') {
+				// 	if(this.editor) {
+				// 		// this.editor.txt.html(newValue);
+				// 		$('#editor-' + this.editor_id + " .w-e-text").html(newValue);
+				// 	}
+				// }
+			},
+		},
+		methods: {
+			// input值发生变化时触发
+			onInput: function($event) {
+				this.$emit('input', $event);
+			},
+			// 日期范围选择时触发 
+			dateRangeOnChange: function(value) {
+				console.log(value);
+				this.dateRangeValue = value;
+				this.start = value[0];
+				this.end = value[1];
+				this.$emit('update:start',  value[0]);
+				this.$emit('update:end',  value[1]);
+			},
+			// 刷新分页 
+			changePage: function() {
+				this.$emit('update:curr', this.curr);
+				this.$emit('update:size', this.size);
+				this.$emit('change');
+			},
+			// 解析枚举 
+			parseJv: function() {
+				for(let key in this.jv) {
+					let value = this.jv[key];
+					let color = '';
+					// 
+					if(value.indexOf('[') != -1 && value.endsWith(']')) {
+						let index = value.indexOf('[');
+						color = value.substring(index + 1, value.length - 1);
+						value = value.substring(0, index);
+						// console.log(color + ' --- ' + value);
+					}
+					// 
+					if(isNaN(key) == false) {
+						key = parseInt(key);
+					}
+					// 
+					this.jvList.push({
+						key: key,
+						value: value,
+						color: color
+					})
+				}
+			},
+			// 解析 value 为 value_arr
+			value_to_arr: function(value) {
+				this.value_arr = sa.isNull(value) ? [] : value.split(',');		
+				for (var i = 0; i < this.value_arr.length; i++) {
+					if(this.value_arr[i] == '' || this.value_arr[i].trim() == '') {
+						sa.arrayDelete(this.value_arr, this.value_arr[i]);
+						i--;
+					}
+				}
+			},
+			// value_arr 数组增加值
+			value_arr_push: function(item) {
+				this.value_arr.push(item);
+				// this.value = this.value_arr.join(',');	
+				this.$emit('input', this.value_arr.join(','));
+			},
+			// value_arr 数组删除值 
+			value_arr_delete: function(item) {
+				sa.arrayDelete(this.value_arr, item);
+				// this.value = this.value_arr.join(',');	
+				this.$emit('input', this.value_arr.join(','));
+			},
+			// 创建富文本编辑器
+			create_editor: function(content) {
+				var E = window.wangEditor;
+				var editor = new E('#editor-' + this.editor_id);
+			
+				editor.config.menus = [
+					'head', 'fontSize', 'fontName', 'italic', 'underline', 'strikeThrough', 'foreColor', 'backColor', 'link', 'list',
+					'justify', 'quote', 'emoticon', 'image', 'table', 'code', 'undo', 'redo' // 重复
+				]
+				editor.config.debug = true; // debug模式
+				// editor.config.uploadFileName = 'file'; // 图片流name
+				editor.config.withCredentials = true; // 跨域携带cookie
+				editor.config.uploadImgMaxSize = 100 * 1024 * 1024;	// 图片大小最大100M
+				// editor.config.uploadImgShowBase64 = true   	// 使用 base64 保存图片
+				// 监听内容变动
+				editor.config.onchange = function (newHtml) {
+					// console.log("change 之后最新的 html", newHtml);
+					this.$emit('input', newHtml);
+				}.bind(this);
+				// 重写上传图片的函数到OSS 
+				editor.config.customUploadImg = function(files, insert) {
+					var file = files[0]; // 文件对象 
+					startUploadImage2(file, function(src) {
+						insert(src);
+						sa.msg('上传成功');
+					});
+				}
+				editor.create(); // 创建
+				editor.txt.html(content);	// 为编辑器赋值
+				this.editor = editor;
+				// setTimeout(function() {
+				// 	$('.editor-box').height($('.editor-box').height());
+				// })
+			},
+			// 为编辑器赋值 
+			editorSet: function(value) {
+				this.editor.txt.html(value);
+			},
+			valueSet(valueReal) {
+				this.valueReal = valueReal;
+			}
+		},
+		created() {
+			// console.log(this.br);
+			if(this.type == 'fast-btn') {
+				this.showBtns = this.show.split(',');
+				for (var i = 0; i < this.showBtns.length; i++) {
+					this.showBtns[i] = this.showBtns[i].trim();
+				}
+			}
+			// 如果是枚举
+			if(this.type == 'enum' || this.type == 'j' || this.type == 'switch') {	
+				this.parseJv();
+			}
+			// 如果是 img-list 等 
+			if(this.type == 'img-list' || this.type == 'audio-list' || this.type == 'video-list' || this.type == 'file-list' || this.type == 'img-video-list') {
+				this.value_to_arr(this.value);
+			}
+			// 如果是富文本
+			if(this.type == 'richtext' || this.type == 'f') {
+				this.editor_id = sa.randomString(32);
+				this.$nextTick(function() {
+					this.create_editor(this.value);
+				})
+			}
+			// 如果是 money-f 
+			if(this.type == 'money-f') {
+				if(this.value) {
+					this.valueReal = this.value / 100;
+				}
+			}
+			
+			
+		}
+	}
+</script>
+
+<style scoped>
+</style>

+ 244 - 0
sa-frame/com/sa-td.vue

@@ -0,0 +1,244 @@
+<template>
+	<!-- 自定义slot -->
+	<el-table-column v-if="$slots.default || $scopedSlots.default" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<slot :row="s.row" :index="s.index"></slot>
+		</template>
+	</el-table-column>
+	<!-- selection框 -->
+	<el-table-column v-else-if="type == 'selection'" type="selection" :width="width || '45px'" :min-width="minWidth"></el-table-column>
+	<!-- 普通td -->
+	<el-table-column v-else-if="type == 'text'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<span v-if="s.row[prop]">{{s.row[prop]}}</span>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- num 数字 -->
+	<el-table-column v-else-if="type == 'num'" :label="name" :width="width" :min-width="minWidth" class-name="tc-num">
+		<template slot-scope="s">
+			<span v-if="s.row[prop]">{{s.row[prop]}}</span>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- icon -->
+	<el-table-column v-else-if="type == 'icon'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<i v-if="s.row[prop]" :class="s.row[prop]" style="font-size: 1.3em;"></i>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- img -->
+	<el-table-column v-else-if="type == 'img'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<img v-if="s.row[prop]" :src="s.row[prop]" class="td-img" @click="sa.showImage(s.row[prop], '400px', '400px')" />
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- audio、video、file -->
+	<el-table-column v-else-if="type == 'audio' || type == 'video' || type == 'file'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<el-link type="info" :href="s.row[prop]" target="_blank" v-if="!sa.isNull(s.row[prop])">预览</el-link>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- img-list -->
+	<el-table-column v-else-if="type == 'img-list'" :label="name" :width="width" :min-width="minWidth || '120px'" show-overflow-tooltip>
+		<template slot-scope="s">
+			<div @click="sa.showImageList(value_to_arr(s.row[prop]))" style="cursor: pointer;" v-if="s.row[prop]">
+				<img :src="value_to_arr(s.row[prop])[0]" class="td-img" />
+				<span style="color: #999; padding-left: 0.5em;">点击预览</span>
+			</div>
+			<div v-else>{{not}}</div>
+		</template>
+	</el-table-column>
+	<!-- xxx-list -->
+	<el-table-column v-else-if="type == 'audio-list' || type == 'video-list' || type == 'file-list' || type == 'img-video-list'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<span v-if="s.row[prop]" style="color: #666;">共 {{value_to_arr(s.row[prop]).length}} 个</span>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	
+	<!-- textarea -->
+	<el-table-column v-else-if="type == 'textarea'" :label="name" :width="width" :min-width="minWidth" show-overflow-tooltip>
+		<template slot-scope="s">
+			<span v-if="s.row[prop]">{{sa.maxLength(s.row[prop], 100)}}</span>
+			<span v-else>{{not}}</span>
+		</template>
+	</el-table-column>
+	<!-- richtext 富文本 -->
+	<el-table-column v-else-if="type == 'richtext' || type == 'f'" :label="name" :width="width" :min-width="minWidth" show-overflow-tooltip>
+		<template slot-scope="s">
+			<span>{{sa.maxLength(sa.text(s.row[prop]), 100)}}</span>
+		</template>
+	</el-table-column>
+	<!-- link -->
+	<el-table-column v-else-if="type == 'link'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<el-link type="primary" :href="s.row[prop]" target="_blank" v-if="!sa.isNull(s.row[prop])">{{s.row[prop]}}</el-link>
+			<div v-else>无</div>
+		</template>
+	</el-table-column>
+	<!-- link-btn -->
+	<el-table-column v-else-if="type == 'link-btn'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<el-link type="primary" @click="$emit('click', s)" v-if="!sa.isNull(s.row[prop])">{{s.row[prop]}}</el-link>
+			<div v-else>无</div>
+		</template>
+	</el-table-column>
+	
+	<!-- 钱 money (单位 元) -->
+	<el-table-column v-else-if="type == 'money'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<b class="c-price" v-if="!sa.isNull(s.row[prop])">¥{{s.row[prop]}}</b>
+			<div v-else>无</div>
+		</template>
+	</el-table-column>
+	<!-- 钱 price-f (单位 分) -->
+	<el-table-column v-else-if="type == 'money-f'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<b class="c-price" v-if="!sa.isNull(s.row[prop])">¥{{s.row[prop] / 100}}</b>
+			<div v-else>无</div>
+		</template>
+	</el-table-column>
+	<!-- 显示枚举 j、num -->
+	<el-table-column v-else-if="type == 'enum' || type == 'j'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<b v-for="j in jvList" :key="j.key" :style="{color: j.color || '#606266'}" v-if="s.row[prop] == j.key">{{j.value}}</b>
+		</template>
+	</el-table-column>
+	<!-- switch 开关 -->
+	<el-table-column v-else-if="type == 'switch'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<el-switch 
+				v-model="s.row[prop]" v-if='jvList.length >= 2' 
+				:active-value="jvList[0].key" :inactive-value="jvList[1].key" 
+				:active-color="jvList[0].color || '#409EFF'" :inactive-color="jvList[1].color || '#ccc'"
+				@change="$emit('change', s)">
+			</el-switch>
+			<span v-for="j in jvList" :key="j.key" :style="{color: '#999'}" v-if="s.row[prop] == j.key">{{j.value}}</span>
+		</template>
+	</el-table-column>
+	<!-- rate 评分 -->
+	<el-table-column v-else-if="type == 'rate'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<el-rate :value="s.row[prop] <= 5 ? s.row[prop] : 5" show-text disabled v-if="!sa.isNull(s.row[prop])"></el-rate>
+			<div v-else>无</div>
+		</template>
+	</el-table-column>
+	<!-- date 日期 -->
+	<el-table-column v-else-if="type == 'date'" :label="name" :width="width" :min-width="minWidth" class-name="tc-date">
+		<template slot-scope="s"><span>{{sa.forDate(s.row[prop]) || not}}</span></template>
+	</el-table-column>
+	<!-- datetime 日期时间 -->
+	<el-table-column v-else-if="type == 'datetime'" :label="name" :width="width" :min-width="minWidth" class-name="tc-date">
+		<template slot-scope="s"><span>{{sa.forDate(s.row[prop], 2) || not}}</span></template>
+	</el-table-column>
+	<!-- time 时间 -->
+	<el-table-column v-else-if="type == 'time'" :label="name" :width="width" :min-width="minWidth" class-name="tc-date">
+		<template slot-scope="s"><span>{{s.row[prop] || not}}</span></template>
+	</el-table-column>
+	<!-- 用户头像 -->
+	<el-table-column v-else-if="type == 'user-avatar'" :label="name" :width="width" :min-width="minWidth">
+		<template slot-scope="s">
+			<img :src="s.row[prop.split(',')[1]]" class="td-img"
+				style="vertical-align: middle; margin-right: 5px;"
+				@click="sa.showImage(s.row[prop.split(',')[1]], '400px', '400px')" />
+			<b>{{s.row[prop.split(',')[0]]}}</b>
+		</template>
+	</el-table-column>
+</template>
+
+<script>
+	module.exports = {
+		// props: ['name', 'value'],
+		props: {
+			// text、img、
+			type: {
+				default: 'text'
+			},
+			// label提示文字
+			name: {},
+			label: {},
+			// 绑定的属性  
+			prop: {},
+			// 宽度 
+			width: {},
+			// 最小宽度
+			minWidth: {},
+			// type=menu时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+			jv: {default: ''},
+			// 空值时显示的文字
+			not: {default: '无'}
+		},
+		data() {
+			return {
+				// type=menu时,解析的值列表    -- 形如:[{key: 1, value: '正常', color: 'green'}]
+				jvList: [],
+				
+				// type = img-list 时,解析的元素List
+				value_arr: [],
+				
+			}
+		},
+		methods: {
+			// 解析枚举 
+			parseJv: function() {
+				for(let key in this.jv) {
+					let value = this.jv[key];
+					let color = '';
+					// 
+					if(value.indexOf('[') != -1 && value.endsWith(']')) {
+						let index = value.indexOf('[');
+						color = value.substring(index + 1, value.length - 1);
+						value = value.substring(0, index);
+						// console.log(color + ' --- ' + value);
+					}
+					// 
+					if(isNaN(key) == false) {
+						key = parseInt(key);
+					}
+					// 
+					this.jvList.push({
+						key: key,
+						value: value,
+						color: color
+					})
+				}
+			},
+			// 解析 value 为 value_arr
+			value_to_arr: function(value) {
+				var value_arr = sa.isNull(value) ? [] : value.split(',');		
+				for (var i = 0; i < value_arr.length; i++) {
+					if(value_arr[i] == '' || value_arr[i].trim() == '') {
+						sa.arrayDelete(value_arr, value_arr[i]);
+						i--;
+					}
+				}
+				// this.value_arr = value_arr;
+				// this.$nextTick(function() {
+				// 	this.value_arr = value_arr;
+				// })
+				return value_arr;
+			},
+		},
+		 mounted() {
+			// console.log(this.$slots);
+			// console.log(this.$scopedSlots.default);
+			// console.log(this.type);
+			this.name = this.name || this.label;
+			// 如果是枚举 
+			if(this.type == 'enum' || this.type == 'j' || this.type == 'switch') {
+				this.parseJv();
+			}
+			// 如果是 img-list 等 
+			// if(this.type == 'img-list' || this.type == 'audio-list' || this.type == 'video-list' || this.type == 'file-list' || this.type == 'img-video-list') {
+			// 	this.value_to_arr(this.value);
+			// }
+		}
+	}
+</script>
+
+<style scoped>
+</style>

BIN
sa-frame/index/admin-loading.gif


+ 106 - 0
sa-frame/index/admin-util.js

@@ -0,0 +1,106 @@
+// ======================== 一些工具方法 ======================== 
+
+var sa_admin_code_util = {
+	// 删除数组某个元素
+	arrayDelete: function(arr, item){
+		var index = arr.indexOf(item);
+		if (index > -1) {
+			arr.splice(index, 1);
+		}
+	},
+	
+	//执行一个函数, 解决layer拉伸或者最大化的时候,iframe高度不能自适应的问题
+	solveLayerBug: function(index) {
+		var selected = '#layui-layer' + index;
+		var height = $(selected).height();
+		var title_height = $(selected).find('.layui-layer-title').height();
+		$(selected).find('iframe').css('height', (height - title_height) + 'px');
+		// var selected = '#layui-layer' + index;
+		// var height = $(selected).height();
+		// var title_height = $(selected).find('.layui-layer-title').height();
+		// $(selected).find('iframe').css('height', (height - title_height) + 'px');
+	},
+	
+	// ======================== 菜单集合相关 ======================== 
+	
+	// 将一维平面数组转换为 Tree 菜单 (根据其指定的parent_id添加到其父菜单的childList)
+	arrayToTree: function(menu_list) {
+		for (var i = 0; i < menu_list.length; i++) {
+			var menu = menu_list[i];
+			// 添加到其指定的父菜单的childList
+			if(menu.parent_id) {
+				var parent_menu = this.findMenuById(menu_list, menu.parent_id);
+				if(parent_menu) {
+					parent_menu.childList = parent_menu.childList || [];
+					parent_menu.childList.push(menu);
+					menu_list.splice(i, 1);	// 从一维中删除 
+					i--;
+				}
+			}
+		}
+		return menu_list;
+	},
+	
+	
+	// 将 menu_list 处理一下 
+	refMenuList: function(menu_list) {
+		for (var i = 0; i < menu_list.length; i++) {
+			var menu = menu_list[i];
+			// 有子项的递归处理 
+			if(menu.childList){
+				menu.children = menu.childList;
+				this.refMenuList(menu.childList);
+			}
+		}
+		return menu_list;
+	},
+	
+	
+	
+	// 返回指定 index 的menu   
+	getMenuById: function(menuList, id) {
+		for (var i = 0; i < menuList.length; i++) {
+			var menu = menuList[i];
+			if(menu.id + '' == id + '') {
+				return menu;
+			}
+			// 如果是二级或多级 
+			if(menu.childList) {
+				var menu2 = this.getMenuById(menu.childList, id);
+				if(menu2 != null) {
+					return menu2;
+				}
+			}
+		}
+		return null;
+	},
+	
+	
+	
+	// 将 Tree 菜单 转换为 一维平面数组 
+	treeToArray: function(menu_list) {
+		var arr = [];
+		function _dg(menu_list) {
+			menu_list = menu_list || [];
+			for (var i = 0; i < menu_list.length; i++) {
+				var menu = menu_list[i];
+				arr.push(menu);
+				// 如果有子菜单 
+				if(menu.childList) {
+					_dg(menu.childList);
+				}
+			}
+		}
+		_dg(menu_list);
+		return arr;
+	},
+	
+	
+}
+
+
+
+
+
+
+

+ 65 - 0
sa-frame/index/index.css

@@ -0,0 +1,65 @@
+*{margin: 0; padding: 0; }
+html,body{height: 100%; background-color: #EEE;} 
+body{height: 100vh;background-color: #EEE;background-image: url(admin-loading.gif); background-repeat: no-repeat;background-position: 50% 50%;}
+.app{height: 100%; font-size: 16px; font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;}
+.app{background-color: #EEE;}
+
+/* 变量 */
+body{
+	--nav-left-width: 200px;
+	--nav-left-width-fold: 64px;
+	--nav-right-1-height: 50px;
+	--nav-right-2-height: 35px;
+}
+
+.nav-left, .nav-right {position: fixed; top: 0; height: 100%;}
+
+/* 左边 */
+.nav-left{width: var(--nav-left-width); left: 0px; z-index: 200; overflow: hidden;}
+.nav-left-top{width: 100%; box-sizing: border-box; height: 85px; line-height: 85px;/* z-index: 100; */ overflow: hidden;}
+.nav-left-bottom{width: 100%; box-sizing: border-box; height: calc(100% - 85px); overflow: hidden;}
+
+/* 右边 */
+.nav-right{width: calc(100% - var(--nav-left-width)); right: 0px; z-index: 100; }
+.nav-right-1{height: var(--nav-right-1-height); line-height: var(--nav-right-1-height); z-index: 200; position: relative; border-bottom: 1px #F1F1F1 solid; box-sizing: border-box; overflow: hidden;}
+.nav-right-2{height: var(--nav-right-2-height); line-height: var(--nav-right-2-height); z-index: 200; position: relative; box-shadow: 0 2px 2px rgba(0,0,0,0.1);}
+.nav-right-3{width: 100%; height: calc(100vh - var(--nav-right-1-height) - var(--nav-right-2-height)); position: relative; overflow: hidden;}
+
+/* .fas{transition: all 0s;} */
+
+/* 所有带动画的元素 */
+.admin-logo,.nav-left,.nav-left-top,.nav-left-bottom, .nav-right/* , .nav-right-2 * */{transition: all 0.2s; }
+
+
+/* 菜单折叠 */
+.app-fold{
+	--nav-left-width: 64px;
+}
+
+/* 菜单折叠时 部分元素隐藏 */
+.app-fold .admin-title, .app-fold .menu-name, .app-fold-right .el-submenu__icon-arrow{display: none;}
+.app-fold .admin-logo{margin-left: 12px !important;}
+
+/* .nav-right-3 包裹了太多 View,不能让它参与动画,因为实在太TM卡了 */
+.nav-right-3{width: calc(100% - var(--nav-left-width)); position: fixed; transition: none;} 
+.app-fold-right .nav-right-3{width: calc(100% - 64px); left: 64px;}
+
+
+/* -------------- 其它 --------------- */
+
+/* 折叠时悬浮菜单样式,防止透明 */
+.el-menu--vertical .el-menu--popup{background-color: #FFF !important; color: red !important;}
+
+/* 最高层级 */
+.z-index-max{z-index: 2147483647;}
+
+
+/* 遮罩样式 */
+.shade-fox{position: absolute; z-index: 1000000; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); color: #FFF; top: 0px;}
+.shade-fox{display: flex; justify-content: center; align-items: center}
+.shade-text{}
+
+/* 去除掉便签的大边框 */
+.layer-note-class .layui-layer-input{outline: 0; box-shadow: none !important; padding: 0.8em !important; font-family: 'Times New Roman', Times, serif;}
+.layer-note-class .layui-layer-input{border: 0px #ddd solid; border-bottom: 1px #ddd solid;}
+

+ 495 - 0
sa-frame/index/index.js

@@ -0,0 +1,495 @@
+// 首页 
+var homeTab = {
+	id: 'home',	// 唯一标识 
+	name: '首页',
+	url: 'main.html',	// 页面地址 
+	isNeedLoad: false,		// 标注:是否需要此刻加载
+	hideClose: true	// 隐藏关闭键 
+}
+
+// sa_admin对象 
+var sa_admin = new Vue({
+	components: {
+		"nav-logo": httpVueLoader('sa-frame/nav/nav-logo.vue'),				// logo 
+		"nav-menu-bar": httpVueLoader('sa-frame/nav/nav-menu-bar.vue'),		// 菜单栏 
+		"nav-tool-bar": httpVueLoader('sa-frame/nav/nav-tool-bar.vue'),		// 工具栏
+		"nav-tab-bar": httpVueLoader('sa-frame/nav/nav-tab-bar.vue'),		// tab栏
+		"nav-view-vessel": httpVueLoader('sa-frame/nav/nav-view-vessel.vue'),	// 视图容器 
+		"com-right-menu": httpVueLoader('sa-frame/nav/com-right-menu.vue'),		// 右键菜单 
+		"com-add-tab": httpVueLoader('sa-frame/nav/com-add-tab.vue'),			// 双击添加 tab 的弹窗 
+	},
+	el: '.app',
+	data: {
+		// ------------------------------- 配置 -------------------------------
+		title: '',		// 页面标题  -- Sa-Admin
+		logo: '',		// logo地址  -- sa-frame/admin-logo.png
+		icon: '',		// icon地址  -- sa-frame/admin-logo.png
+		version: 'v1.40.0',					// 当前版本号
+		updateTime: '2021-9-26',			// 更新日期 
+		githubUrl: 'https://github.com/click33/sa-admin',	// github地址 
+		isRemeOpen: true,		// 是否记住上一次最后打开的窗口 
+		printInfo: true,		// 是否在控制台打印信息 
+		homeTab: homeTab,	// 主页首屏 Tab 
+		menuList: [],		// 全部菜单集合 
+		showList: [],		// 显示的菜单集合(id集合) 
+		
+		plusVersion: 'v1.26.0',				//sa-plus版本
+		plusUpdateTime: '2021-10-24',		//sa-plus版本
+		plusGithubUrl: 'https://github.com/click33/sa-plus',	// github地址 
+		
+		// ------------------------------- 状态 -------------------------------
+		themeV: localStorage.getItem('themeV') || '1',	// 当前 / 默认的主题 
+		isOpen: true,			// 当前是否展开菜单 (整体框架)
+		isOpenRight: true,		// 当前是否展开  (右边) (将右边盒子折叠与菜单折叠分开,这样可以减少动画的卡顿现象) 
+		activeMenuId: '0',		// 正在高亮的菜单id 
+		isDrag: false,			// 当前是否正在拖拽 tab 
+		dragTab: null,			// 当前正在拖拽的 tab 
+		tabList: [homeTab],		// 当前 Tab 集合 
+		viewList: [homeTab],		// 当前 View 集合 
+		nativeTab: homeTab,		// 当前正显示的Tab 
+		user: null	,// user信息
+		dropList: [],			// 头像处下拉列表菜单 
+	},
+	watch: {
+		// 监听title改变时, 页面title也跟着切换 
+		title: function(newValue, oldValue) {
+			document.querySelector('title').innerHTML = newValue;
+		},
+		// 监听 icon_url 网页图标 
+		icon: function(newValue, oldValue) {
+			var icon = newValue;
+			var iconTarget = document.querySelector('.admin-icon');
+			if(iconTarget) {
+				iconTarget.setAttribute('href', icon);
+			}
+		}
+	},
+	methods: {
+		
+		// ------------------- 初始化相关 -------------------- 
+		// 初始化模板, 此方法必须且只能调用一次 
+		init: function(option) {
+			
+			// 打开上次最后的一个窗口  
+			this.showTabByHash();	
+			if(this.nativeTab.id == this.homeTab.id) {
+				this.showHome();
+			}
+			
+			// 打印版本等信息
+			if(this.printInfo) {
+				this.printVesion();
+			}
+			
+			// 手动触发一下窗口变动监听
+			window.onresize();		
+			
+		},
+		// 初始化菜单:
+		// 	showList = 显示菜单id数组  —— (注意是id的数组),你填哪些id哪些菜单才会显示 ,为空时代表显示所有	
+		initMenu: function(showList) {
+			this.setMenuList(window.menuList, showList);
+		},
+		// 写入菜单: 
+		// 	menuList = 全部菜单  —— 可以是已经渲染好的 tree 数组,也可以是一个尚未渲染的一维数组(你只要指定好 parent_id,Sa-Admin内部会自动渲染)
+		// 	showList = 显示菜单id数组  —— (注意是id的数组),你填哪些id哪些菜单才会显示 ,为空时代表显示所有	
+		setMenuList: function(menuList, showList) {
+			// 设置 全部菜单 
+			this.menuList = this.arrayToTree(menuList);
+			// 设置 显示的菜单id 
+			showList = showList || this.getAllId(this.menuList);
+			for (var i = 0; i < showList.length; i++) {
+				showList[i] = showList[i] + '';
+			} 
+			this.showList = showList;
+		},
+		
+		// ------------------- Menu 相关操作 --------------------
+		// 根据 id 查找 Menu 
+		getMenuById: function(id) {
+			return this.findMenuById(this.menuList, id);
+		},
+		// 显示某个菜单,根据id 
+		showMenuById: function(id) {
+			var menu = this.getMenuById(id);
+			if(menu) {
+				this.showTab(menu); 
+			}
+		},
+		// 显示homeTab
+		showHome: function() {
+			this.showTab(this.homeTab); 
+		},
+		// 返回当前所有菜单的 一维数组 形式 (将树形菜单转化为一维数组并返回) 方便遍历 
+		getYwList: function() {
+			var arr = [];
+			function _dg(menuList) {
+				menuList = menuList || [];
+				for (var i = 0; i < menuList.length; i++) {
+					var menu = menuList[i];
+					arr.push(menu);
+					// 如果有子菜单 
+					if(menu.childList) {
+						_dg(menu.childList);
+					}
+				}
+			}
+			_dg(this.menuList);
+			return arr;
+		},
+		// 获取菜单所有id 
+		getAllId: function() {
+			var arr = [];
+			this.getYwList().forEach(function(item) {
+				arr.push(item.id);
+			});
+			return arr;
+		},
+		
+		// ------------------- Tab 相关操作 --------------------
+		// 刷新Tab
+		f5Tab: function(tab) {
+			var cs = '#iframe-' + tab.id;
+			var iframe = document.querySelector(cs);
+			if(iframe) {
+				iframe.setAttribute('src', this.getTabUrl(tab));
+			} else {
+				tab.isNeedLoad = false;
+				this.$nextTick(function() {
+					tab.isNeedLoad = true;
+				})
+			}
+		},
+		// 获取 Tab,根据 id
+		getTabById: function(id) {
+			for (var i = 0; i < this.tabList.length; i++) {
+				if(this.tabList[i].id + '' == id + '') {
+					return this.tabList[i];
+				}
+			}
+			return null;
+		},
+		// 添加一个Tab  {id,name,url}
+		addTab: function(tab) {
+			// 如果没有提供id,则随机一个
+			if(!tab.id) {
+				tab.id = new Date().getTime() + '' + this.randomNum();
+			}
+			// 如果没有指定类型
+			if(tab.view === undefined) {
+				if(this.getUrlExt(tab.url).toLowerCase() == 'vue') {
+					tab.view = httpVueLoader(tab.url);
+				}
+			}
+			if(tab.isNeedLoad === undefined) {
+				// tab.isNeedLoad = true;
+				Vue.set(tab, 'isNeedLoad', true);
+			}
+			// console.log('添加之前:' + JSON.stringify(tab));
+			this.tabList.push(tab);
+			this.viewList.push(tab);
+			// tab 超过 20 个,提示过多,如果用户无视继续添加则超过 30 个后不再提示 
+			if(this.tabList.length > 20 && this.tabList.length < 30) {
+				sa_admin.$message({message: '选项卡过多会造成窗口卡顿,建议您关闭不使用的窗口', type: 'warning'});
+			}
+		},
+		// 显示某个页面  (如果不存在, 则先添加)
+		showTab: function(tab) {
+			// 标注:需要此刻加载 
+			// tab.isNeedLoad = false;	
+			Vue.set(tab, 'isNeedLoad', true);
+			// 如果是外部链接
+			if(tab.is_blank) {
+				return open(tab.url); 
+			}
+			// 如果是当前正在显示的tab , 则直接返回,无需继续操作 
+			if(tab == this.nativeTab) {
+				return;
+			}
+			// 如果是click函数 
+			if(tab.click) {
+				if(tab.click() !== true) {
+					return;
+				}
+			}
+			// 如果这个 tab 还没有添加到 tabList 上 
+			if(this.getTabById(tab.id) == null){
+				this.addTab(tab);
+			}
+			// 然后开始显示这个 tab 
+			this.nativeTab = tab;
+			// this.nativeTab.is_load = true;	// 标注:已经加载过了 
+			this.activeMenuId = tab.id + '';	// 左边自动关联, 如果左边没有,则无效果 
+			
+			// 刷新一下url中的锚链 
+			this.$nextTick(function() {
+				this.f5HashByNativeTab();
+			})
+			
+			// 调整一下滚动条 
+			this.$nextTick(function() {
+				try{
+					this.$refs['nav-tab-bar'].scrollToAuto(); 
+				}catch(e){}
+			})
+		},
+		// 显示一个选项卡, 根据 id , 不存在则不显示 
+		showTabById: function(id) {
+			var tab = this.getTabById(id);
+			if(tab) {
+				this.showTab(tab);
+			}
+		},
+		// 关闭 tab (带动画)
+		closeTab: function(tab, callFn) {
+			
+			// homeTab不能关闭 
+			if(tab == this.homeTab || tab.hideClose){
+				return;
+			}
+			
+			// 执行关闭动画
+			var div = document.querySelector('#tab-' + tab.id);
+			div.style.width = div.offsetWidth + 'px';
+			setTimeout(function() {
+				div.style.width = '0px';
+			}, 0);
+			
+			// 等待动画结束
+			setTimeout(function() {
+				
+				// 如果 tab 为当前正在显示的 tab, 则切换为前一个 tab  
+				if(tab == this.nativeTab) {
+					var index = this.tabList.indexOf(tab); 
+					var preTab = this.tabList[index - 1]; 
+					if(preTab) {
+						this.showTab(preTab); 
+					} else {
+						var nextTab = this.tabList[index + 1]; 
+						this.showTab(nextTab); 
+					}
+				}
+				// 从 tabList 中移除这个 tab 
+				sa_admin_code_util.arrayDelete(this.tabList, tab);
+				sa_admin_code_util.arrayDelete(this.viewList, tab);
+				// 如果有回调 
+				if(callFn) {
+					this.$nextTick(function() {
+						callFn();
+					})
+				}
+			}.bind(this), 150);
+		},
+		// 关闭 tab, 根据 id 
+		closeTabById: function(id, callFn) {
+			var tab = this.getTabById(id);
+			if(tab) {
+				this.closeTab(tab, callFn);
+			}
+		},
+		// 悬浮打开 tab 
+		xfTab: function(tab) {
+			console.log('悬浮');
+			// layer打开
+			var index = layer.open({
+				type: 2,
+				title: tab.name,
+				moveOut: true, // 是否可拖动到外面
+				maxmin: true, // 显示最大化按钮
+				shadeClose: false,
+				shade: 0,
+				area: ['80%', '80%'],
+				zIndex: layer.zIndex,
+				content: this.getTabUrl(tab),
+				// 解决拉伸或者最大化的时候,iframe高度不能自适应的问题
+			    resizing: function (layero) {
+			        sa_admin_code_util.solveLayerBug(index);
+			    },
+				// 操作这个layer的时候置顶它 
+				success: function(layero){
+					layer.setTop(layero); 
+				}
+			});
+			// 解决拉伸或者最大化的时候,iframe高度不能自适应的问题 
+			document.querySelector('#layui-layer' + index + ' .layui-layer-max').onclick = function() {
+				setTimeout(function() {
+					sa_admin_code_util.solveLayerBug(index);
+				}, 200)
+			}
+		},
+		// 新窗口打开 tab 
+		newWinTab: function(tab) {
+			open(this.getTabUrl(tab)); 
+			// this.closeTab(tab);
+		},
+		// 获取指定 tab 所代表 iframe 的 url 地址 (同域下可获取最新地址, 跨域时只能获取初始化时的地址)
+		getTabUrl: function(tab) {
+			var cs = '#iframe-' + tab.id;
+			var iframe = document.querySelector(cs);
+			if(!iframe) {
+				return tab.url;
+			}
+			try{
+				return iframe.contentWindow.location.href;
+			}catch(e){
+				return iframe.getAttribute('src');
+			}
+		},
+		
+		// ------------------- 框架整体相关操作 --------------------
+		// 展开菜单 
+		startOpen: function() {
+			this.isOpen = true;
+			setTimeout(function() {
+				this.isOpenRight = true;
+			}.bind(this), 200);
+		},
+		// 折叠菜单 
+		endOpen: function() {
+			this.isOpen = false;
+			this.isOpenRight = false;
+		},
+		
+		// ------------------- 锚链接路由相关 --------------------
+		// 根据锚链接, 打开窗口
+		showTabByHash: function() {
+			// 如果非记住模式
+			if(this.isRemeOpen == false) {
+				return;
+			}
+			// 获取锚链接中的id
+			var hash = location.hash;
+			var id = hash.replace('#', '');
+			if(id == '') {
+				return;
+			}
+			// 如果已经存在与tabbar中 
+			var tab = this.getTabById(id);
+			if(tab) {
+				return this.showTab(tab);
+			}
+			// 否则从菜单中打开 
+			this.showMenuById(id);
+			// 此时, 仍有一种tab打不开, 那就是自定义tab然后还已经关闭的,
+			// 预设 解决方案: 在localStor里存储所有打开过的tab,
+			// 以后如果有强需求这个功能时, 再实现 
+		},
+		// 根据当前tab刷新一下锚链接 
+		f5HashByNativeTab: function() {
+			// 如果非记住模式
+			if(this.isRemeOpen == false) {
+				return;
+			}
+			location.hash = this.nativeTab.id;
+		},
+		
+		// ------------------- 工具方法 -------------------- 
+		// 弹窗提示 
+		msg: function(msg) {
+			layer.msg(msg)
+		},
+		// 返回随机数 
+		randomNum: function(min, max) {
+			min = min || 1;
+			max = max || 1000000000;
+			return parseInt(Math.random() * (max - min + 1) + min, 10);
+		},
+		// 从 menuList 里查找指定 id 的 menu,支持多级递归 
+		findMenuById: function(menuList, id) {
+			for (var i = 0; i < menuList.length; i++) {
+				var menu = menuList[i];
+				if(menu.id + '' == id + '') {
+					return menu;
+				}
+				// 如果是二级或多级
+				if(menu.childList) {
+					var menu2 = this.findMenuById(menu.childList, id);
+					if(menu2 != null) {
+						return menu2;
+					}
+				}
+			}
+			return null;
+		},
+		// 获取文件后缀
+		getUrlExt: function(url) {
+			if(!url) {
+				return "";
+			}
+			if(url.indexOf('?') > -1) {
+				url = url.split('?')[0];
+			}
+			if(url.indexOf('#') > -1) {
+				url = url.split('#')[0];
+			}
+			var index= url.lastIndexOf(".");
+			if(index == -1) {
+				return "";
+			}
+			var ext = url.substr(index + 1);
+			return ext;
+		},
+		// 将一维平面数组转换为 Tree 菜单 (根据其指定的 parent_id 添加到其父菜单的childList)
+		arrayToTree: function(menuList) {
+			for (var i = 0; i < menuList.length; i++) {
+				var menu = menuList[i];
+				// 如果这个 Menu 指定了 parent_id 属性,则将其转移到其指定的父 Menu 的 childList 属性上 
+				if(menu.parent_id) {
+					var parent_menu = this.findMenuById(menuList, menu.parent_id);
+					if(parent_menu) {
+						menu.parent_menu = parent_menu;
+						parent_menu.childList = parent_menu.childList || [];
+						parent_menu.childList.push(menu);
+						menuList.splice(i, 1);	// 从一维中删除 
+						i--;
+					}
+				}
+			}
+			return menuList;
+		},
+		
+		// ------------------- 其它 -------------------- 
+		// 获取指定 tab 栏的 window 对象, 用于多窗口通信 
+		getTabWindow: function(tabId) {
+			var iframe = document.querySelector('#iframe-' + tabId);
+			if(iframe != null)  {
+				return iframe.contentWindow;
+			}
+			return null;
+		},
+		// 打印版本
+		// printVesion: function() {
+		// 	console.log('欢迎使用Sa-Admin,当前版本:' + this.version + ",更新于:" + this.updateTime + ",GitHub地址:" + this.githubUrl);
+		// 	console.log('如在使用中发现任何bug或者疑问,请加入QQ群交流:782974737,点击加入:' + 'https://jq.qq.com/?_wv=1027&k=5DHN5Ib');
+		// },
+		printVesion: function() {
+			var str = ('欢迎使用sa-plus,当前版本:' + this.plusVersion + ",更新于:" + this.plusUpdateTime + ",GitHub地址:" + this.plusGithubUrl);
+			// console.log('%c%s', 'color: green; font-size: 12px; font-weight: 400; margin-top: 4px; margin-bottom: 4px;', str);
+			var str2 = ('如在使用中发现任何bug或者疑问,请加入QQ群交流:782974737,点击加入:' + 'https://jq.qq.com/?_wv=1027&k=5DHN5Ib');
+			console.log('%c%s', 'color: green; font-size: 12px; margin-top: 2px; margin-bottom: 2px;', str + ' \n' + str2);
+		},
+		
+	},
+	created:function(){
+		
+	}
+});
+var saAdmin = sa_admin;		
+Vue.prototype.sa_admin = sa_admin;
+Vue.prototype.saAdmin = saAdmin;
+
+// 监听窗口大小变动
+window.onresize = function() {
+	if(document.body.clientWidth < 800) {
+		sa_admin.endOpen();
+	} else {
+		sa_admin.startOpen();
+	}
+}
+
+// 监听锚链接变动
+window.onhashchange = function() {
+	sa_admin.showTabByHash();
+}
+
+

+ 225 - 0
sa-frame/index/theme.css

@@ -0,0 +1,225 @@
+/* 样式调整为继承父级 */
+.nav-left .el-submenu__title i,
+.nav-left .el-menu-item i,
+.nav-right-1 .el-dropdown,
+.tab-title:hover .el-icon-caret-right,
+.tab-title.tab-native .el-icon-caret-right {
+	color: inherit;
+}
+
+.el-menu,
+.el-submenu,
+.nav-left .el-submenu__title,
+.nav-left .el-submenu .el-submenu .el-submenu__title,
+.nav-left .el-menu-item {
+	color: inherit;
+	background-color: inherit;
+}
+
+.theme-0 .menu-name,.theme-0 .tab-title-2>span{transition: none !important;}
+
+
+/* 声明变量 */
+body{
+	--menu-bg-color: #222;		/* 菜单 - 背景色 */
+	--menu-color: #FFF;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #000;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #4E5465;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #2D8CF0;		/* 菜单选中 - 背景色 */
+	--menu-active-color: #FFF;				/* 菜单选中 - 文字色 */
+	--tool-bg-color: #FFF;			/* 工具栏 - 背景色 */
+	--tool-color: #333;				/* 工具栏 - 文字色 */
+	--tool-hover-bg-color: #EEE;			/* 工具栏悬浮 - 背景色 */
+	
+	/* --tab-hover-bg-color: var(--menu-active-bg-color); */		/* Tab栏悬浮和选中 - 文字色 */
+	/* --tab-hover-color: var(--menu-active-color); */			/* Tab栏悬浮和选中 - 文字色 */
+	
+	--nav-left-top-border-color: 1px #222 solid;	/* 左上 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #222 solid;	/* 左下 - 右边框颜色 */
+}
+
+/* ========================== 主题 - 0 默认样式 蓝色 ==========================  */
+.theme-0 {}
+
+/* 左上 - 右边框颜色 */
+.theme-0 .nav-left-top{
+	border-right: var(--nav-left-top-border-color);
+}
+/* 左下 - 右边框颜色 */
+.theme-0 .nav-left-bottom{
+	border-right: var(--nav-left-bottom-border-color);
+}
+
+/* 左边栏背景色,前景色 */
+.theme-0 .nav-left {
+	background-color: var(--menu-bg-color);
+	color: var(--menu-color);
+}
+
+/* 二级菜单背景色 */
+.theme-0 .el-submenu .el-menu-item,
+.theme-0 .nav-left .el-submenu .el-submenu .el-submenu__title{
+	background: var(--menu-bg-color-2);
+}
+
+/* 所有菜单悬浮样式*/
+.theme-0 .nav-left .el-submenu__title:hover,
+.theme-0 .nav-left .el-submenu .el-submenu .el-submenu__title:hover,
+.theme-0 .nav-left .el-menu-item:hover{
+	background-color: var(--menu-hover-bg-color);
+}
+/* 所有菜单选中时 */
+.theme-0 .nav-left .el-menu-item.is-active {
+	/* background-color: var(--menu-active-bg-color); */
+	background: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+}
+
+/* 工具栏背景色颜色, 前景色 */
+.theme-0 .nav-right-1 {
+	color: var(--tool-color);
+	background-color: var(--tool-bg-color);
+}
+
+/* 工具栏悬浮颜色 */
+.theme-0 .tool-fox:hover {
+	background-color: var(--tool-hover-bg-color);
+}
+
+/* tab卡片栏 - 悬浮颜色 */
+.theme-0 .tab-title:hover{
+	color: var(--menu-active-bg-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-0 .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+
+/* 以下的主题 logo栏变小 */
+.theme-3 .nav-left-top,
+.theme-4 .nav-left-top,
+.theme-10 .nav-left-top{height: 50px; line-height: 50px; text-indent: 0.3em;}
+
+.theme-3 .nav-left-top .admin-logo,
+.theme-4 .nav-left-top .admin-logo,
+.theme-10 .nav-left-top .admin-logo{width: 28px; height: 28px; position: relative; top: -2px;}
+
+.theme-3 .nav-left-bottom,
+.theme-4 .nav-left-bottom,
+.theme-10 .nav-left-bottom{height: calc(100% - 85px + 36px);}
+
+
+
+
+/* ========================== 主题-1 什么也不覆盖 即:全部取默认样式 ==========================  */
+.theme-1 {}
+
+/* ========================== 主题-2 绿色 ==========================  */
+.theme-2 {
+	--menu-active-bg-color: #009688;	/* 菜单选中 - 背景色 */
+}
+
+/* ========================== 主题-3 白色 清爽 ==========================  */
+.theme-3 {
+	--menu-bg-color: #FFF;		/* 菜单 - 背景色 */
+	--menu-color: #333;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #fafafa;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #ECF5FF;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #ECF5FF;		/* 菜单选中 - 背景色 */
+	--menu-active-color: #409EFF;				/* 菜单选中 - 文字色 */
+	
+	--nav-left-top-border-color: 1px #ddd solid;	/* 左上 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #ddd solid;	/* 左下 - 右边框颜色 */
+}
+/* ----- 附加样式 ----- */
+/* logo下面的边框 */
+.theme-3 .nav-left-top{border-bottom: 1px #eee solid;}
+/* tab卡片栏 - 悬浮颜色 */
+.theme-3 .tab-title:hover{
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-3 .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-color) solid;
+}
+
+
+/* ========================== 主题-4 灰绿色 ==========================  */
+.theme-4 {
+	--menu-bg-color: #EEE;		/* 菜单 - 背景色 */
+	--menu-color: #333;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #DDD;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #ECF5FF;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #009688;		/* 菜单选中 - 背景色 */
+	--menu-active-color: #FFF;				/* 菜单选中 - 文字色 */
+	--tool-bg-color: #222;			/* 工具栏 - 背景色 */
+	--tool-color: #EEE;				/* 工具栏 - 文字色 */
+	--tool-hover-bg-color: #444;			/* 工具栏悬浮 - 背景色 */
+	
+	--nav-left-bottom-border-color: 1px #ddd solid;	/* 左下 - 右边框颜色 */
+}
+
+.theme-4 .nav-left-top{height: 49px; line-height: 49px; text-indent: 0.3em; background-color: #222; color: #FFF;}
+
+/* ========================== 主题-5 红色 ==========================  */
+.theme-5 {
+	--menu-active-bg-color: #dd4949;	/* 菜单选中 - 背景色 */
+}
+
+/* ========================== 主题-6 钛合金  ==========================  */
+.theme-6 {
+	--menu-active-bg-color: #805322;		/* 菜单选中 - 背景色 */
+	--tool-bg-color: #222;				/* 工具栏 - 背景色 */
+	--tool-color: #EEE;					/* 工具栏 - 文字色 */
+	--tool-hover-bg-color: #444;			/* 工具栏悬浮 - 背景色 */
+}
+
+/* ========================== 主题-7 沉淀式黑蓝 ==========================  */
+.theme-7 {
+	--tool-bg-color: #222;				/* 工具栏 - 背景色 */
+	--tool-color: #EEE;					/* 工具栏 - 文字色 */
+	--tool-hover-bg-color: #444;			/* 工具栏悬浮 - 背景色 */
+}
+
+/* ========================== 主题-8 简约式灰蓝 ==========================  */
+.theme-8 {
+	--menu-active-bg-color: #4E5465;		/* 菜单选中 - 背景色 */
+}
+
+/* ========================== 主题-9 紫色 ==========================  */
+.theme-9 {
+	--menu-active-bg-color: #A906B3;		/* 菜单选中 - 背景色 */
+}
+
+/* ========================== 主题-10 简约草绿 ==========================  */
+.theme-10 {
+	--menu-bg-color: #FFF;		/* 菜单 - 背景色 */
+	--menu-color: #333;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #fff;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #ECF5FF;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #73D13D;		/* 菜单选中 - 背景色 */
+	
+	--nav-left-top-border-color: 1px #fff solid;	/* 左下 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #ddd solid;	/* 左下 - 右边框颜色 */
+}
+
+/* logo下面的边框 */
+.theme-10 .nav-left-top{border-bottom: 1px #eee solid;}
+
+/* tab卡片栏 - 悬浮颜色 */
+.theme-10 .tab-title:hover{
+	color: var(--menu-active-bg-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-10 .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}

+ 127 - 0
sa-frame/login/app.js

@@ -0,0 +1,127 @@
+/* -----------------------------------------------
+/* How to use? : Check the GitHub README
+/* ----------------------------------------------- */
+
+/* To load a config file (particles.json) you need to host this demo (MAMP/WAMP/local)... */
+/*
+particlesJS.load('particles-js', 'particles.json', function() {
+  console.log('particles.js loaded - callback');
+});
+*/
+
+/* Otherwise just put the config content (json): */
+
+particlesJS('particles-js',
+
+	{
+		"particles": {
+			"number": {
+				"value": 40,
+				"density": {
+					"enable": true,
+					"value_area": 800
+				}
+			},
+			"color": {
+				"value": "#ffffff"
+			},
+			"shape": {
+				"type": "circle",
+				"stroke": {
+					"width": 0,
+					"color": "#000000"
+				},
+				"polygon": {
+					"nb_sides": 5
+				},
+				"image": {
+					"src": "img/github.svg",
+					"width": 100,
+					"height": 100
+				}
+			},
+			"opacity": {
+				"value": 0.7,
+				"random": false,
+				"anim": {
+					"enable": false,
+					"speed": 1,
+					"opacity_min": 0.1,
+					"sync": false
+				}
+			},
+			"size": {
+				"value": 3,
+				"random": true,
+				"anim": {
+					"enable": false,
+					"speed": 40,
+					"size_min": 0.1,
+					"sync": false
+				}
+			},
+			"line_linked": {
+				"enable": true,
+				"distance": 150,
+				"color": "#ffffff",
+				"opacity": 0.6,
+				"width": 1
+			},
+			"move": {
+				"enable": true,
+				"speed": 6,
+				"direction": "none",
+				"random": false,
+				"straight": false,
+				"out_mode": "out",
+				"bounce": false,
+				"attract": {
+					"enable": false,
+					"rotateX": 600,
+					"rotateY": 1200
+				}
+			}
+		},
+		"interactivity": {
+			"detect_on": "canvas",
+			"events": {
+				"onhover": {
+					"enable": true,
+					"mode": "grab"
+				},
+				"onclick": {
+					"enable": true,
+					"mode": "push"
+				},
+				"resize": true
+			},
+			"modes": {
+				"grab": {
+					"distance": 200,
+					"line_linked": {
+						"opacity": 1
+					}
+				},
+				"bubble": {
+					"distance": 400,
+					"size": 40,
+					"duration": 2,
+					"opacity": 8,
+					"speed": 3
+				},
+				"repulse": {
+					"distance": 200,
+					"duration": 0.4
+				},
+				"push": {
+					"particles_nb": 4
+				},
+				"remove": {
+					"particles_nb": 2
+				}
+			}
+		},
+		"retina_detect": false
+	}
+
+);

BIN
sa-frame/login/bg.jpg


BIN
sa-frame/login/name.png


Різницю між файлами не показано, бо вона завелика
+ 8 - 0
sa-frame/login/particles.min.js


BIN
sa-frame/login/password.png


+ 35 - 0
sa-frame/login/reset.css

@@ -0,0 +1,35 @@
+@charset "utf-8";
+/* CSS Document */
+/*Reset*/
+*{box-sizing:content-box;}
+a:hover, a:focus{text-decoration:none;}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}
+table{border-collapse:collapse;border-spacing:0;}
+body{-webkit-text-size-adjust:none;}
+fieldset,img{border:0;}
+img{ vertical-align: top; max-width: 100%; }
+address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:normal;}
+ol,ul{list-style:none;}
+caption,th{text-align:left;}
+h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}
+q:before,q:after{content:'';}
+abbr,acronym {border:0;}
+.clearfix:after{visibility:hidden;display: block;font-size:0;content:" ";clear:both;height:0;}
+* html .clearfix{ zoom: 1; } /* IE6 */
+*:first-child+html .clearfix { zoom: 1; } /* IE7 */
+.cli{ clear:both; font-size:0; height:0; overflow:hidden;display:block;}
+.lclear{clear:left;font-size:0;height:0;overflow:hidden;}	
+.fl{float:left;}
+.fr{float:right;}
+
+/* ֹ
+iframe{nifm2:expression(this.src='about:blank',this.outerHTML='');}
+script{no2js:expression((this.src.toLowerCase().indexOf('http')==0)?document.close():'');}
+*/
+/* ıԼ˶
+div{word-wrap: break-word;word-break: normal;}  
+p{text-align:justify; text-justify:inter-ideograph;}
+*/
+/*general*/
+body{font-size:12px;font-family:'微软雅黑',"宋体","Arial Narrow",Helvetica,sans-serif;color:#000;line-height:1.2;text-align:left;}
+a{color:#333;text-decoration:none;}

+ 152 - 0
sa-frame/login/style.css

@@ -0,0 +1,152 @@
+@charset "utf-8";
+/* CSS Document */
+/*Reset*/
+*{box-sizing:content-box;}
+a:hover, a:focus{text-decoration:none;}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}
+table{border-collapse:collapse;border-spacing:0;}
+body{-webkit-text-size-adjust:none;}
+fieldset,img{border:0;}
+img{ vertical-align: top; max-width: 100%; }
+address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:normal;}
+ol,ul{list-style:none;}
+caption,th{text-align:left;}
+h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}
+q:before,q:after{content:'';}
+abbr,acronym {border:0;}
+.clearfix:after{visibility:hidden;display: block;font-size:0;content:" ";clear:both;height:0;}
+* html .clearfix{ zoom: 1; } /* IE6 */
+*:first-child+html .clearfix { zoom: 1; } /* IE7 */
+.cli{ clear:both; font-size:0; height:0; overflow:hidden;display:block;}
+.lclear{clear:left;font-size:0;height:0;overflow:hidden;}	
+.fl{float:left;}
+.fr{float:right;}
+
+/* ֹ
+iframe{nifm2:expression(this.src='about:blank',this.outerHTML='');}
+script{no2js:expression((this.src.toLowerCase().indexOf('http')==0)?document.close():'');}
+*/
+/* ıԼ˶
+div{word-wrap: break-word;word-break: normal;}  
+p{text-align:justify; text-justify:inter-ideograph;}
+*/
+/*general*/
+body{font-size:12px;font-family:'微软雅黑',"宋体","Arial Narrow",Helvetica,sans-serif;color:#000;line-height:1.2;text-align:left;}
+a{color:#333;text-decoration:none;}
+
+
+
+/* 以下为手写代码  */
+html,body{ 
+	width:100%;
+	height:100%;
+}
+
+canvas{
+  display:block;
+  vertical-align:bottom;
+}
+
+.count-particles{
+  background: #000022;
+  position: absolute;
+  top: 48px;
+  left: 0;
+  width: 80px;
+  color: #13E8E9;
+  font-size: .8em;
+  text-align: left;
+  text-indent: 4px;
+  line-height: 14px;
+  padding-bottom: 2px;
+  font-family: Helvetica, Arial, sans-serif;
+  font-weight: bold;
+}
+
+.js-count-particles{
+  font-size: 1.1em;
+}
+
+#stats,
+.count-particles{
+  -webkit-user-select: none;
+  margin-top: 5px;
+  margin-left: 5px;
+}
+
+#stats{
+  border-radius: 3px 3px 0 0;
+  overflow: hidden;
+}
+
+.count-particles{
+  border-radius: 0 0 3px 3px;
+}
+
+
+#particles-js{
+	width: 100%;
+	height: 100%;
+	position: relative;
+	/* background-image: url(sa-frame/login/bg.jpg); */
+	background-position: 50% 50%;
+	background-size: cover;
+	background-repeat: no-repeat;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+.sk-rotating-plane {
+	display: none;
+    width: 80px;
+    height: 80px;
+    margin: auto;
+    background-color: white;
+    -webkit-animation: sk-rotating-plane 1.2s infinite ease-in-out;
+    animation: sk-rotating-plane 1.2s infinite ease-in-out;
+    z-index: 1;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin-left: -40px;
+    margin-top: -80px;
+}
+.sk-rotating-plane.active{display: block;}
+
+@keyframes sk-rotating-plane{
+	0% {
+	    -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg);
+	    transform: perspective(120px) rotateX(0deg) rotateY(0deg);
+	}
+	50% {
+	    -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
+	    transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
+	}
+	100% {
+	    -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
+	    transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
+	}
+}
+
+@keyframes login-small{
+	0%{
+		transform: scale(1);-moz-transform: scale(1);	/* Firefox 4 */-webkit-transform: scale(1);	/* Safari 和 Chrome */-o-transform: scale(1);	/* Opera */-ms-transform:scale(1); 	/* IE 9 */
+	}
+	100%{
+		transform: scale(0.2);-moz-transform: scale(0.1);	/* Firefox 4 */-webkit-transform: scale(0.2);	/* Safari 和 Chrome */-o-transform: scale(0.1);	/* Opera */-ms-transform:scale(0.1); 	/* IE 9 */
+	}
+}
+
+.login{z-index: 2;position:absolute;width: 350px;border-radius: 2px;height: 500px;background: white;box-shadow: 0px 0px 5px #333333;top: 50%;left: 50%;margin-top: -250px;margin-left: -175px;transition: all 1s;-moz-transition: all 1s;	/* Firefox 4 */-webkit-transition: all 1s;	/* Safari 和 Chrome */-o-transition: all 1s;	/* Opera */}
+.login-top{font-size: 24px;margin-top: 100px;padding-left: 40px;box-sizing: border-box;color: #333333;margin-bottom: 50px;}
+.login-center{width: 100%;box-sizing: border-box;padding: 0 40px;margin-bottom: 30px;}
+.login-center-img{width: 20px;height: 20px;float: left;margin-top: 5px;}
+.login-center-img>img{width: 100%;}
+.login-center-input{float: left;width: 230px;margin-left: 15px;height: 30px;position: relative;}
+.login-center-input input{z-index: 2;transition: all 0.5s;padding-left: 10px;color: #333333;width: 100%;height: 30px;border: 0;border-bottom: 1px solid #cccccc;border-top: 1px solid #ffffff;border-left: 1px solid #ffffff;border-right: 1px solid #ffffff;box-sizing: border-box;outline: none;position: relative;}
+.login-center-input input:focus{border: 1px solid dodgerblue;}
+.login-center-input-text{background: white;padding: 0 5px;position: absolute;z-index: 0;opacity: 0;height: 20px;top: 50%;margin-top: -10px;font-size: 14px;left: 5px;color: dodgerblue;line-height: 20px;transition: all 0.5s;-moz-transition: all 0.5s;	/* Firefox 4 */-webkit-transition: all 0.5s;	/* Safari 和 Chrome */-o-transition: all 0.5s;	/* Opera */}
+.login-center-input input:focus~.login-center-input-text{top: 0;z-index: 3;opacity: 1;margin-top: -15px;}
+.login.active{-webkit-animation: login-small 0.8s ; animation: login-small 0.8s ;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}
+.login-button{cursor: pointer;width: 250px;text-align: center;height: 40px;line-height: 40px;background-color: dodgerblue;border-radius: 5px;margin: 0 auto;margin-top: 50px;color: white;}
+

+ 108 - 0
sa-frame/menu-list-sp.js

@@ -0,0 +1,108 @@
+// 此处定义所有有关 sa-plus 的路由菜单 
+// 如需添加自定义菜单,请不要更改此文件,请在 menu-list.js 里添加 (没有这个文件就新建) 
+window.menuList = window.menuList || [];
+window.menuList.unshift({
+	id: 'bas',
+	name: '身份相关',
+	isShow: false, // 隐藏显示 
+	info: '身份相关权限,不显示在菜单上',
+	childList: [{
+			id: '1',
+			name: '身份-超管',
+			info: '最高权限,超管身份的代表(请谨慎授权)',
+			isShow: false
+		},
+		{
+			id: '11',
+			name: '身份-普通账号',
+			isShow: false,
+			info: '无特殊权限'
+		},
+		{
+			id: '99',
+			name: '允许进入后台管理',
+			isShow: false,
+			info: '只有拥有这个权限的角色才可以进入后台'
+		},
+	]
+}, {
+	id: 'console',
+	name: '监控中心',
+	icon: 'el-icon-view',
+	info: '对本系统的各种监控',
+	parent: true,
+	childList: [{
+			id: 'sql-console',
+			name: 'SQL监控台',
+			url: 'sa-view-sp/sp-console/sql-console.html',
+			info: 'sql控制台'
+		},
+		{
+			id: 'redis-console',
+			name: 'Redis控制台',
+			url: 'sa-view-sp/sp-console/redis-console.html',
+			info: 'redis常用工具'
+		},
+		{
+			id: 'apilog-list',
+			name: 'API请求日志',
+			url: 'sa-view-sp/sp-apilog/api-log-list.html',
+			info: '记录本系统所有的api请求'
+		},
+		{
+			id: 'form-generator',
+			name: '在线表单构建',
+			url: 'https://mrhj.gitee.io/form-generator/#/'
+		},
+	]
+}, {
+	id: 'auth',
+	name: '权限控制',
+	parent: true,
+	icon: 'el-icon-unlock',
+	info: '对系统角色权限的分配等设计,敏感度较高,请谨慎授权',
+	childList: [{
+			id: 'role-list',
+			name: '角色列表',
+			url: 'sa-view-sp/sp-role/role-list.html',
+			info: '管理系统各种角色',
+			childList: [{id:'role-add',name:'添加角色',info:'添加角色的权限',isShow:false}]
+		},
+		{
+			id: 'menu-list',
+			name: '菜单列表',
+			url: 'sa-view-sp/sp-role/menu-list.html',
+			info: '所有菜单项预览'
+		},
+		{
+			id: 'admin-list',
+			name: '管理员列表',
+			url: 'sa-view-sp/sp-admin/admin-list.html',
+			info: '所有管理员账号'
+		},
+		{
+			id: 'admin-add',
+			name: '管理员添加',
+			url: 'sa-view-sp/sp-admin/admin-add.html',
+			info: '添加一个管理员'
+		},
+		// {id: 'apilog-list', name: '请求日志监控', url: 'sa-view-sp/sp-apilog/api-log-list.html', info: '记录本系统所有的api请求'},
+	]
+}, {
+	id: 'sp-cfg',
+	name: '系统配置',
+	icon: 'el-icon-setting',
+	parent: true,
+	info: '有关系统的一些配置',
+	childList: [{
+			id: 'sp-cfg-app',
+			name: '系统对公配置',
+			url: 'sa-view-sp/sp-cfg/app-cfg.html'
+		},
+		{
+			id: 'sp-cfg-server',
+			name: '服务器私有配置',
+			url: 'sa-view-sp/sp-cfg/server-cfg.html'
+		},
+	]
+}, );

+ 61 - 0
sa-frame/nav/com-add-tab.vue

@@ -0,0 +1,61 @@
+<template>
+	<!-- 双击弹出的窗口 -->
+	<div class="at-form-fox" style="width: 0px; height: 0px; overflow: hidden; ">
+		<div class="at-form-dom" style="width: 300px; padding: 20px 0 10px 0; background-color: #FFF;">
+			<el-form label-width="80px" size="mini">
+				<!-- <h5 style="padding: 0 0 10px 26px;">创建新页面</h5> -->
+				<el-form-item label="标题:">
+					<el-input style="width: 200px;" v-model="atTitle" placeholder="页面标题"></el-input>
+				</el-form-item>
+				<el-form-item label="地址:" style="margin-top: -10px;">
+					<el-input style="width: 200px;" v-model="atUrl" placeholder="https://www.baidu.com/" @keyup.native.enter="atOk()"></el-input>
+				</el-form-item>
+				<el-form-item label="操作:" style="margin-top: -10px;">
+					<el-button type="primary" icon="el-icon-plus" size="mini" @click="atOk()">确定</el-button>
+				</el-form-item>
+			</el-form>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				atTitle: '',		// 添加窗口时: 标题
+				atUrl: '',			// 添加窗口时: 地址 
+			}
+		},
+		methods: {
+			// 双击tab栏空白处, 打开弹窗添加窗口 
+			atOpen: function() {
+				window.r_layer_12345678910 = layer.open({
+					type: 1,
+					// shade: false,
+					shade: 0.5,
+					title: "添加新窗口", //不显示标题
+					content: $('.at-form-dom'), //捕获的元素
+					cancel: function(){
+						
+					}
+				});
+			},
+			// 根据表单添加新窗口 
+			atOk: function() {
+				if(this.atTitle == '' || this.atUrl == '') {
+					return;
+				}
+				this.$root.showTab({id: new Date().getTime(), name: this.atTitle, url: this.atUrl});
+				layer.close(window.r_layer_12345678910);
+				this.atTitle = '';
+				this.atUrl = '';
+			},
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+</style>

+ 160 - 0
sa-frame/nav/com-right-menu.vue

@@ -0,0 +1,160 @@
+<template>
+	<!-- 鼠标右键弹出的盒子 -->
+	<!-- 【向下展开动画,坐标平移动画】二者只可得其一 -->
+	<div class="right-box" :style="rightStyle" v-show="rightShow" tabindex="-1" @blur="right_closeMenu2()">
+		<div class="right-box-2">
+			<div @click="right_closeMenu(); right_f5()"><i class="el-icon-caret-right"></i>刷新</div>
+			<div @click="right_closeMenu(); right_copy()"><i class="el-icon-caret-right"></i>复制</div>
+			<div @click="right_closeMenu(); right_close()"><i class="el-icon-caret-right"></i>关闭</div>
+			<div @click="right_closeMenu(); right_close_other()"><i class="el-icon-caret-right"></i>关闭其它</div>
+			<div @click="right_closeMenu(); right_close_all()"><i class="el-icon-caret-right"></i>关闭所有</div>
+			<div @click="right_closeMenu(); right_xf()"><i class="el-icon-caret-right"></i>悬浮打开</div>
+			<div @click="right_closeMenu(); right_window_open()"><i class="el-icon-caret-right"></i>新窗口打开</div>
+			<div @click="right_closeMenu2();"><i class="el-icon-caret-right"></i>取消</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				rightShow: false,	// 右键菜单是否正在显示 
+				rightTab: null,		// 右键菜单正在操作的 tab 
+				rightStyle: {		// 右键菜单的 style 样式 
+					left: '0px',		// 坐标x 
+					top: '0px',			// 坐标y 
+					maxHeight: '0px'	// 右键菜单的最高高度 (控制是否展开) 
+				},
+			}
+		},
+		methods: {
+			// 展开右键菜单
+			right_showMenu: function(tab, event) {
+				this.rightTab = tab;	// 绑定操作tab  
+				var e = event || window.event;
+				this.rightStyle.left = (e.clientX + 1) + 'px';	// 设置给坐标x
+				this.rightStyle.top = e.clientY + 'px';		// 设置给坐标y
+				this.rightShow = true;	// 显示右键菜单 
+				this.$nextTick(function() {
+					var foxHeight = document.querySelector('.right-box-2').offsetHeight;	// 应该展开多高 
+					this.rightStyle.maxHeight = foxHeight + 'px';	// 展开 
+					document.querySelector('.right-box').focus();		// 获得焦点,以被捕获失去焦点事件
+				});
+			},
+			// 关闭右键菜单 - 立即关闭
+			right_closeMenu: function() {
+				this.rightStyle.maxHeight = '0px';	
+				this.rightShow = false;
+			},
+			// 关闭右键菜单 - 带动画折叠关闭 (失去焦点和点击取消时调用, 为什么不全部调用这个? 因为其它时候调用这个都太卡了) 
+			right_closeMenu2: function() {
+				this.rightStyle.maxHeight = '0px';	
+				// this.rightShow = false;
+			},
+			// 右键 - 刷新
+			right_f5: function() {
+				this.$root.showTab(this.rightTab);	// 先转到 
+				this.$root.f5Tab(this.rightTab);
+			},
+			// 右键 - 复制
+			right_copy: function() {
+				this.$root.showTab({name: this.rightTab.name, url: this.$root.getTabUrl(this.rightTab)});
+			},
+			// 右键 - 悬浮 
+			right_xf: function() {
+				this.$root.closeTab(this.rightTab);   
+				this.$root.xfTab(this.rightTab);
+			},
+			// 右键 - 新窗口打开
+			right_window_open: function() {
+				// this.$root.closeTab(this.rightTab); 
+				this.$root.newWinTab(this.rightTab); 
+			},
+			// 右键 - 关闭 
+			right_close: function() {
+				if(this.rightTab == this.$root.homeTab){
+					return this.$message({
+						dangerouslyUseHTMLString: true,
+						message: '<b>这个不能关闭哦</b>',
+						type: 'warning',
+						showClose: true,
+					});
+				}
+				this.$root.closeTab(this.rightTab);
+			},
+			// 右键 - 关闭其它 
+			right_close_other: function() {
+				var root = this.$root;
+				// 先滑到最左边 
+				root.$refs['nav-tab-bar'].scrollX = 0;	
+				// 递归删除 
+				var i = 0;
+				var deleteFn = function() {
+					// 如果已经遍历全部 
+					if(i >= root.tabList.length) {
+						return;
+					}
+					// 如果在白名单,i++继续遍历, 如果不是,递归删除 
+					var tab = root.tabList[i];
+					if(tab == root.homeTab || tab == this.rightTab){	
+						i++;
+						deleteFn();
+					} else {
+						root.closeTab(tab, function() {
+							deleteFn();
+						});
+					}
+				}.bind(this);
+				deleteFn();
+			},
+			// 右键 - 关闭所有 
+			right_close_all: function() {
+				var root = this.$root;
+				// 先滑到最左边 
+				root.$refs['nav-tab-bar'].scrollX = 0;	
+				// 递归删除 
+				var i = 0;
+				var deleteFn = function() {
+					// 如果已经遍历全部 
+					if(i >= root.tabList.length) {
+						return;
+					}
+					// 如果在白名单,i++继续遍历, 如果不是,递归删除 
+					var tab = root.tabList[i];
+					if(tab == root.homeTab){	
+						i++;
+						deleteFn();
+					} else {
+						root.closeTab(tab, function() {
+							deleteFn();
+						});
+					}
+				}.bind(this);
+				deleteFn();
+			},
+			
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+	
+	/* 右键菜单 样式 */
+	.right-box {
+		position: fixed;
+		z-index: 2147483647;
+		transition: max-height 0.2s;
+		outline:none;
+		max-height: 0px;
+		overflow: hidden;
+		box-shadow: 1px 1px 2px #000;
+	}
+	.right-box-2{font-size: 0.8em; padding: 0.5em 0; border: 1px #aaa solid; border-radius: 1px; background-color: #FFF;}
+	.right-box-2>div {line-height: 2.2em; padding-left: 0.7em; padding-right: 1.8em; cursor: pointer; white-space: nowrap;}
+	.right-box-2>div:hover {background-color: #ddd;color: #2D8CF0;}
+	.right-box-2>div i{ margin-right: 8px;}
+</style>

+ 37 - 0
sa-frame/nav/nav-logo.vue

@@ -0,0 +1,37 @@
+<!-- 左上:logo部分 -->
+<template>
+	<div class="com-logo-box" :title="$root.title" @click="$root.showHome()">
+		<img :src="$root.logo" class="admin-logo" v-if="$root.logo">
+		<span class="admin-title">{{$root.title}}</span>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+			}
+		},
+		methods: {
+		},
+		created() {
+		}
+	}
+</script>
+
+<style scoped>
+	.com-logo-box {
+		cursor: pointer;
+	}
+
+	.admin-logo {
+		width: 40px;
+		height: 40px;
+		border-radius: 50%;
+		vertical-align: middle;
+		margin-left: 22px;
+	}
+	
+	.admin-title{padding-right: 0.5em; margin-left: 0.5em; font-size: 1.05em;}
+	
+</style>

+ 169 - 0
sa-frame/nav/nav-menu-bar.vue

@@ -0,0 +1,169 @@
+<template>
+  <!-- 左下:菜单栏 -->
+  <div class="menu-box-1">
+    <div class="menu-box-2">
+      <!--
+        菜单:
+          unique-opened = 是否只有菜单打开
+          default-active = 正在高亮的菜单id
+          collapse = 是否折叠
+          参考文档:https://element.eleme.cn/#/zh-CN/component/menu
+      -->
+      <el-menu
+          class="el-menu-style-1"
+          :default-active="$root.activeMenuId"
+          :collapse="!$root.isOpen"
+          @select="selectMenu"
+      >
+        <div v-for="(menu, index) in $root.menuList" v-if="!menu.show" :key="index">
+          <!-- 1 如果是子菜单 -->
+          <el-menu-item v-if="!menu.parent && menu.isShow !== false && $root.showList.indexOf(menu.id) > -1"
+                        :index="menu.id + '' ">
+            <span class="menu-i"><i :class="menu.icon" :title="menu.name"></i></span>
+            <span class="menu-name">{{ menu.name }}</span>
+          </el-menu-item>
+          <!-- 1 如果是父菜单 -->
+          <el-submenu v-if="menu.parent && menu.isShow !== false && $root.showList.indexOf(menu.id) > -1"
+                      :index="menu.id + '' ">
+            <template slot="title">
+              <span class="menu-i"><i :class="menu.icon" :title="menu.name"></i></span>
+              <span class="menu-name">{{ menu.name }}</span>
+            </template>
+            <!-- 遍历其子项 -->
+            <div v-for="(menu2, index) in menu.childList" :key="index">
+              <!-- 2 如果是子菜单 -->
+              <el-menu-item v-if="!menu2.parent && menu2.isShow !== false && $root.showList.indexOf(menu2.id) > -1"
+                            :index="menu2.id + '' ">
+                <span class="menu-i"><i :class="menu2.icon" :title="menu2.name"></i></span>
+                <span class="menu-name">{{ menu2.name }}</span>
+              </el-menu-item>
+              <!-- 2 如果是父菜单 -->
+              <el-submenu v-if="menu2.parent && menu2.isShow !== false && $root.showList.indexOf(menu2.id) > -1"
+                          :index="menu2.id + '' ">
+                <template slot="title">
+                  <span class="menu-i"><i :class="menu2.icon" :title="menu2.name"></i></span>
+                  <span class="menu-name">{{ menu2.name }}</span>
+                </template>
+                <!-- 遍历其子项 -->
+                <div v-for="(menu3, index) in menu2.childList" :key="index">
+                  <!-- 3 如果是子菜单 -->
+                  <el-menu-item v-if="!menu3.parent && menu3.isShow !== false && $root.showList.indexOf(menu3.id) > -1"
+                                :index="menu3.id + '' ">
+                    <span class="menu-i"><i :class="menu3.icon" :title="menu3.name"></i></span>
+                    <span class="menu-name">{{ menu3.name }}</span>
+                  </el-menu-item>
+                  <!-- 3 如果是父菜单 -->
+                  <el-submenu v-if="menu3.parent && menu3.isShow !== false && $root.showList.indexOf(menu3.id) > -1"
+                              :index="menu3.id + '' ">
+                    <template slot="title">
+                      <span class="menu-i"><i :class="menu3.icon" :title="menu3.name"></i></span>
+                      <span class="menu-name">{{ menu3.name }}</span>
+                    </template>
+                    <!-- 4 -->
+                    <div v-for="(menu4, index) in menu3.childList" :key="index">
+                      <el-menu-item v-if="menu4.isShow !== false && $root.showList.indexOf(menu4.id) > -1"
+                                    :index="menu4.id + '' ">
+                        <span class="menu-i"><i :class="menu4.icon" :title="menu4.name"></i></span>
+                        <span class="menu-name">{{ menu4.name }}</span>
+                      </el-menu-item>
+                    </div>
+                  </el-submenu>
+                </div>
+              </el-submenu>
+            </div>
+          </el-submenu>
+        </div>
+      </el-menu>
+      <!-- tab被拖拽时的遮罩(左拖拽:关闭) -->
+      <div class="shade-fox" v-if="$root.isDrag"
+           @dragover="$event.preventDefault();"
+           @drop="$event.preventDefault(); $event.stopPropagation(); $root.$refs['com-right-menu'].rightTab = $root.dragTab; $root.$refs['com-right-menu'].right_close();">
+        <span style="font-size: 16px;">关闭</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+module.exports = {
+  data() {
+    return {}
+  },
+  methods: {
+    // 点击子菜单时触发的回调
+    // 参数:index=点击菜单index标识(不是下标,是菜单id),
+    // 		indexArray=所有已经打开的菜单id数组,形如:['1', '1-1', '1-1-1']
+    selectMenu: function (index, indexArray) {
+      this.$root.showMenuById(index);
+    },
+  },
+  created() {
+  }
+}
+</script>
+
+<style scoped>
+/* 1 2 配合,把滚动条隐藏 */
+.menu-box-1 {
+  width: calc(var(--nav-left-width) + 20px);
+  height: 100%;
+  overflow-y: auto;
+}
+
+.menu-box-2 {
+  width: calc(var(--nav-left-width) + 1px);
+  padding-bottom: 200px;
+}
+
+.menu-box-1 i[class^=el-icon-] {
+  font-size: 16px;
+}
+
+.menu-box-2 .menu-i {
+  display: inline-block;
+  vertical-align: top;
+  width: 29px;
+}
+
+/* 动画速度加快 */
+.menu-box-1, .menu-box-2 * {
+  transition: all 0.2s;
+}
+
+/* 隐藏右边框 */
+.el-menu {
+  border: 0px;
+}
+
+/* 一级菜单,高度45px */
+.el-menu-item,
+.el-submenu__title {
+  height: 45px !important;
+  line-height: 45px !important;
+}
+
+/* 二级以下菜单,高度40px */
+.el-submenu .el-menu-item,
+.el-submenu .el-submenu .el-submenu__title {
+  height: 40px !important;
+  line-height: 40px !important;
+}
+
+/* 二级菜单 左边距 */
+.el-submenu .el-menu-item,
+.el-submenu .el-submenu .el-submenu__title {
+  padding-left: 2.5em !important;
+}
+
+/* 三级菜单 左边距 */
+.el-submenu .el-submenu .el-menu-item,
+.el-submenu .el-submenu .el-submenu .el-submenu__title {
+  padding-left: 3.6em !important;
+}
+
+/* 四级菜单 左边距 */
+.el-submenu .el-submenu .el-submenu .el-menu-item {
+  padding-left: 4.7em !important;
+}
+
+</style>

+ 158 - 0
sa-frame/nav/nav-tab-bar.vue

@@ -0,0 +1,158 @@
+<template>
+	<!-- 右边,第二行:tab栏 -->
+	<div class="towards-box">
+		<div class="towards-left" @click="scrollToLeft()" title="向左滑">
+			<i class="el-icon-arrow-left"></i>
+		</div>
+		<div class="towards-middle" @dblclick="$root.$refs['com-add-tab'].atOpen()" @drop="$event.preventDefault(); $event.stopPropagation();">
+			
+			<div class="tab-title-box" :style="{left: scrollX + 'px'}" @dblclick.stop="">
+				<vuedraggable v-model="$root.tabList" chosen-class="chosen-tab" animation="500" >
+			    	<div 
+			    		v-for="tab in $root.tabList" 
+			    		:key="tab.id"
+			    		:id=" 'tab-' + tab.id " 
+			    		class="tab-title" 
+			    		:class=" (tab == $root.nativeTab ? 'tab-native' : '') " 
+			    		@click="$root.showTab(tab)"
+			    		@contextmenu.prevent="$root.$refs['com-right-menu'].right_showMenu(tab, $event)"
+			    		draggable="true"
+			    		@dragstart="$root.isDrag = true; $root.dragTab = tab"
+			    		@dragend="$root.isDrag = false;"
+			    		>
+			    		<div class="tab-title-2">
+			    			<!-- <i class="el-icon-caret-right"></i> -->
+			    			<span>{{tab.name}}</span>
+			    			<i class="el-icon-close" v-if="!tab.hideClose" @click.stop="$root.closeTab(tab)"></i> 
+			    		</div>
+			    	</div>
+				</vuedraggable>
+			</div>
+			
+			
+		</div>
+		<div class="towards-right" @click="scrollToRight()" title="向右滑">
+			<i class="el-icon-arrow-right"></i>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		components: {
+			"vuedraggable": window.vuedraggable,	// vuedraggable 
+		},
+		data() {
+			return {
+				scrollX: 0		,// 滚动条位置 
+			}
+		},
+		methods: {
+			// ------------------- tab左右滑动  -------------------- 
+			// 视角向左滑动一段距离 
+			scrollToLeft: function(scroll_width) {
+				var width = document.querySelector('.nav-right-2').clientWidth;	// 视角宽度
+				this.scrollX += scroll_width || width / 8;	// 视角向左滑动一段距离
+				// 越界检查
+				setTimeout(function() {
+					if(this.scrollX > 0){
+						this.scrollX = 0;
+					}
+				}.bind(this), 200);
+			},
+			// 视角向右滑动一段距离 
+			scrollToRight: function(scroll_width) {
+				var width = document.querySelector('.nav-right-2').clientWidth;	// 视角宽度
+				var tabListWidth = document.querySelector('.tab-title-box').clientWidth;	// title总盒子宽度
+				var rightLimit = (0 - tabListWidth + width / 2);	// 右滑的极限
+				this.scrollX -= scroll_width || width / 8;		// 视角向右滑动一段距离
+				// 越界检查
+				setTimeout(function() {
+					if(this.scrollX < rightLimit){
+						this.scrollX = rightLimit;
+					}
+					// 同时防止左边越界 
+					if(this.scrollX > 0){
+						this.scrollX = 0;
+					}
+				}.bind(this), 200);
+			},
+			// 自动归位
+			scrollToAuto: function() {
+				// console.log('自动归位=========');
+				try{
+					// 最后一个不用归位了 
+					// if(this.nativeTab == this.tabList[this.tabList.length - 1]){
+					// 	return;
+					// }
+					var width = document.querySelector('.nav-right-2').clientWidth;	// 视角宽度
+					var left = document.querySelector('.tab-native').lastChild.offsetLeft;	// 当前native-tilte下一个距离左边的距离
+					// console.log(width, left, this.scrollX);
+					// 如果在视图右边越界
+					if(left + this.scrollX > (width - 200)){
+						return this.scrollToRight();
+					}
+					// 如果在视图左边越界 
+					if(left + this.scrollX < 0) {
+						return this.scrollToLeft();
+					}
+				}catch(e){
+					// throw e;
+				}
+			},
+			// 让鼠标滚轮变为横向滚动
+			initScroll: function() {
+				var scroll_width = 60;  // 设置每次滚动的长度,单位 px
+				var scroll_events = "mousewheel DOMMouseScroll MozMousePixelScroll";  // 鼠标滚轮滚动事件名
+				$('.towards-middle').on(scroll_events, function(e) {
+					var delta = e.originalEvent.wheelDelta;  // 鼠标滚轮滚动度数
+					// 滑轮向上滚动,滚动条向左移动,scrollleft-
+					if(delta > 0) {
+						this.scrollToLeft(scroll_width);
+					}
+					// 滑轮向下滚动,滚动条向右移动,scrollleft+
+					else {
+						this.scrollToRight(scroll_width);
+					}
+				}.bind(this));
+			}
+		},
+		created() {
+			this.$nextTick(function() {
+				this.initScroll();
+			})
+		}
+	}
+</script>
+
+<style scoped>
+	
+	.towards-box>div{height: 100%; position: absolute;}
+	
+	.towards-left,.towards-right{width: 24px; text-align: center; background-color: #FFF; cursor: pointer;} 
+	.towards-left{border-right: 1px #fff solid;}
+	.towards-right{border-left: 1px #fff solid; right: 0px;}
+	.towards-left:hover i,.towards-right:hover i{font-weight: 700;/* font-weight: bold; */}
+	
+	.towards-middle{width: 10000px; overflow: auto;/* calc(100% - 50px) */ left: 25px;background-color: #FFF;}
+	.tab-title-box{display: inline-block; position: absolute; left: 0px; transition: all 0.2s;}
+	.tab-title{font-size: 12px; cursor: pointer; float: left; white-space: nowrap; overflow: hidden; text-decoration: none; color: #333;}
+	.tab-title-2{padding: 0px 10px; /* background-color: #FFF; */ }
+	.tab-title-2{transition: padding 0.1s, margin 0.1s;}
+	/* .tab-title .el-icon-caret-right{color: #EEE; font-size: 1.7em; position: relative; top: 4px;} */
+	.tab-title .el-icon-close{display: inline-block; border-radius: 50%; padding: 1px; color: #ccc; margin-left: -8px;}
+	.tab-title .el-icon-close:hover{background-color: red; color: #FFF;}
+	.tab-title span{display: inline-block; margin-left: 10px; margin-right: 10px;}
+	.tab-title:hover span,.tab-native span{/* font-weight: bold; */}
+	
+	
+	/* 卡片样式 */
+	/* .tab-title-box>div{line-height: 35px;} */
+	.tab-title{transition: width 0.2s, background 0s, border 0.2s;}
+	.tab-native{transition: width 0.2s, background 0.2s, border 0.2s;}
+	.tab-title{border-radius: 1.5px; border: 1px #e5e5e5 solid; line-height: 28px; height: 27px; margin: 3px 1.5px; background-color: #fff;}
+	/* .tab-title.tab-native{border: 1px #409EFF solid; background-color: #409EFF; color: #fff; }
+	.tab-title:hover{border: 1px #409EFF solid;} */
+	/* .chosen-tab .tab-title-2{background-color: red;} */
+	
+</style>

+ 326 - 0
sa-frame/nav/nav-tool-bar.vue

@@ -0,0 +1,326 @@
+<!-- 右边第一行,工具栏 -->
+<template>
+	<div class="tools-panel">
+		<div class="tools-left">
+			<span title="折叠菜单" class="tool-fox" v-if="$root.isOpen == true" @click="$root.endOpen()">
+				<i class="el-icon-s-fold"></i>
+			</span>
+			<span title="展开菜单" class="tool-fox" v-if="$root.isOpen == false" @click="$root.startOpen()">
+				<i class="el-icon-s-unfold"></i>
+			</span>
+			<span title="搜索-input" class="tool-fox search-fox" :class=" isSearch ? 'search-fox-show' : '' ">
+				<el-select v-model="searchText" size="mini" filterable placeholder="请输入菜单关键字" ref="search" 
+					@change="findMenuBySearch" @blur="closeSearch" @keyup.esc.native="closeSearch">
+					<el-option v-for="item in searchList" :key="item.id" :label="item.text" :value="item.id"></el-option>
+				</el-select>
+			</span>
+			<span title="搜索菜单" class="tool-fox" @click="closeSearch()" v-if="!isShowSearchInput">
+				<i class="el-icon-search" style="font-weight: bold;"></i>
+			</span>
+			<span title="搜索菜单" class="tool-fox" @click="startSearch()" v-else>
+				<i class="el-icon-search" style="font-weight: bold;"></i>
+			</span>
+			<span title="刷新" class="tool-fox" @click="$root.f5Tab($root.nativeTab)">
+				<i class="el-icon-refresh-right" style="font-weight: bold;"></i>
+			</span>
+			<span title="当前时间" class="tool-fox">
+				<span style="font-size: 0.90em;">{{nowTime}}</span>
+			</span>
+		</div>
+		<div class="tools-right">
+			<span title="点击登录" class="tool-fox" onclick="location.href='login.html'" v-if="$root.user == null">
+				<span style="font-size: 0.8em; font-weight: bold; position: relative; top: -2px;">未登录</span>
+			</span>
+			<span title="我的信息" class="tool-fox user-info" style="padding: 0;" v-if="$root.user != null">
+				<el-dropdown @command="handleCommand" trigger="click" size="medium">
+					<span class="el-dropdown-link user-name" style="height: 100%; padding: 0 1em; display: inline-block;">
+						<img :src="$root.user.avatar" class="user-avatar">
+						<span>{{$root.user.username}}</span>
+						<i class="el-icon-arrow-down el-icon--right"></i>
+					</span>
+					<el-dropdown-menu slot="dropdown">
+						<el-dropdown-item v-for="drop in $root.dropList" :command="drop.name" :key="drop.name">{{drop.name}}</el-dropdown-item>
+					</el-dropdown-menu>
+				</el-dropdown>
+			</span>
+			<span title="主题" class="tool-fox" style="padding: 0;">
+				<el-dropdown @command="toggleTheme" trigger="click" size="medium">
+					<span class="el-dropdown-link" style="height: 100%; padding: 0 1em; display: inline-block;">
+						<i class="el-icon-price-tag" style="font-weight: bold;"></i>
+						<span style="font-size: 0.9em;">主题</span>
+					</span>
+					<el-dropdown-menu slot="dropdown">
+						<el-dropdown-item :command="t.value" v-for="t in themeList" :key="t.name">
+							<span :style=" $root.themeV == t.value ? 'color: #44f' : '' ">{{t.name}} </span>
+						</el-dropdown-item>
+					</el-dropdown-menu>
+				</el-dropdown>
+			</span> 
+			<span title="便签" class="tool-fox" @click="openNote()">
+				<i class="el-icon-edit" style="font-weight: bold; font-size: 0.9em;"></i>
+				<span style="font-size: 0.9em;">便签</span>
+			</span>
+			<span title="全屏" class="tool-fox" v-if="isFullScreen == false" @click="fullScreen()">
+				<i class="el-icon-rank" style="font-weight: bold; transform: rotate(45deg)"></i>
+			</span>
+			<span title="退出全屏" class="tool-fox" v-if="isFullScreen == true" @click="outFullScreen()">
+				<i class="el-icon-bottom-left" style="font-weight: bold; "></i>
+			</span>
+		</div>
+		<!-- tab被拖拽时的遮罩(tab上拖拽:新窗口打开) -->
+		<div class="shade-fox" v-if="$root.isDrag" 
+			@dragover="$event.preventDefault();" 
+			@drop="$event.preventDefault(); $event.stopPropagation(); $root.newWinTab($root.dragTab);">
+			<span style="font-size: 16px;">新窗口打开</span>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				isSearch: false,	// 当前是否处于搜索模式 
+				isShowSearchInput: true,	// 是否显示打开搜索图标 
+				searchText: '',		// 搜索框已经输入的字符 
+				searchList: [],			// 搜索框 待选列表 
+				
+				isFullScreen: false,	// 是否处于全屏状态 
+				
+				nowTime: '加载中...'	,	// 当前时间 
+				currInterval: null,		// 刷新当前时间的定时器 
+				
+				themeList: [	// 主题数组
+					{name: '蓝色', value: '1'},
+					{name: '绿色', value: '2'},
+					{name: '白色', value: '3'},
+					{name: '灰色', value: '4'},
+					{name: '红色', value: '5'},
+					{name: '紫色', value: '9'},
+					{name: 'pro钛合金', value: '6'},
+					{name: '沉淀黑蓝', value: '7'},
+					{name: '简约灰色', value: '8'},
+					{name: '简约草绿', value: '10'},
+				],
+				
+			}
+		},
+		methods: {
+			// ------------------------------ 搜索相关 ------------------------------
+			// 开启搜索
+			startSearch: function() {
+				this.searchText = '';
+				this.isSearch = true;
+				this.f5SearchList();
+				setTimeout(function() {
+					this.isShowSearchInput = false;
+					this.$refs['search'].focus();	//.$refs['nav-tool-bar'].
+				}.bind(this), 200);
+			},
+			// 关闭搜索
+			closeSearch: function() {
+				this.searchText = '';
+				this.isSearch = false;
+				setTimeout(function() {
+					try{
+						this.isShowSearchInput = true;
+						document.querySelector('body>.el-select-dropdown.el-popper').style.display = 'none';
+					}catch(e){throw e}
+				}.bind(this), 200);
+			},
+			// 查找菜单 
+			findMenuBySearch: function(id) {
+				this.$root.showMenuById(id);
+				this.closeSearch();
+			},
+			// 刷新待选列表 
+			f5SearchList: function() {
+				var searchList = [];
+				
+				let index = 1;
+				function push(id, str) {
+					searchList.push({id: id, text: (index++) + ". " + str});
+				}
+				
+				// 遍历菜单 
+				let childList = this.$root.menuList;
+				let showList = this.$root.showList;
+				for (let menu1 of childList) {
+					if(menu1.isShow === false || showList.indexOf(menu1.id + '') == -1) continue;
+					if(menu1.childList) {
+						for (let menu2 of menu1.childList) {
+							if(menu2.isShow === false || showList.indexOf(menu2.id + '') == -1) continue;
+							if(menu2.childList) {
+								for (let menu3 of menu2.childList) {
+									if(menu3.isShow === false || showList.indexOf(menu3.id + '') == -1) continue;
+									if(menu3.childList) {
+										for (let menu4 of menu3.childList) {
+											if(menu4.isShow === false || showList.indexOf(menu4.id + '') == -1) continue;
+											push(menu4.id, menu1.name + ' > ' + menu2.name + ' > ' + menu3.name + ' > ' + menu4.name);
+										}
+									} else {
+										push(menu3.id, menu1.name + ' > ' + menu2.name + ' > ' + menu3.name);
+									}
+								}
+							} else {
+								push(menu2.id, menu1.name + ' > ' + menu2.name);
+							}
+						}
+					} else {
+						push(menu1.id, menu1.name);
+					}
+				}
+				
+				this.searchList = searchList;
+			},
+			
+			// ------------------------------ 主题 ------------------------------
+			// 切换主题
+			toggleTheme: function(command) {
+				// 开始切换
+				this.$root.themeV = command + "";
+				localStorage.setItem('themeV', command);
+				for (var i = 0; i < this.themeList.length; i++) {
+					if(this.themeList[i].value + '' == command + '') {
+						this.$message('切换成功,' + this.themeList[i].name);
+					}
+				}
+			},
+			
+			// ------------------------------ 全屏 ------------------------------
+			// 进入全屏 
+			fullScreen: function() {
+				this.isFullScreen = true;
+				if(document.documentElement.RequestFullScreen){
+					document.documentElement.RequestFullScreen();
+				}
+				//兼容火狐
+				if(document.documentElement.mozRequestFullScreen){
+					document.documentElement.mozRequestFullScreen();
+				}
+				//兼容谷歌等可以webkitRequestFullScreen也可以webkitRequestFullscreen
+				if(document.documentElement.webkitRequestFullScreen){
+					document.documentElement.webkitRequestFullScreen();
+				}
+				//兼容IE,只能写msRequestFullscreen
+				if(document.documentElement.msRequestFullscreen){
+					document.documentElement.msRequestFullscreen();
+				}
+			},
+			// 退出全屏
+			outFullScreen: function() {
+				this.isFullScreen = false;
+				if(document.exitFullScreen){
+					document.exitFullscreen()
+				}
+				//兼容火狐
+				if(document.mozCancelFullScreen){
+					document.mozCancelFullScreen()
+				}
+				//兼容谷歌等
+				if(document.webkitExitFullscreen){
+					document.webkitExitFullscreen()
+				}
+				//兼容IE
+				if(document.msExitFullscreen){
+					document.msExitFullscreen()
+				}
+			},
+			
+			// ------------------------------ 其它 ------------------------------
+			// 处理userinfo的下拉点击
+			handleCommand: function(command) {
+				this.$root.dropList.forEach(function(drop) {
+					if(drop.name == command) {
+						drop.click();
+					}
+				})
+			},
+			// 打开便签
+			openNote: function() {
+				var w = (document.body.clientWidth * 0.4) + 'px';
+				var h = (document.body.clientHeight * 0.6) + 'px';
+				var default_content = '一个简单的小便签, 关闭浏览器后再次打开仍然可以加载到上一次的记录, 你可以用它来记录一些临时资料';
+				var value = localStorage.getItem('sa_admin_note') || default_content;
+				var index = layer.prompt({
+					title: '一个小便签', 
+					value: value,
+					formType: 2,
+					area: [w, h],
+					btn: ['保存'],
+					maxlength: 99999999,
+					skin: 'layer-note-class' 
+				}, function(pass, index){
+					layer.close(index)					
+				});
+				var se = '#layui-layer' + index + ' .layui-layer-input';
+				var d = document.querySelector(se);
+				d.oninput = function() {
+					localStorage.setItem('sa_admin_note', this.value);
+				}
+			},
+			
+			// 刷新时间
+			initInterval: function() {
+				if(this.currInterval) {
+					clearInterval(this.currInterval);
+				}
+				// 一直更新时间
+				this.currInterval = setInterval(function() {
+					var da = new Date();
+					var Y = da.getFullYear(); //年
+					var M = da.getMonth() + 1; //月
+					var D = da.getDate(); //日
+					var h = da.getHours(); //小时
+					var sx = "凌晨";
+					if (h >= 6) {
+						sx = "上午"
+					}
+					if (h >= 12) {
+						sx = "下午";
+						if (h >= 18) {
+							sx = "晚上";
+						}
+						h -= 12;
+					}
+					var m = da.getMinutes(); //分
+					var s = da.getSeconds(); //秒
+					var z = ['日', '一', '二', '三', '四', '五', '六'][da.getDay()] ; //周几
+					// z = z == 0 ? '日' : z;
+					var zong = "";
+				
+					zong += Y + "-" + M + "-" + D + " " + sx + " " + h + ":" + m + ":" + s + " 周" + z;
+					this.nowTime = zong;
+				}.bind(this), 1000);
+			}
+		
+		},
+		created() {
+			this.initInterval();
+		}
+	}
+</script>
+
+<style scoped>
+	
+	.tools-left{border: 0px #000 solid; float: left;}
+	.tools-right{float: right;}
+	.tool-fox{padding: 0 1em; display: inline-block; cursor: pointer;}
+	.tool-fox, .tool-fox i{transition: all 0.2s;}
+	
+	.user-info{position: relative; top: -2px;}
+	.user-avatar{width: 30px; height: 30px; border-radius: 50%; vertical-align: middle;}
+	.user-info .user-name{font-size: 0.9em;} 
+	
+	/* 搜素框 */
+	.search-fox{display: inline-block; vertical-align: middle; overflow: hidden; max-width: 0px; padding: 0em 0em; margin-left: -5px; transition: all 0.2s;}
+	.search-fox-show{display: inline-block; max-width: 500px; margin-left: 0px; padding: 0 1em;}
+	.search-fox:hover{background-color: rgba(0,0,0,0) !important;}
+	.search-fox .el-input__inner{border-radius: 0px; border-width: 0px; border-bottom-width: 1px; background-color: rgba(0,0,0,0);}
+	.search-fox .el-input__icon{display: none;}
+	
+	/*800之下*/
+	@media(max-width: 800px) {
+		.tools-right{display: none;}
+	}
+</style>

+ 57 - 0
sa-frame/nav/nav-view-vessel.vue

@@ -0,0 +1,57 @@
+<template>
+	<div class="view-vessel">
+		<div class="a-view" v-for="tab in $root.viewList" :key="tab.id" :class="tab == $root.nativeTab ? 'a-view-native' : null">
+			<!-- vue视图 -->
+			<template v-if="tab.view">
+				<component :is="tab.view" class="vue-com-view" v-if="tab.isNeedLoad"></component>
+			</template>
+			<!-- iframe视图 -->
+			<template v-else>
+				<iframe :src="tab.url" :id=" 'iframe-' + tab.id " v-if="tab.isNeedLoad" @load="onloadIframe(tab.id)"></iframe>
+			</template>
+		</div>
+		<!-- tab被拖拽时的遮罩(下托拽:悬浮打开) -->
+		<div class="shade-fox" v-if="$root.isDrag" 
+			@dragover="$event.preventDefault();" 
+			@drop="$event.preventDefault(); $event.stopPropagation(); $root.xfTab($root.dragTab); $root.closeTab($root.dragTab);">
+			<span style="font-size: 24px;">拖拽至此:悬浮打开</span>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			// iframe加载完毕后清除其背景 loading 图标
+			onloadIframe: function(iframeId) {
+				// console.log('iframeId', iframeId);
+				var iframe = document.querySelector('#iframe-' + iframeId);
+				if(iframe != null) {
+					iframe.parentElement.style.backgroundImage='none';
+				}
+			},
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+	
+	.view-vessel{height: 100%; position: relative; border: 0px #000 solid;}
+	.a-view{width: 100%; height: 100%; background-color: #EEE; background: url(../index/admin-loading.gif) no-repeat center 50%; position: absolute; }
+	.a-view{opacity: 0; transition: all 0.2s;}
+	.a-view-native{z-index: 100000; opacity: 1;}
+	
+	.a-view>iframe{width: 100%; height: 100%; border: 0px #000 solid;}
+	.a-view>.vue-com-view{width: 100%; height: 100%; overflow: auto; background-color: #EEE;}
+	
+	/* .iframe-no-scroll{width: calc(100% + 22px); } */
+	
+</style>

+ 134 - 0
sa-frame/sa-code.js

@@ -0,0 +1,134 @@
+// 在使用时,不建议你直接魔改模板的代码,以免在运行时出现意外bug,而是在本文件中根据模板的提供的API,来适应你的业务逻辑 
+// sa-plus 快速开发平台:		http://sa-plus.dev33.cn
+// ....
+
+
+
+// ================================= 示例:一些基本信息 ================================= 
+
+// 设置模板标题 
+// sa_admin.title = "Sa-Admin";
+// sa_admin.logo = 'sa-frame/admin-logo.png';    // 设置logo图标地址   
+// sa_admin.icon = 'sa-frame/admin-logo.png';    // 设置icon图标地址 
+
+
+// ================================= 用户信息 和 菜单 =================================
+sa.ajax('/AccAdmin/fristOpenAdmin', function(res) {
+
+	// 验证权限 
+	// if(!(res.data.admin && res.data.per_list.indexOf('99') > -1)) {
+	// 	return sa.alert('当前账号暂无进入后台权限');
+	// }	
+	
+	// 配置 
+	sa_admin.title = "sa-plus 后台";
+	sa_admin.logo = 'sa-frame/admin-logo.png';    // 设置logo图标地址 
+	sa_admin.icon = "sa-frame/admin-logo.png";    // 设置logo图标地址 
+	
+	
+	// 当前用户信息 
+	sa_admin.user = {
+		username: res.data.admin.name,
+		avatar: !!res.data.admin.avatar ? res.data.admin.avatar : 'sa-frame/admin-logo.png' // 使用logo作为头像 
+		// avatar: res.data.admin.avatar // 此写法为账号头像 
+	};		
+	sa.$sys.setCurrUser(res.data.admin);
+	
+	
+	// 所有菜单
+	// var myMenuList = window.menuList;    // window.menuList 在 menu-list.js 中定义 
+	sa_admin.initMenu(res.data.per_list);    // 初始化菜单   
+	sa.setAuth(res.data.per_list);		// 当前用户权限码集合  
+	
+	// 配置信息 
+	sa.$sys.setAppCfg(res.app_cfg);
+	
+	// 初始化模板(必须调用) 
+	sa_admin.init();	
+	
+}.bind(this), {msg: '正在加载登录信息', login_url: 'login.html'});
+
+
+
+
+// ================================= 示例:设置登录后的头像处,下拉可以出现的选项  =================================
+sa_admin.dropList = [		// 头像点击处可操作的选项
+	{
+		name: '我的资料',
+		click: function() {
+			sa.showIframe('我的资料', 'sa-view-sp/sp-admin/admin-info.html', '700px', '600px');
+		}
+	},
+    {
+        name: '修改名称',
+        click: function () {
+			layer.prompt({title: '请输入新名称'}, function(pass, index){
+				layer.close(index);
+				sa.ajax('/admin/updateInfo', {name: pass}, function(res){
+					sa_admin.user.username = pass;
+					sa.ok2('修改成功');
+				});
+			});
+        }
+	},
+    {
+        name: '修改密码',
+        click: function () {
+			sa.showIframe('修改密码', 'sa-view-sp/sp-admin/update-password.html', '550px', '350px');
+        }
+	},
+    {
+        name: '切换账号',
+        click: function () {
+			// sa.showIframe('切换账号', 'login.html', '70%', '80%');
+			sa.$page.openLogin('login.html');
+        }
+	},
+	{
+		name: '退出登录',
+		click: function() {
+			layer.confirm('退出登录?', function() {
+				sa.ajax('/AccAdmin/doExit', function(res) {
+					layer.alert('注销成功', function() {
+						location.href="login.html";
+					})
+				})
+			});
+		}
+	}
+]
+
+
+// ================================= 示例:js控制打开某个菜单 =================================
+
+// 显示主页选项卡 
+// sa_admin.showHome();
+
+// 显示一个选项卡, 根据id
+// sa_admin.showTabById('1-1');
+
+// 关闭一个选项卡,根据 id 
+// sa_admin.closeTabById('1-1');
+
+// 新增一个选项卡
+// sa_admin.addTab({id: 12345, name: '新页面', url: 'http://web.yanzhi21.com'});	// id不要和已有的菜单id冲突,其它属性均可参照菜单项 
+
+// 新增一个选项卡、并立即显示  
+// sa_admin.showTab({id: 12345, name: '新页面', url: 'http://web.yanzhi21.com'});	// 参数同上 
+
+// 打开一个 菜单,根据 id
+// sa_admin.showMenuById('1-1');	
+
+
+
+// ================================= 示例:调用另一个页面的代码 =================================
+// var win = sa_admin.getTabWindow('2-1');		// 根据id获取其页面的window对象   (如果此页面未打开,则返回空)(跨域模式下无法获取其window对象)
+// win.app.f5();
+
+// 注意:
+// 根据`iframe`的子父通信原则,在子页面中调用父页面的方法,需要加上parent前缀,例如:
+// parent.sa_admin.msg('啦啦啦');		// 调用父页面的弹窗方法 
+
+
+
+

+ 108 - 0
sa-view-sp/console/com-chart-1.vue

@@ -0,0 +1,108 @@
+<!-- 统计图1 -->
+<template>
+	<div class="echarts-div" id='bar-chart' ref="bar-chart"></div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			// 刷新柱状图
+			f5BarChart: function() {
+				// ===========================================  定义数据 
+				var x_name = '';	// new Date().getFullYear() + "年"; // x轴名称
+				var y_name = "注册数量"; // y轴名称
+				var dataArray = []; // 坐标X轴数据
+				var valueArray = []; //  坐标Y轴数据
+			
+				var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+				for (var i in arr) {
+					i = parseInt(i) + 1;
+					dataArray.push(i + '月');
+					if (i < 10) {
+						i = "0" + i;
+					}
+					i = i + "";
+					valueArray.push(sa.randomNum(100, 1000) || 0);
+				}
+				
+				// ===========================================  开始渲染
+			
+				var ele = this.$refs['bar-chart'];
+				var myChart = echarts.init(ele);
+				var option = {
+					tooltip: {
+						trigger: 'axis',
+						formatter: '{b}<br/> ' + y_name + ':{c}',
+						axisPointer: {
+							type: 'shadow'
+						}
+					},
+					grid:{x: 50, y: 30, x2: 25, y2: 25},	//设置canvas内部表格的内距
+					toolbox: {
+						show: true,
+						top: 0,
+						feature: {
+							saveAsImage: {
+								show: true
+							}
+						}
+					},
+					xAxis: {
+						name: x_name,
+						type: 'category',
+						// axisLabel: {
+						// 	'interval': 0
+						// }, //强制不缩略x轴刻度,
+						data: dataArray
+					},
+					yAxis: {
+						name: y_name,
+						type: 'value'
+					},
+					series: [{
+						name: y_name,
+						data: valueArray,
+						type: 'bar',
+						label: {
+							normal: {
+								show: true,
+								position: 'top',
+								formatter: '{c}'
+							}
+						},
+						itemStyle: {
+							normal: {
+								color: '#5DB1FF',
+								label: {
+									show: true,
+									textStyle: {
+										color: 'black'
+									}
+								}
+							}
+						}
+					}]
+				};
+				myChart.setOption(option);
+				window.myChartList.push(myChart);
+				// window.myChartList[0] = myChart;
+				// myChartList[1] = myChart;
+			},
+		},
+		created() {
+			// 刷新所有图标数据
+			this.$nextTick(function() {
+				this.f5BarChart();
+			});
+		}
+	}
+</script>
+
+<style scoped>
+	
+</style>

+ 114 - 0
sa-view-sp/console/com-chart-2.vue

@@ -0,0 +1,114 @@
+<!-- 统计图2 -->
+<template>
+	<div class="echarts-div" id='pic-chart' ref='pic-chart'></div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+			}
+		},
+		methods: {
+			// 刷新饼图
+			f5PieChart: function() {
+				// ===========================================  定义数据
+				var dataArray = [
+					{name: '昵称注册', value: sa.randomNum(100, 1000)},
+					{name: '手机号注册', value: sa.randomNum(100, 1000)},
+					{name: '微信登陆', value: sa.randomNum(100, 1000)},
+					{name: 'QQ登陆', value: sa.randomNum(100, 1000)},
+					{name: '邮箱登录', value: sa.randomNum(100, 1000)},
+					{name: '小程序登录', value: sa.randomNum(100, 1000)},
+					{name: '管理员添加', value: sa.randomNum(100, 1000)},
+				]; // 坐标X轴数据
+			
+				// ===========================================  开始渲染
+			
+				var myChart = echarts.init(document.getElementById('pic-chart'));
+				option = {
+					title: {
+						text: '账号来源',
+						left: 'left',
+						top: 0,
+						textStyle: {
+							color: '#666',
+							fontSize: '14'
+						}
+					},
+					toolbox: {
+						show: true,
+						top: 0,
+						feature: {
+							saveAsImage: {
+								show: true
+							}
+						}
+					},
+					tooltip: {
+						trigger: 'item',
+						formatter: "{a} <br/>{b} : {c} ({d}%)"
+					},
+					series: [{
+						name: '账号来源',
+						type: 'pie',
+						radius: '70%', // 半径大小
+						center: ['50%', '60%'],
+						selectedMode: 'single',
+						roseType: 'radius',
+						data: dataArray.sort(function(a, b) {
+							return a.value - b.value;
+						}),
+						//roseType: 'radius', // 半径模式还是面积模式
+						itemStyle: {
+							normal: {
+								color: function(params) {
+									// build a color map as your need.
+									var colorList = [
+										'#ff7f50','#87cefa','#da70d6','#32cd32','#6495ed',
+										'#ff69b4','#ba55d3','#cd5c5c','#ffa500','#40e0d0',
+										'#1e90ff','#ff6347','#7b68ee','#00fa9a',
+										'#6699FF','#ff6666','#3cb371','#b8860b','#30e0e0'
+									];
+									// '#ffd700',
+									function GetRandomNum(Min, Max) {
+										var Range = Max - Min;
+										var Rand = Math.random();
+										return (Min + Math.round(Rand * Range));
+									}
+									var index = GetRandomNum(0, colorList.length - 1);
+									return colorList[index];
+									//return colorList[params.dataIndex]
+								}
+							}
+						},
+						label: {
+							normal: {
+								formatter: '{b|{b}:}{c}  {per|{d}%}  ',
+								rich: {}
+							}
+						},
+						// 弹出动画 
+						animationType: 'scale',
+						animationEasing: 'elasticOut',
+						animationDelay: function (idx) {
+							return Math.random() * 200;
+						}
+					}]
+				};
+				myChart.setOption(option);
+				window.myChartList.push(myChart);
+			},
+		},
+		created() {
+			// 刷新所有图标数据
+			this.$nextTick(function() {
+				this.f5PieChart();
+			});
+		}
+	}
+</script>
+
+<style scoped>
+	
+</style>

+ 113 - 0
sa-view-sp/console/com-chart-3.vue

@@ -0,0 +1,113 @@
+<!-- 统计图 3 -->
+<template>
+	<div class="echarts-div" id='line-chart' ref='line-chart'></div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			// 刷新折线图
+			f5LineChart: function() {
+				// ===========================================  定义数据
+				var x_name = '';	// "活跃数据"; // x轴名称
+				var y_name = "活跃数据"; // y轴名称
+				var typeArray = ['总计登录', '新增注册'];
+				var dataArray = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];	//   坐标X轴数据
+				var valueArray0 = [84, 126, 262, 201, 148, 133, 86, 186, 232, 215, 326, 412];	// 	
+				var valueArray1 = [284, 296, 382, 501, 348, 273, 266, 327, 412, 515, 526, 712];	// 	
+			
+				// ===========================================  开始渲染
+			
+				var myChart = echarts.init(this.$refs['line-chart']);
+				var option = {
+					tooltip: {
+						trigger: 'axis',
+						axisPointer: {
+							type: 'cross',
+							label: {
+								backgroundColor: '#6a7985'
+							}
+						}
+					},
+					toolbox: {
+						show: true,
+						top: 0,
+						feature: {
+							saveAsImage: {
+								show: true
+							}
+						}
+					},
+					grid:{x: 50, y: 30, x2: 25, y2: 25},	//设置canvas内部表格的内距
+					legend: {
+						data: typeArray
+					},
+					xAxis: {
+						name: x_name,
+						type: 'category',
+						boundaryGap : false,
+						// axisLabel: {
+						// 	'interval': 0
+						// }, //强制不缩略x轴刻度,
+						data: dataArray
+					},
+					yAxis: {
+						name: y_name,
+						type: 'value'
+					},
+					series: [
+						{
+							name: '总计登录',
+							type:'line',
+							data: valueArray1,
+							smooth: true,	// 曲线形式
+							areaStyle: {
+								normal: {
+									color: 'rgba(0, 128, 0, 0.3)' //改变区域颜色
+								}
+							},
+							itemStyle: {
+								normal: {
+									color: 'rgba(0, 128, 0, 0.8)', //改变折线点的颜色
+								}
+							},
+						},
+						{
+							name: '新增注册',
+							type:'line',
+							data: valueArray0,
+							smooth: true,	// 曲线形式
+							areaStyle: {
+								normal: {
+									color: 'rgba(70, 128, 255, 0.3)' //改变区域颜色
+								}
+							},
+							itemStyle: {
+								normal: {
+									color: 'rgba(70, 128, 255, 0.8)', //改变折线点的颜色
+								}
+							},
+						},
+					]
+				};
+				myChart.setOption(option);
+				window.myChartList.push(myChart);
+			},
+		},
+		created() {
+			// 刷新所有图标数据
+			this.$nextTick(function() {
+				this.f5LineChart();
+			});
+		}
+	}
+</script>
+
+<style scoped>
+	
+</style>

+ 49 - 0
sa-view-sp/console/com-intro.vue

@@ -0,0 +1,49 @@
+<template>
+	<div>
+		<el-alert type="success" :closable="false" title="基础">
+			架构:基于iframe,无后台代码,纯html模板,可方便的适配任何后端语言 <br/>
+			模板:提供大量常见示例,以及各种表单的书写方式,助你快速CRUD  <br/>
+			菜单:支持一、二、三级菜单,并开放一系列接口方便的使用js操作菜单 <br/>
+			折叠:折叠或收缩菜单,并且监听窗口大小变化,在拉伸窗口时自动折叠或收缩菜单,自动响应式 <br/>
+			搜索:智能索引,快捷搜索打开某个菜单 <br/>
+			切换:集成swiper动画,滑动、淡入、方块、3D流、3D翻转,五种高大上切换动画,任你选择! <br/>
+			主题:目前保留八种主题:蓝色、绿色、白色、灰色、灰色-展开、pro钛合金、沉淀式黑蓝、简约式灰蓝(切换主题时,可自动保存你的喜好,下次打开时仍然有效) <br/>
+			便签:弹出窗口便签,一个临时小便签,可记录一些临时资料 <br/>
+			全屏:可以在全屏/非全屏之间自由切换 <br/>
+		</el-alert>
+		<el-alert type="warning" :closable="false" title="tabbar栏">	
+			卡片堆积:多卡片自动堆积,与菜单双向关联,切换tab卡时自动显示左侧菜单 <br/>
+			拖动手势:tab卡支持拖拽手势,上拖新窗口打开、下拽悬浮打开、左拽快速关闭 <br/>
+			双击全屏:当卡片属于悬浮状态时,双击标题区域可以快速全屏,再次双击取消全屏 <br/>
+			右键菜单:在tab上右击,可以:刷新、复制、关闭、关闭其它、关闭所有、悬浮打开、新窗口打开、取消 <br/>
+			双击菜单:双击tabbar空白处,可以显示添加新tab窗口 <br/>
+			保留高度:切换卡片时,可自动保留上个卡片的滚动条高度 <br/>
+		</el-alert>
+		<el-alert type="info" :closable="false" title="开放接口">	
+			开放一系列api,助你方便的使用js操作tabbar栏,具体请查看集成文档 <br/>
+			锚链:tab切换自动更改hash锚链,同时监听锚链改变tab,可灵活的用鼠标前后键切换tab窗口 (如不需要此功能可在初始化时关闭) <br/>
+			窗口:可在初始化时,设置是否显示tabbar栏,来控制它是多窗口还是单窗口,具体见使用文档 <br/>
+			更新:功能不断更新中... 你有好的想法也可以告诉我,加群一起交流吧 <br/>
+			文档:使用说明,见文档 <br/>
+		</el-alert>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+</style>

+ 47 - 0
sa-view-sp/console/com-origin.vue

@@ -0,0 +1,47 @@
+<template>
+	<div>
+		<el-alert type="success" :closable="false" title="缘起">
+			<p>虽然已经用过不少优秀的后台模板,但是一直都感觉不尽完美,于是在经过激烈的思想斗争后,我决定牺牲五一假期,
+			亲自做一个尝试一下, 一来正好以后自己的项目中使用,二来也算是为开源界做一点自己的贡献吧</p>
+			<p>开源不易,求鼓励,求star</p>
+		</el-alert>
+		<el-alert type="warning" :closable="false" title="Sa-Admin 介绍">
+			<p>Sa-Admin 是一个多窗口后台模板,纯 html 无后端代码,无需脚手架即可直接运行,流畅、易上手、提高生产力。核心技术栈:Vue + Element-UI + jquery + layer。</p>
+			<div style="height: 10px;"></div>
+			<p>
+				Sa-Admin 最大的特点是无需搭建 vue-cli 脚手架,随便一个 html 预览工具(比如 
+				<el-link style="font-size: 12px; color: #999;" href="https://www.dcloud.io/hbuilderx.html" target="_blank">HBuilderX</el-link>
+				)即可直接运行(采用 http-vue-loader 技术实现)。
+				更多信息请参考项目开源首页。
+			</p>
+		</el-alert>
+		<el-alert type="info" :closable="false" title="功能说明">
+			<li>视图:支持 iframe 和 .vue 两种视图方式,支持一、二、三、四级菜单。</li>
+			<li>操作:工具栏提供常见操作按钮:折叠、搜索、刷新、账号、便签、主题切换、全屏切换。</li>
+			<li>主题:内置十种主题,也可方便的扩展主题。</li>
+			<li>切换:支持拖拽排序、切换视图自动记录hash,刷新页面自动打开上次的视图。</li>
+			<li>右键:tabbar栏支持右键菜单:悬浮打开、新窗口打开、视图复制、快捷关闭等操作。</li>
+			<li>接口:开放一系列api,可方便的用js新建、打开、切换视图等动作。</li>
+			<li>示例:提供大量常见示例,以及各种表单组件的封装,助你快速CRUD。</li>
+		</el-alert>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+</style>

+ 129 - 0
sa-view-sp/console/com-sta-data.vue

@@ -0,0 +1,129 @@
+<!-- 第一行统计数据 -->
+<template>
+	<el-row :gutter="14">
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-user.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">用户</p>
+					<p class="sa-wnk-value">{{sta.userCount}}</p>
+				</div>
+			</div>
+		</el-col>
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-goods.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">商品</p>
+					<p class="sa-wnk-value">{{sta.goodsCount}}</p>
+				</div>
+			</div>
+		</el-col>
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-order.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">订单</p>
+					<p class="sa-wnk-value">{{sta.orderCount}}</p>
+				</div>
+			</div>
+		</el-col>
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-article.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">文章</p>
+					<p class="sa-wnk-value">{{sta.articleCount}}</p>
+				</div>
+			</div>
+		</el-col>
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-comment.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">评论</p>
+					<p class="sa-wnk-value">{{sta.commentCount}}</p>
+				</div>
+			</div>
+		</el-col>
+		<el-col :lg="4" :sm="8" :xs="24">
+			<div class="sa-wnk">
+				<img src="../../static/icon/icon-money.png" >
+				<div class="sa-wnk-tv">
+					<p class="sa-wnk-title">余额</p>
+					<p class="sa-wnk-value">{{sta.moneyCount}}</p>
+				</div>
+			</div>
+		</el-col>
+	</el-row>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				// 统计数据 
+				sta: {
+					userCount: 0,
+					goodsCount: 0,
+					orderCount: 0,
+					articleCount: 0,
+					commentCount: 0,
+					moneyCount: 0,
+				},
+			}
+		},
+		methods: {
+			// 数值跳动 
+			// 对象、属性、结束值、所用时间 
+			slowMotion: function(obj, prop, endValue, time) {
+				let timeNow = 0; 
+				let fn = function() {
+					// 如果已经接近 or 时间已到,则立即结束 
+					var jdz = Math.abs(obj[prop] - endValue);
+					if(jdz < 2 || timeNow >= time) {
+						// console.log('到点了');
+						obj[prop] = endValue;
+					} else {
+						if(jdz < 100) {
+							obj[prop] += 1;
+						} else {
+							obj[prop] += parseInt((endValue - obj[prop]) / 10);		 // 平均一下 
+						}
+						timeNow += 30;
+						setTimeout(fn, 30);
+					}
+				}
+				fn();
+			},
+			// 设置统计数据的数值 
+			setStaDataValue: function(staData) {
+				for (let key in staData) {
+					this.slowMotion(this.sta, key, staData[key], 3000);
+				}
+			},
+		},
+		created() {
+			// 写入数据
+			this.setStaDataValue({
+				userCount: 12361,
+				goodsCount: 12541,
+				orderCount: 63222,
+				articleCount: 10368,
+				commentCount: 2048,
+				moneyCount: 13654.32,
+			});
+		}
+	}
+</script>
+
+<style scoped>
+	/* 第一行 */
+	.sa-wnk{background-color: #FFF; border: 1px #ddd solid; margin-bottom: 14px; min-height: 100px; 
+		cursor: pointer; transition: all 0.3s; overflow: hidden;}
+	.sa-wnk:hover{box-shadow: 0 0 20px #999;}
+	.sa-wnk img{float: left; line-height: 100px; margin: 25px 0px 0 20px; width: 50px; height: 50px; vertical-align: middle;}
+	.sa-wnk .sa-wnk-tv{float: left; margin-left: 10px; max-width: calc(100% - 100px);}
+	.sa-wnk-title{margin-top: 25px; font-size: 16px;}
+	.sa-wnk-value{margin-top: 4px; font-size: 24px; padding-bottom: 20px;}
+</style>

+ 67 - 0
sa-view-sp/console/com-stack.vue

@@ -0,0 +1,67 @@
+<!-- 第一行统计数据 -->
+<template>
+	<div>
+		<div class="btn-box">
+			<el-popover placement="top-start" trigger="hover">
+			    <el-button slot="reference" type="primary" size="small" @click="sa.open('https://jq.qq.com/?_wv=1027&k=5DHN5Ib')">QQ群(782974737)</el-button>
+				<div style="text-align: center;">
+					<img src="http://dev33-yxzj.oss-cn-beijing.aliyuncs.com/dyc/img/2020/01/17/157924554064970545739.png" style="width: 150px; height: 150px;" >
+				</div>
+			</el-popover>
+			<!-- <el-button type="primary" size="small" @click="sa.open('https://jq.qq.com/?_wv=1027&k=5DHN5Ib')">QQ群(782974737)</el-button> -->
+			<el-button type="success" size="small" @click="sa.open('https://github.com/click33/sa-admin')">GitHub 地址 (求star)</el-button>
+			<el-button type="danger" size="small" @click="sa.open('https://gitee.com/click33/sa-admin')">Gitee 地址</el-button>
+			<!-- <el-button type="info" size="small" @click="sa_admin.showMenuById('1-11')">意见吐槽</el-button> -->
+			<el-button type="info" size="small" @click="sa.open('http://sa-app.dev33.cn/wall.html?name=sa-admin')">需求征集</el-button>
+			<el-popover placement="top-start" trigger="hover">
+			    <el-button slot="reference" type="warning" size="small">打赏</el-button>
+				<div style="text-align: center;">
+					<h3 style="margin-bottom: 14px;">请作者喝杯咖啡</h3>
+					<img src="http://oss.dev33.cn/sa-admin/ds-zfb.jpg" style="width: 150px; height: 150px; cursor: pointer;" 
+						@click="sa.showImage('http://oss.dev33.cn/sa-admin/ds-zfb.jpg', '400px', '400px')" />
+					<img src="http://oss.dev33.cn/sa-admin/ds-wx.jpg" style="width: 150px; height: 150px; cursor: pointer;" 
+						@click="sa.showImage('http://oss.dev33.cn/sa-admin/ds-wx.jpg', '400px', '400px')" />
+				</div>
+			</el-popover>
+		</div>
+		<div>
+			<el-table ref="data-table" :data="frameList" size="small" border>
+				<el-table-column label="技术栈" prop="name"></el-table-column>
+				<el-table-column label="框架" prop="value"></el-table-column>
+				<el-table-column label="链接">
+					<template slot-scope="s">
+						<el-link type="primary" :href="s.row.link" target="_blank">{{s.row.link}}</el-link>
+					</template>
+				</el-table-column>
+			</el-table>
+		</div>
+	</div>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				// 技术栈集合
+				frameList: [
+					{name: '基础框架', value: 'Vue @2.6.10', link: 'https://cn.vuejs.org/'},
+					{name: 'UI框架', value: 'Element-UI @2.13.0', link: 'https://element.eleme.cn/#/zh-CN'},
+					{name: 'web弹层', value: 'layer @3.1.1', link: 'http://layer.layui.com/'},
+					{name: '图表引擎', value: 'ECharts @4.2.1', link: 'https://echarts.baidu.com/'},
+					{name: '富文本编辑器', value: 'wangEditor @3.1.1', link: 'http://www.wangeditor.com/'},
+				],
+			}
+		},
+		methods: {
+			
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+	.btn-box{margin-bottom: 4px; }
+	.btn-box .el-button{margin-bottom: 10px; }
+</style>

+ 289 - 0
sa-view-sp/console/com-update-log.vue

@@ -0,0 +1,289 @@
+<!-- 第一行统计数据 -->
+<template>
+	<el-timeline>
+		<!-- ---------- 一个版本 第40个----------- -->
+		<el-timeline-item timestamp="v1.40.0 &emsp; 2021-9-26" placement="top" type="primary">
+			<li>重构:使用 http-vue-loader 重构底层,脱胎换骨</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 第39个----------- -->
+		<el-timeline-item timestamp="v2.4.4 &emsp; 2020-11-14" placement="top" type="primary">
+			<li>修复:当最后一个tab卡片有滑动条时,其它tab滚动条失效的bug</li>
+			<li>修复:sa.js增加部分判断,使之在不引入jquery时放弃执行部分代码</li>
+			<li>修复: layer弹窗回车事件影响到首页便签的bug</li>
+			<li>优化:页面初始打开时,按钮高亮,更鲜艳的颜色 </li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 第38个----------- -->
+		<el-timeline-item timestamp="v2.4.3 &emsp; 2020-10-02" placement="top" type="primary">
+			<li>修复:修复在没有成功初始化的情况下,调整窗口大小控制台报错的bug</li>
+			<li>修复:修复单窗口显示时,面包屑显示位置异常的bug</li>
+			<li>修复:修复main.html页一直显示loading图标的bug</li>
+			<li>新增:离线包新增swiper相关文件</li>
+			<li>优化:优化菜单id为number类型时不能显示的缺点</li>
+			<li>优化:layer的弹窗,双击可以全屏,再次双击缩小 </li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 第37个----------- -->
+		<el-timeline-item timestamp="v2.4.2 &emsp; 2020-09-03" placement="top" type="primary">
+			<li>新增:新增弹窗回车事件,可方便的关闭弹窗</li>
+			<li>新增:新增判断,考虑到table刷新高度时有横向滚动条对高度的影响</li>
+			<li>优化:改变c-item的min-width,避免了某些情况下无法对齐的问题</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 第36个----------- -->
+		<el-timeline-item timestamp="v2.4.1 &emsp; 2020-08-25" placement="top" type="primary">
+			<li>优化:sort_type改为sortType 小驼峰风格</li>
+			<li>优化:查询列表页添加回车事件,更流畅的体验</li>
+			<li>优化:优化sa.showImageList函数,更智能的判断图片数组</li>
+			<li>优化:删除logo小图</li>
+			<li>修复:去掉了站长统计四个字,因为它影响到了布局</li>
+			<li>修复:修复弹出窗口底部按钮间距过大的bug</li>
+			<li>新增:集成登录验证与全局配置方法</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 第35个----------- -->
+		<el-timeline-item timestamp="v2.4.0 &emsp; 2020-08-22" placement="top" type="primary">
+			<li>新增:多行textarea文本域示例</li>
+			<li>新增:评分组件示例</li>
+			<li>新增:新增数据导出功能,纯前端实现,不借助后端也能导出Excel数据</li>
+			<li>新增:增加弹出式新增、页面重置、多选删除、页面重置等快捷操作按钮</li>
+			<li>新增:表格查询页面,在input里回车时提交查询操作</li>
+			<li>新增:新增jq22搜集</li>
+			<li>新增:权限设置页面,新增全选按钮 </li>
+			<li>新增:菜单搜索功能 </li>
+			<li>新增:sa.js新增一系列API,更强大的工具类 </li>
+			<li>升级:二三级菜单也可以添加图标了</li>
+			<li>优化:优化表格增删改查动画,更加流畅的操作</li>
+			<li>优化:全面优化页面布局,更舒服的配色及排版</li>
+			<li>优化:优化登录页面方框圆角尺寸</li>
+			<li>优化:优化阴影样式 </li>
+			<li>集成:集成form-generator,在线拖拉拽代码生成器</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.7 &emsp; 2020-04-18" placement="top" type="primary">
+			<li>新增:首页增加懒加载功能</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.6 &emsp; 2020-04-17" placement="top" type="primary">
+			<li>优化:部分样式</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.6 &emsp; 2020-04-17" placement="top" type="primary">
+			<li>优化:部分样式</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.5 &emsp; 2020-04-17" placement="top" type="primary">
+			<li>优化:部分模板页样式</li>
+			<li>更换:更换堆表单方式为c-item</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.4 &emsp; 2020-03-05" placement="top" type="primary">
+			<li>去除:tab双击刷新</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.3 &emsp; 2020-03-02" placement="top" type="primary">
+			<li>新增:tab双击刷新</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.2 &emsp; 2020-3-1" placement="top" type="primary">
+			<li>新增:初始加载loading图标</li>
+			<li>新增:函数菜单(点击菜单执行一个函数)</li>
+			<li>更新:my-code.js重命名为sa-code.js</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.1 &emsp; 2020-2-29" placement="top" type="primary">
+			<li>修复:部分bug</li>
+			<li>
+				上线:vue单页版上线,传送门:
+				<el-link type="primary" href="http://sa-vue-admin.dev33.cn/" target="_blank">http://sa-vue-admin.dev33.cn/</el-link>
+			</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.3.0 &emsp; 2020-2-25" placement="top" type="primary">
+			<li>优化:改变部分文件夹结构</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.6 &emsp; 2020-2-17" placement="top" type="primary">
+			<li>新增:新增主题:简约式灰蓝</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.5 &emsp; 2020-2-14" placement="top" type="primary">
+			<li>
+				新增:新增在线论坛:
+				<el-link type="primary" href="javascript:parent.sa_admin.showMenuById('1-11');">点击打开,在线提交意见反馈(新)</el-link>
+			</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.4 &emsp; 2020-2-13" placement="top" type="primary">
+			<li>优化:优化整体样式</li>
+			<li>优化:loading加载框的样式</li>
+			<li>增加:tab悬浮打开的z-index自动切换功能</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.3 &emsp; 2020-2-9" placement="top" type="primary">
+			<li>增加:增加底部按钮式的弹窗示例</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.2 &emsp; 2019-7-16" placement="top" type="primary">
+			<li>增加:增加弹出式修改的示例</li>
+			<li>增加:增加窗口之间通信的方法,详细请查看集成文档</li>
+			<li>完善:完善readme.md</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.1 &emsp; 2020-1-31" placement="top" type="primary">
+			<li>修复:替换掉所有bootcss的cdn,因为它太不稳定了,三天一小瘫,五天一大瘫</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.2.0 &emsp; 2020-1-20" placement="top" type="primary">
+			<li>集成:集成鉴权功能,详细请查看文档 </li>
+			<li>新增:新增大量模板示例,可帮助你快速增删改查 </li>
+			<li>修复:在边缘处右键菜单文字变形的bug </li>
+			<li>重构:改了一下首页样式 </li>
+			<li>优化:将element-ui版本更换至了 2.13.0 </li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.1.2 &emsp; 2020-1-18" placement="top" type="primary">
+			<li>修复:修复登录页鼠标不能与背景粒子交互的bug </li>
+			<li>优化:右键关闭其它和关闭全部时,首先滑到做左边,动画更直观 </li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.1.0 &emsp; 2020-1-17" placement="top" type="primary">
+			<li>新增:更换了登录页</li>
+			<li>新增:右键菜单新增复制按钮,可直接复制一个tab在新窗口打开 </li>
+			<li>新增:右键菜单新增折叠关闭动画,失去焦点时和点击取消时,菜单以折叠动画的方式关闭 </li>
+			<li>新增:右键菜单新增盒子阴影,更有立体感</li>
+			<li>新增:新增主题:pro钛合金、沉淀式黑蓝 </li>
+			<li>修复:切换tab时,不能自动滑动的bug</li>
+			<li>重构:重新设置了UI样式,详细参考模板示例</li>
+			<li>注意:表格内操作按钮类样式 .c-button 换成了 .c-btn  </li>
+			<li>重构:修改了sa.js,取消$util对象,所有有关$util的函数全部移到sa对象上,</li>
+			<li>注意:原调用方式sa.$util.getUrlArgs('id') 现改为:sa.p('id')</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.0.2 &emsp; 2020-1-15" placement="top" type="primary">
+			<li>修复bug:tab卡文字向上偏移了1px的问题</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.0.1 &emsp; 2020-1-14" placement="top" type="primary">
+			<li>修复bug:在方块、3D流、3D翻转切换效果下,tab切换错乱的bug</li>
+			<li>修复bug:iframe的url发生改变后,刷新按钮刷新为旧地址的bug</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v2.0.0 &emsp; 2020-1-13" placement="top" type="primary">
+			<li>新增:卡片右击菜单弹出动画 </li>
+			<li>新增:tab右键菜单动画</li>
+			<li>新增:新增tab关闭动画, 在关闭tab、右键关闭其它、关闭全部时有了更流畅的体验</li>
+			<li>新增:tab选项卡拖拽效果,上拽新窗口打开,下拽悬浮打开,左拽关闭,在tab处拖拽一下体验一下吧</li>
+			<li>新增:在tab栏空白处,双击:可以打开添加新tab操作弹窗 </li>
+			<li>新增:增加记住上一次最后打开的窗口功能,刷新也可以记住窗口(在初始化模板时,增加is_reme_open配置项)</li>
+			<li>新增: hash链接跳转功能,可灵活的用鼠标前后键切换tab窗口 </li>
+			<li>新增:便签功能,可以方便的保存一些临时数据 </li>
+			<li>修复:首页114行有个重复的class</li>
+			<li>修复:版本号打印不对 </li>
+			<li>修复:首页homePage的url配置无效</li>
+			<li>
+				因为项目紧迫,这个版本拖了三个月,积攒了不少更新点,也算是憋了一个大招,
+				大家有什么意见或者想要添加功能,可以加入qq群尽情提出来,我只要有时间,一定第一时间更新
+			</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.1.4 &emsp; 2019-10-17" placement="top" type="primary">
+			<li>重写了一下简介</li>
+			<li>优化主题样式</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.1.3 &emsp; 2019-9-3" placement="top" type="primary">
+			<li>更改初始化方式</li>
+			<li>优化UI样式</li>
+			<li>增加新主题:灰色-展开</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.1.2 &emsp; 2019-7-16" placement="top" type="primary">
+			<li>增加右键菜单的失去焦点事件,失去焦点自动消失</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.1 &emsp; 2019-6-26" placement="top" type="primary">
+			<li>优化卡片切换动画,更流畅了,并且添加loading图标,视觉上更加顺畅</li>
+			<li>新增悬浮窗口功能,在卡片标题处右击试试吧</li>
+			<li>更换登录模板页,更漂亮了</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.1.0 &emsp; 2019-6-24" placement="top" type="primary">
+			<li>修复bug:鼠标悬浮tab-title时,偶尔动画混乱的bug</li>
+			<li>修复bug:优化折叠动画,更流畅了</li>
+			<li>集成swiper,窗口切换,更加高大上了</li>
+		</el-timeline-item>
+		<el-timeline-item timestamp="v1.0.8 &emsp; 2019-5-28" placement="top" type="primary">
+			<li>修复bug:菜单折叠时,菜单项箭头仍然显示的问题</li>
+			<li>修复bug:在手机端菜单折叠时,右侧信息排版发生混乱的问题</li>
+			<li>调整了一下字体大小,看着更顺眼了</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.7 &emsp; 2019-5-25" placement="top" type="primary">
+			<li>优化一些动画效果</li>
+			<li>增加模板页:轮播图管理</li>
+			<li>一些样式优化</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.6 &emsp; 2019-5-22" placement="top" type="primary">
+			<li>添加了菜单预览模板(tree树)</li>
+			<li>修复bug:菜单分配权限时,父子级不关联的问题</li>
+			<li>一些样式优化</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.5 &emsp; 2019-5-18" placement="top" type="primary">
+			<li>添加了权限中心模板(tree权限树)</li>
+			<li>优化用户列表模板,弹出式的修改</li>
+			<li>一些其它样式优化</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.4 &emsp; 2019-5-15" placement="top" type="primary">
+			<li>优化折叠动画,避免文字闪动现象</li>
+			<li>sp.setMenuList();接口增加show_list参数,可灵活控制部分菜单的显示与隐藏</li>
+			<li>一些样式优化</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.3 &emsp; 2019-5-14" placement="top" type="primary">
+			<li>添加文章管理模板(wangEditor富文本编辑器)</li>
+			<li>改写了表格里按钮的样式,更鲜艳,增加点击感</li>
+			<li>菜单列表里可以指定is_show=false,使菜单成为隐藏菜单</li>
+			<li>重写了一些接口,可以更加方便的与你的系统集成</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.1 &emsp; 2019-5-5" placement="top" type="primary">
+			<li>修复:周日显示周0的bug</li>
+			<li>新增:增加三级菜单支持</li>
+			<li>新增:主题更换时高亮显示,方便区分</li>
+		</el-timeline-item>
+		<!-- ---------- 一个版本 ----------- -->
+		<el-timeline-item timestamp="v1.0.0 &emsp; 2019-5-2" placement="top" type="primary">
+			<li>第一个版本出炉</li>
+			<li>功能持续更新中...</li>
+		</el-timeline-item>
+	</el-timeline>
+</template>
+
+<script>
+	module.exports = {
+		data() {
+			return {
+				// 技术栈集合
+				frameList: [
+					{name: 'JS引擎', value: 'Vue @2.6.10', link: 'https://cn.vuejs.org/'},
+					{name: 'UI框架', value: 'Element-UI @2.13.0', link: 'https://element.eleme.cn/#/zh-CN'},
+					{name: 'web弹层', value: 'layer @3.1.1', link: 'http://layer.layui.com/'},
+					{name: '切页动画', value: 'Swiper @4.5.0', link: 'https://www.swiper.com.cn/'},
+					{name: '图表引擎', value: 'ECharts @4.2.1', link: 'https://echarts.baidu.com/'},
+					{name: '富文本编辑器', value: 'wangEditor @3.1.1', link: 'http://www.wangeditor.com/'},
+				],
+			}
+		},
+		methods: {
+			
+		},
+		created() {
+			
+		}
+	}
+</script>
+
+<style scoped>
+	.el-timeline-item__timestamp { color: #207EFF;}
+	.el-timeline-item__content{color: #666;}
+</style>

+ 117 - 0
sa-view-sp/console/console-main.html

@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Sa-Admin 控制台</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css">
+		<style type="text/css">
+			.vue-box{margin: 0; padding: 0; height: 100%;}
+			.el-card{border-radius: 0px; border: 1px #ddd solid ; margin-bottom: 14px;}
+			.s-row{/* background-color: antiquewhite; */ padding: 0 14px; padding-bottom: 0px;}
+			.s-row-1{padding-top: 14px;}
+			.s-row-2{/* margin-top: -10px; */}
+			.s-row-2 .el-card .el-card__body{height: 250px;}
+			.s-row-3 .el-card{/* height: 100%; */}
+			
+			.echarts-div{height: 100%;}
+			.s-row-3 .el-alert{margin-bottom: 14px;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			
+			<!-- ------------ 第一栏 - 统计数据 ------------- -->
+			<div class="s-row s-row-1">
+				<com-sta-data></com-sta-data>
+			</div>
+			
+			<!-- ------------ 第二栏 - 图表 ------------- -->
+			<div class="s-row s-row-2">
+				<el-row :gutter="14">
+					<el-col :lg="8" :xs="24">
+						<el-card shadow="never" header="柱状图">
+							<com-chart-1></com-chart-1>
+						</el-card>
+					</el-col>
+					<el-col :lg="8" :xs="24">
+						<el-card shadow="never" header="饼图">
+							<com-chart-2></com-chart-2>
+						</el-card>
+					</el-col>
+					<el-col :lg="8" :xs="24">
+						<el-card shadow="never" header="折线图">
+							<com-chart-3></com-chart-3>
+						</el-card>
+					</el-col>
+				</el-row>
+			</div>
+			
+			<!-- ------------ 第三栏 - 框架信息 ------------- -->
+			<div class="s-row s-row-3">
+				<el-row :gutter="14" type="flex" style="flex-wrap: wrap-reverse;">
+					<!-- 左边 -->
+					<el-col :lg="12" :xs="24">
+						<!-- 技术选型 -->
+						<el-card shadow="never" header="技术选型">
+							<com-stack></com-stack>
+						</el-card>
+						<!-- 更新日志 -->
+						<el-card shadow="never" header="更新日志">
+							<com-update-log></com-update-log>
+						</el-card>
+					</el-col>
+					<!-- 右边 -->
+					<el-col :lg="12" :xs="24">
+						<el-card shadow="never" header="Sa-Admin ">
+							<com-origin></com-origin>
+						</el-card>
+					</el-col>
+				</el-row>
+			</div>
+			
+		</div>
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="https://unpkg.com/echarts@4.6.0/dist/echarts-en.min.js"></script>
+		<script src="../../static/sa.js"></script>
+		<script type="text/javascript">
+			var app = new Vue({
+				components: {
+					'com-sta-data': httpVueLoader('com-sta-data.vue'),
+					'com-chart-1': httpVueLoader('com-chart-1.vue'),
+					'com-chart-2': httpVueLoader('com-chart-2.vue'),
+					'com-chart-3': httpVueLoader('com-chart-3.vue'),
+					'com-stack': httpVueLoader('com-stack.vue'),
+					'com-update-log': httpVueLoader('com-update-log.vue'),
+					'com-origin': httpVueLoader('com-origin.vue'),
+					// 'com-intro': httpVueLoader('com-intro.vue'),
+				},
+				el: '.vue-box',
+				data: {
+				},
+				methods: {
+				},
+				mounted: function() {
+				}
+			})
+			
+			// 设置监听,改变窗口大小时重绘图表 
+			window.myChartList = [];
+			window.onresize = function() {
+				myChartList.forEach(function(myChart) {
+					myChart.resize();
+				})
+			}
+		</script>
+		<!-- 百度统计(下载到本地后请删除) -->
+		<div style="height: 0px; overflow: hidden;">
+			<script type="text/javascript" src="https://v1.cnzz.com/z_stat.php?id=1279021391&web_id=1279021391"></script>
+		</div>
+	</body>
+</html>

+ 98 - 0
sa-view-sp/sp-admin/admin-add.html

@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+	<head>
+	    <title>添加管理员</title>
+	    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<script src="../../static/kj/upload-util.js"></script>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<!-- 参数栏 -->
+			<div class="c-panel">
+				<h4 class="c-title">添加一个管理员</h4>
+				<el-form>
+					<!-- 防止密码框被填充 -->
+					<div style="height: 0px; overflow: hidden;">
+						<el-input></el-input>
+						<el-input type="password"></el-input>
+					</div>
+					<!-- 表单 -->
+					<sa-item type="img" name="头像" v-model="m.avatar" br></sa-item>
+					<sa-item type="text" name="名称" v-model="m.name" br></sa-item>
+					<sa-item type="password" name="密码" v-model="m.password" br></sa-item>
+					<sa-item name="角色" br>
+						<el-select v-model="m.roleId">
+							<el-option label="请选择" :value="0" disabled></el-option>
+							<el-option v-for="role in roleList" :key="role.id" :label="role.name" :value="role.id"></el-option>
+						</el-select>
+					</sa-item>
+					<sa-item name="" br>
+						<el-button type="primary" icon="el-icon-plus" @click="ok()">保存</el-button>
+					</sa-item>
+				</el-form>
+			</div>
+		</div>
+		
+		<script type="text/javascript">
+			function crateModel() {
+				return {
+					id: 0,
+					name: '',
+					avatar: '',
+					password: '',
+					roleId: 0
+				}
+			}
+		</script>
+        <script>
+			
+			var app = new Vue({
+				components: {
+					"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue')
+				},
+				el: '.vue-box',
+				data: {
+					sa: sa, 	// 超级对象
+					m: crateModel(),
+					roleList: []
+				},
+				methods: {
+					// 修改
+					ok: function(){
+						// 表单校验 
+						let m = this.m;
+						sa.checkNull(m.avatar, '请选择一个头像');
+						sa.checkNull(m.name, '请输入昵称');
+						sa.checkNull(m.password, '请输入密码');
+						sa.checkNull(m.roleId, '请选择角色');
+						
+						// 添加
+						sa.ajax('/admin/add', m, function(res){
+							sa.alert('添加成功, 账号id为:' + res.data, function(){
+								this.m = crateModel();
+								// location.reload();
+							}.bind(this));
+						}.bind(this));
+					}
+				},
+				mounted: function(){
+					// 加载角色 
+					sa.ajax('/role/getList', function(res){
+						this.roleList = res.data;	// 数据  
+					}.bind(this), {msg: null});
+				}
+			})
+			
+		</script>
+	</body>
+</html>

+ 88 - 0
sa-view-sp/sp-admin/admin-info.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>资料详情</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style type="text/css">
+			/* 使长度更长点 */
+			/* .c-panel .c-label{width: 10em;} */
+			.vue-box,.c-panel{background-color: #EEE;}
+			.c-panel .c-title{margin-bottom: 20px;}
+			.c-item .c-label{width: 150px;}
+			.c-item .el-input{width: 300px;}
+			/* 链接样式  */
+			.my-link{position: relative; top: -1px; margin-left: 0.5em;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box sbot" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<div class="c-panel">
+					<el-form v-if="m">
+						<div style="height: 20px;"></div>
+						<sa-info name="编号" br>{{m.id}}</sa-info>
+						<sa-info name="名称" br>{{m.name}}</sa-info>
+						<sa-info name="手机" br>{{m.phone || '无'}}</sa-info>
+						<sa-info name="角色" br>{{m.roleName}}</sa-info>
+						<sa-info name="创建账号" br>
+							<span v-if="m.createByAid == -1">无</span>
+							<a v-else :href=" 'admin-info.html?id=' +  m.createByAid">{{m.createByAid}}</a>
+						</sa-info>
+						<sa-info name="创建时间" br>{{sa.forDate(m.createTime, 2)}}</sa-info>
+						<sa-info name="最后登录" br>{{sa.forDate(m.loginTime, 2) || '无'}}</sa-info>
+						<sa-info name="最后登录IP" br>{{m.loginIp || '无'}}</sa-info>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="success" @click="sa.closeCurrIframe()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+			
+		</div>
+		
+		<script type="text/javascript">
+			var app = new Vue({
+				components: {
+					"sa-info": httpVueLoader('../../sa-frame/com/sa-info.vue')
+				},
+				el: '.vue-box',
+				data: {
+					id: parseInt(sa.p('id', 0)),
+					sa: sa,
+					m: null
+				},
+				methods: {
+					// ok
+					ok: function(pageNo) {
+						sa.closeCurrIframe();
+					},
+				},
+				created: function() {
+					if(this.id == 0 || this.id == sa.$sys.getCurrUser().id) {
+						sa.ajax('/admin/getByCurr', function(res) {
+							this.m = res.data;
+						}.bind(this));
+					} else {
+						sa.ajax('/admin/getById?id=' + this.id, function(res) {
+							this.m = res.data;
+						}.bind(this));
+					}
+				}
+			})
+			
+		</script>
+	</body>
+</html>

+ 254 - 0
sa-view-sp/sp-admin/admin-list.html

@@ -0,0 +1,254 @@
+<!DOCTYPE html>
+<html>
+	<head>
+	    <title>管理员列表</title>
+	    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+	    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<script src="../../static/kj/upload-util.js"></script>
+		<style type="text/css">
+			.el-radio{margin-right: 10px;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<div class="c-panel">
+				<!-- ------------- 检索参数 ------------- -->
+				<h4 class="c-title">检索参数</h4>
+				<el-form>
+					<sa-item type="num" name="账号id" v-model="p.id"></sa-item>
+					<sa-item type="text" name="名称" v-model="p.name"></sa-item>
+					<sa-item name="角色">
+						<el-select v-model="p.roleId">
+							<el-option label="全部" value=""></el-option>
+							<el-option v-for="role in roleList" :key="role.id" :label="role.name" :value="role.id"></el-option>
+						</el-select>
+					</sa-item>
+					<el-button type="primary" icon="el-icon-search" @click="p.pageNo = 1; f5()">查询</el-button>
+					<br>
+					<sa-item name="综合排序" class="s-radio-text">
+						<el-radio-group v-model="p.sort_type">
+							<el-radio :label="0">最近添加</el-radio>
+							<el-radio :label="2">最近登录</el-radio>
+							<el-radio :label="3">登录次数</el-radio>
+						</el-radio-group>
+					</sa-item>
+				</el-form>
+				<!-- ------------- 快捷按钮 ------------- -->
+				<sa-item type="fast-btn" show="add,get,delete,export,reset"></sa-item>
+				<!-- ------------- 数据列表 ------------- -->
+				<el-table class="data-table" ref="data-table" :data="dataList">
+					<sa-td type="selection"></sa-td>
+					<sa-td type="num" name="记录id" prop="id" min-width="70px"></sa-td>
+					<sa-td type="user-avatar" name="昵称" prop="name,avatar" min-width="120px"></sa-td>
+					<sa-td type="text" name="手机" prop="phone"></sa-td>
+					<sa-td name="创建人" >
+						<template slot-scope="s">
+							<span v-if="s.row.createByAid == -1">无</span>
+							<el-link v-else @click="sa.$page.openAdminInfo(s.row.createByAid, s.row.name)">{{s.row.createByAid}}</el-link>
+						</template>
+					</sa-td type="text">
+					<sa-td type="text" name="所属角色" prop="roleName"></sa-td>
+					<sa-td type="datetime" name="所属角色" prop="createTime" width="150px"></sa-td>
+					<sa-td type="datetime" name="最后登录" prop="loginTime" width="150px"></sa-td>
+					<sa-td type="text" name="登录次数" prop="loginCount" not="0" width="100px"></sa-td>
+					<sa-td type="switch" name="账号状态" prop="status" :jv="{1: '正常', 2: '禁用[#ff4949]'}" @change="s => updateStatus(s.row)" width="120px"></sa-td>
+					<el-table-column label="操作" fixed="right" width="450px">
+						<template slot-scope="s">
+							<span @click="getInfo(s.row)">
+								<el-button type="success" class="c-btn" icon="el-icon-view">查看</el-button>
+							</span>
+							<span @click="updateName(s.row)">
+								<el-button type="primary" class="c-btn" icon="el-icon-edit">改名称</el-button>
+							</span>
+							<span @click="updateAvatar(s.row)">
+								<el-button type="primary" class="c-btn" icon="el-icon-edit">改头像</el-button>
+							</span>
+							<span @click="updatePassword(s.row)">
+								<el-button type="primary" class="c-btn" icon="el-icon-edit">改密码</el-button>
+							</span>
+							<el-dropdown trigger="click" style="font-size: 0.85em;">
+								<el-button type="primary" class="c-btn">
+									修改角色为 <i class="el-icon-arrow-down el-icon--right"></i>
+								</el-button>
+								<el-dropdown-menu slot="dropdown">
+									<span v-for="role in roleList" :key="role.id" @click="updateRoleId(s.row, role.id, role.name)">
+										<el-dropdown-item :style=" s.row.roleId == role.id ? {color: 'blue'} : null ">{{role.name}}</el-dropdown-item>
+									</span>
+								</el-dropdown-menu>
+							</el-dropdown>
+							<span @click="del(s.row)">
+								<el-button type="danger" class="c-btn" icon="el-icon-delete">删除</el-button>
+							</span>
+						</template>
+					</el-table-column>
+				</el-table>
+				<!-- 分页 -->
+				<sa-item type="page" :curr.sync="p.pageNo" :size.sync="p.pageSize" :total="dataCount" @change="f5()"></sa-item>
+			</div>
+			
+		</div>
+        <script>
+			var app = new Vue({
+				components: {
+					"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue'),
+					"sa-td": httpVueLoader('../../sa-frame/com/sa-td.vue')
+				},
+				el: '.vue-box',
+				data: {
+					sa: sa, 	// 超级对象
+					p: {		// 查询参数
+						id: '',
+						name: '',
+						roleId: '',
+						sort_type: 0,
+						pageNo: 1,
+						pageSize: 10,
+					},
+					dataCount: 0,
+					dataList: [],	// 数据集合
+					roleList: [],	// 角色集合 
+					curr_m: null // 当前操作的 m 
+				},
+				methods: {
+					// 刷新
+					f5: function(isPage){
+						sa.ajax('/admin/getList', this.p, function(res){
+							this.dataList = res.data;	// 数据
+							this.dataCount = res.dataCount;
+							sa.f5TableHeight();		// 刷新表格高度 
+						}.bind(this));
+					},
+					// 新增
+					add: function() {
+						parent.sa_admin.showMenuById('admin-add');
+					},
+					// 查看详情
+					getInfo: function(data) {
+						//sa.showIframe('账号详情', 'admin-info.html?id=' + data.id, '700px', '80%');
+						sa.$page.openAdminInfo(data.id, data.name);
+					},
+					// 查看 - 根据选中的
+					getBySelect: function(data) {
+						var selection = this.$refs['data-table'].selection;
+						if(selection.length == 0) {
+							return sa.msg('请选择一条数据')
+						}
+						this.getInfo(selection[0]);
+					},
+					// 修改名称 
+					updateName: function(data) {
+						layer.prompt({title: '修改账号名称'}, function(pass, index){
+							layer.close(index);
+							sa.ajax('/admin/update', {id: data.id, name: pass}, function(res){
+								data.name = pass;
+								layer.msg('修改成功');
+							})
+						});
+					},
+					// 修改头像 
+					updateAvatar: function(data) {
+						sa.uploadImage(function(src) {
+							var p = {id: data.id, avatar: src};
+							sa.ajax('/admin/updateAvatar', p, function(res) {
+								sa.msg('上传成功');
+								data.avatar = src;  
+							}.bind(this));
+						})
+					},
+					// 修改密码
+					updatePassword: function(data) {
+						layer.prompt({title: '修改密码'}, function(pass, index){
+							layer.close(index);
+							if(pass.length < 4) {
+								return layer.msg('新密码长度请不要低于4位');
+							}
+							sa.ajax('/admin/updatePassword', {id: data.id, password: pass}, function(res){
+								layer.msg('修改成功');
+							})
+						});
+					},
+					// 修改角色 
+					updateRoleId: function(data, roleId, roleName) {
+						if(data.id == sa.$sys.getCurrUser().id) {
+							return sa.alert('不能自己修改自己的角色');  
+						}
+						if(data.roleId == roleId) {
+							return sa.alert('该用户已经是' + roleName + '了');	
+						}
+						var str = '将此账号修改为 [' + roleName + '], 请确认?';
+						layer.confirm(str, {title: '请确认'}, function() {
+							sa.ajax('/admin/updateRole', {id: data.id, roleId: roleId}, function(res) {
+								sa.msg('修改成功');
+								data.roleId = roleId;
+								data.roleName = roleName;
+							}.bind(this));
+						}.bind(this));
+					},
+					// 修改用户的状态
+					updateStatus: function(data) {
+						if(data.id == sa.$sys.getCurrUser().id) {
+							data.status = 3 - data.status;  
+							return sa.alert('不能自己封禁自己');  
+						}
+						var is_ok = false;	// 记录是否成功 
+						var ajax = sa.ajax('/admin/updateStatus', {adminId: data.id, status: data.status}, function(res) {
+							sa.msg('修改成功');
+							is_ok = true;
+						}.bind(this));
+						// 如果未能修改成功, 则回滚 
+						$.when(ajax).done(function() {
+							if(is_ok == false) {
+								data.status = 3 - data.status; 
+							}
+						})
+					},
+					// 删除 
+					del: function (data) {
+						sa.confirm('是否删除,此操作不可撤销', function(){
+							sa.ajax('/admin/delete', {id: data.id},function(res){
+								sa.arrayDelete(app.dataList, data);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							})
+						});
+					},
+					// 批量删除
+					deleteByIds: function() {
+						// 获取选中元素的id列表
+						let selection = this.$refs['data-table'].selection;
+						let ids = sa.getArrayField(selection, 'id');
+						if(selection.length == 0) {
+							return sa.msg('请至少选择一条数据')
+						}
+						// 提交删除 
+						sa.confirm('是否批量删除选中数据?此操作不可撤销', function() {
+							sa.ajax('/admin/deleteByIds', {ids: ids.join(',')}, function(res) {
+								sa.arrayDelete(this.dataList, selection);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							}.bind(this))
+						}.bind(this));
+					},
+				},
+				created: function(){
+					this.f5();
+					sa.onInputEnter();	// 监听回车执行查询 
+					// 加载角色 
+					sa.ajax('/role/getList', function(res){
+						this.roleList = res.data;	// 数据  
+					}.bind(this), {msg: null});
+				}
+			})
+			
+		</script>
+	</body>
+</html>

+ 82 - 0
sa-view-sp/sp-admin/update-password.html

@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>修改密码</title>
+		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style>
+			/* body,.sbot.vue-box{background-color: #EEE !important;} */
+			.c-item .c-label{width: 10em;}
+			.c-item .el-input__inner{width: 300px;}
+        </style>
+	</head>
+	<body>
+		<div class="vue-box sbot" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<div class="c-panel">
+					<div class="c-title">数据添加</div>
+					<el-form v-if="m">
+						<sa-item type="password" name="旧密码" v-model="m.oldPwd" br></sa-item>
+						<sa-item type="password" name="再次输入旧密码" v-model="m.oldPwd2" br></sa-item>
+						<sa-item type="password" name="新密码" v-model="m.newPwd" br></sa-item>
+						<sa-item type="password" name="再次输入新密码" v-model="m.newPwd2" br></sa-item>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="primary" @click="ok()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+
+		</div>
+		<script>
+			var app = new Vue({
+				components: {
+					"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue')
+				},
+				el: '.vue-box',
+				data: {
+					sa: sa,
+					m: {
+						oldPwd: '',
+						oldPwd2: '',
+						newPwd: '',
+						newPwd2: ''
+					},
+				},
+				methods: {
+					// 提交 
+					ok: function() {
+						// 表单校验 
+						let m = this.m;
+						sa.checkNull(m.oldPwd && m.oldPwd2 && m.newPwd && m.newPwd2, '请填写'); 
+						sa.check(m.oldPwd != m.oldPwd2, '旧密码两次输入不一致');
+						sa.check(m.newPwd != m.newPwd2, '新密码两次输入不一致');
+						sa.check(m.newPwd.length < 4, '新密码请不要低于六位数');
+						// 开始修改 
+						sa.ajax('/AdminPassword/update', this.m, function(res) {
+							if(parent != window) {
+								sa.closeCurrIframe();
+								parent.sa.ok2('修改成功');
+							}
+						})
+						//sa.$fast.fastUpdate2('/SysUser/updatePassword', this.m);
+					},
+				},
+				mounted: function() {
+					
+				}
+			})
+		</script>
+	</body>
+</html>

+ 88 - 0
sa-view-sp/sp-apilog/api-log-list-delete.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>角色-添加/修改</title>
+		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style type="text/css">
+			.c-panel .el-form .c-label{width: 6em !important;}
+			.c-panel .el-form .el-input{width: calc(100% - 120px);}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box sbot" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<div class="c-panel">
+					<div class="c-title">数据添加</div>
+					<el-form v-if="m">
+						<!-- no字段: m.id - id -->
+						<div class="c-item br">
+							<label class="c-label">开始日期:</label>
+							<el-date-picker v-model="m.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始日期"></el-date-picker>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">结束日期:</label>
+							<el-date-picker v-model="m.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束日期"></el-date-picker>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">已选范围:</label>
+							<span style="color: red;">{{sa.forDate(m.startTime, 2)}}</span> - 
+							<span style="color: red;">{{sa.forDate(m.endTime, 2)}} </span>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">操作注意:</label>
+							<span style="color: red;">日志删除后不可恢复,请谨慎操作</span>
+						</div>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="primary" @click="ok()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+		</div>
+        <script>
+			
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					id: sa.p('id', 0),		// 获取超链接中的id参数(0=添加,非0=修改) 
+					m: {
+						startTime: '',
+						endTime: '',
+					},		// 实体对象 
+				},
+				methods: {
+					// 提交数据 
+					ok: function(){
+						if(sa.isNull(this.m.startTime) || sa.isNull(this.m.endTime) ) {
+							return sa.error('请选择一个时间范围')
+						}
+						// 开始删除
+						sa.ajax('/SgApilog/deleteByStartEnd', this.m, function(res){
+							sa.alert('操作成功, 共删除 ' + res.data + ' 条请求记录', function() {
+								if(parent.app) {
+									parent.app.f5();
+									sa.closeCurrIframe();	// 关闭本页 
+								} 
+							}.bind(this)); 
+						}.bind(this));
+					},
+				},
+				mounted: function(){
+					
+				}
+			})
+		</script>
+	</body>
+</html>

+ 336 - 0
sa-view-sp/sp-apilog/api-log-list.html

@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>api访问记录-列表</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css">
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style>
+			.req-type-box{color: #FFF;}
+			.req-type-box b{padding: 3px 7px; border-radius: 3px; font-size: 12px;}
+			/* .req-api span{background-color: ; color: #409EFF; border: 1px #409EFF solid; border-radius: 2px; padding: 3px 5px;} */
+			.req-api span{color: #44f; font-weight: 700; margin-left: 3px;}
+			.req-p{display: inline-block; line-height: 1.4; padding: 2px 4px; border-radius: 4px; cursor: pointer;background-color: #fff2f4; color: #c7254e;}
+			.req-ip{background-color: ; color: #409EFF; border: 1px #409EFF solid; border-radius: 2px; padding: 3px 5px;}
+			
+			.req-string{line-height: 1.4; padding: 2px 4px; border-radius: 4px;cursor: pointer; background-color: #ECF5FF;}
+			
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<div class="c-panel">
+				<!-- 参数栏 -->
+				<div class="c-title">检索参数</div>
+				<el-form :inline="true" @submit.native.prevent>
+					<el-form-item label="记录id:">
+						<el-input v-model="p.id" placeholder="精确定位"></el-input>
+					</el-form-item>
+					<el-form-item label="请求ip:">
+						<el-input v-model="p.reqIp" placeholder="IP筛选"></el-input>
+					</el-form-item>
+					<el-form-item label="请求api:">
+						<el-input v-model="p.reqApi" placeholder="API接口筛选"></el-input>
+					</el-form-item>
+					<el-form-item style="min-width: 0px;">
+						<el-button type="primary" icon="el-icon-search" @click="p.pageNo = 1; f5()">查询</el-button>
+					</el-form-item>
+					<br>
+					<el-form-item label="请求token:">
+						<el-input v-model="p.reqToken" placeholder="定向跟踪token"></el-input>
+					</el-form-item>
+					<el-form-item label="userId:">
+						<el-input v-model="p.userId" placeholder="定向跟踪用户"></el-input>
+					</el-form-item>
+					<el-form-item label="adminId:">
+						<el-input v-model="p.adminId" placeholder="定向跟踪用户"></el-input>
+					</el-form-item>
+					<br>
+					<el-form-item label="res状态码:">
+						<el-input v-model="p.resCode" placeholder="状态码筛选"></el-input>
+					</el-form-item>
+					<el-form-item label="请求时间:">
+						<el-date-picker v-model="p.sTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始日期"></el-date-picker>
+						-
+						<el-date-picker v-model="p.eTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束日期"></el-date-picker>
+					</el-form-item>
+					<br />
+					<el-form-item label="综合排序:" class="s-radio-text">
+						<el-radio-group v-model="p.sortType">
+							<el-radio :label="0">默认</el-radio>
+							<el-radio :label="1">请求时间</el-radio>
+							<el-radio :label="2">请求耗时</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-form>
+				<!-- ------------- 快捷按钮 ------------- -->
+				<div class="fast-btn" style="margin-top: -10px;">
+					<el-button type="primary" icon="el-icon-plus" @click="copyAll()">复制全部</el-button>
+					<!-- <el-button type="success" icon="el-icon-edit" @click="update($refs['data-table'].selection[0])"
+						:disabled="!$refs['data-table'] || $refs['data-table'].selection.length != 1">修改</el-button> -->
+					<el-button type="danger" icon="el-icon-delete" @click="deleteByIds()">删除</el-button>
+					<el-button type="warning" icon="el-icon-download" @click="sa.exportExcel()">导出</el-button>
+					<el-button type="info"  icon="el-icon-refresh"  @click="sa.f5()">重置</el-button>
+					<el-button type="danger" icon="el-icon-delete" @click="deleteByStartEnd()">范围删除</el-button>
+					<el-button type="success" icon="el-icon-view" @click="f5StaData()">统计数据</el-button>
+				</div>
+				<!-- ------------- 数据列表 ------------- -->
+				<el-table class="data-table" ref="data-table" :data="dataList" size="small">
+					<el-table-column type="selection" width="45px"></el-table-column>
+					
+					<el-table-column label="请求id" width="110px">
+						<template slot-scope="s">
+							<div style="font-weight: bold;">{{s.row.id}}</div>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求ip" width="130px">
+						<template slot-scope="s">
+							<span class="req-ip">{{(s.row.reqIp)}}</span>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求方式" width="100px">
+						<template slot-scope="s">
+							<span class="req-type-box">
+								<b style="background-color: #00A65A;" v-if=" s.row.reqType == 'GET' ">GET</b>
+								<b style="background-color: #0073B7;" v-else-if=" s.row.reqType == 'POST' ">POST</b>
+								<b style="background-color: #FF6A00;" v-else>{{s.row.reqType}}</b>
+							</span>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求接口" width="250px">
+						<template slot-scope="s">
+							<p class="req-api"><span>{{sa.maxLength(s.row.reqApi)}}</bspan>
+							</p>
+							<span class="req-p" @click="seeReqParame(s.row)">{{sa.maxLength(s.row.reqParame, 70)}}</span>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求返回" min-width="370px">
+						<template slot-scope="s">
+							<p style="padding-left: 3px;">
+								<b style="color: green;" v-if="s.row.resCode == 200">{{s.row.resCode}} - {{s.row.resMsg}}</b>
+								<b style="color: red;" v-else-if="s.row.resCode == 500 || s.row.resCode == 501">{{s.row.resCode}} - {{s.row.resMsg}}</b>
+								<b style="color: blue;" v-else>{{s.row.resCode}} - {{s.row.resMsg}}</b>
+							</p>
+							<p class="req-string" @click="seeResString(s.row)">
+								{{sa.maxLength(s.row.resString, 70)}}
+							</p>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求账号" width="150px">
+						<template slot-scope="s">
+							<p v-if="s.row.userId == 0">userId:&nbsp;&nbsp;&nbsp;0</p>
+							<p v-else>userId:&nbsp;&nbsp;&nbsp;<el-link @click="sa.alert(s.row.userId)">{{s.row.userId}}</el-link>
+							</p>
+							<p v-if="s.row.adminId == 0">adminId:0</p>
+							<p v-else>adminId:<el-link @click="sa.$page.openAdminInfo(s.row.adminId)">{{s.row.adminId}}</el-link>
+							</p>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求token" width="150px" v-if="!sa.isNull(p.reqToken) ">
+						<template slot-scope="s">
+							<div style="width: 130px;">{{s.row.reqToken}}</div>
+						</template>
+					</el-table-column>
+					<el-table-column label="请求时间" width="280px">
+						<template slot-scope="s">
+							<p>
+								开始:{{sa.forDate(s.row.startTime, 'yyyy-MM-dd HH:mm:ss.ms')}} -
+								<b>{{sa.isNull(sa.forDate2(s.row.startTime), '无')}}</b>
+							</p>
+							<p>
+								结束:{{sa.forDate(s.row.endTime, 'yyyy-MM-dd HH:mm:ss.ms')}} -
+								<b style="color: green;">耗时:{{(s.row.costTime + 0.0) / 1000}}s</b>
+							</p>
+						</template>
+					</el-table-column>
+					<el-table-column label="操作" width="120px">
+						<template slot-scope="s">
+							<el-button type="text" @click="copy(s.row)">复制</el-button>
+							<el-button type="text" @click="del(s.row)" v-if="way == 1">删除</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+				<!-- 分页 -->
+				<div class="page-box">
+					<el-pagination 
+						background 
+						layout="total, prev, pager, next, sizes, jumper" 
+						:current-page.sync="p.pageNo"
+						:page-size.sync="p.pageSize" 
+						:total="dataCount" 
+						:page-sizes="[1, 5, 10, 20, 30, 40, 50, 100, 1000]" 
+						@current-change="f5()"
+						@size-change="f5()">
+					</el-pagination>
+				</div>
+			</div>
+		</div>
+		<script>
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					sa: sa,
+					p: { // 查询参数  
+						pageNo: 1,
+						pageSize: 10,
+						id: '',
+						reqToken: '',
+						reqIp: '',
+						reqApi: '',
+						resCode: '',
+						userId: '',
+						adminId: '',
+						sTime: '',
+						eTime: '',
+						sortType: 0
+					},
+					way: sa.p('way', 1),
+					dataCount: 0,
+					dataList: [], // 数据集合
+					isNewestSta: false,	// 当前是否为最新统计数据 
+					staData: {
+						cost_time_count: 0,		// 总计耗时 
+					}
+				},
+				methods: {
+					// 刷新
+					f5: function() {
+						sa.ajax('/SgApilog/getList', sa.removeNull(this.p), function(res) {
+							this.dataList = res.data; // 数据
+							this.dataCount = res.dataCount; // 数据总数 
+							sa.f5TableHeight();		// 刷新表格高度 
+							this.isNewestSta = false;
+						}.bind(this));
+					},
+					// 统计数据
+					f5StaData: function() {
+						var fn = function() {
+							var str = '<b>总计请求:' + this.dataCount + ' 次</b><br/>'
+								+ '<b>总计耗时:' + getDuration(this.staData.cost_time_count) + ' </b>';
+							str = '<big>' + str + '</big>'
+							layer.alert(str, {title: '统计数据'});
+						}.bind(this);
+						if(this.isNewestSta) {
+							fn()
+						} else {
+							sa.ajax('/SgApilog/staBy', sa.removeNull(this.p), function(res) {
+								this.staData = res.data;
+								this.isNewestSta = true;
+								fn();
+							}.bind(this));
+						}
+					},
+					// 复制 
+					copy: function(data) {
+						// sa.showIframe('数据详情', 'api-log-info.html?id=' + data.id);
+						sa.copyText(JSON.stringify(data));
+						sa.ok2('已成功复制到剪贴板');
+					},
+					// 复制全部
+					copyAll: function() {
+						// sa.showIframe('数据详情', 'api-log-info.html?id=' + data.id);
+						sa.copyText(JSON.stringify(this.dataList));
+						sa.ok2('已成功复制到剪贴板');
+					},
+					// 查看:访问参数 
+					seeReqParame: function(data) {
+						var jsonStr = data.reqParame;
+						jsonStr = JSON.stringify(JSON.parse(jsonStr), null, "\t");
+						layer.prompt({
+							title: '请求参数',
+							shadeClose: true,	// 点击遮罩关闭 
+							formType: 2,		// 多行输入 
+							value: jsonStr,		// 要显示的字符串
+							maxlength: 9999999999,	// 最大输入字符长度
+							area: ['600px', '400px'],	// 弹窗尺寸
+							yes: function(index, layero){
+							    layer.close(index); //如果设定了yes回调,需进行手工关闭
+							}
+						})
+					},
+					// 查看:返回参数  
+					seeResString: function(data) {
+						var jsonStr = data.resString;
+						jsonStr = JSON.stringify(JSON.parse(jsonStr), null, "\t");
+						layer.prompt({
+							title: '返回参数',
+							shadeClose: true,	// 点击遮罩关闭 
+							formType: 2,		// 多行输入 
+							value: jsonStr,		// 要显示的字符串
+							maxlength: 9999999999,	// 最大输入字符长度
+							area: ['600px', '400px'],	// 弹窗尺寸
+							yes: function(index, layero){
+							    layer.close(index); //如果设定了yes回调,需进行手工关闭
+							}
+						})
+					},
+					// 删除  
+					del: function(data) {
+						sa.confirm('是否删除,此操作不可撤销', function() {
+							sa.ajax('/SgApilog/delete?id=' + data.id, function(res) {
+								sa.arrayDelete(this.dataList, data);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							}.bind(this))
+						}.bind(this));
+					},
+					// 批量删除
+					deleteByIds: function() {
+						// 获取选中元素的id列表
+						let selection = this.$refs['data-table'].selection;
+						let ids = sa.getArrayField(selection, 'id');
+						if(selection.length == 0) {
+							return sa.msg('请至少选择一条数据')
+						}
+						// 提交删除 
+						sa.confirm('是否批量删除选中数据?此操作不可撤销', function() {
+							sa.ajax('/SgApilog/deleteByIds', {ids: ids.join(',')}, function(res) {
+								sa.arrayDelete(this.dataList, selection);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							}.bind(this))
+						}.bind(this));
+					},
+					// 批量删除
+					deleteByStartEnd: function() {
+						sa.showIframe('批量删除', 'api-log-list-delete.html', '600px', '550px');
+					}
+				},
+				created: function() {
+					this.f5();
+					sa.onInputEnter();	// 监听表单动作 
+				}
+			})
+			
+			function getDuration(my_time) {
+				var days = my_time / 1000 / 60 / 60 / 24;
+				var daysRound = Math.floor(days);
+				var hours = my_time / 1000 / 60 / 60 - (24 * daysRound);
+				var hoursRound = Math.floor(hours);
+				var minutes = my_time / 1000 / 60 - (24 * 60 * daysRound) - (60 * hoursRound);
+				var minutesRound = Math.floor(minutes);
+				var seconds = my_time / 1000 - (24 * 60 * 60 * daysRound) - (60 * 60 * hoursRound) - (60 * minutesRound);
+				seconds = parseInt(seconds);
+				// console.log('转换时间:', daysRound + '天', hoursRound + '时', minutesRound + '分', seconds + '秒');
+				// var time = hoursRound + ':' + minutesRound + ':' + seconds
+				// return time;
+				if(daysRound >= 1) {
+					return daysRound + '天' + hoursRound + '小时';
+				} else if(hoursRound >= 1) {
+					return hoursRound + '小时' + hoursRound + '分';
+				} else if(minutesRound >= 1) {
+					return minutesRound + '分' + seconds + '秒';
+				} else {
+					return seconds + '秒';
+				}
+			}
+		</script>
+	</body>
+</html>

+ 133 - 0
sa-view-sp/sp-cfg/app-cfg.html

@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<html>
+	<head>
+	    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+	    <title>应用对公配置</title>
+	    <!-- 所有的 css js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css">
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<script src="../../static/kj/upload-util.js"></script>
+		<style type="text/css">
+			html,body,.vue-box{height: 100%; overflow: hidden;}
+			/* .vue-box{padding: 0px;} */
+			.c-panel{height: calc(100% - 4em); position: relative;}
+			.c-panel .c-label{width: 10em;}
+			.c-panel .el-input{width: 500px;}
+			.c-panel .el-textarea{width: 500px;}
+			.logo-img{
+				width: 35px; 
+				height: 35px; 
+				border-radius: 2px; 
+				vertical-align: middle; 
+				margin-right: 0.5em;
+				cursor: pointer;
+			}
+			.s-tab{height: 100%; }
+			.el-tabs__content{height: calc(100% - 130px); overflow: auto;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<div class="c-panel" v-if="m != null">
+				<!-- tab卡片 -->
+				<el-tabs class="s-tab" v-model="activeTab">
+					<!-- ---------------------------------- 系统参数 ---------------------------------- -->
+				    <el-tab-pane label="系统参数" name="tab1">
+						<div class="c-item br">
+							<label class="c-label">系统logo:</label>
+							<img :src="m.logoUrl" class="logo-img" v-if="sa.isNull(m.logoUrl) == false" @click="sa.showImage(m.logoUrl, '400px', '400px')">
+							<el-link type="primary" @click="sa.uploadImage(src => {m.logoUrl = src; sa.ok2('上传成功');})">选择上传</el-link>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">系统名称:</label>
+							<el-input v-model="m.appName"></el-input>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">版本编号:</label>
+							<el-input v-model="m.appVersionNo"></el-input>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">更新描述:</label>
+							<el-input v-model="m.appVersionLog"></el-input>
+						</div>
+					</el-tab-pane>
+					
+					<!-- ---------------------------------- 其它配置 ---------------------------------- -->
+					<el-tab-pane label="其它配置" name="tab2">
+						<br>
+						<span>其它配置</span>
+					</el-tab-pane>
+					
+				</el-tabs>
+				
+				<!-- 确定按钮 -->
+				<div style="position: absolute; bottom: 0px; width: calc(100% - 3em); line-height: 80px; background-color: #FFF;">
+					<hr style="height: 2px;">
+					<div class="c-item">
+						<label class="c-label"></label>
+						<el-button type="primary" icon="el-icon-check" @click="ok">保存修改</el-button>
+						<el-button type="primary" icon="el-icon-refresh-right" @click="f5">重置</el-button>
+					</div>
+				</div>
+			</div>
+		</div>
+		<script type="text/javascript">
+			// 创建一个默认的配置对象
+			function create_m() {
+				return {
+					logoUrl: '',	// 系统logo地址 
+					appName: 'sa-plus快速开发框架'	,// 系统名称
+					appVersionNo: 'v1.0.0',	// 系统版本
+					appVersionLog: '更新于2099-10-1',	// 更新日志 
+				}
+			}
+		</script>
+        <script>
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					sa: sa, 	// 超级对象
+					m: null,		// 
+					activeTab: 'tab1',  // 当前显示的tab
+					textareaCfg: { minRows: 3, maxRows: 14} // 文本域的默认配置
+				},
+				methods: {
+					// 初始化配置
+					init: function(str) {
+						// 获取 
+						var cfg = sa.JSONParse(str, {});	// 用户配置
+						var default_cfg = create_m();		// 默认配置
+						// 遍历 
+						for(var key in default_cfg) {
+							if(cfg[key] !== undefined && cfg[key] !== null) {
+								default_cfg[key] = cfg[key];
+							}
+						}
+						// 赋值
+						this.m = default_cfg;
+					},
+					// 刷新
+					f5: function(){
+						sa.ajax('/SpCfg/getCfg', {cfgName: 'app_cfg'}, function(res){
+							this.init(res.data);
+						}.bind(this));
+					},
+					// 提交 
+					ok: function(){
+						sa.ajax('/SpCfg/updateCfg', {cfgName: 'app_cfg', cfgValue: JSON.stringify(this.m)}, function(res){
+							sa.ok2('保存成功');
+						}.bind(this));
+					},
+				},
+				created: function(){
+					this.f5(); 
+				}
+			})
+		</script>
+	</body>
+</html>

+ 131 - 0
sa-view-sp/sp-cfg/server-cfg.html

@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<title>服务器私有配置</title>
+		<!-- 所有的 css js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css">
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<script src="../../static/kj/upload-util.js"></script>
+		<style type="text/css">
+			html,body,.vue-box{height: 100%; overflow: hidden;}
+			/* .vue-box{padding: 0px;} */
+			.c-panel{height: calc(100% - 4em); position: relative;}
+			.c-panel .c-label{width: 10em;}
+			.c-panel .el-input{width: 500px;}
+			.c-panel .el-textarea{width: 500px;}
+			.logo-img{
+				width: 35px; 
+				height: 35px; 
+				border-radius: 2px; 
+				vertical-align: middle; 
+				margin-right: 0.5em;
+				cursor: pointer;
+			}
+			.s-tab{height: 100%; }
+			.el-tabs__content{height: calc(100% - 130px); overflow: auto;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<div class="c-panel" v-if="m != null">
+				<!-- tab卡片 -->
+				<el-tabs class="s-tab" v-model="activeTab">
+					<!-- ---------------------------------- 系统参数 ---------------------------------- -->
+					<el-tab-pane label="系统参数" name="tab1">
+						<!-- <div class="c-item br">
+							<label class="c-label">预留信息:</label>
+							<el-input v-model="m.reserve_info"></el-input>
+						</div> -->
+						<div class="c-item">
+							<label class="c-label">抛出SQL:</label>
+							<el-switch v-model="m.throwOutSql" :active-value="1" :inactive-value="2"></el-switch>
+							<span class="c-remark" v-if="m.throwOutSql==1">开启</span>
+							<span class="c-remark" v-else>关闭</span>
+							<span class="c-remark" style="color: red;">( 抛出sql只为方便调试,建议只在开发环境下打开此选项,生产环境请务必关闭 )</span>
+						</div>
+					</el-tab-pane>
+				</el-tabs>
+				
+				<!-- ---------------------------------- 其它配置 ---------------------------------- -->
+				<el-tab-pane label="其它配置" name="tab2">
+					<br>
+					<span>其它配置</span>
+				</el-tab-pane>
+
+				<!-- 确定按钮 -->
+				<div style="position: absolute; bottom: 0px; width: calc(100% - 3em); line-height: 80px; background-color: #FFF;">
+					<hr style="height: 2px;">
+					<div class="c-item">
+						<label class="c-label"></label>
+						<el-button type="primary" icon="el-icon-check" @click="ok">保存修改</el-button>
+						<el-button type="primary" icon="el-icon-refresh-right" @click="f5">重置</el-button>
+					</div>
+				</div>
+			</div>
+		</div>
+		<script type="text/javascript">
+			// 创建一个默认的配置对象
+			function create_m() {
+				return {
+					reserve_info: '预留信息', // 预留信息 
+					throwOutSql: 2,	// 是否隐藏sql,
+				}
+			}
+		</script>
+		<script>
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					sa: sa, // 超级对象
+					m: null, // 
+					activeTab: 'tab1'
+				},
+				methods: {
+					// 初始化配置
+					init: function(str) {
+						// 获取 
+						var cfg = sa.JSONParse(str, {}); // 用户配置  
+						var default_cfg = create_m(); // 默认配置  
+						// 遍历 
+						for (var key in default_cfg) {
+							if (cfg[key] !== undefined && cfg[key] !== null) {
+								default_cfg[key] = cfg[key];
+							}
+						}
+						// 赋值
+						this.m = default_cfg;
+					},
+					// 刷新
+					f5: function() {
+						sa.ajax('/SpCfg/getCfg', {
+							cfgName: 'server_cfg'
+						}, function(res) {
+							this.init(res.data);
+						}.bind(this));
+					},
+					// 提交 
+					ok: function() {
+						sa.ajax('/SpCfg/updateCfg', {
+							cfgName: 'server_cfg',
+							cfgValue: JSON.stringify(this.m)
+						}, function(res) {
+							sa.ok2('保存成功');
+						}.bind(this));
+					}
+				},
+				created: function() {
+					this.f5();
+				}
+			})
+
+
+
+		</script>
+	</body>
+</html>

+ 344 - 0
sa-view-sp/sp-console/redis-console.html

@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>redis控制台</title>
+		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css">
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style type="text/css">
+			.card-box {
+				min-width: 230px;
+				margin-right: 10px;
+				margin-bottom: 10px;
+				display: inline-block;
+				background-color: #f5f5f5;
+				cursor: pointer;
+				transition: all 0.3s;
+			}
+			.card-box:hover{box-shadow: 0 0 20px #999;}
+
+			.card-box .prop-name {
+				padding-left: 14px;
+				padding-top: 14px;
+				color: #666;
+			}
+
+			.card-box .prop-value {
+				/* border: 1px #000 solid; */
+				padding-left: 14px;
+				height: 40px;
+				line-height: 40px;
+				padding-top: 10px;
+				padding-bottom: 10px;
+				font-size: 26px;
+				color: green;
+			}
+			
+			.f5-pre-btn:hover{cursor: pointer; text-decoration: underline;}
+			
+			.key-div{color: green; cursor: pointer;font-weight: bold;}
+			.not-show,.is-show{padding: 5px 10px; background-color: #eee; cursor: pointer; }
+			.is-show{background-color: rgba(0,0,0,0);}
+			
+			/* .k-input input{font-weight: bold;} */
+			
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<!-- ------------- 总览 ------------- -->
+			<div class="c-panel">
+				<div class="c-title">
+					Redis 控制台
+					<span class="f5-pre-btn" @click="f5_pre(false)">刷新</span>
+				</div>
+				<div style="height: 10px;"></div>
+				<div>
+					<div class="card-box" @click="sa.msg('表点我 >_<')">
+						<p class="prop-name">键值总数</p>
+						<p class="prop-value">{{preData.keys_count}}</p>
+					</div>
+					<div class="card-box" @click="sa.msg('表点我 >_<')">
+						<p class="prop-name">命中次数</p>
+						<p class="prop-value">{{preData.keyspace_hits}}</p>
+					</div>
+					<div class="card-box" @click="sa.msg('表点我 >_<')">
+						<p class="prop-name">已用内存</p>
+						<p class="prop-value">{{preData.used_memory_human}}</p>
+					</div>
+					<div class="card-box" @click="sa.msg('表点我 >_<')">
+						<p class="prop-name">内存峰值</p>
+						<p class="prop-value">{{preData.used_memory_peak_human}}</p>
+					</div>
+					<div class="card-box" @click="sa.msg('表点我 >_<')">
+						<p class="prop-name">启动时间</p>
+						<p class="prop-value">{{preData.uptime_in_seconds_str}}</p>
+					</div>
+				</div>
+			</div>
+			<!-- ------------- 检索参数 ------------- -->
+			<div class="c-panel c-table">
+				<div class="c-title">搜索键值</div>
+				<div class="c-item">
+					<!-- <label class="c-label">搜索键值:</label> -->
+					<el-input v-model="p.k" class="k-input" :placeholder="isLike ? '当前为模糊搜索' : '当前为精确搜索'" @keyup.native.enter="f5()" style="width: 400px;"></el-input>
+				</div>
+				<div class="c-item" style="min-width: 0px;">
+					<el-button type="primary" icon="el-icon-search" @click="f5()">查询</el-button>
+					<el-button type="success" icon="el-icon-plus" @click="add()">添加</el-button>
+					<el-button type="danger" icon="el-icon-delete" @click="deleteByKeys()">删除</el-button>
+					<el-button type="info" icon="el-icon-sort" @click="isLike = !isLike; sa.ok('切换成功')">
+						{{isLike ? '切换为精确搜索' : '切换为模糊搜索'}}
+					</el-button>
+				</div>
+				<div style="height: 10px;"></div>
+				<el-table class="data-table" ref="data-table" :data="dataListShow" size="small">
+					<!-- <el-table-column label="键"></el-table-column> -->
+					<el-table-column type="selection" width="45px"></el-table-column>
+					<el-table-column label="键">
+						<template slot-scope="s">
+							<div class="key-div" @click="sa.copyText(s.row.key); sa.msg('复制成功')">{{s.row.key}}</div>
+						</template>
+					</el-table-column>
+					<el-table-column label="值">
+						<template slot-scope="s">
+							<div class="not-show" @click="get(s.row)" v-if="s.row.is_show == false">点击加载</div>
+							<div class="is-show" v-if="s.row.is_show == true" @click="sa.copyText(s.row.value); sa.msg('复制成功')">{{s.row.value}}</div>
+						</template>
+					</el-table-column>
+					<el-table-column label="TTL (秒)" prop="ttl" width="150px"></el-table-column>
+					<el-table-column label="操作" width="250px">
+						<template slot-scope="s">
+							<el-button type="text" @click="get(s.row)">查询</el-button>
+							<el-button type="text" @click="updateValue(s.row)">修改值</el-button>
+							<el-button type="text" @click="updateTTL(s.row)">修改TTL</el-button>
+							<el-button type="text" @click="del(s.row)">删除</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+				<div class="page-box">
+					<el-pagination background
+						layout="total, prev, pager, next, sizes, jumper" 
+						:current-page.sync="p.pageNo" 
+						:page-size.sync="p.pageSize" 
+						:total="dataCount" 
+						:page-sizes="[1, 10, 20, 50, 100, 1000]" 
+						@current-change="f5ByPage()" 
+						@size-change="f5ByPage()">
+					</el-pagination>
+				</div>
+			</div>
+
+		</div>
+
+		<script>
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					sa: sa, // 超级对象
+					p: { // 查询参数 
+						k: '',
+						pageNo: 1,		// 当前页 
+						pageSize: 10,	// 页大小 
+						sortType: 0	// 排序方式 
+					},
+					isLike: true,	// 是否为模糊匹配
+					dataCount: 0,
+					preData: {
+						keys_count: 0, // key 总数 
+						keyspace_hits: 0,	// 被命中次数 
+						used_memory_human: 0, // 已经占用内存数量 
+						used_memory_peak_human: 0, // 内存消耗峰值 
+						uptime_in_seconds: 0, // redis 已经启动的秒数 
+						uptime_in_seconds_str: '0', // redis 已经启动的时间 
+					},
+					dataList: [],
+					dataListShow: [],
+				},
+				methods: {
+					// 根据分页信息显示出来
+					f5ByPage: function() {
+						var dataListShow = [];
+						var start = (this.p.pageNo - 1) * this.p.pageSize;
+						var end = this.p.pageNo * this.p.pageSize;
+						for (var i = start; i < end; i++) {
+							if(i >= this.dataList.length) {
+								break;
+							}
+							dataListShow.push(this.dataList[i]);
+						}
+						this.dataListShow  = dataListShow;
+						sa.f5TableHeight();		// 刷新表格高度 
+					},
+					// 查询key列表 
+					f5: function() {
+						let k = this.p.k;
+						if(this.isLike && k != '') {
+							k = '*' + k + '*';
+						}
+						sa.ajax('/RedisConsole/getKeys', {k: k}, function(res) {
+							var dataList = [];
+							for (var i = 0; i < res.data.length; i++) {
+								dataList.push({
+									key: res.data[i],	// key
+									value: '',		// value 
+									is_show: false,	// 是否已经显示详情 
+									ttl: '未加载',			// 过期时间 
+								})
+							}
+							this.dataList = dataList;
+							this.f5ByPage();
+							this.dataCount = this.dataList.length;
+						}.bind(this), {
+							success501: function(res) {
+								sa.msg(res.msg);
+								this.dataList = [];
+								this.f5ByPage();
+								this.dataCount = 0;
+							}.bind(this)
+						}); 
+					},
+					// 刷新预览 
+					f5_pre: function(is_f5_keys) {
+						// 基本预览信息
+						sa.ajax('/RedisConsole/getPreInfo', this.p, function(res) {
+							res.data.uptime_in_seconds_str = getDuration(parseInt(res.data.uptime_in_seconds) * 1000);
+							this.preData = res.data;
+							// 如果指定不查询keys列表 
+							if(is_f5_keys === false) {
+								return;
+							}
+							// 如果超过了最大值,则提示一下
+							if(res.data.isGtMax) {
+								var tipStr = 'key值数量已达' + this.preData.keys_count + ',为了避免卡顿已取消返回结果列表(您可以增加筛选条件缩短记录总数)';
+								tipStr = '<b style="color: red;">' + tipStr + '</b>';
+								sa.alert(tipStr);
+							} else {
+								this.f5();
+							}
+						}.bind(this)); 
+					},
+					// 加载详情
+					get: function(data) {
+						sa.ajax('/RedisConsole/getByKey?key=' + data.key, function(res) {
+							data.value = res.data.value;
+							data.ttl = res.data.ttl;
+							data.is_show = true;
+							sa.f5TableHeight();	// 刷新表格高度
+						}.bind(this)); 
+					},
+					// 删除
+					del: function(data) {
+						sa.confirm('是否删除,此操作不可撤销', function() {
+							sa.ajax('/RedisConsole/del?key=' + data.key, function(res) {
+								sa.arrayDelete(app.dataListShow, data);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							})
+						});
+					},
+					// 修改键值 
+					updateValue: function(data) {
+						layer.prompt({
+							title: '修改键值',
+							// shadeClose: true,	// 点击遮罩关闭 
+							formType: 2,		// 多行输入 
+							maxlength: 9999999999,	// 最大输入字符长度
+							area: ['600px', '400px'],	// 弹窗尺寸
+						}, function(pass, index, elem){
+							layer.close(index); //如果设定了yes回调,需进行手工关闭
+							sa.ajax('/RedisConsole/updateValue', {key: data.key, value: pass}, function(res){
+								data.value = pass;
+								layer.msg('修改成功');
+								sa.f5TableHeight();	// 刷新表格高度
+							})
+						});
+					},
+					// 修改ttl 
+					updateTTL: function(data) {
+						sa.prompt('修改TTL', function(pass, index){
+							if(isNaN(pass)) {
+								return sa.error('请输入一个数值');
+							}
+							sa.ajax('/RedisConsole/updateTtl', {key: data.key, ttl: pass}, function(res){
+								data.ttl = pass;
+								sa.ok('修改成功');
+							})
+						});
+					},
+					// 添加
+					add: function() {
+						sa.showIframe('添加键值', 'redis-key-add.html', '800px', '510px');
+					},
+					// 根据id列表删除 
+					deleteByKeys: function() {
+						// 获取选中元素的id列表
+						let selection = this.$refs['data-table'].selection;
+						let keys = sa.getArrayField(selection, 'key');
+						
+						// 判断
+						if(keys.length < 1) {
+							return sa.error('请至少选择一行');
+						}
+						// 删除 
+						sa.confirm('是否删除选中记录,此操作不可撤销', function() {
+							sa.ajax('/RedisConsole/deleteByKeys', {key: keys}, function(res) {
+								sa.arrayDelete(this.dataListShow, selection);
+								sa.ok2('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							}.bind(this))
+						}.bind(this));
+					},
+					// 刷新秒数 
+					auto_f5_run_time: function() {
+						setInterval(function() {
+							if(this.preData.uptime_in_seconds <= 0 || this.preData.uptime_in_seconds > 60 * 60 * 24) {
+								return;
+							}
+							this.preData.uptime_in_seconds++;
+							this.preData.uptime_in_seconds_str = getDuration(parseInt(this.preData.uptime_in_seconds) * 1000);
+						}.bind(this), 1000);
+					}
+				},
+				created: function() {
+					this.f5_pre();
+					this.auto_f5_run_time();
+				}
+			})
+
+
+
+			function getDuration(my_time) {
+				var days = my_time / 1000 / 60 / 60 / 24;
+				var daysRound = Math.floor(days);
+				var hours = my_time / 1000 / 60 / 60 - (24 * daysRound);
+				var hoursRound = Math.floor(hours);
+				var minutes = my_time / 1000 / 60 - (24 * 60 * daysRound) - (60 * hoursRound);
+				var minutesRound = Math.floor(minutes);
+				var seconds = my_time / 1000 - (24 * 60 * 60 * daysRound) - (60 * 60 * hoursRound) - (60 * minutesRound);
+				seconds = parseInt(seconds);
+				// console.log('转换时间:', daysRound + '天', hoursRound + '时', minutesRound + '分', seconds + '秒');
+				// var time = hoursRound + ':' + minutesRound + ':' + seconds
+				// return time;
+				if(daysRound >= 1) {
+					return daysRound + '天' + hoursRound + '小时';
+				} else if(hoursRound >= 1) {
+					return hoursRound + '小时' + hoursRound + '分';
+				} else if(minutesRound >= 1) {
+					return minutesRound + '分' + seconds + '秒';
+				} else {
+					return seconds + '秒';
+				}
+			}
+			// console.log(getDuration(200000));;
+		</script>
+
+	</body>
+</html>

+ 106 - 0
sa-view-sp/sp-console/redis-key-add.html

@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+	<head>
+	    <title>Redis-key值添加</title>
+	    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style type="text/css">
+			.c-panel .c-label{width: 6em;}
+			/*  普通文本和富文本一起变长  */
+			.c-panel .el-form .el-input, .c-panel .el-form .el-textarea__inner{width: 600px;}
+			body{background-color: #FFF;}
+		</style>
+ 
+	</head>
+	<body>
+		<div class="vue-box sbot" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<div class="c-panel">
+					<div class="c-title">数据添加</div>
+					<el-form v-if="m">
+						<div class="c-item br">
+							<label class="c-label">key:</label>
+							<el-input v-model="m.key"></el-input>
+						</div>
+						<div class="c-item br">
+							<label class="c-label" style="vertical-align: top;">value:</label>
+							<div style="display: inline-block;">
+								<el-input v-model="m.value" type="textarea" :autosize="{ minRows: 14, maxRows: 20}"></el-input>
+							</div>
+						</div>
+						<div class="c-item br">
+							<label class="c-label">ttl:</label>
+							<el-input v-model="m.ttl" placeholder="过期时间 单位/毫秒"></el-input>
+						</div>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="primary" @click="ok()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+		</div>
+		<script type="text/javascript">
+			function crate_model() {
+				return {
+					key: '',
+					value: '',
+					ttl: '',
+					is_show: true
+				}
+			}
+		</script>
+        <script>
+			
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					sa: sa, 	// 超级对象
+					m: crate_model()
+				},
+				methods: {
+					// 修改
+					ok: function(){
+						// 开始验证
+						var m = this.m;
+						if(m.key == ''){
+							return sa.error('请输入键');
+						}
+						if(m.value == ''){
+							return sa.error('请输入值');
+						}
+						if(m.ttl == ''){
+							return sa.error('请输入ttl (过期时间)');
+						}
+						if(isNaN(m.ttl)) {
+							return sa.error('ttl 必须是一个数字 ');
+						}
+						// 添加
+						m.ttl = parseInt(m.ttl);
+						sa.ajax('/RedisConsole/set', m, function(res){
+							sa.closeCurrIframe();
+							parent.app.dataListShow.unshift(m);
+							parent.sa.msg('添加成功');
+							parent.sa.f5TableHeight();	// 刷新表格高度
+						}.bind(this));
+					}
+				},
+				mounted: function(){
+					
+				}
+			})
+			
+			
+		</script>
+	</body>
+</html>

+ 20 - 0
sa-view-sp/sp-console/sql-console.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="utf-8">
+		<title>SQL控制台</title>
+	</head>
+	<body>
+		<div style="padding: 1em;">
+			加载中...
+		</div>
+		<script src="../../static/sa.js"></script>
+		<script type="text/javascript">
+			setTimeout(function() {
+				// 跳转到sql监控页
+				console.log(sa.cfg.api_url + '/druid/sql.html');
+				location.href = sa.cfg.api_url + '/druid/sql.html';
+			}, 100)
+		</script>
+	</body>
+</html>

+ 63 - 0
sa-view-sp/sp-role/menu-list.html

@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>菜单预览</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+        <style>
+			body,.el-tree{background-color: #eee;}
+			.el-tree-node{margin: 0.15em 0 !important;}
+			/* 悬浮时颜色更深一点 */
+			.el-tree-node__content:hover{background-color: #CFE8FC !important;}
+        </style>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<!-- 表格 -->
+			<div style="padding: 0 1em;">
+				<div class="c-title">菜单预览</div>
+		        <!-- 树插件 -->
+				<el-tree
+					ref="tree"
+					:data="dataList"
+					node-key="id"
+					:default-expand-all="true"
+					>
+					<span class="custom-tree-node" slot-scope="s">
+						<span style="color: #2D8CF0;" v-if="s.data.isShow == undefined || s.data.isShow == true">{{ s.data.name }}</span>
+						<span style="color: #999;" v-if="s.data.isShow == false">{{ s.data.name }} (隐藏)</span>
+						<span style="color: #999;" v-if="s.data.info">&emsp;———— {{s.data.info}} </span>
+					</span>
+				</el-tree>
+				<br><br><br>
+			</div>
+		</div>
+		<script src="../../sa-frame/menu-list.js"></script>
+		<script src="../../sa-frame/menu-list-sp.js"></script>
+		<script src="../../sa-frame/index/admin-util.js"></script>
+        <script>
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					dataList: [],	// 数据集合 
+				},
+				created: function(){
+					// 全部
+					sa.ajax2('/SysMenu/getList', function(res){
+						menuList = sa_admin_code_util.arrayToTree(menuList);	// 一维转tree 
+						menuList = sa_admin_code_util.refMenuList(menuList);	// 属性处理 
+						this.dataList = menuList;	// 数据  
+					}.bind(this));
+				}
+			})
+		</script>
+	</body>
+</html>

+ 168 - 0
sa-view-sp/sp-role/menu-setup.html

@@ -0,0 +1,168 @@
+
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>权限分配</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+        <style>
+			body,.el-tree{background-color: #eee;}
+			.el-tree-node{margin: 0.15em 0 !important;}
+			/* 悬浮时颜色更深一点 */
+			.el-tree-node__content:hover{background-color: #CFE8FC !important;}
+        </style>
+	</head>
+	<body>
+		<div class="vue-box sbot" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<!-- 表格 -->
+				<div style="padding: 1em 2em;">
+					<el-form>
+						<div class="c-title">所有权限</div>
+						<!-- 此扩展能递归渲染一个权限树,点击深层次节点,父级节点中没有被选中的节点会被自动选中,单独点击父节点,子节点会全部 选中/去选中 -->
+						<el-tree
+							ref="tree"
+							:data="dataList"
+							show-checkbox 
+							node-key="id"
+							:default-expand-all="true"
+							:default-checked-keys="selectList" 
+							:expand-on-click-node="false"
+							:check-on-click-node="true"
+							:check-strictly="true"
+							@node-click="node_click"
+							@check="node_click"
+							>
+							<span class="custom-tree-node" slot-scope="s">
+								<span style="color: #2D8CF0;" v-if="s.data.isShow == undefined || s.data.isShow == true">{{ s.data.name }}</span>
+								<span style="color: #999;" v-if="s.data.isShow == false">{{ s.data.name }} (隐藏)</span>
+								<span style="color: #999;" v-if="s.data.info">&emsp;———— {{s.data.info}} </span>
+							</span>
+						</el-tree>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="success" @click="checkedAll()">全选</el-button>
+				<el-button type="primary" @click="ok()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+		</div>
+		<script src="../../sa-frame/menu-list.js"></script> 
+		<script src="../../sa-frame/menu-list-sp.js"></script> 
+		<script src="../../sa-frame/index/admin-util.js"></script>
+        <script>
+			var roleId = sa.p('roleId');
+			var app = new Vue({
+				el: '.vue-box',
+				data: {
+					p: [],
+					dataList: [],	// 数据集合 
+					selectList: [],	// 默认选中
+					ywList: [],		// 一维数组 
+					haveList: []		// 这个角色用的权限id,拷贝 
+				},
+				methods: {
+					// 保存
+					ok: function(clickCount){
+						if(clickCount === undefined) {
+							clickCount = 5;
+						}
+						// 判断是否改掉了关键权限 
+						var keys = this.$refs.tree.getCheckedKeys();		// 设置完拥有的id列表 
+						var rArr = ['1', '99', 'auth', 'role-list'];		// 敏感菜单id列表 
+						var isR = false;									// 是否给改掉了 
+						rArr.forEach(function(item) {
+							// 只有原先有,现在没有,才会被这样判定 
+							if(this.haveList.indexOf(item) > -1 && keys.indexOf(item) == -1) {
+								isR = true;
+								console.log(item);
+								console.log(this.haveList);
+							}
+						}.bind(this))
+						// 提示 
+						if(isR) {
+							var tipStr = '危险!系统检测到您取消了此角色的重要权限,这将导致与之关联的账号可能会无法正常使用后台,您无论如何都要这样设置吗?';
+							tipStr += '<br/>为保证您不是误操作,您还需要继续点击按钮: ' + clickCount + '次'
+							tipStr = '<b style="color: red;">' + tipStr + '</b>';
+							sa.confirm(tipStr, function(res) {
+								if(clickCount <= 1) {
+									this.ok2();
+								} else {
+									clickCount--;
+									this.ok(clickCount);
+								}
+							}.bind(this))
+						} else {
+							this.ok2();
+						}
+					},
+					// 开始设置 
+					ok2: function() {
+						var str = '';
+						var keys = this.$refs.tree.getCheckedKeys();
+						keys.forEach(function(ts){
+							str += 'code=' + ts + '&';
+						})
+						var url = '/SpRolePermission/updatePcodeByRid?roleId=' + roleId;
+						sa.ajax(url, str,function (res) {
+							sa.alert('设置成功', function(){
+								sa.closeCurrIframe();
+							});
+							// 如果设置的角色与当前登录者的角色一致,则立即显示出来							
+							if(roleId == sa.$sys.getCurrUser().roleId) {
+								top.sa_admin.initMenu(keys);
+							}
+						}.bind(this))
+					},
+					// 点击回调, 处理其子节点跟随父节点的选中
+					node_click: function(node) {
+						var is_select = this.$refs.tree.getCheckedKeys().indexOf(node.id) != -1;	// 此节点现在是否被选中 
+						if(node.children){
+							node.children.forEach(function(item) {
+								this.$refs.tree.setChecked(item.id, is_select);
+								// 递归
+								if(item.children) {
+									this.node_click(item);
+								}
+							}.bind(this))
+						}
+					},
+					// 全选/ 取消全选
+					checkedAll: function() {
+						// console.log(this.$refs.tree.getCheckedKeys().length);
+						// console.log(this.ywList.length);
+						if(this.$refs.tree.getCheckedKeys().length != this.ywList.length) {
+							this.$refs['tree'].setCheckedNodes(this.ywList);
+						} else {
+							this.$refs['tree'].setCheckedNodes([]);
+						}
+					}
+				},
+				created: function(){
+					// 全部
+					menuList = sa_admin_code_util.arrayToTree(menuList);	// 一维转tree 
+					menuList = sa_admin_code_util.refMenuList(menuList);	// 属性处理 
+					this.dataList = menuList;	// 数据  
+					this.ywList = sa_admin_code_util.treeToArray(this.dataList);
+						
+					// 拉取此 roleId 的
+					sa.ajax('/SpRolePermission/getPcodeByRid?roleId=' + roleId, function(res) {
+						this.selectList = res.data;		// 选中的列表 
+						this.haveList = [].concat(this.selectList);
+					}.bind(this))
+				}
+			})
+		</script>
+	</body>
+</html>

+ 104 - 0
sa-view-sp/sp-role/role-add.html

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>角色-添加/修改</title>
+		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+		<style type="text/css">
+			.c-panel .el-form .c-label{width: 6em !important;}
+			.c-panel .el-form .el-input{width: 250px;}
+		</style>
+	</head>
+	<body>
+		<div class="vue-box" :class="{sbot: id}" style="display: none;" :style="'display: block;'">
+			<!-- ------- 内容部分 ------- -->
+			<div class="s-body">
+				<div class="c-panel">
+					<div class="c-title">数据添加</div>
+					<el-form v-if="m">
+						<!-- no字段: m.id - id -->
+						<sa-item type="text" name="角色id" v-model="m.id" br></sa-item>
+						<sa-item type="text" name="角色昵称" v-model="m.name" br></sa-item>
+						<sa-item type="text" name="责任描述" v-model="m.info" br></sa-item>
+						<sa-item name="" class="s-ok" br>
+							<el-button type="primary" icon="el-icon-plus" @click="ok()">保存</el-button>
+						</sa-item>
+					</el-form>
+				</div>
+			</div>
+			<!-- ------- 底部按钮 ------- -->
+			<div class="s-foot">
+				<el-button type="primary" @click="ok()">确定</el-button>
+				<el-button @click="sa.closeCurrIframe()">取消</el-button>
+			</div>
+		</div>
+        <script>
+			
+			var app = new Vue({
+				components: {
+					"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue'),
+				},
+				el: '.vue-box',
+				data: {
+					id: sa.p('id', 0),		// 获取超链接中的id参数(0=添加,非0=修改) 
+					m: null,		// 实体对象 
+				},
+				methods: {
+					// 创建一个 默认Model 
+					createModel: function() {
+						return {
+							id: '', 
+							name: '',
+							info: '',
+							isLock: 2,
+							// createTime: new Date(),
+							is_update: false,
+						}
+					},
+					// 提交数据 
+					ok: function(){
+						// 验证 
+						let m = this.m;		// 获取 m对象 
+						sa.checkNull(m.name, '请输入角色名字');
+						sa.checkNull(m.info, '请输入责任描述');
+						
+						// 开始增加
+						sa.ajax('/role/add', this.m, function(res){
+							sa.alert('增加成功', function() {
+								if(parent.app) {
+									res.data.is_update = false;
+									parent.app.dataList.push(res.data);
+									parent.sa.f5TableHeight();		// 刷新表格高度 
+									sa.closeCurrIframe();	// 关闭本页 
+								} else {
+									app.m = this.createModel();
+								}
+							}.bind(this)); 
+						}.bind(this));
+					},
+					// 添加/修改 完成后的动作
+					clean: function() {
+						if(this.id == 0) {
+							this.m = this.createModel();
+						} else {
+							parent.app.f5();		// 刷新父页面列表
+							sa.closeCurrIframe();	// 关闭本页 
+						}
+					}
+				},
+				mounted: function(){
+					this.m = this.createModel();
+				}
+			})
+		</script>
+	</body>
+</html>

+ 138 - 0
sa-view-sp/sp-role/role-list.html

@@ -0,0 +1,138 @@
+
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>角色列表</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
+		<!-- 所有的 css & js 资源 -->
+		<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
+		<link rel="stylesheet" href="../../static/sa.css"> 
+		<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
+		<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
+		<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
+		<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+		<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+		<script src="../../static/sa.js"></script>
+	</head>
+	<body>
+		<div class="vue-box" style="display: none;" :style="'display: block;'">
+			<div class="c-panel">
+				<!-- 参数栏 -->
+				<div class="c-title">检索参数</div>
+				<el-form @submit.native.prevent>
+					<sa-item type="text" name="角色名称" v-model="p.name"></sa-item>
+					<el-button type="primary" icon="el-icon-search" @click="f5()">查询</el-button>
+					<el-button type="primary" icon="el-icon-plus" @click="add()">新增</el-button>
+				</el-form>
+				<!-- 数据列表 -->
+				<el-table class="data-table" ref="data-table" :data="dataList">
+					<el-table-column label="编号" prop="id" width="70px" > </el-table-column>
+					<el-table-column label="角色名称">
+						<template slot-scope="s">
+							<el-input v-if="s.row.is_update" v-model="s.row.name"></el-input>
+							<span v-else>{{s.row.name}}</span>
+						</template>
+					</el-table-column>
+					<el-table-column label="责任描述">
+						<template slot-scope="s">
+							<el-input v-if="s.row.is_update" v-model="s.row.info"></el-input>
+							<span v-else>{{s.row.info}}</span>
+						</template>
+					</el-table-column>
+					<el-table-column label="是否锁定" title="锁定的角色为系统维持正常运行的重要角色,不可删除">
+						<template slot-scope="s">
+							{{s.row.isLock == 1 ? '是' : '否'}}
+						</template>
+					</el-table-column>
+					<el-table-column label="创建日期">
+						<template slot-scope="s">
+							{{sa.forDate(s.row.createTime, 2)}}
+						</template>
+					</el-table-column>
+					<el-table-column label="操作" width="220px">
+						<template slot-scope="s">
+							<el-button type="text" @click="update(s.row)">
+								<span :style="s.row.is_update ? 'color: red;' : ''">修改</span>
+							</el-button>
+							<el-button type="text" @click="del(s.row)">删除</el-button>
+							<el-button type="text" @click="menu_setup(s.row)">分配权限</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+				<!-- ------------- 分页 ------------- -->
+				<sa-item type="page" :curr.sync="p.pageNo" :size.sync="p.pageSize" :total="dataList.length" :sizes="[1000]" @change="f5()"></sa-item>
+			</div>
+		</div>
+        <script>
+			var app = new Vue({
+				components: {
+					"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue'),
+				},
+				el: '.vue-box',
+				data: {
+					sa: sa, 	// 超级对象
+					p: {	// 查询参数 
+						name: '',
+						pageNo: 1,
+						pageSize: 1000,
+					},
+					dataList: [],	// 数据集合
+				},
+				methods: {
+					// 刷新
+					f5: function(){
+						sa.ajax('/role/getList', this.p, function(res) {
+							this.dataList = sa.listAU(res.data);
+							sa.f5TableHeight();		// 刷新表格高度 
+						}.bind(this));
+					},
+					// 修改
+					update: function (data) {
+						if(data.is_update == false) {
+							data.is_update = true;
+						} else {
+							sa.confirm('是否修改数据?', function(){
+								var data2 = sa.copyJSON(data);
+								data2.createTime = undefined;
+								sa.ajax('/role/update', data2, function(res){
+									sa.ok('修改成功');
+									data.is_update = false;
+								})
+							})
+						}
+					},
+					// 删除
+					del: function (data) {
+						if(data.isLock == 1){
+							return layer.alert('此角色是维持系统正常运行的重要角色,已被锁定,不可删除');
+						};
+						sa.confirm('是否删除,此操作不可撤销', function(){
+							sa.ajax('/role/delete', {id: data.id},function(res){
+								sa.arrayDelete(app.dataList, data);
+								sa.ok('删除成功');
+								sa.f5TableHeight();		// 刷新表格高度 
+							})
+						});
+					},
+					// 添加
+					add: function () {
+						sa.showIframe('新增角色', 'role-add.html?id=-1', '420px', '280px');
+					}, 
+					// 修改权限菜单
+					menu_setup: function(data){
+						var title = '为 ['+data.name+'] 分配权限';
+						sa.showIframe(title, 'menu-setup.html?roleId=' + data.id, '700px', '600px');
+					}
+				},
+				created: function(){
+					this.f5();
+					sa.onInputEnter();	// 监听表单回车执行查询 
+				}
+			})
+			
+			
+		
+		</script>
+	</body>
+</html>

BIN
static/icon/icon-article.png


BIN
static/icon/icon-comment.png


BIN
static/icon/icon-goods.png


BIN
static/icon/icon-money.png


BIN
static/icon/icon-order.png


BIN
static/icon/icon-user.png


BIN
static/img/kulian.png


BIN
static/img/up-icon.png


+ 276 - 0
static/kj/upload-util.js

@@ -0,0 +1,276 @@
+// ======================= upload-util.js 公共方法 ===========================
+// 依赖库:jquery   
+// 本代码更新于:2019-5-1 
+// 新增更简单的写法 
+
+// 相关配置 
+var upload_cfg = {
+	upload_image_url: sa.cfg.api_url + '/upload/image',	// 图片上传地址
+	upload_video_url: sa.cfg.api_url + '/upload/video',	// 视频上传地址
+	upload_audio_url: sa.cfg.api_url + '/upload/audio',	// 音频上传地址
+	upload_apk_url: sa.cfg.api_url + '/upload/apk',	// apk安装包上传地址
+	upload_file_url: sa.cfg.api_url + '/upload/file',	// file上传地址
+}
+
+
+// 将方法挂载到sa对象上
+window.sa = window.sa || {};
+
+// 上传图片   
+sa.uploadImage = function(successCB) {
+	sa.uploadFn(upload_cfg.upload_image_url, successCB);
+}
+// 上传视频   
+sa.uploadVideo = function(successCB) {
+	sa.uploadFn(upload_cfg.upload_video_url, successCB);
+}
+// 上传音频  
+sa.uploadAudio = function(successCB) {
+	sa.uploadFn(upload_cfg.upload_audio_url, successCB);
+}
+// 上传apk 
+sa.uploadApk = function(successCB) {
+	sa.uploadFn(upload_cfg.upload_apk_url, successCB);
+}
+// 上传任意文件 
+sa.uploadFile = function(successCB) {
+	sa.uploadFn(upload_cfg.upload_file_url, successCB);
+}
+// 上传的内部函数  (要上传到的地址,成功的回调)
+sa.uploadFn = function(url, successCB) {
+	// 创建input 
+	var fileInput = document.createElement("input"); //创建input
+	fileInput.type = "file"; //设置类型为file
+	fileInput.id = 'uploadfile-' + randomString(12);
+	fileInput.style.display = 'none';
+	fileInput.onchange = function(evt) {
+		startUpload(evt.target.files[0], url, successCB);
+	}
+	// 添加到body,并触发其点击事件 
+	document.body.appendChild(fileInput);
+	document.querySelector('#' + fileInput.id).click();
+}
+
+// 上传多张图片   
+sa.uploadImageList = function(successCB) {
+	sa.uploadListFn(upload_cfg.upload_image_url, successCB);
+}
+// 上传多个音频   
+sa.uploadAudioList = function(successCB) {
+	sa.uploadListFn(upload_cfg.upload_audio_url, successCB);
+}
+// 上传多个视频 
+sa.uploadVideoList = function(successCB) {
+	sa.uploadListFn(upload_cfg.upload_video_url, successCB);
+}
+// 上传多个文件 
+sa.uploadFileList = function(successCB) {
+	sa.uploadListFn(upload_cfg.upload_file_url, successCB);
+}
+
+// 上传多个的内部函数  (要上传到的地址,成功的回调)
+sa.uploadListFn = function(url, successCB) {
+	// 创建input
+	var fileInput = document.createElement("input"); //创建input
+	fileInput.type = "file"; // 设置类型为file
+	fileInput.multiple = "multiple"; // 多选 
+	fileInput.id = 'uploadfile-' + randomString(12);
+	fileInput.style.display = 'none';
+	fileInput.onchange = function(evt) {
+		// 开始上传 
+		var files = evt.target.files;
+		for (var i = 0; i < files.length; i++) {
+			let ii = i;
+			startUpload(evt.target.files[ii], url, successCB);
+		}
+	}
+	// 添加到body,并触发其点击事件 
+	document.body.appendChild(fileInput);
+	document.querySelector('#' + fileInput.id).click();
+}
+
+
+
+
+// ======================= 以下方法为过时的旧方法 =========================== 
+
+// 开始上传,图片版
+function startUploadImage(file, successCB) {
+	startUpload(file, upload_cfg.upload_image_url, successCB);
+}
+var startUploadImage2 = startUploadImage;	// 兼容以前的写法 
+
+// 开始上传,视频版
+function startUploadVideo(file, successCB) {
+	startUpload(file, upload_cfg.upload_video_url, successCB);
+}
+// 开始上传,音频版
+function startUploadAudio(file, successCB) {
+	startUpload(file, upload_cfg.upload_audio_url, successCB);
+}
+// 开始上传,apk版
+function startUploadApk(file, successCB) {
+	startUpload(file, upload_cfg.upload_apk_url, successCB);
+}
+
+// 开始上传
+function startUpload(file, url, successCB) {
+	
+	// 准备参数 
+	var form = new FormData();
+	form.append('file', file);
+	
+	// 开始上传 
+	$.ajax({
+		url: url,
+		data: form,
+		processData: false, // 默认true,设置为 false,不需要进行序列化处理
+		cache: false, 		// 设置为false将不会从浏览器缓存中加载请求信息
+		contentType: false, // 避免服务器不能正常解析文件
+		dataType: 'json',
+		type: 'post',
+		beforeSend: function (xhr) {
+			show_jdt();
+        },
+		complete: function (xhr) {
+			close_jdt();
+        },
+		xhr: xhrOnProgress(function(e) {
+			var percent = e.loaded / e.total; // 计算进度百分比, 取值结果为 0~1 之间的小无限不循环小数 
+			// progressCB(percent * 100);
+			set_jdt_value(percent * 100);
+			// console.log('进度百分比' + percent);
+		}),
+		success: function(res) { 
+			if(res.code == 200) {
+				successCB(res.data);	// 把地址给回调函数 
+			} else {
+				sa.alert(res.msg);
+			}
+		},
+		error: function(e) {
+			sa.alert('异常: ' + JSON.stringify(e));
+		}
+	});
+	
+}
+
+
+
+
+// ======================= 工具方法 =========================== 
+
+
+
+// 返回后缀名
+function get_suffix(filename) {
+	var pos = filename.lastIndexOf('.');
+	if (pos != -1) {
+		suffix = filename.substring(pos + 1);
+	}
+	return suffix;
+}
+
+// 返回带有上传回调功能的 xhr 
+function xhrOnProgress(fun) {
+	xhrOnProgress.onprogress = fun; //绑定监听
+	//使用闭包实现监听绑
+	return function() {
+		//通过$.ajaxSettings.xhr();获得XMLHttpRequest对象
+		var xhr = $.ajaxSettings.xhr();
+		//判断监听函数是否为函数
+		if (typeof xhrOnProgress.onprogress !== 'function')
+			return xhr;
+		//如果有监听函数并且xhr对象支持绑定时就把监听函数绑定上去
+		if (xhrOnProgress.onprogress && xhr.upload) {
+			xhr.upload.onprogress = xhrOnProgress.onprogress;
+		}
+		return xhr;
+	}
+}
+
+// 
+function randomString(len) {
+  len = len || 32;
+  var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
+  var maxPos = $chars.length;
+  var str = '';
+  for (i = 0; i < len; i++) {
+    str += $chars.charAt(Math.floor(Math.random() * maxPos));
+  }
+  return str;
+}
+
+
+// ======================= 进度条相关 ===========================
+// 显示进度条 
+function show_jdt() {
+	close_jdt();	// 先清除原来的 
+	// 创建节点并添加到body 
+	var str = '' +
+		'<div class="jdt-fox" style="z-index: 999999999; width: 500px; height: 20px; position: fixed; top: calc(50% - 5px); left: calc(50% - 250px); ">'+
+		'	<div class="jdt-fox2" style="width: calc(100% - 100px); height: 6px; margin-top: 7px; border-radius: 3px; float: left; background-color: #FFF; box-shadow: 0 0 10px #aaa;">'+
+		'		<div class="jdt-fox-value" style=" transition: all 0.1s; position: relative; width: 0.0%; height: 100%; border-radius: 3px; background-color: green; box-shadow: 0 0 10px green;">'+
+		'			<div class="jdt-fox-yh" style="position: absolute; right: -10px; top: -5px;">'+
+		'				<div style="transition: all 1s; background-color: green; width: 16px; height: 16px; border-radius: 50%;"></div>'+
+		'			</div>'+
+		'		</div>'+
+		'	</div>'+
+		'	<div class="jdt-value-text" style="float: left; font-size: 14px; margin-left: 14px; color: #111;"> 0.0% </div>'+
+		'</div>';
+	var div = document.createElement("div");
+	div.innerHTML = str;
+	div.className = "jdt-box";
+	document.body.appendChild(div);
+	// 开启圆点的呼吸动画效果
+	if (window.my_interval_index) {
+		clearInterval(window.my_interval_index);
+	}
+	window.my_interval_index = setInterval(function() {
+		if (window.one_num === undefined) {
+			window.one_num = 0;
+		}
+		window.one_num++;
+		var n_px = window.one_num % 2 == 0 ? '0px' : '20px';
+		var box_shadow = "0 0 " + n_px + " green";
+		document.querySelector('.jdt-fox-yh div').style.boxShadow = box_shadow;
+	}, 1000);
+}
+
+// 关闭进度条 
+function close_jdt() {
+	// 先关闭动画 
+	if (window.my_interval_index) {
+		clearInterval(window.my_interval_index);
+	}
+	// 再销毁dom 
+	var box = document.querySelector('.jdt-box');
+	if (box) {
+		box.parentNode.removeChild(box);
+	}
+}
+
+// 设置进度条进度, 参数为一个0~100之间的小数 
+function set_jdt_value(value) {
+	value = parseInt(value * 10) / 10.0 + '%';	// 保证小数点后一位 
+	// 改变进度条宽度
+	var dft = document.querySelector('.jdt-fox-value');
+	if(dft){
+		dft.style.width = value;	
+	}
+	// 改变文字百分比值  
+	var dvt = document.querySelector('.jdt-value-text');
+	if(dvt) {
+		dvt.innerHTML = value;	
+	} 
+	// console.log(value);
+}
+
+
+
+
+
+
+
+
+

Різницю між файлами не показано, бо вона завелика
+ 4662 - 0
static/kj/wangEditor.up.js


+ 195 - 0
static/sa.css

@@ -0,0 +1,195 @@
+/* 
+	更新于2021-9-25 全面优化
+ */
+
+/** 公共css */
+*{margin: 0px;padding: 0px;}
+html{font-size: 10px; height: 100%;}
+body{font-size: 1.4rem; height: 100%;/* background-color: #eeeeee; */  color: #333;} 
+body{font-family: "Helvetica Neue", Helvetica, "PingFang SC", Tahoma, Arial, sans-serif;}
+a{text-decoration: none;}
+a:hover{}
+/* h1,h2,h3,h4,h5,h6{font-weight: 400;} */
+hr{background-color : #ddd; height: 1px; border: none;}
+input,select{outline: 0;}
+
+/* input type=number时不显示按钮 */
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+	-webkit-appearance: none !important;
+	margin: 0;
+}
+
+/* 居中形式的img */
+.cover-img{object-fit: cover; object-position: 50% 30%;}
+
+/* ajax2加载时的转圈圈样式 */
+.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
+.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
+.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
+
+
+/* layer图片预览时, 左右键永远显示 */
+.layui-layer-imgbar, .layui-layer-imguide{display: block !important;}
+.layui-layer-iconext.layui-layer-imgprev{position: fixed; left: 50;}
+.layui-layer-iconext.layui-layer-imgnext{position: fixed; right: 50;}
+
+
+/* ===================== 整体面板 ===================== */
+/* vue盒子 */
+.vue-box{padding: 14px; height: calc(100vh - 28px); overflow: auto; font-size: 14px; background-color: #eeeeee;}
+
+/* 内容-面板 */
+.c-panel{/* margin: 0.7em 1em; */ margin-bottom: 10px; padding: 1em 1.5em; padding-bottom: 5px; background-color: #fff; color: #333; /* box-shadow: 0 0 5px #eee; */}
+.c-title{font-size: 14px; font-weight: bold; line-height: 2em; margin-bottom: 3px;}
+.c-title span{font-weight: 400; font-size: 0.85em; padding-left: 1em; color: #888;}
+
+/* ===================== 表单相关 ===================== */
+.c-panel .el-form{/* padding-top: 10px; */ /* padding-bottom: 15px; */}
+.c-panel .el-form-item{min-width: 278px;}
+.c-panel .el-form-item__label{width: 100px;}
+.c-panel .el-form .el-input{width: 178px;}
+.c-remark{color: #888; margin-left: 0.5em; font-size: 0.9em;}
+
+/* 标签 */
+.c-panel .c-tag{padding: 0px 15px; height: 22px; line-height: 22px; border-radius: 0px; border: 0px;}
+/* 复选框 */
+.c-panel .el-checkbox,.c-panel .el-radio{margin-right: 20px;}
+/* 禁用input的样式 */
+.c-panel .el-input.is-disabled .el-input__inner{color: #999;}
+
+/* 表格的表头颜色深一点 */
+.c-panel .el-table__header tr th{background-color: #F5F5F5; color: #555; padding: 8px 0;}
+
+/* 调整圆角大小: 输入框、文本域、按钮、 */
+.vue-box .el-input__inner,
+.vue-box .el-textarea__inner,
+.vue-box .el-button,
+.s-foot .el-button{border-radius: 2px !important;}
+.vue-box .el-alert{padding: 1em 0.5em; border-radius: 0px !important;}
+
+/* 多个按钮并列时的距离 */
+.vue-box .el-button+.el-button{margin-left: 2px;}
+.vue-box .el-button+.el-button.el-button--text{margin-left: 10px;}
+
+/* 单选button,圆角限制2px */
+.el-radio-button:first-child .el-radio-button__inner{border-radius: 2px 0 0 2px !important;}
+.el-radio-button:last-child .el-radio-button__inner{border-radius: 0 2px 2px 0 !important;}
+
+/* 单选按钮, 文字版 */
+.s-radio-text{}
+.s-radio-text .el-radio__input{display: none;}
+.s-radio-text .el-radio__input.is-checked+.el-radio__label{font-weight: 700;}
+.s-radio-text .el-radio__label{padding-left: 0px; }
+.s-radio-text .el-radio__label:hover{text-decoration:underline;}
+.s-radio-text .hover-line:hover{text-decoration: underline; cursor: pointer;}
+.s-radio-text .el-form-item__content{position: relative; top: -2px;}
+
+
+/* 按钮的样式调整 */
+.el-button--primary{background-color: #1890ff;}
+.el-button--success{background-color: #57C22A;}
+.el-button--danger{background-color: #ee4949; border-color: #de4949;}
+.el-button--warning{background-color: #FF8a10; border-color: #FF8a10; color: #FFF;}
+.el-button--primary:hover,.el-button--primary:focus{background-color: #066CF3; border-color: #066CF3;}
+.el-button--success:hover,.el-button--success:focus{background-color: #2B9939; border-color: #2B9939;}
+.el-button--danger:hover,.el-button--danger:focus{background-color: #d02C2C; border-color: #d02C2C;}
+.el-button--warning:hover,.el-button--warning:focus{background-color: #dd6300; border-color: #dd6300;}
+
+.el-button--info:hover,.el-button--info:focus{background-color: #707379; border-color: #707379;}
+
+/* .c-btn 加强 */
+.el-table .c-btn{padding: 4px 6px; font-size: 12px !important; border-radius: 1px;}
+.c-btn.el-button--danger{background-color: #ca4242; border-color: #ca4242;}
+.c-btn.el-button--danger:hover{background-color: #A00C0C; border-color: #A00C0C;}
+
+/* 表格上面的按钮 */
+.fast-btn{padding-top: 15px; padding-bottom: 10px;} 
+/* .el-table{padding-top: 10px;} */
+/* 表格里的img */
+.td-img{width: 3em; height: 3em; border-radius: 2px; cursor: pointer;}
+.info-img{width: 3em; height: 3em; cursor: pointer; margin-right: 5px; vertical-align: middle;}
+.c-price{color: red;}
+
+/* 分页盒子调整一下间距 */
+.page-box{padding: 2em 0 25px 0; }
+
+/* 流体表单 */
+.cj-form.vue-box{padding-bottom: 0px; background-color: #FFF;}
+.cj-form .c-panel{box-shadow: 0 0 0; margin-top: 0px; margin-bottom: 0px; padding-top: 2.5em; padding-bottom: 0px;}
+.cj-form .c-panel .el-form .el-input{width: 100%;}
+
+
+/* 底部按钮式风格 */
+.sbot{padding: 0px; height: 100vh; background-color: rgba(0,0,0,0); display: flex !important; flex-direction: column;}
+.s-body{/* height: calc(100vh - 50px); */ flex: 1; overflow: auto; /* background-color: #009688s; */ }
+.s-foot{height: 49px !important; line-height: 49px; text-align: right; background-color: #f5f5f5; border-top: 1px #e5e5e5 solid;}
+.sbot .s-foot .el-button{margin-left: -10px; margin-right: 15px; padding: 7px 18px;}
+.sbot .s-body .c-panel{margin-bottom: 0px; padding-top: 14px; padding-bottom: 0px;}
+
+.vue-box .s-foot{display: none;}
+.sbot .s-ok{display: none !important;}
+.sbot .s-foot{display: block;}
+.sbot .c-title{display: none;}
+
+/* ===================== 表单相关 ===================== */
+
+/* 内容-item */
+.c-item {min-width: 270px; min-height: 32px; line-height: 32px; padding-right: 10px; display: inline-block; margin: 0.5em 0;}
+.c-item.br{display: block; margin: 14px 0;}
+/* label样式 */
+.c-item .c-label{width: 6em; color: #333; padding-right: 4px; display: inline-block; text-align: right; vertical-align: top;}
+/* input宽度等样式调整 */
+.c-item .el-input{width: 178px;}
+/* 禁用input的样式 */
+.c-item .el-input.is-disabled .el-input__inner{color: #999;}
+/* 链接 行高设置 */
+.c-item .el-link{line-height: 1.6em;}
+
+/* 表格动画相关 */
+.data-table .el-table__body-wrapper{min-height: 0px; max-height: 60px;}
+.data-table .el-table__body-wrapper{transition: all 0.3s;}
+.el-table .el-table__body-wrapper table td .cell{word-break: normal;}
+
+
+/* 防止下拉框被富文本覆盖 */
+.el-select-dropdown,.el-picker-panel{z-index: 99999 !important;}
+
+/* ===================== sp 新增 ===================== */
+
+/* ==== 图集照片样式 ==== */
+.c-item .image-box{max-width: 700px; padding-left: 0px;}
+.c-item .image-box-2{width: 90px; height: 125px; cursor: pointer; float: left;}
+.c-item .image-box-2 img{width: 90px; height: 90px; border-radius: 2px;}
+.c-item .image-box-info .image-box-2{height: 90px;}
+.c-item .image-box-2{display: inline-block; margin-right: 5px; margin-bottom: 5px;}
+.c-item .image-box-2 p{text-align: center; color: #999; margin-top: -10px;}
+.c-item .up_img{text-align: center; background-color: #f8f8f8; height: 90px;}
+.c-item .up_img img{width: 40px; height: 40px; margin-top: 25px;}
+.c-item .up_img{border: 1px #eee solid;}
+
+/* ==== wang富文本编辑器 ==== */
+.c-item .editor-box{width: 800px; margin-top: 0px; transition: all 0.2s;} 
+.c-item .content-box-info{border: 1px #ddd solid; padding: 1em; overflow: hidden; box-sizing: border-box;}
+.editor-box #editor{min-height: 300px; background-color: #FFF;}
+.editor-box .w-e-toolbar{padding-top: 5px !important;}
+/* 富文本内的编辑器尽量小点 */
+.editor-box img{max-width: 300px !important;}
+.content-box-info img{max-width: 100% !important;}
+
+/* 仿移动端样式兼容 */
+/* .editor-item .editor-box{float: left; width: 400px;} 
+.editor-item .w-e-toolbar{width: 400px; flex-wrap: wrap; } */
+.c-item .fold{height: 100px !important; overflow: hidden;}
+.c-item .el-select-dropdown{z-index: 9999999 !important;}
+
+/* 多行内容的样式 */
+.c-item-mline{display: inline-block; width: calc(100% - 10em);}
+.del-rr{color: red !important; cursor: pointer; margin-left: 0.5em; vertical-align: middle;}
+
+/* ---- 你可以在此处定制全局的字段风格 ---- */
+/* .tc-num{font-weight: bold;} */
+.tc-num{color: blue;}
+/* .tc-date{color: blue;} */
+.data-table .el-link--inner{font-weight: bold;}
+

+ 1294 - 0
static/sa.js

@@ -0,0 +1,1294 @@
+// =========================== sa对象封装一系列工具方法 ===========================  
+var sa = {
+	version: '2.4.3',
+	update_time: '2020-10-2',
+	info: '新增双击layer标题处全屏'
+};
+
+// ===========================  当前环境配置  ======================================= 
+(function(){
+	// 公司开发环境
+	var cfg_dev = {
+		api_url: 'http://127.0.0.1:8080/sp-admin',	// 所有ajax请求接口父地址
+		web_url: 'http://www.baidu.com'		// 此项目前台地址 (此配置项非必须)
+	}
+	// 服务器测试环境
+	var cfg_test = {
+		api_url: 'http://www.baidu.com',
+		web_url: 'http://www.baidu.com'
+	}
+	// 正式生产环境
+	var cfg_prod = {
+		api_url: 'http://www.baidu.com',
+		web_url: 'http://www.baidu.com'
+	}
+	sa.cfg = cfg_dev; // 最终环境 , 上线前请选择正确的环境 
+})();
+
+
+// ===========================  ajax的封装  ======================================= 
+(function(){
+	
+	/** 对ajax的再封装, 这个ajax假设你的接口会返回以下格式的内容 
+		{
+			"code": 200,
+			"msg": "ok",
+			"data": []
+		}
+		如果返回的不是这个格式, 你可能需要改动一下源码, 要么改动服务端适应此ajax, 要么改动这个ajax适应你的服务端 
+	 * @param {Object} url 请求地址
+	 * @param {Object} data 请求参数
+	 * @param {Object} success200 当返回的code码==200时的回调函数  
+	 * @param {Object} 其它配置,可配置项有:
+		{
+			msg: '',		// 默认的提示文字 填null为不提示 
+			type: 'get',	// 设定请求类型 默认post
+			baseUrl: '',	// ajax请求拼接的父路径 默认取 sa.cfg.api_url 
+			sleep: 0,		// ajax模拟的延时毫秒数, 默认0 
+			success500: fn,	// code码等于500时的回调函数 (一般代表服务器错误)
+			success403: fn,	// code码等于403时的回调函数 (一般代表无权限)
+			success401: fn,	// code码等于401时的回调函数 (一般代表未登录)
+			errorfn: fn,	// ajax发生错误时的回调函数 (一般是ajax请求本身发生了错误)
+			complete: fn,	// ajax无论成功还是失败都会执行的回调函数  
+		}
+	 */
+	sa.ajax = function(url, data, success200, cfg){
+		
+		// 如果是简写模式(省略了data参数)
+		if(typeof data === 'function'){
+			cfg = success200;
+			success200 = data;
+			data = {};
+		}
+		
+		// 默认配置
+		var defaultCfg = {
+			msg: '努力加载中...',	// 提示语
+			baseUrl: (url.indexOf('http') === 0 ? '' : sa.cfg.api_url),// 父url,拼接在url前面
+			sleep: 0,	// 休眠n毫秒处理回调函数 
+			type: 'post',	// 默认请求类型 
+			success200: success200,			// code=200, 代表成功 
+			success500: function(res){		// code=500, 代表失败 
+				return layer.alert('失败:' + res.msg);
+			},
+			success403: function(res){		// code=403, 代表权限不足
+				return layer.alert("权限不足," + res.msg, {icon: 5});
+			},
+			success401: function(res){		// code=401, 代表未登录
+				return layer.confirm("您当前暂未登录,是否立即登录?", {}, function(){
+					layer.closeAll();
+					return sa.$page.openLogin(cfg.login_url);
+				});
+			},
+			errorfn: function(xhr, type, errorThrown){		// ajax发生异常时的默认处理函数
+				if(xhr.status == 0){
+					return layer.alert('无法连接到服务器,请检查网络');
+				}
+				return layer.alert("异常:" + JSON.stringify(xhr));
+			},
+			complete: function(xhr, ts) {	// 成功失败都会执行 
+				
+			}
+		}
+		
+		// 将调用者的配置和默认配置合并 
+		cfg = sa.extendJson(cfg, defaultCfg);
+		
+		// 打印请求地址和参数, 以便调试 
+		console.log("请求地址:" + cfg.baseUrl + url);
+		console.log("请求参数:" + JSON.stringify(data));
+		
+		// 开始显示loading图标 
+		if(cfg.msg != null){
+			sa.loading(cfg.msg);
+		}
+		
+		// 开始请求ajax 
+		return $.ajax({
+			url: cfg.baseUrl + url,
+			type: cfg.type, 
+			data: data,
+			dataType: 'json',
+			// xhrFields: {
+			// 	withCredentials: true // 携带跨域cookie
+			// },
+			// crossDomain: true,
+			beforeSend: function(xhr) {
+				xhr.setRequestHeader('X-Requested-With','XMLHttpRequest');
+				// 追加token
+				if(localStorage.tokenName) {
+					xhr.setRequestHeader(localStorage.tokenName, localStorage.tokenValue);
+				}
+			},
+			success: function(res){
+				console.log('返回数据:', res);
+				setTimeout(function() {
+					sa.hideLoading();
+					// 如果相应的处理函数存在
+					if(cfg['success' + res.code] != undefined) {
+						return cfg['success' + res.code](res);
+					}
+					layer.alert('未知状态码:' + JSON.stringify(res));
+				}, cfg.sleep);
+			},
+			error: function(xhr, type, errorThrown){
+				setTimeout(function() {
+					sa.hideLoading();
+					return cfg.errorfn(xhr, type, errorThrown);
+				}, cfg.sleep);
+			},
+			complete: cfg.complete
+		});
+		
+	};
+	
+	// 模拟一个ajax 
+	// 请注意: 本模板中所有ajax请求调用的均为此模拟函数 
+	sa.ajax2 = function(url, data, success200, cfg){
+		// 如果是简写模式(省略了data参数)
+		if(typeof data === 'function'){
+			cfg = success200;
+			success200 = data;
+			data = {};
+		}
+		// 几个默认配置 
+		cfg = cfg || {};
+		cfg.baseUrl = (url.indexOf('http') === 0 ? '' : sa.cfg.api_url);	// 父url,拼接在url前面
+		// 设定一个默认的提示文字 
+		if(cfg.msg == undefined || cfg.msg == null || cfg.msg == '') {
+			cfg.msg = '正在努力加载...';
+		}
+		// 默认延时函数 
+		if(cfg.sleep == undefined || cfg.sleep == null || cfg.sleep == '' || cfg.sleep == 0) {
+			cfg.sleep = 600;
+		}
+		// 默认的模拟数据
+		cfg.res = cfg.res || {
+			code: 200,
+			msg: 'ok',
+			data: []
+		}
+		// 开始loding 
+		sa.loading(cfg.msg);
+		
+		// 打印请求地址和参数, 以便调试 
+		console.log("======= 模拟ajax =======");
+		console.log("请求地址:" + cfg.baseUrl + url);
+		console.log("请求参数:" + JSON.stringify(data));
+		
+		// 模拟ajax的延时 
+		setTimeout(function() {
+			sa.hideLoading();	// 隐藏掉转圈圈 
+			console.log('返回数据:', cfg.res);
+			success200(cfg.res);
+		}, cfg.sleep)
+	};
+	
+})();
+
+
+// ===========================  封装弹窗相关函数   ======================================= 
+(function() {
+	
+	var me = sa;
+	if(window.layer) {
+		layer.ready(function(){});
+	}
+	
+	
+	
+	// tips提示文字  
+	me.msg = function(msg, cfg) {
+		msg = msg || '操作成功';
+		layer.msg(msg, cfg);
+	};
+	
+	// 操作成功的提示  
+	me.ok = function(msg) {
+		msg = msg || '操作成功';
+		layer.msg(msg, {anim: 0, icon: 1 }); 
+	}
+	me.ok2 = function(msg) {
+		msg = msg || '操作成功';
+		layer.msg(msg, {anim: 0, icon: 6 }); 
+	}
+	
+	// 操作失败的提示  
+	me.error = function(msg) {
+		msg = msg || '操作失败';
+		layer.msg(msg, {anim: 6, icon: 2 }); 
+	}
+	me.error2 = function(msg) {
+		msg = msg || '操作失败';
+		layer.msg(msg, {anim: 6, icon: 5 }); 
+	}
+	
+	// alert弹窗 [text=提示文字, okFn=点击确定之后的回调函数]
+	me.alert = function(text, okFn) {
+		// 开始弹窗 
+		layer.alert(text, function(index) {
+			layer.close(index);
+			if(okFn) {
+				okFn();
+			}
+		});
+	};
+
+	// 询问框 [text=提示文字, okFn=点击确定之后的回调函数]
+	me.confirm = function(text, okFn) {
+		layer.confirm(text, {}, function(index) {
+			layer.close(index);
+			if(okFn) {
+				okFn();
+			}
+		}.bind(this));
+	};
+	
+	// 输入框 [title=提示文字, okFn=点击确定后的回调函数, formType=输入框类型(0=文本,1=密码,2=多行文本域) 可省略, value=默认值 可省略 ]  
+	me.prompt = function(title, okFn, formType, value) {
+		layer.prompt({
+			title: title,
+			formType: formType, 
+			value: value
+		}, function(pass, index){
+			layer.close(index);
+			if(okFn) {
+				okFn(pass);
+			}
+		});
+	}
+	
+	// 打开loading
+	me.loading = function(msg) {
+		layer.closeAll();	// 开始前先把所有弹窗关了
+		return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });
+	};
+	
+	// 隐藏loading
+	me.hideLoading = function() {
+		layer.closeAll();
+	};
+	
+	// ============== 一些常用弹窗 ===================== 
+	
+	// 大窗显示一个图片 
+	// 参数: src=地址、w=宽度(默认80%)、h=高度(默认80%)
+	me.showImage = function(src, w, h) {
+		w = w || '80%';
+		h = h || '80%';
+		var content = '<div style="height: 100%; overflow: hidden !important;">' + 
+			'<img src="' + src + ' " style="width: 100%; height: 100%;" />' + 
+		 '</div>';
+		layer.open({
+		    type: 1,
+		    title: false,
+		    shadeClose: true,
+			closeBtn: 0,
+		    area: [w, h], //宽高
+		    content: content
+		});
+	}
+	
+	// 预览一组图片 
+	// srcList=图片路径数组(可以是json样,也可以是逗号切割式), index=打开立即显示哪张(可填下标, 也可填写src路径)
+	me.showImageList = function(srcList, index) {
+		// 如果填的是个string 
+		srcList = srcList || [];
+		if(typeof srcList === 'string') {
+			try{
+				srcList = JSON.parse(srcList);
+			}catch(e){
+				try{
+					srcList = srcList.split(',');	// 尝试字符串切割
+				}catch(e){
+					srcList = [];	
+				}
+			}
+		}
+		// 如果填的是路径 
+		index = index || 0;
+		if(typeof index === 'string') {
+			index = srcList.indexOf(index);
+			index = (index == -1 ? 0 : index);
+		}
+		
+		// 开始展示 
+		var arr_list = [];
+		srcList.forEach(function(item) {
+			arr_list.push({
+				alt: '左右键切换',
+				pid: 1,
+				src: item,
+				thumb: item
+			})
+		})
+		layer.photos({
+			photos: {
+				title: '',
+				id: new Date().getTime(),
+				start: index,
+				data: arr_list
+			}
+			,anim: 5 //0-6的选择,指定弹出图片动画类型,默认随机(请注意,3.0之前的版本用shift参数)
+		});	
+	}
+	
+	// 显示一个iframe 
+	// 参数: 标题,地址,宽,高 , 点击遮罩是否关闭, 默认false 
+	me.showIframe = function(title, url, w, h, shadeClose) {
+		// 参数修正
+		w = w || '95%'; 
+		h = h || '95%'; 
+		shadeClose = (shadeClose === undefined ? false : shadeClose);
+		// 弹出面板 
+		var index = layer.open({
+			type: 2,	
+			title: title,	// 标题 
+			shadeClose: shadeClose,	// 是否点击遮罩关闭
+			maxmin: true, // 显示最大化按钮
+		  	shade: 0.8,		// 遮罩透明度 
+			scrollbar: false,	// 屏蔽掉外层的滚动条
+			moveOut: true,		// 是否可拖动到外面
+		  	area: [w, h],	// 大小 
+		  	content: url,	// 传值 
+			// 解决拉伸或者最大化的时候,iframe高度不能自适应的问题
+			resizing: function(layero) {
+				solveLayerBug(index);
+			}
+		}); 
+		// 解决拉伸或者最大化的时候,iframe高度不能自适应的问题
+		$('#layui-layer' + index + ' .layui-layer-max').click(function() {
+			setTimeout(function() {
+				solveLayerBug(index);
+			}, 200)
+		})
+	}
+	me.showView = me.showIframe;
+	
+	// 显示一个iframe, 底部按钮方式
+	// 参数: 标题,地址,点击确定按钮执行的代码(在子窗口执行),宽,高 
+	me.showIframe2 = function(title, url, evalStr, w, h) {
+		// 参数修正
+		w = w || '95%'; 
+		h = h || '95%'; 
+		// 弹出面板 
+		var index = layer.open({
+			type: 2,	
+			title: title,	// 标题 
+			closeBtn: (title ? 1 : 0),	// 是否显示关闭按钮
+			btn: ['确定', '取消'],
+			shadeClose: false,	// 是否点击遮罩关闭
+			maxmin: true, // 显示最大化按钮
+		  	shade: 0.8,		// 遮罩透明度 
+			scrollbar: false,	// 屏蔽掉外层的滚动条
+			moveOut: true,		// 是否可拖动到外面
+		  	area: [w, h],	// 大小 
+		  	content: url,	// 传值 
+			// 解决拉伸或者最大化的时候,iframe高度不能自适应的问题
+			resizing: function(layero) {
+				
+			},
+			yes: function(index, layero) {
+				var iframe = document.getElementById('layui-layer-iframe' + index);
+				var iframeWindow = iframe.contentWindow;
+				iframeWindow.eval(evalStr);
+			}
+		}); 
+	}
+	
+	
+	// 当前iframe关闭自身  (在iframe中调用)
+	me.closeCurrIframe = function() {
+		try{
+			var index = parent.layer.getFrameIndex(window.name); //先得到当前iframe层的索引
+			parent.layer.close(index); //再执行关闭   
+		}catch(e){
+			//TODO handle the exception
+		}
+	}
+	me.closeCurrView = me.closeCurrIframe;
+	
+	
+	//执行一个函数, 解决layer拉伸或者最大化的时候,iframe高度不能自适应的问题
+	function solveLayerBug(index) {
+		var selected = '#layui-layer' + index;
+		var height = $(selected).height();
+		var title_height = $(selected).find('.layui-layer-title').height();
+		$(selected).find('iframe').css('height', (height - title_height) + 'px');
+	}
+	
+	
+	// 监听回车事件,达到回车关闭弹窗的效果 
+	if(window.$) {
+		$(document).on('keydown', function() {
+			if(event.keyCode === 13 && $(".layui-layer-btn0").length == 1 && !window.is_not_watch_enter && $(this).find('.layui-layer-input').length == 0){
+				$(".layui-layer-btn0").click();
+				return false;
+			}
+		}); 
+	}
+	
+	
+	
+})();
+
+
+// ===========================  常用util函数封装   ======================================= 
+(function () {
+	
+	// 超级对象
+    var me = sa;
+	
+	// ===========================  常用util函数封装   ======================================= 
+	if(true) {
+		
+		// 从url中查询到指定参数值 
+		me.p = function(name, defaultValue){
+			var query = window.location.search.substring(1);
+			var vars = query.split("&");
+			for (var i=0;i<vars.length;i++) {
+				var pair = vars[i].split("=");
+				if(pair[0] == name){return pair[1];}
+			}
+			return(defaultValue == undefined ? null : defaultValue);
+		}
+		me.q = function(name, defaultValue){
+			var query = window.location.search.substring(1);
+			var vars = query.split("&");
+			for (var i=0;i<vars.length;i++) {
+				var pair = vars[i].split("=");
+				if(pair[0] == name){return pair[1];}
+			}
+			return(defaultValue == undefined ? null : defaultValue);
+		}
+		
+		// 判断一个变量是否为null
+		// 返回true或false,如果return_obj有值,则在true的情况下返回return_obj
+		me.isNull = function(obj, return_obj){
+			var flag = [null, undefined, '', 'null', 'undefined'].indexOf(obj) != -1;
+			if(return_obj === undefined){
+				return flag;
+			} else {
+				if(flag){
+					return return_obj;
+				} else {
+					return obj;
+				}
+			}
+		}
+		
+		// 将时间戳转化为指定时间
+		// way:方式(1=年月日,2=年月日时分秒)默认1,  也可以指定格式:yyyy-MM-dd HH:mm:ss  
+		me.forDate = function(inputTime, way) {
+			if(me.isNull(inputTime) == true){
+				return "";
+			}
+			var date = new Date(inputTime);
+			var y = date.getFullYear();  
+			var m = date.getMonth() + 1;  
+			m = m < 10 ? ('0' + m) : m;  
+			var d = date.getDate();  
+			d = d < 10 ? ('0' + d) : d;  
+			var h = date.getHours();
+			h = h < 10 ? ('0' + h) : h;
+			var minute = date.getMinutes();
+			var second = date.getSeconds();
+			minute = minute < 10 ? ('0' + minute) : minute;  
+			second = second < 10 ? ('0' + second) : second; 
+			var ms = date.getMilliseconds();
+			
+			way = way || 1;
+			// way == 1  年月日
+			if(way === 1) {
+				return y + '-' + m + '-' + d;  
+			}
+			// way == 1  年月日时分秒 
+			if(way === 2){
+				return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;  
+			}
+			// way == 具体格式   标准格式: yyyy-MM-dd HH:mm:ss
+			if(typeof way == 'string') {
+				return way.replace("yyyy", y).replace("MM", m).replace("dd", d).replace("HH", h).replace("mm", minute).replace("ss", second).replace("ms", ms);
+			}
+			return y + '-' + m + '-' + d;  
+		};
+		// 时间日期 
+		me.forDatetime = function(inputTime) {
+			return me.forDate(inputTime, 2);
+		}
+		
+		// 将时间转化为 个性化 如:3小时前, 
+		// d1 之于 d2 ,d2不填则默认取当前时间 
+		me.forDate2 = function(d, d2){
+			
+			var hou = "前";
+			
+			if(d == null || d == '') {
+				return '';
+			}
+			if(d2 == null || d2 == '') {
+				d2 = new Date();
+			}
+			d2 = new Date(d2).getTime();
+			
+			var timestamp = new Date(d).getTime() - 1000;
+			var mistiming = Math.round((d2 - timestamp) / 1000);
+			if(mistiming < 0) {
+				mistiming = 0 - mistiming;
+				hou = '后'
+			}
+			var arrr = ['年', '月', '周', '天', '小时', '分钟', '秒'];
+			var arrn = [31536000, 2592000, 604800, 86400, 3600, 60, 1];
+			for (var i = 0; i < arrn.length; i++) {
+				var inm = Math.floor(mistiming / arrn[i]);
+				if (inm != 0) {
+					return inm + arrr[i] + hou;
+				}
+			}
+		}
+		
+		// 综合以上两种方式,进行格式化
+		// 小于24小时的走forDate2,否则forDat 
+		me.forDate3 = function(d, way) {
+			if(d == null || d == '' ) {
+				return '';
+			}
+			var cha = new Date().getTime() - new Date(d).getTime();
+			cha = (cha > 0 ? cha : 0 - cha);
+			if(cha < (86400 * 1000)) {
+				return me.forDate2(d);
+			}
+			return me.forDate(d, way);
+		}
+		
+		// 返回时间差, 此格式数组:[x, x, x, 天, 时, 分, 秒]
+		me.getSJC = function (small_time, big_time) {
+			var date1 = new Date(small_time); //开始时间
+			var date2 = new Date(big_time); //结束时间
+			var date3 = date2.getTime() - date1.getTime(); //时间差秒
+			//计算出相差天数
+			var days = Math.floor(date3 / (24 * 3600 * 1000));
+		
+			//计算出小时数
+			var leave1 = date3 % (24 * 3600 * 1000); //计算天数后剩余的毫秒数
+			var hours = Math.floor(leave1 / (3600 * 1000));
+		
+			//计算相差分钟数
+			var leave2 = leave1 % (3600 * 1000); //计算小时数后剩余的毫秒数
+			var minutes = Math.floor(leave2 / (60 * 1000));
+		
+			//计算相差秒数
+			var leave3 = leave2 % (60 * 1000); //计算分钟数后剩余的毫秒数
+			var seconds = Math.round(leave3 / 1000);
+			
+			// 返回数组
+			return [0, 0, 0, days, hours, minutes, seconds];
+		}
+		
+		// 将日期,加上指定天数
+		me.dateAdd = function(d, n) {
+			var s = new Date(d).getTime();
+			s += 86400000 * n;
+			return new Date(s);
+		}
+		
+		// 转化json,出错返回默认值
+		me.JSONParse = function(obj, default_obj){
+			try{
+				return JSON.parse(obj) || default_obj;
+			}catch(e){
+				return default_obj || {};
+			}
+		}
+		
+		// 截取指定长度字符,默认50
+		me.maxLength = function (str, length) {
+			length = length || 50;
+		    if(!str){
+		        return "";
+		    }
+		    return (str.length > length) ? str.substr(0, length) + ' ...' : str;
+		},
+		
+		// 过滤掉标签
+		me.text = function(str){
+			if(!str){
+			    return "";
+			}
+			return str.replace(/<[^>]+>/g,"");
+		}
+		
+		// 为指定集合的每一项元素添加上is_update属性 
+		me.listAU = function(list){
+			list.forEach(function(ts){
+				ts.is_update  = false;
+			})
+			return list;
+		}
+		
+		// 获得一段文字中所有图片的路径
+		me.getSrcList = function(str){
+			try{
+				var imgReg = /<img.*?(?:>|\/>)/gi;	//匹配图片(g表示匹配所有结果i表示区分大小写)
+				var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i;	//匹配src属性
+				var arr = str.match(imgReg);	// 图片数组
+				var srcList = [];
+				for (var i = 0; i < arr.length; i++) {
+					var src = arr[i].match(srcReg);
+					srcList.push(src[1]);
+				}
+				return srcList;
+			} catch (e){
+				return [];
+			}
+		}
+		
+		// 无精度损失的乘法
+		me.accMul = function(arg1, arg2) {
+			var m = 0,
+				s1 = arg1.toString(),
+				s2 = arg2.toString(),
+				t;
+		 
+			t = s1.split(".");
+			// 判断有没有小数位,避免出错
+			if (t[1]) {
+				m += t[1].length
+			}
+		 
+			t = s2.split(".");
+			if (t[1]) {
+				m += t[1].length
+			}
+		 
+			return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m)
+		}
+		
+		// 正则验证是否为手机号
+		me.isPhone = function(str) {
+			str = str + '';
+			if((/^1[34578]\d{9}$/.test(str))){ 
+				return true; 
+			} 
+			return false;
+		}
+		
+		// 产生随机字符串
+		me.randomString = function(len) {
+		  len = len || 32;
+		  var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
+		  var maxPos = $chars.length;
+		  var str = '';
+		  for (i = 0; i < len; i++) {
+		    str += $chars.charAt(Math.floor(Math.random() * maxPos));
+		  }
+		  return str;
+		}
+		
+		// 刷新页面
+		me.f5 = function() {
+			location.reload();
+		}
+		
+		// 动态加载js 
+		me.loadJS = function(src, onload) {
+			var script = document.createElement("script");
+			script.setAttribute("type", "text/javascript");
+			script.src = src;
+			script.onload = onload;
+			document.body.appendChild(script);
+		}
+		
+		// 产生随机数字 
+		me.randomNum = function(min, max) {
+			return parseInt(Math.random() * (max - min + 1) + min, 10);
+		}
+		
+		// 打开页面
+		me.open = function(url) {
+			window.open(url);
+		}
+		
+		
+		
+		// == if 结束
+	}
+	
+	// ===========================  数组操作   ======================================= 
+	if (true) {
+		
+		// 从数组里获取数据,根据指定数据
+		me.getArrayField = function(arr, prop){
+			var propArr = [];
+			for (var i = 0; i < arr.length; i++) {
+				propArr.push(arr[i][prop]);
+			}
+			return propArr;
+		}
+		
+		// 从数组里获取数据,根据指定数据
+		me.arrayGet = function(arr, prop, value){
+			for (var i = 0; i < arr.length; i++) {
+				if(arr[i][prop] == value){
+					return arr[i];
+				}
+			}
+			return null;
+		}
+		
+		// 从数组删除指定记录
+		me.arrayDelete = function(arr, item){
+			if(item instanceof Array) {
+				for (let i = 0; i < item.length; i++) {
+					let ite = item[i];
+					let index = arr.indexOf(ite);
+					if (index > -1) {
+						arr.splice(index, 1);
+					}
+				}
+			} else {
+				var index = arr.indexOf(item);
+				if (index > -1) {
+					arr.splice(index, 1);
+				}
+			}
+		}
+		
+		// 从数组删除指定id的记录
+		me.arrayDeleteById = function(arr, id){
+			var item = me.arrayGet(arr, 'id', id);
+			me.arrayDelete(arr, item);
+		}
+		
+		// 将数组B添加到数组A的开头
+		me.unshiftArray = function(arrA, arrB){
+			if(arrB){
+		    	arrB.reverse().forEach(function(ts){
+		    		arrA.unshift(ts);
+		    	})
+			}
+			return arrA;
+		}
+		
+		// 将数组B添加到数组A的末尾
+		me.pushArray = function(arrA, arrB){
+			if(arrB){
+		    	arrB.forEach(function(ts){
+		    		arrA.push(ts);
+		    	})
+			}
+			return arrA;
+		}
+		
+		// == if 结束
+	}
+	
+	// ===========================  浏览器相关   ======================================= 
+	if (true) {
+		
+		// set cookie 值 
+		me.setCookie = function setCookie(cname, cvalue, exdays) { 
+			exdays = exdays || 30;
+		    var d = new Date();
+		    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
+		    var expires = "expires=" + d.toGMTString();
+		    document.cookie = cname + "=" + escape(cvalue) + "; " + expires + "; path=/";
+		}
+		
+		// get cookie 值
+		me.getCookie = function(objName){
+			var arrStr = document.cookie.split("; ");
+		    for (var i = 0; i < arrStr.length; i++) {
+		        var temp = arrStr[i].split("=");
+		        if (temp[0] == objName){
+		        	return unescape(temp[1])
+		        };
+		    }
+		    return "";
+		}
+		
+		// 复制指定文本
+		me.copyText = function(str){
+			var oInput = document.createElement('input');
+		    oInput.value = str;
+		    document.body.appendChild(oInput);
+		    oInput.select(); // 选择对象
+		    document.execCommand("Copy"); // 执行浏览器复制命令
+		    oInput.className = 'oInput';
+		    oInput.style.display='none';
+		}
+		
+		// jquery序列化表单增强版: 排除空值
+		me.serializeNotNull = function(selected){
+			var serStr = $(selected).serialize();
+		    return serStr.split("&").filter(function(str){return !str.endsWith("=")}).join("&");
+		}
+		
+		// 将cookie序列化为k=v形式
+		me.strCookie = function(){
+			return document.cookie.replace(/; /g,"&");
+		}
+		
+		// 回到顶部
+		me.goTop = function() {
+			function smoothscroll(){
+				var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
+				if (currentScroll > 0) {
+					 window.requestAnimationFrame(smoothscroll);
+					 window.scrollTo (0,currentScroll - (currentScroll/5));
+				}
+			};
+			smoothscroll();
+		}
+		
+		
+		
+		// == if 结束
+	}
+	
+	// =========================== javascript对象操作   ======================================= 
+	if (true) {
+		// 去除json对象中的空值 
+		me.removeNull = function(obj){
+			var newObj = {};
+			if(obj != undefined && obj != null) {
+				for(var key in obj) {
+					if(obj[key] === undefined || obj[key] === null || obj[key] === '') {
+						// 
+					} else {
+						newObj[key] = obj[key];
+					}
+				}
+			}
+			return newObj;
+		}
+		
+		// JSON 浅拷贝, 返回拷贝后的obj
+		me.copyJSON = function(obj){
+			if(obj === null || obj === undefined) {
+				return obj;
+			};
+			var new_obj = {};
+			for(var key in obj) {
+				new_obj[key] = obj [key];
+			}
+			return new_obj;
+		}
+		
+		// json合并, 将 defaulet配置项 转移到 user配置项里 并返回 user配置项
+		me.extendJson = function(userOption, defaultOption) {
+			if(!userOption) {
+				return defaultOption;
+			};
+			for(var key in defaultOption) {
+				if(userOption[key] === undefined) {
+					userOption[key] = defaultOption[key];
+				} else if(userOption[key] == null){
+					
+				} else if(typeof userOption[key] == "object") {
+					me.extendJson(userOption[key], defaultOption[key]); //深度匹配
+				}
+			}
+			return userOption;
+		}
+		
+		// == if 结束
+	}
+	
+	// ===========================  本地集合存储   ======================================= 
+	if (true) {
+		
+		// 获取指定key的list
+		me.keyListGet = function(key){
+			try{
+				var str = localStorage.getItem('LIST_' + key);
+				if(str == undefined || str == null || str =='' || str == 'undefined' || typeof(JSON.parse(str)) == 'string'){
+					//alert('key' + str);
+					str = '[]';
+				}
+				return JSON.parse(str);
+			}catch(e){
+				return [];
+			}
+		},
+		
+		me.keyListSet = function(key, list){
+			localStorage.setItem('LIST_' + key, JSON.stringify(list));
+		},
+		
+		me.keyListHas = function(key, item){
+			var arr2 = me.keyListGet(key);
+			return arr2.indexOf(item) != -1;
+		},
+		
+		me.keyListAdd = function(key, item){
+			var arr = me.keyListGet(key);
+			arr.push(item);
+			me.keyListSet(key,arr);
+		},
+		
+		me.keyListRemove = function(key, item){
+			var arr = me.keyListGet(key);
+			var index = arr.indexOf(item);
+			if (index > -1) {
+			    arr.splice(index, 1);
+			}
+			me.keyListSet(key,arr);
+		}
+		
+		// == if 结束
+	}
+	
+	
+	// ===========================  对sa-admin的优化   ======================================= 
+	if (true) {
+		
+		// 封装element-ui的导出表格
+		// 参数:选择器(默认.data-count),fileName=导出的文件名称
+		me.exportExcel = function(select, fileName) {
+			
+			// 声明函数 
+			let exportExcel_fn = function(select, fileName) {
+				// 赋默认值
+				select = select || '.data-table';
+				fileName = fileName || 'table.xlsx';
+				// 开始导出
+				let wb = XLSX.utils.table_to_book(document.querySelector(select));   // 这里就是表格
+				let sheet = wb.Sheets.Sheet1;	// 单元表 
+				try{
+					// 强改宽度 
+					sheet['!cols'] = sheet['!cols'] || [];
+					let thList = document.querySelector(select).querySelectorAll('.el-table__header-wrapper tr th');
+					for (var i = 0; i < thList.length; i++) {
+						// 如果是多选框
+						if(thList[i].querySelector('.el-checkbox')) {
+							sheet['!cols'].push({ wch: 5 });	// 强改宽度
+							continue;
+						}
+						sheet['!cols'].push({ wch: 15 });	// 强改宽度
+					}
+					// 强改高度 
+					sheet['!rows'] = sheet['!rows'] || [];
+					let trList = document.querySelector(select).querySelectorAll('.el-table__body-wrapper tbody tr');
+					for (var i = 0; i < trList.length + 1; i++) {
+						sheet['!rows'].push({ hpx: 20 });	// 强改高度 
+					}
+				} catch(e) {
+					console.err(e);
+				}
+				// 开始制作并输出
+				let wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' });
+				// 点击 
+				let blob = new Blob([wbout], { type: 'application/octet-stream'});
+				const a= document.createElement("a")
+				a.href = URL.createObjectURL(blob)
+				a.download = fileName // 这里填保存成的文件名
+				a.click()
+				URL.revokeObjectURL(a.href)
+				a.remove();
+				sa.hideLoading();
+			}
+			
+			sa.loading('正在导出...');
+			// 判断是否首次加载 
+			if(window.XLSX) {
+				return exportExcel_fn(select, fileName);
+			} else {
+				me.loadJS('https://unpkg.com/xlsx@0.16.6/dist/xlsx.core.min.js', function() {
+					return exportExcel_fn(select, fileName);
+				});
+			}
+			
+		}
+		
+		// 刷新表格高度, 请务必在所有表格高度发生变化的地方调用此方法
+		me.f5TableHeight = function() {
+			Vue.nextTick(function() {
+				if($('.el-table.data-table .el-table__body-wrapper table').length == 0) {
+					return;
+				}
+				var _f5Height = function() {
+					var height = $('.el-table .el-table__body-wrapper table').height();
+					height = height == 0 ? 60 : height;
+					// 判断是否有滚动条
+					var tw = $('.el-table .el-table__body-wrapper').get(0);
+					if(tw.scrollWidth > tw.clientWidth) {
+						height = height + 16;
+					}
+					if($('.el-table .el-table__body-wrapper table td').width() == 0) {
+						return;
+					}
+					// 设置高度
+					$('.el-table .el-table__body-wrapper').css('min-height', height);
+					$('.el-table .el-table__body-wrapper').css('max-height', height);
+				};
+				
+				setTimeout(_f5Height, 0)
+				setTimeout(_f5Height, 200)
+			})
+		}
+		
+		// 在表格查询的页面,监听input回车事件,提交查询
+		me.onInputEnter = function(app) {
+			Vue.nextTick(function() {
+				app = app || window.app;
+				// document.querySelectorAll('.el-form input').forEach(function(item) {
+				// 	item.onkeydown = function(e) {
+				// 		var theEvent = e || window.event;
+				// 		var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
+				// 		if (code == 13) {
+				// 			app.p.pageNo = 1;
+				// 			app.f5();
+				// 		}    
+				// 	}
+				// })
+				document.querySelectorAll('.el-form').forEach(function(item) {
+					item.onkeydown = function(e) {
+						var theEvent = e || window.event;
+						var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
+						if (code == 13) {
+							var target = e.target||e.srcElement;
+							if(target.tagName.toLowerCase()=="input") {
+								app.p.pageNo = 1;
+								app.f5();
+							}
+						}
+					}
+				})
+			})
+		}
+		
+		// 如果value为true,则抛出异常 
+		me.check = function(value, errorMsg) {
+			if(value === true) {
+				throw {type: 'sa-error', msg: errorMsg};
+			}
+		}
+		
+		// 如果value为null,则抛出异常 
+		me.checkNull = function(value, errorMsg) {
+			if(me.isNull(value)) {
+				throw {type: 'sa-error', msg: errorMsg};
+			}
+		}
+		
+		// 监听窗口变动
+		if(!window.onresize) {
+			window.onresize = function() {
+				try{
+					me.f5TableHeight();
+				}catch(e){
+					// console.log(e);
+				}
+			}
+		}
+			
+		// 双击layer标题处全屏
+		if(window.$) {
+			$(document).on('mousedown', '.layui-layer-title', function(e) {
+				// console.log('单击中');
+				if(window.layer_title_last_click_time) {
+					var cz = new Date().getTime() - window.layer_title_last_click_time;
+					if(cz < 250) {
+						console.log('双击');
+						$(this).parent().find('.layui-layer-max').click();
+					}
+				}
+				window.layer_title_last_click_time = new Date().getTime();
+			})
+		}
+		
+		// == if 结束
+	}
+	
+	
+	
+	
+})();
+
+
+// ===========================  $sys 有关当前系统的方法  一般不能复制到别的项目中用  ======================================= 
+(function(){
+	
+	// 超级对象
+    var me = {};
+    sa.$sys = me;
+	
+	// ======================= 登录相关 ============================
+	// 写入当前已登陆用户信息
+	me.setCurrUser = function(currUser){
+		localStorage.setItem('currUser', JSON.stringify(currUser));
+	}
+	
+	// 获得当前已登陆用户信息
+	me.getCurrUser = function(){
+		var user = localStorage.getItem("currUser");
+		if(user == undefined || user == null || user == 'null' || user == '' || user == '{}' || user.length < 10){
+			user = {
+				id: '0',
+				username: '未登录'
+			}
+		}else{
+			user = JSON.parse(user);
+		}
+		return user;
+	}
+	
+	// 如果未登录,则强制跳转到登录 
+	me.checkLogin = function(not_login_url){
+		console.log(me.getCurrUser());
+		if(me.getCurrUser().id == 0) {
+			location.href= not_login_url || '../../login.html';
+			throw '未登录,请先登录';
+		}
+	}
+	
+	// 同上, 只不过是以弹窗的形式显示未登录
+	me.checkLoginTs = function(not_login_url){
+		if(me.getCurrUser().id == 0) {
+			sa.$page.openLogin(not_login_url || '../../login.html');
+			throw '未登录,请先登录';
+		}
+	}
+	
+	
+	// ========================= 权限验证 ========================= 
+	
+	// 定义key
+	var pcode_key = 'permission_code';
+	
+	// 写入当前会话的权限码集合
+	sa.setAuth = function(codeList) {
+		sa.keyListSet(pcode_key, codeList);	
+	}
+	
+	// 清除当前会话的权限码集合 
+	sa.clearAuth = function() {
+		sa.keyListSet(pcode_key, []);	
+	}
+	
+	// 检查当前会话是否拥有一个权限码, 返回true和false 
+	sa.isAuth = function(pcode) {
+		return sa.keyListHas(pcode_key, pcode);
+	}
+	
+	// 检查当前会话是否拥有一个权限码, 如果没有, 则跳转到无权限页面 
+	// 注意: 非二级目录页面请注意调整路径问题 
+	sa.checkAuth = function(pcode, not_pcode_url) {
+		var is_have = sa.keyListHas(pcode_key, pcode);	
+		if(is_have == false) {
+			location.href= not_pcode_url || '../../sa-view/error-page/403.html';
+			throw '暂无权限: ' + pcode;
+		}
+	}
+	// 同上, 只不过是以弹窗的形式显示出来无权限来 
+	sa.checkAuthTs = function(pcode, not_pcode_url) {
+		var is_have = sa.keyListHas(pcode_key, pcode);	
+		if(is_have == false) {
+			var url = not_pcode_url || '../../sa-view/error-page/403.html';
+			layer.open({
+				type: 2,	
+				title: false,	// 标题 
+				shadeClose: true,	// 是否点击遮罩关闭
+				shade: 0.8,		// 遮罩透明度 
+				scrollbar: false,	// 屏蔽掉外层的滚动条 
+				closeBtn: false,
+				area: ['700px', '600px'],	// 大小  
+				content: url	// 传值 
+			}); 
+			throw '暂无权限: ' + pcode;
+		}
+	}
+	
+	
+	
+	// ======================= 配置相关 ============================
+	// 写入配置信息
+	me.setAppCfg = function(cfg) {
+		if(typeof cfg != 'string') {
+			cfg = JSON.stringify(cfg);
+		}
+		localStorage.setItem('app_cfg', cfg);
+	}
+	
+	// 获取配置信息
+	me.getAppCfg = function() {
+		var app_cfg = sa.JSONParse(localStorage.getItem('app_cfg'), {}) || {};
+		return app_cfg;
+	}
+	
+	
+	
+	
+})();
+
+
+// ===========================  $page 跳页面相关 避免一次变动,到处乱改 ======================================= 
+(function(){
+	
+	// 超级对象
+    var me={};
+    sa.$page = me;
+	
+	// 打开登录页面
+	me.openLogin = function(login_url) {
+		layer.open({
+			type: 2,
+			// title: '登录',
+			title: false,
+			closeBtn: false,
+			shadeClose: true,
+			shade: 0.8,
+			// area: ['90%', '100%'],
+			area: ['70%', '80%'],
+			// area: ['450px', '360px'],
+			resize: false,
+			content: login_url || '../../login.html'
+		}); 
+	}
+	
+	// 打开admin信息界面 
+	me.openAdminInfo = function(id, username) {
+		var title = username + ' - 账号详情';
+		if(username === undefined) {
+			title = '账号详情';
+		}
+		sa.showIframe(title, '../sp-admin/admin-info.html?id=' + id, '700px', '600px');
+	}
+	
+})();
+
+
+// 如果是sa_admin环境 
+window.sa_admin = window.sa_admin || parent.sa_admin || top.sa_admin;
+window.saAdmin = window.sa_admin;
+
+// 如果当前是Vue环境, 则挂在到 Vue 示例
+if(window.Vue) {
+	// 全局的 sa 对象
+	Vue.prototype.sa = window.sa;
+	Vue.prototype.sa_admin = window.sa_admin;
+	Vue.prototype.saAdmin = window.saAdmin;
+	
+	// 表单校验异常捕获 
+	Vue.config.errorHandler = function(err, vm) {
+		if(err.type == 'sa-error') {
+			return sa.error(err.msg);
+		}
+		throw err;
+	}
+	
+	// Element-UI 全局组件样式  
+	Vue.prototype.$ELEMENT = { size: 'mini', zIndex: 3000 };
+	
+	// 加载全局组件 (注意路径问题)
+	// if(window.httpVueLoader && window.loadComponent !== false) {
+	// 	Vue.component("sa-item", httpVueLoader('../../sa-frame/com/sa-item.vue'));
+	// 	Vue.component("sa-td", httpVueLoader('../../sa-frame/com/sa-td.vue'));
+	// }
+	
+}
+
+// 对外开放, 在模块化时解开此注释 
+// export default sa;
+

Деякі файли не було показано, через те що забагато файлів було змінено