1. 数据库运维 2.首页样式

main
赵培友 3 years ago
parent 74831eb690
commit 72818d64d3
  1. 1
      package.json
  2. 26
      src/api/maintenance/database.js
  3. BIN
      src/assets/img/wel/1.png
  4. BIN
      src/assets/img/wel/2.png
  5. BIN
      src/assets/img/wel/3.png
  6. BIN
      src/assets/img/wel/4.png
  7. 93
      src/const/maintenance/database.js
  8. 3
      src/main.js
  9. 2
      src/page/index/top/index.vue
  10. 114
      src/views/maintenance/beifenzhuye.vue
  11. 241
      src/views/maintenance/database1.vue
  12. 399
      src/views/plugin/workflow/process/components/detail.vue
  13. 1
      src/views/plugin/workflow/process/components/form.vue
  14. 1
      src/views/plugin/workflow/process/start.vue
  15. 1243
      src/views/wel/index.vue

@ -16,6 +16,7 @@
"babel-polyfill": "^6.26.0",
"classlist-polyfill": "^1.2.0",
"crypto-js": "^4.0.0",
"echarts": "^5.4.1",
"element-ui": "^2.15.6",
"js-base64": "^2.5.1",
"js-cookie": "^2.2.0",

@ -0,0 +1,26 @@
import request from '@/router/axios';
const prefix = '/api/blade-workflow/database'
// 查询
export const getList = (query) => {
return request({
url: `${prefix}/list`,
method: 'get',
params:query
})
}
// 下载模板
export const dowmLoadTemplate = () => {
return request({
url: `${prefix}/exportTemplate`,
method: 'get',
responseType: 'blob'
})
}
// 数据库类型
export const getDatabaseType = () => {
return request({
url: "/api/blade-system/dict-biz/dictionary?code=database_type",
method: "get",
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,93 @@
export const tableOption = {
index: true,
indexLabel: "序号",
indexWidth: 120,
labelPosition: "top",
selection: false,
border: false,
headerAlign: "left",
align: "left",
menuAlign: "left",
menuHeaderAlign: "left",
menuBtn: true,
editBtn: false,
delBtn: false,
addBtn: false,
tip: false,
searchMenuSpan: 3, //控制搜索按钮
columnBtn: false,
refreshBtn: false,
header: false,
menuWidth: 220,
dialogCustomClass: "custom",
menu: false,
column: [
{
label: "IP地址",
prop: "dataBaseIp",
type: "input",
align: "left",
},
{
label: "端口号",
prop: "dataBasePort",
type: "input",
align: "left",
},
{
label: "数据库实例",
prop: "dataBaseName",
type: "input",
align: "left",
},
{
label: "数据库类型",
prop: "dataBaseType",
type: "input",
align: "left",
},
{
label: "数据库中文名",
prop: "dataBaseAlias",
type: "input",
align: "left",
},
{
label: "系统名称",
prop: "systemName",
type: "input",
align: "left",
},
{
label: "模块名称",
prop: "systemModuleName",
type: "input",
align: "left",
},
{
label: "管理部门",
prop: "deptName",
type: "input",
align: "left",
},
{
label: "运维公司",
prop: "companyName",
type: "input",
align: "left",
},
{
label: "表名",
prop: "dataTableName",
type: "input",
align: "left",
},
{
label: "中文表别名",
prop: "dataTableAlias",
type: "input",
align: "left",
},
],
};

@ -25,7 +25,8 @@ import website from '@/config/website';
import crudCommon from '@/mixins/crud';
// 业务组件
import tenantPackage from './views/system/tenantpackage';
import * as echarts from 'echarts';
Vue.prototype.$echarts = echarts
// 注册全局crud驱动
window.$crudCommon = crudCommon;
// 加载Vue拓展

@ -1,6 +1,6 @@
<template>
<div class="avue-top">
<div class="top-left">{{tag.label}}</div>
<div class="top-left">{{tag.label==="首页"?"数据看板":tag.label}}</div>
<div class="top-right">
<div class="avatar">
<img :src="userInfo.avatar" alt="" width="56px" height="56px">

@ -1,114 +0,0 @@
<template>
<div class="cus-container">
<div class="content">
<div class="main">
<div class="item" v-for="(item, index) in menuList" :key="index">
<div class="item-top">
<div>
<div class="title">{{ item.title }}</div>
<div class="subTitle">{{ item.subTitle }}</div>
</div>
<div class="item-top-right">
<img :src="require(`@/assets/img/gdwel/${item.id}.png`)" alt="" />
</div>
</div>
<div class="btn" @click="createNow(item.id)">立即创建</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
menuList: [
{
id: 1,
title: "数据管理",
subTitle: "日常数据管理维护工作",
},
{
id: 2,
title: "网络终端维护",
subTitle: "网络终端设备的日常维护工作",
},
{
id: 3,
title: "办公系统维护",
subTitle: "日常办公系统软件维护工作",
},
{
id: 4,
title: "系统优化",
subTitle: "日常优化系统相关工作任务",
},
{
id: 5,
title: "网站维护",
subTitle: "网站网页维护相关工作",
},
{
id: 6,
title: "公众号APP维护",
subTitle: "移动端日常维护工作",
},
],
};
},
methods: {
createNow(index) {},
},
};
</script>
<style scoped lang="scss">
.content {
background: #fff;
height: 760px;
min-height: 760px;
padding: 50px;
box-sizing: border-box;
border-radius: 3px;
}
.main {
display: flex;
flex-wrap: wrap;
.item {
padding: 32px 30px 26px 37px;
box-sizing: border-box;
width: 336px;
height: 184px;
border: 1px solid #cfcfcf;
margin-right: 50px;
margin-bottom: 50px;
.item-top {
margin-bottom: 34px;
display: flex;
justify-content: space-between;
}
.title {
font-size: 20px;
color: #333333;
}
.subTitle {
margin-top: 10px;
color: #999999;
font-size: 14px;
}
.btn {
width: 117px;
height: 37px;
border-radius: 100px 100px 100px 100px;
border: 1px solid #cfcfcf;
font-size: 16px;
color: #666;
text-align: center;
line-height: 37px;
}
}
.item:nth-child(4) {
margin-right: 0;
}
}
</style>

