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',
]
}
This diff is collapsed.
......@@ -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",
},
......
This diff is collapsed.
......@@ -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}
}
for item in payload.get("image"):
item.update({
'length': 0,
'png_total_count': 0,
'png_uncompressed_size': 0,
'png_file_size': 0
while True:
for item in payload.get("image"):
item.update({
'length': 0,
'png_total_count': 0,
'png_uncompressed_size': 0,
'png_file_size': 0
})
rand_index = random.randint(0, len(payload.get("image")))
if rand_index < len(payload.get("image")):
print("------------------------------>")
else:
rand_index = rand_index - 1
print("rand_index ==>", rand_index)
payload.get("image")[rand_index].update({
'length': random.randint(0, 10000),
'png_total_count': random.randint(0, 10000),
'png_uncompressed_size': random.randint(100, 100000),
'png_file_size': random.randint(0, 10000)
})
rand_index = random.randint(0, len(payload.get("image")))
if rand_index < len(payload.get("image")):
print("------------------------------>")
else:
rand_index = rand_index - 1
print("rand_index ==>", rand_index)
r = requests.post(
"http://localhost:3000/api/v1/evm_store/monitor", data=json.dumps(payload))
print(r.status_code)
print(r.json())
payload.get("image")[rand_index].update({
'length': random.randint(0, 10000),
'png_total_count': random.randint(0, 10000),
'png_uncompressed_size': random.randint(100, 100000),
'png_file_size': random.randint(0, 10000)
})
time.sleep(3)
r = requests.post("http://localhost:3000/api/v1/evm_store/monitor", data=json.dumps(payload))
print(r.status_code)
print(r.json())
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)
t = Timer(3, send_request)
t.run()
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; // 大小
This diff is collapsed.
<template>
<a-page-header-wrapper
:loading="false"
:tabList="tabList"
tabActiveKey="articles"
:tabChange="tabChange"
>
menu
</a-page-header-wrapper>
</template>
<script>
import { Avatar, Row, Col, Card, List } from "ant-design-vue";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
export default {
data: () => ({
activitiesLoading: true,
projectLoading: false,
tabList: [
{
key: "articles",
tab: "菜单列表",
},
{
key: "application",
tab: "应用列表",
},
],
}),
components: {
APageHeaderWrapper: PageHeaderWrapper,
AAvatar: Avatar,
ARow: Row,
ACol: Col,
ACard: Card,
ACardGrid: Card.Grid,
ACardMeta: Card.Meta,
AList: List,
},
methods: {
tabChange(e) {
window.console.log(e);
},
},
};
</script>
<style lang="less">
</style>
\ No newline at end of file
......@@ -629,6 +629,10 @@ export default {
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"
>
menu
</a-page-header-wrapper>
<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"
>
<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>
\ No newline at end of file
<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>
This diff is collapsed.
<template>
<div class="table-layout">
<!-- 头部 -->
<div v-if="withBreadcrumb" class="table-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="path in paths" :key="path">{{path}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<Profile :roles="roles" :permissions="permissions">
<!-- 搜索表单部分 -->
<div class="table-search-form">
<div class="form-wrap">
<slot name="search-form"></slot>
</div>
</div>
<slot name="space"></slot>
<!-- 列表和分页部分 -->
<div class="table-content">
<div class="table-wrap">
<slot name="table-wrap"></slot>
</div>
</div>
<slot></slot>
</Profile>
</div>
</template>
<script>
import { Breadcrumb, BreadcrumbItem } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
import Profile from './common/Profile'
export default {
name: 'TableLayout',
components: {
"el-breadcrumb": Breadcrumb,
"el-breadcrumb-item": BreadcrumbItem,
Profile
},
props: {
// 角色
roles: {
type: Array
},
// 权限
permissions: {
type: Array
},
// 是否展示头部面包屑
withBreadcrumb: {
type: Boolean,
default: true
}
},
computed: {
paths () {
return this.$route.meta.paths
}
}
}
</script>
<style lang="scss">
@import "@/assets/style/variables.scss";
.table-layout {
height: 100%;
display: flex;
flex-direction: column;
.not-allow-wrap {
padding-top: 0;
}
}
// 头部
.table-header {
overflow: hidden;
padding: 12px 16px;
flex-shrink: 0;
// 页面路径
.el-breadcrumb {
.el-breadcrumb__item {
.el-breadcrumb__inner {
color: #ABB2BE;
font-size: 12px;
}
&:last-of-type .el-breadcrumb__inner {
color: #606263;
font-size: 14px;
}
}
}
}
// 搜索
.table-search-form {
display: flex;
flex-wrap: wrap;
padding: 0 16px;
.form-wrap {
padding: 16px 16px 0 16px;
width: 100%;
background: #fff;
&:empty {
padding: 0;
}
}
section {
display: inline-block;
margin-left: 16px;
margin-bottom: 18px;
}
}
// 列表和分页
.table-content {
margin-top: 10px;
padding: 0 16px;
.table-wrap {
padding: 16px 16px 0 16px;
background: #fff;
// 工具栏
.toolbar {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
li {
display: inline-block;
margin-right: 6px;
}
}
// 表格
.el-table {
th {
.cell {
color: #666;
}
}
// 复选框列
.el-table-column--selection {
.cell {
text-align: center !important;
}
}
// 多值字段
.table-column-strings {
ul {
li {
display: inline-block;
background: #eee;
border-radius: 3px;
padding: 0 3px;
margin-right: 3px;
margin-bottom: 3px;
}
}
}
// 树视觉调整
[class*=el-table__row--level] .el-table__expand-icon {
position: relative;
left: -6px;
margin-right: 0;
}
}
// 分页
.table-pagination {
padding: 16px 0;
text-align: left;
}
}
}
</style>
<script>
export default {
name: 'BaseOpera',
data () {
return {
title: '',
visible: false,
isWorking: false,
// 接口
api: null,
// 配置数据
configData: {
'field.id': 'id'
}
}
},
methods: {
// 配置
config (extParams = {}) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = require('@/api' + extParams.api)
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
},
/**
* 打开窗口
* @title 窗口标题
* @target 编辑的对象
*/
open (title, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
// 确认新建/修改
confirm () {
if (this.form.id == null || this.form.id === '') {
this.__confirmCreate()
return
}
this.__confirmEdit()
},
// 确认新建
__confirmCreate () {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
// 调用新建接口
this.isWorking = true
this.api.create(this.form)
.then(() => {
this.visible = false
this.$tip.apiSuccess('新建成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
},
// 确认修改
__confirmEdit () {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
// 调用新建接口
this.isWorking = true
this.api.updateById(this.form)
.then(() => {
this.visible = false
this.$tip.apiSuccess('修改成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
}
}
}
</script>
<script>
import { mapState } from 'vuex'
export default {
name: 'BasePage',
data () {
return {
// 超级管理员角色code
adminCode: 'admin'
}
},
computed: {
...mapState(['userInfo']),
// 是否为超级管理员
isAdmin () {
return this.userInfo.roles.findIndex(code => code === this.adminCode) > -1
}
},
methods: {
// 是否包含指定角色
containRoles (roles) {
if (roles == null) {
return true
}
if (this.userInfo == null) {
return false
}
if (this.userInfo.roles == null || this.userInfo.roles.length === 0) {
return false
}
for (const code of roles) {
if (this.userInfo.roles.findIndex(r => r === code) > -1) {
return true
}
}
return false
},
// 是否包含指定权限
containPermissions (permissions) {
if (permissions == null) {
return true
}
if (this.userInfo == null) {
return false
}
if (this.userInfo.permissions == null || this.userInfo.permissions.length === 0) {
return false
}
for (const code of permissions) {
if (this.userInfo.permissions.findIndex(p => p === code) > -1) {
return true
}
}
return false
}
}
}
</script>
<script>
import BasePage from './BasePage'
export default {
name: 'BaseTable',
extends: BasePage,
data () {
return {
// 接口
api: null,
// 模块名称
module: '数据',
// 配置数据
configData: {
// id字段
'field.id': 'id',
// 主字段
'field.main': 'name'
},
// 是否正在执行
isWorking: {
// 搜索中
search: false,
// 删除中
delete: false,
// 导出中
export: false
},
// 表格数据
tableData: {
// 已选中的数据
selectedRows: [],
// 排序的字段
sorts: [],
// 当前页数据
list: [],
// 分页
pagination: {
pageIndex: 1,
pageSize: 10,
total: 0
}
}
}
},
methods: {
// 配置
config (extParams) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = require('@/api' + extParams.api)
extParams.module && (this.module = extParams.module)
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
extParams['field.main'] && (this.configData['field.main'] = extParams['field.main'])
this.tableData.sorts = extParams.sorts
},
// 搜索
search () {
this.handlePageChange(1)
},
// 导出Excel
exportExcel () {
this.__checkApi()
this.$dialog.exportConfirm('确认导出吗?')
.then(() => {
this.isWorking.export = true
this.api.exportExcel({
page: this.tableData.pagination.pageIndex,
capacity: 1000000,
model: this.searchForm,
sorts: this.tableData.sorts
})
.then(response => {
this.download(response)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.export = false
})
})
.catch(() => {})
},
// 搜索框重置
reset () {
this.$refs.searchForm.resetFields()
this.search()
},
// 每页显示数量变更处理
handleSizeChange (pageSize) {
this.tableData.pagination.pageSize = pageSize
this.search()
},
// 行选中处理
handleSelectionChange (selectedRows) {
this.tableData.selectedRows = selectedRows
},
// 排序
handleSortChange (sortData) {
this.tableData.sorts = []
if (sortData.order != null) {
this.tableData.sorts.push({
property: sortData.column.sortBy,
direction: sortData.order === 'descending' ? 'DESC' : 'ASC'
})
}
this.handlePageChange()
},
// 页码变更处理
handlePageChange (pageIndex) {
this.__checkApi()
this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
this.isWorking.search = true
this.api.fetchList({
page: this.tableData.pagination.pageIndex,
capacity: this.tableData.pagination.pageSize,
model: this.searchForm,
sorts: this.tableData.sorts
})
.then(data => {
this.tableData.list = data.records
this.tableData.pagination.total = data.total
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
},
// 删除
deleteById (row, childConfirm = true) {
this.__checkApi()
let message = `确认删除${this.module}${row[this.configData['field.main']]}】吗?`
if (childConfirm && row.children != null && row.children.length > 0) {
message = `确认删除${this.module}${row[this.configData['field.main']]}】及其子${this.module}吗?`
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
this.api.deleteById(row[this.configData['field.id']])
.then(() => {
this.$tip.apiSuccess('删除成功')
this.__afterDelete()
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
})
.catch(() => {})
},
/**
* 批量删除
* @treeMode 是否添加子节点删除确认
*/
deleteByIdInBatch (childConfirm = true) {
this.__checkApi()
if (this.tableData.selectedRows.length === 0) {
this.$tip.warning('请至少选择一条数据')
return
}
let message = `确认删除已选中的 ${this.tableData.selectedRows.length}${this.module}记录吗?`
if (childConfirm) {
const containChildrenRows = []
for (const row of this.tableData.selectedRows) {
if (row.children != null && row.children.length > 0) {
containChildrenRows.push(row[this.configData['field.main']])
}
}
if (containChildrenRows.length > 0) {
message = `本次将删除${this.module}${containChildrenRows.join('')}】及其子${this.module}记录,确认删除吗?`
}
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
this.api.deleteByIdInBatch(this.tableData.selectedRows.map(row => row.id).join(','))
.then(() => {
this.$tip.apiSuccess('删除成功')
this.__afterDelete(this.tableData.selectedRows.length)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
})
.catch(() => {})
},
// 删除处理
__afterDelete (deleteCount = 1) {
// 删除当前页最后一条记录时查询上一页数据
if (this.tableData.list.length - deleteCount === 0) {
this.handlePageChange(this.tableData.pagination.pageIndex - 1 === 0 ? 1 : this.tableData.pagination.pageIndex - 1)
} else {
this.handlePageChange(this.tableData.pagination.pageIndex)
}
},
// 检查接口是否配置
__checkApi () {
if (this.api == null) {
throw new Error('The page is not initialized, you can use method \'this.config\' to initialize this page.')
}
}
}
}
</script>
<template>
<span v-if="content.length <= limit">{{content}}</span>
<el-popover
v-else
v-model="visible"
popper-class="eva-column-detail-popover"
trigger="click"
>
<div class="eva-column-detail">
<pre class="eva-column-detail__main">{{formattedContent}}</pre>
<div class="eva-column-detail__action">
<el-button size="mini" @click="cancel">关闭</el-button>
<el-button
size="mini"
type="primary"
v-clipboard:copy="formattedContent"
v-clipboard:success="copySuccess"
v-clipboard:error="copyFailed"
@click="confirm"
>{{ confirmButtonText }}</el-button>
</div>
</div>
<el-button slot="reference" :type="buttonType">查看</el-button>
</el-popover>
</template>
<script>
export default {
name: 'ColumnDetail',
props: {
// 按钮类型
buttonType: {
type: String
},
// 内容
content: {
type: String,
default: ''
},
// 限制,大于限制时展示查看按钮
limit: {
type: Number,
default: 12
},
// 自动识别数据类型并格式化
analyse: {
type: Boolean,
default: true
},
// 是否允许复制
allowCopy: {
type: Boolean,
default: true
}
},
data () {
return {
visible: false
}
},
computed: {
// 确认按钮文案
confirmButtonText () {
return this.allowCopy ? '复制' : '确定'
},
// 格式化后的内容
formattedContent () {
let content = this.content
if (this.analyse) {
try {
content = JSON.stringify(JSON.parse(this.content), null, 2)
} catch (e) {
}
}
return content
}
},
methods: {
// 点击确认
confirm () {
this.visible = false
this.$emit('confirm')
},
// 点击取消
cancel () {
this.visible = false
this.$emit('cancel')
},
// 复制成功
copySuccess () {
this.$tip.success('复制成功')
},
// 复制失败
copyFailed () {
this.$tip.error('复制失败')
}
}
}
</script>
<style lang="scss">
.eva-column-detail-popover {
max-width: 80%;
}
</style>
<style scoped lang="scss">
.eva-column-detail {
.eva-column-detail__main {
max-height: 500px;
overflow: auto;
}
.eva-column-detail__action {
text-align: right;
}
}
</style>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:clearable="clearable"
:append-to-body="appendToBody"
:inline="inline"
:multiple="multiple"
:flat="multiple"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/department'
export default {
name: 'DepartmentSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
multiple: {
default: false
},
placeholder: {
default: '请选择部门'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有部门
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充部门树
__fillData (list, pool) {
for (const dept of pool) {
if (dept.id === this.excludeId) {
continue
}
const deptNode = {
id: dept.id,
label: dept.name
}
list.push(deptNode)
if (dept.children != null && dept.children.length > 0) {
deptNode.children = []
this.__fillData(deptNode.children, dept.children)
if (deptNode.children.length === 0) {
deptNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<template>
<el-drawer
class="global-window"
title="title"
:visible="visible"
:with-header="true"
:size="width"
:close-on-press-escape="false"
:wrapper-closable="false"
:append-to-body="true"
@close="close"
>
<div slot="title" class="window__header">
<span class="header__btn-back" @click="close"><i class="el-icon-arrow-left"></i></span>{{title}}
</div>
<div class="window__body">
<slot></slot>
</div>
<div v-if="withFooter" class="window__footer">
<slot name="footer">
<el-button @click="confirm" :loading="confirmWorking" type="primary">确定</el-button>
<el-button @click="close">取消</el-button>
</slot>
</div>
</el-drawer>
</template>
<script>
import { Drawer, Button } from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
export default {
name: 'GlobalWindow',
props: {
width: {
type: String,
default: '36%'
},
// 是否包含底部操作
withFooter: {
type: Boolean,
default: true
},
// 确认按钮loading状态
confirmWorking: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ''
},
// 是否展示
visible: {
type: Boolean,
required: true
}
},
components: {
"el-drawer": Drawer,
"el-button": Button
},
methods: {
confirm () {
this.$emit('confirm')
},
close () {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/variables.scss";
// 输入框高度
$input-height: 32px;
.global-window {
// 头部标题
/deep/ .el-drawer__header {
padding: 0 10px 0 0;
line-height: 40px;
border-bottom: 1px solid #eee;
// 返回按钮
.header__btn-back {
display: inline-block;
width: 30px;
background: $primary-color;
color: #fff;
text-align: center;
margin-right: 12px;
border-right: 1px solid #eee;
}
.el-drawer__close-btn:focus {
outline: none;
}
}
// 主体
/deep/ .el-drawer__body {
display: flex;
flex-direction: column;
position: absolute;
top: 40px;
bottom: 0;
width: 100%;
overflow: hidden;
// 内容
.window__body {
height: 100%;
overflow-y: auto;
padding: 12px 16px;
// 标签
.el-form-item__label {
float: none;
}
// 元素宽度为100%
.el-form-item__content > *{
width: 100%;
}
}
// 尾部
.window__footer {
user-select: none;
border-top: 1px solid #eee;
height: 60px;
line-height: 60px;
text-align: center;
}
}
}
</style>
<template>
<div class="main-header">
<div class="header">
<h2>
<i class="el-icon-s-unfold" v-if="menuData.collapse" @click="switchCollapseMenu(null)"></i>
<i class="el-icon-s-fold" v-else @click="switchCollapseMenu(null)"></i>
{{title}}
</h2>
<div class="user">
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img v-if="userInfo != null" :src="userInfo.avatar == null ? '@/assets/images/avatar/man.png' : userInfo.avatar" alt="">{{userInfo | displayName}}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changePwd">修改密码</el-dropdown-item>
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 修改密码 -->
<GlobalWindow
title="修改密码"
:visible.sync="visible.changePwd"
@confirm="confirmChangePwd"
@close="visible.changePwd = false"
>
<el-form :model="changePwdData.form" ref="changePwdDataForm" :rules="changePwdData.rules">
<el-form-item label="原始密码" prop="oldPwd" required>
<el-input v-model="changePwdData.form.oldPwd" type="password" placeholder="请输入原始密码" maxlength="30" show-password></el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPwd" required>
<el-input v-model="changePwdData.form.newPwd" type="password" placeholder="请输入新密码" maxlength="30" show-password></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPwd" required>
<el-input v-model="changePwdData.form.confirmPwd" type="password" placeholder="请再次输入新密码" maxlength="30" show-password></el-input>
</el-form-item>
</el-form>
</GlobalWindow>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import GlobalWindow from './GlobalWindow'
import { logout, updatePwd } from '@/api/system/common'
export default {
name: 'Header',
components: { GlobalWindow },
data () {
return {
visible: {
// 修改密码
changePwd: false
},
isWorking: {
// 修改密码
changePwd: false
},
username: 'bob', // 用户名
// 修改密码弹框
changePwdData: {
form: {
oldPwd: '',
newPwd: '',
confirmPwd: ''
},
rules: {
oldPwd: [
{ required: true, message: '请输入原始密码' }
],
newPwd: [
{ required: true, message: '请输入新密码' }
],
confirmPwd: [
{ required: true, message: '请再次输入新密码' }
]
}
}
}
},
computed: {
...mapState(['menuData', 'userInfo']),
title () {
return this.$route.meta.title
}
},
filters: {
// 展示名称
displayName (userInfo) {
if (userInfo == null) {
return ''
}
if (userInfo.realname != null && userInfo.realname.trim().length > 0) {
return userInfo.realname
}
return userInfo.username
}
},
methods: {
...mapMutations(['setUserInfo', 'switchCollapseMenu']),
// 修改密码
changePwd () {
this.visible.changePwd = true
this.$nextTick(() => {
this.$refs.changePwdDataForm.resetFields()
})
},
// 确定修改密码
confirmChangePwd () {
if (this.isWorking.changePwd) {
return
}
this.$refs.changePwdDataForm.validate((valid) => {
if (!valid) {
return
}
// 验证两次密码输入是否一致
if (this.changePwdData.form.newPwd !== this.changePwdData.form.confirmPwd) {
this.$tip.warning('两次密码输入不一致')
return
}
// 执行修改
this.isWorking.changePwd = true
updatePwd({
oldPwd: this.changePwdData.form.oldPwd,
newPwd: this.changePwdData.form.newPwd
})
.then(() => {
this.$tip.apiSuccess('修改成功')
this.visible.changePwd = false
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.changePwd = false
})
})
},
// 退出登录
logout () {
logout()
.then(() => {
this.$router.push({ name: 'login' })
this.setUserInfo(null)
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.header {
overflow: hidden;
padding: 0 25px;
background: #fff;
height: 100%;
display: flex;
h2 {
width: 50%;
flex-shrink: 0;
line-height: $header-height;
font-size: 19px;
font-weight: 600;
color: #606263;
display: inline;
& > i {
font-size: 20px;
margin-right: 12px;
}
}
.user {
width: 50%;
flex-shrink: 0;
text-align: right;
.el-dropdown {
top: 2px;
}
img {
width: 32px;
position: relative;
top: 10px;
margin-right: 10px;
}
}
}
// 下拉菜单框
.el-dropdown-menu {
width: 140px;
.el-dropdown-menu__item:hover {
background: #E3EDFB;
color: $primary-color;
}
}
</style>
<template>
<div class="light" :class="{normal: !warn && !danger, warn: !danger && warn, danger, mini: mini}">
<em><i></i></em>
</div>
</template>
<script>
export default {
name: 'Light',
props: {
warn: {
type: Boolean,
default: false
},
danger: {
type: Boolean,
default: false
},
mini: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped lang="scss">
$cycle-size01: 16px;
$cycle-size02: 6px;
$normal-color: #00CC99;
$warn-color: #FFCC33;
$danger-color: #FF3300;
@mixin light-status ($cycle-bg) {
em {
background: $cycle-bg;
i {
background: $cycle-bg - 30;
}
}
}
.light {
display: inline-block;
border-radius: 50%;
em {
width: $cycle-size01;
height: $cycle-size01;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
i {
display: block;
width: $cycle-size02;
height: $cycle-size02;
border-radius: 50%;
}
}
&.mini {
em {
width: 12px;
height: 12px;
}
}
// 正常
&.normal {
@include light-status($normal-color);
animation: shine-normal infinite 1s;
}
// 警告
&.warn {
@include light-status($warn-color);
animation: shine-warn infinite .8s;
}
// 危险
&.danger {
@include light-status($danger-color);
animation: shine-danger infinite .5s;
}
}
@keyframes shine-normal {
0% {
box-shadow: 0 0 5px $normal-color + 10;
}
25% {
box-shadow: 0 0 10px $normal-color + 10;
}
50% {
box-shadow: 0 0 15px $normal-color + 10;
}
100% {
box-shadow: 0 0 20px $normal-color + 10;
}
}
@keyframes shine-warn {
0% {
box-shadow: 0 0 5px $warn-color - 50;
}
25% {
box-shadow: 0 0 10px $warn-color - 50;
}
50% {
box-shadow: 0 0 15px $warn-color - 50;
}
100% {
box-shadow: 0 0 20px $warn-color - 50;
}
}
@keyframes shine-danger {
0% {
box-shadow: 0 0 5px $danger-color + 10;
}
25% {
box-shadow: 0 0 10px $danger-color + 10;
}
50% {
box-shadow: 0 0 15px $danger-color + 10;
}
100% {
box-shadow: 0 0 20px $danger-color + 10;
}
}
</style>
<template>
<el-cascader
v-if="visible"
:props="props"
:placeholder="placeholder"
v-model="value"
:clearable="clearable"
@change="$emit('change')"
@input="handleInput"
></el-cascader>
</template>
<script>
import { fetchByParentId } from '@/api/system/location'
export default {
name: 'LocationSelect',
props: {
placeholder: {
default: '请选择地区'
},
level: {
default: 3
},
clearable: {
default: false
},
// 省
provinceId: {},
// 市
cityId: {},
// 区
areaId: {}
},
data () {
const vm = this
return {
// 是否展示,用于重新初始化cascader
visible: true,
// 已选值
value: [],
// 组件配置
props: {
lazy: true,
lazyLoad (node, resolve) {
const { level } = node
fetchByParentId(level === 0 ? -1 : node.value)
.cache()
.then(data => {
resolve(data.map(item => {
return {
label: item.name,
value: item.id,
leaf: level >= vm.level - 1
}
}))
})
.catch(e => {
vm.$tip.apiFailed(e)
})
}
}
}
},
watch: {
provinceId (newValue) {
this.value[0] = newValue
if (this.level === 1) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
},
cityId (newValue) {
if (this.level >= 2) {
this.value[1] = newValue
}
if (this.level === 2) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
},
areaId (newValue) {
if (this.level >= 3) {
this.value[2] = newValue
}
if (this.level === 3) {
if (newValue == null) {
this.value = []
}
this.__rebuild()
}
}
},
methods: {
handleInput (values) {
this.$emit('update:province-id', values[0])
this.$emit('update:city-id', values[1])
this.$emit('update:area-id', values[2])
},
// 重新初始化cascader
__rebuild () {
this.visible = false
this.$nextTick(() => {
this.visible = true
})
}
}
}
</script>
<template>
<div class="menu" :class="{collapse: menuData.collapse}">
<div class="logo">
<div><img src="/logo.png"></div>
<h1 :class="{hidden: menuData.collapse}">eva</h1>
</div>
<scrollbar>
<el-menu
ref="menu"
:default-active="activeIndex"
text-color="#fff"
active-text-color="#fff"
:collapse="menuData.collapse"
:default-openeds="defaultOpeneds"
:collapse-transition="false"
@select="handleSelect"
>
<MenuItems v-for="menu in menuData.list" :key="menu.index" :menu="menu" :is-root-menu="true"/>
</el-menu>
</scrollbar>
</div>
</template>
<script>
import { mapState } from 'vuex'
import MenuItems from './MenuItems'
import Scrollbar from './Scrollbar'
export default {
name: 'Menu',
components: { Scrollbar, MenuItems },
computed: {
...mapState(['menuData']),
// 选中的菜单index
activeIndex () {
let path = this.$route.path
if (path.endsWith('/')) {
path = path.substring(0, path.length - 1)
}
const menuConfig = this.__getMenuConfig(path, 'url', this.menuData.list)
if (menuConfig == null) {
return null
}
return menuConfig.index
},
// 默认展开的菜单index
defaultOpeneds () {
return this.menuData.list.map(menu => menu.index)
}
},
methods: {
// 处理菜单选中
handleSelect (menuIndex) {
const menuConfig = this.__getMenuConfig(menuIndex, 'index', this.menuData.list)
// 找不到页面
try {
require('@/views' + menuConfig.url)
} catch (e) {
this.$tip.error('未找到页面文件@/views' + menuConfig.url + '.vue,请检查菜单路径是否正确')
}
// 点击当前菜单不做处理
if (menuConfig.url === this.$route.path) {
return
}
if (menuConfig.url == null || menuConfig.url.trim().length === 0) {
return
}
this.$router.push(menuConfig.url)
},
// 获取菜单配置
__getMenuConfig (value, key, menus) {
for (const menu of menus) {
if (menu[key] === value) {
return menu
}
if (menu.children != null && menu.children.length > 0) {
const menuConfig = this.__getMenuConfig(value, key, menu.children)
if (menuConfig != null) {
return menuConfig
}
}
}
return null
}
}
}
</script>
<style lang="scss" scoped>
@import "@/assets/style/variables.scss";
.menu {
height: 100%;
display: flex;
flex-direction: column;
// LOGO
.logo {
height: 60px;
flex-shrink: 0;
line-height: 60px;
overflow: hidden;
display: flex;
background: $primary-color - 20;
padding: 0 16px;
& > div {
width: 32px;
flex-shrink: 0;
margin-right: 12px;
img {
width: 100%;
flex-shrink: 0;
vertical-align: middle;
position: relative;
top: -2px;
}
}
h1 {
font-size: 16px;
font-weight: 500;
transition: opacity ease .3s;
overflow: hidden;
&.hidden {
opacity: 0;
}
}
}
}
</style>
<style lang="scss">
@import "@/assets/style/variables.scss";
// 菜单样式
.el-menu {
border-right: 0 !important;
user-select: none;
background: $primary-color !important;
.el-menu-item {
background: $primary-color;
// 选中状态
&.is-active {
background: $primary-color - 40 !important;
}
// 悬浮
&:hover {
background-color: $primary-color - 12;
}
&:focus {
background: $primary-color;
}
}
// 子菜单
.el-submenu {
.el-submenu__title{
background-color: $primary-color;
}
&.is-active {
.el-submenu__title{
background-color: $primary-color - 20;
}
.el-menu .el-menu-item{
background-color: $primary-color - 20;
// 悬浮
&:hover {
background-color: $primary-color - 30;
}
}
}
// 菜单上下箭头
.el-submenu__title i {
color: #f7f7f7;
}
}
// 菜单图标
i:not(.el-submenu__icon-arrow) {
color: #f7f7f7 !important;
position: relative;
top: -1px;
// 自定义图标
&[class^="eva-icon-"] {
width: 24px;
margin-right: 5px;
background-size: 15px;
}
}
}
</style>
<template>
<el-menu-item v-if="menu.children == null || menu.children.length == 0" :key="menu.index" :index="menu.index">
<i :class="menu.icon"></i>
<span slot="title">{{menu.label}}</span>
</el-menu-item>
<el-submenu v-else :index="menu.index">
<template slot="title">
<i :class="menu.icon"></i>
<span slot="title">{{menu.label}}</span>
</template>
<MenuItems v-for="child in menu.children" :menu="child" :key="child.index"/>
</el-submenu>
</template>
<script>
export default {
name: 'MenuItems',
props: {
menu: {
type: Object,
required: true
}
}
}
</script>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:append-to-body="appendToBody"
:clearable="clearable"
:inline="inline"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/menu'
export default {
name: 'MenuSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
placeholder: {
default: '请选择菜单'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有菜单
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充菜单树
__fillData (list, pool) {
for (const menu of pool) {
if (menu.id === this.excludeId) {
continue
}
const menuNode = {
id: menu.id,
label: menu.name
}
list.push(menuNode)
if (menu.children != null && menu.children.length > 0) {
menuNode.children = []
this.__fillData(menuNode.children, menu.children)
if (menuNode.children.length === 0) {
menuNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<template>
<div class="not-allow">
<slot>
<div class="content">
<img src="../../assets/images/not-allow.png">
<h2>无权访问</h2>
<p>如您需要访问该页面,请联系系统管理员</p>
</div>
</slot>
</div>
</template>
<script>
export default {
name: 'NotAllow'
}
</script>
<style scoped lang="scss">
.not-allow {
height: 100%;
background: #fff;
box-sizing: border-box;
padding-top: 160px;
.content {
height: 200px;
text-align: center;
h2 {
font-size: 18px;
font-weight: normal;
margin-top: 8px;
}
p {
font-size: 13px;
color: #999;
margin: 6px 0;
}
}
}
</style>
<template>
<div class="table-pagination">
<el-pagination
:current-page="pagination.pageIndex"
:page-sizes="[10, 20, 30, 40]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="sizeChange"
@current-change="currentChange"
background>
</el-pagination>
</div>
</template>
<script>
export default {
name: 'Pagination',
props: {
pagination: {
type: Object,
default: function () {
return {}
}
}
},
data () {
return {
}
},
methods: {
sizeChange (value) {
this.$emit('size-change', value)
},
currentChange (value) {
this.$emit('current-change', value)
}
}
}
</script>
<template>
<TreeSelect
:placeholder="placeholder"
:value="value"
:data="data"
:clearable="clearable"
:append-to-body="appendToBody"
:inline="inline"
:multiple="multiple"
:flat="multiple"
@input="$emit('input', $event)"
/>
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/position'
export default {
name: 'PositionSelect',
components: { TreeSelect },
props: {
value: {},
inline: {
default: true
},
multiple: {
default: false
},
placeholder: {
default: '请选择岗位'
},
// 是否可清空
clearable: {
default: false
},
appendToBody: {
default: false
},
// 需被排除的部门ID
excludeId: {}
},
data () {
return {
data: []
}
},
watch: {
excludeId () {
this.fetchData()
}
},
methods: {
// 获取所有岗位
fetchData () {
fetchTree()
.then(records => {
this.data = []
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// 填充岗位树
__fillData (list, pool) {
for (const dept of pool) {
if (dept.id === this.excludeId) {
continue
}
const deptNode = {
id: dept.id,
label: dept.name
}
list.push(deptNode)
if (dept.children != null && dept.children.length > 0) {
deptNode.children = []
this.__fillData(deptNode.children, dept.children)
if (deptNode.children.length === 0) {
deptNode.children = undefined
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>
<style scoped lang="scss">
.inline {
width: 178px;
}
.vue-treeselect {
line-height: 30px;
/deep/ .vue-treeselect__control {
height: 32px;
.vue-treeselect__single-value {
line-height: 30px;
}
}
}
</style>
<template>
<div v-if="containRoles(roles) && containPermissions(permissions)">
<slot></slot>
</div>
<div v-else class="not-allow-wrap">
<slot name="not-allow"><NotAllow/></slot>
</div>
</template>
<script>
import BasePage from '../base/BasePage'
import NotAllow from './NotAllow'
export default {
name: 'Profile',
components: { NotAllow },
extends: BasePage,
props: {
permissions: {
type: Array
},
roles: {
type: Array
}
}
}
</script>
<style scoped lang="scss">
.not-allow-wrap {
height: 100%;
padding: 10px 16px;
box-sizing: border-box;
}
</style>
<template>
<vue-scroll :ops="options">
<slot></slot>
</vue-scroll>
</template>
<script>
import VueScroll from 'vuescroll'
export default {
name: 'Scrollbar',
components: { VueScroll },
data () {
return {
options: {
bar: {
background: 'rgba(20,20,20,.3)'
}
}
}
}
}
</script>
<template>
<div class="search-form-collapse" :class="{'collapse__hidden': !showMore}">
<slot></slot>
<el-button v-if="!showMore" class="collapse__switch" @click="showMore = true">更多查询...</el-button>
<el-button v-else class="collapse__switch" @click="showMore = false">收起</el-button>
</div>
</template>
<script>
export default {
name: 'SearchFormCollapse',
data () {
return {
showMore: false
}
}
}
</script>
<style scoped lang="scss">
.search-form-collapse {
position: relative;
padding-right: 75px;
height: auto;
.collapse__switch {
position: absolute;
top: 0;
right: 0;
}
&.collapse__hidden {
height: 50px;
overflow: hidden;
padding-right: 250px;
/deep/ section {
position: absolute;
top: 0;
right: 100px;
}
}
}
</style>
<!-- 组件详情请参阅官方文档:https://www.vue-treeselect.cn/ -->
<template>
<vue-tree-select
:class="{inline}"
:placeholder="placeholder"
:value="value"
:options="data"
:clearable="clearable"
:flat="flat"
:append-to-body="appendToBody"
:multiple="multiple"
no-children-text="无记录"
no-options-text="无记录"
no-results-text="未匹配到数据"
@input="$emit('input', $event)"
/>
</template>
<script>
import VueTreeSelect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'TreeSelect',
props: {
inline: {
default: false
},
multiple: {
default: false
},
flat: {
default: false
},
value: {},
placeholder: {
default: '请选择'
},
// 是否可清空
clearable: {
default: false
},
data: {
type: Array,
required: true
},
appendToBody: {
default: false
}
},
components: { VueTreeSelect }
}
</script>
<style scoped lang="scss">
.inline {
width: 178px;
}
.vue-treeselect {
line-height: 30px;
/deep/ .vue-treeselect__control {
height: 32px;
.vue-treeselect__single-value {
line-height: 30px;
}
}
}
</style>
<template>
<div class="value">
<i class="el-icon-loading" v-if="data == null"></i>
<slot v-else>{{getValue()}}{{suffix}}</slot>
</div>
</template>
<script>
export default {
name: 'Value',
props: {
data: {
type: Object
},
prop: {
type: String
},
suffix: {
type: String
},
handler: {
type: Function
}
},
methods: {
getValue () {
if (this.data == null) {
return ''
}
if (this.prop == null) {
return this.data
}
const props = this.prop.split('.')
let i = 0
let value = this.data
while (i < props.length) {
value = value[props[i]]
i++
}
if (this.handler == null) {
return value
}
return this.handler(value)
}
}
}
</script>
<style scoped lang="scss">
.value {
word-break: break-all;
.el-icon-loading {
font-size: 16px;
color: #999;
position: relative;
top: 1px;
}
}
</style>
<template>
<component :is="component" :value="values" :inline="false" @input="handleInput" multiple/>
</template>
<script>
export default {
name: 'CustomSelect',
props: {
value: {},
businessCode: {
type: String,
required: true
}
},
computed: {
// vuetreeselect值类型匹配(解决编辑时无法删除已有值的BUG)
values () {
if (this.businessCode === 'DEPARTMENT' || this.businessCode === 'POSITION') {
const values = []
for (const id of this.value) {
values.push(parseInt(id))
}
return values
}
return this.value
},
component () {
// 部门选择器
if (this.businessCode === 'DEPARTMENT') {
return () => import('@/components/common/DepartmentSelect')
}
// 岗位选择器
if (this.businessCode === 'POSITION') {
return () => import('@/components/common/PositionSelect')
}
return null
}
},
methods: {
handleInput (value) {
this.$emit('input', value)
this.$emit('change', value)
}
}
}
</script>
<style scoped>
</style>
<template>
<el-select
class="data-perm-module-select"
:class="{select__block: !inline}"
:value="value"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option v-for="module in modules" :key="module.businessCode" :value="module.businessCode" :label="module.moduleName"/>
</el-select>
</template>
<script>
import { fetchModules } from '@/api/system/dataPermission'
export default {
name: 'DataPermModuleSelect',
props: {
value: {},
placeholder: {
default: '请选择权限模块'
},
inline: {
default: true
},
disabled: {},
clearable: {
default: false
}
},
data () {
return {
modules: []
}
},
created () {
fetchModules()
.cache()
.then(data => {
this.modules = data
})
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>
<template>
<el-select
class="data-perm-type-select"
:class="{select__block: !inline}"
:value="value"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option v-for="type in filterTypes" :key="type.code" :value="type.code" :label="type.remark"/>
</el-select>
</template>
<script>
import { fetchTypes } from '@/api/system/dataPermission'
export default {
name: 'DataPermTypeSelect',
props: {
value: {},
// 模块名称
module: {},
placeholder: {
default: '请选择权限类型'
},
inline: {
default: true
},
disabled: {},
clearable: {
default: false
}
},
data () {
return {
types: []
}
},
computed: {
filterTypes () {
if (this.module == null || this.module === '') {
return []
}
const types = []
for (const type of this.types) {
if (type.modules.length === 0 || type.modules.indexOf(this.module) !== -1) {
types.push(type)
}
}
return types
}
},
created () {
fetchTypes()
.cache()
.then(data => {
this.types = data
})
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>
<template>
<GlobalWindow
:title="title"
:visible.sync="visible"
:confirm-working="isWorking"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="业务模块" prop="businessCode" required>
<DataPermModuleSelect v-model="form.businessCode" :disabled="form.id != null" :inline="false" @change="handleBusinessChange"/>
</el-form-item>
<el-form-item label="角色" prop="roleId" required>
<RoleSelect v-model="form.roleId" :disabled="form.id != null" :inline="false"/>
</el-form-item>
<el-form-item label="权限类型" prop="type" required>
<DataPermTypeSelect v-model="form.type" :module="form.businessCode" :inline="false" @change="handleTypeChange"/>
</el-form-item>
<el-form-item v-show="showCustomData" label="自定义数据" prop="customData">
<CustomSelect v-if="visible" v-model="customData" :business-code="form.businessCode" @change="handleCustomDataChange"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" v-trim :rows="3" maxlength="500"/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import RoleSelect from '@/components/system/role/RoleSelect'
import DataPermModuleSelect from './DataPermModuleSelect'
import DataPermTypeSelect from './DataPermTypeSelect'
import CustomSelect from './CustomSelect'
export default {
name: 'OperaDataPermissionWindow',
extends: BaseOpera,
components: { CustomSelect, RoleSelect, DataPermTypeSelect, DataPermModuleSelect, GlobalWindow },
data () {
return {
// 自定义数据
customData: [],
// 展示自定义数据标识
showCustomData: false,
// 表单数据
form: {
id: null,
businessCode: '',
roleId: '',
type: '',
remark: '',
customData: ''
},
// 验证规则
rules: {
businessCode: [
{ required: true, message: '请选择业务模块' }
],
roleId: [
{ required: true, message: '请选择角色' }
],
type: [
{ required: true, message: '请选择权限类型' }
]
}
}
},
methods: {
/**
* @title 窗口标题
* @target 编辑的对象
*/
open (title, target) {
this.title = title
this.visible = true
// 新建
if (target == null) {
this.$nextTick(() => {
this.customData = []
this.showCustomData = false
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
// 编辑
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
this.customData = this.form.customData == null || this.form.customData === '' ? [] : this.form.customData.split(',')
this.handleTypeChange()
})
},
// 业务模块切换
handleBusinessChange () {
this.form.customData = ''
this.customData = []
this.handleTypeChange()
},
// 权限类型切换
handleTypeChange () {
if ((this.form.type === 11 || this.form.type === 21) && this.form.businessCode != null && this.form.businessCode !== '') {
this.showCustomData = true
} else {
this.showCustomData = false
}
},
// 自定义数据变化
handleCustomDataChange (values) {
this.form.customData = values.join(',')
}
},
created () {
this.config({
api: '/system/dataPermission',
'field.id': 'id'
})
}
}
</script>
<template>
<GlobalWindow
: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="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>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment