一、业务需求
公司因业务发展的需要,需自主研发云管控台,在实现以往云管控台的功能的基础上对架构和业务逻辑进行优化,使其更加符合公司的实际业务、运维可控、更新迭代更及时。
作为本项目的前端开发,我主要负责云资源从申请到开通的全部功能。其中资源申请部分最为复杂,需要实现ECS、EVS、EIP、NAT、各种数据库等十几种云产品的申请,若各个页面单独完成,由使用的地方自己引入会显得比较杂乱,通常会用到长串的if-else。另外,各种云产品的申请页面均不相同,但又有部分类似,类似的部分每个页面都写一份,对于后续的维护将是灾难。
二、需求分析
通过业务需求,可以看出这部分业务主要存在两个痛点,一是有十几种产品,需要对外提供统一的接入方式;二是不同的产品之间差异比较大,但是又有部分类似之处。
对于痛点一,直接使用策略模式即可,将所有产品封装在一个Content类中,由这个类统一向外提供服务即可。
痛点二则相对复杂,需要仔细分析不同产品之间的相同和不同之处,将一个产品需要用到的所有功能拆分为尽可能小的可复用的功能模块,这些功能模块要尽可能满足单一职责原则。经过仔细分析和拆解,将资源申请需要使用到的功能模块差分为
- Region:产品所属区域以及可用区组件
- Spec: 产品规格组件
- SystemDisk:系统盘组件
- DataDisk:数据盘组件
- Image:服务器镜像组件
- Network:网络模块组件,包含了VPC、子网、安全组
- Name:资源名称组件
- Size:大小组件,例如EIP的带宽,EVS的存储大小等
- password:密码组件
部分产品需聚合的组件如下:
产品 | 组件组合 |
---|---|
ECS | Region、Spec、SystemDisk、DataDisk、Image、Network、Name、password |
EVS | Region、Spec、Size |
RDS | Region、Spec、Name、Size、Network |
Redis | Region、Spec、Name、Network、password |
不同的组件各自实现自己独有的功能,例如 Region 组件内部实现区域和可用区的查询、展示、和区域可用区选择后的双向绑定;Spec 组件实现查询当前产品的所有规格,并处理成可在页面显示选择的样式;Network 组件实现可用VPC、子网、安全组的查询、显示、选择后的数据绑定等。
三、结构设计与详细设计
按(二)中的分析结果,可以得到此部分的设计类图。如下:
- Content: 聚合了 ResourceStrategy , 通过接收 category (产品类别)来调用不同的产品申请页面,实现根据不同产品选择不同页面,并向外部提供了getOrderData() 方法,供外部获取当前产品选择的配置订单信息;
- ResourceStrategy: 实际并不存在,因为vue中没有组件实现接口这种操作,这算是一种约定,所有的产品都必须实现 getOrderData() 方法,这是方便 Content 调用;
- FormComponent: 实际也并不存在,就是规定了 Region、Spec、SystemDisk 等各个小模块必须要实现validate() 方法,这个方法是用于验证当前模块数据是否可用
- Region、Spec等:就是具体模块实现Region 组件内部实现区域和可用区的查询、展示、和区域可用区选择后的双向绑定;Spec 组件实现查询当前产品的所有规格,并处理成可在页面显示选择的样式;Network 组件实现可用VPC、子网、安全组的查询、显示、选择后的数据绑定等。
- ECS/EVS等各类云产品:通过聚合Region、Spec等组件实现各个产品的完整功能
四、代码实现
Content: 利用 Vue 的 component 组件实现策略选择,代码如下:
VUE
<template>
<component
ref="resourceInfoRef"
:is="curComponent"
:category="props.category"
@spec-change="emitSpecChange"
/>
</template>
<script lang="ts" setup>
import ECS from './ECS.vue'
import EIP from './EIP.vue'
import EVS from './EVS.vue'
import REDIS from './REDIS.vue'
... // 产品很多,这里省略
const components = {
// 服务器
[ResourceMapEnum.ECS]: ECS,
// 存储
[ResourceMapEnum.EVS]: EVS,
[ResourceMapEnum.EIP]: EIP,
// 数据库
[ResourceMapEnum.REDIS]: REDIS,
...
}
const props = defineProps({
category: {
type: Object as PropType<CategoryVO>,
default: () => {},
required: true
}
})
const curComponent = shallowRef()
onMounted(() => {
initConponent()
})
watch(
() => props.category,
() => {
initConponent()
emitSpecChange(null)
},
{ deep: true }
)
const initConponent = () => {
const { productType } = props.category
// productType 查看字典 product_type 对应的值
if (productType && curComponent.value !== components[productType]) {
curComponent.value = components[productType]
}
}
const resourceInfoRef = ref()
const getOrderData = () => {
return resourceInfoRef.value?.getOrderData?.()
}
defineExpose({ getOrderData })
</script>
其中一个产品的部分代码
VUE
<template>
<div class="rescoure-main-page" v-loading="!productLoaded">
<div class="module-title">规格配置</div>
<div class="module-content">
<!-- 区域和可用区,分别绑定regionCode 和 zoneCode -->
<ResourceArea
ref="areaRef"
v-model:region-code="formData.regionCode"
v-model:region-name="formData.regionName"
v-model:zone-code="formData.zoneCode"
v-model:zone-name="formData.zoneName"
/>
<!-- 规格 v-if="areaLoaded" -->
<ResourceSpec
ref="specRef"
:category-id="props.category.id"
:region-code="formData.regionCode"
:zone-code="formData.zoneCode"
:product-type="props.category.productType"
v-model:spec-code="formData.specCode"
v-model:spec-name="formData.specName"
v-model:spec-price="formData.specPrice"
/>
<!-- 系统盘 -->
<Disk
v-if="areaLoaded"
:region-code="formData.regionCode"
:zone-code="formData.zoneCode"
v-model:system-disk="formData.orderAttr.systemDisk"
/>
<!-- 数据盘 -->
<DataDisk
ref="dataDiskRef"
:region-code="formData.regionCode"
:zone-code="formData.zoneCode"
v-model:disks="formData.orderAttr.dataDisk"
/>
<!-- 镜像 -->
<Image
ref="imageRef"
:region-code="formData.regionCode"
:zone-code="formData.zoneCode"
v-model:image-id="formData.orderAttr.imageId"
v-model:image-name="formData.orderAttr.imageName"
/>
<!-- 云服务器名称 -->
<ResourceName
ref="resourceNameRef"
:label="'云服务器名称'"
:zone-code="formData.zoneCode"
:product-type="props.category.productType"
v-model:resource-name="formData.resourceName"
/>
<!-- 密码 -->
<password
ref="passwordRef"
v-model:password="formData.orderAttr.password"
/>
</div>
<div class="module-title">网络配置</div>
<div class="module-content">
<!-- 网络 -->
<NetWork
ref="networkRef"
v-model:vpc-id="formData.orderAttr.vpcId"
v-model:vpc-name="formData.orderAttr.vpcName"
v-model:subnet-id="formData.orderAttr.subnetId"
v-model:subnet-name="formData.orderAttr.subnetName"
v-model:security-group-id="formData.orderAttr.securityGroupId"
v-model:security-group-name="formData.orderAttr.securityGroupName"
v-model:custom-i-p="formData.orderAttr.customIP"
:region-code="formData.regionCode"
:ip-type="formData.orderAttr.version"
:regionCloudResource="formData.regionCloudResource"
/>
</div>
</div>
</template>
<script lang="ts" setup>
const formData = ref({
regionCloudResource: '',
productType: '',
regionCode: '',
zoneCode: '',
regionName: '', // 区域名称
zoneName: '', // 可用区名称
architecture: '', // 架构类型,
resourceType: props.category.productType,
resourceName: '', // 云服务器名称
specCode: '', // 产品规格ID
specPrice: 0,
specName: '',
orderAttr: {
imageId: '', // 镜像ID
imageName: '',
system: '', // 系统
vpcId: '', // 虚拟私有云ID
vpcName: '',
version: '',
versionName: '',
securityGroupId: '', // 安全组ID
securityGroupName: '',
subnetId: '', // 子网ID
subnetName: '',
customIP: '', // 自定义IP
systemDisk: {
type: '', // 系统盘SKU ID
price: 0,
name: '',
size: 0 // 系统盘大小
},
dataDisk: [],
password: '', // 密码
secureImageId: '' // 安全镜像ID
}
})
const areaRef = ref()
const specRef = ref()
const addressVersionRef = ref()
const systemDiskRef = ref()
const dataDiskRef = ref()
const imageRef = ref()
const resourceNameRef = ref()
const passwordRef = ref()
const networkRef = ref()
const verifyFormData = () => {
let result = true
try {
areaRef.value.validate()
specRef.value.validate()
addressVersionRef.value?.validate()
systemDiskRef.value.validate()
dataDiskRef.value.validate()
imageRef.value.validate()
resourceNameRef.value.validate()
passwordRef.value.validate()
networkRef.value.validate()
} catch (error) {
console.log(error)
result = false
}
return result
}
const getOrderData = () => {
return formData.value
}
defineExpose({ getOrderData })
</script>
各个小模块中使用了 Vue3 的 defineModel 来实现多个值的绑定,具体小模块的代码就属于核心代码啦!这里就不展示了。
五、个人的一点点感想
其实,在实际开发中并不是按上面所写的步骤一步一步往下做的,类图都是代码都写完了快上线之前才抽时间画出来的。但是在开发这个模块之前确实有花了几天时间反复看老系统,边看边琢磨边问懂业务的同事才慢慢理解业务,从而将复杂的页面按职责拆分为不同的小模块单独开发。可能本身是学过 java 以及面向对象设计原则的原因,脑子里有接口的概念,所以设计时特意让每个小模块实现了同样方法,并且每种资源类型也实现了同样的方法。
另外,申请这个业务一开始技术经理的策略是按产品分开做,例如A程序员负责ECS/EIP...,B程序员负责RDS,Redis...这种策略,我加入这个模块开发时,已经有同事开发完成ECS的页面,但是看了她的代码人直接麻了。贴下面给大家看看,光是定义的变量都看麻了(其实这位同事的样式写得挺好,我拆分的小模块就是以她的代码为基础改造的)。我分析完后直接给技术经理商量改变了策略,我一个人完成所有资源的申请部分,其他同事分别完成资源管理的部分,若是分开写,后续直接无法维护。
VUE
<template>
<div class="rescoure-main-page">
<div class="module-title">规格配置</div>
<div>
<!-- 节点 -->
<div class="info-item">
<span class="info-label">节点</span>
<div class="info-main">
<div>
<el-radio-group v-model="nodeResult" @change="handleAvailableArea">
<el-radio-button
class="radio-option-btn"
v-for="item in nodeListOption"
:key="item.nodeCode"
:label="item.nodeCode"
>{{ item.nodeName }}</el-radio-button
>
</el-radio-group>
</div>
</div>
</div>
<!-- 可用区 -->
<div class="info-item">
<span class="info-label">可用区</span>
<div class="info-main">
<el-radio-group v-model="availableArea" @change="handleAvailableArea">
<el-radio-button
class="radio-option-btn"
v-for="item in availableAreaList"
:key="item.zoneCode"
:label="item.zoneCode"
>{{ item.zoneName }}</el-radio-button
>
</el-radio-group>
<el-tooltip
popper-class="el-popover-self "
placement="bottom-start"
effect="light"
trigger="hover"
>
<template #content>
<div class="popover-tips">xxx
</div>
</template>
<el-icon class="icon-quest bottom-icon-style"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</div>
<!-- 规格版本 -->
<div class="info-item">
<div class="info-label">规格版本</div>
<div class="info-main">
<el-radio-group v-model="specType" @change="changSpecType">
<el-radio-button
class="radio-option-btn"
v-for="(item, index) in specTypeList"
:key="index"
:label="item.categoryCode"
>{{ item.category }}</el-radio-button
>
</el-radio-group>
</div>
</div>
<!-- 规格 -->
<div class="info-item">
<span class="info-label">规格</span>
<div class="info-main">
<div class="specification-main">
<div class="spec-filter">
<el-input
class="info-input-180"
v-model="standardName"
style="width: 250px"
placeholder="请输入规格名称"
clearable
@clear="standardclick"
>
<template #suffix>
<el-icon class="el-icon-search" @click="handleQueryStandard"><Search /></el-icon>
</template>
</el-input>
</div>
<div class="specification-table">
<el-table
border
v-if="specType"
:data="performDataList"
style="width: 100%"
max-height="280"
ref="singleTable"
highlight-current-row
:header-cell-style="headClass"
@current-change="handleCurrentChange"
>
<el-table-column prop="specName" label="规格名称" width="440" />
<el-table-column prop="monthPrice" label="规格参考价(¥)" width="140">
<template #default="scope">
<div>{{ scope.row.monthPrice }}元/月</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
<!-- 系统盘 -->
<div class="info-item">
<span class="info-label">系统盘</span>
<div class="info-main">
<el-select
class="info-select-140"
v-model="systemDisk"
placeholder="请选择"
@change="handleSystemDisk"
value-key="value"
:popper-append-to-body="false"
>
<el-option
v-for="(item, index) in systemDiskList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-input-number
class="info-input-number system-disk-num"
:max="ECSmax"
:min="ECSmin"
@keydown="channelInputLimit"
v-model="currentSystemValueEcs"
/>
<span class="input-number-unit">GB</span>
<div class="create-btn-list">
<el-checkbox v-model="customizeVal" @click="handlerOpenCustomize"
>开启自定义备份策略</el-checkbox
>
<el-select
v-model="snapTactics"
placeholder="请选择自动快照策略"
size="small"
class="selece-snap"
style="width: 150px"
>
<el-option
v-for="item in optionsSnap"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span class="create-btn">创建自定义快照策略>>></span>
</div>
</div>
</div>
<div class="chang">可选硬盘容量:{{ ECSmin }}--{{ ECSmax }}</div>
<!-- 数据盘 -->
<div class="datadisk-info-main" v-for="(item, index) in datadiskData" :key="item.id">
<div class="info-item">
<span class="info-label">
<template v-if="index == 0">数据盘</template>
</span>
<div class="info-main">
<el-select
class="info-select-140"
v-model="item.systemNumDisk"
placeholder="请选择"
@change="handleDataDisk(item)"
:popper-append-to-body="false"
>
<el-option
v-for="i in DiskList"
:key="i.specCode"
:label="i.specName"
:value="i.specCode"
/>
</el-select>
<!-- :step="item.dataStepValue" -->
<el-input-number
class="info-input-number system-disk-num"
v-model="item.systemDiskSizeValue"
@change="handleDataDiskSize(item)"
@keyup.enter="handleDataDiskSizeBlur(item)"
@keydown="channelInputNum(item)"
:min="item.min"
:max="item.max"
/>
<span class="input-number-unit">GB</span>
<div>
<el-icon class="delete-btn-icon" @click="handleDelDatadisk(index)"
><Delete
/></el-icon>
</div>
</div>
</div>
<div class="chang">可选硬盘容量:{{ item.min }}--{{ item.max }}</div>
</div>
<div class="info-item">
<span class="info-label"></span>
<div class="info-main">
<div>
<div class="create-btn-list">
<span class="heard-icon">-</span>
<span class="create-btn" @click="openSnapDataDisks">快照创建数据盘</span>
</div>
<div class="create-btn-list">
<span class="heard-icon">-</span>
<el-checkbox v-model="customizeVal" @click="handlerOpenCustomize"
>开启自定义备份策略</el-checkbox
>
<el-select
v-model="snapTactics"
placeholder="请选择自动快照策略"
size="small"
class="selece-snap"
style="width: 150px"
>
<el-option
v-for="item in optionsSnap"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span class="create-btn">创建自定义快照策略>>></span>
</div>
</div>
</div>
</div>
<div class="info-item">
<span class="info-label"></span>
<div class="info-main">
<div
class="add-datadisk"
:class="DATADISTNUM - maxNumDisk === 0 ? 'add-datadisk-disabled' : ''"
@click="handleAdddDtadisk"
>
<span style="color: #3369ff">+添加数据盘</span> 您还可以挂载
{{ DATADISTNUM - maxNumDisk }} 块磁盘
</div>
</div>
</div>
<!-- 镜像 -->
<div class="info-item">
<span class="info-label">镜像</span>
<div class="info-main">
<div class="specification-main">
<el-radio-group v-model="mirror" @change="handleMirror">
<el-tooltip
v-for="item in mirrorImList"
:key="item.value"
effect="light"
popper-class="question-mark-tips"
placement="top-start"
>
<template #content>
<div class="popover-tips">
<span>
{{ item.label }}:
<br />
{{ item.info }}
</span>
</div>
</template>
<el-radio-button class="radio-option-btn" :key="item.value" :label="item.value">
{{ item.label }}
</el-radio-button>
</el-tooltip>
</el-radio-group>
<div class="system-version-select">
<el-select
v-model="operatingSystemVal"
placeholder="--请选择操作系统--"
@change="handleoPeratingSystem"
:popper-append-to-body="false"
clearable
:disabled="!!sendRequestCount"
style="margin-right: 12px"
>
<el-option
v-for="item in operatingSystemListVal"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-model="operateSysVersionVal"
@change="handleOperateSysVersionEcs"
value-key="id"
placeholder="--请选择操作系统版本--"
class="info-select-320"
:disabled="!!sendRequestCount"
clearable
>
<el-option
v-for="item in operateSysVersionsDataListVal"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-tooltip
popper-class="el-popover-self"
placement="bottom-start"
effect="light"
trigger="hover"
>
<template #content>
<div class="popover-tips">
镜像:
<br />xxx
<br />
<br />公共镜像:
<br />xxx
<br />
</div>
</template>
<el-icon class="icon-quest"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</div>
</div>
</div>
<!-- 云服务器名称 -->
<div class="info-item">
<span class="info-label">云服务器名称</span>
<div class="info-main">
<div>
<el-input v-model="cloudServerName" placeholder="请输入云服务器名称" clearable />
<div class="popup-tip" v-if="cloudServerName == ''">请输入云服务器名称</div>
</div>
</div>
</div>
<!-- 登录凭证 -->
<div class="info-item">
<span class="info-label">登录凭证</span>
<div class="info-main">
<div>
<el-radio-group v-model="loginVoucher" @change="handleAvailableArea">
<el-radio-button label="1">密码</el-radio-button>
<el-radio-button label="2">创建后设置</el-radio-button>
</el-radio-group>
<div class="popup-tip" v-if="loginVoucher == '2'"
>xxx</div
>
</div>
</div>
</div>
<!-- 用户名 -->
<div class="info-item" v-if="loginVoucher == '1'">
<span class="info-label">用户名</span>
<div class="info-main">
<div>
<div v-if="operatingSystem == 'Windows'">Administrator</div>
<div v-else class="show-info-box">root</div>
<div class="popup-tip">xxx</div>
</div>
</div>
</div>
<!-- 密码 -->
<div class="info-item" v-if="loginVoucher == '1'">
<span class="info-label">密码</span>
<div class="info-main">
<el-input
v-model="password"
ref="passwordInput"
maxlength="26"
minlength="8"
placeholder="请输入密码"
type="password"
@focus="handleFocusPassword"
clearable
/>
<el-tooltip
v-if="!password"
ref="popoverRef"
:virtual-ref="passwordInput"
placement="right"
effect="light"
>
<template #content>
<div class="popover-tips">
<h4 class="h4">密码规则</h4>
<div class="dl">
<el-icon class="icon-style-red"><WarningFilled /></el-icon>
<span>xxx</span>
<br />
<el-icon class="icon-style-red"><WarningFilled /></el-icon>
<span
>xxx</span
>
<br />
<el-icon class="icon-style-success"><CircleCheckFilled /></el-icon>
<span>xxx</span>
</div>
</div>
</template>
</el-tooltip>
</div>
</div>
<!-- 再次确认密码 -->
<div class="info-item">
<span class="info-label">再次确认密码</span>
<div class="info-main">
<div>
<el-input
class="info-input-240"
v-model="confirmPassword"
placeholder="请再次输入密码"
maxlength="26"
minlength="8"
type="password"
@input="handlePasswordVerifyComfirm"
clearable
/>
<div class="popup-tip" v-if="passwordVerify2">两次输入密码不一样</div>
<div class="popup-tip">xxx</div>
</div>
</div>
</div>
</div>
<div class="module-title">网络配置</div>
<div>
<!-- 网络 -->
<div class="info-item">
<span class="info-label">网络</span>
<div class="info-main">
<div>
<div class="spec-filter">
<el-select
class="info-select-240"
v-model="sleVpcId"
@change="handlePrivateCloud"
placeholder="请选择虚拟私有云"
value-key="id"
:popper-append-to-body="false"
clearable
>
<el-option
v-for="item in vpclistVuex"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<img
@click="hnadleVpcListRefresh"
src="@/assets/imgs/refresh.png"
width="16"
height="16"
class="refresh"
alt=""
:class="refreshVpcLoading ? 'refresh-rotate' : ''"
/>
<el-select
class="info-select-240"
v-model="subnet"
@change="handleSubnet"
placeholder="请选择子网"
value-key="id"
:disabled="netchild"
:popper-append-to-body="false"
clearable
>
<el-option
v-for="item in subnetList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<img
@click="hnadleSubnetListRefresh"
src="@/assets/imgs/refresh.png"
width="16"
height="16"
class="refresh"
alt=""
:class="refreshSubnetLoading ? 'refresh-rotate' : ''"
/>
<!-- <el-tooltip
popper-class="el-popover-self"
placement="bottom-start"
effect="light"
trigger="hover"
>
<template #content>
<div class="popover-tips">
虚拟私有云:
<br />xxx
<br />
</div>
</template>
<el-icon class="icon-quest"><QuestionFilled /></el-icon>
</el-tooltip> -->
</div>
<div class="check-ip-type">
<el-checkbox v-model="checkedAllocationIp">指定主网卡主私网IP地址 </el-checkbox>
<div class="input-number-ip">
<el-input
v-model="ipA"
@input="(val) => (ipA = val.replace(/[^0-9]/g, '').slice(0, 4))"
/>
<span class="dian-dian">.</span>
<el-input
v-model="ipB"
@input="(val) => (ipB = val.replace(/[^0-9]/g, '').slice(0, 4))"
/>
<span class="dian-dian">.</span>
<el-input
v-model="ipC"
@input="(val) => (ipC = val.replace(/[^0-9]/g, '').slice(0, 4))"
/>
<span class="dian-dian">.</span>
<el-input
v-model="ipD"
@input="(val) => (ipD = val.replace(/[^0-9]/g, '').slice(0, 4))"
/>
</div>
</div>
<!-- <el-select
class="info-select-160"
v-model="allocationIp"
@change="handleAllocationIp"
placeholder="自动分配IP地址"
value-key="value"
:popper-append-to-body="false"
>
<el-option label="自动分配IP地址" value="00" />
</el-select> -->
<div class="info-main-tips-box">
<p class="info-main-tips"
>如需创建新的安全组,您可<span
style="color: #3369ff; cursor: pointer"
@click="creatGroup"
>
前往xxx创建 </span
>。</p
>
</div>
</div>
</div>
</div>
<!-- 安全组 -->
<div class="info-item">
<span class="info-label">安全组</span>
<div class="info-main">
<div>
<div class="spec-filter">
<el-select
class="info-select-280"
v-model="securityGroupId"
placeholder="请选择安全组"
:popper-append-to-body="false"
@change="handleSecurityGroup"
clearable
>
<el-option
v-for="item in securityGroupList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<img
@click="hnadleSecurityGroupRefresh"
src="@/assets/imgs/refresh.png"
width="16"
height="16"
class="refresh"
alt=""
:class="refreshSecurityGroupLoading ? 'refresh-rotate' : ''"
/>
</div>
<div class="info-main-tips-box">
<p class="info-main-tips"
>如需创建新的安全组,您可<span
style="color: #3369ff; cursor: pointer"
@click="creatGroup"
>
前往xxx创建 </span
>。</p
>
<p class="info-main-tips"
>xxx</p
>
<p class="info-main-tips tips-color"
>xxx</p
>
</div>
</div>
</div>
</div>
</div>
<ConfigureView />
<el-dialog
v-model="showSnapDataDisks"
title="用快照创建数据盘"
width="500"
:before-close="handleCloseDataDisks"
>
<span>快照列表</span>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="showSnapDataDisks = false"> 确定 </el-button>
<el-button @click="showSnapDataDisks = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {
QuestionFilled,
Search,
Delete,
WarningFilled,
CircleCheckFilled
} from '@element-plus/icons-vue'
import ConfigureView from '../ConfigureView.vue'
const nodeResult = ref('1') //节点
const nodeListOption = ref() // 节点option
const availableArea = ref(1)
const availableAreaList = ref() // 可用区数据
const specType = ref(1)
const specTypeList = ref() // 规格版本数据
const standardName = ref() // 输入的规格名称
const performDataList = ref() //规格名称表格数据
const singleTable = ref() // 规格表格ref
const systemDisk = ref() // 系统盘
const systemDiskList = ref() // 系统盘options数据
const ECSmax = ref(1024)
const ECSmin = ref(1)
const currentSystemValueEcs = ref() // 系统盘GB数
const datadiskData = ref() // 挂载的数据盘总数
const DiskList = ref() // 数据盘options数据
const DATADISTNUM = ref(5) // 最多可以挂载5个
const maxNumDisk = ref()
const customizeVal = ref(false) //是否自定义备份策略
const showSnapDataDisks = ref()
const snapTactics = ref() //快照策略
const optionsSnap = ref() //快照策略option
const mirror = ref('1') //镜像
const mirrorImList = ref()
const operatingSystemVal = ref()
const sendRequestCount = ref(false)
const operatingSystemListVal = ref()
const operateSysVersionVal = ref()
const operateSysVersionsDataListVal = ref()
const loginVoucher = ref('1') //登录凭证
const cloudServerName = ref('2344444') //云服务器名称
const operatingSystem = ref()
const password = ref(1234556) //密码
const passwordInput = ref() //密码ref
const popoverRef = ref() // 密码规则ref
const confirmPassword = ref() // 确认密码
const passwordVerify2 = ref() // 判断两次密码是否一致
const sleVpcId = ref() // 虚拟私有云
const vpclistVuex = ref() // 虚拟私有云option
const subnet = ref() // 子网
const netchild = ref(false) // 是否禁用子网
const subnetList = ref() // 子网option
const allocationIp = ref() // IP地址
const checkedAllocationIp = ref() // 是否指定主网卡ip地址
const ipA = ref()
const ipB = ref()
const ipC = ref()
const ipD = ref()
const refreshVpcLoading = ref(false) // 控制刷新样式,获取数据时为true
const refreshSubnetLoading = ref(false) // 控制刷新样式,获取数据时为true
const securityGroupId = ref() // 安全组
const securityGroupList = ref() // 安全组option
const refreshSecurityGroupLoading = ref(false)
// 可用区切换
const handleAvailableArea = () => {}
//规格版本切换
const changSpecType = () => {}
// 清空规格名称输入框
const standardclick = () => {}
// 根据规格名称查询
const handleQueryStandard = () => {}
const handleCurrentChange = () => {}
// 系统盘选择
const handleSystemDisk = () => {}
// 数据盘选择
const handleDataDisk = (item) => {}
// 规格名称表格样式
const headClass = () => {
return { height: '42px', background: 'rgb(244, 247, 252)' }
}
// 修改系统盘GB
const channelInputLimit = () => {}
// 数据盘数据变动
const handleDataDiskSize = (item) => {}
const handleDataDiskSizeBlur = (item) => {}
const channelInputNum = (item) => {}
// 添加数据盘
const handleAdddDtadisk = () => {
// 添加数据盘
const num = DATADISTNUM.value - maxNumDisk.value
if (num === 0) {
return
}
if (DiskList.value.length > 0) {
let dataitem = {
systemDisk: DiskList.value[0].specCode,
dataName: DiskList.value[0].specName
}
datadiskData.value.push(dataitem)
maxNumDisk.value = datadiskData.value.length
// $store.commit('ecs/setDatadiskData', datadiskData.value)
}
}
// 删除数据盘
const handleDelDatadisk = (index) => {
datadiskData.value.splice(index, 1)
maxNumDisk.value = datadiskData.value.length
}
// 快照创建数据盘
const openSnapDataDisks = () => {
showSnapDataDisks.value = true
}
// 关闭快照创建数据盘
const handleCloseDataDisks = () => {
showSnapDataDisks.value = false
}
// 是否自定义备份策略
const handlerOpenCustomize = () => {}
// 镜像部分
const handleMirror = () => {}
const handleoPeratingSystem = () => {}
const handleOperateSysVersionEcs = () => {}
// 显示密码规则
const handleFocusPassword = () => {
unref(popoverRef).popperRef?.delayHide?.()
}
// 判断密码是否一致
const handlePasswordVerifyComfirm = (v) => {
if (password.value !== v) {
passwordVerify2.value = true
} else {
passwordVerify2.value = false
}
}
// 选择虚拟私有云
const handlePrivateCloud = () => {}
// 网络刷新私有云
const hnadleVpcListRefresh = () => {}
// 选择子网
const handleSubnet = () => {}
// 刷新子网
const hnadleSubnetListRefresh = () => {}
// 选择ip地址
const handleAllocationIp = () => {}
// 选择安全组
const handleSecurityGroup = () => {}
// 刷新安全组
const hnadleSecurityGroupRefresh = () => {}
// 跳转控制台
const creatGroup = () => {}
// 假数据专用
const getPseudoInfo = () => {
const nodeListOption222 = [
{ nodeCode: '1', nodeName: '节点A' },
{ nodeCode: '2', nodeName: '节点B' },
{ nodeCode: '3', nodeName: '节点C' },
]
const pseudoArea = [
{ zoneCode: 1, zoneName: '可用区1' },
{ zoneCode: 2, zoneName: '可用区2' }
]
const specType222 = [
{ categoryCode: 1, category: '通用型' },
{ categoryCode: 2, category: '通用计算增强型' },
{ categoryCode: 3, category: 'AI加速型' }
]
const sepcList = [
{ specName: 'spec1', monthPrice: '31.00' },
{ specName: 'spec2', monthPrice: '31.00' }
]
const systemdisk111 = [
{ name: 'disk1', value: '40' },
{ name: 'disk2', value: '20' }
]
const DiskList222 = [
{ specCode: '1', specName: '搞笑云盘' },
{ specCode: '2', specName: '好的云南' },
{ specCode: '3', specName: '是的师大' }
]
const datadiskData222 = [
{ systemNumDisk: '1', systemDiskSizeValue: '22', min: 3, max: 88 }
]
const mirrorImList222 = [
{
value: '1',
label: '公共镜像',
},
{ value: '2', label: '自定义镜像', info: '用户自己的镜像' }
]
const operatingSystemVal222 = ['EulerOs', 'Other']
const operateSysVersionsDataListVal222 = [
{ id: 1, name: 'hshsus' },
{ id: 2, name: 'xgxgxg' },
{ id: 3, name: 'lklklk' }
]
nodeListOption.value = nodeListOption222
availableAreaList.value = pseudoArea
specTypeList.value = specType222
performDataList.value = sepcList
systemDiskList.value = systemdisk111
DiskList.value = DiskList222
datadiskData.value = datadiskData222
maxNumDisk.value = datadiskData.value.length
mirrorImList.value = mirrorImList222
operatingSystemListVal.value = operatingSystemVal222
operateSysVersionsDataListVal.value = operateSysVersionsDataListVal222
// 高亮列表第一行
singleTable.value.setCurrentRow(performDataList.value[0])
}
// 高亮显示规格列表第一行
watch(
() => performDataList,
() => {
nextTick(() => {
singleTable.value.setCurrentRow(performDataList.value[0])
})
}
)
onMounted(() => {
getPseudoInfo()
})
</script>
<style lang="scss" src="./styleFiles/styleCss.scss" scoped></style>
经过以往的项目,也发现很多前端的同事主要都是关注Vue语法、或者具体业务实现了,在程序设计方面有所缺失,因此有以下几点小小的感想
5.1 前端也需要懂设计原则
单一职责原则、里氏替换原则、开闭原则、依赖倒置原则、迪米特法则,接口隔离原则 六大面向对象程序设计原则是值得每一个前端开发深入学习和理解的。初学不知其然,但是在工作中已经受到其潜移默化的影响。
在实际开发中,是否需要封装,封装到什么程度往往是很难以衡量的,见过将一个简单页面拆得稀碎的,也见过复杂页面从不拆分组件的。不同的人有不同的思维方式,想法不一样,因此做法也不一样。设计原则就是统一思想的利器,如果大家都学过,并且深入理解了,那在实际运用中应该也会写出比较类似的代码吧。学习设计原则之前我自己也出现过复杂页面没有拆分导致后期只有自己能维护的情况。
5.2 前端也需要懂设计模式
设计模式,简单来说就是面向对象程序设计原则的一些具体的实践经验。按java来说,设计原则就像是定义好的接口,而设计模式就是设计原则接口的具体实现类。如果设计原则太抽象,从设计模式学习也不错,即使学完没记住几个,但是也会对你有潜移默化的影响。
5.3 考个软考中级
如果有空的话,考个软考中级,已经有开发经验的同事,通过以考促学的方式,将软件设计相关的课程系统的在学习一遍是个不错的选择。部分公司考证还有补贴、涨工资等福利哦。如何准备软考,可以参考我的另一篇文章《纯自学,软件设计师、系统架构设计师一把过》
注:文中使用的代码已获得开发者本人同意