#5281 算力资源页面增加展示已有算力资源规格

Merged
ychao_1983 merged 42 commits from fix-4944 into V20240129 2 months ago
  1. +14
    -25
      entity/ai_task.go
  2. +7
    -4
      models/card_request.go
  3. +1
    -3
      models/resource_exclusive_pool.go
  4. +111
    -7
      models/resource_queue.go
  5. +266
    -0
      models/resource_specification.go
  6. +1
    -0
      modules/structs/card_requests.go
  7. +2
    -0
      routers/admin/resources.go
  8. +6
    -0
      routers/api/v1/api.go
  9. +65
    -0
      routers/card_request/card_request.go
  10. +35
    -0
      routers/resources/acc_card.go
  11. +1
    -1
      routers/routes/routes.go
  12. +5
    -2
      services/card_request/card_request.go
  13. +3
    -1
      services/cloudbrain/resource/resource_queue.go
  14. +13
    -1
      services/cloudbrain/resource/resource_specification.go
  15. +1
    -1
      web_src/less/_admin.less
  16. +61
    -0
      web_src/vuepages/apis/modules/computingpower.js
  17. +2
    -0
      web_src/vuepages/langs/config/en-US.js
  18. +2
    -0
      web_src/vuepages/langs/config/zh-CN.js
  19. +27
    -2
      web_src/vuepages/pages/computingpower/components/DemandForm.vue
  20. +560
    -0
      web_src/vuepages/pages/computingpower/components/Resources.vue
  21. +45
    -17
      web_src/vuepages/pages/computingpower/demand/index.vue
  22. +16
    -1
      web_src/vuepages/pages/resources/components/QueueDialog.vue
  23. +28
    -9
      web_src/vuepages/pages/resources/queue/index.vue

+ 14
- 25
entity/ai_task.go View File

@@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/models"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
)
@@ -148,7 +147,13 @@ type AITaskDetailInfo struct {
}

func (a *AITaskDetailInfo) Tr(language string) {
a.AICenter = getAiCenterShow(a.AICenter, language)
aiCenterInfo := strings.Split(a.AICenter, "+")
aiCenterCode := aiCenterInfo[0]
aiCenterName := ""
if len(aiCenterCode) >= 2 {
aiCenterName = aiCenterInfo[1]
}
a.AICenter = models.GetAiCenterShow(aiCenterCode, aiCenterName, language)
}

func (a *AITaskDetailInfo) TryToRemoveDatasets(currentUser *models.User) {
@@ -167,28 +172,6 @@ func (a *AITaskDetailInfo) TryToRemoveSDKCode(currentUser *models.User) {
}
}

func getAiCenterShow(aiCenter string, language string) string {
aiCenterInfo := strings.Split(aiCenter, "+")
if len(aiCenterInfo) == 2 {
if setting.AiCenterCodeAndNameAndLocMapInfo != nil {
if info, ok := setting.AiCenterCodeAndNameAndLocMapInfo[aiCenterInfo[0]]; ok {
if language == defaultLanguage {
return info.Content
} else {
return info.ContentEN
}
} else {
return aiCenterInfo[1]
}
} else {
return aiCenterInfo[1]
}
}
return ""
}

var defaultLanguage = "zh-CN"

