cmdb增加一个功能界面(主机管理)

时间:July 22, 2019 分类:

目录:

后端代码

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseBadRequest
from django.db.models import F, Q
from django.db.models import Count, Sum
from django.db.models import Min
from django.db import DatabaseError, transaction
from inventory.models import Machine, Idc, Specification, Storage, Tags

from cmdb.util import DateEncoder
import json
import requests
import time

def machine_filter_options(request):
    idcs = Idc.objects.all().values('id', 'name', 'code')
    idc_options = []
    for idc in idcs:
        idc_options.append({'value': idc['id'], 'label': idc['name'] + '(' + idc['code'] + ')'})
    tag_options = list(Tags.objects.all().values_list('tag', flat=True))
    return HttpResponse(json.dumps({
        'idcOptions': idc_options,
        'tagOptions': tag_options}))


def machine_edit(request, id):
    idcs = Idc.objects.all()
    idc_specification_options = []
    for idc in idcs:
        specifications = Specification.objects.annotate(value=F('id'), label=F('name')).filter(idc=idc).values('value', 'label')
        idc_specification_options.append({'value': idc.code, 'label':idc.name + '(' + idc.code + ')', 'children': list(specifications)})
    storag_media_options = [{'value': _[0], 'label': _[1]} for _ in Storage.media_choices]
    machine_data = {'id': '', 
                    'ip': '', 
                    'hostname': '', 
                    'idcSpecification': [], 
                    'storages': [{'media': '', 'volume': ''}],
                    'tags': [] }
    if id is not None:
        try:
            machine = Machine.objects.get(id=id)
            machine_data['id'] = id
            machine_data['hostname'] = machine.hostname
            machine_data['tags'] = list(machine.tags.all().values_list('tag', flat=True))
            if machine.specification is not None:
                machine_data['idcSpecification'] = [machine.specification.idc.code, machine.specification.id]
            machine_data['ip'] = machine.ip
            machine_data['storages'] = []
            for storage in machine.storage_set.all():
                machine_data['storages'].append({'media': storage.media, 'volume': storage.volume})
            if not machine_data['storages']:
                machine_data['storages'] = [{'media': '', 'volume': ''}]
        except Exception:
            return HttpResponseBadRequest()
    context = {'idcSpecificationOptions': idc_specification_options,
              'storageMediaOptions': storag_media_options, 
              'machineForm': machine_data}
    print(context)
    return HttpResponse(json.dumps(context, cls=DateEncoder))


@transaction.atomic
def add_machine(request):
    machine_data = json.loads(request.body.decode())
    hostname = machine_data.get('hostname').strip()
    ip = machine_data.get('ip')
    idc_specification = machine_data.get('idcSpecification')
    storages = machine_data.get('storages')
    tags = machine_data.get('tags')
    defaults = {'hostname': hostname, 'ip': ip}
    if len(idc_specification) > 1:
        defaults['specification'] = Specification.objects.get(id=idc_specification[1])
    machine, status = Machine.objects.get_or_create(ip=ip, defaults=defaults)
    if status:
        storage_list = []
        for storage in storages:
            storage_list.append(Storage(machine=machine, volume=storage.get('volume'), media=storage.get('media')))
        Storage.objects.bulk_create(storage_list)
        for tag in tags:
            t, _ = Tags.objects.get_or_create(tag=tag, defaults={"tag": tag})
            print(t)
            machine.tags.add(t)
            machine.save()
        return HttpResponse(json.dumps({'id': machine.id}), status=201)
    elif machine is not None:
        machine_data = json.loads(request.body.decode())
        machine_data['id'] = machine.id
        return do_change_machine(machine_data)
    else:
        return HttpResponseBadRequest()


