Procházet zdrojové kódy

!1076 增加首页可拖拽看板
Merge pull request !1076 from SW/next

zuohuaijun před 1 rokem
rodič
revize
30b61d584c

+ 40 - 0
.vscode/launch.json

@@ -0,0 +1,40 @@
+{
+  // Use IntelliSense to learn about possible attributes.
+  // Hover to view descriptions of existing attributes.
+  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "web",
+      "command": "vite",
+      "request": "launch",
+      "type": "node-terminal",
+      "cwd": "${workspaceFolder}/Web"
+    },
+    {
+      "name": ".NET",
+      "type": "coreclr",
+      "request": "launch",
+      "preLaunchTask": "build",
+      "program": "${workspaceFolder}/Admin.NET/Admin.NET.Web.Entry/bin/Debug/net6.0/Admin.NET.Web.Entry.dll",
+      "args": [],
+      "cwd": "${workspaceFolder}/Admin.NET/Admin.NET.Web.Entry",
+      "stopAtEntry": false,
+      "serverReadyAction": {
+        "action": "openExternally",
+        "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+      },
+      "env": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "sourceFileMap": {
+        "/Views": "${workspaceFolder}/Views"
+      }
+    },
+    {
+      "name": ".NET Core Attach",
+      "type": "coreclr",
+      "request": "attach"
+    }
+  ]
+}

+ 41 - 0
.vscode/tasks.json

@@ -0,0 +1,41 @@
+{
+  "version": "2.0.0",
+  "tasks": [
+    {
+      "label": "build",
+      "command": "dotnet",
+      "type": "process",
+      "args": [
+        "build",
+        "${workspaceFolder}/Admin.NET/Admin.NET.sln",
+        "/property:GenerateFullPaths=true",
+        "/consoleloggerparameters:NoSummary;ForceNoAlign"
+      ],
+      "problemMatcher": "$msCompile"
+    },
+    {
+      "label": "publish",
+      "command": "dotnet",
+      "type": "process",
+      "args": [
+        "publish",
+        "${workspaceFolder}/Admin.NET/Admin.NET.sln",
+        "/property:GenerateFullPaths=true",
+        "/consoleloggerparameters:NoSummary;ForceNoAlign"
+      ],
+      "problemMatcher": "$msCompile"
+    },
+    {
+      "label": "watch",
+      "command": "dotnet",
+      "type": "process",
+      "args": [
+        "watch",
+        "run",
+        "--project",
+        "${workspaceFolder}/Admin.NET/Admin.NET.sln"
+      ],
+      "problemMatcher": "$msCompile"
+    }
+  ]
+}

+ 3 - 0
Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs

@@ -51,6 +51,9 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
             return;
         }
 
+        var loggingMonitor1 = JSON.Deserialize<JObject>(jsonStr);
+        // 不记录数据校验日志
+        if (loggingMonitor1["validation"] != null) return;
         var loggingMonitor = JSON.Deserialize<dynamic>(jsonStr);
         // 不记录数据校验日志
         if (loggingMonitor.validation != null) return;

+ 1 - 0
Web/package.json

@@ -59,6 +59,7 @@
 		"vue-router": "^4.3.2",
 		"vue-signature-pad": "^3.0.2",
 		"vue3-tree-org": "^4.2.2",
+		"vuedraggable": "4.0.3",
 		"xlsx-js-style": "^1.2.0"
 	},
 	"devDependencies": {

binární
Web/src/assets/img/ver.svg


+ 74 - 0
Web/src/components/scEcharts/echarts-theme-T.js

@@ -0,0 +1,74 @@
+const T = {
+	"color": [
+		"#409EFF",
+		"#36CE9E",
+		"#f56e6a",
+		"#626c91",
+		"#edb00d",
+		"#909399"
+	],
+	'grid': {
+		'left': '3%',
+		'right': '3%',
+		'bottom': '10',
+		'top': '40',
+		'containLabel': true
+	},
+	"legend": {
+		"textStyle": {
+			"color": "#999"
+		},
+		"inactiveColor": "rgba(128,128,128,0.4)"
+	},
+	"categoryAxis": {
+		"axisLine": {
+			"show": true,
+			"lineStyle": {
+				"color": "rgba(128,128,128,0.2)",
+				"width": 1
+			}
+		},
+		"axisTick": {
+			"show": false,
+			"lineStyle": {
+				"color": "#333"
+			}
+		},
+		"axisLabel": {
+			"color": "#999"
+		},
+		"splitLine": {
+			"show": false,
+			"lineStyle": {
+				"color": [
+					"#eee"
+				]
+			}
+		},
+		"splitArea": {
+			"show": false,
+			"areaStyle": {
+				"color": [
+					"rgba(255,255,255,0.01)",
+					"rgba(0,0,0,0.01)"
+				]
+			}
+		}
+	},
+	"valueAxis": {
+		"axisLine": {
+			"show": false,
+			"lineStyle": {
+				"color": "#999"
+			}
+		},
+		"splitLine": {
+			"show": true,
+			"lineStyle": {
+				"color": "rgba(128,128,128,0.2)"
+			}
+		}
+	}
+}
+
+export default T

+ 68 - 0
Web/src/components/scEcharts/index.vue

@@ -0,0 +1,68 @@
+<template>
+	<div ref="scEcharts" :style="{height:height, width:width}"></div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+import T from './echarts-theme-T.js';
+
+echarts.registerTheme('T', T);
+const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);
+
+export default {
+	...echarts,
+	name: "scEcharts",
+	props: {
+		height: {type: String, default: "100%"},
+		width: {type: String, default: "100%"},
+		nodata: {type: Boolean, default: false},
+		option: {
+			type: Object, default: () => {
+			}
+		}
+	},
+	data() {
+		return {
+			isActivat: false,
+			myChart: null
+		}
+	},
+	watch: {
+		option: {
+			deep: true,
+			handler(v) {
+				unwarp(this.myChart).setOption(v);
+			}
+		}
+	},
+	computed: {
+		myOptions: function () {
+			return this.option || {};
+		}
+	},
+	activated() {
+		if (!this.isActivat) {
+			this.$nextTick(() => {
+				this.myChart.resize()
+			})
+		}
+	},
+	deactivated() {
+		this.isActivat = false;
+	},
+	mounted() {
+		this.isActivat = true;
+		this.$nextTick(() => {
+			this.draw();
+		})
+	},
+	methods: {
+		draw() {
+			let myChart = echarts.init(this.$refs.scEcharts, 'T');
+			myChart.setOption(this.myOptions);
+			this.myChart = myChart;
+			window.addEventListener('resize', () => myChart.resize());
+		}
+	}
+}
+</script>