type CreateTaskRes struct {
ID int64 `json:"id"`
Status string `json:"status"`
@@ -231,7 +214,13 @@ type AITaskBriefInfo struct {
}

func (a *AITaskBriefInfo) Tr(language string) {
a.AICenter = getAiCenterShow(a.AICenter, language)
aiCenterInfo := strings.Split(a.AICenter, "+")
aiCenterCode := aiCenterInfo[0]
aiCenterName := ""
if len(aiCenterCode) >= 2 {
aiCenterName = aiCenterInfo[1]
}
a.AICenter = models.GetAiCenterShow(aiCenterCode, aiCenterName, language)
}

type AITaskListRes struct {


+ 7
- 4
models/card_request.go View File

@@ -34,6 +34,7 @@ type CardRequest struct {
Contact string
PhoneNumber string
EmailAddress string
Wechat string
Org string `xorm:"varchar(500)"`
Description string `xorm:"varchar(3000)"`
Status int
@@ -59,6 +60,7 @@ type CardRequestSpecRes struct {
Contact string
PhoneNumber string
EmailAddress string
Wechat string
Org string
Description string
Status int
@@ -118,6 +120,7 @@ type CardRequestSpecShow struct {
Contact string `json:"contact"`
PhoneNumber string `json:"phone_number"`
EmailAddress string `json:"email_address"`
Wechat string `json:"wechat"`
Org string `json:"org"`
Description string `json:"description"`
Status int `json:"status"`
@@ -282,8 +285,8 @@ func SearchCardRequest(opts *CardRequestOptions) (int64, []*CardRequestSpecRes,
cond = cond.And(builder.Or(builder.Like{"LOWER(card_request.contact)", lowerKeyWord},
builder.Like{"LOWER(card_request.acc_cards_num)", lowerKeyWord},
builder.Like{"LOWER(card_request.description)", lowerKeyWord}, builder.Like{"LOWER(card_request.description)", lowerKeyWord},
builder.Like{"LOWER(card_request.phone_number)", lowerKeyWord}, builder.Like{"LOWER(card_request.org)", lowerKeyWord},
builder.Like{"LOWER(\"user\".name)", lowerKeyWord}))
builder.Like{"LOWER(card_request.phone_number)", lowerKeyWord}, builder.Like{"LOWER(card_request.wechat)", lowerKeyWord},
builder.Like{"LOWER(card_request.org)", lowerKeyWord}, builder.Like{"LOWER(\"user\".name)", lowerKeyWord}))
}
if opts.UserID != 0 {
cond = cond.And(builder.Eq{"\"user\".id": opts.UserID})
@@ -331,7 +334,7 @@ func SearchCardRequest(opts *CardRequestOptions) (int64, []*CardRequestSpecRes,
cond = cond.And(builder.NewCond().Or(builder.Eq{"card_request.delete_unix": 0}).Or(builder.IsNull{"card_request.delete_unix"}))
cols := []string{"card_request.id", "card_request.compute_resource", "card_request.contact", "card_request.card_type", "card_request.acc_cards_num",
"card_request.disk_capacity", "card_request.resource_type", "card_request.begin_date", "card_request.end_date", "card_request.uid",
"card_request.phone_number", "card_request.email_address", "card_request.org", "card_request.description", "card_request.status", "card_request.review",
"card_request.phone_number", "card_request.email_address", "card_request.wechat", "card_request.org", "card_request.description", "card_request.status", "card_request.review",
"card_request.created_unix"}
var count int64
var err error
@@ -437,6 +440,6 @@ func SearchCardRequest(opts *CardRequestOptions) (int64, []*CardRequestSpecRes,
}

func UpdateCardRequest(cardRequest *CardRequest) error {
_, err := x.ID(cardRequest.ID).Cols("compute_resource", "contact", "card_type", "acc_cards_num", "disk_capacity", "resource_type", "begin_date", "end_date", "phone_number", "email_address", "org", "description", "begin_unix", "end_unix").Update(cardRequest)
_, err := x.ID(cardRequest.ID).Cols("compute_resource", "contact", "card_type", "acc_cards_num", "disk_capacity", "resource_type", "begin_date", "end_date", "phone_number", "wechat", "email_address", "org", "description", "begin_unix", "end_unix").Update(cardRequest)
return err
}

+ 1
- 3
models/resource_exclusive_pool.go View File

@@ -17,8 +17,6 @@ type ResourceExclusivePool struct {
CreatedBy int64
UpdatedTime timeutil.TimeStamp `xorm:"updated"`
UpdatedBy int64
DeleteTime timeutil.TimeStamp `xorm:"deleted"`
DeletedBy int64
}

func FindExclusivePools() ([]*ResourceExclusivePool, error) {
@@ -42,7 +40,7 @@ func IsQueueInExclusivePool(queueId int64) bool {

func FindExclusiveQueueIds() []int64 {
existsIds := make([]int64, 0)
err := x.Table("resource_exclusive_pool").Distinct("queue_id").Where("delete_time=? OR delete_time IS NULL", 0).Find(&existsIds)
err := x.Table("resource_exclusive_pool").Distinct("queue_id").Find(&existsIds)
if err != nil {
log.Error("FindQueuesExclusiveMap err.%v", err)
return existsIds


+ 111
- 7
models/resource_queue.go View File

@@ -2,6 +2,7 @@ package models

import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"errors"
"strconv"
@@ -27,6 +28,7 @@ type ResourceQueue struct {
CardsTotalNum int
HasInternet int //0 unknown;1 no internet;2 has internet
IsAutomaticSync bool
IsAvailable bool
Remark string
DeletedTime timeutil.TimeStamp `xorm:"deleted"`
CreatedTime timeutil.TimeStamp `xorm:"created"`
@@ -50,6 +52,7 @@ func (r ResourceQueue) ConvertToRes() *ResourceQueueRes {
UpdatedTime: r.UpdatedTime,
Remark: r.Remark,
HasInternet: AICenterInternetStatus(r.HasInternet),
IsAvailable: r.IsAvailable,
}
}

@@ -66,9 +69,14 @@ type ResourceQueueReq struct {
Remark string
QueueName string
QueueType string
IsAvailable int
}

func (r ResourceQueueReq) ToDTO() ResourceQueue {
isAvailable := false
if r.IsAvailable == 2 {
isAvailable = true
}
q := ResourceQueue{
QueueCode: r.QueueCode,
Cluster: r.Cluster,
@@ -83,6 +91,7 @@ func (r ResourceQueueReq) ToDTO() ResourceQueue {
UpdatedBy: r.CreatorId,
QueueName: r.QueueName,
QueueType: r.QueueType,
IsAvailable: isAvailable,
}
if r.Cluster == OpenICluster {
if r.AiCenterCode == AICenterOfCloudBrainOne {
@@ -104,6 +113,7 @@ type SearchResourceQueueOptions struct {
AccCardType string
HasInternet SpecInternetQuery
QueueType string
IsAvailable int
IsQueueExclusive int
}

@@ -133,6 +143,10 @@ type ResourceAiCenterRes struct {
AiCenterName string
}

func (r *ResourceAiCenterRes) Tr(language string) {
r.AiCenterName = GetAiCenterShow(r.AiCenterCode, r.AiCenterName, language)
}

type GetQueueCodesOptions struct {
Cluster string
}
@@ -166,6 +180,7 @@ type ResourceQueueRes struct {
UpdatedTime timeutil.TimeStamp
Remark string
HasInternet AICenterInternetStatus
IsAvailable bool
IsQueueExclusive bool
}

@@ -176,8 +191,54 @@ func InsertResourceQueue(queue ResourceQueue) (int64, error) {
func UpdateResourceQueueById(queueId int64, queue ResourceQueue) (int64, error) {
return x.ID(queueId).Update(&queue)
}
func UpdateResourceCardsTotalNumAndInternetStatus(queueId int64, queue ResourceQueue) (int64, error) {
return x.ID(queueId).Cols("cards_total_num", "remark", "has_internet", "queue_type", "queue_name").Update(&queue)
func UpdateResourceCardsTotalNumAndInternetStatus(queueId int64, queue ResourceQueue, isAvailable int) (int64, error) {
sess := x.NewSession()
if err := sess.Begin(); err != nil {
sess.Close()
return 0, err
}
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()

cols := []string{"cards_total_num", "remark", "has_internet", "queue_type", "queue_name"}
if isAvailable > 0 {
if isAvailable == 1 {
cols = append(cols, "is_available")
queue.IsAvailable = false
} else if isAvailable == 2 {
cols = append(cols, "is_available")
queue.IsAvailable = true
}
}

n, err := sess.ID(queueId).Cols(cols...).Update(&queue)
if err != nil {
return 0, err
}
specIds := make([]int64, 0)
if err = sess.Cols("resource_specification.id").Table("resource_specification").
In("queue_id", queueId).Find(&specIds); err != nil {
return 0, err
}
if len(specIds) == 0 {
return n, nil
}
if isAvailable == 1 {
if _, err = sess.Cols("status", "is_available").Table("resource_specification").In("id", specIds).Update(&ResourceSpecification{Status: SpecOffShelf, IsAvailable: false}); err != nil {
return 0, err
}
} else if isAvailable == 2 {
if _, err = sess.Cols("is_available").Table("resource_specification").In("id", specIds).Update(&ResourceSpecification{IsAvailable: true}); err != nil {
return 0, err
}
}
sess.Commit()
return n, nil
}

func SearchResourceQueue(opts SearchResourceQueueOptions) (int64, []ResourceQueue, error) {
@@ -205,6 +266,13 @@ func SearchResourceQueue(opts SearchResourceQueueOptions) (int64, []ResourceQueu
if opts.QueueType != "" {
cond = cond.And(builder.Eq{"queue_type": opts.QueueType})
}
if opts.IsAvailable > 0 {
if opts.IsAvailable == 1 {
cond = cond.And(builder.Eq{"is_available": false})
} else if opts.IsAvailable == 2 {
cond = cond.And(builder.Eq{"is_available": true})
}
}
if opts.IsQueueExclusive > 0 {
queueIds := FindExclusiveQueueIds()
if opts.IsQueueExclusive == 1 {
@@ -348,10 +416,7 @@ func SyncGrampusQueues(updateList []ResourceQueue, insertList []ResourceQueue, e
}

if len(deleteQueueIds) > 0 {
if _, err = sess.In("id", deleteQueueIds).Update(&ResourceQueue{Remark: "自动同步时被下架"}); err != nil {
return err
}
if _, err = sess.In("id", deleteQueueIds).Delete(&ResourceQueue{}); err != nil {
if _, err = sess.Cols("is_available").Table("resource_queue").In("id", deleteQueueIds).Update(&ResourceQueue{IsAvailable: false}); err != nil {
return err
}

@@ -362,7 +427,7 @@ func SyncGrampusQueues(updateList []ResourceQueue, insertList []ResourceQueue, e
return err
}
if len(deleteSpcIds) > 0 {
if _, err = sess.In("id", deleteSpcIds).Update(&ResourceSpecification{Status: SpecOffShelf}); err != nil {
if _, err = sess.Cols("status", "is_available").Table("resource_specification").In("id", deleteSpcIds).Update(&ResourceSpecification{Status: SpecOffShelf, IsAvailable: false}); err != nil {
return err
}
}
@@ -375,6 +440,9 @@ func SyncGrampusQueues(updateList []ResourceQueue, insertList []ResourceQueue, e
if _, err = sess.ID(v.ID).Update(&v); err != nil {
return err
}
if _, err = sess.ID(v.ID).Cols("is_available").Table("resource_queue").Update(&v); err != nil {
return err
}
}

}
@@ -399,6 +467,17 @@ func GetResourceAiCenters() ([]ResourceAiCenterRes, error) {
return r, nil
}

func GetAvailableResourceAiCenters() ([]*ResourceAiCenterRes, error) {
r := make([]*ResourceAiCenterRes, 0)
sql := "SELECT t.ai_center_code, t.ai_center_name FROM (SELECT DISTINCT resource_queue.ai_center_code, resource_queue.ai_center_name,resource_queue.cluster FROM resource_queue inner join resource_specification on resource_queue.id = resource_specification.queue_id inner join resource_scene_spec on resource_specification.id = resource_scene_spec.spec_id WHERE (resource_queue.deleted_time IS NULL OR resource_queue.deleted_time=0) and resource_queue.is_available = true and resource_specification.status = 2 ) t ORDER BY cluster desc,ai_center_code asc"

err := x.SQL(sql).Find(&r)
if err != nil {
return nil, err
}
return r, nil
}

func GetExclusiveQueueIds(opts FindSpecsOptions) []*ResourceExclusivePool {
pools, err := FindExclusivePools()
if err != nil {
@@ -446,3 +525,28 @@ func IsUserInExclusivePool(userId int64) bool {
}
return false
}

var defaultLanguage = "zh-CN"

func GetAiCenterShow(aiCenterCode, aiCenterName, language string) string {
if aiCenterCode == "" {
return aiCenterName
}
if aiCenterName == "" {
aiCenterName = aiCenterCode
}
if setting.AiCenterCodeAndNameAndLocMapInfo != nil {
if info, ok := setting.AiCenterCodeAndNameAndLocMapInfo[aiCenterCode]; ok {
if language == defaultLanguage {
return info.Content
} else {
return info.ContentEN
}
} else {
return aiCenterName
}
} else {
return aiCenterName
}
return ""
}

+ 266
- 0
models/resource_specification.go View File

@@ -909,3 +909,269 @@ func GetGrampusSpecs() (map[string]*Specification, error) {
}
return grampusSpecs, nil
}

type GetResourceListOpts struct {
ListOptions
Resource []string
AccCardType string
AccCardNum int
ExcludeAccCardNums []int
AICenterCode string
MinPrice int
MaxPrice int
}

type ResourceDetailInfo struct {
Spec ResourceSpecificationRes
IsQueueExclusive bool
AICenterList []ResourceAiCenterRes
}

type ResourceInfo4CardRequest struct {
ComputeResource string
AccCardType string
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
IsExclusive bool
IsSpecExclusive string
AICenterList []*ResourceAiCenterRes
}

func (r *ResourceInfo4CardRequest) Tr(language string) {
if r.AICenterList == nil {
return
}
for i := 0; i < len(r.AICenterList); i++ {
r.AICenterList[i].Tr(language)
}
}

type ResourceWithAICenter4CardRequest struct {
Cluster string
AICenterCode string
AICenterName string
ComputeResource string
AccCardType string
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
IsExclusive bool
IsSpecExclusive string
}

func GetResourceListPaging(opts GetResourceListOpts) ([]*ResourceInfo4CardRequest, int64, error) {
cond := builder.NewCond()
resourceList := make([]string, 0)
for i := 0; i < len(opts.Resource); i++ {
if opts.Resource[i] != "" {
resourceList = append(resourceList, opts.Resource[i])
}
}
if len(resourceList) > 0 {
cond = cond.And(builder.In("resource_queue.compute_resource", resourceList))
}
if opts.AccCardType != "" {
cond = cond.And(builder.Eq{"resource_queue.acc_card_type": opts.AccCardType})
}
if opts.AccCardNum >= 0 {
if opts.AccCardNum > 999 {
cond = cond.And(builder.NotIn("resource_specification.acc_cards_num", opts.ExcludeAccCardNums))
} else {
cond = cond.And(builder.Eq{"resource_specification.acc_cards_num": opts.AccCardNum})
}
}
if opts.AICenterCode != "" {
cond = cond.And(builder.Eq{"resource_queue.ai_center_code": opts.AICenterCode})
}
if opts.MaxPrice >= 0 && opts.MinPrice >= 0 && opts.MaxPrice < opts.MinPrice {
opts.MaxPrice = -1
opts.MinPrice = -1
}
if opts.MaxPrice >= 0 {
cond = cond.And(builder.Lte{"resource_specification.unit_price": opts.MaxPrice})
}
if opts.MinPrice >= 0 {
cond = cond.And(builder.Gte{"resource_specification.unit_price": opts.MinPrice})
}
cond = cond.And(builder.Or(builder.Eq{"resource_queue.deleted_time": 0}, builder.IsNull{"resource_queue.deleted_time"}))
cond = cond.And(builder.Eq{"resource_specification.status": 2})
//先按多字段去重分页查询资源规格
//再基于结果查询智算中心信息
resourceInfos := make([]*ResourceInfo4CardRequest, 0)
err := x.Table("resource_specification").
Join("LEFT", "resource_exclusive_pool", "resource_specification.queue_id = resource_exclusive_pool.queue_id").
Join("INNER", "resource_queue", "resource_specification.queue_id = resource_queue.id").
Join("INNER", "resource_scene_spec", "resource_specification.id = resource_scene_spec.spec_id").
Join("INNER", "resource_scene", "resource_scene.id = resource_scene_spec.scene_id").
Select("Distinct resource_queue.compute_resource, resource_queue.acc_card_type," +
"resource_specification.acc_cards_num, resource_specification.cpu_cores, resource_specification.mem_gi_b, " +
"resource_specification.gpu_mem_gi_b, resource_specification.share_mem_gi_b, resource_specification.unit_price," +
"COALESCE(resource_exclusive_pool.queue_id IS NOT NULL, false) AS is_exclusive,resource_scene.is_spec_exclusive").
Where(cond).
OrderBy(" resource_queue.compute_resource DESC,resource_queue.acc_card_type ,is_exclusive," +
"resource_specification.acc_cards_num DESC,resource_specification.gpu_mem_gi_b DESC," +
"resource_specification.cpu_cores DESC,resource_specification.mem_gi_b DESC").
Find(&resourceInfos)

if err != nil {
return nil, 0, err
}
tmpResourceInfos := make([]*ResourceInfo4CardRequest, 0)
for i := 0; i < len(resourceInfos); i++ {
//此处是为了过滤那些专属池中的规格又被配置到共享场景中的情况
if !resourceInfos[i].IsExclusive || (resourceInfos[i].IsExclusive && resourceInfos[i].IsSpecExclusive == "") {
tmpResourceInfos = append(tmpResourceInfos, resourceInfos[i])
}
}
resourceInfos = tmpResourceInfos
if len(resourceInfos) == 0 {
return []*ResourceInfo4CardRequest{}, 0, nil
}

total := int64(len(resourceInfos))
startIndex := int64((opts.Page - 1) * opts.PageSize)
endIndex := int64(opts.Page * opts.PageSize)
if startIndex >= total {
return []*ResourceInfo4CardRequest{}, 0, nil
}
if endIndex > total {
endIndex = total
}
resourceInfos = resourceInfos[startIndex:endIndex]

newCond := builder.NewCond()
for _, spec := range resourceInfos {
if spec.IsExclusive {
newCond = newCond.Or(builder.And(builder.Eq{"resource_queue.compute_resource": spec.ComputeResource},
builder.Eq{"resource_queue.acc_card_type": spec.AccCardType},
builder.Eq{"resource_specification.acc_cards_num": spec.AccCardsNum},
builder.Eq{"resource_specification.cpu_cores": spec.CpuCores},
builder.Eq{"resource_specification.mem_gi_b": spec.MemGiB},
builder.Eq{"resource_specification.gpu_mem_gi_b": spec.GPUMemGiB},
builder.Eq{"resource_specification.share_mem_gi_b": spec.ShareMemGiB},
builder.Eq{"resource_specification.unit_price": spec.UnitPrice},
builder.NotNull{"resource_exclusive_pool.queue_id"}))
} else if spec.IsSpecExclusive == SpecExclusive {
newCond = newCond.Or(builder.And(builder.Eq{"resource_queue.compute_resource": spec.ComputeResource},
builder.Eq{"resource_queue.acc_card_type": spec.AccCardType},
builder.Eq{"resource_specification.acc_cards_num": spec.AccCardsNum},
builder.Eq{"resource_specification.cpu_cores": spec.CpuCores},
builder.Eq{"resource_specification.mem_gi_b": spec.MemGiB},
builder.Eq{"resource_specification.gpu_mem_gi_b": spec.GPUMemGiB},
builder.Eq{"resource_specification.share_mem_gi_b": spec.ShareMemGiB},
builder.Eq{"resource_specification.unit_price": spec.UnitPrice},
builder.Eq{"resource_scene.is_spec_exclusive": SpecExclusive},
builder.IsNull{"resource_exclusive_pool.queue_id"}))
} else {
newCond = newCond.Or(builder.And(builder.Eq{"resource_queue.compute_resource": spec.ComputeResource},
builder.Eq{"resource_queue.acc_card_type": spec.AccCardType},
builder.Eq{"resource_specification.acc_cards_num": spec.AccCardsNum},
builder.Eq{"resource_specification.cpu_cores": spec.CpuCores},
builder.Eq{"resource_specification.mem_gi_b": spec.MemGiB},
builder.Eq{"resource_specification.gpu_mem_gi_b": spec.GPUMemGiB},
builder.Eq{"resource_specification.share_mem_gi_b": spec.ShareMemGiB},
builder.Eq{"resource_specification.unit_price": spec.UnitPrice},
builder.Or(builder.Eq{"resource_scene.is_spec_exclusive": SpecPublic}, builder.IsNull{"resource_scene.is_spec_exclusive"}),
builder.IsNull{"resource_exclusive_pool.queue_id"}))
}

}
newCond = newCond.And(builder.Or(builder.Eq{"resource_queue.deleted_time": 0}, builder.IsNull{"resource_queue.deleted_time"}))
newCond = newCond.And(builder.Eq{"resource_specification.status": 2})

withCenterInfos := make([]ResourceWithAICenter4CardRequest, 0)
err = x.Table("resource_specification").
Join("LEFT", "resource_exclusive_pool", "resource_specification.queue_id = resource_exclusive_pool.queue_id").
Join("INNER", "resource_queue", "resource_specification.queue_id = resource_queue.id").
Join("INNER", "resource_scene_spec", "resource_specification.id = resource_scene_spec.spec_id").
Join("INNER", "resource_scene", "resource_scene.id = resource_scene_spec.scene_id").
Select("resource_queue.cluster,resource_queue.ai_center_code,resource_queue.ai_center_name,resource_queue.compute_resource, resource_queue.acc_card_type," +
"resource_specification.acc_cards_num, resource_specification.cpu_cores, resource_specification.mem_gi_b, " +
"resource_specification.gpu_mem_gi_b, resource_specification.share_mem_gi_b, resource_specification.unit_price," +
"COALESCE(resource_exclusive_pool.queue_id IS NOT NULL, false) AS is_exclusive,resource_scene.is_spec_exclusive").
Where(newCond).
Find(&withCenterInfos)

if err != nil {
return nil, 0, err
}
tmpMap := make(map[string][]*ResourceAiCenterRes, 0)
for i := 0; i < len(withCenterInfos); i++ {
t := withCenterInfos[i]
key := fmt.Sprintf("%s_%s_%d_%d_%f_%f_%f_%d_%t_%s", t.ComputeResource, t.AccCardType, t.AccCardsNum,
t.CpuCores, t.MemGiB, t.GPUMemGiB, t.ShareMemGiB, t.UnitPrice, t.IsExclusive, t.IsSpecExclusive)
if _, exists := tmpMap[key]; exists {
centerExists := false
for _, center := range tmpMap[key] {
if center.AiCenterCode == t.AICenterCode {
centerExists = true
}
}
if centerExists {
continue
}
tmpMap[key] = append(tmpMap[key], &ResourceAiCenterRes{
AiCenterCode: t.AICenterCode,
AiCenterName: t.AICenterName,
})
} else {
tmpMap[key] = []*ResourceAiCenterRes{{
AiCenterCode: t.AICenterCode,
AiCenterName: t.AICenterName,
}}
}
}

for i := 0; i < len(resourceInfos); i++ {
t := resourceInfos[i]
key := fmt.Sprintf("%s_%s_%d_%d_%f_%f_%f_%d_%t_%s", t.ComputeResource, t.AccCardType, t.AccCardsNum,
t.CpuCores, t.MemGiB, t.GPUMemGiB, t.ShareMemGiB, t.UnitPrice, t.IsExclusive, t.IsSpecExclusive)
resourceInfos[i].AICenterList = tmpMap[key]
}
return resourceInfos, total, nil
}

type AccCardInfo struct {
ComputeSource string
CardList []string
}

func GetAccCardList() ([]AccCardInfo, error) {
res := make([]AccCardInfo, 0)
r := make([]*Specification, 0)
err := x.Where("resource_specification.status = ? and (resource_queue.deleted_time = 0 or resource_queue.deleted_time is null)", SpecOnShelf).
Join("INNER", "resource_queue", "resource_queue.id = resource_specification.queue_id").
Join("INNER", "resource_scene_spec", "resource_scene_spec.spec_id = resource_specification.id").
Join("INNER", "resource_scene", "resource_scene_spec.scene_id = resource_scene.id").
OrderBy("resource_queue.compute_resource asc,resource_queue.acc_card_type asc").
Unscoped().Distinct("resource_queue.compute_resource,resource_queue.acc_card_type").Find(&r)
if err != nil {
return nil, err
}
tmpMap := make(map[string][]string, 0)
keys := make([]string, 0)
for i := 0; i < len(r); i++ {
spec := r[i]
if _, exists := tmpMap[spec.ComputeResource]; exists {
tmpMap[spec.ComputeResource] = append(tmpMap[spec.ComputeResource], spec.AccCardType)
} else {
keys = append(keys, spec.ComputeResource)
tmpMap[spec.ComputeResource] = []string{spec.AccCardType}
}
}
for i := 0; i < len(keys); i++ {
res = append(res, AccCardInfo{
ComputeSource: keys[i],
CardList: tmpMap[keys[i]],
})
}

return res, nil
}

+ 1
- 0
modules/structs/card_requests.go View File

@@ -12,6 +12,7 @@ type CardReq struct {
Contact string `json:"contact" binding:"Required"`
PhoneNumber string `json:"phone_number" binding:"Required"`
EmailAddress string `json:"email_address" binding:"Required;Email;MaxSize(254)"`
Wechat string `json:"wechat" binding:"Required;MaxSize(254)"`
Org string `json:"org" binding:"MaxSize(500)"`
Description string `json:"description" binding:"MaxSize(3000)"`
Review string `json:"review"`


+ 2
- 0
routers/admin/resources.go View File

@@ -49,6 +49,7 @@ func GetResourceQueueList(ctx *context.Context) {
accCardType := ctx.Query("card")
hasInternet := ctx.QueryInt("hasInternet")
queueType := ctx.Query("queueType")
isAvailable := ctx.QueryInt("isAvailable")
isQueueExclusive := ctx.QueryInt("isQueueExclusive")

if pageSize > 1000 {
@@ -64,6 +65,7 @@ func GetResourceQueueList(ctx *context.Context) {
AccCardType: accCardType,
HasInternet: models.SpecInternetQuery(hasInternet),
QueueType: queueType,
IsAvailable: isAvailable,
IsQueueExclusive: isQueueExclusive,
})
if err != nil {


+ 6
- 0
routers/api/v1/api.go View File

@@ -59,6 +59,7 @@
package v1

import (
"code.gitea.io/gitea/routers/resources"
"net/http"
"strings"

@@ -1491,6 +1492,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/wechat/material", authentication.GetMaterial)
m.Get("/cloudbrain/get_newest_job", repo.GetNewestJobs)
m.Get("/cloudbrain/get_center_info", repo.GetAICenterInfo)

m.Group("/resources", func() {
m.Get("/acc_card/list", resources.GetAccCardList)
m.Get("/ai_center/available", resources.GetAvailableAICenterList)
})
}, securityHeaders(), context.APIContexter(), sudo())
}



+ 65
- 0
routers/card_request/card_request.go View File

@@ -1,7 +1,10 @@
package card_request

import (
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/cloudbrain/resource"
"net/http"
"strconv"
"strings"
"time"

@@ -52,6 +55,67 @@ func GetCardRequestList(ctx *context.Context) {
getRequestShowList(ctx, opts, false)
}

func GetResourceList(ctx *context.Context) {
page := ctx.QueryInt("page")
pageSize := ctx.QueryInt("pageSize")
r := ctx.QueryStrings("resource")
accCardType := ctx.Query("accCardType")
accCardNum := ctx.QueryInt("accCardNum")
excludeAccCardNumStr := ctx.Query("excludeAccCardNums")
centerCode := ctx.Query("centerCode")
minPrice := ctx.QueryInt("minPrice")
maxPrice := ctx.QueryInt("maxPrice")
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 15
}
excludeAccCardNums := make([]int, 0)
if excludeAccCardNumStr != "" {
numStrArray := strings.Split(excludeAccCardNumStr, "|")
for _, s := range numStrArray {
if s == "" {
continue
}
n, err := strconv.Atoi(s)
if err == nil {
excludeAccCardNums = append(excludeAccCardNums, n)
}
}
}
opts := models.GetResourceListOpts{
ListOptions: models.ListOptions{
Page: page,
PageSize: pageSize,
},
Resource: r,
AccCardType: accCardType,
AccCardNum: accCardNum,
ExcludeAccCardNums: excludeAccCardNums,
AICenterCode: centerCode,
MinPrice: minPrice,
MaxPrice: maxPrice,
}
res, total, err := resource.GetResourceListPaging(opts)
if err != nil {
log.Error("GetResourceList err.opts=%+v,%v", opts, err)
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}
if res != nil {
for i := 0; i < len(res); i++ {
res[i].Tr(ctx.Language())
}
}
resultMap := make(map[string]interface{})
resultMap["list"] = res
resultMap["total"] = total
resultMap["page"] = page
resultMap["pageSize"] = pageSize
ctx.JSON(http.StatusOK, response.OuterSuccessWithData(resultMap))
}

func GetMyCardRequestList(ctx *context.Context) {

page := ctx.QueryInt("page")
@@ -241,6 +305,7 @@ func getRequestShowList(ctx *context.Context, opts *models.CardRequestOptions, c
customShow.Review = v.Review
customShow.PhoneNumber = v.PhoneNumber
customShow.EmailAddress = v.EmailAddress
customShow.Wechat = v.Wechat
customShow.Contact = v.Contact
customShow.Specs = v.Specs
customShow.Org = v.Org


+ 35
- 0
routers/resources/acc_card.go View File

@@ -0,0 +1,35 @@
package resources

import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/cloudbrain/resource"
"net/http"
)

func GetAccCardList(ctx *context.Context) {
list, err := resource.GetAccCardList()
if err != nil {
log.Error("GetAccCardList error.%v", err)
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}

m := map[string]interface{}{"list": list}
ctx.JSON(http.StatusOK, response.OuterSuccessWithData(m))
}

func GetAvailableAICenterList(ctx *context.Context) {
list, err := resource.GetAvailableAICenter()
if err != nil {
log.Error("GetAvailableAICenterList error.%v", err)
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}
for i := 0; i < len(list); i++ {
list[i].Tr(ctx.Language())
}
m := map[string]interface{}{"list": list}
ctx.JSON(http.StatusOK, response.OuterSuccessWithData(m))
}

+ 1
- 1
routers/routes/routes.go View File

@@ -451,7 +451,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/card_request", func() {
m.Get("/creation/required", card_request.GetCreationInfo)
m.Get("/list", card_request.GetCardRequestList)
m.Get("/resource/list", card_request.GetResourceList)
}, ignSignIn)

m.Group("/card_request", func() {


+ 5
- 2
services/card_request/card_request.go View File

@@ -21,9 +21,9 @@ func GetCreationInfo() (map[string][]string, error) {

for _, xpuInfo := range xpuInfoBase {
if _, ok := xpuInfoMap[xpuInfo.ResourceType]; ok {
xpuInfoMap[xpuInfo.ResourceType] = append(xpuInfoMap[xpuInfo.ResourceType], xpuInfo.CardTypeShow)
xpuInfoMap[xpuInfo.ResourceType] = append(xpuInfoMap[xpuInfo.ResourceType], xpuInfo.CardType)
} else {
xpuInfoMap[xpuInfo.ResourceType] = []string{xpuInfo.CardTypeShow}
xpuInfoMap[xpuInfo.ResourceType] = []string{xpuInfo.CardType}
}
}
return xpuInfoMap, nil
@@ -53,6 +53,7 @@ func UpdateCardRequestAdmin(cardReq api.CardReq) error {
DiskCapacity: cardReq.DiskCapacity,
Contact: cardReq.Contact,
PhoneNumber: cardReq.PhoneNumber,
Wechat: cardReq.Wechat,
BeginDate: cardReq.BeginDate,
EndDate: cardReq.EndDate,
Description: cardReq.Description,
@@ -91,6 +92,7 @@ func UpdateCardRequest(cardReq api.CardReq, request *models.CardRequest) error {
request.Contact = cardReq.Contact
request.EmailAddress = cardReq.EmailAddress
request.PhoneNumber = cardReq.PhoneNumber
request.Wechat = cardReq.Wechat

beginTime, err := time.Parse(DATE_LAYOUT, cardReq.BeginDate)
if err != nil {
@@ -121,6 +123,7 @@ func CreateCardRequest(cardReq api.CardReq, uid int64) error {
DiskCapacity: cardReq.DiskCapacity,
Contact: cardReq.Contact,
PhoneNumber: cardReq.PhoneNumber,
Wechat: cardReq.Wechat,
BeginDate: cardReq.BeginDate,
EndDate: cardReq.EndDate,
Description: cardReq.Description,


+ 3
- 1
services/cloudbrain/resource/resource_queue.go View File

@@ -23,7 +23,7 @@ func UpdateResourceQueue(queueId int64, req models.ResourceQueueReq) error {
QueueName: req.QueueName,
Remark: req.Remark,
HasInternet: req.HasInternet,
}); err != nil {
}, req.IsAvailable); err != nil {
return err
}
return nil
@@ -159,6 +159,7 @@ func SyncGrampusQueue(doerId int64) error {
QueueCode: queue.QueueCode,
QueueName: queue.QueueName,
QueueType: queue.QueueType,
IsAvailable: true,
})
} else {
existIds = append(existIds, oldQueue.ID)
@@ -171,6 +172,7 @@ func SyncGrampusQueue(doerId int64) error {
HasInternet: hasInternet,
QueueName: queue.QueueName,
QueueType: queue.QueueType,
IsAvailable: true,
})
}



+ 13
- 1
services/cloudbrain/resource/resource_specification.go View File

@@ -210,7 +210,7 @@ func ResourceSpecOnShelf(doerId int64, id int64, unitPrice int) *response.BizErr
if spec == nil {
return response.SPECIFICATION_NOT_EXIST
}
if q, err := models.GetResourceQueue(&models.ResourceQueue{ID: spec.QueueId}); err != nil || q == nil {
if q, err := models.GetResourceQueue(&models.ResourceQueue{ID: spec.QueueId}); err != nil || q == nil || !q.IsAvailable {
return response.RESOURCE_QUEUE_NOT_AVAILABLE
}
if !spec.IsAvailable {
@@ -700,3 +700,15 @@ func InitQueueAndSpec(opt models.FindSpecsOptions, aiCenterName string, remark s
IsAvailable: true,
})
}

func GetResourceListPaging(opts models.GetResourceListOpts) ([]*models.ResourceInfo4CardRequest, int64, error) {
return models.GetResourceListPaging(opts)
}

func GetAccCardList() ([]models.AccCardInfo, error) {
return models.GetAccCardList()
}

func GetAvailableAICenter() ([]*models.ResourceAiCenterRes, error) {
return models.GetAvailableResourceAiCenters()
}

+ 1
- 1
web_src/less/_admin.less View File

@@ -93,7 +93,7 @@
margin-right: 10px !important;
border: 1px solid #d4d4d5;
border-radius: 4px;
box-shadow: 0 1px 2px 0 rgb(34 36 38 / 15%);
box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 15%);
background-color: #fafafa !important;
.item {
align-self: flex-start !important;


+ 61
- 0
web_src/vuepages/apis/modules/computingpower.js View File

@@ -1,6 +1,67 @@
import service from "../service";
import Qs from 'qs';

// 算力需求-算力资源查询智算中心列表
export const getAvailableAiCenterList = () => {
return service({
url: `/api/v1/resources/ai_center/available`,
method: 'get',
params: {},
data: {},
});
}

// 查询智算列表
export const getAiCenterList = () => {
return service({
url: `/explore/card_request/resources/queue/centers`,
method: 'get',
params: {},
data: {},
});
}

// 查询所有资源队列名称列表
export const getResQueueCode = (params) => { // cluster
return service({
url: `/explore/card_request/resources/queue/codes`,
method: 'get',
params,
});
}

// 获取资源规格清单
// params -cluster,resource,available,
export const getSpecificationList = (params) => {
return service({
url: `/explore/card_request/specification/list`,
method: 'get',
params,
});
};

/* 算力资源 */
// 查询卡类型数据
export const getAccCardList = () => {
return service({
url: `/api/v1/resources/acc_card/list`,
method: 'get',
params: {}
});
};

// 查询算力资源列表
// params-page,pageSize,resource-GPU|NPU...,accCardType-ASCEND910|...,accCardNum:-1|1|2|4|8...,excludeAccCardNums-"1|2|4|8",centerCode,minPrice,maxPrice-积分值,未填请传-1
export const getResourceList = (params) => {
return service({
url: `/explore/card_request/resource/list`,
method: 'get',
params: { ...params },
paramsSerializer: _params => Qs.stringify(_params, { arrayFormat: 'repeat' }),
});
};

/* 算力需求 */
// 获取创建算力计算资源和卡类型信息
export const getDemandCreationRequired = (params) => {
return service({


+ 2
- 0
web_src/vuepages/langs/config/en-US.js View File

@@ -143,6 +143,8 @@ const en = {
resQueueCode: 'Resources Queue Code',
resQueueName: 'Resources Queue Name',
resQueueType: "Resources Queue Type",
resQueueIsAvailable: "Resources Queue Is Available",
resQueueIsAvailableAll: "Resources Queue Is Available(All)",
allResQueueType: "All Resources Queue Type",
whichCluster: 'Cluster',
allCluster: 'All Clusters',


+ 2
- 0
web_src/vuepages/langs/config/zh-CN.js View File

@@ -142,6 +142,8 @@ const zh = {
resQueueCode: "资源池(队列)编码",
resQueueName: "资源池(队列)名称",
resQueueType: "资源池(队列)类型",
resQueueIsAvailable: "资源池(队列)是否可用",
resQueueIsAvailableAll: "资源池(队列)是否可用(全部)",
allResQueueType: "全部资源池(队列)类型",
whichCluster: "所属集群",
allCluster: "全部集群",


+ 27
- 2
web_src/vuepages/pages/computingpower/components/DemandForm.vue View File

@@ -15,7 +15,8 @@
</el-select>
</el-form-item>
<el-form-item label="卡数(卡)" prop="acc_cards_num">
<el-input :disabled="disabledEdit" v-model="form.acc_cards_num" placeholder="请输入卡数,数值(4)或范围(8-16)"></el-input>
<el-input :disabled="disabledEdit" ref="accCardsNumRef" v-model="form.acc_cards_num"
placeholder="请输入卡数,例如数值(4)或者范围(8-16)"></el-input>
</el-form-item>
<el-form-item label="存储容量(GB)" prop="disk_capacity">
<el-input :disabled="disabledEdit" v-model.number="form.disk_capacity" placeholder="请输入存储容量"></el-input>
@@ -33,6 +34,9 @@
<el-form-item label="联系人姓名" prop="contact">
<el-input :disabled="disabledEdit" v-model="form.contact" placeholder="请输入" maxlength="255"></el-input>
</el-form-item>
<el-form-item label="微信" prop="wechat">
<el-input :disabled="disabledEdit" v-model="form.wechat" placeholder="请输入" maxlength="127"></el-input>
</el-form-item>
<el-form-item label="电话" prop="phone_number">
<el-input :disabled="disabledEdit" v-model="form.phone_number" placeholder="请输入" maxlength="255"></el-input>
</el-form-item>
@@ -69,6 +73,9 @@
<script>
import { getDemandCreationRequired, postDemand, updateDemand } from '~/apis/modules/computingpower';
import { formatDate } from 'element-ui/lib/utils/date-util';
import { ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';

export default {
name: "DemandForm",
props: {
@@ -85,6 +92,7 @@ export default {
resource_type: '',
use_time: '',
contact: '',
wechat: '',
phone_number: '',
email_address: '',
org: '',
@@ -135,6 +143,7 @@ export default {
resource_type: [{ required: true, message: ' ' }],
use_time: [{ required: true, message: ' ' }],
contact: [{ required: true, message: ' ' }],
wechat: [{ required: true, message: ' ' }],
phone_number: [
{ required: true, message: ' ' },
{
@@ -191,7 +200,7 @@ export default {
this.cardTypeList = [];
},
changeComputeResource() {
this.cardTypeList = this.computeResourceCardTypeMap[this.form.compute_resource].map(itm => ({ k: itm, v: itm }));
this.cardTypeList = this.computeResourceCardTypeMap[this.form.compute_resource].map(itm => ({ k: itm, v: getListValueWithKey(ACC_CARD_TYPE, itm) }));
this.form.card_type = this.cardTypeList.length ? this.cardTypeList[0].k : '';
},
initEdit() {
@@ -203,6 +212,22 @@ export default {
this.changeComputeResource();
this.form.card_type = this.data['card_type'];
},
initApply(data) {
this.form.compute_resource = '';
this.form.card_type = '';
this.cardTypeList = [];
this.form.acc_cards_num = '';
const computeResource = data.ComputeResource.indexOf('-GPGPU') >= 0 ? 'GPGPU' : data.ComputeResource;
if (this.computeResourceList.find(item => item.k == computeResource)) {
this.form.compute_resource = computeResource;
this.changeComputeResource();
}
if (this.cardTypeList.find(item => item.k == data.AccCardType)) {
this.form.card_type = data.AccCardType;
}
this.form.acc_cards_num = data.AccCardsNum.toString();
this.$refs['accCardsNumRef'].focus();
},
onSubmit() {
if (this.submitLoading) return;
for (let key in this.form) {


+ 560
- 0
web_src/vuepages/pages/computingpower/components/Resources.vue View File

@@ -0,0 +1,560 @@
<template>
<div class="resources-c">
<div class="conds-c">
<div class="conds-item">
<div class="conds-item-tit">计算资源:</div>
<div class="conds-item-content">
<div class="sel-item" :class="item.k == conds.compute_resource ? 'active' : ''"
v-for="(item, index) in computeResourceList" :key="index" @click="changeConds(item.k, 'compute_resource')">
{{ item.v }}
</div>
</div>
</div>
<div class="conds-item">
<div class="conds-item-tit">卡类型:</div>
<div class="conds-item-content">
<div class="sel-item" :class="item.k == conds.card_type ? 'active' : ''" v-for="(item, index) in cardTypeList"
:key="index" @click="changeConds(item.k, 'card_type')">
{{ item.v }}
</div>
</div>
</div>
<div class="conds-item">
<div class="conds-item-tit">卡数:</div>
<div class="conds-item-content">
<div class="sel-item" :class="item.k == conds.acc_cards_num ? 'active' : ''"
v-for="(item, index) in cardNumList" :key="index" @click="changeConds(item.k, 'acc_cards_num')">
{{ item.v }}
</div>
</div>
</div>
<div class="conds-item">
<div class="conds-item-tit">算力中心:</div>
<div class="conds-item-content">
<div class="sel-item" :class="item.k == conds.ai_center ? 'active' : ''" v-for="(item, index) in aiCenterList"
:key="index" @click="changeConds(item.k, 'ai_center')">
{{ item.v }}
</div>
</div>
</div>
<div class="conds-item">
<div class="conds-item-tit">价格区间:</div>
<div class="conds-item-content">
<el-input class="price-s" v-model="conds.price_start" @input="inputPrice('price_start')"
@blur="checkPrice('price_start')"></el-input><span class="to">至</span><el-input v-model="conds.price_end"
@input="inputPrice('price_end')" @blur="checkPrice('price_end')" class="price-e"></el-input><span
class="unit">积分/卡时</span>
<el-button type="primary" size="mini" v-if="conds.price_start != '' || conds.price_end != ''"
@click="clearPriceConds">清除</el-button>
</div>
</div>
</div>
<div class="content-c">
<div class="list-c" v-loading="loading">
<div class="list-item" v-for="(item, index) in list" :key="index">
<div class="top">
<div class="left">
<div class="title">
<div class="name">{{ item.AccCardTypeShow || item.AccCardType }}</div>
<div v-if="!item.IsExclusive" class="type">共享池</div>
<div v-if="item.IsExclusive" class="type exclusive">专属池</div>
</div>
<div class="attributes">
<div class="attribute">
<span class="tit">{{ item.ComputeResource }}:</span><span class="val">{{ item.AccCardsNum }} * {{
item.AccCardTypeShow || item.AccCardType }}
</span>
</div>
<div class="attribute" v-if="item.CpuCores">
<span class="tit">CPU:</span><span class="val">{{ item.CpuCores }}</span>
</div>
<div class="attribute" v-if="item.GPUMemGiB">
<span class="tit">显存:</span><span class="val">{{ item.GPUMemGiB }}GB</span>
</div>
<div class="attribute" v-if="item.MemGiB">
<span class="tit">内存:</span><span class="val">{{ item.MemGiB }}GB</span>
</div>
</div>
</div>
<div class="right">
<span class="price">{{ item.UnitPrice }}</span> 积分/卡时
</div>
</div>
<div class="bottom">
<div class="left">
<div class="ai-center" v-for="(center) in item.AICenterList" :key="center.AiCenterCode">
{{ center.AiCenterName }}
</div>
</div>
<div class="right">
<div class="apply"
v-if="item.IsExclusive || (item.IsExclusive == false && item.IsSpecExclusive == 'exclusive')"
@click="applyUse(item)">需申请使用</div>
<div class="available" v-else>可直接使用</div>
</div>
</div>
</div>
<div class="demand-item no-data" v-if="(!list.length && !loading)">
<div class="item-empty">
<div class="item-empty-icon"></div>
<div class="item-empty-tips">{{ $t('modelObj.model_square_empty') }}</div>
</div>
</div>
</div>
</div>
<div class="pagination-c">
<el-pagination background layout="total, sizes, prev, pager, next, jumper" :current-page.sync="conds.page"
:page-size.sync="conds.page_size" :page-sizes="paginationInfo.pageSizes" :total="paginationInfo.total"
@current-change="currentChange" @size-change="sizeChange">
</el-pagination>
</div>
</div>
</template>

<script>
import { getAccCardList, getAvailableAiCenterList, getResourceList } from '~/apis/modules/computingpower';
import { ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';

export default {
name: "Resources",
props: {
condtions: { type: Object, default: () => ({}) },
active: { type: Boolean, defalut: false },
},
data() {
return {
computeResourceList: [{ k: '', v: '全部' }],
cardTypeList: [{ k: '', v: '全部' }],
cardNumList: [{ k: '', v: '全部' }, ...[1, 2, 4, 8].map(itm => ({ k: itm, v: itm })), { k: 9999, v: '其它' }],
aiCenterList: [{ k: '', v: '全部' }],
computeResourceMap: {},
cardTypeMap: {},
conds: {
compute_resource: '',
card_type: '',
acc_cards_num: '',
ai_center: '',
price_start: '',
price_end: '',
page: 1,
page_size: 15,
},
_price_start: '',
_price_end: '',
list: [1, 2, 3, 4, 5, 6],
paginationInfo: {
pageSizes: [15, 30, 50],
total: 0,
},
loading: false,
};
},
computed: {},
watch: {
active: {
handler(newValue, oValue) {
if (newValue) {
this.search();
}
}
}
},
methods: {
changeConds(value, type) {
if (type == 'compute_resource') {
this.cardTypeList.splice(1, Infinity);
for (let key in this.cardTypeMap) {
if (value && value != key) continue;
this.cardTypeMap[key].forEach(item => {
this.cardTypeList.push({
k: item,
v: getListValueWithKey(ACC_CARD_TYPE, item),
});
})
}
this.conds['card_type'] = '';
}
this.conds[type] = value;
this.conds.page = 1;
this.search();
},
inputPrice(type) {
if (!Number.isInteger(Number(this.conds[type])) || this.conds[type].length > 3) {
this.conds[type] = this.conds[type].slice(0, this.conds[type].length - 1);
}
},
checkPrice(type) {
if (this.conds.price_start && this.conds.price_end && Number(this.conds.price_start) > Number(this.conds.price_end)) {
this.conds[type] = '';
}
if (this._price_start != this.conds.price_start || this._price_end != this.conds.price_end) {
this.conds.page = 1;
this.search();
}
this._price_start = this.conds.price_start;
this._price_end = this.conds.price_end;
},
clearPriceConds() {
this._price_start = this.conds.price_start = '';
this._price_end = this.conds.price_end = '';
this.conds.page = 1;
this.search();
},
currentChange(page) {
this.conds.page = page;
this.search();
},
sizeChange(pageSize) {
this.conds.page_size = pageSize;
this.search();
},
search() {
const params = {
page: this.conds.page,
pageSize: this.conds.page_size,
resource: this.conds.compute_resource === '' ? '' : this.computeResourceMap[this.conds.compute_resource] || [this.conds.compute_resource],
accCardType: this.conds.card_type,
accCardNum: this.conds.acc_cards_num === '' ? -1 : Number(this.conds.acc_cards_num),
excludeAccCardNums: this.conds.acc_cards_num == 9999 ? this.cardNumList.slice(1, this.cardNumList.length - 1).map(itm => itm.k).join('|') : undefined,
centerCode: this.conds.ai_center,
minPrice: this.conds.price_start === '' ? -1 : Number(this.conds.price_start),
maxPrice: this.conds.price_end === '' ? -1 : Number(this.conds.price_end),
};
// console.log('search conds', this.conds);
// console.log('search params', params);
this.loading = true;
getResourceList(params).then(res => {
this.loading = false;
res = res.data;
if (res.code == 0) {
const data = res.data || {};
this.paginationInfo.total = data.total;
this.list = (data.list || []).map(item => {
return {
AccCardTypeShow: getListValueWithKey(ACC_CARD_TYPE, item.AccCardType),
...item,
}
});
}
}).catch(err => {
this.loading = false;
console.log(err);
});
},
applyUse(data) {
this.$emit('apply', data);
},
setConds() {
for (let key in this.conds) {
if (this.condtions[key]) {
this.conds[key] = this.condtions[key];
}
}
this._price_start = this.conds.price_start || '';
this._price_end = this.conds.price_end || '';
}
},
created() {
// this.setConds();
getAccCardList().then(res => {
res = res.data;
if (res.code == 0) {
this.computeResourceList.splice(1, Infinity);
this.cardTypeList.splice(1, Infinity);
const data = res.data.list || [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
const computeSource = item.ComputeSource;
const cardList = item.CardList;
const computeSourceKey = computeSource.indexOf('-GPGPU') > 0 ? 'GPGPU' : computeSource;
if (this.computeResourceMap[computeSourceKey]) {
this.computeResourceMap[computeSourceKey].push(computeSource);
} else {
this.computeResourceMap[computeSourceKey] = [computeSource];
}
if (this.cardTypeMap[computeSourceKey]) {
this.cardTypeMap[computeSourceKey].push(...cardList);
} else {
this.cardTypeMap[computeSourceKey] = cardList;
}
}
for (let key in this.computeResourceMap) {
this.computeResourceMap[key] = Array.from(new Set(this.computeResourceMap[key]));
this.computeResourceList.push({
k: key,
v: key,
});
}
for (let key in this.cardTypeMap) {
this.cardTypeMap[key] = Array.from(new Set(this.cardTypeMap[key]));
this.cardTypeMap[key].forEach(item => {
this.cardTypeList.push({
k: item,
v: getListValueWithKey(ACC_CARD_TYPE, item),
});
})
}
}
}).catch(err => {
console.log(err);
});
getAvailableAiCenterList().then(res => {
res = res.data;
if (res.code == 0) {
this.aiCenterList.splice(1, Infinity);
const data = res?.data?.list || [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
this.aiCenterList.push({
k: item.AiCenterCode,
v: item.AiCenterName,
});
}
}
}).catch(err => {
console.log(err);
});
},
mounted() {
this.search();
},
};
</script>

<style scoped lang="less">
.resources-c {
margin-top: 20px;
}

.conds-c {
.conds-item {
display: flex;

.conds-item-tit {
width: 100px;
display: flex;
padding-top: 7px;
justify-content: flex-end;
padding-right: 10px;
}

.conds-item-content {
display: flex;
flex-wrap: wrap;
width: 0;
flex: 1;
align-items: center;

.sel-item {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border-radius: 4px;
background-color: rgba(248, 249, 250, 1);
color: rgba(65, 80, 88, 1);
font-size: 12px;
padding: 0 14px;
cursor: pointer;
margin-right: 12px;
margin-bottom: 16px;

&.active {
background-color: rgba(3, 102, 214, 1);
color: rgba(255, 255, 255, 1);
}
}

.price-s,
.price-e {
width: 52px;

/deep/.el-input__inner {
text-align: center;
padding: 0 8px;
height: 30px;
}
}

.to,
.unit {
margin: 0 10px;
}
}
}
}

.content-c {
margin-top: 20px;

.list-c {
.list-item {
padding: 18px 22px;
border-radius: 15px;
background-color: rgba(255, 255, 255, 1);
box-shadow: 0px 5px 10px 0px rgba(157, 197, 226, 0.2);
border: 1px solid rgba(157, 197, 226, 0.4);
margin-bottom: 20px;

.top {
display: flex;

.left {
width: 0;
flex: 1;

.title {
display: flex;
align-items: center;

.name {
color: rgba(16, 16, 16, 1);
font-size: 16px;
margin-right: 10px;
font-weight: bold;
}

.type {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
height: 18px;
border-radius: 2px;
background-color: rgba(91, 185, 115, 1);
color: rgba(251, 251, 251, 1);
font-size: 12px;
text-align: center;

&.exclusive {
background-color: rgba(50, 145, 248, 1);
}
}
}

.attributes {
display: flex;
align-items: center;
margin-top: 13px;

.attribute {
margin-right: 16px;

.tit {
color: rgb(136, 136, 136);
}
}
}
}

.right {
width: 100px;
padding-top: 14px;
text-align: right;
font-size: 14px;

.price {
color: rgba(16, 16, 16, 1);
font-size: 20px;
margin-right: 2px;
}
}
}

.bottom {
border-top: 1px solid rgba(157, 197, 226, 0.2);
margin-top: 14px;
padding-top: 6px;
display: flex;
align-items: center;

.left {
width: 0;
flex: 1;
display: flex;
flex-wrap: wrap;

.ai-center {
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
margin-right: 10px;
margin-top: 10px;
height: 30px;
border-radius: 4px;
background-color: rgba(50, 145, 248, 0.1);
color: rgba(50, 145, 248, 1);
font-size: 12px;
text-align: center;
border: 1px solid rgba(50, 145, 248, 0.6);
}
}

.right {
width: 100px;
display: flex;
align-items: center;
justify-content: flex-end;

.available {
color: rgba(39, 177, 72, 1);
font-size: 12px;
}

.apply {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
padding: 0 12px;
border-radius: 4px;
color: rgba(50, 145, 248, 1);
font-size: 12px;
border: 1px solid rgba(50, 145, 248, 1);
cursor: pointer;
}
}
}
}
}
}

.no-data {
display: flex;
justify-content: center;
padding: 0 0;
width: 100%;

.item-empty {
height: 391px;
width: 100%;
overflow: hidden;
padding: 15px;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
background-color: rgba(245, 245, 246, 0.5);

.item-empty-icon {
height: 80px;
width: 100%;
background: url(/img/empty-box.svg) center center no-repeat;
}

.item-empty-tips {
text-align: center;
margin-top: 20px;
font-size: 18px;
color: rgb(63, 63, 64);
}
}
}

.pagination-c {
text-align: center;
margin: 10px 0;
}
</style>

+ 45
- 17
web_src/vuepages/pages/computingpower/demand/index.vue View File

@@ -1,27 +1,33 @@
<template>
<div class="ui container">
<div class="top-head">
<div class="title">算力需求</div>
<div class="title">算力资源</div>
<div class="descr">为了满足用户的个性化算力需求,启智社区可以根据用户的需求定制算力池,欢迎提交您的算力需求。</div>
</div>
<div class="main-content">
<div class="top-area">
<div class="tab-c">
<div class="tab-item" :class="activeTab == 0 ? 'active' : ''" @click="changeTab(0)">算力需求广场</div>
<div class="tab-item" v-if="isLogin" :class="activeTab == 1 ? 'active' : ''" @click="changeTab(1)">我的需求</div>
<div class="tab-item" :class="activeTab == 0 ? 'active' : ''" @click="changeTab(0)">算力资源</div>
<div class="tab-item" :class="activeTab == 1 ? 'active' : ''" @click="changeTab(1)">算力需求广场</div>
<div class="tab-item" v-if="isLogin" :class="activeTab == 2 ? 'active' : ''" @click="changeTab(2)">我的需求</div>
</div>
<div class="operate-c">
<el-button size="default" v-if="isOperator" type="primary" @click="goOperate">需求处理</el-button>
</div>
</div>
<div class="middle-area">
<div class="left-area">
<div class="tab-content" v-if="activeTab == 0">
<div class="left-area" v-loading="loading">
<div class="tab-content" v-show="activeTab == 0">
<Resources :active="activeTab == 0" @apply="applyUse">
</Resources>
</div>
<div class="tab-content" v-if="activeTab == 1">
<div class="demand-item demand-item-all" v-for="(item, index) in dataAll" :key="index">
<div class="demand-item-top">
<div class="demand-item-line">
<div class="demand-item-line-block"><span>计算资源:</span><span>{{ item.compute_resource }}</span></div>
<div class="demand-item-line-block"><span>卡类型:</span><span>{{ item.card_type }}</span></div>
<div class="demand-item-line-block"><span>卡类型:</span><span>{{ item.card_type_show || item.card_type
}}</span></div>
<div class="demand-item-line-block"><span>卡数(卡):</span><span>{{ item.acc_cards_num }}</span></div>
</div>
<div class="demand-item-line">
@@ -54,12 +60,13 @@
</div>
</div>
</div>
<div class="tab-content" v-if="activeTab == 1">
<div class="tab-content" v-if="activeTab == 2">
<div class="demand-item demand-item-my" v-for="(item, index) in dataMy" :key="index">
<div class="demand-item-top">
<div class="demand-item-line">
<div class="demand-item-line-block"><span>计算资源:</span><span>{{ item.compute_resource }}</span></div>
<div class="demand-item-line-block"><span>卡类型:</span><span>{{ item.card_type }}</span></div>
<div class="demand-item-line-block"><span>卡类型:</span><span>{{ item.card_type_show || item.card_type
}}</span></div>
<div class="demand-item-line-block"><span>卡数(卡):</span><span>{{ item.acc_cards_num }}</span></div>
</div>
<div class="demand-item-line">
@@ -106,7 +113,7 @@
</div>
</div>
</div>
<div class="pagination-c">
<div class="pagination-c" v-if="activeTab != 0">
<el-pagination background layout="total, sizes, prev, pager, next, jumper"
:current-page.sync="paginationInfo.page" :page-sizes="paginationInfo.pageSizes"
:page-size.sync="paginationInfo.pageSize" :total="paginationInfo.total" @current-change="currentChange"
@@ -125,12 +132,15 @@
</template>

<script>
import Resources from '../components/Resources.vue';
import DemandForm from '../components/DemandForm.vue';
import DemandEditDialog from '../components/DemandEditDialog.vue';
import { getDemandList, getDemandMyList } from '~/apis/modules/computingpower';
import dayjs from 'dayjs';
import { lang } from '~/langs';
import { timeSinceUnix } from '~/utils';
import { ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';

export default {
data() {
@@ -149,7 +159,7 @@ export default {
},
};
},
components: { DemandForm, DemandEditDialog },
components: { Resources, DemandForm, DemandEditDialog },
methods: {
changeTab(tab) {
if (tab == this.activeTab) return;
@@ -158,7 +168,7 @@ export default {
this.paginationInfo.total = 0;
this.getData();
this.$nextTick(() => {
this.$refs['demandFormRef'].resetForm();
this.$refs['demandFormRef']?.resetForm();
});
},
getData() {
@@ -166,7 +176,8 @@ export default {
pageSize: this.paginationInfo.pageSize,
page: this.paginationInfo.page,
}
const subApi = this.activeTab == 0 ? getDemandList : getDemandMyList;
const subApi = this.activeTab == 1 ? getDemandList : this.activeTab == 2 ? getDemandMyList : '';
if (!subApi) return;
this.loading = true;
subApi(params).then(res => {
this.loading = false;
@@ -174,10 +185,16 @@ export default {
if (res.code == 0) {
const data = res.data;
this.paginationInfo.total = data.total;
if (this.activeTab == 0) {
this.dataAll = data.cardRequestList || [];
} else {
this.dataMy = data.cardRequestList || [];
const cardRequestList = (data.cardRequestList || []).map(item => {
return {
...item,
card_type_show: getListValueWithKey(ACC_CARD_TYPE, item.card_type),
}
})
if (this.activeTab == 1) {
this.dataAll = cardRequestList;
} else if (this.activeTab == 2) {
this.dataMy = cardRequestList;
}
}
}).catch(err => {
@@ -193,7 +210,7 @@ export default {
this.getData();
},
sizeChange(pageSize) {
this.pageSize = pageSize;
this.paginationInfo.pageSize = pageSize;
this.getData();
},
calcFromNow(unix) {
@@ -214,6 +231,15 @@ export default {
this.getData();
},
demandUpdateError() { },
applyUse(data) {
if (!this.isLogin) {
window.location.href = `/user/login?redirect_to=${encodeURIComponent(window.location.href)}`;
return;
}
this.$nextTick(() => {
this.$refs['demandFormRef']?.initApply(data);
});
},
},
created() {
this.getData();
@@ -306,6 +332,8 @@ export default {
padding-top: 22px;

.form-container {
top: 10px;
position: sticky;
border: 1px solid rgb(229, 231, 235);
background: rgb(249, 250, 251);
border-radius: 10px;


+ 16
- 1
web_src/vuepages/pages/resources/components/QueueDialog.vue View File

@@ -91,6 +91,16 @@
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.resQueueIsAvailable') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.IsAvailable">
<el-option v-for="item in isAvailableList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row" style="margin-top: 10px">
<div class="title"><span>{{ $t('resourcesManagement.remark') }}</span></div>
<div class="content" style="width: 400px">
@@ -111,6 +121,7 @@
</BaseDialog>
</div>
</template>

<script>
import BaseDialog from '~/components/BaseDialog.vue';
import { addResQueue, updateResQueue } from '~/apis/modules/resources';
@@ -136,6 +147,7 @@ export default {
computingTypeList: [...COMPUTER_RESOURCES],
cardTypeList: [...ACC_CARD_TYPE],
networkTypeList: [...NETWORK_TYPE_VALUE],
isAvailableList: [{ k: 1, v: this.$t('resourcesManagement.notAvailable') }, { k: 2, v: this.$t('resourcesManagement.available') }],
dataInfo: {},
};
},
@@ -157,6 +169,7 @@ export default {
AccCardType: '',
CardsTotalNum: '',
HasInternet: '',
IsAvailable: '',
Remark: '',
}
},
@@ -166,6 +179,7 @@ export default {
//
} else if (this.type === 'edit') {
this.dataInfo = Object.assign(this.dataInfo, { ...this.data });
this.dataInfo.IsAvailable = this.data.IsAvailable ? 2 : 1;
}
this.$emit("open");
},
@@ -181,7 +195,7 @@ export default {
},
confirm() {
if (!this.dataInfo.QueueCode || !this.dataInfo.Cluster || !this.dataInfo.AiCenterCode || !this.dataInfo.ComputeResource
|| !this.dataInfo.AccCardType || !this.dataInfo.CardsTotalNum || this.dataInfo.HasInternet === '') {
|| !this.dataInfo.AccCardType || !this.dataInfo.CardsTotalNum || this.dataInfo.HasInternet === '' || this.dataInfo.IsAvailable === '') {
this.$message({
type: 'info',
message: this.$t('pleaseCompleteTheInformationFirst'),
@@ -228,6 +242,7 @@ export default {
},
};
</script>

<style scoped lang="less">
.dlg-content {
margin: 20px 0 25px 0;


+ 28
- 9
web_src/vuepages/pages/resources/queue/index.vue View File

@@ -24,8 +24,11 @@
<el-select class="select" size="medium" v-model="selNetworkType" @change="selectChange">
<el-option v-for="item in networkTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selIsAvailable" @change="selectChange">
<el-option v-for="item in isAvailableList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
<div class="right">
<div class="tools-bar-btn-c">
<el-button size="medium" icon="el-icon-refresh" @click="syncComputerNetwork" v-loading="syncLoading">
{{ $t('resourcesManagement.syncAiNetwork') }}</el-button>
<el-button type="primary" icon="el-icon-plus" size="medium" @click="showDialog('add')">
@@ -40,7 +43,7 @@
header-align="center">
<template slot-scope="scope">
<span :title="scope.row.Cluster">{{ `${scope.row.QueueCode}${scope.row.QueueName ?
'【' + scope.row.QueueName + '】' : ''}` }}</span>
'【' + scope.row.QueueName + '】' : ''}` }}</span>
</template>
</el-table-column>
<el-table-column prop="QueueType" :label="$t('resourcesManagement.resQueueType')" align="center"
@@ -56,6 +59,7 @@
</el-table-column>
<el-table-column prop="ClusterName" :label="$t('resourcesManagement.whichCluster')" align="center"
header-align="center">

<template slot-scope="scope">
<span :title="scope.row.Cluster">{{ scope.row.ClusterName }}</span>
</template>
@@ -73,25 +77,36 @@
header-align="center"></el-table-column>
<el-table-column prop="HasInternetStr" :label="$t('cloudbrainObj.networkType')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="IsAvailableStr" :label="$t('resourcesManagement.resQueueIsAvailable')" align="center"
header-align="center">

<template slot-scope="scope">
<span :style="{ color: scope.row.IsAvailable ? 'rgb(82, 196, 26)' : 'rgb(245, 34, 45)' }">{{
scope.row.IsAvailableStr
}}</span>
</template>
</el-table-column>
<el-table-column prop="UpdatedTimeStr" :label="$t('resourcesManagement.lastUpdateTime')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="Remark" :label="$t('resourcesManagement.remark')" align="left" header-align="center"
min-width="160">
</el-table-column>
<el-table-column :label="$t('operation')" align="center" header-align="center" width="80">

<template slot-scope="scope">
<span v-if="scope.row.Cluster !== 'C2Net'" class="op-btn" @click="showDialog('edit', scope.row)">{{
$t('edit')
}}</span>
$t('edit')
}}</span>
<span v-else class="op-btn" style="color:rgb(187, 187, 187);cursor:not-allowed">{{
$t('edit')
}}</span>
$t('edit')
}}</span>
</template>
</el-table-column>

<template slot="empty">
<span style="font-size: 12px">{{
loading ? $t('loading') : $t('noData')
}}</span>
loading ? $t('loading') : $t('noData')
}}</span>
</template>
</el-table>
</div>
@@ -135,6 +150,8 @@ export default {
cardTypeList: [{ k: '', v: this.$t('resourcesManagement.allAccCardType') }, ...ACC_CARD_TYPE],
selNetworkType: 0,
networkTypeList: [{ k: 0, v: this.$t('resourcesManagement.allNetworkType') }, ...NETWORK_TYPE],
selIsAvailable: 0,
isAvailableList: [{ k: 0, v: this.$t('resourcesManagement.resQueueIsAvailableAll') }, { k: 1, v: this.$t('resourcesManagement.notAvailable') }, { k: 2, v: this.$t('resourcesManagement.available') }],
syncLoading: false,
loading: false,
tableData: [],
@@ -177,6 +194,7 @@ export default {
resource: this.selComputingType,
card: this.selCardType,
hasInternet: this.selNetworkType,
isAvailable: this.selIsAvailable,
page: this.pageInfo.curpage,
pageSize: this.pageInfo.pageSize,
};
@@ -195,6 +213,7 @@ export default {
ComputeResourceName: getListValueWithKey(this.computingTypeList, item.ComputeResource),
AccCardTypeName: getListValueWithKey(this.cardTypeList, item.AccCardType),
HasInternetStr: getListValueWithKey(NETWORK_TYPE_VALUE, item.HasInternet),
IsAvailableStr: item.IsAvailable ? this.$t('resourcesManagement.available') : this.$t('resourcesManagement.notAvailable'),
UpdatedTimeStr: formatDate(new Date(item.UpdatedTime * 1000), 'yyyy-MM-dd HH:mm:ss'),
}
});
@@ -294,7 +313,7 @@ export default {
}
}

.right {
.tools-bar-btn-c {
display: flex;
}
}


Loading…
Cancel
Save