def get_machines(request):
    search_data = json.loads(request.body.decode())
    search = search_data.get('search')
    hostname = search_data.get('hostname')
    ip = search_data.get('ip')
    idc = search_data.get('idc')
    hdd = search_data.get('hdd')
    ssd = search_data.get('ssd')
    tags = search_data.get('tags')
    order_by = search_data.get('orderBy')
    order = search_data.get('order')
    size = search_data.get('size')
    page = search_data.get('page')
    machines = Machine.objects
    if search:
        machines = machines.filter(Q(hostname__icontains=search) | Q(specification__idc__name=search) |
            Q(specification__idc__code=search) | Q(tags__tag__icontains=search) | Q(ip__icontains=search) )
    else:
        if hostname:
            machines = machines.filter(hostname__icontains=hostname)
        if ip:
            machines = machines.filter(ip__contains=ip)
        if idc:
            machines = machines.filter(specification__idc__id__in=idc)
        if hdd:
            machines = machines.filter(storage__media=1).annotate(
                hdd_sum=Sum(F('storage__volume'))).filter(hdd_sum__gte=hdd)
        if ssd:
            machines = machines.filter(storage__media=0).annotate(
                ssd_sum=Sum(F('storage__volume'))).filter(ssd_sum__gte=ssd)
        if tags:
            machines = machines.filter(tags__tag__in=tags)
    machines = machines.distinct()
    if order_by and order:
        if order_by == 'idc':
            order_by = 'specification__idc'
        #elif order_by == 'ip':
        #    machines = machines.annotate(ip=ip)
        elif order_by == 'storages':
            machines = Machine.objects.filter(id__in=machines.values_list('id', flat=True)).annotate(storages=Sum('storage__volume'))
        else:
            pass
        if order == 'ascending':
            machines = machines.order_by(order_by)
        if order == 'descending':
            machines = machines.order_by('-' + order_by)
    else:
        machines = machines.order_by('id')
    start = (page - 1) * size
    end = size * page
    total = len(machines)
    machines = machines[start : end]
    machine_list = []
    start_time = int(time.time()) - 120
    end_time = start_time + 60
    for machine in machines:
        id = machine.id
        hostname = machine.hostname
        if machine.specification is not None:
            idc = str(machine.specification.idc)
            specification = machine.specification.name
        else:
            idc = ''
            specification = ''
        storage_list = []
        for storage in machine.storage_set.all().values('media').annotate(
                volume_sum=Sum('volume')).values('media','volume_sum'):
            media = 'SSD' if storage.get('media') == 0 else 'HDD'
            storage_list.append('%s : %d GB' % (media, storage.get('volume_sum')))
        storage_details = []
        for storage_detail in machine.storage_set.all():
            volume = storage_detail.volume
            media = storage_detail.get_media_display()
            storage_details.append({'volume': volume, 'media': media})
        ip = machine.ip
        tags = list(machine.tags.all().values_list('tag', flat=True))
        created = machine.created.strftime('%Y-%m-%d %H:%M:%S')
        lastModified = machine.last_modified.strftime('%Y-%m-%d %H:%M:%S')

        machine_list.append({'id': id, 'hostname': hostname, 'ip': ip,'idc': idc, 
            'specification': specification, 'storages': storage_list, 'storageDetails': storage_details,
            'tags': tags,'created': created, 'lastModified': lastModified})
    return HttpResponse(json.dumps({'tableData': machine_list, 'total': total}))


def change_machine(request):
    machine_data = json.loads(request.body.decode())
    return do_change_machine(machine_data)

@transaction.atomic
def do_change_machine(machine_data):
    id = machine_data.get('id')
    hostname = machine_data.get('hostname').strip()
    idc_specification = machine_data.get('idcSpecification')
    storages = machine_data.get('storages')
    tags = machine_data.get('tags')
    try:
        machine = Machine.objects.get(id=id)
        machine.hostname = hostname
        if len(idc_specification) > 1:
            machine.specification = Specification.objects.get(id=idc_specification[1])
        machine.tags.clear()
        for tag in tags:
            obj, created = Tags.objects.get_or_create(tag=tag)
            machine.tags.add(obj)
        machine.save()
        Storage.objects.filter(machine__id=id).delete()
        storage_list = []
        for storage in storages:
            storage_list.append(Storage(machine=Machine.objects.get(id=id),
                media=storage.get('media'), volume=storage.get('volume')))
        Storage.objects.bulk_create(storage_list)
    except DatabaseError:
        return HttpResponseBadRequest()
    return HttpResponse(json.dumps({'id': machine.id}), status=202)


@transaction.atomic
def delete_machine(request):
    id_list = json.loads(request.body.decode()).get('idList')
    try:
        for machine in Machine.objects.filter(id__in=id_list):
            machine.delete()
    except DatabaseError:
        return HttpResponseBadRequest()
    return HttpResponse(status=204)

前端代码

主机管理

src/components/MachineManagement.vue