+ 2 - 2
Web/src/router/index.ts

@@ -84,11 +84,11 @@ export function formatTwoStageRoutes(arr: any) {
 			// 路径:/@/layout/routerView/parent.vue
 			if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
 				cacheList.push(v.name);
-				const stores = useKeepALiveNames(pinia);
-				stores.setCacheKeepAlive(cacheList);
 			}
 		}
 	});
+	const stores = useKeepALiveNames(pinia);
+	stores.setCacheKeepAlive(cacheList);
 	return newArr;
 }
 

+ 35 - 0
Web/src/views/home/test.vue

@@ -0,0 +1,35 @@
+<template>
+	<div v-if="pageLoading">
+		<el-main>
+			<el-card shadow="never">
+				<el-skeleton :rows="1"></el-skeleton>
+			</el-card>
+			<el-card shadow="never" style="margin-top: 15px">
+				<el-skeleton></el-skeleton>
+			</el-card>
+		</el-main>
+	</div>
+	<div v-else>
+		<widgets></widgets>
+	</div>
+</template>
+
+<script setup lang='ts' name='homepage'>
+import { defineAsyncComponent, onMounted, reactive } from 'vue';
+// import { useHomepage } from 'src/stores/homepage';
+const widgets = defineAsyncComponent(() => import('./widgets/index.vue'));
+// const homepageStore = useHomepage();
+
+const state = reactive({
+	name: 'dashboard',
+	pageLoading: true,
+	dashboard: '0',
+});
+
+onMounted(() => {
+	state.pageLoading = false;
+});
+</script>
+
+<style>
+</style>

+ 27 - 0
Web/src/views/home/widgets/components/about.vue

