AdjustConfigForm.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <template>
  2. <div>
  3. <page-header :title="$t('compute.text_1100')" style="margin-bottom: 7px;" />
  4. <a-card :bordered="false" size="small">
  5. <template #title>
  6. <dialog-selected-tips :name="$t('compute.vminstance-container')" :count="params.data.length" :action="$t('compute.text_1100')" />
  7. </template>
  8. <dialog-table :data="params.data" :columns="columns" />
  9. </a-card>
  10. <page-body needMarginBottom>
  11. <div class="form-wrapper">
  12. <a-form
  13. v-bind="formItemLayout"
  14. :form="form.fc">
  15. <a-form-item :label="$t('compute.text_1058')" class="mb-0">
  16. <cpu-radio
  17. :decorator="decorators.vcpu"
  18. :options="form.fi.cpuMem.cpus || []"
  19. :disable-options="disableCpus"
  20. :disabled="runningArm"
  21. :extra="cpuExtra"
  22. :max="form.fd.vcpu < 32 ? 32 : 128"
  23. :form="form"
  24. :hypervisor="hypervisor"
  25. :showCpuSocketsInit="form.fi.showCpuSockets"
  26. :cpuSocketsInit="selectedItem.vcpu_count / selectedItem.cpu_sockets"
  27. :serverStatus="selectedItem.status"
  28. @change="cpuChange" />
  29. </a-form-item>
  30. <a-form-item :label="$t('compute.text_369')" class="mb-0">
  31. <mem-radio :decorator="decorators.vmem" :options="form.fi.cpuMem.mems_mb || []" :disable-options="disableMems" :disabled="runningArm" :extra="cpuExtra" />
  32. </a-form-item>
  33. <a-form-item :label="$t('compute.text_1041')" v-if="isOpenWorkflow">
  34. <a-input v-decorator="decorators.reason" :placeholder="$t('compute.text_1105')" />
  35. </a-form-item>
  36. <a-form-item :label="$t('compute.text_494')" :extra="$t('compute.text_1106')">
  37. <a-switch :checkedChildren="$t('compute.text_115')" :unCheckedChildren="$t('compute.text_116')" v-decorator="decorators.autoStart" :disabled="isSomeRunning" />
  38. </a-form-item>
  39. </a-form>
  40. </div>
  41. </page-body>
  42. <page-footer>
  43. <div slot="right">
  44. <div class="d-flex align-items-center">
  45. <div v-if="hasMeterService" class="mr-4 d-flex align-items-center">
  46. <div class="text-truncate">{{$t('compute.text_286')}}</div>
  47. <div class="ml-2 prices">
  48. <div class="hour error-color d-flex">
  49. <template v-if="price">
  50. <m-animated-number :value="price" :formatValue="priceFormat" />
  51. <discount-price class="ml-2 mini-text" :discount="discount" :origin="originPrice" />
  52. </template>
  53. <template v-else>---</template>
  54. </div>
  55. <div class="tips text-truncate">
  56. <span v-html="priceTips" />
  57. </div>
  58. </div>
  59. </div>
  60. <a-button class="mr-3" type="primary" @click="handleConfirm" :loading="loading">{{confirmText}}</a-button>
  61. <a-button @click="cancel">{{$t('compute.text_908')}}</a-button>
  62. </div>
  63. </div>
  64. </page-footer>
  65. </div>
  66. </template>
  67. <script>
  68. import { mapGetters } from 'vuex'
  69. import * as R from 'ramda'
  70. import _ from 'lodash'
  71. import CpuRadio from '@Compute/sections/CpuRadio'
  72. import MemRadio from '@Compute/sections/MemRadio'
  73. import { SERVER_TYPE } from '@Compute/constants'
  74. import { Manager } from '@/utils/manager'
  75. import WindowsMixin from '@/mixins/windows'
  76. import WorkflowMixin from '@/mixins/workflow'
  77. import {
  78. getNameDescriptionTableColumn,
  79. getIpsTableColumn,
  80. getProjectTableColumn,
  81. getStatusTableColumn,
  82. getRegionTableColumn,
  83. } from '@/utils/common/tableColumn'
  84. import { findPlatform } from '@/utils/common/hypervisor'
  85. import { sizestr } from '@/utils/utils'
  86. import DiscountPrice from '@/sections/DiscountPrice'
  87. export default {
  88. name: 'AdjustConfig',
  89. components: {
  90. CpuRadio,
  91. MemRadio,
  92. DiscountPrice,
  93. },
  94. mixins: [WindowsMixin, WorkflowMixin],
  95. props: {
  96. params: {
  97. type: Object,
  98. },
  99. },
  100. data () {
  101. const itemData = this.params.data[0]
  102. const autoStart = this.params.data.some(val => val.status === 'running')
  103. return {
  104. loading: false,
  105. action: this.$t('compute.text_1100'),
  106. form: {
  107. fc: this.$form.createForm(this, {
  108. onValuesChange: this.onValuesChange,
  109. }),
  110. fd: {
  111. vcpu: 2,
  112. vmem: 2048,
  113. hypervisor: itemData.hypervisor,
  114. },
  115. fi: {
  116. cpuMem: {}, // cpu 和 内存 的关联关系
  117. capability: {},
  118. imageMsg: {}, // 当前选中的 image
  119. showCpuSockets: false,
  120. cpuSockets: 1,
  121. },
  122. },
  123. beforeDataDisks: [],
  124. decorators: {
  125. vcpu: [
  126. 'vcpu',
  127. {
  128. initialValue: itemData.vcpu_count,
  129. },
  130. ],
  131. vmem: [
  132. 'vmem',
  133. {
  134. initialValue: itemData.vmem_size,
  135. },
  136. ],
  137. reason: [
  138. 'reason',
  139. {
  140. initialValue: '',
  141. },
  142. ],
  143. autoStart: [
  144. 'autoStart',
  145. {
  146. valuePropName: 'checked',
  147. initialValue: autoStart,
  148. },
  149. ],
  150. },
  151. formItemLayout: {
  152. wrapperCol: {
  153. span: 20,
  154. xxl: {
  155. span: 22,
  156. },
  157. },
  158. labelCol: {
  159. span: 4,
  160. xxl: {
  161. span: 2,
  162. },
  163. },
  164. },
  165. diskLoaded: false,
  166. domain: itemData.domain_id,
  167. cloudaccountId: itemData.account_id,
  168. origin_price: null,
  169. price: null,
  170. priceFormat: null,
  171. currency: '',
  172. priceTips: '--',
  173. discount: 0,
  174. }
  175. },
  176. computed: {
  177. ...mapGetters(['isAdminMode', 'scope', 'userInfo']),
  178. scopeParams () {
  179. if (this.$store.getters.isAdminMode) {
  180. return {
  181. project_domain: this.params.data[0].domain_id,
  182. }
  183. }
  184. return { scope: this.$store.getters.scope }
  185. },
  186. selectedItems () {
  187. return this.params.data
  188. },
  189. selectedItem () {
  190. return this.params.data[0]
  191. },
  192. count () {
  193. return this.selectedItems.length || 1
  194. },
  195. isSomeRunning () {
  196. return this.params.data.some(val => val.status === 'running')
  197. },
  198. isSomeArm () {
  199. return this.selectedItem.os_arch === 'arm'
  200. },
  201. runningArm () {
  202. return this.isSomeArm && this.isSomeRunning
  203. },
  204. hypervisor () {
  205. return this.selectedItem.hypervisor
  206. },
  207. type () {
  208. const brand = this.selectedItem.brand
  209. return findPlatform(brand)
  210. },
  211. disableCpus () {
  212. const cpu = this.selectedItem.vcpu_count
  213. const cpus = this.form.fi.cpuMem.cpus || []
  214. if (this.isSomeRunning && cpus.length > 0) {
  215. return cpus.filter((item) => { return item < cpu })
  216. }
  217. return []
  218. },
  219. disableMems () {
  220. const vmem = this.selectedItem.vmem_size
  221. const mems = this.form.fi.cpuMem.mems_mb || []
  222. if (this.isSomeRunning && mems.length > 0) {
  223. return mems.filter((item) => { return item < vmem })
  224. }
  225. return []
  226. },
  227. hasMeterService () { // 是否有计费的服务
  228. const { services } = this.$store.getters.userInfo
  229. const meterService = services.find(val => val.type === 'meter')
  230. if (meterService && meterService.status === true) {
  231. return true
  232. }
  233. return false
  234. },
  235. // 是否为包年包月
  236. isPackage () {
  237. return this.selectedItem.billing_type === 'prepaid'
  238. },
  239. durationNum () {
  240. if (this.isPackage) {
  241. const { duration } = this.form.fd
  242. let num = parseInt(duration)
  243. if (num && duration.endsWith('Y')) {
  244. num *= 12 // 1年=12月
  245. } else if (num && duration.endsWith('W')) {
  246. num *= 0.25 // 1周=0.25月
  247. }
  248. return num
  249. }
  250. return 0
  251. },
  252. confirmText () {
  253. return this.isOpenWorkflow ? this.$t('compute.text_288') : this.$t('compute.text_907')
  254. },
  255. cpuExtra () {
  256. if (this.runningArm) {
  257. return this.$t('compute.text_1366')
  258. }
  259. return null
  260. },
  261. memExtra () {
  262. if (this.runningArm) {
  263. return this.$t('compute.text_1367')
  264. }
  265. return null
  266. },
  267. isPublic () {
  268. return this.params.data[0].cloud_env === SERVER_TYPE.public
  269. },
  270. originPrice () {
  271. if (this.origin_price) {
  272. this.$emit('getOriginPrice', this.origin_price)
  273. }
  274. return this.origin_price
  275. },
  276. columns () {
  277. return [
  278. getNameDescriptionTableColumn({
  279. hideField: true,
  280. addLock: true,
  281. addBackup: true,
  282. edit: false,
  283. editDesc: false,
  284. slotCallback: row => {
  285. return (
  286. <side-page-trigger>{row.name}</side-page-trigger>
  287. )
  288. },
  289. }),
  290. getIpsTableColumn({ field: 'ip', title: 'IP' }),
  291. {
  292. field: 'instance_type',
  293. title: this.$t('compute.text_295'),
  294. showOverflow: 'ellipsis',
  295. minWidth: 120,
  296. sortable: true,
  297. slots: {
  298. default: ({ row }) => {
  299. const ret = []
  300. if (row.instance_type) {
  301. ret.push(<div class='text-truncate' style={{ color: '#0A1F44' }}>{row.instance_type}</div>)
  302. }
  303. const config = row.vcpu_count + 'C' + sizestr(row.vmem_size, 'M', 1024) + (row.disk ? sizestr(row.disk, 'M', 1024) : '')
  304. return ret.concat(<div class='text-truncate' style={{ color: '#53627C' }}>{config}</div>)
  305. },
  306. },
  307. },
  308. getStatusTableColumn({ statusModule: 'server' }),
  309. getProjectTableColumn(),
  310. getRegionTableColumn(),
  311. ]
  312. },
  313. },
  314. watch: {
  315. priceTips: {
  316. handler (val) {
  317. let ret = `${this.currency} ${this.price && this.price.toFixed(2)}`
  318. ret += !this.isPackage ? this.$t('compute.text_296') : ''
  319. this.$bus.$emit('VMGetPrice', `${ret} ${val}`)
  320. },
  321. immediate: true,
  322. },
  323. dataDiskType (val, oldV) {
  324. if (val !== oldV) {
  325. this.getPriceList()
  326. }
  327. },
  328. },
  329. created () {
  330. this.serversManager = new Manager('servers', 'v2')
  331. this.zonesM2 = new Manager('zones', 'v2')
  332. this.serverskusM = new Manager('serverskus')
  333. this.loadData(this.params.data)
  334. this.fetchInstanceSpecs()
  335. },
  336. beforeDestroy () {
  337. this.serversManager = null
  338. this.zonesM2 = null
  339. this.serverskusM = null
  340. },
  341. methods: {
  342. async loadData (data) {
  343. this.data = data
  344. if (this.data.length > 0) {
  345. try {
  346. const { data } = await this.capability(this.data[0].zone_id)
  347. this.form.fi.capability = data
  348. } catch (error) { }
  349. }
  350. const conf = this.maxConfig()
  351. this.form.fd.vcpu_count = conf[0]
  352. this.form.fd.vmem = Math.ceil(conf[1]) * 1024
  353. this.$nextTick(() => {
  354. this.form.fc.setFieldsValue({ vcpu: this.form.fd.vcpu_count, vmem: this.form.fd.vmem })
  355. })
  356. },
  357. maxConfig () {
  358. let cpu = 0
  359. let mem = 0
  360. for (let i = 0; i < this.data.length; i++) {
  361. if (cpu < this.data[i].vcpu_count) {
  362. cpu = this.data[i].vcpu_count
  363. }
  364. if (mem < this.data[i].vmem_size) {
  365. mem = this.data[i].vmem_size
  366. }
  367. }
  368. return [cpu, mem / 1024]
  369. },
  370. async doChangeSettingsSubmit (values) {
  371. const params = {
  372. vmem_size: values.vmem,
  373. vcpu_count: values.vcpu,
  374. auto_start: values.autoStart,
  375. }
  376. const ids = this.params.data.map(item => item.id)
  377. return this.serversManager.batchPerformAction({
  378. ids,
  379. steadyStatus: ['running', 'ready'],
  380. action: 'change-config',
  381. data: params,
  382. })
  383. },
  384. async handleConfirm () {
  385. this.loading = true
  386. try {
  387. const values = await this.form.fc.validateFields()
  388. const res = await this.doChangeSettingsSubmit(values)
  389. const isOk = res.data.data.every(item => item.status === 200)
  390. if (isOk) {
  391. this.$message.success(this.$t('compute.text_423'))
  392. this.$store.commit('keepAlive/ADD_DELAY_EVENT', { name: 'ResourceListSingleRefresh', params: this.data.map(item => item.id) })
  393. this.cancel()
  394. }
  395. } catch (error) {
  396. throw error
  397. } finally {
  398. this.loading = false
  399. }
  400. },
  401. cpuChange (cpu) {
  402. if (cpu) {
  403. if (R.is(Object, this.form.fi.cpuMem)) {
  404. const memOpts = _.get(this.form.fi, `cpuMem.cpu_mems_mb[${cpu}]`) || []
  405. this.form.fi.cpuMem.mems_mb = memOpts
  406. this.form.fc.setFieldsValue({
  407. vmem: Math.max(this.selectedItem.vmem_size, memOpts[0]),
  408. })
  409. }
  410. }
  411. },
  412. fetchInstanceSpecs () {
  413. const params = {
  414. usable: true,
  415. zone: this.selectedItem.zone_id,
  416. provider: this.selectedItem.provider,
  417. }
  418. this.serverskusM.get({ id: 'instance-specs', params })
  419. .then(({ data }) => {
  420. this.form.fi.cpuMem = data
  421. const vcpuDecorator = this.decorators.vcpu
  422. const vcpuInit = vcpuDecorator[1].initialValue
  423. this.cpuChange(vcpuInit)
  424. })
  425. },
  426. onValuesChange (props, values) {
  427. Object.keys(values).forEach((key) => {
  428. this.$set(this.form.fd, key, values[key])
  429. })
  430. },
  431. async capability (v) { // 可用区查询
  432. const params = {
  433. show_emulated: true,
  434. }
  435. if (this.isAdminMode) {
  436. params.project_domain = this.selectedItem.domain_id
  437. }
  438. return this.zonesM2.get({ id: `${v}/capability`, params })
  439. },
  440. cancel () {
  441. this.$router.push({ name: 'VMContainerInstance' })
  442. },
  443. },
  444. }
  445. </script>
  446. <style lang="less" scoped>
  447. .form-wrapper {
  448. padding-left: 22px;
  449. }
  450. .prices {
  451. .hour {
  452. font-size: 24px;
  453. }
  454. .tips {
  455. color: #999;
  456. font-size: 12px;
  457. }
  458. }
  459. </style>