<template>
  <div>
    <div class="div-header">
      <el-row>
        <el-col :span="3">
          <!--判定是否有增加服务器权限-->
          <el-button type="primary" size="mini" icon="el-icon-plus"
            :disabled="$store.state.permissions.indexOf('inventory.add_machine') < 0" @click="handleAdd">添加服务器
          </el-button>
        </el-col>
        <el-col :span="3">
          <!--判定是否有删除服务器权限,显示和点击都判断-->
          <el-badge :value="multipleSelection.length"
          :hidden="$store.state.permissions.indexOf('inventory.delete_machine') < 0 ||
          !multipleSelection.length > 0" :max="100" class="item">
            <el-button type="danger" size="mini" icon="el-icon-delete"
              :disabled="$store.state.permissions.indexOf('inventory.delete_machine') < 0 ||
              !multipleSelection.length > 0" @click="deleteMutiple">
              批量删除
            </el-button>
          </el-badge>
        </el-col>
        <el-col :offset="6" :span="4">
          <el-input placeholder="请输入搜索内容" clearable size="mini" suffix-icon="el-icon-search"
            v-model="searchForm.search" @input="handleSearch">
          </el-input>
        </el-col>
        <el-col :offset="1" :span="7">
          <div class="div-button">
            <el-button type="info" size="mini" @click="customizationVisible=!customizationVisible"
              icon="el-icon-edit">
              自定义列
            </el-button>
            <el-tooltip class="item" effect="dark" content="打开高级筛选后普通搜索失效"
              placement="bottom-end">
              <el-button type="info" size="mini" @click="searchFormVisible=!searchFormVisible">
                <i class="fas fa-filter"></i> 高级筛选
              </el-button>
            </el-tooltip>
          </div>
        </el-col>
      </el-row>
      <!--自定义筛选组件-->
      <machineFilter :searchFormVisible.sync="searchFormVisible" :searchForm="searchForm"
        @advancedSearch="advancedSearch">
      </machineFilter>
    </div>
    <el-table stripe border ref="table" :data="tableData" style="width: 100%" v-loading="loading"
      element-loading-text="拼命加载中" @row-click="handleRowClick" @row-dblclick="handleDoubleClick"
      @selection-change="handleSelectionChange" @sort-change="sortChange"
      @expand-change="handleExpandChange" :row-style="changeStyle">
      <el-table-column type="selection"></el-table-column>
      <el-table-column type="expand" label=">">
        <template slot-scope="props">
          <el-form label-position="left" label-width="80px" :inline="true" class="table-expand">
            <el-form-item label="服务器名:">
              <span>{{ props.row.hostname }}</span>
            </el-form-item>
            <el-form-item label="IP:">
              <span>{{ props.row.ip }}</span>
            </el-form-item>
            <el-form-item label="机房:">
              <span>{{ props.row.idc }}</span>
            </el-form-item>
            <el-form-item label="机架位:">
              <span>{{ props.row.specification }}</span>
            </el-form-item>
            <el-form-item label="存储:">
              <span v-for="(item, index) in props.row.storageDetails">
                {{ item.media }} : {{ item.volume }} <br>
              </span>
            </el-form-item>
            <el-form-item label="标签:">
              <span>{{ props.row.tags.join(', ') }}</span>
            </el-form-item>
            <el-form-item label="创建时间:">
              <span>{{ props.row.created }}</span>
            </el-form-item>
            <el-form-item label="最后更新:">
              <span>{{ props.row.lastModified }}</span>
            </el-form-item>
          </el-form>
        </template>
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('id') > -1" prop="id" sortable="custom"
        label="ID">
      </el-table-column>
      <el-table-column prop="hostname" sortable="custom" label="主机名"></el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('ip') > -1" prop="ip" sortable="custom" 
        label="IP" width="130">
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('idc') > -1" prop="idc" sortable="custom"
        label="机房">
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('specification') > -1" prop="specification" sortable="custom"
        label="机架位">
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('storage') > -1" prop="storages"
        sortable="custom" label="存储" width="140">
        <template slot-scope="scope">
          <span v-for="(item, index) in scope.row.storages">{{ item }}<br></span>
        </template>
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('tags') > -1" prop="tags" label="标签">
        <template slot-scope="scope">
          <span>
            <el-tag size="mini" type="success" v-for="(tag, index) in scope.row.tags" :key="index">
            {{ tag }}
            </el-tag>
          </span>
        </template>
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('created') > -1" prop="created"
        sortable="custom" label="创建时间">
      </el-table-column>
      <el-table-column v-if="checkedColumns.indexOf('lastModified') > -1" prop="lastModified"
        sortable="custom" label="最后更新">
      </el-table-column>
      <el-table-column label="操作" width="100">
        <template slot-scope="scope">
          <el-button size="mini" icon="el-icon-edit"
            :disabled="$store.state.permissions.indexOf('inventory.change_machine') < 0"
            @click="handleEdit(scope.$index, scope.row)">编辑
          </el-button>
          <br>
          <el-button style="margin-top: 5px" v-if="$store.state.user.isSuperuser" size="mini" icon="el-icon-delete"
            type="danger" @click="handleDelete(scope.$index, scope.row)">删除
          </el-button>
          <el-button v-if="false" size="mini" type="warning"
            :disabled="$store.state.permissions.indexOf('inventory.connect_machine') < 0"
            @click="handleConnect(scope.$index, scope.row)">
            <i class="fas fa-plug"></i> 连接
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog title="自定义列" :visible.sync="customizationVisible">
      <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll"
        @change="handleCheckAllChange">全选
      </el-checkbox>
      <el-checkbox-group v-model="checkedColumns" @change="checkColumn">
        <el-checkbox label="id">ID</el-checkbox>
        <el-checkbox label="idc">机房</el-checkbox>
        <el-checkbox label="rack">机架位</el-checkbox>
        <el-checkbox label="ip">IP地址</el-checkbox>
        <el-checkbox label="storage">存储</el-checkbox>
        <el-checkbox label="tags">标签</el-checkbox>
        <el-checkbox label="created">创建时间</el-checkbox>
        <el-checkbox label="lastModified">最后更新</el-checkbox>
      </el-checkbox-group>
    </el-dialog>
    <pagination :total="total" :pageSizes="[10, 20, 50, 100]" :pageSize.sync="searchForm.size"
      :currentPage.sync="searchForm.page" @reloadTable="reloadTable">
    </pagination>
  </div>