@ -0,0 +1,241 @@
<template>
<div class="cus-container">
<el-form :model="searchForm">
<div class="search">
<div style="display: flex; align-items: center">
<el-select
v-model="searchForm.projectInfoId"
placeholder="数据库实例名"
class="search-select"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
>
</el-option>
</el-select>
<el-select
v-model="searchForm.dataBaseType"
placeholder="数据库类型"
class="search-select"
>
<el-option
v-for="item in databaseTypeList"
:key="item.dictKey"
:label="item.dictValue"
:value="item.dictKey"
>
</el-option>
</el-select>
<el-select
v-model="searchForm.manageDeptId"
placeholder="运维公司"
class="search-select"
>
<el-option
v-for="item in deptsList"
:key="item.id"
:label="item.fullName"
:value="item.id"
>
</el-option>
</el-select>
<el-select
v-model="searchForm.maintenanceDeptId"
placeholder="管理部门"
class="search-select"
>
<el-option
v-for="item in usersList"
:key="item.id"
:label="item.name"
:value="item.id"
>
</el-option>
</el-select>
<div style="display: flex">
<el-button class="search-btn" @click="searchHandle(1)"
>查询</el-button
>
<div class="search-reset" @click="searchHandle(2)">
<i class="el-icon-refresh-right" style="font-size: 20px"></i>
</div>
</div>
</div>
<div style="display: flex">
<el-button class="search-btn" @click="download">下载模板</el-button>
<el-upload
accept=".xls,.xlsx"
class="upload-demo"
:show-file-list="false"
:headers="headers"
action="/api/blade-workflow/database/dataImport"
:on-success="handleSuccess"
>
<el-button class="search-btn" style="margin: 0; background: #2ee27c"
>导入</el-button
>
</el-upload>
</div>
</div>
</el-form>
<div style="margin-top: 30px">
<avue-crud
id="avue-id"
ref="crud"
v-model="form"
:option="option"
:data="tableData"
:page.sync="page"
:table-loading="loading"
@current-change="currentChange"
@size-change="sizeChange"
>
<template slot="dataBaseType" slot-scope="{ row }">
{{ row.dataBaseType | dataBaseTypeFl }}
</template>
</avue-crud>
</div>
</div>
</template>
<script>
import {
getList,
dowmLoadTemplate,
getDatabaseType,
} from "@/api/maintenance/database.js";
import { tableOption } from "@/const/maintenance/database.js";
import { getToken } from "@/util/auth";
let that;
export default {
data() {
return {
form: {},
searchForm: {},
option: tableOption,
page: {
current: 1,
total: 0,
size: 10,
},
//
tableData: [],
loading: false,
//
headers: {
"Blade-Auth": "",
},
databaseTypeList: [],
};
},
filters: {
dataBaseTypeFl: (data) => {
for (const i in that.databaseTypeList) {
const element = that.databaseTypeList[i];
if (data == element.dictKey) {
return element.dictValue;
}
}
},
},
beforeCreate() {
that = this;
},
created() {
this.dicBiz();
this.onLoad();
this.headers["Blade-Auth"] = "bearer " + getToken();
},
methods: {
dicBiz() {
getDatabaseType().then((res) => {
this.databaseTypeList = res.data.data;
});
},
//
onLoad() {
this.loading = true;
const { current, size } = this.page;
getList({ current, size, ...this.searchForm }).then((res) => {
const { total, records } = res.data.data;
this.page.total = total;
this.tableData = records;
this.loading = false;
});
},
//
currentChange(currentPage) {
this.page.current = currentPage;
this.onLoad();
},
sizeChange(pageSize) {
this.page.size = pageSize;
this.onLoad();
},
//
searchHandle(index) {
this.page.current = 1;
if (index === 2) {
this.searchForm = {};
}
this.onLoad();
},
//
download() {
dowmLoadTemplate().then((res) => {
const blob = res.data;
const link = document.createElement("a");
let binaryData = [];
binaryData.push(blob);
link.href = window.URL.createObjectURL(new Blob(binaryData));
link.download = "数据库运维模板.xlsx";
document.body.appendChild(link);
link.click();
window.setTimeout(function () {
URL.revokeObjectURL(blob);
document.body.removeChild(link);
}, 0);
this.$message.success("下载模板成功!");
});
},
handleSuccess() {
this.$message.success("导入成功");
this.onLoad();
},
},
};
</script>
<style lang="scss" scoped>
.search {
display: flex;
justify-content: space-between;
}
.search-select {
width: 150px;
margin-right: 20px;
}
.search-input {
width: 288px;
}
.search-btn {
width: 130px;
height: 46px !important;
background: #2e92f6;
color: #fff;
margin: 0 20px;
}
.search-reset {
width: 46px;
height: 44px !important;
background: #ff9130;
margin-left: 0px;
color: #fff;
text-align: center;
line-height: 46px;
}
/deep/ .el-input__inner {
height: 46px;
}
</style>

