Commit 0587447b authored by wanli's avatar wanli

🐞 fix(资源监视模块):

资源监视模块增加更多信息展示、前端数据库缓存、后端接口超时优化
parent 5237c65e
'''
Author: your name
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
Description: In User Settings Edit
FilePath: \evm-store\backend\controller\monitor.py
'''
import logging
from model.monitor import session, System, Lvgl, Evm, Image, Device, Request, User
logger = logging.getLogger(__name__)
class SystemResource(object):
def get(self):
result = session.query(System).all()
......@@ -117,16 +121,8 @@ def insert_data(msg):
if result:
watch_id = result.id
else:
user = session.query(User).filter(User.account=="evm").first()
if user:
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
logger.info("设备不存在")
return None
if msg.get("request"):
msg.get("request").update({ "watch": watch_id })
......
'''
Author: your name
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
Description: In User Settings Edit
FilePath: \evm-store\backend\view\monitor.py
......@@ -216,7 +216,7 @@ class NotifyHandler(BaseWebsocket):
def on_heartbeat(self):
# 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接
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)
del self._clients[i]
className = self.__class__.__name__
......@@ -327,16 +327,16 @@ class DeviceMessageHandler(BaseHandler):
logger.info(data)
data.update({ 'request': {
data.get("system", {}).update({
'host': self.request.remote_ip,
'path': self.request.path,
'protocol': self.request.protocol
} })
})
insert_data(data)
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)
self.write(json.dumps({ 'code': 100, 'message': 'success' }))
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 = {
presets: [
'@vue/cli-plugin-babel/preset'
'@vue/cli-plugin-babel/preset',
]
}
export default function Database() {
/**
* 数据库对象
* @param {[type]} option [description]
*/
// var option = {
// dbName: '', // 数据库名
// version: 1, // 数据库版本号
// primaryKey: 'id' // 需要保存的数据字段
// keyNames: [{ // 需要保存的数据字段
// key: '', // 字段名
// unique: // 当前这条数据是否能重复 (最常用) 默认false
// }]
// };
var IndexedDB = function IndexedDB(option) {
// 设置unique默认值为false
var list;
if (option.keyNames) {
list = option.keyNames;
for (var i = 0; i < list.length; i++) {
if (!list[i].unique) {
list[i].unique = false;
}
}
}
this.dbName = option.dbName;
this.version = option.version;
this.primaryKey = option.primaryKey || "id";
this.keyNames = option.keyNames;
this.db = null; // 存储数据库对象
this.allData = []; // 存储返回的所有数据
};
IndexedDB.prototype = {
/**
* 初始化数据库
* @return {[type]} [description]
*/
init: function () {
const _this = this;
return new Promise((resolve, reject) => {
var dbName = _this.dbName,
version = _this.version,
primaryKey = _this.primaryKey,
keyNames = _this.keyNames;
var indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB;
// 判断浏览器是否支持indexedDB
if (!indexedDB) {
reject(new Error("您的浏览器不支持当前内容"));
return false;
}
// 判断数据库名称是否为空
if (!dbName) {
reject(new Error(null, "数据库名称不能为空"));
return false;
}
// 打开数据库
var DBOpenRequest = window.indexedDB.open(dbName, version);
// 数据库打开成功
DBOpenRequest.onsuccess = function (event) {
_this.db = DBOpenRequest.result;
resolve(event);
};
// 数据库打开失败
DBOpenRequest.onerror = function (event) {
reject(event);
};
// 首次创建或者版本变更(更高版本)
DBOpenRequest.onupgradeneeded = function (event) {
_this.db = event.target.result;
_this.db.onerror = function (event) {
console.log(event);
reject(new Error("数据库打开失败"));
};
// 创建一个数据库存储对象
/*
// createIndex的语法
objectStore.createIndex(indexName, keyPath, {
unique: false, // true
multiEntry: false, // 如果为true,同时keyPath是数组元素,则所以会加到所有数组元素上
locale: 'auto' // 目前仅Firefox支持,可以是字符串,类似en-US, 或 zh,也可以是auto或者null, undefined
})
*/
if (!_this.db.objectStoreNames.contains(dbName)) {
var objectStore = _this.db.createObjectStore(dbName, {
keyPath: primaryKey,
autoIncrement: true,
});
}
keyNames.forEach(function (currentValue, index, array) {
/**
* 创建索引
* @string indexName 设置当前 index 的名字
* @string property 从存储数据中,指明 index 所指的属性。
* @string property 从存储数据中,指明 index 所指的属性。
* @object options options 有三个选项:
* unique: 当前这条数据是否能重复 (最常用)
* multiEntry: 设置当前的 property 为数组时,会给数组里面每个元素都设置一个 index 值。
* locale:目前仅支持Firefox(43+),这允许您为索引指定区域设置。
*/
console.log(index, array);
// objectStore.createIndex('indexName', 'property', options);
objectStore.createIndex(currentValue.key, currentValue.key, {
unique: currentValue.unique || false,
});
});
};
});
},
/**
* 添加数据
* @param {Array || Object} option 存入数据库的数据
*/
set: function (option) {
var _this = this;
var dbName = _this.dbName;
// version = _this.version,
// primaryKey = _this.primaryKey,
// keyNames = _this.keyNames;
var transaction = this.db.transaction([dbName], "readwrite");
var objectStore = transaction.objectStore(dbName);
if (Array.isArray(option)) {
for (var i = 0; i < option.length; i++) {
objectStore.add(option[i]).onsuccess = function (event) {
console.log("数据写入成功", event);
};
transaction.onerror = function (event) {
console.log("数据库中已有该数据", event);
};
}
} else {
objectStore.add(option).onsuccess = function (event) {
console.log("数据写入成功", event);
};
transaction.onerror = function (event) {
console.log("数据库中已有该数据", event);
};
}
},
/**
* 更新单条或多条数据
* @param {Array || object} option 更新的数据
* @return {[type]} [description]
*/
updateData: function (option) {
var _this = this;
var dbName = _this.dbName;
var transaction = _this.db.transaction([dbName], "readwrite");
var objectStore = transaction.objectStore(dbName);
if (Array.isArray(option)) {
for (var i = 0; i < option.length; i++) {
objectStore.put(option[i]).onsuccess = function (event) {
console.log("数据更新成功", event);
};
transaction.onerror = function (event) {
console.log("数据更新成功", event);
};
}
} else {
objectStore.put(option).onsuccess = function (event) {
console.log("数据更新成功", event);
};
transaction.onerror = function (event) {
console.log("数据更新成功", event);
};
}
},
/**
* 遍历数据表里所有的数据
* @return {Array || Obiect} 返回数据表里所有的数据
*/
getAllData: function (callback) {
var _this = this;
var dbName = _this.dbName;
return new Promise((resolve, reject) => {
_this.allData = [];
var objectStore = _this.db.transaction(dbName).objectStore(dbName);
objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
if (callback && callback(cursor.value)) {
_this.allData.push(cursor.value);
}
cursor.continue();
} else {
resolve(_this.allData);
}
};
objectStore.openCursor().onerror = function (event) {
reject(new Error(event));
};
});
},
/**
* 获取数据表里单条数据
* @param {String || Number} value 数据字段的值(按主键搜索)
* @return {[type]} [description]
*/
getData: function (value) {
var _this = this;
var dbName = _this.dbName;
return new Promise((resolve, reject) => {
var objectStore = _this.db.transaction(dbName).objectStore(dbName);
var request = objectStore.get(value);
request.onerror = function (event) {
console.log("获取失败", event);
reject(new Error(event));
};
request.onsuccess = function (event) {
if (request.result) {
console.log(request.result);
resolve(request.result);
} else {
console.log("未获得数据记录", event);
reject(new Error(event));
}
};
});
},
/**
* 删除数据库
* @return {[type]} [description]
*/
deleteDB: function () {
var _this = this;
var dbName = _this.dbName;
window.indexedDB.deleteDatabase(dbName);
console.log(dbName + "数据库已删除");
},
/**
* 关闭数据库
* @return {[type]} [description]
*/
closeDB: function () {
var _this = this;
_this.db.close();
console.log("数据库已关闭");
},
/**
* 删除某一条记录
* @param {String || Number} value 主键的值
* @return {[type]} [description]
*/
deleteData: function (value) {
var _this = this;
var dbName = _this.dbName;
return new Promise((resolve, reject) => {
var request = _this.db
.transaction(dbName, "readwrite")
.objectStore(dbName)
.delete(value);
request.onsuccess = function (event) {
console.log("删除成功", event);
resolve(event);
};
request.onerror = function (event) {
console.log("删除成功", event);
reject(new Error(event));
};
});
},
/**
* 删除存储空间全部记录
* @return {[type]} [description]
*/
clearData: function () {
var _this = this;
var dbName = _this.dbName;
var store = _this.db.transaction(dbName, "readwrite").objectStore(dbName);
store.clear();
console.log("已删除存储空间" + dbName + "全部记录");
},
/**
* 数据查询
* @param {String} key 数据字段名
* @param {String || Number} value 按字段查询的值
* @return {[type]} [description]
*/
searchData: function (key, value) {
var _this = this;
var dbName = _this.dbName;
return new Promise((resolve, reject) => {
var store = _this.db
.transaction(dbName, "readonly")
.objectStore(dbName);
var index = store.index(key);
var request = index.get(value);
request.onerror = function (event) {
console.error("搜索出错");
reject(new Error(event));
};
request.onsuccess = function (e) {
var result = e.target.result;
if (result) {
resolve(result);
} else {
console.log("您搜索的数据不存在");
resolve(null);
}
};
});
},
};
return IndexedDB;
}
......@@ -31,6 +31,8 @@ import { wsNotify } from "@/utils/eventBus.js";
// dataList.push(randomData());
// }
let chart = null
const seriesData = {
heap_total_size: [],
heap_used_size: [],
......@@ -65,7 +67,6 @@ export default {
data() {
return {
loading: null,
chart: null,
timer: null,
series: [
{
......@@ -131,15 +132,19 @@ export default {
});
wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize()
if (chart) chart.resize()
});
wsNotify.eventBus.$on("clear-evm-chart", () => {
this.setOptions()
});
},
beforeDestroy() {
if (!this.chart) {
if (!chart) {
return;
}
this.chart.dispose();
this.chart = null;
chart.dispose();
chart = null;
},
methods: {
cleanData() {
......@@ -151,7 +156,7 @@ export default {
this.series.forEach((item) => {
item.data = [];
});
this.chart.setOption({ series: this.series });
chart.setOption({ series: this.series });
},
handleMessage(data) {
if (!data || data.length == 0) this.cleanData()
......@@ -169,18 +174,18 @@ export default {
});
this.$nextTick(() => {
this.chart &&
this.chart.setOption({
chart &&
chart.setOption({
series: this.series,
});
});
},
initChart() {
this.chart = echarts.init(this.$el, "macarons");
chart = echarts.init(this.$el, "macarons");
this.setOptions();
},
setOptions() {
this.chart.setOption({
chart.setOption({
title: {
text: "EVM",
},
......
......@@ -9,6 +9,8 @@ import resize from "./mixins/resize";
import { getDateTimeString } from "@/utils/utils";
import { wsNotify } from "@/utils/eventBus.js";
let chart = null
const seriesData = {
frag_pct: [],
free_biggest_size: [],
......@@ -50,7 +52,6 @@ export default {
},
data() {
return {
chart: null,
series: [
{
name: "frag_pct",
......@@ -155,15 +156,19 @@ export default {
});
wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize()
if (chart) chart.resize()
});
wsNotify.eventBus.$on("clear-lvgl-chart", () => {
this.setOptions()
});
},
beforeDestroy() {
if (!this.chart) {
if (!chart) {
return;
}
this.chart.dispose();
this.chart = null;
chart.dispose();
chart = null;
},
methods: {
handleData(data) {
......@@ -173,7 +178,7 @@ export default {
this.series.forEach(item => {
item.data = []
});
this.chart.setOption({ series: this.series });
chart.setOption({ series: this.series });
data.forEach((item) => {
this.handleMessage(item);
......@@ -191,18 +196,18 @@ export default {
});
this.$nextTick(() => {
this.chart &&
this.chart.setOption({
chart &&
chart.setOption({
series: this.series,
});
});
},
initChart() {
this.chart = echarts.init(this.$el, "macarons");
chart = echarts.init(this.$el, "macarons");
this.setOptions();
},
setOptions() {
this.chart.setOption({
chart.setOption({
title: {
text: "LVGL",
},
......
......@@ -15,6 +15,8 @@ const seriesData = {
used_space_size: [],
};
let chart = null
export default {
mixins: [resize],
props: {
......@@ -46,7 +48,6 @@ export default {
},
data() {
return {
chart: null,
series: [
{
name: "free_size",
......@@ -80,11 +81,7 @@ export default {
data: seriesData.used_space_size,
},
],
legendData: [
"free_size",
"free_space_size",
"used_space_size"
],
legendData: Object.keys(seriesData),
};
},
watch: {
......@@ -107,15 +104,19 @@ export default {
});
wsNotify.eventBus.$on("resize", () => {
if (this.chart) this.chart.resize()
if (chart) chart.resize()
});
wsNotify.eventBus.$on("clear-system-chart", () => {
this.setOptions()
})
},
beforeDestroy() {
if (!this.chart) {
if (!chart) {
return;
}
this.chart.dispose();
this.chart = null;
chart.dispose();
chart = null;
},
methods: {
handleData(data) {
......@@ -125,7 +126,8 @@ export default {
this.series.forEach(item => {
item.data = []
});
this.chart.setOption({ series: this.series });
// chart.dispose();
chart.setOption({ series: this.series });
data.forEach((item) => {
this.handleMessage(item);
......@@ -135,26 +137,27 @@ export default {
Object.keys(data).forEach((k) => {
var t = getDateTimeString(new Date());
if (k == "timestamp") t = data[k];
if (this.legendData.includes(k))
if (this.legendData.includes(k)) {
seriesData[k].push({
name: k,
value: [t, data[k]],
});
}
});
this.$nextTick(() => {
this.chart &&
this.chart.setOption({
chart &&
chart.setOption({
series: this.series,
});
});
},
initChart() {
this.chart = echarts.init(this.$el, "macarons");
chart = echarts.init(this.$el, "macarons");
this.setOptions();
},
setOptions() {
this.chart.setOption({
chart.setOption({
title: {
text: "SYSTEM",
},
......
......@@ -236,7 +236,7 @@
<grid-item
:x="0"
:y="10"
:w="12"
:w="8"
:h="10"
i="5"
@resize="resizeEvent"
......@@ -245,7 +245,7 @@
@container-resized="containerResizedEvent"
@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>
<el-table
element-loading-text="Loading"
......@@ -297,19 +297,43 @@
</el-table>
</div>
</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
:x="0"
:y="20"
:w="12"
:h="7"
i="6"
i="7"
@resize="resizeEvent"
@move="moveEvent"
@resized="resizedEvent"
@container-resized="containerResizedEvent"
@moved="movedEvent"
>
<SystemChart :chartData="system"></SystemChart>
<SystemChart
:chartData="system"
:dataList="systemHistory"
></SystemChart>
</grid-item>
<grid-item
:x="0"
......@@ -323,7 +347,7 @@
@container-resized="containerResizedEvent"
@moved="movedEvent"
>
<EvmChart :chartData="evm"></EvmChart>
<EvmChart :chartData="evm" :dataList="evmHistory"></EvmChart>
</grid-item>
<grid-item
:x="0"
......@@ -337,7 +361,7 @@
@container-resized="containerResizedEvent"
@moved="movedEvent"
>
<LvglChart :chartData="lvgl"></LvglChart>
<LvglChart :chartData="lvgl" :dataList="lvglHistory"></LvglChart>
</grid-item>
</grid-layout>
</div>
......@@ -350,17 +374,43 @@ import LvglChart from "./components/LvglChart";
import SystemChart from "./components/SystemChart";
import { GridLayout, GridItem } from "vue-grid-layout";
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 {
name: "Monitor",
data() {
return {
watchs: [],
pngList: [],
globalData: null,
device: null,
devices: {},
deviceList: null,
system: {},
systemList: [],
systemHistory: [],
evmHistory: [],
lvglHistory: [],
evm: {},
evmList: [],
lvgl: {},
......@@ -387,10 +437,11 @@ export default {
{ 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: 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: 20, w: 12, h: 7, i: "6", static: false },
{ x: 0, y: 27, w: 12, h: 7, i: "7", static: false },
{ x: 0, y: 34, w: 12, h: 7, i: "8", static: false },
{ x: 0, y: 10, w: 8, h: 10, i: "5", static: false },
{ x: 8, y: 10, w: 4, h: 10, i: "6", static: false },
{ x: 0, y: 20, w: 12, h: 7, i: "7", 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,
resizable: true,
......@@ -408,54 +459,19 @@ export default {
return row.highlight ? "success-row" : "";
},
moveEvent(i, newX, newY) {
const msg = "MOVE i=" + i + ", X=" + newX + ", Y=" + newY;
console.log(msg);
console.log(i, newX, newY);
},
movedEvent(i, newX, newY) {
const msg = "MOVED i=" + i + ", X=" + newX + ", Y=" + newY;
console.log(msg);
console.log(i, newX, newY);
},
resizeEvent(i, newH, newW, newHPx, newWPx) {
const msg =
"RESIZE i=" +
i +
", H=" +
newH +
", W=" +
newW +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
console.log(i, newH, newW, newHPx, newWPx);
},
resizedEvent(i, newX, newY, newHPx, newWPx) {
const msg =
"RESIZED i=" +
i +
", X=" +
newX +
", Y=" +
newY +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
console.log(i, newX, newY, newHPx, newWPx);
},
containerResizedEvent(i, newH, newW, newHPx, newWPx) {
const msg =
"CONTAINER RESIZED i=" +
i +
", H=" +
newH +
", W=" +
newW +
", H(px)=" +
newHPx +
", W(px)=" +
newWPx;
console.log(msg);
console.log(i, newH, newW, newHPx, newWPx);
},
layoutCreatedEvent(newLayout) {
console.log("Created layout: ", newLayout);
......@@ -527,9 +543,10 @@ export default {
},
handleMessage(msg) {
if (msg.type !== "report" || !msg.imei) return null;
// 如果接收到的数据不是当前选中的设备,那么则直接丢弃
// 将设备发送过来的消息存储到浏览器中
// 这里可以优化,将所有数据,保存到indexed datebase中
if (this.device && msg.imei != this.device) return null;
if (monitor.db) monitor.set(msg);
if (!this.deviceList) {
this.deviceList = [];
......@@ -543,9 +560,13 @@ export default {
else this.device = msg.imei;
}
// 如果接收到的数据不是当前选中的设备,那么则直接丢弃
if (msg.imei != this.device) {
return null;
}
// 处理单位
this.processData(msg);
// this.devices[msg.imei] = msg;
this.globalData = msg;
this.resetData();
},
......@@ -569,9 +590,35 @@ export default {
},
onSelectChange(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.resetData();
console.log(res);
},
resetData() {
wsNotify.eventBus.$emit("resize");
......@@ -581,11 +628,12 @@ export default {
this.systemList = [
{
imei: this.globalData.imei,
...this.globalData.system,
...this.globalData.request,
...this.globalData.system
},
];
console.log(this.globalData)
// 这里需要特殊处理下,先判断uri是否存在,不存在则添加,存在则更新
let uris = [];
this.imageList.forEach((item) => {
......@@ -600,6 +648,8 @@ export default {
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);
if (target) {
target.length = item.length;
......@@ -608,8 +658,7 @@ export default {
if (
item.png_uncompressed_size &&
item.png_uncompressed_size !== target.png_uncompressed_size
)
{
) {
target.highlight = true;
target.png_uncompressed_size = item.png_uncompressed_size;
}
......@@ -618,7 +667,6 @@ export default {
item.png_file_size !== target.png_file_size
)
target.png_file_size = item.png_file_size;
} else {
this.imageList.push(item);
}
......@@ -633,7 +681,29 @@ export default {
},
},
mounted() {},
destroyed() {
// 页面关闭则销毁该数据库
// monitor.deleteDB()
},
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;
wsNotify.eventBus.$on("open", (message) => {
this.sendMsg();
......
......@@ -3,5 +3,5 @@
}
.el-table .success-row {
background: greenyellow;
background: #87e8de;
}
'''
Author: your name
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
Description: In User Settings Edit
FilePath: \evm-store\tools\build_out\tests\http_interval.py
'''
import json
import time
import random
import requests
from threading import Timer
from threading import Timer, Thread
def send_request():
def send_request(imei):
payload = {
"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},
"evm":{"heap_total_size":2097152,"heap_used_size":575072,"heap_map_size":8192,"stack_total_size":102400,"stack_used_size":1312},
"image":[
{"uri":"evue_launcher","length":13515,"png_total_count":0,"png_uncompressed_size":0,"png_file_size":0},
{"uri":"kdgs_1_startup","length":3666,"png_total_count":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_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}
"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},
"evm": {"heap_total_size": 2097152, "heap_used_size": 575072, "heap_map_size": 8192, "stack_total_size": 102400, "stack_used_size": 1312},
"image": [
{"uri": "evue_launcher", "length": 13515, "png_total_count": 0,
"png_uncompressed_size": 0, "png_file_size": 0},
{"uri": "kdgs_1_startup", "length": 3666, "png_total_count": 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_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"):
item.update({
'length': 0,
......@@ -49,13 +94,40 @@ def send_request():
'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.json())
t = Timer(3, send_request)
t.run()
time.sleep(3)
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__":
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
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
Description: In User Settings Edit
FilePath: \evm-store\backend\view\monitor.py
......@@ -154,6 +154,7 @@ class NotifyHandler(BaseWebsocket):
try:
className = self.__class__.__name__
message = json.loads(message)
logger.info(message)
# 判断消息类型
if message.get("type") and message.get("token"):
# 获取token值,检验正确与否,获取uuid
......@@ -189,6 +190,7 @@ class NotifyHandler(BaseWebsocket):
# self.close()
elif message.get("type") == "heartbeat": # 心跳包
# 收到心跳包消息,更新接收数据时间
logger.info("////////////////////////")
for c in self._clients:
if c.get("uuid") == payload.get("sub").get("uuid"):
c["ts"] = int(time.time())
......@@ -209,7 +211,8 @@ class NotifyHandler(BaseWebsocket):
def on_heartbeat(self):
# 心跳定时器,固定间隔扫描连接列表,当连接超时,主动剔除该连接
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)
del self._clients[i]
className = self.__class__.__name__
......
module.exports = {
"presets": [ [ "@vue/app", { useBuiltIns: "entry" } ] ],
"plugins": [
["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }]
]
}
presets: [["@vue/app", { useBuiltIns: "entry" }]],
plugins: [
[
"import",
{ libraryName: "ant-design-vue", libraryDirectory: "es", style: true },
],
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
],
};
......@@ -16,6 +16,7 @@
"core-js": "^3.9.0",
"cropperjs": "^1.5.11",
"echarts": "^5.1.2",
"element-ui": "^2.15.3",
"js-cookie": "^2.2.1",
"npm-check-updates": "^11.7.1",
"numeral": "^2.0.6",
......@@ -26,6 +27,7 @@
"vue-grid-layout": "^2.3.12",
"vue-i18n": "^8.1.0",
"vue-router": "^3.0.1",
"vue-treeselect": "^1.0.7",
"vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0"
},
......@@ -34,6 +36,7 @@
"@vue/cli-plugin-eslint": "^3.3.0",
"@vue/cli-service": "^3.3.0",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-import": "^1.9.1",
"eslint": "^7.30.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({
{
path: "/system/setting/menu",
name: "menu",
component: () => import("@/views/System/Menu"),
component: () => import("@/views/System/menu"),
},
{
path: "/system/setting/module",
path: "/system/setting/perssion",
name: "module",
component: () => import("@/views/System/Role"),
component: () => import("@/views/System/perssion"),
},
{
path: "/system/setting/config",
path: "/system/setting/data-permission",
name: "config",
component: () => import("@/views/System/Role"),
component: () => import("@/views/System/role"),
},
{
path: "/system/setting/dict",
name: "dict",
component: () => import("@/views/System/Role"),
component: () => import("@/views/System/dict"),
},
{
path: "/system/setting/area",
path: "/system/setting/location",
name: "area",
component: () => import("@/views/System/Role"),
component: () => import("@/views/System/location"),
},
{
path: "/system/setting/file-manager",
......@@ -158,14 +158,14 @@ const router = new Router({
],
},
{
path: "/system/role",
name: "role",
component: () => import("@/views/System/Role"),
path: "/system/trace-log",
name: "trace",
component: () => import("@/views/System/traceLog"),
},
{
path: "/system/admin",
name: "admin",
component: () => import("@/views/System/Role"),
path: "/system/user",
name: "user",
component: () => import("@/views/System/user"),
},
],
},
......@@ -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; // 大小
<template>
<a-page-header-wrapper title="查询表格">
<a-card :bordered="false">
<div class="tableList">
<div class="tableListForm">
<a-form v-show="!expandForm" layout="inline">
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="用户" v-decorator="['username']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="性别" v-decorator="['gender']">
<a-select placeholder="请选择" style="width: 100%">
<a-option value="male">male</a-option>
<a-option value="female">female</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<span class="submitButtons">
<a-button type="primary" htmlType="submit"> 查询 </a-button>
<a-button :style="{ marginLeft: '8px' }"> 重置 </a-button>
<a :style="{ marginLeft: '8px' }" @click="toggleForm">
展开 <a-icon type="down" />
</a>
</span>
</a-col>
</a-row>
</a-form>
<a-form v-show="expandForm" layout="inline">
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="用户" v-decorator="['username']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="性别" v-decorator="['gender']">
<a-select placeholder="请选择" style="width: 100%">
<a-option value="male">male</a-option>
<a-option value="female">female</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="姓名" v-decorator="['name']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="时间" v-decorator="['registered']">
<a-range-picker style="width: 100%" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="邮箱" v-decorator="['email']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="国籍" v-decorator="['nat']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<div style="overflow: hidden">
<div :style="{ float: 'right', marginBottom: '24px' }">
<a-button type="primary" htmlType="submit"> 查询 </a-button>
<a-button :style="{ marginLeft: '8px' }"> 重置 </a-button>
<a :style="{ marginLeft: '8px' }" @click="toggleForm">
收起 <a-icon type="up" />
</a>
</div>
</div>
</a-form>
</div>
<div class="tableListOperator">
<a-button icon="plus" type="primary"> 新建 </a-button>
<span v-show="selectedRowKeys.length > 0">
<a-button>批量操作</a-button>
<!-- <a-dropdown overlay={menu}>
<a-button>
更多操作 <icon type="down" />
</a-button>
</a-dropdown> -->
</span>
</div>
<!-- <StandardTable
selectedRows={selectedRows}
loading={loading}
data={data}
columns={this.columns}
onSelectRow={this.handleSelectRows}
onChange={this.handleStandardTableChange}
/> -->
<a-table
:columns="columns"
:rowKey="(record) => record.login.uuid"
:dataSource="users.data"
:pagination="users.pagination"
:loading="loading"
@change="handleTableChange"
>
<!-- :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}" -->
<template slot="login" slot-scope="login">
{{ login.username }}
</template>
<template slot="name" slot-scope="name">
{{ name.first }} {{ name.last }}
</template>
<template slot="registered" slot-scope="registered">
{{ registered.date }} ({{ registered.age }})
</template>
<template
slot="expandedRowRender"
slot-scope="record"
style="margin: 0"
>
<p :style="[sya, syb]">
<a-avatar
:src="record.picture.large"
shape="square"
:size="128"
/>
</p>
<p :style="[sya]">Personal</p>
<a-row>
<a-col :span="6">
<a-description-item
title="Name"
:content="record.name.first + ' ' + record.name.last"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="Account"
:content="record.login.username"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="City"
:content="record.location.city"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="Postcode"
:content="record.location.postcode"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="6">
<a-description-item title="Country" :content="record.nat" />
</a-col>
<a-col :span="6">
<a-description-item
title="Birthday"
:content="record.dob.date + ' (' + record.dob.age + ')'"
/>
</a-col>
<a-col :span="12">
<a-description-item
title="Timezone"
:content="record.location.timezone.description"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="12"> </a-col>
<a-col :span="12"> </a-col>
</a-row>
<a-divider />
<p :style="[sya]">Contacts</p>
<a-row>
<a-col :span="6">
<a-description-item title="Email" :content="record.email" />
</a-col>
<a-col :span="6">
<a-description-item title="Cell" :content="record.cell" />
</a-col>
<a-col :span="6">
<a-description-item title="Phone" :content="record.phone" />
</a-col>
<a-col :span="6">
<a-description-item
title="Coordinates"
:content="
record.location.coordinates.latitude +
' ' +
record.location.coordinates.longitude
"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-description-item
title="Registered"
:content="
record.registered.date + ' (' + record.registered.age + ')'
"
/>
</a-col>
</a-row>
</template>
<template slot="action" slot-scope="text, record">
<a href="javascript:;">查看</a>
<a-divider type="vertical" />
<a href="javascript:;">配置</a>
</template>
</a-table>
</div>
</a-card>
</a-page-header-wrapper>
</template>
<script>
import {
Avatar,
Row,
Col,
Card,
List,
Button,
Form,
Icon,
Table,
Divider,
Dropdown,
Input,
Select,
DatePicker,
} from "ant-design-vue";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
import DescriptionItem from "@/components/DescriptionItem";
const columns = [
{
title: "用户名",
dataIndex: "login",
sorter: true,
width: "12%",
scopedSlots: { customRender: "login" },
},
{
title: "姓名",
dataIndex: "name",
sorter: true,
width: "15%",
scopedSlots: { customRender: "name" },
},
{
title: "性别",
dataIndex: "gender",
filters: [
{ text: "Male", value: "male" },
{ text: "Female", value: "female" },
],
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "国籍",
dataIndex: "nat",
},
{
title: "Registered",
dataIndex: "registered",
scopedSlots: { customRender: "registered" },
},
{
title: "Action",
key: "action",
scopedSlots: { customRender: "action" },
},
];
import { mapGetters } from "vuex";
export default {
data: () => ({
expandForm: false,
selectedRowKeys: [],
columns,
sya: {
fontSize: "16px",
color: "rgba(0,0,0,0.85)",
lineHeight: "24px",
display: "block",
marginBottom: "16px",
},
syb: {
marginBottom: "24px",
},
}),
async asyncData({ store, route }, config = { results: 15 }) {
await store.dispatch("frontend/openapi/getUsers", {
...config,
path: route.path,
});
},
computed: {
...mapGetters({
loading: "frontend/openapi/loading",
users: "frontend/openapi/getUsers",
}),
},
components: {
APageHeaderWrapper: PageHeaderWrapper,
AAvatar: Avatar,
ARow: Row,
ACol: Col,
ACard: Card,
ACardGrid: Card.Grid,
ACardMeta: Card.Meta,
AList: List,
AButton: Button,
AForm: Form,
AFormItem: Form.Item,
AIcon: Icon,
ATable: Table,
ADescriptionItem: DescriptionItem,
ADivider: Divider,
ADropdown: Dropdown,
AInput: Input,
ASelect: Select,
AOption: Select.Option,
ARangePicker: DatePicker.RangePicker,
},
methods: {
toggleForm() {
this.expandForm = !this.expandForm;
},
onSelectChange(selectedRowKeys) {
window.console.log("selectedRowKeys changed: ", selectedRowKeys);
this.selectedRowKeys = selectedRowKeys;
},
handleTableChange(pagination, filters, sorter) {
const pager = { ...this.users.pagination };
pager.current = pagination.current;
this.users.pagination = pager;
this.$options.asyncData(
{ store: this.$store, route: this.$route },
{
results: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
}
);
},
},
mounted() {
this.$options.asyncData(
{ store: this.$store, route: this.$route },
{ results: 15 }
);
},
};
</script>
<style lang="less">
@import url("styles/tableList.less");
</style>
\ No newline at end of file
<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 {
flex-direction: row;
& > .grid-node {
flex: 1;
& > h3 {
font-weight: bold;
font-size: 17px;
}
& > h3, p {
display: flex;
justify-content: center;
......
<template>
<a-page-header-wrapper
:loading="false"
:tabList="tabList"
tabActiveKey="articles"
:tabChange="tabChange"
<TableLayout class="menu-layout" :permissions="['system:menu:query']">
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:menu:create', 'system:menu:delete', 'system:menu:sort']">
<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
</a-page-header-wrapper>
<el-table-column type="selection" width="55" fixed="left"></el-table-column>
<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>
<script>
import { Avatar, Row, Col, Card, List } from "ant-design-vue";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
import { Table, TableColumn, Button } from "element-ui"
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 {
data: () => ({
activitiesLoading: true,
projectLoading: false,
tabList: [
{
key: "articles",
tab: "菜单列表",
},
{
key: "application",
tab: "应用列表",
},
],
}),
name: 'SystemMenu',
extends: BaseTable,
components: {
APageHeaderWrapper: PageHeaderWrapper,
AAvatar: Avatar,
ARow: Row,
ACol: Col,
ACard: Card,
ACardGrid: Card.Grid,
ACardMeta: Card.Meta,
AList: List,
"el-table": Table,
"el-table-column": TableColumn,
"el-button": Button,
OperaMenuWindow,
TableLayout
},
data () {
return {
// 是否正在处理中
isWorking: {
sort: false
}
}
},
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>
<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>
<template>
<a-page-header-wrapper title="查询表格">
<a-card :bordered="false">
<div class="tableList">
<div class="tableListForm">
<a-form v-show="!expandForm" layout="inline">
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="用户" v-decorator="['username']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="性别" v-decorator="['gender']">
<a-select placeholder="请选择" style="width: 100%">
<a-option value="male">male</a-option>
<a-option value="female">female</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<span class="submitButtons">
<a-button type="primary" htmlType="submit"> 查询 </a-button>
<a-button :style="{ marginLeft: '8px' }"> 重置 </a-button>
<a :style="{ marginLeft: '8px' }" @click="toggleForm">
展开 <a-icon type="down" />
</a>
</span>
</a-col>
</a-row>
</a-form>
<a-form v-show="expandForm" layout="inline">
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="用户" v-decorator="['username']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="性别" v-decorator="['gender']">
<a-select placeholder="请选择" style="width: 100%">
<a-option value="male">male</a-option>
<a-option value="female">female</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="姓名" v-decorator="['name']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="{ md: 8, lg: 24, xl: 48 }">
<a-col :md="8" :sm="24">
<a-form-item label="时间" v-decorator="['registered']">
<a-range-picker style="width: 100%" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="邮箱" v-decorator="['email']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="国籍" v-decorator="['nat']">
<a-input placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<div style="overflow: hidden">
<div :style="{ float: 'right', marginBottom: '24px' }">
<a-button type="primary" htmlType="submit"> 查询 </a-button>
<a-button :style="{ marginLeft: '8px' }"> 重置 </a-button>
<a :style="{ marginLeft: '8px' }" @click="toggleForm">
收起 <a-icon type="up" />
</a>
</div>
</div>
</a-form>
</div>
<div class="tableListOperator">
<a-button icon="plus" type="primary"> 新建 </a-button>
<span v-show="selectedRowKeys.length > 0">
<a-button>批量操作</a-button>
<!-- <a-dropdown overlay={menu}>
<a-button>
更多操作 <icon type="down" />
</a-button>
</a-dropdown> -->
</span>
</div>
<!-- <StandardTable
selectedRows={selectedRows}
loading={loading}
data={data}
columns={this.columns}
onSelectRow={this.handleSelectRows}
onChange={this.handleStandardTableChange}
/> -->
<a-table
:columns="columns"
:rowKey="(record) => record.login.uuid"
:dataSource="users.data"
:pagination="users.pagination"
:loading="loading"
@change="handleTableChange"
<TableLayout :permissions="['system:role:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="角色编码" prop="code">
<el-input v-model="searchForm.code" v-trim placeholder="请输入角色编码" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="角色名称" prop="name">
<el-input v-model="searchForm.name" v-trim placeholder="请输入角色名称" @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>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:role:create', 'system:role:delete']">
<li v-permissions="['system:role:create']"><el-button type="primary" @click="$refs.operaRoleWindow.open('新建角色')" icon="el-icon-plus">新建</el-button></li>
<li v-permissions="['system:role:delete']"><el-button @click="deleteByIdInBatch" icon="el-icon-delete">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:default-sort = "{prop: 'createTime', order: 'descending'}"
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<!-- :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}" -->
<template slot="login" slot-scope="login">
{{ login.username }}
</template>
<template slot="name" slot-scope="name">
{{ name.first }} {{ name.last }}
</template>
<template slot="registered" slot-scope="registered">
{{ registered.date }} ({{ registered.age }})
</template>
<template
slot="expandedRowRender"
slot-scope="record"
style="margin: 0"
<el-table-column type="selection" fixed="left" width="55"></el-table-column>
<el-table-column prop="code" label="角色编码" fixed="left" min-width="100px"></el-table-column>
<el-table-column prop="name" label="角色名称" fixed="left" min-width="100px"></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" sortable="custom" sort-by="role.CREATE_TIME"></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
v-if="containPermissions(['system:role:update', 'system:role:createRolePermission', 'system:role:createRoleMenu', 'system:role:delete'])"
label="操作"
min-width="270"
fixed="right"
>
<p :style="[sya, syb]">
<a-avatar
:src="record.picture.large"
shape="square"
:size="128"
/>
</p>
<p :style="[sya]">Personal</p>
<a-row>
<a-col :span="6">
<a-description-item
title="Name"
:content="record.name.first + ' ' + record.name.last"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="Account"
:content="record.login.username"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="City"
:content="record.location.city"
/>
</a-col>
<a-col :span="6">
<a-description-item
title="Postcode"
:content="record.location.postcode"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="6">
<a-description-item title="Country" :content="record.nat" />
</a-col>
<a-col :span="6">
<a-description-item
title="Birthday"
:content="record.dob.date + ' (' + record.dob.age + ')'"
/>
</a-col>
<a-col :span="12">
<a-description-item
title="Timezone"
:content="record.location.timezone.description"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="12"> </a-col>
<a-col :span="12"> </a-col>
</a-row>
<a-divider />
<p :style="[sya]">Contacts</p>
<a-row>
<a-col :span="6">
<a-description-item title="Email" :content="record.email" />
</a-col>
<a-col :span="6">
<a-description-item title="Cell" :content="record.cell" />
</a-col>
<a-col :span="6">
<a-description-item title="Phone" :content="record.phone" />
</a-col>
<a-col :span="6">
<a-description-item
title="Coordinates"
:content="
record.location.coordinates.latitude +
' ' +
record.location.coordinates.longitude
"
/>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-description-item
title="Registered"
:content="
record.registered.date + ' (' + record.registered.age + ')'
"
/>
</a-col>
</a-row>
<template v-if="isAdmin || (row.code !== adminCode && userInfo.roles.findIndex(code => code === row.code) === -1)" slot-scope="{row}">
<el-button type="text" @click="$refs.operaRoleWindow.open('编辑角色', row)" icon="el-icon-edit" v-permissions="['system:role:update']">编辑</el-button>
<el-button type="text" @click="$refs.permissionConfigWindow.open(row)" v-permissions="['system:role:createRolePermission']">配置权限</el-button>
<el-button type="text" @click="$refs.menuConfigWindow.open(row)" icon="el-icon-menu" v-permissions="['system:role:createRoleMenu']">授权菜单</el-button>
<el-button v-if="!row.fixed" type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:role:delete']">删除</el-button>
</template>
<template slot="action" slot-scope="text, record">
<a href="javascript:;">查看</a>
<a-divider type="vertical" />
<a href="javascript:;">配置</a>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
</a-table>
</div>
</a-card>
</a-page-header-wrapper>
<!-- 新建/修改 -->
<OperaRoleWindow ref="operaRoleWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 配置权限 -->
<PermissionConfigWindow ref="permissionConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 授权菜单 -->
<MenuConfigWindow ref="menuConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
</TableLayout>
</template>
<script>
import {
Avatar,
Row,
Col,
Card,
List,
Button,
Form,
Icon,
Table,
Divider,
Dropdown,
Input,
Select,
DatePicker,
} from "ant-design-vue";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
import DescriptionItem from "@/components/DescriptionItem";
const columns = [
{
title: "用户名",
dataIndex: "login",
sorter: true,
width: "12%",
scopedSlots: { customRender: "login" },
},
{
title: "姓名",
dataIndex: "name",
sorter: true,
width: "15%",
scopedSlots: { customRender: "name" },
},
{
title: "性别",
dataIndex: "gender",
filters: [
{ text: "Male", value: "male" },
{ text: "Female", value: "female" },
],
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "国籍",
dataIndex: "nat",
},
{
title: "Registered",
dataIndex: "registered",
scopedSlots: { customRender: "registered" },
},
{
title: "Action",
key: "action",
scopedSlots: { customRender: "action" },
},
];
import { mapGetters } from "vuex";
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaRoleWindow from '@/components/system/role/OperaRoleWindow'
import PermissionConfigWindow from '@/components/system/role/PermissionConfigWindow'
import MenuConfigWindow from '@/components/system/role/MenuConfigWindow'
export default {
data: () => ({
expandForm: false,
selectedRowKeys: [],
columns,
sya: {
fontSize: "16px",
color: "rgba(0,0,0,0.85)",
lineHeight: "24px",
display: "block",
marginBottom: "16px",
},
syb: {
marginBottom: "24px",
},
}),
async asyncData({ store, route }, config = { results: 15 }) {
await store.dispatch("frontend/openapi/getUsers", {
...config,
path: route.path,
});
},
computed: {
...mapGetters({
loading: "frontend/openapi/loading",
users: "frontend/openapi/getUsers",
}),
},
components: {
APageHeaderWrapper: PageHeaderWrapper,
AAvatar: Avatar,
ARow: Row,
ACol: Col,
ACard: Card,
ACardGrid: Card.Grid,
ACardMeta: Card.Meta,
AList: List,
AButton: Button,
AForm: Form,
AFormItem: Form.Item,
AIcon: Icon,
ATable: Table,
ADescriptionItem: DescriptionItem,
ADivider: Divider,
ADropdown: Dropdown,
AInput: Input,
ASelect: Select,
AOption: Select.Option,
ARangePicker: DatePicker.RangePicker,
},
methods: {
toggleForm() {
this.expandForm = !this.expandForm;
},
onSelectChange(selectedRowKeys) {
window.console.log("selectedRowKeys changed: ", selectedRowKeys);
this.selectedRowKeys = selectedRowKeys;
},
handleTableChange(pagination, filters, sorter) {
const pager = { ...this.users.pagination };
pager.current = pagination.current;
this.users.pagination = pager;
this.$options.asyncData(
{ store: this.$store, route: this.$route },
{
results: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
name: 'SystemRole',
extends: BaseTable,
components: { MenuConfigWindow, PermissionConfigWindow, OperaRoleWindow, TableLayout, Pagination },
data () {
return {
// 搜索
searchForm: {
code: '',
name: '',
remark: ''
}
}
);
},
},
mounted() {
this.$options.asyncData(
{ store: this.$store, route: this.$route },
{ results: 15 }
);
},
};
created () {
this.config({
module: '角色',
api: '/system/role',
sorts: [{
property: 'role.CREATE_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<style lang="less">
@import url("styles/tableList.less");
</style>
\ No newline at end of file
<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>
<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="parentId">
<DepartmentSelect v-if="visible" ref="departmentSelect" placeholder="请选择上级部门" v-model="form.parentId" :exclude-id="excludeDeptId" :inline="false"/>
</el-form-item>
<el-form-item label="部门编码" prop="code" required>
<el-input v-model="form.code" placeholder="请输入部门编码" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="部门名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入部门名称" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系电话" v-trim maxlength="11"/>
</el-form-item>
<el-form-item label="部门邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入部门邮箱" v-trim maxlength="200"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import { checkMobile, checkEmail } from '@/utils/form'
export default {
name: 'OperaDepartmentWindow',
extends: BaseOpera,
components: { DepartmentSelect, GlobalWindow },
data () {
return {
// 需排除选择的部门ID
excludeDeptId: null,
// 表单数据
form: {
id: null,
parentId: null,
code: '',
name: '',
phone: '',
email: ''
},
// 验证规则
rules: {
code: [
{ required: true, message: '请输入部门编码' }
],
name: [
{ required: true, message: '请输入部门名称' }
],
phone: [
{ validator: checkMobile }
],
email: [
{ validator: checkEmail }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @target 编辑的部门对象
* @parent 新建时的上级部门对象
* @departmentList 部门列表
*/
open (title, target, parent) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.excludeDeptId = null
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.parentId = parent == null ? null : parent.id
})
return
}
// 编辑
this.$nextTick(() => {
this.excludeDeptId = target.id
for (const key in this.form) {
this.form[key] = target[key]
}
})
}
},
created () {
this.config({
api: '/system/department'
})
}
}
</script>
<template>
<GlobalWindow
:title="dictName + '数据管理'"
width="78%"
:visible.sync="visible"
:with-footer="false"
>
<TableLayout :with-breadcrumb="false">
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar">
<li><el-button type="primary" @click="$refs.operaDictDataWindow.open('新建字典数据', searchForm.dictId)" icon="el-icon-plus">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="label" label="数据标签" min-width="100px"></el-table-column>
<el-table-column prop="code" label="数据值" min-width="100px"></el-table-column>
<el-table-column prop="disabled" label="状态" min-width="100px">
<template slot-scope="{row}">{{row.disabled | disabledText}}</template>
</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="updateUser" label="更新人" min-width="100px">
<template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="100px"></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="100px"></el-table-column>
<el-table-column
label="操作"
min-width="120"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaDictDataWindow.open('编辑字典数据', dictId, row)" icon="el-icon-edit">编辑</el-button>
<el-button type="text" @click="deleteById(row)" icon="el-icon-delete">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
<!-- 新建/修改 -->
<OperaDictDataWindow ref="operaDictDataWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
</TableLayout>
</GlobalWindow>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import Pagination from '@/components/common/Pagination'
import GlobalWindow from '@/components/common/GlobalWindow'
import TableLayout from '@/layouts/TableLayout'
import OperaDictDataWindow from './OperaDictDataWindow'
export default {
name: 'DictDataManagerWindow',
extends: BaseTable,
components: { OperaDictDataWindow, TableLayout, GlobalWindow, Pagination },
data () {
return {
visible: false,
searchForm: {
// 字典ID
dictId: null
},
// 字典名称
dictName: ''
}
},
methods: {
// 打开数据管理
open (dictId, dictName) {
this.searchForm.dictId = dictId
this.dictName = dictName
this.visible = true
this.search()
}
},
created () {
this.config({
api: '/system/dictData',
'field.main': 'label'
})
}
}
</script>
<style scoped lang="scss">
/deep/ .window__body {
.table-content {
padding: 0;
.table-wrap {
padding-top: 0;
}
}
}
</style>
<template>
<GlobalWindow
:title="title"
:visible.sync="visible"
:confirm-working="isWorking.create"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="数据标签" prop="label" required>
<el-input v-model="form.label" placeholder="请输入数据标签" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="数据值" prop="code" required>
<el-input v-model="form.code" placeholder="请输入数据值" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="状态" prop="disabled" required class="form-item-status">
<el-switch v-model="form.disabled" :active-value="false" :inactive-value="true"/>
<span class="status-text">{{form.disabled | disabledText}}</span>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaDictDataWindow',
extends: BaseOpera,
components: { GlobalWindow },
data () {
return {
// 表单数据
form: {
id: null,
dictId: null,
code: '',
label: '',
disabled: false
},
// 验证规则
rules: {
label: [
{ required: true, message: '请输入数据标签' }
],
code: [
{ required: true, message: '请输入数据值' }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @dict 所属字典ID
* @target 编辑的字典数据对象
*/
open (title, dictId, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.dictId = dictId
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
}
},
created () {
this.config({
api: '/system/dictData'
})
}
}
</script>
<style scoped lang="scss">
.form-item-status {
.status-text {
color: #999;
margin-left: 6px;
font-size: 13px;
vertical-align: middle;
}
}
</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="code" required>
<el-input v-model="form.code" placeholder="请输入字典编码" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="字典名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入字典名称" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" v-trim maxlength="500"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaDictWindow',
extends: BaseOpera,
components: { GlobalWindow },
data () {
return {
// 表单数据
form: {
id: null,
code: '',
name: '',
remark: ''
},
// 验证规则
rules: {
code: [
{ required: true, message: '请输入字典编码' }
],
name: [
{ required: true, message: '请输入字典名称' }
]
}
}
},
created () {
this.config({
api: '/system/dict'
})
}
}
</script>
<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="name" required>
<el-input v-model="form.name" maxlength="50" placeholder="请输入地区名称" v-trim/>
</el-form-item>
<el-form-item label="地区简称" prop="shortName" required>
<el-input v-model="form.shortName" maxlength="50" placeholder="请输入地区简称" v-trim/>
</el-form-item>
<el-form-item label="地区全称" prop="fullName" required>
<el-input v-model="form.fullName" maxlength="100" placeholder="请输入地区全称" v-trim/>
</el-form-item>
<el-form-item label="地区拼音" prop="pinyin" required>
<el-input v-model="form.pinyin" maxlength="100" placeholder="请输入地区名称拼音" v-trim/>
</el-form-item>
<el-form-item label="区号" prop="areaCode">
<el-input v-model="form.areaCode" maxlength="20" placeholder="请输入地区区号" v-trim/>
</el-form-item>
<el-form-item label="邮编" prop="postalCode">
<el-input v-model="form.postalCode" maxlength="20" placeholder="请输入地区邮编" v-trim/>
</el-form-item>
<el-form-item label="首字母" prop="firstLetter" required>
<el-input v-model="form.firstLetter" maxlength="1" placeholder="请输入地区名称首字母" v-trim/>
</el-form-item>
<el-form-item label="经度" prop="lng">
<el-input v-model="form.lng" maxlength="50" placeholder="请输入地区经度" v-trim/>
</el-form-item>
<el-form-item label="纬度" prop="lat">
<el-input v-model="form.lat" maxlength="50" placeholder="请输入地区纬度" v-trim/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaLocationWindow',
extends: BaseOpera,
components: { GlobalWindow },
data () {
return {
// 表单数据
form: {
id: null,
parentId: '',
shortName: '',
name: '',
fullName: '',
level: '',
pinyin: '',
areaCode: '',
postalCode: '',
firstLetter: '',
lng: '',
lat: ''
},
// 验证规则
rules: {
name: [
{ required: true, message: '请输入地区名称' }
],
shortName: [
{ required: true, message: '请输入地区简称' }
],
fullName: [
{ required: true, message: '请输入地区全称' }
],
pinyin: [
{ required: true, message: '请输入地区名称拼音' }
],
firstLetter: [
{ required: true, message: '请输入地区名称首字母' }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @parent 父区域ID
* @target 编辑的区域对象
*/
open (title, target, parent) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.parentId = parent == null ? null : parent.id
this.form.level = parent == null ? 1 : parent.level + 1
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
}
},
created () {
this.config({
api: '/system/location',
'field.id': 'id'
})
}
}
</script>
<template>
<GlobalWindow
class="handle-table-dialog"
:title="title"
:visible.sync="visible"
:confirm-working="isWorking"
@confirm="confirm"
>
<p class="tip" v-if="form.parent != null && form.id == null"><em>{{parentName}}</em> 新建子菜单</p>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="上级菜单" prop="parentId">
<MenuSelect v-if="visible" v-model="form.parentId" placeholder="请选择上级菜单" :exclude-id="excludeMenuId" clearable :inline="false"/>
</el-form-item>
<el-form-item label="菜单名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入菜单名称" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="访问路径" prop="path">
<el-input v-model="form.path" placeholder="请输入访问路径" v-trim maxlength="200"/>
</el-form-item>
<el-form-item label="图标" prop="icon" class="form-item-icon">
<el-radio-group v-model="form.icon">
<el-radio :label="icon" v-for="icon in icons" :key="icon">
<i :class="{[icon]: true}"></i>
</el-radio>
</el-radio-group>
</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 { Form, FormItem, Radio, RadioGroup, Input } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
import BaseOpera from '@/src/views/System/components/base/BaseOpera'
import GlobalWindow from '@/src/views/System/components/common/GlobalWindow'
import MenuSelect from '@/src/views/System/components/common/MenuSelect'
import icons from '@/utils/icons'
export default {
name: 'OperaMenuWindow',
extends: BaseOpera,
components: {
"el-form": Form,
"el-form-item": FormItem,
"el-radio": Radio,
"el-radio-group": RadioGroup,
"el-input": Input,
MenuSelect,
GlobalWindow
},
data () {
return {
icons,
// 上级菜单名称
parentName: '',
// 需排除选择的菜单ID
excludeMenuId: null,
// 表单数据
form: {
id: null,
parentId: null,
name: '',
path: '',
icon: '',
remark: ''
},
// 验证规则
rules: {
name: [
{ required: true, message: '请输入菜单名称' }
]
}
}
},
methods: {
/**
* @title: 窗口标题
* @target: 编辑的菜单对象
* @parent: 新建时的上级菜单
*/
open (title, target, parent) {
this.title = title
this.visible = true
// 新建,menu为空时表示新建菜单
if (target == null) {
this.excludeMenuId = null
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.parentId = parent == null ? null : parent.id
this.parentName = parent == null ? null : parent.name
})
return
}
// 编辑
this.$nextTick(() => {
this.excludeMenuId = target.id
for (const key in this.form) {
this.form[key] = target[key]
}
})
}
},
created () {
this.config({
api: '/system/menu'
})
}
}
</script>
<style scoped lang="scss">
@import "@/styles/variables";
$icon-background-color: $primary-color;
.global-window {
.tip {
margin-bottom: 12px;
em {
font-style: normal;
color: $primary-color;
font-weight: bold;
}
}
// 图标
/deep/ .form-item-icon {
.el-form-item__content {
height: 193px;
overflow-y: auto;
}
.el-radio-group {
background: $icon-background-color;
padding: 4px;
.el-radio {
margin-right: 0;
color: #fff;
padding: 6px;
&.is-checked {
background: $icon-background-color - 30;
border-radius: 10px;
}
.el-radio__input.is-checked + .el-radio__label {
color: #fff;
}
}
.el-radio__input {
display: none;
}
.el-radio__label {
padding-left: 0;
// element-ui图标
i {
font-size: 30px;
}
// 自定义图标
[class^="eva-icon-"], [class*=" eva-icon-"] {
width: 30px;
height: 30px;
background-size: 25px;
vertical-align: bottom;
}
}
.el-radio--small {
height: auto;
padding: 8px;
margin-left: 0;
}
}
}
}
</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="code" required>
<el-input v-model="form.code" placeholder="请输入权限编码" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="权限名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入权限名称" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="权限备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入权限备注" type="textarea" :rows="3" v-trim maxlength="500"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaPermissionWindow',
extends: BaseOpera,
components: { GlobalWindow },
data () {
return {
// 原权限码
originPermissionCode: '',
// 表单数据
form: {
id: null,
code: '',
name: '',
remark: ''
},
// 验证规则
rules: {
code: [
{ required: true, message: '请输入权限编码' }
],
name: [
{ required: true, message: '请输入权限名称' }
]
}
}
},
methods: {
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(() => {
this.originPermissionCode = target.code
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
confirm () {
if (this.form.id == null || this.form.id === '') {
this.__confirmCreate()
return
}
if (this.originPermissionCode === this.form.code) {
this.__confirmEdit()
return
}
// 修改了权限编码
this.$dialog.confirm('检测到您修改了权限编码,权限编码修改后前后端均可能需要调整代码,确认修改吗?', '提示', {
confirmButtonText: '确认修改',
type: 'warning'
})
.then(() => {
this.__confirmEdit()
})
}
},
created () {
this.config({
api: '/system/permission'
})
}
}
</script>
<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="parentId">
<PositionSelect v-if="visible" v-model="form.parentId" placeholder="请选择上级岗位" :exclude-id="excludePositionId" clearable :inline="false"/>
</el-form-item>
<el-form-item label="岗位编码" prop="code" required>
<el-input v-model="form.code" placeholder="请输入岗位编码" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="岗位名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入岗位名称" v-trim maxlength="50"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import PositionSelect from '@/components/common/PositionSelect'
export default {
name: 'OperaPositionWindow',
extends: BaseOpera,
components: { PositionSelect, GlobalWindow },
data () {
return {
// 需排除选择的岗位ID
excludePositionId: null,
// 表单数据
form: {
id: null,
parentId: null,
code: '',
name: ''
},
// 验证规则
rules: {
code: [
{ required: true, message: '请输入岗位编码' }
],
name: [
{ required: true, message: '请输入岗位名称' }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @target 编辑的岗位对象
* @parent 新建时的上级岗位对象
* @positionList 岗位列表
*/
open (title, target, parent) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.excludePositionId = null
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.parentId = parent == null ? null : parent.id
})
return
}
// 编辑
this.$nextTick(() => {
this.excludePositionId = target.id
for (const key in this.form) {
this.form[key] = target[key]
}
})
}
},
created () {
this.config({
api: '/system/position'
})
}
}
</script>
<template>
<GlobalWindow
class="position-user-window"
width="80%"
:title="positionName + '人员列表'"
: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" v-trim placeholder="请输入用户名" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="姓名" prop="realname">
<el-input v-model="searchForm.realname" v-trim placeholder="请输入姓名" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="searchForm.mobile" v-trim placeholder="请输入手机号码" @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>
<template v-slot:table-wrap>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<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'
export default {
name: 'PositionUserWindow',
extends: BaseTable,
components: { Pagination, GlobalWindow, TableLayout },
data () {
return {
positionId: null,
positionName: '',
visible: false,
// 搜索表单
searchForm: {
positionId: null,
username: '',
realname: '',
mobile: ''
}
}
},
methods: {
// 打开查看人员窗口
open (positionId, positionName) {
this.positionId = positionId
this.positionName = positionName
this.searchForm.positionId = positionId
this.visible = true
this.search()
}
},
created () {
this.config({
api: '/system/user'
})
}
}
</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;
}
}
}
// 列表头像处理
.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>
<template>
<GlobalWindow
class="menu-config-dialog"
:visible.sync="visible"
:confirm-working="isWorking"
width="576px"
title="授权菜单"
@confirm="confirm"
>
<p class="tip" v-if="role != null">为角色 <em>{{role.name}}</em> 配置可访问的菜单</p>
<el-tree
ref="menuTree"
:data="menus"
show-checkbox
node-key="id"
default-expand-all
:default-checked-keys="selectedIds"
:expand-on-click-node="false"
:check-on-click-node="true"
:props="{children: 'children',label: 'name'}">
</el-tree>
</GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { createRoleMenu } from '@/api/system/role'
import { fetchTree as fetchMenuList } from '@/api/system/menu'
export default {
name: 'MenuConfigWindow',
components: { GlobalWindow },
data () {
return {
visible: false,
isWorking: false,
// 角色
role: null,
// 所有菜单
menus: [],
// 已选中的菜单
selectedIds: []
}
},
methods: {
/**
* @role 角色对象
*/
open (role) {
fetchMenuList({})
.then(records => {
this.role = role
this.menus = records
// 如果为固定角色,则固定菜单不可更改
this.__resetDisabled(this.menus, this.role)
// 找出叶节点
role.menus = role.menus.filter(menu => role.menus.findIndex(m => m.parentId === menu.id) === -1)
// 初始化选中
this.selectedIds = role.menus.map(r => r.id)
this.visible = true
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 确认选择菜单
confirm () {
const selectedMenus = this.$refs.menuTree.getCheckedNodes(false, true)
this.isWorking = true
createRoleMenu({
roleId: this.role.id,
menuIds: selectedMenus.map(menu => menu.id)
})
.then(() => {
this.$tip.apiSuccess('菜单授权成功')
this.visible = false
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
},
// 重置disabled
__resetDisabled (menus, role) {
if (menus == null || menus.length === 0) {
return
}
for (const menu of menus) {
menu.disabled = false
if (role.fixed && menu.fixed) {
menu.disabled = true
}
this.__resetDisabled(menu.children, role)
}
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.global-window {
.tip {
margin-bottom: 12px;
em {
font-style: normal;
color: $primary-color;
font-weight: bold;
}
}
}
</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="code" required>
<el-input v-model="form.code" placeholder="请输入角色编码" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="角色名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入角色名称" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="角色备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入角色备注" :rows="3" v-trim maxlength="500"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaRoleWindow',
extends: BaseOpera,
components: { GlobalWindow },
data () {
return {
// 原角色码
originRoleCode: '',
// 表单数据
form: {
id: null,
code: '',
name: '',
remark: ''
},
// 验证规则
rules: {
code: [
{ required: true, message: '请输入角色编码' }
],
name: [
{ required: true, message: '请输入角色名称' }
]
}
}
},
methods: {
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(() => {
this.originRoleCode = target.code
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
confirm () {
if (this.form.id == null || this.form.id === '') {
this.__confirmCreate()
return
}
if (this.originRoleCode === this.form.code) {
this.__confirmEdit()
return
}
// 修改了角色编码
this.$dialog.confirm('检测到您修改了角色编码,角色编码修改后前后端均可能需要调整代码,确认修改吗?', '提示', {
confirmButtonText: '确认修改',
type: 'warning'
})
.then(() => {
this.__confirmEdit()
})
}
},
created () {
this.config({
api: '/system/role'
})
}
}
</script>
<template>
<GlobalWindow
:visible.sync="visible"
:confirm-working="isWorking"
width="582px"
title="配置角色权限"
@confirm="confirm"
>
<p class="tip" v-if="role != null">为角色 <em>{{role.name}}</em> 配置权限</p>
<p class="tip-warn"><i class="el-icon-warning"></i>提醒:权限配置后需重新登录后生效</p>
<el-transfer
ref="permissionTransfer"
v-model="selectedIds"
filterable
:filter-method="filterPermissions"
:titles="['未授权权限', '已授权权限']"
:props="{
key: 'id',
label: 'name'
}"
:data="permissions">
</el-transfer>
</GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { createRolePermission } from '@/api/system/role'
import { fetchAll } from '@/api/system/permission'
export default {
name: 'PermissionConfigWindow',
components: { GlobalWindow },
data () {
return {
visible: false,
isWorking: false,
// 角色对象
role: null,
// 权限列表
permissions: [],
// 已选中的权限ID
selectedIds: []
}
},
methods: {
/**
* @role 角色对象
*/
open (role) {
if (this.$refs.permissionTransfer) {
this.$refs.permissionTransfer.clearQuery('left')
this.$refs.permissionTransfer.clearQuery('right')
}
fetchAll()
.then(records => {
this.role = role
this.permissions = records
// 如果为固定角色,则固定权限不可更改
if (this.role.fixed) {
for (const perm of this.permissions) {
if (perm.fixed) {
perm.disabled = true
}
}
}
this.selectedIds = role.permissions.map(r => r.id)
this.visible = true
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 确认选择权限
confirm () {
this.isWorking = true
createRolePermission({
roleId: this.role.id,
permissionIds: this.selectedIds
})
.then(() => {
this.$tip.apiSuccess('权限配置成功,用户重新登录后生效')
this.visible = false
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
},
// 搜索权限
filterPermissions (query, item) {
const lowerCaseQuery = query.toLowerCase()
return item.code.toLowerCase().indexOf(lowerCaseQuery) > -1 || item.name.toLowerCase().indexOf(lowerCaseQuery) > -1
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.global-window {
.tip {
em {
font-style: normal;
color: $primary-color;
font-weight: bold;
}
}
.tip-warn {
margin: 4px 0 12px 0;
font-size: 12px;
color: #999;
i {
color: orange;
margin-right: 4px;
font-size: 14px;
position: relative;
top: 1px;
}
}
}
</style>
<template>
<el-select
class="role-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="role in roles" :key="role.id" :value="role.id" :label="role.name"/>
</el-select>
</template>
<script>
import { fetchAll } from '@/api/system/role'
export default {
name: 'RoleSelect',
props: {
value: {},
placeholder: {
default: '请选择角色'
},
inline: {
default: true
},
disabled: {},
clearable: {
default: false
}
},
data () {
return {
roles: []
}
},
created () {
fetchAll()
.then(data => {
this.roles = 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="username" required>
<el-input v-model="form.username" placeholder="请输入用户名" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="姓名" prop="realname" required>
<el-input v-model="form.realname" placeholder="请输入姓名" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="性别" prop="sex" required>
<el-radio-group v-model="form.sex">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar" required>
<el-radio-group v-model="form.avatar" class="form-item-avatar">
<el-radio label="/avatar/man.png" border><img src="/avatar/man.png" alt=""></el-radio>
<el-radio label="/avatar/woman.png" border><img src="/avatar/woman.png" alt=""></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.id == null" label="初始密码" prop="password" required>
<el-input v-model="form.password" type="password" placeholder="请输入初始密码" maxlength="30" show-password/>
</el-form-item>
<el-form-item label="所属部门" prop="departmentId" required>
<DepartmentSelect v-model="form.departmentId" placeholder="请选择用户所属部门" :inline="false" clearable/>
</el-form-item>
<el-form-item label="岗位" prop="positionId">
<PositionSelect v-model="form.positionIds" placeholder="请选择用户所在岗位" :inline="false" :multiple="true" clearable/>
</el-form-item>
<el-form-item label="工号" prop="empNo">
<el-input v-model="form.empNo" placeholder="请输入工号" v-trim maxlength="50"/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入手机号码" v-trim maxlength="11"/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" v-trim maxlength="200"/>
</el-form-item>
<el-form-item label="生日" prop="birthday">
<el-date-picker v-model="form.birthday" value-format="yyyy-MM-dd" placeholder="请选择用户生日"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import PositionSelect from '@/components/common/PositionSelect'
import { checkMobile, checkEmail } from '@/utils/form'
export default {
name: 'OperaUserWindow',
extends: BaseOpera,
components: { PositionSelect, DepartmentSelect, GlobalWindow },
data () {
return {
// 表单数据
form: {
id: null,
username: '', // 用户名
realname: '', // 姓名
empNo: '', // 工号
departmentId: null, // 所属部门ID
positionIds: [], // 所属岗位ID集
avatar: '/avatar/man.png', // 头像
password: '', // 密码
mobile: '', // 手机号码
email: '', // 邮箱
sex: '1', // 性别
birthday: '' // 生日
},
// 验证规则
rules: {
username: [
{ required: true, message: '请输入用户名' }
],
realname: [
{ required: true, message: '请输入姓名' }
],
password: [
{ required: true, message: '请输入密码' }
],
departmentId: [
{ required: true, message: '请选择部门' }
],
avatar: [
{ required: true, message: '请选择用户头像' }
],
sex: [
{ required: true, message: '请选择用户性别' }
],
mobile: [
{ validator: checkMobile }
],
email: [
{ validator: checkEmail }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @target 编辑的用户对象
*/
open (title, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.departmentId = null
this.form.positionIds = []
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
this.form.departmentId = target.department == null ? null : target.department.id
this.form.positionIds = target.positions == null ? [] : target.positions.map(p => p.id)
})
}
},
created () {
this.config({
api: '/system/user'
})
}
}
</script>
<style lang="scss" scoped>
.global-window {
/deep/ .el-date-editor {
width: 100%;
}
// 表单头像处理
/deep/ .form-item-avatar {
.el-radio.is-bordered {
height: auto;
padding: 6px;
margin: 0 10px 0 0;
.el-radio__input {
display: none;
}
.el-radio__label {
padding: 0;
width: 48px;
display: block;
img {
width: 100%;
}
}
}
}
}
</style>
<template>
<GlobalWindow
:visible.sync="visible"
:confirm-working="isWorking"
width="576px"
title="重置密码"
@confirm="confirm"
>
<p class="tip" v-if="user != null">为用户 <em>{{user.realname}}</em> 重置密码</p>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="新密码" prop="password" required>
<el-input v-model="form.password" type="password" placeholder="请输入新密码" maxlength="30" show-password></el-input>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { resetPwd } from '@/api/system/user'
export default {
name: 'ResetPwdWindow',
components: { GlobalWindow },
data () {
return {
isWorking: false,
visible: false,
user: null,
form: {
password: ''
},
rules: {
password: [
{ required: true, message: '请输入密码' }
]
}
}
},
methods: {
open (user) {
this.user = user
this.visible = true
this.$nextTick(() => {
this.$refs.form.resetFields()
})
},
// 确认重置密码
confirm () {
if (this.isWorking) {
return
}
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
this.isWorking = true
resetPwd({
id: this.user.id,
password: this.form.password
})
.then(() => {
this.$tip.apiSuccess('密码重置成功')
this.visible = false
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// 角色配置
.global-window {
.tip {
margin-bottom: 12px;
em {
font-style: normal;
color: $primary-color;
font-weight: bold;
}
}
}
</style>
<template>
<GlobalWindow
:visible.sync="visible"
:confirm-working="isWorking"
width="582px"
title="配置用户角色"
@confirm="confirm"
>
<p class="tip" v-if="user != null">为用户 <em>{{user.realname}}</em> 配置角色</p>
<p class="tip-warn"><i class="el-icon-warning"></i>提醒:角色配置后需重新登录后生效</p>
<el-transfer
v-model="selectedIds"
:titles="['未授权角色', '已授权角色']"
:props="{
key: 'id',
label: 'name'
}"
:data="roles">
</el-transfer>
</GlobalWindow>
</template>
<script>
import BasePage from '@/components/base/BasePage'
import GlobalWindow from '@/components/common/GlobalWindow'
import { createUserRole } from '@/api/system/user'
import { fetchAll as fetchAllRoles } from '@/api/system/role'
export default {
name: 'RoleConfigWindow',
extends: BasePage,
components: { GlobalWindow },
data () {
return {
visible: false,
isWorking: false,
// 用户
user: null,
// 角色列表
roles: null,
// 已选中的角色ID
selectedIds: []
}
},
methods: {
open (user) {
fetchAllRoles()
.then(records => {
this.roles = records
this.user = user
// 如果为固定用户,则固定角色不可更改
if (this.user.fixed) {
for (const role of this.roles) {
if (role.fixed) {
role.disabled = true
}
}
}
// 如果当前用户为非超级管理员用户,则不允许授权超级管理员角色
if (!this.isAdmin) {
for (const role of this.roles) {
if (role.code === this.adminCode) {
role.disabled = true
}
}
}
this.selectedIds = this.user.roles.map(r => r.id)
this.visible = true
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 确认选择角色
confirm () {
if (this.isWorking) {
return
}
this.isWorking = true
createUserRole({
userId: this.user.id,
roleIds: this.selectedIds
})
.then(() => {
this.$tip.apiSuccess('角色配置成功,用户重新登录后生效')
this.visible = false
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
},
// 关闭
close () {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// 角色配置
.global-window {
.tip {
em {
font-style: normal;
color: $primary-color;
font-weight: bold;
}
}
.tip-warn {
margin: 4px 0 12px 0;
font-size: 12px;
color: #999;
i {
color: orange;
margin-right: 4px;
font-size: 14px;
position: relative;
top: 1px;
}
}
}
</style>
<template>
<TableLayout :permissions="['system:datapermission:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="业务模块" prop="businessCode">
<DataPermModuleSelect v-model="searchForm.businessCode" clearable @change="search"/>
</el-form-item>
<el-form-item label="角色" prop="roleId">
<RoleSelect v-model="searchForm.roleId" clearable @change="search"/>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:datapermission:create', 'system:datapermission:delete']">
<li><el-button type="primary" @click="$refs.operaDataPermissionWindow.open('新建数据权限')" icon="el-icon-plus" v-permissions="['system:datapermission:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:datapermission:delete']">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="businessCode" label="业务模块" min-width="100px">
<template slot-scope="{row}">{{row.businessCode | moduleText(modules)}}</template>
</el-table-column>
<el-table-column prop="roleId" label="角色" min-width="100px">
<template slot-scope="{row}">{{row.role.name}}</template>
</el-table-column>
<el-table-column prop="type" label="权限类型" min-width="140px">
<template slot-scope="{row}">{{row.type | typeText(types)}}</template>
</el-table-column>
<el-table-column prop="disabled" label="是否启用" min-width="100px">
<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 prop="remark" label="备注" min-width="100px"></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
v-if="containPermissions(['system:datapermission:update', 'system:datapermission:delete'])"
label="操作"
min-width="120"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaDataPermissionWindow.open('编辑数据权限', row)" icon="el-icon-edit" v-permissions="['system:datapermission:update']">编辑</el-button>
<el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:datapermission:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
>
</pagination>
</template>
<!-- 新建/修改 -->
<OperaDataPermissionWindow ref="operaDataPermissionWindow" @success="handlePageChange"/>
</TableLayout>
</template>
<script>
import { Table, TableColumn, Button } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
import BaseTable from './base/BaseTable'
import TableLayout from './TableLayout'
import Pagination from './common/Pagination'
import OperaDataPermissionWindow from './system/datapermission/OperaDataPermissionWindow'
import DataPermModuleSelect from './system/datapermission/DataPermModuleSelect'
import RoleSelect from './system/role/RoleSelect'
// 获取模块名称
const __getModuleName = function (businessCode, modules) {
for (const module of modules) {
if (module.businessCode === businessCode) {
return module.moduleName
}
}
return '未知'
}
export default {
name: 'DataPermission',
extends: BaseTable,
components: {
"el-table": Table,
"el-table-column": TableColumn,
"el-button": Button,
RoleSelect,
DataPermModuleSelect,
TableLayout,
Pagination,
OperaDataPermissionWindow
},
data () {
return {
// 数据权限模块
modules: [],
// 数据权限类型
types: [],
// 搜索
searchForm: {
businessCode: '',
roleId: null,
type: ''
}
}
},
filters: {
// 数据权限类型文案
typeText (value, types) {
for (const type of types) {
if (type.code === value) {
return type.remark
}
}
return '未知'
},
// 数据权限模块文案
moduleText (value, modules) {
return __getModuleName(value, modules)
}
},
methods: {
// 启用/禁用菜单
switchDisabled (row) {
if (!row.disabled) {
this.__updateStatus(row)
return
}
this.$dialog.disableConfirm(`确认禁用 ${__getModuleName(row.businessCode, this.modules)}/${row.role.name} 数据权限吗?`)
.then(() => {
this.__updateStatus(row)
}).catch(() => {
row.disabled = !row.disabled
})
},
// 删除行
deleteById (row) {
this.$dialog.deleteConfirm(`确认删除【${__getModuleName(row.businessCode, this.modules)}/${row.role.name}】数据权限吗?`)
.then(() => {
this.isWorking.delete = true
this.api.deleteById(row.id)
.then(() => {
this.$tip.apiSuccess('删除成功')
this.__afterDelete()
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
})
.catch(() => {})
},
// 修改状态
__updateStatus (row) {
this.api.updateStatus({
id: row.id,
disabled: row.disabled
})
.then(() => {
this.$tip.apiSuccess('修改成功')
})
.catch(e => {
row.disabled = !row.disabled
this.$tip.apiFailed(e)
})
}
},
async created () {
this.config({
module: '数据权限',
api: '/system/dataPermission'
})
// 初始化数据权限模块
await this.api.fetchModules()
.then(data => {
this.modules = data
})
// 初始化数据权限模块
await this.api.fetchTypes()
.then(data => {
this.types = data
})
.catch(e => {
console.log(e)
})
// 执行搜索
this.search()
}
}
</script>
<template>
<TableLayout :permissions="['system:department:query']">
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:department:create', 'system:department:delete']">
<li><el-button type="primary" @click="$refs.operaDepartmentWindow.open('新建部门')" icon="el-icon-plus" v-permissions="['system:department:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:department:delete']">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
row-key="id"
stripe
default-expand-all
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" fixed="left" width="55"></el-table-column>
<el-table-column prop="name" label="部门名称" fixed="left" min-width="200px"></el-table-column>
<el-table-column prop="code" label="部门编码" fixed="left" min-width="100px"></el-table-column>
<el-table-column prop="userCount" label="部门人数" min-width="100px"></el-table-column>
<el-table-column prop="phone" label="联系电话" min-width="100px"></el-table-column>
<el-table-column prop="email" label="部门邮箱" min-width="180px"></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
v-if="containPermissions(['system:department:update', 'system:department:create', 'system:department:delete', 'system:department:queryUsers'])"
label="操作"
min-width="310"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaDepartmentWindow.open('编辑部门', row)" icon="el-icon-edit" v-permissions="['system:department:update']">编辑</el-button>
<el-button type="text" @click="$refs.operaDepartmentWindow.open('新建下级部门', null, row)" icon="el-icon-edit" v-permissions="['system:department:create']">新建下级部门</el-button>
<el-button type="text" @click="$refs.departmentUserWindow.open(row.id, row.name)" icon="el-icon-user-solid" v-permissions="['system:department:queryUsers']">查看人员</el-button>
<el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:department:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- 新建/修改 -->
<OperaDepartmentWindow ref="operaDepartmentWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 查看人员 -->
<DepartmentUserWindow ref="departmentUserWindow"/>
</TableLayout>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import { fetchTree } from '@/api/system/department'
import BaseTable from '@/components/base/BaseTable'
import OperaDepartmentWindow from '@/components/system/department/OperaDepartmentWindow'
import DepartmentUserWindow from '@/components/system/department/DepartmentUserWindow'
export default {
name: 'SystemDepartment',
extends: BaseTable,
components: { DepartmentUserWindow, OperaDepartmentWindow, TableLayout },
data () {
return {
// 搜索
searchForm: {
name: ''
}
}
},
methods: {
// 查询数据
handlePageChange () {
this.tableData.list.splice(0, this.tableData.list.length)
this.isWorking.search = true
fetchTree()
.then(records => {
this.tableData.list = records
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
}
},
created () {
this.config({
module: '部门',
api: '/system/department'
})
this.search()
}
}
</script>
<style lang="scss" scoped>
.table-layout {
/deep/ .table-content {
margin-top: 0;
}
}
</style>
<template>
<TableLayout :permissions="['system:dict:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="字典编码" prop="code">
<el-input v-model="searchForm.code" v-trim placeholder="请输入字典编码" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="字典名称" prop="name">
<el-input v-model="searchForm.name" v-trim placeholder="请输入字典名称" @keypress.enter.native="search"/>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:dict:create', 'system:dict:delete']">
<li><el-button type="primary" @click="$refs.operaDictWindow.open('新建字典')" icon="el-icon-plus" v-permissions="['system:dict:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:dict:delete']">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:default-sort = "{prop: 'createTime', order: 'descending'}"
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" fixed="left" width="55"></el-table-column>
<el-table-column prop="code" label="字典编码" fixed="left" min-width="100px"></el-table-column>
<el-table-column prop="name" label="字典名称" fixed="left" min-width="100px"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="100px"></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" sortable="custom" sort-by="dict.CREATE_TIME"></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
v-if="containPermissions(['system:dict:update', 'system:dict:delete'])"
label="操作"
min-width="210"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaDictWindow.open('编辑字典', row)" icon="el-icon-edit" v-permissions="['system:dict:update']">编辑</el-button>
<el-button type="text" @click="$refs.dictDataManagerWindow.open(row.id, row.name)" icon="el-icon-edit" v-permissions="['system:dict:update']">数据管理</el-button>
<el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:dict:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
<!-- 新建/修改 -->
<OperaDictWindow ref="operaDictWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 数据管理 -->
<DictDataManagerWindow ref="dictDataManagerWindow"/>
</TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaDictWindow from '@/components/system/dict/OperaDictWindow'
import DictDataManagerWindow from '@/components/system/dict/DictDataManagerWindow'
export default {
name: 'SystemDict',
extends: BaseTable,
components: { DictDataManagerWindow, OperaDictWindow, TableLayout, Pagination },
data () {
return {
// 搜索
searchForm: {
code: '',
name: ''
}
}
},
created () {
this.config({
module: '字典',
api: '/system/dict',
sorts: [{
property: 'dict.CREATE_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<template>
<TableLayout :permissions="['system:location:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="名称" prop="name">
<el-input v-model="searchForm.name" placeholder="请输入名称" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="地区范围" prop="parentId">
<LocationSelect :city-id.sync="searchForm.parentId" placeholder="请选择地区范围" :level="2" clearable @change="search"/>
</el-form-item>
<el-form-item label="地区层级" prop="level">
<el-select v-model="searchForm.level" placeholder="请选择地区层级" clearable @change="search">
<el-option value="1" label="省"/>
<el-option value="2" label="市"/>
<el-option value="3" label="区/县"/>
</el-select>
</el-form-item>
<el-form-item label="区号" prop="areaCode">
<el-input v-model="searchForm.areaCode" placeholder="请输入区号" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="邮编" prop="postalCode">
<el-input v-model="searchForm.postalCode" placeholder="请输入邮编" @keypress.enter.native="search"></el-input>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:location:create']">
<li><el-button type="primary" @click="$refs.operaLocationWindow.open('新建地区')" icon="el-icon-plus" v-permissions="['system:location:create']">新建</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
>
<el-table-column prop="name" label="名称" min-width="100px"></el-table-column>
<el-table-column prop="shortName" label="简称" min-width="100px"></el-table-column>
<el-table-column prop="fullName" label="全称" min-width="180px"></el-table-column>
<el-table-column prop="pinyin" label="拼音" min-width="100px"></el-table-column>
<el-table-column prop="level" label="层级" min-width="80px">
<template slot-scope="{row}">
{{ row.level | levelText }}
</template>
</el-table-column>
<el-table-column prop="areaCode" label="区号" min-width="100px"></el-table-column>
<el-table-column prop="postalCode" label="邮编" min-width="100px"></el-table-column>
<el-table-column prop="firstLetter" label="首字母" min-width="100px"></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 prop="lng" label="经度" min-width="100px"></el-table-column>
<el-table-column prop="lat" label="纬度" min-width="100px"></el-table-column>
<el-table-column
v-if="containPermissions(['system:location:update', 'system:location:delete'])"
label="操作"
min-width="160"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaLocationWindow.open('编辑地区', row)" icon="el-icon-edit" v-permissions="['system:location:update']">编辑</el-button>
<el-button type="text" v-if="row.level === 1" @click="$refs.operaLocationWindow.open('新增市', null, row)" icon="el-icon-edit" v-permissions="['system:location:create']">新增市</el-button>
<el-button type="text" v-if="row.level === 2" @click="$refs.operaLocationWindow.open('新增区/县', null, row)" icon="el-icon-edit" v-permissions="['system:location:create']">新增区/县</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
>
</pagination>
</template>
<!-- 新建/修改 -->
<OperaLocationWindow ref="operaLocationWindow" @success="handlePageChange"/>
</TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
import OperaLocationWindow from '@/components/system/location/OperaLocationWindow'
import LocationSelect from '@/components/common/LocationSelect'
import { updateStatus } from '@/api/system/location'
export default {
name: 'SystemLocation',
extends: BaseTable,
components: { LocationSelect, TableLayout, Pagination, OperaLocationWindow },
data () {
return {
// 搜索
searchForm: {
parentId: null,
level: null,
name: '',
areaCode: '',
postalCode: ''
}
}
},
filters: {
levelText (value) {
if (value === 1) {
return ''
}
if (value === 2) {
return ''
}
if (value === 3) {
return '区/县'
}
return '未知'
}
},
methods: {
// 启用/禁用
switchDisabled (row) {
if (!row.disabled) {
this.__updateMenuStatus(row)
return
}
this.$dialog.disableConfirm(`确认禁用 ${row.fullName} 地区吗?`)
.then(() => {
this.__updateMenuStatus(row)
}).catch(() => {
row.disabled = !row.disabled
})
},
// 修改菜单状态
__updateMenuStatus (row) {
updateStatus({
id: row.id,
disabled: row.disabled
})
.then(() => {
this.$tip.apiSuccess('修改成功')
})
.catch(e => {
row.disabled = !row.disabled
this.$tip.apiFailed(e)
})
}
},
created () {
this.config({
module: '地区',
api: '/system/location',
'field.id': 'id',
'field.main': 'fullName'
})
this.search()
}
}
</script>
<template>
<TableLayout :permissions="['system:loginLog:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="登录用户名" prop="loginUsername">
<el-input v-model="searchForm.loginUsername" placeholder="请输入登录用户名" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="登录IP" prop="ip">
<el-input v-model="searchForm.ip" placeholder="请输入登录IP" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="服务器IP" prop="serverIp">
<el-input v-model="searchForm.serverIp" placeholder="请输入服务器IP" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="是否登录成功" prop="success">
<el-select v-model="searchForm.success" placeholder="请选择是否登录状态" clearable @change="search">
<el-option value="true" label="登录成功"/>
<el-option value="false" label="登录失败"/>
</el-select>
</el-form-item>
<el-form-item label="登录时间" prop="loginTime">
<el-date-picker
v-model="searchDateRange"
type="datetimerange"
range-separator="至"
value-format="yyyy-MM-dd HH:mm:ss"
start-placeholder="开始时间"
end-placeholder="结束时间"
@change="handleSearchTimeChange"
></el-date-picker>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button type="primary" :loading="isWorking.export" @click="exportExcel">导出</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
:default-sort="{prop: 'loginTime', order: 'descending'}"
@sort-change="handleSortChange"
>
<el-table-column prop="loginUsername" label="登录用户名" min-width="100px"></el-table-column>
<el-table-column prop="ip" label="登录IP" min-width="120px"></el-table-column>
<el-table-column prop="location" label="登录地址" min-width="160px"></el-table-column>
<el-table-column prop="clientInfo" label="客户端" min-width="160px"></el-table-column>
<el-table-column prop="osInfo" label="操作系统" min-width="100px"></el-table-column>
<el-table-column prop="platform" label="登录平台" min-width="100px"></el-table-column>
<el-table-column prop="loginTime" label="登录时间" min-width="160px" sortable="custom" sort-by="LOGIN_TIME"></el-table-column>
<el-table-column prop="systemVersion" label="系统版本" min-width="100px"></el-table-column>
<el-table-column prop="serverIp" label="服务器IP" min-width="120px"></el-table-column>
<el-table-column prop="success" label="状态" min-width="100px">
<template slot-scope="{row}">
{{row.success | statusText}}
</template>
</el-table-column>
<el-table-column prop="reason" label="失败原因" min-width="160px"></el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
</TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
export default {
name: 'SystemLoginLog',
extends: BaseTable,
components: { TableLayout, Pagination },
data () {
return {
// 搜索时间范围
searchDateRange: [],
// 搜索
searchForm: {
loginUsername: '',
ip: '',
serverIp: '',
success: '',
startTime: null,
endTime: null
}
}
},
filters: {
// 登录状态
statusText (value) {
if (value) {
return '登录成功'
}
return '登录失败'
}
},
methods: {
// 时间搜索范围变化
handleSearchTimeChange (value) {
this.searchForm.startTime = null
this.searchForm.endTime = null
if (value != null) {
this.searchForm.startTime = value[0]
this.searchForm.endTime = value[1]
}
this.search()
}
},
created () {
this.config({
module: '登录日志',
api: '/system/loginLog',
'field.id': 'id',
'field.main': 'id',
sorts: [{
property: 'LOGIN_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<template>
<Profile :permissions="['system:monitor:query']">
<div class="monitor">
<div class="toolbar">
<el-switch v-model="autoRefresh" @change="changeAutoRefresh"/><label>{{autoRefresh | autoRefreshText}}</label>
</div>
<ul>
<li class="wrap">
<h2>CPU<Light v-if="data != null" :warn="data.cpu.useRatio > 60" :danger="data.cpu.useRatio > 80"/></h2>
<div>
<dl>
<dt>物理核数</dt>
<dd><Value :data="data" prop="cpu.physicalCount"/></dd>
</dl>
<dl>
<dt>逻辑核数</dt>
<dd><Value :data="data" prop="cpu.logicalCount"/></dd>
</dl>
<dl class="important">
<dt>当前使用率</dt>
<dd><Value :data="data" prop="cpu.useRatio" :handler="keep2decimals" suffix="%"/></dd>
</dl>
<dl>
<dt>当前空闲率</dt>
<dd><Value :data="data" prop="cpu.freeRatio" :handler="keep2decimals" suffix="%"/></dd>
</dl>
</div>
</li>
<li class="wrap">
<h2>内存<Light v-if="data != null" :warn="data.memory.useRatio > 60" :danger="data.memory.useRatio > 80"/></h2>
<div>
<dl>
<dt>总空间</dt>
<dd><Value :data="data" prop="memory.size" suffix="G" :handler="toG"/></dd>
</dl>
<dl>
<dt>空闲空间</dt>
<dd><Value :data="data" prop="memory.freeSpace" suffix="G" :handler="toG"/></dd>
</dl>
<dl class="important">
<dt>已用空间</dt>
<dd><Value :data="data" prop="memory.usedSpace" suffix="G" :handler="toG"/></dd>
</dl>
<dl class="important">
<dt>使用率</dt>
<dd><Value :data="data" prop="memory.useRatio" suffix="%" :handler="keep2decimals"/></dd>
</dl>
</div>
</li>
<li class="wrap">
<h2>JVM<Light v-if="data != null" :warn="data.jvm.memory.useRatio > 60" :danger="data.jvm.memory.useRatio > 80"/></h2>
<div>
<dl>
<dt>安装路径</dt>
<dd><Value :data="data" prop="jvm.home"/></dd>
</dl>
<dl>
<dt>版本</dt>
<dd><Value :data="data" prop="jvm.version"/></dd>
</dl>
<dl>
<dt>启动时间</dt>
<dd><Value :data="data" prop="jvm.bootTime"/></dd>
</dl>
<dl>
<dt>运行时长</dt>
<dd><Value :data="data" prop="jvm.runtime"/></dd>
</dl>
<dl>
<dt>总空间</dt>
<dd><Value :data="data" prop="jvm.memory.size" suffix="M" :handler="keep2decimals"/></dd>
</dl>
<dl>
<dt>空闲空间</dt>
<dd><Value :data="data" prop="jvm.memory.freeSpace" suffix="M" :handler="keep2decimals"/></dd>
</dl>
<dl class="important">
<dt>已用空间</dt>
<dd><Value :data="data" prop="jvm.memory.usedSpace" suffix="M" :handler="keep2decimals"/></dd>
</dl>
<dl class="important">
<dt>使用率</dt>
<dd><Value :data="data" prop="jvm.memory.useRatio" suffix="%" :handler="keep2decimals"/></dd>
</dl>
</div>
</li>
<li class="wrap">
<h2>服务器</h2>
<div>
<dl>
<dt>操作系统</dt>
<dd><Value :data="data" prop="osName"/></dd>
</dl>
<dl>
<dt>系统版本</dt>
<dd><Value :data="data" prop="osVersion"/></dd>
</dl>
<dl>
<dt>系统架构</dt>
<dd><Value :data="data" prop="osArch"/></dd>
</dl>
<dl class="important">
<dt>IP地址</dt>
<dd><Value :data="data" prop="ip"/></dd>
</dl>
<dl>
<dt>MAC地址</dt>
<dd><Value :data="data" prop="mac"/></dd>
</dl>
<dl>
<dt>服务器时间</dt>
<dd><Value :data="data" prop="currentTime"/></dd>
</dl>
</div>
</li>
</ul>
<div class="wrap">
<h2>磁盘信息</h2>
<el-table :data="data ? data.disks : []" v-loading="loading">
<el-table-column prop="name" label="磁盘名称"/>
<el-table-column prop="dir" label="磁盘路径"/>
<el-table-column prop="fsType" label="文件系统"/>
<el-table-column prop="size" label="总空间">
<template slot-scope="{row}">
{{toG(row.size)}}G
</template>
</el-table-column>
<el-table-column prop="freeSpace" label="可用空间">
<template slot-scope="{row}">
{{toG(row.freeSpace)}}G
</template>
</el-table-column>
<el-table-column prop="usedSpace" label="已用空间">
<template slot-scope="{row}">
<label class="important">{{toG(row.usedSpace)}}G</label>
</template>
</el-table-column>
<el-table-column prop="useRatio" label="已用占比">
<template slot-scope="{row}">
<label class="important">{{keep2decimals(row.useRatio)}}%</label>
</template>
</el-table-column>
<el-table-column>
<template slot-scope="{row}">
<Light :warn="row.useRatio > 60" :danger="row.useRatio > 80" :mini="true"/>
</template>
</el-table-column>
</el-table>
</div>
</div>
</Profile>
</template>
<script>
import { getSystemInfo } from '@/api/system/monitor'
import Value from '@/components/common/Value'
import Light from '@/components/common/Light'
import Profile from '../../components/common/Profile'
export default {
name: 'SystemMonitor',
components: { Profile, Light, Value },
data () {
return {
// 加载中
loading: false,
// 自动刷新标识
autoRefresh: false,
// 数据
data: null,
// 自动刷新定时器
interval: null
}
},
filters: {
autoRefreshText (value) {
if (value) {
return '已开启自动刷新'
}
return '已关闭自动刷新'
}
},
methods: {
// 切换自动刷新
changeAutoRefresh (value) {
if (this.interval != null) {
clearInterval(this.interval)
}
if (value) {
this.getSystemInfo()
this.interval = setInterval(() => {
this.getSystemInfo()
}, 3000)
}
},
// 获取系统信息
getSystemInfo () {
if (this.loading) {
return
}
this.loading = true
getSystemInfo()
.then(data => {
this.data = data
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.loading = false
})
},
// 单位转为G
toG (value) {
return Math.round(value / 1024 * 100) / 100
},
// 转为比率
keep2decimals (value) {
return Math.round(value * 100) / 100
}
},
beforeRouteLeave (from, to, next) {
clearInterval(this.interval)
next()
},
created () {
this.getSystemInfo()
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.monitor {
padding: 20px 20px;
}
// 工具栏
.toolbar {
margin-bottom: 12px;
background: #fff;
padding: 8px 16px;
label {
font-size: 12px;
margin-left: 8px;
color: #999;
}
}
ul {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
li {
width: 49.5%;
min-height: 200px;
flex-shrink: 0;
}
}
// 信息模块
.wrap {
background: #fff;
box-shadow: 2px 2px 10px -5px #999;
border-radius: 8px;
margin-bottom: 16px;
h2 {
border-bottom: 1px solid #eee;
font-size: 18px;
font-weight: normal;
line-height: 40px;
padding: 0 16px;
color: #555;
position: relative;
.light {
position: absolute;
top: 12px;
right: 12px;
}
}
& > div {
padding: 0 16px;
font-size: 14px;
dl {
display: flex;
dt {
width: 80px;
text-align: right;
flex-shrink: 0;
color: #999;
}
dd {
width: 100%;
margin: 0;
padding-left: 12px;
color: #555;
overflow: hidden;
}
}
}
}
// 重要信息
.important {
color: $primary-color;
font-weight: bold;
& > dd > div {
color: $primary-color;
font-weight: bold;
}
}
</style>
<template>
<TableLayout :permissions="['system:permission:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="权限编码" prop="code">
<el-input v-model="searchForm.code" v-trim placeholder="请输入权限编码" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="权限名称" prop="name">
<el-input v-model="searchForm.name" v-trim placeholder="请输入权限名称" @keypress.enter.native="search"/>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:permission:create', 'system:permission:delete']">
<li><el-button type="primary" @click="$refs.operaPermissionWindow.open('新建系统权限')" icon="el-icon-plus" v-permissions="['system:permission:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:permission:delete']">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:default-sort = "{prop: 'createTime', order: 'descending'}"
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" fixed="left" width="55"></el-table-column>
<el-table-column prop="code" label="权限编码" fixed="left" min-width="200px"></el-table-column>
<el-table-column prop="name" label="权限名称" fixed="left" min-width="120px"></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" sortable="custom" sort-by="perm.CREATE_TIME"></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
v-if="containPermissions(['system:permission:update', 'system:permission:delete'])"
label="操作"
min-width="130"
fixed="right"
>
<template slot-scope="{row}">
<el-button v-if="!row.fixed" type="text" @click="$refs.operaPermissionWindow.open('编辑系统权限', row)" icon="el-icon-edit" v-permissions="['system:permission:update']">编辑</el-button>
<el-button v-if="!row.fixed" type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:permission:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
<!-- 新建/修改 -->
<OperaPermissionWindow ref="operaPermissionWindow" @success="handlePageChange"/>
</TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaPermissionWindow from '@/components/system/permission/OperaPermissionWindow'
export default {
name: 'SystemPermission',
extends: BaseTable,
components: { OperaPermissionWindow, TableLayout, Pagination },
data () {
return {
// 搜索
searchForm: {
code: '',
name: '',
remark: ''
}
}
},
created () {
this.config({
module: '权限',
api: '/system/permission',
sorts: [{
property: 'perm.CREATE_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<template>
<TableLayout :permissions="['system:position:query']">
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:position:create', 'system:position:delete']">
<li><el-button type="primary" @click="$refs.operaPositionWindow.open('新建岗位')" icon="el-icon-plus" v-permissions="['system:position:create']">新建</el-button></li>
<li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:position:delete']">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
row-key="id"
stripe
default-expand-all
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" fixed="left"></el-table-column>
<el-table-column prop="name" label="岗位名称" fixed="left" min-width="200px"></el-table-column>
<el-table-column prop="code" label="岗位编码" fixed="left" min-width="100px"></el-table-column>
<el-table-column prop="userCount" label="岗位人数" min-width="100px"></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
v-if="containPermissions(['system:position:update', 'system:position:query', 'system:position:delete'])"
label="操作"
min-width="310"
fixed="right"
>
<template slot-scope="{row}">
<el-button type="text" @click="$refs.operaPositionWindow.open('编辑岗位', row)" icon="el-icon-edit" v-permissions="['system:position:update']">编辑</el-button>
<el-button type="text" @click="$refs.operaPositionWindow.open('新增下级岗位', null, row)" icon="el-icon-edit" v-permissions="['system:position:update']">新增下级岗位</el-button>
<el-button type="text" @click="$refs.positionUserWindow.open(row.id, row.name)" icon="el-icon-user-solid" v-permissions="['system:position:queryUsers']">查看人员</el-button>
<el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:position:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- 新建/修改 -->
<OperaPositionWindow ref="operaPositionWindow" @success="handlePageChange"/>
<!-- 人员管理 -->
<PositionUserWindow ref="positionUserWindow"/>
</TableLayout>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaPositionWindow from '@/components/system/position/OperaPositionWindow'
import PositionUserWindow from '@/components/system/position/PositionUserWindow'
import { fetchTree } from '@/api/system/position'
export default {
name: 'SystemPosition',
extends: BaseTable,
components: { PositionUserWindow, OperaPositionWindow, TableLayout },
methods: {
// 查询数据
handlePageChange () {
this.isWorking.search = true
fetchTree()
.then(records => {
this.tableData.list = records
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
}
},
created () {
this.config({
module: '岗位',
api: '/system/position'
})
this.search()
}
}
</script>
<style lang="scss" scoped>
.table-layout {
/deep/ .table-content {
margin-top: 0;
}
}
</style>
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
</template>
<script>
import { Table, TableColumn } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
export default {
data() {
return {
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀区金沙江路 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: "上海市普陀区金沙江路 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: "上海市普陀区金沙江路 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: "上海市普陀区金沙江路 1516 弄",
},
],
};
},
components: {
"el-table": Table,
"el-table-column": TableColumn
}
};
</script>
\ No newline at end of file
<template>
<TableLayout :permissions="['system:traceLog:query']">
<!-- 搜索表单 -->
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
<el-form-item label="用户姓名" prop="userRealname">
<el-input v-model="searchForm.userRealname" placeholder="请输入固化用户姓名" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="业务模块" prop="operaModule">
<el-input v-model="searchForm.operaModule" placeholder="请输入业务模块" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="请求地址" prop="requestUri">
<el-input v-model="searchForm.requestUri" placeholder="请输入请求地址" @keypress.enter.native="search"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.status" clearable @change="search">
<el-option value="-1" label="未处理"/>
<el-option value="0" label="失败"/>
<el-option value="1" label="成功"/>
</el-select>
</el-form-item>
<el-form-item label="异常等级" prop="exceptionLevel">
<el-select v-model="searchForm.exceptionLevel" clearable @change="search">
<el-option value="10" label="高"/>
<el-option value="5" label="中"/>
<el-option value="0" label="低"/>
</el-select>
</el-form-item>
<el-form-item label="操作时间范围">
<el-date-picker
v-model="searchDateRange"
type="datetimerange"
range-separator="至"
value-format="yyyy-MM-dd HH:mm:ss"
start-placeholder="开始时间"
end-placeholder="结束时间"
@change="handleSearchTimeChange"
></el-date-picker>
</el-form-item>
<section>
<el-button type="primary" @click="search">搜索</el-button>
<el-button type="primary" :loading="isWorking.export" @click="exportExcel">导出</el-button>
<el-button @click="reset">重置</el-button>
</section>
</el-form>
<div slot="space" class="status-bar">
<label class="status-normal">正常</label>
<label class="status-warn">警告异常(需排查)</label>
<label class="status-danger">系统异常(需修复)</label>
</div>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
stripe
:default-sort="{prop: 'operaTime', order: 'descending'}"
:row-class-name="tableRowClassName"
@sort-change="handleSortChange"
>
<el-table-column prop="operaModule" label="业务模块" min-width="100px"></el-table-column>
<el-table-column prop="operaRemark" label="操作说明" min-width="100px"></el-table-column>
<el-table-column prop="requestMethod" label="请求方式" min-width="80px"></el-table-column>
<el-table-column prop="requestUri" label="请求地址" min-width="200px"></el-table-column>
<el-table-column prop="status" label="状态" min-width="80px">
<template slot-scope="{row}">
{{row.status | statusText}}
</template>
</el-table-column>
<el-table-column prop="requestParams" label="请求参数" min-width="80px">
<template slot-scope="{row}">
<ColumnDetail v-if="row.requestParams != null" :content="row.requestParams" :limit="0"/>
</template>
</el-table-column>
<el-table-column prop="requestResult" label="请求结果" min-width="80px">
<template slot-scope="{row}">
<ColumnDetail v-if="row.requestResult != null" :content="row.requestResult"/>
</template>
</el-table-column>
<el-table-column prop="exceptionLevel" label="异常等级" sortable="custom" sort-by="EXCEPTION_LEVEL" min-width="100px">
<template slot-scope="{row}">
{{row.exceptionLevel | exceptionLevelText}}
</template>
</el-table-column>
<el-table-column prop="exceptionStack" label="异常信息" min-width="170px">
<template slot-scope="{row}">
<ColumnDetail v-if="row.exceptionStack != null" :content="row.exceptionStack" :button-type="getExceptionButtonType(row.exceptionLevel)"/>
</template>
</el-table-column>
<el-table-column prop="operaSpendTime" label="请求耗时(ms)" sortable="custom" sort-by="OPERA_SPEND_TIME" min-width="120px"></el-table-column>
<el-table-column prop="userRealname" label="操作人" min-width="100px"></el-table-column>
<el-table-column prop="operaTime" label="操作时间" sortable="custom" sort-by="OPERA_TIME" min-width="140px"></el-table-column>
<el-table-column prop="platform" label="操作平台" min-width="100px"></el-table-column>
<el-table-column prop="systemVersion" label="系统版本" min-width="80px"></el-table-column>
<el-table-column prop="serverIp" label="处理服务器IP" min-width="100px"></el-table-column>
<el-table-column prop="ip" label="用户IP" min-width="100px"></el-table-column>
<el-table-column prop="clientInfo" label="用户客户端" min-width="200px"></el-table-column>
<el-table-column prop="osInfo" label="用户操作系统" min-width="100px"></el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
</TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import ColumnDetail from '../../components/common/ColumnDetail'
export default {
name: 'SystemTraceLog',
extends: BaseTable,
components: { ColumnDetail, TableLayout, Pagination },
data () {
return {
// 搜索时间反胃
searchDateRange: [],
// 搜索
searchForm: {
userRealname: '',
operaModule: '',
requestUri: '',
status: null,
exceptionLevel: null,
startTime: null,
endTime: null
}
}
},
filters: {
// 状态
statusText (value) {
if (value === 1) {
return '成功'
}
if (value === 0) {
return '失败'
}
return '未处理'
},
// 异常等级
exceptionLevelText (value) {
if (value == null) {
return ''
}
if (value === 0) {
return ''
}
if (value === 5) {
return ''
}
if (value === 10) {
return ''
}
return '未知'
}
},
methods: {
// 搜索框重置
reset () {
this.$refs.searchForm.resetFields()
this.searchDateRange = []
this.searchForm.startTime = null
this.searchForm.endTime = null
this.search()
},
// 标记行class
tableRowClassName ({ row }) {
if (row.exceptionLevel === 5 || row.status === -1) {
return 'warning-log'
} else if (row.exceptionLevel === 10) {
return 'danger-log'
}
return ''
},
// 获取异常查看按钮类型
getExceptionButtonType (level) {
if (level === 5) {
return 'warning'
}
if (level === 10) {
return 'danger'
}
return null
},
// 时间搜索范围变化
handleSearchTimeChange (value) {
this.searchForm.startTime = null
this.searchForm.endTime = null
if (value != null) {
this.searchForm.startTime = value[0]
this.searchForm.endTime = value[1]
}
this.search()
}
},
created () {
this.config({
api: '/system/traceLog',
sorts: [{
property: 'OPERA_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<style scoped lang="scss">
// 状态栏
.status-bar {
padding: 0 16px;
[class^=status-] {
font-size: 13px;
margin-right: 12px;
line-height: 40px;
&::before {
position: relative;
top: 2px;
display: inline-block;
content: '';
width: 12px;
height: 12px;
border: 1px solid #ccc;
background: #fff;
margin-right: 6px;
}
}
.status-warn::before {
background-color: oldlace;
border-color: orange;
}
.status-danger::before {
background-color: #fdf0f0;
border-color: indianred;
}
}
/deep/ .table-content {
margin-top: 0;
}
// 警告级日志
/deep/ .warning-log td {
background-color: oldlace !important;
}
// 危险级日志
/deep/ .danger-log td {
background-color: #fdf0f0 !important;
}
</style>
<template>
<TableLayout :permissions="['system:user:query']">
<!-- 搜索表单 -->
<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" v-trim placeholder="请输入用户名" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="姓名" prop="realname">
<el-input v-model="searchForm.realname" v-trim placeholder="请输入姓名" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="searchForm.mobile" v-trim placeholder="请输入手机号码" @keypress.enter.native="search"/>
</el-form-item>
<el-form-item label="所属部门" prop="rootDeptId">
<DepartmentSelect v-model="searchForm.rootDeptId" placeholder="请选择所属部门" clearable/>
</el-form-item>
<el-form-item label="岗位" prop="positionId">
<PositionSelect v-model="searchForm.positionId" placeholder="请选择岗位" clearable/>
</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>
<!-- 表格和分页 -->
<template v-slot:table-wrap>
<ul class="toolbar" v-permissions="['system:user:create', 'system:user:delete']">
<li v-permissions="['system:user:create']"><el-button icon="el-icon-plus" type="primary" @click="$refs.operaUserWindow.open('新建用户')">新建</el-button></li>
<li v-permissions="['system:user:delete']"><el-button icon="el-icon-delete" @click="deleteByIdInBatch">删除</el-button></li>
</ul>
<el-table
v-loading="isWorking.search"
:data="tableData.list"
:default-sort = "{prop: 'createTime', order: 'descending'}"
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<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="120px"></el-table-column>
<el-table-column prop="empNo" label="工号" sortable="custom" sort-by="EMP_NO" 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="160px" 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="性别" sortable="custom" sort-by="SEX" 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="生日" sortable="custom" sort-by="BIRTHDAY" min-width="100px"></el-table-column>
<el-table-column prop="roles" label="角色" min-width="160px" class-name="table-column-strings">
<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-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="创建时间" sortable="custom" sort-by="CREATE_TIME" 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="更新时间" sortable="custom" sort-by="UPDATE_TIME" min-width="140px"></el-table-column>
<el-table-column
v-if="containPermissions(['system:user:update', 'system:user:createUserRole', 'system:user:resetPwd', 'system:user:delete'])"
label="操作"
width="270"
fixed="right"
>
<template v-if="isAdmin || (row.id !== userInfo.id && row.roles.findIndex(r => r.code === adminCode) === -1)" slot-scope="{row}">
<el-button type="text" icon="el-icon-edit" @click="$refs.operaUserWindow.open('编辑用户', row)" v-permissions="['system:user:update']">编辑</el-button>
<el-button type="text" icon="el-icon-s-custom" @click="$refs.roleConfigWindow.open(row)" v-permissions="['system:user:createUserRole']">配置角色</el-button>
<el-button type="text" @click="$refs.resetPwdWindow.open(row)" v-permissions="['system:user:resetPwd']">重置密码</el-button>
<el-button v-if="!row.fixed" type="text" icon="el-icon-delete" @click="deleteById(row)" v-permissions="['system:user:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
@size-change="handleSizeChange"
@current-change="handlePageChange"
:pagination="tableData.pagination"
></pagination>
</template>
<!-- 新建/修改 -->
<OperaUserWindow ref="operaUserWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 配置角色 -->
<RoleConfigWindow ref="roleConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
<!-- 重置密码 -->
<ResetPwdWindow ref="resetPwdWindow"/>
</TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaUserWindow from '@/components/system/user/OperaUserWindow'
import RoleConfigWindow from '@/components/system/user/RoleConfigWindow'
import ResetPwdWindow from '@/components/system/user/ResetPwdWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import PositionSelect from '@/components/common/PositionSelect'
export default {
name: 'SystemUser',
extends: BaseTable,
components: { PositionSelect, DepartmentSelect, ResetPwdWindow, RoleConfigWindow, OperaUserWindow, TableLayout, Pagination },
data () {
return {
// 搜索
searchForm: {
username: '', // 名字
realname: '', // 姓名
rootDeptId: null, // 部门ID
positionId: null, // 岗位ID
mobile: '' // 手机号码
}
}
},
created () {
this.config({
module: '用户',
api: '/system/user',
'field.main': 'realname',
sorts: [{
property: 'CREATE_TIME',
direction: 'DESC'
}]
})
this.search()
}
}
</script>
<style scoped lang="scss">
@import "@/styles/variables.scss";
// 列表头像处理
.table-column-avatar {
img {
width: 48px;
}
}
</style>
......@@ -257,6 +257,14 @@
dependencies:
"@babel/types" "^7.14.5"
"@babel/helper-module-imports@7.0.0-beta.35":
version "7.0.0-beta.35"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.35.tgz#308e350e731752cdb4d0f058df1d704925c64e0a"
integrity sha512-vaC1KyIZSuyWb3Lj277fX0pxivyHwuDU4xZsofqgYAbkDxNieMg2vuhzP5AgMweMY7fCQUMTi+BgPqTLjkxXFg==
dependencies:
"@babel/types" "7.0.0-beta.35"
lodash "^4.2.0"
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
......@@ -803,6 +811,15 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@7.0.0-beta.35":
version "7.0.0-beta.35"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.35.tgz#cf933a9a9a38484ca724b335b88d83726d5ab960"
integrity sha512-y9XT11CozHDgjWcTdxmhSj13rJVXpa5ZXwjjOiTedjaM0ba5ItqdS02t31EhPl7HtOWxsZkYCCUNrSfrOisA6w==
dependencies:
esutils "^2.0.2"
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@babel/types@^7.14.5", "@babel/types@^7.7.0":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff"
......@@ -872,6 +889,122 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@interactjs/actions@1.10.11", "@interactjs/actions@^1.10.2":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/actions/-/actions-1.10.11.tgz#ec68fd60bee751f80c650964b5ba299eb6afe78c"
integrity sha512-P39zeefr4hkmKx+5nZ+mrH1s0l2YJ3gIHrthXmE81n6MlMa42m0WtHcTms4C5JTTNBP2EEDY+KGgGxSnmJKvUw==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/auto-scroll@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/auto-scroll/-/auto-scroll-1.10.11.tgz#0c0ac7dbb55aa7d7df6c0a04c77ebb3148cbdf54"
integrity sha512-feHNjhi0EMNLV2nQcEgjYPz2mI54aeSW2RiaoNtFLyBvtXKp0b4DmluwDv6DvuXmUpDwD5g/Hk1gGM2rgl7iqQ==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/auto-start@1.10.11", "@interactjs/auto-start@^1.10.2":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/auto-start/-/auto-start-1.10.11.tgz#5ce045740f35be36640ebad053db7b5652e18e70"
integrity sha512-cIg5CcalCPtC6AiGq6j/0hKUtL2MweEpvw12FuB19sz2Q9Dye0J4GliHKhOYvtumNinnvfVAZ4FZMqZEuX7YZA==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/core@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/core/-/core-1.10.11.tgz#8b0203492c1ba6f8432f20b718ae53707fcfc724"
integrity sha512-aJ50ccVeszpJt7wPH7Yfqm7f1aG1SA94qd90P0NaESh5/QUXn4CESO6igobo4DFHQ5z+1Rfdl8aphP4JxlH4gw==
"@interactjs/dev-tools@1.10.11", "@interactjs/dev-tools@^1.10.2":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/dev-tools/-/dev-tools-1.10.11.tgz#8d4b5b650cf74e800909f52962008700143a4304"
integrity sha512-BP2FNfMbF7zLuOAUGMkDhCo1e1B0fnqyb9ih/Y8yAIJuoLrZxP/9htbsS1vZOIVZ4UgtrId4cYOwfcAZBMQtmw==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/inertia@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/inertia/-/inertia-1.10.11.tgz#58864173310985b8247d84e347148ea6cd7b88a8"
integrity sha512-h+sknCzRqBSyHy4ctPNsq56mxkAMMdwHWD6en7rDEw899gdGKYaXVDVdv1jMfiwNRw0eRFBNoCiol8r3a/a3Jw==
dependencies:
"@interactjs/offset" "1.10.11"
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/interact@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/interact/-/interact-1.10.11.tgz#d96e3f949ee4001a6a34dc363a232646f9dd2b1b"
integrity sha512-0iZJ9l547JuBA/lKxK4ARGYVmMqRSsAdA8gXL1zWe51qEIQq8PyWmMipoi8JbDaL7exC2THKwkXu5uq5ndT+iA==
dependencies:
"@interactjs/core" "1.10.11"
"@interactjs/types" "1.10.11"
"@interactjs/utils" "1.10.11"
"@interactjs/interactjs@^1.10.2":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/interactjs/-/interactjs-1.10.11.tgz#d0fdd6b03c1c855043b1f608a10b2f5ccac4b4b7"
integrity sha512-cGOxf6rp3Y8/sk88LhIT0XDn4gCiCzAnUG5Kkj9SAqiUO6BK/9+Wbp1IBkNaPgl/8uG8gNHh/dXBrlBBNcqJAg==
dependencies:
"@interactjs/actions" "1.10.11"
"@interactjs/auto-scroll" "1.10.11"
"@interactjs/auto-start" "1.10.11"
"@interactjs/core" "1.10.11"
"@interactjs/dev-tools" "1.10.11"
"@interactjs/inertia" "1.10.11"
"@interactjs/interact" "1.10.11"
"@interactjs/modifiers" "1.10.11"
"@interactjs/offset" "1.10.11"
"@interactjs/pointer-events" "1.10.11"
"@interactjs/reflow" "1.10.11"
"@interactjs/utils" "1.10.11"
"@interactjs/modifiers@1.10.11", "@interactjs/modifiers@^1.10.2":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/modifiers/-/modifiers-1.10.11.tgz#f40962a97fd3e110e66b79664796c24f3c4e6cd6"
integrity sha512-ltqX1RSqeAIikixlQBlyEUdclT5+rbfIGi3sIdLLYaIZQnltYkWqL9MHKx/w5b+hV+Mc0p5MLUFWJbTdkSCZ9g==
dependencies:
"@interactjs/snappers" "1.10.11"
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/offset@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/offset/-/offset-1.10.11.tgz#512242f330dc80cdbda4feda8fb34c0491f50496"
integrity sha512-mBT7eIfy5ivofECiv+VwtEwwIMLV54fT9ujSMWJPduxdSYIHepUWgEf/3zjJknFh6jQc7pqz9dtjvVvyzRCLlQ==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/pointer-events@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/pointer-events/-/pointer-events-1.10.11.tgz#ff4c74a75d7711fc1006ebf32ea344e35bffe938"
integrity sha512-yBT8JJVMZ+MgBay5l1WAHnL8ch/mZsRfaFahti+QFYeQyRloDtsWmEMDSYI/Onyy9+hS3gN/ge77ArGciZZ0Ow==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/reflow@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/reflow/-/reflow-1.10.11.tgz#43d2ad8ca002bf98091273d179fd70b1cabfb9e2"
integrity sha512-NSCtcCkjImOYSbxzzv2kFqR9t49J8KlhEr9UoePc7GyLbNXsiv3WQ3n0ehZd7CgZXQDiVXnP2UnmIOv5Zd4HQg==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/snappers@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/snappers/-/snappers-1.10.11.tgz#3eb6e45ab8319c0dd4b60b284c55c87561aaadb1"
integrity sha512-yYtOMUZ7aFUZ1IYheq9Tj5hZ4J1r5dnaXhLF44WsI/awQ5L0DjZf07GPWof0B+7rZHEVudxyQNbPfFmb+1K94Q==
optionalDependencies:
"@interactjs/interact" "1.10.11"
"@interactjs/types@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.11.tgz#29be25d503f9c7842df062fa3cda5b044a47cf2a"
integrity sha512-YRsVFWjL8Gkkvlx3qnjeaxW4fnibSJ9791g8BA7Pv5ANByI64WmtR1vU7A2rXcrOn8XvyCEfY0ss1s8NhZP+MA==
"@interactjs/utils@1.10.11":
version "1.10.11"
resolved "https://registry.yarnpkg.com/@interactjs/utils/-/utils-1.10.11.tgz#939d0f128dfa96c673276cca3eb7f313d92daabf"
integrity sha512-410ZoxKF+r1roeSelL+WHXfdryUMg5iykC1XwQ3l6XqNw43IMACzyvTH6k6Pwxj7w7x42nce0Qdn1GQ3Y8xyCw==
"@intervolga/optimize-cssnano-plugin@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz#be7c7846128b88f6a9b1d1261a0ad06eb5c0fdf8"
......@@ -1140,7 +1273,7 @@
resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-3.12.1.tgz#bdfde8f7123561ab06e4e4c60b854cc5092f5ab1"
integrity sha512-Bym92EN+lj+cNRN2ozbYyH+V8DMXWGbCDUk+hiJ4EYDBZfBkZKvalk1/mOBFwyxiopnnbOEBAAhL/UuMQ1xARg==
"@vue/cli-plugin-babel@^3.0.3":
"@vue/cli-plugin-babel@^3.3.0":
version "3.12.1"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-3.12.1.tgz#9a79159de8cd086b013fa6d78a39830b2e2ec706"
integrity sha512-Zetvz8PikLCGomeKOKu8pC9YQ7cfxs7pGpvEOzaxGdhMnebhjAYR6i6dOB57A6N5lhxQksXCtYTv26QgfiIpdg==
......@@ -1151,7 +1284,7 @@
babel-loader "^8.0.5"
webpack "^4.0.0"
"@vue/cli-plugin-eslint@^3.0.3":
"@vue/cli-plugin-eslint@^3.3.0":
version "3.12.1"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-3.12.1.tgz#302c463867f38e790bb996eafdf7159c782dc8cf"
integrity sha512-tVTZlEZsy3sQbO4LLWFK11yzlWwqVAqaM+IY+BeWHITBzEJKh2KmouG+x6x/reXiU3qROsMJ4Ej3Hs8buSMWyQ==
......@@ -1166,7 +1299,7 @@
eslint "^4.19.1"
eslint-plugin-vue "^4.7.1"
"@vue/cli-service@^3.0.3":
"@vue/cli-service@^3.3.0":
version "3.12.1"
resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-3.12.1.tgz#13220b1c189254e7c003390df329086f9b6e77e6"
integrity sha512-PDxNrTGnSKzeV1ruFlsRIAO8JcPizwT0EJXq9GeyooU+p+sOkv7aKkCBJQVYNjZapD1NOGWx6CvAAC/wAW+gew==
......@@ -1879,6 +2012,13 @@ async-validator@^3.0.3:
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-3.5.2.tgz#68e866a96824e8b2694ff7a831c1a25c44d5e500"
integrity sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==
async-validator@~1.8.1:
version "1.8.5"
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-1.8.5.tgz#dc3e08ec1fd0dddb67e60842f02c0cd1cec6d7f0"
integrity sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==
dependencies:
babel-runtime "6.x"
async@^2.4.0, async@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
......@@ -1948,7 +2088,7 @@ babel-eslint@^10.0.1, babel-eslint@^10.1.0:
eslint-visitor-keys "^1.0.0"
resolve "^1.12.0"
babel-helper-vue-jsx-merge-props@^2.0.3:
babel-helper-vue-jsx-merge-props@^2.0.0, babel-helper-vue-jsx-merge-props@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
integrity sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==
......@@ -1963,6 +2103,13 @@ babel-loader@^8.0.5:
make-dir "^3.1.0"
schema-utils "^2.6.5"
babel-plugin-component@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/babel-plugin-component/-/babel-plugin-component-1.1.1.tgz#9b023a23ff5c9aae0fd56c5a18b9cab8c4d45eea"
integrity sha512-WUw887kJf2GH80Ng/ZMctKZ511iamHNqPhd9uKo14yzisvV7Wt1EckIrb8oq/uCz3B3PpAW7Xfl7AkTLDYT6ag==
dependencies:
"@babel/helper-module-imports" "7.0.0-beta.35"
babel-plugin-dynamic-import-node@^2.2.0, babel-plugin-dynamic-import-node@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
......@@ -2044,6 +2191,11 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
batch-processor@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8"
integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=
batch@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
......@@ -3694,7 +3846,7 @@ deepmerge@1.3.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.3.2.tgz#1663691629d4dbfe364fa12a2a4f0aa86aa3a050"
integrity sha1-FmNpFinU2/42T6EqKk8KqGqjoFA=
deepmerge@^1.5.2:
deepmerge@^1.2.0, deepmerge@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
......@@ -4049,6 +4201,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
echarts@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.1.2.tgz#aa1ab0cef5b74fa2f7c620261a5f286893d30fd1"
integrity sha512-okUhO4sw22vwZp+rTPNjd/bvTdpug4K4sHNHyrV8NdAncIX9/AarlolFqtJCAYKGFYhUBNjIWu1EznFrSWTFxg==
dependencies:
tslib "2.0.3"
zrender "5.1.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
......@@ -4064,6 +4224,25 @@ electron-to-chromium@^1.3.723:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09"
integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==
element-resize-detector@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.3.tgz#5078d9b99398fe4c589f8c8df94ff99e5d413ff3"
integrity sha512-+dhNzUgLpq9ol5tyhoG7YLoXL3ssjfFW+0gpszXPwRU6NjGr1fVHMEAF8fVzIiRJq57Nre0RFeIjJwI8Nh2NmQ==
dependencies:
batch-processor "1.0.0"
element-ui@^2.15.3:
version "2.15.3"
resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.15.3.tgz#55108ab82a3bcc646e7b0570871c48ba96300652"
integrity sha512-yGcK0AspuC827Nq7GUHct83cywAKIDo+kpp/rtov5ptmK1hZ8FMlt2SKbcozmSabmpdBNroMgqxqXl6IT1zy1A==
dependencies:
async-validator "~1.8.1"
babel-helper-vue-jsx-merge-props "^2.0.0"
deepmerge "^1.2.0"
normalize-wheel "^1.0.1"
resize-observer-polyfill "^1.5.0"
throttle-debounce "^1.0.1"
elliptic@^6.5.3:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
......@@ -6896,7 +7075,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
......@@ -7298,7 +7477,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2:
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
dependencies:
encoding "^0.1.12"
minipass "^3.1.0"
minipass-sized "^1.0.3"
minizlib "^2.0.0"
......@@ -7727,6 +7905,11 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
normalize-wheel@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=
npm-bundled@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1"
......@@ -9506,7 +9689,7 @@ reselect@^3.0.1:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
resize-observer-polyfill@^1.5.1:
resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
......@@ -10752,6 +10935,11 @@ thread-loader@^2.1.2:
loader-utils "^1.1.0"
neo-async "^2.6.0"
throttle-debounce@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz#51853da37be68a155cb6e827b3514a3c422e89cd"
integrity sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg==
through2@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
......@@ -10922,6 +11110,11 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
tslib@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==
tslib@^1.10.0, tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
......@@ -11417,6 +11610,18 @@ vue-eslint-parser@^7.8.0:
lodash "^4.17.21"
semver "^6.3.0"
vue-grid-layout@^2.3.12:
version "2.3.12"
resolved "https://registry.yarnpkg.com/vue-grid-layout/-/vue-grid-layout-2.3.12.tgz#b6396357b86a66805c117431d7c193d2e066edda"
integrity sha512-x9l4KxfH0MeB4xImanrnnTihksq8LYk3f40hm1sdiTHF2bYM+Xhae6eQsvFWEFwbYq7RVNvB80qwis1vInB+WQ==
dependencies:
"@interactjs/actions" "^1.10.2"
"@interactjs/auto-start" "^1.10.2"
"@interactjs/dev-tools" "^1.10.2"
"@interactjs/interactjs" "^1.10.2"
"@interactjs/modifiers" "^1.10.2"
element-resize-detector "^1.2.1"
vue-hot-reload-api@^2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
......@@ -11469,7 +11674,12 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@^2.5.17:
vue-treeselect@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/vue-treeselect/-/vue-treeselect-1.0.7.tgz#589f689019b6c91d1b3bba946dbdb63565b13f05"
integrity sha512-NNtr0elV/diw9KSmkiXhKzUaG9FPx8PSg5dHXcl288ip94qfUyXnA5M6mgtWREMlw4Ufs4+lQHDuCONy7sIccg==
vue@^2.6.12:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
......@@ -11922,3 +12132,10 @@ yorkie@^2.0.0:
is-ci "^1.0.10"
normalize-path "^1.0.0"
strip-indent "^2.0.0"
zrender@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.1.1.tgz#0515f4f8cc0f4742f02a6b8819550a6d13d64c5c"
integrity sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==
dependencies:
tslib "2.0.3"
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