</template>
<script>
  import moment from 'moment';
  import machineFilter from '@/components/MachineFilter.vue';
  import pagination from '@/components/Pagination.vue';
  export default {
    components: {
      'machineFilter': machineFilter,
      'pagination': pagination
    },
    data: function () {
      return {
        searchForm: {
          search: '',
          hostname: '',
          ip: '',
          idc: '',
          ip: '', 
          hdd: '',
          ssd: '',
          page: 1,
          size: 10,
          orderBy: '',
          order: ''
        },
        customizationVisible: false,
        searchFormVisible: false,
        checkAll: false,
        isIndeterminate: false,
        columnList: ['id', 'idc', 'rack', 'ip', 'storage', 'tags', 'created', 'lastModified'],
        checkedColumns: [],
        raidOptions: [],
        idcOptions: [],
        categoryOptions: [],
        stateOptions: [],
        manufacturerOptions: [],
        loading: false,
        tableData: [],
        total: 0,
        expandRowKeys: [],
        multipleSelection: []
      }
    },
    methods: {
      handleCheckAllChange: function(val) {
        this.checkedColumns = val ? this.columnList : [];
        this.isIndeterminate = false;
      },
      checkColumn: function(val) {
        let checkedCount = val.length;
        this.checkAll = checkedCount === this.columnList.length;
        this.isIndeterminate = checkedCount > 0 && checkedCount < this.columnList.length;
      },
      handleSelectionChange: function (val) {
        this.multipleSelection = val;
      },
      changeStyle: function (row, index) {
        if (moment().isAfter(moment(row.row.warrantyExpire))) {
          console.log(row.warrantyExpire);
          return 'color:red; font-style:italic; font-weight: bold';
        }
      },
      reloadTable: function () {
        var _this = this;
        this.loading = true;
        this.$http.post('/api/inventory/getMachines/', this.searchForm).then(function (response) {
          _this.tableData = response.data.tableData;
          _this.total = response.data.total;
          _this.loading = false;
        }).catch(function (error) {
          _this.$message.error('获取服务器列表失败');
          _this.loading = false;
        });
      },
      handleSearch: function () {
        this.expandRowKeys = [];
        this.searchForm.page = 1;
        setTimeout(this.reloadTable, 500);
      },
      handleEdit: function (index, row) {
        this.$router.push({name: 'MachineChange', params: {id: row.id}});
      },
      handleDelete: function (index, row) {
        var idList = [row.id];
        this.deleteSubmit(idList);
      },
      handleRowClick: function (row, event, column) {
        if(column.id === 'el-table_1_column_21') {
          return;
        }
        if (this.expandRowKeys.indexOf(row.id) > -1) {
          this.$refs.table.toggleRowExpansion(row, false);
        } else {
          this.$refs.table.toggleRowExpansion(row, true);
        }
      },
      handleDoubleClick: function (row, event) {
        this.handleConnect(null, row);
      },
      handleConnect: function (index, row) {
        //window.open('/api/inventory/connectMachine/?target=' + row.hostname);
      },
      deleteMutiple: function () {
        var idList = []
        this.multipleSelection.forEach(v => {idList.push(v.id)});
        this.deleteSubmit(idList);
      },
      handleAdd: function () {
        this.$router.push({name: 'MachineAdd'});
      },
      deleteSubmit: function (idList) {
        var _this = this;
        this.$confirm('删除服务器信息: [' + idList.toString() + '], 是否继续?', '确认', {
          confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
          }).then(() => {
          _this.$http.post('/api/inventory/deleteMachine/', {'idList': idList}).then(function (response) {
            _this.$notify({ title: '成功', message: '删除服务器信息成功', type: 'success'});
            _this.reloadTable();
          }).catch(function (error) {
            if (error.response.status === 403) {
              _this.$message.warning('没有权限操作');
            } else {
              _this.$message.error('删除服务器信息失败');
            }
          });
        }).catch(() => {
          this.$message({type: 'info', message: '已取消删除'});
        });
      },
      sortChange: function (sort) {
        this.searchForm.orderBy = sort.prop;
        this.searchForm.order = sort.order;
        this.reloadTable();
      },
      advancedSearch: function () {
        this.searchForm.search = '';
        this.searchForm.page = 1;
        this.reloadTable();
      },
      handleExpandChange: function (row, expandedRows) {
        this.expandRowKeys = [];
        expandedRows.forEach(row => {
          this.expandRowKeys.push(row.id);
        });
      },
      getUsage: function () {
        var _this = this;
        var startTime = Number(moment().subtract(2, 'minutes').format('X'));
        var endTime = Number(moment().subtract(1, 'minutes').format('X'));
        this.tableData.forEach(v => {
          var url = '';
          if (v.idc.indexOf('xh') > -1) {
            hostnameList['xh'].push(v.hostname);
            url = '/xhfalcon/v1/graph/history';
          } else if (v.idc.indexOf('sj') > -1) {
            hostnameList['sj'].push(v.hostname);
            url = '/sjfalcon/v1/graph/history';
          } else if (v.idc.indexOf('sm') > -1){
            hostnameList['sm'].push(v.hostname);
            url = '/smfalcon/v1/graph/history';
          } else {
            hostnameList['unknown'].push(v.hostname);
          }
          this.$http.post(url, {"step": 60, "hostnames": [v.hostname],
            "start_time": startTime, "end_time": endTime,
            "counters": ["cpu.idle", "mem.memused.percent",
            "df.statistics.used.percent"], "consol_fun": "AVERAGE"})
            .then(function (response) {
            console.log(response.data);
            response.data.forEach(vdata => {
              if (vdata.counter === 'cpu.idle') {
                v['cpuUsage'] = 100 - parseInt(vdata.Values[0].value);
              }
              if (vdata.counter === 'mem.memused.percent') {
                v['memoryUsage'] = parseInt(vdata.Values[0].value);
              }
              if (vdata.counter === 'df.statistics.used.percent') {
                v['storageUsage'] = parseInt(vdata.Values[0].value);
              }
            });
            console.log(_this.tableData);
          }).catch(function (error) {
            console.log(error);
          });
        });
      },
      s2ab: function (s) {
        var buf = new ArrayBuffer(s.length)
        var view = new Uint8Array(buf)
        for (var i = 0; i !== s.length; ++i) {
          view[i] = s.charCodeAt(i) & 0xFF
        }
        return buf
      },
      writeExcel: function (data) {
        var xlsx = require('xlsx');
        var worksheet = xlsx.utils.aoa_to_sheet(data);
        var sheetName = 'source id详情';
        var workbook = {SheetNames: [sheetName], Sheets: {}};
        workbook.Sheets[sheetName] = worksheet;
        var wopts = {bookType: 'xlsx', bookSST: false, type: 'binary'};
        var wbout = xlsx.write(workbook, wopts);
        // the saveAs call downloads a file on the local machine
        var FileSaver = require('file-saver');
        FileSaver.saveAs(new Blob([this.s2ab(wbout)], {type: 'application/octet-stream'}),
          '机器列表.xlsx');
      },
      exportExcel: function () {
        var headerData = [['ID', '主机名', 'IP', '机房', '机架位', '存储',
          '标签', '创建时间', '最后更新']];
        var _this = this;
        this.$http.post('/api/inventory/exportMachines/', this.searchForm).then(function (response) {
            var exportData = headerData.concat(response.data)
            _this.writeExcel(exportData);
          })
          .catch(function (response) {
            _this.$message.error('导出数据错误');
            console.log(response);
          }
        )
      }
    },
    mounted: function () {
      this.checkedColumns = ['idc', 'rack', 'storage', 'tags'];
      this.reloadTable();
    }
  }