@ -1,65 +1,106 @@
<template>
<basic-container>
<avue-skeleton :loading="waiting"
avatar
:rows="8">
<avue-affix id="avue-view"
:offset-top="114">
<div class="header">
<avue-title :value="process.processDefinitionName"></avue-title>
<div v-if="process.status != 'todo'">
主题<avue-select v-model="theme"
size="mini"
:clearable="false"
:dic="themeList"></avue-select>
</div>
</div>
</avue-affix>
<avue-skeleton :loading="waiting" avatar :rows="8">
<el-tabs v-model="activeName">
<el-tab-pane label="申请信息"
name="first">
<el-tab-pane label="申请信息" name="first">
<el-card shadow="never">
<div id="printBody"
:class="process.status != 'todo' ? `wf-theme-${theme}`: ''">
<avue-form v-if="summaryOption && ((summaryOption.column && summaryOption.column.length > 0) || (summaryOption.group && summaryOption.group.length > 0))"
v-model="form"
ref="summaryForm"
:option="summaryOption"
:upload-preview="handleUploadPreview"
style="margin-bottom: 20px;"></avue-form>
<avue-form v-if="option && ((option.column && option.column.length > 0) || (option.group && option.group.length > 0))"
v-model="form"
ref="form"
:defaults.sync="defaults"
:option="option"
:upload-preview="handleUploadPreview">
<div
id="printBody"
:class="process.status != 'todo' ? `wf-theme-${theme}` : ''"
>
<avue-form
v-if="
summaryOption &&
((summaryOption.column && summaryOption.column.length > 0) ||
(summaryOption.group && summaryOption.group.length > 0))
"
v-model="form"
ref="summaryForm"
:option="summaryOption"
:upload-preview="handleUploadPreview"
style="margin-bottom: 20px"
></avue-form>
<avue-form
v-if="
option &&
((option.column && option.column.length > 0) ||
(option.group && option.group.length > 0))
"
v-model="form"
ref="form"
:defaults.sync="defaults"
:option="option"
:upload-preview="handleUploadPreview"
>
<template slot="uploadrecord">
<el-table
v-if="tableData.length > 0"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="name" label="附件名称" width="250">
<template slot-scope="scope">
<span
style="color: blue; cursor: pointer"
@click="download(scope.row)"
>{{ scope.row.name }}</span
>
</template>
</el-table-column>
<el-table-column
prop="createUser"
label="上传人"
width="180"
>
</el-table-column>
<el-table-column
prop="createTime"
label="上传时间"
width="240"
>
</el-table-column>
<el-table-column label="操作">
<template>
<i
class="el-icon-delete"
style="cursor: pointer"
@click="deleteFile(index)"
></i>
</template>
</el-table-column>
</el-table>
<span v-else></span>
</template>
</avue-form>
</div>
</el-card>
<el-card shadow="never"
style="margin-top: 20px"
v-if="process.status == 'todo'">
<wf-examine-form ref="examineForm"
:comment.sync="comment"
:process="process"
@user-select="handleUserSelect"></wf-examine-form>
<el-card
shadow="never"
style="margin-top: 20px"
v-if="process.status == 'todo'"
>
<wf-examine-form
ref="examineForm"
:comment.sync="comment"
:process="process"
@user-select="handleUserSelect"
></wf-examine-form>
</el-card>
</el-tab-pane>
<el-tab-pane label="流转信息"
name="second">
<el-card shadow="never"
style="margin-top: 5px;">
<el-tab-pane label="流转信息" name="second">
<el-card shadow="never" style="margin-top: 5px">
<wf-flow :flow="flow"></wf-flow>
</el-card>
</el-tab-pane>
<el-tab-pane label="流程跟踪"
name="third">
<el-tab-pane label="流程跟踪" name="third">
<template v-if="activeName == 'third'">
<el-card shadow="never"
style="margin-top: 5px;">
<wf-design ref="bpmn"
style="height: 500px;"
:options="bpmnOption"></wf-design>
<el-card shadow="never" style="margin-top: 5px">
<wf-design
ref="bpmn"
style="height: 500px"
:options="bpmnOption"
></wf-design>
</el-card>
</template>
</el-tab-pane>
@ -67,191 +108,223 @@
</avue-skeleton>
<!-- 底部按钮 -->
<wf-button :loading="submitLoading"
:button-list="buttonList"
:process="process"
:comment="comment"
@examine="handleExamine"
@user-select="handleUserSelect"
@print="handlePrint"
@rollback="handleRollbackTask"
@terminate="handleTerminateProcess"
@withdraw="handleWithdrawTask"></wf-button>
<wf-button
:loading="submitLoading"
:button-list="buttonList"
:process="process"
:comment="comment"
@examine="handleExamine"
@user-select="handleUserSelect"
@print="handlePrint"
@rollback="handleRollbackTask"
@terminate="handleTerminateProcess"
@withdraw="handleWithdrawTask"
></wf-button>
<!-- 人员选择弹窗 -->
<user-select ref="user-select"
:check-type="checkType"
:default-checked="defaultChecked"
@onConfirm="handleUserSelectConfirm"></user-select>
<user-select
ref="user-select"
:check-type="checkType"
:default-checked="defaultChecked"
@onConfirm="handleUserSelectConfirm"
></user-select>
</basic-container>
</template>
<script>
import WfExamineForm from './examForm.vue'
import WfButton from './button.vue'
import WfFlow from './flow.vue'
import userSelect from './user-select'
import WfExamineForm from "./examForm.vue";
import WfButton from "./button.vue";
import WfFlow from "./flow.vue";
import userSelect from "./user-select";
import exForm from '../../mixins/ex-form'
import theme from '../../mixins/theme'
import exForm from "../../mixins/ex-form";
import theme from "../../mixins/theme";
export default {
mixins: [exForm, theme],
components: { userSelect, WfExamineForm, WfButton, WfFlow },
watch: {
'$route.params.params': {
"$route.params.params": {
handler(val) {
if (val) {
const param = JSON.parse(Buffer.from(val, 'base64').toString())
const { taskId, processInsId } = param
if (taskId && processInsId) this.getDetail(taskId, processInsId)
const param = JSON.parse(Buffer.from(val, "base64").toString());
const { taskId, processInsId } = param;
if (taskId && processInsId) this.getDetail(taskId, processInsId);
}
},
immediate: true
}
immediate: true,
},
},
data() {
return {
activeName: 'first',
activeName: "first",
defaults: {},
form: {},
option: {},
vars: [], //
submitLoading: false, // loading
summaryOption: {}, // option
}
tableData: [],
};
},
methods: {
//
getDetail(taskId, processInsId) {
this.getTaskDetail(taskId, processInsId).then(res => {
const { process, form } = res
const { variables, status } = process
this.getTaskDetail(taskId, processInsId).then((res) => {
const { process, form } = res;
const { variables, status } = process;
let { allForm, taskForm, formList } = form
let { allForm, taskForm, formList } = form;
if (formList && formList.length > 0) {
const options = {
menuBtn: false,
detail: true,
group: []
}
formList.forEach(f => {
const { content, taskName, taskKey } = f
const { option } = this.handleResolveOption(eval('(' + content + ')'), taskForm, 'done')
group: [],
};
formList.forEach((f) => {
const { content, taskName, taskKey } = f;
const { option } = this.handleResolveOption(
eval("(" + content + ")"),
taskForm,
"done"
);
options.group.push({
label: taskName || taskKey,
collapse: allForm ? false : true,
column: option.column
})
})
this.summaryOption = options
column: option.column,
});
});
this.summaryOption = options;
}
if (allForm) {
const { option, vars } = this.handleResolveOption(eval('(' + allForm + ')'), taskForm, status)
option.menuBtn = false
const { option, vars } = this.handleResolveOption(
eval("(" + allForm + ")"),
taskForm,
status
);
option.menuBtn = false;
for (let key in variables) {
if (!variables[key]) delete variables[key]
if (!variables[key]) delete variables[key];
}
if (option.column && process.variables && process.variables.serialNumber) {
if (
option.column &&
process.variables &&
process.variables.serialNumber
) {
option.column.unshift({
label: '流水号',
prop: 'serialNumber',
label: "流水号",
prop: "serialNumber",
span: 24,
detail: true,
})
});
}
this.option = option
this.vars = vars
this.option = option;
this.vars = vars;
}
this.form = variables
this.waiting = false
})
this.form = variables;
this.waiting = false;
});
},
handleResolveOption(option, taskForm, status) {
const { column, group } = option
let vars = []
if (status != 'todo') { //
option.detail = true
if (column && column.length > 0) { // column
column.forEach(col => this.handleResolveEvent(col))
const { column, group } = option;
let vars = [];
if (status != "todo") {
//
option.detail = true;
if (column && column.length > 0) {
// column
column.forEach((col) => this.handleResolveEvent(col));
}
if (group && group.length > 0) { // group
group.forEach(gro => {
if (group && group.length > 0) {
// group
group.forEach((gro) => {
if (gro.column && gro.column.length > 0) {
gro.column.forEach(col => this.handleResolveEvent(col))
gro.column.forEach((col) => this.handleResolveEvent(col));
}
})
});
}
} else {
const columnFilter = this.filterAvueColumn(column, taskForm)
const columnArr = columnFilter.column
vars = columnFilter.vars || []
const columnFilter = this.filterAvueColumn(column, taskForm);
const columnArr = columnFilter.column;
vars = columnFilter.vars || [];
const groupArr = []
if (group && group.length > 0) { // group
group.forEach(gro => {
const groupFilter = this.filterAvueColumn(gro.column, taskForm)
gro.column = groupFilter.column
vars = vars.concat(groupFilter.vars)
if (gro.column.length > 0) groupArr.push(gro)
})
const groupArr = [];
if (group && group.length > 0) {
// group
group.forEach((gro) => {
const groupFilter = this.filterAvueColumn(gro.column, taskForm);
gro.column = groupFilter.column;
vars = vars.concat(groupFilter.vars);
if (gro.column.length > 0) groupArr.push(gro);
});
}
option.column = columnArr
option.group = groupArr
option.column = columnArr;
option.group = groupArr;
}
return { option, vars }
return { option, vars };
},
handleResolveEvent(col) {
const _this = this
delete col.value
let event = ['change', 'blur', 'click', 'focus']
event.forEach(e => {
if (col[e]) col[e] = eval((col[e] + '').replace(/this/g, '_this'))
})
if (col.event) Object.keys(col.event).forEach(key => col.event[key] = eval((col.event[key] + '').replace(/this/g, '_this')))
const _this = this;
delete col.value;
let event = ["change", "blur", "click", "focus"];
event.forEach((e) => {
if (col[e]) col[e] = eval((col[e] + "").replace(/this/g, "_this"));
});
if (col.event)
Object.keys(col.event).forEach(
(key) =>
(col.event[key] = eval(
(col.event[key] + "").replace(/this/g, "_this")
))
);
if (col.type == 'dynamic') col.children.column.forEach(cc => _this.handleResolveEvent(cc))
if (col.type == "dynamic")
col.children.column.forEach((cc) => _this.handleResolveEvent(cc));
},
//
handleExamine(pass) {
this.submitLoading = true
const { form, summaryForm } = this.$refs
this.submitLoading = true;
const { form, summaryForm } = this.$refs;
if (form) {
this.$refs.form.validate((valid, done, msg) => {
if (valid) {
const variables = {}
this.vars.forEach(v => {
if (v != 'comment' && this.form[v]) variables[v] = this.form[v]
})
const variables = {};
this.vars.forEach((v) => {
if (v != "comment" && this.form[v]) variables[v] = this.form[v];
});
this.handleCompleteTask(pass, variables).then(() => {
this.$message.success("处理成功")
this.handleCloseTag('/plugin/workflow/process/todo')
}).catch(() => {
if (typeof done == 'function') done()
this.submitLoading = false
})
this.handleCompleteTask(pass, variables)
.then(() => {
this.$message.success("处理成功");
this.handleCloseTag("/plugin/workflow/process/todo");
})
.catch(() => {
if (typeof done == "function") done();
this.submitLoading = false;
});
} else {
done()
this.submitLoading = false
done();
this.submitLoading = false;
if (msg) {
const key = Object.keys(msg)[0]
const rules = msg[key]
this.$message.error(rules.map(r => r.message).join(' | '))
const key = Object.keys(msg)[0];
const rules = msg[key];
this.$message.error(rules.map((r) => r.message).join(" | "));
}
}
})
});
} else if (summaryForm) {
this.handleCompleteTask(pass, {}).then(() => {
this.$message.success("处理成功")
this.handleCloseTag('/plugin/workflow/process/todo')
}).catch(() => {
this.submitLoading = false
})
} else this.$message.error('找不到需要提交的表单')
this.handleCompleteTask(pass, {})
.then(() => {
this.$message.success("处理成功");
this.handleCloseTag("/plugin/workflow/process/todo");
})
.catch(() => {
this.submitLoading = false;
});
} else this.$message.error("找不到需要提交的表单");
},
}
}
},
};
</script>
<style lang="scss" scoped>
@ -264,4 +337,4 @@ export default {
justify-content: space-between;
padding: 0 10px 10px 0;
}
</style>
</style>

@ -54,6 +54,7 @@
</template>
<template slot="xitongmingchengshujuku">
<el-select
filterable
v-model="form.xitongmingchengshujuku"
placeholder="请选择系统名称/数据库"
@change="systemChange"

@ -106,6 +106,7 @@ export default {
color: #666;
text-align: center;
line-height: 37px;
cursor:pointer;
}
}
.item:nth-child(4) {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save