index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. <template>
  2. <div>
  3. <a-radio-group v-model="skuType" @change="skuTypeChange">
  4. <a-radio-button
  5. v-for="item of skuInfo.categoryOptions"
  6. :value="item.key"
  7. :key="item.key"
  8. :disabled="item.disabled">{{ item.label }}</a-radio-button>
  9. </a-radio-group>
  10. <vxe-grid
  11. row-id="id"
  12. ref="tableRef"
  13. min-height="260"
  14. resizable
  15. :columns="tableColumn"
  16. :data="skuResults"
  17. :radio-config="{ reserve: true }"
  18. @cell-click="skuChange"
  19. @radio-change="skuChange">
  20. <template v-slot:empty>
  21. <loader :loading="skuLoading || !canSkuShow" />
  22. </template>
  23. </vxe-grid>
  24. <div class="sku-pagebar">
  25. <vxe-pager
  26. :current-page.sync="skuPage.currentPage"
  27. :page-size.sync="skuPage.pageSize"
  28. :total="skuPage.totalResult"
  29. :layouts="['PrevJump', 'PrevPage', 'Jump', 'PageCount', 'NextPage', 'NextJump', 'Total']"
  30. @page-change="skuPageChangeHandle" />
  31. <div class="mt-1" v-if="selectedTip">{{$t('compute.text_171', [ selectedTip ])}}</div>
  32. </div>
  33. <div class="mt-1" v-if="unfindTip" style="color: red;">{{$t('compute.sku_unfind_tip', [ unfindTip ])}}</div>
  34. <div class="mt-1" v-if="disableSkuType && !supportSkuTypes.length" style="color: red;">{{$t('compute.disable_sku_type_tip')}}</div>
  35. </div>
  36. </template>
  37. <script>
  38. import * as R from 'ramda'
  39. import { ALL_SKU_CATEGORY_OPT, SKU_CATEGORY_MAP } from '@Compute/constants'
  40. import { Manager } from '@/utils/manager'
  41. import { PROVIDER_MAP, HYPERVISORS_MAP } from '@/constants'
  42. import { sizestr } from '@/utils/utils'
  43. import i18n from '@/locales'
  44. const keys = ['hour_price', 'month_price', 'year_price']
  45. const units = [i18n.t('compute.text_172'), i18n.t('compute.text_173'), i18n.t('compute.text_174')]
  46. export default {
  47. name: 'SKU',
  48. props: {
  49. billType: {
  50. type: String,
  51. },
  52. hypervisor: {
  53. validator: val => {
  54. if (val) return R.is(String, val)
  55. return true
  56. },
  57. },
  58. value: { // v-decorator 的props
  59. required: true,
  60. },
  61. priceUnit: {
  62. type: Object,
  63. validator: val => {
  64. if (!R.isNil(val.key) || !R.isNil(val.unit)) {
  65. return keys.includes(val.key) && units.includes(val.unit)
  66. }
  67. return false
  68. },
  69. default: () => ({
  70. key: 'hour_price',
  71. unit: i18n.t('compute.text_172'),
  72. }),
  73. },
  74. skuParams: {
  75. type: Object,
  76. default: () => ({}),
  77. },
  78. type: {
  79. type: String,
  80. required: true,
  81. validator: val => ['idc', 'private', 'public'].includes(val),
  82. },
  83. hasMeterService: {
  84. type: Boolean,
  85. default: true,
  86. },
  87. instanceType: {
  88. type: String,
  89. },
  90. requireSysDiskType: {
  91. type: Array,
  92. },
  93. requireDataDiskTypes: {
  94. type: Array,
  95. },
  96. skuFilter: {
  97. type: Function,
  98. default: (items) => { return items },
  99. },
  100. canSkuShow: {
  101. type: Boolean,
  102. default: () => true,
  103. },
  104. skuDisabled: {
  105. type: Boolean,
  106. default: () => false,
  107. },
  108. dataSku: {
  109. type: Object,
  110. default: () => ({}),
  111. },
  112. isAdjustConfig: {
  113. type: Boolean,
  114. default: () => false,
  115. },
  116. dataList: {
  117. type: Array,
  118. default: () => [],
  119. },
  120. initSkuData: {
  121. type: Object,
  122. default: () => ({}),
  123. },
  124. supportSkuTypes: {
  125. type: Array,
  126. default: () => [],
  127. },
  128. disableSkuType: {
  129. type: Boolean,
  130. default: () => false,
  131. },
  132. },
  133. data () {
  134. return {
  135. skuList: [], // 套餐列表
  136. ratesList: [], // 套餐价格列表
  137. rateLoading: false,
  138. skuLoading: false,
  139. selectedSkuData: {},
  140. skuType: ALL_SKU_CATEGORY_OPT.key,
  141. skuPage: {
  142. currentPage: 1,
  143. pageSize: 10,
  144. totalResult: 0,
  145. },
  146. skuTypes: [],
  147. hasOriginSku: false,
  148. unfindTip: '',
  149. }
  150. },
  151. computed: {
  152. isSameSku () {
  153. return this.dataList.length === 1 || this.dataList.map(item => item.instance_type).filter(item => item).length === 1
  154. },
  155. customConfig () {
  156. return {
  157. checkMethod: ({ row }) => {
  158. return true
  159. },
  160. }
  161. },
  162. isPublic () {
  163. return this.type === 'public'
  164. },
  165. isPrivate () {
  166. return this.type === 'private'
  167. },
  168. isIDC () {
  169. return this.type === 'idc'
  170. },
  171. tableColumn () {
  172. const column = [
  173. { field: 'instance_type_category_i18n', title: this.$t('compute.text_175') },
  174. { field: 'region', title: this.$t('compute.text_177') },
  175. { field: 'name', title: this.$t('compute.text_178') },
  176. { field: 'cpu_core_count', title: this.$t('compute.text_179') },
  177. { field: 'cpu_arch', title: this.$t('compute.cpu_arch'), slots: { default: ({ row }) => { return row.cpu_arch ? this.$t(`compute.cpu_arch.${row.cpu_arch}`) : '-' } } },
  178. { field: 'memory_size_mb_compute', title: this.$t('compute.text_180') },
  179. ]
  180. if (this.skuDisabled) {
  181. column.unshift({
  182. field: 'radio',
  183. width: 40,
  184. slots: {
  185. default: ({ row }) => {
  186. if (row.id === this.selectedSkuData?.id) {
  187. return [<vxe-radio disabled></vxe-radio>]
  188. }
  189. return [<vxe-radio disabled value={false}></vxe-radio>]
  190. },
  191. },
  192. })
  193. } else {
  194. column.unshift({
  195. field: 'radio',
  196. width: 40,
  197. slots: {
  198. default: ({ row }) => {
  199. if (row.id === this.selectedSkuData?.id) {
  200. return [<vxe-radio></vxe-radio>]
  201. }
  202. if (this.disableSkuType && this.supportSkuTypes.length && !this.supportSkuTypes.includes(row.name)) {
  203. return [<vxe-radio disabled value={false}></vxe-radio>]
  204. }
  205. return [<vxe-radio value={false}></vxe-radio>]
  206. },
  207. },
  208. })
  209. }
  210. const providerColumn = {
  211. field: 'provider',
  212. title: this.$t('compute.text_176'),
  213. slots: {
  214. default: ({ row }) => {
  215. return [
  216. this.getHypervisor(row),
  217. ]
  218. },
  219. },
  220. }
  221. if (this.isPublic) {
  222. column.splice(1, 0, providerColumn)
  223. if (this.hasMeterService) {
  224. column.push({
  225. field: 'hour_price',
  226. title: this.$t('compute.text_181'),
  227. slots: {
  228. default: ({ row }) => {
  229. const price = this.getFormatPrice(row.hour_price)
  230. if (price > 0) {
  231. let ret = [<a-icon type="loading" />]
  232. if (!this.rateLoading) {
  233. ret = [
  234. <span style="color: rgb(230, 139, 80);">{ price }</span>,
  235. <span> {this.$t(`currencys.${row.currency}`)} / { this.priceUnit.unit }</span>,
  236. ]
  237. }
  238. return ret
  239. }
  240. return [<span style="color: rgb(230, 139, 80);">--</span>]
  241. },
  242. },
  243. })
  244. }
  245. }
  246. return column
  247. },
  248. skuInfo () {
  249. let currentCategory = SKU_CATEGORY_MAP[this.hypervisor] || []
  250. if (this.isPublic) {
  251. currentCategory = SKU_CATEGORY_MAP.public_cloud
  252. }
  253. const categoryOptions = {
  254. [ALL_SKU_CATEGORY_OPT.key]: {
  255. ...ALL_SKU_CATEGORY_OPT,
  256. disabled: false,
  257. },
  258. }
  259. currentCategory.forEach(item => {
  260. let type = this.hypervisor
  261. if (this.isPublic) {
  262. type = 'public_cloud'
  263. }
  264. categoryOptions[item] = {
  265. label: this.getI18NValue(`skuCategoryOptions.${type}.${item}`, item),
  266. key: item,
  267. disabled: !this.skuTypes.includes(item) || this.skuDisabled,
  268. }
  269. })
  270. const skuOptions = {
  271. [ALL_SKU_CATEGORY_OPT.key]: [],
  272. }
  273. // 套餐去重
  274. const skuSet = new Set()
  275. for (let i = 0, len = this.skuList.length; i < len; i++) {
  276. const item = this.skuList[i]
  277. const flag = `${item.name}-${item.provider}-${item.region_ext_id}`
  278. if (skuSet.has(flag)) {
  279. continue
  280. }
  281. skuSet.add(flag)
  282. const key = item.local_category
  283. const category = item.instance_type_category
  284. if (!skuOptions[key]) {
  285. skuOptions[key] = []
  286. }
  287. let hypervisor = this.hypervisor
  288. if (this.isPublic) {
  289. hypervisor = item.provider.toLowerCase()
  290. }
  291. // 翻译类型
  292. item.instance_type_category_i18n = this.getI18NValue(`skuCategoryOptions['${hypervisor}']['${category}']`, category)
  293. item.memory_size_mb_compute = item.memory_size_mb / 1024
  294. if (this.isPublic) {
  295. item.rate_key = this.genRateKey(item)
  296. if (this.ratesMap[item.rate_key]) {
  297. item.hour_price = this.ratesMap[item.rate_key][this.priceUnit.key]
  298. item.currency = this.ratesMap[item.rate_key].currency
  299. }
  300. }
  301. if (this.isSkuEnabled(item)) {
  302. skuOptions[key].push(item)
  303. skuOptions[ALL_SKU_CATEGORY_OPT.key].push(item)
  304. }
  305. }
  306. return {
  307. categoryOptions,
  308. skuOptions,
  309. }
  310. },
  311. ratesMap () {
  312. const ret = {}
  313. for (let i = 0, len = this.ratesList.length; i < len; i++) {
  314. const item = this.ratesList[i]
  315. ret[item.data_key] = item
  316. }
  317. return ret
  318. },
  319. skuResults () {
  320. if (!this.canSkuShow) return []
  321. const ret = this.skuInfo.skuOptions[this.skuType]
  322. if (ret && ret.length > 0 && ret[0].hour_price) {
  323. ret.sort((a, b) => a.hour_price - b.hour_price)
  324. }
  325. return ret
  326. },
  327. selectedTip () {
  328. if (this.selectedSkuData?.id) {
  329. return this.$t('compute.text_182', [
  330. this.selectedSkuData.name,
  331. this.selectedSkuData.instance_type_category_i18n,
  332. this.selectedSkuData.cpu_core_count,
  333. sizestr(this.selectedSkuData.memory_size_mb, 'M', 1024),
  334. ])
  335. }
  336. return null
  337. },
  338. },
  339. watch: {
  340. skuParams: {
  341. handler (val, oldV) {
  342. if (!R.isEmpty(val)) {
  343. if (!R.equals(val, oldV)) {
  344. this.resetPageInfo()
  345. this.fetchData()
  346. }
  347. } else {
  348. this.skuList = []
  349. this.setSku({})
  350. }
  351. },
  352. },
  353. skuResults: {
  354. handler (val, oldV) {
  355. if (!R.equals(val, oldV)) {
  356. if (val.length) {
  357. this.setSku(val[0], false)
  358. } else {
  359. this.setSku({})
  360. }
  361. }
  362. },
  363. deep: true,
  364. },
  365. },
  366. created () {
  367. this.skusM = new Manager('serverskus')
  368. this.ratesM = new Manager('cloud_sku_rates', 'v1')
  369. if (this.skuParams && !R.isEmpty(this.skuParams)) {
  370. this.fetchData()
  371. }
  372. },
  373. methods: {
  374. fetchData () {
  375. this.fetchSkuTypes()
  376. this.fetchSkuList().then(this.fetchCloudSkuRatesList)
  377. },
  378. getFormatPrice (price) {
  379. if (price) {
  380. return price.toFixed(2)
  381. }
  382. return '0'
  383. },
  384. skuChange ({ row } = {}) {
  385. if (this.skuDisabled) return
  386. if (!row) return
  387. // 与 tableColumn 中单选列一致:非当前选中且不在 supportSkuTypes 内时为 disabled,不触发切换
  388. if (
  389. this.disableSkuType &&
  390. this.supportSkuTypes.length &&
  391. !this.supportSkuTypes.includes(row.name) &&
  392. row.id !== this.selectedSkuData?.id
  393. ) {
  394. return
  395. }
  396. this.setSku(row, true)
  397. },
  398. skuTypeChange () {
  399. this.fetchData()
  400. if (this.skuResults && this.skuResults.length) {
  401. this.setSku(this.skuResults[0], true)
  402. }
  403. },
  404. setSku (skuData, isSkuChange) {
  405. if (!skuData) return
  406. let chooseSku = skuData
  407. if (!isSkuChange && this.instanceType) {
  408. const extSku = this.skuList.find(item => item.name === this.instanceType)
  409. if (extSku) {
  410. chooseSku = extSku
  411. } else {
  412. if (this.isAdjustConfig && this.isSameSku && !this.hasOriginSku) {
  413. chooseSku = this.dataSku
  414. this.unfindTip = this.dataSku?.name
  415. }
  416. }
  417. }
  418. if (!isSkuChange && this.initSkuData?.name && this.skuList.some(item => item.name && item.name === this.initSkuData?.name)) {
  419. const sku = this.skuList.find(item => item.name && item.name === this.initSkuData?.name)
  420. if (sku) {
  421. chooseSku = sku
  422. }
  423. }
  424. this.$nextTick(() => {
  425. this.selectedSkuData = chooseSku
  426. this.$refs.tableRef && this.$refs.tableRef.setRadioRow(chooseSku)
  427. this.$emit('change', chooseSku)
  428. })
  429. },
  430. getHypervisor (data) {
  431. return data.provider ? PROVIDER_MAP[data.provider].label : PROVIDER_MAP.OneCloud.label
  432. },
  433. genRateKey (data) {
  434. const provider = this.getSkuItemProvider(data)
  435. let ret = `${provider}::${data.region_ext_id}::${data.name}`
  436. if (provider === HYPERVISORS_MAP.ucloud.key || provider === HYPERVISORS_MAP.azure.key) {
  437. ret = `${provider}::${data.region_ext_id}::::instance::${data.name}`
  438. }
  439. return ret
  440. },
  441. getI18NValue (key, originVal) {
  442. if (this.$te(key)) {
  443. return this.$t(key)
  444. }
  445. return originVal
  446. },
  447. async fetchSkuList () {
  448. try {
  449. this.skuLoading = true
  450. this.skuList = []
  451. if (this.skuParams.cpu_core_count === 0) {
  452. delete this.skuParams.cpu_core_count
  453. }
  454. if (this.skuParams.memory_size_mb === 0) {
  455. delete this.skuParams.memory_size_mb
  456. }
  457. const params = {
  458. ...this.skuParams,
  459. limit: this.skuPage.pageSize,
  460. offset: (this.skuPage.currentPage - 1) * this.skuPage.pageSize,
  461. '@local_category': this.skuType,
  462. // prepaid_status: 'available',
  463. }
  464. if (this.skuType === 'all') {
  465. delete params['@local_category']
  466. }
  467. if (!this.skuParams.zone_id) {
  468. params.distinct = true
  469. }
  470. params.enabled = true
  471. let { data } = await this.skusM.list({ params: params })
  472. this.skuPage.pageSize = data.limit || 10
  473. this.skuPage.totalResult = data.total || 0
  474. if (typeof this.skuFilter === 'function') {
  475. data = this.skuFilter(data.data)
  476. }
  477. if (this.skuParams && !R.isEmpty(this.skuParams)) { // 防止网络延迟导致 skuParams 已经为空了,但却赋值了
  478. this.skuList = data
  479. if (this.skuList && this.skuList.length) {
  480. if (this.isAdjustConfig && this.isSameSku && !this.hasOriginSku) {
  481. if (this.skuList.some(item => item.name && item.name === this.dataSku?.name)) {
  482. this.hasOriginSku = true
  483. }
  484. }
  485. this.setSku(this.skuResults[0], false)
  486. }
  487. }
  488. this.skuLoading = false
  489. return data
  490. } catch (error) {
  491. this.skuLoading = false
  492. throw error
  493. }
  494. },
  495. fetchCloudSkuRatesList () { // 公有云套餐价格
  496. if (!this.hasMeterService) return // 没有 meter 服务
  497. if (!this.isPublic) return
  498. if (!this.skuList || !this.skuList.length) return
  499. let paramKeys = this.skuList.map(item => {
  500. const provider = this.getSkuItemProvider(item)
  501. let ret = `${provider}::${item.region_ext_id || 'NA'}::${item.name || 'NA'}`
  502. if (provider === HYPERVISORS_MAP.ucloud.key || provider === HYPERVISORS_MAP.azure.key) {
  503. ret = `${provider}::${item.region_ext_id}::::instance::${item.name}`
  504. }
  505. return ret
  506. })
  507. paramKeys = Array.from(new Set(paramKeys))
  508. const params = {
  509. param_keys: paramKeys.join('$'),
  510. }
  511. this.rateLoading = true
  512. this.ratesM.list({ params })
  513. .then(({ data: { data = [] } }) => {
  514. this.ratesList = data
  515. this.rateLoading = false
  516. })
  517. .catch(() => {
  518. this.rateLoading = false
  519. })
  520. },
  521. getSkuItemProvider (item) { // 兼容阿里金融云
  522. let provider = item.provider.toLowerCase()
  523. if (this.isPublic && item.cloud_env) {
  524. provider = item.cloud_env.toLowerCase()
  525. }
  526. return provider
  527. },
  528. isSupportDiskTypes (supported, required) {
  529. for (let i = 0; i < required.length; i++) {
  530. if (!supported.includes(required[i])) {
  531. return false
  532. }
  533. }
  534. return true
  535. },
  536. isSkuEnabled (item) {
  537. if (this.requireSysDiskType && item.sys_disk_type && !this.isSupportDiskTypes(item.sys_disk_type.split(','), this.requireSysDiskType)) {
  538. return false
  539. }
  540. if (this.requireDataDiskTypes && item.data_disk_types && !this.isSupportDiskTypes(item.data_disk_types.split(','), this.requireDataDiskTypes)) {
  541. return false
  542. }
  543. return true
  544. },
  545. skuPageChangeHandle ({ currentPage = 1, pageSize = 10 }) {
  546. this.skuPage = {
  547. ...this.skuPage,
  548. currentPage,
  549. pageSize,
  550. }
  551. this.fetchData()
  552. },
  553. async fetchSkuTypes () {
  554. try {
  555. const params = {
  556. ...this.skuParams,
  557. field: 'local_category',
  558. // postpaid_status: 'available',
  559. }
  560. delete params.limit
  561. delete params.offset
  562. delete params['@local_category']
  563. if (this.skuParams.cpu_core_count === 0) {
  564. delete params.cpu_core_count
  565. }
  566. if (this.skuParams.memory_size_mb === 0) {
  567. delete params.memory_size_mb
  568. }
  569. const { data } = await this.skusM.get({ id: 'distinct-field', params })
  570. this.skuTypes = data[params.field]
  571. } catch (error) {
  572. console.log(error)
  573. }
  574. },
  575. resetPageInfo () {
  576. this.skuPage = {
  577. currentPage: 1,
  578. pageSize: 10,
  579. totalResult: 0,
  580. }
  581. },
  582. },
  583. }
  584. </script>
  585. <style lang="scss" scoped>
  586. .sku-pagebar {
  587. display: flex;
  588. flex-direction: row-reverse;
  589. justify-content: space-between;
  590. align-items: center;
  591. }
  592. </style>