</script>

<style scoped>
  .div-search {
    margin: 10px;
  }
  .div-search .el-input {
    width: 300px;
  }
  .table-expand {
    font-size: 0;
  }
  .table-expand label {
    width: 90px;
    color: #99a9bf;
  }
  .table-expand .el-form-item {
    margin-right: 0;
    margin-bottom: 0;
    width: 50%;
  }
</style>

主机编辑

src/components/MachineEdit.vue

<template>
  <div>
    <el-alert v-if="changeMode" title="修改服务器" center type="info" :closable="false"></el-alert>
    <el-alert v-else title="编辑服务器" center type="info" :closable="false"></el-alert>
    <div class="div-form">
      <el-form label-position="left" status-icon ref="machineForm" :model="machineForm" :rules="rules"
        label-width="120px">
        <el-form-item v-if="changeMode" label="ID" prop="id">
          <el-input :disabled="changeMode" v-model="machineForm.id"></el-input>
        </el-form-item>
        <el-form-item label="主机名" prop="hostname">
          <el-input v-model.trim="machineForm.hostname" placeholder="请输入主机名"></el-input>
        </el-form-item>
        <el-form-item label="机房/机架位" prop="idcSpecification">
          <el-cascader v-model="machineForm.idcSpecification" placeholder="请选择机房/机架位"
            :options="idcSpecificationOptions" filterable clearable change-on-select> 
          </el-cascader>
        </el-form-item>
        <el-form-item v-for="(storage, index) in machineForm.storages"
          :key="index + machineForm.storages.length" :label="'存储信息 ' + index"
          prop="storages[index]">
          <storageEdit ref="storageEdit" :storageMediaOptions="storageMediaOptions"
            :storageForm="machineForm.storages[index]">
          </storageEdit>
          <el-button size="mini" v-if="index === 0" @click.prevent="addItem('storages', {media: '', volume: ''})">
            添加
          </el-button>
          <el-button size="mini" v-else @click.prevent="removeItem('storages', index)">
            删除
          </el-button>
        </el-form-item>

        <el-form-item label="IP地址" prop="ip">
          <el-input v-model.trim="machineForm.ip" placeholder="请输入IP地址"></el-input>
        </el-form-item>
        <el-form-item label="标签" props="tags">
          <tagEdit :tags.sync="machineForm.tags"></tagEdit>
        </el-form-item>
      </el-form>
      <div class="div-button">
        <el-button @click="resetForm('machineForm')">重置</el-button>
        <el-button type="primary" @click="saveSubmit('machineForm')">保存</el-button>
      </div>
    </div>
  </div>
