Commit 0587447b authored by wanli's avatar wanli

🐞 fix(资源监视模块):

资源监视模块增加更多信息展示、前端数据库缓存、后端接口超时优化
parent 5237c65e
''' '''
Author: your name Author: your name
Date: 2021-06-29 19:24:32 Date: 2021-06-29 19:24:32
LastEditTime: 2021-07-21 11:43:14 LastEditTime: 2021-07-23 22:26:56
LastEditors: Please set LastEditors LastEditors: Please set LastEditors
Description: In User Settings Edit Description: In User Settings Edit
FilePath: \evm-store\backend\controller\monitor.py FilePath: \evm-store\backend\controller\monitor.py
''' '''
import logging
from model.monitor import session, System, Lvgl, Evm, Image, Device, Request, User from model.monitor import session, System, Lvgl, Evm, Image, Device, Request, User
logger = logging.getLogger(__name__)
class SystemResource(object): class SystemResource(object):
def get(self): def get(self):
result = session.query(System).all() result = session.query(System).all()
...@@ -117,16 +121,8 @@ def insert_data(msg): ...@@ -117,16 +121,8 @@ def insert_data(msg):
if result: if result:
watch_id = result.id watch_id = result.id
else: else:
user = session.query(User).filter(User.account=="evm").first() logger.info("设备不存在")
if user: return None
result = Device(imei=msg.get("imei"), name="watch_{}".format(msg.get("imei")), type="watch", create_by=user.id, update_by=user.id)
session.add(result)
session.flush()
session.commit()
result = session.query(Device).filter_by(imei=msg.get("imei")).first()
if result:
watch_id = result.id
if msg.get("request"): if msg.get("request"):
msg.get("request").update({ "watch": watch_id }) msg.get("request").update({ "watch": watch_id })
......
''' '''
Author: your name Author: your name
Date: 2021-06-29 19:33:41 Date: 2021-06-29 19:33:41
LastEditTime: 2021-07-21 12:04:34 LastEditTime: 2021-07-24 01:18:54
LastEditors: Please set LastEditors LastEditors: Please set LastEditors
Description: In User Settings Edit Description: In User Settings Edit
FilePath: \evm-store\backend\view\monitor.py FilePath: \evm-store\backend\view\monitor.py
...@@ -216,7 +216,7 @@ class NotifyHandler(BaseWebsocket): ...@@ -216,7 +216,7 @@ class NotifyHandler(BaseWebsocket):
def on_heartbeat(self): def on_heartbeat(self):
# 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接 # 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接
for i in range(len(self._clients) - 1, -1, -1): for i in range(len(self._clients) - 1, -1, -1):
if int(time.time()) - self._clients[i].get("ts") > 5: if int(time.time()) - self._clients[i].get("ts") > 30:
# self._clients.pop(i) # self._clients.pop(i)
del self._clients[i] del self._clients[i]
className = self.__class__.__name__ className = self.__class__.__name__
...@@ -327,16 +327,16 @@ class DeviceMessageHandler(BaseHandler): ...@@ -327,16 +327,16 @@ class DeviceMessageHandler(BaseHandler):
logger.info(data) logger.info(data)
data.update({ 'request': { data.get("system", {}).update({
'host': self.request.remote_ip, 'host': self.request.remote_ip,
'path': self.request.path, 'path': self.request.path,
'protocol': self.request.protocol 'protocol': self.request.protocol
} }) })
insert_data(data) insert_data(data)
data['type'] = 'report' data['type'] = 'report'
data['request'].update({ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) data['system'].update({ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S") })
NotifyHandler.broadcastMessage(data) NotifyHandler.broadcastMessage(data)
self.write(json.dumps({ 'code': 100, 'message': 'success' })) self.write(json.dumps({ 'code': 100, 'message': 'success' }))
except Exception as e: except Exception as e:
......
/*
* @Author: your name
* @Date: 2021-07-15 09:33:39
* @LastEditTime: 2021-07-23 17:39:47
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \evm-store\frontend\babel.config.js
*/
module.exports = { module.exports = {
presets: [ presets: [
'@vue/cli-plugin-babel/preset' '@vue/cli-plugin-babel/preset',
] ]
} }
This diff is collapsed.
...@@ -31,6 +31,8 @@ import { wsNotify } from "@/utils/eventBus.js"; ...@@ -31,6 +31,8 @@ import { wsNotify } from "@/utils/eventBus.js";
// dataList.push(randomData()); // dataList.push(randomData());
// } // }
let chart = null
const seriesData = { const seriesData = {
heap_total_size: [], heap_total_size: [],
heap_used_size: [], heap_used_size: [],
...@@ -65,7 +67,6 @@ export default { ...@@ -65,7 +67,6 @@ export default {
data() { data() {
return { return {
loading: null, loading: null,
chart: null,
timer: null, timer: null,
series: [ series: [
{ {
...@@ -131,15 +132,19 @@ export default { ...@@ -131,15 +132,19 @@ export default {
}); });
wsNotify.eventBus.$on("resize", () => { wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize() if (chart) chart.resize()
});
wsNotify.eventBus.$on("clear-evm-chart", () => {
this.setOptions()
}); });
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!chart) {
return; return;
} }
this.chart.dispose(); chart.dispose();
this.chart = null; chart = null;
}, },
methods: { methods: {
cleanData() { cleanData() {
...@@ -151,7 +156,7 @@ export default { ...@@ -151,7 +156,7 @@ export default {
this.series.forEach((item) => { this.series.forEach((item) => {
item.data = []; item.data = [];
}); });
this.chart.setOption({ series: this.series }); chart.setOption({ series: this.series });
}, },
handleMessage(data) { handleMessage(data) {
if (!data || data.length == 0) this.cleanData() if (!data || data.length == 0) this.cleanData()
...@@ -169,18 +174,18 @@ export default { ...@@ -169,18 +174,18 @@ export default {
}); });
this.$nextTick(() => { this.$nextTick(() => {
this.chart && chart &&
this.chart.setOption({ chart.setOption({
series: this.series, series: this.series,
}); });
}); });
}, },
initChart() { initChart() {
this.chart = echarts.init(this.$el, "macarons"); chart = echarts.init(this.$el, "macarons");
this.setOptions(); this.setOptions();
}, },
setOptions() { setOptions() {
this.chart.setOption({ chart.setOption({
title: { title: {
text: "EVM", text: "EVM",
}, },
......
...@@ -9,6 +9,8 @@ import resize from "./mixins/resize"; ...@@ -9,6 +9,8 @@ import resize from "./mixins/resize";
import { getDateTimeString } from "@/utils/utils"; import { getDateTimeString } from "@/utils/utils";
import { wsNotify } from "@/utils/eventBus.js"; import { wsNotify } from "@/utils/eventBus.js";
let chart = null
const seriesData = { const seriesData = {
frag_pct: [], frag_pct: [],
free_biggest_size: [], free_biggest_size: [],
...@@ -50,7 +52,6 @@ export default { ...@@ -50,7 +52,6 @@ export default {
}, },
data() { data() {
return { return {
chart: null,
series: [ series: [
{ {
name: "frag_pct", name: "frag_pct",
...@@ -155,15 +156,19 @@ export default { ...@@ -155,15 +156,19 @@ export default {
}); });
wsNotify.eventBus.$on("resize", () => { wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize() if (chart) chart.resize()
});
wsNotify.eventBus.$on("clear-lvgl-chart", () => {
this.setOptions()
}); });
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!chart) {
return; return;
} }
this.chart.dispose(); chart.dispose();
this.chart = null; chart = null;
}, },
methods: { methods: {
handleData(data) { handleData(data) {
...@@ -173,7 +178,7 @@ export default { ...@@ -173,7 +178,7 @@ export default {
this.series.forEach(item => { this.series.forEach(item => {
item.data = [] item.data = []
}); });
this.chart.setOption({ series: this.series }); chart.setOption({ series: this.series });
data.forEach((item) => { data.forEach((item) => {
this.handleMessage(item); this.handleMessage(item);
...@@ -191,18 +196,18 @@ export default { ...@@ -191,18 +196,18 @@ export default {
}); });
this.$nextTick(() => { this.$nextTick(() => {
this.chart && chart &&
this.chart.setOption({ chart.setOption({
series: this.series, series: this.series,
}); });
}); });
}, },
initChart() { initChart() {
this.chart = echarts.init(this.$el, "macarons"); chart = echarts.init(this.$el, "macarons");
this.setOptions(); this.setOptions();
}, },
setOptions() { setOptions() {
this.chart.setOption({ chart.setOption({
title: { title: {
text: "LVGL", text: "LVGL",
}, },
......
...@@ -15,6 +15,8 @@ const seriesData = { ...@@ -15,6 +15,8 @@ const seriesData = {
used_space_size: [], used_space_size: [],
}; };
let chart = null
export default { export default {
mixins: [resize], mixins: [resize],
props: { props: {
...@@ -46,7 +48,6 @@ export default { ...@@ -46,7 +48,6 @@ export default {
}, },
data() { data() {
return { return {
chart: null,
series: [ series: [
{ {
name: "free_size", name: "free_size",
...@@ -80,11 +81,7 @@ export default { ...@@ -80,11 +81,7 @@ export default {
data: seriesData.used_space_size, data: seriesData.used_space_size,
}, },
], ],
legendData: [ legendData: Object.keys(seriesData),
"free_size",
"free_space_size",
"used_space_size"
],
}; };
}, },
watch: { watch: {
...@@ -107,15 +104,19 @@ export default { ...@@ -107,15 +104,19 @@ export default {
}); });
wsNotify.eventBus.$on("resize", () => { wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize() if (chart) chart.resize()
}); });
wsNotify.eventBus.$on("clear-system-chart", () => {
this.setOptions()
})
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!chart) {
return; return;
} }
this.chart.dispose(); chart.dispose();
this.chart = null; chart = null;
}, },
methods: { methods: {
handleData(data) { handleData(data) {
...@@ -125,7 +126,8 @@ export default { ...@@ -125,7 +126,8 @@ export default {
this.series.forEach(item => { this.series.forEach(item => {
item.data = [] item.data = []
}); });
this.chart.setOption({ series: this.series }); // chart.dispose();
chart.setOption({ series: this.series });
data.forEach((item) => { data.forEach((item) => {
this.handleMessage(item); this.handleMessage(item);
...@@ -135,26 +137,27 @@ export default { ...@@ -135,26 +137,27 @@ export default {
Object.keys(data).forEach((k) => { Object.keys(data).forEach((k) => {
var t = getDateTimeString(new Date()); var t = getDateTimeString(new Date());
if (k == "timestamp") t = data[k]; if (k == "timestamp") t = data[k];
if (this.legendData.includes(k)) if (this.legendData.includes(k)) {
seriesData[k].push({ seriesData[k].push({
name: k, name: k,
value: [t, data[k]], value: [t, data[k]],
}); });
}
}); });
this.$nextTick(() => { this.$nextTick(() => {
this.chart && chart &&
this.chart.setOption({ chart.setOption({
series: this.series, series: this.series,
}); });
}); });
}, },
initChart() { initChart() {
this.chart = echarts.init(this.$el, "macarons"); chart = echarts.init(this.$el, "macarons");
this.setOptions(); this.setOptions();
}, },
setOptions() { setOptions() {
this.chart.setOption({ chart.setOption({
title: { title: {
text: "SYSTEM", text: "SYSTEM",
}, },
......
...@@ -236,7 +236,7 @@ ...@@ -236,7 +236,7 @@
<grid-item <grid-item
:x="0" :x="0"
:y="10" :y="10"
:w="12" :w="8"
:h="10" :h="10"
i="5" i="5"
@resize="resizeEvent" @resize="resizeEvent"
...@@ -245,7 +245,7 @@ ...@@ -245,7 +245,7 @@
@container-resized="containerResizedEvent" @container-resized="containerResizedEvent"
@moved="movedEvent" @moved="movedEvent"
> >
<div style="width: 100%; height: 100%; overflow-y: auto"> <div style="width: 100%; height: 100%;overflow-y: auto;">
<p class="item-title">APP</p> <p class="item-title">APP</p>
<el-table <el-table
element-loading-text="Loading" element-loading-text="Loading"
...@@ -297,19 +297,43 @@ ...@@ -297,19 +297,43 @@
</el-table> </el-table>
</div> </div>
</grid-item> </grid-item>
<grid-item
:x="8"
:y="10"
:w="4"
:h="10"
i="6"
@resize="resizeEvent"
@move="moveEvent"
@resized="resizedEvent"
@container-resized="containerResizedEvent"
@moved="movedEvent"
>
<div style="width: 100%; height: 100%;overflow-y: auto;">
<el-table :data="pngList" style="width: 100%">
<el-table-column prop="uri" label="uri" min-width="180"></el-table-column>
<el-table-column prop="filesize" label="file size" width="100"></el-table-column>
<el-table-column prop="uncompressed_size" label="origin size" width="100"></el-table-column>
<el-table-column prop="ratio" label="ratio" width="100"> </el-table-column>
</el-table>
</div>
</grid-item>
<grid-item <grid-item
:x="0" :x="0"
:y="20" :y="20"
:w="12" :w="12"
:h="7" :h="7"
i="6" i="7"
@resize="resizeEvent" @resize="resizeEvent"
@move="moveEvent" @move="moveEvent"
@resized="resizedEvent" @resized="resizedEvent"
@container-resized="containerResizedEvent" @container-resized="containerResizedEvent"
@moved="movedEvent" @moved="movedEvent"
> >
<SystemChart :chartData="system"></SystemChart> <SystemChart
:chartData="system"
:dataList="systemHistory"
></SystemChart>
</grid-item> </grid-item>
<grid-item <grid-item
:x="0" :x="0"
...@@ -323,7 +347,7 @@ ...@@ -323,7 +347,7 @@
@container-resized="containerResizedEvent" @container-resized="containerResizedEvent"
@moved="movedEvent" @moved="movedEvent"
> >
<EvmChart :chartData="evm"></EvmChart> <EvmChart :chartData="evm" :dataList="evmHistory"></EvmChart>
</grid-item> </grid-item>
<grid-item <grid-item
:x="0" :x="0"
...@@ -337,7 +361,7 @@ ...@@ -337,7 +361,7 @@
@container-resized="containerResizedEvent" @container-resized="containerResizedEvent"
@moved="movedEvent" @moved="movedEvent"
> >
<LvglChart :chartData="lvgl"></LvglChart> <LvglChart :chartData="lvgl" :dataList="lvglHistory"></LvglChart>
</grid-item> </grid-item>
</grid-layout> </grid-layout>
</div> </div>
...@@ -350,17 +374,43 @@ import LvglChart from "./components/LvglChart"; ...@@ -350,17 +374,43 @@ import LvglChart from "./components/LvglChart";
import SystemChart from "./components/SystemChart"; import SystemChart from "./components/SystemChart";
import { GridLayout, GridItem } from "vue-grid-layout"; import { GridLayout, GridItem } from "vue-grid-layout";
import { wsNotify } from "@/utils/eventBus.js"; import { wsNotify } from "@/utils/eventBus.js";
import Database from "@/utils/indexedDB";
const dbObject = {
dbName: "evmiot", // 数据库名
version: 1, // 版本号
primaryKey: "id", // 主键
keyNames: [
{
// 需要存储的数据字段对象
key: "system.timestamp", // 字段名
unique: false, // 当前这条数据是否能重复 (最常用) 默认false
},
{
key: "imei",
unique: false,
},
],
};
const indexedDb = Database();
let monitor = new indexedDb(dbObject);
export default { export default {
name: "Monitor", name: "Monitor",
data() { data() {
return { return {
watchs: [], watchs: [],
pngList: [],
globalData: null, globalData: null,
device: null, device: null,
devices: {}, devices: {},
deviceList: null, deviceList: null,
system: {}, system: {},
systemList: [], systemList: [],
systemHistory: [],
evmHistory: [],
lvglHistory: [],
evm: {}, evm: {},
evmList: [], evmList: [],
lvgl: {}, lvgl: {},
...@@ -387,10 +437,11 @@ export default { ...@@ -387,10 +437,11 @@ export default {
{ x: 6, y: 0, w: 6, h: 5, i: "2", static: true }, { x: 6, y: 0, w: 6, h: 5, i: "2", static: true },
{ x: 0, y: 5, w: 6, h: 5, i: "3", static: false }, { x: 0, y: 5, w: 6, h: 5, i: "3", static: false },
{ x: 6, y: 5, w: 6, h: 5, i: "4", static: false }, { x: 6, y: 5, w: 6, h: 5, i: "4", static: false },
{ x: 0, y: 10, w: 12, h: 10, i: "5", static: false }, { x: 0, y: 10, w: 8, h: 10, i: "5", static: false },
{ x: 0, y: 20, w: 12, h: 7, i: "6", static: false }, { x: 8, y: 10, w: 4, h: 10, i: "6", static: false },
{ x: 0, y: 27, w: 12, h: 7, i: "7", static: false }, { x: 0, y: 20, w: 12, h: 7, i: "7", static: false },
{ x: 0, y: 34, w: 12, h: 7, i: "8", static: false }, { x: 0, y: 27, w: 12, h: 7, i: "8", static: false },
{ x: 0, y: 34, w: 12, h: 7, i: "9", static: false },
], ],
draggable: true, draggable: true,
resizable: true, resizable: true,
...@@ -408,54 +459,19 @@ export default { ...@@ -408,54 +459,19 @@ export default {
return row.highlight ? "success-row" : ""; return row.highlight ? "success-row" : "";
}, },
moveEvent(i, newX, newY) { moveEvent(i, newX, newY) {
const msg = "MOVE i=" + i + ", X=" + newX + ", Y=" + newY; console.log(i, newX, newY);
console.log(msg);
}, },
movedEvent(i, newX, newY) { movedEvent(i, newX, newY) {
const msg = "MOVED i=" + i + ", X=" + newX + ", Y=" + newY; console.log(i, newX, newY);
console.log(msg);
}, },
resizeEvent(i, newH, newW, newHPx, newWPx) { resizeEvent(i, newH, newW, newHPx, newWPx) {
const msg = console.log(i, newH, newW, newHPx, newWPx);
"RESIZE i=" +
i +
", H=" +
newH +
", W=" +
newW +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
}, },
resizedEvent(i, newX, newY, newHPx, newWPx) { resizedEvent(i, newX, newY, newHPx, newWPx) {
const msg = console.log(i, newX, newY, newHPx, newWPx);
"RESIZED i=" +
i +
", X=" +
newX +
", Y=" +
newY +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
}, },
containerResizedEvent(i, newH, newW, newHPx, newWPx) { containerResizedEvent(i, newH, newW, newHPx, newWPx) {
const msg = console.log(i, newH, newW, newHPx, newWPx);
"CONTAINER RESIZED i=" +
i +
", H=" +
newH +
", W=" +
newW +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
}, },
layoutCreatedEvent(newLayout) { layoutCreatedEvent(newLayout) {
console.log("Created layout: ", newLayout); console.log("Created layout: ", newLayout);
...@@ -527,9 +543,10 @@ export default { ...@@ -527,9 +543,10 @@ export default {
}, },
handleMessage(msg) { handleMessage(msg) {
if (msg.type !== "report" || !msg.imei) return null; if (msg.type !== "report" || !msg.imei) return null;
// 如果接收到的数据不是当前选中的设备,那么则直接丢弃
// 将设备发送过来的消息存储到浏览器中
// 这里可以优化,将所有数据,保存到indexed datebase中 // 这里可以优化,将所有数据,保存到indexed datebase中
if (this.device && msg.imei != this.device) return null; if (monitor.db) monitor.set(msg);
if (!this.deviceList) { if (!this.deviceList) {
this.deviceList = []; this.deviceList = [];
...@@ -543,9 +560,13 @@ export default { ...@@ -543,9 +560,13 @@ export default {
else this.device = msg.imei; else this.device = msg.imei;
} }
// 如果接收到的数据不是当前选中的设备,那么则直接丢弃
if (msg.imei != this.device) {
return null;
}
// 处理单位 // 处理单位
this.processData(msg); this.processData(msg);
// this.devices[msg.imei] = msg;
this.globalData = msg; this.globalData = msg;
this.resetData(); this.resetData();
}, },
...@@ -569,9 +590,35 @@ export default { ...@@ -569,9 +590,35 @@ export default {
}, },
onSelectChange(res) { onSelectChange(res) {
this.device = res; this.device = res;
monitor
.getAllData((params) => {
return params.imei && params.imei == res;
})
.then((res) => {
console.log(res);
const systemList = [],
evmList = [],
lvglList = [];
res.forEach((item) => {
systemList.push(item.system);
evmList.push(item.evm);
lvglList.push(item.lvgl);
});
wsNotify.eventBus.$emit("clear-system-chart");
wsNotify.eventBus.$emit("clear-evm-chart");
wsNotify.eventBus.$emit("clear-lvgl-chart");
// this.systemHistory = systemList;
// this.evmHistory = evmList;
// this.lvglHistory = lvglList;
// 清空图标数据
})
.catch((err) => {
console.log(err);
});
// this.processData(this.devices[this.device]); // this.processData(this.devices[this.device]);
// this.resetData(); // this.resetData();
console.log(res);
}, },
resetData() { resetData() {
wsNotify.eventBus.$emit("resize"); wsNotify.eventBus.$emit("resize");
...@@ -581,11 +628,12 @@ export default { ...@@ -581,11 +628,12 @@ export default {
this.systemList = [ this.systemList = [
{ {
imei: this.globalData.imei, imei: this.globalData.imei,
...this.globalData.system, ...this.globalData.system
...this.globalData.request,
}, },
]; ];
console.log(this.globalData)
// 这里需要特殊处理下,先判断uri是否存在,不存在则添加,存在则更新 // 这里需要特殊处理下,先判断uri是否存在,不存在则添加,存在则更新
let uris = []; let uris = [];
this.imageList.forEach((item) => { this.imageList.forEach((item) => {
...@@ -600,6 +648,8 @@ export default { ...@@ -600,6 +648,8 @@ export default {
item.highlight = false; item.highlight = false;
} }
if (item.png_detail && item.png_detail.length) this.pngList = item.png_detail
const target = this.imageList.find((img) => img.uri === item.uri); const target = this.imageList.find((img) => img.uri === item.uri);
if (target) { if (target) {
target.length = item.length; target.length = item.length;
...@@ -608,8 +658,7 @@ export default { ...@@ -608,8 +658,7 @@ export default {
if ( if (
item.png_uncompressed_size && item.png_uncompressed_size &&
item.png_uncompressed_size !== target.png_uncompressed_size item.png_uncompressed_size !== target.png_uncompressed_size
) ) {
{
target.highlight = true; target.highlight = true;
target.png_uncompressed_size = item.png_uncompressed_size; target.png_uncompressed_size = item.png_uncompressed_size;
} }
...@@ -618,7 +667,6 @@ export default { ...@@ -618,7 +667,6 @@ export default {
item.png_file_size !== target.png_file_size item.png_file_size !== target.png_file_size
) )
target.png_file_size = item.png_file_size; target.png_file_size = item.png_file_size;
} else { } else {
this.imageList.push(item); this.imageList.push(item);
} }
...@@ -633,7 +681,29 @@ export default { ...@@ -633,7 +681,29 @@ export default {
}, },
}, },
mounted() {}, mounted() {},
destroyed() {
// 页面关闭则销毁该数据库
// monitor.deleteDB()
},
created() { created() {
monitor
.init()
.then((res) => {
console.log(res);
if (monitor.db) {
monitor.set({
system: {
free_size: 0,
free_space_size: 0,
used_space_size: 0,
},
});
}
})
.catch((err) => {
console.log(err);
});
this.socket = wsNotify; this.socket = wsNotify;
wsNotify.eventBus.$on("open", (message) => { wsNotify.eventBus.$on("open", (message) => {
this.sendMsg(); this.sendMsg();
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
} }
.el-table .success-row { .el-table .success-row {
background: greenyellow; background: #87e8de;
} }
''' '''
Author: your name Author: your name
Date: 2021-07-22 19:01:41 Date: 2021-07-22 19:01:41
LastEditTime: 2021-07-22 21:46:18 LastEditTime: 2021-07-24 00:27:21
LastEditors: Please set LastEditors LastEditors: Please set LastEditors
Description: In User Settings Edit Description: In User Settings Edit
FilePath: \evm-store\tools\build_out\tests\http_interval.py FilePath: \evm-store\tools\build_out\tests\http_interval.py
''' '''
import json import json
import time
import random import random
import requests import requests
from threading import Timer from threading import Timer, Thread
def send_request():
def send_request(imei):
payload = { payload = {
"system":{"free_size":1769792,"free_space_size":5156864,"used_space_size":1134592}, "system": {"free_size": 1769792, "free_space_size": 5156864, "used_space_size": 1134592},
"lvgl":{"total_size":0,"free_cnt":0,"free_size":0,"free_biggest_size":0,"used_cnt":0,"used_pct":0,"frag_pct":0}, "lvgl": {"total_size": 0, "free_cnt": 0, "free_size": 0, "free_biggest_size": 0, "used_cnt": 0, "used_pct": 0, "frag_pct": 0},
"evm":{"heap_total_size":2097152,"heap_used_size":575072,"heap_map_size":8192,"stack_total_size":102400,"stack_used_size":1312}, "evm": {"heap_total_size": 2097152, "heap_used_size": 575072, "heap_map_size": 8192, "stack_total_size": 102400, "stack_used_size": 1312},
"image":[ "image": [
{"uri":"evue_launcher","length":13515,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0}, {"uri": "evue_launcher", "length": 13515, "png_total_count": 0,
{"uri":"kdgs_1_startup","length":3666,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0}, "png_uncompressed_size": 0, "png_file_size": 0},
{"uri":"kdgs_1_index","length":5482,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0}, {"uri": "kdgs_1_startup", "length": 3666, "png_total_count": 0,
{"uri":"kdgs_1_story","length":5509,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0}, "png_uncompressed_size": 0, "png_file_size": 0},
{"uri":"kdgs_1_storyList","length":9196,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0}, {"uri": "kdgs_1_index", "length": 5482, "png_total_count": 0,
{"uri":"kdgs_1_storyPlay","length":25791,"png_total_count":6,"png_uncompressed_size":319376,"png_file_size":10770} "png_uncompressed_size": 0, "png_file_size": 0},
{"uri": "kdgs_1_story", "length": 5509, "png_total_count": 0,
"png_uncompressed_size": 0, "png_file_size": 0},
{"uri": "kdgs_1_storyList", "length": 9196, "png_total_count": 0,
"png_uncompressed_size": 0, "png_file_size": 0},
{"uri": "kdgs_1_storyPlay", "length": 25791, "png_total_count": 6, "png_uncompressed_size": 319376, "png_file_size": 10770, "png_detail": [
{
"uri": "C:/../../test/watch_appstore/kdgs_1_playBackground.png",
"filesize": 7774,
"uncompressed_size": 259200,
"ratio": 33.341908
},
{
"uri": "C:/../../test/watch_appstore/kdgs_1_playLb.png",
"filesize": 482,
"uncompressed_size": 12544,
"ratio": 26.024897
},
{
"uri": "C:/../../test/watch_appstore/kdgs_1_playNLike.png",
"filesize": 1094,
"uncompressed_size": 12544,
"ratio": 11.466179
},
{
"uri": "C:/../../test/watch_appstore/kdgs_1_playYl.png",
"filesize": 745,
"uncompressed_size": 12544,
"ratio": 16.837584
},
{
"uri": "C:/../../test/watch_appstore/kdgs_1_playNext.png",
"filesize": 484,
"uncompressed_size": 12544,
"ratio": 25.917355
},
{
"uri": "C:/../../test/watch_appstore/kdgs_1_play_bs.png",
"filesize": 191,
"uncompressed_size": 10000,
"ratio": 52.356022
}
]}
], ],
"imei":"352099001761481","datetime":{"second":55,"minute":48,"hour":15,"day":21,"month":7,"year":2021,"weekday":3} "imei": imei, "datetime": {"second": 55, "minute": 48, "hour": 15, "day": 21, "month": 7, "year": 2021, "weekday": 3}
} }
while True:
for item in payload.get("image"): for item in payload.get("image"):
item.update({ item.update({
'length': 0, 'length': 0,
...@@ -49,13 +94,40 @@ def send_request(): ...@@ -49,13 +94,40 @@ def send_request():
'png_file_size': random.randint(0, 10000) 'png_file_size': random.randint(0, 10000)
}) })
r = requests.post("http://localhost:3000/api/v1/evm_store/monitor", data=json.dumps(payload)) r = requests.post(
"http://localhost:3000/api/v1/evm_store/monitor", data=json.dumps(payload))
print(r.status_code) print(r.status_code)
print(r.json()) print(r.json())
t = Timer(3, send_request) time.sleep(3)
t.run()
class myThread(Thread):
def __init__(self, threadID, name, counter, imei):
Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
self.imei = imei
def run(self):
print("开始线程:" + self.name)
print(self.counter, self.imei)
send_request(self.imei)
print("退出线程:" + self.name)
if __name__ == "__main__": if __name__ == "__main__":
send_request() # send_request()
# 创建新线程
thread1 = myThread(1, "Thread-1", 1, "352099001761481")
thread2 = myThread(2, "Thread-2", 2, "866866040000447")
# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("退出主线程")
''' '''
Author: your name Author: your name
Date: 2021-06-29 19:33:41 Date: 2021-06-29 19:33:41
LastEditTime: 2021-07-22 19:30:38 LastEditTime: 2021-07-24 01:15:03
LastEditors: Please set LastEditors LastEditors: Please set LastEditors
Description: In User Settings Edit Description: In User Settings Edit
FilePath: \evm-store\backend\view\monitor.py FilePath: \evm-store\backend\view\monitor.py
...@@ -154,6 +154,7 @@ class NotifyHandler(BaseWebsocket): ...@@ -154,6 +154,7 @@ class NotifyHandler(BaseWebsocket):
try: try:
className = self.__class__.__name__ className = self.__class__.__name__
message = json.loads(message) message = json.loads(message)
logger.info(message)
# 判断消息类型 # 判断消息类型
if message.get("type") and message.get("token"): if message.get("type") and message.get("token"):
# 获取token值,检验正确与否,获取uuid # 获取token值,检验正确与否,获取uuid
...@@ -189,6 +190,7 @@ class NotifyHandler(BaseWebsocket): ...@@ -189,6 +190,7 @@ class NotifyHandler(BaseWebsocket):
# self.close() # self.close()
elif message.get("type") == "heartbeat": # 心跳包 elif message.get("type") == "heartbeat": # 心跳包
# 收到心跳包消息,更新接收数据时间 # 收到心跳包消息,更新接收数据时间
logger.info("////////////////////////")
for c in self._clients: for c in self._clients:
if c.get("uuid") == payload.get("sub").get("uuid"): if c.get("uuid") == payload.get("sub").get("uuid"):
c["ts"] = int(time.time()) c["ts"] = int(time.time())
...@@ -209,7 +211,8 @@ class NotifyHandler(BaseWebsocket): ...@@ -209,7 +211,8 @@ class NotifyHandler(BaseWebsocket):
def on_heartbeat(self): def on_heartbeat(self):
# 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接 # 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接
for i in range(len(self._clients) - 1, -1, -1): for i in range(len(self._clients) - 1, -1, -1):
if int(time.time()) - self._clients[i].get("ts") > 5: if int(time.time()) - self._clients[i].get("ts") > 30:
logger.info("################################################")
# self._clients.pop(i) # self._clients.pop(i)
del self._clients[i] del self._clients[i]
className = self.__class__.__name__ className = self.__class__.__name__
......
module.exports = { module.exports = {
"presets": [ [ "@vue/app", { useBuiltIns: "entry" } ] ], presets: [["@vue/app", { useBuiltIns: "entry" }]],
"plugins": [ plugins: [
["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }] [
] "import",
} { libraryName: "ant-design-vue", libraryDirectory: "es", style: true },
],
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
],
};
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"core-js": "^3.9.0", "core-js": "^3.9.0",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"echarts": "^5.1.2", "echarts": "^5.1.2",
"element-ui": "^2.15.3",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"npm-check-updates": "^11.7.1", "npm-check-updates": "^11.7.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
"vue-grid-layout": "^2.3.12", "vue-grid-layout": "^2.3.12",
"vue-i18n": "^8.1.0", "vue-i18n": "^8.1.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-treeselect": "^1.0.7",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0"
}, },
...@@ -34,6 +36,7 @@ ...@@ -34,6 +36,7 @@
"@vue/cli-plugin-eslint": "^3.3.0", "@vue/cli-plugin-eslint": "^3.3.0",
"@vue/cli-service": "^3.3.0", "@vue/cli-service": "^3.3.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-import": "^1.9.1", "babel-plugin-import": "^1.9.1",
"eslint": "^7.30.0", "eslint": "^7.30.0",
"eslint-plugin-vue": "^7.13.0", "eslint-plugin-vue": "^7.13.0",
......
import request from '@/utils/request'
// 查询
export function fetchTree (data) {
return request.post('/system/menu/treeList', data)
}
// 新建
export function create (data) {
return request.post('/system/menu/create', data)
}
// 修改
export function updateById (data) {
return request.post('/system/menu/updateById', data)
}
// 修改状态
export function updateStatus (data) {
return request.post('/system/menu/updateStatus', data)
}
// 删除
export function deleteById (id) {
return request.get(`/system/menu/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/menu/delete/batch', {
params: {
ids
}
})
}
// 查询菜单树
export function fetchMenuTree () {
return request.get('/system/menu/treeNodes')
}
// 排序
export function sort (data) {
return request.post('/system/menu/updateSort', data)
}
...@@ -128,27 +128,27 @@ const router = new Router({ ...@@ -128,27 +128,27 @@ const router = new Router({
{ {
path: "/system/setting/menu", path: "/system/setting/menu",
name: "menu", name: "menu",
component: () => import("@/views/System/Menu"), component: () => import("@/views/System/menu"),
}, },
{ {
path: "/system/setting/module", path: "/system/setting/perssion",
name: "module", name: "module",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/perssion"),
}, },
{ {
path: "/system/setting/config", path: "/system/setting/data-permission",
name: "config", name: "config",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/role"),
}, },
{ {
path: "/system/setting/dict", path: "/system/setting/dict",
name: "dict", name: "dict",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/dict"),
}, },
{ {
path: "/system/setting/area", path: "/system/setting/location",
name: "area", name: "area",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/location"),
}, },
{ {
path: "/system/setting/file-manager", path: "/system/setting/file-manager",
...@@ -158,14 +158,14 @@ const router = new Router({ ...@@ -158,14 +158,14 @@ const router = new Router({
], ],
}, },
{ {
path: "/system/role", path: "/system/trace-log",
name: "role", name: "trace",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/traceLog"),
}, },
{ {
path: "/system/admin", path: "/system/user",
name: "admin", name: "user",
component: () => import("@/views/System/Role"), component: () => import("@/views/System/user"),
}, },
], ],
}, },
...@@ -236,6 +236,13 @@ const router = new Router({ ...@@ -236,6 +236,13 @@ const router = new Router({
}, },
], ],
}, },
{
path: "/table",
component: BasicLayout,
children: [
{ path: "/table", component: () => import("@/views/System/table") },
],
},
], ],
}); });
......
// 主色调
$primary-color: #2E68EC;
// 头部高度
$header-height: 60px;
// 菜单宽度
$menu-width: 208px;
// 页面最小宽度
$page-min-width: 1000px;
// 字体
$font-color: #282828; // 颜色
$font-size: 12px; // 大小
This diff is collapsed.
<template>
<a-page-header-wrapper
:loading="false"
:tabList="tabList"
tabActiveKey="articles"
:tabChange="tabChange"
>
menu
</a-page-header-wrapper>
</template>
<script>
import { Avatar, Row, Col, Card, List } from "ant-design-vue";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
export default {
data: () => ({
activitiesLoading: true,
projectLoading: false,
tabList: [
{
key: "articles",
tab: "菜单列表",
},
{
key: "application",
tab: "应用列表",
},
],
}),
components: {
APageHeaderWrapper: PageHeaderWrapper,
AAvatar: Avatar,
ARow: Row,
ACol: Col,
ACard: Card,
ACardGrid: Card.Grid,
ACardMeta: Card.Meta,
AList: List,
},
methods: {
tabChange(e) {
window.console.log(e);
},
},
};
</script>
<style lang="less">
</style>
\ No newline at end of file
...@@ -629,6 +629,10 @@ export default { ...@@ -629,6 +629,10 @@ export default {
flex-direction: row; flex-direction: row;
& > .grid-node { & > .grid-node {
flex: 1; flex: 1;
& > h3 {
font-weight: bold;
font-size: 17px;
}
& > h3, p { & > h3, p {
display: flex; display: flex;
justify-content: center; justify-content: center;
......
<template> <template>
<a-page-header-wrapper <TableLayout class="menu-layout" :permissions="['system:menu:query']">
:loading="false" <!-- 表格和分页 -->
:tabList="tabList" <template v-slot:table-wrap>
tabActiveKey="articles" <ul class="toolbar" v-permissions="['system:menu:create', 'system:menu:delete', 'system:menu:sort']">
:tabChange="tabChange" <li><el-button type="primary" @click="$refs.operaMenuWindow.open('新建一级菜单')" icon="el-icon-plus" v-permissions="['system:menu:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:menu:delete']">删除</el-button></li>
<li><el-button @click="sort('top')" :loading="isWorking.sort" icon="el-icon-sort-up" v-permissions="['system:menu:sort']">上移</el-button></li>
<li><el-button @click="sort('bottom')" :loading="isWorking.sort" icon="el-icon-sort-down" v-permissions="['system:menu:sort']">下移</el-button></li>
</ul>
<el-table
ref="table"
v-loading="isWorking.search"
:data="tableData.list"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
row-key="id"
stripe
default-expand-all
@selection-change="handleSelectionChange"
> >
menu <el-table-column type="selection" width="55" fixed="left"></el-table-column>
</a-page-header-wrapper> <el-table-column prop="name" label="菜单名称" fixed="left" min-width="160px"></el-table-column>
<el-table-column prop="icon" label="图标" min-width="80px" class-name="table-column-icon">
<template slot-scope="{row}">
<i v-if="row.icon != null && row.icon !== ''" :class="{[row.icon]: true}"></i>
<template v-else>未设置</template>
</template>
</el-table-column>
<el-table-column prop="path" label="访问路径" min-width="140px"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="120px"></el-table-column>
<el-table-column prop="createUser" label="创建人" min-width="100px">
<template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="140px"></el-table-column>
<el-table-column prop="updateUser" label="更新人" min-width="100px">
<template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
<el-table-column prop="disabled" label="是否启用" min-width="80px">
<template slot-scope="{row}">
<el-switch v-model="row.disabled" :active-value="false" :inactive-value="true" @change="switchDisabled(row)"/>
</template>
</el-table-column>
<el-table-column
v-if="containPermissions(['system:menu:update', 'system:menu:create', 'system:menu:delete'])"
label="操作"
min-width="220"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" icon="el-icon-edit" @click="$refs.operaMenuWindow.open('编辑菜单', row)" v-permissions="['system:menu:update']">编辑</el-button>
<el-button type="text" icon="el-icon-plus" @click="$refs.operaMenuWindow.open('新建子菜单', null, row)" v-permissions="['system:menu:create']">新建子菜单</el-button>
<el-button v-if="!row.fixed" type="text" icon="el-icon-delete" @click="deleteById(row)" v-permissions="['system:menu:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- 新建/修改 -->
<OperaMenuWindow ref="operaMenuWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
</TableLayout>
</template> </template>
<script> <script>
import { Avatar, Row, Col, Card, List } from "ant-design-vue"; import { Table, TableColumn, Button } from "element-ui"
import PageHeaderWrapper from "@/components/PageHeaderWrapper"; import 'element-ui/lib/theme-chalk/index.css'
import TableLayout from './components/TableLayout'
import BaseTable from './components/base/BaseTable'
import OperaMenuWindow from './components/system/menu/OperaMenuWindow'
import { fetchTree, updateStatus, sort } from '@/api/system/menu'
export default { export default {
data: () => ({ name: 'SystemMenu',
activitiesLoading: true, extends: BaseTable,
projectLoading: false,
tabList: [
{
key: "articles",
tab: "菜单列表",
},
{
key: "application",
tab: "应用列表",
},
],
}),
components: { components: {
APageHeaderWrapper: PageHeaderWrapper, "el-table": Table,
AAvatar: Avatar, "el-table-column": TableColumn,
ARow: Row, "el-button": Button,
ACol: Col, OperaMenuWindow,
ACard: Card, TableLayout
ACardGrid: Card.Grid, },
ACardMeta: Card.Meta, data () {
AList: List, return {
// 是否正在处理中
isWorking: {
sort: false
}
}
}, },
methods: { methods: {
tabChange(e) { // 查询数据
window.console.log(e); handlePageChange () {
this.isWorking.search = true
fetchTree()
.then(records => {
this.tableData.list = records
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
}, },
// 排序
sort (direction) {
if (this.isWorking.sort) {
return
}
if (this.tableData.selectedRows.length === 0) {
this.$tip.warning('请选择一条数据')
return
}
if (this.tableData.selectedRows.length > 1) {
this.$tip.warning('排序时仅允许选择一条数据')
return
}
const menuId = this.tableData.selectedRows[0].id
// 找到菜单范围
let menuPool
for (const rootMenu of this.tableData.list) {
const parent = this.__findParent(menuId, rootMenu)
if (parent != null) {
menuPool = parent.children
}
}
menuPool = menuPool || this.tableData.list
const menuIndex = menuPool.findIndex(menu => menu.id === menuId)
// 上移校验
if (direction === 'top' && menuIndex === 0) {
this.$tip.warning('菜单已到顶部')
return
}
// 下移校验
if (direction === 'bottom' && menuIndex === menuPool.length - 1) {
this.$tip.warning('菜单已到底部')
return
}
this.isWorking.sort = true
sort({
id: this.tableData.selectedRows[0].id,
direction
})
.then(() => {
if (direction === 'top') {
menuPool.splice(menuIndex, 0, menuPool.splice(menuIndex - 1, 1)[0])
} else {
menuPool.splice(menuIndex, 0, menuPool.splice(menuIndex + 1, 1)[0])
}
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.sort = false
})
}, },
}; // 启用/禁用菜单
switchDisabled (row) {
if (!row.disabled) {
this.__updateMenuStatus(row)
return
}
this.$dialog.disableConfirm(`确认禁用 ${row.name} 菜单吗?`)
.then(() => {
this.__updateMenuStatus(row)
}).catch(() => {
row.disabled = !row.disabled
})
},
// 查询父节点
__findParent (id, parent) {
if (parent.children === 0) {
return
}
for (const menu of parent.children) {
if (menu.id === id) {
return parent
}
if (menu.children.length > 0) {
const m = this.__findParent(id, menu)
if (m != null) {
return m
}
}
}
return null
},
// 修改菜单状态
__updateMenuStatus (row) {
updateStatus({
id: row.id,
parentId: row.parentId,
disabled: row.disabled
})
.then(() => {
this.$tip.apiSuccess('修改成功')
})
.catch(e => {
row.disabled = !row.disabled
this.$tip.apiFailed(e)
})
}
},
created () {
this.config({
module: '菜单',
api: '/system/menu'
})
this.search()
}
}
</script> </script>
<style lang="less">
<style lang="scss" scoped>
@import "@/styles/variables.scss";
.menu-layout {
/deep/ .table-content {
margin-top: 0;
}
}
// 图标列
.table-column-icon {
// element-ui图标
i {
background-color: $primary-color;
opacity: 0.72;
font-size: 20px;
color: #fff;
padding: 4px;
border-radius: 50%;
}
// 自定义图标
[class^="eva-icon-"] {
width: 20px;
height: 20px;
background-size: 16px;
vertical-align: middle;
}
}
</style> </style>
This diff is collapsed.
<template>
<div class="table-layout">
<!-- 头部 -->
<div v-if="withBreadcrumb" class="table-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="path in paths" :key="path">{{path}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<Profile :roles="roles" :permissions="permissions">
<!-- 搜索表单部分 -->
<div class="table-search-form">
<div class="form-wrap">
<slot name="search-form"></slot>
</div>
</div>
<slot name="space"></slot>
<!-- 列表和分页部分 -->
<div class="table-content">
<div class="table-wrap">
<slot name="table-wrap"></slot>
</div>
</div>
<slot></slot>
</Profile>
</div>
</template>
<script>
import { Breadcrumb, BreadcrumbItem } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
import Profile from './common/Profile'
export default {
name: 'TableLayout',
components: {
"el-breadcrumb": Breadcrumb,
"el-breadcrumb-item": BreadcrumbItem,
Profile
},
props: {
// 角色
roles: {
type: Array
},
// 权限
permissions: {
type: Array
},
// 是否展示头部面包屑
withBreadcrumb: {
type: Boolean,
default: true
}
},
computed: {
paths () {
return this.$route.meta.paths
}
}
}
</script>
<style lang="scss">
@import "@/assets/style/variables.scss";
.table-layout {
height: 100%;
display: flex;
flex-direction: column;
.not-allow-wrap {
padding-top: 0;
}
}
// 头部
.table-header {
overflow: hidden;
padding: 12px 16px;
flex-shrink: 0;
// 页面路径
.el-breadcrumb {
.el-breadcrumb__item {
.el-breadcrumb__inner {
color: #ABB2BE;
font-size: 12px;
}
&:last-of-type .el-breadcrumb__inner {
color: #606263;
font-size: 14px;
}
}
}
}
// 搜索
.table-search-form {
display: flex;
flex-wrap: wrap;
padding: 0 16px;
.form-wrap {
padding: 16px 16px 0 16px;
width: 100%;
background: #fff;
&:empty {
padding: 0;
}
}
section {
display: inline-block;
margin-left: 16px;
margin-bottom: 18px;
}
}
// 列表和分页
.table-content {
margin-top: 10px;
padding: 0 16px;
.table-wrap {
padding: 16px 16px 0 16px;
background: #fff;
// 工具栏
.toolbar {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
li {
display: inline-block;
margin-right: 6px;
}
}
// 表格
.el-table {
th {
.cell {
color: #666;
}
}
// 复选框列
.el-table-column--selection {
.cell {
text-align: center !important;
}
}
// 多值字段
.table-column-strings {
ul {
li {
display: inline-block;
background: #eee;
border-radius: 3px;
padding: 0 3px;
margin-right: 3px;
margin-bottom: 3px;
}
}
}
// 树视觉调整
[class*=el-table__row--level] .el-table__expand-icon {
position: relative;
left: -6px;
margin-right: 0;
}
}
// 分页
.table-pagination {
padding: 16px 0;
text-align: left;
}
}
}
</style>
<script>
export default {
name: 'BaseOpera',
data () {
return {
title: '',
visible: false,
isWorking: false,
// 接口
api: null,
// 配置数据
configData: {
'field.id': 'id'
}
}
},
methods: {
// 配置
config (extParams = {}) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = require('@/api' + extParams.api)
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
},
/**
* 打开窗口
* @title 窗口标题
* @target 编辑的对象
*/
open (title, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
// 确认新建/修改
confirm () {
if (this.form.id == null || this.form.id === '') {
this.__confirmCreate()
return
}
this.__confirmEdit()
},
// 确认新建
__confirmCreate () {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
// 调用新建接口
this.isWorking = true
this.api.create(this.form)
.then(() => {
this.visible = false
this.$tip.apiSuccess('新建成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
},
// 确认修改
__confirmEdit () {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
// 调用新建接口
this.isWorking = true
this.api.updateById(this.form)
.then(() => {
this.visible = false
this.$tip.apiSuccess('修改成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
}
}
}
</script>
<script>
import { mapState } from 'vuex'
export default {
name: 'BasePage',
data () {
return {
// 超级管理员角色code
adminCode: 'admin'
}
},
computed: {
...mapState(['userInfo']),
// 是否为超级管理员
isAdmin () {
return this.userInfo.roles.findIndex(code => code === this.adminCode) > -1
}
},
methods: {
// 是否包含指定角色
containRoles (roles) {
if (roles == null) {
return true
}
if (this.userInfo == null) {
return false
}
if (this.userInfo.roles == null || this.userInfo.roles.length === 0) {
return false
}
for (const code of roles) {
if (this.userInfo.roles.findIndex(r => r === code) > -1) {
return true
}
}
return false
},
// 是否包含指定权限
containPermissions (permissions) {
if (permissions == null) {
return true
}
if (this.userInfo == null) {
return false
}
if (this.userInfo.permissions == null || this.userInfo.permissions.length === 0) {
return false
}
for (const code of permissions) {
if (this.userInfo.permissions.findIndex(p => p === code) > -1) {
return true
}
}
return false
}
}
}
</script>
<script>
import BasePage from './BasePage'
export default {
name: 'BaseTable',
extends: BasePage,
data () {
return {
// 接口
api: null,
// 模块名称
module: '数据',
// 配置数据
configData: {
// id字段
'field.id': 'id',
// 主字段
'field.main': 'name'
},
// 是否正在执行
isWorking: {
// 搜索中
search: false,
// 删除中
delete: false,
// 导出中
export: false
},
// 表格数据
tableData: {
// 已选中的数据
selectedRows: [],
// 排序的字段
sorts: [],
// 当前页数据
list: [],
// 分页
pagination: {
pageIndex: 1,
pageSize: 10,
total: 0
}
}
}
},
methods: {
// 配置
config (extParams) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = require('@/api' + extParams.api)
extParams.module && (this.module = extParams.module)
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
extParams['field.main'] && (this.configData['field.main'] = extParams['field.main'])
this.tableData.sorts = extParams.sorts
},
// 搜索
search () {
this.handlePageChange(1)
},
// 导出Excel
exportExcel () {
this.__checkApi()
this.$dialog.exportConfirm('确认导出吗?')
.then(() => {
this.isWorking.export = true
this.api.exportExcel({
page: this.tableData.pagination.pageIndex,
capacity: 1000000,
model: this.searchForm,
sorts: this.tableData.sorts
})
.then(response => {
this.download(response)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.export = false
})
})
.catch(() => {})
},
// 搜索框重置
reset () {
this.$refs.searchForm.resetFields()
this.search()
},
// 每页显示数量变更处理
handleSizeChange (pageSize) {
this.tableData.pagination.pageSize = pageSize
this.search()
},
// 行选中处理
handleSelectionChange (selectedRows) {
this.tableData.selectedRows = selectedRows
},
// 排序
handleSortChange (sortData) {
this.tableData.sorts = []
if (sortData.order != null) {
this.tableData.sorts.push({
property: sortData.column.sortBy,
direction: sortData.order === 'descending' ? 'DESC' : 'ASC'
})
}
this.handlePageChange()
},
// 页码变更处理
handlePageChange (pageIndex) {
this.__checkApi()
this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
this.isWorking.search = true
this.api.fetchList({
page: this.tableData.pagination.pageIndex,
capacity: this.tableData.pagination.pageSize,
model: this.searchForm,
sorts: this.tableData.sorts
})
.then(data => {
this.tableData.list = data.records
this.tableData.pagination.total = data.total
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
},
// 删除
deleteById (row, childConfirm = true) {
this.__checkApi()
let message = `确认删除${this.module}${row[this.configData['field.main']]}】吗?`
if (childConfirm && row.children != null && row.children.length > 0) {
message = `确认删除${this.module}${row[this.configData['field.main']]}】及其子${this.module}吗?`
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
this.api.deleteById(row[this.configData['field.id']])
.then(() => {
this.$tip.apiSuccess('删除成功')
this.__afterDelete()
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
})
.catch(() => {})
},
/**
* 批量删除
* @treeMode 是否添加子节点删除确认
*/
deleteByIdInBatch (childConfirm = true) {
this.__checkApi()
if (this.tableData.selectedRows.length === 0) {
this.$tip.warning('请至少选择一条数据')
return
}
let message = `确认删除已选中的 ${this.tableData.selectedRows.length}${this.module}记录吗?`
if (childConfirm) {
const containChildrenRows = []
for (const row of this.tableData.selectedRows) {
if (row.children != null && row.children.length > 0) {
containChildrenRows.push(row[this.configData['field.main']])
}
}
if (containChildrenRows.length > 0) {
message = `本次将删除${this.module}${containChildrenRows.join('')}】及其子${this.module}记录,确认删除吗?`
}
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
this.api.deleteByIdInBatch(this.tableData.selectedRows.map(row => row.id).join(','))
.then(() => {
this.$tip.apiSuccess('删除成功')
this.__afterDelete(this.tableData.selectedRows.length)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
})
.catch(() => {})
},
// 删除处理
__afterDelete (deleteCount = 1) {
// 删除当前页最后一条记录时查询上一页数据
if (this.tableData.list.length - deleteCount === 0) {
this.handlePageChange(this.tableData.pagination.pageIndex - 1 === 0 ? 1 : this.tableData.pagination.pageIndex - 1)
} else {
this.handlePageChange(this.tableData.pagination.pageIndex)
}
},
// 检查接口是否配置
__checkApi () {
if (this.api == null) {
throw new Error('The page is not initialized, you can use method \'this.config\' to initialize this page.')
}
}
}
}
</script>
<template>
<span v-if="content.length <= limit">{{content}}</span>
<el-popover
v-else
v-model="visible"
popper-class="eva-column-detail-popover"
trigger="click"
>
<div class="eva-column-detail">
<pre class="eva-column-detail__main">{{formattedContent}}</pre>
<div class="eva-column-detail__action">
<el-button size="mini" @click="cancel">关闭</el-button>
<el-button
size="mini"
type="primary"
v-clipboard:copy="formattedContent"
v-clipboard:success="copySuccess"
v-clipboard:error="copyFailed"
@click="confirm"
>{{ confirmButtonText }}</el-button>
</div>
</div>
<el-button slot="reference" :type="buttonType">查看</el-button>
</el-popover>
</template>
<script>
export default {
name: 'ColumnDetail',
props: {
// 按钮类型
buttonType: {
type: String
},
// 内容
content: {
type: String,
default: ''
},
// 限制,大于限制时展示查看按钮
limit: {
type: Number,
default: 12
},
// 自动识别数据类型并格式化
analyse: {
type: Boolean,
default: true
},
// 是否允许复制
allowCopy: {
type: Boolean,
default: true
}
},
data () {
return {
visible: false
}
},
computed: {
// 确认按钮文案
confirmButtonText () {
return this.allowCopy ? '复制' : '确定'
},
// 格式化后的内容
formattedContent () {
let content = this.content
if (this.analyse) {
try {
content = JSON.stringify(JSON.parse(this.content), null, 2)
} catch (e) {
}
}
return content
}
},
methods: {
// 点击确认
confirm () {
this.visible = false
this.$emit('confirm')
},
// 点击取消
cancel () {
this.visible = false
this.$emit('cancel')
},
// 复制成功
copySuccess () {
this.$tip.success('复制成功')
},
// 复制失败
copyFailed () {
this.$tip.error('复制失败')
}
}
}
</script>
<style lang="scss">
.eva-column-detail-popover {
max-width: 80%;
}
</style>
<style scoped lang="scss">
.eva-column-detail {
.eva-column-detail__main {
max-height: 500px;
overflow: auto;
}
.eva-column-detail__action {
text-align: right;
}
}
</style>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:clearable="clearable"
:append-to-body="appendToBody"
:inline="inline"
:multiple="multiple"
:flat="multiple"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/department'
export default {
name: 'DepartmentSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
multiple: {
default: false
},
placeholder: {
default: '请选择部门'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有部门
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充部门树
__fillData (list, pool) {
for (const dept of pool) {
if (dept.id === this.excludeId) {
continue
}
const deptNode = {
id: dept.id,
label: dept.name
}
list.push(deptNode)
if (dept.children != null && dept.children.length > 0) {
deptNode.children = []
this.__fillData(deptNode.children, dept.children)
if (deptNode.children.length === 0) {
deptNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<template>
<el-drawer
class="global-window"
title="title"
:visible="visible"
:with-header="true"
:size="width"
:close-on-press-escape="false"
:wrapper-closable="false"
:append-to-body="true"
@close="close"
>
<div slot="title" class="window__header">
<span class="header__btn-back" @click="close"><i class="el-icon-arrow-left"></i></span>{{title}}
</div>
<div class="window__body">
<slot></slot>
</div>
<div v-if="withFooter" class="window__footer">
<slot name="footer">
<el-button @click="confirm" :loading="confirmWorking" type="primary">确定</el-button>
<el-button @click="close">取消</el-button>
</slot>
</div>
</el-drawer>
</template>
<script>
import { Drawer, Button } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
export default {
name: 'GlobalWindow',
props: {
width: {
type: String,
default: '36%'
},
// 是否包含底部操作
withFooter: {
type: Boolean,
default: true
},
// 确认按钮loading状态
confirmWorking: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ''
},
// 是否展示
visible: {
type: Boolean,
required: true
}
},
components: {
"el-drawer": Drawer,
"el-button": Button
},
methods: {
confirm () {
this.$emit('confirm')
},
close () {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/variables.scss";
// 输入框高度
$input-height: 32px;
.global-window {
// 头部标题
/deep/ .el-drawer__header {
padding: 0 10px 0 0;
line-height: 40px;
border-bottom: 1px solid #eee;
// 返回按钮
.header__btn-back {
display: inline-block;
width: 30px;
background: $primary-color;
color: #fff;
text-align: center;
margin-right: 12px;
border-right: 1px solid #eee;
}
.el-drawer__close-btn:focus {
outline: none;
}
}
// 主体
/deep/ .el-drawer__body {
display: flex;
flex-direction: column;
position: absolute;
top: 40px;
bottom: 0;
width: 100%;
overflow: hidden;
// 内容
.window__body {
height: 100%;
overflow-y: auto;
padding: 12px 16px;
// 标签
.el-form-item__label {
float: none;
}
// 元素宽度为100%
.el-form-item__content > *{
width: 100%;
}
}
// 尾部
.window__footer {
user-select: none;
border-top: 1px solid #eee;
height: 60px;
line-height: 60px;
text-align: center;
}
}
}
</style>
<template>
<div class="main-header">
<div class="header">
<h2>
<i class="el-icon-s-unfold" v-if="menuData.collapse" @click="switchCollapseMenu(null)"></i>
<i class="el-icon-s-fold" v-else @click="switchCollapseMenu(null)"></i>
{{title}}
</h2>
<div class="user">
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img v-if="userInfo != null" :src="userInfo.avatar == null ? '@/assets/images/avatar/man.png' : userInfo.avatar" alt="">{{userInfo | displayName}}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changePwd">修改密码</el-dropdown-item>
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 修改密码 -->
<GlobalWindow
title="修改密码"
:visible.sync="visible.changePwd"
@confirm="confirmChangePwd"
@close="visible.changePwd = false"
>
<el-form :model="changePwdData.form" ref="changePwdDataForm" :rules="changePwdData.rules">
<el-form-item label="原始密码" prop="oldPwd" required>
<el-input v-model="changePwdData.form.oldPwd" type="password" placeholder="请输入原始密码" maxlength="30" show-password></el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPwd" required>
<el-input v-model="changePwdData.form.newPwd" type="password" placeholder="请输入新密码" maxlength="30" show-password></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPwd" required>
<el-input v-model="changePwdData.form.confirmPwd" type="password" placeholder="请再次输入新密码" maxlength="30" show-password></el-input>
</el-form-item>
</el-form>
</GlobalWindow>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import GlobalWindow from './GlobalWindow'
import { logout, updatePwd } from '@/api/system/common'
export default {
name: 'Header',
components: { GlobalWindow },
data () {
return {
visible: {
// 修改密码
changePwd: false
},
isWorking: {
// 修改密码
changePwd: false
},
username: 'bob', // 用户名
// 修改密码弹框
changePwdData: {
form: {
oldPwd: '',
newPwd: '',
confirmPwd: ''
},
rules: {
oldPwd: [
{ required: true, message: '请输入原始密码' }
],
newPwd: [
{ required: true, message: '请输入新密码' }
],
confirmPwd: [
{ required: true, message: '请再次输入新密码' }
]
}
}
}
},
computed: {
...mapState(['menuData', 'userInfo']),
title () {
return this.$route.meta.title
}
},
filters: {
// 展示名称
displayName (userInfo) {
if (userInfo == null) {
return ''
}
if (userInfo.realname != null && userInfo.realname.trim().length > 0) {
return userInfo.realname
}
return userInfo.username
}
},
methods: {
...mapMutations(['setUserInfo', 'switchCollapseMenu']),
// 修改密码
changePwd () {
this.visible.changePwd = true
this.$nextTick(() => {
this.$refs.changePwdDataForm.resetFields()
})
},
// 确定修改密码
confirmChangePwd () {
if (this.isWorking.changePwd) {
return
}
this.$refs.changePwdDataForm.validate((valid) => {
if (!valid) {
return
}
// 验证两次密码输入是否一致
if (this.changePwdData.form.newPwd !== this.changePwdData.form.confirmPwd) {
this.$tip.warning('两次密码输入不一致')
return
}
// 执行修改
this.isWorking.changePwd = true
updatePwd({
oldPwd: this.changePwdData.form.oldPwd,
newPwd: this.changePwdData.form.newPwd
})
.then(() => {
this.$tip.apiSuccess('修改成功')
this.visible.changePwd = false
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.changePwd = false
})
})
},
// 退出登录
logout () {
logout()
.then(() => {
this.$router.push({ name: 'login' })
this.setUserInfo(null)
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.header {
overflow: hidden;
padding: 0 25px;
background: #fff;
height: 100%;
display: flex;
h2 {
width: 50%;
flex-shrink: 0;
line-height: $header-height;
font-size: 19px;
font-weight: 600;
color: #606263;
display: inline;
& > i {
font-size: 20px;
margin-right: 12px;
}
}
.user {
width: 50%;
flex-shrink: 0;
text-align: right;
.el-dropdown {
top: 2px;
}
img {
width: 32px;
position: relative;
top: 10px;
margin-right: 10px;
}
}
}
// 下拉菜单框
.el-dropdown-menu {
width: 140px;
.el-dropdown-menu__item:hover {
background: #E3EDFB;
color: $primary-color;
}
}
</style>
<template>
<div class="light" :class="{normal: !warn && !danger, warn: !danger && warn, danger, mini: mini}">
<em><i></i></em>
</div>
</template>
<script>
export default {
name: 'Light',
props: {
warn: {
type: Boolean,
default: false
},
danger: {
type: Boolean,
default: false
},
mini: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped lang="scss">
$cycle-size01: 16px;
$cycle-size02: 6px;
$normal-color: #00CC99;
$warn-color: #FFCC33;
$danger-color: #FF3300;
@mixin light-status ($cycle-bg) {
em {
background: $cycle-bg;
i {
background: $cycle-bg - 30;
}
}
}
.light {
display: inline-block;
border-radius: 50%;
em {
width: $cycle-size01;
height: $cycle-size01;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
i {
display: block;
width: $cycle-size02;
height: $cycle-size02;
border-radius: 50%;
}
}
&.mini {
em {
width: 12px;
height: 12px;
}
}
// 正常
&.normal {
@include light-status($normal-color);
animation: shine-normal infinite 1s;
}
// 警告
&.warn {
@include light-status($warn-color);
animation: shine-warn infinite .8s;
}
// 危险
&.danger {
@include light-status($danger-color);
animation: shine-danger infinite .5s;
}
}
@keyframes shine-normal {
0% {
box-shadow: 0 0 5px $normal-color + 10;
}
25% {
box-shadow: 0 0 10px $normal-color + 10;
}
50% {
box-shadow: 0 0 15px $normal-color + 10;
}
100% {
box-shadow: 0 0 20px $normal-color + 10;
}
}
@keyframes shine-warn {
0% {
box-shadow: 0 0 5px $warn-color - 50;
}
25% {
box-shadow: 0 0 10px $warn-color - 50;
}
50% {
box-shadow: 0 0 15px $warn-color - 50;
}
100% {
box-shadow: 0 0 20px $warn-color - 50;
}
}
@keyframes shine-danger {
0% {
box-shadow: 0 0 5px $danger-color + 10;
}
25% {
box-shadow: 0 0 10px $danger-color + 10;
}
50% {
box-shadow: 0 0 15px $danger-color + 10;
}
100% {
box-shadow: 0 0 20px $danger-color + 10;
}
}
</style>
<template>
<el-cascader
v-if="visible"
:props="props"
:placeholder="placeholder"
v-model="value"
:clearable="clearable"
@change="$emit('change')"
@input="handleInput"
></el-cascader>
</template>
<script>
import { fetchByParentId } from '@/api/system/location'
export default {
name: 'LocationSelect',
props: {
placeholder: {
default: '请选择地区'
},
level: {
default: 3
},
clearable: {
default: false
},
// 省
provinceId: {},
// 市
cityId: {},
// 区
areaId: {}
},
data () {
const vm = this
return {
// 是否展示,用于重新初始化cascader
visible: true,
// 已选值
value: [],
// 组件配置
props: {
lazy: true,
lazyLoad (node, resolve) {
const { level } = node
fetchByParentId(level === 0 ? -1 : node.value)
.cache()
.then(data => {
resolve(data.map(item => {
return {
label: item.name,
value: item.id,
leaf: level >= vm.level - 1
}
}))
})
.catch(e => {
vm.$tip.apiFailed(e)
})
}
}
}
},
watch: {
provinceId (newValue) {
this.value[0] = newValue
if (this.level === 1) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
},
cityId (newValue) {
if (this.level >= 2) {
this.value[1] = newValue
}
if (this.level === 2) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
},
areaId (newValue) {
if (this.level >= 3) {
this.value[2] = newValue
}
if (this.level === 3) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
}
},
methods: {
handleInput (values) {
this.$emit('update:province-id', values[0])
this.$emit('update:city-id', values[1])
this.$emit('update:area-id', values[2])
},
// 重新初始化cascader
__rebuild () {
this.visible = false
this.$nextTick(() => {
this.visible = true
})
}
}
}
</script>
<template>
<div class="menu" :class="{collapse: menuData.collapse}">
<div class="logo">
<div><img src="/logo.png"></div>
<h1 :class="{hidden: menuData.collapse}">eva</h1>
</div>
<scrollbar>
<el-menu
ref="menu"
:default-active="activeIndex"
text-color="#fff"
active-text-color="#fff"
:collapse="menuData.collapse"
:default-openeds="defaultOpeneds"
:collapse-transition="false"
@select="handleSelect"
>
<MenuItems v-for="menu in menuData.list" :key="menu.index" :menu="menu" :is-root-menu="true"/>
</el-menu>
</scrollbar>
</div>
</template>
<script>
import { mapState } from 'vuex'
import MenuItems from './MenuItems'
import Scrollbar from './Scrollbar'
export default {
name: 'Menu',
components: { Scrollbar, MenuItems },
computed: {
...mapState(['menuData']),
// 选中的菜单index
activeIndex () {
let path = this.$route.path
if (path.endsWith('/')) {
path = path.substring(0, path.length - 1)
}
const menuConfig = this.__getMenuConfig(path, 'url', this.menuData.list)
if (menuConfig == null) {
return null
}
return menuConfig.index
},
// 默认展开的菜单index
defaultOpeneds () {
return this.menuData.list.map(menu => menu.index)
}
},
methods: {
// 处理菜单选中
handleSelect (menuIndex) {
const menuConfig = this.__getMenuConfig(menuIndex, 'index', this.menuData.list)
// 找不到页面
try {
require('@/views' + menuConfig.url)
} catch (e) {
this.$tip.error('未找到页面文件@/views' + menuConfig.url + '.vue,请检查菜单路径是否正确')
}
// 点击当前菜单不做处理
if (menuConfig.url === this.$route.path) {
return
}
if (menuConfig.url == null || menuConfig.url.trim().length === 0) {
return
}
this.$router.push(menuConfig.url)
},
// 获取菜单配置
__getMenuConfig (value, key, menus) {
for (const menu of menus) {
if (menu[key] === value) {
return menu
}
if (menu.children != null && menu.children.length > 0) {
const menuConfig = this.__getMenuConfig(value, key, menu.children)
if (menuConfig != null) {
return menuConfig
}
}
}
return null
}
}
}
</script>
<style lang="scss" scoped>
@import "@/assets/style/variables.scss";
.menu {
height: 100%;
display: flex;
flex-direction: column;
// LOGO
.logo {
height: 60px;
flex-shrink: 0;
line-height: 60px;
overflow: hidden;
display: flex;
background: $primary-color - 20;
padding: 0 16px;
& > div {
width: 32px;
flex-shrink: 0;
margin-right: 12px;
img {
width: 100%;
flex-shrink: 0;
vertical-align: middle;
position: relative;
top: -2px;
}
}
h1 {
font-size: 16px;
font-weight: 500;
transition: opacity ease .3s;
overflow: hidden;
&.hidden {
opacity: 0;
}
}
}
}
</style>
<style lang="scss">
@import "@/assets/style/variables.scss";
// 菜单样式
.el-menu {
border-right: 0 !important;
user-select: none;
background: $primary-color !important;
.el-menu-item {
background: $primary-color;
// 选中状态
&.is-active {
background: $primary-color - 40 !important;
}
// 悬浮
&:hover {
background-color: $primary-color - 12;
}
&:focus {
background: $primary-color;
}
}
// 子菜单
.el-submenu {
.el-submenu__title{
background-color: $primary-color;
}
&.is-active {
.el-submenu__title{
background-color: $primary-color - 20;
}
.el-menu .el-menu-item{
background-color: $primary-color - 20;
// 悬浮
&:hover {
background-color: $primary-color - 30;
}
}
}
// 菜单上下箭头
.el-submenu__title i {
color: #f7f7f7;
}
}
// 菜单图标
i:not(.el-submenu__icon-arrow) {
color: #f7f7f7 !important;
position: relative;
top: -1px;
// 自定义图标
&[class^="eva-icon-"] {
width: 24px;
margin-right: 5px;
background-size: 15px;
}
}
}
</style>
<template>
<el-menu-item v-if="menu.children == null || menu.children.length == 0" :key="menu.index" :index="menu.index">
<i :class="menu.icon"></i>
<span slot="title">{{menu.label}}</span>
</el-menu-item>
<el-submenu v-else :index="menu.index">
<template slot="title">
<i :class="menu.icon"></i>
<span slot="title">{{menu.label}}</span>
</template>
<MenuItems v-for="child in menu.children" :menu="child" :key="child.index"/>
</el-submenu>
</template>
<script>
export default {
name: 'MenuItems',
props: {
menu: {
type: Object,
required: true
}
}
}
</script>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:append-to-body="appendToBody"
:clearable="clearable"
:inline="inline"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/menu'
export default {
name: 'MenuSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
placeholder: {
default: '请选择菜单'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有菜单
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充菜单树
__fillData (list, pool) {
for (const menu of pool) {
if (menu.id === this.excludeId) {
continue
}
const menuNode = {
id: menu.id,
label: menu.name
}
list.push(menuNode)
if (menu.children != null && menu.children.length > 0) {
menuNode.children = []
this.__fillData(menuNode.children, menu.children)
if (menuNode.children.length === 0) {
menuNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<template>
<div class="not-allow">
<slot>
<div class="content">
<img src="../../assets/images/not-allow.png">
<h2>无权访问</h2>
<p>如您需要访问该页面,请联系系统管理员</p>
</div>
</slot>
</div>
</template>
<script>
export default {
name: 'NotAllow'
}
</script>
<style scoped lang="scss">
.not-allow {
height: 100%;
background: #fff;
box-sizing: border-box;
padding-top: 160px;
.content {
height: 200px;
text-align: center;
h2 {
font-size: 18px;
font-weight: normal;
margin-top: 8px;
}
p {
font-size: 13px;
color: #999;
margin: 6px 0;
}
}
}
</style>
<template>
<div class="table-pagination">
<el-pagination
:current-page="pagination.pageIndex"
:page-sizes="[10, 20, 30, 40]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="sizeChange"
@current-change="currentChange"
background>
</el-pagination>
</div>
</template>
<script>
export default {
name: 'Pagination',
props: {
pagination: {
type: Object,
default: function () {
return {}
}
}
},
data () {
return {
}
},
methods: {
sizeChange (value) {
this.$emit('size-change', value)
},
currentChange (value) {
this.$emit('current-change', value)
}
}
}
</script>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:clearable="clearable"
:append-to-body="appendToBody"
:inline="inline"
:multiple="multiple"
:flat="multiple"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/position'
export default {
name: 'PositionSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
multiple: {
default: false
},
placeholder: {
default: '请选择岗位'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有岗位
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充岗位树
__fillData (list, pool) {
for (const dept of pool) {
if (dept.id === this.excludeId) {
continue
}
const deptNode = {
id: dept.id,
label: dept.name
}
list.push(deptNode)
if (dept.children != null && dept.children.length > 0) {
deptNode.children = []
this.__fillData(deptNode.children, dept.children)
if (deptNode.children.length === 0) {
deptNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<style scoped lang="scss">
.inline {
width: 178px;
}
.vue-treeselect {
line-height: 30px;
/deep/ .vue-treeselect__control {
height: 32px;
.vue-treeselect__single-value {
line-height: 30px;
}
}
}
</style>
<template>
<div v-if="containRoles(roles) && containPermissions(permissions)">
<slot></slot>
</div>
<div v-else class="not-allow-wrap">
<slot name="not-allow"><NotAllow/></slot>
</div>
</template>
<script>
import BasePage from '../base/BasePage'
import NotAllow from './NotAllow'
export default {
name: 'Profile',
components: { NotAllow },
extends: BasePage,
props: {
permissions: {
type: Array
},
roles: {
type: Array
}
}
}
</script>
<style scoped lang="scss">
.not-allow-wrap {
height: 100%;
padding: 10px 16px;
box-sizing: border-box;
}
</style>
<template>
<vue-scroll :ops="options">
<slot></slot>
</vue-scroll>
</template>
<script>
import VueScroll from 'vuescroll'
export default {
name: 'Scrollbar',
components: { VueScroll },
data () {
return {
options: {
bar: {
background: 'rgba(20,20,20,.3)'
}
}
}
}
}
</script>
<template>
<div class="search-form-collapse" :class="{'collapse__hidden': !showMore}">
<slot></slot>
<el-button v-if="!showMore" class="collapse__switch" @click="showMore = true">更多查询...</el-button>
<el-button v-else class="collapse__switch" @click="showMore = false">收起</el-button>
</div>
</template>
<script>
export default {
name: 'SearchFormCollapse',
data () {
return {
showMore: false
}
}
}
</script>
<style scoped lang="scss">
.search-form-collapse {
position: relative;
padding-right: 75px;
height: auto;
.collapse__switch {
position: absolute;
top: 0;
right: 0;
}
&.collapse__hidden {
height: 50px;
overflow: hidden;
padding-right: 250px;
/deep/ section {
position: absolute;
top: 0;
right: 100px;
}
}
}
</style>
<!-- 组件详情请参阅官方文档:https://www.vue-treeselect.cn/ -->
<template>
<vue-tree-select
:class="{inline}"
:placeholder="placeholder"
:value="value"
:options="data"
:clearable="clearable"
:flat="flat"
:append-to-body="appendToBody"
:multiple="multiple"
no-children-text="无记录"
no-options-text="无记录"
no-results-text="未匹配到数据"
@input="$emit('input', $event)"
/>
</template>
<script>
import VueTreeSelect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'TreeSelect',
props: {
inline: {
default: false
},
multiple: {
default: false
},
flat: {
default: false
},
value: {},
placeholder: {
default: '请选择'
},
// 是否可清空
clearable: {
default: false
},
data: {
type: Array,
required: true
},
appendToBody: {
default: false
}
},
components: { VueTreeSelect }
}
</script>
<style scoped lang="scss">
.inline {
width: 178px;
}
.vue-treeselect {
line-height: 30px;
/deep/ .vue-treeselect__control {
height: 32px;
.vue-treeselect__single-value {
line-height: 30px;
}
}
}
</style>
<template>
<div class="value">
<i class="el-icon-loading" v-if="data == null"></i>
<slot v-else>{{getValue()}}{{suffix}}</slot>
</div>
</template>
<script>
export default {
name: 'Value',
props: {
data: {
type: Object
},
prop: {
type: String
},
suffix: {
type: String
},
handler: {
type: Function
}
},
methods: {
getValue () {
if (this.data == null) {
return ''
}
if (this.prop == null) {
return this.data
}
const props = this.prop.split('.')
let i = 0
let value = this.data
while (i < props.length) {
value = value[props[i]]
i++
}
if (this.handler == null) {
return value
}
return this.handler(value)
}
}
}
</script>
<style scoped lang="scss">
.value {
word-break: break-all;
.el-icon-loading {
font-size: 16px;
color: #999;
position: relative;
top: 1px;
}
}
</style>
<template>
<component :is="component" :value="values" :inline="false" @input="handleInput" multiple/>
</template>
<script>
export default {
name: 'CustomSelect',
props: {
value: {},
businessCode: {
type: String,
required: true
}
},
computed: {
// vuetreeselect值类型匹配(解决编辑时无法删除已有值的BUG)
values () {
if (this.businessCode === 'DEPARTMENT' || this.businessCode === 'POSITION') {
const values = []
for (const id of this.value) {
values.push(parseInt(id))
}
return values
}
return this.value
},
component () {
// 部门选择器
if (this.businessCode === 'DEPARTMENT') {
return () => import('@/components/common/DepartmentSelect')
}
// 岗位选择器
if (this.businessCode === 'POSITION') {
return () => import('@/components/common/PositionSelect')
}
return null
}
},
methods: {
handleInput (value) {
this.$emit('input', value)
this.$emit('change', value)
}
}
}
</script>
<style scoped>
</style>
<template>
<el-select
class="data-perm-module-select"
:class="{select__block: !inline}"
:value="value"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option v-for="module in modules" :key="module.businessCode" :value="module.businessCode" :label="module.moduleName"/>
</el-select>
</template>
<script>
import { fetchModules } from '@/api/system/dataPermission'
export default {
name: 'DataPermModuleSelect',
props: {
value: {},
placeholder: {
default: '请选择权限模块'
},
inline: {
default: true
},
disabled: {},
clearable: {
default: false
}
},
data () {
return {
modules: []
}
},
created () {
fetchModules()
.cache()
.then(data => {
this.modules = data
})
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>
<template>
<el-select
class="data-perm-type-select"
:class="{select__block: !inline}"
:value="value"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option v-for="type in filterTypes" :key="type.code" :value="type.code" :label="type.remark"/>
</el-select>
</template>
<script>
import { fetchTypes } from '@/api/system/dataPermission'
export default {
name: 'DataPermTypeSelect',
props: {
value: {},
// 模块名称
module: {},
placeholder: {
default: '请选择权限类型'
},
inline: {
default: true
},
disabled: {},
clearable: {
default: false
}
},
data () {
return {
types: []
}
},
computed: {
filterTypes () {
if (this.module == null || this.module === '') {
return []
}
const types = []
for (const type of this.types) {
if (type.modules.length === 0 || type.modules.indexOf(this.module) !== -1) {
types.push(type)
}
}
return types
}
},
created () {
fetchTypes()
.cache()
.then(data => {
this.types = data
})
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>
<template>
<GlobalWindow
:title="title"
:visible.sync="visible"
:confirm-working="isWorking"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="业务模块" prop="businessCode" required>
<DataPermModuleSelect v-model="form.businessCode" :disabled="form.id != null" :inline="false" @change="handleBusinessChange"/>
</el-form-item>
<el-form-item label="角色" prop="roleId" required>
<RoleSelect v-model="form.roleId" :disabled="form.id != null" :inline="false"/>
</el-form-item>
<el-form-item label="权限类型" prop="type" required>
<DataPermTypeSelect v-model="form.type" :module="form.businessCode" :inline="false" @change="handleTypeChange"/>
</el-form-item>
<el-form-item v-show="showCustomData" label="自定义数据" prop="customData">
<CustomSelect v-if="visible" v-model="customData" :business-code="form.businessCode" @change="handleCustomDataChange"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" v-trim :rows="3" maxlength="500"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import RoleSelect from '@/components/system/role/RoleSelect'
import DataPermModuleSelect from './DataPermModuleSelect'
import DataPermTypeSelect from './DataPermTypeSelect'
import CustomSelect from './CustomSelect'
export default {
name: 'OperaDataPermissionWindow',
extends: BaseOpera,
components: { CustomSelect, RoleSelect, DataPermTypeSelect, DataPermModuleSelect, GlobalWindow },
data () {
return {
// 自定义数据
customData: [],
// 展示自定义数据标识
showCustomData: false,
// 表单数据
form: {
id: null,
businessCode: '',
roleId: '',
type: '',
remark: '',
customData: ''
},
// 验证规则
rules: {
businessCode: [
{ required: true, message: '请选择业务模块' }
],
roleId: [
{ required: true, message: '请选择角色' }
],
type: [
{ required: true, message: '请选择权限类型' }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @target 编辑的对象
*/
open (title, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.customData = []
this.showCustomData = false
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
this.customData = this.form.customData == null || this.form.customData === '' ? [] : this.form.customData.split(',')
this.handleTypeChange()
})
},
// 业务模块切换
handleBusinessChange () {
this.form.customData = ''
this.customData = []
this.handleTypeChange()
},
// 权限类型切换
handleTypeChange () {
if ((this.form.type === 11 || this.form.type === 21) && this.form.businessCode != null && this.form.businessCode !== '') {
this.showCustomData = true
} else {
this.showCustomData = false
}
},
// 自定义数据变化
handleCustomDataChange (values) {
this.form.customData = values.join(',')
}
},
created () {
this.config({
api: '/system/dataPermission',
'field.id': 'id'
})
}
}
</script>
<template>
<GlobalWindow
class="position-user-window"
width="80%"
:title="departmentName + '人员列表'"
:visible.sync="visible"
:with-footer="false"
>
<TableLayout :with-breadcrumb="false">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="80px" inline>
<el-form-item label="用户名" prop="username">
<el-input v-model="searchForm.username" placeholder="请输入用户名" v-trim @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="姓名" prop="realname">
<el-input v-model="searchForm.realname" placeholder="请输入姓名" v-trim @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="searchForm.mobile" placeholder="请输入手机号码" v-trim @keypress.enter.native="search"/>
</el-form-item>
<section>
<el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<div slot="space" class="toolbar">
<el-switch v-model="onlyCurrentDept" @change="search" :disabled="isWorking.search"/>
<label>仅查看当前部门人员</label>
</div>
<template v-slot:table-wrap>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column prop="avatar" label="头像" width="80px" class-name="table-column-avatar" fixed="left">
<template slot-scope="{row}">
<img :src="row.avatar == null ? '/static/avatar/man.png' : row.avatar">
</template>
</el-table-column>
<el-table-column prop="realname" label="姓名" min-width="100px" fixed="left"></el-table-column>
<el-table-column prop="username" label="用户名" min-width="100px"></el-table-column>
<el-table-column prop="empNo" label="工号" min-width="80px"></el-table-column>
<el-table-column prop="department" label="部门" min-width="120px">
<template slot-scope="{row}">{{row.department == null ? '' : row.department.name}}</template>
</el-table-column>
<el-table-column prop="position" label="岗位" min-width="120px" class-name="table-column-strings">
<template slot-scope="{row}">
<ul>
<li v-for="position in row.positions" :key="position.id">{{position.name}}</li>
</ul>
</template>
</el-table-column>
<el-table-column prop="sex" label="性别" min-width="80px">
<template slot-scope="{row}">
{{row.sex | sex}}
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机号码" min-width="100px"></el-table-column>
<el-table-column prop="email" label="邮箱" min-width="180px"></el-table-column>
<el-table-column prop="birthday" label="生日" min-width="100px"></el-table-column>
<el-table-column prop="birthday" label="角色" min-width="160px" class-name="table-column-role">
<template slot-scope="{row}">
<ul>
<li v-for="role in row.roles" :key="role.id">{{role.name}}</li>
</ul>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
</TableLayout>
</GlobalWindow>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import GlobalWindow from '@/components/common/GlobalWindow'
import Pagination from '@/components/common/Pagination'
import { fetchUserList } from '@/api/system/department'
export default {
name: 'DepartmentUserWindow',
extends: BaseTable,
components: { Pagination, GlobalWindow, TableLayout },
data () {
return {
departmentId: null,
departmentName: '',
visible: false,
// 仅查看当前部门
onlyCurrentDept: true,
// 搜索表单
searchForm: {
departmentId: null,
username: '',
realname: '',
mobile: ''
}
}
},
methods: {
// 打开查看人员窗口
open (departmentId, departmentName) {
this.departmentId = departmentId
this.departmentName = departmentName
this.searchForm.departmentId = departmentId
this.visible = true
this.search()
},
// 处理分页
handlePageChange (pageIndex) {
// 仅查看当前部门处理
this.searchForm.strictDeptId = null
this.searchForm.rootDeptId = this.searchForm.departmentId
if (this.onlyCurrentDept) {
this.searchForm.strictDeptId = this.searchForm.departmentId
this.searchForm.rootDeptId = null
}
this.tableData.pagination.pageIndex = pageIndex
this.isWorking.search = true
fetchUserList({
page: pageIndex,
capacity: this.tableData.pagination.pageSize,
model: this.searchForm
})
.then(data => {
this.tableData.list = data.records
this.tableData.pagination.total = data.total
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
}
}
}
</script>
<style scoped lang="scss">
.position-user-window {
/deep/ .table-search-form {
padding: 0;
}
/deep/ .window__body {
background: #f7f7f7;
.table-content {
padding: 0;
.table-wrap {
padding: 0;
}
}
}
.toolbar {
margin-top: 10px;
padding: 6px 12px;
background: #fff;
font-size: 13px;
label {
margin-left: 8px;
vertical-align: middle;
color: #999;
}
}
// 列表头像处理
.table-column-avatar {
img {
width: 48px;
}
}
// 列表角色处理
.table-column-role {
ul {
li {
display: inline-block;
background: #eee;
border-radius: 3px;
padding: 0 3px;
margin-right: 3px;
}
}
}
}
</style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment