Commit 7abfc032 authored by cjf's avatar cjf

feat: 主机管理联调完毕

parent ff72b7eb
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"vue.codeActions.enabled": false
} }
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</template> </template>
<script setup> <script setup>
import { reactive, ref, onBeforeMount, toRefs } from "vue"; import { reactive, ref, onBeforeMount, watch } from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [String, Number], type: [String, Number],
...@@ -35,6 +35,13 @@ const emit = defineEmits(["update:modelValue", "change"]); ...@@ -35,6 +35,13 @@ const emit = defineEmits(["update:modelValue", "change"]);
const nowIndex = ref(""); const nowIndex = ref("");
watch(
() => props.modelValue,
() => {
nowIndex.value = props.modelValue;
}
);
const changeInner = (val) => { const changeInner = (val) => {
nowIndex.value = val; nowIndex.value = val;
emit("update:modelValue", val); emit("update:modelValue", val);
...@@ -42,7 +49,7 @@ const changeInner = (val) => { ...@@ -42,7 +49,7 @@ const changeInner = (val) => {
}; };
onBeforeMount(() => { onBeforeMount(() => {
nowIndex.value = props.default; nowIndex.value = props.modelValue;
}); });
</script> </script>
......
...@@ -178,4 +178,9 @@ const getRowInfo = (row, key) => { ...@@ -178,4 +178,9 @@ const getRowInfo = (row, key) => {
$_prop_path: propPath, $_prop_path: propPath,
}; };
}; };
defineExpose({
clearSelection,
getRowInfo,
});
</script> </script>
<template> <template>
<el-table <el-table
:height="height"
ref="table" ref="table"
class="bg-table" class="bg-table"
:data="rows" :data="rows"
...@@ -13,7 +12,7 @@ ...@@ -13,7 +12,7 @@
<template v-slot:empty> <template v-slot:empty>
<div class="empty_container"> <div class="empty_container">
<img src="../assets/imgs/img-no-data.png" alt="" /> <img src="../assets/imgs/img-no-data.png" alt="" />
<div class="text">暂无数据</div> <div class="text">{{ emptyText }}</div>
</div> </div>
</template> </template>
<el-table-column v-if="paddingLeft > 10" :width="paddingLeft - 10"></el-table-column> <el-table-column v-if="paddingLeft > 10" :width="paddingLeft - 10"></el-table-column>
...@@ -24,7 +23,7 @@ ...@@ -24,7 +23,7 @@
<!-- 序号 --> <!-- 序号 -->
</el-table-column> </el-table-column>
<el-table-column <el-table-column
v-for="(header) in headers" v-for="header in headers"
:width="header.width" :width="header.width"
:min-width="header.minWidth" :min-width="header.minWidth"
:align="header.align" :align="header.align"
...@@ -52,21 +51,10 @@ ...@@ -52,21 +51,10 @@
<script setup> <script setup>
import { watch, ref, nextTick } from "vue"; import { watch, ref, nextTick } from "vue";
import { selectTableMixin } from "./hook/mixin-select-table"; import { selectTableMixin } from "./hook/mixin-select-table";
const { const { nowSelectData, allSelectData, initAllSelectData, selectData, initSelectTableData, runPage, dealSelectData } =
nowSelectData, selectTableMixin();
allSelectData,
initAllSelectData,
selectData,
initSelectTableData,
runPage,
dealSelectData,
} = selectTableMixin();
const props = defineProps({ const props = defineProps({
height: {
type: [Number, String],
default: "auto",
},
headers: { headers: {
type: Array, type: Array,
require: true, require: true,
...@@ -87,6 +75,10 @@ const props = defineProps({ ...@@ -87,6 +75,10 @@ const props = defineProps({
type: String, type: String,
default: "序号", default: "序号",
}, },
emptyText: {
type: String,
default: "暂无数据",
},
stripe: { stripe: {
type: Boolean, type: Boolean,
default: false, default: false,
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
directory: 'file', directory: 'file',
uniqueCode: false, uniqueCode: false,
}" }"
:limit="limit"
:http-request="httpRequest"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-success="handleSuccess" :on-success="handleSuccess"
...@@ -95,6 +97,9 @@ const props = defineProps({ ...@@ -95,6 +97,9 @@ const props = defineProps({
type: String, type: String,
default: "", default: "",
}, },
httpRequest: {
type: Function,
},
limit: { limit: {
type: Number, type: Number,
default: 9999, default: 9999,
...@@ -123,10 +128,6 @@ watch( ...@@ -123,10 +128,6 @@ watch(
} }
); );
const handleBeforeUpload = (file) => { const handleBeforeUpload = (file) => {
if (state.fileList && state.fileList.length >= props.limit) {
ElMessage.error(`只允许上传${props.limit}个文件`);
return false;
}
let temp = file.name.split("."); let temp = file.name.split(".");
let type = temp[temp.length - 1].toLocaleLowerCase(); let type = temp[temp.length - 1].toLocaleLowerCase();
let fileTypesOk = props.fileTypes.indexOf(type) > -1 || props.fileTypes.length == 0; let fileTypesOk = props.fileTypes.indexOf(type) > -1 || props.fileTypes.length == 0;
...@@ -143,7 +144,7 @@ const handleBeforeUpload = (file) => { ...@@ -143,7 +144,7 @@ const handleBeforeUpload = (file) => {
return fileTypesOk && fileMaxSizeOk; return fileTypesOk && fileMaxSizeOk;
}; };
const handleExceed = (file, fileList) => { const handleExceed = (file, fileList) => {
console.log(file, fileList); ElMessage.error(`只允许上传${props.limit}个文件`);
}; };
const handlePreview = (val) => { const handlePreview = (val) => {
let a = document.createElement("a"); // 生成一个a元素 let a = document.createElement("a"); // 生成一个a元素
......
<template> <template>
<div> <div>
<el-table <el-table :data="tableData" style="width: 100%">
:data="tableData"
style="width: 100%">
<el-table-column <el-table-column
v-for="(item,index) in header" v-for="(item, index) in header"
:key="'header'+index" :key="'header' + index"
prop="date" prop="date"
:label="item.name" :label="item.name"
:width="item.width"> :width="item.width">
<template v-slot:default="scope"> <template v-slot:default="scope">
<span class="table-span" v-for="(it,idx) in item.value" :key="'it'+idx"> <span class="table-span" v-for="(it, idx) in item.value" :key="'it' + idx">
<el-input <el-input
v-if="it.type == 'input'" v-if="it.type == 'input'"
v-show="it.show==undefined?true:typeof(it.show)=='function'?it.show(scope):it.show" v-show="it.show == undefined ? true : typeof it.show == 'function' ? it.show(scope) : it.show"
@change="changeData({...scope,type:it.value,check:it.check||null})" @change="changeData({ ...scope, type: it.value, check: it.check || null })"
v-model="scope.row[it.value]" v-model="scope.row[it.value]"
:style="{width:it.width}" :style="{ width: it.width }"
:placeholder="it.placeholder" :placeholder="it.placeholder"
:disabled="it.disabled" :disabled="it.disabled"></el-input>
></el-input>
<el-select <el-select
v-if="it.type == 'select'" v-if="it.type == 'select'"
v-show="it.show==undefined?true:typeof(it.show)=='function'?it.show(scope):it.show" v-show="it.show == undefined ? true : typeof it.show == 'function' ? it.show(scope) : it.show"
@change="changeData({...scope,type:it.value,check:it.check||null})" @change="changeData({ ...scope, type: it.value, check: it.check || null })"
:multiple="it.multiple || false" :multiple="it.multiple || false"
v-model="scope.row[it.value]" v-model="scope.row[it.value]"
:style="{width:it.width}" :style="{ width: it.width }"
:disabled="it.disabled" :disabled="it.disabled"
:placeholder="it.placeholder" :placeholder="it.placeholder">
>
<el-option <el-option
v-for="(itx, idxm) in it.arr" v-for="(itx, idxm) in it.arr"
:key="idxm + 600" :key="idxm + 600"
:label="itx.label" :label="itx.label"
:value="itx.value" :value="itx.value"
:disabled="itx.disabled" :disabled="itx.disabled"></el-option>
></el-option>
</el-select> </el-select>
<span v-if="it.type == 'text'" v-show="it.show==undefined?true:typeof(it.show)=='function'?it.show(scope):it.show">{{it.value}}</span> <span
<span v-if="it.type == 'value'" v-show="it.show==undefined?true:typeof(it.show)=='function'?it.show(scope):it.show">{{scope.row[it.value]}}</span> v-if="it.type == 'text'"
<span class="button" v-if="it.type == 'button'" v-show="it.show==undefined?true:typeof(it.show)=='function'?it.show(scope):it.show" @click="it.callback(scope)">{{it.value}}</span> v-show="it.show == undefined ? true : typeof it.show == 'function' ? it.show(scope) : it.show"
>{{ it.value }}</span
>
<span
v-if="it.type == 'value'"
v-show="it.show == undefined ? true : typeof it.show == 'function' ? it.show(scope) : it.show"
>{{ scope.row[it.value] }}</span
>
<span
class="button"
v-if="it.type == 'button'"
v-show="it.show == undefined ? true : typeof it.show == 'function' ? it.show(scope) : it.show"
@click="it.callback(scope)"
>{{ it.value }}</span
>
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
...@@ -51,50 +60,38 @@ ...@@ -51,50 +60,38 @@
<script> <script>
export default { export default {
props: { props: {
header:{ header: {
type:Array, type: Array,
default:()=>[] default: () => [],
}, },
tableData:{ tableData: {
type:Array, type: Array,
default:()=>[] default: () => [],
}
}, },
components: {
}, },
components: {},
data() { data() {
return { return {};
};
},
watch: {
},
computed: {
},
created() {
},
mounted() {
}, },
watch: {},
computed: {},
created() {},
mounted() {},
methods: { methods: {
changeData(val){ changeData(val) {
this.$emit('change',val) this.$emit("change", val);
} },
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.table-span{ .table-span {
float: left; float: left;
margin-right: 10px; margin-right: 10px;
line-height: 40px; line-height: 40px;
} }
.button{ .button {
cursor: pointer; cursor: pointer;
color: #2b4695; color: #2b4695;
} }
......
...@@ -30,9 +30,8 @@ import store from "@/store"; ...@@ -30,9 +30,8 @@ import store from "@/store";
import i18n from "./i18n/i18n.js"; import i18n from "./i18n/i18n.js";
import axios from "./request/http.js"; import axios from "./request/http.js";
import api from "./request/api.js";
import config from "../package.json"; import config from "../package.json";
console.log(config.version);
const createVue = createApp(App); const createVue = createApp(App);
// createVue.use(ElementPlus, { locale }); // createVue.use(ElementPlus, { locale });
...@@ -60,6 +59,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { ...@@ -60,6 +59,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
} }
createVue.config.globalProperties.$axios = axios; createVue.config.globalProperties.$axios = axios;
createVue.config.globalProperties.$api = api;
import menu from "./router/function.js"; import menu from "./router/function.js";
......
<template>
<div class="detail_container">
<bg-breadcrumb></bg-breadcrumb>
<div class="main_container">
<div class="content_main bg-scroll">
<el-form
label-position="top"
label-width="100px"
ref="ruleFormRef"
:model="formData"
:rules="formRules"
style="max-width: 70%">
<el-form-item label="主机分组名称" prop="host_name">
<el-input
:disabled="route.query.id"
clearable
v-model="formData.host_name"
maxlength="20"
placeholder="请输入主机分组名称"
show-word-limit />
</el-form-item>
</el-form>
<bg-inner-tabs v-model="currentTab" :data="tabOptions"></bg-inner-tabs>
<listTable v-show="currentTab == 0" ref="listTableRef" :initTableData="initTableData"></listTable>
<div class="upload_container" v-show="currentTab == 1">
<div class="upload_top">
<div class="required-item">上传文件</div>
<div>
模版下载:<span class="file" @click="downloadFile(`/主机IP模板.xlsx`)"
>IP主机模板<bg-icon icon="#bg-ic-to-bottom"></bg-icon
></span>
</div>
</div>
<bg-upload
:httpRequest="httpRequest"
style="width: 360px"
v-model="attachment"
customTips
:limit="1"
:fileTypes="['xlsx', 'xls']">
<span>将文件拖到此处,或 点击上传</span><br />
<span>支持上传一个后缀为.xlsx或.xls的文件,文件大小不超过20M,最多解析1000个IP</span>
</bg-upload>
</div>
</div>
<div class="content_foot apaas_button">
<el-button type="default" @click="back"> 取消 </el-button>
<el-button type="primary" @click="save"> 保存 </el-button>
</div>
</div>
<checkStatusDialog v-model="checkStatusVisible" :enter="1" :checkStatusData="checkStatusData"></checkStatusDialog>
</div>
</template>
<script setup>
import { reactive, ref, onMounted, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import listTable from "../components/list-table.vue";
import checkStatusDialog from "../components/check-status-dialog.vue";
import { downloadFileFormat } from "@/services/helper.js";
const { proxy } = getCurrentInstance();
const { $api } = proxy;
const router = useRouter();
const route = useRoute();
const formData = reactive({
host_name: "",
});
const formRules = reactive({
host_name: [{ required: true, message: "主机分组名称不能为空", trigger: "blur" }],
});
const attachment = ref([]);
const downloadFile = (data) => {
const a = document.createElement("a"); // 创建a标签
a.setAttribute("download", ""); // download属性
a.setAttribute("href", data); // href链接
a.click(); // 自执行点击事件
};
const currentTab = ref(0);
const tabOptions = ref(["列表维护", "文件上传"]);
const ruleFormRef = ref();
const listTableRef = ref();
const checkStatusVisible = ref(false);
const checkStatusData = ref({});
const save = () => {
ruleFormRef.value.validate((valid) => {
if (currentTab.value == 0) {
let { flag, list } = listTableRef.value.getData();
if (flag && valid) {
checkStatusData.value = {
detection_type: 2,
host_manage_list: list,
host_name: formData.host_name,
};
checkStatusVisible.value = true;
}
} else {
if (attachment.value.length == 0) {
return ElMessage.error("请上传文件");
}
checkStatusData.value = {
detection_type: 3,
file_name: attachment.value[0].url.split("/").at(-1),
host_file_url: attachment.value[0].url,
host_name: formData.host_name,
};
if (valid) {
checkStatusVisible.value = true;
}
}
});
};
const back = () => {
router.back();
};
const initTableData = ref([]);
const getHostManageDetail = () => {
$api.autoMaintenance.getHostManageDetail(route.query.id).then((res) => {
if (res.data.code == 200) {
let data = res.data.data;
formData.host_name = data.host_name;
initTableData.value = data.host_list || [];
if (data.host_file_url) {
currentTab.value = 1;
attachment.value = [
{
name: downloadFileFormat(data.host_file_url),
url: data.host_file_url,
},
];
}
} else {
ElMessage.error(res.data.data);
}
});
};
const httpRequest = (file) => {
let formData = new FormData();
formData.append("upload_file", file.file);
$api.autoMaintenance.uploadHostManageFile(formData).then((res) => {
if (res.data.code == 200) {
let data = res.data.data;
file.onSuccess({
data,
});
} else {
ElMessage.error(res.data.data);
}
});
};
onMounted(() => {
if (route.query.id) {
getHostManageDetail();
}
});
</script>
<style lang="scss" scoped>
.main_container {
.content_main {
padding: 30px;
height: 0;
flex: auto;
overflow: auto;
}
.content_foot {
border-top: 1px solid #e6e9ef;
padding: 16px 16px 0 16px;
display: flex;
justify-content: flex-end;
}
.upload_container {
width: 360px;
.upload_top {
margin: 16px 0 10px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
color: #404a62;
.file {
color: #3759be;
cursor: pointer;
}
}
}
.required-item::before {
content: "*";
color: #d75138;
margin-right: 4px;
}
}
</style>
<template>
<el-dialog
class="dialog_box check_status_dialog"
title="连接状态检测"
v-model="dialogVisible"
width="1024px"
@closed="resetDialog">
<div class="main">
<div class="title" v-if="props.enter == 0">主机分组名称:{{ props.checkStatusData.host_name }}</div>
<div class="loading-container">
<span class="icon_box" :class="{ ic_animation: stateValue == 0 }"
><bg-icon :class="getClassName(stateValue)" :icon="getIcon(stateValue)"
/></span>
<span>{{ getText(stateValue) }}</span>
</div>
<div class="table-container">
<div class="table-top">
<span>异常状态列表</span>
<el-button type="primary" @click="exportFile" :disabled="stateValue == 0 || tableTotal == 0">
导出
</el-button>
</div>
<div class="status-lists">
<div class="table">
<bg-table
:empty-text="'无异常数据'"
:height="300"
ref="taskTable"
:headers="taskHeaders"
:rows="taskRows"
:isIndex="true"
:stripe="true">
<template v-slot:voucher_type="{ row }">
{{ row.voucher_type == 0 ? "密码验证" : "密钥验证" }}
</template>
</bg-table>
</div>
<bg-pagination
v-if="taskRows.length > 0"
:page="filter.page"
:size="filter.size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination>
</div>
</div>
</div>
<template v-slot:footer>
<div class="apaas_button">
<el-button type="default" @click="dialogVisible = false">{{ props.enter == 0 ? "关闭" : "取消" }}</el-button>
<el-button :disabled="stateValue != 1" v-if="props.enter != 0" type="primary" @click="save"
>剔除异常数据并保存</el-button
>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { reactive, ref, onBeforeMount, getCurrentInstance, computed, watch, nextTick, watchEffect } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage } from "element-plus";
const { proxy } = getCurrentInstance();
const { $api } = proxy;
const router = useRouter();
const route = useRoute();
const props = defineProps({
modelValue: {
type: Boolean,
},
checkStatusData: {
type: Object,
},
// 0:列表页 1:详情页
enter: {
type: Number,
},
});
const emit = defineEmits(["update:modelValue"]);
const dialogVisible = computed({
get() {
if (props.modelValue) {
postHostManageState();
}
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
});
const stateUUID = ref("");
const postHostManageState = () => {
const formData = new FormData();
formData.append("detection_type", props.checkStatusData.detection_type);
switch (props.checkStatusData.detection_type) {
case 1:
formData.append("id", props.checkStatusData.id);
break;
case 2:
formData.append("host_manage_list", JSON.stringify(props.checkStatusData.host_manage_list));
break;
case 3:
formData.append("file_name", props.checkStatusData.file_name);
break;
}
$api.autoMaintenance.postHostManageState(formData).then((res) => {
if (res.data.code == 200) {
let data = res.data.data;
stateUUID.value = data;
stateValue.value = 1;
getTableRows();
} else {
stateValue.value = 2;
ElMessage.error(res.data.data);
}
});
};
const exportFile = () => {
let params = {
detection_type: props.enter == 0 ? 1 : 0,
};
switch (params.detection_type) {
case 1:
params.id = props.checkStatusData.id;
break;
case 0:
params.uuid = stateUUID.value;
break;
}
ElMessage.success("开始导出");
let url = `/v1/api/automated_mainten/host_manage/export?`;
const paramsArray = [];
for (const [key, value] of Object.entries(params)) {
paramsArray.push(`${key}=${value}`);
}
url += paramsArray.join("&");
const a = document.createElement("a");
const event = new MouseEvent("click");
a.href = url;
a.dispatchEvent(event);
};
const stateValue = ref(0);
const getIcon = (stateValue = 0) => {
const icons = ["#bg-ic-s-circle-restart", "#bg-ic-s-circle-check", "#bg-ic-s-circle-close"];
return icons[stateValue];
};
const getClassName = (stateValue = 0) => {
const classNames = ["pending", "success", "error"];
return classNames[stateValue];
};
const getText = (stateValue = 0) => {
const text = ["状态检测中", "状态检测完成", "状态检测失败"];
return text[stateValue];
};
const taskHeaders = [
{
prop: "ip",
label: "IP",
},
{
prop: "port",
label: "端口",
},
{
prop: "voucher_type",
label: "凭证类型",
},
{
prop: "user_name",
label: "用户名",
},
{
prop: "password",
label: "密码",
},
];
const taskRows = ref([]);
const filter = ref({
page: 1,
size: 10,
});
const tableTotal = ref(0);
const changePage = (page) => {
filter.value.page = page;
getTableRows();
}; // 改变页码
const changeSize = (size) => {
filter.value.size = size;
changePage(1);
}; // 改变每页条数
const getTableRows = () => {
let params = {
detection_type: props.enter == 0 ? 1 : 2,
};
switch (params.detection_type) {
case 1:
params.id = props.checkStatusData.id;
params.page = filter.value.page;
params.page_size = filter.value.size;
break;
case 2:
params.uuid = stateUUID.value;
params.page = filter.value.page;
params.page_size = filter.value.size;
break;
}
$api.autoMaintenance.getHostManageIPExceptionList(params).then((res) => {
if (res.data.code == 200) {
let data = res.data.data || [];
taskRows.value = data;
tableTotal.value = res.data.total;
} else {
ElMessage.error(res.data.data);
}
});
};
const save = () => {
let data = {
uuid: stateUUID.value,
};
if (props.checkStatusData.detection_type == 3) {
data.host_file_url = props.checkStatusData.host_file_url;
}
if (route.query.id) {
// 编辑
data.id = Number(route.query.id);
$api.autoMaintenance.putHostManageEdit(data).then((res) => {
if (res.data.code == 200) {
ElMessage.success("保存成功");
router.back();
} else {
ElMessage.error(res.data.data);
}
});
} else {
// 新增
data.host_name = props.checkStatusData.host_name;
$api.autoMaintenance.postHostManageAdd(data).then((res) => {
if (res.data.code == 200) {
ElMessage.success("保存成功");
router.back();
} else {
ElMessage.error(res.data.data);
}
});
}
};
const resetDialog = () => {
stateValue.value = 0;
taskRows.value = [];
tableTotal.value = 0;
filter.value.page = 1;
filter.value.size = 10;
};
</script>
<style lang="scss" scoped>
.dialog_box {
.main {
.title {
text-align: left;
margin-bottom: 10px;
font-weight: bold;
font-size: 16px;
}
.loading-container {
margin: auto;
text-align: center;
width: 200px;
background-color: #eaedf5;
border-radius: 4px;
font-size: 14px;
color: #404a62;
padding: 10px 0;
.icon_box {
margin-right: 8px;
display: inline-block;
}
.pending {
font-size: 14px;
color: #3759be;
}
.success {
font-size: 14px;
color: #429e8a;
}
.error {
font-size: 14px;
color: #d75138;
}
.ic_animation {
animation: loading-rotate 1.5s linear infinite;
}
@keyframes loading-rotate {
100% {
transform: rotate(360deg);
}
}
}
.table-container {
.table-top {
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 0;
font-weight: bold;
font-size: 16px;
}
.status-lists {
margin-top: 16px;
overflow: hidden;
padding-bottom: 10px;
}
}
}
}
</style>
<style lang="scss">
.check_status_dialog {
.el-dialog__footer {
border-top: 1px solid #e6e9ef;
}
.bg-table .el-table__empty-block {
height: unset !important;
.empty_container {
height: unset;
padding-top: 46px;
}
}
}
</style>
<template>
<el-form style="margin: 16px 0" ref="ruleFormRef" class="rule-form" :model="tableRows">
<bg-table-pro
class="input-table"
:headers="headers"
:data="tableRows"
ref="inputTable"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
row-key="id"
default-expand-all
:border="true"
:stripe="false">
<template #ip="{ row }">
<el-form-item
:rules="{
required: true,
message: 'IP不能为空',
trigger: 'blur',
}"
style="width: 100%"
:prop="`${row.$row_path}.ip`">
<el-input clearable placeholder="请输入IP" v-model="row.ip" />
</el-form-item>
</template>
<template #port="{ row }">
<el-form-item style="width: 100%">
<el-input clearable placeholder="请输入端口号" v-model="row.port" />
</el-form-item>
</template>
<template #voucher_type="{ row }">
<el-form-item
:rules="{
required: true,
message: '凭证类型不能为空',
trigger: 'change',
}"
style="width: 100%"
:prop="`${row.$row_path}.voucher_type`">
<el-select @change="(val) => changeType(val, row)" v-model="row.voucher_type" placeholder="请选择凭证类型">
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</template>
<template #user_name="{ row }">
<el-form-item
:rules="{
required: true,
message: '用户名不能为空',
trigger: 'blur',
}"
style="width: 100%"
:prop="`${row.$row_path}.user_name`">
<el-input clearable placeholder="请输入用户名" v-model="row.user_name" />
</el-form-item>
</template>
<template #password="{ row }">
<el-form-item
v-if="row.voucher_type != 1"
:rules="{
required: true,
message: '密码不能为空',
trigger: 'blur',
}"
style="width: 100%"
:prop="`${row.$row_path}.password`">
<el-input clearable placeholder="请输入密码" v-model="row.password" />
</el-form-item>
<el-form-item v-else required style="width: 100%">
<el-input disabled />
</el-form-item>
</template>
<template #action="{ row }">
<bg-table-btn
:click="
() => {
addInputConf(row);
}
"
:disabled="tableRows.length == 5">
新增
</bg-table-btn>
<bg-table-btn
:click="
() => {
removeInputConf(row);
}
"
:disabled="tableRows.length == 1">
删除
</bg-table-btn>
</template>
</bg-table-pro>
</el-form>
</template>
<script setup>
import { ref, reactive, toRefs, watch } from "vue";
import { v4 as uuidv4 } from "uuid";
const props = defineProps({
initTableData: {
default: () => {
return [];
},
},
});
const ruleFormRef = ref(null);
const inputTable = ref(null);
const headers = reactive([
{
label: "IP(IP之间用英文逗号分隔)",
required: true,
prop: "ip",
width: 300,
},
{
label: "端口号(不填写,默认为22)",
prop: "port",
},
{
label: "凭证类型",
required: true,
prop: "voucher_type",
},
{
label: "用户名",
required: true,
prop: "user_name",
},
{
label: "密码",
required: true,
prop: "password",
},
{
label: "操作",
prop: "action",
width: 120,
},
]);
const typeOptions = [
{
value: 0,
label: "密码验证",
},
{
value: 1,
label: "密钥验证",
},
];
const state = reactive({
inputTable,
tableRows: [],
});
const { tableRows } = toRefs(state);
const addInputConf = (row) => {
let tempRow = createRow();
if (row) {
let { index, rows } = getInputRowInfo(row);
rows.splice(index + 1, 0, tempRow);
} else {
state.tableRows = [tempRow];
}
updateInputConfRows();
}; //增加
const updateInputConfRows = () => {
let recursionItems = (items, parentType, parentPath) => {
for (let i = 0; i < items.length; i++) {
let item = items[i];
item.$parent_type = parentType;
item.$row_path = `${parentPath}[${i}]`;
if (item.children && item.children.length) {
let basePath = `${item.$row_path}.children.`;
recursionItems(item.children, item.paramType, basePath);
}
}
};
recursionItems(state.tableRows, "", "");
clearValidate();
}; // 更新 $parent_type 和 $row_path 字段
const clearValidate = () => {
ruleFormRef.value && ruleFormRef.value.clearValidate();
};
const createRow = () => {
return {
id: uuidv4(),
ip: "",
port: "22",
voucher_type: "",
user_name: "",
password: "",
};
};
const getInputRowInfo = (row) => {
return state.inputTable && state.inputTable.getRowInfo(row, `id`);
}; //获取当前行的数据信息
const removeInputConf = (row) => {
let { index, rows } = getInputRowInfo(row);
rows.splice(index, 1);
if (rows.length === 0) {
rows.push(createRow());
}
updateInputConfRows();
}; // 删除
const initTable = () => {
if (props.initTableData && props.initTableData.length >= 1) {
state.tableRows = props.initTableData;
updateInputConfRows();
} else {
addInputConf();
}
};
const changeType = (val, row) => {
if (val == 2) {
row.password = "";
}
};
const getData = () => {
ruleFormRef.value && ruleFormRef.value.validate((valid) => {});
let flag = true;
let list = state.tableRows.map((item) => {
if (
item.ip == "" ||
item.voucher_type === "" ||
item.user_name == "" ||
(item.voucher_type === 0 && item.password == "")
) {
flag = false;
}
return {
ip: item.ip,
port: item.port,
voucher_type: item.voucher_type,
user_name: item.user_name,
password: item.password,
};
});
return {
list,
flag,
};
};
watch(
() => {
return props.initTableData;
},
() => {
initTable();
},
{
immediate: true,
}
);
defineExpose({
getData,
clearValidate,
});
</script>
<style lang="scss" scoped>
.rule-form {
::v-deep(.bg-table .el-form-item .el-form-item__error) {
bottom: -13px;
}
}
</style>
<template>
<div class="my-business-detail">
<div class="breadcrumb">
<bg-breadcrumb />
</div>
<div class="content bg-scroll">
<div class="go-back">
<goBack />
</div>
<gap-title :hasLine="true" title="基本信息"></gap-title>
<div class="info">
<Info :labelData="labelData" :valueData="basicInfo"> </Info>
</div>
<gap-title :hasLine="true" title="主机列表"></gap-title>
<div class="info">
<div class="host-lists">
<bg-table border ref="hostTable" :headers="hostHeaders" :rows="hostRows" height="100%" :isIndex="true">
</bg-table>
</div>
<div style="margin-top: 10px" v-if="fileUrl">
<a download :href="fileUrl">{{ downloadFileFormat(fileUrl) }}</a>
</div>
</div>
<gap-title :hasLine="true" title="任务列表"></gap-title>
<div class="info">
<div class="task-lists">
<div class="table">
<bg-table ref="taskTable" :headers="taskHeaders" :rows="taskRows" :isIndex="true" :stripe="true">
</bg-table>
</div>
<!-- <bg-pagination
:page="filter.page"
:size="filter.size"
:total="tableTotal"
@change-page="changePage"
@change-size="changeSize">
</bg-pagination> -->
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
import bgBreadcrumb from "@/components/bg-breadcrumb.vue";
import Info from "@/components/warn-detail/info.vue";
import gapTitle from "@/components/gap-title.vue";
import goBack from "@/components/go-back/index.vue";
import { downloadFileFormat } from "@/services/helper.js";
import { ElMessage } from "element-plus";
const { proxy } = getCurrentInstance();
const { $api } = proxy;
const router = useRouter();
const route = useRoute();
const labelData = [
[
{
label: "主机分组名称",
prop: "host_name",
},
],
[
{
label: "任务数量",
prop: "task_cnt",
},
{
label: "IP数量",
prop: "ip_cnt",
},
],
[
{
label: "创建人",
prop: "create_user",
},
{
label: "创建时间",
prop: "create_time",
},
],
];
const basicInfo = ref({
host_name: "",
task_cnt: "",
ip_cnt: "",
create_user: "",
create_time: "",
});
const hostHeaders = [
{
prop: "ip",
label: "IP",
},
{
prop: "port",
label: "端口",
},
];
const hostRows = ref([,]);
const taskHeaders = [
{
prop: "task_name",
label: "任务名称",
},
{
prop: "exec_cnt",
label: "执行次数",
},
{
prop: "task_desc",
label: "描述",
},
];
const taskRows = ref([]);
const filter = ref({
page: 1,
size: 10,
});
const tableTotal = ref(0);
const changePage = (page) => {
filter.value.page = page;
getTableRows();
}; // 改变页码
const changeSize = (size) => {
filter.value.size = size;
changePage(1);
}; // 改变每页条数
const getTableRows = () => {};
const fileUrl = ref("");
const downloadFile = (url) => {
if (url) {
const a = document.createElement("a"); // 创建a标签
a.setAttribute("download", ""); // download属性
a.setAttribute("href", url); // href链接
a.click(); // 自执行点击事件
}
};
const getHostManageDetail = () => {
$api.autoMaintenance.getHostManageDetail(route.query.id).then((res) => {
if (res.data.code == 200) {
let data = res.data.data;
// 基本信息
basicInfo.value.host_name = data.host_name;
basicInfo.value.task_cnt = data.task_cnt;
basicInfo.value.ip_cnt = data.ip_cnt;
basicInfo.value.create_user = data.create_user;
basicInfo.value.create_time = data.create_time.split("+")[0].replace("T", " ").replace("Z", " ");
// 主机列表
hostRows.value = data.host_list || [];
fileUrl.value = data.host_file_url;
// 任务列表
taskRows.value = data.task_list || [];
} else {
ElMessage.error(res.data.data);
}
});
};
onMounted(() => {
getHostManageDetail();
});
</script>
<style lang="scss" scoped>
.my-business-detail {
width: 100%;
height: 100%;
padding: 0 24px 16px;
.breadcrumb {
width: 100%;
height: 46px;
}
.content {
width: 100%;
height: calc(100% - 46px);
background-color: #ffffff;
box-shadow: 0px 1px 4px 0px rgba(0, 7, 101, 0.15);
border-radius: 6px;
padding: 24px;
.go-back {
margin-bottom: 24px;
}
:deep(.gap-title) {
margin-bottom: 16px;
}
.info,
.feedback-info {
max-width: 1072px;
width: 100%;
padding: 0 8px 0;
&:not(:last-child) {
padding-bottom: 24px;
}
.status {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
$statusObj: (
finish: #48ad97,
close: #9e9e9e,
info: #3759be,
);
@each $status, $color in $statusObj {
&-#{$status} {
background-color: $color;
}
}
}
.status-body {
display: flex;
align-items: center;
}
.host-lists {
margin-top: 16px;
}
.task-lists {
margin-top: 16px;
overflow: hidden;
padding-bottom: 10px;
}
}
}
}
</style>
import autoMaintenance from "./api/auto-maintenance";
export default {
autoMaintenance,
};
import axios from "@/request/http"; // 导入http中创建的axios实例
const autoMaintenance = {
getHostManageList(params) {
return axios({
method: "get",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/page_list`,
params,
});
},
getHostManageDetail(id) {
return axios({
method: "get",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/details?id=${id}`,
});
},
postHostManageState(data) {
return axios({
method: "post",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/state`,
data,
headers: {
"Content-Type": "multipart/form-data",
},
timeout: 0,
});
},
getHostManageIPExceptionList(params) {
return axios({
method: "get",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/ip_exception_list`,
params,
});
},
postHostManageAdd(data) {
return axios({
method: "post",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/add`,
data,
});
},
putHostManageEdit(data) {
return axios({
method: "put",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/edit`,
data,
});
},
uploadHostManageFile(data) {
return axios({
method: "post",
baseURL: "/v1",
url: `/api/add_file`,
data,
headers: {
"Content-Type": "multipart/form-data",
},
});
},
deleteHostManage(data) {
return axios({
method: "delete",
baseURL: "/v1",
url: `/api/automated_mainten/host_manage/del`,
data,
});
},
};
export default autoMaintenance;
...@@ -65,6 +65,12 @@ export default { ...@@ -65,6 +65,12 @@ export default {
changeOrigin: true, // true/false, Default: false - changes the origin of the host header to the target URL changeOrigin: true, // true/false, Default: false - changes the origin of the host header to the target URL
secure: false, //解决证书缺失问题 secure: false, //解决证书缺失问题
}, },
"/v1": {
target: "https://so.wodcloud.com/v1", // 所要代理的目标地址
rewrite: (path) => path.replace(/^\/v1/, ""), // 重写传过来的path路径,比如 `/api/index/1?id=10&name=zs`(注意:path路径最前面有斜杠(/),因此,正则匹配的时候不要忘了是斜杠(/)开头的;选项的 key 也是斜杠(/)开头的)
changeOrigin: true, // true/false, Default: false - changes the origin of the host header to the target URL
secure: false, //解决证书缺失问题
},
}, },
}, },
build: { build: {
......
...@@ -784,6 +784,11 @@ immutable@^4.0.0: ...@@ -784,6 +784,11 @@ immutable@^4.0.0:
resolved "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz" resolved "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz"
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
install@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776"
integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==
is-binary-path@~2.1.0: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz" resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz"
...@@ -905,9 +910,9 @@ markdown-it@^13.0.1: ...@@ -905,9 +910,9 @@ markdown-it@^13.0.1:
mdurl "^1.0.1" mdurl "^1.0.1"
uc.micro "^1.0.5" uc.micro "^1.0.5"
mavon-editor@^3.0.0: mavon-editor@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.npmmirror.com/mavon-editor/-/mavon-editor-3.0.1.tgz" resolved "https://registry.yarnpkg.com/mavon-editor/-/mavon-editor-3.0.1.tgz#0c2660569ded5b29e59d0e429af61eb618783a90"
integrity sha512-973cYCwv+AB+fcecsU6Ua6UXATxDMaY0Q7QzKQ/GmRW1sg+3DolZDnCGXth7XHDgrmqKTO57N42fVYujt0wfFw== integrity sha512-973cYCwv+AB+fcecsU6Ua6UXATxDMaY0Q7QzKQ/GmRW1sg+3DolZDnCGXth7XHDgrmqKTO57N42fVYujt0wfFw==
dependencies: dependencies:
xss "^1.0.10" xss "^1.0.10"
...@@ -1083,6 +1088,11 @@ ssr-window@^3.0.0-alpha.1: ...@@ -1083,6 +1088,11 @@ ssr-window@^3.0.0-alpha.1:
resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz" resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz"
integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA== integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==
string-format@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b"
integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==
supports-preserve-symlinks-flag@^1.0.0: supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
...@@ -1161,6 +1171,11 @@ vue-demi@*: ...@@ -1161,6 +1171,11 @@ vue-demi@*:
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz" resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A== integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
vue-demi@^0.14.5:
version "0.14.5"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.5.tgz#676d0463d1a1266d5ab5cba932e043d8f5f2fbd9"
integrity sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==
vue-i18n@^9.1.7: vue-i18n@^9.1.7:
version "9.1.10" version "9.1.10"
resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.1.10.tgz" resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.1.10.tgz"
......
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