</template>

<script>
  import moment from 'moment';
  import storageEdit from '@/components/StorageEdit.vue';
  import tagEdit from '@/components/TagEdit.vue';
  export default {
    components: {
      'storageEdit': storageEdit,
      'tagEdit': tagEdit
    },
    data: function () {
      return {
        changeMode: false,
        idcSpecificationOptions: [],
        storageMediaOptions: [],
        machineForm: {
          id: '',
          hostname: '',
          idcSpecification: [],
          storages: [{media: '', volume: ''}],
          ip: '',
          tags: [],
        },
        rules: {
          hostname: [
            {required: true, message: '请输入服务器名', trigger: 'blur'},
            {max: 50, message: '长度不得超过50个字符', trigger: 'blur'}
          ],
          comments: [
            {max: 500, message: '长度不得超过500个字符', trigger: 'blur'}
          ]
        }
      }
    },
    watch: {
      // 如果路由有变化,会再次执行该方法
      $route: function (to, from) {
        if (to.path === '/machineAdd') {
          this.changeMode = false;
        } else {
          this.changeMode = true;
          this.machineForm.id = to.path.split('/')[2];
        }
        this.initialize();
      }
    },
    methods: {
      initialize: function () {
        var _this = this;
        var url = '';
        if (this.changeMode) {
          url = '/api/inventory/machineChange/' + this.machineForm.id + '/';
        } else {
          url = '/api/inventory/machineAdd/';
        }
        this.$http.get(url).then(function (response) {
          _this.machineForm = response.data.machineForm;
          _this.idcSpecificationOptions = response.data.idcSpecificationOptions;
          _this.storageMediaOptions = response.data.storageMediaOptions;
        }).catch(function (error) {
          _this.$message.error('获取编辑项数据错误')
        });
      },
      removeItem: function (list, item) {
        var index = this.machineForm[list].indexOf(item)
        if (index !== 0) {
          this.machineForm[list].splice(index, 1)
        }
      },
      addItem: function (list, item) {
        this.machineForm[list].push(item);
      },
      checkForm: function () {
        var valid = true;
        var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
        if (!reg.test(this.machineForm.ip)){
            this.$notify.error({title: '错误', message: '请输入正确的IP地址'});
            valid = false;
            return valid;
        }
        return valid;
      },
      saveSubmit: function (formName) {
        if (!this.checkForm()) {
          return false;
        }
        const promises = this.$refs.storageEdit.map(function (storage) {
            return storage.validateForm();
          });
        var _this = this;
        Promise.all(promises).then(function (valid) {
          console.log(valid);
          _this.$refs[formName].validate((valid) => {
            if (valid) {
              var url = '';
              _this.changeMode ? url = '/api/inventory/changeMachine/' : url = '/api/inventory/addMachine/';
              _this.$http.post(url, _this.machineForm).then(function (response) {
                _this.$alert('保存服务器信息成功', '成功', {
                  confirmButtonText: '确定',
                  callback: action => {
                    var machineId = response.data.id;
                    if (machineId) {
                      _this.$router.push({name: 'MachineChange', params: {id: machineId}});
                    }
                  }
                });
              }).catch(function (error) {
                if (error.response.status === 403) {
                  _this.$message.warning('没有权限操作');
                } else {
                  _this.$message.error('保存服务器信息失败');
                }
              });
            } else {
              _this.$message.error('未通过表单验证');
              return false;
            }
          });
        }).catch(function (invalid) {
          console.log(invalid);
          _this.$message.error('未通过表单验证');
        });

      },
      resetForm: function (formName) {
        this.$refs[formName].resetFields();
      }
    },
    mounted: function () {
      this.machineForm.id = this.$route.params.id;
      if (typeof this.machineForm.id === "undefined" || this.machineForm.id === null ||
        this.machineForm.id === '') {
        this.changeMode = false;
      } else {
        this.changeMode = true;
      }
      this.initialize();
    }
  }
