BottomBar.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <template>
  2. <div class="create-server-result-wrap">
  3. <page-footer>
  4. <template v-slot:left>
  5. <div
  6. v-for="(tip, idx) of tips"
  7. :key="idx"
  8. class="d-flex flex-column justify-content-center flex-grow-1 content">
  9. <div
  10. v-for="obj of tip"
  11. :key="obj.label"
  12. class="d-flex align-items-center">
  13. <span class="label" :class="obj.labelClass">{{ obj.label }}:</span>
  14. <template v-if="obj.value">
  15. <span class="value config text-truncate" :class="obj.valueClass">{{ obj.value }}</span>
  16. </template>
  17. <template v-else>
  18. <span class="value placeholder text-truncate" :class="obj.valueClass">------</span>
  19. </template>
  20. </div>
  21. </div>
  22. </template>
  23. <template v-slot:right>
  24. <div class="d-flex align-items-center">
  25. <div v-if="hasMeterService" class="mr-4 d-flex align-items-center">
  26. <div class="text-truncate">{{$t('compute.text_286')}}</div>
  27. <div class="ml-2 prices">
  28. <div class="hour d-flex">
  29. <template v-if="price">
  30. <m-animated-number :value="originPrice" :formatValue="priceFormat" />
  31. </template>
  32. <template v-else>---</template>
  33. </div>
  34. <div class="tips text-truncate">
  35. <span v-html="priceTips" />
  36. </div>
  37. </div>
  38. </div>
  39. <a-button
  40. :title="confirmText"
  41. class="text-truncate"
  42. type="primary"
  43. native-type="submit"
  44. html-type="submit"
  45. style="width: 120px;"
  46. :loading="loading"
  47. :disabled="disabled || !!errors.length">{{ confirmText }}</a-button>
  48. </div>
  49. <side-errors :error-title="$t('compute.text_290')" :errors="errors" @update:errors="changeErrors" />
  50. </template>
  51. </page-footer>
  52. </div>
  53. </template>
  54. <script>
  55. import * as R from 'ramda'
  56. import _ from 'lodash'
  57. import { SERVER_TYPE, BILL_TYPES_MAP, EIP_TYPES_MAP } from '@Compute/constants'
  58. import { sizestr } from '@/utils/utils'
  59. import { PriceFetcher } from '@/utils/common/price'
  60. import SideErrors from '@/sections/SideErrors'
  61. import { diskSupportTypeMedium, getOriginDiskKey } from '@/utils/common/hypervisor'
  62. export default {
  63. name: 'BottomBar',
  64. components: {
  65. SideErrors,
  66. },
  67. props: {
  68. loading: {
  69. type: Boolean,
  70. default: false,
  71. },
  72. form: {
  73. type: Object,
  74. required: true,
  75. },
  76. errors: {
  77. type: Object,
  78. required: true,
  79. },
  80. type: {
  81. type: String,
  82. required: true,
  83. },
  84. resourceType: { // 资源池类型
  85. type: String,
  86. },
  87. isOpenWorkflow: {
  88. type: Boolean,
  89. default: false,
  90. },
  91. isServertemplate: {
  92. type: Boolean,
  93. default: false,
  94. },
  95. hasMeterService: {
  96. type: Boolean,
  97. default: true,
  98. },
  99. dataDiskSizes: {
  100. type: Array,
  101. default: () => [],
  102. },
  103. },
  104. data () {
  105. this.getPriceList = _.debounce(this._getPriceList2, 500)
  106. return {
  107. origin_price: null,
  108. price: null,
  109. priceFormat: null,
  110. currency: '',
  111. priceTips: '--',
  112. disabled: false,
  113. }
  114. },
  115. computed: {
  116. fd () {
  117. return this.form.fd
  118. },
  119. fi () {
  120. return this.form.fi
  121. },
  122. isPublic () {
  123. return this.type === SERVER_TYPE.public
  124. },
  125. isIDC () {
  126. return this.type === SERVER_TYPE.idc
  127. },
  128. // 是否为包年包月
  129. isPackage () {
  130. return this.fd.billType === BILL_TYPES_MAP.package.key
  131. },
  132. name () {
  133. return this.fd.name
  134. },
  135. zone () {
  136. let ret = this.fd.zone ? this.fd.zone.label : ''
  137. if (this.isPublic) {
  138. ret = this.fd.sku ? this.fd.sku.zone : ''
  139. }
  140. return ret
  141. },
  142. vmType () {
  143. let ret = this.$t('compute.text_291', [this.$t('dictionary.server')])
  144. if (this.fd.gpuEnable) {
  145. ret = `GPU${this.$t('dictionary.server')}`
  146. }
  147. return ret
  148. },
  149. dataDisk () {
  150. const diskValueArr = []
  151. R.forEachObjIndexed(value => {
  152. diskValueArr.push(value)
  153. }, this.fd.dataDiskSizes)
  154. return diskValueArr.reduce((prevDisk, diskValue) => prevDisk + diskValue, 0)
  155. },
  156. disk () {
  157. const diskValueArr = [this.fd.systemDiskSize]
  158. R.forEachObjIndexed(value => {
  159. diskValueArr.push(value)
  160. }, this.fd.dataDiskSizes)
  161. return diskValueArr.reduce((prevDisk, diskValue) => prevDisk + diskValue, 0)
  162. },
  163. config () {
  164. if (this.fd?.sku?.id) {
  165. return this.$t('compute.text_182', [
  166. this.fd.sku.name,
  167. this.fd.sku.instance_type_category_i18n,
  168. this.fd.sku.cpu_core_count,
  169. sizestr(this.fd.sku.memory_size_mb, 'M', 1024),
  170. ])
  171. }
  172. return null
  173. },
  174. image () {
  175. return _.get(this.fd, 'image.label') || ''
  176. },
  177. tips () {
  178. const ret = [
  179. [
  180. { label: this.$t('compute.text_175'), labelClass: 'label-w-120', value: this.vmType },
  181. { label: this.$t('compute.text_295'), labelClass: 'label-w-120', value: this.config },
  182. ],
  183. [
  184. { label: this.$t('compute.text_498'), labelClass: 'label-w-120', value: this.isPackage ? this.$t('billingType.prepaid') : this.$t('billingType.postpaid') },
  185. { label: this.$t('cloudenv.buy_num'), labelClass: 'label-w-120', value: `${this.fd?.count}${this.$t('common_62')}` },
  186. ],
  187. [
  188. { label: this.$t('compute.text_177'), labelClass: 'label-w-120', value: this.fd.sku?.region },
  189. { label: this.$t('compute.text_49'), labelClass: 'label-w-120', value: this.systemDisk },
  190. ],
  191. ]
  192. return ret
  193. },
  194. durationNum () {
  195. if (this.isPackage) {
  196. const { duration } = this.fd
  197. let num = parseInt(duration)
  198. if (num && duration.endsWith('Y')) {
  199. num *= 12 // 1年=12月
  200. } else if (num && duration.endsWith('W')) {
  201. num *= 0.25 // 1周=0.25月
  202. }
  203. return num
  204. }
  205. return 0
  206. },
  207. confirmText () {
  208. return this.$t('cloudenv.add_to_list')
  209. },
  210. dataDiskObj () {
  211. if (R.is(Object, this.fd.dataDiskTypes)) {
  212. const keys = Object.keys(this.fd.dataDiskTypes)
  213. if (keys && keys.length) {
  214. return this.fd.dataDiskTypes[keys[0]]
  215. }
  216. }
  217. if (R.is(Object, this.fd.dataDiskSizes)) {
  218. const keys = Object.keys(this.fd.dataDiskSizes)
  219. if (keys && keys.length) {
  220. const disk = this.fd[`dataDiskTypes[${keys[0]}]`]
  221. return disk
  222. }
  223. }
  224. return null
  225. },
  226. dataDiskType () {
  227. if (this.dataDiskObj && this.dataDiskObj.key) return this.dataDiskObj.key
  228. return ''
  229. },
  230. dataDiskLabel () {
  231. if (this.dataDiskObj && this.dataDiskObj.label) return this.dataDiskObj.label
  232. return ''
  233. },
  234. originPrice () {
  235. if (this.origin_price) {
  236. this.$emit('getOriginPrice', this.origin_price)
  237. }
  238. return this.origin_price
  239. },
  240. systemDisk () {
  241. if (this.fd?.systemDiskSize) {
  242. return `${this.fd.systemDiskSize}GB ${_.get(this.fd, 'systemDiskType.label')}`
  243. }
  244. return '--'
  245. },
  246. },
  247. watch: {
  248. priceTips: {
  249. handler (val) {
  250. let ret = `${this.currency} ${this.originPrice && this.originPrice.toFixed(2)}`
  251. ret += !this.isPackage ? this.$t('compute.text_296') : ''
  252. this.$bus.$emit('VMGetPrice', `${ret} ${val}`)
  253. },
  254. immediate: true,
  255. },
  256. dataDiskType (val, oldV) {
  257. if (val !== oldV) {
  258. this.getPriceList()
  259. }
  260. },
  261. 'fd.eip_type' (val, oldV) {
  262. this.getPriceList()
  263. },
  264. 'fd.eip_bw' (val, oldV) {
  265. this.getPriceList()
  266. },
  267. 'fd.backupEnable' (val, oldV) {
  268. this.getPriceList()
  269. },
  270. 'fd.eip_bgp_type' (val, oldV) {
  271. this.getPriceList()
  272. },
  273. 'fd.gpuEnable' (val, oldV) {
  274. this.getPriceList()
  275. },
  276. },
  277. created () {
  278. this.baywatch([
  279. 'fd.sku.id',
  280. 'fd.gcounts',
  281. 'fd.duration',
  282. 'fd.billType',
  283. 'fd.systemDiskSize',
  284. 'fd.systemDiskType.key',
  285. 'fd.count',
  286. 'dataDiskSizes',
  287. 'fd.gpu',
  288. 'fd.gpuCount',
  289. ], (val) => {
  290. if (val) {
  291. this.getPriceList()
  292. }
  293. })
  294. this.$bus.$on('VMCreateDisabled', (val) => {
  295. this.disabled = val
  296. })
  297. },
  298. methods: {
  299. changeErrors (errors) {
  300. this.$emit('update:errors', {})
  301. },
  302. baywatch (props, watcher) {
  303. const iterator = function (prop) {
  304. this.$watch(prop, watcher)
  305. }
  306. props.forEach(iterator, this)
  307. },
  308. async _getPriceList2 () {
  309. const f = this.fd
  310. if (!this.hasMeterService) return // 如果没有 meter 服务则取消调用
  311. if (R.isEmpty(f.sku) || R.isNil(f.sku)) return
  312. if (this.fi.createType === SERVER_TYPE.public && (R.isNil(f.sku.region_ext_id) || R.isEmpty(f.sku.region_ext_id))) return
  313. if (!R.is(Number, f.count)) return
  314. if (R.isNil(f.systemDiskSize)) return
  315. const pf = new PriceFetcher()
  316. pf.initialForm(this.$store.getters.scope, f.sku, f.duration, f.billType, this.isPublic)
  317. // add price items
  318. if (this.fi.createType !== SERVER_TYPE.public) {
  319. // server instance
  320. pf.addCpu(f.vcpu)
  321. pf.addMem(f.vmem / 1024)
  322. // gpu
  323. if (f.gpuEnable && f.gpu && f.gpu.indexOf('=') >= 0) {
  324. const tmps = f.gpu.split('=')[1].split(':')
  325. if (tmps.length >= 2) {
  326. pf.addGpu(`${tmps[0]}.${tmps[1]}`, f.gpuCount || 0)
  327. }
  328. }
  329. } else {
  330. // server instance
  331. pf.addServer(f.sku.name, 1)
  332. // others
  333. }
  334. // disks
  335. const { systemDiskSize, systemDiskType, hypervisor } = f
  336. const { systemDiskMedium, dataDiskMedium } = this.form.fi
  337. let systemDisk = systemDiskType.key
  338. // 磁盘区分介质
  339. if (diskSupportTypeMedium(hypervisor)) {
  340. systemDisk = getOriginDiskKey(systemDisk)
  341. }
  342. if (this.fi.createType !== SERVER_TYPE.public) systemDisk = `${systemDiskMedium}::${systemDisk}`
  343. pf.addDisk(systemDisk, systemDiskSize, 'sys')
  344. if (this.dataDiskType) {
  345. const datadisks = this.dataDiskSizes || (this.dataDisk ? [this.dataDisk] : [])
  346. let dataDisk = this.dataDiskType
  347. // 磁盘区分介质
  348. if (diskSupportTypeMedium(hypervisor)) {
  349. dataDisk = getOriginDiskKey(dataDisk)
  350. }
  351. if (this.fi.createType !== SERVER_TYPE.public) dataDisk = `${dataDiskMedium}::${dataDisk}`
  352. pf.addDisks(dataDisk, datadisks)
  353. }
  354. // eip
  355. if (f.eip_bw && f.eip_type === EIP_TYPES_MAP.new.key) {
  356. pf.addEipBandwidth(f.eip_bgp_type || '', f.eip_bw)
  357. }
  358. const price = await pf.getPriceObj()
  359. price.setOptions({ count: this.fd.count || 0, backupEnbale: this.fd.backupEnbale })
  360. this.currency = price.currency
  361. this.price = price.price
  362. this.priceFormat = price.priceFormat
  363. this.origin_price = price.originPrice
  364. this.priceTips = price.priceTips
  365. },
  366. },
  367. }
  368. </script>
  369. <style lang="less" scoped>
  370. @import "../../../../../../src/styles/less/theme";
  371. .create-server-result-wrap {
  372. position: relative;
  373. font-size: 12px;
  374. .content {
  375. width: 80%;
  376. .label {
  377. &.label-w-50 {
  378. width: 50px;
  379. }
  380. &.label-w-80 {
  381. width: 80px;
  382. }
  383. &.label-w-120 {
  384. width: 120px;
  385. }
  386. }
  387. .value {
  388. &.name-value {
  389. width: 100px;
  390. }
  391. &.placeholder {
  392. color: #888;
  393. font-style: italic;
  394. }
  395. }
  396. @media screen and (max-width: 1366px) {
  397. .value {
  398. max-width: 154px;
  399. }
  400. }
  401. }
  402. .prices {
  403. .hour {
  404. color: @error-color;
  405. font-size: 24px;
  406. }
  407. .tips {
  408. color: #999;
  409. font-size: 12px;
  410. }
  411. }
  412. .btns-wrapper {
  413. position: absolute;
  414. right: 20px;
  415. }
  416. }
  417. </style>