@@ -0,0 +1,27 @@
+<template>
+	<el-card shadow="hover" header="关于项目" class="item-background">
+		<p>高性能 / 精致 / 优雅。基于Vue3 + Element-Plus 的中后台前端解决方案,如果喜欢就点个星星支持一下。</p>
+		<p>
+			<a href='https://gitee.com/pi3b/Admin.NET.git' target="_blank">
+				<img src='https://gitee.com/pi3b/Admin.NET/badge/star.svg?theme=dark' alt='star' style="vertical-align: middle">
+			</a>
+		</p>
+	</el-card>
+</template>
+
+<script>
+	export default {
+		title: "关于项目",
+		icon: "el-icon-setting",
+		description: "点个星星支持一下",
+		data() {
+			return {
+				
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.item-background p {color: #999;margin-top:10px;line-height: 1.8;}
+</style>

+ 98 - 0
Web/src/views/home/widgets/components/echarts.vue

@@ -0,0 +1,98 @@
+<template>
+	<el-card shadow="hover" header="实时收入" v-loading="loading">
+		<scEcharts ref="c1" height="300px" :option="option"></scEcharts>
+	</el-card>
+</template>
+
+<script>
+	import scEcharts from '/@/components/scEcharts/index.vue';
+
+	export default {
+		title: "实时收入",
+		icon: "el-icon-data-line",
+		description: "Echarts组件演示",
+		components: {
+			scEcharts
+		},
+		data() {
+			return {
+				loading: true,
+				option: {}
+			}
+		},
+		created() {
+			var _this = this;
+			setTimeout(function() {
+				_this.loading = false
+			}, 500);
+
+			var option = {
+				tooltip: {
+					trigger: 'axis'
+				},
+				xAxis: {
+					boundaryGap: false,
+					type: 'category',
+					data: (function (){
+						var now = new Date();
+						var res = [];
+						var len = 30;
+						while (len--) {
+							res.unshift(now.toLocaleTimeString().replace(/^\D*/,''));
+							now = new Date(now - 2000);
+						}
+						return res;
+					})()
+				},
+				yAxis: [{
+					type: 'value',
+					name: '价格',
+					"splitLine": {
+						"show": false
+					}
+				}],
+				series: [
+					{
+						name: '收入',
+						type: 'line',
+						symbol: 'none',
+						lineStyle: {
+							width: 1,
+							color: '#409EFF'
+						},
+						areaStyle: {
+							opacity: 0.1,
+							color: '#79bbff'
+						},
+						data: (function (){
+							var res = [];
+							var len = 30;
+							while (len--) {
+								res.push(Math.round(Math.random() * 0));
+							}
+							return res;
+						})()
+					},
+				],
+			};
+			this.option = option;
+
+		},
+		mounted(){
+			 var _this = this;
+			setInterval(function (){
+				var o = _this.option;
+
+				o.series[0].data.shift()
+				o.series[0].data.push(Math.round(Math.random() * 100));
+
+				o.xAxis.data.shift();
+				o.xAxis.data.push((new Date()).toLocaleTimeString().replace(/^\D*/, ''));
+
+
+				//_this.$refs.c1.myChart.setOption(o)
+			},2100)
+
+		},
+	}
+</script>

+ 9 - 0
Web/src/views/home/widgets/components/index.js

@@ -0,0 +1,9 @@
+import {markRaw} from 'vue';
+const resultComps = {}
+let requireComponent = import.meta.glob('./*.vue',{eager: true})
+Object.keys(requireComponent).forEach(fileName => {
+	//replace(/(\.\/|\.js)/g, '')
+	resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, '$1')] = requireComponent[fileName].default
+})
+export default markRaw(resultComps)
+

+ 208 - 0
Web/src/views/home/widgets/components/myapp.vue

@@ -0,0 +1,208 @@
+<template>
+	<el-card shadow="hover" header="快捷入口">
+		<ul class="myMods">
+			<!-- <li v-for="mod in myMods" :key="mod.path" :style="{ background:mod.meta.color||'#eeeeee'}"> -->
+			<li v-for="mod in myMods" :key="mod.path">
+				<a v-if="mod.meta.type == 'link'" :href="mod.path" target="_blank">
+					<el-icon><component :is="mod.meta.icon || el - icon - menu" /></el-icon>
+					<p>{{ mod.meta.title }}</p>
+				</a>
+				<router-link v-else :to="{ path: mod.path }">
+					<el-icon><component :is="mod.meta.icon || el - icon - menu" /></el-icon>
+					<p>{{ mod.meta.title }}</p>
+				</router-link>
+			</li>
+			<li class="modItem-add" @click="addMods">
+				<a href="javascript:void(0)">
+					<el-icon><el-icon-plus :style="{ color: '#fff' }" /></el-icon>
+				</a>
+			</li>
+		</ul>
+
+		<el-drawer title="添加应用" v-model="modsDrawer" :size="570" destroy-on-close>
+			<div class="setMods">
+				<h4>我的常用 ( {{ myMods.length }} )</h4>
+				<draggable tag="ul" v-model="myMods" animation="200" item-key="path" group="people">
+					<template #item="{ element }">
+						<li>
+							<el-icon><component :is="element.meta.icon || el - icon - menu" /></el-icon>
+							<p>{{ element.meta.title }}</p>
+						</li>
+					</template>
+				</draggable>
+			</div>
+			<div class="setMods">
+				<h4>全部应用 ( {{ filterMods.length }} )</h4>
+				<draggable tag="ul" v-model="filterMods" animation="200" item-key="path" :sort="false" group="people">
+					<template #item="{ element }">
+						<li :style="{ background: element.meta.color || '#909399' }">
+							<el-icon><component :is="element.meta.icon || el - icon - menu" /></el-icon>
+							<p>{{ element.meta.title }}</p>
+						</li>
+					</template>
+				</draggable>
+			</div>
+			<template #footer>
+				<el-button @click="modsDrawer = false">取消</el-button>
+				<el-button type="primary" @click="saveMods">保存</el-button>
+			</template>
+		</el-drawer>
+	</el-card>
+</template>
+
+<script>
+import draggable from 'vuedraggable';
+import tool from '../tool';
+import { useRequestOldRoutes } from '/@/stores/requestOldRoutes';
+export default {
+	title: '快捷入口',
+	icon: 'el-icon-monitor',
+	description: '可以配置的快捷入口',
+	components: {
+		draggable,
+	},
+	data() {
+		return {
+			mods: [],
+			myMods: [],
+			myModsName: [],
+			filterMods: [],
+			modsDrawer: false,
+		};
+	},
+	mounted() {
+		this.getMods();
+	},
+	methods: {
+		addMods() {
+			this.modsDrawer = true;
+		},
+		getMods() {
+			this.myModsName = tool.data.get('my-mods') || [];
+			var menuTree = useRequestOldRoutes().requestOldRoutes || [];
+			this.filterMenu(menuTree);
+			this.myMods = this.mods.filter((item) => {
+				return this.myModsName.includes(item.name);
+			});
+			this.filterMods = this.mods.filter((item) => {
+				return !this.myModsName.includes(item.name);
+			});
+		},
+		filterMenu(map) {
+			map.forEach((item) => {
+				if (item.meta.isHide || item.type == 3 || item.status != 1) {
+					return false;
+				}
+				if (item.meta.isIframe) {
+					item.path = `/i/${item.name}`;
+				}
+				if (item.children && item.children.length > 0) {
+					this.filterMenu(item.children);
+				} else {
+					this.mods.push(item);
+				}
+			});
+		},
+		saveMods() {
+			const myModsName = this.myMods.map((v) => v.name);
+			tool.data.set('my-mods', myModsName);
+			this.$message.success('设置常用成功');
+			this.modsDrawer = false;
+		},
+	},
+};
+</script>
+
+<style scoped lang='scss'>
+.myMods {
+	list-style: none;
+	margin: -10px;
+}
+.myMods li {
+	display: inline-block;
+	width: 100px;
+	height: 100px;
+	vertical-align: top;
+	transition: all 0.3s ease;
+	margin: 10px;
+	border-radius: 5px;
+	background: var(--el-color-primary);
+}
+.myMods li:hover {
+	opacity: 0.5;
+}
+.myMods li a {
+	width: 100%;
+	height: 100%;
+	padding: 10px;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	text-align: center;
+	color: #fff;
+}
+.myMods li i {
+	font-size: 26px;
+	color: #fff;
+}
+.myMods li p {
+	font-size: 14px;
+	color: #fff;
+	margin-top: 10px;
+	width: 100%;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+
+.modItem-add {
+	border: 1px dashed #ddd;
+	cursor: pointer;
+}
+.modItem-add i {
+	font-size: 30px;
+	color: #999 !important;
+}
+.modItem-add:hover,
+.modItem-add:hover i {
+	border-color: #409eff;
+	color: #409eff !important;
+}
+
+.setMods {
+	padding: 0 20px;
+}
+.setMods h4 {
+	font-size: 14px;
+	font-weight: normal;
+}
+.setMods ul {
+	margin: 20px -5px;
+	min-height: 90px;
+}
+.setMods li {
+	display: inline-block;
+	width: 80px;
+	height: 80px;
+	text-align: center;
+	margin: 5px;
+	color: #fff;
+	vertical-align: top;
+	padding: 4px;
+	padding-top: 15px;
+	cursor: move;
+	border-radius: 3px;
+	background: var(--el-color-primary);
+}
+.setMods li i {
+	font-size: 20px;
+}
+.setMods li p {
+	font-size: 12px;
+	margin-top: 10px;
+}
+.setMods li.sortable-ghost {
+	opacity: 0.3;
+}
+</style>

+ 32 - 0
Web/src/views/home/widgets/components/progress.vue

@@ -0,0 +1,32 @@
+<template>
+	<el-card shadow="hover" header="进度环">
+		<div class="progress">
+			<el-progress type="dashboard" :percentage="85.5" :width="160">
+				<template #default="{ percentage }">
+				    <div class="percentage-value">{{ percentage }}%</div>
+				    <div class="percentage-label">当前进度</div>
+				  </template>
+			</el-progress>
+		</div>
+
+	</el-card>
+</template>
+
+<script>
+	export default {
+		title: "进度环",
+		icon: "el-icon-odometer",
+		description: "进度环原子组件演示",
+		data() {
+			return {
+
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.progress {text-align: center;}
+	.progress .percentage-value {font-size: 28px;}
+	.progress .percentage-label {font-size: 12px;margin-top: 10px;}
+</style>

+ 50 - 0
Web/src/views/home/widgets/components/time.vue

@@ -0,0 +1,50 @@
+<template>
+	<el-card shadow="hover" header="时钟" class="item-background">
+		<div class="time">
+			<h2>{{ time }}</h2>
+			<p>{{ day }}</p>
+		</div>
+	</el-card>
+</template>
+
+<script>
+import tool from '../tool';
+export default {
+	title: '时钟',
+	icon: 'el-icon-clock',
+	description: '演示部件效果',
+	data() {
+		return {
+			time: '',
+			day: '',
+		};
+	},
+	mounted() {
+		this.showTime();
+		setInterval(() => {
+			this.showTime();
+		}, 1000);
+	},
+	methods: {
+		showTime() {
+			this.time = tool.dateFormat(new Date(), 'hh:mm:ss');
+			this.day = tool.dateFormat(new Date(), 'yyyy年MM月dd日');
+		},
+	},
+};
+</script>
+
+<style scoped>
+.item-background {
+	background: linear-gradient(to right, #8e54e9, #4776e6);
+	color: #fff;
+}
+.time h2 {
+	font-size: 40px;
+}
+.time p {
+	font-size: 14px;
+	margin-top: 13px;
+	opacity: 0.7;
+}
+</style>

+ 42 - 0
Web/src/views/home/widgets/components/ver.vue

@@ -0,0 +1,42 @@
+<template>
+	<el-card shadow="hover" header="版本信息">
+		<div style="height: 210px;text-align: center;">
+			<img src="/@/assets/img/ver.svg" style="height:140px" />
+			<h2 style="margin-top: 15px;">admin.net </h2>
+			<p style="margin-top: 5px;">最新版本 {{ ver }}</p>
+		</div>
+		<div style="margin-top: 20px;">
+			<el-button type="primary" plain round @click="golog">更新日志</el-button>
+			<el-button type="primary" plain round @click="gogit">gitee</el-button>
+		</div>
+	</el-card>
+</template>
+
+<script>
+// import demo from '@/api/model/demo.js'
+export default {
+	title: "版本信息",
+	icon: "el-icon-monitor",
+	description: "当前项目版本信息",
+	data() {
+		return {
+			ver: 'loading...'
+		}
+	},
+	mounted() {
+		this.getVer()
+	},
+	methods: {
+		async getVer() {
+			// const ver = await demo.ver.get()
+			this.ver = '11'
+		},
+		golog() {
+			window.open("https://gitee.com/pi3b/Admin.NET/issues")
+		},
+		gogit() {
+			window.open("https://gitee.com/pi3b/Admin.NET.git")
+		}
+	}
+}
+</script>

+ 103 - 0
Web/src/views/home/widgets/components/welcome.vue

@@ -0,0 +1,103 @@
+<template>
+	<el-card shadow="hover" header="欢迎">
+		<div class="welcome">
+			<div class="logo">
+				<img src="/@/assets/logo.png">
+				<h2>欢迎体验myhomepage</h2>
+			</div>
+			<div class="tips">
+				<div class="tips-item">
+					<div class="tips-item-icon"><el-icon><el-icon-menu /></el-icon></div>
+					<div class="tips-item-message">这里是项目控制台,你可以点击右上方的“自定义”按钮来添加移除或者移动部件。</div>
+				</div>
+				<div class="tips-item">
+					<div class="tips-item-icon"><el-icon><el-icon-promotion /></el-icon></div>
+					<div class="tips-item-message">在提高前端算力、减少带宽请求和代码执行力上多次优化,并且持续着。</div>
+				</div>
+				<div class="tips-item">
+					<div class="tips-item-icon"><el-icon><el-icon-milk-tea /></el-icon></div>
+					<div class="tips-item-message">项目目的:让前端工作更快乐</div>
+				</div>
+			</div>
+			<div class="actions">
+				<el-button type="primary" icon="el-icon-check" size="large" @click="godoc">文档</el-button>
+			</div>
+		</div>
+	</el-card>
+</template>
+
+<script>
+export default {
+	title: "欢迎",
+	icon: "el-icon-present",
+	description: "项目特色以及文档链接",
+	data() {
+		return {
+
+		}
+	},
+	methods: {
+		godoc() {
+			window.open("https://gitee.com/pi3b/Admin.NET.git")
+		}
+	}
+}
+</script>
+
+<style scoped>
+.welcome {}
+
+.welcome .logo {
+	text-align: center;
+}
+
+.welcome .logo img {
+	vertical-align: bottom;
+	width: 100px;
+	height: 100px;
+	margin-bottom: 20px;
+}
+
+.welcome .logo h2 {
+	font-size: 30px;
+	font-weight: normal;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tips {
+	margin-top: 20px;
+	padding: 0 40px;
+}
+
+.tips-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 7.5px 0;
+}
+
+.tips-item-icon {
+	width: 40px;
+	height: 40px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 50%;
+	font-size: 18px;
+	margin-right: 20px;
+	color: var(--el-color-primary);
+	background: rgba(180, 180, 180, 0.1);
+}
+
+.tips-item-message {
+	flex: 1;
+	font-size: 14px;
+}
+
+.actions {
+	text-align: center;
+	margin: 40px 0 20px 0;
+}
+</style>

+ 451 - 0
Web/src/views/home/widgets/index.vue

@@ -0,0 +1,451 @@
+<template>
+	<div :class="['widgets-home', customizing ? 'customizing' : '']" ref="main">
+		<div class="widgets-content">
+			<div class="widgets-top">
+				<div class="widgets-top-title">控制台</div>
+				<div class="widgets-top-actions">
+					<el-button v-if="customizing" type="primary" icon="el-icon-check" round @click="save">完成</el-button>
+					<el-button v-else type="primary" icon="el-icon-edit" round @click="custom">自定义</el-button>
+				</div>
+			</div>
+			<div class="widgets" ref="widgets">
+				<div class="widgets-wrapper">
+					<div v-if="nowCompsList.length <= 0" class="no-widgets">
+						<el-empty image="img/no-widgets.svg" description="没有部件啦" :image-size="280"></el-empty>
+					</div>
+					<el-row :gutter="15">
+						<el-col v-for="(item, index) in grid.layout" v-bind:key="index" :md="item" :xs="24">
+							<draggable
+								v-model="grid.copmsList[index]"
+								animation="200"
+								handle=".customize-overlay"
+								group="people"
+								item-key="com"
+								dragClass="aaaaa"
+								force-fallback
+								fallbackOnBody
+								class="draggable-box"
+							>
+								<template #item="{ element }">
+									<div class="widgets-item">
+										<component :is="allComps[element]"></component>
+										<div v-if="customizing" class="customize-overlay">
+											<el-button class="close" type="danger" plain icon="el-icon-close" size="small" @click="remove(element)"></el-button>
+											<label
+												><el-icon><component :is="allComps[element].icon" /></el-icon>{{ allComps[element].title }}</label
+											>
+										</div>
+									</div>
+								</template>
+							</draggable>
+						</el-col>
+					</el-row>
+				</div>
+			</div>
+		</div>
+		<div v-if="customizing" class="widgets-aside">
+			<el-container>
+				<el-header>
+					<div class="widgets-aside-title">
+						<el-icon><el-icon-circle-plus-filled /></el-icon>添加部件
+					</div>
+					<div class="widgets-aside-close" @click="close()">
+						<el-icon><el-icon-close /></el-icon>
+					</div>
+				</el-header>
+				<el-header style="height: auto">
+					<div class="selectLayout">
+						<div class="selectLayout-item item01" :class="{ active: grid.layout.join(',') == '12,6,6' }" @click="setLayout([12, 6, 6])">
+							<el-row :gutter="2">
+								<el-col :span="12"><span></span></el-col>
+								<el-col :span="6"><span></span></el-col>
+								<el-col :span="6"><span></span></el-col>
+							</el-row>
+						</div>
+						<div class="selectLayout-item item02" :class="{ active: grid.layout.join(',') == '24,16,8' }" @click="setLayout([24, 16, 8])">
+							<el-row :gutter="2">
+								<el-col :span="24"><span></span></el-col>
+								<el-col :span="16"><span></span></el-col>
+								<el-col :span="8"><span></span></el-col>
+							</el-row>
+						</div>
+						<div class="selectLayout-item item03" :class="{ active: grid.layout.join(',') == '24' }" @click="setLayout([24])">
+							<el-row :gutter="2">
+								<el-col :span="24"><span></span></el-col>
+								<el-col :span="24"><span></span></el-col>
+								<el-col :span="24"><span></span></el-col>
+							</el-row>
+						</div>
+					</div>
+				</el-header>
+				<el-main class="nopadding">
+					<div class="widgets-list">
+						<div v-if="myCompsList.length <= 0" class="widgets-list-nodata">
+							<el-empty description="没有部件啦" :image-size="60"></el-empty>
+						</div>
+						<div v-for="item in myCompsList" :key="item.title" class="widgets-list-item">
+							<div class="item-logo">
+								<el-icon><component :is="item.icon" /></el-icon>
+							</div>
+							<div class="item-info">
+								<h2>{{ item.title }}</h2>
+								<p>{{ item.description }}</p>
+							</div>
+							<div class="item-actions">
+								<el-button type="primary" icon="el-icon-plus" size="small" @click="push(item)"></el-button>
+							</div>
+						</div>
+					</div>
+				</el-main>
+				<el-footer style="height: 51px">
+					<el-button size="small" @click="backDefaul()">恢复默认</el-button>
+				</el-footer>
+			</el-container>
+		</div>
+	</div>
+</template>
+
+<script>
+import { ConsoleLogger } from '@microsoft/signalr/dist/esm/Utils';
+import draggable from 'vuedraggable';
+import allComps from './components';
+import tool from './tool';
+export default {
+	components: {
+		draggable,
+	},
+	data() {
+		return {
+			customizing: false,
+			allComps: allComps,
+			selectLayout: [],
+			defaultGrid: {
+				//默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6]
+				layout: [12, 6, 6],
+				//小组件分布,com取值:views/home/components 文件名
+				copmsList: [['welcome'], ['about', 'ver'], ['time', 'progress']],
+			},
+			grid: [],
+		};
+	},
+	created() {
+		this.grid = tool.data.get('grid') || JSON.parse(JSON.stringify(this.defaultGrid));
+	},
+	mounted() {
+		// this.$emit('on-mounted');
+	},
+	computed: {
+		allCompsList() {
+			var allCompsList = [];
+			for (var key in this.allComps) {
+				allCompsList.push({
+					key: key,
+					title: allComps[key].title,
+					icon: allComps[key].icon,
+					description: allComps[key].description,
+				});
+			}
+			var myCopmsList = this.grid.copmsList.reduce(function (a, b) {
+				return a.concat(b);
+			});
+			for (let comp of allCompsList) {
+				const _item = myCopmsList.find((item) => {
+					return item === comp.key;
+				});
+				if (_item) {
+					//如果界面有,则右边不可选
+					comp.disabled = true;
+				}
+			}
+			return allCompsList;
+		},
+		myCompsList() {
+			var myGrid = tool.data.get('DASHBOARDGRID');
+			if (!myGrid) myGrid = ['welcome', 'myapp', 'ver', 'time', 'progress', 'echarts', 'about'];
+			return this.allCompsList.filter((item) => !item.disabled && myGrid.includes(item.key));
+		},
+		nowCompsList() {
+			return this.grid.copmsList.reduce(function (a, b) {
+				return a.concat(b);
+			});
+		},
+	},
+	methods: {
+		//开启自定义
+		custom() {
+			this.customizing = true;
+			const oldWidth = this.$refs.widgets.offsetWidth;
+			this.$nextTick(() => {
+				const scale = this.$refs.widgets.offsetWidth / oldWidth;
+				this.$refs.widgets.style.setProperty('transform', `scale(${scale})`);
+			});
+		},
+		//设置布局
+		setLayout(layout) {
+			this.grid.layout = layout;
+			if (layout.join(',') == '24') {
+				this.grid.copmsList[0] = [...this.grid.copmsList[0], ...this.grid.copmsList[1], ...this.grid.copmsList[2]];
+				this.grid.copmsList[1] = [];
+				this.grid.copmsList[2] = [];
+			}
+		},
+		//追加
+		push(item) {
+			let target = this.grid.copmsList[0];
+			target.push(item.key);
+		},
+		//隐藏组件
+		remove(item) {
+			var newCopmsList = this.grid.copmsList;
+			newCopmsList.forEach((obj, index) => {
+				var newObj = obj.filter((o) => o != item);
+				newCopmsList[index] = newObj;
+			});
+		},
+		//保存
+		save() {
+			this.customizing = false;
+			this.$refs.widgets.style.removeProperty('transform');
+			tool.data.set('grid', this.grid);
+		},
+		//恢复默认
+		backDefaul() {
+			this.customizing = false;
+			this.$refs.widgets.style.removeProperty('transform');
+			this.grid = JSON.parse(JSON.stringify(this.defaultGrid));
+			tool.data.remove('grid');
+		},
+		//关闭
+		close() {
+			this.customizing = false;
+			this.$refs.widgets.style.removeProperty('transform');
+		},
+	},
+};
+</script>
+
+<style scoped lang="scss">
+.widgets-home {
+	display: flex;
+	flex-direction: row;
+	flex: 1;
+	height: 100%;
+}
+.widgets-content {
+	flex: 1;
+	overflow: auto;
+	overflow-x: hidden;
+	padding: 15px;
+}
+.widgets-aside {
+	width: 360px;
+	background: #fff;
+	box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+	position: relative;
+	overflow: auto;
+}
+.widgets-aside-title {
+	font-size: 14px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.widgets-aside-title i {
+	margin-right: 10px;
+	font-size: 18px;
+}
+.widgets-aside-close {
+	font-size: 18px;
+	width: 30px;
+	height: 30px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 3px;
+	cursor: pointer;
+}
+.widgets-aside-close:hover {
+	background: rgba(180, 180, 180, 0.1);
+}
+
+.widgets-top {
+	margin-bottom: 15px;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+.widgets-top-title {
+	font-size: 18px;
+	font-weight: bold;
+}
+
+.widgets {
+	transform-origin: top left;
+	transition: transform 0.15s;
+}
+
+.draggable-box {
+	height: 100%;
+}
+
+.customizing .widgets-wrapper {
+	margin-right: -360px;
+}
+.customizing .widgets-wrapper .el-col {
+	padding-bottom: 15px;
+}
+.customizing .widgets-wrapper .draggable-box {
+	border: 1px dashed var(--el-color-primary);
+	padding: 15px;
+}
+.customizing .widgets-wrapper .no-widgets {
+	display: none;
+}
+.customizing .widgets-item {
+	position: relative;
+	margin-bottom: 15px;
+}
+.customize-overlay {
+	position: absolute;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	z-index: 1;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	background: rgba(255, 255, 255, 0.9);
+	cursor: move;
+}
+.customize-overlay label {
+	background: var(--el-color-primary);
+	color: #fff;
+	height: 40px;
+	padding: 0 30px;
+	border-radius: 40px;
+	font-size: 18px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	cursor: move;
+}
+.customize-overlay label i {
+	margin-right: 15px;
+	font-size: 24px;
+}
+.customize-overlay .close {
+	position: absolute;
+	top: 15px;
+	right: 15px;
+}
+.customize-overlay .close:focus,
+.customize-overlay .close:hover {
+	background: var(--el-button-hover-color);
+}
+
+.widgets-list {
+}
+.widgets-list-item {
+	display: flex;
+	flex-direction: row;
+	padding: 15px;
+	align-items: center;
+}
+.widgets-list-item .item-logo {
+	width: 40px;
+	height: 40px;
+	border-radius: 50%;
+	background: rgba(180, 180, 180, 0.1);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 18px;
+	margin-right: 15px;
+	color: #6a8bad;
+}
+.widgets-list-item .item-info {
+	flex: 1;
+}
+.widgets-list-item .item-info h2 {
+	font-size: 16px;
+	font-weight: normal;
+	cursor: default;
+}
+.widgets-list-item .item-info p {
+	font-size: 12px;
+	color: #999;
+	cursor: default;
+}
+.widgets-list-item:hover {
+	background: rgba(180, 180, 180, 0.1);
+}
+
+.widgets-wrapper .sortable-ghost {
+	opacity: 0.5;
+}
+
+.selectLayout {
+	width: 100%;
+	display: flex;
+}
+.selectLayout-item {
+	width: 60px;
+	height: 60px;
+	border: 2px solid var(--el-border-color-light);
+	padding: 5px;
+	cursor: pointer;
+	margin-right: 15px;
+}
+.selectLayout-item span {
+	display: block;
+	background: var(--el-border-color-light);
+	height: 46px;
+}
+.selectLayout-item.item02 span {
+	height: 30px;
+}
+.selectLayout-item.item02 .el-col:nth-child(1) span {
+	height: 14px;
+	margin-bottom: 2px;
+}
+.selectLayout-item.item03 span {
+	height: 14px;
+	margin-bottom: 2px;
+}
+.selectLayout-item:hover {
+	border-color: var(--el-color-primary);
+}
+.selectLayout-item.active {
+	border-color: var(--el-color-primary);
+}
+.selectLayout-item.active span {
+	background: var(--el-color-primary);
+}
+
+.dark {
+	.widgets-aside {
+		background: #2b2b2b;
+	}
+	.customize-overlay {
+		background: rgba(43, 43, 43, 0.9);
+	}
+}
+
+@media (max-width: 992px) {
+	.customizing .widgets {
+		transform: scale(1) !important;
+	}
+	.customizing .widgets-aside {
+		width: 100%;
+		position: absolute;
+		top: 50%;
+		right: 0;
+		bottom: 0;
+	}
+	.customizing .widgets-wrapper {
+		margin-right: 0;
+	}
+}
+</style>

+ 224 - 0
Web/src/views/home/widgets/tool.js

@@ -0,0 +1,224 @@
+/*
+ * @Descripttion: 工具集
+ * @version: 1.2
+ * @LastEditors: sakuya
+ * @LastEditTime: 2022年5月24日00:28:56
+ */
+
+// import CryptoJS from 'crypto-js';
+
+const tool = {}
+
+/* localStorage */
+tool.data = {
+	set(key, data, datetime = 0) {
+        let cacheValue = {
+            content: data,
+            datetime: parseInt(datetime) === 0 ? 0 : new Date().getTime() + parseInt(datetime) * 1000
+        }
+        return localStorage.setItem(key, JSON.stringify(cacheValue))
+	},
+	get(key) {
+        try {
+            const value = JSON.parse(localStorage.getItem(key))
+            if (value) {
+                let nowTime = new Date().getTime()
+                if (nowTime > value.datetime && value.datetime != 0) {
+                    localStorage.removeItem(key)
+                    return null;
+                }
+                return value.content
+            }
+            return null
+        } catch (err) {
+            return null
+        }
+	},
+	remove(key) {
+		return localStorage.removeItem(key)
+	},
+	clear() {
+		return localStorage.clear()
+	}
+}
+
+/*sessionStorage*/
+tool.session = {
+	set(table, settings) {
+		var _set = JSON.stringify(settings)
+		return sessionStorage.setItem(table, _set);
+	},
+	get(table) {
+		var data = sessionStorage.getItem(table);
+		try {
+			data = JSON.parse(data)
+		} catch (err) {
+			return null
+		}
+		return data;
+	},
+	remove(table) {
+		return sessionStorage.removeItem(table);
+	},
+	clear() {
+		return sessionStorage.clear();
+	}
+}
+
+/*cookie*/
+tool.cookie = {
+	set(name, value, config={}) {
+		var cfg = {
+			expires: null,
+			path: null,
+			domain: null,
+			secure: false,
+			httpOnly: false,
+			...config
+		}
+		var cookieStr = `${name}=${escape(value)}`
+		if(cfg.expires){
+			var exp = new Date()
+			exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000)
+			cookieStr += `;expires=${exp.toGMTString()}`
+		}
+		if(cfg.path){
+			cookieStr += `;path=${cfg.path}`
+		}
+		if(cfg.domain){
+			cookieStr += `;domain=${cfg.domain}`
+		}
+		document.cookie = cookieStr
+	},
+	get(name){
+		var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"))
+		if(arr != null){
+			return unescape(arr[2])
+		}else{
+			return null
+		}
+	},
+	remove(name){
+		var exp = new Date()
+		exp.setTime(exp.getTime() - 1)
+		document.cookie = `${name}=;expires=${exp.toGMTString()}`
+	}
+}
+
+/* Fullscreen */
+tool.screen = function (element) {
+	var isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement);
+	if(isFull){
+		if(document.exitFullscreen) {
+			document.exitFullscreen();
+		}else if (document.msExitFullscreen) {
+			document.msExitFullscreen();
+		}else if (document.mozCancelFullScreen) {
+			document.mozCancelFullScreen();
+		}else if (document.webkitExitFullscreen) {
+			document.webkitExitFullscreen();
+		}
+	}else{
+		if(element.requestFullscreen) {
+			element.requestFullscreen();
+		}else if(element.msRequestFullscreen) {
+			element.msRequestFullscreen();
+		}else if(element.mozRequestFullScreen) {
+			element.mozRequestFullScreen();
+		}else if(element.webkitRequestFullscreen) {
+			element.webkitRequestFullscreen();
+		}
+	}
+}
+
+/* 复制对象 */
+tool.objCopy = function (obj) {
+	return JSON.parse(JSON.stringify(obj));
+}
+
+/* 日期格式化 */
+tool.dateFormat = function (date, fmt='yyyy-MM-dd hh:mm:ss') {
+	date = new Date(date)
+	var o = {
+		"M+" : date.getMonth()+1,                 //月份
+		"d+" : date.getDate(),                    //日
+		"h+" : date.getHours(),                   //小时
+		"m+" : date.getMinutes(),                 //分
+		"s+" : date.getSeconds(),                 //秒
+		"q+" : Math.floor((date.getMonth()+3)/3), //季度
+		"S"  : date.getMilliseconds()             //毫秒
+	};
+	if(/(y+)/.test(fmt)) {
+		fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));
+	}
+	for(var k in o) {
+		if(new RegExp("("+ k +")").test(fmt)){
+			fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
+		}
+	}
+	return fmt;
+}
+
+/* 千分符 */
+tool.groupSeparator = function (num) {
+	num = num + '';
+	if(!num.includes('.')){
+		num += '.'
+	}
+	return num.replace(/(\d)(?=(\d{3})+\.)/g, function ($0, $1) {
+		return $1 + ',';
+	}).replace(/\.$/, '');
+}
+
+// /* 常用加解密 */
+// tool.crypto = {
+// 	//MD5加密
+// 	MD5(data){
+// 		return CryptoJS.MD5(data).toString()
+// 	},
+// 	//BASE64加解密
+// 	BASE64: {
+// 		encrypt(data){
+// 			return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data))
+// 		},
+// 		decrypt(cipher){
+// 			return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8)
+// 		}
+// 	},
+// 	//AES加解密
+// 	AES: {
+// 		encrypt(data, secretKey, config={}){
+// 			if(secretKey.length % 8 != 0){
+// 				console.warn("[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。")
+// 			}
+// 			const result = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(secretKey), {
+// 				iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
+// 				mode: CryptoJS.mode[config.mode || "ECB"],
+// 				padding: CryptoJS.pad[config.padding || "Pkcs7"]
+// 			})
+// 			return result.toString()
+// 		},
+// 		decrypt(cipher, secretKey, config={}){
+// 			const result = CryptoJS.AES.decrypt(cipher, CryptoJS.enc.Utf8.parse(secretKey), {
+// 				iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
+// 				mode: CryptoJS.mode[config.mode || "ECB"],
+// 				padding: CryptoJS.pad[config.padding || "Pkcs7"]
+// 			})
+// 			return CryptoJS.enc.Utf8.stringify(result);
+// 		}
+// 	}
+// }
+
+// 查找树
+tool.treeFind = (tree, func) => {
+	for (const data of tree) {
+		if (func(data)) return data
+		if (data.children) {
+			const res = tool.treeFind(data.children, func)
+			if (res) return res
+		}
+	}
+	return null
+}
+
+export default tool