</script>

<style scoped>
  .div-form {
    margin-top: 10px;
    margin-left: 15%;
    margin-right: 15%;
  }
  .el-form-item {
    width: 90%;
  }
  .el-select {
    width: 100%;
  }
  .el-cascader {
    width: 100%;
  }
  div.el-cpu div.el-input-group__prepend {
    width: 50%;
  }
  .el-cpu-model {
    width: 400px;
  }
  .el-nic-model {
    width: 350px;
  }
  .el-nic-count {
    width: 87%;
    float: left;
  }
  .el-nic-button {
    width: 13%;
    float: left;
  }
  .el-ip-address {
    width: 87%;
    float: left;
  }
  .el-ip-button {
    width: 13%;
    float: left;
  }
  .div-button {
    margin-left: 30%;
    margin-bottom: 10px;
  }
</style>

存储编辑

src/components/StorageEdit.vue

<template>
  <el-form label-position="left" size="mini" status-icon ref="storageForm" :model="storageForm"
    :rules="rules" label-width="100px">
    <el-form-item label="存储" prop="volume">
      <el-input placeholder="请输存储容量" v-model.number="storageForm.volume">
        <el-select class="el-storage-media" v-model="storageForm.media" slot="prepend"
          placeholder="请选择存储介质">
          <el-option v-for="(item, index) in storageMediaOptions" :key="index" :label="item.label"
            :value="item.value">
          </el-option>
        </el-select>
        <template slot="append">GB</template>
      </el-input>
    </el-form-item>
  </el-form>
</template>

<script>
  export default {
    props: ['storageMediaOptions', 'storageForm'],
    data: function () {
      var validateStorage = (rule, value, callback) => {
        if (this.storageForm.media === '') {
          callback(new Error('请选择存储介质'));
        } else {
          if(typeof value === 'number' && Number.isInteger(value)){
            callback();
          } else {
            callback(new Error('请输入正确存储大小'));
          }
        }
      };
      return {
        rules: {
          volume: [
            {required: true, validator: validateStorage, trigger: 'blur'}
          ]
        }
      }
    },
    methods: {
      validateForm: function () {
        return new Promise( (resolve, reject) => {
          this.$refs['storageForm'].validate( (valid) => {
            console.log(valid);
            if (valid) {
              resolve(valid);
            } else {
              reject(valid);
            }
          });
        });
      },
      resetForm: function () {
        this.$refs['storageForm'].resetFields();
      }
    }
  }
</script>

<style scoped>
  .el-storage-media {
    width: 150px;
  }
  .el-form-item {
    margin-top: 20px;
    margin-bottom: 20px;
  }
</style>

标签编辑

src/components/TagEdit.vue

<template>
  <div>
    <el-tag type="success" v-for="(tag, index) in tags" :key="index" closable
      :disable-transitions="false" @close="handleClose(tag)">
      {{tag}}
    </el-tag>
    <el-input class="input-new-tag" v-if="inputVisible" v-model="inputValue" ref="saveTagInput"
      size="small" @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm">
    </el-input>
    <el-button v-else class="button-new-tag" size="small" @click="showInput">+ New Tag</el-button>
  </div>
</template>>
<script>
  export default {
    props: ['tags'],
    data: function() {
      return {
        inputVisible: false,
        inputValue: '',
        newTags: []
      };
    },
    watch: {
        tags: function(val){
          this.newTags = val;
        }
    },
    methods: {
      handleClose: function (tag) {
        this.tags.splice(this.tags.indexOf(tag), 1);
      },
      showInput: function() {
        this.inputVisible = true;
        this.$nextTick(_ => {
          this.$refs.saveTagInput.$refs.input.focus();
        });
      },
      handleInputConfirm: function() {
        let inputValue = this.inputValue;
        if (inputValue) {
          this.newTags.push(inputValue);
          this.$emit("update:tags", this.newTags);
        }
        this.inputVisible = false;
        this.inputValue = '';
      }
    }
  }
</script>
<style scoped>
  .el-tag + .el-tag {
    margin-left: 10px;
  }
  .button-new-tag {
    margin-left: 10px;
    height: 32px;
    line-height: 30px;
    padding-top: 0;
    padding-bottom: 0;
  }
  .input-new-tag {
    width: 90px;
    margin-left: 10px;
    vertical-align: bottom;
  }
</style>

主机自定义过滤

src/components/MachineFilter.vue

<template>
  <div v-show="searchFormVisible" class="div-search">
    <el-form ref="searchForm" label-position="left" :model="searchForm" label-width="80px"
      size="mini" :inline="true">
      <el-form-item label="服务器名">
        <el-input class="el-input-pure" v-model="searchForm.hostname"></el-input>
      </el-form-item>
      <el-form-item label="IP">
        <el-input v-model="searchForm.ip"></el-input>
      </el-form-item>
      <el-form-item label="机房">
        <el-select v-model="searchForm.idc" multiple filterable clearable placeholder="请选择机房">
          <el-option v-for="(item, index) in idcOptions" :key="index" :label="item.label" :value="item.value">
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="HDD">
        <el-input v-model.number="searchForm.hdd">
          <template slot="prepend">>=</template>
          <template slot="append">GB</template>
        </el-input>
      </el-form-item>
      <el-form-item label="SSD">
        <el-input v-model.number="searchForm.ssd">
          <template slot="prepend">>=</template>
          <template slot="append">GB</template>
        </el-input>
      </el-form-item>
      <el-form-item label="上架时间">
        <el-date-picker v-model="searchForm.productiveRange" type="daterange" unlink-panels
          range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
          :picker-options="$store.state.datePickerOptions">
        </el-date-picker>
      </el-form-item>
      <el-form-item label="修改时间">
        <el-date-picker v-model="searchForm.warrantyExpireRange" type="daterange" unlink-panels
          range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
          :picker-options="$store.state.datePickerOptions">
        </el-date-picker>
      </el-form-item>
      <el-form-item label="标签">
        <el-select v-model="searchForm.tags" multiple filterable clearable
          placeholder="请选择服务器标签">
          <el-option v-for="(item, index) in tagOptions" :key="index" :label="item"
            :value="item">
          </el-option>
        </el-select>
      </el-form-item>
    </el-form>
    <el-button type="primary" size="mini" @click="advancedSearch">筛选</el-button>
    <el-button size="mini" @click="hideFilter">取消</el-button>
  </div>
</template>

<script>
  export default {
    props: ['searchFormVisible', 'searchForm'],
    data: function () {
      return {
        idcOptions: [],
        tagOptions: []
      }
    },
    methods: {
      initialize: function () {
        var _this = this;
        this.$http.get('/api/inventory/machineFilterOptions/').then(function (response) {
          _this.idcOptions = response.data.idcOptions;
          _this.tagOptions = response.data.tagOptions;
        }).catch(function (error) {
           _this.$message.error('获取筛选项数据错误');
        });
      },
      advancedSearch: function () {
        this.$emit('advancedSearch');
      },
      hideFilter: function () {
        this.$emit('update:searchFormVisible', false);
      }
    },
    mounted: function () {
      this.initialize();
    }
  }
</script>

<style scoped>
  .el-date-editor--daterange.el-input, .el-date-editor--daterange.el-input__inner, .el-date-editor--timerange.el-input, .el-date-editor--timerange.el-input__inner {
    width: 300px;
  }
  .el-form-item {
    width: 45%;
  }
  .el-input {
    width: 300px;
  }
  .el-select {
    width: 300px;
  }
</style>